feat(frontend): 控制台 next-intl 中英双语

接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
This commit is contained in:
ryan
2026-08-16 16:49:16 +08:00
parent 64fbaa7ef1
commit 5a8722ff07
219 changed files with 21437 additions and 5212 deletions
@@ -1,6 +1,7 @@
'use client';
import { useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { format } from 'date-fns';
@@ -49,18 +50,18 @@ import {
TableRow,
} from '@/components/ui/table';
const STATUS_LABELS: Record<TaskExecutionStatus, string> = {
pending: '等待中',
running: '执行中',
succeeded: '成功',
failed: '失败',
const STATUS_LABELS_KEYS: Record<TaskExecutionStatus, string> = {
pending: 'statusPending',
running: 'statusRunning',
succeeded: 'statusSucceeded',
failed: 'statusFailed',
};
const TRIGGER_LABELS: Record<string, string> = {
system: '系统',
manual: '手动',
retry: '重试',
schedule: '定时',
const TRIGGER_LABELS_KEYS: Record<string, string> = {
system: 'triggerSystem',
manual: 'triggerManual',
retry: 'triggerRetry',
schedule: 'triggerSchedule',
};
const PAGE_SIZE = 10;
@@ -85,6 +86,7 @@ function statusVariant(status: TaskExecutionStatus) {
}
export function TaskExecutionsManager() {
const t = useTranslations('admin.tasks');
const queryClient = useQueryClient();
const [executionsPage, setExecutionsPage] = useState(1);
const [executionStatus, setExecutionStatus] = useState<
@@ -129,16 +131,16 @@ export function TaskExecutionsManager() {
const retryMutation = useMutation({
mutationFn: (id: string) => services.adminTask.retryTaskExecution(id),
onSuccess: (taskID) => {
toast.success('任务已重新下发', {
description: `新任务 ID:${taskID}`,
toast.success(t('retrySuccess'), {
description: t('retrySuccessDesc', { taskId: taskID }),
});
void queryClient.invalidateQueries({
queryKey: ['admin', 'task-executions'],
});
},
onError: (err: Error) => {
toast.error('任务重试失败', {
description: err.message || '未知错误',
toast.error(t('retryFailed'), {
description: err.message || t('unknownError'),
});
},
});
@@ -187,14 +189,14 @@ export function TaskExecutionsManager() {
}
>
<SelectTrigger size='sm' className='w-[120px]'>
<SelectValue placeholder='状态' />
<SelectValue placeholder={t('colStatus')} />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>全部状态</SelectItem>
<SelectItem value='pending'>等待中</SelectItem>
<SelectItem value='running'>执行中</SelectItem>
<SelectItem value='succeeded'>成功</SelectItem>
<SelectItem value='failed'>失败</SelectItem>
<SelectItem value='all'>{t('allStatuses')}</SelectItem>
<SelectItem value='pending'>{t('statusPending')}</SelectItem>
<SelectItem value='running'>{t('statusRunning')}</SelectItem>
<SelectItem value='succeeded'>{t('statusSucceeded')}</SelectItem>
<SelectItem value='failed'>{t('statusFailed')}</SelectItem>
</SelectContent>
</Select>
<Select
@@ -202,10 +204,10 @@ export function TaskExecutionsManager() {
onValueChange={handleTaskTypeChange}
>
<SelectTrigger size='sm' className='w-[180px]'>
<SelectValue placeholder='任务类型' />
<SelectValue placeholder={t('taskTypeFilter')} />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>全部任务</SelectItem>
<SelectItem value='all'>{t('allTasks')}</SelectItem>
{taskTypes.map((task) => (
<SelectItem key={task.type} value={task.type}>
{task.name || task.type}
@@ -224,7 +226,7 @@ export function TaskExecutionsManager() {
) : (
<RefreshCw className='size-4' />
)}
刷新
{t('refresh')}
</Button>
</div>
</div>
@@ -240,22 +242,22 @@ export function TaskExecutionsManager() {
) : executionsLoading && executions.length === 0 ? (
<LoadingStateWithBorder
icon={Activity}
description='加载任务执行记录中...'
description={t('loadingExecutions')}
/>
) : executions.length === 0 ? (
<EmptyStateWithBorder icon={Activity} description='暂无任务执行记录' />
<EmptyStateWithBorder icon={Activity} description={t('noExecutions')} />
) : (
<div className='rounded-lg border bg-card'>
<Table className='min-w-[900px]'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead className='w-[180px]'>任务</TableHead>
<TableHead className='w-[100px]'>状态</TableHead>
<TableHead className='w-[110px]'>触发</TableHead>
<TableHead className='w-[120px]'>重试</TableHead>
<TableHead className='w-[120px]'>耗时</TableHead>
<TableHead className='min-w-[220px]'>结果/错误</TableHead>
<TableHead className='w-[170px]'>创建时间</TableHead>
<TableHead className='w-[180px]'>{t('colTask')}</TableHead>
<TableHead className='w-[100px]'>{t('colStatus')}</TableHead>
<TableHead className='w-[110px]'>{t('colTrigger')}</TableHead>
<TableHead className='w-[120px]'>{t('colRetry')}</TableHead>
<TableHead className='w-[120px]'>{t('colDuration')}</TableHead>
<TableHead className='min-w-[220px]'>{t('colResult')}</TableHead>
<TableHead className='w-[170px]'>{t('colCreatedAt')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -277,12 +279,13 @@ export function TaskExecutionsManager() {
</TableCell>
<TableCell>
<Badge variant={statusVariant(execution.status)}>
{STATUS_LABELS[execution.status] || execution.status}
{t(STATUS_LABELS_KEYS[execution.status]) ||
execution.status}
</Badge>
</TableCell>
<TableCell>
<Badge variant='outline'>
{TRIGGER_LABELS[execution.triggered_by] ||
{t(TRIGGER_LABELS_KEYS[execution.triggered_by]) ||
execution.triggered_by}
</Badge>
</TableCell>
@@ -307,7 +310,11 @@ export function TaskExecutionsManager() {
<div className='flex items-center justify-between'>
<div className='text-xs text-muted-foreground'>
共 {executionsTotal} 条,当前第 {executionsPage}/{totalPages} 页
{t('paginationInfo', {
total: executionsTotal,
current: executionsPage,
totalPages,
})}
</div>
<div className='flex items-center gap-2'>
<Button
@@ -317,7 +324,7 @@ export function TaskExecutionsManager() {
disabled={executionsPage <= 1 || executionsLoading}
>
<ChevronLeft className='size-4' />
上一页
{t('prevPage')}
</Button>
<Button
variant='outline'
@@ -327,7 +334,7 @@ export function TaskExecutionsManager() {
}
disabled={executionsPage >= totalPages || executionsLoading}
>
下一页
{t('nextPage')}
<ChevronRight className='size-4' />
</Button>
</div>
@@ -336,11 +343,11 @@ export function TaskExecutionsManager() {
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
<SheetContent className='w-full p-0 sm:max-w-[640px]'>
<SheetHeader className='border-b'>
<SheetTitle>任务执行详情</SheetTitle>
<SheetTitle>{t('executionDetailTitle')}</SheetTitle>
<SheetDescription>
{selectedExecution?.task_name ||
selectedExecution?.task_type ||
'任务记录'}
t('executionRecord')}
</SheetDescription>
</SheetHeader>
@@ -348,32 +355,35 @@ export function TaskExecutionsManager() {
{detailLoading && !selectedExecution ? (
<LoadingStateWithBorder
icon={Activity}
description='加载任务详情中...'
description={t('loadingDetail')}
/>
) : selectedExecution ? (
<div className='space-y-5 py-4'>
<div className='grid grid-cols-2 gap-3'>
<div className='rounded-lg border p-3'>
<div className='text-xs text-muted-foreground'>状态</div>
<div className='text-xs text-muted-foreground'>
{t('detailStatus')}
</div>
<div className='mt-2'>
<Badge variant={statusVariant(selectedExecution.status)}>
{STATUS_LABELS[selectedExecution.status] ||
{t(STATUS_LABELS_KEYS[selectedExecution.status]) ||
selectedExecution.status}
</Badge>
</div>
</div>
<div className='rounded-lg border p-3'>
<div className='text-xs text-muted-foreground'>
触发来源
{t('detailTrigger')}
</div>
<div className='mt-2 text-sm font-medium'>
{TRIGGER_LABELS[selectedExecution.triggered_by] ||
selectedExecution.triggered_by}
{t(
TRIGGER_LABELS_KEYS[selectedExecution.triggered_by],
) || selectedExecution.triggered_by}
</div>
</div>
<div className='rounded-lg border p-3'>
<div className='text-xs text-muted-foreground'>
重试次数
{t('detailRetryCount')}
</div>
<div className='mt-2 font-mono text-sm'>
{selectedExecution.retry_count}/
@@ -381,7 +391,9 @@ export function TaskExecutionsManager() {
</div>
</div>
<div className='rounded-lg border p-3'>
<div className='text-xs text-muted-foreground'>耗时</div>
<div className='text-xs text-muted-foreground'>
{t('detailDuration')}
</div>
<div className='mt-2 font-mono text-sm'>
{formatDuration(selectedExecution.duration)}
</div>
@@ -389,7 +401,7 @@ export function TaskExecutionsManager() {
</div>
<div className='grid gap-2'>
<Label>任务标识</Label>
<Label>{t('detailTaskId')}</Label>
<div className='rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs break-all'>
{selectedExecution.task_id}
</div>
@@ -397,25 +409,25 @@ export function TaskExecutionsManager() {
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
<div className='grid gap-2'>
<Label>创建时间</Label>
<Label>{t('detailCreatedAt')}</Label>
<div className='font-mono text-xs text-muted-foreground'>
{formatDateTime(selectedExecution.created_at)}
</div>
</div>
<div className='grid gap-2'>
<Label>开始时间</Label>
<Label>{t('detailStartedAt')}</Label>
<div className='font-mono text-xs text-muted-foreground'>
{formatDateTime(selectedExecution.started_at)}
</div>
</div>
<div className='grid gap-2'>
<Label>结束时间</Label>
<Label>{t('detailFinishedAt')}</Label>
<div className='font-mono text-xs text-muted-foreground'>
{formatDateTime(selectedExecution.finished_at)}
</div>
</div>
<div className='grid gap-2'>
<Label>更新时间</Label>
<Label>{t('detailUpdatedAt')}</Label>
<div className='font-mono text-xs text-muted-foreground'>
{formatDateTime(selectedExecution.updated_at)}
</div>
@@ -423,7 +435,7 @@ export function TaskExecutionsManager() {
</div>
<div className='grid gap-2'>
<Label>执行结果</Label>
<Label>{t('detailResult')}</Label>
<div className='min-h-10 rounded-md border bg-muted/30 px-3 py-2 text-sm whitespace-pre-wrap break-all'>
{selectedExecution.result || '-'}
</div>
@@ -431,7 +443,7 @@ export function TaskExecutionsManager() {
{selectedExecution.error_message && (
<div className='grid gap-2'>
<Label>错误信息</Label>
<Label>{t('detailError')}</Label>
<div className='rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive whitespace-pre-wrap break-all'>
{selectedExecution.error_message}
</div>
@@ -439,23 +451,23 @@ export function TaskExecutionsManager() {
)}
<div className='grid gap-2'>
<Label>Payload</Label>
<Label>{t('detailPayload')}</Label>
<pre className='max-h-40 overflow-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed'>
{selectedExecution.payload || '{}'}
</pre>
</div>
<div className='grid gap-2'>
<Label>执行日志</Label>
<Label>{t('detailLog')}</Label>
<pre className='min-h-48 max-h-[420px] overflow-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed whitespace-pre-wrap'>
{selectedExecution.log || '暂无日志'}
{selectedExecution.log || t('noLog')}
</pre>
</div>
</div>
) : (
<EmptyStateWithBorder
icon={Activity}
description='未选择任务记录'
description={t('noExecutionSelected')}
/>
)}
</div>
@@ -471,7 +483,7 @@ export function TaskExecutionsManager() {
) : (
<RefreshCw className='size-4' />
)}
刷新详情
{t('refreshDetail')}
</Button>
{selectedExecution &&
selectedExecution.status === 'failed' &&
@@ -485,7 +497,7 @@ export function TaskExecutionsManager() {
) : (
<RotateCcw className='size-4' />
)}
重试任务
{t('retryTask')}
</Button>
)}
</SheetFooter>
@@ -1,6 +1,7 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -88,10 +89,10 @@ const TASK_CONFIGS: Record<
},
};
const LOG_DATABASE_LABELS: Record<string, string> = {
postgres: 'PostgreSQL(主库)',
sqlite: 'SQLite(主库)',
clickhouse: 'ClickHouse',
const LOG_DATABASE_LABEL_KEYS: Record<string, string> = {
postgres: 'dbPostgresPrimary',
sqlite: 'dbSqlitePrimary',
clickhouse: 'dbClickhouse',
};
const DEFAULT_TASK_CONFIG = {
@@ -108,6 +109,7 @@ function DatePickerWithTime({
date: Date | undefined;
setDate: (date: Date | undefined) => void;
}) {
const t = useTranslations('admin.tasks');
const timeString = date ? format(date, 'HH:mm:ss') : '';
const handleDateSelect = (newDate: Date | undefined) => {
@@ -163,7 +165,7 @@ function DatePickerWithTime({
)}
>
<CalendarIcon className='mr-1 size-3' />
{date ? format(date, 'yyyy-MM-dd') : <span>选择日期</span>}
{date ? format(date, 'yyyy-MM-dd') : <span>{t('selectDate')}</span>}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'>
@@ -190,6 +192,7 @@ function DatePickerWithTime({
}
export function TaskManager() {
const t = useTranslations('admin.tasks');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [taskTypes, setTaskTypes] = useState<TaskMeta[]>([]);
@@ -210,11 +213,11 @@ export function TaskManager() {
const data = await services.adminTask.getTaskTypes();
setTaskTypes(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('加载任务类型失败'));
setError(err instanceof Error ? err : new Error(t('loadTaskTypesFailed')));
} finally {
setLoading(false);
}
}, []);
}, [t]);
const [logDbStatus, setLogDbStatus] = useState<LogDatabaseStatus | null>(
null,
@@ -302,8 +305,11 @@ export function TaskManager() {
const taskID = await services.adminTask.dispatchTask(params);
toast.success('任务下发成功', {
description: `已成功将任务 ${targetTask?.name || selectedTaskType} 加入队列:${taskID}`,
toast.success(t('dispatchSuccess'), {
description: t('dispatchSuccessDesc', {
name: targetTask?.name || selectedTaskType,
taskId: taskID,
}),
});
setDialogOpen(false);
@@ -312,8 +318,8 @@ export function TaskManager() {
setUserId('');
setParamValues({});
} catch (err) {
toast.error('任务下发失败', {
description: err instanceof Error ? err.message : '未知错误',
toast.error(t('dispatchFailed'), {
description: err instanceof Error ? err.message : t('unknownError'),
});
} finally {
setDispatching(false);
@@ -345,10 +351,10 @@ export function TaskManager() {
) : loading && taskTypes.length === 0 ? (
<LoadingStateWithBorder
icon={Layers}
description='加载任务类型中...'
description={t('loadingTaskTypes')}
/>
) : taskTypes.length === 0 ? (
<EmptyStateWithBorder icon={Layers} description='暂无可用任务类型' />
<EmptyStateWithBorder icon={Layers} description={t('noTaskTypes')} />
) : (
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4'>
{taskTypes.map((task, index) => {
@@ -386,13 +392,15 @@ export function TaskManager() {
variant='outline'
className='text-[9px] h-4.5 bg-background/50 font-mono text-muted-foreground border-border/50 px-1'
>
类型:{task.type}
{t('taskType')}
{task.type}
</Badge>
<Badge
variant='outline'
className='text-[9px] h-4.5 bg-background/50 font-mono text-muted-foreground border-border/50 px-1'
>
重试:{task.max_retry}
{t('taskRetry')}
{task.max_retry}
</Badge>
</div>
</div>
@@ -401,16 +409,23 @@ export function TaskManager() {
<div className='pt-3 mt-3 border-t border-border/50 space-y-1.5'>
<div className='flex items-center justify-between gap-2'>
<span className='text-[10px] text-muted-foreground shrink-0'>
日志主库
{t('logPrimaryDb')}
</span>
<span className='text-[10px] font-mono text-foreground truncate'>
{LOG_DATABASE_LABELS[logDbStatus.active_database] ||
logDbStatus.active_database}
{LOG_DATABASE_LABEL_KEYS[
logDbStatus.active_database
]
? t(
LOG_DATABASE_LABEL_KEYS[
logDbStatus.active_database
],
)
: logDbStatus.active_database}
</span>
</div>
<div className='flex items-center justify-between gap-2'>
<span className='text-[10px] text-muted-foreground shrink-0'>
保留天数
{t('retentionDays')}
</span>
<span className='text-[10px] font-mono text-muted-foreground truncate'>
{retentionSummary || '-'}
@@ -418,7 +433,7 @@ export function TaskManager() {
</div>
<div className='flex items-center justify-between gap-2'>
<span className='text-[10px] text-muted-foreground shrink-0'>
迁移状态
{t('migrationStatus')}
</span>
<Badge
variant={
@@ -429,8 +444,8 @@ export function TaskManager() {
className='text-[10px] h-5 px-1.5'
>
{logDbStatus.migration === 'migrating'
? '迁移中'
: '空闲'}
? t('migrating')
: t('idle')}
</Badge>
</div>
</div>
@@ -443,7 +458,7 @@ export function TaskManager() {
onClick={() => openDispatchDialog(task.type)}
>
<Play className='size-3 mr-1' />
立即执行
{t('executeNow')}
</Button>
</div>
</div>
@@ -457,15 +472,13 @@ export function TaskManager() {
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>下发任务</DialogTitle>
<DialogDescription>
配置任务参数并启动,任务将在后台异步执行。
</DialogDescription>
<DialogTitle>{t('dispatchTask')}</DialogTitle>
<DialogDescription>{t('dispatchTaskDesc')}</DialogDescription>
</DialogHeader>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label>执行任务</Label>
<Label>{t('executeTask')}</Label>
<div className='flex items-center gap-2 p-2 rounded-lg bg-muted/50'>
{(() => {
const meta = getSelectedTaskMeta();
@@ -495,20 +508,20 @@ export function TaskManager() {
return (
<>
<div className='grid gap-2'>
<Label>开始时间</Label>
<Label>{t('startTime')}</Label>
<DatePickerWithTime
date={startTime}
setDate={setStartTime}
/>
<p className='text-xs text-muted-foreground'>
选择同步的起始时间点
{t('startTimeDesc')}
</p>
</div>
<div className='grid gap-2'>
<Label>结束时间</Label>
<Label>{t('endTime')}</Label>
<DatePickerWithTime date={endTime} setDate={setEndTime} />
<p className='text-xs text-muted-foreground'>
选择同步的结束时间点
{t('endTimeDesc')}
</p>
</div>
</>
@@ -572,8 +585,8 @@ export function TaskManager() {
/>
<span className='text-xs text-muted-foreground'>
{paramValues[param.name] === 'true'
? '开启'
: '关闭'}
? t('paramOn')
: t('paramOff')}
</span>
</div>
) : isSwitchTarget &&
@@ -593,12 +606,14 @@ export function TaskManager() {
className='w-full text-xs'
size='sm'
>
<SelectValue placeholder='选择目标日志库...' />
<SelectValue placeholder={t('selectLogDb')} />
</SelectTrigger>
<SelectContent>
{availableLogDbTargets.map((target) => (
<SelectItem key={target} value={target}>
{LOG_DATABASE_LABELS[target] || target}
{LOG_DATABASE_LABEL_KEYS[target]
? t(LOG_DATABASE_LABEL_KEYS[target])
: target}
</SelectItem>
))}
</SelectContent>
@@ -639,7 +654,7 @@ export function TaskManager() {
return (
<div className='flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 p-3 rounded-md border border-dashed'>
<Info className='h-4 w-4' />
<span>此任务无需额外配置参数,确认后将直接下发。</span>
<span>{t('noParamsHint')}</span>
</div>
);
}
@@ -654,7 +669,7 @@ export function TaskManager() {
disabled={dispatching}
className='h-8 text-xs'
>
取消
{t('cancel')}
</Button>
<Button
onClick={handleDispatch}
@@ -666,7 +681,7 @@ export function TaskManager() {
) : (
<Play className='size-3' />
)}
{dispatching ? '下发中' : '启动'}
{dispatching ? t('dispatching') : t('start')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,6 +1,7 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -39,6 +40,7 @@ import { LoadingStateWithBorder } from '@/components/layout/loading';
import { EmptyStateWithBorder } from '@/components/layout/empty';
export function TaskSchedulesManager() {
const t = useTranslations('admin.tasks');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [schedules, setSchedules] = useState<Schedule[]>([]);
@@ -73,11 +75,11 @@ export function TaskSchedulesManager() {
setSchedules(schedulesData || []);
setTaskTypes(taskTypesData || []);
} catch (err) {
setError(err instanceof Error ? err : new Error('加载定时任务配置失败'));
setError(err instanceof Error ? err : new Error(t('loadSchedulesFailed')));
} finally {
setLoading(false);
}
}, []);
}, [t]);
useEffect(() => {
fetchData();
@@ -130,12 +132,12 @@ export function TaskSchedulesManager() {
);
toast.success(
updated.is_active
? `已启用定时任务:${updated.name}`
: `已停用定时任务:${updated.name}`,
? t('scheduleEnabled', { name: updated.name })
: t('scheduleDisabled', { name: updated.name }),
);
} catch (err) {
toast.error('修改任务状态失败', {
description: err instanceof Error ? err.message : '未知错误',
toast.error(t('changeScheduleStatusFailed'), {
description: err instanceof Error ? err.message : t('unknownError'),
});
}
};
@@ -161,11 +163,11 @@ export function TaskSchedulesManager() {
const handleSubmit = async () => {
if (!name.trim()) {
toast.error('任务名称不能为空');
toast.error(t('scheduleNameRequired'));
return;
}
if (!cron.trim()) {
toast.error('Cron 表达式不能为空');
toast.error(t('cronRequired'));
return;
}
@@ -203,7 +205,7 @@ export function TaskSchedulesManager() {
setSchedules((prev) =>
prev.map((s) => (s.id === editingSchedule.id ? updated : s)),
);
toast.success('定时任务更新成功');
toast.success(t('scheduleUpdateSuccess'));
} else {
const req: CreateScheduleRequest = {
name,
@@ -214,13 +216,13 @@ export function TaskSchedulesManager() {
};
const created = await services.adminTask.createSchedule(req);
setSchedules((prev) => [created, ...prev]);
toast.success('定时任务创建成功');
toast.success(t('scheduleCreateSuccess'));
}
setDialogOpen(false);
} catch (err) {
toast.error('提交失败', {
description: err instanceof Error ? err.message : '未知错误',
toast.error(t('submitFailed'), {
description: err instanceof Error ? err.message : t('unknownError'),
});
} finally {
setSubmitLoading(false);
@@ -238,11 +240,11 @@ export function TaskSchedulesManager() {
setDeleteLoading(true);
await services.adminTask.deleteSchedule(deletingId);
setSchedules((prev) => prev.filter((s) => s.id !== deletingId));
toast.success('定时任务删除成功');
toast.success(t('scheduleDeleteSuccess'));
setDeleteOpen(false);
} catch (err) {
toast.error('删除失败', {
description: err instanceof Error ? err.message : '未知错误',
toast.error(t('deleteFailed'), {
description: err instanceof Error ? err.message : t('unknownError'),
});
} finally {
setDeleteLoading(false);
@@ -274,7 +276,7 @@ export function TaskSchedulesManager() {
) : (
<RefreshCw className='size-4' />
)}
刷新
{t('refresh')}
</Button>
<Button
size='sm'
@@ -283,7 +285,7 @@ export function TaskSchedulesManager() {
variant={'secondary'}
>
<Plus className='size-4 mr-1' />
新增定时任务
{t('addSchedule')}
</Button>
</div>
</div>
@@ -299,28 +301,34 @@ export function TaskSchedulesManager() {
) : loading && schedules.length === 0 ? (
<LoadingStateWithBorder
icon={Clock}
description='加载定时任务配置中...'
description={t('loadingSchedules')}
/>
) : schedules.length === 0 ? (
<EmptyStateWithBorder
icon={Clock}
description='暂无定时任务配置,点击上方按钮新增'
description={t('noSchedules')}
/>
) : (
<div className='rounded-lg border bg-card'>
<Table className='min-w-[800px]'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead className='w-[180px]'>任务名称</TableHead>
<TableHead className='w-[160px]'>关联异步任务</TableHead>
<TableHead className='w-[120px]'>Cron 表达式</TableHead>
<TableHead className='w-[180px]'>
{t('colScheduleName')}
</TableHead>
<TableHead className='w-[160px]'>
{t('colAssociatedTask')}
</TableHead>
<TableHead className='w-[120px]'>{t('colCron')}</TableHead>
<TableHead className='min-w-[200px]'>
执行参数 (Payload)
{t('colPayload')}
</TableHead>
<TableHead className='w-[100px] text-center'>
启用状态
{t('colEnabled')}
</TableHead>
<TableHead className='w-[120px] text-right'>
{t('colActions')}
</TableHead>
<TableHead className='w-[120px] text-right'>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -385,19 +393,19 @@ export function TaskSchedulesManager() {
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>
{editingSchedule ? '修改定时任务' : '新增定时任务'}
{editingSchedule ? t('editSchedule') : t('addSchedule')}
</DialogTitle>
<DialogDescription>
配置定时任务的调度参数和运行载荷。
{t('scheduleDialogDesc')}
</DialogDescription>
</DialogHeader>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label htmlFor='sched-name'>任务名称</Label>
<Label htmlFor='sched-name'>{t('scheduleName')}</Label>
<Input
id='sched-name'
placeholder='例如:每日数据清理'
placeholder={t('scheduleNamePlaceholder')}
value={name}
onChange={(e) => setName(e.target.value)}
className='text-xs'
@@ -405,7 +413,7 @@ export function TaskSchedulesManager() {
</div>
<div className='grid gap-2'>
<Label htmlFor='sched-type'>关联异步任务</Label>
<Label htmlFor='sched-type'>{t('associatedTask')}</Label>
<select
id='sched-type'
value={selectedTaskType}
@@ -422,24 +430,24 @@ export function TaskSchedulesManager() {
</div>
<div className='grid gap-2'>
<Label htmlFor='sched-cron'>Cron 表达式</Label>
<Label htmlFor='sched-cron'>{t('cronExpression')}</Label>
<Input
id='sched-cron'
placeholder='e.g. 0 */2 * * * 或 @daily'
placeholder={t('cronPlaceholder')}
value={cron}
onChange={(e) => setCron(e.target.value)}
className='text-xs font-mono'
/>
<p className='text-[10px] text-muted-foreground'>
使用标准 5 位 Cron 字段格式(分钟、小时、日期、月份、星期几)。
{t('cronHint')}
</p>
</div>
<div className='flex items-center justify-between rounded-lg border p-3'>
<div className='space-y-0.5'>
<Label htmlFor='sched-active'>启用状态</Label>
<Label htmlFor='sched-active'>{t('scheduleActive')}</Label>
<p className='text-[10px] text-muted-foreground'>
决定此定时任务是否会定时触发执行。
{t('scheduleActiveDesc')}
</p>
</div>
<Switch
@@ -459,14 +467,14 @@ export function TaskSchedulesManager() {
return (
<div className='flex items-center gap-2 text-xs text-muted-foreground bg-muted/40 p-3 rounded-md border border-dashed'>
<Info className='h-4 w-4 shrink-0' />
<span>所选任务类型不需要运行参数参数。</span>
<span>{t('noParamsRequired')}</span>
</div>
);
}
return (
<div className='space-y-4 pt-2 border-t'>
<Label className='text-xs font-semibold'>
运行参数配置 (Payload)
{t('payloadConfig')}
</Label>
{targetTask.params.map((param) => (
<div
@@ -509,8 +517,8 @@ export function TaskSchedulesManager() {
/>
<span className='text-xs text-muted-foreground'>
{paramValues[param.name] === 'true'
? '开启'
: '关闭'}
? t('paramOn')
: t('paramOff')}
</span>
</div>
) : (
@@ -547,7 +555,7 @@ export function TaskSchedulesManager() {
disabled={submitLoading}
className='h-8 text-xs'
>
取消
{t('cancel')}
</Button>
<Button
onClick={handleSubmit}
@@ -555,7 +563,7 @@ export function TaskSchedulesManager() {
className='h-8 text-xs'
>
{submitLoading && <Spinner className='size-3 mr-1' />}
{editingSchedule ? '保存修改' : '确认创建'}
{editingSchedule ? t('saveChanges') : t('confirmCreate')}
</Button>
</DialogFooter>
</DialogContent>
@@ -565,9 +573,9 @@ export function TaskSchedulesManager() {
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogContent className='sm:max-w-[400px]'>
<DialogHeader>
<DialogTitle>删除定时任务</DialogTitle>
<DialogTitle>{t('deleteScheduleTitle')}</DialogTitle>
<DialogDescription>
确定要删除此定时任务吗?该操作不可撤销,且会立即取消其在调度器中的调度计划。
{t('deleteScheduleDesc')}
</DialogDescription>
</DialogHeader>
<DialogFooter className='gap-2 sm:gap-0'>
@@ -577,7 +585,7 @@ export function TaskSchedulesManager() {
disabled={deleteLoading}
className='h-8 text-xs'
>
取消
{t('cancel')}
</Button>
<Button
variant='destructive'
@@ -586,7 +594,7 @@ export function TaskSchedulesManager() {
className='h-8 text-xs'
>
{deleteLoading && <Spinner className='size-3 mr-1' />}
确认删除
{t('confirmDelete')}
</Button>
</DialogFooter>
</DialogContent>