'use client'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Spinner } from '@/components/ui/spinner'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Calendar as CalendarIcon, Clock, Info, Layers, Play, } from 'lucide-react'; import type { DispatchTaskRequest, TaskMeta } from '@/lib/services/admin'; import services from '@/lib/services'; import { buildTaskPayload } from '@/lib/task-param-utils'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import { format } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import { Calendar } from '@/components/ui/calendar'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; const TASK_CONFIGS: Record< string, { icon: React.ComponentType<{ className?: string }>; color: string; gradient: string; } > = { order_sync: { icon: Layers, color: 'text-blue-600 dark:text-blue-400', gradient: 'from-blue-500/10 via-blue-500/5 to-transparent border-blue-200/50 dark:border-blue-800/50 hover:border-blue-400 dark:hover:border-blue-500', }, user_gamification: { icon: Play, color: 'text-amber-600 dark:text-amber-400', gradient: 'from-amber-500/10 via-amber-500/5 to-transparent border-amber-200/50 dark:border-amber-800/50 hover:border-amber-400 dark:hover:border-amber-500', }, dispute_auto_refund: { icon: Clock, color: 'text-rose-600 dark:text-rose-400', gradient: 'from-rose-500/10 via-rose-500/5 to-transparent border-rose-200/50 dark:border-rose-800/50 hover:border-rose-400 dark:hover:border-rose-500', }, }; const DEFAULT_TASK_CONFIG = { icon: Layers, color: 'text-zinc-600 dark:text-zinc-400', gradient: 'from-zinc-500/10 via-zinc-500/5 to-transparent border-zinc-200/50 dark:border-zinc-800/50 hover:border-zinc-400 dark:hover:border-zinc-500', }; function DatePickerWithTime({ date, setDate, }: { date: Date | undefined; setDate: (date: Date | undefined) => void; }) { const timeString = date ? format(date, 'HH:mm:ss') : ''; const handleDateSelect = (newDate: Date | undefined) => { if (!newDate) { setDate(undefined); return; } const updated = new Date(newDate); if (date) { updated.setHours(date.getHours()); updated.setMinutes(date.getMinutes()); updated.setSeconds(date.getSeconds()); } else { updated.setHours(0, 0, 0, 0); } setDate(updated); }; const handleTimeChange = (e: React.ChangeEvent) => { const newTime = e.target.value; if (!newTime) return; if (date) { const [hours, minutes, seconds] = newTime.split(':').map(Number); const updated = new Date(date); updated.setHours(hours || 0); updated.setMinutes(minutes || 0); updated.setSeconds(seconds || 0); setDate(updated); } else { const today = new Date(); const [hours, minutes, seconds] = newTime.split(':').map(Number); today.setHours(hours || 0); today.setMinutes(minutes || 0); today.setSeconds(seconds || 0); setDate(today); } }; return (
); } export function TaskManager() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [taskTypes, setTaskTypes] = useState([]); const [dispatching, setDispatching] = useState(false); const [selectedTaskType, setSelectedTaskType] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [startTime, setStartTime] = useState(undefined); const [endTime, setEndTime] = useState(undefined); const [userId, setUserId] = useState(''); const [paramValues, setParamValues] = useState>({}); const fetchTaskTypes = useCallback(async () => { try { setLoading(true); setError(null); const data = await services.adminTask.getTaskTypes(); setTaskTypes(data); } catch (err) { setError(err instanceof Error ? err : new Error('加载任务类型失败')); } finally { setLoading(false); } }, []); useEffect(() => { fetchTaskTypes(); }, [fetchTaskTypes]); useEffect(() => { if (selectedTaskType) { const targetTask = taskTypes.find((t) => t.type === selectedTaskType); if (targetTask?.params) { const initialValues: Record = {}; targetTask.params.forEach((p) => { initialValues[p.name] = ''; }); setParamValues(initialValues); } else { setParamValues({}); } } }, [selectedTaskType, taskTypes]); const handleDispatch = async () => { if (!selectedTaskType) return; try { setDispatching(true); const targetTask = taskTypes.find((t) => t.type === selectedTaskType); const params: DispatchTaskRequest = { task_type: selectedTaskType, }; if (targetTask?.supports_time) { if (startTime) params.start_time = startTime.toISOString(); if (endTime) params.end_time = endTime.toISOString(); } if (targetTask?.type === 'user_gamification') { if (userId) params.user_id = userId; } // Handle dynamic parameters — type coercion (number vs string) is // centralised in buildTaskPayload; do not inline it here. if (targetTask?.params && targetTask.params.length > 0) { const { payload, error } = buildTaskPayload( targetTask.params, paramValues, ); if (error) { toast.error(error); setDispatching(false); return; } params.payload = payload ?? undefined; } const taskID = await services.adminTask.dispatchTask(params); toast.success('任务下发成功', { description: `已成功将任务 ${targetTask?.name || selectedTaskType} 加入队列:${taskID}`, }); setDialogOpen(false); setStartTime(undefined); setEndTime(undefined); setUserId(''); setParamValues({}); } catch (err) { toast.error('任务下发失败', { description: err instanceof Error ? err.message : '未知错误', }); } finally { setDispatching(false); } }; const openDispatchDialog = (type: string) => { setSelectedTaskType(type); setDialogOpen(true); }; const getSelectedTaskMeta = () => { return taskTypes.find((t) => t.type === selectedTaskType); }; return (

{error ? (
) : loading && taskTypes.length === 0 ? ( ) : taskTypes.length === 0 ? ( ) : (
{taskTypes.map((task, index) => { const config = TASK_CONFIGS[task.type] || DEFAULT_TASK_CONFIG; return (

{task.name}

{task.description}

{task.queue}
类型:{task.type} 重试:{task.max_retry}
); })}
)}
下发任务 配置任务参数并启动,任务将在后台异步执行。
{(() => { const meta = getSelectedTaskMeta(); if (!meta) return null; const config = TASK_CONFIGS[meta.type] || DEFAULT_TASK_CONFIG; const Icon = config.icon; return ( <>
{meta.name} {meta.type}
); })()}
{(() => { const targetTask = getSelectedTaskMeta(); if (targetTask?.supports_time) { return ( <>

选择同步的起始时间点

选择同步的结束时间点

); } return null; })()} {(() => { const targetTask = getSelectedTaskMeta(); if ( !targetTask || !targetTask.params || targetTask.params.length === 0 ) { return null; } return (
{targetTask.params.map((param) => (
{param.type === 'text' ? (