基于 Electron、Vue3 和 TypeScript 的辅助创作工具全链路开发方案:涵盖画布系统到数据持久化的完整实现
引言
在数字内容创作领域,高效的辅助工具是连接创意与实现的关键桥梁。创作者需要一款集可视化画布、节点关系管理、数据持久化于一体的专业工具,以应对复杂场景下的逻辑梳理与流程管控。
为此,本文提出一套基于 Electron + Vue3 + TypeScript 的全链路开发方案,深度整合 Konva.js 图形渲染、Pinia 状态管理及 Lowdb 轻量化数据存储,构建从画布交互系统到数据持久化的完整技术体系。方案聚焦 栅格化布局、双模式切换、约束性交互规则等核心功能,通过模块化架构设计与策略模式解耦,实现高可用、易扩展的辅助创作工具。
无论你是桌面应用开发者、可视化工具设计者,还是创意产业技术赋能者,本文将为你呈现:
- 画布系统:如何通过 Konva.js 实现高效的节点渲染与连线交互,支持万级元素流畅操作;
- 数据架构:双数据库结构(事件库与画布库)如何隔离业务逻辑与空间关系,保障数据一致性;
- 最佳实践:Electron 主渲染进程协作、命令模式实现撤销/重做、策略模式扩展约束规则等工程化经验。
通过这套方案,你将掌握从需求分析到落地实现的全流程技术细节,为打造专业级创作工具奠定坚实基础。
第一部分:功能要求总结及初步方案
一、辅助创作工具核心功能
- 画布系统
- 双模式:编辑模式(可操作元素)/浏览模式(仅查看)
- 栅格化布局(固定间距a=100px)
- 视图控制:缩放/全景移动
- 节点系统
- 圆形基础节点(4向锚点)
- 栅格对齐(坐标必须为a的整数倍)
- 事件元数据存储(时间/地点/人物等)
- 连线系统
- 直角折线连接
- 防重叠规则
- 碰撞检测(不可穿越节点)
- 交互规则
- 复合选择逻辑(单选/多选/框选)
- 形心锚点生成(框选移动中心点)
- 约束性复制(禁止单独连线复制)
- 节点插入机制(连带位移效应)
- 数据持久化
- 双数据库结构:
- 事件数据库(MD结构化存储)
- 画布数据库(节点/连线空间关系)
- 输出系统
- 可配置MD文件生成
- 显示选项过滤(标题/时间等)
一、技术栈推荐
- 前端框架:Electron + Vue3 + TypeScript(桌面应用开发)
- 图形库:Konva.js(Canvas交互处理)
- 状态管理:Pinia
- 数据存储:Lowdb(基于JSON的本地数据库)
- UI组件库:Naive UI
- 打包工具:electron-builder
二、模块划分方案
-
核心模块:
- CanvasManager.ts(画布管理)
- NodeSystem/
│─ NodeManager.ts(节点管理)
│─ AnchorSystem.ts(锚点系统)
│─ WireManager.ts(连线管理)
│─ ConstraintRules.ts(约束规则验证) - EventSystem/
│─ EventEditor.ts(事件编辑)
│─ TemplateManager.ts(模板管理)
-
数据模块:
- schemas/
│─ EventSchema.ts(事件数据模型)
│─ CanvasSchema.ts(画布元素模型) - db/
│─ EventDB.ts(事件数据库)
│─ CanvasDB.ts(画布数据库)
- schemas/
-
UI模块:
- Toolbars/
│─ MainToolbar.vue
│─ OperationToolbar.vue
│─ EventList.vue - Canvas/
│─ KonvaCanvas.vue
│─ GridLayer.vue
- Toolbars/
三、核心数据结构设计(TypeScript接口)
// 节点接口
interface ICanvasNode {
id: string;
x: number; // 格栅对齐坐标
y: number;
anchors: {
top: boolean;
bottom: boolean;
left: boolean;
right: boolean;
};
style: {
radius: number;
color: string;
};
}
// 连线接口
interface IWire {
id: string;
from: { nodeId: string; anchor: AnchorType };
to: { nodeId: string; anchor: AnchorType };
path: Array<{x: number, y: number}>; // 折线路径点
style: {
stroke: string;
dash: number[];
};
}
// 事件数据接口
interface IEvent {
id: string;
title: string;
time: string;
location: string;
characters: string[];
foreshadowing: string;
summary: string;
details: string;
displayFlags: {
showTitle: boolean;
showTime: boolean;
showCharacters: boolean;
showForeshadowing: boolean;
};
}
四、开发步骤规划
-
阶段一:基础框架搭建(预计2周)
- 安装Electron基础模板
- 配置Vue3+TS开发环境
- 实现画布网格渲染(GridLayer)
- 建立基础节点渲染系统
- 实现LowDB数据库连接
-
阶段二:核心交互开发(预计3周)
- 节点锚点系统
- 连线约束规则模块
- 框选与形心锚点计算
- 移动/复制约束逻辑
- 节点事件编辑模态框
-
阶段三:工具栏与辅助功能(预计2周)
- 实现可折叠工具栏组件
- 主题切换功能
- 导出MD文件功能
- 撤销/重做堆栈实现
- 列表排序功能
-
阶段四:优化与测试(预计1周)
- 性能优化(虚拟滚动列表)
- 碰撞检测算法优化
- 单元测试(使用Vitest)
- 打包成exe文件
五、关键依赖清单
# 核心依赖
npm install electron vue@next konva@8.4.3 pinia naive-ui
# 开发依赖
npm install -D electron-builder vitest lowdb sass
六、核心算法建议
- 格栅定位算法:
const GRID_SIZE = 50;
function snapToGrid(pos: number): number {
return Math.round(pos / GRID_SIZE) * GRID_SIZE;
}
- 连线路径生成算法(直角折线):
function generateRightAnglePath(start: Point, end: Point): Point[] {
const midX = (start.x + end.x) / 2;
return [
start,
{ x: midX, y: start.y },
{ x: midX, y: end.y },
end
];
}
七、代码组织建议
/my-novel-helper
├─ electron/
│ ├─ main.js # 主进程入口
│ └─ preload.js # 进程通信脚本
├─ src/
│ ├─ renderer/
│ │ ├─ core/ # 核心逻辑
│ │ ├─ components/ # Vue组件
│ │ ├─ stores/ # Pinia状态库
│ │ └─ assets/ # 静态资源
│ ├─ db/ # 数据库模块
│ └─ types/ # TS类型定义
├─ package.json
└─ tsconfig.json
八、开发建议:
- 使用单向数据流架构,避免直接操作DOM
- 对画布操作使用Command模式实现撤销/重做
- 采用事件总线处理跨模块通信
- 对约束规则实现策略模式,便于扩展
九、后续可逐步实现的功能扩展点:
- 导入JSON/MD文件功能
- 时间轴视图
- 关系图谱分析
- 自动布局算法
- 版本快照功能
这种架构设计能有效隔离画布操作、业务逻辑和数据持久化层,符合模块化开发需求。每个阶段完成后都可独立测试运行,降低后期维护复杂度。
第二部分:模块划分方案详解(含可视化说明)
一、模块架构全景图
二、核心模块深度解析
1. CanvasManager.ts
▨ 职责类比:画布系统的"交通指挥中心"
▨ 关键功能流程图:
▨ 代码实例:
// 模式切换实现
class CanvasManager {
private editMode = ref(true);
switchMode(isEdit: boolean) {
this.editMode.value = isEdit;
GridLayer.setGridVisible(isEdit); // 格栅可见性
NodeSystem.setDraggable(isEdit); // 节点可拖动状态
WireManager.setInteractive(isEdit); // 连线可编辑
}
}
2. NodeSystem 子系统
▨ 模块协作关系:
▨ 典型场景示例:
当用户按住Shift键点击锚点新增节点时:
NodeManager
接收点击事件- 调用
ConstraintRules.checkInsertPosition()
验证位置合法性 - 通过后
AnchorSystem.generateNewAnchors()
创建新锚点 WireManager.adjustExistingWires()
调整已有连线
▨ 数据结构示例:
// 节点内存结构
{
id: 'NODE_2023-08-20_08:45:00',
x: 300, // 必为100的整数倍
y: 200,
anchors: {
top: true, // 存在上锚点
right: false // 无右锚点
},
style: {
radius: 12,
color: '#4CAF50'
}
}
3. ConstraintRules.ts
▨ 规则验证流程图:
▨ 典型约束实现:
// 连线防重叠规则
function checkWireOverlap(newWire: IWire) {
const allWires = WireManager.getAllWires();
return allWires.some(existingWire => {
// 使用矢量比对算法
const path1 = simplifyPath(newWire.path);
const path2 = simplifyPath(existingWire.path);
return isPathOverlap(path1, path2);
});
}
三、数据模块详解
1. 数据结构设计
▨ 事件数据模型:
// EventSchema.ts
export interface IEvent {
id: string; // 唯一标识符
nodeId: string; // 关联的节点ID
title: string; // 事件标题
time: string; // ISO8601时间格式
characters: string[]; // 涉及人物列表
foreshadowing: string;// 伏笔标记
displayOptions: { // 显示配置
showTime: boolean;
showCharacters: boolean;
};
}
▨ 数据库关系图:
2. 数据流示意图:
第三部分:核心数据结构深度解析
一、节点系统数据结构(可视化说明)
▨ 字段解释表:
字段 | 示例值 | 作用说明 | 新手类比 |
---|---|---|---|
x | 300 | 横向坐标(像素) | 棋盘上的列编号 |
y | 200 | 纵向坐标(像素) | 棋盘上的行编号 |
anchors.top | true | 顶部是否有连接点 | 机器人的顶部充电接口 |
style.radius | 12 | 节点显示大小 | 纽扣的直径尺寸 |
二、连线系统设计原理
1. 路径存储策略
▨ 路径点数据结构示例:
// 从(200,300)到(400,300)的连线路径
const wirePath = [
{ x: 200, y: 300 }, // 起点
{ x: 300, y: 300 }, // 中间转折点
{ x: 300, y: 400 }, // 第二个转折点
{ x: 400, y: 400 } // 终点
]
2. 样式控制逻辑:
// 连线样式管理器
class WireStyleManager {
private static presetStyles = {
default: { stroke: '#666', dash: [] },
selected: { stroke: '#2196F3', dash: [5,5] },
error: { stroke: '#FF5722', dash: [10,5] }
};
updateWireStyle(wire: IWire, status: 'default' | 'selected' | 'error') {
Object.assign(wire.style, this.presetStyles[status]);
}
}
第四部分:开发步骤拆解(含里程碑图示)
分阶段开发重点说明
1. 阶段一:基础框架搭建
▨ 关键技术点:
-
使用Electron的
BrowserWindow
创建窗口 -
实现画布网格的数学计算:
// 网格绘制算法 function drawGrid(ctx: CanvasRenderingContext2D) { const spacing = 100; // 格栅间距 for(let x = 0; x < ctx.canvas.width; x += spacing){ ctx.moveTo(x, 0); ctx.lineTo(x, ctx.canvas.height); } // 同理绘制纵向线条... }
▨ 新手常见问题:
- Q:为什么节点位置需要对齐格栅?
- A:就像停车场车位需要标准间距,保证元素排列整齐和连线规范
2. 阶段二:核心交互开发
▨ 关键技术点:
-
锚点碰撞检测算法:
function findNearestAnchor(pos: Point) { return nodes.reduce((nearest, node) => { const anchors = getAnchorPositions(node); const dist = calculateDistance(pos, anchors); return dist < nearest.dist ? { node, dist } : nearest; }, { dist: Infinity }); }
▨ 可视化调试技巧:
// 开发时开启调试模式显示锚点半径
const DEBUG_MODE = true;
function drawAnchors() {
if(DEBUG_MODE) {
ctx.fillStyle = 'rgba(255,0,0,0.3)';
ctx.fillRect(anchor.x-5, anchor.y-5, 10, 10);
}
}
第五部分:关键技术点详解(含实战示例)
一、Electron 主进程与渲染进程协作
▨ 典型代码结构:
// 主进程 main.js
const { app, BrowserWindow, ipcMain } = require('electron')
ipcMain.handle('save-data', async (event, data) => {
await fs.writeFile('data.json', JSON.stringify(data))
})
function createWindow() {
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
})
win.loadFile('index.html')
}
二、Konva.js 图形系统实践
1. 节点渲染实现:
// 单个节点渲染组件
const NodeComponent = ({ node }) => {
const [selected, setSelected] = useState(false);
return (
<Group x={node.x} y={node.y}>
{/* 主体圆形 */}
<Circle
radius={node.style.radius}
fill={selected ? '#FFEE58' : node.style.color}
onClick={handleSelect}
/>
{/* 锚点可视化 */}
{Object.entries(node.anchors).map(([direction, visible]) => (
visible && <AnchorPoint direction={direction} />
))}
</Group>
);
};
2. 连线交互示意图:
三、Pinia 状态管理实战
1. 状态仓库设计:
// stores/canvas.ts
export const useCanvasStore = defineStore('canvas', {
state: () => ({
nodes: [] as ICanvasNode[],
wires: [] as IWire[],
gridSize: 100
}),
actions: {
addNode(newNode: ICanvasNode) {
this.nodes.push(newNode);
},
// 其他操作方法...
}
})
2. 组件中调用示例:
<script setup>
import { useCanvasStore } from './stores/canvas'
const store = useCanvasStore()
// 添加新节点
const handleClick = () => {
store.addNode({
id: Date.now().toString(),
x: 0,
y: 0,
anchors: { top: true, bottom: false, left: false, right: false },
style: { radius: 12, color: '#4CAF50' }
})
}
</script>
四、Lowdb 数据库操作
1. 数据库初始化:
// db/EventDB.ts
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'
type EventData = {
events: IEvent[]
}
const adapter = new JSONFile<EventData>('events.json')
const db = new Low(adapter, { events: [] })
export const initializeDB = async () => {
await db.read()
db.data ||= { events: [] }
await db.write()
}
2. 增删改查示例:
// 添加事件
export const addEvent = async (event: IEvent) => {
db.data.events.push(event)
await db.write()
}
// 查询未闭合伏笔
export const getUnresolvedForeshadowing = () => {
return db.data.events.filter(e =>
e.foreshadowing && !e.foreshadowing.resolved
)
}
五、撤销/重做实现方案
1. 命令模式架构:
2. 具体实现代码:
class AddNodeCommand implements Command {
private node: ICanvasNode
private store: ReturnType<typeof useCanvasStore>
constructor(node: ICanvasNode) {
this.node = node
this.store = useCanvasStore()
}
execute() {
this.store.addNode(this.node)
}
undo() {
this.store.nodes = this.store.nodes.filter(n => n.id !== this.node.id)
}
}
第六部分:复杂交互逻辑深度解析
一、形心锚点计算算法(带可视化推导)
1. 计算原理图示:
2. 数学公式实现:
function calculateCentroid(elements: (ICanvasNode | IWire)[]): Point {
let totalX = 0, totalY = 0, count = 0;
elements.forEach(element => {
if ('x' in element) { // 节点类型
totalX += element.x;
totalY += element.y;
count++;
} else { // 连线类型
element.path.forEach(point => {
totalX += point.x;
totalY += point.y;
count++;
});
}
});
return {
x: Math.round(totalX / count / GRID_SIZE) * GRID_SIZE,
y: Math.round(totalY / count / GRID_SIZE) * GRID_SIZE
};
}
3. 调试可视化技巧:
// 开发时显示形心标记
function debugShowCentroid(ctx, centroid) {
ctx.fillStyle = 'rgba(255,0,0,0.5)';
ctx.beginPath();
ctx.arc(centroid.x, centroid.y, 8, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#FF0000';
ctx.setLineDash([5, 3]);
ctx.beginPath();
ctx.moveTo(centroid.x-15, centroid.y);
ctx.lineTo(centroid.x+15, centroid.y);
ctx.moveTo(centroid.x, centroid.y-15);
ctx.lineTo(centroid.x, centroid.y+15);
ctx.stroke();
}
二、节点插入位移算法(带分步演示)
1. 操作流程图解:
2. 核心代码实现:
class DisplacementEngine {
static shiftNodes(startX: number, direction: 'left'|'right') {
const store = useCanvasStore();
const offset = direction === 'right' ? GRID_SIZE : -GRID_SIZE;
// 获取需要移动的节点
const affectedNodes = store.nodes
.filter(node => node.x >= startX)
.sort((a, b) => a.x - b.x);
// 执行位移(需从最右侧开始处理)
const sortedNodes = direction === 'right'
? affectedNodes.reverse()
: affectedNodes;
sortedNodes.forEach(node => {
node.x += offset;
// 更新关联连线
WireManager.updateWirePositions(node.id);
});
}
}
3. 位移效果可视化:
初始状态:
[节点A]-(连线)-[节点B]-(连线)-[节点C]
插入新节点后:
[节点A]-(连线)-[新节点]-(连线)-[节点B(原x+100)]-(连线)-[节点C(原x+100)]
三、连线防重叠算法详解
1. 碰撞检测原理:
2. 核心数学方法:
// 线段交叉检测函数
function isLineSegmentsIntersect(a1: Point, a2: Point, b1: Point, b2: Point) {
const denominator = (b2.y - b1.y)*(a2.x - a1.x) - (b2.x - b1.x)*(a2.y - a1.y);
// 线段平行处理
if (denominator === 0) return false;
const ua = ((b2.x - b1.x)*(a1.y - b1.y) - (b2.y - b1.y)*(a1.x - b1.x)) / denominator;
const ub = ((a2.x - a1.x)*(a1.y - b1.y) - (a2.y - a1.y)*(a1.x - b1.x)) / denominator;
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
}
3. 性能优化策略:
// 空间分区加速检测
const SPACE_GRID = 200; // 分区尺寸
function checkCollisionFast(newWire: IWire) {
// 建立空间索引
const gridMap = new Map<string, IWire[]>();
// 将现有连线分配到网格
allWires.forEach(wire => {
wire.path.forEach(point => {
const gridKey = `${Math.floor(point.x/SPACE_GRID)}_${Math.floor(point.y/SPACE_GRID)}`;
gridMap.set(gridKey, [...(gridMap.get(gridKey) || []), wire]);
});
});
// 只检查新连线所在网格
return newWire.path.some(point => {
const gridKey = `${Math.floor(point.x/SPACE_GRID)}_${Math.floor(point.y/SPACE_GRID)}`;
return gridMap.get(gridKey)?.some(wire => checkCollision(newWire, wire)) || false;
});
}
第七部分:事件系统与导出功能实现
一、事件编辑系统实现
1. 模态框组件架构:
2. 核心组件代码:
<!-- EventModal.vue -->
<template>
<div class="modal-mask">
<div class="modal-container">
<div class="header">
<h3>事件编辑器 - {{ nodeId }}</h3>
</div>
<div class="form-section">
<label>事件名称:
<input v-model="formData.title" />
<input type="checkbox" v-model="formData.displayFlags.showTitle" />
</label>
<!-- 其他字段类似 -->
</div>
<button @click="saveChanges">保存</button>
<button @click="close">取消</button>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ nodeId: string }>()
// 从数据库加载初始数据
const initialData = await EventDB.getEventByNode(props.nodeId)
const formData = reactive({ ...initialData })
const saveChanges = async () => {
await EventDB.updateEvent(props.nodeId, formData)
emit('update:node')
}
</script>
3. 显示控制逻辑:
// 节点渲染时检查显示配置
function renderNodeInfo(node: ICanvasNode) {
const eventData = EventDB.getEventByNode(node.id)
let infoText = ''
if(eventData.displayFlags.showTitle) infoText += eventData.title + '\n'
if(eventData.displayFlags.showTime) infoText += eventData.time + '\n'
context.fillText(infoText, node.x, node.y - 20)
}
二、Markdown导出系统实现
1. 转换流程图:
2. 核心转换代码:
class MDExporter {
static async generate() {
const events = await EventDB.getAllEvents()
const canvasData = CanvasDB.getSnapshot()
let mdContent = `# 故事大纲\n\n`
// 时间线部分
mdContent += '## 事件时间线\n'
events.sort(byTime).forEach(event => {
mdContent += `### ${event.title}\n`
mdContent += `**时间:** ${event.time}\n\n`
mdContent += `${event.summary}\n\n`
})
// 关系图谱
mdContent += '## 关系图谱\n```mermaid\ngraph TD\n'
canvasData.wires.forEach(wire => {
const fromNode = canvasData.nodes.find(n => n.id === wire.from.nodeId)!
const toNode = canvasData.nodes.find(n => n.id === wire.to.nodeId)!
mdContent += `${fromNode.id}["${fromNode.title}"] --> ${toNode.id}["${toNode.title}"]\n`
})
mdContent += '```\n'
return mdContent
}
}
3. 文件保存实现:
// 主进程处理文件保存
ipcMain.handle('export-md', async (event) => {
const content = await MDExporter.generate()
const { filePath } = await dialog.showSaveDialog({
title: '导出Markdown',
filters: [{ name: 'Markdown', extensions: ['md'] }]
})
if(filePath) {
await fs.promises.writeFile(filePath, content)
return { success: true, path: filePath }
}
return { success: false }
})
三、主题切换系统实现
1. CSS变量控制方案:
/* 全局样式表 */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--grid-color: #eeeeee;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--grid-color: #404040;
}
.canvas-container {
background-color: var(--bg-color);
}
2. 主题切换控制器:
// themeManager.ts
class ThemeManager {
private currentTheme = ref<'light' | 'dark'>('light')
toggleTheme() {
this.currentTheme.value = this.currentTheme.value === 'light' ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', this.currentTheme.value)
}
watchTheme(callback: (theme: string) => void) {
watch(this.currentTheme, callback)
}
}
第八部分:调试与优化策略
1. 性能监控面板:
// 开发时显示调试信息
function renderDebugOverlay() {
if(!DEBUG_MODE) return
ctx.fillStyle = 'rgba(0,0,0,0.7)'
ctx.fillRect(10, 10, 200, 120)
ctx.fillStyle = '#00FF00'
ctx.textAlign = 'left'
ctx.fillText(`节点数量: ${nodes.length}`, 15, 30)
ctx.fillText(`连线数量: ${wires.length}`, 15, 50)
ctx.fillText(`内存占用: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`, 15, 70)
}
2. 自动化测试示例:
// 约束规则测试用例
describe('连线约束规则', () => {
test('禁止重复连线', () => {
const wire1 = createWire('A', 'B')
const wire2 = createWire('A', 'B')
expect(ConstraintRules.checkWireExists(wire2)).toBe(true)
})
test('允许不同锚点的连线', () => {
const wire1 = createWire('A.top', 'B.bottom')
const wire2 = createWire('A.right', 'B.left')
expect(ConstraintRules.checkWireExists(wire2)).toBe(false)
})
})
第九部分:开发建议深度解析
一、单向数据流架构实践
1. 数据流向示意图:
2. 具体实现示例:
// 正确做法:通过Store更新状态
function handleMoveNode(nodeId: string, newPos: Point) {
const store = useCanvasStore();
store.updateNodePosition(nodeId, snapToGrid(newPos));
}
// 错误做法:直接修改DOM
function badPractice(nodeElement: HTMLElement) {
// 直接操作DOM元素(禁止!)
nodeElement.style.left = '300px';
}
3. 优势对比表:
方式 | 调试难度 | 可维护性 | 状态追溯 | 组件复用性 |
---|---|---|---|---|
单向数据流 | 低 | 高 | 完整 | 高 |
直接DOM操作 | 高 | 低 | 困难 | 低 |
二、Command模式实现撤销/重做
1. 类结构设计:
2. 核心实现代码:
// 命令管理器
class CommandManager {
private stack: Command[] = [];
private pointer = -1;
execute(command: Command) {
this.stack.splice(this.pointer + 1);
this.stack.push(command);
command.execute();
this.pointer++;
}
undo() {
if (this.pointer >= 0) {
this.stack[this.pointer--].undo();
}
}
redo() {
if (this.pointer < this.stack.length - 1) {
this.stack[++this.pointer].execute();
}
}
}
// 使用示例
const cmd = new AddNodeCommand(newNode);
commandManager.execute(cmd);
三、事件总线实现跨模块通信
1. 通信架构图:
2. 基于Vue的实现:
// eventBus.ts
import mitt from 'mitt';
type Events = {
nodeSelected: ICanvasNode;
wireCreated: IWire;
modeChanged: 'edit' | 'view';
};
export const eventBus = mitt<Events>();
// 组件A发送事件
eventBus.emit('nodeSelected', currentNode);
// 组件B监听事件
eventBus.on('nodeSelected', (node) => {
showNodeDetail(node);
});
四、策略模式实现约束规则
1. 策略模式结构:
2. 可扩展规则实现:
// 策略接口
interface IConstraintStrategy {
check(context: OperationContext): boolean;
}
// 具体策略
class MoveStrategy implements IConstraintStrategy {
check(context: MoveContext) {
// 实现移动约束逻辑
}
}
// 策略管理器
class ConstraintManager {
private strategies: Map<OperationType, IConstraintStrategy> = new Map();
register(type: OperationType, strategy: IConstraintStrategy) {
this.strategies.set(type, strategy);
}
validate(type: OperationType, context: OperationContext) {
return this.strategies.get(type)?.check(context) ?? true;
}
}
// 注册新策略示例
const manager = new ConstraintManager();
manager.register('COPY', new CopyConstraintStrategy());
第十部分:扩展功能实现指南
一、导入功能实现方案
1. MD文件解析流程:
2. 关键解析代码:
class MDImporter {
static parse(content: string) {
const events = this.extractEvents(content);
const relations = this.extractMermaidRelations(content);
return {
events,
canvasData: this.buildCanvasData(relations)
};
}
private static extractMermaidRelations(content: string) {
const mermaidBlocks = content.match(/```mermaid([\s\S]*?)```/g);
// 解析Mermaid语法中的节点关系...
}
}
二、时间轴视图实现
1. 双视图联动设计:
2. 时间轴组件示例:
<template>
<div class="timeline">
<div v-for="event in sortedEvents"
:key="event.id"
class="timeline-item"
:style="{ left: calcPosition(event.time) }"
@click="selectNode(event.nodeId)">
{{ event.title }}
</div>
</div>
</template>
<script setup>
const calcPosition = (time) => {
const start = new Date('2023-01-01').getTime();
const totalDays = 365;
const day = (new Date(time) - start) / (1000*3600*24);
return `${(day / totalDays * 100)}%`;
}
</script>
三、自动布局算法实现
1. 力导向布局伪代码:
# 简化的布局算法
def force_directed_layout(nodes, wires):
for _ in range(iterations):
# 节点间斥力
for node1 in nodes:
for node2 in nodes:
if node1 != node2:
repel(node1, node2)
# 连线拉力
for wire in wires:
attract(wire.fromNode, wire.toNode)
# 更新位置
update_positions()
2. 实现建议:
-
使用现有库:
d3-force
(推荐) -
自定义参数:
const simulation = d3.forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-50)) .force("link", d3.forceLink(wires).distance(100)) .force("grid", gridForce(100)); // 自定义格栅对齐力
四、版本快照实现方案
1. 快照管理设计:
2. 核心实现代码:
class SnapshotManager {
private snapshots: ProjectSnapshot[] = [];
private currentVersion = 0;
createSnapshot() {
const snapshot = {
canvas: CanvasDB.export(),
events: EventDB.export(),
timestamp: new Date()
};
this.snapshots = this.snapshots.slice(0, this.currentVersion + 1);
this.snapshots.push(snapshot);
this.currentVersion++;
}
restore(version: number) {
const snapshot = this.snapshots[version];
CanvasDB.import(snapshot.canvas);
EventDB.import(snapshot.events);
}
}