Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b5ab072fa | ||
|
|
1c8952e063 | ||
|
|
4385f9aa19 | ||
|
|
7609c35d7d | ||
|
|
73b8e6ad47 | ||
|
|
6ad31778a2 | ||
|
|
6042967b33 | ||
|
|
20736f0fb1 | ||
|
|
fca4c517e4 | ||
|
|
813f24751f | ||
|
|
364cbfe6c8 | ||
|
|
58d41e5852 | ||
|
|
7c2b7fb4cf | ||
|
|
a99f049589 | ||
|
|
2512ef72b7 | ||
|
|
4409d6ed5b | ||
|
|
4acfd06184 | ||
|
|
5450c0e248 | ||
|
|
cc697ad399 | ||
|
|
7d5a1d0c6f | ||
|
|
dc2ed9b970 | ||
|
|
d1a89cda51 | ||
|
|
bd220050c7 | ||
|
|
c4020f2725 | ||
|
|
e36c82c123 | ||
|
|
c8f8ffc3e8 | ||
|
|
aaa4f9e202 | ||
|
|
89831aee21 | ||
|
|
8ff1902937 | ||
|
|
b5fe33ce47 | ||
|
|
d3586b4c77 | ||
|
|
e127003ee9 | ||
|
|
9125cd7ae4 | ||
|
|
d1c01ea847 | ||
|
|
1dea91f295 | ||
|
|
ff795eb21e | ||
|
|
c0fa083328 | ||
|
|
ad9887b102 | ||
|
|
4ea20216f1 | ||
|
|
676d1362d8 | ||
|
|
7b9db415b1 | ||
|
|
70deedfdb1 | ||
|
|
64afcc47d9 | ||
|
|
3df45e9767 | ||
|
|
f4858ee73f | ||
|
|
3eface01cd | ||
|
|
725c0c6f79 | ||
|
|
76b9e76bf7 | ||
|
|
c8f4060988 | ||
|
|
1dba371374 | ||
|
|
2ffa8724af | ||
|
|
73cca10bbb | ||
|
|
f368fd590f | ||
|
|
b04e5f7fa6 | ||
|
|
a610fb6e08 | ||
|
|
68ab70c7eb | ||
|
|
c7ad2ac2ca | ||
|
|
2d79408644 | ||
|
|
86b8fb7485 | ||
|
|
0349b0c75c | ||
|
|
559133bcb2 | ||
|
|
126e571ed1 | ||
|
|
5692db6621 | ||
|
|
7545ead561 | ||
|
|
e3531e9e96 | ||
|
|
92d51710c1 | ||
|
|
45ec4b0321 | ||
|
|
fa492b1528 | ||
|
|
361e00de1f | ||
|
|
abe175d87f | ||
|
|
3e5f33851c | ||
|
|
d7b399ef76 | ||
|
|
44ea460b28 | ||
|
|
5cf474c7c2 | ||
|
|
e957ae978c | ||
|
|
6043ddf69e | ||
|
|
5b9fd5a12d | ||
|
|
ffa7dee85a | ||
|
|
c6dc0832e2 | ||
|
|
62b96b3390 | ||
|
|
842ba96631 | ||
|
|
fd5bfc653e | ||
|
|
6f71760057 | ||
|
|
191c74b061 | ||
|
|
4ab6b24793 | ||
|
|
bfdde2b1c2 | ||
|
|
7069ec3827 | ||
|
|
940428b683 | ||
|
|
3316ac45ff | ||
|
|
d883c982b3 | ||
|
|
6644050290 | ||
|
|
cb4bcd67c5 | ||
|
|
9dfd6c94dc | ||
|
|
5f7b64a68f | ||
|
|
c9d0241369 | ||
|
|
72de66f87b | ||
|
|
f3cb259965 | ||
|
|
30905c6e6a | ||
|
|
273947b733 | ||
|
|
54683bf87b | ||
|
|
1db7b10ae6 | ||
|
|
b63bcaf495 | ||
|
|
6f9442f7fb | ||
|
|
54a8646b94 | ||
|
|
def2f09c3d |
@@ -12,7 +12,6 @@ import Workers from '@/pages/Workers';
|
|||||||
import Reviews from '@/pages/Reviews';
|
import Reviews from '@/pages/Reviews';
|
||||||
import Alerts from '@/pages/Alerts';
|
import Alerts from '@/pages/Alerts';
|
||||||
import KnowledgeBase from '@/pages/KnowledgeBase';
|
import KnowledgeBase from '@/pages/KnowledgeBase';
|
||||||
import DagCanvas from '@/pages/DagCanvas';
|
|
||||||
|
|
||||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
@@ -62,7 +61,6 @@ export default function App() {
|
|||||||
<Route path="reviews" element={<Reviews />} />
|
<Route path="reviews" element={<Reviews />} />
|
||||||
<Route path="alerts" element={<Alerts />} />
|
<Route path="alerts" element={<Alerts />} />
|
||||||
<Route path="knowledge" element={<KnowledgeBase />} />
|
<Route path="knowledge" element={<KnowledgeBase />} />
|
||||||
<Route path="dag" element={<DagCanvas />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
AlertOutlined,
|
AlertOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
ApartmentOutlined,
|
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
@@ -63,11 +62,6 @@ const menuItems: MenuProps['items'] = [
|
|||||||
icon: <CheckSquareOutlined />,
|
icon: <CheckSquareOutlined />,
|
||||||
label: '任务管理',
|
label: '任务管理',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: '/dag',
|
|
||||||
icon: <ApartmentOutlined />,
|
|
||||||
label: 'DAG 编排',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: '/workers',
|
key: '/workers',
|
||||||
icon: <RobotOutlined />,
|
icon: <RobotOutlined />,
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Modal, Descriptions, Spin, Tag, Typography, Empty } from 'antd';
|
||||||
|
import { taskApi } from '@/api';
|
||||||
|
import type { Task } from '@/types';
|
||||||
|
import { TaskStatusTag, PriorityTag } from '@/components/constants';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
taskId: number | null;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务详情弹窗:在 DAG 画布/项目页内点击任务卡片时弹出,不跳转页面 */
|
||||||
|
export default function TaskDetailModal({ taskId, open, onClose }: Props) {
|
||||||
|
const [task, setTask] = useState<Task | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || taskId == null) return;
|
||||||
|
setLoading(true);
|
||||||
|
setTask(null);
|
||||||
|
taskApi
|
||||||
|
.get(taskId)
|
||||||
|
.then(setTask)
|
||||||
|
.catch(() => setTask(null))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [open, taskId]);
|
||||||
|
|
||||||
|
const renderJsonBlock = (raw?: string) => {
|
||||||
|
if (!raw) return <Text type="secondary">无</Text>;
|
||||||
|
let pretty = raw;
|
||||||
|
try {
|
||||||
|
pretty = JSON.stringify(JSON.parse(raw), null, 2);
|
||||||
|
} catch {
|
||||||
|
/* keep raw */
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
background: '#f6f8fa',
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: 12,
|
||||||
|
maxHeight: 260,
|
||||||
|
overflow: 'auto',
|
||||||
|
fontSize: 12,
|
||||||
|
margin: 0,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pretty}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={task ? `任务 #${task.id} ${task.title}` : '任务详情'}
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={null}
|
||||||
|
width={720}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: 40 }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
) : !task ? (
|
||||||
|
<Empty description="任务不存在或加载失败" />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Descriptions column={2} bordered size="small">
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
<TaskStatusTag status={task.status} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="优先级">
|
||||||
|
<PriorityTag priority={task.priority} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="任务类型">{task.task_type}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="需要审核">
|
||||||
|
{task.requires_review ? (
|
||||||
|
<Tag color="purple">是</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag>否</Tag>
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
{task.review_status && (
|
||||||
|
<Descriptions.Item label="审核状态">{task.review_status}</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
{task.worker_id && (
|
||||||
|
<Descriptions.Item label="AI Worker">#{task.worker_id}</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
{task.due_date && (
|
||||||
|
<Descriptions.Item label="截止时间">
|
||||||
|
{dayjs(task.due_date).format('YYYY-MM-DD HH:mm')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
{task.started_at && (
|
||||||
|
<Descriptions.Item label="开始时间">
|
||||||
|
{dayjs(task.started_at).format('YYYY-MM-DD HH:mm')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
{task.completed_at && (
|
||||||
|
<Descriptions.Item label="完成时间">
|
||||||
|
{dayjs(task.completed_at).format('YYYY-MM-DD HH:mm')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
<Descriptions.Item label="创建时间">
|
||||||
|
{dayjs(task.created_at).format('YYYY-MM-DD HH:mm')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
{task.description && (
|
||||||
|
<Descriptions.Item label="描述" span={2}>
|
||||||
|
{task.description}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Paragraph strong style={{ marginBottom: 8 }}>
|
||||||
|
输入(Prompt / 参数)
|
||||||
|
</Paragraph>
|
||||||
|
{renderJsonBlock(task.input_data)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Paragraph strong style={{ marginBottom: 8 }}>
|
||||||
|
输出结果
|
||||||
|
</Paragraph>
|
||||||
|
{renderJsonBlock(task.output_data)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
import type { Node, Edge, Connection, NodeProps } from 'reactflow';
|
import type { Node, Edge, Connection, NodeProps } from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import {
|
import {
|
||||||
Card,
|
|
||||||
Select,
|
|
||||||
Button,
|
Button,
|
||||||
Space,
|
Space,
|
||||||
message,
|
message,
|
||||||
@@ -30,9 +28,10 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { projectApi, dagApi } from '@/api';
|
import { dagApi } from '@/api';
|
||||||
import type { Project, TaskStatus, DagGraphNode, DagGraphEdge } from '@/types';
|
import type { TaskStatus, DagGraphNode, DagGraphEdge } from '@/types';
|
||||||
import { taskStatusConfig, priorityConfig } from '@/components/constants';
|
import { taskStatusConfig, priorityConfig } from '@/components/constants';
|
||||||
|
import TaskDetailModal from '@/components/TaskDetailModal';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -52,6 +51,7 @@ function TaskNode({ data }: NodeProps) {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
@@ -121,23 +121,19 @@ function layoutNodes(graphNodes: DagGraphNode[], graphEdges: DagGraphEdge[]) {
|
|||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DagCanvas() {
|
interface Props {
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
projectId: number;
|
||||||
const [projectId, setProjectId] = useState<number | undefined>();
|
}
|
||||||
|
|
||||||
|
/** DAG 编排画布(嵌入项目详情页使用) */
|
||||||
|
export default function DagCanvas({ projectId }: Props) {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [executing, setExecuting] = useState(false);
|
const [executing, setExecuting] = useState(false);
|
||||||
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
|
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
|
||||||
|
const [detailTaskId, setDetailTaskId] = useState<number | null>(null);
|
||||||
// Load projects
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
useEffect(() => {
|
|
||||||
projectApi.list().then((ps) => {
|
|
||||||
setProjects(ps);
|
|
||||||
if (ps.length > 0 && !projectId) setProjectId(ps[0].id);
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadGraph = useCallback(async (pid: number) => {
|
const loadGraph = useCallback(async (pid: number) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -182,7 +178,7 @@ export default function DagCanvas() {
|
|||||||
// Connect: add dependency (drag from source(bottom) to target(top))
|
// Connect: add dependency (drag from source(bottom) to target(top))
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
async (conn: Connection) => {
|
async (conn: Connection) => {
|
||||||
if (!conn.source || !conn.target || !projectId) return;
|
if (!conn.source || !conn.target) return;
|
||||||
try {
|
try {
|
||||||
await dagApi.addDependency({
|
await dagApi.addDependency({
|
||||||
task_id: Number(conn.target),
|
task_id: Number(conn.target),
|
||||||
@@ -205,9 +201,15 @@ export default function DagCanvas() {
|
|||||||
message.error(e.response?.data?.detail || '添加依赖失败');
|
message.error(e.response?.data?.detail || '添加依赖失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[projectId, setEdges]
|
[setEdges]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Node click -> open task detail modal
|
||||||
|
const onNodeClick = useCallback((_: unknown, node: Node) => {
|
||||||
|
setDetailTaskId(Number(node.id));
|
||||||
|
setDetailOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Delete selected edge
|
// Delete selected edge
|
||||||
const removeSelectedEdge = useCallback(async () => {
|
const removeSelectedEdge = useCallback(async () => {
|
||||||
if (!selectedEdge) return;
|
if (!selectedEdge) return;
|
||||||
@@ -269,70 +271,73 @@ export default function DagCanvas() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card
|
<div
|
||||||
title="DAG 编排画布"
|
style={{
|
||||||
extra={
|
display: 'flex',
|
||||||
<Space>
|
justifyContent: 'space-between',
|
||||||
<Select
|
alignItems: 'center',
|
||||||
style={{ width: 220 }}
|
marginBottom: 8,
|
||||||
placeholder="选择项目"
|
}}
|
||||||
value={projectId}
|
|
||||||
onChange={setProjectId}
|
|
||||||
options={projects.map((p) => ({ label: p.name, value: p.id }))}
|
|
||||||
/>
|
|
||||||
<Tooltip title="刷新">
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => projectId && loadGraph(projectId)} loading={loading} />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={selectedEdge ? '删除选中的依赖连线' : '先点击选中一条连线'}>
|
|
||||||
<Button
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
disabled={!selectedEdge}
|
|
||||||
onClick={removeSelectedEdge}
|
|
||||||
>
|
|
||||||
删除依赖
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<ThunderboltOutlined />}
|
|
||||||
onClick={executeAll}
|
|
||||||
loading={executing}
|
|
||||||
disabled={nodes.length === 0}
|
|
||||||
>
|
|
||||||
执行 DAG
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 8 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
拖拽节点底部锚点到另一节点顶部建立依赖;点击节点查看详情;点击连线可选中删除。
|
||||||
操作说明:从节点底部拖拽到另一节点顶部可建立依赖(上游 → 下游);点击连线可选中并删除;支持拖拽移动节点、缩放画布。
|
</Text>
|
||||||
</Text>
|
<Space>
|
||||||
</div>
|
<Tooltip title="刷新">
|
||||||
<div style={{ height: 'calc(100vh - 260px)', minHeight: 480, border: '1px solid #f0f0f0', borderRadius: 8 }}>
|
<Button size="small" icon={<ReloadOutlined />} onClick={() => loadGraph(projectId)} loading={loading} />
|
||||||
{nodes.length === 0 && !loading ? (
|
</Tooltip>
|
||||||
<Empty description="该项目暂无任务,请先在任务管理中创建任务" style={{ marginTop: 120 }} />
|
<Tooltip title={selectedEdge ? '删除选中的依赖连线' : '先点击选中一条连线'}>
|
||||||
) : (
|
<Button
|
||||||
<ReactFlow
|
size="small"
|
||||||
nodes={nodes}
|
danger
|
||||||
edges={edges}
|
icon={<DeleteOutlined />}
|
||||||
onNodesChange={onNodesChange}
|
disabled={!selectedEdge}
|
||||||
onEdgesChange={onEdgesChange}
|
onClick={removeSelectedEdge}
|
||||||
onConnect={onConnect}
|
|
||||||
onEdgeClick={(_, edge) => setSelectedEdge(edge)}
|
|
||||||
onPaneClick={() => setSelectedEdge(null)}
|
|
||||||
nodeTypes={nodeTypes}
|
|
||||||
fitView
|
|
||||||
deleteKeyCode={[]}
|
|
||||||
>
|
>
|
||||||
<Background gap={16} />
|
删除依赖
|
||||||
<Controls />
|
</Button>
|
||||||
{minimap}
|
</Tooltip>
|
||||||
</ReactFlow>
|
<Button
|
||||||
)}
|
size="small"
|
||||||
</div>
|
type="primary"
|
||||||
</Card>
|
icon={<ThunderboltOutlined />}
|
||||||
|
onClick={executeAll}
|
||||||
|
loading={executing}
|
||||||
|
disabled={nodes.length === 0}
|
||||||
|
>
|
||||||
|
执行 DAG
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div style={{ height: 560, border: '1px solid #f0f0f0', borderRadius: 8 }}>
|
||||||
|
{nodes.length === 0 && !loading ? (
|
||||||
|
<Empty description="该项目暂无任务,请先在任务管理中创建任务" style={{ marginTop: 160 }} />
|
||||||
|
) : (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onEdgeClick={(_, edge) => setSelectedEdge(edge)}
|
||||||
|
onPaneClick={() => setSelectedEdge(null)}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
deleteKeyCode={[]}
|
||||||
|
>
|
||||||
|
<Background gap={16} />
|
||||||
|
<Controls />
|
||||||
|
{minimap}
|
||||||
|
</ReactFlow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TaskDetailModal
|
||||||
|
taskId={detailTaskId}
|
||||||
|
open={detailOpen}
|
||||||
|
onClose={() => setDetailOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -12,15 +12,20 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Empty,
|
Empty,
|
||||||
List,
|
List,
|
||||||
|
Tabs,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
|
ApartmentOutlined,
|
||||||
|
ProfileOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { projectApi, taskApi, artifactApi } from '@/api';
|
import { projectApi, taskApi, artifactApi } from '@/api';
|
||||||
import type { Project, ProjectStats, Task, Artifact } from '@/types';
|
import type { Project, ProjectStats, Task, Artifact } from '@/types';
|
||||||
import { ProjectStatusTag, TaskStatusTag } from '@/components/constants';
|
import { ProjectStatusTag, TaskStatusTag } from '@/components/constants';
|
||||||
|
import DagCanvas from '@/pages/DagCanvas';
|
||||||
|
import TaskDetailModal from '@/components/TaskDetailModal';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
@@ -28,11 +33,20 @@ const { Title, Text } = Typography;
|
|||||||
export default function ProjectDetail() {
|
export default function ProjectDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const activeTab = searchParams.get('tab') || 'overview';
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [project, setProject] = useState<Project | null>(null);
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
const [stats, setStats] = useState<ProjectStats | null>(null);
|
const [stats, setStats] = useState<ProjectStats | null>(null);
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [artifacts, setArtifacts] = useState<Artifact[]>([]);
|
const [artifacts, setArtifacts] = useState<Artifact[]>([]);
|
||||||
|
const [detailTaskId, setDetailTaskId] = useState<number | null>(null);
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
|
|
||||||
|
const openTaskDetail = (taskId: number) => {
|
||||||
|
setDetailTaskId(taskId);
|
||||||
|
setDetailOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -75,36 +89,8 @@ export default function ProjectDetail() {
|
|||||||
].filter((item) => item.count > 0)
|
].filter((item) => item.count > 0)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
const overviewTab = (
|
||||||
<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 }}>
|
<Card style={{ marginBottom: 16 }}>
|
||||||
<Descriptions column={2} bordered size="small">
|
<Descriptions column={2} bordered size="small">
|
||||||
<Descriptions.Item label="项目名称">{project.name}</Descriptions.Item>
|
<Descriptions.Item label="项目名称">{project.name}</Descriptions.Item>
|
||||||
@@ -198,7 +184,7 @@ export default function ProjectDetail() {
|
|||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
title={
|
title={
|
||||||
<a onClick={() => navigate('/tasks')}>{task.title}</a>
|
<a onClick={() => openTaskDetail(task.id)}>{task.title}</a>
|
||||||
}
|
}
|
||||||
description={dayjs(task.created_at).format('MM-DD HH:mm')}
|
description={dayjs(task.created_at).format('MM-DD HH:mm')}
|
||||||
/>
|
/>
|
||||||
@@ -231,6 +217,73 @@ export default function ProjectDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={(key) => setSearchParams({ tab: key })}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'overview',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<ProfileOutlined /> 概览
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: overviewTab,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dag',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<ApartmentOutlined /> DAG 编排
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<Card size="small" title="DAG 编排画布">
|
||||||
|
<DagCanvas projectId={project.id} />
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TaskDetailModal
|
||||||
|
taskId={detailTaskId}
|
||||||
|
open={detailOpen}
|
||||||
|
onClose={() => setDetailOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/components/AppLayout.tsx","./src/components/constants.tsx","./src/pages/Alerts.tsx","./src/pages/DagCanvas.tsx","./src/pages/Dashboard.tsx","./src/pages/KnowledgeBase.tsx","./src/pages/Login.tsx","./src/pages/ProjectDetail.tsx","./src/pages/Projects.tsx","./src/pages/Reviews.tsx","./src/pages/Tasks.tsx","./src/pages/Workers.tsx","./src/stores/auth.ts","./src/types/index.ts"],"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/components/AppLayout.tsx","./src/components/TaskDetailModal.tsx","./src/components/constants.tsx","./src/pages/Alerts.tsx","./src/pages/DagCanvas.tsx","./src/pages/Dashboard.tsx","./src/pages/KnowledgeBase.tsx","./src/pages/Login.tsx","./src/pages/ProjectDetail.tsx","./src/pages/Projects.tsx","./src/pages/Reviews.tsx","./src/pages/Tasks.tsx","./src/pages/Workers.tsx","./src/stores/auth.ts","./src/types/index.ts"],"version":"5.9.3"}
|
||||||
Reference in New Issue
Block a user