feat: AI Worker 平台 MVP v1.0.0 - 多租户/项目管理/AI Worker/任务编排/HITL审核/成本治理
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user