feat: DAG画布嵌入项目详情页,任务节点点击弹详情Modal

This commit is contained in:
2026-08-13 23:13:00 +08:00
parent cc697ad399
commit 5450c0e248
+85 -80
View File
@@ -14,8 +14,6 @@ import {
import type { Node, Edge, Connection, NodeProps } from 'reactflow';
import 'reactflow/dist/style.css';
import {
Card,
Select,
Button,
Space,
message,
@@ -30,9 +28,10 @@ import {
ReloadOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { projectApi, dagApi } from '@/api';
import type { Project, TaskStatus, DagGraphNode, DagGraphEdge } from '@/types';
import { dagApi } from '@/api';
import type { TaskStatus, DagGraphNode, DagGraphEdge } from '@/types';
import { taskStatusConfig, priorityConfig } from '@/components/constants';
import TaskDetailModal from '@/components/TaskDetailModal';
const { Text } = Typography;
@@ -52,6 +51,7 @@ function TaskNode({ data }: NodeProps) {
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
overflow: 'hidden',
cursor: 'pointer',
}}
>
<Handle type="target" position={Position.Top} />
@@ -121,23 +121,19 @@ function layoutNodes(graphNodes: DagGraphNode[], graphEdges: DagGraphEdge[]) {
return positions;
}
export default function DagCanvas() {
const [projects, setProjects] = useState<Project[]>([]);
const [projectId, setProjectId] = useState<number | undefined>();
interface Props {
projectId: number;
}
/** DAG 编排画布(嵌入项目详情页使用) */
export default function DagCanvas({ projectId }: Props) {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [loading, setLoading] = useState(false);
const [executing, setExecuting] = useState(false);
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
// Load projects
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 [detailTaskId, setDetailTaskId] = useState<number | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const loadGraph = useCallback(async (pid: number) => {
setLoading(true);
@@ -182,7 +178,7 @@ export default function DagCanvas() {
// Connect: add dependency (drag from source(bottom) to target(top))
const onConnect = useCallback(
async (conn: Connection) => {
if (!conn.source || !conn.target || !projectId) return;
if (!conn.source || !conn.target) return;
try {
await dagApi.addDependency({
task_id: Number(conn.target),
@@ -205,9 +201,15 @@ export default function DagCanvas() {
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
const removeSelectedEdge = useCallback(async () => {
if (!selectedEdge) return;
@@ -269,70 +271,73 @@ export default function DagCanvas() {
return (
<div>
<Card
title="DAG 编排画布"
extra={
<Space>
<Select
style={{ width: 220 }}
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={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
}}
>
<div style={{ marginBottom: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
线
</Text>
</div>
<div style={{ height: 'calc(100vh - 260px)', minHeight: 480, border: '1px solid #f0f0f0', borderRadius: 8 }}>
{nodes.length === 0 && !loading ? (
<Empty description="该项目暂无任务,请先在任务管理中创建任务" style={{ marginTop: 120 }} />
) : (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onEdgeClick={(_, edge) => setSelectedEdge(edge)}
onPaneClick={() => setSelectedEdge(null)}
nodeTypes={nodeTypes}
fitView
deleteKeyCode={[]}
<Text type="secondary" style={{ fontSize: 12 }}>
线
</Text>
<Space>
<Tooltip title="刷新">
<Button size="small" icon={<ReloadOutlined />} onClick={() => loadGraph(projectId)} loading={loading} />
</Tooltip>
<Tooltip title={selectedEdge ? '删除选中的依赖连线' : '先点击选中一条连线'}>
<Button
size="small"
danger
icon={<DeleteOutlined />}
disabled={!selectedEdge}
onClick={removeSelectedEdge}
>
<Background gap={16} />
<Controls />
{minimap}
</ReactFlow>
)}
</div>
</Card>
</Button>
</Tooltip>
<Button
size="small"
type="primary"
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>
);
}