♻️ Refactor document selector and cache management logic
This commit is contained in:
@@ -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']
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
});
|
||||
253
frontend/src/stores/tabStore.ts
Normal file
253
frontend/src/stores/tabStore.ts
Normal 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']
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user