diff --git a/.cursor/rules/vue-rules.mdc b/.cursor/rules/vue-rules.mdc new file mode 100644 index 0000000..2cc94fc --- /dev/null +++ b/.cursor/rules/vue-rules.mdc @@ -0,0 +1,69 @@ +--- +description: +globs: +alwaysApply: true +--- + This is a game guide editor that imitates draw.io, which introduces some game-specific interactive components into traditional flowchart editing. For instance, Shikigami selection and Yuhun selection - in essence, these are pre-set images. + + The project previously utilized a large amount of AI-generated code, making it difficult to maintain in its current state. I will now follow the official Logic-Flow core component examples to refactor the project, improving both my understanding and future development efficiency. + + You are an expert in TypeScript, Node.js, NuxtJS, Vue 3, Shadcn Vue, Radix Vue, VueUse, and Tailwind. + + Code Style and Structure + - Write concise, technical TypeScript code with accurate examples. + - Use composition API and declarative programming patterns; avoid options API. + - Prefer iteration and modularization over code duplication. + - Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError). + - Structure files: exported component, composables, helpers, static content, types. + + Naming Conventions + - Use lowercase with dashes for directories (e.g., components/auth-wizard). + - Use PascalCase for component names (e.g., AuthWizard.vue). + - Use camelCase for composables (e.g., useAuthState.ts). + + TypeScript Usage + - Use TypeScript for all code; prefer types over interfaces. + - Avoid enums; use const objects instead. + - Use Vue 3 with TypeScript, leveraging defineComponent and PropType. + + Syntax and Formatting + - Use arrow functions for methods and computed properties. + - Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements. + - Use template syntax for declarative rendering. + + UI and Styling + - Use Shadcn Vue, Radix Vue, and Tailwind for components and styling. + - Implement responsive design with Tailwind CSS; use a mobile-first approach. + + Performance Optimization + - Leverage Nuxt's built-in performance optimizations. + - Use Suspense for asynchronous components. + - Implement lazy loading for routes and components. + - Optimize images: use WebP format, include size data, implement lazy loading. + + Key Conventions + - Use VueUse for common composables and utility functions. + - Use Pinia for state management. + - Optimize Web Vitals (LCP, CLS, FID). + - Utilize Nuxt's auto-imports feature for components and composables. + + Nuxt-specific Guidelines + - Follow Nuxt 3 directory structure (e.g., pages/, components/, composables/). + - Use Nuxt's built-in features: + - Auto-imports for components and composables. + - File-based routing in the pages/ directory. + - Server routes in the server/ directory. + - Leverage Nuxt plugins for global functionality. + - Use useFetch and useAsyncData for data fetching. + - Implement SEO best practices using Nuxt's useHead and useSeoMeta. + + Vue 3 and Composition API Best Practices + - Use @@ -136,7 +128,7 @@ const activeFileGroups = computed(() => { } .sidebar { - width: 20%; /* 侧边栏宽度 */ + width: 230px; /* 侧边栏宽度 */ background-color: #f0f0f0; /* 背景色 */ flex-shrink: 0; /* 防止侧边栏被压缩 */ overflow-y: auto; /* 允许侧边栏内容滚动 */ @@ -153,8 +145,8 @@ const activeFileGroups = computed(() => { height: 100%; /* 确保内容区域占满父容器 */ overflow-y: auto; /* 允许内容滚动 */ min-height: 100vh; /* 允许容器扩展 */ - //display: inline-block; - max-width: 100%; } + + + - \ No newline at end of file diff --git a/src/components/DialogManager.vue b/src/components/DialogManager.vue new file mode 100644 index 0000000..848e898 --- /dev/null +++ b/src/components/DialogManager.vue @@ -0,0 +1,43 @@ + + + \ No newline at end of file diff --git a/src/components/ProjectExplorer.vue b/src/components/ProjectExplorer.vue index d23fade..91ec0c4 100644 --- a/src/components/ProjectExplorer.vue +++ b/src/components/ProjectExplorer.vue @@ -34,7 +34,7 @@ - - diff --git a/src/components/Toolbar.vue b/src/components/Toolbar.vue index 6a633bf..7a4fc31 100644 --- a/src/components/Toolbar.vue +++ b/src/components/Toolbar.vue @@ -77,13 +77,14 @@ \ No newline at end of file diff --git a/src/components/Yys.vue b/src/components/Yys.vue index 40c07cd..1928d6a 100644 --- a/src/components/Yys.vue +++ b/src/components/Yys.vue @@ -113,8 +113,8 @@ + + + + \ No newline at end of file diff --git a/src/components/flow/FlowEditor.vue b/src/components/flow/FlowEditor.vue new file mode 100644 index 0000000..d266eaa --- /dev/null +++ b/src/components/flow/FlowEditor.vue @@ -0,0 +1,179 @@ + + + + + \ No newline at end of file diff --git a/src/components/flow/PropertyPanel.vue b/src/components/flow/PropertyPanel.vue new file mode 100644 index 0000000..18e1451 --- /dev/null +++ b/src/components/flow/PropertyPanel.vue @@ -0,0 +1,243 @@ + + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/common/ImageNode.vue b/src/components/flow/nodes/common/ImageNode.vue new file mode 100644 index 0000000..9f108d0 --- /dev/null +++ b/src/components/flow/nodes/common/ImageNode.vue @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/common/TextNode.vue b/src/components/flow/nodes/common/TextNode.vue new file mode 100644 index 0000000..c2d1703 --- /dev/null +++ b/src/components/flow/nodes/common/TextNode.vue @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/yys/PropertySelect.vue b/src/components/flow/nodes/yys/PropertySelect.vue new file mode 100644 index 0000000..db231a6 --- /dev/null +++ b/src/components/flow/nodes/yys/PropertySelect.vue @@ -0,0 +1,341 @@ + + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/yys/PropertySelectNode.vue b/src/components/flow/nodes/yys/PropertySelectNode.vue new file mode 100644 index 0000000..8df4972 --- /dev/null +++ b/src/components/flow/nodes/yys/PropertySelectNode.vue @@ -0,0 +1,160 @@ + + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/yys/ShikigamiGroup.vue b/src/components/flow/nodes/yys/ShikigamiGroup.vue new file mode 100644 index 0000000..2f86bc7 --- /dev/null +++ b/src/components/flow/nodes/yys/ShikigamiGroup.vue @@ -0,0 +1,627 @@ + + + + \ No newline at end of file diff --git a/src/components/ShikigamiProperty.vue b/src/components/flow/nodes/yys/ShikigamiProperty.vue similarity index 98% rename from src/components/ShikigamiProperty.vue rename to src/components/flow/nodes/yys/ShikigamiProperty.vue index 03a93f6..bd6283d 100644 --- a/src/components/ShikigamiProperty.vue +++ b/src/components/flow/nodes/yys/ShikigamiProperty.vue @@ -123,10 +123,10 @@ + + + + \ No newline at end of file diff --git a/src/components/flow/nodes/yys/YuhunSelect.vue b/src/components/flow/nodes/yys/YuhunSelect.vue new file mode 100644 index 0000000..b750d6c --- /dev/null +++ b/src/components/flow/nodes/yys/YuhunSelect.vue @@ -0,0 +1,120 @@ + + + \ No newline at end of file diff --git a/src/components/flow/nodes/yys/YuhunSelectNode.vue b/src/components/flow/nodes/yys/YuhunSelectNode.vue new file mode 100644 index 0000000..c07d488 --- /dev/null +++ b/src/components/flow/nodes/yys/YuhunSelectNode.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/main.js b/src/main.js index 2ce0f0d..71c403c 100644 --- a/src/main.js +++ b/src/main.js @@ -16,7 +16,7 @@ import zh from './locales/zh.json' import ja from './locales/ja.json' import { createPinia } from 'pinia' // 导入 Pinia -import {useFilesStore} from "@/ts/files"; +import { useFilesStore } from './ts/useStore'; const app = createApp(App) @@ -62,6 +62,4 @@ app.use(pinia) // 使用 Pinia .use(Vue3DraggableResizable) .mount('#app') -const filesStore = useFilesStore(); -filesStore.setupAutoSave(); -filesStore.initializeWithPrompt(); \ No newline at end of file +const filesStore = useFilesStore(); \ No newline at end of file diff --git a/src/ts/files.ts b/src/ts/files.ts deleted file mode 100644 index c5ec2f7..0000000 --- a/src/ts/files.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {defineStore} from 'pinia'; -import {ElMessageBox} from "element-plus"; -import {useGlobalMessage} from "./useGlobalMessage"; - -const { showMessage } = useGlobalMessage(); - -function getDefaultState() { - return { - fileList: [{ - "label": "File 1", - "name": "1", - "visible": true, - "type":"PVE", - "groups": [ - { - "shortDescription": "", - "groupInfo": [ - {}, {}, {}, {}, {} - ], - "details": "" - } - ] - }], - activeFile: "1", - }; -} - -function saveStateToLocalStorage(state) { - localStorage.setItem('filesStore', JSON.stringify(state)); -} - -function clearFilesStoreLocalStorage() { - localStorage.removeItem('filesStore') -} - -function loadStateFromLocalStorage() { - return JSON.parse(localStorage.getItem('filesStore')); -} - -export const useFilesStore = defineStore('files', { - state: () => getDefaultState(), - getters: { - visibleFiles: (state) => state.fileList.filter(file => file.visible), - }, - actions: { - initializeWithPrompt() { - const savedState = loadStateFromLocalStorage(); - const defaultState = getDefaultState(); - - const isSame = JSON.stringify(savedState) === JSON.stringify(defaultState); - if (savedState && !isSame) { - ElMessageBox.confirm( - '检测到有未保存的旧数据,是否恢复?', - '提示', - { - confirmButtonText: '恢复', - cancelButtonText: '不恢复', - type: 'warning', - } - ).then(() => { - this.fileList = savedState.fileList || []; - this.activeFile = savedState.activeFile || "1"; - showMessage('success', '数据已恢复'); - }).catch(() => { - clearFilesStoreLocalStorage(); - showMessage('info', '选择了不恢复旧数据'); - }); - } - }, - setupAutoSave() { - setInterval(() => { - saveStateToLocalStorage(this.$state); - }, 30000); // 设置间隔时间为30秒 - }, - addFile(file) { - this.fileList.push({...file, visible: true}); - this.activeFile = file.name; - }, - setActiveFile(fileId: number) { - this.activeFile = fileId; - }, - setVisible(fileId: number, visibility: boolean) { - const file = this.fileList.find(file => file.name === fileId); - if (file) { - file.visible = visibility; - } - }, - closeTab(fileName: String) { - const file = this.fileList.find(file => file.name === fileName); - if (file) { - file.visible = false; - if (this.activeFile === fileName) { - const nextVisibleFile = this.visibleFiles[0]; - this.activeFile = nextVisibleFile ? nextVisibleFile.name : -1; - } - } - }, - async deleteFile(fileId: string) { - try { - if (this.fileList.length === 1) { - showMessage('warning', '无法删除'); - return; - } - await ElMessageBox.confirm('确定要删除此文件吗?', '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - }); - - const index = this.fileList.findIndex(file => file.name === fileId); - if (index > -1) { - this.fileList.splice(index, 1); - if (this.activeFile === fileId) { - const nextVisibleFile = this.visibleFiles[0]; - this.activeFile = nextVisibleFile ? nextVisibleFile.name : "-1"; - } - } - showMessage('success', '删除成功!'); - } catch (error) { - showMessage('info', '已取消删除'); - } - }, - renameFile(fileId, newName) { - const file = this.fileList.find(file => file.name === fileId); - if (file) { - file.label = newName; - } - }, - }, -}); \ No newline at end of file diff --git a/src/ts/useDialogs.ts b/src/ts/useDialogs.ts new file mode 100644 index 0000000..b76e400 --- /dev/null +++ b/src/ts/useDialogs.ts @@ -0,0 +1,29 @@ +import { reactive } from 'vue' + +const dialogs = reactive({ + shikigami: { show: false, data: null, node: null, callback: null }, + yuhun: { show: false, data: null, node: null, callback: null }, + property: { show: false, data: null, node: null, callback: null } +}) + +function openDialog(type: string, data = null, node = null, callback = null) { + dialogs[type].show = true + dialogs[type].data = data + dialogs[type].node = node + dialogs[type].callback = callback +} + +function closeDialog(type: string) { + dialogs[type].show = false + dialogs[type].data = null + dialogs[type].node = null + dialogs[type].callback = null +} + +export function useDialogs() { + return { + dialogs, + openDialog, + closeDialog + } +} \ No newline at end of file diff --git a/src/ts/useLogicFlow.ts b/src/ts/useLogicFlow.ts new file mode 100644 index 0000000..01fe6c1 --- /dev/null +++ b/src/ts/useLogicFlow.ts @@ -0,0 +1,16 @@ +import type LogicFlow from '@logicflow/core'; + +let logicFlowInstance: LogicFlow | null = null; + +export function setLogicFlowInstance(lf: LogicFlow) { + logicFlowInstance = lf; +} + +export function getLogicFlowInstance(): LogicFlow | null { + return logicFlowInstance; +} + +export function destroyLogicFlowInstance() { + logicFlowInstance?.destroy(); + logicFlowInstance = null; +} \ No newline at end of file diff --git a/src/ts/useStore.ts b/src/ts/useStore.ts new file mode 100644 index 0000000..9c058a1 --- /dev/null +++ b/src/ts/useStore.ts @@ -0,0 +1,326 @@ +import {defineStore} from 'pinia'; +import {ref, computed} from 'vue'; +// import type { Edge, Node, ViewportTransform } from '@vue-flow/core'; +import {ElMessageBox} from "element-plus"; +import {useGlobalMessage} from "./useGlobalMessage"; +import {getLogicFlowInstance} from "./useLogicFlow"; + +const {showMessage} = useGlobalMessage(); + +// localStorage 防抖定时器 +let localStorageDebounceTimer: NodeJS.Timeout | null = null; +const LOCALSTORAGE_DEBOUNCE_DELAY = 1000; // 1秒防抖 + +interface FlowFile { + label: string; + name: string; + visible: boolean; + type: string; + graphRawData?: object; + transform?: { + "SCALE_X": number, + "SCALE_Y": number, + "TRANSLATE_X": number, + "TRANSLATE_Y": number + }; +} + +function getDefaultState() { + return { + "fileList": [ + { + "label": "File 1", + "name": "File 1", + "visible": true, + "type": "FLOW", + "graphRawData": { + "nodes": [], + "edges": [] + }, + "transform": { + "SCALE_X": 1, + "SCALE_Y": 1, + "TRANSLATE_X": 0, + "TRANSLATE_Y": 0 + } + } + ], + "activeFile": "File 1" + }; +} + +function clearFilesStoreLocalStorage() { + localStorage.removeItem('filesStore'); +} + +function loadStateFromLocalStorage() { + try { + const data = localStorage.getItem('filesStore'); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error('从 localStorage 加载数据失败:', error); + return null; + } +} + +function saveStateToLocalStorage(state: any) { + // 清除之前的防抖定时器 + if (localStorageDebounceTimer) { + clearTimeout(localStorageDebounceTimer); + } + + // 设置新的防抖定时器 + localStorageDebounceTimer = setTimeout(() => { + try { + localStorage.setItem('filesStore', JSON.stringify(state)); + console.log('数据已防抖保存到 localStorage'); + } catch (error) { + console.error('保存到 localStorage 失败:', error); + // 如果 localStorage 满了,尝试清理一些数据 + try { + localStorage.clear(); + localStorage.setItem('filesStore', JSON.stringify(state)); + } catch (clearError) { + console.error('清理 localStorage 后仍无法保存:', clearError); + } + } + }, LOCALSTORAGE_DEBOUNCE_DELAY); +} + + +export const useFilesStore = defineStore('files', () => { + // 文件列表状态 + const fileList = ref([]); + const activeFile = ref(''); + + // 计算属性:获取可见的文件 + const visibleFiles = computed(() => { + return fileList.value.filter(file => file.visible); + }); + + // 导入数据 + const importData = (data: any) => { + try { + if (data.fileList && Array.isArray(data.fileList)) { + // 新版本格式:包含 fileList 和 activeFile + fileList.value = data.fileList; + activeFile.value = data.activeFile || data[0]?.name; + showMessage('success', '数据导入成功'); + } else if (Array.isArray(data) && data[0]?.visible === true) { + // 兼容旧版本格式:直接是 fileList 数组 + fileList.value = data; + activeFile.value = data[0]?.name || "1"; + showMessage('success', '数据导入成功'); + } else { + // 兼容更旧版本格式:仅包含 groups 数组 + const newFile = { + label: `File ${fileList.value.length + 1}`, + name: String(fileList.value.length + 1), + visible: true, + type: "FLOW", + groups: data, + graphRawData: { + nodes: [], + edges: [] + }, + transform: { + SCALE_X: 1, + SCALE_Y: 1, + TRANSLATE_X: 0, + TRANSLATE_Y: 0 + } + }; + fileList.value.push(newFile); + activeFile.value = newFile.name; + showMessage('success', '数据导入成功'); + } + } catch (error) { + console.error('Failed to import file', error); + showMessage('error', '数据导入失败'); + } + }; + + // 导出数据 + const exportData = () => { + try { + const dataStr = JSON.stringify({ + fileList: fileList.value, + activeFile: activeFile.value + }, null, 2); + const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'yys-editor-files.json'; + link.click(); + URL.revokeObjectURL(url); + showMessage('success', '数据导出成功'); + } catch (error) { + console.error('导出数据失败:', error); + showMessage('error', '数据导出失败'); + } + }; + + // 初始化时检查是否有未保存的数据 + const initializeWithPrompt = () => { + const savedState = loadStateFromLocalStorage(); + const defaultState = getDefaultState(); + + // 如果没有保存的数据,使用默认状态 + if (!savedState) { + fileList.value = defaultState.fileList; + activeFile.value = defaultState.activeFile; + return; + } + + const isSame = JSON.stringify(savedState) === JSON.stringify(defaultState); + if (savedState && !isSame) { + ElMessageBox.confirm( + '检测到有未保存的旧数据,是否恢复?', + '提示', + { + confirmButtonText: '恢复', + cancelButtonText: '不恢复', + type: 'warning', + } + ).then(() => { + fileList.value = savedState.fileList || []; + activeFile.value = savedState.activeFile || "1"; + showMessage('success', '数据已恢复'); + }).catch(() => { + clearFilesStoreLocalStorage(); + fileList.value = defaultState.fileList; + activeFile.value = defaultState.activeFile; + showMessage('info', '选择了不恢复旧数据'); + }); + } else { + // 如果有保存的数据且与默认状态相同,直接使用保存的数据 + fileList.value = savedState.fileList || defaultState.fileList; + activeFile.value = savedState.activeFile || defaultState.activeFile; + } + }; + + // 设置自动更新 + const setupAutoSave = () => { + console.log('自动更新功能已启动,每30秒更新一次'); + setInterval(() => { + updateTab(); // 使用统一的更新方法 + }, 30000); // 设置间隔时间为30秒 + }; + + // 添加新文件 + const addTab = () => { + // 添加文件前先保存 + updateTab(); + + requestAnimationFrame(() => { + const newFileName = `File ${fileList.value.length + 1}`; + const newFile = { + label: newFileName, + name: newFileName, + visible: true, + type: 'FLOW', + graphRawData: {}, + transform: { + SCALE_X: 1, + SCALE_Y: 1, + TRANSLATE_X: 0, + TRANSLATE_Y: 0 + } + }; + fileList.value.push(newFile); + activeFile.value = newFileName; + }); + }; + + // 关闭文件标签 + const removeTab = (fileName: string | undefined) => { + if (!fileName) return; + + const index = fileList.value.findIndex(file => file.name === fileName); + if (index === -1) return; + + fileList.value.splice(index, 1); + + // 如果关闭的是当前活动文件,则切换到其他文件 + if (activeFile.value === fileName) { + activeFile.value = fileList.value[Math.max(0, index - 1)]?.name || ''; + } + + // 关闭文件后立即更新 + updateTab(); + }; + + // 更新指定 Tab - 内存操作即时,localStorage 操作防抖 + const updateTab = (fileName?: string) => { + try { + const targetFile = fileName || activeFile.value; + + // 先同步 LogicFlow 数据到内存 + syncLogicFlowDataToStore(targetFile); + + // 再保存到 localStorage(带防抖) + const state = { + fileList: fileList.value, + activeFile: activeFile.value + }; + saveStateToLocalStorage(state); + } catch (error) { + console.error('更新 Tab 失败:', error); + showMessage('error', '数据更新失败'); + } + }; + + // 获取当前 Tab 数据 + const getTab = (fileName?: string) => { + const targetFile = fileName || activeFile.value; + return fileList.value.find(f => f.name === targetFile); + }; + + // 同步 LogicFlow 画布数据到 store 的内部方法 + const syncLogicFlowDataToStore = (fileName?: string) => { + const logicFlowInstance = getLogicFlowInstance(); + const targetFile = fileName || activeFile.value; + + if (logicFlowInstance && targetFile) { + try { + // 获取画布最新数据 + const graphData = logicFlowInstance.getGraphRawData(); + const transform = logicFlowInstance.getTransform(); + + if (graphData) { + // 直接保存原始数据到 GraphRawData + const file = fileList.value.find(f => f.name === targetFile); + if (file) { + file.graphRawData = graphData; + file.transform = transform; + console.log(`已同步画布数据到文件 "${targetFile}"`); + } + } + } catch (error) { + console.warn('同步画布数据失败:', error); + } + } + }; + + + + + + return { + importData, + exportData, + + initializeWithPrompt, + setupAutoSave, + + addTab, + removeTab, + updateTab, + getTab, + + fileList, + activeFile, + visibleFiles, + }; +}); \ No newline at end of file