import { useEffect, useState, type ReactNode } from 'react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { useCreateTask, useSettings, useUpdateTask } from '@/hooks/useMonitor' import { describeSchedule } from '@/lib/monitorFormat' import type { MonitorMode, MonitorTask, ScheduleMode, TaskCreatePayload, } from '@/types/monitor' interface TaskEditorDialogProps { open: boolean onOpenChange: (open: boolean) => void task?: MonitorTask | null } const TEXTAREA_CLASS = 'w-full rounded-md border border-cyber-border-DEFAULT bg-cyber-bg-tertiary p-2 font-mono text-[11px] text-cyber-text-primary outline-none focus-visible:border-cyber-neon-cyan/50' // The backend enforces this floor too; every run launches a browser and hits // XHS several times, so tight polling is what triggers rate limiting. const INTERVAL_OPTIONS = [ { value: '30', label: '30 分钟' }, { value: '60', label: '1 小时' }, { value: '120', label: '2 小时' }, { value: '360', label: '6 小时' }, { value: '720', label: '12 小时' }, { value: '1440', label: '1 天' }, { value: '4320', label: '3 天' }, { value: '10080', label: '7 天' }, ] const SCHEDULE_MODE_OPTIONS: Array<{ value: ScheduleMode; label: string }> = [ { value: 'interval', label: '固定间隔' }, { value: 'daily', label: '每天定时' }, { value: 'weekly', label: '每周定时' }, ] const HOURS = Array.from({ length: 24 }, (_, hour) => hour) const MINUTES = Array.from({ length: 60 }, (_, minute) => minute) const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] const pad = (value: number) => String(value).padStart(2, '0') function toggleNumber(values: number[], value: number): number[] { return values.includes(value) ? values.filter((item) => item !== value) : [...values, value].sort((a, b) => a - b) } /** A selectable pill. Used for the hours of the day and the weekdays. */ function Chip({ active, onClick, children, }: { active: boolean onClick: () => void children: ReactNode }) { return ( ) } export function TaskEditorDialog({ open, onOpenChange, task }: TaskEditorDialogProps) { const isEdit = Boolean(task) const createTask = useCreateTask() const updateTask = useUpdateTask() const { data: settings } = useSettings() const [name, setName] = useState('') const [mode, setMode] = useState('creator') const [intervalMinutes, setIntervalMinutes] = useState('360') const [scheduleMode, setScheduleMode] = useState('interval') const [scheduleHours, setScheduleHours] = useState([]) const [scheduleDays, setScheduleDays] = useState([]) const [scheduleMinute, setScheduleMinute] = useState('0') const [maxNotes, setMaxNotes] = useState('20') const [enableComments, setEnableComments] = useState(true) const [maxComments, setMaxComments] = useState('50') const [notifyEnabled, setNotifyEnabled] = useState(false) const [targets, setTargets] = useState('') // Reset the form whenever the dialog is (re)opened. For a new task the // starting values come from the Settings page, so those defaults actually // govern rather than being overwritten by hardcoded numbers here. useEffect(() => { if (!open) return setName(task?.name ?? '') setMode(task?.mode ?? 'creator') setIntervalMinutes( String(task?.interval_minutes ?? settings?.values['collect.default_interval_minutes'] ?? 360), ) setScheduleMode(task?.schedule_mode ?? 'interval') setScheduleHours(task?.schedule_hours ?? []) setScheduleDays(task?.schedule_days ?? []) setScheduleMinute(String(task?.schedule_minute ?? 0)) setMaxNotes( String(task?.max_notes_count ?? settings?.values['collect.default_max_notes'] ?? 20), ) setEnableComments(task?.enable_comments ?? true) setMaxComments( String(task?.max_comments_count ?? settings?.values['collect.default_max_comments'] ?? 50), ) setNotifyEnabled(task?.notify_enabled ?? false) setTargets(task ? task.targets.map((t) => t.raw_value || t.external_id).join('\n') : '') }, [open, task, settings]) const targetList = targets .split(/[\n,]+/) .map((value) => value.trim()) .filter(Boolean) // Switching into a clock mode with nothing chosen would be invalid, so seed a // sensible starting point -- the operator then edits rather than fills a blank. const selectScheduleMode = (next: ScheduleMode) => { setScheduleMode(next) if (next !== 'interval' && scheduleHours.length === 0) setScheduleHours([9]) if (next === 'weekly' && scheduleDays.length === 0) setScheduleDays([0, 1, 2, 3, 4]) } const pending = createTask.isPending || updateTask.isPending // Note-mode and creator-mode targets are different shapes, so switching mode // would silently mis-parse the list. The backend decides parsing from the // task's stored mode, hence mode is fixed once created. const clockIncomplete = scheduleMode !== 'interval' && (scheduleHours.length === 0 || (scheduleMode === 'weekly' && scheduleDays.length === 0)) const canSubmit = name.trim().length > 0 && targetList.length > 0 && !pending && !clockIncomplete const handleSubmit = () => { const payload: TaskCreatePayload = { name: name.trim(), mode, interval_minutes: Number(intervalMinutes), schedule_mode: scheduleMode, // Interval mode has no clock times, so send none rather than leave stale // ones behind from a mode the operator tried and abandoned. schedule_hours: scheduleMode === 'interval' ? [] : scheduleHours, schedule_days: scheduleMode === 'weekly' ? scheduleDays : [], schedule_minute: Number(scheduleMinute), max_notes_count: Number(maxNotes), enable_comments: enableComments, max_comments_count: Number(maxComments), run_timeout_seconds: 3600, enabled: true, notify_enabled: notifyEnabled, targets: targetList, } const done = () => onOpenChange(false) if (isEdit && task) { updateTask.mutate({ id: task.id, payload }, { onSuccess: done }) } else { createTask.mutate(payload, { onSuccess: done }) } } return ( {isEdit ? '编辑监控任务' : '新建监控任务'} 定时重复采集目标,并与上一次快照对比,产出新增作品、新增评论和互动数据变化。
setName(event.target.value)} placeholder="例如:竞品博主监控" className="h-9 text-xs" />
{isEdit && (

类型创建后不可更改

)}
{SCHEDULE_MODE_OPTIONS.map((option) => ( ))}
{scheduleMode === 'interval' ? (

从上一轮开始计时, 所以某轮跑得久也不会让下一轮紧接着触发。

) : (
{scheduleMode === 'weekly' && (
星期
{WEEKDAYS.map((label, day) => ( setScheduleDays((prev) => toggleNumber(prev, day))} > {label} ))}
)}
时间(可多选,点一次选中、再点取消)
{HOURS.map((hour) => ( setScheduleHours((prev) => toggleNumber(prev, hour))} > {pad(hour)} ))}
分钟 所有时间点共用
{describeSchedule( scheduleMode, Number(intervalMinutes), scheduleHours, scheduleDays, Number(scheduleMinute), )} 按服务器本地时间
)}