diff --git a/frontend/src/pages/Tasks.tsx b/frontend/src/pages/Tasks.tsx new file mode 100644 index 0000000..a801fb8 --- /dev/null +++ b/frontend/src/pages/Tasks.tsx @@ -0,0 +1,464 @@ +import { useEffect, useState, useCallback } from 'react'; +import { + Row, + Col, + Card, + Button, + Space, + Modal, + Form, + Input, + Select, + DatePicker, + InputNumber, + Typography, + Segmented, + Table, + Tag, + Popconfirm, + message, + Spin, + Empty, +} from 'antd'; +import { + PlusOutlined, + ReloadOutlined, + EditOutlined, + DeleteOutlined, + PlayCircleOutlined, +} from '@ant-design/icons'; +import { taskApi, projectApi, workerApi } from '@/api'; +import type { Task, TaskCreate, TaskStatus, Project, Worker } from '@/types'; +import { TaskStatusTag, PriorityTag, taskStatusConfig, taskStatusList } from '@/components/constants'; +import TaskDetailModal from '@/pages/TaskDetail'; +import dayjs from 'dayjs'; + +const { Title, Text } = Typography; +const { TextArea } = Input; + +const priorities = [ + { value: 'low', label: '低' }, + { value: 'medium', label: '中' }, + { value: 'high', label: '高' }, + { value: 'urgent', label: '紧急' }, +]; + +export default function Tasks() { + const [tasks, setTasks] = useState([]); + const [projects, setProjects] = useState([]); + const [workers, setWorkers] = useState([]); + const [loading, setLoading] = useState(false); + const [viewMode, setViewMode] = useState('kanban'); + const [filterProject, setFilterProject] = useState(); + const [filterStatus, setFilterStatus] = useState(); + + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + const [saving, setSaving] = useState(false); + const [detailTask, setDetailTask] = useState(null); + + const loadData = useCallback(() => { + setLoading(true); + Promise.all([ + taskApi.list({ project_id: filterProject, status: filterStatus }), + projectApi.list(), + workerApi.list(), + ]) + .then(([t, p, w]) => { + setTasks(t); + setProjects(p); + setWorkers(w); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [filterProject, filterStatus]); + + useEffect(() => { + loadData(); + }, [loadData]); + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ priority: 'medium', max_retries: 3 }); + setModalOpen(true); + }; + + const openEdit = (task: Task) => { + setEditing(task); + form.setFieldsValue({ + title: task.title, + description: task.description, + priority: task.priority, + max_retries: task.max_retries, + }); + setModalOpen(true); + }; + + const handleSave = async () => { + try { + const values = await form.validateFields(); + setSaving(true); + + const payload: TaskCreate = { + title: values.title, + description: values.description, + priority: values.priority, + max_retries: values.max_retries, + }; + + if (values.due_date) { + payload.due_date = values.due_date.toISOString(); + } + + if (values.input_data) { + try { + payload.input_data = JSON.parse(values.input_data); + } catch { + message.error('输入数据 JSON 格式错误'); + setSaving(false); + return; + } + } + + if (editing) { + await taskApi.update(editing.id, payload); + message.success('任务更新成功'); + } else { + const projectId = values.project_id || filterProject || projects[0]?.id; + if (!projectId) { + message.error('请先选择项目'); + setSaving(false); + return; + } + await taskApi.create(projectId, payload); + message.success('任务创建成功'); + } + setModalOpen(false); + loadData(); + } catch (err) { + // form validation error + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + try { + await taskApi.delete(id); + message.success('删除成功'); + loadData(); + } catch { + /* handled */ + } + }; + + const handleExecute = async (task: Task) => { + try { + const result = await taskApi.execute(task.id); + if (result.error) { + message.error(`执行失败:${result.error}`); + } else { + message.success('任务执行成功'); + } + loadData(); + } catch { + /* handled */ + } + }; + + // ============ Kanban View ============ + const renderKanban = () => ( + + + {taskStatusList.map((status) => { + const colTasks = tasks.filter((t) => t.status === status); + const cfg = taskStatusConfig[status]; + return ( + + + + {cfg.label} + {colTasks.length} + + } + styles={{ body: { backgroundColor: '#f5f5f5', minHeight: 300, padding: 8 } }} + > + {colTasks.length === 0 ? ( +
+ 暂无任务 +
+ ) : ( + colTasks.map((task) => ( + setDetailTask(task)} + style={{ marginBottom: 8 }} + > +
+ {task.title} +
+ + + {task.cost > 0 && ( + ¥{task.cost.toFixed(2)} + )} + + {task.description && ( +
+ {task.description} +
+ )} +
+ )) + )} +
+ + ); + })} +
+
+ ); + + // ============ Table View ============ + const tableColumns = [ + { + title: '任务名称', + dataIndex: 'title', + key: 'title', + render: (text: string, record: Task) => ( + setDetailTask(record)}>{text} + ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: TaskStatus) => , + }, + { + title: '优先级', + dataIndex: 'priority', + key: 'priority', + render: (p: Task['priority']) => , + }, + { + title: '花费', + dataIndex: 'cost', + key: 'cost', + render: (v: number) => (v > 0 ? `¥${v.toFixed(2)}` : '-'), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + render: (t: string) => dayjs(t).format('MM-DD HH:mm'), + }, + { + title: '操作', + key: 'action', + width: 280, + render: (_: unknown, record: Task) => ( + + + + + + + + + + + ({ + value: s, + label: taskStatusConfig[s].label, + }))} + /> + + + + + {viewMode === 'kanban' ? ( + renderKanban() + ) : ( + + {loading ? ( +
+ +
+ ) : tasks.length > 0 ? ( + + ) : ( + + )} + + )} + + {/* Create/Edit Modal */} + setModalOpen(false)} + onOk={handleSave} + confirmLoading={saving} + okText={editing ? '保存' : '创建'} + cancelText="取消" + width={600} + > +
+ {!editing && ( + + + + +