基于 design.html 的现有实现,开发一个新的流程图可视化系统。支持节点和边的交互式展示,提供清晰的层级结构和详细信息面板。
设计原则:
架构原则:
┌─────────────────────────────────────────────────────┐
│ 顶部导航栏 │
├─────────────────────────────────┬───────────────────┤
│ │ │
│ │ │
│ 主体内容区域 │ 右侧详情面板 │
│ (流程图可视化) │ │
│ │ │
│ │ │
└─────────────────────────────────┴───────────────────┘
实现文件:
src/components/TopBar/TopBar.tsxsrc/components/TopBar/TopBar.module.csssrc/components/TopBar/types.ts内容:
标题区域
筛选条件区域
数据结构:
interface FilterConfig {
status?: "running" | "completed" | "failed";
traceId?: string;
}
实现文件:
src/components/MainContent/MainContent.tsxsrc/components/MainContent/MainContent.module.csssrc/components/FlowChart/FlowChart.tsx功能: 流程图可视化展示
交互行为:
视觉效果:
数据需求:
interface Node {
id: string;
label: string;
type: string;
parentId?: string;
metadata: Record<string, any>;
}
interface PathInfo {
nodes: Node[];
edges: Edge[];
nearestEdge?: Edge;
}
交互行为:
数据需求:
interface Edge {
id: string;
source: string;
target: string;
label: string;
content: EdgeContent;
}
interface EdgeContent {
id: string;
title: string;
children?: EdgeContent[];
data: Record<string, any>;
}
实现文件:
src/components/DetailPanel/DetailPanel.tsxsrc/components/DetailPanel/DetailPanel.module.csssrc/components/DetailPanel/NodeDetail.tsxsrc/components/DetailPanel/EdgeDetail.tsx显示模式:
节点详情模式
边详情模式
数据结构:
interface DetailPanelState {
type: "node" | "edge" | null;
data: Node | Edge | null;
}
Base URL: http://43.106.118.91:8000
接口: GET /api/traces?status=running&limit=20
用途: 获取 trace_id 列表,顶部 title 显示选中 trace 的 task
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | string | 否 | 过滤状态:running / completed / failed |
| mode | string | 否 | 过滤模式:call / agent |
| limit | int | 否 | 返回数量(默认 50,最大 100)|
响应数据:
interface TraceListResponse {
traces: Array<{
trace_id: string;
mode: string;
task: string; // 任务描述,用于顶部标题显示
status: string;
total_messages: number;
total_tokens: number;
total_cost: number;
current_goal_id: string;
created_at: string;
}>;
total: number;
}
接口: GET /api/traces/{trace_id}
用途: 获取完整的 GoalTree 数据,根据 goal_tree.goals 渲染流程节点
响应数据:
interface TraceDetailResponse {
trace_id: string;
mode: string;
task: string;
status: string;
total_messages: number;
total_tokens: number;
total_cost: number;
created_at: string;
completed_at: string | null;
goal_tree: {
mission: string;
current_id: string | null;
goals: Array<Goal>;
};
branches: Record<string, BranchContext>;
}
接口: GET /api/traces/{trace_id}/messages?goal_id={goal_id}
用途: 获取指定 Goal 关联的所有 Messages,用于渲染流程节点之间的边的详细执行内容
响应数据:
interface MessagesResponse {
messages: Array<Message>;
total: number;
}
连接地址: ws://43.106.118.91:8000/api/traces/{trace_id}/watch?since_event_id=0
事件类型:
connected - 连接成功goal_added - 新增节点goal_updated - 节点状态变化message_added - 新消息trace_completed - 任务完成branch_started - 分支开始branch_completed - 分支完成src/
├── components/ # 组件目录
│ ├── TopBar/
│ │ ├── TopBar.tsx # 顶部导航栏组件
│ │ ├── TopBar.module.css # 组件样式
│ │ └── types.ts # 类型定义
│ ├── MainContent/
│ │ ├── MainContent.tsx
│ │ └── MainContent.module.css
│ ├── FlowChart/
│ │ ├── FlowChart.tsx
│ │ ├── FlowChart.module.css
│ │ └── hooks/
│ │ └── useFlowChart.ts
│ ├── DetailPanel/
│ │ ├── DetailPanel.tsx
│ │ ├── DetailPanel.module.css
│ │ ├── NodeDetail.tsx
│ │ └── EdgeDetail.tsx
│ └── common/ # 通用组件
│ ├── Loading.tsx
│ └── ErrorBoundary.tsx
│
├── api/ # API 请求层(接口独立文件夹)
│ ├── client.ts # HTTP 客户端配置
│ ├── websocket.ts # WebSocket 连接管理
│ ├── traceApi.ts # Trace 相关接口
│ ├── goalApi.ts # Goal 相关接口
│ └── messageApi.ts # Message 相关接口
│
├── styles/ # 全局样式
│ ├── variables.css # CSS 变量
│ ├── reset.css # 样式重置
│ ├── global.css # 全局样式
│ └── themes/ # 主题
│ ├── light.css
│ └── dark.css
│
├── hooks/ # 自定义 Hooks
│ ├── useTrace.ts
│ ├── useWebSocket.ts
│ └── useTheme.ts
│
├── store/ # 状态管理
│ ├── index.ts
│ ├── traceStore.ts
│ └── uiStore.ts
│
├── utils/ # 工具函数
│ ├── dagGenerator.ts
│ ├── pathCalculator.ts
│ └── formatters.ts
│
├── types/ # TypeScript 类型定义
│ ├── trace.ts
│ ├── goal.ts
│ └── message.ts
│
├── App.tsx # 应用根组件
├── main.tsx # 应用入口
└── vite-env.d.ts # Vite 类型声明
1. API 层(src/api/)
src/api/traceApi.ts - Trace 相关接口
import { client } from "./client";
import type { TraceListResponse, TraceDetailResponse } from "@/types/trace";
export const traceApi = {
// 获取 Trace 列表
async fetchTraces(params?: { status?: string; limit?: number }): Promise<TraceListResponse> {
const response = await client.get("/api/traces", { params });
return response.data;
},
// 获取 Trace 详情
async fetchTraceDetail(traceId: string): Promise<TraceDetailResponse> {
const response = await client.get(`/api/traces/${traceId}`);
return response.data;
},
};
src/api/messageApi.ts - Message 相关接口
import { client } from "./client";
import type { MessagesResponse } from "@/types/message";
export const messageApi = {
// 获取 Goal 的 Messages
async fetchMessages(traceId: string, goalId: string): Promise<MessagesResponse> {
const response = await client.get(`/api/traces/${traceId}/messages`, { params: { goal_id: goalId } });
return response.data;
},
};
src/api/client.ts - HTTP 客户端配置
import axios from "axios";
export const client = axios.create({
baseURL: "http://43.106.118.91:8000",
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
// 请求拦截器
client.interceptors.request.use(
(config) => {
// 可以在这里添加 token 等
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
client.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error("API Error:", error);
return Promise.reject(error);
},
);
2. 组件层(src/components/)
src/components/TopBar/TopBar.tsx - 顶部导航栏组件
import React, { useEffect, useState } from 'react';
import { traceApi } from '@/api/traceApi';
import styles from './TopBar.module.css';
interface TopBarProps {
onTraceSelect: (traceId: string) => void;
}
export const TopBar: React.FC<TopBarProps> = ({ onTraceSelect }) => {
const [traces, setTraces] = useState([]);
const [selectedTraceId, setSelectedTraceId] = useState('');
const [title, setTitle] = useState('流程图可视化系统');
useEffect(() => {
loadTraces();
}, []);
const loadTraces = async () => {
try {
const data = await traceApi.fetchTraces({
status: 'running',
limit: 20
});
setTraces(data.traces);
} catch (error) {
console.error('Failed to load traces:', error);
}
};
const handleTraceChange = (traceId: string) => {
setSelectedTraceId(traceId);
const trace = traces.find(t => t.trace_id === traceId);
if (trace) {
setTitle(trace.task);
onTraceSelect(traceId);
}
};
return (
<header className={styles.topbar}>
<div className={styles.title}>
<h1>{title}</h1>
</div>
<div className={styles.filters}>
<select
value={selectedTraceId}
onChange={(e) => handleTraceChange(e.target.value)}
className={styles.select}
>
<option value="">选择 Trace</option>
{traces.map(trace => (
<option key={trace.trace_id} value={trace.trace_id}>
{trace.task}
</option>
))}
</select>
<button onClick={loadTraces} className={styles.button}>
刷新
</button>
</div>
</header>
);
};
3. 自定义 Hooks(src/hooks/)
src/hooks/useTrace.ts - Trace 数据管理 Hook
import { useState, useEffect } from "react";
import { traceApi } from "@/api/traceApi";
import type { TraceDetailResponse } from "@/types/trace";
export const useTrace = (traceId: string | null) => {
const [trace, setTrace] = useState<TraceDetailResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!traceId) return;
const loadTrace = async () => {
setLoading(true);
setError(null);
try {
const data = await traceApi.fetchTraceDetail(traceId);
setTrace(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
loadTrace();
}, [traceId]);
return { trace, loading, error };
};
src/hooks/useWebSocket.ts - WebSocket 连接 Hook
import { useEffect, useRef, useState } from "react";
interface UseWebSocketOptions {
onMessage?: (data: any) => void;
onError?: (error: Event) => void;
onClose?: () => void;
}
export const useWebSocket = (traceId: string | null, options: UseWebSocketOptions = {}) => {
const wsRef = useRef<WebSocket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
if (!traceId) return;
const url = `ws://43.106.118.91:8000/api/traces/${traceId}/watch?since_event_id=0`;
const ws = new WebSocket(url);
ws.onopen = () => {
console.log("WebSocket connected");
setConnected(true);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
options.onMessage?.(data);
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
options.onError?.(error);
};
ws.onclose = () => {
console.log("WebSocket closed");
setConnected(false);
options.onClose?.();
};
wsRef.current = ws;
return () => {
ws.close();
};
}, [traceId]);
return { connected, ws: wsRef.current };
};
4. 应用入口(src/App.tsx)
import React, { useState } from 'react';
import { TopBar } from './components/TopBar/TopBar';
import { MainContent } from './components/MainContent/MainContent';
import { DetailPanel } from './components/DetailPanel/DetailPanel';
import { useTrace } from './hooks/useTrace';
import { useWebSocket } from './hooks/useWebSocket';
import './styles/global.css';
function App() {
const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null);
const [selectedNode, setSelectedNode] = useState(null);
const [selectedEdge, setSelectedEdge] = useState(null);
const { trace, loading } = useTrace(selectedTraceId);
useWebSocket(selectedTraceId, {
onMessage: (data) => {
console.log('WebSocket message:', data);
// 处理实时更新
}
});
return (
<div className="app">
<TopBar onTraceSelect={setSelectedTraceId} />
<MainContent
trace={trace}
loading={loading}
onNodeClick={setSelectedNode}
onEdgeClick={setSelectedEdge}
/>
{(selectedNode || selectedEdge) && (
<DetailPanel
node={selectedNode}
edge={selectedEdge}
onClose={() => {
setSelectedNode(null);
setSelectedEdge(null);
}}
/>
)}
</div>
);
}
export default App;
关注点分离:
src/api/ 目录src/components/ 目录src/styles/ 目录(全局)+ 组件内 .module.csssrc/styles/themes/ 目录src/hooks/ 和 src/store/ 目录src/types/ 目录禁止做法:
推荐做法:
src/api/ 层调用src/styles/variables.css
:root {
/* 颜色 */
--color-primary: #0070f3;
--color-success: #00c853;
--color-warning: #ff9800;
--color-error: #f44336;
/* 背景色 */
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
/* 文字颜色 */
--text-primary: #333333;
--text-secondary: #666666;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* 字体 */
--font-size-sm: 12px;
--font-size-md: 14px;
--font-size-lg: 16px;
}
使用 CSS Modules 避免样式冲突:
src/components/TopBar/TopBar.module.css
.topbar {
height: 60px;
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-lg);
}
.title h1 {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.filters {
display: flex;
gap: var(--spacing-md);
}
使用 Zustand 进行状态管理:
import create from "zustand";
interface AppState {
currentTraceId: string | null;
trace: TraceDetailResponse | null;
selectedNode: Node | null;
selectedEdge: Edge | null;
setCurrentTraceId: (id: string) => void;
setTrace: (trace: TraceDetailResponse) => void;
setSelectedNode: (node: Node | null) => void;
setSelectedEdge: (edge: Edge | null) => void;
}
export const useAppStore = create<AppState>((set) => ({
currentTraceId: null,
trace: null,
selectedNode: null,
selectedEdge: null,
setCurrentTraceId: (id) => set({ currentTraceId: id }),
setTrace: (trace) => set({ trace }),
setSelectedNode: (node) => set({ selectedNode: node }),
setSelectedEdge: (edge) => set({ selectedEdge: edge }),
}));
npm run build推荐方案: 使用 React 控制 SVG 渲染,D3.js 仅用于布局计算
技术架构:
d3.tree(), d3.hierarchy())src/components/FlowChart/FlowChart.tsx
import React, { useMemo, useRef, useEffect, useState } from 'react';
import * as d3 from 'd3';
import styles from './FlowChart.module.css';
import type { Goal } from '@/types/goal';
interface FlowChartProps {
goals: Goal[];
onNodeClick?: (node: Goal) => void;
onEdgeClick?: (edge: Edge) => void;
}
interface LayoutNode extends d3.HierarchyPointNode<Goal> {
x: number;
y: number;
}
export const FlowChart: React.FC<FlowChartProps> = ({
goals,
onNodeClick,
onEdgeClick
}) => {
const svgRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 1200, height: 800 });
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
// 使用 D3 计算布局
const layoutData = useMemo(() => {
if (!goals || goals.length === 0) return null;
// 构建层级数据结构
const root = buildHierarchy(goals);
// 使用 d3.tree() 计算布局
const treeLayout = d3.tree<Goal>()
.size([dimensions.height - 100, dimensions.width - 200])
.separation((a, b) => (a.parent === b.parent ? 1.2 : 1.5));
const treeData = treeLayout(root);
return {
nodes: treeData.descendants() as LayoutNode[],
links: treeData.links()
};
}, [goals, dimensions]);
// 处理节点点击
const handleNodeClick = (node: LayoutNode) => {
setSelectedNodeId(node.data.id);
onNodeClick?.(node.data);
};
// 处理边点击
const handleEdgeClick = (link: any) => {
const edge = {
id: `${link.source.data.id}-${link.target.data.id}`,
source: link.source.data.id,
target: link.target.data.id,
data: link.target.data
};
onEdgeClick?.(edge);
};
if (!layoutData) return <div>Loading...</div>;
return (
<div className={styles.container}>
<svg
ref={svgRef}
width={dimensions.width}
height={dimensions.height}
className={styles.svg}
>
{/* 定义箭头标记 */}
<defs>
<ArrowMarkers />
</defs>
{/* 渲染连接线(边) */}
<g className={styles.links}>
{layoutData.links.map((link, index) => (
<Edge
key={index}
link={link}
selected={selectedNodeId === link.target.data.id}
onClick={() => handleEdgeClick(link)}
/>
))}
</g>
{/* 渲染节点 */}
<g className={styles.nodes}>
{layoutData.nodes.map((node) => (
<Node
key={node.data.id}
node={node}
selected={selectedNodeId === node.data.id}
onClick={() => handleNodeClick(node)}
/>
))}
</g>
</svg>
</div>
);
};
// 构建层级数据结构
function buildHierarchy(goals: Goal[]): d3.HierarchyNode<Goal> {
const goalsMap = new Map(goals.map(g => [g.id, g]));
const root = goals.find(g => !g.parent_id);
if (!root) throw new Error('No root goal found');
function buildTree(goal: Goal): any {
const children = goals
.filter(g => g.parent_id === goal.id)
.map(child => buildTree(child));
return {
...goal,
children: children.length > 0 ? children : undefined
};
}
return d3.hierarchy(buildTree(root));
}
src/components/FlowChart/ArrowMarkers.tsx
import React from 'react';
export const ArrowMarkers: React.FC = () => {
return (
<>
{/* 默认箭头 - 蓝色 */}
<marker
id="arrow-default"
viewBox="0 -5 10 10"
refX={10}
refY={0}
markerWidth={6}
markerHeight={6}
orient="auto"
>
<path
d="M0,-5L10,0L0,5"
fill="#4e79a7"
stroke="none"
/>
</marker>
{/* 成功状态箭头 - 绿色 */}
<marker
id="arrow-success"
viewBox="0 -5 10 10"
refX={10}
refY={0}
markerWidth={6}
markerHeight={6}
orient="auto"
>
<path
d="M0,-5L10,0L0,5"
fill="#00c853"
stroke="none"
/>
</marker>
{/* 失败状态箭头 - 红色 */}
<marker
id="arrow-error"
viewBox="0 -5 10 10"
refX={10}
refY={0}
markerWidth={6}
markerHeight={6}
orient="auto"
>
<path
d="M0,-5L10,0L0,5"
fill="#f44336"
stroke="none"
/>
</marker>
{/* 运行中状态箭头 - 橙色 */}
<marker
id="arrow-running"
viewBox="0 -5 10 10"
refX={10}
refY={0}
markerWidth={6}
markerHeight={6}
orient="auto"
>
<path
d="M0,-5L10,0L0,5"
fill="#ff9800"
stroke="none"
/>
</marker>
{/* 选中状态箭头 - 高亮蓝色 */}
<marker
id="arrow-selected"
viewBox="0 -5 10 10"
refX={10}
refY={0}
markerWidth={8}
markerHeight={8}
orient="auto"
>
<path
d="M0,-5L10,0L0,5"
fill="#0070f3"
stroke="none"
/>
</marker>
</>
);
};
src/components/FlowChart/Edge.tsx
import React from 'react';
import styles from './Edge.module.css';
interface EdgeProps {
link: any;
selected: boolean;
onClick: () => void;
}
export const Edge: React.FC<EdgeProps> = ({ link, selected, onClick }) => {
const { source, target } = link;
// 计算 Bezier 曲线路径(垂直布局)
const createPath = () => {
const sourceX = source.y;
const sourceY = source.x;
const targetX = target.y;
const targetY = target.x;
// 使用三次 Bezier 曲线创建平滑连接
const midY = (sourceY + targetY) / 2;
return `M${sourceX},${sourceY} C${sourceX},${midY} ${targetX},${midY} ${targetX},${targetY}`;
};
// 根据目标节点状态选择箭头样式
const getMarkerUrl = () => {
if (selected) return 'url(#arrow-selected)';
const status = target.data.status;
switch (status) {
case 'completed':
return 'url(#arrow-success)';
case 'failed':
return 'url(#arrow-error)';
case 'running':
return 'url(#arrow-running)';
default:
return 'url(#arrow-default)';
}
};
// 根据状态选择线条颜色
const getStrokeColor = () => {
if (selected) return '#0070f3';
const status = target.data.status;
switch (status) {
case 'completed':
return '#00c853';
case 'failed':
return '#f44336';
case 'running':
return '#ff9800';
default:
return '#4e79a7';
}
};
return (
<g className={styles.edge}>
{/* 透明的宽路径用于点击检测 */}
<path
d={createPath()}
fill="none"
stroke="transparent"
strokeWidth={20}
onClick={onClick}
style={{ cursor: 'pointer' }}
/>
{/* 实际显示的路径 */}
<path
d={createPath()}
fill="none"
stroke={getStrokeColor()}
strokeWidth={selected ? 3 : 2}
markerEnd={getMarkerUrl()}
className={selected ? styles.selected : ''}
style={{
transition: 'all 0.3s ease',
opacity: selected ? 1 : 0.7
}}
/>
</g>
);
};
src/components/FlowChart/Node.tsx
import React from 'react';
import styles from './Node.module.css';
interface NodeProps {
node: any;
selected: boolean;
onClick: () => void;
}
export const Node: React.FC<NodeProps> = ({ node, selected, onClick }) => {
const { x, y, data } = node;
// 根据状态选择节点颜色
const getNodeColor = () => {
switch (data.status) {
case 'completed':
return '#e8f5e9';
case 'failed':
return '#ffebee';
case 'running':
return '#fff3e0';
default:
return '#e3f2fd';
}
};
const getBorderColor = () => {
if (selected) return '#0070f3';
switch (data.status) {
case 'completed':
return '#00c853';
case 'failed':
return '#f44336';
case 'running':
return '#ff9800';
default:
return '#4e79a7';
}
};
return (
<g
className={styles.node}
transform={`translate(${y},${x})`}
onClick={onClick}
style={{ cursor: 'pointer' }}
>
{/* 节点背景 */}
<rect
x={-60}
y={-25}
width={120}
height={50}
rx={8}
fill={getNodeColor()}
stroke={getBorderColor()}
strokeWidth={selected ? 3 : 2}
className={selected ? styles.selected : ''}
style={{
transition: 'all 0.3s ease',
filter: selected ? 'drop-shadow(0 4px 8px rgba(0,0,0,0.2))' : 'none'
}}
/>
{/* 节点文本 */}
<text
textAnchor="middle"
dy=".35em"
fontSize={14}
fill="#333"
fontWeight={selected ? 600 : 400}
>
{truncateText(data.description || data.id, 12)}
</text>
{/* 状态指示器 */}
{data.status === 'running' && (
<circle
cx={50}
cy={-15}
r={4}
fill="#ff9800"
className={styles.pulse}
/>
)}
</g>
);
};
function truncateText(text: string, maxLength: number): string {
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
}
src/components/FlowChart/useFlowChartData.ts
import { useState, useEffect, useCallback } from "react";
import { useWebSocket } from "@/hooks/useWebSocket";
import type { Goal } from "@/types/goal";
export const useFlowChartData = (traceId: string | null) => {
const [goals, setGoals] = useState<Goal[]>([]);
// 处理 WebSocket 消息
const handleWebSocketMessage = useCallback((event: any) => {
const { type, data } = event;
switch (type) {
case "goal_added":
// 新增节点 - 动态添加到图中
setGoals((prev) => [...prev, data.goal]);
break;
case "goal_updated":
// 更新节点状态 - 触发重新渲染
setGoals((prev) => prev.map((g) => (g.id === data.goal.id ? { ...g, ...data.goal } : g)));
break;
case "message_added":
// 新消息添加 - 可能需要更新边的数据
// 这里可以触发边的高亮或动画效果
console.log("New message:", data.message);
break;
case "trace_completed":
// 任务完成 - 可以显示完成状态
console.log("Trace completed");
break;
default:
console.log("Unknown event type:", type);
}
}, []);
// 建立 WebSocket 连接
useWebSocket(traceId, {
onMessage: handleWebSocketMessage,
});
return { goals, setGoals };
};
src/components/FlowChart/FlowChart.module.css
.container {
width: 100%;
height: 100%;
overflow: auto;
background: #fafafa;
}
.svg {
display: block;
margin: 0 auto;
}
.links {
pointer-events: none;
}
.links path {
pointer-events: stroke;
}
.nodes {
pointer-events: all;
}
/* 边的样式 */
.edge path {
transition:
stroke-width 0.3s ease,
opacity 0.3s ease;
}
.edge.selected path {
stroke-width: 3px;
opacity: 1;
}
/* 节点的样式 */
.node rect {
transition: all 0.3s ease;
}
.node.selected rect {
filter: drop-shadow(0 4px 8px rgba(0, 112, 243, 0.3));
}
/* 运行中状态的脉冲动画 */
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}
src/components/MainContent/MainContent.tsx
import React, { useEffect } from 'react';
import { FlowChart } from '../FlowChart/FlowChart';
import { useFlowChartData } from '../FlowChart/useFlowChartData';
import { traceApi } from '@/api/traceApi';
import styles from './MainContent.module.css';
interface MainContentProps {
traceId: string | null;
onNodeClick?: (node: any) => void;
onEdgeClick?: (edge: any) => void;
}
export const MainContent: React.FC<MainContentProps> = ({
traceId,
onNodeClick,
onEdgeClick
}) => {
const { goals, setGoals } = useFlowChartData(traceId);
// 初始加载数据
useEffect(() => {
if (!traceId) return;
const loadInitialData = async () => {
try {
const trace = await traceApi.fetchTraceDetail(traceId);
setGoals(trace.goal_tree.goals);
} catch (error) {
console.error('Failed to load trace data:', error);
}
};
loadInitialData();
}, [traceId]);
if (!traceId) {
return (
<div className={styles.empty}>
<p>请选择一个 Trace 查看流程图</p>
</div>
);
}
return (
<div className={styles.container}>
<FlowChart
goals={goals}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
/>
</div>
);
};
1. React + D3 Hybrid 模式:
d3.tree(), d3.hierarchy())2. 实时更新策略:
3. 箭头样式设计:
<marker> 定义箭头4. 性能优化:
useMemo 缓存布局计算结果useCallback 避免不必要的重新渲染5. 交互体验: