🚧 Refactor basic services
This commit is contained in:
@@ -161,7 +161,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
|
||||
|
||||
// 初始化语言设置
|
||||
const initializeLanguage = async (): Promise<void> => {
|
||||
const initLanguage = async (): Promise<void> => {
|
||||
try {
|
||||
// 如果配置未加载,先加载配置
|
||||
if (!state.configLoaded) {
|
||||
@@ -210,7 +210,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
|
||||
// 语言相关方法
|
||||
setLanguage,
|
||||
initializeLanguage,
|
||||
initLanguage,
|
||||
|
||||
// 主题相关方法
|
||||
setSystemTheme,
|
||||
|
||||
@@ -2,12 +2,11 @@ import {defineStore} from 'pinia';
|
||||
import {computed, ref} from 'vue';
|
||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {useTabStore} from "@/stores/tabStore";
|
||||
import type {EditorViewState} from '@/stores/editorStore';
|
||||
|
||||
export const useDocumentStore = defineStore('document', () => {
|
||||
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
|
||||
|
||||
// === 核心状态 ===
|
||||
const documents = ref<Record<number, Document>>({});
|
||||
@@ -15,7 +14,6 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
const currentDocument = ref<Document | null>(null);
|
||||
|
||||
// === 编辑器状态持久化 ===
|
||||
// 修复:使用统一的 EditorViewState 类型定义
|
||||
const documentStates = ref<Record<number, EditorViewState>>({});
|
||||
|
||||
// === UI状态 ===
|
||||
@@ -26,15 +24,18 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// === 计算属性 ===
|
||||
const documentList = computed(() =>
|
||||
Object.values(documents.value).sort((a, b) => {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
|
||||
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
})
|
||||
);
|
||||
|
||||
// === 私有方法 ===
|
||||
const setDocuments = (docs: Document[]) => {
|
||||
documents.value = {};
|
||||
docs.forEach(doc => {
|
||||
documents.value[doc.id] = doc;
|
||||
if (doc.id !== undefined) {
|
||||
documents.value[doc.id] = doc;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -64,15 +65,7 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
clearError();
|
||||
};
|
||||
|
||||
const toggleDocumentSelector = () => {
|
||||
if (showDocumentSelector.value) {
|
||||
closeDocumentSelector();
|
||||
} else {
|
||||
openDocumentSelector();
|
||||
}
|
||||
};
|
||||
|
||||
// === 文档操作方法 ===
|
||||
|
||||
// 在新窗口中打开文档
|
||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||
@@ -94,7 +87,7 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
const createNewDocument = async (title: string): Promise<Document | null> => {
|
||||
try {
|
||||
const doc = await DocumentService.CreateDocument(title);
|
||||
if (doc) {
|
||||
if (doc && doc.id !== undefined) {
|
||||
documents.value[doc.id] = doc;
|
||||
return doc;
|
||||
}
|
||||
@@ -123,8 +116,6 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// 打开文档
|
||||
const openDocument = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
closeDocumentSelector();
|
||||
|
||||
// 获取完整文档数据
|
||||
const doc = await DocumentService.GetDocumentByID(docId);
|
||||
if (!doc) {
|
||||
@@ -150,12 +141,12 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
const doc = documents.value[docId];
|
||||
if (doc) {
|
||||
doc.title = title;
|
||||
doc.updatedAt = new Date().toISOString();
|
||||
doc.updated_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
if (currentDocument.value?.id === docId) {
|
||||
currentDocument.value.title = title;
|
||||
currentDocument.value.updatedAt = new Date().toISOString();
|
||||
currentDocument.value.updated_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
// 同步更新标签页标题
|
||||
@@ -172,11 +163,6 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// 删除文档
|
||||
const deleteDocument = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
// 检查是否是默认文档(使用ID判断)
|
||||
if (docId === DEFAULT_DOCUMENT_ID.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await DocumentService.DeleteDocument(docId);
|
||||
|
||||
// 更新本地状态
|
||||
@@ -191,7 +177,7 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// 如果删除的是当前文档,切换到第一个可用文档
|
||||
if (currentDocumentId.value === docId) {
|
||||
const availableDocs = Object.values(documents.value);
|
||||
if (availableDocs.length > 0) {
|
||||
if (availableDocs.length > 0 && availableDocs[0].id !== undefined) {
|
||||
await openDocument(availableDocs[0].id);
|
||||
} else {
|
||||
currentDocumentId.value = null;
|
||||
@@ -218,8 +204,10 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// 如果URL中没有指定文档ID,则使用持久化的文档ID
|
||||
await openDocument(currentDocumentId.value);
|
||||
} else {
|
||||
// 否则打开默认文档
|
||||
await openDocument(DEFAULT_DOCUMENT_ID.value);
|
||||
// 否则打开第一个文档
|
||||
if (documents.value[0].id) {
|
||||
await openDocument(documents.value[0].id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize document store:', error);
|
||||
@@ -227,7 +215,6 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
};
|
||||
|
||||
return {
|
||||
DEFAULT_DOCUMENT_ID,
|
||||
// 状态
|
||||
documents,
|
||||
documentList,
|
||||
@@ -247,7 +234,6 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
deleteDocument,
|
||||
openDocumentSelector,
|
||||
closeDocumentSelector,
|
||||
toggleDocumentSelector,
|
||||
setError,
|
||||
clearError,
|
||||
initialize,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {EditorView} from '@codemirror/view';
|
||||
import {EditorState, Extension} from '@codemirror/state';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useDocumentStore} from './documentStore';
|
||||
import {ExtensionID} 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';
|
||||
@@ -28,7 +27,6 @@ import {AsyncManager} from '@/common/utils/asyncManager';
|
||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
|
||||
import {EDITOR_CONFIG} from '@/common/constant/editor';
|
||||
import {createHttpClientExtension} from "@/views/editor/extensions/httpclient";
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
export interface DocumentStats {
|
||||
@@ -93,84 +91,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
}, { delay: 500 }); // 500ms 内的多次输入只清理一次
|
||||
|
||||
// === 私有方法 ===
|
||||
|
||||
/**
|
||||
* 检查位置是否在代码块分隔符区域内
|
||||
*/
|
||||
const isPositionInDelimiter = (view: EditorView, pos: number): boolean => {
|
||||
try {
|
||||
const blocks = view.state.field(blockState, false);
|
||||
if (!blocks) return false;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 调整光标位置到有效的内容区域
|
||||
* 如果位置在分隔符内,移动到该块的内容开始位置
|
||||
*/
|
||||
const adjustCursorPosition = (view: EditorView, pos: number): number => {
|
||||
try {
|
||||
const blocks = view.state.field(blockState, false);
|
||||
if (!blocks || blocks.length === 0) return pos;
|
||||
|
||||
// 如果位置在分隔符内,移动到该块的内容开始位置
|
||||
for (const block of blocks) {
|
||||
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
|
||||
return block.content.from;
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
} catch {
|
||||
return pos;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 恢复编辑器的光标位置(自动滚动到光标处)
|
||||
*/
|
||||
const restoreEditorState = (instance: EditorInstance, documentId: number): void => {
|
||||
const savedState = instance.editorState;
|
||||
|
||||
if (savedState) {
|
||||
// 有保存的状态,恢复光标位置
|
||||
let pos = Math.min(savedState.cursorPos, instance.view.state.doc.length);
|
||||
|
||||
// 确保位置不在分隔符上
|
||||
if (isPositionInDelimiter(instance.view, pos)) {
|
||||
pos = adjustCursorPosition(instance.view, pos);
|
||||
}
|
||||
|
||||
// 修复:设置光标位置并居中滚动(更好的用户体验)
|
||||
instance.view.dispatch({
|
||||
selection: {anchor: pos, head: pos},
|
||||
effects: EditorView.scrollIntoView(pos, {
|
||||
y: "center", // 垂直居中显示
|
||||
yMargin: 100 // 上下留一些边距
|
||||
})
|
||||
});
|
||||
} else {
|
||||
// 首次打开或没有记录,光标在文档末尾
|
||||
const docLength = instance.view.state.doc.length;
|
||||
instance.view.dispatch({
|
||||
selection: {anchor: docLength, head: docLength},
|
||||
effects: EditorView.scrollIntoView(docLength, {
|
||||
y: "center",
|
||||
yMargin: 100
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 缓存化的语法树确保方法
|
||||
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||
@@ -245,7 +165,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
increaseFontSize: () => configStore.increaseFontSizeLocal(),
|
||||
decreaseFontSize: () => configStore.decreaseFontSizeLocal(),
|
||||
onSave: () => configStore.saveFontSize(),
|
||||
saveDelay: 500
|
||||
saveDelay: 1000
|
||||
});
|
||||
|
||||
// 统计扩展
|
||||
@@ -260,9 +180,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
enableAutoDetection: true
|
||||
});
|
||||
|
||||
const httpExtension = createHttpClientExtension();
|
||||
|
||||
|
||||
// 再次检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
@@ -277,7 +194,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
|
||||
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
||||
const dynamicExtensions = await createDynamicExtensions(documentId);
|
||||
const dynamicExtensions = await createDynamicExtensions();
|
||||
|
||||
// 最终检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
@@ -296,7 +213,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
...dynamicExtensions,
|
||||
...httpExtension,
|
||||
];
|
||||
|
||||
// 创建编辑器状态
|
||||
@@ -320,7 +236,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
lastModified: new Date(),
|
||||
autoSaveTimer: createTimerManager(),
|
||||
syntaxTreeCache: null,
|
||||
// 修复:创建实例时从 documentStore 读取持久化的编辑器状态
|
||||
editorState: documentStore.documentStates[documentId]
|
||||
};
|
||||
|
||||
@@ -401,9 +316,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// 恢复编辑器状态(光标位置和滚动位置)
|
||||
restoreEditorState(instance, documentId);
|
||||
|
||||
// 聚焦编辑器
|
||||
instance.view.focus();
|
||||
|
||||
@@ -433,7 +345,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
instance.isDirty = false;
|
||||
instance.lastModified = new Date();
|
||||
}
|
||||
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -462,15 +373,14 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}, getAutoSaveDelay());
|
||||
};
|
||||
|
||||
// === 公共API ===
|
||||
|
||||
// 设置编辑器容器
|
||||
const setEditorContainer = (container: HTMLElement | null) => {
|
||||
containerElement.value = container;
|
||||
|
||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||
if (container && documentStore.currentDocument) {
|
||||
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content);
|
||||
if (container && documentStore.currentDocument && documentStore.currentDocument.id !== undefined) {
|
||||
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content || '');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -696,20 +606,20 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
};
|
||||
|
||||
// 更新扩展
|
||||
const updateExtension = async (id: ExtensionID, enabled: boolean, config?: any) => {
|
||||
// 如果只是更新启用状态
|
||||
if (config === undefined) {
|
||||
await ExtensionService.UpdateExtensionEnabled(id, enabled);
|
||||
} else {
|
||||
// 如果需要更新配置
|
||||
await ExtensionService.UpdateExtensionState(id, enabled, config);
|
||||
const updateExtension = async (key: string, enabled: boolean, config?: any) => {
|
||||
// 更新启用状态
|
||||
await ExtensionService.UpdateExtensionEnabled(key, enabled);
|
||||
|
||||
// 如果需要更新配置
|
||||
if (config !== undefined) {
|
||||
await ExtensionService.UpdateExtensionConfig(key, config);
|
||||
}
|
||||
|
||||
// 更新前端编辑器扩展 - 应用于所有实例
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
// 直接更新前端扩展至所有视图
|
||||
manager.updateExtension(id, enabled, config);
|
||||
manager.updateExtension(key, enabled, config);
|
||||
}
|
||||
|
||||
// 重新加载扩展配置
|
||||
@@ -723,23 +633,10 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 监听文档切换
|
||||
watch(() => documentStore.currentDocument, async (newDoc, oldDoc) => {
|
||||
if (newDoc && containerElement.value) {
|
||||
// 修复:在切换到新文档前,只保存旧文档的光标位置
|
||||
if (oldDoc && oldDoc.id !== newDoc.id && currentEditor.value) {
|
||||
const oldInstance = editorCache.get(oldDoc.id);
|
||||
if (oldInstance) {
|
||||
const currentState: EditorViewState = {
|
||||
cursorPos: currentEditor.value.state.selection.main.head
|
||||
};
|
||||
// 同时保存到实例和 documentStore
|
||||
oldInstance.editorState = currentState;
|
||||
documentStore.documentStates[oldDoc.id] = currentState;
|
||||
}
|
||||
}
|
||||
|
||||
if (newDoc && newDoc.id !== undefined && containerElement.value) {
|
||||
// 等待 DOM 更新完成,再加载新文档的编辑器
|
||||
await nextTick();
|
||||
loadEditor(newDoc.id, newDoc.content);
|
||||
loadEditor(newDoc.id, newDoc.content || '');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { Extension, ExtensionID } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { Extension } from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import { ExtensionService } from '@/../bindings/voidraft/internal/services';
|
||||
|
||||
export const useExtensionStore = defineStore('extension', () => {
|
||||
@@ -12,9 +12,9 @@ export const useExtensionStore = defineStore('extension', () => {
|
||||
extensions.value.filter(ext => ext.enabled)
|
||||
);
|
||||
|
||||
// 获取启用的扩展ID列表
|
||||
// 获取启用的扩展ID列表 (key)
|
||||
const enabledExtensionIds = computed(() =>
|
||||
enabledExtensions.value.map(ext => ext.id)
|
||||
enabledExtensions.value.map(ext => ext.key).filter((k): k is string => k !== undefined)
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,8 @@ export const useExtensionStore = defineStore('extension', () => {
|
||||
*/
|
||||
const loadExtensions = async (): Promise<void> => {
|
||||
try {
|
||||
extensions.value = await ExtensionService.GetAllExtensions();
|
||||
const result = await ExtensionService.GetAllExtensions();
|
||||
extensions.value = result.filter((ext): ext is Extension => ext !== null);
|
||||
} catch (err) {
|
||||
console.error('[ExtensionStore] Failed to load extensions:', err);
|
||||
}
|
||||
@@ -31,8 +32,8 @@ export const useExtensionStore = defineStore('extension', () => {
|
||||
/**
|
||||
* 获取扩展配置
|
||||
*/
|
||||
const getExtensionConfig = (id: ExtensionID): any => {
|
||||
const extension = extensions.value.find(ext => ext.id === id);
|
||||
const getExtensionConfig = (key: string): any => {
|
||||
const extension = extensions.value.find(ext => ext.key === key);
|
||||
return extension?.config ?? {};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref} from 'vue';
|
||||
import {ExtensionID, KeyBinding, KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {KeyBinding} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {GetAllKeyBindings} from '@/../bindings/voidraft/internal/services/keybindingservice';
|
||||
|
||||
export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
@@ -14,13 +14,14 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
|
||||
// 按扩展分组的快捷键
|
||||
const keyBindingsByExtension = computed(() => {
|
||||
const groups = new Map<ExtensionID, KeyBinding[]>();
|
||||
const groups = new Map<string, KeyBinding[]>();
|
||||
|
||||
for (const binding of keyBindings.value) {
|
||||
if (!groups.has(binding.extension)) {
|
||||
groups.set(binding.extension, []);
|
||||
const ext = binding.extension || '';
|
||||
if (!groups.has(ext)) {
|
||||
groups.set(ext, []);
|
||||
}
|
||||
groups.get(binding.extension)!.push(binding);
|
||||
groups.get(ext)!.push(binding);
|
||||
}
|
||||
|
||||
return groups;
|
||||
@@ -28,13 +29,13 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
|
||||
// 获取指定扩展的快捷键
|
||||
const getKeyBindingsByExtension = computed(() =>
|
||||
(extension: ExtensionID) =>
|
||||
(extension: string) =>
|
||||
keyBindings.value.filter(kb => kb.extension === extension)
|
||||
);
|
||||
|
||||
// 按命令获取快捷键
|
||||
const getKeyBindingByCommand = computed(() =>
|
||||
(command: KeyBindingCommand) =>
|
||||
(command: string) =>
|
||||
keyBindings.value.find(kb => kb.command === command)
|
||||
);
|
||||
|
||||
@@ -42,13 +43,14 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
* 从后端加载快捷键配置
|
||||
*/
|
||||
const loadKeyBindings = async (): Promise<void> => {
|
||||
keyBindings.value = await GetAllKeyBindings();
|
||||
const result = await GetAllKeyBindings();
|
||||
keyBindings.value = result.filter((kb): kb is KeyBinding => kb !== null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否存在指定命令的快捷键
|
||||
*/
|
||||
const hasCommand = (command: KeyBindingCommand): boolean => {
|
||||
const hasCommand = (command: string): boolean => {
|
||||
return keyBindings.value.some(kb => kb.command === command && kb.enabled);
|
||||
};
|
||||
|
||||
@@ -57,9 +59,11 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
* 获取扩展相关的所有扩展ID
|
||||
*/
|
||||
const getAllExtensionIds = computed(() => {
|
||||
const extensionIds = new Set<ExtensionID>();
|
||||
const extensionIds = new Set<string>();
|
||||
for (const binding of keyBindings.value) {
|
||||
extensionIds.add(binding.extension);
|
||||
if (binding.extension) {
|
||||
extensionIds.add(binding.extension);
|
||||
}
|
||||
}
|
||||
return Array.from(extensionIds);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useSystemStore = defineStore('system', () => {
|
||||
});
|
||||
|
||||
// 初始化系统信息
|
||||
const initializeSystemInfo = async (): Promise<void> => {
|
||||
const initSystemInfo = async (): Promise<void> => {
|
||||
if (isLoading.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
@@ -102,7 +102,7 @@ export const useSystemStore = defineStore('system', () => {
|
||||
titleBarHeight,
|
||||
|
||||
// 方法
|
||||
initializeSystemInfo,
|
||||
initSystemInfo,
|
||||
setWindowOnTop,
|
||||
toggleWindowOnTop,
|
||||
resetWindowOnTop,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {defineStore} from 'pinia';
|
||||
import {computed, readonly, ref} from 'vue';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useDocumentStore} from './documentStore';
|
||||
import type {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
|
||||
export interface Tab {
|
||||
documentId: number; // 直接使用文档ID作为唯一标识
|
||||
@@ -55,6 +55,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
*/
|
||||
const addOrActivateTab = (document: Document) => {
|
||||
const documentId = document.id;
|
||||
if (documentId === undefined) return;
|
||||
|
||||
if (hasTab(documentId)) {
|
||||
// 标签页已存在,无需重复添加
|
||||
@@ -64,7 +65,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
// 创建新标签页
|
||||
const newTab: Tab = {
|
||||
documentId,
|
||||
title: document.title
|
||||
title: document.title || ''
|
||||
};
|
||||
|
||||
tabsMap.value[documentId] = newTab;
|
||||
|
||||
@@ -1,159 +1,161 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { ThemeService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfigStore } from './configStore';
|
||||
import { useEditorStore } from './editorStore';
|
||||
import type { ThemeColors } from '@/views/editor/theme/types';
|
||||
import { cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap } from '@/views/editor/theme/presets';
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref} from 'vue';
|
||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models';
|
||||
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useEditorStore} from './editorStore';
|
||||
import type {ThemeColors} from '@/views/editor/theme/types';
|
||||
import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets';
|
||||
|
||||
type ThemeColorConfig = { [_: string]: any };
|
||||
type ThemeOption = { name: string; type: ThemeType };
|
||||
|
||||
const resolveThemeName = (name?: string) =>
|
||||
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
|
||||
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
|
||||
|
||||
const createThemeOptions = (type: ThemeType): ThemeOption[] =>
|
||||
themePresetList
|
||||
.filter(preset => preset.type === type)
|
||||
.map(preset => ({ name: preset.name, type: preset.type }));
|
||||
themePresetList
|
||||
.filter(preset => preset.type === type)
|
||||
.map(preset => ({name: preset.name, type: preset.type}));
|
||||
|
||||
const darkThemeOptions = createThemeOptions(ThemeType.ThemeTypeDark);
|
||||
const lightThemeOptions = createThemeOptions(ThemeType.ThemeTypeLight);
|
||||
const darkThemeOptions = createThemeOptions(ThemeType.TypeDark);
|
||||
const lightThemeOptions = createThemeOptions(ThemeType.TypeLight);
|
||||
|
||||
const cloneColors = (colors: ThemeColorConfig): ThemeColors =>
|
||||
JSON.parse(JSON.stringify(colors)) as ThemeColors;
|
||||
JSON.parse(JSON.stringify(colors)) as ThemeColors;
|
||||
|
||||
const getPresetColors = (name: string): ThemeColors => {
|
||||
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
|
||||
const colors = cloneThemeColors(preset.colors);
|
||||
colors.themeName = name;
|
||||
return colors;
|
||||
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
|
||||
const colors = cloneThemeColors(preset.colors);
|
||||
colors.themeName = name;
|
||||
return colors;
|
||||
};
|
||||
|
||||
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
|
||||
const safeName = resolveThemeName(themeName);
|
||||
try {
|
||||
const theme = await ThemeService.GetThemeByName(safeName);
|
||||
if (theme?.colors) {
|
||||
const colors = cloneColors(theme.colors);
|
||||
colors.themeName = safeName;
|
||||
return colors;
|
||||
const safeName = resolveThemeName(themeName);
|
||||
try {
|
||||
const theme = await ThemeService.GetThemeByKey(safeName);
|
||||
if (theme?.colors) {
|
||||
const colors = cloneColors(theme.colors);
|
||||
colors.themeName = safeName;
|
||||
return colors;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load theme override:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load theme override:', error);
|
||||
}
|
||||
return getPresetColors(safeName);
|
||||
return getPresetColors(safeName);
|
||||
};
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const configStore = useConfigStore();
|
||||
const currentColors = ref<ThemeColors | null>(null);
|
||||
const configStore = useConfigStore();
|
||||
const currentColors = ref<ThemeColors | null>(null);
|
||||
|
||||
const currentTheme = computed(
|
||||
() => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
|
||||
const isDarkMode = computed(
|
||||
() =>
|
||||
currentTheme.value === SystemThemeType.SystemThemeDark ||
|
||||
(currentTheme.value === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
);
|
||||
|
||||
const availableThemes = computed<ThemeOption[]>(() =>
|
||||
isDarkMode.value ? darkThemeOptions : lightThemeOptions
|
||||
);
|
||||
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
const themeMap = {
|
||||
[SystemThemeType.SystemThemeAuto]: 'auto',
|
||||
[SystemThemeType.SystemThemeDark]: 'dark',
|
||||
[SystemThemeType.SystemThemeLight]: 'light',
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', themeMap[theme]);
|
||||
};
|
||||
|
||||
const loadThemeColors = async (themeName?: string) => {
|
||||
const targetName = resolveThemeName(
|
||||
themeName || configStore.config?.appearance?.currentTheme
|
||||
const currentTheme = computed(
|
||||
() => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
currentColors.value = await fetchThemeColors(targetName);
|
||||
};
|
||||
|
||||
const initializeTheme = async () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
await loadThemeColors();
|
||||
};
|
||||
const isDarkMode = computed(
|
||||
() =>
|
||||
currentTheme.value === SystemThemeType.SystemThemeDark ||
|
||||
(currentTheme.value === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
);
|
||||
|
||||
const setTheme = async (theme: SystemThemeType) => {
|
||||
await configStore.setSystemTheme(theme);
|
||||
applyThemeToDOM(theme);
|
||||
refreshEditorTheme();
|
||||
};
|
||||
const availableThemes = computed<ThemeOption[]>(() =>
|
||||
isDarkMode.value ? darkThemeOptions : lightThemeOptions
|
||||
);
|
||||
|
||||
const switchToTheme = async (themeName: string) => {
|
||||
if (!themePresetMap[themeName]) {
|
||||
console.error('Theme not found:', themeName);
|
||||
return false;
|
||||
}
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
const themeMap = {
|
||||
[SystemThemeType.SystemThemeAuto]: 'auto',
|
||||
[SystemThemeType.SystemThemeDark]: 'dark',
|
||||
[SystemThemeType.SystemThemeLight]: 'light',
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', themeMap[theme]);
|
||||
};
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
await configStore.setCurrentTheme(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
const loadThemeColors = async (themeName?: string) => {
|
||||
const targetName = resolveThemeName(
|
||||
themeName || configStore.config?.appearance?.currentTheme
|
||||
);
|
||||
currentColors.value = await fetchThemeColors(targetName);
|
||||
};
|
||||
|
||||
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
|
||||
if (!currentColors.value) return;
|
||||
Object.assign(currentColors.value, colors);
|
||||
};
|
||||
const initTheme = async () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
await loadThemeColors();
|
||||
};
|
||||
|
||||
const saveCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
}
|
||||
const setTheme = async (theme: SystemThemeType) => {
|
||||
await configStore.setSystemTheme(theme);
|
||||
applyThemeToDOM(theme);
|
||||
refreshEditorTheme();
|
||||
};
|
||||
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
currentColors.value.themeName = themeName;
|
||||
const switchToTheme = async (themeName: string) => {
|
||||
if (!themePresetMap[themeName]) {
|
||||
console.error('Theme not found:', themeName);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ThemeService.UpdateTheme(themeName, currentColors.value as unknown as ThemeColorConfig);
|
||||
await loadThemeColors(themeName);
|
||||
await configStore.setCurrentTheme(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
|
||||
if (!currentColors.value) return;
|
||||
Object.assign(currentColors.value, colors);
|
||||
};
|
||||
|
||||
const resetCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
}
|
||||
const saveCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
}
|
||||
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
await ThemeService.ResetTheme(themeName);
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
currentColors.value.themeName = themeName;
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
await ThemeService.UpdateTheme(themeName, currentColors.value as ThemeColorConfig);
|
||||
|
||||
const refreshEditorTheme = () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
const editorStore = useEditorStore();
|
||||
editorStore?.applyThemeSettings();
|
||||
};
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
currentTheme,
|
||||
currentColors,
|
||||
isDarkMode,
|
||||
setTheme,
|
||||
switchToTheme,
|
||||
initializeTheme,
|
||||
updateCurrentColors,
|
||||
saveCurrentTheme,
|
||||
resetCurrentTheme,
|
||||
refreshEditorTheme,
|
||||
applyThemeToDOM,
|
||||
};
|
||||
const resetCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
}
|
||||
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
await ThemeService.ResetTheme(themeName);
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
|
||||
const refreshEditorTheme = () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
const editorStore = useEditorStore();
|
||||
editorStore?.applyThemeSettings();
|
||||
};
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
currentTheme,
|
||||
currentColors,
|
||||
isDarkMode,
|
||||
setTheme,
|
||||
switchToTheme,
|
||||
initTheme,
|
||||
updateCurrentColors,
|
||||
saveCurrentTheme,
|
||||
resetCurrentTheme,
|
||||
refreshEditorTheme,
|
||||
applyThemeToDOM,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user