feat: AI Worker 平台 MVP v1.0.0 - 多租户/项目管理/AI Worker/任务编排/HITL审核/成本治理

This commit is contained in:
2026-08-12 13:31:21 +08:00
parent 27ad31ff89
commit 162c5be49f
+464
View File
@@ -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<Task[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [workers, setWorkers] = useState<Worker[]>([]);
const [loading, setLoading] = useState(false);
const [viewMode, setViewMode] = useState<string>('kanban');
const [filterProject, setFilterProject] = useState<string | undefined>();
const [filterStatus, setFilterStatus] = useState<TaskStatus | undefined>();
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<Task | null>(null);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const [detailTask, setDetailTask] = useState<Task | null>(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 = () => (
<Spin spinning={loading}>
<Row gutter={[12, 12]}>
{taskStatusList.map((status) => {
const colTasks = tasks.filter((t) => t.status === status);
const cfg = taskStatusConfig[status];
return (
<Col xs={24} sm={12} md={6} key={status}>
<Card
size="small"
title={
<Space>
<span
style={{
display: 'inline-block',
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: cfg.color,
}}
/>
{cfg.label}
<Tag>{colTasks.length}</Tag>
</Space>
}
styles={{ body: { backgroundColor: '#f5f5f5', minHeight: 300, padding: 8 } }}
>
{colTasks.length === 0 ? (
<div style={{ textAlign: 'center', padding: 20, color: '#999' }}>
</div>
) : (
colTasks.map((task) => (
<Card
key={task.id}
size="small"
className="kanban-card"
onClick={() => setDetailTask(task)}
style={{ marginBottom: 8 }}
>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{task.title}
</div>
<Space size={4}>
<PriorityTag priority={task.priority} />
{task.cost > 0 && (
<Tag color="orange">¥{task.cost.toFixed(2)}</Tag>
)}
</Space>
{task.description && (
<div
style={{
fontSize: 12,
color: '#999',
marginTop: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{task.description}
</div>
)}
</Card>
))
)}
</Card>
</Col>
);
})}
</Row>
</Spin>
);
// ============ Table View ============
const tableColumns = [
{
title: '任务名称',
dataIndex: 'title',
key: 'title',
render: (text: string, record: Task) => (
<a onClick={() => setDetailTask(record)}>{text}</a>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: TaskStatus) => <TaskStatusTag status={status} />,
},
{
title: '优先级',
dataIndex: 'priority',
key: 'priority',
render: (p: Task['priority']) => <PriorityTag priority={p} />,
},
{
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) => (
<Space>
<Button
type="link"
size="small"
onClick={() => setDetailTask(record)}
>
</Button>
<Button
type="link"
size="small"
icon={<PlayCircleOutlined />}
onClick={() => handleExecute(record)}
disabled={record.status === 'done' || record.status === 'cancelled'}
>
</Button>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
/>
<Popconfirm
title="确定删除此任务?"
onConfirm={() => handleDelete(record.id)}
>
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
flexWrap: 'wrap',
gap: 8,
}}
>
<Title level={4} style={{ margin: 0 }}>
</Title>
<Space>
<Button icon={<ReloadOutlined />} onClick={loadData}>
</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</Space>
</div>
<Card style={{ marginBottom: 16 }}>
<Space wrap>
<Select
placeholder="筛选项目"
allowClear
style={{ width: 200 }}
value={filterProject}
onChange={setFilterProject}
options={projects.map((p) => ({ value: p.id, label: p.name }))}
/>
<Select
placeholder="筛选状态"
allowClear
style={{ width: 150 }}
value={filterStatus}
onChange={setFilterStatus}
options={taskStatusList.map((s) => ({
value: s,
label: taskStatusConfig[s].label,
}))}
/>
<Segmented
options={[
{ label: '看板', value: 'kanban' },
{ label: '列表', value: 'table' },
]}
value={viewMode}
onChange={setViewMode}
/>
</Space>
</Card>
{viewMode === 'kanban' ? (
renderKanban()
) : (
<Card>
{loading ? (
<div style={{ textAlign: 'center', padding: 48 }}>
<Spin />
</div>
) : tasks.length > 0 ? (
<Table
dataSource={tasks}
columns={tableColumns}
rowKey="id"
pagination={{ pageSize: 15 }}
/>
) : (
<Empty description="暂无任务" />
)}
</Card>
)}
{/* Create/Edit Modal */}
<Modal
title={editing ? '编辑任务' : '新建任务'}
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={handleSave}
confirmLoading={saving}
okText={editing ? '保存' : '创建'}
cancelText="取消"
width={600}
>
<Form form={form} layout="vertical">
{!editing && (
<Form.Item
name="project_id"
label="所属项目"
rules={[{ required: true, message: '请选择项目' }]}
>
<Select
placeholder="选择项目"
options={projects.map((p) => ({
value: p.id,
label: p.name,
}))}
/>
</Form.Item>
)}
<Form.Item
name="title"
label="任务标题"
rules={[{ required: true, message: '请输入任务标题' }]}
>
<Input placeholder="请输入任务标题" />
</Form.Item>
<Form.Item name="description" label="任务描述">
<TextArea rows={3} placeholder="请输入任务描述" />
</Form.Item>
<Form.Item name="priority" label="优先级">
<Select options={priorities} />
</Form.Item>
<Form.Item name="max_retries" label="最大重试次数">
<InputNumber min={0} max={10} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="due_date" label="截止时间">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="input_data" label="输入数据 (JSON)">
<TextArea
rows={4}
placeholder='例如:{"prompt": "分析数据"}'
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
</Form>
</Modal>
{/* Detail Modal */}
<TaskDetailModal
task={detailTask}
workers={workers}
onClose={() => setDetailTask(null)}
onUpdated={() => {
loadData();
}}
/>
</div>
);
}