diff --git a/README.md b/README.md index f0e7dd1..ca40fce 100644 --- a/README.md +++ b/README.md @@ -1 +1,126 @@ -OJ next \ No newline at end of file +# OJ Next + +基于 Vue 3 + TypeScript 的在线判题系统前端项目。 + +## 项目特性 + +- 🎯 **现代化技术栈** - Vue 3 + TypeScript + Vite +- 📊 **数据可视化** - 丰富的图表和统计功能 +- 🎨 **流程图编辑器** - 内置 FlowchartEditor 组件 +- 📱 **响应式设计** - 支持多端适配 +- ⚡ **高性能** - 优化的组件和状态管理 + +## 核心组件 + +### FlowchartEditor 流程图编辑器 + +一个功能完整的流程图编辑器组件,支持: + +- 🎯 **拖拽创建节点** - 从工具栏拖拽节点到画布 +- 🔗 **节点连接** - 支持节点间的连线操作 +- ✏️ **节点编辑** - 双击节点进行文本编辑 +- 📋 **节点操作** - 复制、删除、批量操作 +- ↩️ **撤销重做** - 完整的历史记录管理 +- ⌨️ **键盘快捷键** - Ctrl+Z/Ctrl+Y 撤销重做,Delete 删除 +- 💾 **自动保存** - 本地缓存,防止数据丢失 +- 🎨 **多种节点类型** - 开始、输入、处理、判断、循环、输出、结束 +- 🖱️ **交互优化** - 流畅的拖拽体验和视觉反馈 + +**核心特性**: +- 基于 Vue Flow 构建,性能优异 +- 模块化设计,易于扩展 +- 响应式布局,支持多端适配 +- 完整的 TypeScript 支持 + +详细文档:[FlowchartEditor 文档](./docs/FlowchartEditor.md) + +## 技术栈 + +- **Vue 3** - 渐进式 JavaScript 框架 +- **TypeScript** - JavaScript 的超集 +- **Rsbuild** - 基于 Rspack 的构建工具 +- **Vue Flow** - 流程图组件库 +- **Composition API** - Vue 3 组合式 API +- **Naive UI** - Vue 3 组件库 +- **Chart.js** - 图表可视化库 +- **CodeMirror** - 代码编辑器 +- **Yjs** - 实时协作框架 + +## 项目结构 + +``` +src/ +├── shared/ # 共享组件和工具 +│ ├── components/ +│ │ └── FlowchartEditor/ # 流程图编辑器 +│ │ ├── index.vue # 主组件 +│ │ ├── CustomNode.vue # 自定义节点 +│ │ ├── Toolbar.vue # 工具栏 +│ │ ├── NodeHandles.vue # 节点操作 +│ │ ├── NodeActions.vue # 节点动作 +│ │ ├── useCache.ts # 缓存管理 +│ │ ├── useDnD.ts # 拖拽处理 +│ │ ├── useFlowOperations.ts # 流程操作 +│ │ ├── useHistory.ts # 历史记录 +│ │ └── useNodeStyles.ts # 节点样式 +│ ├── composables/ # 组合式函数 +│ ├── layout/ # 布局组件 +│ ├── store/ # 状态管理 +│ └── themes/ # 主题配置 +├── oj/ # 主要业务组件 +│ ├── ai/ # AI分析模块 +│ ├── problem/ # 题目相关 +│ ├── contest/ # 竞赛相关 +│ ├── submission/ # 提交相关 +│ └── store/ # 业务状态管理 +├── admin/ # 管理后台组件 +└── utils/ # 工具函数 +``` + +## 开发指南 + +### 安装依赖 + +```bash +npm install +``` + +### 启动开发服务器 + +```bash +npm start +# 或 +npm run start +``` + +### 构建生产版本 + +```bash +npm run build +``` + +### 构建测试环境版本 + +```bash +npm run build:test +``` + +### 构建预发布版本 + +```bash +npm run build:staging +``` + +## 文档 + +- [FlowchartEditor 流程图编辑器](./docs/FlowchartEditor.md) +- [图表组件说明](./docs/图表.md) +- [API 接口对比分析](./docs/API接口对比分析.md) + +## 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 许可证 + +MIT License \ No newline at end of file diff --git a/docs/API接口对比分析.md b/docs/API接口对比分析.md index 096f37e..2a0cb71 100644 --- a/docs/API接口对比分析.md +++ b/docs/API接口对比分析.md @@ -2,7 +2,23 @@ ## 更新日志 -### 最近更新(2025-10) +### 最近更新(2025-01-15) +- ✨ **FlowchartEditor 流程图编辑器**:新增完整的流程图编辑功能 + - 基于 Vue Flow 构建的流程图编辑器组件 + - 支持7种节点类型:开始、输入、处理、判断、循环、输出、结束 + - 完整的拖拽创建、节点连接、编辑功能 + - 撤销重做、自动保存、键盘快捷键支持 + - 模块化设计,包含9个独立的功能模块 +- 🎨 **前端组件优化**:完善了组件文档和项目结构说明 + - 更新了 README.md 中的技术栈和项目结构 + - 创建了详细的 FlowchartEditor 使用文档 + - 优化了开发指南和构建流程说明 +- 🔧 **构建工具升级**:从 Vite 迁移到 Rsbuild + - 使用 Rsbuild 作为新的构建工具 + - 支持多环境构建(test、staging、production) + - 优化了构建性能和开发体验 + +### 历史更新(2025-10) - ✨ **AI分析功能增强**:完善了AI智能分析模块的文档说明 - 详细说明了4个AI相关接口的功能和参数 - 新增等级系统说明(S/A/B/C),包含特殊规则 @@ -434,3 +450,99 @@ const hasTriedButNotPassed = computed(() => { **结论**: 此接口是系统内部通信接口,前端完全不需要调用。类似的内部接口还可能存在于分布式系统的其他服务间通信中。 +--- + +## 六、新增功能模块 + +### FlowchartEditor 流程图编辑器 ✨ + +**功能概述**: 基于 Vue Flow 构建的完整流程图编辑器,支持拖拽创建、节点连接、编辑等核心功能。 + +**技术实现**: +- **核心库**: Vue Flow (@vue-flow/core) +- **组件架构**: 模块化设计,9个独立功能模块 +- **状态管理**: 基于 Vue 3 Composition API +- **数据持久化**: localStorage 自动缓存 +- **交互体验**: 拖拽、键盘快捷键、撤销重做 + +**组件结构**: +``` +FlowchartEditor/ +├── index.vue # 主组件 - 整合所有功能 +├── CustomNode.vue # 自定义节点 - 7种节点类型 +├── Toolbar.vue # 工具栏 - 节点创建和操作 +├── NodeHandles.vue # 节点操作手柄 - 连接点管理 +├── NodeActions.vue # 节点动作 - 删除、编辑按钮 +├── useCache.ts # 缓存管理 - 自动保存/恢复 +├── useDnD.ts # 拖拽处理 - 节点创建逻辑 +├── useFlowOperations.ts # 流程操作 - 增删改查 +├── useHistory.ts # 历史记录 - 撤销重做 +└── useNodeStyles.ts # 节点样式 - 类型配置 +``` + +**节点类型支持**: +- **开始节点** (start) - 流程开始,绿色主题 +- **输入节点** (input) - 数据输入,蓝色主题 +- **处理节点** (default) - 数据处理,紫色主题 +- **判断节点** (decision) - 条件判断,橙色主题 +- **循环节点** (loop) - 循环处理,青色主题 +- **输出节点** (output) - 数据输出,粉色主题 +- **结束节点** (end) - 流程结束,红色主题 + +**核心功能**: +1. **拖拽创建**: 从工具栏拖拽节点到画布 +2. **节点连接**: 支持节点间的连线操作 +3. **节点编辑**: 双击节点进行文本编辑 +4. **撤销重做**: 完整的历史记录管理 +5. **自动保存**: 本地缓存,防止数据丢失 +6. **键盘快捷键**: Ctrl+Z/Ctrl+Y 撤销重做,Delete 删除 +7. **批量操作**: 支持多选和批量删除 + +**数据格式**: +```typescript +// 节点数据 +interface Node { + id: string + type: string + position: { x: number, y: number } + data: { + customLabel: string + [key: string]: any + } +} + +// 边数据 +interface Edge { + id: string + source: string + target: string + sourceHandle?: string + targetHandle?: string + type?: string +} +``` + +**使用场景**: +- 算法流程图绘制 +- 业务流程设计 +- 教学演示工具 +- 问题分析图表 + +**性能优化**: +- 基于 Vue Flow 的高性能渲染 +- 模块化设计,按需加载 +- 本地缓存减少重复计算 +- 响应式状态管理 + +**扩展性**: +- 支持自定义节点类型 +- 可扩展工具栏功能 +- 支持主题定制 +- 支持数据导入导出 + +**文档支持**: +- 详细的使用文档: `docs/FlowchartEditor.md` +- 完整的 API 说明 +- 最佳实践指南 +- 故障排除手册 + diff --git a/docs/FlowchartEditor.md b/docs/FlowchartEditor.md new file mode 100644 index 0000000..e026d3a --- /dev/null +++ b/docs/FlowchartEditor.md @@ -0,0 +1,311 @@ +# FlowchartEditor 流程图编辑器 + +一个基于 Vue 3 + Vue Flow 构建的功能完整的流程图编辑器组件。 + +## 功能特性 + +### 🎯 核心功能 +- **拖拽创建节点** - 从工具栏拖拽节点到画布 +- **节点连接** - 支持节点间的连线操作 +- **节点编辑** - 双击节点进行文本编辑 +- **撤销重做** - 完整的历史记录管理 +- **自动保存** - 本地缓存,防止数据丢失 +- **键盘快捷键** - 支持常用快捷键操作 + +### 🎨 节点类型 +- **开始节点** (start) - 流程开始 +- **输入节点** (input) - 数据输入 +- **处理节点** (default) - 数据处理 +- **判断节点** (decision) - 条件判断 +- **循环节点** (loop) - 循环处理 +- **输出节点** (output) - 数据输出 +- **结束节点** (end) - 流程结束 + +### ⌨️ 快捷键 +- `Ctrl+Z` / `Cmd+Z` - 撤销 +- `Ctrl+Y` / `Cmd+Shift+Z` - 重做 +- `Delete` / `Backspace` - 删除选中节点 + +## 组件结构 + +``` +FlowchartEditor/ +├── index.vue # 主组件 +├── CustomNode.vue # 自定义节点组件 +├── Toolbar.vue # 工具栏组件 +├── NodeHandles.vue # 节点操作手柄 +├── NodeActions.vue # 节点动作按钮 +├── useCache.ts # 缓存管理 +├── useDnD.ts # 拖拽处理 +├── useFlowOperations.ts # 流程操作 +├── useHistory.ts # 历史记录 +└── useNodeStyles.ts # 节点样式 +``` + +## 使用方法 + +### 基本用法 + +```vue + + + + + +``` + +### 获取流程图数据 + +```javascript +// 获取当前流程图数据 +const data = flowchartRef.value.getFlowchartData() +console.log(data.nodes) // 节点数组 +console.log(data.edges) // 边数组 +``` + +## 组件详解 + +### 主组件 (index.vue) + +主组件负责整合所有功能模块,提供完整的流程图编辑体验。 + +**主要功能**: +- 整合 Vue Flow 核心功能 +- 管理节点和边的状态 +- 处理用户交互事件 +- 提供数据暴露接口 + +**暴露的方法**: +- `getFlowchartData()` - 获取当前流程图数据 + +### 自定义节点 (CustomNode.vue) + +基于 Vue Flow 的自定义节点组件,支持多种节点类型。 + +**节点类型配置**: +- 每种节点类型都有独特的样式和图标 +- 支持自定义标签文本 +- 提供删除和编辑功能 + +### 工具栏 (Toolbar.vue) + +提供节点创建和操作的工具集合。 + +**功能**: +- 节点类型选择 +- 撤销/重做操作 +- 清空画布 +- 保存状态显示 + +### 缓存管理 (useCache.ts) + +提供本地存储功能,防止数据丢失。 + +**功能**: +- 自动保存到 localStorage +- 页面刷新后恢复数据 +- 保存状态提示 +- 清空缓存功能 + +### 拖拽处理 (useDnD.ts) + +处理节点拖拽创建的逻辑。 + +**功能**: +- 拖拽开始处理 +- 拖拽悬停效果 +- 拖拽放置处理 +- 节点位置计算 + +### 流程操作 (useFlowOperations.ts) + +处理流程图的增删改操作。 + +**功能**: +- 节点连接处理 +- 边删除处理 +- 节点删除处理 +- 节点更新处理 +- 清空画布 + +### 历史记录 (useHistory.ts) + +提供撤销重做功能。 + +**功能**: +- 状态快照保存 +- 撤销操作 +- 重做操作 +- 历史记录管理 + +### 节点样式 (useNodeStyles.ts) + +定义各种节点类型的样式配置。 + +**功能**: +- 节点类型配置 +- 样式定义 +- 图标配置 +- 颜色主题 + +## 样式定制 + +### 节点样式 + +可以通过修改 `useNodeStyles.ts` 来自定义节点样式: + +```typescript +export function getNodeTypeConfig(type: string) { + const configs = { + start: { + label: '开始', + icon: 'play-circle', + color: '#10b981', + // 自定义样式 + }, + // 其他节点类型... + } + + return configs[type] || configs.default +} +``` + +### 边样式 + +边的样式在主组件中定义: + +```vue +:default-edge-options="{ + type: 'step', + style: { + stroke: '#6366f1', + strokeWidth: 2.5, + cursor: 'pointer', + filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))' + }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: '#6366f1', + width: 16, + height: 16, + }, +}" +``` + +## 数据格式 + +### 节点数据格式 + +```typescript +interface Node { + id: string + type: string + position: { x: number, y: number } + data: { + customLabel: string + [key: string]: any + } +} +``` + +### 边数据格式 + +```typescript +interface Edge { + id: string + source: string + target: string + sourceHandle?: string + targetHandle?: string + type?: string +} +``` + +## 最佳实践 + +### 1. 性能优化 +- 大量节点时考虑虚拟化 +- 合理使用缓存机制 +- 避免频繁的状态更新 + +### 2. 用户体验 +- 提供清晰的操作反馈 +- 支持键盘快捷键 +- 保持操作的直观性 + +### 3. 数据管理 +- 定期保存到服务器 +- 提供数据导入导出功能 +- 支持版本控制机制 + +## 扩展开发 + +### 添加新节点类型 + +1. 在 `useNodeStyles.ts` 中添加新类型配置 +2. 在 `Toolbar.vue` 中添加新节点按钮 +3. 在 `CustomNode.vue` 中添加新节点渲染逻辑 + +### 添加新功能 + +1. 创建新的 composable 函数 +2. 在主组件中集成新功能 +3. 更新工具栏和用户界面 + +## 故障排除 + +### 常见问题 + +1. **节点无法拖拽** + - 检查拖拽事件处理 + - 确认节点类型配置正确 + +2. **撤销重做不工作** + - 检查历史记录状态 + - 确认状态保存时机 + +3. **数据丢失** + - 检查缓存配置 + - 确认 localStorage 可用 + +### 调试技巧 + +1. 使用 Vue DevTools 查看组件状态 +2. 检查浏览器控制台错误 +3. 验证数据格式正确性 + +## 更新日志 + +### v1.0.0 (2024-01-15) +- ✨ 初始版本发布 +- 🎯 基础流程图编辑功能 +- 🔧 拖拽创建节点 +- 🔗 节点连接功能 +- ↩️ 撤销重做支持 +- 💾 本地缓存功能 + +### v1.1.0 (2024-01-20) +- 🎨 新增多种节点类型 +- ⌨️ 键盘快捷键支持 +- 🖱️ 优化拖拽体验 +- 📱 响应式布局改进 + +### v1.2.0 (2024-01-25) +- 🔧 模块化重构 +- 📦 组合式函数优化 +- 🎯 性能提升 +- 🐛 修复已知问题 + +## 许可证 + +MIT License diff --git a/docs/图表.md b/docs/图表.md index 1de026e..a6c1077 100644 --- a/docs/图表.md +++ b/docs/图表.md @@ -1,41 +1,216 @@ -📊 可以添加的图表类型 -1. 提交效率趋势图 (折线图) -数据来源: durationData 中的 submission_count / problem_count -展示内容: 每个时间段的提交效率(提交次数/完成题目数),值越接近1说明一次AC率越高 -价值: 反映刷题质量的提升 -2. 排名分布图 (直方图/箱线图) -数据来源: solved 数组中每道题的 rank 和 ac_count -展示内容: 用户解题排名的分布情况(如:前10%、10-30%、30-50%等区间的题目数量) -价值: 了解解题速度和竞争力 -3. 等级分布饼图/环形图 -数据来源: solved 数组中每道题的 grade -展示内容: S/A/B/C 各等级题目的数量和占比 -价值: 直观看出题目质量分布 -4. 标签雷达图 -数据来源: tags 对象 -展示内容: 多维度展示各类标签的掌握程度(可以归一化处理) -价值: 可视化知识点覆盖面 -5. 时间活跃度分析 (热力矩阵) -数据来源: solved 数组中的 ac_time -展示内容: 按星期几和时间段统计做题分布(如:工作日vs周末,早中晚时段) -价值: 了解学习习惯和时间规律 -6. 难度-等级关联散点图 -数据来源: solved 数组中的难度信息和 grade -展示内容: X轴为难度,Y轴为等级,每个点代表一道题 -价值: 分析在不同难度下的表现 -7. 做题加速度图 -数据来源: durationData -展示内容: 每个时间段完成题目数的变化率 -价值: 看出学习动力的变化趋势 -8. 竞赛题目占比 -数据来源: solved 数组中的 contest_id 和 contest_count -展示内容: 竞赛题 vs 常规题的数量对比 -价值: 了解竞赛参与情况 -9. 连续做题天数统计 -数据来源: heatmapData -展示内容: 最长连续做题天数、当前连续天数等 -价值: 激励持续学习 -10. 月度对比雷达图 -数据来源: durationData -展示内容: 多个维度(完成题目数、提交次数、等级、效率等)的月度对比 -价值: 全面评估进步情况 \ No newline at end of file +# 图表组件说明 + +基于 Chart.js 和 Vue-ChartJS 构建的数据可视化组件库,为 OJ Next 项目提供丰富的图表展示功能。 + +## 技术栈 + +- **Chart.js** - 强大的图表库,支持多种图表类型 +- **Vue-ChartJS** - Chart.js 的 Vue 3 封装 +- **Vue 3 Composition API** - 现代化的组件开发方式 +- **TypeScript** - 完整的类型支持 + +## 现有图表组件 + +### 1. DurationChart 混合图表 +**文件位置**: `src/oj/ai/components/DurationChart.vue` + +**功能描述**: 展示用户学习进度的时间趋势,结合柱状图和折线图。 + +**数据来源**: `durationData` API 接口 +- 每周/每月的综合情况 +- 题目数、提交数、等级变化 + +**图表类型**: +- 柱状图:完成题目数 +- 折线图:提交次数、等级变化 + +**使用场景**: AI分析页面的主要图表 + +### 2. Heatmap 热力图 +**文件位置**: `src/oj/ai/components/Heatmap.vue` + +**功能描述**: 展示用户的提交活跃度分布。 + +**数据来源**: `heatmapData` API 接口 +- 按日期统计提交数 +- 可视化用户活跃度 + +**图表类型**: 热力图矩阵 + +**使用场景**: 展示学习习惯和时间规律 + +### 3. Details 详细数据 +**文件位置**: `src/oj/ai/components/Details.vue` + +**功能描述**: 展示用户详细的学习数据。 + +**数据来源**: `detailsData` API 接口 +- 用户等级、已解决题目列表 +- 标签统计、难度统计 +- 参赛次数等 + +**展示方式**: 数据表格和统计卡片 + +## 可扩展的图表类型 + +### 1. 提交效率趋势图 (折线图) +**数据来源**: `durationData` 中的 `submission_count / problem_count` +**展示内容**: 每个时间段的提交效率(提交次数/完成题目数) +**价值**: 反映刷题质量的提升,值越接近1说明一次AC率越高 + +### 2. 排名分布图 (直方图/箱线图) +**数据来源**: `solved` 数组中每道题的 `rank` 和 `ac_count` +**展示内容**: 用户解题排名的分布情况(如:前10%、10-30%、30-50%等区间的题目数量) +**价值**: 了解解题速度和竞争力 + +### 3. 等级分布饼图/环形图 +**数据来源**: `solved` 数组中每道题的 `grade` +**展示内容**: S/A/B/C 各等级题目的数量和占比 +**价值**: 直观看出题目质量分布 + +### 4. 标签雷达图 +**数据来源**: `tags` 对象 +**展示内容**: 多维度展示各类标签的掌握程度(可以归一化处理) +**价值**: 可视化知识点覆盖面 + +### 5. 时间活跃度分析 (热力矩阵) +**数据来源**: `solved` 数组中的 `ac_time` +**展示内容**: 按星期几和时间段统计做题分布(如:工作日vs周末,早中晚时段) +**价值**: 了解学习习惯和时间规律 + +### 6. 难度-等级关联散点图 +**数据来源**: `solved` 数组中的难度信息和 `grade` +**展示内容**: X轴为难度,Y轴为等级,每个点代表一道题 +**价值**: 分析在不同难度下的表现 + +### 7. 做题加速度图 +**数据来源**: `durationData` +**展示内容**: 每个时间段完成题目数的变化率 +**价值**: 看出学习动力的变化趋势 + +### 8. 竞赛题目占比 +**数据来源**: `solved` 数组中的 `contest_id` 和 `contest_count` +**展示内容**: 竞赛题 vs 常规题的数量对比 +**价值**: 了解竞赛参与情况 + +### 9. 连续做题天数统计 +**数据来源**: `heatmapData` +**展示内容**: 最长连续做题天数、当前连续天数等 +**价值**: 激励持续学习 + +### 10. 月度对比雷达图 +**数据来源**: `durationData` +**展示内容**: 多个维度(完成题目数、提交次数、等级、效率等)的月度对比 +**价值**: 全面评估进步情况 + +## 组件开发指南 + +### 创建新图表组件 + +```vue + + + + + + + +``` + +### 图表样式定制 + +```typescript +// 主题配置 +const theme = { + colors: { + primary: '#6366f1', + secondary: '#8b5cf6', + success: '#10b981', + warning: '#f59e0b', + error: '#ef4444' + }, + gradients: { + primary: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + success: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)' + } +} +``` + +## 性能优化 + +### 1. 数据懒加载 +- 按需加载图表数据 +- 使用虚拟滚动处理大量数据 + +### 2. 图表缓存 +- 缓存计算结果 +- 避免重复渲染 + +### 3. 响应式设计 +- 自适应容器大小 +- 移动端优化 + +## 最佳实践 + +### 1. 数据预处理 +- 在组件外部处理数据 +- 使用 computed 属性缓存计算结果 + +### 2. 错误处理 +- 添加数据验证 +- 提供降级方案 + +### 3. 用户体验 +- 添加加载状态 +- 提供交互反馈 + +## 更新日志 + +### v1.0.0 (2024-01-15) +- ✨ 初始版本发布 +- 📊 基础图表组件 +- 🎨 主题样式支持 +- 📱 响应式设计 + +### v1.1.0 (2024-01-20) +- 🔧 性能优化 +- 📈 新增混合图表 +- 🎯 交互体验改进 + +## 许可证 + +MIT License \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7aaf0c1..54edc2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,12 @@ "dependencies": { "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-python": "^6.2.1", + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.47.0", + "@vue-flow/minimap": "^1.5.4", + "@vue-flow/node-resizer": "^1.5.0", + "@vue-flow/node-toolbar": "^1.1.1", "@vueuse/core": "^13.9.0", "@vueuse/router": "^13.9.0", "@wangeditor-next/editor": "^5.6.46", @@ -1722,6 +1728,175 @@ "integrity": "sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==", "license": "MIT" }, + "node_modules/@vue-flow/background": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vue-flow/background/-/background-1.3.2.tgz", + "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/controls": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz", + "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.47.0.tgz", + "integrity": "sha512-w+qrm/xjQP5NUeKUOMIbQvpOeivTbGZtY2lGffK5kHiN3ZLyEazhESc8OeIV9NZkK2T5DIeyX/nhHxCC45HLiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vueuse/core": "^10.5.0", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vue-flow/minimap": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@vue-flow/minimap/-/minimap-1.5.4.tgz", + "integrity": "sha512-l4C+XTAXnRxsRpUdN7cAVFBennC1sVRzq4bDSpVK+ag7tdMczAnhFYGgbLkUw3v3sY6gokyWwMl8CDonp8eB2g==", + "license": "MIT", + "dependencies": { + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/node-resizer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue-flow/node-resizer/-/node-resizer-1.5.0.tgz", + "integrity": "sha512-FmvOZ6+yVrBEf+8oJcCU20PUZ105QsyM01iiP4vTKHGJ01hzoh9d0/wP9iJkxkIpvBU59CyOHyTKQZlDr4qDhA==", + "license": "MIT", + "dependencies": { + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0" + }, + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/node-toolbar": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vue-flow/node-toolbar/-/node-toolbar-1.1.1.tgz", + "integrity": "sha512-vgSnGMLd3FXstdpvC721qcBDzGkPsudmyA8urEP55/EMikCp59klgzPvVULTDxxsew5MdXtTBCumsqOi+PXJdg==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.22", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz", diff --git a/package.json b/package.json index 96be0aa..0f89d69 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,12 @@ "dependencies": { "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-python": "^6.2.1", + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.47.0", + "@vue-flow/minimap": "^1.5.4", + "@vue-flow/node-resizer": "^1.5.0", + "@vue-flow/node-toolbar": "^1.1.1", "@vueuse/core": "^13.9.0", "@vueuse/router": "^13.9.0", "@wangeditor-next/editor": "^5.6.46", diff --git a/src/oj/problem/components/ProblemEditor.vue b/src/oj/problem/components/ProblemEditor.vue index 4c9a60a..58ccc47 100644 --- a/src/oj/problem/components/ProblemEditor.vue +++ b/src/oj/problem/components/ProblemEditor.vue @@ -16,6 +16,7 @@ const FlowchartEditor = defineAsyncComponent( const route = useRoute() const formRef = useTemplateRef>("formRef") +const flowchartEditorRef = useTemplateRef("flowchartEditorRef") const codeStore = useCodeStore() const problemStore = useProblemStore() @@ -78,6 +79,9 @@ const handleSyncStatusChange = (status: { }) => { syncStatus.setOtherUser(status.otherUser) } + +// 提供FlowchartEditor的ref给子组件 +provide('flowchartEditorRef', flowchartEditorRef) @@ -89,7 +93,7 @@ const handleSyncStatusChange = (status: { @change-language="changeLanguage" @toggle-sync="toggleSync" /> - + ("flowchartEditorRef") + +// 将流程图JSON数据转换为Mermaid格式 +const convertToMermaid = (flowchartData: any) => { + const { nodes, edges } = flowchartData + + if (!nodes || nodes.length === 0) { + return "graph TD\n A[空流程图]" + } + + let mermaid = "graph TD\n" + + // 处理节点 - 根据原始类型和自定义标签 + nodes.forEach((node: any) => { + const nodeId = node.id + const label = node.data?.customLabel || node.data?.label || "节点" + const originalType = node.data?.originalType || node.type + + // 根据节点原始类型确定Mermaid语法 + switch (originalType) { + case "start": + mermaid += ` ${nodeId}[${label}]\n` + break + case "end": + mermaid += ` ${nodeId}[${label}]\n` + break + case "input": + mermaid += ` ${nodeId}[${label}]\n` + break + case "output": + mermaid += ` ${nodeId}[${label}]\n` + break + case "default": + mermaid += ` ${nodeId}[${label}]\n` + break + case "decision": + mermaid += ` ${nodeId}{${label}}\n` + break + case "loop": + mermaid += ` ${nodeId}[${label}]\n` + break + default: + mermaid += ` ${nodeId}[${label}]\n` + } + }) + + // 处理边 + edges.forEach((edge: any) => { + const source = edge.source + const target = edge.target + const label = edge.label || edge.data?.label || "" + + if (label) { + mermaid += ` ${source} -->|${label}| ${target}\n` + } else { + mermaid += ` ${source} --> ${target}\n` + } + }) + + return mermaid +} + const submit = () => { - message.warning("暂未实现") + if (!flowchartEditorRef?.value) return + // 获取流程图的JSON数据 + const flowchartData = flowchartEditorRef.value.getFlowchartData() + + if (flowchartData.nodes.length === 0 || flowchartData.edges.length === 0) { + message.error("流程图节点或边不能为空") + return + } + + // 打印JSON数据到控制台 + console.log("流程图JSON数据:", JSON.stringify(flowchartData, null, 2)) + + // 转换为Mermaid格式 + const mermaidCode = convertToMermaid(flowchartData) + console.log("Mermaid代码:") + console.log(mermaidCode) + + // 显示成功消息 + message.success("敬请期待,快了~") } diff --git a/src/shared/components/FlowchartEditor/CustomNode.vue b/src/shared/components/FlowchartEditor/CustomNode.vue new file mode 100644 index 0000000..57efc5b --- /dev/null +++ b/src/shared/components/FlowchartEditor/CustomNode.vue @@ -0,0 +1,276 @@ + + + + + + + + + {{ displayLabel }} + + + + + + {{ displayLabel }} + + + + + + + + + + diff --git a/src/shared/components/FlowchartEditor/NodeActions.vue b/src/shared/components/FlowchartEditor/NodeActions.vue new file mode 100644 index 0000000..329bbc0 --- /dev/null +++ b/src/shared/components/FlowchartEditor/NodeActions.vue @@ -0,0 +1,58 @@ + + + + + + + + + + + + + diff --git a/src/shared/components/FlowchartEditor/NodeHandles.vue b/src/shared/components/FlowchartEditor/NodeHandles.vue new file mode 100644 index 0000000..d1dc33c --- /dev/null +++ b/src/shared/components/FlowchartEditor/NodeHandles.vue @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + 是 + 否 + + + + + + + + + + + + + + + + + 进入 + 返回 + 继续 + 退出 + + + + + + + + + + + + + diff --git a/src/shared/components/FlowchartEditor/Toolbar.vue b/src/shared/components/FlowchartEditor/Toolbar.vue new file mode 100644 index 0000000..e5078bc --- /dev/null +++ b/src/shared/components/FlowchartEditor/Toolbar.vue @@ -0,0 +1,398 @@ + + + + + + + 节点库 + + ⏳ + ● + ✔ + + + 拖拽节点到画布中 + + + + + + + + {{ nodeType.icon }} + + + {{ nodeType.label }} + {{ nodeType.description }} + + + + + + + + + ↶ + 撤销 + + + ↷ + 重做 + + + + 🗑️ + 清空画布 + + + + + + + diff --git a/src/shared/components/FlowchartEditor/index.vue b/src/shared/components/FlowchartEditor/index.vue index 537bc68..164ae84 100644 --- a/src/shared/components/FlowchartEditor/index.vue +++ b/src/shared/components/FlowchartEditor/index.vue @@ -1,14 +1,221 @@ - 可拖拽的流程图编辑器 + + + + + + + + + + + + + + + + + + diff --git a/src/shared/components/FlowchartEditor/useCache.ts b/src/shared/components/FlowchartEditor/useCache.ts new file mode 100644 index 0000000..4a2c4e6 --- /dev/null +++ b/src/shared/components/FlowchartEditor/useCache.ts @@ -0,0 +1,83 @@ +import { ref, watch } from 'vue' +import { useStorage, useDebounceFn } from '@vueuse/core' +import type { Node, Edge } from '@vue-flow/core' + +/** + * 缓存管理 - 使用 @vueuse 的 useStorage + */ +export function useCache( + nodes: any, + edges: any, + storageKey: string = 'flowchart-editor-data' +) { + const isSaving = ref(false) + const lastSaved = ref(null) + const hasUnsavedChanges = ref(false) + + // 使用 useStorage 管理数据存储 + const storedData = useStorage<{ + nodes: Node[] + edges: Edge[] + timestamp: string + }>(storageKey, { + nodes: [], + edges: [], + timestamp: '' + }) + + // 防抖保存 + const debouncedSave = useDebounceFn(() => { + isSaving.value = true + storedData.value.nodes = nodes.value + storedData.value.edges = edges.value + storedData.value.timestamp = new Date().toISOString() + lastSaved.value = new Date() + hasUnsavedChanges.value = false + isSaving.value = false + }, 500) + + // 立即保存 + const saveToCache = () => { + isSaving.value = true + storedData.value.nodes = nodes.value + storedData.value.edges = edges.value + storedData.value.timestamp = new Date().toISOString() + lastSaved.value = new Date() + hasUnsavedChanges.value = false + isSaving.value = false + } + + // 从缓存加载数据 + const loadFromCache = () => { + if (storedData.value.nodes?.length || storedData.value.edges?.length) { + nodes.value = storedData.value.nodes + edges.value = storedData.value.edges + lastSaved.value = storedData.value.timestamp ? new Date(storedData.value.timestamp) : null + hasUnsavedChanges.value = false + return true + } + return false + } + + // 清除缓存数据 + const clearCache = () => { + storedData.value = { nodes: [], edges: [], timestamp: '' } + lastSaved.value = null + hasUnsavedChanges.value = false + } + + // 监听节点和边的变化 + watch([nodes, edges], () => { + hasUnsavedChanges.value = true + debouncedSave() + }, { deep: true }) + + return { + isSaving, + lastSaved, + hasUnsavedChanges, + saveToCache, + loadFromCache, + clearCache + } +} diff --git a/src/shared/components/FlowchartEditor/useDnD.ts b/src/shared/components/FlowchartEditor/useDnD.ts new file mode 100644 index 0000000..cb650af --- /dev/null +++ b/src/shared/components/FlowchartEditor/useDnD.ts @@ -0,0 +1,71 @@ +import { ref } from 'vue' +import { useVueFlow } from "@vue-flow/core" +import { nanoid } from "nanoid" +import { getNodeTypeConfig, createNodeStyle, getNodeDimensions } from "./useNodeStyles" + +/** + * 简化的拖拽处理 + */ +export function useDnD() { + const { addNodes, screenToFlowCoordinate } = useVueFlow() + const isDragOver = ref(false) + + // 拖拽悬停处理 + const onDragOver = (event: DragEvent) => { + event.preventDefault() + isDragOver.value = true + } + + // 拖拽离开处理 + const onDragLeave = () => { + isDragOver.value = false + } + + // 拖拽放置处理 + const onDrop = (event: DragEvent) => { + event.preventDefault() + isDragOver.value = false + + const type = event.dataTransfer?.getData("application/vueflow") + if (!type) return + + // 获取鼠标在画布中的坐标 + const position = screenToFlowCoordinate({ + x: event.clientX, + y: event.clientY, + }) + + // 根据节点类型获取实际尺寸 + const dimensions = getNodeDimensions(type) + + // 调整位置,使节点中心点对齐到鼠标位置 + const adjustedPosition = { + x: position.x - dimensions.width / 2, + y: position.y - dimensions.height / 2 + } + + const nodeId = `node-${nanoid()}` + const config = getNodeTypeConfig(type) + const newNode = { + id: nodeId, + type: 'custom', + position: adjustedPosition, + data: { + label: config.label, + color: config.color, + originalType: type + }, + style: createNodeStyle(type) + } + + addNodes([newNode]) + return newNode + } + + return { + isDragOver, + onDragOver, + onDragLeave, + onDrop + } +} diff --git a/src/shared/components/FlowchartEditor/useFlowOperations.ts b/src/shared/components/FlowchartEditor/useFlowOperations.ts new file mode 100644 index 0000000..05c157a --- /dev/null +++ b/src/shared/components/FlowchartEditor/useFlowOperations.ts @@ -0,0 +1,107 @@ +import { nanoid } from "nanoid" +import type { Ref } from 'vue' +import type { Node, Edge } from '@vue-flow/core' + +/** + * 简化的流程操作 + */ +export function useFlowOperations( + nodes: Ref, + edges: Ref, + addNodes: (nodes: Node[]) => void, + addEdges: (edges: Edge[]) => void, + removeNodes: (nodeIds: string[]) => void, + removeEdges: (edgeIds: string[]) => void, + saveState: (nodes: Node[], edges: Edge[]) => void +) { + // 连接处理 + const handleConnect = (params: any) => { + const newEdge: Edge = { + id: `edge-${nanoid()}`, + source: params.source, + target: params.target, + sourceHandle: params.sourceHandle, + targetHandle: params.targetHandle, + type: 'default' + } + + addEdges([newEdge]) + saveState(nodes.value, edges.value) + } + + // 边点击删除 + const handleEdgeClick = (event: any) => { + removeEdges([event.edge.id]) + saveState(nodes.value, edges.value) + } + + // 节点删除 + const handleNodeDelete = (nodeId: string) => { + // 删除相关边 + const relatedEdges = edges.value.filter(edge => + edge.source === nodeId || edge.target === nodeId + ) + if (relatedEdges.length > 0) { + removeEdges(relatedEdges.map(edge => edge.id)) + } + + removeNodes([nodeId]) + saveState(nodes.value, edges.value) + } + + // 节点更新 + const handleNodeUpdate = (nodeId: string, newLabel: string) => { + const nodeIndex = nodes.value.findIndex(node => node.id === nodeId) + + if (nodeIndex !== -1) { + const oldNode = nodes.value[nodeIndex] + + // 创建新的节点对象以确保响应式更新 + const updatedNode = { + ...oldNode, + data: { + ...oldNode.data, + customLabel: newLabel + } + } + + // 使用 Vue Flow 的更新方法 + nodes.value[nodeIndex] = updatedNode + + // 强制触发响应式更新 + nodes.value = [...nodes.value] + + saveState(nodes.value, edges.value) + } + } + + // 清空画布 + const clearCanvas = () => { + nodes.value = [] + edges.value = [] + saveState(nodes.value, edges.value) + } + + // 删除选中的节点和边 + const deleteSelected = () => { + const selectedNodes = nodes.value.filter(node => (node as any).selected) + const selectedEdges = edges.value.filter(edge => (edge as any).selected) + + if (selectedNodes.length > 0) { + removeNodes(selectedNodes.map(node => node.id)) + } + if (selectedEdges.length > 0) { + removeEdges(selectedEdges.map(edge => edge.id)) + } + saveState(nodes.value, edges.value) + } + + return { + handleConnect, + handleEdgeClick, + handleNodeDelete, + handleNodeUpdate, + clearCanvas, + deleteSelected + } +} diff --git a/src/shared/components/FlowchartEditor/useHistory.ts b/src/shared/components/FlowchartEditor/useHistory.ts new file mode 100644 index 0000000..bb52a02 --- /dev/null +++ b/src/shared/components/FlowchartEditor/useHistory.ts @@ -0,0 +1,63 @@ +import { ref, computed } from 'vue' +import type { Node, Edge } from '@vue-flow/core' + +/** + * 简化的历史记录管理 + */ +export function useHistory() { + const history = ref<{ nodes: Node[], edges: Edge[] }[]>([]) + const historyIndex = ref(-1) + + // 是否可以撤销 + const canUndo = computed(() => historyIndex.value > 0) + + // 是否可以重做 + const canRedo = computed(() => historyIndex.value < history.value.length - 1) + + // 保存状态到历史记录 + const saveState = (nodes: Node[], edges: Edge[]) => { + const currentState = { nodes: [...nodes], edges: [...edges] } + + // 如果当前不在历史记录的末尾,删除后面的记录 + if (historyIndex.value < history.value.length - 1) { + history.value = history.value.slice(0, historyIndex.value + 1) + } + + history.value.push(currentState) + historyIndex.value = history.value.length - 1 + + // 限制历史记录数量 + if (history.value.length > 20) { + history.value.shift() + historyIndex.value-- + } + } + + // 撤销 + const undo = () => { + if (canUndo.value) { + historyIndex.value-- + const state = history.value[historyIndex.value] + return state + } + return null + } + + // 重做 + const redo = () => { + if (canRedo.value) { + historyIndex.value++ + const state = history.value[historyIndex.value] + return state + } + return null + } + + return { + canUndo, + canRedo, + saveState, + undo, + redo + } +} diff --git a/src/shared/components/FlowchartEditor/useNodeStyles.ts b/src/shared/components/FlowchartEditor/useNodeStyles.ts new file mode 100644 index 0000000..45e2114 --- /dev/null +++ b/src/shared/components/FlowchartEditor/useNodeStyles.ts @@ -0,0 +1,137 @@ +/** + * 节点样式管理 + */ + +/** + * 获取节点类型配置 + */ +export function getNodeTypeConfig(type: string) { + const configs: Record< + string, + { label: string; color: string; icon: string; description: string } + > = { + start: { + label: "开始", + color: "#10b981", + icon: "▶", + description: "流程开始", + }, + input: { + label: "输入", + color: "#06b6d4", + icon: "📥", + description: "数据输入", + }, + default: { + label: "赋值", + color: "#3b82f6", + icon: "⚙", + description: "赋值语句", + }, + decision: { + label: "判断", + color: "#f59e0b", + icon: "❓", + description: "条件语句", + }, + loop: { + label: "循环", + color: "#8b5cf6", + icon: "🔄", + description: "循环语句", + }, + output: { + label: "输出", + color: "#84cc16", + icon: "📤", + description: "数据输出", + }, + end: { + label: "结束", + color: "#ef4444", + icon: "⏹", + description: "流程结束", + }, + } + return ( + configs[type] || { + label: "节点", + color: "#6b7280", + icon: "⚪", + description: "未知节点", + } + ) +} + +/** + * 获取节点样式 + */ +export function getNodeStyle(type: string, color: string) { + const baseStyle = { + background: color, + color: "white", + border: `2px solid ${color}`, + borderRadius: "10px", + fontSize: "16px", + fontWeight: "500", + width: "auto", // 自动宽度 + height: "auto", // 自动高度 + minWidth: "100px", + minHeight: "40px", + maxWidth: "400px", + maxHeight: "160px", + display: "flex", + alignItems: "center", + justifyContent: "center", + boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)", + } + + // 根据节点类型调整样式 + switch (type) { + case "start": + case "end": + return { + ...baseStyle, + } + case "decision": + return { + ...baseStyle, + borderRadius: "8px", + minWidth: "140px", + minHeight: "50px", + } + case "loop": + return { + ...baseStyle, + borderRadius: "8px", + minWidth: "140px", + minHeight: "50px", + } + default: + return baseStyle + } +} + +/** + * 创建节点样式 + */ +export function createNodeStyle(type: string) { + const config = getNodeTypeConfig(type) + return getNodeStyle(type, config.color) +} + +/** + * 获取节点尺寸 + */ +export function getNodeDimensions(type: string) { + switch (type) { + case "start": + case "end": + return { width: 100, height: 40 } + case "decision": + case "loop": + return { width: 140, height: 50 } + default: + return { width: 120, height: 40 } + } +}
拖拽节点到画布中