🚧 Optimize
This commit is contained in:
@@ -1,123 +1,175 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, readonly, ref} from 'vue';
|
||||
import type {GitBackupConfig} from '@/../bindings/voidraft/internal/models';
|
||||
import {BackupService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, readonly, ref, shallowRef, watchEffect, onScopeDispose } from 'vue';
|
||||
import type { GitBackupConfig } from '@/../bindings/voidraft/internal/models';
|
||||
import { BackupService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||
|
||||
// 备份状态枚举
|
||||
export enum BackupStatus {
|
||||
IDLE = 'idle',
|
||||
PUSHING = 'pushing',
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
// 备份操作结果类型
|
||||
export interface BackupResult {
|
||||
status: BackupStatus;
|
||||
message?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// 类型守卫函数
|
||||
const isBackupError = (error: unknown): error is Error => {
|
||||
return error instanceof Error;
|
||||
};
|
||||
|
||||
// 工具类型:提取错误消息
|
||||
type ErrorMessage<T> = T extends Error ? string : string;
|
||||
|
||||
|
||||
/**
|
||||
* Minimalist Backup Store
|
||||
*/
|
||||
export const useBackupStore = defineStore('backup', () => {
|
||||
// Core state
|
||||
const config = ref<GitBackupConfig | null>(null);
|
||||
const isPushing = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const isInitialized = ref(false);
|
||||
// === 核心状态 ===
|
||||
const config = shallowRef<GitBackupConfig | null>(null);
|
||||
|
||||
// 统一的备份结果状态
|
||||
const backupResult = ref<BackupResult>({
|
||||
status: BackupStatus.IDLE
|
||||
});
|
||||
|
||||
// === 定时器管理 ===
|
||||
const statusTimer = createTimerManager();
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onScopeDispose(() => {
|
||||
statusTimer.clear();
|
||||
});
|
||||
|
||||
// === 外部依赖 ===
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// === 计算属性 ===
|
||||
const isEnabled = computed(() => configStore.config.backup.enabled);
|
||||
const isConfigured = computed(() => Boolean(configStore.config.backup.repo_url?.trim()));
|
||||
|
||||
// 派生状态计算属性
|
||||
const isPushing = computed(() => backupResult.value.status === BackupStatus.PUSHING);
|
||||
const isSuccess = computed(() => backupResult.value.status === BackupStatus.SUCCESS);
|
||||
const isError = computed(() => backupResult.value.status === BackupStatus.ERROR);
|
||||
const errorMessage = computed(() =>
|
||||
backupResult.value.status === BackupStatus.ERROR ? backupResult.value.message : null
|
||||
);
|
||||
|
||||
// === 状态管理方法 ===
|
||||
|
||||
/**
|
||||
* 设置备份状态
|
||||
* @param status 备份状态
|
||||
* @param message 可选消息
|
||||
* @param autoHide 是否自动隐藏(毫秒)
|
||||
*/
|
||||
const setBackupStatus = <T extends BackupStatus>(
|
||||
status: T,
|
||||
message?: T extends BackupStatus.ERROR ? string : string,
|
||||
autoHide?: number
|
||||
): void => {
|
||||
statusTimer.clear();
|
||||
|
||||
// Backup result states
|
||||
const pushSuccess = ref(false);
|
||||
const pushError = ref(false);
|
||||
backupResult.value = {
|
||||
status,
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 自动隐藏逻辑
|
||||
if (autoHide && (status === BackupStatus.SUCCESS || status === BackupStatus.ERROR)) {
|
||||
statusTimer.set(() => {
|
||||
if (backupResult.value.status === status) {
|
||||
backupResult.value = { status: BackupStatus.IDLE };
|
||||
}
|
||||
}, autoHide);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除当前状态
|
||||
*/
|
||||
const clearStatus = (): void => {
|
||||
statusTimer.clear();
|
||||
backupResult.value = { status: BackupStatus.IDLE };
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理错误的通用方法
|
||||
*/
|
||||
const handleError = (error: unknown): void => {
|
||||
const message: ErrorMessage<typeof error> = isBackupError(error)
|
||||
? error.message
|
||||
: 'Backup operation failed';
|
||||
|
||||
// Timers for auto-hiding status icons and error messages
|
||||
let pushStatusTimer: number | null = null;
|
||||
let errorTimer: number | null = null;
|
||||
setBackupStatus(BackupStatus.ERROR, message, 5000);
|
||||
};
|
||||
|
||||
// 获取configStore
|
||||
const configStore = useConfigStore();
|
||||
// === 业务逻辑方法 ===
|
||||
|
||||
/**
|
||||
* 推送到远程仓库
|
||||
* 使用现代 async/await 和错误处理
|
||||
*/
|
||||
const pushToRemote = async (): Promise<void> => {
|
||||
// 前置条件检查
|
||||
if (isPushing.value || !isConfigured.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const isEnabled = computed(() => configStore.config.backup.enabled);
|
||||
const isConfigured = computed(() => configStore.config.backup.repo_url);
|
||||
try {
|
||||
setBackupStatus(BackupStatus.PUSHING);
|
||||
|
||||
await BackupService.PushToRemote();
|
||||
|
||||
setBackupStatus(BackupStatus.SUCCESS, 'Backup completed successfully', 3000);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 清除状态显示
|
||||
const clearPushStatus = () => {
|
||||
if (pushStatusTimer !== null) {
|
||||
window.clearTimeout(pushStatusTimer);
|
||||
pushStatusTimer = null;
|
||||
}
|
||||
pushSuccess.value = false;
|
||||
pushError.value = false;
|
||||
};
|
||||
/**
|
||||
* 重试备份操作
|
||||
*/
|
||||
const retryBackup = async (): Promise<void> => {
|
||||
if (isError.value) {
|
||||
await pushToRemote();
|
||||
}
|
||||
};
|
||||
|
||||
// 清除错误信息和错误图标
|
||||
const clearError = () => {
|
||||
if (errorTimer !== null) {
|
||||
window.clearTimeout(errorTimer);
|
||||
errorTimer = null;
|
||||
}
|
||||
error.value = null;
|
||||
pushError.value = false;
|
||||
};
|
||||
// === 响应式副作用 ===
|
||||
|
||||
// 监听配置变化,自动清除错误状态
|
||||
watchEffect(() => {
|
||||
if (isEnabled.value && isConfigured.value && isError.value) {
|
||||
// 配置修复后清除错误状态
|
||||
clearStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置错误信息和错误图标并自动清除
|
||||
const setErrorWithAutoHide = (errorMessage: string, hideAfter: number = 3000) => {
|
||||
clearError();
|
||||
clearPushStatus();
|
||||
error.value = errorMessage;
|
||||
pushError.value = true;
|
||||
errorTimer = window.setTimeout(() => {
|
||||
error.value = null;
|
||||
pushError.value = false;
|
||||
errorTimer = null;
|
||||
}, hideAfter);
|
||||
};
|
||||
// === 返回的 API ===
|
||||
return {
|
||||
// 只读状态
|
||||
config: readonly(config),
|
||||
backupResult: readonly(backupResult),
|
||||
|
||||
// Push to remote repository
|
||||
const pushToRemote = async () => {
|
||||
if (isPushing.value || !isConfigured.value) return;
|
||||
// 计算属性
|
||||
isEnabled,
|
||||
isConfigured,
|
||||
isPushing,
|
||||
isSuccess,
|
||||
isError,
|
||||
errorMessage,
|
||||
|
||||
isPushing.value = true;
|
||||
clearError(); // 清除之前的错误信息
|
||||
clearPushStatus();
|
||||
|
||||
try {
|
||||
await BackupService.PushToRemote();
|
||||
// 显示成功状态,并设置3秒后自动消失
|
||||
pushSuccess.value = true;
|
||||
pushStatusTimer = window.setTimeout(() => {
|
||||
pushSuccess.value = false;
|
||||
pushStatusTimer = null;
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
setErrorWithAutoHide(err?.message || 'Backup operation failed');
|
||||
} finally {
|
||||
isPushing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化备份服务
|
||||
const initialize = async () => {
|
||||
if (!isEnabled.value) return;
|
||||
|
||||
// 避免重复初始化
|
||||
if (isInitialized.value) return;
|
||||
|
||||
clearError(); // 清除之前的错误信息
|
||||
try {
|
||||
await BackupService.Initialize();
|
||||
isInitialized.value = true;
|
||||
} catch (err: any) {
|
||||
setErrorWithAutoHide(err?.message || 'Failed to initialize backup service');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
// State
|
||||
config: readonly(config),
|
||||
isPushing: readonly(isPushing),
|
||||
error: readonly(error),
|
||||
isInitialized: readonly(isInitialized),
|
||||
pushSuccess: readonly(pushSuccess),
|
||||
pushError: readonly(pushError),
|
||||
|
||||
// Computed
|
||||
isEnabled,
|
||||
isConfigured,
|
||||
|
||||
// Methods
|
||||
pushToRemote,
|
||||
initialize,
|
||||
clearError
|
||||
};
|
||||
// 方法
|
||||
pushToRemote,
|
||||
retryBackup,
|
||||
clearStatus
|
||||
} as const;
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {ConfigUtils} from '@/common/utils/configUtils';
|
||||
import {FONT_OPTIONS} from '@/common/constant/fonts';
|
||||
import {SupportedLocaleType, SUPPORTED_LOCALES} from '@/common/constant/locales';
|
||||
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
|
||||
import {
|
||||
NumberConfigKey,
|
||||
GENERAL_CONFIG_KEY_MAP,
|
||||
@@ -29,19 +29,6 @@ import {
|
||||
} from '@/common/constant/config';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
|
||||
// 获取浏览器的默认语言
|
||||
const getBrowserLanguage = (): SupportedLocaleType => {
|
||||
const browserLang = navigator.language;
|
||||
const langCode = browserLang.split('-')[0];
|
||||
|
||||
// 检查是否支持此语言
|
||||
const supportedLang = SUPPORTED_LOCALES.find(locale =>
|
||||
locale.code.startsWith(langCode) || locale.code.split('-')[0] === langCode
|
||||
);
|
||||
|
||||
return supportedLang?.code || 'zh-CN';
|
||||
};
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const {locale} = useI18n();
|
||||
|
||||
@@ -231,7 +218,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language);
|
||||
locale.value = frontendLocale as any;
|
||||
} catch (_error) {
|
||||
const browserLang = getBrowserLanguage();
|
||||
const browserLang = SUPPORTED_LOCALES[0].code;
|
||||
locale.value = browserLang as any;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 currentDocumentId = ref<number | null>(null);
|
||||
@@ -34,7 +34,7 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
|
||||
// === 错误处理 ===
|
||||
const setError = (docId: number, message: string) => {
|
||||
selectorError.value = { docId, message };
|
||||
selectorError.value = {docId, message};
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
@@ -88,25 +88,8 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 保存新文档
|
||||
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 () => {
|
||||
// 获取文档列表
|
||||
const getDocumentMetaList = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const docs = await DocumentService.ListAllDocumentsMeta();
|
||||
@@ -199,7 +182,7 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// === 初始化 ===
|
||||
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
||||
try {
|
||||
await updateDocuments();
|
||||
await getDocumentMetaList();
|
||||
|
||||
// 优先使用URL参数中的文档ID
|
||||
if (urlDocumentId && documents.value[urlDocumentId]) {
|
||||
@@ -208,11 +191,8 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// 如果URL中没有指定文档ID,则使用持久化的文档ID
|
||||
await openDocument(currentDocumentId.value);
|
||||
} else {
|
||||
// 否则获取第一个文档ID并打开
|
||||
const firstDocId = await DocumentService.GetFirstDocumentID();
|
||||
if (firstDocId && documents.value[firstDocId]) {
|
||||
await openDocument(firstDocId);
|
||||
}
|
||||
// 否则打开默认文档
|
||||
await openDocument(DEFAULT_DOCUMENT_ID.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize document store:', error);
|
||||
@@ -231,11 +211,10 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
isLoading,
|
||||
|
||||
// 方法
|
||||
updateDocuments,
|
||||
getDocumentMetaList,
|
||||
openDocument,
|
||||
openDocumentInNewWindow,
|
||||
createNewDocument,
|
||||
saveNewDocument,
|
||||
updateDocumentMetadata,
|
||||
deleteDocument,
|
||||
openDocumentSelector,
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, shallowRef, type ShallowRef } from 'vue';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { ensureSyntaxTree } from '@codemirror/language';
|
||||
import { LruCache, type CacheItem, type DisposableCacheItem, createHash } from '@/common/cache';
|
||||
import { removeExtensionManagerView } from '@/views/editor/manager';
|
||||
|
||||
/** 语法树缓存信息 */
|
||||
interface SyntaxTreeCache {
|
||||
readonly lastDocLength: number;
|
||||
readonly lastContentHash: string;
|
||||
readonly lastParsed: Date;
|
||||
}
|
||||
|
||||
/** 编辑器状态 */
|
||||
interface EditorState {
|
||||
content: string;
|
||||
isDirty: boolean;
|
||||
lastModified: Date;
|
||||
}
|
||||
|
||||
/** 编辑器缓存项 */
|
||||
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 containerElement: ShallowRef<HTMLElement | null> = shallowRef(null);
|
||||
|
||||
// === 内部方法 ===
|
||||
const cleanupEditor = (item: EditorCacheItem): void => {
|
||||
try {
|
||||
// 清除自动保存定时器
|
||||
if (item.autoSaveTimer) {
|
||||
clearTimeout(item.autoSaveTimer);
|
||||
item.autoSaveTimer = null;
|
||||
}
|
||||
|
||||
// 从扩展管理器中移除视图
|
||||
removeExtensionManagerView(item.documentId);
|
||||
|
||||
// 移除DOM元素
|
||||
item.view.dom?.remove();
|
||||
|
||||
// 销毁编辑器
|
||||
item.view.destroy?.();
|
||||
} catch (error) {
|
||||
console.error(`Failed to cleanup editor ${item.documentId}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const createEditorCacheItem = (
|
||||
documentId: number,
|
||||
view: EditorView,
|
||||
content: string
|
||||
): EditorCacheItem => {
|
||||
const now = new Date();
|
||||
|
||||
const item: EditorCacheItem = {
|
||||
id: documentId,
|
||||
lastAccessed: now,
|
||||
createdAt: now,
|
||||
view,
|
||||
documentId,
|
||||
state: {
|
||||
content,
|
||||
isDirty: false,
|
||||
lastModified: now
|
||||
},
|
||||
autoSaveTimer: null,
|
||||
syntaxTreeCache: null,
|
||||
dispose: () => cleanupEditor(item)
|
||||
};
|
||||
|
||||
return 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>({
|
||||
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 = (): HTMLElement | null => containerElement.value;
|
||||
|
||||
// 基础缓存操作
|
||||
const addEditor = (documentId: number, view: EditorView, content: string): void => {
|
||||
const item = createEditorCacheItem(documentId, view, content);
|
||||
cache.set(documentId, item);
|
||||
|
||||
// 初始化语法树缓存
|
||||
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 clearAll = (): void => {
|
||||
cache.clear();
|
||||
};
|
||||
|
||||
// 编辑器状态管理
|
||||
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 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;
|
||||
|
||||
buildSyntaxTree(view, item);
|
||||
};
|
||||
|
||||
const cleanupExpiredSyntaxTrees = (): void => {
|
||||
const now = Date.now();
|
||||
allEditors.value.forEach(item => {
|
||||
if (item.syntaxTreeCache &&
|
||||
(now - item.syntaxTreeCache.lastParsed.getTime()) > CACHE_CONFIG.syntaxTreeExpireTime) {
|
||||
item.syntaxTreeCache = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
// 容器管理
|
||||
setContainer,
|
||||
getContainer,
|
||||
|
||||
// 基础缓存操作
|
||||
addEditor,
|
||||
getEditor,
|
||||
hasEditor,
|
||||
removeEditor,
|
||||
clearAll,
|
||||
|
||||
// 编辑器状态管理
|
||||
updateEditorContent,
|
||||
markEditorDirty,
|
||||
|
||||
// 自动保存管理
|
||||
setAutoSaveTimer,
|
||||
clearAutoSaveTimer,
|
||||
|
||||
// 语法树管理
|
||||
ensureSyntaxTreeCached,
|
||||
cleanupExpiredSyntaxTrees,
|
||||
|
||||
// 计算属性
|
||||
cacheSize,
|
||||
cacheStats,
|
||||
allEditors,
|
||||
dirtyEditors
|
||||
};
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, nextTick, ref, watch} from 'vue';
|
||||
import {nextTick, ref, watch} 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 {useEditorCacheStore} from './editorCacheStore';
|
||||
import {ExtensionID, SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {DocumentService, ExtensionService} 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';
|
||||
@@ -15,15 +15,11 @@ 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,
|
||||
removeExtensionManagerView,
|
||||
setExtensionManagerView
|
||||
} from '@/views/editor/manager';
|
||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
||||
import {useExtensionStore} from './extensionStore';
|
||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||
import {AsyncOperationManager} from '@/common/async';
|
||||
|
||||
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
|
||||
|
||||
export interface DocumentStats {
|
||||
lines: number;
|
||||
@@ -31,48 +27,136 @@ export interface DocumentStats {
|
||||
selectedCharacters: number;
|
||||
}
|
||||
|
||||
interface EditorInstance {
|
||||
view: EditorView;
|
||||
documentId: number;
|
||||
content: string;
|
||||
isDirty: boolean;
|
||||
lastModified: Date;
|
||||
autoSaveTimer: number | null;
|
||||
syntaxTreeCache: {
|
||||
lastDocLength: number;
|
||||
lastContentHash: string;
|
||||
lastParsed: Date;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
// === 依赖store ===
|
||||
const configStore = useConfigStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const themeStore = useThemeStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
const editorCacheStore = useEditorCacheStore();
|
||||
|
||||
// === 核心状态 ===
|
||||
const editorCache = ref<{
|
||||
lru: number[];
|
||||
instances: Record<number, EditorInstance>;
|
||||
containerElement: HTMLElement | null;
|
||||
}>({
|
||||
lru: [],
|
||||
instances: {},
|
||||
containerElement: null
|
||||
});
|
||||
|
||||
const currentEditor = ref<EditorView | null>(null);
|
||||
const documentStats = ref<DocumentStats>({
|
||||
lines: 0,
|
||||
characters: 0,
|
||||
selectedCharacters: 0
|
||||
});
|
||||
|
||||
|
||||
// 编辑器加载状态
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 异步操作管理器
|
||||
const operationManager = new AsyncOperationManager({
|
||||
debug: false,
|
||||
autoCleanup: true
|
||||
});
|
||||
// 异步操作竞态条件控制
|
||||
const operationSequence = ref(0);
|
||||
const pendingOperations = ref(new Map<number, AbortController>());
|
||||
const currentLoadingDocumentId = ref<number | null>(null);
|
||||
|
||||
// 自动保存设置
|
||||
// 自动保存设置 - 从配置动态获取
|
||||
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
||||
|
||||
// 生成新的操作序列号
|
||||
const getNextOperationId = () => ++operationSequence.value;
|
||||
|
||||
// 取消之前的操作
|
||||
const cancelPreviousOperations = (excludeId?: number) => {
|
||||
pendingOperations.value.forEach((controller, id) => {
|
||||
if (id !== excludeId) {
|
||||
controller.abort();
|
||||
pendingOperations.value.delete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
const isOperationValid = (operationId: number, documentId: number) => {
|
||||
return (
|
||||
pendingOperations.value.has(operationId) &&
|
||||
!pendingOperations.value.get(operationId)?.signal.aborted &&
|
||||
currentLoadingDocumentId.value === documentId
|
||||
);
|
||||
};
|
||||
|
||||
// === 私有方法 ===
|
||||
|
||||
// 生成内容哈希
|
||||
const generateContentHash = (content: string): string => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return hash.toString();
|
||||
};
|
||||
|
||||
// 缓存化的语法树确保方法
|
||||
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance) return;
|
||||
|
||||
const docLength = view.state.doc.length;
|
||||
const content = view.state.doc.toString();
|
||||
const contentHash = generateContentHash(content);
|
||||
const now = new Date();
|
||||
|
||||
// 检查是否需要重新构建语法树
|
||||
const cache = instance.syntaxTreeCache;
|
||||
const shouldRebuild = !cache ||
|
||||
cache.lastDocLength !== docLength ||
|
||||
cache.lastContentHash !== contentHash ||
|
||||
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
|
||||
|
||||
if (shouldRebuild) {
|
||||
try {
|
||||
ensureSyntaxTree(view.state, docLength, 5000);
|
||||
|
||||
// 更新缓存
|
||||
instance.syntaxTreeCache = {
|
||||
lastDocLength: docLength,
|
||||
lastContentHash: contentHash,
|
||||
lastParsed: now
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to ensure syntax tree:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 创建编辑器实例
|
||||
const createEditorInstance = async (
|
||||
content: string,
|
||||
signal: AbortSignal,
|
||||
content: string,
|
||||
operationId: number,
|
||||
documentId: number
|
||||
): Promise<EditorView> => {
|
||||
if (!editorCacheStore.getContainer()) {
|
||||
if (!editorCache.value.containerElement) {
|
||||
throw new Error('Editor container not set');
|
||||
}
|
||||
|
||||
// 检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
// 检查操作是否仍然有效
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
@@ -111,24 +195,24 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
enableAutoDetection: true
|
||||
});
|
||||
|
||||
// 再次检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
// 再次检查操作有效性
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 快捷键扩展
|
||||
const keymapExtension = await createDynamicKeymapExtension();
|
||||
|
||||
// 检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
// 检查操作有效性
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
||||
const dynamicExtensions = await createDynamicExtensions(documentId);
|
||||
|
||||
// 最终检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
// 最终检查操作有效性
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
@@ -166,43 +250,91 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
return view;
|
||||
};
|
||||
|
||||
// 添加编辑器到缓存
|
||||
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,
|
||||
syntaxTreeCache: null
|
||||
};
|
||||
|
||||
// 添加到LRU列表
|
||||
editorCache.value.lru.push(documentId);
|
||||
|
||||
// 初始化语法树缓存
|
||||
ensureSyntaxTreeCached(view, 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,
|
||||
signal: AbortSignal
|
||||
documentId: number,
|
||||
content: string,
|
||||
operationId: number
|
||||
): Promise<EditorView> => {
|
||||
// 检查缓存
|
||||
const cached = editorCacheStore.getEditor(documentId);
|
||||
const cached = editorCache.value.instances[documentId];
|
||||
if (cached) {
|
||||
updateLRU(documentId);
|
||||
return cached.view;
|
||||
}
|
||||
|
||||
// 检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
// 检查操作是否仍然有效
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 创建新的编辑器实例
|
||||
const view = await createEditorInstance(content, signal, documentId);
|
||||
|
||||
// 最终检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
const view = await createEditorInstance(content, operationId, documentId);
|
||||
|
||||
// 最终检查操作有效性
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
// 如果操作已取消,清理创建的实例
|
||||
view.destroy();
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 添加到缓存
|
||||
editorCacheStore.addEditor(documentId, view, content);
|
||||
addEditorToCache(documentId, view, content);
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
// 显示编辑器
|
||||
const showEditor = (documentId: number) => {
|
||||
const instance = editorCacheStore.getEditor(documentId);
|
||||
if (!instance || !editorCacheStore.getContainer()) return;
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance || !editorCache.value.containerElement) return;
|
||||
|
||||
try {
|
||||
// 移除当前编辑器DOM
|
||||
@@ -211,19 +343,18 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
|
||||
// 确保容器为空
|
||||
const container = editorCacheStore.getContainer();
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
|
||||
// 将目标编辑器DOM添加到容器
|
||||
container.appendChild(instance.view.dom);
|
||||
}
|
||||
editorCache.value.containerElement.innerHTML = '';
|
||||
|
||||
// 将目标编辑器DOM添加到容器
|
||||
editorCache.value.containerElement.appendChild(instance.view.dom);
|
||||
currentEditor.value = instance.view;
|
||||
|
||||
// 设置扩展管理器视图
|
||||
setExtensionManagerView(instance.view, documentId);
|
||||
|
||||
// 更新LRU
|
||||
updateLRU(documentId);
|
||||
|
||||
// 重新测量和聚焦编辑器
|
||||
nextTick(() => {
|
||||
// 将光标定位到文档末尾并滚动到该位置
|
||||
@@ -232,12 +363,12 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
selection: {anchor: docLength, head: docLength},
|
||||
scrollIntoView: true
|
||||
});
|
||||
|
||||
// 滚动到文档底部
|
||||
|
||||
// 滚动到文档底部(将光标位置滚动到可见区域)
|
||||
instance.view.focus();
|
||||
|
||||
|
||||
// 使用缓存的语法树确保方法
|
||||
editorCacheStore.ensureSyntaxTreeCached(instance.view, documentId);
|
||||
ensureSyntaxTreeCached(instance.view, documentId);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error showing editor:', error);
|
||||
@@ -246,19 +377,20 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 保存编辑器内容
|
||||
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
||||
const instance = editorCacheStore.getEditor(documentId);
|
||||
if (!instance || !instance.state.isDirty) return true;
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance || !instance.isDirty) return true;
|
||||
|
||||
try {
|
||||
const content = instance.view.state.doc.toString();
|
||||
const lastModified = instance.state.lastModified;
|
||||
|
||||
const lastModified = instance.lastModified;
|
||||
|
||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||
|
||||
// 检查在保存期间内容是否又被修改了
|
||||
if (instance.state.lastModified === lastModified) {
|
||||
editorCacheStore.updateEditorContent(documentId, content);
|
||||
// isDirty 已在 updateEditorContent 中设置为 false
|
||||
if (instance.lastModified === lastModified) {
|
||||
instance.content = content;
|
||||
instance.isDirty = false;
|
||||
instance.lastModified = new Date();
|
||||
}
|
||||
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
||||
|
||||
@@ -271,23 +403,31 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 内容变化处理
|
||||
const onContentChange = (documentId: number) => {
|
||||
const instance = editorCacheStore.getEditor(documentId);
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance) return;
|
||||
|
||||
editorCacheStore.markEditorDirty(documentId);
|
||||
instance.isDirty = true;
|
||||
instance.lastModified = new Date();
|
||||
|
||||
// 清理语法树缓存,下次访问时重新构建
|
||||
instance.syntaxTreeCache = null;
|
||||
|
||||
// 清除之前的定时器并设置新的自动保存定时器
|
||||
const timer = setTimeout(() => {
|
||||
// 清除之前的定时器
|
||||
if (instance.autoSaveTimer) {
|
||||
clearTimeout(instance.autoSaveTimer);
|
||||
}
|
||||
|
||||
// 设置新的自动保存定时器
|
||||
instance.autoSaveTimer = window.setTimeout(() => {
|
||||
saveEditorContent(documentId);
|
||||
}, getAutoSaveDelay());
|
||||
editorCacheStore.setAutoSaveTimer(documentId, timer as unknown as number);
|
||||
};
|
||||
|
||||
// === 公共API ===
|
||||
|
||||
// 设置编辑器容器
|
||||
const setEditorContainer = (container: HTMLElement | null) => {
|
||||
editorCacheStore.setContainer(container);
|
||||
editorCache.value.containerElement = container;
|
||||
|
||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||
if (container && documentStore.currentDocument) {
|
||||
@@ -299,6 +439,9 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
const loadEditor = async (documentId: number, content: string) => {
|
||||
// 设置加载状态
|
||||
isLoading.value = true;
|
||||
// 生成新的操作ID
|
||||
const operationId = getNextOperationId();
|
||||
const abortController = new AbortController();
|
||||
|
||||
try {
|
||||
// 验证参数
|
||||
@@ -306,69 +449,72 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
throw new Error('Invalid parameters for loadEditor');
|
||||
}
|
||||
|
||||
// 使用异步操作管理器执行加载操作
|
||||
const result = await operationManager.executeOperation(
|
||||
documentId,
|
||||
async (signal) => {
|
||||
// 保存当前编辑器内容
|
||||
if (currentEditor.value) {
|
||||
const currentDocId = documentStore.currentDocumentId;
|
||||
if (currentDocId && currentDocId !== documentId) {
|
||||
await saveEditorContent(currentDocId);
|
||||
// 取消之前的操作并设置当前操作
|
||||
cancelPreviousOperations();
|
||||
currentLoadingDocumentId.value = documentId;
|
||||
pendingOperations.value.set(operationId, abortController);
|
||||
|
||||
// 检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
}
|
||||
// 保存当前编辑器内容
|
||||
if (currentEditor.value) {
|
||||
const currentDocId = documentStore.currentDocumentId;
|
||||
if (currentDocId && currentDocId !== documentId) {
|
||||
await saveEditorContent(currentDocId);
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取或创建编辑器
|
||||
const view = await getOrCreateEditor(documentId, content, signal);
|
||||
|
||||
// 检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
const instance = editorCacheStore.getEditor(documentId);
|
||||
if (instance && instance.state.content !== content) {
|
||||
// 确保编辑器视图有效
|
||||
if (view && view.state && view.dispatch) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: content
|
||||
}
|
||||
});
|
||||
editorCacheStore.updateEditorContent(documentId, content);
|
||||
}
|
||||
}
|
||||
|
||||
// 最终检查操作是否被取消
|
||||
if (signal.aborted) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 显示编辑器
|
||||
showEditor(documentId);
|
||||
|
||||
return view;
|
||||
},
|
||||
'loadEditor'
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.error?.message !== 'Operation cancelled') {
|
||||
console.error('Failed to load editor:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取或创建编辑器
|
||||
const view = await getOrCreateEditor(documentId, content, operationId);
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新内容(如果需要)
|
||||
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;
|
||||
// 清理语法树缓存,因为内容已更新
|
||||
instance.syntaxTreeCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 最终检查操作有效性
|
||||
if (!isOperationValid(operationId, documentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示编辑器
|
||||
showEditor(documentId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load editor:', error);
|
||||
if (error instanceof Error && error.message === 'Operation cancelled') {
|
||||
console.log(`Editor loading cancelled for document ${documentId}`);
|
||||
} else {
|
||||
console.error('Failed to load editor:', error);
|
||||
}
|
||||
} finally {
|
||||
// 清理操作记录
|
||||
pendingOperations.value.delete(operationId);
|
||||
if (currentLoadingDocumentId.value === documentId) {
|
||||
currentLoadingDocumentId.value = null;
|
||||
}
|
||||
|
||||
// 延迟一段时间后再取消加载状态
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
@@ -378,20 +524,49 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 移除编辑器
|
||||
const removeEditor = (documentId: number) => {
|
||||
// 取消该文档的所有操作
|
||||
operationManager.cancelResourceOperations(documentId);
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (instance) {
|
||||
try {
|
||||
// 如果正在加载这个文档,取消操作
|
||||
if (currentLoadingDocumentId.value === documentId) {
|
||||
cancelPreviousOperations();
|
||||
currentLoadingDocumentId.value = null;
|
||||
}
|
||||
|
||||
// 从扩展管理器中移除视图
|
||||
removeExtensionManagerView(documentId);
|
||||
// 清除自动保存定时器
|
||||
if (instance.autoSaveTimer) {
|
||||
clearTimeout(instance.autoSaveTimer);
|
||||
instance.autoSaveTimer = null;
|
||||
}
|
||||
|
||||
// 清除当前编辑器引用
|
||||
const instance = editorCacheStore.getEditor(documentId);
|
||||
if (instance && currentEditor.value === instance.view) {
|
||||
currentEditor.value = null;
|
||||
// 从扩展管理器中移除视图
|
||||
removeExtensionManagerView(documentId);
|
||||
|
||||
// 移除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);
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存中移除编辑器
|
||||
editorCacheStore.removeEditor(documentId);
|
||||
};
|
||||
|
||||
// 更新文档统计
|
||||
@@ -401,7 +576,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 应用字体设置
|
||||
const applyFontSettings = () => {
|
||||
editorCacheStore.allEditors.forEach(instance => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
updateFontConfig(instance.view, {
|
||||
fontFamily: configStore.config.editing.fontFamily,
|
||||
fontSize: configStore.config.editing.fontSize,
|
||||
@@ -413,7 +588,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 应用主题设置
|
||||
const applyThemeSettings = () => {
|
||||
editorCacheStore.allEditors.forEach(instance => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
updateEditorTheme(instance.view,
|
||||
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
@@ -422,7 +597,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 应用Tab设置
|
||||
const applyTabSettings = () => {
|
||||
editorCacheStore.allEditors.forEach(instance => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
updateTabConfig(
|
||||
instance.view,
|
||||
configStore.config.editing.tabSize,
|
||||
@@ -436,21 +611,37 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
const applyKeymapSettings = async () => {
|
||||
// 确保所有编辑器实例的快捷键都更新
|
||||
await Promise.all(
|
||||
editorCacheStore.allEditors.map(instance =>
|
||||
Object.values(editorCache.value.instances).map(instance =>
|
||||
updateKeymapExtension(instance.view)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 清理所有编辑器
|
||||
// 清空所有编辑器
|
||||
const clearAllEditors = () => {
|
||||
// 取消所有挂起的操作
|
||||
operationManager.cancelAllOperations();
|
||||
cancelPreviousOperations();
|
||||
currentLoadingDocumentId.value = null;
|
||||
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
// 清除自动保存定时器
|
||||
if (instance.autoSaveTimer) {
|
||||
clearTimeout(instance.autoSaveTimer);
|
||||
}
|
||||
|
||||
// 从扩展管理器移除
|
||||
removeExtensionManagerView(instance.documentId);
|
||||
|
||||
// 移除DOM元素
|
||||
if (instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
// 销毁编辑器
|
||||
instance.view.destroy();
|
||||
});
|
||||
|
||||
// 清理所有编辑器
|
||||
editorCacheStore.clearAll();
|
||||
|
||||
// 清除当前编辑器引用
|
||||
editorCache.value.instances = {};
|
||||
editorCache.value.lru = [];
|
||||
currentEditor.value = null;
|
||||
};
|
||||
|
||||
@@ -479,37 +670,9 @@ 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()) {
|
||||
if (newDoc && editorCache.value.containerElement) {
|
||||
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
||||
nextTick(() => {
|
||||
loadEditor(newDoc.id, newDoc.content);
|
||||
@@ -517,6 +680,15 @@ 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);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentEditor,
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
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']
|
||||
}
|
||||
});
|
||||
@@ -133,7 +133,7 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新编辑器主题(在主题颜色更改后调用)
|
||||
// 刷新编辑器主题
|
||||
const refreshEditorTheme = () => {
|
||||
// 使用当前主题重新应用DOM主题
|
||||
const theme = currentTheme.value;
|
||||
|
||||
@@ -149,7 +149,6 @@ export const useTranslationStore = defineStore('translation', () => {
|
||||
if (defaultTargetLang.value) {
|
||||
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
||||
if (validatedLang !== defaultTargetLang.value) {
|
||||
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
|
||||
defaultTargetLang.value = validatedLang;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +1,236 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref} from 'vue';
|
||||
import {CheckForUpdates, ApplyUpdate, RestartApplication} from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
||||
import {SelfUpdateResult} from '@/../bindings/voidraft/internal/services/models';
|
||||
import {useConfigStore} from './configStore';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, readonly, ref, shallowRef, onScopeDispose } from 'vue';
|
||||
import { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
||||
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
|
||||
import { useConfigStore } from './configStore';
|
||||
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||
import * as runtime from "@wailsio/runtime";
|
||||
|
||||
// 更新状态枚举
|
||||
export enum UpdateStatus {
|
||||
IDLE = 'idle',
|
||||
CHECKING = 'checking',
|
||||
UPDATE_AVAILABLE = 'update_available',
|
||||
UPDATING = 'updating',
|
||||
UPDATE_SUCCESS = 'update_success',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
// 更新操作结果类型
|
||||
export interface UpdateOperationResult {
|
||||
status: UpdateStatus;
|
||||
result?: SelfUpdateResult;
|
||||
message?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// 类型守卫函数
|
||||
const isUpdateError = (error: unknown): error is Error => {
|
||||
return error instanceof Error;
|
||||
};
|
||||
|
||||
export const useUpdateStore = defineStore('update', () => {
|
||||
// 状态
|
||||
const isChecking = ref(false);
|
||||
const isUpdating = ref(false);
|
||||
const updateResult = ref<SelfUpdateResult | null>(null);
|
||||
const hasCheckedOnStartup = ref(false);
|
||||
const updateSuccess = ref(false);
|
||||
const errorMessage = ref('');
|
||||
// === 核心状态 ===
|
||||
const hasCheckedOnStartup = ref(false);
|
||||
|
||||
// 统一的更新操作结果状态
|
||||
const updateOperation = ref<UpdateOperationResult>({
|
||||
status: UpdateStatus.IDLE
|
||||
});
|
||||
|
||||
// === 定时器管理 ===
|
||||
const statusTimer = createTimerManager();
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onScopeDispose(() => {
|
||||
statusTimer.clear();
|
||||
});
|
||||
|
||||
// === 外部依赖 ===
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// === 计算属性 ===
|
||||
|
||||
// 派生状态计算属性
|
||||
const isChecking = computed(() => updateOperation.value.status === UpdateStatus.CHECKING);
|
||||
const isUpdating = computed(() => updateOperation.value.status === UpdateStatus.UPDATING);
|
||||
const hasUpdate = computed(() => updateOperation.value.status === UpdateStatus.UPDATE_AVAILABLE);
|
||||
const updateSuccess = computed(() => updateOperation.value.status === UpdateStatus.UPDATE_SUCCESS);
|
||||
const isError = computed(() => updateOperation.value.status === UpdateStatus.ERROR);
|
||||
|
||||
// 数据访问计算属性
|
||||
const updateResult = computed(() => updateOperation.value.result || undefined);
|
||||
const errorMessage = computed(() =>
|
||||
updateOperation.value.status === UpdateStatus.ERROR ? updateOperation.value.message : ''
|
||||
);
|
||||
|
||||
// === 状态管理方法 ===
|
||||
|
||||
/**
|
||||
* 设置更新状态
|
||||
* @param status 更新状态
|
||||
* @param result 可选的更新结果
|
||||
* @param message 可选消息
|
||||
* @param autoHide 是否自动隐藏(毫秒)
|
||||
*/
|
||||
const setUpdateStatus = <T extends UpdateStatus>(
|
||||
status: T,
|
||||
result?: SelfUpdateResult,
|
||||
message?: string,
|
||||
autoHide?: number
|
||||
): void => {
|
||||
updateOperation.value = {
|
||||
status,
|
||||
result,
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 自动隐藏功能
|
||||
if (autoHide && autoHide > 0) {
|
||||
statusTimer.set(() => {
|
||||
if (updateOperation.value.status === status) {
|
||||
updateOperation.value = { status: UpdateStatus.IDLE };
|
||||
}
|
||||
}, autoHide);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除状态
|
||||
*/
|
||||
const clearStatus = (): void => {
|
||||
statusTimer.clear();
|
||||
updateOperation.value = { status: UpdateStatus.IDLE };
|
||||
};
|
||||
|
||||
// === 业务方法 ===
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
* @returns Promise<boolean> 是否成功检查
|
||||
*/
|
||||
const checkForUpdates = async (): Promise<boolean> => {
|
||||
if (isChecking.value) return false;
|
||||
|
||||
setUpdateStatus(UpdateStatus.CHECKING);
|
||||
|
||||
try {
|
||||
const result = await CheckForUpdates();
|
||||
|
||||
if (result?.error) {
|
||||
setUpdateStatus(UpdateStatus.ERROR, result, result.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result?.hasUpdate) {
|
||||
setUpdateStatus(UpdateStatus.UPDATE_AVAILABLE, result);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 没有更新,设置为空闲状态
|
||||
setUpdateStatus(UpdateStatus.IDLE, result || undefined);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
const message = isUpdateError(error) ? error.message : 'Network error';
|
||||
setUpdateStatus(UpdateStatus.ERROR, undefined, message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用更新
|
||||
* @returns Promise<boolean> 是否成功应用更新
|
||||
*/
|
||||
const applyUpdate = async (): Promise<boolean> => {
|
||||
if (isUpdating.value) return false;
|
||||
|
||||
setUpdateStatus(UpdateStatus.UPDATING);
|
||||
|
||||
try {
|
||||
const result = await ApplyUpdate();
|
||||
|
||||
if (result?.error) {
|
||||
setUpdateStatus(UpdateStatus.ERROR, result || undefined, result.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result?.updateApplied) {
|
||||
setUpdateStatus(UpdateStatus.UPDATE_SUCCESS, result || undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
setUpdateStatus(UpdateStatus.ERROR, result || undefined, 'Update failed');
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
const message = isUpdateError(error) ? error.message : 'Update failed';
|
||||
setUpdateStatus(UpdateStatus.ERROR, undefined, message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重启应用
|
||||
* @returns Promise<boolean> 是否成功重启
|
||||
*/
|
||||
const restartApplication = async (): Promise<boolean> => {
|
||||
try {
|
||||
await RestartApplication();
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message = isUpdateError(error) ? error.message : 'Restart failed';
|
||||
setUpdateStatus(UpdateStatus.ERROR, undefined, message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 启动时检查更新
|
||||
*/
|
||||
const checkOnStartup = async (): Promise<void> => {
|
||||
if (hasCheckedOnStartup.value) return;
|
||||
|
||||
if (configStore.config.updates.autoUpdate) {
|
||||
await checkForUpdates();
|
||||
}
|
||||
hasCheckedOnStartup.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开发布页面
|
||||
*/
|
||||
const openReleaseURL = async (): Promise<void> => {
|
||||
const result = updateResult.value;
|
||||
if (result?.assetURL) {
|
||||
await runtime.Browser.OpenURL(result.assetURL);
|
||||
}
|
||||
};
|
||||
|
||||
// === 公共接口 ===
|
||||
return {
|
||||
// 只读状态
|
||||
hasCheckedOnStartup: readonly(hasCheckedOnStartup),
|
||||
|
||||
// 计算属性
|
||||
const hasUpdate = computed(() => updateResult.value?.hasUpdate || false);
|
||||
isChecking,
|
||||
isUpdating,
|
||||
hasUpdate,
|
||||
updateSuccess,
|
||||
isError,
|
||||
updateResult,
|
||||
errorMessage,
|
||||
|
||||
// 检查更新
|
||||
const checkForUpdates = async (): Promise<boolean> => {
|
||||
if (isChecking.value) return false;
|
||||
|
||||
// 重置错误信息
|
||||
errorMessage.value = '';
|
||||
isChecking.value = true;
|
||||
try {
|
||||
const result = await CheckForUpdates();
|
||||
if (result) {
|
||||
updateResult.value = result;
|
||||
if (result.error) {
|
||||
errorMessage.value = result.error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Network error';
|
||||
return false;
|
||||
} finally {
|
||||
isChecking.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 应用更新
|
||||
const applyUpdate = async (): Promise<boolean> => {
|
||||
if (isUpdating.value) return false;
|
||||
|
||||
// 重置错误信息
|
||||
errorMessage.value = '';
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
const result = await ApplyUpdate();
|
||||
if (result) {
|
||||
updateResult.value = result;
|
||||
|
||||
if (result.error) {
|
||||
errorMessage.value = result.error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.updateApplied) {
|
||||
updateSuccess.value = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Update failed';
|
||||
return false;
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重启应用
|
||||
const restartApplication = async (): Promise<boolean> => {
|
||||
try {
|
||||
await RestartApplication();
|
||||
return true;
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Restart failed';
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动时检查更新
|
||||
const checkOnStartup = async () => {
|
||||
if (hasCheckedOnStartup.value) return;
|
||||
const configStore = useConfigStore();
|
||||
|
||||
if (configStore.config.updates.autoUpdate) {
|
||||
await checkForUpdates();
|
||||
}
|
||||
hasCheckedOnStartup.value = true;
|
||||
};
|
||||
|
||||
// 打开发布页面
|
||||
const openReleaseURL = async () => {
|
||||
if (updateResult.value?.assetURL) {
|
||||
await runtime.Browser.OpenURL(updateResult.value.assetURL);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置状态
|
||||
const reset = () => {
|
||||
updateResult.value = null;
|
||||
isChecking.value = false;
|
||||
isUpdating.value = false;
|
||||
updateSuccess.value = false;
|
||||
errorMessage.value = '';
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isChecking,
|
||||
isUpdating,
|
||||
updateResult,
|
||||
hasCheckedOnStartup,
|
||||
updateSuccess,
|
||||
errorMessage,
|
||||
|
||||
// 计算属性
|
||||
hasUpdate,
|
||||
|
||||
// 方法
|
||||
checkForUpdates,
|
||||
applyUpdate,
|
||||
restartApplication,
|
||||
checkOnStartup,
|
||||
openReleaseURL,
|
||||
reset
|
||||
};
|
||||
});
|
||||
// 方法
|
||||
checkForUpdates,
|
||||
applyUpdate,
|
||||
restartApplication,
|
||||
checkOnStartup,
|
||||
openReleaseURL,
|
||||
clearStatus,
|
||||
|
||||
// 内部状态管理
|
||||
setUpdateStatus
|
||||
};
|
||||
});
|
||||
@@ -1,20 +1,24 @@
|
||||
import {computed} from 'vue';
|
||||
import {computed, ref} from 'vue';
|
||||
import {defineStore} from 'pinia';
|
||||
import {IsDocumentWindowOpen} from "@/../bindings/voidraft/internal/services/windowservice";
|
||||
|
||||
|
||||
export const useWindowStore = defineStore('window', () => {
|
||||
|
||||
const DOCUMENT_ID_KEY = ref<string>('documentId');
|
||||
|
||||
// 判断是否为主窗口
|
||||
const isMainWindow = computed(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return !urlParams.has('documentId');
|
||||
return !urlParams.has(DOCUMENT_ID_KEY.value);
|
||||
});
|
||||
|
||||
// 获取当前窗口的documentId
|
||||
const currentDocumentId = computed(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('documentId');
|
||||
return urlParams.get(DOCUMENT_ID_KEY.value);
|
||||
});
|
||||
|
||||
/**
|
||||
* 判断文档窗口是否打开
|
||||
* @param documentId 文档ID
|
||||
|
||||
Reference in New Issue
Block a user