hookehuyr

feat: initialize logicflow editor migration base

1 +node_modules
2 +dist
3 +apps/demo/dist
4 +playwright-report
5 +test-results
6 +.pnpm-store
7 +.DS_Store
1 +# 仓库协作规则
2 +
3 +## 文档规则
4 +
5 +- 本仓库中的需求文档、设计文档、计划文档、进度文档、说明文档统一使用中文编写。
6 +- 遇到路径、命令、库名、API 名称、类型名等技术标识时,可保留原文,不强制翻译。
7 +- 新增文档时,标题、章节说明、结论、风险、验收标准等面向人的内容必须使用中文。
8 +
9 +## 代码注释规则
10 +
11 +- 页面主要逻辑上的注释统一使用中文。
12 +- 组件页面中与业务流程、交互状态、核心分支、数据转换相关的关键注释必须使用中文。
13 +- 注释应简洁明确,优先解释“为什么这样做”或“这一段主要负责什么”,避免无意义的逐行翻译式注释。
14 +
15 +## 执行要求
16 +
17 +- 修改已有文档时,优先检查是否存在英文标题或英文说明,若有则同步转成中文。
18 +- 新增页面或重构页面逻辑时,需要检查主要逻辑处是否补充了必要的中文注释。
1 +<!doctype html>
2 +<html lang="zh-CN">
3 + <head>
4 + <meta charset="UTF-8" />
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 + <title>流程编辑器迁移基线页</title>
7 + </head>
8 + <body>
9 + <div id="app"></div>
10 + <script type="module" src="/src/main.ts"></script>
11 + </body>
12 +</html>
13 +
1 +<script setup lang="ts">
2 +import { computed, onMounted, ref } from 'vue'
3 +import { ElMessage } from 'element-plus'
4 +import { FlowEditor, normalizeGraphData } from '@flow-editor'
5 +import type {
6 + FlowEditorRef,
7 + FlowEditorInputGraphData,
8 + ToolbarButtonDefinition,
9 +} from '@flow-editor'
10 +
11 +const editorRef = ref<FlowEditorRef | null>(null)
12 +const currentVersionLabel = ref('基线版本')
13 +const latestAction = ref('等待初始化')
14 +const flowData = ref<FlowEditorInputGraphData>({
15 + nodes: [],
16 + edges: [],
17 +})
18 +
19 +const toolbarButtonHandler = (buttons: ToolbarButtonDefinition[]) => {
20 + const hiddenKeys = new Set(['grid', 'miniMap', 'delete', 'undo', 'redo'])
21 + return buttons.filter((button) => !hiddenKeys.has(button.key))
22 +}
23 +
24 +const hasLoadedRemoteBootstrap = computed(() => {
25 + const normalizedGraph = normalizeGraphData(flowData.value)
26 + return normalizedGraph.nodes.length > 0 || normalizedGraph.edges.length > 0
27 +})
28 +
29 +function onRef(instance: FlowEditorRef) {
30 + editorRef.value = instance
31 +}
32 +
33 +function centerCanvas() {
34 + editorRef.value?.commander.fitView()
35 + latestAction.value = '已执行居中/适应画布'
36 +}
37 +
38 +function saveFlowDraft() {
39 + latestAction.value = '当前为迁移阶段演示版,已模拟保存成功'
40 + ElMessage.success('当前为迁移阶段演示版,已模拟保存成功')
41 +}
42 +
43 +function openPreviewPanel() {
44 + editorRef.value?.openPreview()
45 + latestAction.value = '已打开流程预览'
46 +}
47 +
48 +async function loadBootstrapData() {
49 + // 这里保留旧项目的接口语义,方便把 Playwright 拦截能力平移到新项目。
50 + const requestJson = async <T,>(url: string, fallback: T): Promise<T> => {
51 + try {
52 + const response = await fetch(url)
53 + if (!response.ok) {
54 + return fallback
55 + }
56 +
57 + const payload = (await response.json()) as { data?: T }
58 + return payload.data ?? fallback
59 + } catch {
60 + return fallback
61 + }
62 + }
63 +
64 + const [versionList, graph] = await Promise.all([
65 + requestJson<Array<{ note?: string }>>('/admin/index.php?m=mod&a=flow_version', [
66 + { note: '基线版本' },
67 + ]),
68 + requestJson<FlowEditorInputGraphData>('/admin/index.php?m=srv&a=flow_nodes', {
69 + nodes: [],
70 + edges: [],
71 + }),
72 + ])
73 +
74 + currentVersionLabel.value = versionList[0]?.note || '基线版本'
75 + flowData.value = graph
76 + editorRef.value?.read(graph)
77 +}
78 +
79 +function bindTestContract() {
80 + window.__FLOW_EDITOR_TEST_API__ = {
81 + getEditorContract: () => {
82 + const instance = editorRef.value
83 +
84 + return {
85 + hasEditor: Boolean(instance),
86 + hasCommander: Boolean(instance?.commander),
87 + hasDelete: typeof instance?.commander?.delete === 'function',
88 + hasUndo: typeof instance?.commander?.undo === 'function',
89 + hasRedo: typeof instance?.commander?.redo === 'function',
90 + hasOpenModel: typeof instance?.openModel === 'function',
91 + hasCloseModel: typeof instance?.closeModel === 'function',
92 + hasAddNode: typeof instance?.addNode === 'function',
93 + hasUpdateModel: typeof instance?.updateModel === 'function',
94 + hasGraphRead: typeof instance?.read === 'function',
95 + }
96 + },
97 + }
98 +}
99 +
100 +onMounted(async () => {
101 + bindTestContract()
102 + await loadBootstrapData()
103 + bindTestContract()
104 +})
105 +</script>
106 +
107 +<template>
108 + <div class="app">
109 + <header class="page-header">
110 + <div>
111 + <p class="page-eyebrow">流程编辑器重构迁移</p>
112 + <h1>新项目基线页</h1>
113 + </div>
114 + <div class="version-card">
115 + <span>当前启用版本</span>
116 + <strong>{{ currentVersionLabel }}</strong>
117 + </div>
118 + </header>
119 +
120 + <main class="page-main">
121 + <FlowEditor
122 + :data="flowData"
123 + :grid="false"
124 + :mini-map="false"
125 + :on-ref="onRef"
126 + :toolbar-button-handler="toolbarButtonHandler"
127 + >
128 + <template #toolbar-extra>
129 + <button
130 + type="button"
131 + class="toolbar-button toolbar-button--accent"
132 + @click="centerCanvas"
133 + >
134 + 居中
135 + </button>
136 + <button
137 + type="button"
138 + class="toolbar-button toolbar-button--accent"
139 + @click="saveFlowDraft"
140 + >
141 + 保存
142 + </button>
143 + <button
144 + type="button"
145 + class="toolbar-button toolbar-button--accent"
146 + data-testid="open-preview-panel"
147 + @click="openPreviewPanel"
148 + >
149 + 预览测试
150 + </button>
151 + </template>
152 + </FlowEditor>
153 +
154 + <aside class="status-panel">
155 + <h2>迁移状态</h2>
156 + <p>当前目标:先建立新项目最小壳层与测试护栏,再逐步补齐 LogicFlow 实现。</p>
157 + <p>接口基线:{{ hasLoadedRemoteBootstrap ? '已接入初始化 mock 语义' : '使用本地兜底空图数据' }}</p>
158 + <p>最近动作:{{ latestAction }}</p>
159 + </aside>
160 + </main>
161 + </div>
162 +</template>
163 +
164 +<style scoped>
165 +.app {
166 + min-height: 100vh;
167 + padding: 24px;
168 + background:
169 + radial-gradient(circle at top left, rgba(43, 104, 168, 0.14), transparent 32%),
170 + linear-gradient(180deg, #f5f7fb 0%, #eef2f8 100%);
171 + color: #18314f;
172 + font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
173 +}
174 +
175 +.page-header {
176 + display: flex;
177 + align-items: flex-start;
178 + justify-content: space-between;
179 + gap: 16px;
180 + margin-bottom: 20px;
181 +}
182 +
183 +.page-header h1 {
184 + margin: 6px 0 0;
185 + font-size: 28px;
186 +}
187 +
188 +.page-eyebrow {
189 + margin: 0;
190 + font-size: 12px;
191 + letter-spacing: 0.14em;
192 + color: #52749b;
193 +}
194 +
195 +.version-card {
196 + min-width: 180px;
197 + padding: 14px 16px;
198 + border: 1px solid rgba(24, 49, 79, 0.1);
199 + border-radius: 16px;
200 + background: rgba(255, 255, 255, 0.9);
201 + box-shadow: 0 18px 40px rgba(34, 61, 96, 0.08);
202 +}
203 +
204 +.version-card span {
205 + display: block;
206 + margin-bottom: 6px;
207 + font-size: 12px;
208 + color: #52749b;
209 +}
210 +
211 +.version-card strong {
212 + font-size: 18px;
213 +}
214 +
215 +.page-main {
216 + display: grid;
217 + grid-template-columns: minmax(0, 1fr) 320px;
218 + gap: 20px;
219 +}
220 +
221 +.status-panel {
222 + padding: 18px;
223 + border: 1px solid rgba(24, 49, 79, 0.08);
224 + border-radius: 20px;
225 + background: rgba(255, 255, 255, 0.88);
226 + box-shadow: 0 18px 32px rgba(34, 61, 96, 0.08);
227 +}
228 +
229 +.status-panel h2 {
230 + margin: 0 0 12px;
231 + font-size: 18px;
232 +}
233 +
234 +.status-panel p {
235 + margin: 0 0 10px;
236 + line-height: 1.6;
237 +}
238 +
239 +.toolbar-button {
240 + border: none;
241 + border-radius: 999px;
242 + background: #ffffff;
243 + color: #18314f;
244 +}
245 +
246 +.toolbar-button--accent {
247 + background: linear-gradient(135deg, #2b68a8 0%, #3f8ec6 100%);
248 + color: #ffffff;
249 +}
250 +
251 +@media (max-width: 960px) {
252 + .page-main {
253 + grid-template-columns: 1fr;
254 + }
255 +
256 + .page-header {
257 + flex-direction: column;
258 + }
259 +}
260 +</style>
1 +import { createApp } from 'vue'
2 +import ElementPlus from 'element-plus'
3 +import 'element-plus/dist/index.css'
4 +import '@logicflow/core/lib/style/index.css'
5 +import '@logicflow/extension/lib/style/index.css'
6 +import App from './App.vue'
7 +
8 +createApp(App).use(ElementPlus).mount('#app')
This diff is collapsed. Click to expand it.
1 +/// <reference types="vite/client" />
2 +
3 +declare module '*.vue' {
4 + import type { DefineComponent } from 'vue'
5 +
6 + const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
7 + export default component
8 +}
9 +
10 +declare global {
11 + interface Window {
12 + __FLOW_EDITOR_TEST_API__?: {
13 + getEditorContract: () => {
14 + hasEditor: boolean
15 + hasCommander: boolean
16 + hasDelete: boolean
17 + hasUndo: boolean
18 + hasRedo: boolean
19 + hasOpenModel: boolean
20 + hasCloseModel: boolean
21 + hasAddNode: boolean
22 + hasUpdateModel: boolean
23 + hasGraphRead: boolean
24 + }
25 + }
26 + }
27 +}
28 +
29 +export {}
30 +
This diff is collapsed. Click to expand it.
1 +{
2 + "name": "vue-flow-editor2",
3 + "version": "0.1.0",
4 + "private": true,
5 + "type": "module",
6 + "packageManager": "pnpm@10.9.0",
7 + "scripts": {
8 + "dev": "vite --host 127.0.0.1 --port 4173",
9 + "build": "vite build",
10 + "preview": "vite preview --host 127.0.0.1 --port 4173",
11 + "test:unit": "vitest run",
12 + "test:unit:watch": "vitest",
13 + "test:e2e": "node ./scripts/run-e2e.mjs"
14 + },
15 + "dependencies": {
16 + "@logicflow/core": "^2.1.11",
17 + "@logicflow/extension": "^2.1.15",
18 + "element-plus": "^2.13.6",
19 + "vue": "^3.5.31"
20 + },
21 + "devDependencies": {
22 + "@playwright/test": "^1.58.2",
23 + "@types/node": "^25.5.0",
24 + "@vitejs/plugin-vue": "^6.0.5",
25 + "@vue/test-utils": "^2.4.6",
26 + "jsdom": "^29.0.1",
27 + "typescript": "^6.0.2",
28 + "vite": "^8.0.3",
29 + "vitest": "^4.1.2",
30 + "vue-tsc": "^3.2.6"
31 + }
32 +}
This diff is collapsed. Click to expand it.
1 +import type { FlowEdgeData, FlowGraphData, FlowNodeData } from '../../types'
2 +import type {
3 + FlowEditorInputEdge,
4 + FlowEditorInputGraphData,
5 + FlowEditorInputNode,
6 + LogicFlowGraphData,
7 +} from '../schema/types'
8 +
9 +function isLegacyEdge(edge: FlowEditorInputEdge): edge is {
10 + source?: string
11 + target?: string
12 + sourceAnchor?: number
13 + targetAnchor?: number
14 + [key: string]: unknown
15 +} {
16 + return 'source' in edge || 'target' in edge
17 +}
18 +
19 +function isLegacyNode(node: FlowEditorInputNode): node is {
20 + text?: string
21 + shape?: string
22 + activity?: string
23 + control?: string
24 + [key: string]: unknown
25 +} {
26 + return 'shape' in node || 'text' in node || 'activity' in node || 'control' in node
27 +}
28 +
29 +function inferNodeType(node: FlowEditorInputNode): string {
30 + if ('type' in node && typeof node.type === 'string' && node.type) {
31 + return node.type
32 + }
33 +
34 + if ('id' in node && node.id === 'start-node') {
35 + return 'start'
36 + }
37 +
38 + if ('id' in node && node.id === 'end-node') {
39 + return 'end'
40 + }
41 +
42 + if (isLegacyNode(node)) {
43 + if (node.activity) {
44 + return 'activity'
45 + }
46 +
47 + if (node.control) {
48 + return 'control'
49 + }
50 +
51 + if (node.shape === 'start' || node.shape === 'start-node') {
52 + return 'start'
53 + }
54 +
55 + if (node.shape === 'end' || node.shape === 'end-node') {
56 + return 'end'
57 + }
58 + }
59 +
60 + return 'custom'
61 +}
62 +
63 +function inferLogicFlowNodeType(node: FlowNodeData): string {
64 + if (node.type === 'start' || node.type === 'end') {
65 + return 'circle'
66 + }
67 +
68 + return 'rect'
69 +}
70 +
71 +function normalizeNode(node: FlowEditorInputNode): FlowNodeData {
72 + const label = 'label' in node && typeof node.label === 'string' && node.label
73 + ? node.label
74 + : isLegacyNode(node) && typeof node.text === 'string' && node.text
75 + ? node.text
76 + : node.id
77 +
78 + return {
79 + ...node,
80 + id: node.id,
81 + label,
82 + type: inferNodeType(node),
83 + x: typeof node.x === 'number' ? node.x : 120,
84 + y: typeof node.y === 'number' ? node.y : 120,
85 + }
86 +}
87 +
88 +function normalizeEdge(edge: FlowEditorInputEdge, index: number): FlowEdgeData {
89 + if (!isLegacyEdge(edge)) {
90 + return {
91 + ...edge,
92 + id: edge.id || `edge-${edge.sourceNodeId}-${edge.targetNodeId}-${index}`,
93 + label: edge.label ?? '',
94 + }
95 + }
96 +
97 + const sourceNodeId = typeof edge.source === 'string' ? edge.source : ''
98 + const targetNodeId = typeof edge.target === 'string' ? edge.target : ''
99 + const label = typeof edge.label === 'string' && edge.label
100 + ? edge.label
101 + : typeof edge.text === 'string'
102 + ? edge.text
103 + : ''
104 +
105 + return {
106 + ...edge,
107 + id: typeof edge.id === 'string' && edge.id ? edge.id : `edge-${sourceNodeId}-${targetNodeId}-${index}`,
108 + sourceNodeId,
109 + targetNodeId,
110 + label,
111 + }
112 +}
113 +
114 +export function normalizeGraphData(graphData: FlowEditorInputGraphData): FlowGraphData {
115 + return {
116 + nodes: graphData.nodes.map((node) => normalizeNode(node)),
117 + edges: graphData.edges.map((edge, index) => normalizeEdge(edge, index)),
118 + }
119 +}
120 +
121 +export function cloneNormalizedGraphData(graphData: FlowGraphData): FlowGraphData {
122 + return {
123 + nodes: graphData.nodes.map((node) => ({ ...node })),
124 + edges: graphData.edges.map((edge) => ({ ...edge })),
125 + }
126 +}
127 +
128 +export function toLogicFlowGraphData(graphData: FlowEditorInputGraphData): LogicFlowGraphData {
129 + const normalizedGraph = normalizeGraphData(graphData)
130 +
131 + return {
132 + nodes: normalizedGraph.nodes.map((node) => ({
133 + id: node.id,
134 + type: inferLogicFlowNodeType(node),
135 + x: node.x ?? 120,
136 + y: node.y ?? 120,
137 + text: node.label,
138 + properties: {
139 + ...node,
140 + },
141 + })),
142 + edges: normalizedGraph.edges.map((edge) => ({
143 + id: edge.id,
144 + type: 'polyline',
145 + sourceNodeId: edge.sourceNodeId,
146 + targetNodeId: edge.targetNodeId,
147 + text: edge.label,
148 + properties: {
149 + ...edge,
150 + },
151 + })),
152 + }
153 +}
154 +
1 +import type { FlowEdgeData, FlowGraphData, FlowNodeData } from '../../types'
2 +
3 +export interface LegacyFlowNode {
4 + id: string
5 + x?: number
6 + y?: number
7 + text?: string
8 + label?: string
9 + shape?: string
10 + type?: string
11 + desc?: string
12 + activity?: string
13 + control?: string
14 + data?: Record<string, unknown>
15 + [key: string]: unknown
16 +}
17 +
18 +export interface LegacyFlowEdge {
19 + id?: string
20 + shape?: string
21 + source?: string
22 + target?: string
23 + sourceAnchor?: number
24 + targetAnchor?: number
25 + text?: string
26 + label?: string
27 + [key: string]: unknown
28 +}
29 +
30 +export interface LegacyFlowGraphData {
31 + nodes: LegacyFlowNode[]
32 + edges: LegacyFlowEdge[]
33 +}
34 +
35 +export interface LogicFlowNodeData {
36 + id: string
37 + type: string
38 + x: number
39 + y: number
40 + text: string
41 + properties: Record<string, unknown>
42 +}
43 +
44 +export interface LogicFlowEdgeData {
45 + id: string
46 + type: string
47 + sourceNodeId: string
48 + targetNodeId: string
49 + text?: string
50 + properties: Record<string, unknown>
51 +}
52 +
53 +export interface LogicFlowGraphData {
54 + nodes: LogicFlowNodeData[]
55 + edges: LogicFlowEdgeData[]
56 +}
57 +
58 +export type FlowEditorInputNode = FlowNodeData | LegacyFlowNode
59 +export type FlowEditorInputEdge = FlowEdgeData | LegacyFlowEdge
60 +export type FlowEditorInputGraphData = FlowGraphData | LegacyFlowGraphData
61 +
1 +export { default as FlowEditor } from './components/FlowEditor.vue'
2 +export {
3 + cloneNormalizedGraphData,
4 + normalizeGraphData,
5 + toLogicFlowGraphData,
6 +} from './core/adapter/g6-to-logicflow'
7 +export type {
8 + FlowEditorCommander,
9 + FlowEditorRef,
10 + FlowEditorState,
11 + FlowEdgeData,
12 + FlowGraphData,
13 + FlowNodeData,
14 + ToolbarButtonDefinition,
15 +} from './types'
16 +export type {
17 + FlowEditorInputGraphData,
18 + LegacyFlowEdge,
19 + LegacyFlowGraphData,
20 + LegacyFlowNode,
21 + LogicFlowEdgeData,
22 + LogicFlowGraphData,
23 + LogicFlowNodeData,
24 +} from './core/schema/types'
1 +export interface FlowNodeData {
2 + id: string
3 + label: string
4 + type?: string
5 + x?: number
6 + y?: number
7 + [key: string]: unknown
8 +}
9 +
10 +export interface FlowEdgeData {
11 + id: string
12 + sourceNodeId: string
13 + targetNodeId: string
14 + label?: string
15 + [key: string]: unknown
16 +}
17 +
18 +export interface FlowGraphData {
19 + nodes: FlowNodeData[]
20 + edges: FlowEdgeData[]
21 +}
22 +
23 +export interface ToolbarButtonDefinition {
24 + key: string
25 + label: string
26 +}
27 +
28 +export interface FlowEditorState {
29 + canvasProps: {
30 + grid: boolean
31 + miniMap: boolean
32 + zoom: number
33 + }
34 + graphData: FlowGraphData
35 + selectedNodeIds: string[]
36 + isModelOpen: boolean
37 + isPreviewOpen: boolean
38 +}
39 +
40 +export interface FlowEditorCommander {
41 + switchGrid: () => void
42 + switchMiniMap: () => void
43 + fitView: () => void
44 + actualView: () => void
45 + zoomIn: () => void
46 + zoomOut: () => void
47 + delete: () => void
48 + undo: () => void
49 + redo: () => void
50 + selectAll: () => void
51 +}
52 +
53 +export interface FlowEditorRef {
54 + editorState: FlowEditorState
55 + commander: FlowEditorCommander
56 + openModel: () => void
57 + closeModel: () => void
58 + addNode: (node?: Partial<FlowNodeData>) => FlowNodeData
59 + updateModel: (patch?: Record<string, unknown>) => void
60 + openPreview: () => void
61 + read: (graphData: FlowGraphData) => void
62 + clearStates: () => void
63 +}
64 +
1 +import { defineConfig } from '@playwright/test'
2 +
3 +export default defineConfig({
4 + testDir: './tests/e2e',
5 + timeout: 30_000,
6 + retries: 0,
7 + use: {
8 + baseURL: 'http://127.0.0.1:4174',
9 + headless: true,
10 + },
11 +})
This diff is collapsed. Click to expand it.
1 +packages:
2 + - apps/*
3 + - packages/*
4 +
This diff is collapsed. Click to expand it.
1 +import { spawn } from 'node:child_process'
2 +
3 +const VITE_READY_PATTERN = /127\.0\.0\.1:4174/
4 +const START_TIMEOUT = 20_000
5 +
6 +function waitForServer(serverProcess) {
7 + return new Promise((resolve, reject) => {
8 + const timeout = setTimeout(() => {
9 + reject(new Error('等待 Vite 测试服务启动超时'))
10 + }, START_TIMEOUT)
11 +
12 + const handleOutput = (chunk) => {
13 + const text = chunk.toString()
14 + process.stdout.write(text)
15 +
16 + if (VITE_READY_PATTERN.test(text)) {
17 + clearTimeout(timeout)
18 + serverProcess.stdout.off('data', handleOutput)
19 + serverProcess.stderr.off('data', handleOutput)
20 + resolve()
21 + }
22 + }
23 +
24 + serverProcess.stdout.on('data', handleOutput)
25 + serverProcess.stderr.on('data', handleOutput)
26 + serverProcess.once('exit', (code) => {
27 + clearTimeout(timeout)
28 + reject(new Error(`Vite 测试服务启动失败,退出码:${code ?? 'unknown'}`))
29 + })
30 + })
31 +}
32 +
33 +function killProcessTree(serverProcess) {
34 + return new Promise((resolve) => {
35 + if (serverProcess.killed) {
36 + resolve()
37 + return
38 + }
39 +
40 + serverProcess.once('exit', () => resolve())
41 + serverProcess.kill('SIGTERM')
42 +
43 + setTimeout(() => {
44 + if (!serverProcess.killed) {
45 + serverProcess.kill('SIGKILL')
46 + }
47 + }, 3_000)
48 + })
49 +}
50 +
51 +async function main() {
52 + const playwrightArgs = process.argv.slice(2)
53 + const serverProcess = spawn(
54 + 'npx',
55 + ['vite', '--host', '127.0.0.1', '--port', '4174', '--strictPort'],
56 + {
57 + stdio: ['ignore', 'pipe', 'pipe'],
58 + env: process.env,
59 + },
60 + )
61 +
62 + try {
63 + await waitForServer(serverProcess)
64 +
65 + const testProcess = spawn('npx', ['playwright', 'test', ...playwrightArgs], {
66 + stdio: 'inherit',
67 + env: process.env,
68 + })
69 +
70 + const exitCode = await new Promise((resolve, reject) => {
71 + testProcess.once('exit', (code) => resolve(code ?? 1))
72 + testProcess.once('error', reject)
73 + })
74 +
75 + process.exitCode = exitCode
76 + } finally {
77 + await killProcessTree(serverProcess)
78 + }
79 +}
80 +
81 +main().catch((error) => {
82 + console.error(error)
83 + process.exitCode = 1
84 +})
1 +# 任务计划:流程编辑器重构调研
2 +
3 +## 目标
4 +
5 +输出一份完整的需求与迁移设计方案,用于将 `vue-flow-editor` 重构为基于 Vue 3、Element Plus、LogicFlow 2.0、Vite 的新项目,并采用“测试优先、分阶段迁移”的安全升级策略。
6 +
7 +## 当前阶段
8 +
9 +阶段 10
10 +
11 +## 阶段拆分
12 +
13 +### 阶段 1:需求与旧项目调研
14 +- [x] 理解用户诉求
15 +- [x] 确认新项目当前起点
16 +- [x] 审计旧项目架构、行为能力和对外 API
17 +- [x] 记录迁移约束与测试风险
18 +- **状态:** 已完成
19 +
20 +### 阶段 2:迁移策略与安全方案设计
21 +- [x] 定义分阶段迁移路径
22 +- [x] 定义重构前的基线保护策略
23 +- [x] 定义兼容口径与验收清单
24 +- **状态:** 已完成
25 +
26 +### 阶段 3:文档编写
27 +- [x] 编写需求与设计文档
28 +- [x] 编写分阶段迁移方案
29 +- [x] 编写测试迁移与验证方案
30 +- **状态:** 已完成
31 +
32 +### 阶段 4:复核与收敛
33 +- [x] 基于旧项目再次核对关键假设
34 +- [x] 收紧风险、开放问题和优先级
35 +- [x] 形成可直接开工的初始方案
36 +- **状态:** 已完成
37 +
38 +### 阶段 5:实施计划细化
39 +- [x] 将迁移方案细化成可执行任务清单
40 +- [x] 补充双仓目录规划和阶段性产物
41 +- [x] 输出下一步的明确实施顺序
42 +- **状态:** 已完成
43 +
44 +### 阶段 6:旧项目基线测试落地
45 +- [x] 固定旧项目基线回归页说明
46 +- [x] 接入 Playwright 最小测试基础设施
47 +- [x] 跑通第一条旧项目冒烟测试
48 +- **状态:** 已完成
49 +
50 +### 阶段 7:新项目骨架与测试底座初始化
51 +- [x] 初始化 Vue 3 + Vite + TypeScript + pnpm workspace 工程
52 +- [x] 接入 Element Plus 与 LogicFlow 2.x 基础依赖
53 +- [x] 建立 `apps/demo``packages/flow-editor``tests` 目录结构
54 +- [x] 迁入首批基线测试语义(冒烟、工具栏、实例契约)
55 +- [x] 跑通新项目 `build``test:unit``test:e2e`
56 +- **状态:** 已完成
57 +
58 +### 阶段 8:新项目数据适配层
59 +- [x] 梳理旧项目图数据与当前规范数据的差异
60 +- [x] 实现 `g6-to-logicflow` 兼容适配层
61 +- [x] 让 demo 与编辑器内部统一走适配器
62 +- [x] 补齐单节点、线性流、分支流三类夹具与单元测试
63 +- [x] 跑通适配层相关验证
64 +- **状态:** 已完成
65 +
66 +### 阶段 9:新项目基础编辑闭环
67 +- [x] 接通工具栏主要按钮的真实点击行为
68 +- [x] 支持新增节点、选中节点、删除节点
69 +- [x] 支持打开预览面板
70 +- [x] 为基础交互补齐 E2E 回归
71 +- **状态:** 已完成
72 +
73 +### 阶段 10:新项目命令闭环增强
74 +- [x] 移除遮挡画布的过渡提示层
75 +- [x] 支持节点名称更新
76 +- [x] 支持基于当前选中节点创建连线
77 +- [x] 支持图数据撤销/重做
78 +- [x] 为命令闭环补齐 E2E 回归
79 +- **状态:** 已完成
80 +
81 +## 关键问题
82 +1. 旧项目里哪些行为必须被视为严格兼容项,哪些属于可接受的现代化调整?
83 +2. 如何在迁移编辑器内核之前,先建立可靠的自动化回归护栏?
84 +3. 应该优先迁移哪些能力,才能保证每一阶段都可验证、可回退?
85 +
86 +## 已做决策
87 +| 决策 | 原因 |
88 +|------|------|
89 +| 先写文档和迁移设计,再启动代码搭建 | 新仓库当前为空,且用户明确希望优先保证升级安全性 |
90 +| 采用“行为优先”的迁移方式,而不是“依赖升级优先” | 旧项目存在混合技术栈,直接升级依赖风险高 |
91 +| 将测试视为本次迁移的一等产物 | 用户无法在重构后进行大量人工点击回归 |
92 +| 先把旧项目 `doc/index.vue` 作为第一份基线回归样板 | 该页面已覆盖 hooks、事件、插槽和复杂业务接入 |
93 +| 在新项目大规模开发前先迁移基线测试 | 这是保住现有业务行为最稳妥的方式 |
94 +| 继续补一份“实施级迁移计划”文档 | 这样后续可以按任务直接开工,而不是重复讨论阶段划分 |
95 +| 旧项目基线测试先采用“Node 16 + 接口拦截”的运行方式 | 这是当前成本最低、稳定性最高的自动化起步方案 |
96 +| 新项目先用“兼容壳层 + 最小 LogicFlow 2.x 实例”承接测试语义 | 这样可以在不提前实现全部交互的前提下,尽快建立新仓回归护栏 |
97 +| 新项目 E2E 改为脚本自管 Vite 生命周期 | 当前环境中 Playwright `webServer` 预检查会被本机代理/旧服务误导,脚本启动更稳 |
98 +| 新项目内部图数据统一先归一化,再映射到 LogicFlow 2.x | 这样后续命令系统、事件兼容层和保存链路都只面对一套稳定结构 |
99 +| 先做“可操作的基础编辑闭环”,再补复杂命令和业务面板 | 这样用户可以尽早看到真实可交互页面,而不是一直停留在只读状态 |
100 +| 连线创建和撤销/重做先基于兼容层图数据历史实现 | 先把用户操作闭环跑通,再逐步下沉到更完整的 LogicFlow 命令栈 |
101 +
102 +## 遇到的问题
103 +| 问题 | 次数 | 处理方式 |
104 +|------|------|----------|
105 +| `rg --files` 在新仓库中没有返回文件 | 1 | 通过 `ls -la` 确认新仓库确实为空仓库,因此先从规划与文档开始 |
106 +| `corepack pnpm install` 出现签名 `keyid` 校验失败 | 1 | 改为使用 `npx pnpm@10.9.0 install` 绕过本机 corepack 问题 |
107 +| Playwright `webServer` 会误判本地端口已被占用 | 1 | 移除 `webServer` 配置,改为仓库脚本显式启动 Vite 并回收进程 |
108 +
109 +## 备注
110 +- 在做重大决策前,重新回看本计划文件
111 +- 继续将旧项目调研结论记录到 `findings.md`
112 +- 最终设计文档路径为 `docs/plans/2026-03-31-flow-editor-rebuild-design.md`
113 +- 实施计划文档路径为 `docs/plans/2026-03-31-flow-editor-migration-implementation-plan.md`
1 +import { expect, test } from '@playwright/test'
2 +import { mockFlowBootstrap } from '../helpers/mock-flow-api'
3 +
4 +test('新项目基线页应暴露稳定的编辑器实例契约', async ({ page }) => {
5 + await mockFlowBootstrap(page)
6 +
7 + await page.goto('/')
8 + await expect(page.locator('.vue-flow-editor')).toBeVisible()
9 +
10 + const contract = await page.evaluate(() => {
11 + return window.__FLOW_EDITOR_TEST_API__?.getEditorContract()
12 + })
13 +
14 + expect(contract?.hasEditor).toBe(true)
15 + expect(contract?.hasCommander).toBe(true)
16 + expect(contract?.hasDelete).toBe(true)
17 + expect(contract?.hasUndo).toBe(true)
18 + expect(contract?.hasRedo).toBe(true)
19 + expect(contract?.hasOpenModel).toBe(true)
20 + expect(contract?.hasCloseModel).toBe(true)
21 + expect(contract?.hasAddNode).toBe(true)
22 + expect(contract?.hasUpdateModel).toBe(true)
23 + expect(contract?.hasGraphRead).toBe(true)
24 +})
25 +
1 +import { expect, test } from '@playwright/test'
2 +import { mockFlowBootstrap } from '../helpers/mock-flow-api'
3 +
4 +test('新项目页面应支持新增节点并删除当前选中节点', async ({ page }) => {
5 + await mockFlowBootstrap(page)
6 +
7 + await page.goto('/')
8 +
9 + await expect(page.getByTestId('node-count-value')).toHaveText('2')
10 +
11 + await page.getByTestId('add-approval-node').click()
12 +
13 + await expect(page.getByTestId('node-count-value')).toHaveText('3')
14 + await expect(page.getByTestId('selected-node-label')).toContainText('审批节点')
15 +
16 + await page.getByTestId('delete-selected-node').click()
17 +
18 + await expect(page.getByTestId('node-count-value')).toHaveText('2')
19 + await expect(page.getByTestId('selected-node-label')).toHaveText('未选中节点')
20 +})
21 +
22 +test('新项目页面应支持缩放操作并打开预览面板', async ({ page }) => {
23 + await mockFlowBootstrap(page)
24 +
25 + await page.goto('/')
26 +
27 + await expect(page.getByTestId('zoom-value')).toHaveText('1.0')
28 +
29 + await page.getByTestId('toolbar-zoomIn').click()
30 + await expect(page.getByTestId('zoom-value')).toHaveText('1.1')
31 +
32 + await page.getByTestId('toolbar-actualView').click()
33 + await expect(page.getByTestId('zoom-value')).toHaveText('1.0')
34 +
35 + await page.getByTestId('open-preview-panel').click()
36 + await expect(page.getByTestId('preview-panel')).toBeVisible()
37 + await expect(page.getByTestId('preview-panel')).toContainText('审批节点')
38 +})
39 +
40 +test('新项目页面应支持更新节点名称并创建连线', async ({ page }) => {
41 + await mockFlowBootstrap(page)
42 +
43 + await page.goto('/')
44 +
45 + await page.getByTestId('node-chip-start').click()
46 + await expect(page.getByTestId('selected-node-label')).toContainText('开始节点')
47 +
48 + await page.getByTestId('selected-node-name-input').fill('发起流程')
49 + await page.getByTestId('save-node-name').click()
50 + await expect(page.getByTestId('selected-node-label')).toContainText('发起流程')
51 +
52 + await page.getByTestId('add-copy-node').click()
53 + await expect(page.getByTestId('node-count-value')).toHaveText('3')
54 +
55 + await page.getByTestId('node-chip-approve').click()
56 + await page.getByTestId('create-edge-from-selection').click()
57 +
58 + await expect(page.getByTestId('edge-count-value')).toHaveText('2')
59 + await expect(page.getByTestId('selected-count-value')).toHaveText('2')
60 +})
61 +
62 +test('新项目页面应支持撤销与重做图数据改动', async ({ page }) => {
63 + await mockFlowBootstrap(page)
64 +
65 + await page.goto('/')
66 +
67 + await expect(page.getByTestId('node-count-value')).toHaveText('2')
68 +
69 + await page.getByTestId('add-copy-node').click()
70 + await expect(page.getByTestId('node-count-value')).toHaveText('3')
71 +
72 + await page.getByTestId('command-undo').click()
73 + await expect(page.getByTestId('node-count-value')).toHaveText('2')
74 +
75 + await page.getByTestId('command-redo').click()
76 + await expect(page.getByTestId('node-count-value')).toHaveText('3')
77 +})
1 +import { expect, test } from '@playwright/test'
2 +import { EditorPage } from '../helpers/editor-page'
3 +import { mockFlowBootstrap } from '../helpers/mock-flow-api'
4 +
5 +test('新项目基线页可以正常打开并渲染编辑器骨架', async ({ page }) => {
6 + await mockFlowBootstrap(page)
7 +
8 + const editorPage = new EditorPage(page)
9 + await editorPage.gotoHome()
10 +
11 + await expect(editorPage.app).toBeVisible()
12 + await expect(editorPage.editor).toBeVisible()
13 + await expect(editorPage.toolbar).toBeVisible()
14 + await expect(editorPage.canvas).toBeVisible()
15 +})
16 +
1 +import { expect, test } from '@playwright/test'
2 +import { mockFlowBootstrap } from '../helpers/mock-flow-api'
3 +
4 +test('新项目基线页应先对齐核心工具栏展示语义', async ({ page }) => {
5 + await mockFlowBootstrap(page)
6 +
7 + await page.goto('/')
8 +
9 + const toolbar = page.locator('.vue-flow-editor-toolbar')
10 +
11 + await expect(toolbar).toBeVisible()
12 + await expect(toolbar.getByText('适应画布', { exact: true })).toBeVisible()
13 + await expect(toolbar.getByText('实际尺寸', { exact: true })).toBeVisible()
14 + await expect(toolbar.getByText('放大', { exact: true })).toBeVisible()
15 + await expect(toolbar.getByText('缩小', { exact: true })).toBeVisible()
16 + await expect(toolbar.getByText('预览', { exact: true })).toBeVisible()
17 + await expect(toolbar.getByText('居中', { exact: true })).toBeVisible()
18 + await expect(toolbar.getByText('保存', { exact: true })).toBeVisible()
19 + await expect(toolbar.getByText('预览测试', { exact: true })).toBeVisible()
20 + await expect(toolbar.getByText('网格')).toHaveCount(0)
21 + await expect(toolbar.getByText('缩略图')).toHaveCount(0)
22 + await expect(toolbar.getByText('删除')).toHaveCount(0)
23 + await expect(toolbar.getByText('撤销')).toHaveCount(0)
24 + await expect(toolbar.getByText('重做')).toHaveCount(0)
25 +})
26 +
1 +{
2 + "nodes": [
3 + {
4 + "id": "start-node",
5 + "text": "开始节点",
6 + "shape": "start",
7 + "x": 160,
8 + "y": 180
9 + }
10 + ],
11 + "edges": []
12 +}
13 +
1 +{
2 + "nodes": [
3 + {
4 + "id": "start-node",
5 + "text": "开始节点",
6 + "shape": "start",
7 + "x": 120,
8 + "y": 200
9 + },
10 + {
11 + "id": "review-node",
12 + "text": "审批节点",
13 + "activity": "approval",
14 + "shape": "activity",
15 + "x": 320,
16 + "y": 200
17 + },
18 + {
19 + "id": "approve-node",
20 + "text": "通过分支",
21 + "control": "branch-yes",
22 + "shape": "control",
23 + "x": 520,
24 + "y": 140
25 + },
26 + {
27 + "id": "reject-node",
28 + "text": "驳回分支",
29 + "control": "branch-no",
30 + "shape": "control",
31 + "x": 520,
32 + "y": 260
33 + },
34 + {
35 + "id": "end-node",
36 + "text": "结束节点",
37 + "shape": "end",
38 + "x": 740,
39 + "y": 200
40 + }
41 + ],
42 + "edges": [
43 + {
44 + "source": "start-node",
45 + "target": "review-node",
46 + "shape": "flow-polyline-round",
47 + "text": "提交审批"
48 + },
49 + {
50 + "source": "review-node",
51 + "target": "approve-node",
52 + "shape": "flow-polyline-round",
53 + "text": "同意"
54 + },
55 + {
56 + "source": "review-node",
57 + "target": "reject-node",
58 + "shape": "flow-polyline-round",
59 + "text": "驳回"
60 + },
61 + {
62 + "source": "approve-node",
63 + "target": "end-node",
64 + "shape": "flow-polyline-round",
65 + "text": "完成"
66 + },
67 + {
68 + "source": "reject-node",
69 + "target": "end-node",
70 + "shape": "flow-polyline-round",
71 + "text": "结束"
72 + }
73 + ]
74 +}
75 +
1 +{
2 + "nodes": [
3 + {
4 + "id": "start",
5 + "label": "开始节点",
6 + "type": "start",
7 + "x": 120,
8 + "y": 180
9 + },
10 + {
11 + "id": "approve",
12 + "label": "审批节点",
13 + "type": "approval",
14 + "x": 320,
15 + "y": 180
16 + },
17 + {
18 + "id": "finish",
19 + "label": "结束节点",
20 + "type": "end",
21 + "x": 520,
22 + "y": 180
23 + }
24 + ],
25 + "edges": [
26 + {
27 + "id": "edge-start-approve",
28 + "sourceNodeId": "start",
29 + "targetNodeId": "approve",
30 + "label": "提交审批"
31 + },
32 + {
33 + "id": "edge-approve-finish",
34 + "sourceNodeId": "approve",
35 + "targetNodeId": "finish",
36 + "label": "审批通过"
37 + }
38 + ]
39 +}
40 +
1 +# 操作语义夹具规划
2 +
3 +当前阶段先完成目录占位,后续会把旧项目高价值操作脚本逐步整理为可复用的语义夹具,例如:
4 +
5 +- 选择单个节点
6 +- 全选流程元素
7 +- 删除并撤销
8 +- 新增节点
9 +- 新增连线
10 +
11 +这样后面无论底层是旧实现还是 LogicFlow 实现,都可以复用同一套操作断言。
1 +import type { Locator, Page } from '@playwright/test'
2 +
3 +export class EditorPage {
4 + readonly page: Page
5 + readonly app: Locator
6 + readonly editor: Locator
7 + readonly toolbar: Locator
8 + readonly canvas: Locator
9 +
10 + constructor(page: Page) {
11 + this.page = page
12 + this.app = page.locator('.app')
13 + this.editor = page.locator('.vue-flow-editor')
14 + this.toolbar = page.locator('.vue-flow-editor-toolbar')
15 + this.canvas = page.locator('.vue-flow-editor-canvas-target')
16 + }
17 +
18 + async gotoHome() {
19 + await this.page.goto('/')
20 + }
21 +}
22 +
1 +import type { Page } from '@playwright/test'
2 +
3 +export async function mockFlowBootstrap(page: Page) {
4 + await page.route('**/admin/**', async (route) => {
5 + const url = route.request().url()
6 +
7 + if (url.includes('a=flow_version') && url.includes('m=mod')) {
8 + await route.fulfill({
9 + status: 200,
10 + contentType: 'application/json',
11 + body: JSON.stringify({
12 + code: 1,
13 + data: [
14 + {
15 + id: 1,
16 + code: 1,
17 + status: '1',
18 + note: '基线版本',
19 + },
20 + ],
21 + }),
22 + })
23 + return
24 + }
25 +
26 + if (url.includes('a=flow_nodes') && url.includes('m=srv')) {
27 + await route.fulfill({
28 + status: 200,
29 + contentType: 'application/json',
30 + body: JSON.stringify({
31 + code: 1,
32 + data: {
33 + nodes: [
34 + {
35 + id: 'start',
36 + text: '开始节点',
37 + shape: 'start',
38 + x: 120,
39 + y: 180,
40 + },
41 + {
42 + id: 'approve',
43 + text: '审批节点',
44 + activity: 'approval',
45 + shape: 'activity',
46 + x: 320,
47 + y: 180,
48 + },
49 + ],
50 + edges: [
51 + {
52 + source: 'start',
53 + target: 'approve',
54 + shape: 'flow-polyline-round',
55 + text: '提交审批',
56 + },
57 + ],
58 + },
59 + }),
60 + })
61 + return
62 + }
63 +
64 + await route.fulfill({
65 + status: 200,
66 + contentType: 'application/json',
67 + body: JSON.stringify({
68 + code: 1,
69 + data: {},
70 + }),
71 + })
72 + })
73 +}
1 +import { mount } from '@vue/test-utils'
2 +import { describe, expect, it } from 'vitest'
3 +import type { FlowEditorRef } from '@flow-editor'
4 +import FlowEditor from '../../packages/flow-editor/src/components/FlowEditor.vue'
5 +
6 +describe('FlowEditor 兼容层', () => {
7 + it('应通过 onRef 暴露最小实例契约', () => {
8 + let editorInstance: FlowEditorRef | null = null
9 +
10 + mount(FlowEditor, {
11 + props: {
12 + enableEngine: false,
13 + onRef: (instance: FlowEditorRef) => {
14 + editorInstance = instance
15 + },
16 + },
17 + })
18 +
19 + expect(editorInstance).not.toBeNull()
20 + expect(typeof editorInstance?.commander.delete).toBe('function')
21 + expect(typeof editorInstance?.commander.undo).toBe('function')
22 + expect(typeof editorInstance?.commander.redo).toBe('function')
23 + expect(typeof editorInstance?.openModel).toBe('function')
24 + expect(typeof editorInstance?.closeModel).toBe('function')
25 + expect(typeof editorInstance?.addNode).toBe('function')
26 + expect(typeof editorInstance?.updateModel).toBe('function')
27 + expect(typeof editorInstance?.read).toBe('function')
28 + })
29 +})
1 +import { describe, expect, it } from 'vitest'
2 +import {
3 + normalizeGraphData,
4 + toLogicFlowGraphData,
5 +} from '../../packages/flow-editor/src/core/adapter/g6-to-logicflow'
6 +import baseSingleNode from '../fixtures/graphs/base-single-node.json'
7 +import branchingFlow from '../fixtures/graphs/branching-flow.json'
8 +import linearApprovalFlow from '../fixtures/graphs/linear-approval-flow.json'
9 +
10 +describe('g6-to-logicflow 适配器', () => {
11 + it('应能把旧项目单节点数据归一化为兼容结构', () => {
12 + const normalizedGraph = normalizeGraphData(baseSingleNode)
13 +
14 + expect(normalizedGraph.nodes).toHaveLength(1)
15 + expect(normalizedGraph.nodes[0]).toMatchObject({
16 + id: 'start-node',
17 + label: '开始节点',
18 + type: 'start',
19 + x: 160,
20 + y: 180,
21 + })
22 + expect(normalizedGraph.edges).toHaveLength(0)
23 + })
24 +
25 + it('应兼容已经整理过的新结构图数据', () => {
26 + const normalizedGraph = normalizeGraphData(linearApprovalFlow)
27 +
28 + expect(normalizedGraph.nodes).toHaveLength(3)
29 + expect(normalizedGraph.edges).toHaveLength(2)
30 + expect(normalizedGraph.nodes[1]).toMatchObject({
31 + id: 'approve',
32 + label: '审批节点',
33 + type: 'approval',
34 + })
35 + expect(normalizedGraph.edges[0]).toMatchObject({
36 + sourceNodeId: 'start',
37 + targetNodeId: 'approve',
38 + label: '提交审批',
39 + })
40 + })
41 +
42 + it('应能把旧项目分支流程映射为 LogicFlow 2.x 可消费结构', () => {
43 + const logicFlowGraph = toLogicFlowGraphData(branchingFlow)
44 +
45 + expect(logicFlowGraph.nodes).toHaveLength(5)
46 + expect(logicFlowGraph.edges).toHaveLength(5)
47 + expect(logicFlowGraph.nodes[0]).toMatchObject({
48 + id: 'start-node',
49 + type: 'circle',
50 + text: '开始节点',
51 + })
52 + expect(logicFlowGraph.nodes[2]).toMatchObject({
53 + id: 'approve-node',
54 + type: 'rect',
55 + text: '通过分支',
56 + })
57 + expect(logicFlowGraph.edges[0]).toMatchObject({
58 + sourceNodeId: 'start-node',
59 + targetNodeId: 'review-node',
60 + type: 'polyline',
61 + text: '提交审批',
62 + })
63 + expect(logicFlowGraph.edges[0].id).toContain('edge-start-node-review-node')
64 + })
65 +})
1 +{
2 + "compilerOptions": {
3 + "target": "ES2022",
4 + "useDefineForClassFields": true,
5 + "module": "ESNext",
6 + "moduleResolution": "Bundler",
7 + "strict": true,
8 + "jsx": "preserve",
9 + "resolveJsonModule": true,
10 + "isolatedModules": true,
11 + "esModuleInterop": true,
12 + "lib": ["ES2022", "DOM", "DOM.Iterable"],
13 + "skipLibCheck": true,
14 + "types": ["node", "vitest/globals"],
15 + "baseUrl": ".",
16 + "paths": {
17 + "@flow-editor": ["packages/flow-editor/src/index.ts"],
18 + "@flow-editor/*": ["packages/flow-editor/src/*"]
19 + }
20 + },
21 + "include": [
22 + "env.d.ts",
23 + "vite.config.ts",
24 + "vitest.config.ts",
25 + "playwright.config.ts",
26 + "apps/**/*.ts",
27 + "apps/**/*.vue",
28 + "packages/**/*.ts",
29 + "packages/**/*.vue",
30 + "tests/**/*.ts"
31 + ]
32 +}
33 +
1 +import { resolve } from 'node:path'
2 +import { defineConfig } from 'vite'
3 +import vue from '@vitejs/plugin-vue'
4 +
5 +export default defineConfig({
6 + root: resolve(__dirname, 'apps/demo'),
7 + plugins: [vue()],
8 + resolve: {
9 + alias: {
10 + '@flow-editor': resolve(__dirname, 'packages/flow-editor/src/index.ts'),
11 + '@flow-editor/components': resolve(__dirname, 'packages/flow-editor/src/components'),
12 + '@flow-editor-src': resolve(__dirname, 'packages/flow-editor/src'),
13 + },
14 + },
15 + server: {
16 + host: '127.0.0.1',
17 + port: 4173,
18 + },
19 +})
20 +
1 +import { resolve } from 'node:path'
2 +import { defineConfig } from 'vitest/config'
3 +import vue from '@vitejs/plugin-vue'
4 +
5 +export default defineConfig({
6 + plugins: [vue()],
7 + resolve: {
8 + alias: {
9 + '@flow-editor': resolve(__dirname, 'packages/flow-editor/src/index.ts'),
10 + '@flow-editor/components': resolve(__dirname, 'packages/flow-editor/src/components'),
11 + '@flow-editor-src': resolve(__dirname, 'packages/flow-editor/src'),
12 + },
13 + },
14 + test: {
15 + environment: 'jsdom',
16 + include: ['tests/unit/**/*.spec.ts'],
17 + },
18 +})
19 +