feat(studio): 新增 Studio 工作流编辑/运行页,优化顶部三栏对齐

- 后端:项目/运行 API、上下文服务与数据模型
- 前端:Studio 列表、编辑页(R1/R2 布局)、运行页与节点图
- 编辑页顶部:CSS Grid 统一标签行与控件行对齐,项目按钮独立第三行
- Docker 开发配置与文档脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 21:24:57 +08:00
parent bc130d98f4
commit fa6907fb8d
48 changed files with 8795 additions and 20 deletions

View 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) => ({