diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 6719df7..961398e 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -9,6 +9,7 @@ export {} declare module 'vue' { export interface GlobalComponents { 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'] MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default'] MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default'] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5937771..3c4ed1e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -40,8 +40,6 @@ "@codemirror/view": "^6.37.2", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", - "@types/uuid": "^10.0.0", - "@vueuse/core": "^13.3.0", "codemirror": "^6.0.1", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", @@ -2103,18 +2101,6 @@ "undici-types": "~7.8.0" } }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "license": "MIT" - }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.34.1", "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", @@ -2610,44 +2596,6 @@ "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==", "license": "MIT" }, - "node_modules/@vueuse/core": { - "version": "13.3.0", - "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.3.0.tgz", - "integrity": "sha512-uYRz5oEfebHCoRhK4moXFM3NSCd5vu2XMLOq/Riz5FdqZMy2RvBtazdtL3gEcmDyqkztDe9ZP/zymObMIbiYSg==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "13.3.0", - "@vueuse/shared": "13.3.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@vueuse/metadata": { - "version": "13.3.0", - "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.3.0.tgz", - "integrity": "sha512-42IzJIOYCKIb0Yjv1JfaKpx8JlCiTmtCWrPxt7Ja6Wzoq0h79+YVXmBV03N966KEmDEESTbp5R/qO3AB5BDnGw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "13.3.0", - "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.3.0.tgz", - "integrity": "sha512-L1QKsF0Eg9tiZSFXTgodYnu0Rsa2P0En2LuLrIs/jgrkyiDuJSsPZK+tx+wU0mMsYHUYEjNsuE41uqqkuR8VhA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, "node_modules/@wailsio/runtime": { "version": "3.0.0-alpha.66", "resolved": "https://registry.npmmirror.com/@wailsio/runtime/-/runtime-3.0.0-alpha.66.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3a1c606..911e530 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,8 +44,6 @@ "@codemirror/view": "^6.37.2", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", - "@types/uuid": "^10.0.0", - "@vueuse/core": "^13.3.0", "codemirror": "^6.0.1", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", diff --git a/frontend/src/assets/styles/variables.css b/frontend/src/assets/styles/variables.css index e996367..81d4fbf 100644 --- a/frontend/src/assets/styles/variables.css +++ b/frontend/src/assets/styles/variables.css @@ -23,6 +23,11 @@ --dark-scrollbar-track: #2a2a2a; --dark-scrollbar-thumb: #555555; --dark-scrollbar-thumb-hover: #666666; + --dark-selection-bg: rgba(181, 206, 168, 0.1); + --dark-selection-text: #b5cea8; + --dark-danger-color: #ff6b6b; + --dark-bg-primary: #1a1a1a; + --dark-bg-hover: #2a2a2a; /* 浅色主题颜色变量 */ --light-toolbar-bg: #f8f9fa; @@ -45,6 +50,11 @@ --light-scrollbar-track: #f1f3f4; --light-scrollbar-thumb: #c1c1c1; --light-scrollbar-thumb-hover: #a8a8a8; + --light-selection-bg: rgba(59, 130, 246, 0.15); + --light-selection-text: #2563eb; + --light-danger-color: #dc3545; + --light-bg-primary: #ffffff; + --light-bg-hover: #f1f3f4; /* 默认使用深色主题 */ --toolbar-bg: var(--dark-toolbar-bg); @@ -68,6 +78,11 @@ --scrollbar-track: var(--dark-scrollbar-track); --scrollbar-thumb: var(--dark-scrollbar-thumb); --scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover); + --selection-bg: var(--dark-selection-bg); + --selection-text: var(--dark-selection-text); + --text-danger: var(--dark-danger-color); + --bg-primary: var(--dark-bg-primary); + --bg-hover: var(--dark-bg-hover); color-scheme: light dark; } @@ -96,6 +111,11 @@ --scrollbar-track: var(--dark-scrollbar-track); --scrollbar-thumb: var(--dark-scrollbar-thumb); --scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover); + --selection-bg: var(--dark-selection-bg); + --selection-text: var(--dark-selection-text); + --text-danger: var(--dark-danger-color); + --bg-primary: var(--dark-bg-primary); + --bg-hover: var(--dark-bg-hover); } } @@ -123,6 +143,11 @@ --scrollbar-track: var(--light-scrollbar-track); --scrollbar-thumb: var(--light-scrollbar-thumb); --scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover); + --selection-bg: var(--light-selection-bg); + --selection-text: var(--light-selection-text); + --text-danger: var(--light-danger-color); + --bg-primary: var(--light-bg-primary); + --bg-hover: var(--light-bg-hover); } } @@ -149,6 +174,11 @@ --scrollbar-track: var(--light-scrollbar-track); --scrollbar-thumb: var(--light-scrollbar-thumb); --scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover); + --selection-bg: var(--light-selection-bg); + --selection-text: var(--light-selection-text); + --text-danger: var(--light-danger-color); + --bg-primary: var(--light-bg-primary); + --bg-hover: var(--light-bg-hover); } /* 手动选择深色主题 */ @@ -174,4 +204,7 @@ --scrollbar-track: var(--dark-scrollbar-track); --scrollbar-thumb: var(--dark-scrollbar-thumb); --scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover); + --selection-bg: var(--dark-selection-bg); + --selection-text: var(--dark-selection-text); + --text-danger: var(--dark-danger-color); } \ No newline at end of file diff --git a/frontend/src/components/toolbar/BlockLanguageSelector.vue b/frontend/src/components/toolbar/BlockLanguageSelector.vue index ccf07cb..3f6ca13 100644 --- a/frontend/src/components/toolbar/BlockLanguageSelector.vue +++ b/frontend/src/components/toolbar/BlockLanguageSelector.vue @@ -505,11 +505,12 @@ const scrollToCurrentLanguage = () => { } &.active { - background-color: rgba(181, 206, 168, 0.1); - color: #b5cea8; + background-color: var(--selection-bg); + color: var(--selection-text); .language-alias { - color: rgba(181, 206, 168, 0.7); + color: var(--selection-text); + opacity: 0.7; } } diff --git a/frontend/src/components/toolbar/DocumentSelector.vue b/frontend/src/components/toolbar/DocumentSelector.vue new file mode 100644 index 0000000..9964352 --- /dev/null +++ b/frontend/src/components/toolbar/DocumentSelector.vue @@ -0,0 +1,666 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/toolbar/Toolbar.vue b/frontend/src/components/toolbar/Toolbar.vue index 13f679d..53e1b49 100644 --- a/frontend/src/components/toolbar/Toolbar.vue +++ b/frontend/src/components/toolbar/Toolbar.vue @@ -7,6 +7,7 @@ import {useUpdateStore} from '@/stores/updateStore'; import * as runtime from '@wailsio/runtime'; import {useRouter} from 'vue-router'; import BlockLanguageSelector from './BlockLanguageSelector.vue'; +import DocumentSelector from './DocumentSelector.vue'; import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages'; @@ -95,7 +96,7 @@ watch( {immediate: true} ); -// 定期更新格式化按钮状态(作为备用机制) +// 定期更新格式化按钮状态 let formatButtonUpdateTimer: number | null = null; const isLoaded = ref(false); @@ -158,6 +159,9 @@ watch(isLoaded, async (newLoaded) => { {{ configStore.config.editing.fontSize }}px + + + diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 793e61a..d6056c1 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -1,4 +1,5 @@ export default { + locale: 'en-US', titlebar: { minimize: 'Minimize', maximize: 'Maximize', @@ -18,6 +19,23 @@ export default { searchLanguage: 'Search language...', noLanguageFound: 'No language found', formatHint: 'Current block supports formatting, use Ctrl+Shift+F shortcut for formatting', + // Document selector + selectDocument: 'Select Document', + searchOrCreateDocument: 'Search or enter new document name...', + createDocument: 'Create', + noDocumentFound: 'No document found', + loading: 'Loading...', + rename: 'Rename', + delete: 'Delete', + confirm: 'Confirm', + confirmDelete: 'Click again to confirm delete', + documentNameTooLong: 'Document name cannot exceed {max} characters', + documentNameRequired: 'Document name cannot be empty', + cannotDeleteLastDocument: 'Cannot delete the last document', + cannotDeleteDefaultDocument: 'Cannot delete the default document', + unknownTime: 'Unknown time', + invalidDate: 'Invalid date', + timeError: 'Time error', }, languages: { 'zh-CN': 'Chinese', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 76b651c..01bcf0b 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1,4 +1,5 @@ export default { + locale: 'zh-CN', titlebar: { minimize: '最小化', maximize: '最大化', @@ -18,6 +19,23 @@ export default { searchLanguage: '搜索语言...', noLanguageFound: '未找到匹配的语言', formatHint: '当前区块支持格式化,使用 Ctrl+Shift+F 进行格式化', + // 文档选择器 + selectDocument: '选择文档', + searchOrCreateDocument: '搜索或输入新文档名...', + createDocument: '创建', + noDocumentFound: '没有找到文档', + loading: '加载中...', + rename: '重命名', + delete: '删除', + confirm: '确认', + confirmDelete: '再次点击确认删除', + documentNameTooLong: '文档名称不能超过{max}个字符', + documentNameRequired: '文档名称不能为空', + cannotDeleteLastDocument: '无法删除最后一个文档', + cannotDeleteDefaultDocument: '无法删除默认文档', + unknownTime: '未知时间', + invalidDate: '无效日期', + timeError: '时间错误', }, languages: { 'zh-CN': '简体中文', diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts index fd5424b..0494eb9 100644 --- a/frontend/src/stores/documentStore.ts +++ b/frontend/src/stores/documentStore.ts @@ -3,114 +3,222 @@ import {computed, ref} from 'vue'; import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {Document} from '@/../bindings/voidraft/internal/models/models'; +const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID + export const useDocumentStore = defineStore('document', () => { - // 状态 + // === 核心状态 === + const documents = ref>({}); + const recentDocumentIds = ref([SCRATCH_DOCUMENT_ID]); + const currentDocumentId = ref(null); const currentDocument = ref(null); + + // === UI状态 === + const showDocumentSelector = ref(false); const isLoading = ref(false); - const isSaving = ref(false); - const lastSaved = ref(null); - // 计算属性 - const documentContent = computed(() => currentDocument.value?.content ?? ''); - const documentTitle = computed(() => currentDocument.value?.title ?? ''); - const hasDocument = computed(() => !!currentDocument.value); - const isSaveInProgress = computed(() => isSaving.value); - const lastSavedTime = computed(() => lastSaved.value); + // === 计算属性 === + const documentList = computed(() => + Object.values(documents.value).sort((a, b) => { + const aIndex = recentDocumentIds.value.indexOf(a.id); + const bIndex = recentDocumentIds.value.indexOf(b.id); - // 加载文档 - const loadDocument = async (documentId = 1): Promise => { - if (isLoading.value) return currentDocument.value; + // 按最近使用排序 + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; - isLoading.value = true; + // 然后按更新时间排序 + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }) + ); + + // === 私有方法 === + const addRecentDocument = (docId: number) => { + const recent = recentDocumentIds.value.filter(id => id !== docId); + recent.unshift(docId); + recentDocumentIds.value = recent.slice(0, 100); // 保留最近100个 + }; + + const setDocuments = (docs: Document[]) => { + documents.value = {}; + docs.forEach(doc => { + documents.value[doc.id] = doc; + }); + }; + + // === 公共API === + + // 更新文档列表 + const updateDocuments = async () => { try { - const doc = await DocumentService.GetDocumentByID(documentId); + const docs = await DocumentService.ListAllDocumentsMeta(); + if (docs) { + setDocuments(docs.filter((doc): doc is Document => doc !== null)); + } + } catch (error) { + console.error('Failed to update documents:', error); + } + }; + + // 打开文档 + const openDocument = async (docId: number): Promise => { + try { + closeDialog(); + + // 获取完整文档数据 + const doc = await DocumentService.GetDocumentByID(docId); + if (!doc) { + throw new Error(`Document ${docId} not found`); + } + + currentDocumentId.value = docId; + currentDocument.value = doc; + addRecentDocument(docId); + + return true; + } catch (error) { + console.error('Failed to open document:', error); + return false; + } + }; + + // 创建新文档 + const createNewDocument = async (title: string): Promise => { + try { + const newDoc = await DocumentService.CreateDocument(title); + if (!newDoc) { + throw new Error('Failed to create document'); + } + + // 更新文档列表 + documents.value[newDoc.id] = newDoc; + + return newDoc; + } catch (error) { + console.error('Failed to create document:', error); + return null; + } + }; + + // 保存新文档 + const saveNewDocument = async (title: string, content: string): Promise => { + try { + const newDoc = await createNewDocument(title); + if (!newDoc) return false; + + // 更新内容 + await DocumentService.UpdateDocumentContent(newDoc.id, content); + newDoc.content = content; + + return true; + } catch (error) { + console.error('Failed to save new document:', error); + return false; + } + }; + + // 更新文档元数据 + const updateDocumentMetadata = async (docId: number, title: string, newPath?: string): Promise => { + try { + await DocumentService.UpdateDocumentTitle(docId, title); + + // 更新本地状态 + const doc = documents.value[docId]; if (doc) { - currentDocument.value = doc; - return doc; + doc.title = title; + doc.updatedAt = new Date(); } - return null; - } catch (error) { - return null; - } finally { - isLoading.value = false; - } - }; - // 保存文档内容 - const saveDocumentContent = async (content: string): Promise => { - // 如果内容没有变化,直接返回成功 - if (currentDocument.value?.content === content) { - return true; - } - - // 如果正在保存中,直接返回 - if (isSaving.value) { - return false; - } - - isSaving.value = true; - try { - const documentId = currentDocument.value?.id || 1; - await DocumentService.UpdateDocumentContent(documentId, content); - - const now = new Date(); - lastSaved.value = now; - - // 更新本地副本 - if (currentDocument.value) { - currentDocument.value.content = content; - currentDocument.value.updatedAt = now; + if (currentDocument.value?.id === docId) { + currentDocument.value.title = title; + currentDocument.value.updatedAt = new Date(); } return true; } catch (error) { + console.error('Failed to update document metadata:', error); return false; - } finally { - isSaving.value = false; } }; - // 保存文档标题 - const saveDocumentTitle = async (title: string): Promise => { - if (!currentDocument.value || currentDocument.value.title === title) { - return true; - } - + // 删除文档 + const deleteDocument = async (docId: number): Promise => { try { - await DocumentService.UpdateDocumentTitle(currentDocument.value.id, title); - const now = new Date(); - lastSaved.value = now; - currentDocument.value.title = title; - currentDocument.value.updatedAt = now; + // 检查是否是默认文档(使用ID判断) + if (docId === SCRATCH_DOCUMENT_ID) { + return false; + } + + await DocumentService.DeleteDocument(docId); + + // 更新本地状态 + delete documents.value[docId]; + recentDocumentIds.value = recentDocumentIds.value.filter(id => id !== docId); + + // 如果删除的是当前文档,切换到第一个可用文档 + if (currentDocumentId.value === docId) { + const availableDocs = Object.values(documents.value); + if (availableDocs.length > 0) { + await openDocument(availableDocs[0].id); + } else { + currentDocumentId.value = null; + currentDocument.value = null; + } + } + return true; } catch (error) { + console.error('Failed to delete document:', error); return false; } }; - // 初始化 + // === UI控制 === + const openDocumentSelector = () => { + closeDialog(); + showDocumentSelector.value = true; + }; + + const closeDialog = () => { + showDocumentSelector.value = false; + }; + + // === 初始化 === const initialize = async (): Promise => { - await loadDocument(); + try { + await updateDocuments(); + + // 获取第一个文档ID并打开 + const firstDocId = await DocumentService.GetFirstDocumentID(); + if (firstDocId && documents.value[firstDocId]) { + await openDocument(firstDocId); + } + } catch (error) { + console.error('Failed to initialize document store:', error); + } }; return { // 状态 + documents, + documentList, + recentDocumentIds, + currentDocumentId, currentDocument, + showDocumentSelector, isLoading, - isSaving, - lastSaved, - - // 计算属性 - documentContent, - documentTitle, - hasDocument, - isSaveInProgress, - lastSavedTime, // 方法 - loadDocument, - saveDocumentContent, - saveDocumentTitle, - initialize + updateDocuments, + openDocument, + createNewDocument, + saveNewDocument, + updateDocumentMetadata, + deleteDocument, + openDocumentSelector, + closeDialog, + initialize, }; }); \ No newline at end of file diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index f0be247..2b5f166 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -1,24 +1,26 @@ import {defineStore} from 'pinia'; -import {ref, watch} from 'vue'; +import {ref, watch, nextTick} from 'vue'; import {EditorView} from '@codemirror/view'; import {EditorState, Extension} from '@codemirror/state'; import {useConfigStore} from './configStore'; import {useDocumentStore} from './documentStore'; import {useThemeStore} from './themeStore'; -import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models'; -import {ExtensionService} from '@/../bindings/voidraft/internal/services'; +import {SystemThemeType, ExtensionID} from '@/../bindings/voidraft/internal/models/models'; +import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {ensureSyntaxTree} from "@codemirror/language" import {createBasicSetup} from '@/views/editor/basic/basicSetup'; import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension'; import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension'; import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; -import {createAutoSavePlugin} from '@/views/editor/basic/autoSaveExtension'; +import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager'; import {useExtensionStore} from './extensionStore'; +import {ExtensionService} from '@/../bindings/voidraft/internal/services'; import createCodeBlockExtension from "@/views/editor/extensions/codeblock"; -import {triggerFontChange} from '@/views/editor/extensions/checkbox'; + +const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例 export interface DocumentStats { lines: number; @@ -26,78 +28,50 @@ export interface DocumentStats { selectedCharacters: number; } +interface EditorInstance { + view: EditorView; + documentId: number; + content: string; + isDirty: boolean; + lastModified: Date; + autoSaveTimer: number | null; +} + export const useEditorStore = defineStore('editor', () => { - // 引用配置store + // === 依赖store === const configStore = useConfigStore(); const documentStore = useDocumentStore(); const themeStore = useThemeStore(); const extensionStore = useExtensionStore(); - // 状态 + // === 核心状态 === + const editorCache = ref<{ + lru: number[]; + instances: Record; + containerElement: HTMLElement | null; + }>({ + lru: [], + instances: {}, + containerElement: null + }); + + const currentEditor = ref(null); const documentStats = ref({ lines: 0, characters: 0, selectedCharacters: 0 }); - // 编辑器视图 - const editorView = ref(null); - // 编辑器是否已初始化 - const isEditorInitialized = ref(false); - // 编辑器容器元素 - const editorContainer = ref(null); - // 方法 - function setEditorView(view: EditorView | null) { - editorView.value = view; - } + // 自动保存设置 - 从配置动态获取 + const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay; - // 设置编辑器容器 - function setEditorContainer(container: HTMLElement | null) { - editorContainer.value = container; - // 如果编辑器已经创建但容器改变了,需要重新挂载 - if (editorView.value && container && editorView.value.dom.parentElement !== container) { - container.appendChild(editorView.value.dom); - // 重新挂载后立即滚动到底部 - scrollToBottom(editorView.value as EditorView); + // === 私有方法 === + + // 创建编辑器实例 + const createEditorInstance = async (content: string): Promise => { + if (!editorCache.value.containerElement) { + throw new Error('Editor container not set'); } - } - - // 更新文档统计信息 - function updateDocumentStats(stats: DocumentStats) { - documentStats.value = stats; - } - - // 应用字体大小 - function applyFontSize() { - if (!editorView.value) return; - // 更新编辑器的字体大小 - const editorDOM = editorView.value.dom; - if (editorDOM) { - editorDOM.style.fontSize = `${configStore.config.editing.fontSize}px`; - editorView.value?.requestMeasure(); - } - } - - // 滚动到文档底部的辅助函数 - const scrollToBottom = (view: EditorView) => { - if (!view) return; - - const lines = view.state.doc.lines; - if (lines > 0) { - const lastLinePos = view.state.doc.line(lines).to; - view.dispatch({ - effects: EditorView.scrollIntoView(lastLinePos) - }); - } - }; - - // 创建编辑器 - const createEditor = async (initialDoc: string = '') => { - if (isEditorInitialized.value || !editorContainer.value) return; - - // 加载文档内容 - await documentStore.initialize(); - const docContent = documentStore.documentContent || initialDoc; // 获取基本扩展 const basicExtensions = createBasicSetup(); @@ -107,14 +81,14 @@ export const useEditorStore = defineStore('editor', () => { configStore.config.appearance.systemTheme || SystemThemeType.SystemThemeAuto ); - // 获取Tab相关扩展 + // Tab相关扩展 const tabExtensions = getTabExtensions( configStore.config.editing.tabSize, configStore.config.editing.enableTabIndent, configStore.config.editing.tabType ); - // 创建字体扩展 + // 字体扩展 const fontExtension = createFontExtensionFromBackend({ fontFamily: configStore.config.editing.fontFamily, fontSize: configStore.config.editing.fontSize, @@ -122,180 +96,426 @@ export const useEditorStore = defineStore('editor', () => { fontWeight: configStore.config.editing.fontWeight }); - // 创建统计信息更新扩展 - const statsExtension = createStatsUpdateExtension( - updateDocumentStats - ); + // 统计扩展 + const statsExtension = createStatsUpdateExtension(updateDocumentStats); + // 内容变化扩展 + const contentChangeExtension = createContentChangePlugin(); - // 创建自动保存插件 - const autoSaveExtension = createAutoSavePlugin({ - debounceDelay: configStore.config.editing.autoSaveDelay - }); - // 代码块功能 + // 代码块扩展 const codeBlockExtension = createCodeBlockExtension({ showBackground: true, enableAutoDetection: true }); - // 创建动态快捷键扩展 + // 快捷键扩展 const keymapExtension = await createDynamicKeymapExtension(); - // 创建动态扩展 + // 动态扩展 const dynamicExtensions = await createDynamicExtensions(); // 组合所有扩展 const extensions: Extension[] = [ keymapExtension, - themeExtension, ...basicExtensions, + themeExtension, ...tabExtensions, fontExtension, statsExtension, - autoSaveExtension, + contentChangeExtension, codeBlockExtension, ...dynamicExtensions ]; // 创建编辑器状态 const state = EditorState.create({ - doc: docContent, + doc: content, extensions }); // 创建编辑器视图 const view = new EditorView({ - state, - parent: editorContainer.value + state }); - // 将编辑器实例保存到store - setEditorView(view); + // 初始化语法树 + ensureSyntaxTree(view.state, view.state.doc.length, 5000); - // 设置编辑器视图到扩展管理器 - setExtensionManagerView(view); + // 将光标定位到文档末尾 + const docLength = view.state.doc.length; + view.dispatch({ + selection: { anchor: docLength, head: docLength } + }); - isEditorInitialized.value = true; - - scrollToBottom(view); - - ensureSyntaxTree(view.state, view.state.doc.length, 5000) - - // 应用初始字体大小 - applyFontSize(); + return view; }; - // 重新配置编辑器 - const reconfigureTabSettings = () => { - if (!editorView.value) return; - updateTabConfig( - editorView.value as EditorView, - configStore.config.editing.tabSize, - configStore.config.editing.enableTabIndent, - configStore.config.editing.tabType + // 添加编辑器到缓存 + const addEditorToCache = (documentId: number, view: EditorView, content: string) => { + // 如果缓存已满,移除最少使用的编辑器 + if (editorCache.value.lru.length >= NUM_EDITOR_INSTANCES) { + const oldestId = editorCache.value.lru.shift(); + if (oldestId && editorCache.value.instances[oldestId]) { + const oldInstance = editorCache.value.instances[oldestId]; + // 清除自动保存定时器 + if (oldInstance.autoSaveTimer) { + clearTimeout(oldInstance.autoSaveTimer); + } + // 移除DOM元素 + if (oldInstance.view.dom.parentElement) { + oldInstance.view.dom.remove(); + } + oldInstance.view.destroy(); + delete editorCache.value.instances[oldestId]; + } + } + + // 添加新的编辑器实例 + editorCache.value.instances[documentId] = { + view, + documentId, + content, + isDirty: false, + lastModified: new Date(), + autoSaveTimer: null + }; + + // 添加到LRU列表 + editorCache.value.lru.push(documentId); + }; + + // 更新LRU + const updateLRU = (documentId: number) => { + const lru = editorCache.value.lru; + const index = lru.indexOf(documentId); + if (index > -1) { + lru.splice(index, 1); + } + lru.push(documentId); + }; + + // 获取或创建编辑器 + const getOrCreateEditor = async (documentId: number, content: string): Promise => { + // 检查缓存 + const cached = editorCache.value.instances[documentId]; + if (cached) { + updateLRU(documentId); + return cached.view; + } + + // 创建新的编辑器实例 + const view = await createEditorInstance(content); + addEditorToCache(documentId, view, content); + + return view; + }; + + // 显示编辑器 + const showEditor = (documentId: number) => { + const instance = editorCache.value.instances[documentId]; + if (!instance || !editorCache.value.containerElement) return; + + try { + // 移除当前编辑器DOM + if (currentEditor.value && currentEditor.value.dom && currentEditor.value.dom.parentElement) { + currentEditor.value.dom.remove(); + } + + // 确保容器为空 + editorCache.value.containerElement.innerHTML = ''; + + // 将目标编辑器DOM添加到容器 + editorCache.value.containerElement.appendChild(instance.view.dom); + currentEditor.value = instance.view; + + // 设置扩展管理器视图 + setExtensionManagerView(instance.view); + + // 更新LRU + updateLRU(documentId); + + // 重新测量和聚焦编辑器 + nextTick(() => { + instance.view.requestMeasure(); + // 将光标定位到文档末尾 + const docLength = instance.view.state.doc.length; + instance.view.dispatch({ + selection: { anchor: docLength, head: docLength } + }); + instance.view.focus(); + }); + } catch (error) { + console.error('Error showing editor:', error); + } + }; + + // 保存编辑器内容 + const saveEditorContent = async (documentId: number): Promise => { + const instance = editorCache.value.instances[documentId]; + if (!instance || !instance.isDirty) return true; + + try { + const content = instance.view.state.doc.toString(); + await DocumentService.UpdateDocumentContent(documentId, content); + + instance.content = content; + instance.isDirty = false; + instance.lastModified = new Date(); + + return true; + } catch (error) { + console.error('Failed to save editor content:', error); + return false; + } + }; + + // 内容变化处理 + const onContentChange = (documentId: number) => { + const instance = editorCache.value.instances[documentId]; + if (!instance) return; + + instance.isDirty = true; + instance.lastModified = new Date(); + + // 清除之前的定时器 + if (instance.autoSaveTimer) { + clearTimeout(instance.autoSaveTimer); + } + + // 设置新的自动保存定时器 + instance.autoSaveTimer = window.setTimeout(() => { + saveEditorContent(documentId); + }, getAutoSaveDelay()); + }; + + // === 公共API === + + // 设置编辑器容器 + const setEditorContainer = (container: HTMLElement | null) => { + editorCache.value.containerElement = container; + + // 如果设置容器时已有当前文档,立即加载编辑器 + if (container && documentStore.currentDocument) { + loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content); + } + }; + + // 加载编辑器 + const loadEditor = async (documentId: number, content: string) => { + try { + // 验证参数 + if (!documentId) { + throw new Error('Invalid parameters for loadEditor'); + } + + // 保存当前编辑器内容 + if (currentEditor.value) { + const currentDocId = documentStore.currentDocumentId; + if (currentDocId && currentDocId !== documentId) { + await saveEditorContent(currentDocId); + } + } + + // 获取或创建编辑器 + const view = await getOrCreateEditor(documentId, content); + + // 更新内容(如果需要) + const instance = editorCache.value.instances[documentId]; + if (instance && instance.content !== content) { + // 确保编辑器视图有效 + if (view && view.state && view.dispatch) { + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: content + } + }); + instance.content = content; + instance.isDirty = false; + } + } + + // 显示编辑器 + showEditor(documentId); + + } catch (error) { + console.error('Failed to load editor:', error); + } + }; + + // 移除编辑器 + const removeEditor = (documentId: number) => { + const instance = editorCache.value.instances[documentId]; + if (instance) { + try { + // 清除自动保存定时器 + if (instance.autoSaveTimer) { + clearTimeout(instance.autoSaveTimer); + instance.autoSaveTimer = null; + } + + // 移除DOM元素 + if (instance.view && instance.view.dom && instance.view.dom.parentElement) { + instance.view.dom.remove(); + } + + // 销毁编辑器 + if (instance.view && instance.view.destroy) { + instance.view.destroy(); + } + + // 清理引用 + if (currentEditor.value === instance.view) { + currentEditor.value = null; + } + + delete editorCache.value.instances[documentId]; + + const lruIndex = editorCache.value.lru.indexOf(documentId); + if (lruIndex > -1) { + editorCache.value.lru.splice(lruIndex, 1); + } + } catch (error) { + console.error('Error removing editor:', error); + } + } + }; + + // 更新文档统计 + const updateDocumentStats = (stats: DocumentStats) => { + documentStats.value = stats; + }; + + // 应用字体设置 + const applyFontSettings = () => { + Object.values(editorCache.value.instances).forEach(instance => { + updateFontConfig(instance.view, { + fontFamily: configStore.config.editing.fontFamily, + fontSize: configStore.config.editing.fontSize, + lineHeight: configStore.config.editing.lineHeight, + fontWeight: configStore.config.editing.fontWeight + }); + }); + }; + + // 应用主题设置 + const applyThemeSettings = () => { + Object.values(editorCache.value.instances).forEach(instance => { + updateEditorTheme(instance.view, + themeStore.currentTheme || SystemThemeType.SystemThemeAuto + ); + }); + }; + + // 应用Tab设置 + const applyTabSettings = () => { + Object.values(editorCache.value.instances).forEach(instance => { + updateTabConfig( + instance.view, + configStore.config.editing.tabSize, + configStore.config.editing.enableTabIndent, + configStore.config.editing.tabType + ); + }); + }; + + // 应用快捷键设置 + const applyKeymapSettings = async () => { + await Promise.all( + Object.values(editorCache.value.instances).map(instance => + updateKeymapExtension(instance.view) + ) ); }; - // 重新配置字体设置 - const reconfigureFontSettings = () => { - if (!editorView.value) return; - updateFontConfig(editorView.value as EditorView, { - fontFamily: configStore.config.editing.fontFamily, - fontSize: configStore.config.editing.fontSize, - lineHeight: configStore.config.editing.lineHeight, - fontWeight: configStore.config.editing.fontWeight + // 清空所有编辑器 + const clearAllEditors = () => { + Object.values(editorCache.value.instances).forEach(instance => { + // 清除自动保存定时器 + if (instance.autoSaveTimer) { + clearTimeout(instance.autoSaveTimer); + } + // 移除DOM元素 + if (instance.view.dom.parentElement) { + instance.view.dom.remove(); + } + // 销毁编辑器 + instance.view.destroy(); }); + + editorCache.value.instances = {}; + editorCache.value.lru = []; + currentEditor.value = null; }; - // 销毁编辑器 - const destroyEditor = () => { - if (editorView.value) { - editorView.value.destroy(); - editorView.value = null; - isEditorInitialized.value = false; - } - }; - - // 监听Tab设置变化 - watch([ - () => configStore.config.editing.tabSize, - () => configStore.config.editing.enableTabIndent, - () => configStore.config.editing.tabType, - ], () => { - reconfigureTabSettings(); - }); - - // 监听字体大小变化 - watch([ - () => configStore.config.editing.fontFamily, - () => configStore.config.editing.fontSize, - () => configStore.config.editing.lineHeight, - () => configStore.config.editing.fontWeight, - ], () => { - reconfigureFontSettings(); - applyFontSize(); - // 通知checkbox扩展字体已变化,需要重新渲染 - if (editorView.value) { - triggerFontChange(editorView.value as EditorView); - } - }); - - // 监听主题变化 - watch(() => themeStore.currentTheme, (newTheme) => { - if (editorView.value && newTheme) { - updateEditorTheme(editorView.value as EditorView, newTheme); - } - }); - - // 扩展管理方法 - const updateExtension = async (id: any, enabled: boolean, config?: any) => { + // 更新扩展 + const updateExtension = async (id: ExtensionID, enabled: boolean, config?: any) => { try { // 如果只是更新启用状态 if (config === undefined) { - await ExtensionService.UpdateExtensionEnabled(id, enabled) + await ExtensionService.UpdateExtensionEnabled(id, enabled); } else { // 如果需要更新配置 - await ExtensionService.UpdateExtensionState(id, enabled, config) + await ExtensionService.UpdateExtensionState(id, enabled, config); } // 更新前端编辑器扩展 - const manager = getExtensionManager() + const manager = getExtensionManager(); if (manager) { - manager.updateExtension(id, enabled, config || {}) + manager.updateExtension(id, enabled, config || {}); } // 重新加载扩展配置 - await extensionStore.loadExtensions() + await extensionStore.loadExtensions(); // 更新快捷键映射 - if (editorView.value) { - updateKeymapExtension(editorView.value) + if (currentEditor.value) { + updateKeymapExtension(currentEditor.value); } } catch (error) { - throw error + throw error; } - } + }; - // 监听扩展状态变化,自动更新快捷键 - watch(() => extensionStore.enabledExtensions.length, () => { - if (editorView.value) { - updateKeymapExtension(editorView.value) + // 监听文档切换 + watch(() => documentStore.currentDocument, (newDoc) => { + if (newDoc && editorCache.value.containerElement) { + loadEditor(newDoc.id, newDoc.content); } - }) + }); + + // 监听配置变化 + watch(() => configStore.config.editing.fontSize, applyFontSettings); + watch(() => configStore.config.editing.fontFamily, applyFontSettings); + watch(() => configStore.config.editing.lineHeight, applyFontSettings); + watch(() => configStore.config.editing.fontWeight, applyFontSettings); + watch(() => configStore.config.editing.tabSize, applyTabSettings); + watch(() => configStore.config.editing.enableTabIndent, applyTabSettings); + watch(() => configStore.config.editing.tabType, applyTabSettings); + watch(() => themeStore.currentTheme, applyThemeSettings); return { // 状态 + currentEditor, documentStats, - editorView, - isEditorInitialized, - editorContainer, // 方法 setEditorContainer, - createEditor, - reconfigureTabSettings, - reconfigureFontSettings, - destroyEditor, - updateExtension + loadEditor, + removeEditor, + clearAllEditors, + onContentChange, + + // 配置更新方法 + applyFontSettings, + applyThemeSettings, + applyTabSettings, + applyKeymapSettings, + + // 扩展管理方法 + updateExtension, + + editorView: currentEditor, }; }); \ No newline at end of file diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index 79c7360..a17e6a3 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -1,39 +1,30 @@