feat(locale): add content translation operations
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
import { api } from '@/api/client'
|
||||||
|
import type { ApiResponse, ContentTranslation, ListParams, PaginatedResponse } from '@/api/types'
|
||||||
|
|
||||||
|
const BASE = '/api/admin/v1/content-translations'
|
||||||
|
|
||||||
|
export function fetchContentTranslations(params: ListParams) {
|
||||||
|
return api.get<PaginatedResponse<ContentTranslation>>(BASE, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function retryContentTranslation(translation: ContentTranslation) {
|
||||||
|
return api.post<ApiResponse<ContentTranslation>>(
|
||||||
|
`${BASE}/${translation.content_type}/${translation.id}/retry`,
|
||||||
|
)
|
||||||
|
}
|
||||||
+120
-12
@@ -759,6 +759,43 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/admin/v1/content-translations": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["getApiAdminV1ContentTranslations"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/admin/v1/content-translations/{contentType}/{localization}/retry": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
/** @example architecto */
|
||||||
|
contentType: string;
|
||||||
|
/** @example architecto */
|
||||||
|
localization: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["postApiAdminV1ContentTranslationsContentTypeLocalizationRetry"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/admin/v1/studio/category-briefs": {
|
"/api/admin/v1/studio/category-briefs": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -4357,7 +4394,7 @@ export interface paths {
|
|||||||
/** @example architecto */
|
/** @example architecto */
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
/**
|
/**
|
||||||
* @example scheduled_booking
|
* @example quote
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
dispatch_mode?: "instant_accept" | "quote" | "scheduled_booking" | "manual_selection" | null;
|
dispatch_mode?: "instant_accept" | "quote" | "scheduled_booking" | "manual_selection" | null;
|
||||||
@@ -4677,7 +4714,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example expired
|
* @example taken_by_other
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "sent" | "delivered" | "viewed" | "accepted" | "rejected" | "ignored" | "expired" | "taken_by_other" | null;
|
status?: "sent" | "delivered" | "viewed" | "accepted" | "rejected" | "ignored" | "expired" | "taken_by_other" | null;
|
||||||
@@ -4871,12 +4908,12 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example fulfilled
|
* @example canceled
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "created" | "confirmed" | "started" | "consumed" | "paid" | "fulfilled" | "canceled" | null;
|
status?: "created" | "confirmed" | "started" | "consumed" | "paid" | "fulfilled" | "canceled" | null;
|
||||||
/**
|
/**
|
||||||
* @example UNPAID
|
* @example PAID
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
payment_status?: "UNPAID" | "PAID" | null;
|
payment_status?: "UNPAID" | "PAID" | null;
|
||||||
@@ -5161,7 +5198,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example online
|
* @example busy
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "offline" | "online" | "inactive" | "busy" | null;
|
status?: "offline" | "online" | "inactive" | "busy" | null;
|
||||||
@@ -5452,12 +5489,12 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example public
|
* @example private
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
visibility?: "public" | "friends" | "private" | null;
|
visibility?: "public" | "friends" | "private" | null;
|
||||||
/**
|
/**
|
||||||
* @example all
|
* @example visible
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "blocked" | "visible" | "all" | null;
|
status?: "blocked" | "visible" | "all" | null;
|
||||||
@@ -5786,7 +5823,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example pending
|
* @example blocked
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "pending" | "blocked" | "all" | null;
|
status?: "pending" | "blocked" | "all" | null;
|
||||||
@@ -6383,7 +6420,7 @@ export interface paths {
|
|||||||
type?: string | null;
|
type?: string | null;
|
||||||
/** @example true */
|
/** @example true */
|
||||||
verified?: boolean | null;
|
verified?: boolean | null;
|
||||||
/** @example false */
|
/** @example true */
|
||||||
is_global?: boolean | null;
|
is_global?: boolean | null;
|
||||||
/**
|
/**
|
||||||
* @description Must be at least 1.
|
* @description Must be at least 1.
|
||||||
@@ -7880,7 +7917,7 @@ export interface paths {
|
|||||||
* @example b
|
* @example b
|
||||||
*/
|
*/
|
||||||
q?: string | null;
|
q?: string | null;
|
||||||
/** @example false */
|
/** @example true */
|
||||||
status?: boolean | null;
|
status?: boolean | null;
|
||||||
/** @example false */
|
/** @example false */
|
||||||
is_robot?: boolean | null;
|
is_robot?: boolean | null;
|
||||||
@@ -9960,7 +9997,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example all
|
* @example blocked
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "pending" | "blocked" | "all" | null;
|
status?: "pending" | "blocked" | "all" | null;
|
||||||
@@ -10395,7 +10432,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example android
|
* @example desktop
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
platform?: "ios" | "android" | "web" | "desktop" | null;
|
platform?: "ios" | "android" | "web" | "desktop" | null;
|
||||||
@@ -10540,6 +10577,77 @@ export interface components {
|
|||||||
}
|
}
|
||||||
export type $defs = Record<string, never>;
|
export type $defs = Record<string, never>;
|
||||||
export interface operations {
|
export interface operations {
|
||||||
|
getApiAdminV1ContentTranslations: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
/**
|
||||||
|
* @example filex
|
||||||
|
* @enum {string|null}
|
||||||
|
*/
|
||||||
|
content_type?: "filex" | "comment" | null;
|
||||||
|
/**
|
||||||
|
* @example stale
|
||||||
|
* @enum {string|null}
|
||||||
|
*/
|
||||||
|
status?: "pending" | "ready" | "failed" | "stale" | "skipped" | null;
|
||||||
|
/**
|
||||||
|
* @description Must not be greater than 35 characters.
|
||||||
|
* @example sr_BA
|
||||||
|
*/
|
||||||
|
target_locale?: string | null;
|
||||||
|
/**
|
||||||
|
* @description Must be at least 1.
|
||||||
|
* @example 22
|
||||||
|
*/
|
||||||
|
page?: number | null;
|
||||||
|
/**
|
||||||
|
* @description Must be at least 1. Must not be greater than 100.
|
||||||
|
* @example 7
|
||||||
|
*/
|
||||||
|
page_size?: number | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
/** @example false */
|
||||||
|
success?: boolean;
|
||||||
|
/** @example Authentication required */
|
||||||
|
message?: string;
|
||||||
|
/** @example UNAUTHENTICATED */
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
postApiAdminV1ContentTranslationsContentTypeLocalizationRetry: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
/** @example architecto */
|
||||||
|
contentType: string;
|
||||||
|
/** @example architecto */
|
||||||
|
localization: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: never;
|
||||||
|
};
|
||||||
Dimzou: {
|
Dimzou: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@@ -937,3 +937,23 @@ export interface ResumeImportItem {
|
|||||||
metrics?: Record<string, number> | null
|
metrics?: Record<string, number> | null
|
||||||
result?: Record<string, unknown> | null
|
result?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContentTranslation {
|
||||||
|
kind: 'content_translation'
|
||||||
|
id: number
|
||||||
|
content_type: 'filex' | 'comment'
|
||||||
|
entity_id: number
|
||||||
|
source_locale: string | null
|
||||||
|
target_locale: string
|
||||||
|
source_hash: string
|
||||||
|
status: 'pending' | 'ready' | 'failed' | 'stale' | 'skipped'
|
||||||
|
translation_type: 'machine' | 'human' | null
|
||||||
|
provider: string | null
|
||||||
|
model: string | null
|
||||||
|
attempts: number
|
||||||
|
last_error: string | null
|
||||||
|
next_retry_at: string | null
|
||||||
|
completed_at: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const PERM = {
|
|||||||
LOCALE_CREATE: 'admin:locale:create',
|
LOCALE_CREATE: 'admin:locale:create',
|
||||||
LOCALE_UPDATE: 'admin:locale:update',
|
LOCALE_UPDATE: 'admin:locale:update',
|
||||||
LOCALE_DELETE: 'admin:locale:delete',
|
LOCALE_DELETE: 'admin:locale:delete',
|
||||||
|
TRANSLATION_READ: 'admin:translation:read',
|
||||||
|
TRANSLATION_RETRY: 'admin:translation:retry',
|
||||||
SID_MANAGE: 'admin:sid:manage',
|
SID_MANAGE: 'admin:sid:manage',
|
||||||
SID_OVERRIDE: 'admin:sid:override',
|
SID_OVERRIDE: 'admin:sid:override',
|
||||||
NOTIFICATION_READ: 'admin:notification:read',
|
NOTIFICATION_READ: 'admin:notification:read',
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export const NAV_GROUPS: NavGroup[] = [
|
|||||||
{ label: '用户角色', to: '/admin-users', icon: Users, permission: 'auth:role:assign' },
|
{ label: '用户角色', to: '/admin-users', icon: Users, permission: 'auth:role:assign' },
|
||||||
{ label: 'SID 管理', to: '/sid', icon: Hash, permission: 'admin:sid:manage' },
|
{ label: 'SID 管理', to: '/sid', icon: Hash, permission: 'admin:sid:manage' },
|
||||||
{ label: '语言设置', to: '/locales', icon: Languages, permission: 'admin:locale:read' },
|
{ label: '语言设置', to: '/locales', icon: Languages, permission: 'admin:locale:read' },
|
||||||
|
{ label: '内容翻译', to: '/content-translations', icon: Languages, permission: 'admin:translation:read' },
|
||||||
{ label: '推送设备', to: '/devices', icon: Smartphone, permission: 'admin:notification:read' },
|
{ label: '推送设备', to: '/devices', icon: Smartphone, permission: 'admin:notification:read' },
|
||||||
{ label: '推送应用', to: '/push-apps', icon: Send, permission: 'admin:notification:read' },
|
{ label: '推送应用', to: '/push-apps', icon: Send, permission: 'admin:notification:read' },
|
||||||
{ label: '官方账号', to: '/official-accounts', icon: MessagesSquare, permission: 'admin:party:read' },
|
{ label: '官方账号', to: '/official-accounts', icon: MessagesSquare, permission: 'admin:party:read' },
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-r
|
|||||||
import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations'
|
import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations'
|
||||||
import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices'
|
import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices'
|
||||||
import { Route as AuthedDbStudioRouteImport } from './routes/_authed/db-studio'
|
import { Route as AuthedDbStudioRouteImport } from './routes/_authed/db-studio'
|
||||||
|
import { Route as AuthedContentTranslationsRouteImport } from './routes/_authed/content-translations'
|
||||||
import { Route as AuthedCommentReportsRouteImport } from './routes/_authed/comment-reports'
|
import { Route as AuthedCommentReportsRouteImport } from './routes/_authed/comment-reports'
|
||||||
import { Route as AuthedAdminUsersRouteImport } from './routes/_authed/admin-users'
|
import { Route as AuthedAdminUsersRouteImport } from './routes/_authed/admin-users'
|
||||||
import { Route as AuthedUsersIndexRouteImport } from './routes/_authed/users/index'
|
import { Route as AuthedUsersIndexRouteImport } from './routes/_authed/users/index'
|
||||||
@@ -131,6 +132,12 @@ const AuthedDbStudioRoute = AuthedDbStudioRouteImport.update({
|
|||||||
path: '/db-studio',
|
path: '/db-studio',
|
||||||
getParentRoute: () => AuthedRoute,
|
getParentRoute: () => AuthedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedContentTranslationsRoute =
|
||||||
|
AuthedContentTranslationsRouteImport.update({
|
||||||
|
id: '/content-translations',
|
||||||
|
path: '/content-translations',
|
||||||
|
getParentRoute: () => AuthedRoute,
|
||||||
|
} as any)
|
||||||
const AuthedCommentReportsRoute = AuthedCommentReportsRouteImport.update({
|
const AuthedCommentReportsRoute = AuthedCommentReportsRouteImport.update({
|
||||||
id: '/comment-reports',
|
id: '/comment-reports',
|
||||||
path: '/comment-reports',
|
path: '/comment-reports',
|
||||||
@@ -238,6 +245,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/admin-users': typeof AuthedAdminUsersRoute
|
'/admin-users': typeof AuthedAdminUsersRoute
|
||||||
'/comment-reports': typeof AuthedCommentReportsRoute
|
'/comment-reports': typeof AuthedCommentReportsRoute
|
||||||
|
'/content-translations': typeof AuthedContentTranslationsRoute
|
||||||
'/db-studio': typeof AuthedDbStudioRoute
|
'/db-studio': typeof AuthedDbStudioRoute
|
||||||
'/devices': typeof AuthedDevicesRoute
|
'/devices': typeof AuthedDevicesRoute
|
||||||
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
||||||
@@ -274,6 +282,7 @@ export interface FileRoutesByTo {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/admin-users': typeof AuthedAdminUsersRoute
|
'/admin-users': typeof AuthedAdminUsersRoute
|
||||||
'/comment-reports': typeof AuthedCommentReportsRoute
|
'/comment-reports': typeof AuthedCommentReportsRoute
|
||||||
|
'/content-translations': typeof AuthedContentTranslationsRoute
|
||||||
'/db-studio': typeof AuthedDbStudioRoute
|
'/db-studio': typeof AuthedDbStudioRoute
|
||||||
'/devices': typeof AuthedDevicesRoute
|
'/devices': typeof AuthedDevicesRoute
|
||||||
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
||||||
@@ -313,6 +322,7 @@ export interface FileRoutesById {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/_authed/admin-users': typeof AuthedAdminUsersRoute
|
'/_authed/admin-users': typeof AuthedAdminUsersRoute
|
||||||
'/_authed/comment-reports': typeof AuthedCommentReportsRoute
|
'/_authed/comment-reports': typeof AuthedCommentReportsRoute
|
||||||
|
'/_authed/content-translations': typeof AuthedContentTranslationsRoute
|
||||||
'/_authed/db-studio': typeof AuthedDbStudioRoute
|
'/_authed/db-studio': typeof AuthedDbStudioRoute
|
||||||
'/_authed/devices': typeof AuthedDevicesRoute
|
'/_authed/devices': typeof AuthedDevicesRoute
|
||||||
'/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
'/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
||||||
@@ -353,6 +363,7 @@ export interface FileRouteTypes {
|
|||||||
| '/login'
|
| '/login'
|
||||||
| '/admin-users'
|
| '/admin-users'
|
||||||
| '/comment-reports'
|
| '/comment-reports'
|
||||||
|
| '/content-translations'
|
||||||
| '/db-studio'
|
| '/db-studio'
|
||||||
| '/devices'
|
| '/devices'
|
||||||
| '/dimzou-translations'
|
| '/dimzou-translations'
|
||||||
@@ -389,6 +400,7 @@ export interface FileRouteTypes {
|
|||||||
| '/login'
|
| '/login'
|
||||||
| '/admin-users'
|
| '/admin-users'
|
||||||
| '/comment-reports'
|
| '/comment-reports'
|
||||||
|
| '/content-translations'
|
||||||
| '/db-studio'
|
| '/db-studio'
|
||||||
| '/devices'
|
| '/devices'
|
||||||
| '/dimzou-translations'
|
| '/dimzou-translations'
|
||||||
@@ -427,6 +439,7 @@ export interface FileRouteTypes {
|
|||||||
| '/login'
|
| '/login'
|
||||||
| '/_authed/admin-users'
|
| '/_authed/admin-users'
|
||||||
| '/_authed/comment-reports'
|
| '/_authed/comment-reports'
|
||||||
|
| '/_authed/content-translations'
|
||||||
| '/_authed/db-studio'
|
| '/_authed/db-studio'
|
||||||
| '/_authed/devices'
|
| '/_authed/devices'
|
||||||
| '/_authed/dimzou-translations'
|
| '/_authed/dimzou-translations'
|
||||||
@@ -587,6 +600,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedDbStudioRouteImport
|
preLoaderRoute: typeof AuthedDbStudioRouteImport
|
||||||
parentRoute: typeof AuthedRoute
|
parentRoute: typeof AuthedRoute
|
||||||
}
|
}
|
||||||
|
'/_authed/content-translations': {
|
||||||
|
id: '/_authed/content-translations'
|
||||||
|
path: '/content-translations'
|
||||||
|
fullPath: '/content-translations'
|
||||||
|
preLoaderRoute: typeof AuthedContentTranslationsRouteImport
|
||||||
|
parentRoute: typeof AuthedRoute
|
||||||
|
}
|
||||||
'/_authed/comment-reports': {
|
'/_authed/comment-reports': {
|
||||||
id: '/_authed/comment-reports'
|
id: '/_authed/comment-reports'
|
||||||
path: '/comment-reports'
|
path: '/comment-reports'
|
||||||
@@ -726,6 +746,7 @@ declare module '@tanstack/react-router' {
|
|||||||
interface AuthedRouteChildren {
|
interface AuthedRouteChildren {
|
||||||
AuthedAdminUsersRoute: typeof AuthedAdminUsersRoute
|
AuthedAdminUsersRoute: typeof AuthedAdminUsersRoute
|
||||||
AuthedCommentReportsRoute: typeof AuthedCommentReportsRoute
|
AuthedCommentReportsRoute: typeof AuthedCommentReportsRoute
|
||||||
|
AuthedContentTranslationsRoute: typeof AuthedContentTranslationsRoute
|
||||||
AuthedDbStudioRoute: typeof AuthedDbStudioRoute
|
AuthedDbStudioRoute: typeof AuthedDbStudioRoute
|
||||||
AuthedDevicesRoute: typeof AuthedDevicesRoute
|
AuthedDevicesRoute: typeof AuthedDevicesRoute
|
||||||
AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute
|
AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute
|
||||||
@@ -762,6 +783,7 @@ interface AuthedRouteChildren {
|
|||||||
const AuthedRouteChildren: AuthedRouteChildren = {
|
const AuthedRouteChildren: AuthedRouteChildren = {
|
||||||
AuthedAdminUsersRoute: AuthedAdminUsersRoute,
|
AuthedAdminUsersRoute: AuthedAdminUsersRoute,
|
||||||
AuthedCommentReportsRoute: AuthedCommentReportsRoute,
|
AuthedCommentReportsRoute: AuthedCommentReportsRoute,
|
||||||
|
AuthedContentTranslationsRoute: AuthedContentTranslationsRoute,
|
||||||
AuthedDbStudioRoute: AuthedDbStudioRoute,
|
AuthedDbStudioRoute: AuthedDbStudioRoute,
|
||||||
AuthedDevicesRoute: AuthedDevicesRoute,
|
AuthedDevicesRoute: AuthedDevicesRoute,
|
||||||
AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute,
|
AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute,
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import type { ColumnDef } from '@tanstack/react-table'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { fetchContentTranslations, retryContentTranslation } from '@/api/modules/content-translations'
|
||||||
|
import type { ContentTranslation } from '@/api/types'
|
||||||
|
import { PERM, requirePermission } from '@/auth/permissions'
|
||||||
|
import { useCan } from '@/auth/store'
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
|
import { DataTable } from '@/components/data-table/data-table'
|
||||||
|
import { PageHeader } from '@/components/page-header'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { handleApiError } from '@/lib/errors'
|
||||||
|
import { useListPage } from '@/lib/list-page'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/_authed/content-translations')({
|
||||||
|
beforeLoad: () => requirePermission(PERM.TRANSLATION_READ),
|
||||||
|
component: ContentTranslationsPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusLabels: Record<ContentTranslation['status'], string> = {
|
||||||
|
pending: '等待中', ready: '已完成', failed: '失败', stale: '源内容已变更', skipped: '已跳过',
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContentTranslationsPage() {
|
||||||
|
const canRetry = useCan(PERM.TRANSLATION_RETRY)
|
||||||
|
const [contentType, setContentType] = useState('all')
|
||||||
|
const [status, setStatus] = useState('all')
|
||||||
|
const [localeInput, setLocaleInput] = useState('')
|
||||||
|
const [targetLocale, setTargetLocale] = useState('')
|
||||||
|
const filters: Record<string, string> = {}
|
||||||
|
if (contentType !== 'all') filters.content_type = contentType
|
||||||
|
if (status !== 'all') filters.status = status
|
||||||
|
if (targetLocale) filters.target_locale = targetLocale
|
||||||
|
const list = useListPage('content-translations', fetchContentTranslations, filters)
|
||||||
|
|
||||||
|
const retry = async (translation: ContentTranslation) => {
|
||||||
|
try {
|
||||||
|
await retryContentTranslation(translation)
|
||||||
|
toast.success('已使用普通优先级重新入队')
|
||||||
|
void list.query.refetch()
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: ColumnDef<ContentTranslation>[] = [
|
||||||
|
{ header: '内容', cell: ({ row }) => <div><Badge variant="outline">{row.original.content_type === 'filex' ? 'FileX' : '评论'}</Badge><code className="ml-2 text-xs text-muted-foreground">#{row.original.entity_id}</code></div> },
|
||||||
|
{ header: '语言', cell: ({ row }) => <span>{row.original.source_locale ?? '自动检测'} → <Badge variant="secondary">{row.original.target_locale}</Badge></span> },
|
||||||
|
{ header: '状态', cell: ({ row }) => <div><div>{statusLabels[row.original.status]}</div>{row.original.last_error && <div className="max-w-72 truncate text-xs text-destructive" title={row.original.last_error}>{row.original.last_error}</div>}</div> },
|
||||||
|
{ header: '提供方', cell: ({ row }) => row.original.provider ? <div><div>{row.original.provider}</div><div className="text-xs text-muted-foreground">{row.original.model}</div></div> : '—' },
|
||||||
|
{ header: '尝试', accessorKey: 'attempts' },
|
||||||
|
{ header: '更新时间', cell: ({ row }) => new Date(row.original.updated_at).toLocaleString('zh-CN') },
|
||||||
|
{ header: '操作', cell: ({ row }) => canRetry && row.original.status !== 'pending' ? <ConfirmDialog trigger={<Button size="sm" variant="outline">重新入队</Button>} title="重新执行此翻译?" description="任务会进入对应语言组和内容类型的普通优先级队列。" onConfirm={() => retry(row.original)} /> : <span className="text-xs text-muted-foreground">—</span> },
|
||||||
|
]
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<PageHeader title="内容翻译" description="FileX 和评论的按需机器翻译状态与重试" actions={<div className="flex items-center gap-2">
|
||||||
|
<Input value={localeInput} onChange={(event) => setLocaleInput(event.target.value)} onBlur={() => setTargetLocale(localeInput.trim())} onKeyDown={(event) => { if (event.key === 'Enter') setTargetLocale(localeInput.trim()) }} placeholder="目标语言" className="h-8 w-28" />
|
||||||
|
<Select value={contentType} onValueChange={setContentType}><SelectTrigger size="sm" className="w-28"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="all">全部内容</SelectItem><SelectItem value="filex">FileX</SelectItem><SelectItem value="comment">评论</SelectItem></SelectContent></Select>
|
||||||
|
<Select value={status} onValueChange={setStatus}><SelectTrigger size="sm" className="w-28"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="all">全部状态</SelectItem>{Object.entries(statusLabels).map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}</SelectContent></Select>
|
||||||
|
</div>} />
|
||||||
|
<DataTable columns={columns} data={list.rows} pagination={list.pagination} loading={list.loading} emptyText="没有内容翻译任务" onPageChange={list.setPage} onPageSizeChange={list.setPageSize} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user