"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 {AdminService, DispatchTaskRequest, TaskMeta} 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, 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 AdminService.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 AdminService.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' ? (