diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 4aa14dc..38354e3 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -461,6 +461,11 @@ export class GeneralConfig { */ "enableTabs": boolean; + /** + * 是否启用内存监视器 + */ + "enableMemoryMonitor": boolean; + /** Creates a new GeneralConfig instance. */ constructor($$source: Partial = {}) { if (!("alwaysOnTop" in $$source)) { @@ -490,6 +495,9 @@ export class GeneralConfig { if (!("enableTabs" in $$source)) { this["enableTabs"] = false; } + if (!("enableMemoryMonitor" in $$source)) { + this["enableMemoryMonitor"] = false; + } Object.assign(this, $$source); } @@ -556,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配置 */ @@ -1264,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 更新设置配置 */ @@ -1298,16 +1243,6 @@ export class UpdatesConfig { */ "autoUpdate": boolean; - /** - * 主要更新源 - */ - "primarySource": UpdateSourceType; - - /** - * 备用更新源 - */ - "backupSource": UpdateSourceType; - /** * 更新前是否备份 */ @@ -1323,11 +1258,6 @@ export class UpdatesConfig { */ "github": GithubConfig; - /** - * Gitea配置 - */ - "gitea": GiteaConfig; - /** Creates a new UpdatesConfig instance. */ constructor($$source: Partial = {}) { if (!("version" in $$source)) { @@ -1336,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; } @@ -1351,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); } @@ -1362,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); } @@ -1391,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/keybindingservice.ts b/frontend/bindings/voidraft/internal/services/keybindingservice.ts index c73ee8c..89b3e90 100644 --- a/frontend/bindings/voidraft/internal/services/keybindingservice.ts +++ b/frontend/bindings/voidraft/internal/services/keybindingservice.ts @@ -89,13 +89,21 @@ export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(3432755175, id, key) as any; return $resultPromise; } +/** + * UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态 + */ +export function UpdateKeyBindingPreventDefault(id: number, preventDefault: boolean): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(202386744, id, preventDefault) as any; + return $resultPromise; +} + // Private type creation functions const $$createType0 = models$0.KeyBinding.createFrom; const $$createType1 = $Create.Array($$createType0); 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/components.d.ts b/frontend/components.d.ts index 6b74695..8b2a0b4 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -11,6 +11,8 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AccordionContainer: typeof import('./src/components/accordion/AccordionContainer.vue')['default'] + AccordionItem: typeof import('./src/components/accordion/AccordionItem.vue')['default'] BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default'] DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default'] LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default'] @@ -22,6 +24,8 @@ declare module 'vue' { TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default'] TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default'] TabItem: typeof import('./src/components/tabs/TabItem.vue')['default'] + Toast: typeof import('./src/components/toast/Toast.vue')['default'] + ToastContainer: typeof import('./src/components/toast/ToastContainer.vue')['default'] Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default'] WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default'] WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default'] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dcc0b89..5be1c9d 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,7 +77,6 @@ "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", "unplugin-vue-components": "^30.0.0", @@ -217,7 +216,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -266,7 +264,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -293,7 +290,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -321,7 +317,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -534,7 +529,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -611,7 +605,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz", "integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -621,7 +614,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.6.tgz", "integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1445,8 +1437,7 @@ "version": "1.5.0", "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.0.tgz", "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@lezer/cpp": { "version": "1.1.3", @@ -1500,7 +1491,6 @@ "resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -1532,7 +1522,6 @@ "resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz", "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -1565,7 +1554,6 @@ "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.5.tgz", "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -2481,21 +2469,21 @@ "license": "MIT" }, "node_modules/@toml-tools/lexer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@toml-tools/lexer/-/lexer-1.0.1.tgz", - "integrity": "sha512-jn2fl8m/9QPcUD507Hbt2W3TVMKzF5HEY8xKIxqY2r2dTG2udeCKlo2ejJ5k/RSOJsWNIuw+Ir/nxW5PItUApA==", + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz", + "integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==", "license": "MIT", "dependencies": { "chevrotain": "^11.0.1" } }, "node_modules/@toml-tools/parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@toml-tools/parser/-/parser-1.0.1.tgz", - "integrity": "sha512-W+YdnB8KDgKjIqhoArEXjiTTPnKSXVvI/B+raHfou9+sip3rxhzVsELn46GG7dZyNHyu9pS+gYgYrdF9c5AQDg==", + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz", + "integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==", "license": "MIT", "dependencies": { - "@toml-tools/lexer": "^1.0.1", + "@toml-tools/lexer": "^1.0.0", "chevrotain": "^11.0.1" } }, @@ -2847,7 +2835,6 @@ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2925,7 +2912,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -3617,7 +3603,6 @@ "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4131,7 +4116,6 @@ "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -4471,7 +4455,6 @@ "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -4881,7 +4864,6 @@ "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5296,7 +5278,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5611,7 +5592,6 @@ "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.3.0" } @@ -6244,6 +6224,18 @@ "lodash": "4.17.21" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -7164,7 +7156,6 @@ "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -7313,7 +7304,6 @@ "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.4.tgz", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7385,9 +7375,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7567,7 +7557,6 @@ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7670,7 +7659,6 @@ "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.1.tgz", "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -8115,7 +8103,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8235,7 +8222,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8550,7 +8536,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -8661,7 +8646,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8921,7 +8905,6 @@ "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -8944,7 +8927,6 @@ "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", diff --git a/frontend/package.json b/frontend/package.json index 5f4dde5..88428dd 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/public/images/blockImage.svg b/frontend/public/images/blockImage.svg new file mode 100644 index 0000000..0e7675a --- /dev/null +++ b/frontend/public/images/blockImage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/colorSelector.svg b/frontend/public/images/colorSelector.svg new file mode 100644 index 0000000..311ce13 --- /dev/null +++ b/frontend/public/images/colorSelector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/contextMenu.svg b/frontend/public/images/contextMenu.svg new file mode 100644 index 0000000..f099e52 --- /dev/null +++ b/frontend/public/images/contextMenu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/fold.svg b/frontend/public/images/fold.svg new file mode 100644 index 0000000..eb764f8 --- /dev/null +++ b/frontend/public/images/fold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/highlightTrailingWhitespace.svg b/frontend/public/images/highlightTrailingWhitespace.svg new file mode 100644 index 0000000..25c90d8 --- /dev/null +++ b/frontend/public/images/highlightTrailingWhitespace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/highlightWhitespace.svg b/frontend/public/images/highlightWhitespace.svg new file mode 100644 index 0000000..c3229c3 --- /dev/null +++ b/frontend/public/images/highlightWhitespace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/httpClient.svg b/frontend/public/images/httpClient.svg new file mode 100644 index 0000000..641302b --- /dev/null +++ b/frontend/public/images/httpClient.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/hyperlink.svg b/frontend/public/images/hyperlink.svg new file mode 100644 index 0000000..e84e57b --- /dev/null +++ b/frontend/public/images/hyperlink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/lineNumbers.svg b/frontend/public/images/lineNumbers.svg new file mode 100644 index 0000000..e473bbb --- /dev/null +++ b/frontend/public/images/lineNumbers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/markdown.svg b/frontend/public/images/markdown.svg new file mode 100644 index 0000000..fc2e331 --- /dev/null +++ b/frontend/public/images/markdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/minimap.svg b/frontend/public/images/minimap.svg new file mode 100644 index 0000000..4fd7e4c --- /dev/null +++ b/frontend/public/images/minimap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/rainbowBrackets.svg b/frontend/public/images/rainbowBrackets.svg new file mode 100644 index 0000000..040de39 --- /dev/null +++ b/frontend/public/images/rainbowBrackets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/search.svg b/frontend/public/images/search.svg new file mode 100644 index 0000000..c58e8b2 --- /dev/null +++ b/frontend/public/images/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/translator.svg b/frontend/public/images/translator.svg new file mode 100644 index 0000000..f728a3a --- /dev/null +++ b/frontend/public/images/translator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 039bbe5..4551002 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -6,7 +6,10 @@ import {useKeybindingStore} from '@/stores/keybindingStore'; import {useThemeStore} from '@/stores/themeStore'; import {useUpdateStore} from '@/stores/updateStore'; import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue'; +import ToastContainer from '@/components/toast/ToastContainer.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 +17,7 @@ const keybindingStore = useKeybindingStore(); const themeStore = useThemeStore(); const updateStore = useUpdateStore(); const translationStore = useTranslationStore(); +const {locale} = useI18n(); onBeforeMount(async () => { // 并行初始化配置、系统信息和快捷键配置 @@ -22,9 +26,8 @@ onBeforeMount(async () => { systemStore.initSystemInfo(), keybindingStore.loadKeyBindings(), ]); - - // 初始化语言和主题 - await configStore.initLanguage(); + + locale.value = configStore.config.appearance.language || LanguageType.LangEnUS; await themeStore.initTheme(); await translationStore.loadTranslators(); @@ -39,6 +42,7 @@ onBeforeMount(async () => {
+ diff --git a/frontend/src/assets/images/translator.svg b/frontend/src/assets/images/translator.svg new file mode 100644 index 0000000..2c84e25 --- /dev/null +++ b/frontend/src/assets/images/translator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/common/constant/config.ts b/frontend/src/common/constant/config.ts index 6d9aba3..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'; @@ -24,6 +23,7 @@ export const CONFIG_KEY_MAP = { enableWindowSnap: 'general.enableWindowSnap', enableLoadingAnimation: 'general.enableLoadingAnimation', enableTabs: 'general.enableTabs', + enableMemoryMonitor: 'general.enableMemoryMonitor', // editing fontSize: 'editing.fontSize', fontFamily: 'editing.fontFamily', @@ -88,6 +88,7 @@ export const DEFAULT_CONFIG: AppConfig = { enableWindowSnap: true, enableLoadingAnimation: true, enableTabs: false, + enableMemoryMonitor: true, }, editing: { fontSize: CONFIG_LIMITS.fontSize.default, @@ -108,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/frontend/src/common/constant/editor.ts b/frontend/src/common/constant/editor.ts index 1616f7e..f73d48d 100644 --- a/frontend/src/common/constant/editor.ts +++ b/frontend/src/common/constant/editor.ts @@ -5,7 +5,7 @@ // 编辑器实例管理 export const EDITOR_CONFIG = { /** 最多缓存的编辑器实例数量 */ - MAX_INSTANCES: 5, + MAX_INSTANCES: 10, /** 语法树缓存过期时间(毫秒) */ SYNTAX_TREE_CACHE_TIMEOUT: 30000, /** 加载状态延迟时间(毫秒) */ 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/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/accordion/AccordionContainer.vue b/frontend/src/components/accordion/AccordionContainer.vue new file mode 100644 index 0000000..24dd01b --- /dev/null +++ b/frontend/src/components/accordion/AccordionContainer.vue @@ -0,0 +1,105 @@ + + + + + + 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/components/tabs/TabContainer.vue b/frontend/src/components/tabs/TabContainer.vue index 55a824a..5973029 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,14 @@ 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'; +import { useEditorStateStore } from '@/stores/editorStateStore'; const tabStore = useTabStore(); +const documentStore = useDocumentStore(); +const editorStore = useEditorStore(); +const editorStateStore = useEditorStateStore(); // DOM 引用 const tabBarRef = ref(); @@ -50,8 +56,36 @@ const contextMenuTargetId = ref(null); // 标签页操作 -const switchToTab = (documentId: number) => { - tabStore.switchToTabAndDocument(documentId); +const switchToTab = async (documentId: number) => { + + // 保存旧文档的光标位置 + const oldDocId = documentStore.currentDocumentId; + if (oldDocId) { + const cursorPos = editorStore.getCurrentCursorPosition(); + editorStateStore.saveCursorPosition(oldDocId, cursorPos); + } + + // 如果旧文档有未保存修改,保存它 + if (oldDocId && editorStore.hasUnsavedChanges(oldDocId)) { + try { + const content = editorStore.getCurrentContent(); + await documentStore.saveDocument(oldDocId, content); + editorStore.syncAfterSave(oldDocId); + } catch (error) { + console.error('save document error:', error); + } + } + + // 切换文档 + await tabStore.switchToTabAndDocument(documentId); + + // 切换到新编辑器 + await editorStore.switchToEditor(documentId); + + // 更新标签页 + if (documentStore.currentDocument && tabStore.isTabsEnabled) { + tabStore.addOrActivateTab(documentStore.currentDocument); + } }; const closeTab = (documentId: number) => { @@ -150,7 +184,7 @@ onUnmounted(() => { }); // 监听当前活跃标签页的变化 -watch(() => tabStore.currentDocumentId, () => { +watch(() => documentStore.currentDocumentId, () => { scrollToActiveTab(); }); 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 13d5bca..071fdc3 100644 --- a/frontend/src/components/toolbar/BlockLanguageSelector.vue +++ b/frontend/src/components/toolbar/BlockLanguageSelector.vue @@ -51,13 +51,13 @@ let editorScope: ReturnType | null = null; // 更新当前块语言信息 const updateCurrentBlockLanguage = () => { - if (!editorStore.editorView) { + if (!editorStore.currentEditor) { currentBlockLanguage.value = { name: 'text', auto: false }; return; } try { - const state = editorStore.editorView.state; + const state = editorStore.currentEditor.state; const activeBlock = getActiveNoteBlock(state as any); if (activeBlock) { const newLanguage = { @@ -128,7 +128,7 @@ const setupEventListeners = (view: any) => { // 监听编辑器状态变化 watch( - () => editorStore.editorView, + () => editorStore.currentEditor, (newView) => { if (newView) { setupEventListeners(newView); @@ -175,13 +175,13 @@ const closeLanguageMenu = () => { // 选择语言 - 优化性能 const selectLanguage = (languageId: SupportedLanguage) => { - if (!editorStore.editorView) { + if (!editorStore.currentEditor) { closeLanguageMenu(); return; } try { - const view = editorStore.editorView; + const view = editorStore.currentEditor; const state = view.state; const dispatch = view.dispatch; @@ -294,9 +294,11 @@ const scrollToCurrentLanguage = () => { -
    - -
    + + +
    + +
    { {{ t('toolbar.noLanguageFound') }}
    -
    +
    + diff --git a/frontend/src/components/toolbar/Toolbar.vue b/frontend/src/components/toolbar/Toolbar.vue index 4821b5d..2191455 100644 --- a/frontend/src/components/toolbar/Toolbar.vue +++ b/frontend/src/components/toolbar/Toolbar.vue @@ -3,6 +3,7 @@ import {useI18n} from 'vue-i18n'; import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue'; import {useConfigStore} from '@/stores/configStore'; import {useEditorStore} from '@/stores/editorStore'; +import {useEditorStateStore} from '@/stores/editorStateStore'; import {useUpdateStore} from '@/stores/updateStore'; import {useWindowStore} from '@/stores/windowStore'; import {useSystemStore} from '@/stores/systemStore'; @@ -15,6 +16,7 @@ import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode import {createDebounce} from '@/common/utils/debounce'; const editorStore = useEditorStore(); +const editorStateStore = useEditorStateStore(); const configStore = useConfigStore(); const updateStore = useUpdateStore(); const windowStore = useWindowStore(); @@ -25,7 +27,6 @@ const router = useRouter(); const canFormatCurrentBlock = ref(false); const isLoaded = shallowRef(false); -const { documentStats } = toRefs(editorStore); const { config } = toRefs(configStore); // 窗口置顶状态 @@ -57,14 +58,14 @@ const goToSettings = () => { // 执行格式化 const formatCurrentBlock = () => { - if (!canFormatCurrentBlock.value || !editorStore.editorView) return; - formatBlockContent(editorStore.editorView); + if (!canFormatCurrentBlock.value || !editorStore.currentEditor) return; + formatBlockContent(editorStore.currentEditor); }; // 统一更新按钮状态 const updateButtonStates = () => { - const view: any = editorStore.editorView; + const view: any = editorStore.currentEditor; if (!view) { canFormatCurrentBlock.value = false; return; @@ -125,7 +126,7 @@ const setupEditorListeners = (view: any) => { // 监听编辑器视图变化 watch( - () => editorStore.editorView, + () => editorStore.currentEditor, (newView) => { // 在 scope 中管理副作用 editorScope.run(() => { @@ -191,11 +192,13 @@ const updateButtonTitle = computed(() => { }); // 统计数据的计算属性 -const statsData = computed(() => ({ - lines: documentStats.value.lines, - characters: documentStats.value.characters, - selectedCharacters: documentStats.value.selectedCharacters -})); +const statsData = computed(() => { + const docId = editorStore.currentEditorId; + if (!docId) { + return { lines: 0, characters: 0, selectedCharacters: 0 }; + } + return editorStateStore.getDocumentStats(docId); +}); @@ -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/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(); +};