mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-01-23 22:43:28 +00:00
temp
This commit is contained in:
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
||||||
27
.serena/memories/project_overview.md
Normal file
27
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 项目概览(yys-editor)
|
||||||
|
|
||||||
|
目标与定位
|
||||||
|
- yys-editor 是一个基于浏览器的可视化编辑器,用于拖拽排布素材并生成展示效果(围绕式神/御魂等要素)。
|
||||||
|
- 适合快速制作、调整与导出展示图(如阵容/排行等)。
|
||||||
|
|
||||||
|
技术要点
|
||||||
|
- Vue 3 + Vite + Element Plus + Pinia + vue-i18n
|
||||||
|
- 入口:`index.html` -> `/src/main.js` -> `App.vue`
|
||||||
|
- 路径别名:`@` -> `src/`
|
||||||
|
- 资源:大量静态图片位于 `public/assets/`
|
||||||
|
- 语言:中文、日文(从浏览器语言推断,fallback 为 `zh`)
|
||||||
|
|
||||||
|
当前现状
|
||||||
|
- 包管理器:npm(存在 `package-lock.json`)
|
||||||
|
- 脚本:`dev`/`build`/`preview`/`lint`/`format`
|
||||||
|
- Lint/Format:ESLint + Prettier(无项目级 Prettier 配置文件,使用默认)
|
||||||
|
- 测试:未配置自动化测试
|
||||||
|
|
||||||
|
典型编辑流程
|
||||||
|
1) `npm install`
|
||||||
|
2) `npm run dev` 在 http://localhost:5173/ 进行开发与手动验收
|
||||||
|
3) `npm run lint` + `npm run format` 保持风格统一
|
||||||
|
4) `npm run build` 产出 `dist/`,`npm run preview` 进行生产包预览
|
||||||
|
|
||||||
|
其它
|
||||||
|
- 状态存储使用 Pinia,并通过 `localStorage` 自动保存;应用启动时支持恢复上次未保存内容。
|
||||||
30
.serena/memories/project_structure.md
Normal file
30
.serena/memories/project_structure.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# yys-editor 项目结构(概要)
|
||||||
|
|
||||||
|
根目录
|
||||||
|
- `index.html`:Vite 入口 HTML,挂载点 `#app`,引入 `/src/main.js`。
|
||||||
|
- `package.json` / `package-lock.json`:npm 包与脚本;包管理器为 npm。
|
||||||
|
- `vite.config.js`:Vite 配置,`@` -> `src/`。
|
||||||
|
- `jsconfig.json`:编辑器路径提示(`@/*` -> `./src/*`)。
|
||||||
|
- `.gitignore`:忽略 `node_modules/`、`dist/` 等。
|
||||||
|
- `.vscode/`:推荐扩展(Volar)。
|
||||||
|
- `README.md`:项目说明(中文)。
|
||||||
|
- `public/`:静态资源目录(大量图片素材:`assets/Shikigami`, `assets/Yuhun` 等)。
|
||||||
|
|
||||||
|
`src/`
|
||||||
|
- `main.js`:应用入口,注册 Element Plus、Icons、vue-i18n、Pinia、vue3-draggable-resizable;挂载 `App.vue`。
|
||||||
|
- `App.vue`:主布局(工具栏、侧边栏、工作区 Tab),根据文件类型切换主要编辑视图。
|
||||||
|
- `components/`:
|
||||||
|
- 核心:`Yys.vue`, `YysRank.vue`, `Toolbar.vue`, `ProjectExplorer.vue` 等
|
||||||
|
- 基础:`ShikigamiSelect.vue`, `YuhunSelect.vue`, `Watermark.vue`, `HelloWorld.vue` 等
|
||||||
|
- `components/icons/`:若干图标组件
|
||||||
|
- `assets/`:基础样式 `base.css`, `main.css` 与 logo 等
|
||||||
|
- `data/`:若干 JSON 数据(如 `Shikigami.json`, `Yuhun.json`, `property.json`, `updateLog.json`)
|
||||||
|
- `locales/`:多语言资源 `zh.json` 与 `ja.json`
|
||||||
|
- `ts/`:脚本与 store
|
||||||
|
- `files.ts`:Pinia store(文件页签、可见性、删除/重命名;含 `localStorage` 自动保存与启动恢复提示)
|
||||||
|
- `useGlobalMessage.ts`:全局消息(Element Plus)
|
||||||
|
- `types/`:类型定义(如后续扩展)
|
||||||
|
|
||||||
|
说明
|
||||||
|
- 未见 `router` 相关文件;当前为单页多区域布局。
|
||||||
|
- 构建产物输出到 `dist/`(`npm run build`)。
|
||||||
33
.serena/memories/style_and_conventions.md
Normal file
33
.serena/memories/style_and_conventions.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# yys-editor 技术栈与风格约定
|
||||||
|
|
||||||
|
技术栈
|
||||||
|
- 前端框架:Vue 3(Composition API,部分 SFC 使用 `<script setup lang="ts">`)
|
||||||
|
- 构建与开发:Vite
|
||||||
|
- UI 组件:Element Plus(含 `@element-plus/icons-vue`)
|
||||||
|
- 状态管理:Pinia(示例:`src/ts/files.ts` 存储文件页签等,含本地存储自动保存)
|
||||||
|
- 国际化:vue-i18n(`src/locales/zh.json` + `src/locales/ja.json`,默认从浏览器语言推断,fallback 为 `zh`)
|
||||||
|
- 拖拽缩放:vue3-draggable-resizable
|
||||||
|
- 其它依赖:`@vueup/vue-quill`(如后续使用富文本)/ `html2canvas` / `vuedraggable` 等
|
||||||
|
|
||||||
|
工程与路径
|
||||||
|
- 别名:`@` 指向 `src/`(见 `vite.config.js` 与 `jsconfig.json`)。
|
||||||
|
- 入口:`index.html` -> `/src/main.js` -> `App.vue`。
|
||||||
|
- 静态资源:`public/`(大量图片素材位于 `public/assets/...`)。
|
||||||
|
|
||||||
|
代码风格
|
||||||
|
- 代码格式:Prettier(未见项目级配置,使用默认规则;脚本仅格式化 `src/`)。
|
||||||
|
- 语法检查:ESLint(`eslint-plugin-vue` + `@vue/eslint-config-prettier`;脚本已启用 `--fix`)。
|
||||||
|
- 组件命名:PascalCase 单文件组件(`.vue`),按功能归档于 `src/components/`。
|
||||||
|
- 类型:以 JS 为主,SFC 中可使用 TS(经 Vite/ESBuild 处理,无专门 `tsconfig.json`,有 `jsconfig.json` 路径提示)。
|
||||||
|
- 国际化:新增/修改文案时,请同步维护 `zh.json` 与 `ja.json`,避免硬编码 UI 文案。
|
||||||
|
- 状态与持久化:Pinia store 通过 `localStorage` 自动保存,注意 key 兼容;涉及 schema 变更时考虑迁移逻辑。
|
||||||
|
|
||||||
|
样式
|
||||||
|
- 常规样式位于 `src/assets/*.css`,组件中常用 `<style scoped>` 以局部隔离。
|
||||||
|
- UI 主题由 Element Plus 提供,如需自定义主题请统一约定变量来源。
|
||||||
|
|
||||||
|
提交与约定
|
||||||
|
- 未发现提交规范配置(如 commitlint/husky),建议信息清晰、原子化提交。必要时中英文均可,但保持一致性。
|
||||||
|
|
||||||
|
测试
|
||||||
|
- 未配置自动化测试框架;变更请重点进行手动验收(参见 `task_completion_checklist.md`)。
|
||||||
26
.serena/memories/suggested_commands.md
Normal file
26
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# yys-editor 常用命令(Windows/PowerShell)
|
||||||
|
|
||||||
|
- 安装依赖:
|
||||||
|
- `npm install`
|
||||||
|
- 本地开发(Vite 开发服务器,默认 http://localhost:5173/):
|
||||||
|
- `npm run dev`
|
||||||
|
- 生产构建(输出到 `dist/`):
|
||||||
|
- `npm run build`
|
||||||
|
- 本地预览生产包:
|
||||||
|
- `npm run preview`
|
||||||
|
- 代码检查(ESLint,自动修复):
|
||||||
|
- `npm run lint`
|
||||||
|
- 代码格式化(Prettier,仅 `src/` 目录):
|
||||||
|
- `npm run format`
|
||||||
|
|
||||||
|
辅助命令(PowerShell 常用):
|
||||||
|
- 进入目录/查看/读取文件:`cd`, `ls` (Get-ChildItem), `cat file` (Get-Content -Raw)
|
||||||
|
- 删除/复制/移动:`rm -r -Force path` (Remove-Item), `cp src dst` (Copy-Item), `mv src dst` (Move-Item)
|
||||||
|
- Git:`git status`, `git add -p`, `git commit -m "msg"`, `git switch -c <branch>`, `git push`
|
||||||
|
- 进程占用端口排查(可选):`Get-Process`、`netstat -ano | findstr 5173`
|
||||||
|
- 快速全文搜索(若已安装):`rg "pattern"`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 包管理器使用 npm(见 `package-lock.json`)。
|
||||||
|
- Vite 默认端口 5173,未在 `vite.config.js` 中覆写。
|
||||||
|
- 项目未配置自动化测试命令。
|
||||||
22
.serena/memories/task_completion_checklist.md
Normal file
22
.serena/memories/task_completion_checklist.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 任务完成前检查清单(yys-editor)
|
||||||
|
|
||||||
|
功能与兼容
|
||||||
|
- 本地自测通过:`npm run dev` 无报错、关键路径可用(切换 Tab、新增/删除文件、编辑视图渲染、国际化切换等)。
|
||||||
|
- UI 走查:常用分辨率下布局正常(Toolbar、侧边栏、工作区滚动)。
|
||||||
|
- 资源校验:新增素材放入 `public/assets/...`,路径正确;体积适中(尽量压缩)。
|
||||||
|
- i18n:新增/修改的 UI 文案同步维护 `src/locales/zh.json` 与 `src/locales/ja.json`。
|
||||||
|
|
||||||
|
代码质量
|
||||||
|
- 语法检查:`npm run lint`(包含 `--fix` 自动修复)。
|
||||||
|
- 代码格式:`npm run format`(当前仅格式化 `src/` 目录)。
|
||||||
|
- 引用规范:优先使用 `@/...` 别名;避免相对路径层级过深。
|
||||||
|
- 状态与持久化:如更改 `files` store 结构,评估 `localStorage` 兼容/迁移策略。
|
||||||
|
|
||||||
|
构建与预览
|
||||||
|
- 生产构建:`npm run build` 成功,产物在 `dist/`。
|
||||||
|
- 生产预览:`npm run preview` 本地检查基础路由与功能。
|
||||||
|
|
||||||
|
提交
|
||||||
|
- 更新文档(如需):`README.md` 或注释。
|
||||||
|
- Git 提交清晰、原子:`git add -p`、`git commit -m "feat/fix: ..."`。
|
||||||
|
- 如涉及较大静态资源,建议说明来源与 License。
|
||||||
84
.serena/project.yml
Normal file
84
.serena/project.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# list of languages for which language servers are started; choose from:
|
||||||
|
# al bash clojure cpp csharp csharp_omnisharp
|
||||||
|
# dart elixir elm erlang fortran go
|
||||||
|
# haskell java julia kotlin lua markdown
|
||||||
|
# nix perl php python python_jedi r
|
||||||
|
# rego ruby ruby_solargraph rust scala swift
|
||||||
|
# terraform typescript typescript_vts yaml zig
|
||||||
|
# Note:
|
||||||
|
# - For C, use cpp
|
||||||
|
# - For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||||
|
# The first language is the default language and the respective language server will be used as a fallback.
|
||||||
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
|
languages:
|
||||||
|
- vue
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "yys-editor"
|
||||||
|
included_optional_tools: []
|
||||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/Thumbs.db": true,
|
||||||
|
".mule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
26
AGENTS.md
Normal file
26
AGENTS.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## Documentation Lookup
|
||||||
|
|
||||||
|
When documentation lookup is required:
|
||||||
|
- The agent should use Context7 as the primary source of truth.
|
||||||
|
- Do not rely on assumptions if documentation can be retrieved via Context7.
|
||||||
|
|
||||||
|
## Progress Update Rules
|
||||||
|
|
||||||
|
The project progress is tracked in:
|
||||||
|
- `/docs/1management/plan.md`
|
||||||
|
|
||||||
|
When the user explicitly states that a task or step is completed (e.g. “当前任务完成”, “这一步做完了”, “可以更新进度了”):
|
||||||
|
|
||||||
|
The agent MUST:
|
||||||
|
1. Identify the corresponding step or module in `plan.md`.
|
||||||
|
2. Update the completion status or percentage of that specific step.
|
||||||
|
3. Recalculate and update the overall progress to reflect this change.
|
||||||
|
4. Keep all other progress entries unchanged.
|
||||||
|
|
||||||
|
The agent MUST NOT:
|
||||||
|
- Update progress proactively without explicit confirmation from the user.
|
||||||
|
- Guess completion status or infer progress implicitly.
|
||||||
|
- Modify unrelated sections in `plan.md`.
|
||||||
|
|
||||||
|
If the mapping between the completed task and `plan.md` is ambiguous:
|
||||||
|
- Ask for clarification before making any changes.
|
||||||
@@ -56,12 +56,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import LogicFlow, { EventType } from '@logicflow/core';
|
import LogicFlow, { EventType } from '@logicflow/core';
|
||||||
import type { Position, NodeData, EdgeData, BaseNodeModel, GraphModel } from '@logicflow/core';
|
import type { Position, NodeData, EdgeData, BaseNodeModel, GraphModel, GraphData } from '@logicflow/core';
|
||||||
import '@logicflow/core/lib/style/index.css';
|
import '@logicflow/core/lib/style/index.css';
|
||||||
import { Menu, Label, Snapshot, SelectionSelect } from '@logicflow/extension';
|
import { Menu, Label, Snapshot, SelectionSelect } from '@logicflow/extension';
|
||||||
import '@logicflow/extension/lib/style/index.css';
|
import '@logicflow/extension/lib/style/index.css';
|
||||||
import '@logicflow/core/es/index.css';
|
import '@logicflow/core/es/index.css';
|
||||||
import '@logicflow/extension/es/index.css';
|
import '@logicflow/extension/es/index.css';
|
||||||
|
import { translateEdgeData, translateNodeData } from '@logicflow/core/es/keyboard/shortcut';
|
||||||
|
|
||||||
import { register } from '@logicflow/vue-node-registry';
|
import { register } from '@logicflow/vue-node-registry';
|
||||||
import ShikigamiSelectNode from './nodes/yys/ShikigamiSelectNode.vue';
|
import ShikigamiSelectNode from './nodes/yys/ShikigamiSelectNode.vue';
|
||||||
@@ -76,6 +77,10 @@ import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlo
|
|||||||
type AlignType = 'left' | 'right' | 'top' | 'bottom' | 'hcenter' | 'vcenter';
|
type AlignType = 'left' | 'right' | 'top' | 'bottom' | 'hcenter' | 'vcenter';
|
||||||
type DistributeType = 'horizontal' | 'vertical';
|
type DistributeType = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
const MOVE_STEP = 2;
|
||||||
|
const MOVE_STEP_LARGE = 10;
|
||||||
|
const COPY_TRANSLATION = 40;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
height?: string;
|
height?: string;
|
||||||
}>();
|
}>();
|
||||||
@@ -101,6 +106,280 @@ const { showMessage } = useGlobalMessage();
|
|||||||
|
|
||||||
// 当前选中节点
|
// 当前选中节点
|
||||||
const selectedNode = ref<any>(null);
|
const selectedNode = ref<any>(null);
|
||||||
|
const copyBuffer = ref<GraphData | null>(null);
|
||||||
|
let nextPasteDistance = COPY_TRANSLATION;
|
||||||
|
|
||||||
|
function isInputLike(event?: KeyboardEvent) {
|
||||||
|
const target = event?.target as HTMLElement | null;
|
||||||
|
if (!target) return false;
|
||||||
|
const tag = target.tagName?.toLowerCase();
|
||||||
|
return ['input', 'textarea', 'select', 'option'].includes(tag) || target.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipShortcut(event?: KeyboardEvent) {
|
||||||
|
const lfInstance = lf.value as any;
|
||||||
|
if (!lfInstance) return true;
|
||||||
|
if (lfInstance.keyboard?.disabled) return true;
|
||||||
|
if (lfInstance.graphModel?.textEditElement) return true;
|
||||||
|
if (isInputLike(event)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureMeta(meta?: Record<string, any>) {
|
||||||
|
const next: Record<string, any> = meta ? { ...meta } : {};
|
||||||
|
if (next.visible == null) next.visible = true;
|
||||||
|
if (next.locked == null) next.locked = false;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMetaToModel(model: BaseNodeModel, metaInput?: Record<string, any>) {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
const meta = ensureMeta(metaInput ?? (model.getProperties?.() as any)?.meta ?? (model as any)?.properties?.meta);
|
||||||
|
model.visible = meta.visible !== false;
|
||||||
|
model.draggable = !meta.locked;
|
||||||
|
model.setHittable?.(!meta.locked);
|
||||||
|
model.setHitable?.(!meta.locked);
|
||||||
|
model.setIsShowAnchor?.(!meta.locked);
|
||||||
|
model.setRotatable?.(!meta.locked);
|
||||||
|
model.setResizable?.(!meta.locked);
|
||||||
|
|
||||||
|
if (lfInstance) {
|
||||||
|
const connectedEdges = lfInstance.getNodeEdges(model.id);
|
||||||
|
connectedEdges.forEach((edgeModel) => {
|
||||||
|
edgeModel.visible = meta.visible !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNodeModel(model: BaseNodeModel) {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return;
|
||||||
|
|
||||||
|
const props = (model.getProperties?.() as any) ?? (model as any)?.properties ?? {};
|
||||||
|
const incomingMeta = ensureMeta(props.meta);
|
||||||
|
const currentMeta = ensureMeta((model as any)?.properties?.meta);
|
||||||
|
const needPersist =
|
||||||
|
currentMeta.visible !== incomingMeta.visible ||
|
||||||
|
currentMeta.locked !== incomingMeta.locked ||
|
||||||
|
currentMeta.groupId !== incomingMeta.groupId;
|
||||||
|
|
||||||
|
if (needPersist) {
|
||||||
|
lfInstance.setProperties(model.id, { ...props, meta: incomingMeta });
|
||||||
|
}
|
||||||
|
applyMetaToModel(model, incomingMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAllNodes() {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return;
|
||||||
|
lfInstance.graphModel?.nodes.forEach((model: BaseNodeModel) => normalizeNodeModel(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeMeta(model: BaseNodeModel, updater: (meta: Record<string, any>) => Record<string, any>) {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return;
|
||||||
|
const props = (model.getProperties?.() as any) ?? (model as any)?.properties ?? {};
|
||||||
|
const nextMeta = updater(ensureMeta(props.meta));
|
||||||
|
lfInstance.setProperties(model.id, { ...props, meta: nextMeta });
|
||||||
|
applyMetaToModel(model, nextMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedNodeModels() {
|
||||||
|
const graphModel = lf.value?.graphModel;
|
||||||
|
if (!graphModel) return [];
|
||||||
|
return [...graphModel.selectNodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectGroupNodeIds(models: BaseNodeModel[]) {
|
||||||
|
const graphModel = lf.value?.graphModel;
|
||||||
|
if (!graphModel) return [];
|
||||||
|
const ids = new Set<string>();
|
||||||
|
models.forEach((model) => {
|
||||||
|
const meta = ensureMeta((model.getProperties?.() as any)?.meta ?? (model as any)?.properties?.meta);
|
||||||
|
if (meta.locked) return;
|
||||||
|
if (meta.groupId) {
|
||||||
|
graphModel.nodes.forEach((node) => {
|
||||||
|
const peerMeta = ensureMeta((node.getProperties?.() as any)?.meta ?? (node as any)?.properties?.meta);
|
||||||
|
if (peerMeta.groupId === meta.groupId && !peerMeta.locked) {
|
||||||
|
ids.add(node.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ids.add(model.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSelectedNodes(deltaX: number, deltaY: number) {
|
||||||
|
const graphModel = lf.value?.graphModel;
|
||||||
|
if (!graphModel) return;
|
||||||
|
const targets = collectGroupNodeIds(getSelectedNodeModels());
|
||||||
|
if (!targets.length) return;
|
||||||
|
graphModel.moveNodes(targets, deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedElements(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return true;
|
||||||
|
|
||||||
|
const { nodes, edges } = lfInstance.getSelectElements(true);
|
||||||
|
const lockedNodes = nodes.filter((node) => ensureMeta((node as any).properties?.meta).locked);
|
||||||
|
edges.forEach((edge) => edge.id && lfInstance.deleteEdge(edge.id));
|
||||||
|
nodes
|
||||||
|
.filter((node) => {
|
||||||
|
const meta = ensureMeta((node as any).properties?.meta);
|
||||||
|
return !meta.locked && meta.visible !== false;
|
||||||
|
})
|
||||||
|
.forEach((node) => node.id && lfInstance.deleteNode(node.id));
|
||||||
|
|
||||||
|
if (lockedNodes.length) {
|
||||||
|
showMessage('warning', '部分节点已锁定,未删除');
|
||||||
|
}
|
||||||
|
updateSelectedCount();
|
||||||
|
selectedNode.value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLockSelected(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const models = getSelectedNodeModels();
|
||||||
|
if (!models.length) {
|
||||||
|
showMessage('info', '请选择节点后再执行锁定/解锁');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasUnlocked = models.some((model) => !ensureMeta((model.getProperties?.() as any)?.meta).locked);
|
||||||
|
models.forEach((model) => {
|
||||||
|
updateNodeMeta(model, (meta) => ({ ...meta, locked: hasUnlocked ? true : false }));
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleVisibilitySelected(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const models = getSelectedNodeModels();
|
||||||
|
if (!models.length) {
|
||||||
|
showMessage('info', '请选择节点后再执行显示/隐藏');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasVisible = models.some((model) => ensureMeta((model.getProperties?.() as any)?.meta).visible !== false);
|
||||||
|
models.forEach((model) => {
|
||||||
|
updateNodeMeta(model, (meta) => ({ ...meta, visible: !hasVisible ? true : false }));
|
||||||
|
});
|
||||||
|
if (hasVisible) {
|
||||||
|
selectedNode.value = null;
|
||||||
|
}
|
||||||
|
updateSelectedCount();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupSelectedNodes(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const models = getSelectedNodeModels().filter((model) => !ensureMeta((model.getProperties?.() as any)?.meta).locked);
|
||||||
|
if (models.length < 2) {
|
||||||
|
showMessage('warning', '请选择至少两个未锁定的节点进行分组');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const groupId = `group_${Date.now().toString(36)}`;
|
||||||
|
models.forEach((model) => {
|
||||||
|
updateNodeMeta(model, (meta) => ({ ...meta, groupId }));
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ungroupSelectedNodes(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const models = getSelectedNodeModels();
|
||||||
|
if (!models.length) {
|
||||||
|
showMessage('info', '请选择节点后再执行解组');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
models.forEach((model) => {
|
||||||
|
updateNodeMeta(model, (meta) => {
|
||||||
|
const nextMeta = { ...meta };
|
||||||
|
delete nextMeta.groupId;
|
||||||
|
return nextMeta;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowMove(direction: 'left' | 'right' | 'up' | 'down', event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const step = (event?.shiftKey ? MOVE_STEP_LARGE : MOVE_STEP) * (direction === 'left' || direction === 'up' ? -1 : 1);
|
||||||
|
if (direction === 'left' || direction === 'right') {
|
||||||
|
moveSelectedNodes(step, 0);
|
||||||
|
} else {
|
||||||
|
moveSelectedNodes(0, step);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapGroupIds(nodes: GraphData['nodes']) {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
const seed = Date.now().toString(36);
|
||||||
|
nodes.forEach((node, index) => {
|
||||||
|
const meta = ensureMeta((node as any).properties?.meta);
|
||||||
|
if (meta.groupId) {
|
||||||
|
if (!map.has(meta.groupId)) {
|
||||||
|
map.set(meta.groupId, `group_${seed}_${index}`);
|
||||||
|
}
|
||||||
|
meta.groupId = map.get(meta.groupId);
|
||||||
|
}
|
||||||
|
(node as any).properties = { ...(node as any).properties, meta };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return true;
|
||||||
|
const elements = lfInstance.getSelectElements(false);
|
||||||
|
if (!elements.nodes.length && !elements.edges.length) {
|
||||||
|
copyBuffer.value = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const nodes = elements.nodes.map((node) => translateNodeData(JSON.parse(JSON.stringify(node)), COPY_TRANSLATION));
|
||||||
|
const edges = elements.edges.map((edge) => translateEdgeData(JSON.parse(JSON.stringify(edge)), COPY_TRANSLATION));
|
||||||
|
remapGroupIds(nodes);
|
||||||
|
copyBuffer.value = { nodes, edges };
|
||||||
|
nextPasteDistance = COPY_TRANSLATION;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePaste(event?: KeyboardEvent) {
|
||||||
|
if (shouldSkipShortcut(event)) return true;
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance || !copyBuffer.value) return true;
|
||||||
|
|
||||||
|
lfInstance.clearSelectElements();
|
||||||
|
const added = lfInstance.addElements(copyBuffer.value, nextPasteDistance);
|
||||||
|
if (added) {
|
||||||
|
added.nodes.forEach((model) => {
|
||||||
|
normalizeNodeModel(model);
|
||||||
|
lfInstance.selectElementById(model.id, true);
|
||||||
|
});
|
||||||
|
added.edges.forEach((edge) => lfInstance.selectElementById(edge.id, true));
|
||||||
|
copyBuffer.value.nodes.forEach((node) => translateNodeData(node, COPY_TRANSLATION));
|
||||||
|
copyBuffer.value.edges.forEach((edge) => translateEdgeData(edge, COPY_TRANSLATION));
|
||||||
|
nextPasteDistance += COPY_TRANSLATION;
|
||||||
|
updateSelectedCount(lfInstance.graphModel);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeDrag(args: { data: NodeData; deltaX: number; deltaY: number }) {
|
||||||
|
const { data, deltaX, deltaY } = args;
|
||||||
|
if (!deltaX && !deltaY) return;
|
||||||
|
const graphModel = lf.value?.graphModel;
|
||||||
|
if (!graphModel) return;
|
||||||
|
const model = graphModel.getNodeModelById(data.id);
|
||||||
|
if (!model) return;
|
||||||
|
const targets = collectGroupNodeIds([model]).filter((id) => id !== model.id);
|
||||||
|
if (!targets.length) return;
|
||||||
|
graphModel.moveNodes(targets, deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
function updateSelectedCount(model?: GraphModel) {
|
function updateSelectedCount(model?: GraphModel) {
|
||||||
const lfInstance = lf.value;
|
const lfInstance = lf.value;
|
||||||
@@ -127,7 +406,11 @@ function applySnapGrid(enabled: boolean) {
|
|||||||
function getSelectedRects() {
|
function getSelectedRects() {
|
||||||
const lfInstance = lf.value;
|
const lfInstance = lf.value;
|
||||||
if (!lfInstance) return [];
|
if (!lfInstance) return [];
|
||||||
return lfInstance.graphModel.selectNodes.map((model: BaseNodeModel) => {
|
const unlocked = lfInstance.graphModel.selectNodes.filter((model: BaseNodeModel) => {
|
||||||
|
const meta = ensureMeta((model.getProperties?.() as any)?.meta ?? (model as any)?.properties?.meta);
|
||||||
|
return !meta.locked && meta.visible !== false;
|
||||||
|
});
|
||||||
|
return unlocked.map((model: BaseNodeModel) => {
|
||||||
const bounds = model.getBounds();
|
const bounds = model.getBounds();
|
||||||
const width = bounds.maxX - bounds.minX;
|
const width = bounds.maxX - bounds.minX;
|
||||||
const height = bounds.maxY - bounds.minY;
|
const height = bounds.maxY - bounds.minY;
|
||||||
@@ -242,6 +525,9 @@ onMounted(() => {
|
|||||||
allowRotate: true,
|
allowRotate: true,
|
||||||
overlapMode: -1,
|
overlapMode: -1,
|
||||||
snapline: true,
|
snapline: true,
|
||||||
|
keyboard: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
plugins: [Menu, Label, Snapshot, SelectionSelect],
|
plugins: [Menu, Label, Snapshot, SelectionSelect],
|
||||||
pluginsOptions: {
|
pluginsOptions: {
|
||||||
label: {
|
label: {
|
||||||
@@ -257,6 +543,26 @@ onMounted(() => {
|
|||||||
const lfInstance = lf.value;
|
const lfInstance = lf.value;
|
||||||
if (!lfInstance) return;
|
if (!lfInstance) return;
|
||||||
|
|
||||||
|
lfInstance.keyboard.off(['cmd + c', 'ctrl + c']);
|
||||||
|
lfInstance.keyboard.off(['cmd + v', 'ctrl + v']);
|
||||||
|
lfInstance.keyboard.off(['backspace']);
|
||||||
|
|
||||||
|
const bindShortcut = (keys: string | string[], handler: (event?: KeyboardEvent) => boolean | void) => {
|
||||||
|
lfInstance.keyboard.on(keys, (event: KeyboardEvent) => handler(event));
|
||||||
|
};
|
||||||
|
|
||||||
|
bindShortcut(['del', 'backspace'], deleteSelectedElements);
|
||||||
|
bindShortcut(['left'], (event) => handleArrowMove('left', event));
|
||||||
|
bindShortcut(['right'], (event) => handleArrowMove('right', event));
|
||||||
|
bindShortcut(['up'], (event) => handleArrowMove('up', event));
|
||||||
|
bindShortcut(['down'], (event) => handleArrowMove('down', event));
|
||||||
|
bindShortcut(['cmd + c', 'ctrl + c'], handleCopy);
|
||||||
|
bindShortcut(['cmd + v', 'ctrl + v'], handlePaste);
|
||||||
|
bindShortcut(['cmd + g', 'ctrl + g'], groupSelectedNodes);
|
||||||
|
bindShortcut(['cmd + u', 'ctrl + u'], ungroupSelectedNodes);
|
||||||
|
bindShortcut(['cmd + l', 'ctrl + l'], toggleLockSelected);
|
||||||
|
bindShortcut(['cmd + shift + h', 'ctrl + shift + h'], toggleVisibilitySelected);
|
||||||
|
|
||||||
lfInstance.extension.menu.addMenuConfig({
|
lfInstance.extension.menu.addMenuConfig({
|
||||||
nodeMenu: [
|
nodeMenu: [
|
||||||
{
|
{
|
||||||
@@ -307,6 +613,7 @@ onMounted(() => {
|
|||||||
lfInstance.render({
|
lfInstance.render({
|
||||||
// 渲染的数据
|
// 渲染的数据
|
||||||
});
|
});
|
||||||
|
normalizeAllNodes();
|
||||||
lfInstance.updateEditConfig({
|
lfInstance.updateEditConfig({
|
||||||
multipleSelectKey: 'shift',
|
multipleSelectKey: 'shift',
|
||||||
snapGrid: snapGridEnabled.value
|
snapGrid: snapGridEnabled.value
|
||||||
@@ -320,6 +627,15 @@ onMounted(() => {
|
|||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lfInstance.on(EventType.NODE_DRAG, (args) => handleNodeDrag(args as any));
|
||||||
|
|
||||||
|
lfInstance.on(EventType.NODE_ADD, ({ data }) => {
|
||||||
|
const model = lfInstance.getNodeModelById(data.id);
|
||||||
|
if (model) normalizeNodeModel(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
lfInstance.on(EventType.GRAPH_RENDERED, () => normalizeAllNodes());
|
||||||
|
|
||||||
// 监听空白点击事件,取消选中
|
// 监听空白点击事件,取消选中
|
||||||
lfInstance.on(EventType.BLANK_CLICK, () => {
|
lfInstance.on(EventType.BLANK_CLICK, () => {
|
||||||
selectedNode.value = null;
|
selectedNode.value = null;
|
||||||
@@ -337,6 +653,10 @@ onMounted(() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const model = lfInstance.getNodeModelById(nodeId);
|
||||||
|
if (model && data.properties?.meta) {
|
||||||
|
applyMetaToModel(model, data.properties.meta);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
lfInstance.on('selection:selected', () => updateSelectedCount());
|
lfInstance.on('selection:selected', () => updateSelectedCount());
|
||||||
|
|||||||
Reference in New Issue
Block a user