Complete the document saving service

This commit is contained in:
2025-05-17 15:50:34 +08:00
parent bd0bbc9674
commit 1246166231
16 changed files with 1781 additions and 30 deletions

View File

@@ -4,30 +4,46 @@ import {EditorState, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {useEditorStore} from '@/stores/editorStore';
import {useConfigStore} from '@/stores/configStore';
import {useDocumentStore} from '@/stores/documentStore';
import {useLogStore} from '@/stores/logStore';
import {createBasicSetup} from './extensions/basicSetup';
import {
createStatsUpdateExtension,
createWheelZoomHandler,
getTabExtensions,
updateStats,
updateTabConfig
updateTabConfig,
createAutoSavePlugin,
createSaveShortcutPlugin,
} from './extensions';
import { useI18n } from 'vue-i18n';
import { DocumentService } from '@/../bindings/voidraft/internal/services';
const editorStore = useEditorStore();
const configStore = useConfigStore();
const documentStore = useDocumentStore();
const logStore = useLogStore();
const { t } = useI18n();
const props = defineProps({
initialDoc: {
type: String,
default: '// 在此处编写文本...'
default: ''
}
});
const editorElement = ref<HTMLElement | null>(null);
const editorCreated = ref(false);
let isDestroying = false;
// 创建编辑器
const createEditor = () => {
if (!editorElement.value) return;
const createEditor = async () => {
if (!editorElement.value || editorCreated.value) return;
editorCreated.value = true;
// 加载文档内容
await documentStore.initialize();
const docContent = documentStore.documentContent || props.initialDoc;
// 获取基本扩展
const basicExtensions = createBasicSetup();
@@ -44,16 +60,35 @@ const createEditor = () => {
editorStore.updateDocumentStats
);
// 创建保存快捷键插件
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
if (editorStore.editorView) {
handleManualSave();
}
});
// 创建自动保存插件
const autoSavePlugin = createAutoSavePlugin({
debounceDelay: 300, // 300毫秒的输入防抖
onSave: (success) => {
if (success) {
documentStore.lastSaved = new Date();
}
}
});
// 组合所有扩展
const extensions: Extension[] = [
...basicExtensions,
...tabExtensions,
statsExtension
statsExtension,
saveShortcutPlugin,
autoSavePlugin
];
// 创建编辑器状态
const state = EditorState.create({
doc: props.initialDoc,
doc: docContent,
extensions
});
@@ -71,6 +106,7 @@ const createEditor = () => {
// 立即更新统计信息,不等待用户交互
updateStats(view, editorStore.updateDocumentStats);
};
// 创建滚轮事件处理器
@@ -79,6 +115,20 @@ const handleWheel = createWheelZoomHandler(
configStore.decreaseFontSize
);
// 手动保存文档
const handleManualSave = async () => {
if (!editorStore.editorView || isDestroying) return;
const view = editorStore.editorView as EditorView;
const content = view.state.doc.toString();
// 使用文档存储的强制保存方法
const success = await documentStore.forceSaveDocument(content);
if (success) {
logStore.info(t('document.manualSaveSuccess'));
}
};
// 重新配置编辑器(仅在必要时)
const reconfigureTabSettings = () => {
if (!editorStore.editorView) return;
@@ -118,12 +168,14 @@ onMounted(() => {
});
onBeforeUnmount(() => {
isDestroying = true;
// 移除滚轮事件监听
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', handleWheel);
}
// 销毁编辑器
// 直接销毁编辑器
if (editorStore.editorView) {
editorStore.editorView.destroy();
editorStore.setEditorView(null);

View File

@@ -0,0 +1,117 @@
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { DocumentService } from '@/../bindings/voidraft/internal/services';
import { useDebounceFn } from '@vueuse/core';
// 定义自动保存配置选项
export interface AutoSaveOptions {
// 保存回调
onSave?: (success: boolean) => void;
// 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端
debounceDelay?: number;
}
/**
* 创建自动保存插件
* @param options 配置选项
* @returns EditorView.Plugin
*/
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
const {
onSave = () => {},
debounceDelay = 1000 // 默认1000ms延迟原为300ms
} = options;
return ViewPlugin.fromClass(
class {
private isActive: boolean = true;
private isSaving: boolean = false;
private contentUpdateFn: (view: EditorView) => void;
constructor(private view: EditorView) {
// 创建内容更新函数
this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay);
}
/**
* 创建防抖的内容更新函数
*/
private createDebouncedUpdateFn(delay: number): (view: EditorView) => void {
// 使用VueUse的防抖函数创建一个新函数
return useDebounceFn(async (view: EditorView) => {
// 如果插件已不活跃或正在保存中,不发送
if (!this.isActive || this.isSaving) return;
this.isSaving = true;
const content = view.state.doc.toString();
try {
await DocumentService.UpdateActiveDocumentContent(content);
onSave(true);
} catch (err) {
console.error('Failed to update document content:', err);
onSave(false);
} finally {
this.isSaving = false;
}
}, delay);
}
update(update: ViewUpdate) {
// 如果内容没有变化,直接返回
if (!update.docChanged) return;
// 调用防抖函数
this.contentUpdateFn(this.view);
}
destroy() {
// 标记插件不再活跃
this.isActive = false;
// 直接发送最终内容
const content = this.view.state.doc.toString();
DocumentService.UpdateActiveDocumentContent(content)
.then(() => console.log('Successfully sent final content on destroy'))
.catch(err => console.error('Failed to send content on destroy:', err));
}
}
);
}
/**
* 创建处理保存快捷键的插件
* @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;
}
});
}
/**
* 手动触发文档保存
* @param view 编辑器视图
* @returns Promise<boolean>
*/
export async function saveDocument(view: EditorView): Promise<boolean> {
try {
const content = view.state.doc.toString();
// 更新内容
await DocumentService.UpdateActiveDocumentContent(content);
// 强制保存到磁盘
await DocumentService.ForceSave();
return true;
} catch (err) {
console.error('Failed to save document:', err);
return false;
}
}

View File

@@ -1,4 +1,5 @@
// 统一导出所有扩展
export * from './tabExtension';
export * from './wheelZoomExtension';
export * from './statsExtension';
export * from './statsExtension';
export * from './autoSaveExtension';

View File

@@ -31,5 +31,17 @@ export default {
languages: {
'zh-CN': '简体中文',
'en-US': 'English'
},
document: {
loadSuccess: 'Document loaded successfully',
loadFailed: 'Failed to load document',
saveSuccess: 'Document saved successfully',
saveFailed: 'Failed to save document',
manualSaveSuccess: 'Manually saved successfully',
settings: {
loadFailed: 'Failed to load save settings',
saveSuccess: 'Save settings updated',
saveFailed: 'Failed to update save settings'
}
}
};

View File

@@ -31,5 +31,17 @@ export default {
languages: {
'zh-CN': '简体中文',
'en-US': 'English'
},
document: {
loadSuccess: '文档加载成功',
loadFailed: '文档加载失败',
saveSuccess: '文档保存成功',
saveFailed: '文档保存失败',
manualSaveSuccess: '手动保存成功',
settings: {
loadFailed: '加载保存设置失败',
saveSuccess: '保存设置已更新',
saveFailed: '保存设置更新失败'
}
}
};

View File

@@ -0,0 +1,123 @@
import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {Document} from '@/../bindings/voidraft/internal/models/models';
import {useLogStore} from './logStore';
import {useI18n} from 'vue-i18n';
export const useDocumentStore = defineStore('document', () => {
const logStore = useLogStore();
const { t } = useI18n();
// 状态
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);
// 加载文档
async function loadDocument() {
if (isLoading.value) return;
isLoading.value = true;
try {
activeDocument.value = await DocumentService.GetActiveDocument();
logStore.info(t('document.loadSuccess'));
} catch (err) {
console.error('Failed to load document:', err);
logStore.error(t('document.loadFailed'));
activeDocument.value = null;
} finally {
isLoading.value = false;
}
}
// 保存文档
async function saveDocument(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;
}
logStore.info(t('document.saveSuccess'));
return true;
} catch (err) {
console.error('Failed to save document:', err);
logStore.error(t('document.saveFailed'));
return false;
} finally {
isSaving.value = false;
}
}
// 强制保存文档到磁盘
async function forceSaveDocument(content: string): Promise<boolean> {
if (isSaving.value) return false;
isSaving.value = true;
try {
// 先更新内容
await DocumentService.UpdateActiveDocumentContent(content);
// 然后强制保存
await DocumentService.ForceSave();
lastSaved.value = new Date();
// 如果我们有活动文档,更新本地副本
if (activeDocument.value) {
activeDocument.value.content = content;
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
logStore.info(t('document.manualSaveSuccess'));
return true;
} catch (err) {
console.error('Failed to force save document:', err);
logStore.error(t('document.saveFailed'));
return false;
} finally {
isSaving.value = false;
}
}
// 初始化
async function initialize() {
await loadDocument();
}
return {
// 状态
activeDocument,
isLoading,
isSaving,
lastSaved,
// 计算属性
documentContent,
documentTitle,
hasActiveDocument,
isSaveInProgress,
lastSavedTime,
// 方法
loadDocument,
saveDocument,
forceSaveDocument,
initialize
};
});