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

This commit is contained in:
2026-08-12 13:31:18 +08:00
parent 0d74675de0
commit e7374b977e
+227
View File
@@ -0,0 +1,227 @@
import { useEffect, useState } from 'react';
import {
Card,
Descriptions,
Spin,
Typography,
Button,
Space,
Row,
Col,
Statistic,
Tag,
Empty,
List,
} from 'antd';
import {
ArrowLeftOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { projectApi, taskApi, artifactApi } from '@/api';
import type { Project, ProjectStats, Task, Artifact } from '@/types';
import { ProjectStatusTag, TaskStatusTag } from '@/components/constants';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
export default function ProjectDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [project, setProject] = useState<Project | null>(null);
const [stats, setStats] = useState<ProjectStats | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);
const [artifacts, setArtifacts] = useState<Artifact[]>([]);
useEffect(() => {
if (!id) return;
setLoading(true);
Promise.all([
projectApi.get(id),
projectApi.stats(id),
taskApi.list({ project_id: id }),
artifactApi.list({ project_id: id }),
])
.then(([p, s, t, a]) => {
setProject(p);
setStats(s);
setTasks(t);
setArtifacts(a);
})
.catch(() => {})
.finally(() => setLoading(false));
}, [id]);
if (loading) {
return (
<div style={{ textAlign: 'center', padding: 48 }}>
<Spin size="large" />
</div>
);
}
if (!project) {
return <Empty description="项目不存在" />;
}
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/projects')}
>
</Button>
<Title level={4} style={{ margin: 0 }}>
{project.name}
</Title>
<ProjectStatusTag status={project.status} />
</Space>
<Button
icon={<ReloadOutlined />}
onClick={() => window.location.reload()}
>
</Button>
</div>
<Card style={{ marginBottom: 16 }}>
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="项目名称">{project.name}</Descriptions.Item>
<Descriptions.Item label="状态">
<ProjectStatusTag status={project.status} />
</Descriptions.Item>
<Descriptions.Item label="预算">
{project.budget != null ? `¥${project.budget.toFixed(2)}` : '-'}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{dayjs(project.created_at).format('YYYY-MM-DD HH:mm')}
</Descriptions.Item>
{project.start_date && (
<Descriptions.Item label="开始时间">
{dayjs(project.start_date).format('YYYY-MM-DD')}
</Descriptions.Item>
)}
{project.end_date && (
<Descriptions.Item label="结束时间">
{dayjs(project.end_date).format('YYYY-MM-DD')}
</Descriptions.Item>
)}
<Descriptions.Item label="项目描述" span={2}>
{project.description || '暂无描述'}
</Descriptions.Item>
</Descriptions>
</Card>
{stats && (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={12} md={6}>
<Card>
<Statistic title="总任务数" value={stats.total_tasks} />
</Card>
</Col>
<Col xs={12} md={6}>
<Statistic
title="已完成"
value={stats.completed_tasks}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col xs={12} md={6}>
<Card>
<Statistic
title="活跃 AI 工作者"
value={stats.active_workers}
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col xs={12} md={6}>
<Card>
<Statistic
title="总花费"
value={stats.total_cost}
precision={2}
prefix="¥"
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
</Row>
)}
{stats && (
<Card title="任务状态分布" size="small" style={{ marginBottom: 16 }}>
<Space wrap>
{Object.entries(stats.tasks_by_status || {}).map(([status, count]) => (
<Tag key={status} style={{ padding: '4px 12px', fontSize: 14 }}>
<TaskStatusTag status={status as Task['status']} /> {count}
</Tag>
))}
{Object.keys(stats.tasks_by_status || {}).length === 0 && (
<Text type="secondary"></Text>
)}
</Space>
</Card>
)}
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Card title="项目任务" size="small">
{tasks.length > 0 ? (
<List
size="small"
dataSource={tasks.slice(0, 10)}
renderItem={(task) => (
<List.Item
key={task.id}
actions={[<TaskStatusTag status={task.status} />]}
>
<List.Item.Meta
title={
<a onClick={() => navigate('/tasks')}>{task.title}</a>
}
description={dayjs(task.created_at).format('MM-DD HH:mm')}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无任务" />
)}
</Card>
</Col>
<Col xs={24} md={12}>
<Card title="产出物" size="small">
{artifacts.length > 0 ? (
<List
size="small"
dataSource={artifacts.slice(0, 10)}
renderItem={(art) => (
<List.Item key={art.id}>
<List.Item.Meta
title={art.name}
description={`${art.type} · ${dayjs(art.created_at).format('MM-DD HH:mm')}`}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无产出物" />
)}
</Card>
</Col>
</Row>
</div>
);
}