♻️ Refactor document selector and cache management logic

This commit is contained in:
2025-09-29 00:26:05 +08:00
parent bc0569af93
commit 3077d5a7c5
31 changed files with 3660 additions and 1382 deletions

View File

@@ -4,45 +4,27 @@ import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
import {Document} from '@/../bindings/voidraft/internal/models/models';
export const useDocumentStore = defineStore('document', () => {
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
// === 核心状态 ===
const documents = ref<Record<number, Document>>({});
const recentDocumentIds = ref<number[]>([DEFAULT_DOCUMENT_ID.value]);
const currentDocumentId = ref<number | null>(null);
const currentDocument = ref<Document | null>(null);
// === UI状态 ===
const showDocumentSelector = ref(false);
const selectorError = ref<{ docId: number; message: string } | null>(null);
const isLoading = ref(false);
// === 计算属性 ===
const documentList = computed(() =>
Object.values(documents.value).sort((a, b) => {
const aIndex = recentDocumentIds.value.indexOf(a.id);
const bIndex = recentDocumentIds.value.indexOf(b.id);
// 按最近使用排序
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
// 然后按更新时间排序
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 => {
@@ -50,7 +32,35 @@ export const useDocumentStore = defineStore('document', () => {
});
};
// === 公共API ===
// === 错误处理 ===
const setError = (docId: number, message: string) => {
selectorError.value = { docId, message };
};
const clearError = () => {
selectorError.value = null;
};
// === UI控制方法 ===
const openDocumentSelector = () => {
showDocumentSelector.value = true;
clearError();
};
const closeDocumentSelector = () => {
showDocumentSelector.value = false;
clearError();
};
const toggleDocumentSelector = () => {
if (showDocumentSelector.value) {
closeDocumentSelector();
} else {
openDocumentSelector();
}
};
// === 文档操作方法 ===
// 在新窗口中打开文档
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
@@ -63,22 +73,57 @@ export const useDocumentStore = defineStore('document', () => {
}
};
// 创建新文档
const createNewDocument = async (title: string): Promise<Document | null> => {
try {
const doc = await DocumentService.CreateDocument(title);
if (doc) {
documents.value[doc.id] = doc;
return doc;
}
return null;
} catch (error) {
console.error('Failed to create document:', error);
return null;
}
};
// 保存新文档
const saveNewDocument = async (title: string, content: string): Promise<Document | null> => {
try {
const doc = await DocumentService.CreateDocument(title);
if (doc) {
await DocumentService.UpdateDocumentContent(doc.id, content);
doc.content = content;
documents.value[doc.id] = doc;
return doc;
}
return null;
} catch (error) {
console.error('Failed to save new document:', error);
return null;
}
};
// 更新文档列表
const updateDocuments = async () => {
try {
isLoading.value = true;
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);
} finally {
isLoading.value = false;
}
};
// 打开文档
const openDocument = async (docId: number): Promise<boolean> => {
try {
closeDialog();
closeDocumentSelector();
// 获取完整文档数据
const doc = await DocumentService.GetDocumentByID(docId);
@@ -88,7 +133,6 @@ export const useDocumentStore = defineStore('document', () => {
currentDocumentId.value = docId;
currentDocument.value = doc;
addRecentDocument(docId);
return true;
} catch (error) {
@@ -97,41 +141,6 @@ export const useDocumentStore = defineStore('document', () => {
}
};
// 创建新文档
const createNewDocument = async (title: string): Promise<Document | null> => {
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<boolean> => {
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): Promise<boolean> => {
try {
@@ -168,7 +177,6 @@ export const useDocumentStore = defineStore('document', () => {
// 更新本地状态
delete documents.value[docId];
recentDocumentIds.value = recentDocumentIds.value.filter(id => id !== docId);
// 如果删除的是当前文档,切换到第一个可用文档
if (currentDocumentId.value === docId) {
@@ -188,16 +196,6 @@ export const useDocumentStore = defineStore('document', () => {
}
};
// === UI控制 ===
const openDocumentSelector = () => {
closeDialog();
showDocumentSelector.value = true;
};
const closeDialog = () => {
showDocumentSelector.value = false;
};
// === 初始化 ===
const initialize = async (urlDocumentId?: number): Promise<void> => {
try {
@@ -226,10 +224,10 @@ export const useDocumentStore = defineStore('document', () => {
// 状态
documents,
documentList,
recentDocumentIds,
currentDocumentId,
currentDocument,
showDocumentSelector,
selectorError,
isLoading,
// 方法
@@ -241,13 +239,16 @@ export const useDocumentStore = defineStore('document', () => {
updateDocumentMetadata,
deleteDocument,
openDocumentSelector,
closeDialog,
closeDocumentSelector,
toggleDocumentSelector,
setError,
clearError,
initialize,
};
}, {
persist: {
key: 'voidraft-document',
storage: localStorage,
pick: ['currentDocumentId']
pick: ['currentDocumentId', 'documents']
}
});

View File

@@ -1,28 +1,45 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { computed, shallowRef, type ShallowRef } from 'vue';
import { EditorView } from '@codemirror/view';
import { ensureSyntaxTree } from '@codemirror/language';
import { LRUCache, type CacheItem, type DisposableCacheItem, createContentHash } from '@/common/cache';
import { LruCache, type CacheItem, type DisposableCacheItem, createHash } from '@/common/cache';
import { removeExtensionManagerView } from '@/views/editor/manager';
// 编辑器缓存项接口
export interface EditorCacheItem extends CacheItem, DisposableCacheItem {
view: EditorView;
documentId: number;
/** 语法树缓存信息 */
interface SyntaxTreeCache {
readonly lastDocLength: number;
readonly lastContentHash: string;
readonly lastParsed: Date;
}
/** 编辑器状态 */
interface EditorState {
content: string;
isDirty: boolean;
lastModified: Date;
autoSaveTimer: number | null;
syntaxTreeCache: {
lastDocLength: number;
lastContentHash: string;
lastParsed: Date;
} | null;
}
/** 编辑器缓存项 */
export interface EditorCacheItem extends CacheItem, DisposableCacheItem {
readonly view: EditorView;
readonly documentId: number;
state: EditorState;
autoSaveTimer: number | null;
syntaxTreeCache: SyntaxTreeCache | null;
}
// === 缓存配置 ===
const CACHE_CONFIG = {
maxSize: 5,
syntaxTreeExpireTime: 30000, // 30秒
} as const;
export const useEditorCacheStore = defineStore('editorCache', () => {
// 清理编辑器实例的函数
const cleanupEditorInstance = (item: EditorCacheItem) => {
// === 状态 ===
const containerElement: ShallowRef<HTMLElement | null> = shallowRef(null);
// === 内部方法 ===
const cleanupEditor = (item: EditorCacheItem): void => {
try {
// 清除自动保存定时器
if (item.autoSaveTimer) {
@@ -34,20 +51,15 @@ export const useEditorCacheStore = defineStore('editorCache', () => {
removeExtensionManagerView(item.documentId);
// 移除DOM元素
if (item.view && item.view.dom && item.view.dom.parentElement) {
item.view.dom.remove();
}
item.view.dom?.remove();
// 销毁编辑器
if (item.view && item.view.destroy) {
item.view.destroy();
}
item.view.destroy?.();
} catch (error) {
console.error('Error cleaning up editor instance:', error);
console.error(`Failed to cleanup editor ${item.documentId}:`, error);
}
};
// 创建编辑器缓存项
const createEditorCacheItem = (
documentId: number,
view: EditorView,
@@ -61,171 +73,166 @@ export const useEditorCacheStore = defineStore('editorCache', () => {
createdAt: now,
view,
documentId,
content,
isDirty: false,
lastModified: now,
state: {
content,
isDirty: false,
lastModified: now
},
autoSaveTimer: null,
syntaxTreeCache: null,
dispose: () => cleanupEditorInstance(item)
dispose: () => cleanupEditor(item)
};
return item;
};
// 编辑器缓存配置
const EDITOR_CACHE_CONFIG = {
maxSize: 5, // 最多缓存5个编辑器实例
onEvict: (item: EditorCacheItem) => {
// 清理被驱逐的编辑器实例
cleanupEditorInstance(item);
const shouldRebuildSyntaxTree = (
item: EditorCacheItem,
docLength: number,
contentHash: string
): boolean => {
const { syntaxTreeCache } = item;
if (!syntaxTreeCache) return true;
const now = Date.now();
const isExpired = (now - syntaxTreeCache.lastParsed.getTime()) > CACHE_CONFIG.syntaxTreeExpireTime;
const isContentChanged = syntaxTreeCache.lastDocLength !== docLength ||
syntaxTreeCache.lastContentHash !== contentHash;
return isExpired || isContentChanged;
};
const buildSyntaxTree = (view: EditorView, item: EditorCacheItem): void => {
const docLength = view.state.doc.length;
const content = view.state.doc.toString();
const contentHash = createHash(content);
if (!shouldRebuildSyntaxTree(item, docLength, contentHash)) {
return;
}
try {
ensureSyntaxTree(view.state, docLength, 5000);
item.syntaxTreeCache = {
lastDocLength: docLength,
lastContentHash: contentHash,
lastParsed: new Date()
};
} catch (error) {
console.warn(`Failed to build syntax tree for editor ${item.documentId}:`, error);
}
};
// 编辑器缓存实例
const cache = new LRUCache<EditorCacheItem>(EDITOR_CACHE_CONFIG);
// 容器元素
const containerElement = ref<HTMLElement | null>(null);
// 设置容器元素
const setContainer = (element: HTMLElement | null) => {
// === 缓存实例 ===
const cache = new LruCache<EditorCacheItem>({
maxSize: CACHE_CONFIG.maxSize,
onEvict: cleanupEditor
});
// === 计算属性 ===
const cacheSize = computed(() => cache.size());
const cacheStats = computed(() => cache.getStats());
const allEditors = computed(() => cache.getAll());
const dirtyEditors = computed(() =>
allEditors.value.filter(item => item.state.isDirty)
);
// === 公共方法 ===
// 容器管理
const setContainer = (element: HTMLElement | null): void => {
containerElement.value = element;
};
// 获取容器元素
const getContainer = () => containerElement.value;
const getContainer = (): HTMLElement | null => containerElement.value;
// 添加编辑器到缓存
const addEditor = (documentId: number, view: EditorView, content: string) => {
// 基础缓存操作
const addEditor = (documentId: number, view: EditorView, content: string): void => {
const item = createEditorCacheItem(documentId, view, content);
cache.set(documentId, item);
// 初始化语法树缓存
ensureSyntaxTreeCached(view, documentId);
buildSyntaxTree(view, item);
};
// 获取编辑器实例
const getEditor = (documentId: number): EditorCacheItem | null => {
return cache.get(documentId);
};
// 检查编辑器是否存在
const hasEditor = (documentId: number): boolean => {
return cache.has(documentId);
};
// 移除编辑器
const removeEditor = (documentId: number): boolean => {
return cache.remove(documentId);
};
// 获取所有编辑器实例
const getAllEditors = (): EditorCacheItem[] => {
return cache.getAll();
};
// 清空所有编辑器
const clearAll = () => {
const clearAll = (): void => {
cache.clear();
};
// 获取缓存大小
const size = (): number => {
return cache.size();
// 编辑器状态管理
const updateEditorContent = (documentId: number, content: string): boolean => {
const item = cache.get(documentId);
if (!item) return false;
item.state.content = content;
item.state.isDirty = false;
item.state.lastModified = new Date();
item.syntaxTreeCache = null; // 清理语法树缓存
return true;
};
// 获取缓存统计信息
const getStats = () => {
return cache.getStats();
const markEditorDirty = (documentId: number): boolean => {
const item = cache.get(documentId);
if (!item) return false;
item.state.isDirty = true;
item.state.lastModified = new Date();
item.syntaxTreeCache = null; // 清理语法树缓存
return true;
};
// 缓存化的语法树确保方法
// 自动保存管理
const setAutoSaveTimer = (documentId: number, timer: number): boolean => {
const item = cache.get(documentId);
if (!item) return false;
// 清除之前的定时器
if (item.autoSaveTimer) {
clearTimeout(item.autoSaveTimer);
}
item.autoSaveTimer = timer;
return true;
};
const clearAutoSaveTimer = (documentId: number): boolean => {
const item = cache.get(documentId);
if (!item || !item.autoSaveTimer) return false;
clearTimeout(item.autoSaveTimer);
item.autoSaveTimer = null;
return true;
};
// 语法树管理
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
const item = cache.get(documentId);
if (!item) return;
const docLength = view.state.doc.length;
const content = view.state.doc.toString();
const contentHash = createContentHash(content);
const now = new Date();
// 检查是否需要重新构建语法树
const syntaxCache = item.syntaxTreeCache;
const shouldRebuild = !syntaxCache ||
syntaxCache.lastDocLength !== docLength ||
syntaxCache.lastContentHash !== contentHash ||
(now.getTime() - syntaxCache.lastParsed.getTime()) > 30000; // 30秒过期
if (shouldRebuild) {
try {
ensureSyntaxTree(view.state, docLength, 5000);
// 更新缓存
item.syntaxTreeCache = {
lastDocLength: docLength,
lastContentHash: contentHash,
lastParsed: now
};
} catch (error) {
console.warn('Failed to ensure syntax tree:', error);
}
}
buildSyntaxTree(view, item);
};
// 更新编辑器内容
const updateEditorContent = (documentId: number, content: string) => {
const item = cache.get(documentId);
if (item) {
item.content = content;
item.isDirty = false;
item.lastModified = new Date();
// 清理语法树缓存,因为内容已更新
item.syntaxTreeCache = null;
}
};
// 标记编辑器为脏状态
const markEditorDirty = (documentId: number) => {
const item = cache.get(documentId);
if (item) {
item.isDirty = true;
item.lastModified = new Date();
// 清理语法树缓存,下次访问时重新构建
item.syntaxTreeCache = null;
}
};
// 设置自动保存定时器
const setAutoSaveTimer = (documentId: number, timer: number) => {
const item = cache.get(documentId);
if (item) {
// 清除之前的定时器
if (item.autoSaveTimer) {
clearTimeout(item.autoSaveTimer);
}
item.autoSaveTimer = timer;
}
};
// 清除自动保存定时器
const clearAutoSaveTimer = (documentId: number) => {
const item = cache.get(documentId);
if (item && item.autoSaveTimer) {
clearTimeout(item.autoSaveTimer);
item.autoSaveTimer = null;
}
};
// 获取脏状态的编辑器
const getDirtyEditors = (): EditorCacheItem[] => {
return cache.getAll().filter(item => item.isDirty);
};
// 清理过期的语法树缓存
const cleanupExpiredSyntaxTrees = () => {
const now = new Date();
cache.getAll().forEach(item => {
const cleanupExpiredSyntaxTrees = (): void => {
const now = Date.now();
allEditors.value.forEach(item => {
if (item.syntaxTreeCache &&
(now.getTime() - item.syntaxTreeCache.lastParsed.getTime()) > 30000) {
(now - item.syntaxTreeCache.lastParsed.getTime()) > CACHE_CONFIG.syntaxTreeExpireTime) {
item.syntaxTreeCache = null;
}
});
@@ -241,17 +248,24 @@ export const useEditorCacheStore = defineStore('editorCache', () => {
getEditor,
hasEditor,
removeEditor,
getAllEditors,
clearAll,
size,
getStats,
ensureSyntaxTreeCached,
// 编辑器状态管理
updateEditorContent,
markEditorDirty,
// 自动保存管理
setAutoSaveTimer,
clearAutoSaveTimer,
getDirtyEditors,
cleanupExpiredSyntaxTrees
// 语法树管理
ensureSyntaxTreeCached,
cleanupExpiredSyntaxTrees,
// 计算属性
cacheSize,
cacheStats,
allEditors,
dirtyEditors
};
});

View File

@@ -1,5 +1,5 @@
import {defineStore} from 'pinia';
import {nextTick, ref, watch} from 'vue';
import {computed, nextTick, ref, watch} from 'vue';
import {EditorView} from '@codemirror/view';
import {EditorState, Extension} from '@codemirror/state';
import {useConfigStore} from './configStore';
@@ -15,10 +15,15 @@ import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/b
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
import {
createDynamicExtensions,
getExtensionManager,
removeExtensionManagerView,
setExtensionManagerView
} from '@/views/editor/manager';
import {useExtensionStore} from './extensionStore';
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
import {AsyncOperationManager} from '@/common/async-operation';
import {AsyncOperationManager} from '@/common/async';
export interface DocumentStats {
lines: number;
@@ -41,7 +46,7 @@ export const useEditorStore = defineStore('editor', () => {
characters: 0,
selectedCharacters: 0
});
// 编辑器加载状态
const isLoading = ref(false);
@@ -58,7 +63,7 @@ export const useEditorStore = defineStore('editor', () => {
// 创建编辑器实例
const createEditorInstance = async (
content: string,
content: string,
signal: AbortSignal,
documentId: number
): Promise<EditorView> => {
@@ -163,8 +168,8 @@ export const useEditorStore = defineStore('editor', () => {
// 获取或创建编辑器
const getOrCreateEditor = async (
documentId: number,
content: string,
documentId: number,
content: string,
signal: AbortSignal
): Promise<EditorView> => {
// 检查缓存
@@ -180,7 +185,7 @@ export const useEditorStore = defineStore('editor', () => {
// 创建新的编辑器实例
const view = await createEditorInstance(content, signal, documentId);
// 最终检查操作是否被取消
if (signal.aborted) {
// 如果操作已取消,清理创建的实例
@@ -209,11 +214,11 @@ export const useEditorStore = defineStore('editor', () => {
const container = editorCacheStore.getContainer();
if (container) {
container.innerHTML = '';
// 将目标编辑器DOM添加到容器
container.appendChild(instance.view.dom);
}
currentEditor.value = instance.view;
// 设置扩展管理器视图
@@ -227,10 +232,10 @@ export const useEditorStore = defineStore('editor', () => {
selection: {anchor: docLength, head: docLength},
scrollIntoView: true
});
// 滚动到文档底部
instance.view.focus();
// 使用缓存的语法树确保方法
editorCacheStore.ensureSyntaxTreeCached(instance.view, documentId);
});
@@ -242,18 +247,18 @@ export const useEditorStore = defineStore('editor', () => {
// 保存编辑器内容
const saveEditorContent = async (documentId: number): Promise<boolean> => {
const instance = editorCacheStore.getEditor(documentId);
if (!instance || !instance.isDirty) return true;
if (!instance || !instance.state.isDirty) return true;
try {
const content = instance.view.state.doc.toString();
const lastModified = instance.lastModified;
const lastModified = instance.state.lastModified;
await DocumentService.UpdateDocumentContent(documentId, content);
// 检查在保存期间内容是否又被修改了
if (instance.lastModified === lastModified) {
if (instance.state.lastModified === lastModified) {
editorCacheStore.updateEditorContent(documentId, content);
// isDirty 已在 updateEditorContent 中设置为 false
// isDirty 已在 updateEditorContent 中设置为 false
}
// 如果内容在保存期间被修改了,保持 isDirty 状态
@@ -310,7 +315,7 @@ export const useEditorStore = defineStore('editor', () => {
const currentDocId = documentStore.currentDocumentId;
if (currentDocId && currentDocId !== documentId) {
await saveEditorContent(currentDocId);
// 检查操作是否被取消
if (signal.aborted) {
throw new Error('Operation cancelled');
@@ -328,7 +333,7 @@ export const useEditorStore = defineStore('editor', () => {
// 更新内容
const instance = editorCacheStore.getEditor(documentId);
if (instance && instance.content !== content) {
if (instance && instance.state.content !== content) {
// 确保编辑器视图有效
if (view && view.state && view.dispatch) {
view.dispatch({
@@ -396,7 +401,7 @@ export const useEditorStore = defineStore('editor', () => {
// 应用字体设置
const applyFontSettings = () => {
editorCacheStore.getAllEditors().forEach(instance => {
editorCacheStore.allEditors.forEach(instance => {
updateFontConfig(instance.view, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
@@ -408,7 +413,7 @@ export const useEditorStore = defineStore('editor', () => {
// 应用主题设置
const applyThemeSettings = () => {
editorCacheStore.getAllEditors().forEach(instance => {
editorCacheStore.allEditors.forEach(instance => {
updateEditorTheme(instance.view,
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
);
@@ -417,7 +422,7 @@ export const useEditorStore = defineStore('editor', () => {
// 应用Tab设置
const applyTabSettings = () => {
editorCacheStore.getAllEditors().forEach(instance => {
editorCacheStore.allEditors.forEach(instance => {
updateTabConfig(
instance.view,
configStore.config.editing.tabSize,
@@ -431,7 +436,7 @@ export const useEditorStore = defineStore('editor', () => {
const applyKeymapSettings = async () => {
// 确保所有编辑器实例的快捷键都更新
await Promise.all(
editorCacheStore.getAllEditors().map(instance =>
editorCacheStore.allEditors.map(instance =>
updateKeymapExtension(instance.view)
)
);
@@ -441,10 +446,10 @@ export const useEditorStore = defineStore('editor', () => {
const clearAllEditors = () => {
// 取消所有挂起的操作
operationManager.cancelAllOperations();
// 清理所有编辑器
editorCacheStore.clearAll();
// 清除当前编辑器引用
currentEditor.value = null;
};
@@ -474,6 +479,34 @@ export const useEditorStore = defineStore('editor', () => {
await applyKeymapSettings();
};
// === 配置监听相关的 computed 属性 ===
// 字体相关配置的 computed 属性
const fontSettings = computed(() => ({
fontSize: configStore.config.editing.fontSize,
fontFamily: configStore.config.editing.fontFamily,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
}));
// Tab相关配置的 computed 属性
const tabSettings = computed(() => ({
tabSize: configStore.config.editing.tabSize,
enableTabIndent: configStore.config.editing.enableTabIndent,
tabType: configStore.config.editing.tabType
}));
// === 配置监听器 ===
// 监听字体配置变化
watch(fontSettings, applyFontSettings, { deep: true });
// 监听Tab配置变化
watch(tabSettings, applyTabSettings, { deep: true });
// 监听主题变化
watch(() => themeStore.currentTheme, applyThemeSettings);
// 监听文档切换
watch(() => documentStore.currentDocument, (newDoc) => {
if (newDoc && editorCacheStore.getContainer()) {
@@ -484,16 +517,6 @@ export const useEditorStore = defineStore('editor', () => {
}
});
// 监听配置变化
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,

View File

@@ -1,5 +1,7 @@
import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {GetSystemInfo} from '@/../bindings/voidraft/internal/services/systemservice';
import type {SystemInfo} from '@/../bindings/voidraft/internal/services/models';
import * as runtime from '@wailsio/runtime';
export interface SystemEnvironment {
@@ -19,7 +21,7 @@ export const useSystemStore = defineStore('system', () => {
// 状态
const environment = ref<SystemEnvironment | null>(null);
const isLoading = ref(false);
// 窗口置顶状态管理
const isWindowOnTop = ref<boolean>(false);
@@ -42,7 +44,24 @@ export const useSystemStore = defineStore('system', () => {
isLoading.value = true;
try {
environment.value = await runtime.System.Environment();
const systemInfo: SystemInfo | null = await GetSystemInfo();
if (systemInfo) {
environment.value = {
OS: systemInfo.os,
Arch: systemInfo.arch,
Debug: systemInfo.debug,
OSInfo: {
Name: systemInfo.osInfo?.name || '',
Branding: systemInfo.osInfo?.branding || '',
Version: systemInfo.osInfo?.version || '',
ID: systemInfo.osInfo?.id || '',
},
PlatformInfo: systemInfo.platformInfo || {},
};
} else {
environment.value = null;
}
} catch (_err) {
environment.value = null;
} finally {
@@ -94,4 +113,4 @@ export const useSystemStore = defineStore('system', () => {
storage: localStorage,
pick: ['isWindowOnTop']
}
});
});

View File

@@ -0,0 +1,253 @@
import {defineStore} from 'pinia';
import {computed, ref, watch} from 'vue';
import {useDocumentStore} from './documentStore';
import {useEditorCacheStore} from './editorCacheStore';
import type {Document} from '@/../bindings/voidraft/internal/models/models';
/** 标签页信息 */
export interface TabInfo {
id: number;
title: string;
isActive: boolean;
lastAccessed: Date;
document?: Document;
}
export const useTabStore = defineStore('tab', () => {
// === 依赖的 Store ===
const documentStore = useDocumentStore();
const editorCacheStore = useEditorCacheStore();
// === 状态 ===
const openTabIds = ref<number[]>([]);
const activeTabId = ref<number | null>(null);
// === 计算属性 ===
// 获取所有打开的标签页信息
const openTabs = computed((): TabInfo[] => {
return openTabIds.value.map(id => {
const document = documentStore.documents[id];
const editorItem = editorCacheStore.getEditor(id);
return {
id,
title: document?.title || `Document ${id}`,
isDirty: editorItem?.state.isDirty || false,
isActive: id === activeTabId.value,
lastAccessed: editorItem?.lastAccessed || new Date(),
document
};
}).sort((a, b) => {
// 按最后访问时间排序,最近访问的在前
return b.lastAccessed.getTime() - a.lastAccessed.getTime();
});
});
// 获取当前活跃的标签页
const activeTab = computed((): TabInfo | null => {
if (!activeTabId.value) return null;
return openTabs.value.find(tab => tab.id === activeTabId.value) || null;
});
// 标签页数量
const tabCount = computed(() => openTabIds.value.length);
// 是否有标签页打开
const hasTabs = computed(() => tabCount.value > 0);
// === 私有方法 ===
// 添加标签页到列表
const addTabToList = (documentId: number): void => {
if (!openTabIds.value.includes(documentId)) {
openTabIds.value.push(documentId);
}
};
// 从列表中移除标签页
const removeTabFromList = (documentId: number): void => {
const index = openTabIds.value.indexOf(documentId);
if (index > -1) {
openTabIds.value.splice(index, 1);
}
};
// === 公共方法 ===
// 打开标签页
const openTab = async (documentId: number): Promise<boolean> => {
try {
// 使用 documentStore 的 openDocument 方法
const success = await documentStore.openDocument(documentId);
if (success) {
// 添加到标签页列表
addTabToList(documentId);
// 设置为活跃标签页
activeTabId.value = documentId;
return true;
}
return false;
} catch (error) {
console.error('Failed to open tab:', error);
return false;
}
};
// 切换到指定标签页
const switchToTab = async (documentId: number): Promise<boolean> => {
// 如果标签页已经是活跃状态,直接返回
if (activeTabId.value === documentId) {
return true;
}
// 如果标签页不在打开列表中,先打开它
if (!openTabIds.value.includes(documentId)) {
return await openTab(documentId);
}
// 切换到已打开的标签页
try {
const success = await documentStore.openDocument(documentId);
if (success) {
activeTabId.value = documentId;
return true;
}
return false;
} catch (error) {
console.error('Failed to switch tab:', error);
return false;
}
};
// 关闭标签页
const closeTab = async (documentId: number): Promise<boolean> => {
try {
// 检查是否有未保存的更改
const editorItem = editorCacheStore.getEditor(documentId);
if (editorItem?.state.isDirty) {
// 这里可以添加确认对话框逻辑
console.warn(`Document ${documentId} has unsaved changes`);
}
// 从标签页列表中移除
removeTabFromList(documentId);
// 如果关闭的是当前活跃标签页,需要切换到其他标签页
if (activeTabId.value === documentId) {
if (openTabIds.value.length > 0) {
// 切换到最近访问的标签页
const nextTab = openTabs.value[0];
if (nextTab) {
await switchToTab(nextTab.id);
}
} else {
// 没有其他标签页了
activeTabId.value = null;
}
}
// 从编辑器缓存中移除(可选,取决于是否要保持缓存)
// editorCacheStore.removeEditor(documentId);
return true;
} catch (error) {
console.error('Failed to close tab:', error);
return false;
}
};
// 关闭所有标签页
const closeAllTabs = async (): Promise<boolean> => {
// 清空标签页列表
openTabIds.value = [];
activeTabId.value = null;
return true;
};
// 关闭其他标签页(保留指定标签页)
const closeOtherTabs = async (keepDocumentId: number): Promise<boolean> => {
try {
const tabsToClose = openTabIds.value.filter(id => id !== keepDocumentId);
// 检查其他标签页是否有未保存的更改
const dirtyOtherTabs = tabsToClose.filter(id => {
const editorItem = editorCacheStore.getEditor(id);
return editorItem?.state.isDirty;
});
if (dirtyOtherTabs.length > 0) {
console.warn(`${dirtyOtherTabs.length} other tabs have unsaved changes`);
// 这里可以添加确认对话框逻辑
}
// 只保留指定的标签页
openTabIds.value = [keepDocumentId];
// 如果保留的标签页不是当前活跃的,切换到它
if (activeTabId.value !== keepDocumentId) {
await switchToTab(keepDocumentId);
}
return true;
} catch (error) {
console.error('Failed to close other tabs:', error);
return false;
}
};
// 获取标签页信息
const getTabInfo = (documentId: number): TabInfo | null => {
return openTabs.value.find(tab => tab.id === documentId) || null;
};
// 检查标签页是否打开
const isTabOpen = (documentId: number): boolean => {
return openTabIds.value.includes(documentId);
};
// === 监听器 ===
// 监听 documentStore 的当前文档变化,同步标签页状态
watch(
() => documentStore.currentDocument,
(newDoc) => {
if (newDoc) {
// 确保当前文档在标签页列表中
addTabToList(newDoc.id);
activeTabId.value = newDoc.id;
}
},
{immediate: true}
);
return {
// 状态
openTabIds,
activeTabId,
// 计算属性
openTabs,
activeTab,
tabCount,
hasTabs,
// 方法
openTab,
switchToTab,
closeTab,
closeAllTabs,
closeOtherTabs,
getTabInfo,
isTabOpen,
};
}, {
persist: {
key: 'voidraft-tabs',
storage: localStorage,
pick: ['openTabIds', 'activeTabId']
}
});