This commit is contained in:
ryan
2026-06-08 14:57:19 +08:00
parent 72c74803be
commit f136c9abdb
9 changed files with 821 additions and 36 deletions
+107 -35
View File
@@ -6,6 +6,7 @@ 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 {Spinner} from "@/components/ui/spinner"
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
import {Calendar as CalendarIcon, Clock, FileText, Info, Layers, Play} from "lucide-react"
@@ -140,6 +141,7 @@ export function TaskManager() {
const [startTime, setStartTime] = useState<Date | undefined>(undefined)
const [endTime, setEndTime] = useState<Date | undefined>(undefined)
const [userId, setUserId] = useState("")
const [paramValues, setParamValues] = useState<Record<string, string>>({})
const fetchTaskTypes = useCallback(async () => {
try {
@@ -158,6 +160,21 @@ export function TaskManager() {
fetchTaskTypes()
}, [fetchTaskTypes])
useEffect(() => {
if (selectedTaskType) {
const targetTask = taskTypes.find(t => t.type === selectedTaskType)
if (targetTask?.params) {
const initialValues: Record<string, string> = {}
targetTask.params.forEach(p => {
initialValues[p.name] = ""
})
setParamValues(initialValues)
} else {
setParamValues({})
}
}
}, [selectedTaskType, taskTypes])
const handleDispatch = async () => {
if (!selectedTaskType) return
@@ -170,7 +187,7 @@ export function TaskManager() {
task_type: selectedTaskType
}
if (targetTask?.type === 'order_sync') {
if (targetTask?.supports_time) {
if (startTime) params.start_time = startTime.toISOString()
if (endTime) params.end_time = endTime.toISOString()
}
@@ -179,6 +196,21 @@ export function TaskManager() {
if (userId) params.user_id = userId
}
// Handle dynamic parameters
if (targetTask?.params && targetTask.params.length > 0) {
const payloadData: Record<string, string> = {}
for (const param of targetTask.params) {
const val = (paramValues[param.name] || "").trim()
if (param.required && !val) {
toast.error(`${param.label}不能为空`)
setDispatching(false)
return
}
payloadData[param.name] = val
}
params.payload = JSON.stringify(payloadData)
}
const taskID = await AdminService.dispatchTask(params)
toast.success('任务下发成功', {
@@ -189,6 +221,7 @@ export function TaskManager() {
setStartTime(undefined)
setEndTime(undefined)
setUserId("")
setParamValues({})
} catch (err) {
toast.error('任务下发失败', {
description: err instanceof Error ? err.message : '未知错误'
@@ -318,42 +351,81 @@ export function TaskManager() {
</div>
</div>
{selectedTaskType === 'order_sync' && (
<>
<div className="grid gap-2">
<Label>开始时间</Label>
<DatePickerWithTime date={startTime} setDate={setStartTime} />
<p className="text-xs text-muted-foreground">选择同步的起始时间点</p>
</div>
<div className="grid gap-2">
<Label>结束时间</Label>
<DatePickerWithTime date={endTime} setDate={setEndTime} />
<p className="text-xs text-muted-foreground">选择同步的结束时间点</p>
</div>
</>
)}
{(() => {
const targetTask = getSelectedTaskMeta()
if (targetTask?.supports_time) {
return (
<>
<div className="grid gap-2">
<Label>开始时间</Label>
<DatePickerWithTime date={startTime} setDate={setStartTime} />
<p className="text-xs text-muted-foreground">选择同步的起始时间点</p>
</div>
<div className="grid gap-2">
<Label>结束时间</Label>
<DatePickerWithTime date={endTime} setDate={setEndTime} />
<p className="text-xs text-muted-foreground">选择同步的结束时间点</p>
</div>
</>
)
}
return null
})()}
{selectedTaskType === 'user_gamification' && (
<div className="grid gap-2">
<Label htmlFor="user-id">用户 ID</Label>
<Input
id="user-id"
type="number"
placeholder="e.g. 10086"
className="font-mono text-xs"
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<p className="text-xs text-muted-foreground">指定需要更新积分数据的用户 ID</p>
</div>
)}
{(() => {
const targetTask = getSelectedTaskMeta()
if (!targetTask || !targetTask.params || targetTask.params.length === 0) {
return null
}
return (
<div className="space-y-4">
{targetTask.params.map((param) => (
<div key={param.name} className="grid gap-2">
<Label htmlFor={`param-${param.name}`} className="flex items-center gap-1">
{param.label}
{param.required && <span className="text-destructive font-bold">*</span>}
</Label>
{param.type === 'text' ? (
<Textarea
id={`param-${param.name}`}
placeholder={param.placeholder}
className="text-xs min-h-[80px]"
value={paramValues[param.name] || ""}
onChange={(e) => setParamValues(prev => ({ ...prev, [param.name]: e.target.value }))}
/>
) : (
<Input
id={`param-${param.name}`}
type={param.type === 'number' ? 'number' : 'text'}
placeholder={param.placeholder}
className="text-xs"
value={paramValues[param.name] || ""}
onChange={(e) => setParamValues(prev => ({ ...prev, [param.name]: e.target.value }))}
/>
)}
{param.description && (
<p className="text-[10px] text-muted-foreground">{param.description}</p>
)}
</div>
))}
</div>
)
})()}
{!['order_sync', 'user_gamification'].includes(selectedTaskType || '') && (
<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>
</div>
)}
{(() => {
const targetTask = getSelectedTaskMeta()
const hasTime = targetTask?.supports_time
const hasParams = targetTask?.params && targetTask.params.length > 0
if (!hasTime && !hasParams) {
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>
</div>
)
}
return null
})()}
</div>
<DialogFooter>
@@ -193,6 +193,14 @@ export class AdminService extends BaseService {
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map(p => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
type: p.Type || p.type || '',
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
+36
View File
@@ -46,6 +46,24 @@ export interface UpdateSystemConfigRequest {
// ==================== 任务管理 ====================
/**
* 任务参数响应
*/
export interface TaskParamResponse {
Name?: string;
name?: string;
Label?: string;
label?: string;
Type?: string;
type?: string;
Required?: boolean;
required?: boolean;
Placeholder?: string;
placeholder?: string;
Description?: string;
description?: string;
}
/**
* 任务类型响应
*/
@@ -64,6 +82,20 @@ export interface TaskTypeResponse {
max_retry?: number;
Queue?: string;
queue?: string;
Params?: TaskParamResponse[];
params?: TaskParamResponse[];
}
/**
* 任务参数定义
*/
export interface TaskParam {
name: string;
label: string;
type: string;
required: boolean;
placeholder: string;
description: string;
}
/**
@@ -84,6 +116,8 @@ export interface TaskMeta {
max_retry: number;
/** 队列名称 */
queue: string;
/** 任务所需的自定义参数定义 */
params?: TaskParam[];
}
/**
@@ -98,6 +132,8 @@ export interface DispatchTaskRequest {
end_time?: string;
/** 用户 ID(可选,仅部分任务需要) */
user_id?: string;
/** 任务自定义参数 JSON 字符串 */
payload?: string;
}
export type TaskExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed';