From 9ec22add5502be800f4a4b39a31a2ce9517e7431 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Thu, 1 Jan 2026 00:02:34 +0800 Subject: [PATCH 1/7] :bug: Fixed theme invalidation issue --- frontend/src/stores/themeStore.ts | 52 ++++++++++--------- .../src/views/editor/basic/themeExtension.ts | 22 ++------ .../views/settings/pages/AppearancePage.vue | 7 +-- go.mod | 10 ++-- go.sum | 23 ++++---- version.txt | 2 +- 6 files changed, 50 insertions(+), 66 deletions(-) diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts index 9d584e5..c37c239 100644 --- a/frontend/src/stores/themeStore.ts +++ b/frontend/src/stores/themeStore.ts @@ -4,12 +4,11 @@ import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models'; import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models'; import {ThemeService} from '@/../bindings/voidraft/internal/services'; import {useConfigStore} from './configStore'; -import {useEditorStore} from './editorStore'; import type {ThemeColors} from '@/views/editor/theme/types'; import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets'; // 类型定义 -type ThemeOption = {name: string; type: ThemeType}; +type ThemeOption = { name: string; type: ThemeType }; // 解析主题名称,确保返回有效的主题 const resolveThemeName = (name?: string): string => @@ -62,15 +61,11 @@ export const useThemeStore = defineStore('theme', () => { // 从服务器获取主题颜色 const fetchThemeColors = async (themeName: string): Promise => { const safeName = resolveThemeName(themeName); - try { - const theme = await ThemeService.GetThemeByName(safeName); - if (theme?.colors) { - const colors = cloneThemeColors(theme.colors as ThemeColors); - colors.themeName = safeName; - return colors; - } - } catch (error) { - console.error('Failed to load theme override:', error); + const theme = await ThemeService.GetThemeByName(safeName); + if (theme?.colors) { + const colors = cloneThemeColors(theme.colors as ThemeColors); + colors.themeName = safeName; + return colors; } return getPresetColors(safeName); }; @@ -80,21 +75,35 @@ export const useThemeStore = defineStore('theme', () => { const targetName = resolveThemeName( themeName || configStore.config?.appearance?.currentTheme ); + currentColors.value = getPresetColors(targetName); currentColors.value = await fetchThemeColors(targetName); + + }; + + // 获取可用的主题颜色 + const getEffectiveColors = (): ThemeColors => { + const targetName = resolveThemeName( + currentColors.value?.themeName || configStore.config?.appearance?.currentTheme + ); + return currentColors.value ?? getPresetColors(targetName); + }; + + // 同步应用到 DOM 与编辑器 + const applyAllThemes = () => { + applyThemeToDOM(currentTheme.value); }; // 初始化主题 const initTheme = async () => { applyThemeToDOM(currentTheme.value); await loadThemeColors(); - refreshEditorTheme(); + applyAllThemes(); }; // 设置系统主题 const setTheme = async (theme: SystemThemeType) => { await configStore.setSystemTheme(theme); - applyThemeToDOM(theme); - refreshEditorTheme(); + applyAllThemes(); }; // 切换到指定主题 @@ -106,7 +115,7 @@ export const useThemeStore = defineStore('theme', () => { await loadThemeColors(themeName); await configStore.setCurrentTheme(themeName); - refreshEditorTheme(); + applyAllThemes(); return true; }; @@ -128,7 +137,7 @@ export const useThemeStore = defineStore('theme', () => { await ThemeService.UpdateTheme(themeName, currentColors.value); await loadThemeColors(themeName); - refreshEditorTheme(); + applyAllThemes(); return true; }; @@ -142,16 +151,10 @@ export const useThemeStore = defineStore('theme', () => { await ThemeService.ResetTheme(themeName); await loadThemeColors(themeName); - refreshEditorTheme(); + applyAllThemes(); return true; }; - // 刷新编辑器主题 - const refreshEditorTheme = () => { - applyThemeToDOM(currentTheme.value); - const editorStore = useEditorStore(); - editorStore?.applyThemeSettings(); - }; return { availableThemes, @@ -164,7 +167,8 @@ export const useThemeStore = defineStore('theme', () => { updateCurrentColors, saveCurrentTheme, resetCurrentTheme, - refreshEditorTheme, applyThemeToDOM, + applyAllThemes, + getEffectiveColors, }; }); diff --git a/frontend/src/views/editor/basic/themeExtension.ts b/frontend/src/views/editor/basic/themeExtension.ts index a2cba19..34aa12e 100644 --- a/frontend/src/views/editor/basic/themeExtension.ts +++ b/frontend/src/views/editor/basic/themeExtension.ts @@ -9,15 +9,11 @@ export const themeCompartment = new Compartment(); /** * 根据主题类型获取主题扩展 */ -const getThemeExtension = (): Extension | null => { +const getThemeExtension = (): Extension => { const themeStore = useThemeStore(); - // 直接获取当前主题颜色配置 - const colors = themeStore.currentColors; - - if (!colors) { - return null; - } + // 获取有效主题颜色 + const colors = themeStore.getEffectiveColors(); // 使用颜色配置创建主题 return createThemeByColors(colors); @@ -28,12 +24,6 @@ const getThemeExtension = (): Extension | null => { */ export const createThemeExtension = (): Extension => { const extension = getThemeExtension(); - - // 如果主题未加载,返回空扩展 - if (!extension) { - return themeCompartment.of([]); - } - return themeCompartment.of(extension); }; @@ -48,11 +38,6 @@ export const updateEditorTheme = (view: EditorView): void => { try { const extension = getThemeExtension(); - // 如果主题未加载,不更新 - if (!extension) { - return; - } - view.dispatch({ effects: themeCompartment.reconfigure(extension) }); @@ -60,4 +45,3 @@ export const updateEditorTheme = (view: EditorView): void => { console.error('Failed to update editor theme:', error); } }; - diff --git a/frontend/src/views/settings/pages/AppearancePage.vue b/frontend/src/views/settings/pages/AppearancePage.vue index 3d390d6..b0e9af6 100644 --- a/frontend/src/views/settings/pages/AppearancePage.vue +++ b/frontend/src/views/settings/pages/AppearancePage.vue @@ -133,16 +133,13 @@ const updateLocalColor = (colorKey: string, value: string) => { const applyChanges = async () => { try { if (!tempColors.value) return; - + // 更新 store 中的颜色 themeStore.updateCurrentColors(tempColors.value); // 保存到数据库 await themeStore.saveCurrentTheme(); - - // 刷新编辑器主题 - themeStore.refreshEditorTheme(); - + // 清除未保存标记 hasUnsavedChanges.value = false; } catch (error) { diff --git a/go.mod b/go.mod index c056522..0f06906 100644 --- a/go.mod +++ b/go.mod @@ -13,15 +13,15 @@ require ( github.com/knadh/koanf/v2 v2.3.0 github.com/mattn/go-sqlite3 v1.14.32 github.com/stretchr/testify v1.11.1 - github.com/wailsapp/wails/v3 v3.0.0-alpha.51 + github.com/wailsapp/wails/v3 v3.0.0-alpha.54 golang.org/x/net v0.48.0 golang.org/x/sys v0.39.0 golang.org/x/text v0.32.0 - resty.dev/v3 v3.0.0-beta.5 + resty.dev/v3 v3.0.0-beta.6 ) require ( - ariga.io/atlas v0.38.0 // indirect + ariga.io/atlas v1.0.0 // indirect code.gitea.io/sdk/gitea v0.22.1 // indirect dario.cat/mergo v1.0.2 // indirect git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect @@ -48,11 +48,11 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/inflect v0.21.5 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/godbus/dbus/v5 v5.2.1 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v74 v74.0.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.8.0 // indirect diff --git a/go.sum b/go.sum index cc3c60a..2f0b190 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -ariga.io/atlas v0.38.0 h1:MwbtwVtDWJFq+ECyeTAz2ArvewDnpeiw/t/sgNdDsdo= -ariga.io/atlas v0.38.0/go.mod h1:D7XMK6ei3GvfDqvzk+2VId78j77LdqHrqPOWamn51/s= +ariga.io/atlas v1.0.0 h1:v9DQH49xK+SM2kKwk4OQBjfz/KNRMUR+pvDiEIxSJto= +ariga.io/atlas v1.0.0/go.mod h1:esBbk3F+pi/mM2PvbCymDm+kWhaOk4PaaiegQdNELk8= code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA= code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -76,17 +76,17 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.2.1 h1:I4wwMdWSkmI57ewd+elNGwLRf2/dtSaFz1DujfWYvOk= -github.com/godbus/dbus/v5 v5.2.1/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -178,8 +178,8 @@ github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+ github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v3 v3.0.0-alpha.51 h1:n8KT0H4lvtWld9tMIiHVX4nrR0wEMT2zy5hM/R6luMU= -github.com/wailsapp/wails/v3 v3.0.0-alpha.51/go.mod h1:yaz8baG0+YzoiN8J6osn0wKiEi0iUux0ZU5NsZFu6OQ= +github.com/wailsapp/wails/v3 v3.0.0-alpha.54 h1:XlQ+9dwDtOyfxIyA0h1AeT0zdUP3SFdhLkpIgPjWnWc= +github.com/wailsapp/wails/v3 v3.0.0-alpha.54/go.mod h1:yaz8baG0+YzoiN8J6osn0wKiEi0iUux0ZU5NsZFu6OQ= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= @@ -238,7 +238,6 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -251,5 +250,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -resty.dev/v3 v3.0.0-beta.5 h1:NV1xbqOLzSq7XMTs1t/HLPvu7xrxoXzF90SR4OO6faQ= -resty.dev/v3 v3.0.0-beta.5/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= +resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU= +resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= diff --git a/version.txt b/version.txt index 86ea391..ead9729 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -VERSION=1.5.5 +VERSION=1.5.6 From 76f6c30b9d2370fe877d6f5b2875e9ae226059e7 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Thu, 1 Jan 2026 02:27:21 +0800 Subject: [PATCH 2/7] :art: Optimize code & Upgrade dependencies --- frontend/package-lock.json | 195 ++++++----- frontend/package.json | 7 +- frontend/src/App.vue | 8 +- frontend/src/common/utils/asyncManager.ts | 265 -------------- frontend/src/common/utils/configUtils.ts | 41 +-- frontend/src/common/utils/domDiff.test.ts | 329 ------------------ frontend/src/common/utils/domDiff.ts | 180 ---------- frontend/src/components/tabs/TabContainer.vue | 21 +- .../components/toolbar/DocumentSelector.vue | 23 +- frontend/src/stores/configStore.ts | 202 +++++------ frontend/src/stores/documentStore.ts | 25 +- frontend/src/stores/editorStore.ts | 202 +++-------- frontend/src/stores/tabStore.ts | 34 +- frontend/src/stores/themeStore.ts | 4 +- frontend/src/views/editor/Editor.vue | 9 +- .../src/views/settings/pages/EditingPage.vue | 16 +- .../src/views/settings/pages/GeneralPage.vue | 2 +- 17 files changed, 316 insertions(+), 1247 deletions(-) delete mode 100644 frontend/src/common/utils/asyncManager.ts delete mode 100644 frontend/src/common/utils/domDiff.test.ts delete mode 100644 frontend/src/common/utils/domDiff.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9e3b0b7..8cf1144 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,7 +36,7 @@ "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.3", - "@codemirror/view": "^6.39.6", + "@codemirror/view": "^6.39.8", "@cospaia/prettier-plugin-clojure": "^0.0.2", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.5", @@ -63,7 +63,7 @@ "prettier": "^3.7.4", "sass": "^1.97.1", "vue": "^3.5.26", - "vue-i18n": "^11.2.7", + "vue-i18n": "^11.2.8", "vue-pick-colors": "^1.8.0", "vue-router": "^4.6.4" }, @@ -77,9 +77,8 @@ "eslint": "^9.39.2", "eslint-plugin-vue": "^10.6.2", "globals": "^16.5.0", - "happy-dom": "^20.0.11", "typescript": "^5.9.3", - "typescript-eslint": "^8.50.1", + "typescript-eslint": "^8.51.0", "unplugin-vue-components": "^30.0.0", "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", @@ -610,9 +609,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.39.6", - "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.6.tgz", - "integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==", + "version": "6.39.8", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.8.tgz", + "integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -1341,13 +1340,13 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.7.tgz", - "integrity": "sha512-+Ra9I/LAzXDnmv/IrTO03WMCiLya7pHRmGJvNl9fKwx/W4REJ0xaMk2PxCRqnxcBsX443amEMdebQ3R1geiuIw==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.8.tgz", + "integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==", "license": "MIT", "dependencies": { - "@intlify/message-compiler": "11.2.7", - "@intlify/shared": "11.2.7" + "@intlify/message-compiler": "11.2.8", + "@intlify/shared": "11.2.8" }, "engines": { "node": ">= 16" @@ -1357,12 +1356,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.7.tgz", - "integrity": "sha512-TFamC+GzJAotAFwUNvbtRVBgvuSn2nCwKNresmPUHv3IIVMmXJt7QQJj/DORI1h8hs46ZF6L0Fs2xBohSOE4iQ==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.8.tgz", + "integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.2.7", + "@intlify/shared": "11.2.8", "source-map-js": "^1.0.2" }, "engines": { @@ -1373,9 +1372,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.7.tgz", - "integrity": "sha512-uvlkvc/0uQ4FDlHQZccpUnmcOwNcaI3i+69ck2YJ+GqM35AoVbuS63b+YfirV4G0SZh64Ij2UMcFRMmB4nr95w==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.8.tgz", + "integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==", "license": "MIT", "engines": { "node": ">= 16" @@ -2865,23 +2864,25 @@ "resolved": "https://registry.npmmirror.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", - "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/type-utils": "8.50.1", - "@typescript-eslint/utils": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2891,7 +2892,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.1", + "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2907,16 +2908,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.50.1.tgz", - "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "engines": { @@ -2932,14 +2933,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", - "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.1", - "@typescript-eslint/types": "^8.50.1", + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "engines": { @@ -2954,14 +2955,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", - "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2972,9 +2973,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", - "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", "dev": true, "license": "MIT", "engines": { @@ -2989,17 +2990,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", - "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3014,9 +3015,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.50.1.tgz", - "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -3028,21 +3029,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", - "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.1", - "@typescript-eslint/tsconfig-utils": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3082,16 +3083,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.50.1.tgz", - "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3106,13 +3107,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", - "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -5733,6 +5734,8 @@ "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -5748,6 +5751,8 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5757,7 +5762,9 @@ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -8160,9 +8167,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.3.0.tgz", + "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", "dev": true, "license": "MIT", "engines": { @@ -8231,16 +8238,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.50.1.tgz", - "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.50.1", - "@typescript-eslint/parser": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/utils": "8.50.1" + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8946,13 +8953,13 @@ } }, "node_modules/vue-i18n": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.7.tgz", - "integrity": "sha512-LPv8bAY5OA0UvFEXl4vBQOBqJzRrlExy92tWgRuwW7tbykHf7CH71G2Y4TM2OwGcIS4+hyqKHS2EVBqaYwPY9Q==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.8.tgz", + "integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.2.7", - "@intlify/shared": "11.2.7", + "@intlify/core-base": "11.2.8", + "@intlify/shared": "11.2.8", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -9041,6 +9048,8 @@ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=12" } diff --git a/frontend/package.json b/frontend/package.json index e5b18e1..6155074 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,7 +50,7 @@ "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.3", - "@codemirror/view": "^6.39.6", + "@codemirror/view": "^6.39.8", "@cospaia/prettier-plugin-clojure": "^0.0.2", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.5", @@ -77,7 +77,7 @@ "prettier": "^3.7.4", "sass": "^1.97.1", "vue": "^3.5.26", - "vue-i18n": "^11.2.7", + "vue-i18n": "^11.2.8", "vue-pick-colors": "^1.8.0", "vue-router": "^4.6.4" }, @@ -91,9 +91,8 @@ "eslint": "^9.39.2", "eslint-plugin-vue": "^10.6.2", "globals": "^16.5.0", - "happy-dom": "^20.0.11", "typescript": "^5.9.3", - "typescript-eslint": "^8.50.1", + "typescript-eslint": "^8.51.0", "unplugin-vue-components": "^30.0.0", "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 039bbe5..2725e15 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,6 +7,8 @@ import {useThemeStore} from '@/stores/themeStore'; import {useUpdateStore} from '@/stores/updateStore'; import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue'; import {useTranslationStore} from "@/stores/translationStore"; +import {useI18n} from "vue-i18n"; +import {LanguageType} from "../bindings/voidraft/internal/models"; const configStore = useConfigStore(); const systemStore = useSystemStore(); @@ -14,6 +16,7 @@ const keybindingStore = useKeybindingStore(); const themeStore = useThemeStore(); const updateStore = useUpdateStore(); const translationStore = useTranslationStore(); +const {locale} = useI18n(); onBeforeMount(async () => { // 并行初始化配置、系统信息和快捷键配置 @@ -22,9 +25,8 @@ onBeforeMount(async () => { systemStore.initSystemInfo(), keybindingStore.loadKeyBindings(), ]); - - // 初始化语言和主题 - await configStore.initLanguage(); + + locale.value = configStore.config.appearance.language || LanguageType.LangEnUS; await themeStore.initTheme(); await translationStore.loadTranslators(); diff --git a/frontend/src/common/utils/asyncManager.ts b/frontend/src/common/utils/asyncManager.ts deleted file mode 100644 index 2ad46a1..0000000 --- a/frontend/src/common/utils/asyncManager.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * 操作信息接口 - */ -interface OperationInfo { - controller: AbortController; - createdAt: number; - timeout?: number; - timeoutId?: NodeJS.Timeout; -} - -/** - * 异步操作管理器 - * 用于管理异步操作的竞态条件,确保只有最新的操作有效 - * 支持操作超时和自动清理机制 - * - * @template T 操作上下文的类型 - */ -export class AsyncManager { - private operationSequence = 0; - private pendingOperations = new Map(); - private currentContext: T | null = null; - private defaultTimeout: number; - - /** - * 创建异步操作管理器 - * - * @param defaultTimeout 默认超时时间(毫秒),0表示不设置超时 - */ - constructor(defaultTimeout: number = 0) { - this.defaultTimeout = defaultTimeout; - } - - /** - * 生成新的操作ID - * - * @returns 新的操作ID - */ - getNextOperationId(): number { - return ++this.operationSequence; - } - - /** - * 开始新的操作 - * - * @param context 操作上下文 - * @param options 操作选项 - * @returns 操作ID和AbortController - */ - startOperation( - context: T, - options?: { - excludeId?: number; - timeout?: number; - } - ): { operationId: number; abortController: AbortController } { - const operationId = this.getNextOperationId(); - const abortController = new AbortController(); - const timeout = options?.timeout ?? this.defaultTimeout; - - // 取消之前的操作 - this.cancelPreviousOperations(options?.excludeId); - - // 创建操作信息 - const operationInfo: OperationInfo = { - controller: abortController, - createdAt: Date.now(), - timeout: timeout > 0 ? timeout : undefined - }; - - // 设置超时处理 - if (timeout > 0) { - operationInfo.timeoutId = setTimeout(() => { - this.cancelOperation(operationId, 'timeout'); - }, timeout); - } - - // 设置当前上下文和操作 - this.currentContext = context; - this.pendingOperations.set(operationId, operationInfo); - - return { operationId, abortController }; - } - - /** - * 检查操作是否仍然有效 - * - * @param operationId 操作ID - * @param context 操作上下文 - * @returns 操作是否有效 - */ - isOperationValid(operationId: number, context?: T): boolean { - const operationInfo = this.pendingOperations.get(operationId); - const contextValid = context === undefined || this.currentContext === context; - - return ( - operationInfo !== undefined && - !operationInfo.controller.signal.aborted && - contextValid - ); - } - - /** - * 完成操作 - * - * @param operationId 操作ID - */ - completeOperation(operationId: number): void { - const operationInfo = this.pendingOperations.get(operationId); - if (operationInfo) { - // 清理超时定时器 - if (operationInfo.timeoutId) { - clearTimeout(operationInfo.timeoutId); - } - this.pendingOperations.delete(operationId); - } - } - - /** - * 取消指定操作 - * - * @param operationId 操作ID - * @param reason 取消原因 - */ - cancelOperation(operationId: number, reason?: string): void { - const operationInfo = this.pendingOperations.get(operationId); - if (operationInfo) { - // 清理超时定时器 - if (operationInfo.timeoutId) { - clearTimeout(operationInfo.timeoutId); - } - // 取消操作 - operationInfo.controller.abort(reason); - this.pendingOperations.delete(operationId); - } - } - - /** - * 取消之前的操作(修复并发bug) - * - * @param excludeId 要排除的操作ID(不取消该操作) - */ - cancelPreviousOperations(excludeId?: number): void { - // 创建要取消的操作ID数组,避免在遍历时修改Map - const operationIdsToCancel: number[] = []; - - for (const [operationId] of this.pendingOperations) { - if (excludeId === undefined || operationId !== excludeId) { - operationIdsToCancel.push(operationId); - } - } - - // 批量取消操作 - for (const operationId of operationIdsToCancel) { - this.cancelOperation(operationId, 'superseded'); - } - } - - /** - * 取消所有操作 - */ - cancelAllOperations(): void { - // 创建要取消的操作ID数组,避免在遍历时修改Map - const operationIdsToCancel = Array.from(this.pendingOperations.keys()); - - // 批量取消操作 - for (const operationId of operationIdsToCancel) { - this.cancelOperation(operationId, 'cancelled'); - } - this.currentContext = null; - } - - /** - * 清理过期操作(手动清理超时操作) - * - * @param maxAge 最大存活时间(毫秒) - * @returns 清理的操作数量 - */ - cleanupExpiredOperations(maxAge: number): number { - const now = Date.now(); - const expiredOperationIds: number[] = []; - - for (const [operationId, operationInfo] of this.pendingOperations) { - if (now - operationInfo.createdAt > maxAge) { - expiredOperationIds.push(operationId); - } - } - - // 批量取消过期操作 - for (const operationId of expiredOperationIds) { - this.cancelOperation(operationId, 'expired'); - } - - return expiredOperationIds.length; - } - - /** - * 获取操作统计信息 - * - * @returns 操作统计信息 - */ - getOperationStats(): { - total: number; - withTimeout: number; - averageAge: number; - oldestAge: number; - } { - const now = Date.now(); - let withTimeout = 0; - let totalAge = 0; - let oldestAge = 0; - - for (const operationInfo of this.pendingOperations.values()) { - const age = now - operationInfo.createdAt; - totalAge += age; - oldestAge = Math.max(oldestAge, age); - - if (operationInfo.timeout) { - withTimeout++; - } - } - - return { - total: this.pendingOperations.size, - withTimeout, - averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0, - oldestAge - }; - } - - /** - * 获取当前上下文 - * - * @returns 当前上下文 - */ - getCurrentContext(): T | null { - return this.currentContext; - } - - /** - * 设置当前上下文 - * - * @param context 新的上下文 - */ - setCurrentContext(context: T | null): void { - this.currentContext = context; - } - - /** - * 获取待处理操作数量 - * - * @returns 待处理操作数量 - */ - get pendingCount(): number { - return this.pendingOperations.size; - } - - /** - * 检查是否有待处理的操作 - * - * @returns 是否有待处理的操作 - */ - hasPendingOperations(): boolean { - return this.pendingOperations.size > 0; - } -} \ No newline at end of file diff --git a/frontend/src/common/utils/configUtils.ts b/frontend/src/common/utils/configUtils.ts index 267da48..f6559bc 100644 --- a/frontend/src/common/utils/configUtils.ts +++ b/frontend/src/common/utils/configUtils.ts @@ -1,42 +1,13 @@ -import { LanguageType } from '@/../bindings/voidraft/internal/models/models'; -import type { SupportedLocaleType } from '@/common/constant/locales'; - /** * 配置工具类 */ export class ConfigUtils { - /** - * 将后端语言类型转换为前端语言代码 - */ - static backendLanguageToFrontend(language: LanguageType): SupportedLocaleType { - return language === LanguageType.LangZhCN ? 'zh-CN' : 'en-US'; - } - /** - * 将前端语言代码转换为后端语言类型 - */ - static frontendLanguageToBackend(locale: SupportedLocaleType): LanguageType { - return locale === 'zh-CN' ? LanguageType.LangZhCN : LanguageType.LangEnUS; - } + /** + * 验证数值是否在指定范围内 + */ + static clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); + } - /** - * 验证数值是否在指定范围内 - */ - static clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(max, value)); - } - - /** - * 验证配置值是否有效 - */ - static isValidConfigValue(value: T, validValues: readonly T[]): boolean { - return validValues.includes(value); - } - - /** - * 获取配置的默认值 - */ - static getDefaultValue(key: string, defaults: Record): T { - return defaults[key]?.default; - } } \ No newline at end of file diff --git a/frontend/src/common/utils/domDiff.test.ts b/frontend/src/common/utils/domDiff.test.ts deleted file mode 100644 index 5b11d6f..0000000 --- a/frontend/src/common/utils/domDiff.test.ts +++ /dev/null @@ -1,329 +0,0 @@ -/** - * DOM Diff 算法单元测试 - */ -import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import { morphNode, morphHTML, morphWithKeys } from './domDiff'; - -describe('DOM Diff Algorithm', () => { - let container: HTMLElement; - - beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - }); - - describe('morphNode - 基础功能', () => { - test('应该更新文本节点内容', () => { - const fromNode = document.createTextNode('Hello'); - const toNode = document.createTextNode('World'); - container.appendChild(fromNode); - - morphNode(fromNode, toNode); - - expect(fromNode.nodeValue).toBe('World'); - }); - - test('应该保持相同的文本节点不变', () => { - const fromNode = document.createTextNode('Hello'); - const toNode = document.createTextNode('Hello'); - container.appendChild(fromNode); - - const originalNode = fromNode; - morphNode(fromNode, toNode); - - expect(fromNode).toBe(originalNode); - expect(fromNode.nodeValue).toBe('Hello'); - }); - - test('应该替换不同类型的节点', () => { - const fromNode = document.createElement('span'); - fromNode.textContent = 'Hello'; - const toNode = document.createElement('div'); - toNode.textContent = 'World'; - container.appendChild(fromNode); - - morphNode(fromNode, toNode); - - expect(container.firstChild?.nodeName).toBe('DIV'); - expect(container.firstChild?.textContent).toBe('World'); - }); - }); - - describe('morphNode - 属性更新', () => { - test('应该添加新属性', () => { - const fromEl = document.createElement('div'); - const toEl = document.createElement('div'); - toEl.setAttribute('class', 'test'); - toEl.setAttribute('id', 'myid'); - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.getAttribute('class')).toBe('test'); - expect(fromEl.getAttribute('id')).toBe('myid'); - }); - - test('应该更新已存在的属性', () => { - const fromEl = document.createElement('div'); - fromEl.setAttribute('class', 'old'); - const toEl = document.createElement('div'); - toEl.setAttribute('class', 'new'); - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.getAttribute('class')).toBe('new'); - }); - - test('应该删除不存在的属性', () => { - const fromEl = document.createElement('div'); - fromEl.setAttribute('class', 'test'); - fromEl.setAttribute('id', 'myid'); - const toEl = document.createElement('div'); - toEl.setAttribute('class', 'test'); - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.getAttribute('class')).toBe('test'); - expect(fromEl.hasAttribute('id')).toBe(false); - }); - }); - - describe('morphNode - 子节点更新', () => { - test('应该添加新子节点', () => { - const fromEl = document.createElement('ul'); - fromEl.innerHTML = '
  • 1
  • 2
  • '; - const toEl = document.createElement('ul'); - toEl.innerHTML = '
  • 1
  • 2
  • 3
  • '; - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.children.length).toBe(3); - expect(fromEl.children[2].textContent).toBe('3'); - }); - - test('应该删除多余的子节点', () => { - const fromEl = document.createElement('ul'); - fromEl.innerHTML = '
  • 1
  • 2
  • 3
  • '; - const toEl = document.createElement('ul'); - toEl.innerHTML = '
  • 1
  • 2
  • '; - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.children.length).toBe(2); - expect(fromEl.textContent).toBe('12'); - }); - - test('应该更新子节点内容', () => { - const fromEl = document.createElement('div'); - fromEl.innerHTML = '

    Old

    '; - const toEl = document.createElement('div'); - toEl.innerHTML = '

    New

    '; - container.appendChild(fromEl); - - const originalP = fromEl.querySelector('p'); - morphNode(fromEl, toEl); - - // 应该保持同一个 p 元素,只更新内容 - expect(fromEl.querySelector('p')).toBe(originalP); - expect(fromEl.querySelector('p')?.textContent).toBe('New'); - }); - }); - - describe('morphHTML - HTML 字符串更新', () => { - test('应该从 HTML 字符串更新元素', () => { - const element = document.createElement('div'); - element.innerHTML = '

    Old

    '; - container.appendChild(element); - - morphHTML(element, '

    New

    '); - - expect(element.innerHTML).toBe('

    New

    '); - }); - - test('应该处理复杂的 HTML 结构', () => { - const element = document.createElement('div'); - element.innerHTML = '

    Title

    Paragraph

    '; - container.appendChild(element); - - morphHTML(element, '

    New Title

    New Paragraph

    Extra'); - - expect(element.children.length).toBe(3); - expect(element.querySelector('h1')?.textContent).toBe('New Title'); - expect(element.querySelector('p')?.textContent).toBe('New Paragraph'); - expect(element.querySelector('span')?.textContent).toBe('Extra'); - }); - }); - - describe('morphWithKeys - 基于 key 的智能 diff', () => { - test('应该保持相同 key 的节点', () => { - const fromEl = document.createElement('ul'); - fromEl.innerHTML = ` -
  • A
  • -
  • B
  • -
  • C
  • - `; - const toEl = document.createElement('ul'); - toEl.innerHTML = ` -
  • A Updated
  • -
  • B
  • -
  • C
  • - `; - container.appendChild(fromEl); - - const originalA = fromEl.querySelector('[data-key="a"]'); - morphWithKeys(fromEl, toEl); - - expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA); - expect(originalA?.textContent).toBe('A Updated'); - }); - - test('应该重新排序节点', () => { - const fromEl = document.createElement('ul'); - fromEl.innerHTML = ` -
  • A
  • -
  • B
  • -
  • C
  • - `; - const toEl = document.createElement('ul'); - toEl.innerHTML = ` -
  • C
  • -
  • A
  • -
  • B
  • - `; - container.appendChild(fromEl); - - morphWithKeys(fromEl, toEl); - - const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key')); - expect(keys).toEqual(['c', 'a', 'b']); - }); - - test('应该添加新的 key 节点', () => { - const fromEl = document.createElement('ul'); - fromEl.innerHTML = ` -
  • A
  • -
  • B
  • - `; - const toEl = document.createElement('ul'); - toEl.innerHTML = ` -
  • A
  • -
  • B
  • -
  • C
  • - `; - container.appendChild(fromEl); - - morphWithKeys(fromEl, toEl); - - expect(fromEl.children.length).toBe(3); - expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C'); - }); - - test('应该删除不存在的 key 节点', () => { - const fromEl = document.createElement('ul'); - fromEl.innerHTML = ` -
  • A
  • -
  • B
  • -
  • C
  • - `; - const toEl = document.createElement('ul'); - toEl.innerHTML = ` -
  • A
  • -
  • C
  • - `; - container.appendChild(fromEl); - - morphWithKeys(fromEl, toEl); - - expect(fromEl.children.length).toBe(2); - expect(fromEl.querySelector('[data-key="b"]')).toBeNull(); - }); - }); - - describe('性能测试', () => { - test('应该高效处理大量节点', () => { - const fromEl = document.createElement('ul'); - for (let i = 0; i < 1000; i++) { - const li = document.createElement('li'); - li.textContent = `Item ${i}`; - fromEl.appendChild(li); - } - - const toEl = document.createElement('ul'); - for (let i = 0; i < 1000; i++) { - const li = document.createElement('li'); - li.textContent = `Updated Item ${i}`; - toEl.appendChild(li); - } - - container.appendChild(fromEl); - - const startTime = performance.now(); - morphNode(fromEl, toEl); - const endTime = performance.now(); - - expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成 - expect(fromEl.children.length).toBe(1000); - expect(fromEl.children[0].textContent).toBe('Updated Item 0'); - }); - }); - - describe('边界情况', () => { - test('应该处理空节点', () => { - const fromEl = document.createElement('div'); - const toEl = document.createElement('div'); - container.appendChild(fromEl); - - expect(() => morphNode(fromEl, toEl)).not.toThrow(); - }); - - test('应该处理只有文本的节点', () => { - const fromEl = document.createElement('div'); - fromEl.textContent = 'Hello'; - const toEl = document.createElement('div'); - toEl.textContent = 'World'; - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.textContent).toBe('World'); - }); - - test('应该处理嵌套的复杂结构', () => { - const fromEl = document.createElement('div'); - fromEl.innerHTML = ` -
    -
    - Text -
    -
    - `; - - const toEl = document.createElement('div'); - toEl.innerHTML = ` -
    -
    - Updated Text - New -
    -
    - `; - - container.appendChild(fromEl); - - morphNode(fromEl, toEl); - - expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true); - expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text'); - expect(fromEl.querySelector('strong')?.textContent).toBe('New'); - }); - }); -}); - diff --git a/frontend/src/common/utils/domDiff.ts b/frontend/src/common/utils/domDiff.ts deleted file mode 100644 index b898338..0000000 --- a/frontend/src/common/utils/domDiff.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * 轻量级 DOM Diff 算法实现 - * 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动 - */ - -/** - * 比较并更新两个 DOM 节点 - * @param fromNode 原节点 - * @param toNode 目标节点 - */ -export function morphNode(fromNode: Node, toNode: Node): void { - // 节点类型不同,直接替换 - if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) { - fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode); - return; - } - - // 文本节点:比较内容 - if (fromNode.nodeType === Node.TEXT_NODE) { - if (fromNode.nodeValue !== toNode.nodeValue) { - fromNode.nodeValue = toNode.nodeValue; - } - return; - } - - // 元素节点:更新属性和子节点 - if (fromNode.nodeType === Node.ELEMENT_NODE) { - const fromEl = fromNode as Element; - const toEl = toNode as Element; - - // 更新属性 - morphAttributes(fromEl, toEl); - - // 更新子节点 - morphChildren(fromEl, toEl); - } -} - -/** - * 更新元素属性 - */ -function morphAttributes(fromEl: Element, toEl: Element): void { - // 移除旧属性 - const fromAttrs = fromEl.attributes; - for (let i = fromAttrs.length - 1; i >= 0; i--) { - const attr = fromAttrs[i]; - if (!toEl.hasAttribute(attr.name)) { - fromEl.removeAttribute(attr.name); - } - } - - // 添加/更新新属性 - const toAttrs = toEl.attributes; - for (let i = 0; i < toAttrs.length; i++) { - const attr = toAttrs[i]; - const fromValue = fromEl.getAttribute(attr.name); - if (fromValue !== attr.value) { - fromEl.setAttribute(attr.name, attr.value); - } - } -} - -/** - * 更新子节点(核心 diff 算法) - */ -function morphChildren(fromEl: Element, toEl: Element): void { - const fromChildren = Array.from(fromEl.childNodes); - const toChildren = Array.from(toEl.childNodes); - - const fromLen = fromChildren.length; - const toLen = toChildren.length; - const minLen = Math.min(fromLen, toLen); - - // 1. 更新公共部分 - for (let i = 0; i < minLen; i++) { - morphNode(fromChildren[i], toChildren[i]); - } - - // 2. 移除多余的旧节点 - if (fromLen > toLen) { - for (let i = fromLen - 1; i >= toLen; i--) { - fromEl.removeChild(fromChildren[i]); - } - } - - // 3. 添加新节点 - if (toLen > fromLen) { - for (let i = fromLen; i < toLen; i++) { - fromEl.appendChild(toChildren[i].cloneNode(true)); - } - } -} - -/** - * 优化版:使用 key 进行更智能的 diff(可选) - * 适用于有 data-key 属性的元素 - */ -export function morphWithKeys(fromEl: Element, toEl: Element): void { - const toChildren = Array.from(toEl.children) as Element[]; - - // 构建 from 的 key 映射 - const fromKeyMap = new Map(); - Array.from(fromEl.children).forEach((child) => { - const key = child.getAttribute('data-key'); - if (key) { - fromKeyMap.set(key, child); - } - }); - - const processedKeys = new Set(); - - // 按照 toChildren 的顺序处理 - toChildren.forEach((toChild, toIndex) => { - const key = toChild.getAttribute('data-key'); - if (!key) return; - - processedKeys.add(key); - const fromChild = fromKeyMap.get(key); - - if (fromChild) { - // 找到对应节点,更新内容 - morphNode(fromChild, toChild); - - // 确保节点在正确的位置 - const currentNode = fromEl.children[toIndex]; - if (currentNode !== fromChild) { - // 将 fromChild 移动到正确位置 - fromEl.insertBefore(fromChild, currentNode); - } - } else { - // 新节点,插入到正确位置 - const currentNode = fromEl.children[toIndex]; - fromEl.insertBefore(toChild.cloneNode(true), currentNode || null); - } - }); - - // 删除不再存在的节点(从后往前删除,避免索引问题) - const childrenToRemove: Element[] = []; - fromKeyMap.forEach((child, key) => { - if (!processedKeys.has(key)) { - childrenToRemove.push(child); - } - }); - childrenToRemove.forEach(child => { - fromEl.removeChild(child); - }); -} - -/** - * 高级 API:直接从 HTML 字符串更新元素 - */ -export function morphHTML(element: Element, htmlString: string): void { - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = htmlString; - - // 更新元素的子节点列表 - morphChildren(element, tempContainer); -} - -/** - * 批量更新(使用 DocumentFragment) - */ -export function batchMorph(element: Element, htmlString: string): void { - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = htmlString; - - const fragment = document.createDocumentFragment(); - Array.from(tempContainer.childNodes).forEach(node => { - fragment.appendChild(node); - }); - - // 清空原内容 - while (element.firstChild) { - element.removeChild(element.firstChild); - } - - // 批量插入 - element.appendChild(fragment); -} - diff --git a/frontend/src/components/tabs/TabContainer.vue b/frontend/src/components/tabs/TabContainer.vue index 55a824a..2a37e23 100644 --- a/frontend/src/components/tabs/TabContainer.vue +++ b/frontend/src/components/tabs/TabContainer.vue @@ -7,7 +7,7 @@ v-for="tab in tabStore.tabs" :key="tab.documentId" :tab="tab" - :isActive="tab.documentId === tabStore.currentDocumentId" + :isActive="tab.documentId === documentStore.currentDocumentId" :canClose="tabStore.canCloseTab" @click="switchToTab" @close="closeTab" @@ -35,8 +35,12 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'; import TabItem from './TabItem.vue'; import TabContextMenu from './TabContextMenu.vue'; import { useTabStore } from '@/stores/tabStore'; +import { useDocumentStore } from '@/stores/documentStore'; +import { useEditorStore } from '@/stores/editorStore'; const tabStore = useTabStore(); +const documentStore = useDocumentStore(); +const editorStore = useEditorStore(); // DOM 引用 const tabBarRef = ref(); @@ -50,8 +54,17 @@ const contextMenuTargetId = ref(null); // 标签页操作 -const switchToTab = (documentId: number) => { - tabStore.switchToTabAndDocument(documentId); +const switchToTab = async (documentId: number) => { + await tabStore.switchToTabAndDocument(documentId); + + const doc = documentStore.currentDocument; + if (doc && doc.id !== undefined && editorStore.hasContainer) { + await editorStore.loadEditor(doc.id, doc.content || ''); + } + + if (doc && tabStore.isTabsEnabled) { + tabStore.addOrActivateTab(doc); + } }; const closeTab = (documentId: number) => { @@ -150,7 +163,7 @@ onUnmounted(() => { }); // 监听当前活跃标签页的变化 -watch(() => tabStore.currentDocumentId, () => { +watch(() => documentStore.currentDocumentId, () => { scrollToActiveTab(); }); diff --git a/frontend/src/components/toolbar/DocumentSelector.vue b/frontend/src/components/toolbar/DocumentSelector.vue index 77eb67f..743f536 100644 --- a/frontend/src/components/toolbar/DocumentSelector.vue +++ b/frontend/src/components/toolbar/DocumentSelector.vue @@ -2,6 +2,7 @@ import {computed, nextTick, reactive, ref, watch} from 'vue'; import {useDocumentStore} from '@/stores/documentStore'; import {useTabStore} from '@/stores/tabStore'; +import {useEditorStore} from '@/stores/editorStore'; import {useWindowStore} from '@/stores/windowStore'; import {useI18n} from 'vue-i18n'; import {useConfirm} from '@/composables'; @@ -16,6 +17,7 @@ interface DocumentItem extends Document { const documentStore = useDocumentStore(); const tabStore = useTabStore(); +const editorStore = useEditorStore(); const windowStore = useWindowStore(); const {t} = useI18n(); @@ -103,13 +105,20 @@ const selectDoc = async (doc: Document) => { return; } + const success = await documentStore.openDocument(doc.id); - if (success) { - if (tabStore.isTabsEnabled) { - tabStore.addOrActivateTab(doc); - } - closeMenu(); + if (!success) return; + + const fullDoc = documentStore.currentDocument; + if (fullDoc && editorStore.hasContainer) { + await editorStore.loadEditor(fullDoc.id!, fullDoc.content || ''); } + + if (fullDoc && tabStore.isTabsEnabled) { + tabStore.addOrActivateTab(fullDoc); + } + + closeMenu(); }; const createDoc = async (title: string) => { @@ -190,6 +199,10 @@ const openInNewWindow = async (doc: Document, event: Event) => { event.stopPropagation(); if (doc.id === undefined) return; try { + // 在打开新窗口前,如果启用了标签且该文档有标签,先关闭标签 + if (tabStore.isTabsEnabled && tabStore.hasTab(doc.id)) { + await tabStore.closeTab(doc.id); + } await documentStore.openDocumentInNewWindow(doc.id); } catch (error) { console.error('Failed to open document in new window:', error); diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index eb51e92..9962061 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -1,18 +1,10 @@ import {defineStore} from 'pinia'; import {computed, reactive} from 'vue'; import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services'; -import { - AppConfig, - AuthMethod, - EditingConfig, - LanguageType, - SystemThemeType, - TabType -} from '@/../bindings/voidraft/internal/models/models'; +import {AppConfig, AuthMethod, LanguageType, SystemThemeType, TabType} from '@/../bindings/voidraft/internal/models/models'; import {useI18n} from 'vue-i18n'; import {ConfigUtils} from '@/common/utils/configUtils'; import {FONT_OPTIONS} from '@/common/constant/fonts'; -import {SUPPORTED_LOCALES} from '@/common/constant/locales'; import { CONFIG_KEY_MAP, CONFIG_LIMITS, @@ -36,12 +28,6 @@ export const useConfigStore = defineStore('config', () => { // Font options (no longer localized) const fontOptions = computed(() => FONT_OPTIONS); - // 计算属性 - const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]); - const limits = Object.fromEntries( - (['fontSize', 'tabSize', 'lineHeight'] as const).map(key => [key, createLimitComputed(key)]) - ) as Record>; - // 统一配置更新方法 const updateConfig = async (key: K, value: any): Promise => { if (!state.configLoaded && !state.isLoading) { @@ -99,39 +85,12 @@ export const useConfigStore = defineStore('config', () => { } }; - // 通用数值调整器工厂 - const createAdjuster = (key: T) => { - const limit = CONFIG_LIMITS[key]; - const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max); - - return { - increase: async () => await updateConfig(key, clamp(state.config.editing[key] + 1)), - decrease: async () => await updateConfig(key, clamp(state.config.editing[key] - 1)), - set: async (value: number) => await updateConfig(key, clamp(value)), - reset: async () => await updateConfig(key, limit.default), - increaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] + 1)), - decreaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] - 1)) - }; - }; - - const createEditingToggler = (key: T) => - async () => await updateConfig(key as ConfigKey, !state.config.editing[key] as EditingConfig[T]); - - // 枚举值切换器 - const createEnumToggler = (key: 'tabType', values: readonly T[]) => - async () => { - const currentIndex = values.indexOf(state.config.editing[key] as T); - const nextIndex = (currentIndex + 1) % values.length; - return await updateConfig(key, values[nextIndex]); - }; - // 重置配置 const resetConfig = async (): Promise => { if (state.isLoading) return; state.isLoading = true; try { - await ConfigService.ResetConfig(); const appConfig = await ConfigService.GetConfig(); if (appConfig) { @@ -142,57 +101,25 @@ export const useConfigStore = defineStore('config', () => { } }; - // 语言设置方法 - const setLanguage = async (language: LanguageType): Promise => { - await updateConfig('language', language); - const frontendLocale = ConfigUtils.backendLanguageToFrontend(language); - locale.value = frontendLocale as any; + // 辅助函数:限制数值范围 + const clampValue = (value: number, key: NumberConfigKey): number => { + const limit = CONFIG_LIMITS[key]; + return ConfigUtils.clamp(value, limit.min, limit.max); }; - // 系统主题设置方法 - const setSystemTheme = async (systemTheme: SystemThemeType): Promise => { - await updateConfig('systemTheme', systemTheme); - }; + // 计算属性 + const fontConfig = computed(() => ({ + fontSize: state.config.editing.fontSize, + fontFamily: state.config.editing.fontFamily, + lineHeight: state.config.editing.lineHeight, + fontWeight: state.config.editing.fontWeight + })); - // 当前主题设置方法 - const setCurrentTheme = async (themeName: string): Promise => { - await updateConfig('currentTheme', themeName); - }; - - - // 初始化语言设置 - const initLanguage = async (): Promise => { - try { - // 如果配置未加载,先加载配置 - if (!state.configLoaded) { - await initConfig(); - } - - // 同步前端语言设置 - const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language); - locale.value = frontendLocale as any; - } catch (_error) { - const browserLang = SUPPORTED_LOCALES[0].code; - locale.value = browserLang as any; - } - }; - - // 创建数值调整器实例 - const adjusters = { - fontSize: createAdjuster('fontSize'), - tabSize: createAdjuster('tabSize'), - lineHeight: createAdjuster('lineHeight') - }; - - // 创建切换器实例 - const togglers = { - tabIndent: createEditingToggler('enableTabIndent'), - alwaysOnTop: async () => { - await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop); - await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop); - }, - tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values) - }; + const tabConfig = computed(() => ({ + tabSize: state.config.editing.tabSize, + enableTabIndent: state.config.editing.enableTabIndent, + tabType: state.config.editing.tabType + })); return { // 状态 @@ -200,53 +127,84 @@ export const useConfigStore = defineStore('config', () => { configLoaded: computed(() => state.configLoaded), isLoading: computed(() => state.isLoading), fontOptions, - - // 限制常量 - ...limits, + fontConfig, + tabConfig, // 核心方法 initConfig, resetConfig, // 语言相关方法 - setLanguage, - initLanguage, + setLanguage: (value: LanguageType) => { + updateConfig('language', value); + locale.value = value as any; + }, // 主题相关方法 - setSystemTheme, - setCurrentTheme, + setSystemTheme: (value: SystemThemeType) => updateConfig('systemTheme', value), + setCurrentTheme: (value: string) => updateConfig('currentTheme', value), // 字体大小操作 - ...adjusters.fontSize, - increaseFontSize: adjusters.fontSize.increase, - decreaseFontSize: adjusters.fontSize.decrease, - resetFontSize: adjusters.fontSize.reset, - setFontSize: adjusters.fontSize.set, - // 字体大小操作 - increaseFontSizeLocal: adjusters.fontSize.increaseLocal, - decreaseFontSizeLocal: adjusters.fontSize.decreaseLocal, - saveFontSize: () => saveConfig('fontSize'), - - // Tab操作 - toggleTabIndent: togglers.tabIndent, - setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value), - ...adjusters.tabSize, - increaseTabSize: adjusters.tabSize.increase, - decreaseTabSize: adjusters.tabSize.decrease, - setTabSize: adjusters.tabSize.set, - toggleTabType: togglers.tabType, - - // 行高操作 - setLineHeight: adjusters.lineHeight.set, - - // 窗口操作 - toggleAlwaysOnTop: togglers.alwaysOnTop, - setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value), + setFontSize: async (value: number) => { + await updateConfig('fontSize', clampValue(value, 'fontSize')); + }, + increaseFontSize: async () => { + const newValue = state.config.editing.fontSize + 1; + await updateConfig('fontSize', clampValue(newValue, 'fontSize')); + }, + decreaseFontSize: async () => { + const newValue = state.config.editing.fontSize - 1; + await updateConfig('fontSize', clampValue(newValue, 'fontSize')); + }, + resetFontSize: async () => { + await updateConfig('fontSize', CONFIG_LIMITS.fontSize.default); + }, + increaseFontSizeLocal: () => { + updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize + 1, 'fontSize')); + }, + decreaseFontSizeLocal: () => { + updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize - 1, 'fontSize')); + }, + saveFontSize: async () => { + await saveConfig('fontSize'); + }, // 字体操作 setFontFamily: (value: string) => updateConfig('fontFamily', value), setFontWeight: (value: string) => updateConfig('fontWeight', value), + // 行高操作 + setLineHeight: async (value: number) => { + await updateConfig('lineHeight', clampValue(value, 'lineHeight')); + }, + + // Tab操作 + setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value), + setTabSize: async (value: number) => { + await updateConfig('tabSize', clampValue(value, 'tabSize')); + }, + increaseTabSize: async () => { + const newValue = state.config.editing.tabSize + 1; + await updateConfig('tabSize', clampValue(newValue, 'tabSize')); + }, + decreaseTabSize: async () => { + const newValue = state.config.editing.tabSize - 1; + await updateConfig('tabSize', clampValue(newValue, 'tabSize')); + }, + toggleTabType: async () => { + const values = CONFIG_LIMITS.tabType.values; + const currentIndex = values.indexOf(state.config.editing.tabType as typeof values[number]); + const nextIndex = (currentIndex + 1) % values.length; + await updateConfig('tabType', values[nextIndex]); + }, + + // 窗口操作 + toggleAlwaysOnTop: async () => { + await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop); + await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop); + }, + setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value), + // 路径操作 setDataPath: (value: string) => updateConfigLocal('dataPath', value), diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts index c5ee635..dd584aa 100644 --- a/frontend/src/stores/documentStore.ts +++ b/frontend/src/stores/documentStore.ts @@ -3,7 +3,6 @@ import {computed, ref} from 'vue'; import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice'; import {Document} from '@/../bindings/voidraft/internal/models/ent/models'; -import {useTabStore} from "@/stores/tabStore"; import type {EditorViewState} from '@/stores/editorStore'; export const useDocumentStore = defineStore('document', () => { @@ -70,10 +69,6 @@ export const useDocumentStore = defineStore('document', () => { // 在新窗口中打开文档 const openDocumentInNewWindow = async (docId: number): Promise => { try { - const tabStore = useTabStore(); - if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) { - tabStore.closeTab(docId); - } await OpenDocumentWindow(docId); return true; } catch (error) { @@ -112,7 +107,7 @@ export const useDocumentStore = defineStore('document', () => { } }; - // 打开文档 + // 打开文档 - 只负责文档数据管理 const openDocument = async (docId: number): Promise => { try { // 获取完整文档数据 @@ -131,7 +126,7 @@ export const useDocumentStore = defineStore('document', () => { } }; - // 更新文档元数据 + // 更新文档元数据 - 只负责文档数据管理 const updateDocumentMetadata = async (docId: number, title: string): Promise => { try { await DocumentService.UpdateDocumentTitle(docId, title); @@ -148,10 +143,6 @@ export const useDocumentStore = defineStore('document', () => { currentDocument.value.updated_at = new Date().toISOString(); } - // 同步更新标签页标题 - const tabStore = useTabStore(); - tabStore.updateTabTitle(docId, title); - return true; } catch (error) { console.error('Failed to update document metadata:', error); @@ -159,7 +150,7 @@ export const useDocumentStore = defineStore('document', () => { } }; - // 删除文档 + // 删除文档 - 只负责文档数据管理 const deleteDocument = async (docId: number): Promise => { try { await DocumentService.DeleteDocument(docId); @@ -167,12 +158,6 @@ export const useDocumentStore = defineStore('document', () => { // 更新本地状态 delete documents.value[docId]; - // 同步清理标签页 - const tabStore = useTabStore(); - if (tabStore.hasTab(docId)) { - tabStore.closeTab(docId); - } - // 如果删除的是当前文档,切换到第一个可用文档 if (currentDocumentId.value === docId) { const availableDocs = Object.values(documents.value); @@ -192,7 +177,7 @@ export const useDocumentStore = defineStore('document', () => { }; // === 初始化 === - const initialize = async (urlDocumentId?: number): Promise => { + const initDocument = async (urlDocumentId?: number): Promise => { try { await getDocumentMetaList(); @@ -235,7 +220,7 @@ export const useDocumentStore = defineStore('document', () => { closeDocumentSelector, setError, clearError, - initialize, + initDocument, }; }, { persist: { diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 0688d95..a5b504f 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -1,5 +1,5 @@ import {defineStore} from 'pinia'; -import {computed, nextTick, ref, watch} from 'vue'; +import {computed, nextTick, ref} from 'vue'; import {EditorView} from '@codemirror/view'; import {EditorState, Extension} from '@codemirror/state'; import {useConfigStore} from './configStore'; @@ -24,7 +24,6 @@ import { import {useExtensionStore} from './extensionStore'; import createCodeBlockExtension from "@/views/editor/extensions/codeblock"; import {LruCache} from '@/common/utils/lruCache'; -import {AsyncManager} from '@/common/utils/asyncManager'; import {generateContentHash} from "@/common/utils/hashUtils"; import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; import {EDITOR_CONFIG} from '@/common/constant/editor'; @@ -37,7 +36,6 @@ export interface DocumentStats { selectedCharacters: number; } -// 修复:只保存光标位置,恢复时自动滚动到光标处 export interface EditorViewState { cursorPos: number; } @@ -54,7 +52,6 @@ interface EditorInstance { lastContentHash: string; lastParsed: Date; } | null; - // 修复:使用统一的类型,可选但不是 undefined | {...} editorState?: EditorViewState; } @@ -74,14 +71,9 @@ export const useEditorStore = defineStore('editor', () => { characters: 0, selectedCharacters: 0 }); - + // 编辑器加载状态 const isLoading = ref(false); - // 修复:使用操作计数器精确管理加载状态 - const loadingOperations = ref(0); - - // 异步操作管理器 - const operationManager = new AsyncManager(); // 自动保存设置 - 从配置动态获取 const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay; @@ -91,7 +83,7 @@ export const useEditorStore = defineStore('editor', () => { if (instance) { instance.syntaxTreeCache = null; } - }, { delay: 500 }); // 500ms 内的多次输入只清理一次 + }, {delay: 500}); // 500ms 内的多次输入只清理一次 // 缓存化的语法树确保方法 @@ -106,42 +98,33 @@ export const useEditorStore = defineStore('editor', () => { // 检查是否需要重新构建语法树 const cache = instance.syntaxTreeCache; - const shouldRebuild = !cache || - cache.lastDocLength !== docLength || + const shouldRebuild = !cache || + cache.lastDocLength !== docLength || cache.lastContentHash !== contentHash || (now.getTime() - cache.lastParsed.getTime()) > EDITOR_CONFIG.SYNTAX_TREE_CACHE_TIMEOUT; if (shouldRebuild) { - try { - ensureSyntaxTree(view.state, docLength, 5000); - - // 更新缓存 - instance.syntaxTreeCache = { - lastDocLength: docLength, - lastContentHash: contentHash, - lastParsed: now - }; - } catch (error) { - console.warn('Failed to ensure syntax tree:', error); - } + ensureSyntaxTree(view.state, docLength, 5000); + + // 更新缓存 + instance.syntaxTreeCache = { + lastDocLength: docLength, + lastContentHash: contentHash, + lastParsed: now + }; + } }; // 创建编辑器实例 const createEditorInstance = async ( - content: string, - operationId: number, + content: string, documentId: number ): Promise => { if (!containerElement.value) { throw new Error('Editor container not set'); } - // 检查操作是否仍然有效 - if (!operationManager.isOperationValid(operationId, documentId)) { - throw new Error('Operation cancelled'); - } - // 获取基本扩展 const basicExtensions = createBasicSetup(); @@ -185,27 +168,12 @@ export const useEditorStore = defineStore('editor', () => { // 光标位置持久化扩展 const cursorPositionExtension = createCursorPositionExtension(documentId); - // 再次检查操作有效性 - if (!operationManager.isOperationValid(operationId, documentId)) { - throw new Error('Operation cancelled'); - } - // 快捷键扩展 const keymapExtension = await createDynamicKeymapExtension(); - // 检查操作有效性 - if (!operationManager.isOperationValid(operationId, documentId)) { - throw new Error('Operation cancelled'); - } - // 动态扩展,传递文档ID以便扩展管理器可以预初始化 const dynamicExtensions = await createDynamicExtensions(); - // 最终检查操作有效性 - if (!operationManager.isOperationValid(operationId, documentId)) { - throw new Error('Operation cancelled'); - } - // 组合所有扩展 const extensions: Extension[] = [ keymapExtension, @@ -224,8 +192,8 @@ export const useEditorStore = defineStore('editor', () => { // 获取保存的光标位置 const savedState = documentStore.documentStates[documentId]; const docLength = content.length; - const initialCursorPos = savedState?.cursorPos !== undefined - ? Math.min(savedState.cursorPos, docLength) + const initialCursorPos = savedState?.cursorPos !== undefined + ? Math.min(savedState.cursorPos, docLength) : docLength; @@ -233,7 +201,7 @@ export const useEditorStore = defineStore('editor', () => { const state = EditorState.create({ doc: content, extensions, - selection: { anchor: initialCursorPos, head: initialCursorPos } + selection: {anchor: initialCursorPos, head: initialCursorPos} }); return new EditorView({ @@ -271,9 +239,8 @@ export const useEditorStore = defineStore('editor', () => { // 获取或创建编辑器 const getOrCreateEditor = async ( - documentId: number, - content: string, - operationId: number + documentId: number, + content: string ): Promise => { // 检查缓存 const cached = editorCache.get(documentId); @@ -281,29 +248,8 @@ export const useEditorStore = defineStore('editor', () => { return cached.view; } - // 检查操作是否仍然有效 - if (!operationManager.isOperationValid(operationId, documentId)) { - throw new Error('Operation cancelled'); - } - // 创建新的编辑器实例 - const view = await createEditorInstance(content, operationId, documentId); - - // 完善取消操作时的清理逻辑 - if (!operationManager.isOperationValid(operationId, documentId)) { - // 如果操作已取消,彻底清理创建的实例 - try { - // 移除 DOM 元素(如果已添加到文档) - if (view.dom && view.dom.parentElement) { - view.dom.remove(); - } - // 销毁编辑器视图 - view.destroy(); - } catch (error) { - console.error('Error cleaning up cancelled editor:', error); - } - throw new Error('Operation cancelled'); - } + const view = await createEditorInstance(content, documentId); addEditorToCache(documentId, view, content); @@ -333,10 +279,10 @@ export const useEditorStore = defineStore('editor', () => { requestAnimationFrame(() => { // 滚动到当前光标位置 scrollToCursor(instance.view); - + // 聚焦编辑器 instance.view.focus(); - + // 使用缓存的语法树确保方法 ensureSyntaxTreeCached(instance.view, documentId); }); @@ -354,7 +300,7 @@ export const useEditorStore = defineStore('editor', () => { try { const content = instance.view.state.doc.toString(); const lastModified = instance.lastModified; - + await DocumentService.UpdateDocumentContent(documentId, content); // 检查在保存期间内容是否又被修改了 @@ -381,7 +327,7 @@ export const useEditorStore = defineStore('editor', () => { // 立即设置脏标记和修改时间(切换文档时需要判断) instance.isDirty = true; instance.lastModified = new Date(); - + // 优使用防抖清理语法树缓存 debouncedClearSyntaxCache.debouncedFn(instance); @@ -392,24 +338,18 @@ export const useEditorStore = defineStore('editor', () => { }; + // 检查容器是否已设置 + const hasContainer = computed(() => containerElement.value !== null); + // 设置编辑器容器 const setEditorContainer = (container: HTMLElement | null) => { containerElement.value = container; - - // 如果设置容器时已有当前文档,立即加载编辑器 - if (container && documentStore.currentDocument && documentStore.currentDocument.id !== undefined) { - loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content || ''); - } + // watch 会自动监听并加载编辑器,无需手动调用 }; // 加载编辑器 const loadEditor = async (documentId: number, content: string) => { - // 修复:使用计数器精确管理加载状态 - loadingOperations.value++; isLoading.value = true; - - // 开始新的操作 - const { operationId } = operationManager.startOperation(documentId); try { // 验证参数 @@ -422,33 +362,25 @@ export const useEditorStore = defineStore('editor', () => { const currentDocId = documentStore.currentDocumentId; if (currentDocId && currentDocId !== documentId) { await saveEditorContent(currentDocId); - - // 检查操作是否仍然有效 - if (!operationManager.isOperationValid(operationId, documentId)) { - return; - } } } // 获取或创建编辑器 - const view = await getOrCreateEditor(documentId, content, operationId); + const view = await getOrCreateEditor(documentId, content); - // 检查操作是否仍然有效 - if (!operationManager.isOperationValid(operationId, documentId)) { - return; - } - - // 更新内容(如果需要) + // 更新内容 const instance = editorCache.get(documentId); if (instance && instance.content !== content) { // 确保编辑器视图有效 if (view && view.state && view.dispatch) { + const contentLength = content.length; view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: content - } + }, + selection: {anchor: contentLength, head: contentLength} }); instance.content = content; instance.isDirty = false; @@ -460,32 +392,14 @@ export const useEditorStore = defineStore('editor', () => { } } - // 最终检查操作有效性 - if (!operationManager.isOperationValid(operationId, documentId)) { - return; - } - // 显示编辑器 showEditor(documentId); } catch (error) { - if (error instanceof Error && error.message === 'Operation cancelled') { - console.log(`Editor loading cancelled for document ${documentId}`); - } else { - console.error('Failed to load editor:', error); - } + console.error('Failed to load editor:', error); } finally { - // 完成操作 - operationManager.completeOperation(operationId); - - // 修复:使用计数器精确管理加载状态,避免快速切换时状态不准确 - loadingOperations.value--; - // 延迟一段时间后再取消加载状态,但要确保所有操作都完成了 setTimeout(() => { - if (loadingOperations.value <= 0) { - loadingOperations.value = 0; - isLoading.value = false; - } + isLoading.value = false; }, EDITOR_CONFIG.LOADING_DELAY); } }; @@ -495,12 +409,6 @@ export const useEditorStore = defineStore('editor', () => { const instance = editorCache.get(documentId); if (instance) { try { - // 如果正在加载这个文档,取消操作 - if (operationManager.getCurrentContext() === documentId) { - operationManager.cancelAllOperations(); - } - - // 修复:移除前先保存内容(如果有未保存的修改) if (instance.isDirty) { await saveEditorContent(documentId); } @@ -583,9 +491,6 @@ export const useEditorStore = defineStore('editor', () => { // 清空所有编辑器 const clearAllEditors = () => { - // 取消所有挂起的操作 - operationManager.cancelAllOperations(); - editorCache.clear((_documentId, instance) => { // 清除自动保存定时器 instance.autoSaveTimer.clear(); @@ -598,7 +503,7 @@ export const useEditorStore = defineStore('editor', () => { // 销毁编辑器 instance.view.destroy(); }); - + currentEditor.value = null; }; @@ -606,7 +511,7 @@ export const useEditorStore = defineStore('editor', () => { const updateExtension = async (id: number, enabled: boolean, config?: any) => { // 更新启用状态 await ExtensionService.UpdateExtensionEnabled(id, enabled); - + // 如果需要更新配置 if (config !== undefined) { await ExtensionService.UpdateExtensionConfig(id, config); @@ -614,7 +519,7 @@ export const useEditorStore = defineStore('editor', () => { // 重新加载扩展配置 await extensionStore.loadExtensions(); - + // 获取更新后的扩展名称 const extension = extensionStore.extensions.find(ext => ext.id === id); if (!extension) return; @@ -630,38 +535,12 @@ export const useEditorStore = defineStore('editor', () => { await applyKeymapSettings(); }; - // 监听文档切换 - watch(() => documentStore.currentDocument, async (newDoc, oldDoc) => { - if (newDoc && newDoc.id !== undefined && containerElement.value) { - // 等待 DOM 更新完成,再加载新文档的编辑器 - await nextTick(); - loadEditor(newDoc.id, newDoc.content || ''); - } - }); - - // 创建字体配置的计算属性 - const fontConfig = computed(() => ({ - fontSize: configStore.config.editing.fontSize, - fontFamily: configStore.config.editing.fontFamily, - lineHeight: configStore.config.editing.lineHeight, - fontWeight: configStore.config.editing.fontWeight - })); - // 创建Tab配置的计算属性 - const tabConfig = computed(() => ({ - tabSize: configStore.config.editing.tabSize, - enableTabIndent: configStore.config.editing.enableTabIndent, - tabType: configStore.config.editing.tabType - })); - // 监听字体配置变化 - watch(fontConfig, applyFontSettings, { deep: true }); - // 监听Tab配置变化 - watch(tabConfig, applyTabSettings, { deep: true }); - return { // 状态 currentEditor, documentStats, isLoading, + hasContainer, // 方法 setEditorContainer, @@ -670,7 +549,6 @@ export const useEditorStore = defineStore('editor', () => { clearAllEditors, onContentChange, - // 配置更新方法 applyFontSettings, applyThemeSettings, applyTabSettings, diff --git a/frontend/src/stores/tabStore.ts b/frontend/src/stores/tabStore.ts index dcd1413..bd398bc 100644 --- a/frontend/src/stores/tabStore.ts +++ b/frontend/src/stores/tabStore.ts @@ -18,12 +18,9 @@ export const useTabStore = defineStore('tab', () => { const tabsMap = ref>({}); const tabOrder = ref([]); // 维护标签页顺序 const draggedTabId = ref(null); - - // === 计算属性 === - + const isTabsEnabled = computed(() => configStore.config.general.enableTabs); const canCloseTab = computed(() => tabOrder.value.length > 1); - const currentDocumentId = computed(() => documentStore.currentDocumentId); // 按顺序返回标签页数组(用于UI渲染) const tabs = computed(() => { @@ -75,7 +72,7 @@ export const useTabStore = defineStore('tab', () => { /** * 关闭标签页 */ - const closeTab = (documentId: number) => { + const closeTab = async (documentId: number) => { if (!hasTab(documentId)) return; const tabIndex = tabOrder.value.indexOf(documentId); @@ -95,7 +92,7 @@ export const useTabStore = defineStore('tab', () => { if (nextIndex >= 0 && tabOrder.value[nextIndex]) { const nextDocumentId = tabOrder.value[nextIndex]; - switchToTabAndDocument(nextDocumentId); + await switchToTabAndDocument(nextDocumentId); } } }; @@ -120,15 +117,15 @@ export const useTabStore = defineStore('tab', () => { /** * 切换到指定标签页并打开对应文档 */ - const switchToTabAndDocument = (documentId: number) => { + const switchToTabAndDocument = async (documentId: number) => { if (!hasTab(documentId)) return; - + // 如果点击的是当前已激活的文档,不需要重复请求 if (documentStore.currentDocumentId === documentId) { return; } - documentStore.openDocument(documentId); + await documentStore.openDocument(documentId); }; /** @@ -172,8 +169,8 @@ export const useTabStore = defineStore('tab', () => { /** * 初始化标签页(当前文档) */ - const initializeTab = () => { - // 先验证并清理无效的标签页(处理持久化的脏数据) + const initTab = () => { + // 先验证并清理无效的标签页 validateTabs(); if (isTabsEnabled.value) { @@ -189,7 +186,7 @@ export const useTabStore = defineStore('tab', () => { /** * 关闭其他标签页(除了指定的标签页) */ - const closeOtherTabs = (keepDocumentId: number) => { + const closeOtherTabs = async (keepDocumentId: number) => { if (!hasTab(keepDocumentId)) return; // 获取所有其他标签页的ID @@ -200,14 +197,14 @@ export const useTabStore = defineStore('tab', () => { // 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档 if (otherTabIds.includes(documentStore.currentDocumentId!)) { - switchToTabAndDocument(keepDocumentId); + await switchToTabAndDocument(keepDocumentId); } }; /** * 关闭指定标签页右侧的所有标签页 */ - const closeTabsToRight = (documentId: number) => { + const closeTabsToRight = async (documentId: number) => { const index = getTabIndex(documentId); if (index === -1) return; @@ -219,14 +216,14 @@ export const useTabStore = defineStore('tab', () => { // 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档 if (rightTabIds.includes(documentStore.currentDocumentId!)) { - switchToTabAndDocument(documentId); + await switchToTabAndDocument(documentId); } }; /** * 关闭指定标签页左侧的所有标签页 */ - const closeTabsToLeft = (documentId: number) => { + const closeTabsToLeft = async (documentId: number) => { const index = getTabIndex(documentId); if (index <= 0) return; @@ -238,7 +235,7 @@ export const useTabStore = defineStore('tab', () => { // 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档 if (leftTabIds.includes(documentStore.currentDocumentId!)) { - switchToTabAndDocument(documentId); + await switchToTabAndDocument(documentId); } }; @@ -262,7 +259,6 @@ export const useTabStore = defineStore('tab', () => { // 计算属性 isTabsEnabled, canCloseTab, - currentDocumentId, // 方法 addOrActivateTab, @@ -273,7 +269,7 @@ export const useTabStore = defineStore('tab', () => { switchToTabAndDocument, moveTab, getTabIndex, - initializeTab, + initTab, clearAllTabs, updateTabTitle, validateTabs, diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts index c37c239..2eeea5f 100644 --- a/frontend/src/stores/themeStore.ts +++ b/frontend/src/stores/themeStore.ts @@ -6,6 +6,7 @@ import {ThemeService} from '@/../bindings/voidraft/internal/services'; import {useConfigStore} from './configStore'; import type {ThemeColors} from '@/views/editor/theme/types'; import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets'; +import {useEditorStore} from "@/stores/editorStore"; // 类型定义 type ThemeOption = { name: string; type: ThemeType }; @@ -91,11 +92,12 @@ export const useThemeStore = defineStore('theme', () => { // 同步应用到 DOM 与编辑器 const applyAllThemes = () => { applyThemeToDOM(currentTheme.value); + const editorStore = useEditorStore(); + editorStore.applyThemeSettings(); }; // 初始化主题 const initTheme = async () => { - applyThemeToDOM(currentTheme.value); await loadThemeColors(); applyAllThemes(); }; diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index 4e584a9..2d68ec1 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -28,11 +28,16 @@ onMounted(async () => { const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined; - await documentStore.initialize(urlDocumentId); + await documentStore.initDocument(urlDocumentId); editorStore.setEditorContainer(editorElement.value); - await tabStore.initializeTab(); + const currentDoc = documentStore.currentDocument; + if (currentDoc && currentDoc.id !== undefined) { + await editorStore.loadEditor(currentDoc.id, currentDoc.content || ''); + } + + await tabStore.initTab(); }); onBeforeUnmount(() => { diff --git a/frontend/src/views/settings/pages/EditingPage.vue b/frontend/src/views/settings/pages/EditingPage.vue index 3e70bbc..2574220 100644 --- a/frontend/src/views/settings/pages/EditingPage.vue +++ b/frontend/src/views/settings/pages/EditingPage.vue @@ -1,5 +1,6 @@ diff --git a/frontend/src/components/toast/Toast.vue b/frontend/src/components/toast/Toast.vue new file mode 100644 index 0000000..9a6d64a --- /dev/null +++ b/frontend/src/components/toast/Toast.vue @@ -0,0 +1,292 @@ + + + + + + diff --git a/frontend/src/components/toast/ToastContainer.vue b/frontend/src/components/toast/ToastContainer.vue new file mode 100644 index 0000000..405c47d --- /dev/null +++ b/frontend/src/components/toast/ToastContainer.vue @@ -0,0 +1,168 @@ + + + + + + diff --git a/frontend/src/components/toast/index.ts b/frontend/src/components/toast/index.ts new file mode 100644 index 0000000..9a9b94c --- /dev/null +++ b/frontend/src/components/toast/index.ts @@ -0,0 +1,80 @@ +import { useToastStore } from './toastStore'; +import type { ToastOptions } from './types'; + +class ToastService { + private getStore() { + return useToastStore(); + } + + /** + * 显示一个通知 + */ + show(options: ToastOptions): string { + return this.getStore().add(options); + } + + /** + * 显示成功通知 + */ + success(message: string, title?: string, options?: Partial): string { + return this.show({ + message, + title, + type: 'success', + ...options, + }); + } + + /** + * 显示错误通知 + */ + error(message: string, title?: string, options?: Partial): string { + return this.show({ + message, + title, + type: 'error', + ...options, + }); + } + + /** + * 显示警告通知 + */ + warning(message: string, title?: string, options?: Partial): string { + return this.show({ + message, + title, + type: 'warning', + ...options, + }); + } + + /** + * 显示信息通知 + */ + info(message: string, title?: string, options?: Partial): string { + return this.show({ + message, + title, + type: 'info', + ...options, + }); + } + + /** + * 关闭指定的通知 + */ + close(id: string): void { + this.getStore().remove(id); + } + + /** + * 清空所有通知 + */ + clear(): void { + this.getStore().clear(); + } +} +export const toast = new ToastService(); +export default toast; + diff --git a/frontend/src/components/toast/toastStore.ts b/frontend/src/components/toast/toastStore.ts new file mode 100644 index 0000000..33f9af8 --- /dev/null +++ b/frontend/src/components/toast/toastStore.ts @@ -0,0 +1,55 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { Toast, ToastOptions } from './types'; + +export const useToastStore = defineStore('toast', () => { + const toasts = ref([]); + let idCounter = 0; + + /** + * 添加一个 Toast + */ + const add = (options: ToastOptions): string => { + const id = `toast-${Date.now()}-${idCounter++}`; + + const toast: Toast = { + id, + message: options.message, + type: options.type || 'info', + title: options.title, + duration: options.duration ?? 4000, + position: options.position || 'top-right', + closable: options.closable ?? true, + createdAt: Date.now(), + }; + + toasts.value.push(toast); + + return id; + }; + + /** + * 移除指定 Toast + */ + const remove = (id: string) => { + const index = toasts.value.findIndex(t => t.id === id); + if (index > -1) { + toasts.value.splice(index, 1); + } + }; + + /** + * 清空所有 Toast + */ + const clear = () => { + toasts.value = []; + }; + + return { + toasts, + add, + remove, + clear, + }; +}); + diff --git a/frontend/src/components/toast/types.ts b/frontend/src/components/toast/types.ts new file mode 100644 index 0000000..550fcbb --- /dev/null +++ b/frontend/src/components/toast/types.ts @@ -0,0 +1,52 @@ +/** + * Toast 通知类型定义 + */ + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export type ToastPosition = + | 'top-right' + | 'top-left' + | 'bottom-right' + | 'bottom-left' + | 'top-center' + | 'bottom-center'; + +export interface ToastOptions { + /** + * Toast 消息内容 + */ + message: string; + + /** + * Toast 类型 + */ + type?: ToastType; + + /** + * 标题(可选) + */ + title?: string; + + /** + * 持续时间(毫秒),0 表示不自动关闭 + */ + duration?: number; + + /** + * 显示位置 + */ + position?: ToastPosition; + + /** + * 是否可关闭 + */ + closable?: boolean; +} + +export interface Toast extends Required> { + id: string; + title?: string; + createdAt: number; +} + diff --git a/frontend/src/components/toolbar/BlockLanguageSelector.vue b/frontend/src/components/toolbar/BlockLanguageSelector.vue index 2ba0558..071fdc3 100644 --- a/frontend/src/components/toolbar/BlockLanguageSelector.vue +++ b/frontend/src/components/toolbar/BlockLanguageSelector.vue @@ -294,9 +294,11 @@ const scrollToCurrentLanguage = () => { -
    - -
    + + +
    + +
    { {{ t('toolbar.noLanguageFound') }}
    -
    +
    + diff --git a/frontend/src/stores/backupStore.ts b/frontend/src/stores/backupStore.ts index 8d13e36..7ab02b6 100644 --- a/frontend/src/stores/backupStore.ts +++ b/frontend/src/stores/backupStore.ts @@ -4,7 +4,6 @@ import { BackupService } from '@/../bindings/voidraft/internal/services'; export const useBackupStore = defineStore('backup', () => { const isSyncing = ref(false); - const error = ref(null); const sync = async (): Promise => { if (isSyncing.value) { @@ -12,12 +11,11 @@ export const useBackupStore = defineStore('backup', () => { } isSyncing.value = true; - error.value = null; try { await BackupService.Sync(); } catch (e) { - error.value = e instanceof Error ? e.message : String(e); + throw e; } finally { isSyncing.value = false; } @@ -25,7 +23,6 @@ export const useBackupStore = defineStore('backup', () => { return { isSyncing, - error, sync }; }); \ No newline at end of file diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts index d715321..e1cf3f5 100644 --- a/frontend/src/stores/documentStore.ts +++ b/frontend/src/stores/documentStore.ts @@ -16,33 +16,15 @@ export const useDocumentStore = defineStore('document', () => { // === UI状态 === const showDocumentSelector = ref(false); - const selectorError = ref<{ docId: number; message: string } | null>(null); const isLoading = ref(false); - // === 错误处理 === - const setError = (docId: number, message: string) => { - selectorError.value = {docId, message}; - // 3秒后自动清除错误状态 - setTimeout(() => { - if (selectorError.value?.docId === docId) { - selectorError.value = null; - } - }, 3000); - }; - - const clearError = () => { - selectorError.value = null; - }; - // === UI控制方法 === const openDocumentSelector = () => { showDocumentSelector.value = true; - clearError(); }; const closeDocumentSelector = () => { showDocumentSelector.value = false; - clearError(); }; @@ -217,7 +199,6 @@ export const useDocumentStore = defineStore('document', () => { currentDocumentId, currentDocument, showDocumentSelector, - selectorError, isLoading, getDocumentList, @@ -236,8 +217,6 @@ export const useDocumentStore = defineStore('document', () => { // UI 控制 openDocumentSelector, closeDocumentSelector, - setError, - clearError, // 初始化 initDocument, diff --git a/frontend/src/views/settings/pages/BackupPage.vue b/frontend/src/views/settings/pages/BackupPage.vue index b7f4a17..36c37f1 100644 --- a/frontend/src/views/settings/pages/BackupPage.vue +++ b/frontend/src/views/settings/pages/BackupPage.vue @@ -2,48 +2,18 @@ import {useConfigStore} from '@/stores/configStore'; import {useBackupStore} from '@/stores/backupStore'; import {useI18n} from 'vue-i18n'; -import {computed, ref, watch, onUnmounted} from 'vue'; +import {computed} from 'vue'; import SettingSection from '../components/SettingSection.vue'; import SettingItem from '../components/SettingItem.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue'; import {AuthMethod} from '@/../bindings/voidraft/internal/models/models'; import {DialogService} from '@/../bindings/voidraft/internal/services'; +import toast from '@/components/toast'; const {t} = useI18n(); const configStore = useConfigStore(); const backupStore = useBackupStore(); -// 消息显示状态 -const message = ref(null); -const isError = ref(false); -let messageTimer: ReturnType | null = null; - -const clearMessage = () => { - if (messageTimer) { - clearTimeout(messageTimer); - messageTimer = null; - } - message.value = null; -}; - -// 监听同步完成,显示消息并自动消失 -watch(() => backupStore.isSyncing, (syncing, wasSyncing) => { - if (wasSyncing && !syncing) { - clearMessage(); - if (backupStore.error) { - message.value = backupStore.error; - isError.value = true; - messageTimer = setTimeout(clearMessage, 5000); - } else { - message.value = 'Sync successful'; - isError.value = false; - messageTimer = setTimeout(clearMessage, 3000); - } - } -}); - -onUnmounted(clearMessage); - const authMethodOptions = computed(() => [ {value: AuthMethod.Token, label: t('settings.backup.authMethods.token')}, {value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')}, @@ -64,6 +34,15 @@ const selectSshKeyFile = async () => { configStore.setSshKeyPath(selectedPath.trim()); } }; + +const handleSync = async () => { + try { + await backupStore.sync(); + toast.success('Sync successful'); + } catch (e) { + toast.error(e instanceof Error ? e.message : String(e)); + } +}; \ No newline at end of file diff --git a/frontend/src/views/settings/pages/TestPage.vue b/frontend/src/views/settings/pages/TestPage.vue index ae993bb..8e146a1 100644 --- a/frontend/src/views/settings/pages/TestPage.vue +++ b/frontend/src/views/settings/pages/TestPage.vue @@ -72,6 +72,72 @@ + + + + + + + + + + + + + + + +
    + + + + +
    +
    + +
    + + +
    +
    +
    + @@ -91,6 +157,8 @@ import { ref } from 'vue'; import * as TestService from '@/../bindings/voidraft/internal/services/testservice'; import SettingSection from '../components/SettingSection.vue'; import SettingItem from '../components/SettingItem.vue'; +import toast from '@/components/toast'; +import type { ToastPosition, ToastType } from '@/components/toast/types'; // Badge测试状态 const badgeText = ref(''); @@ -102,6 +170,12 @@ const notificationSubtitle = ref(''); const notificationBody = ref(''); const notificationStatus = ref<{ type: string; message: string } | null>(null); +// Toast 测试状态 +const toastMessage = ref('This is a test toast notification!'); +const toastTitle = ref(''); +const toastPosition = ref('top-right'); +const toastDuration = ref(4000); + // 清除状态 const clearStatus = ref<{ type: string; message: string } | null>(null); @@ -172,13 +246,57 @@ const clearAll = async () => { showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`); } }; + +// Toast 相关函数 +const showToast = (type: ToastType) => { + const message = toastMessage.value || `This is a ${type} toast notification!`; + const title = toastTitle.value || undefined; + + const options = { + position: toastPosition.value, + duration: toastDuration.value, + }; + + switch (type) { + case 'success': + toast.success(message, title, options); + break; + case 'error': + toast.error(message, title, options); + break; + case 'warning': + toast.warning(message, title, options); + break; + case 'info': + toast.info(message, title, options); + break; + } +}; + +const showMultipleToasts = () => { + const positions: ToastPosition[] = ['top-right', 'top-left', 'bottom-right', 'bottom-left']; + const types: ToastType[] = ['success', 'error', 'warning', 'info']; + + positions.forEach((position, index) => { + setTimeout(() => { + const type = types[index % types.length]; + toast.show({ + type, + message: `Toast from ${position}`, + title: `${type.charAt(0).toUpperCase() + type.slice(1)} Toast`, + position, + duration: 5000, + }); + }, index * 200); + }); +}; + +const clearAllToasts = () => { + toast.clear(); +}; + diff --git a/frontend/src/components/accordion/AccordionItem.vue b/frontend/src/components/accordion/AccordionItem.vue new file mode 100644 index 0000000..2903f07 --- /dev/null +++ b/frontend/src/components/accordion/AccordionItem.vue @@ -0,0 +1,187 @@ + + + + + + diff --git a/frontend/src/components/accordion/index.ts b/frontend/src/components/accordion/index.ts new file mode 100644 index 0000000..1664d35 --- /dev/null +++ b/frontend/src/components/accordion/index.ts @@ -0,0 +1,3 @@ +export { default as AccordionContainer } from './AccordionContainer.vue'; +export { default as AccordionItem } from './AccordionItem.vue'; + diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index b79feac..5607cc1 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -1,5 +1,14 @@ export default { locale: 'en-US', + common: { + ok: 'OK', + cancel: 'Cancel', + edit: 'Edit', + delete: 'Delete', + confirm: 'Confirm', + save: 'Save', + reset: 'Reset' + }, titlebar: { minimize: 'Minimize', maximize: 'Maximize', @@ -56,6 +65,19 @@ export default { }, resetToDefault: 'Reset to Default', confirmReset: 'Confirm Reset?', + noKeybinding: 'Not Set', + waitingForKey: 'Waiting...', + clickToSet: 'Click to set keybinding', + editKeybinding: 'Edit keybinding', + config: { + enabled: 'Enabled', + preventDefault: 'Prevent Default', + keybinding: 'Keybinding' + }, + keyPlaceholder: 'Enter key, press Enter to add', + invalidFormat: 'Invalid format', + conflict: 'Conflict: {command}', + maxKeysReached: 'Maximum 4 keys allowed', commands: { showSearch: 'Show search panel', hideSearch: 'Hide search panel', @@ -178,6 +200,7 @@ export default { enableWindowSnap: 'Enable Window Snapping', enableLoadingAnimation: 'Enable Loading Animation', enableTabs: 'Enable Tabs', + enableMemoryMonitor: 'Enable Memory Monitor', startup: 'Startup Settings', startAtLogin: 'Start at Login', dataStorage: 'Data Storage', @@ -223,6 +246,7 @@ export default { categoryEditing: 'Editing Enhancement', categoryUI: 'UI Enhancement', categoryTools: 'Tools', + enabled: 'Enabled', configuration: 'Configuration', resetToDefault: 'Reset to Default Configuration', }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index a91e23a..b8fe43f 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1,5 +1,14 @@ export default { locale: 'zh-CN', + common: { + ok: '确定', + cancel: '取消', + edit: '编辑', + delete: '删除', + confirm: '确认', + save: '保存', + reset: '重置' + }, titlebar: { minimize: '最小化', maximize: '最大化', @@ -56,6 +65,19 @@ export default { }, resetToDefault: '重置为默认', confirmReset: '确认重置?', + noKeybinding: '未设置', + waitingForKey: '等待输入...', + clickToSet: '点击设置快捷键', + editKeybinding: '编辑快捷键', + config: { + enabled: '启用', + preventDefault: '阻止默认', + keybinding: '快捷键' + }, + keyPlaceholder: '输入键名, 回车添加', + invalidFormat: '格式错误', + conflict: '冲突: {command}', + maxKeysReached: '最多只能添加4个键', commands: { showSearch: '显示搜索面板', hideSearch: '隐藏搜索面板', @@ -179,6 +201,7 @@ export default { enableWindowSnap: '启用窗口吸附', enableLoadingAnimation: '启用加载动画', enableTabs: '启用标签页', + enableMemoryMonitor: '启用内存监视器', startup: '启动设置', startAtLogin: '开机自启动', dataStorage: '数据存储', @@ -226,6 +249,7 @@ export default { categoryEditing: '编辑增强', categoryUI: '界面增强', categoryTools: '工具扩展', + enabled: '启用', configuration: '配置', resetToDefault: '重置为默认配置', }, diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index 9962061..259ebd3 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -233,6 +233,9 @@ export const useConfigStore = defineStore('config', () => { // 标签页配置相关方法 setEnableTabs: (value: boolean) => updateConfig('enableTabs', value), + // 内存监视器配置相关方法 + setEnableMemoryMonitor: (value: boolean) => updateConfig('enableMemoryMonitor', value), + // 快捷键模式配置相关方法 setKeymapMode: (value: any) => updateConfig('keymapMode', value), diff --git a/frontend/src/views/editor/extensions/codeblock/index.ts b/frontend/src/views/editor/extensions/codeblock/index.ts index 97a8b4c..a2e5acb 100644 --- a/frontend/src/views/editor/extensions/codeblock/index.ts +++ b/frontend/src/views/editor/extensions/codeblock/index.ts @@ -39,6 +39,9 @@ export interface CodeBlockOptions { /** 新建块时的默认语言 */ defaultLanguage?: SupportedLanguage; + + /** 分隔符高度(像素) */ + separatorHeight?: number; } /** @@ -86,6 +89,7 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens showBackground = true, enableAutoDetection = true, defaultLanguage = 'text', + separatorHeight = 12, } = options; return [ @@ -104,7 +108,8 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens // 视觉装饰系统 ...getBlockDecorationExtensions({ - showBackground + showBackground, + separatorHeight }), // 光标保护(防止方向键移动到分隔符上) diff --git a/frontend/src/views/settings/Settings.vue b/frontend/src/views/settings/Settings.vue index ef461c9..640f1a4 100644 --- a/frontend/src/views/settings/Settings.vue +++ b/frontend/src/views/settings/Settings.vue @@ -1,12 +1,17 @@ -
    -
    -
    {{ t('keybindings.headers.shortcut') }}
    -
    {{ t('keybindings.headers.extension') }}
    -
    {{ t('keybindings.headers.description') }}
    -
    - -
    + - -
    - - + + +
    + +
    + {{ t('keybindings.config.enabled') }} + +
    + + +
    + {{ t('keybindings.config.preventDefault') }} + +
    + + +
    + {{ t('keybindings.config.keybinding') }} +
    +
    + + + + + + + + +
    +
    +
    - -
    {{ binding.extension }}
    -
    {{ binding.description }}
    -
    -
    + +
    @@ -375,167 +448,275 @@ const confirmKeybinding = async () => { } } -.key-bindings-container { +.binding-title { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + gap: 16px; - .key-bindings-header { - display: flex; - padding: 0 0 8px 0; - border-bottom: 1px solid var(--settings-border); - color: var(--text-muted); - font-size: 12px; - font-weight: 500; - } - - .key-binding-row { - display: flex; - padding: 10px 0; - border-bottom: 1px solid var(--settings-border); - align-items: center; - transition: background-color 0.2s ease; - - &:hover { - background-color: var(--settings-hover); - } - } - - .keybinding-col { - width: 150px; - display: flex; - gap: 4px; - padding: 0 10px 0 0; - color: var(--settings-text); - align-items: center; - cursor: pointer; - transition: all 0.2s ease; - - &:hover:not(.editing) .key-badge { - border-color: #4a9eff; - } - - &.editing { - cursor: default; - } - - .key-badge { - background-color: var(--settings-input-bg); - padding: 2px 6px; - border-radius: 3px; - font-size: 11px; - border: 1px solid var(--settings-input-border); - color: var(--settings-text); - transition: border-color 0.2s ease; - white-space: nowrap; - - &.waiting { - border: none; - background-color: transparent; - padding: 0; - color: #4a9eff; - font-style: italic; - animation: colorPulse 1.5s ease-in-out infinite; - } - - &.captured { - background-color: #4a9eff; - color: white; - border-color: #4a9eff; - - &.conflict { - background-color: #dc3545; - border-color: #dc3545; - animation: shake 0.6s ease-in-out; - } - } - } - } - - .btn-mini { - width: 16px; - height: 16px; - min-width: 16px; - border: none; - border-radius: 2px; - cursor: pointer; - font-size: 10px; - transition: opacity 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - line-height: 1; - margin-left: auto; - - &.btn-confirm { - background-color: #28a745; - color: white; - - &:hover:not(:disabled) { - opacity: 0.85; - } - - &:disabled { - background-color: var(--settings-input-border); - cursor: not-allowed; - opacity: 0.5; - } - } - - &.btn-cancel { - background-color: #dc3545; - color: white; - margin-left: 2px; - - &:hover { - opacity: 0.85; - } - } - } - - .extension-col { - width: 80px; - padding: 0 10px 0 0; - font-size: 13px; - color: var(--settings-text); - text-transform: capitalize; - } - - .description-col { - flex: 1; - font-size: 13px; - color: var(--settings-text); + &.disabled { + opacity: 0.5; } } -@keyframes colorPulse { - 0%, 100% { - color: #4a9eff; +.binding-name { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.binding-description { + font-size: 13px; + font-weight: 500; + color: var(--settings-text); +} + +.binding-extension { + font-size: 11px; + color: var(--text-muted); + text-transform: capitalize; +} + +.binding-keys { + display: flex; + gap: 4px; + align-items: center; +} + +.key-badge { + background-color: var(--settings-input-bg); + padding: 2px 6px; + border-radius: 3px; + font-size: 11px; + border: 1px solid var(--settings-input-border); + color: var(--settings-text); + white-space: nowrap; +} + +.key-badge-empty { + font-size: 11px; + color: var(--text-muted); + font-style: italic; +} + +.binding-config { + display: flex; + flex-direction: column; + gap: 16px; +} + +.config-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.config-label { + font-size: 13px; + color: var(--settings-text); + font-weight: 500; +} + +// Switch 开关样式 +.switch { + position: relative; + display: inline-block; + width: 36px; + height: 20px; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + + &:checked + .slider { + background-color: #4a9eff; + + &:before { + transform: translateX(16px); + } + } + + &:focus + .slider { + box-shadow: 0 0 1px #4a9eff; + } + } +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--settings-input-border); + transition: 0.3s; + border-radius: 20px; + + &:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; + } +} + +.key-input-wrapper { + flex: 1; + display: flex; + align-items: center; + gap: 8px; +} + +.key-tags { + display: flex; + gap: 6px; + align-items: center; + flex-wrap: wrap; + flex: 1; + min-height: 28px; +} + +.key-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + height: 28px; + background-color: var(--settings-input-bg); + border: 1px solid var(--settings-input-border); + border-radius: 4px; + font-size: 12px; + font-weight: 500; + color: var(--settings-text); + transition: all 0.2s ease; + box-sizing: border-box; + + &:hover { + border-color: #4a9eff; + + .key-tag-remove { + opacity: 1; + } + } +} + +.key-tag-text { + user-select: none; +} + +.key-tag-remove { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 16px; + height: 16px; + border: none; + background: none; + color: var(--text-muted); + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0; + margin: 0; + opacity: 0.6; + transition: all 0.2s ease; + + &:hover { + color: #e74c3c; opacity: 1; } - 50% { - color: #2080ff; - opacity: 0.6; - } } -@keyframes shake { - 0%, 100% { - transform: translateX(0); - } - 10%, 30%, 50%, 70%, 90% { - transform: translateX(-4px); - } - 20%, 40%, 60%, 80% { - transform: translateX(4px); - } -} - -.coming-soon-placeholder { - padding: 20px; - background-color: var(--settings-card-bg); - border-radius: 6px; +.key-tag-add { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border: 1px dashed var(--settings-input-border); + border-radius: 4px; + background-color: transparent; color: var(--text-muted); - text-align: center; - font-style: italic; - font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; + + &:hover { + border-color: #4a9eff; + background-color: var(--settings-input-bg); + color: #4a9eff; + } } - \ No newline at end of file + +.key-input { + padding: 4px 8px; + height: 28px; + border: 1px solid #4a9eff; + border-radius: 4px; + background-color: var(--settings-input-bg); + color: var(--settings-text); + font-size: 12px; + width: 60px; + outline: none; + box-sizing: border-box; + + &::placeholder { + color: var(--text-muted); + font-size: 11px; + } +} + +.btn-mini { + width: 24px; + height: 24px; + min-width: 24px; + border: none; + border-radius: 3px; + cursor: pointer; + font-size: 12px; + transition: opacity 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; + flex-shrink: 0; + + &.btn-confirm { + background-color: #28a745; + color: white; + + &:hover:not(:disabled) { + opacity: 0.85; + } + + &:disabled { + background-color: var(--settings-input-border); + cursor: not-allowed; + opacity: 0.5; + } + } + + &.btn-cancel { + background-color: #dc3545; + color: white; + + &:hover { + opacity: 0.85; + } + } +} + diff --git a/internal/models/config.go b/internal/models/config.go index 42e6852..1f5a2f9 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -79,6 +79,7 @@ type GeneralConfig struct { // 界面设置 EnableLoadingAnimation bool `json:"enableLoadingAnimation"` // 是否启用加载动画 EnableTabs bool `json:"enableTabs"` // 是否启用标签页模式 + EnableMemoryMonitor bool `json:"enableMemoryMonitor"` // 是否启用内存监视器 } // HotkeyCombo 热键组合定义 @@ -188,6 +189,7 @@ func NewDefaultAppConfig() *AppConfig { EnableGlobalHotkey: false, EnableLoadingAnimation: true, // 默认启用加载动画 EnableTabs: false, // 默认不启用标签页模式 + EnableMemoryMonitor: true, // 默认启用内存监视器 GlobalHotkey: HotkeyCombo{ Ctrl: false, Shift: false, diff --git a/internal/models/key_binding.go b/internal/models/key_binding.go index 48f956c..304c450 100644 --- a/internal/models/key_binding.go +++ b/internal/models/key_binding.go @@ -106,7 +106,7 @@ const ( CopyBlockImage KeyBindingName = "copyBlockImage" // 复制块为图片 ) -const defaultExtension = "editor" +const DefaultExtension = "editor" // NewDefaultKeyBindings 创建默认快捷键配置 func NewDefaultKeyBindings() []KeyBinding { @@ -135,7 +135,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockSelectAll, Type: Standard, Key: "Mod-a", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -143,7 +143,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockAddAfterCurrent, Type: Standard, Key: "Mod-Enter", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -151,7 +151,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockAddAfterLast, Type: Standard, Key: "Mod-Shift-Enter", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -159,7 +159,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockAddBeforeCurrent, Type: Standard, Key: "Alt-Enter", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -167,7 +167,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockGotoPrevious, Type: Standard, Key: "Mod-ArrowUp", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -175,7 +175,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockGotoNext, Type: Standard, Key: "Mod-ArrowDown", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -183,7 +183,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockSelectPrevious, Type: Standard, Key: "Mod-Shift-ArrowUp", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -191,7 +191,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockSelectNext, Type: Standard, Key: "Mod-Shift-ArrowDown", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -199,7 +199,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockDelete, Type: Standard, Key: "Mod-Shift-d", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -207,7 +207,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockMoveUp, Type: Standard, Key: "Shift-Mod-ArrowUp", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -215,7 +215,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockMoveDown, Type: Standard, Key: "Shift-Mod-ArrowDown", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -223,7 +223,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockDeleteLine, Type: Standard, Key: "Mod-Shift-k", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -231,7 +231,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockMoveLineUp, Type: Standard, Key: "Alt-ArrowUp", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -239,7 +239,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockMoveLineDown, Type: Standard, Key: "Alt-ArrowDown", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -247,7 +247,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockTransposeChars, Type: Standard, Key: "Mod-t", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -255,7 +255,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockFormat, Type: Standard, Key: "Mod-Shift-f", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -263,7 +263,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockCopy, Type: Standard, Key: "Mod-c", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -271,7 +271,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockCut, Type: Standard, Key: "Mod-x", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -279,7 +279,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockPaste, Type: Standard, Key: "Mod-v", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -335,7 +335,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: HistoryUndo, Type: Standard, Key: "Mod-z", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -344,7 +344,7 @@ func NewDefaultKeyBindings() []KeyBinding { Type: Standard, Key: "Mod-Shift-z", Windows: "Ctrl-y", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -352,7 +352,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: HistoryUndoSelection, Type: Standard, Key: "Mod-u", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -361,7 +361,7 @@ func NewDefaultKeyBindings() []KeyBinding { Type: Standard, Key: "Mod-Shift-u", Windows: "Alt-u", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -373,7 +373,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Ctrl-ArrowLeft", Windows: "Alt-ArrowLeft", Linux: "Alt-ArrowLeft", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -383,7 +383,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Ctrl-ArrowRight", Windows: "Alt-ArrowRight", Linux: "Alt-ArrowRight", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -391,7 +391,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectSyntaxLeft, Type: Standard, Key: "Shift-Alt-ArrowLeft", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -399,7 +399,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectSyntaxRight, Type: Standard, Key: "Shift-Alt-ArrowRight", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -407,7 +407,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CopyLineUp, Type: Standard, Key: "Shift-Alt-ArrowUp", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -415,7 +415,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CopyLineDown, Type: Standard, Key: "Shift-Alt-ArrowDown", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -423,9 +423,9 @@ func NewDefaultKeyBindings() []KeyBinding { Name: InsertBlankLine, Type: Standard, Key: "Mod-Enter", - Extension: defaultExtension, - Enabled: true, - PreventDefault: true, + Extension: DefaultExtension, + Enabled: false, + PreventDefault: false, }, { Name: SelectLine, @@ -433,7 +433,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Ctrl-l", Windows: "Alt-l", Linux: "Alt-l", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -441,7 +441,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectParentSyntax, Type: Standard, Key: "Mod-i", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -449,7 +449,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SimplifySelection, Type: Standard, Key: "Escape", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -459,7 +459,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Cmd-Alt-ArrowUp", Windows: "Ctrl-Alt-ArrowUp", Linux: "Ctrl-Alt-ArrowUp", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -469,7 +469,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Cmd-Alt-ArrowDown", Windows: "Ctrl-Alt-ArrowDown", Linux: "Ctrl-Alt-ArrowDown", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -479,7 +479,7 @@ func NewDefaultKeyBindings() []KeyBinding { Windows: "Ctrl-ArrowLeft", Linux: "Ctrl-ArrowLeft", Macos: "Alt-ArrowLeft", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -489,7 +489,7 @@ func NewDefaultKeyBindings() []KeyBinding { Windows: "Ctrl-ArrowRight", Linux: "Ctrl-ArrowRight", Macos: "Alt-ArrowRight", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -499,7 +499,7 @@ func NewDefaultKeyBindings() []KeyBinding { Windows: "Ctrl-Shift-ArrowLeft", Linux: "Ctrl-Shift-ArrowLeft", Macos: "Alt-Shift-ArrowLeft", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -509,7 +509,7 @@ func NewDefaultKeyBindings() []KeyBinding { Windows: "Ctrl-Shift-ArrowRight", Linux: "Ctrl-Shift-ArrowRight", Macos: "Alt-Shift-ArrowRight", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -519,7 +519,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Ctrl-k", Windows: "Ctrl-k", Linux: "Ctrl-k", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -527,7 +527,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteToLineStart, Type: Standard, Key: "Mod-Shift-Backspace", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -535,7 +535,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorLineStart, Type: Standard, Key: "Home", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -543,7 +543,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorLineEnd, Type: Standard, Key: "End", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -551,7 +551,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectLineStart, Type: Standard, Key: "Shift-Home", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -559,7 +559,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectLineEnd, Type: Standard, Key: "Shift-End", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -567,7 +567,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorDocStart, Type: Standard, Key: "Mod-Home", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -575,7 +575,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorDocEnd, Type: Standard, Key: "Mod-End", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -583,7 +583,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectDocStart, Type: Standard, Key: "Mod-Shift-Home", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -591,7 +591,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectDocEnd, Type: Standard, Key: "Mod-Shift-End", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -599,7 +599,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectMatchingBracket, Type: Standard, Key: "Mod-Shift-p", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -609,7 +609,7 @@ func NewDefaultKeyBindings() []KeyBinding { Macos: "Ctrl-o", Windows: "Ctrl-o", Linux: "Ctrl-o", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -618,7 +618,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: IndentLess, Type: Standard, Key: "Mod-[", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -626,7 +626,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: IndentMore, Type: Standard, Key: "Mod-]", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -634,7 +634,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: IndentSelection, Type: Standard, Key: "Mod-Alt-\\", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -642,7 +642,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorMatchingBracket, Type: Standard, Key: "Shift-Mod-\\", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -650,7 +650,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: ToggleComment, Type: Standard, Key: "Mod-/", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -658,7 +658,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: ToggleBlockComment, Type: Standard, Key: "Shift-Alt-a", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -667,7 +667,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: InsertNewlineAndIndent, Type: Standard, Key: "Enter", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -675,7 +675,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteCharBackward, Type: Standard, Key: "Backspace", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -683,7 +683,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteCharForward, Type: Standard, Key: "Delete", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: false, }, @@ -691,7 +691,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteGroupBackward, Type: Standard, Key: "Mod-Backspace", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -699,7 +699,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteGroupForward, Type: Standard, Key: "Mod-Delete", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -712,7 +712,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorCharLeft, Type: Emacs, Key: "Ctrl-b", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -720,7 +720,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectCharLeft, Type: Emacs, Key: "Shift-Ctrl-b", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -728,7 +728,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorCharRight, Type: Emacs, Key: "Ctrl-f", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -736,7 +736,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectCharRight, Type: Emacs, Key: "Shift-Ctrl-f", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -746,7 +746,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorLineUp, Type: Emacs, Key: "Ctrl-p", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -754,7 +754,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectLineUp, Type: Emacs, Key: "Shift-Ctrl-p", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -762,7 +762,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorLineDown, Type: Emacs, Key: "Ctrl-n", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -770,7 +770,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectLineDown, Type: Emacs, Key: "Shift-Ctrl-n", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -780,7 +780,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorLineStart, Type: Emacs, Key: "Ctrl-a", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -788,7 +788,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectLineStart, Type: Emacs, Key: "Shift-Ctrl-a", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -796,7 +796,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorLineEnd, Type: Emacs, Key: "Ctrl-e", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -804,7 +804,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SelectLineEnd, Type: Emacs, Key: "Shift-Ctrl-e", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -814,7 +814,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorPageDown, Type: Emacs, Key: "Ctrl-v", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -822,7 +822,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: CursorPageUp, Type: Emacs, Key: "Alt-v", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -832,7 +832,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteCharForward, Type: Emacs, Key: "Ctrl-d", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -840,7 +840,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteCharBackward, Type: Emacs, Key: "Ctrl-h", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -848,7 +848,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteToLineEnd, Type: Emacs, Key: "Ctrl-k", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -856,7 +856,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: DeleteGroupBackward, Type: Emacs, Key: "Ctrl-Alt-h", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -864,7 +864,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: SplitLine, Type: Emacs, Key: "Ctrl-o", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -872,7 +872,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockTransposeChars, Type: Emacs, Key: "Ctrl-t", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -882,7 +882,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockSelectAll, Type: Emacs, Key: "Mod-Shift-a", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, @@ -890,7 +890,7 @@ func NewDefaultKeyBindings() []KeyBinding { Name: BlockPaste, Type: Emacs, Key: "Mod-Shift-v", - Extension: defaultExtension, + Extension: DefaultExtension, Enabled: true, PreventDefault: true, }, diff --git a/internal/services/keybinding_service.go b/internal/services/keybinding_service.go index 6230cbe..4acf995 100644 --- a/internal/services/keybinding_service.go +++ b/internal/services/keybinding_service.go @@ -6,6 +6,7 @@ import ( "voidraft/internal/models" "voidraft/internal/models/ent" + "voidraft/internal/models/ent/extension" "voidraft/internal/models/ent/keybinding" "voidraft/internal/models/schema/mixin" @@ -112,45 +113,74 @@ func (s *KeyBindingService) SyncKeyBindings(ctx context.Context) error { // GetKeyBindings 根据类型获取快捷键 func (s *KeyBindingService) GetKeyBindings(ctx context.Context, kbType models.KeyBindingType) ([]*ent.KeyBinding, error) { - if kbType == models.Standard { - // Standard 模式:只返回 type=standard 且 enabled=true - return s.db.Client.KeyBinding.Query(). - Where( - keybinding.Type(string(kbType)), - keybinding.Enabled(true), - ). - All(ctx) - } - - // Emacs 模式:获取所有 enabled=true 的快捷键 - allEnabled, err := s.db.Client.KeyBinding.Query(). - Where(keybinding.Enabled(true)). + // 获取启用的扩展名称集合 + enabledExts, err := s.db.Client.Extension.Query(). + Where(extension.Enabled(true)). All(ctx) if err != nil { - return nil, fmt.Errorf("query enabled key bindings error: %w", err) + return nil, fmt.Errorf("query enabled extensions error: %w", err) + } + enabledExtMap := make(map[string]bool, len(enabledExts)) + for _, ext := range enabledExts { + enabledExtMap[ext.Name] = true + } + + if kbType == models.Standard { + // Standard 模式:返回扩展已启用的快捷键 + bindings, err := s.db.Client.KeyBinding.Query(). + Where(keybinding.Type(string(kbType))). + Order(ent.Asc(keybinding.FieldID)). + All(ctx) + if err != nil { + return nil, err + } + return filterByExtension(bindings, enabledExtMap), nil + } + + // Emacs 模式:获取所有快捷键 + allBindings, err := s.db.Client.KeyBinding.Query(). + Order(ent.Asc(keybinding.FieldID)). + All(ctx) + if err != nil { + return nil, fmt.Errorf("query key bindings error: %w", err) } // 构建 emacs 快捷键的 name 集合 emacsNames := make(map[string]bool) - for _, kb := range allEnabled { + for _, kb := range allBindings { if kb.Type == string(models.Emacs) { emacsNames[kb.Name] = true } } - // 过滤:去掉与 emacs 冲突的 standard 快捷键 + // 过滤:去掉与 emacs 冲突的 standard 快捷键,并过滤扩展未启用的 var result []*ent.KeyBinding - for _, kb := range allEnabled { - // 如果是 standard 类型,且与 emacs 有 name 冲突,则跳过 + for _, kb := range allBindings { if kb.Type == string(models.Standard) && emacsNames[kb.Name] { continue } + // editor 扩展始终包含,不检查启用状态 + if kb.Extension != models.DefaultExtension && !enabledExtMap[kb.Extension] { + continue + } result = append(result, kb) } return result, nil } +// filterByExtension 过滤出扩展已启用的快捷键 +func filterByExtension(bindings []*ent.KeyBinding, enabledExtMap map[string]bool) []*ent.KeyBinding { + result := make([]*ent.KeyBinding, 0, len(bindings)) + for _, kb := range bindings { + // editor 扩展始终包含,不检查启用状态 + if kb.Extension == models.DefaultExtension || enabledExtMap[kb.Extension] { + result = append(result, kb) + } + } + return result +} + // GetKeyBindingByID 根据ID获取快捷键 func (s *KeyBindingService) GetKeyBindingByID(ctx context.Context, id int) (*ent.KeyBinding, error) { kb, err := s.db.Client.KeyBinding.Get(ctx, id) @@ -163,7 +193,7 @@ func (s *KeyBindingService) GetKeyBindingByID(ctx context.Context, id int) (*ent return kb, nil } -// UpdateKeyBindingKeys 更新快捷键绑定(根据操作系统自动判断更新哪个字段) +// UpdateKeyBindingKeys 更新快捷键绑定 func (s *KeyBindingService) UpdateKeyBindingKeys(ctx context.Context, id int, key string) error { kb, err := s.GetKeyBindingByID(ctx, id) if err != nil { @@ -204,6 +234,20 @@ func (s *KeyBindingService) UpdateKeyBindingEnabled(ctx context.Context, id int, Exec(ctx) } +// UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态 +func (s *KeyBindingService) UpdateKeyBindingPreventDefault(ctx context.Context, id int, preventDefault bool) error { + kb, err := s.GetKeyBindingByID(ctx, id) + if err != nil { + return err + } + if kb == nil { + return fmt.Errorf("key binding not found: id=%d", id) + } + return s.db.Client.KeyBinding.UpdateOneID(kb.ID). + SetPreventDefault(preventDefault). + Exec(ctx) +} + // GetDefaultKeyBindings 获取默认快捷键配置 func (s *KeyBindingService) GetDefaultKeyBindings() []models.KeyBinding { return models.NewDefaultKeyBindings() From 532d30aa935cf0db91721ddfd6b757e6cd8f9576 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sat, 3 Jan 2026 23:06:08 +0800 Subject: [PATCH 6/7] :sparkles: Added code collapse state persistence --- frontend/package-lock.json | 18 +-- .../common/prettier/plugins/toml/printer.ts | 49 +++++--- frontend/src/stores/editorStateStore.ts | 30 ++++- frontend/src/stores/editorStore.ts | 11 ++ .../views/editor/basic/foldStateExtension.ts | 113 ++++++++++++++++++ .../editor/extensions/codeblock/index.ts | 4 +- go.mod | 7 +- go.sum | 35 ++++-- 8 files changed, 222 insertions(+), 45 deletions(-) create mode 100644 frontend/src/views/editor/basic/foldStateExtension.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8cf1144..dc31225 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,8 +42,8 @@ "@lezer/lr": "^1.4.5", "@prettier/plugin-xml": "^3.4.2", "@replit/codemirror-lang-svelte": "^6.0.0", - "@toml-tools/lexer": "^1.0.0", - "@toml-tools/parser": "^1.0.0", + "@toml-tools/lexer": "^1.0.1", + "@toml-tools/parser": "^1.0.1", "@types/katex": "^0.16.7", "@zumer/snapdom": "^2.0.1", "codemirror": "^6.0.2", @@ -2468,21 +2468,21 @@ "license": "MIT" }, "node_modules/@toml-tools/lexer": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz", - "integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.1.tgz", + "integrity": "sha512-jn2fl8m/9QPcUD507Hbt2W3TVMKzF5HEY8xKIxqY2r2dTG2udeCKlo2ejJ5k/RSOJsWNIuw+Ir/nxW5PItUApA==", "license": "MIT", "dependencies": { "chevrotain": "^11.0.1" } }, "node_modules/@toml-tools/parser": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz", - "integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.1.tgz", + "integrity": "sha512-W+YdnB8KDgKjIqhoArEXjiTTPnKSXVvI/B+raHfou9+sip3rxhzVsELn46GG7dZyNHyu9pS+gYgYrdF9c5AQDg==", "license": "MIT", "dependencies": { - "@toml-tools/lexer": "^1.0.0", + "@toml-tools/lexer": "^1.0.1", "chevrotain": "^11.0.1" } }, diff --git a/frontend/src/common/prettier/plugins/toml/printer.ts b/frontend/src/common/prettier/plugins/toml/printer.ts index a2c3e0f..5c7fc09 100644 --- a/frontend/src/common/prettier/plugins/toml/printer.ts +++ b/frontend/src/common/prettier/plugins/toml/printer.ts @@ -35,7 +35,6 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor { // Helper methods public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[]; public visitSingle: (ctx: TomlContext) => Doc | string; - public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string; constructor() { super(); @@ -57,26 +56,38 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor { const singleElement = getSingle(ctx); return this.visit(singleElement); }; + } - // Store reference to inherited visit method and override it - const originalVisit = Object.getPrototypeOf(this).visit?.bind(this); - this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => { - if (!ctx) { - return ''; - } - + /** + * Override visit method to handle TOML CST nodes + * Accepts both single node and array of nodes as per base class signature + */ + visit(cstNode: any, param?: any): any { + // Handle array of nodes + if (Array.isArray(cstNode)) { + return cstNode.map(node => this.visit(node, param)); + } + + const ctx = cstNode; + if (!ctx) { + return ''; + } + + // 确保节点有name属性才调用基类方法 + if (ctx.name) { // Try to use the inherited visit method first + const originalVisit = super.visit; if (originalVisit) { try { - return originalVisit(ctx, inParam); + return originalVisit.call(this, ctx, param); } catch (error) { - console.warn('Original visit method failed:', error); + // Fallback to manual dispatch } } - + // Fallback: manually dispatch based on node name/type const methodName = ctx.name; - if (methodName && typeof (this as any)[methodName] === 'function') { + if (typeof (this as any)[methodName] === 'function') { const visitMethod = (this as any)[methodName]; try { if (ctx.children) { @@ -88,16 +99,16 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor { console.warn(`Visit method ${methodName} failed:`, error); } } - - // Final fallback: return image if available - return ctx.image || ''; - }; + } + + // Final fallback: return image if available + return ctx.image || ''; } /** * Visit the root TOML document */ - toml(ctx: TomlDocument): Doc { + toml(ctx: any): Doc { // Handle empty toml document if (!ctx.expression) { return [line]; @@ -164,7 +175,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor { /** * Visit an expression (keyval, table, or comment) */ - expression(ctx: TomlExpression): Doc | string { + expression(ctx: any): Doc | string { if (ctx.keyval) { let keyValDoc = this.visit(ctx.keyval[0]); if (ctx.Comment) { @@ -189,7 +200,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor { /** * Visit a key-value pair */ - keyval(ctx: TomlKeyVal): Doc { + keyval(ctx: any): Doc { const keyDoc = this.visit(ctx.key[0]); const valueDoc = this.visit(ctx.val[0]); return [keyDoc, ' = ', valueDoc]; diff --git a/frontend/src/stores/editorStateStore.ts b/frontend/src/stores/editorStateStore.ts index aae440a..dd85dc8 100644 --- a/frontend/src/stores/editorStateStore.ts +++ b/frontend/src/stores/editorStateStore.ts @@ -7,6 +7,16 @@ export interface DocumentStats { selectedCharacters: number; } +export interface FoldRange { + // 字符偏移(备用) + from: number; + to: number; + + // 行号 + fromLine: number; + toLine: number; +} + export const useEditorStateStore = defineStore('editorState', () => { // 光标位置存储 Record const cursorPositions = ref>({}); @@ -14,6 +24,9 @@ export const useEditorStateStore = defineStore('editorState', () => { // 文档统计数据存储 Record const documentStats = ref>({}); + // 折叠状态存储 Record + const foldStates = ref>({}); + // 保存光标位置 const saveCursorPosition = (docId: number, position: number) => { cursorPositions.value[docId] = position; @@ -38,25 +51,40 @@ export const useEditorStateStore = defineStore('editorState', () => { }; }; + // 保存折叠状态 + const saveFoldState = (docId: number, foldRanges: FoldRange[]) => { + foldStates.value[docId] = foldRanges; + }; + + // 获取折叠状态 + const getFoldState = (docId: number): FoldRange[] => { + return foldStates.value[docId] || []; + }; + // 清除文档状态 const clearDocumentState = (docId: number) => { delete cursorPositions.value[docId]; delete documentStats.value[docId]; + delete foldStates.value[docId]; }; // 清除所有状态 const clearAllStates = () => { cursorPositions.value = {}; documentStats.value = {}; + foldStates.value = {}; }; return { cursorPositions, documentStats, + foldStates, saveCursorPosition, getCursorPosition, saveDocumentStats, getDocumentStats, + saveFoldState, + getFoldState, clearDocumentState, clearAllStates }; @@ -64,7 +92,7 @@ export const useEditorStateStore = defineStore('editorState', () => { persist: { key: 'voidraft-editor-state', storage: localStorage, - pick: ['cursorPositions'] + pick: ['cursorPositions', 'foldStates'] } }); diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 0538d35..10678fc 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -13,6 +13,7 @@ import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension'; import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension'; +import {createFoldStateExtension, restoreFoldState} from '@/views/editor/basic/foldStateExtension'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import { createDynamicExtensions, @@ -118,6 +119,9 @@ export const useEditorStore = defineStore('editor', () => { // 光标位置持久化扩展 const cursorPositionExtension = createCursorPositionExtension(docId); + // 折叠状态持久化扩展 + const foldStateExtension = createFoldStateExtension(docId); + // 快捷键扩展 const keymapExtension = await createDynamicKeymapExtension(); @@ -136,6 +140,7 @@ export const useEditorStore = defineStore('editor', () => { contentChangeExtension, codeBlockExtension, cursorPositionExtension, + foldStateExtension, ...dynamicExtensions, ]; @@ -227,6 +232,12 @@ export const useEditorStore = defineStore('editor', () => { requestAnimationFrame(() => { scrollToCursor(instance.view); instance.view.focus(); + + // 恢复折叠状态 + const savedFoldState = editorStateStore.getFoldState(instance.documentId); + if (savedFoldState.length > 0) { + restoreFoldState(instance.view, savedFoldState); + } }); } catch (error) { console.error('Error showing editor:', error); diff --git a/frontend/src/views/editor/basic/foldStateExtension.ts b/frontend/src/views/editor/basic/foldStateExtension.ts new file mode 100644 index 0000000..7291714 --- /dev/null +++ b/frontend/src/views/editor/basic/foldStateExtension.ts @@ -0,0 +1,113 @@ +import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view'; +import {foldedRanges, foldEffect, unfoldEffect} from '@codemirror/language'; +import {StateEffect} from '@codemirror/state'; +import {useEditorStateStore, type FoldRange} from '@/stores/editorStateStore'; +import {createDebounce} from '@/common/utils/debounce'; + +/** + * 折叠状态持久化扩展 + */ +export function createFoldStateExtension(documentId: number) { + return ViewPlugin.fromClass( + class FoldStatePlugin { + private readonly editorStateStore = useEditorStateStore(); + private readonly debouncedSave; + + constructor(private view: EditorView) { + const {debouncedFn, flush} = createDebounce( + () => this.saveFoldState(), + {delay: 500} + ); + this.debouncedSave = {fn: debouncedFn, flush}; + } + + update(update: ViewUpdate) { + // 检查是否有折叠/展开操作 + const hasFoldChange = update.transactions.some(tr => + tr.effects.some(effect => + effect.is(foldEffect) || effect.is(unfoldEffect) + ) + ); + + if (hasFoldChange) { + this.debouncedSave.fn(); + } + } + + destroy() { + // 销毁时立即执行待保存的操作 + this.debouncedSave.flush(); + // 再保存一次确保最新状态 + this.saveFoldState(); + } + + private saveFoldState() { + const foldRanges: FoldRange[] = []; + const foldCursor = foldedRanges(this.view.state).iter(); + const doc = this.view.state.doc; + + // 遍历所有折叠区间 + while (foldCursor.value !== null) { + const from = foldCursor.from; + const to = foldCursor.to; + + // 同时记录字符偏移和行号 + const fromLine = doc.lineAt(from).number; + const toLine = doc.lineAt(to).number; + + foldRanges.push({ + from, + to, + fromLine, + toLine + }); + + foldCursor.next(); + } + + this.editorStateStore.saveFoldState(documentId, foldRanges); + } + } + ); +} + +/** + * 恢复折叠状态(基于行号,更稳定) + * @param view 编辑器视图 + * @param foldRanges 要恢复的折叠区间 + */ +export function restoreFoldState(view: EditorView, foldRanges: FoldRange[]) { + if (foldRanges.length === 0) return; + + const doc = view.state.doc; + const effects: StateEffect[] = []; + + for (const range of foldRanges) { + try { + // 优先使用行号恢复 + if (range.fromLine && range.toLine) { + // 确保行号在有效范围内 + if (range.fromLine >= 1 && range.toLine <= doc.lines && range.fromLine <= range.toLine) { + const fromPos = doc.line(range.fromLine).from; + const toPos = doc.line(range.toLine).to; + + effects.push(foldEffect.of({from: fromPos, to: toPos})); + continue; + } + } + + // 使用字符偏移 + if (range.from >= 0 && range.to <= doc.length && range.from < range.to) { + effects.push(foldEffect.of({from: range.from, to: range.to})); + } + } catch (error) { + // 忽略无效的折叠区间 + console.warn('Failed to restore fold range:', range, error); + } + } + + if (effects.length > 0) { + view.dispatch({effects}); + } +} + diff --git a/frontend/src/views/editor/extensions/codeblock/index.ts b/frontend/src/views/editor/extensions/codeblock/index.ts index a2e5acb..c9cff98 100644 --- a/frontend/src/views/editor/extensions/codeblock/index.ts +++ b/frontend/src/views/editor/extensions/codeblock/index.ts @@ -89,7 +89,6 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens showBackground = true, enableAutoDetection = true, defaultLanguage = 'text', - separatorHeight = 12, } = options; return [ @@ -108,8 +107,7 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens // 视觉装饰系统 ...getBlockDecorationExtensions({ - showBackground, - separatorHeight + showBackground }), // 光标保护(防止方向键移动到分隔符上) diff --git a/go.mod b/go.mod index 0f06906..1e11711 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,9 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/v2 v2.3.0 - github.com/mattn/go-sqlite3 v1.14.32 + github.com/mattn/go-sqlite3 v1.14.33 github.com/stretchr/testify v1.11.1 - github.com/wailsapp/wails/v3 v3.0.0-alpha.54 + github.com/wailsapp/wails/v3 v3.0.0-alpha.55 golang.org/x/net v0.48.0 golang.org/x/sys v0.39.0 golang.org/x/text v0.32.0 @@ -48,6 +48,7 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/inflect v0.21.5 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -79,7 +80,6 @@ require ( github.com/skeema/knownhosts v1.3.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect - github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.17.0 // indirect github.com/zclconf/go-cty-yaml v1.2.0 // indirect @@ -92,7 +92,6 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2f0b190..1859f42 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,16 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creativeprojects/go-selfupdate v1.5.2 h1:3KR3JLrq70oplb9yZzbmJ89qRP78D1AN/9u+l3k0LJ4= github.com/creativeprojects/go-selfupdate v1.5.2/go.mod h1:BCOuwIl1dRRCmPNRPH0amULeZqayhKyY2mH/h4va7Dk= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= @@ -68,6 +76,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA= @@ -76,6 +86,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -103,6 +115,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -137,14 +151,18 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= @@ -172,14 +190,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= -github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v3 v3.0.0-alpha.54 h1:XlQ+9dwDtOyfxIyA0h1AeT0zdUP3SFdhLkpIgPjWnWc= -github.com/wailsapp/wails/v3 v3.0.0-alpha.54/go.mod h1:yaz8baG0+YzoiN8J6osn0wKiEi0iUux0ZU5NsZFu6OQ= +github.com/wailsapp/wails/v3 v3.0.0-alpha.55 h1:Wxwxc4EN6axDAvH/O5n3uoZQ+XRY/HQZ5rMdn0npq78= +github.com/wailsapp/wails/v3 v3.0.0-alpha.55/go.mod h1:AyH9vRcseorpL3p5XvxKgK0Lv/agJ7pTmcPdy25xZPo= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= @@ -190,6 +208,8 @@ github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCT github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= gitlab.com/gitlab-org/api/client-go v1.10.0 h1:VlB9gXQdG6w643lH53VduUHVnCWQG5Ty86VbXnyi70A= gitlab.com/gitlab-org/api/client-go v1.10.0/go.mod h1:U3QKvjbT1J1FrgLsA7w/XlhoBIendUqB4o3/Ht3UhEQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= @@ -204,7 +224,6 @@ golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= @@ -242,8 +261,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From b6c325198de18ec1cfd2a06e2ac5e0ce6f79645b Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sat, 3 Jan 2026 23:19:43 +0800 Subject: [PATCH 7/7] :art: Removed support for Gitea and optimized update timeouts --- .../voidraft/internal/models/models.ts | 96 +------------ .../voidraft/internal/services/models.ts | 2 +- frontend/src/common/constant/config.ts | 10 +- internal/models/config.go | 39 +----- internal/services/self_update_service.go | 130 ++++-------------- 5 files changed, 33 insertions(+), 244 deletions(-) diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 2d76900..38354e3 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -564,49 +564,6 @@ export class GitBackupConfig { } } -/** - * GiteaConfig Gitea配置 - */ -export class GiteaConfig { - /** - * Gitea服务器URL - */ - "baseURL": string; - - /** - * 仓库所有者 - */ - "owner": string; - - /** - * 仓库名称 - */ - "repo": string; - - /** Creates a new GiteaConfig instance. */ - constructor($$source: Partial = {}) { - if (!("baseURL" in $$source)) { - this["baseURL"] = ""; - } - if (!("owner" in $$source)) { - this["owner"] = ""; - } - if (!("repo" in $$source)) { - this["repo"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new GiteaConfig instance from a string or object. - */ - static createFrom($$source: any = {}): GiteaConfig { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new GiteaConfig($$parsedSource as Partial); - } -} - /** * GithubConfig GitHub配置 */ @@ -1272,26 +1229,6 @@ export enum TabType { TabTypeTab = "tab", }; -/** - * UpdateSourceType 更新源类型 - */ -export enum UpdateSourceType { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * UpdateSourceGithub GitHub更新源 - */ - UpdateSourceGithub = "github", - - /** - * UpdateSourceGitea Gitea更新源 - */ - UpdateSourceGitea = "gitea", -}; - /** * UpdatesConfig 更新设置配置 */ @@ -1306,16 +1243,6 @@ export class UpdatesConfig { */ "autoUpdate": boolean; - /** - * 主要更新源 - */ - "primarySource": UpdateSourceType; - - /** - * 备用更新源 - */ - "backupSource": UpdateSourceType; - /** * 更新前是否备份 */ @@ -1331,11 +1258,6 @@ export class UpdatesConfig { */ "github": GithubConfig; - /** - * Gitea配置 - */ - "gitea": GiteaConfig; - /** Creates a new UpdatesConfig instance. */ constructor($$source: Partial = {}) { if (!("version" in $$source)) { @@ -1344,12 +1266,6 @@ export class UpdatesConfig { if (!("autoUpdate" in $$source)) { this["autoUpdate"] = false; } - if (!("primarySource" in $$source)) { - this["primarySource"] = ("" as UpdateSourceType); - } - if (!("backupSource" in $$source)) { - this["backupSource"] = ("" as UpdateSourceType); - } if (!("backupBeforeUpdate" in $$source)) { this["backupBeforeUpdate"] = false; } @@ -1359,9 +1275,6 @@ export class UpdatesConfig { if (!("github" in $$source)) { this["github"] = (new GithubConfig()); } - if (!("gitea" in $$source)) { - this["gitea"] = (new GiteaConfig()); - } Object.assign(this, $$source); } @@ -1370,14 +1283,10 @@ export class UpdatesConfig { * Creates a new UpdatesConfig instance from a string or object. */ static createFrom($$source: any = {}): UpdatesConfig { - const $$createField6_0 = $$createType9; - const $$createField7_0 = $$createType10; + const $$createField4_0 = $$createType9; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("github" in $$parsedSource) { - $$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); - } - if ("gitea" in $$parsedSource) { - $$parsedSource["gitea"] = $$createField7_0($$parsedSource["gitea"]); + $$parsedSource["github"] = $$createField4_0($$parsedSource["github"]); } return new UpdatesConfig($$parsedSource as Partial); } @@ -1399,4 +1308,3 @@ var $$createType6 = (function $$initCreateType6(...args): any { const $$createType7 = $Create.Map($Create.Any, $Create.Any); const $$createType8 = HotkeyCombo.createFrom; const $$createType9 = GithubConfig.createFrom; -const $$createType10 = GiteaConfig.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index f99485d..85a31f0 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -285,7 +285,7 @@ export class SelfUpdateResult { "error": string; /** - * 更新源(github/gitea) + * 更新源(github) */ "source": string; diff --git a/frontend/src/common/constant/config.ts b/frontend/src/common/constant/config.ts index 67ef212..c3ad2ba 100644 --- a/frontend/src/common/constant/config.ts +++ b/frontend/src/common/constant/config.ts @@ -5,7 +5,6 @@ import { LanguageType, SystemThemeType, TabType, - UpdateSourceType } from '@/../bindings/voidraft/internal/models/models'; import {FONT_OPTIONS} from './fonts'; @@ -110,19 +109,12 @@ export const DEFAULT_CONFIG: AppConfig = { updates: { version: "1.0.0", autoUpdate: true, - primarySource: UpdateSourceType.UpdateSourceGithub, - backupSource: UpdateSourceType.UpdateSourceGitea, backupBeforeUpdate: true, - updateTimeout: 30, + updateTimeout: 120, github: { owner: "landaiqing", repo: "voidraft", }, - gitea: { - baseURL: "https://git.landaiqing.cn", - owner: "landaiqing", - repo: "voidraft", - } }, backup: { enabled: false, diff --git a/internal/models/config.go b/internal/models/config.go index 1f5a2f9..1729458 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -39,29 +39,12 @@ const ( SystemThemeAuto SystemThemeType = "auto" ) -// UpdateSourceType 更新源类型 -type UpdateSourceType string - -const ( - // UpdateSourceGithub GitHub更新源 - UpdateSourceGithub UpdateSourceType = "github" - // UpdateSourceGitea Gitea更新源 - UpdateSourceGitea UpdateSourceType = "gitea" -) - // GithubConfig GitHub配置 type GithubConfig struct { Owner string `json:"owner"` // 仓库所有者 Repo string `json:"repo"` // 仓库名称 } -// GiteaConfig Gitea配置 -type GiteaConfig struct { - BaseURL string `json:"baseURL"` // Gitea服务器URL - Owner string `json:"owner"` // 仓库所有者 - Repo string `json:"repo"` // 仓库名称 -} - // GeneralConfig 通用设置配置 type GeneralConfig struct { AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶 @@ -120,14 +103,11 @@ type AppearanceConfig struct { // UpdatesConfig 更新设置配置 type UpdatesConfig struct { - Version string `json:"version"` // 当前版本号 - AutoUpdate bool `json:"autoUpdate"` // 是否自动更新 - PrimarySource UpdateSourceType `json:"primarySource"` // 主要更新源 - BackupSource UpdateSourceType `json:"backupSource"` // 备用更新源 - BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份 - UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒) - Github GithubConfig `json:"github"` // GitHub配置 - Gitea GiteaConfig `json:"gitea"` // Gitea配置 + Version string `json:"version"` // 当前版本号 + AutoUpdate bool `json:"autoUpdate"` // 是否自动更新 + BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份 + UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒) + Github GithubConfig `json:"github"` // GitHub配置 } // Git备份相关类型定义 @@ -221,19 +201,12 @@ func NewDefaultAppConfig() *AppConfig { Updates: UpdatesConfig{ Version: version.Version, AutoUpdate: true, - PrimarySource: UpdateSourceGitea, - BackupSource: UpdateSourceGithub, BackupBeforeUpdate: true, - UpdateTimeout: 30, + UpdateTimeout: 120, Github: GithubConfig{ Owner: "landaiqing", Repo: "voidraft", }, - Gitea: GiteaConfig{ - BaseURL: "https://git.landaiqing.cn", - Owner: "landaiqing", - Repo: "voidraft", - }, }, Backup: GitBackupConfig{ Enabled: false, diff --git a/internal/services/self_update_service.go b/internal/services/self_update_service.go index d0bbf1a..a503c0a 100644 --- a/internal/services/self_update_service.go +++ b/internal/services/self_update_service.go @@ -25,7 +25,7 @@ type SelfUpdateResult struct { AssetURL string `json:"assetURL"` // 下载链接 ReleaseNotes string `json:"releaseNotes"` // 发布说明 Error string `json:"error"` // 错误信息 - Source string `json:"source"` // 更新源(github/gitea) + Source string `json:"source"` // 更新源(github) } // SelfUpdateService 自我更新服务 @@ -66,33 +66,6 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes return nil, err } - result := &SelfUpdateResult{ - CurrentVersion: config.Updates.Version, - HasUpdate: false, - UpdateApplied: false, - } - - // 尝试主要更新源 - primaryResult, err := s.checkSourceForUpdates(ctx, config.Updates.PrimarySource, config) - if err == nil && primaryResult != nil { - s.handleUpdateBadge(primaryResult) - return primaryResult, nil - } - - // 尝试备用更新源 - backupResult, backupErr := s.checkSourceForUpdates(ctx, config.Updates.BackupSource, config) - if backupErr != nil { - result.Error = fmt.Sprintf("both sources failed: %v; %v", err, backupErr) - s.handleUpdateBadge(result) - return result, errors.New(result.Error) - } - - s.handleUpdateBadge(backupResult) - return backupResult, nil -} - -// checkSourceForUpdates 根据更新源类型检查更新 -func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceType models.UpdateSourceType, config *models.AppConfig) (*SelfUpdateResult, error) { timeout := config.Updates.UpdateTimeout if timeout <= 0 { timeout = 30 @@ -104,28 +77,21 @@ func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceTyp CurrentVersion: config.Updates.Version, HasUpdate: false, UpdateApplied: false, - Source: string(sourceType), - } - - var release *selfupdate.Release - var found bool - var err error - - switch sourceType { - case models.UpdateSourceGithub: - release, found, err = s.checkGithubUpdates(timeoutCtx, config) - case models.UpdateSourceGitea: - release, found, err = s.checkGiteaUpdates(timeoutCtx, config) - default: - return nil, fmt.Errorf("unsupported source: %s", sourceType) + Source: "github", } + // 检查 GitHub 更新 + release, found, err := s.checkGithubUpdates(timeoutCtx, config) if err != nil { - return result, fmt.Errorf("check failed: %w", err) + result.Error = fmt.Sprintf("check github updates failed: %v", err) + s.handleUpdateBadge(result) + return result, err } if !found { - return result, fmt.Errorf("no release for %s/%s", runtime.GOOS, runtime.GOARCH) + result.Error = fmt.Sprintf("no release for %s/%s", runtime.GOOS, runtime.GOARCH) + s.handleUpdateBadge(result) + return result, errors.New(result.Error) } result.LatestVersion = release.Version() @@ -133,6 +99,7 @@ func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceTyp result.ReleaseNotes = release.ReleaseNotes result.HasUpdate = release.GreaterThan(config.Updates.Version) + s.handleUpdateBadge(result) return result, nil } @@ -141,18 +108,6 @@ func (s *SelfUpdateService) createGithubUpdater() (*selfupdate.Updater, error) { return selfupdate.NewUpdater(selfupdate.Config{}) } -// createGiteaUpdater 创建Gitea更新器 -func (s *SelfUpdateService) createGiteaUpdater(config *models.AppConfig) (*selfupdate.Updater, error) { - source, err := selfupdate.NewGiteaSource(selfupdate.GiteaConfig{ - BaseURL: config.Updates.Gitea.BaseURL, - }) - if err != nil { - return nil, fmt.Errorf("create gitea source failed: %w", err) - } - - return selfupdate.NewUpdater(selfupdate.Config{Source: source}) -} - // checkGithubUpdates 检查GitHub更新 func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) { updater, err := s.createGithubUpdater() @@ -164,17 +119,6 @@ func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context, config *mode return updater.DetectLatest(ctx, repo) } -// checkGiteaUpdates 检查Gitea更新 -func (s *SelfUpdateService) checkGiteaUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) { - updater, err := s.createGiteaUpdater(config) - if err != nil { - return nil, false, err - } - - repo := selfupdate.NewRepositorySlug(config.Updates.Gitea.Owner, config.Updates.Gitea.Repo) - return updater.DetectLatest(ctx, repo) -} - // ApplyUpdate 应用更新 func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) { s.mu.Lock() @@ -201,23 +145,17 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, return nil, fmt.Errorf("locate executable failed: %w", err) } - // 尝试主要源 - result, err := s.performUpdate(ctx, config.Updates.PrimarySource, exe, config) - if err == nil { - return result, nil - } - - // 尝试备用源 - result, err = s.performUpdate(ctx, config.Updates.BackupSource, exe, config) + // 执行 GitHub 更新 + result, err := s.performUpdate(ctx, exe, config) if err != nil { - return nil, fmt.Errorf("update failed from both sources: %w", err) + return nil, fmt.Errorf("update failed: %w", err) } return result, nil } // performUpdate 执行更新操作(包括检测、备份、下载、应用) -func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models.UpdateSourceType, exe string, config *models.AppConfig) (*SelfUpdateResult, error) { +func (s *SelfUpdateService) performUpdate(ctx context.Context, exe string, config *models.AppConfig) (*SelfUpdateResult, error) { timeout := config.Updates.UpdateTimeout if timeout <= 0 { timeout = 30 @@ -225,8 +163,13 @@ func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models checkCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() - // 获取更新器和版本信息 - updater, release, found, err := s.getUpdateFromSource(checkCtx, sourceType, config) + // 获取 GitHub 更新信息 + updater, err := s.createGithubUpdater() + if err != nil { + return nil, fmt.Errorf("create github updater failed: %w", err) + } + + release, found, err := s.checkGithubUpdates(checkCtx, config) if err != nil || !found { return nil, fmt.Errorf("detect release failed: %w", err) } @@ -236,7 +179,7 @@ func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models LatestVersion: release.Version(), AssetURL: release.AssetURL, ReleaseNotes: release.ReleaseNotes, - Source: string(sourceType), + Source: "github", HasUpdate: release.GreaterThan(config.Updates.Version), } @@ -269,33 +212,6 @@ func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models return result, nil } -// getUpdateFromSource 从指定源获取更新信息 -func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType models.UpdateSourceType, config *models.AppConfig) (*selfupdate.Updater, *selfupdate.Release, bool, error) { - var updater *selfupdate.Updater - var release *selfupdate.Release - var found bool - var err error - - switch sourceType { - case models.UpdateSourceGithub: - updater, err = s.createGithubUpdater() - if err != nil { - return nil, nil, false, err - } - release, found, err = s.checkGithubUpdates(ctx, config) - case models.UpdateSourceGitea: - updater, err = s.createGiteaUpdater(config) - if err != nil { - return nil, nil, false, err - } - release, found, err = s.checkGiteaUpdates(ctx, config) - default: - return nil, nil, false, fmt.Errorf("unsupported source: %s", sourceType) - } - - return updater, release, found, err -} - // handleUpdateSuccess 处理更新成功后的操作 func (s *SelfUpdateService) handleUpdateSuccess(result *SelfUpdateResult) { // 更新配置版本