feat(studio): 新增 Studio 工作流编辑/运行页,优化顶部三栏对齐
- 后端:项目/运行 API、上下文服务与数据模型 - 前端:Studio 列表、编辑页(R1/R2 布局)、运行页与节点图 - 编辑页顶部:CSS Grid 统一标签行与控件行对齐,项目按钮独立第三行 - Docker 开发配置与文档脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
233
frontend/src/components/Studio/StudioRunNodeGraph.jsx
Normal file
233
frontend/src/components/Studio/StudioRunNodeGraph.jsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
|
||||
|
||||
import { buildGraphLayout } from './edit/variableUtils';
|
||||
|
||||
|
||||
|
||||
import './StudioRunNodeGraph.css';
|
||||
|
||||
|
||||
|
||||
const NODE_STATUS_LABEL = {
|
||||
|
||||
pending: '待执行',
|
||||
|
||||
active: '进行中',
|
||||
|
||||
completed: '已完成',
|
||||
|
||||
skipped: '已跳过',
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
function defaultEdgePath(from, to, nodeW, nodeH) {
|
||||
|
||||
const x1 = from.x + nodeW / 2;
|
||||
|
||||
const y1 = from.y + nodeH;
|
||||
|
||||
const x2 = to.x + nodeW / 2;
|
||||
|
||||
const y2 = to.y;
|
||||
|
||||
const dy = Math.max(24, (y2 - y1) * 0.45);
|
||||
|
||||
return `M ${x1} ${y1} C ${x1} ${y1 + dy}, ${x2} ${y2 - dy}, ${x2} ${y2}`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function StudioRunNodeGraph({
|
||||
|
||||
pipeline,
|
||||
|
||||
nodeStates,
|
||||
|
||||
currentNodeId,
|
||||
|
||||
variant = 'sidebar',
|
||||
|
||||
}) {
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const [view, setView] = useState({ x: 0, y: 0, scale: 1 });
|
||||
|
||||
const [dragging, setDragging] = useState(null);
|
||||
|
||||
const [selectedId, setSelectedId] = useState(currentNodeId);
|
||||
|
||||
const [nodeOverrides, setNodeOverrides] = useState({});
|
||||
|
||||
|
||||
|
||||
const isCenter = variant === 'center';
|
||||
|
||||
|
||||
|
||||
const layout = useMemo(
|
||||
|
||||
() => buildGraphLayout(pipeline, nodeStates, { vertical: true }),
|
||||
|
||||
[pipeline, nodeStates]
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
const { nodeW, nodeH } = layout;
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setSelectedId(currentNodeId);
|
||||
|
||||
}, [currentNodeId]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const el = containerRef.current;
|
||||
|
||||
if (!el || !layout.width) return;
|
||||
|
||||
const cw = el.clientWidth || 200;
|
||||
|
||||
const ch = el.clientHeight || 160;
|
||||
|
||||
const scale = Math.min(1, (cw - 16) / layout.width, (ch - 16) / layout.height);
|
||||
|
||||
setView({
|
||||
|
||||
x: (cw - layout.width * scale) / 2,
|
||||
|
||||
y: Math.max(8, (ch - layout.height * scale) / 2),
|
||||
|
||||
scale: Math.max(0.45, scale),
|
||||
|
||||
});
|
||||
|
||||
}, [layout.width, layout.height, pipeline, variant]);
|
||||
|
||||
|
||||
|
||||
const nodeMap = useMemo(
|
||||
|
||||
() => Object.fromEntries(layout.nodes.map((n) => [n.id, n])),
|
||||
|
||||
[layout.nodes]
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
const handleWheel = useCallback((e) => {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const delta = e.deltaY > 0 ? 0.92 : 1.08;
|
||||
|
||||
setView((v) => ({
|
||||
|
||||
...v,
|
||||
|
||||
scale: Math.min(2.5, Math.max(0.35, v.scale * delta)),
|
||||
|
||||
}));
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const handleBgMouseDown = useCallback(
|
||||
|
||||
(e) => {
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
setDragging({ type: 'pan', startX: e.clientX, startY: e.clientY, origin: { ...view } });
|
||||
|
||||
},
|
||||
|
||||
[view]
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
const handleNodeMouseDown = useCallback(
|
||||
|
||||
(e, nodeId) => {
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
setSelectedId(nodeId);
|
||||
|
||||
const node = nodeMap[nodeId];
|
||||
|
||||
if (!node) return;
|
||||
|
||||
setDragging({
|
||||
|
||||
type: 'node',
|
||||
|
||||
nodeId,
|
||||
|
||||
startX: e.clientX,
|
||||
|
||||
startY: e.clientY,
|
||||
|
||||
origin: { x: node.x, y: node.y },
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
[nodeMap]
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!dragging) return undefined;
|
||||
|
||||
|
||||
|
||||
const onMove = (e) => {
|
||||
|
||||
if (dragging.type === 'pan') {
|
||||
|
||||
setView((v) => ({
|
||||
|
||||
...v,
|
||||
|
||||
x: dragging.origin.x + (e.clientX - dragging.startX),
|
||||
|
||||
y: dragging.origin.y + (e.clientY - dragging.startY),
|
||||
|
||||
}));
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (dragging.type === 'node') {
|
||||
|
||||
const dx = (e.clientX - dragging.startX) / view.scale;
|
||||
|
||||
const dy = (e.clientY - dragging.startY) / view.scale;
|
||||
|
||||
setNodeOverrides((prev) => ({
|
||||
Reference in New Issue
Block a user