mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
fix(frontend): keep browser API on same-origin rewrites in dev
Document that NEXT_PUBLIC_WAVELET_BACKEND_URL must stay unset for local next dev so axios hits /api/* and Next rewrites proxy to the backend.
This commit is contained in:
@@ -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'),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -190,11 +192,11 @@ export function TaskExecutionsManager() {
|
||||
<SelectValue placeholder='状态' />
|
||||
</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
|
||||
@@ -205,7 +207,7 @@ export function TaskExecutionsManager() {
|
||||
<SelectValue placeholder='任务类型' />
|
||||
</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,24 @@ 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 +281,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 +312,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 +326,7 @@ export function TaskExecutionsManager() {
|
||||
disabled={executionsPage <= 1 || executionsLoading}
|
||||
>
|
||||
<ChevronLeft className='size-4' />
|
||||
上一页
|
||||
{t('prevPage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -327,7 +336,7 @@ export function TaskExecutionsManager() {
|
||||
}
|
||||
disabled={executionsPage >= totalPages || executionsLoading}
|
||||
>
|
||||
下一页
|
||||
{t('nextPage')}
|
||||
<ChevronRight className='size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -336,7 +345,7 @@ 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 ||
|
||||
@@ -348,32 +357,34 @@ 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] ||
|
||||
{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 +392,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 +402,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 +410,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 +436,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 +444,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 +452,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 +484,7 @@ export function TaskExecutionsManager() {
|
||||
) : (
|
||||
<RefreshCw className='size-4' />
|
||||
)}
|
||||
刷新详情
|
||||
{t('refreshDetail')}
|
||||
</Button>
|
||||
{selectedExecution &&
|
||||
selectedExecution.status === 'failed' &&
|
||||
@@ -485,7 +498,7 @@ export function TaskExecutionsManager() {
|
||||
) : (
|
||||
<RotateCcw className='size-4' />
|
||||
)}
|
||||
重试任务
|
||||
{t('retryTask')}
|
||||
</Button>
|
||||
)}
|
||||
</SheetFooter>
|
||||
|
||||
@@ -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';
|
||||
@@ -166,6 +167,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[]>([]);
|
||||
@@ -186,11 +188,13 @@ 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]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskTypes();
|
||||
@@ -249,8 +253,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);
|
||||
|
||||
@@ -259,8 +266,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);
|
||||
@@ -292,10 +299,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) => {
|
||||
@@ -333,13 +340,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>
|
||||
@@ -351,7 +360,7 @@ export function TaskManager() {
|
||||
onClick={() => openDispatchDialog(task.type)}
|
||||
>
|
||||
<Play className='size-3 mr-1' />
|
||||
立即执行
|
||||
{t('executeNow')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,15 +374,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();
|
||||
@@ -403,20 +410,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>
|
||||
</>
|
||||
@@ -474,8 +481,8 @@ export function TaskManager() {
|
||||
/>
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{paramValues[param.name] === 'true'
|
||||
? '开启'
|
||||
: '关闭'}
|
||||
? t('paramOn')
|
||||
: t('paramOff')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -513,7 +520,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>
|
||||
);
|
||||
}
|
||||
@@ -528,7 +535,7 @@ export function TaskManager() {
|
||||
disabled={dispatching}
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDispatch}
|
||||
@@ -540,7 +547,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,13 @@ 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 +134,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 +165,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 +207,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 +218,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 +242,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 +278,7 @@ export function TaskSchedulesManager() {
|
||||
) : (
|
||||
<RefreshCw className='size-4' />
|
||||
)}
|
||||
刷新
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
@@ -283,7 +287,7 @@ export function TaskSchedulesManager() {
|
||||
variant={'secondary'}
|
||||
>
|
||||
<Plus className='size-4 mr-1' />
|
||||
新增定时任务
|
||||
{t('addSchedule')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,28 +303,31 @@ export function TaskSchedulesManager() {
|
||||
) : loading && schedules.length === 0 ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Clock}
|
||||
description='加载定时任务配置中...'
|
||||
description={t('loadingSchedules')}
|
||||
/>
|
||||
) : schedules.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Clock}
|
||||
description='暂无定时任务配置,点击上方按钮新增'
|
||||
/>
|
||||
<EmptyStateWithBorder icon={Clock} 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 +392,17 @@ export function TaskSchedulesManager() {
|
||||
<DialogContent className='sm:max-w-[500px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingSchedule ? '修改定时任务' : '新增定时任务'}
|
||||
{editingSchedule ? t('editSchedule') : t('addSchedule')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
配置定时任务的调度参数和运行载荷。
|
||||
</DialogDescription>
|
||||
<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 +410,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 +427,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 +464,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 +514,8 @@ export function TaskSchedulesManager() {
|
||||
/>
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{paramValues[param.name] === 'true'
|
||||
? '开启'
|
||||
: '关闭'}
|
||||
? t('paramOn')
|
||||
: t('paramOff')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -547,7 +552,7 @@ export function TaskSchedulesManager() {
|
||||
disabled={submitLoading}
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
@@ -555,7 +560,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,10 +570,8 @@ export function TaskSchedulesManager() {
|
||||
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<DialogContent className='sm:max-w-[400px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>删除定时任务</DialogTitle>
|
||||
<DialogDescription>
|
||||
确定要删除此定时任务吗?该操作不可撤销,且会立即取消其在调度器中的调度计划。
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t('deleteScheduleTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('deleteScheduleDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className='gap-2 sm:gap-0'>
|
||||
<Button
|
||||
@@ -577,7 +580,7 @@ export function TaskSchedulesManager() {
|
||||
disabled={deleteLoading}
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
@@ -586,7 +589,7 @@ export function TaskSchedulesManager() {
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
{deleteLoading && <Spinner className='size-3 mr-1' />}
|
||||
确认删除
|
||||
{t('confirmDelete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user