✨ Use SQLite instead of JSON storage
This commit is contained in:
@@ -4,103 +4,113 @@ import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
export const useDocumentStore = defineStore('document', () => {
|
||||
// 状态
|
||||
const activeDocument = ref<Document | null>(null);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const lastSaved = ref<Date | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const documentContent = computed(() => activeDocument.value?.content ?? '');
|
||||
const documentTitle = computed(() => activeDocument.value?.meta?.title ?? '');
|
||||
const hasActiveDocument = computed(() => !!activeDocument.value);
|
||||
const isSaveInProgress = computed(() => isSaving.value);
|
||||
const lastSavedTime = computed(() => lastSaved.value);
|
||||
|
||||
// 加载文档
|
||||
const loadDocument = async (): Promise<Document | null> => {
|
||||
if (isLoading.value) return null;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const doc = await DocumentService.GetActiveDocument();
|
||||
activeDocument.value = doc;
|
||||
return doc;
|
||||
} catch (error) {
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存文档
|
||||
const saveDocument = async (content: string): Promise<boolean> => {
|
||||
if (isSaving.value) return false;
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await DocumentService.UpdateActiveDocumentContent(content);
|
||||
lastSaved.value = new Date();
|
||||
|
||||
// 更新本地副本
|
||||
if (activeDocument.value) {
|
||||
activeDocument.value.content = content;
|
||||
activeDocument.value.meta.lastUpdated = lastSaved.value;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 强制保存文档到磁盘
|
||||
const forceSaveDocument = async (): Promise<boolean> => {
|
||||
if (isSaving.value) return false;
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await DocumentService.ForceSave();
|
||||
lastSaved.value = new Date();
|
||||
|
||||
// 更新时间戳
|
||||
if (activeDocument.value) {
|
||||
activeDocument.value.meta.lastUpdated = lastSaved.value;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
const initialize = async (): Promise<void> => {
|
||||
await loadDocument();
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
activeDocument,
|
||||
isLoading,
|
||||
isSaving,
|
||||
lastSaved,
|
||||
|
||||
const currentDocument = ref<Document | null>(null);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const lastSaved = ref<Date | null>(null);
|
||||
|
||||
// 计算属性
|
||||
documentContent,
|
||||
documentTitle,
|
||||
hasActiveDocument,
|
||||
isSaveInProgress,
|
||||
lastSavedTime,
|
||||
|
||||
// 方法
|
||||
loadDocument,
|
||||
saveDocument,
|
||||
forceSaveDocument,
|
||||
initialize
|
||||
};
|
||||
const documentContent = computed(() => currentDocument.value?.content ?? '');
|
||||
const documentTitle = computed(() => currentDocument.value?.title ?? '');
|
||||
const hasDocument = computed(() => !!currentDocument.value);
|
||||
const isSaveInProgress = computed(() => isSaving.value);
|
||||
const lastSavedTime = computed(() => lastSaved.value);
|
||||
|
||||
// 加载文档
|
||||
const loadDocument = async (documentId = 1): Promise<Document | null> => {
|
||||
if (isLoading.value) return currentDocument.value;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const doc = await DocumentService.GetDocumentByID(documentId);
|
||||
if (doc) {
|
||||
currentDocument.value = doc;
|
||||
return doc;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存文档内容
|
||||
const saveDocumentContent = async (content: string): Promise<boolean> => {
|
||||
// 如果内容没有变化,直接返回成功
|
||||
if (currentDocument.value?.content === content) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果正在保存中,直接返回
|
||||
if (isSaving.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const documentId = currentDocument.value?.id || 1;
|
||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||
|
||||
const now = new Date();
|
||||
lastSaved.value = now;
|
||||
|
||||
// 更新本地副本
|
||||
if (currentDocument.value) {
|
||||
currentDocument.value.content = content;
|
||||
currentDocument.value.updatedAt = now;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存文档标题
|
||||
const saveDocumentTitle = async (title: string): Promise<boolean> => {
|
||||
if (!currentDocument.value || currentDocument.value.title === title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await DocumentService.UpdateDocumentTitle(currentDocument.value.id, title);
|
||||
const now = new Date();
|
||||
lastSaved.value = now;
|
||||
currentDocument.value.title = title;
|
||||
currentDocument.value.updatedAt = now;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
const initialize = async (): Promise<void> => {
|
||||
await loadDocument();
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentDocument,
|
||||
isLoading,
|
||||
isSaving,
|
||||
lastSaved,
|
||||
|
||||
// 计算属性
|
||||
documentContent,
|
||||
documentTitle,
|
||||
hasDocument,
|
||||
isSaveInProgress,
|
||||
lastSavedTime,
|
||||
|
||||
// 方法
|
||||
loadDocument,
|
||||
saveDocumentContent,
|
||||
saveDocumentTitle,
|
||||
initialize
|
||||
};
|
||||
});
|
@@ -5,16 +5,15 @@ import {EditorState, Extension} from '@codemirror/state';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useDocumentStore} from './documentStore';
|
||||
import {useThemeStore} from './themeStore';
|
||||
import {useKeybindingStore} from './keybindingStore';
|
||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||
import {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';
|
||||
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||
import {createAutoSavePlugin, createSaveShortcutPlugin} from '@/views/editor/basic/autoSaveExtension';
|
||||
import {createAutoSavePlugin} from '@/views/editor/basic/autoSaveExtension';
|
||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager';
|
||||
import {useExtensionStore} from './extensionStore';
|
||||
@@ -128,21 +127,10 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
updateDocumentStats
|
||||
);
|
||||
|
||||
// 创建保存快捷键插件
|
||||
const saveShortcutExtension = createSaveShortcutPlugin(() => {
|
||||
if (editorView.value) {
|
||||
handleManualSave();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建自动保存插件
|
||||
const autoSaveExtension = createAutoSavePlugin({
|
||||
debounceDelay: 300, // 300毫秒的输入防抖
|
||||
onSave: (success) => {
|
||||
if (success) {
|
||||
documentStore.lastSaved = new Date();
|
||||
}
|
||||
}
|
||||
debounceDelay: configStore.config.editing.autoSaveDelay
|
||||
});
|
||||
// 代码块功能
|
||||
const codeBlockExtension = createCodeBlockExtension({
|
||||
@@ -164,7 +152,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
...tabExtensions,
|
||||
fontExtension,
|
||||
statsExtension,
|
||||
saveShortcutExtension,
|
||||
autoSaveExtension,
|
||||
codeBlockExtension,
|
||||
...dynamicExtensions
|
||||
@@ -220,19 +207,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 手动保存文档
|
||||
const handleManualSave = async () => {
|
||||
if (!editorView.value) return;
|
||||
|
||||
const view = editorView.value as EditorView;
|
||||
const content = view.state.doc.toString();
|
||||
|
||||
// 先更新内容
|
||||
await DocumentService.UpdateActiveDocumentContent(content);
|
||||
// 然后调用强制保存方法
|
||||
await documentStore.forceSaveDocument();
|
||||
};
|
||||
|
||||
// 销毁编辑器
|
||||
const destroyEditor = () => {
|
||||
if (editorView.value) {
|
||||
@@ -283,7 +257,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
// 如果需要更新配置
|
||||
await ExtensionService.UpdateExtensionState(id, enabled, config)
|
||||
}
|
||||
|
||||
|
||||
// 更新前端编辑器扩展
|
||||
const manager = getExtensionManager()
|
||||
if (manager) {
|
||||
@@ -292,7 +266,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 重新加载扩展配置
|
||||
await extensionStore.loadExtensions()
|
||||
|
||||
|
||||
// 更新快捷键映射
|
||||
if (editorView.value) {
|
||||
updateKeymapExtension(editorView.value)
|
||||
@@ -321,7 +295,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
createEditor,
|
||||
reconfigureTabSettings,
|
||||
reconfigureFontSettings,
|
||||
handleManualSave,
|
||||
destroyEditor,
|
||||
updateExtension
|
||||
};
|
||||
|
@@ -1,98 +1,109 @@
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { DocumentService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
|
||||
// 定义自动保存配置选项
|
||||
// 自动保存配置选项
|
||||
export interface AutoSaveOptions {
|
||||
// 保存回调
|
||||
onSave?: (success: boolean) => void;
|
||||
// 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端
|
||||
// 防抖延迟(毫秒)
|
||||
debounceDelay?: number;
|
||||
// 保存状态回调
|
||||
onSaveStart?: () => void;
|
||||
onSaveSuccess?: () => void;
|
||||
onSaveError?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单防抖函数
|
||||
*/
|
||||
function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number
|
||||
): T & { cancel: () => void } {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
const debounced = ((...args: Parameters<T>) => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
timeoutId = null;
|
||||
func(...args);
|
||||
}, delay);
|
||||
}) as T & { cancel: () => void };
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自动保存插件
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @returns EditorView.Plugin
|
||||
*/
|
||||
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
|
||||
const {
|
||||
onSave = () => {},
|
||||
debounceDelay = 2000
|
||||
const {
|
||||
debounceDelay = 2000,
|
||||
onSaveStart = () => {},
|
||||
onSaveSuccess = () => {},
|
||||
onSaveError = () => {}
|
||||
} = options;
|
||||
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
private isActive: boolean = true;
|
||||
private isSaving: boolean = false;
|
||||
private readonly contentUpdateFn: (view: EditorView) => void;
|
||||
class AutoSavePlugin {
|
||||
private documentStore = useDocumentStore();
|
||||
private debouncedSave: ((content: string) => void) & { cancel: () => void };
|
||||
private isDestroyed = false;
|
||||
private lastContent = '';
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
// 创建内容更新函数,简单传递内容给后端
|
||||
this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay);
|
||||
this.lastContent = view.state.doc.toString();
|
||||
this.debouncedSave = debounce(
|
||||
(content: string) => this.performSave(content),
|
||||
debounceDelay
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建防抖的内容更新函数
|
||||
*/
|
||||
private createDebouncedUpdateFn(delay: number): (view: EditorView) => void {
|
||||
// 使用VueUse的防抖函数创建一个新函数
|
||||
return useDebounceFn(async (view: EditorView) => {
|
||||
// 如果插件已不活跃或正在保存中,不发送
|
||||
if (!this.isActive || this.isSaving) return;
|
||||
private async performSave(content: string): Promise<void> {
|
||||
if (this.isDestroyed) return;
|
||||
|
||||
try {
|
||||
onSaveStart();
|
||||
const success = await this.documentStore.saveDocumentContent(content);
|
||||
|
||||
this.isSaving = true;
|
||||
const content = view.state.doc.toString();
|
||||
|
||||
try {
|
||||
// 简单将内容传递给后端,让后端处理保存策略
|
||||
await DocumentService.UpdateActiveDocumentContent(content);
|
||||
onSave(true);
|
||||
} catch (err) {
|
||||
// 静默处理错误,不在控制台打印
|
||||
onSave(false);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
if (success) {
|
||||
this.lastContent = content;
|
||||
onSaveSuccess();
|
||||
} else {
|
||||
onSaveError();
|
||||
}
|
||||
}, delay);
|
||||
} catch (error) {
|
||||
onSaveError();
|
||||
}
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
// 如果内容没有变化,直接返回
|
||||
if (!update.docChanged) return;
|
||||
if (!update.docChanged || this.isDestroyed) return;
|
||||
|
||||
// 调用防抖函数
|
||||
this.contentUpdateFn(this.view);
|
||||
const newContent = this.view.state.doc.toString();
|
||||
if (newContent === this.lastContent) return;
|
||||
|
||||
this.debouncedSave(newContent);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 标记插件不再活跃
|
||||
this.isActive = false;
|
||||
this.isDestroyed = true;
|
||||
this.debouncedSave.cancel();
|
||||
|
||||
// 静默发送最终内容,忽略错误
|
||||
const content = this.view.state.doc.toString();
|
||||
DocumentService.UpdateActiveDocumentContent(content).then();
|
||||
// 如果内容有变化,立即保存
|
||||
const currentContent = this.view.state.doc.toString();
|
||||
if (currentContent !== this.lastContent) {
|
||||
this.documentStore.saveDocumentContent(currentContent).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建处理保存快捷键的插件
|
||||
* @param onSave 保存回调
|
||||
* @returns EditorView.Plugin
|
||||
*/
|
||||
export function createSaveShortcutPlugin(onSave: () => void) {
|
||||
return EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
// Ctrl+S / Cmd+S
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
onSave();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user