✨ Complete the document saving service
This commit is contained in:
@@ -18,6 +18,11 @@ export class AppConfig {
|
||||
*/
|
||||
"editor": EditorConfig;
|
||||
|
||||
/**
|
||||
* 文档配置
|
||||
*/
|
||||
"document": DocumentConfig;
|
||||
|
||||
/**
|
||||
* 路径配置
|
||||
*/
|
||||
@@ -33,6 +38,9 @@ export class AppConfig {
|
||||
if (!("editor" in $$source)) {
|
||||
this["editor"] = (new EditorConfig());
|
||||
}
|
||||
if (!("document" in $$source)) {
|
||||
this["document"] = (new DocumentConfig());
|
||||
}
|
||||
if (!("paths" in $$source)) {
|
||||
this["paths"] = (new PathsConfig());
|
||||
}
|
||||
@@ -50,15 +58,19 @@ export class AppConfig {
|
||||
const $$createField0_0 = $$createType0;
|
||||
const $$createField1_0 = $$createType1;
|
||||
const $$createField2_0 = $$createType2;
|
||||
const $$createField3_0 = $$createType3;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("editor" in $$parsedSource) {
|
||||
$$parsedSource["editor"] = $$createField0_0($$parsedSource["editor"]);
|
||||
}
|
||||
if ("document" in $$parsedSource) {
|
||||
$$parsedSource["document"] = $$createField1_0($$parsedSource["document"]);
|
||||
}
|
||||
if ("paths" in $$parsedSource) {
|
||||
$$parsedSource["paths"] = $$createField1_0($$parsedSource["paths"]);
|
||||
$$parsedSource["paths"] = $$createField2_0($$parsedSource["paths"]);
|
||||
}
|
||||
if ("metadata" in $$parsedSource) {
|
||||
$$parsedSource["metadata"] = $$createField2_0($$parsedSource["metadata"]);
|
||||
$$parsedSource["metadata"] = $$createField3_0($$parsedSource["metadata"]);
|
||||
}
|
||||
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
||||
}
|
||||
@@ -99,6 +111,127 @@ export class ConfigMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Document 表示一个文档
|
||||
*/
|
||||
export class Document {
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
"meta": DocumentMeta;
|
||||
|
||||
/**
|
||||
* 文档内容
|
||||
*/
|
||||
"content": string;
|
||||
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("meta" in $$source)) {
|
||||
this["meta"] = (new DocumentMeta());
|
||||
}
|
||||
if (!("content" in $$source)) {
|
||||
this["content"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Document instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Document {
|
||||
const $$createField0_0 = $$createType4;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("meta" in $$parsedSource) {
|
||||
$$parsedSource["meta"] = $$createField0_0($$parsedSource["meta"]);
|
||||
}
|
||||
return new Document($$parsedSource as Partial<Document>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DocumentConfig 定义文档配置
|
||||
*/
|
||||
export class DocumentConfig {
|
||||
/**
|
||||
* 详细保存选项
|
||||
*/
|
||||
"saveOptions": SaveOptions;
|
||||
|
||||
/** Creates a new DocumentConfig instance. */
|
||||
constructor($$source: Partial<DocumentConfig> = {}) {
|
||||
if (!("saveOptions" in $$source)) {
|
||||
this["saveOptions"] = (new SaveOptions());
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DocumentConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): DocumentConfig {
|
||||
const $$createField0_0 = $$createType5;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("saveOptions" in $$parsedSource) {
|
||||
$$parsedSource["saveOptions"] = $$createField0_0($$parsedSource["saveOptions"]);
|
||||
}
|
||||
return new DocumentConfig($$parsedSource as Partial<DocumentConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DocumentMeta 文档元数据
|
||||
*/
|
||||
export class DocumentMeta {
|
||||
/**
|
||||
* 文档唯一标识
|
||||
*/
|
||||
"id": string;
|
||||
|
||||
/**
|
||||
* 文档标题
|
||||
*/
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* 最后更新时间
|
||||
*/
|
||||
"lastUpdated": time$0.Time;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
"createdAt": time$0.Time;
|
||||
|
||||
/** Creates a new DocumentMeta instance. */
|
||||
constructor($$source: Partial<DocumentMeta> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
if (!("lastUpdated" in $$source)) {
|
||||
this["lastUpdated"] = null;
|
||||
}
|
||||
if (!("createdAt" in $$source)) {
|
||||
this["createdAt"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DocumentMeta instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): DocumentMeta {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new DocumentMeta($$parsedSource as Partial<DocumentMeta>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EditorConfig 定义编辑器配置
|
||||
*/
|
||||
@@ -221,6 +354,49 @@ export class PathsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SaveOptions 保存选项
|
||||
*/
|
||||
export class SaveOptions {
|
||||
/**
|
||||
* 自动保存延迟(毫秒)- 内容变更后多久自动保存
|
||||
*/
|
||||
"autoSaveDelay": number;
|
||||
|
||||
/**
|
||||
* 变更字符阈值,超过此阈值立即触发保存
|
||||
*/
|
||||
"changeThreshold": number;
|
||||
|
||||
/**
|
||||
* 最小保存间隔(毫秒)- 两次保存之间的最小时间间隔,避免频繁IO
|
||||
*/
|
||||
"minSaveInterval": number;
|
||||
|
||||
/** Creates a new SaveOptions instance. */
|
||||
constructor($$source: Partial<SaveOptions> = {}) {
|
||||
if (!("autoSaveDelay" in $$source)) {
|
||||
this["autoSaveDelay"] = 0;
|
||||
}
|
||||
if (!("changeThreshold" in $$source)) {
|
||||
this["changeThreshold"] = 0;
|
||||
}
|
||||
if (!("minSaveInterval" in $$source)) {
|
||||
this["minSaveInterval"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SaveOptions instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SaveOptions {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new SaveOptions($$parsedSource as Partial<SaveOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TabType 定义了制表符类型
|
||||
*/
|
||||
@@ -243,5 +419,8 @@ export enum TabType {
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = EditorConfig.createFrom;
|
||||
const $$createType1 = PathsConfig.createFrom;
|
||||
const $$createType2 = ConfigMetadata.createFrom;
|
||||
const $$createType1 = DocumentConfig.createFrom;
|
||||
const $$createType2 = PathsConfig.createFrom;
|
||||
const $$createType3 = ConfigMetadata.createFrom;
|
||||
const $$createType4 = DocumentMeta.createFrom;
|
||||
const $$createType5 = SaveOptions.createFrom;
|
||||
|
142
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
142
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DocumentService 提供文档管理功能
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* ForceSave 强制保存当前文档
|
||||
*/
|
||||
export function ForceSave(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2767091023) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetActiveDocument 获取当前活动文档
|
||||
*/
|
||||
export function GetActiveDocument(): Promise<models$0.Document | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1785823398) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetActiveDocumentContent 获取当前活动文档内容
|
||||
*/
|
||||
export function GetActiveDocumentContent(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(922617063) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDiffInfo 获取两个文本之间的详细差异信息
|
||||
*/
|
||||
export function GetDiffInfo(oldText: string, newText: string): Promise<$models.DiffResult> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2490726526, oldText, newText) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetSaveSettings 获取文档保存设置
|
||||
*/
|
||||
export function GetSaveSettings(): Promise<models$0.DocumentConfig | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4257471801) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize 初始化文档服务
|
||||
*/
|
||||
export function Initialize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3418008221) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* LoadDefaultDocument 加载默认文档
|
||||
*/
|
||||
export function LoadDefaultDocument(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2343023569) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SaveDocumentSync 同步保存文档内容 (用于页面关闭前同步保存)
|
||||
*/
|
||||
export function SaveDocumentSync(content: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3770207288, content) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 实现应用程序关闭时的服务关闭逻辑
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(638578044) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetSaveCallback 设置保存回调函数
|
||||
*/
|
||||
export function SetSaveCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(675315211, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown 关闭文档服务,确保所有数据保存
|
||||
*/
|
||||
export function Shutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3444504909) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateActiveDocumentContent 更新当前活动文档内容
|
||||
*/
|
||||
export function UpdateActiveDocumentContent(content: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1486276638, content) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateSaveSettings 更新文档保存设置
|
||||
*/
|
||||
export function UpdateSaveSettings(docConfig: models$0.DocumentConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1245479534, docConfig) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Document.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $models.DiffResult.createFrom;
|
||||
const $$createType3 = models$0.DocumentConfig.createFrom;
|
||||
const $$createType4 = $Create.Nullable($$createType3);
|
@@ -2,6 +2,10 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as ConfigService from "./configservice.js";
|
||||
import * as DocumentService from "./documentservice.js";
|
||||
export {
|
||||
ConfigService
|
||||
ConfigService,
|
||||
DocumentService
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
||||
|
141
frontend/bindings/voidraft/internal/services/models.ts
Normal file
141
frontend/bindings/voidraft/internal/services/models.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* DiffResult 包含差异比较的结果信息
|
||||
*/
|
||||
export class DiffResult {
|
||||
/**
|
||||
* 编辑操作列表
|
||||
*/
|
||||
"Edits": Edit[];
|
||||
|
||||
/**
|
||||
* 插入的字符数
|
||||
*/
|
||||
"InsertCount": number;
|
||||
|
||||
/**
|
||||
* 删除的字符数
|
||||
*/
|
||||
"DeleteCount": number;
|
||||
|
||||
/**
|
||||
* 变更的行数
|
||||
*/
|
||||
"ChangedLines": number;
|
||||
|
||||
/**
|
||||
* 总变更字符数(插入+删除)
|
||||
*/
|
||||
"TotalChanges": number;
|
||||
|
||||
/**
|
||||
* 变更的token数(如单词、标识符等)
|
||||
*/
|
||||
"ChangedTokens": number;
|
||||
|
||||
/** Creates a new DiffResult instance. */
|
||||
constructor($$source: Partial<DiffResult> = {}) {
|
||||
if (!("Edits" in $$source)) {
|
||||
this["Edits"] = [];
|
||||
}
|
||||
if (!("InsertCount" in $$source)) {
|
||||
this["InsertCount"] = 0;
|
||||
}
|
||||
if (!("DeleteCount" in $$source)) {
|
||||
this["DeleteCount"] = 0;
|
||||
}
|
||||
if (!("ChangedLines" in $$source)) {
|
||||
this["ChangedLines"] = 0;
|
||||
}
|
||||
if (!("TotalChanges" in $$source)) {
|
||||
this["TotalChanges"] = 0;
|
||||
}
|
||||
if (!("ChangedTokens" in $$source)) {
|
||||
this["ChangedTokens"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DiffResult instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): DiffResult {
|
||||
const $$createField0_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("Edits" in $$parsedSource) {
|
||||
$$parsedSource["Edits"] = $$createField0_0($$parsedSource["Edits"]);
|
||||
}
|
||||
return new DiffResult($$parsedSource as Partial<DiffResult>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit 表示单个编辑操作
|
||||
*/
|
||||
export class Edit {
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
"Type": EditType;
|
||||
|
||||
/**
|
||||
* 操作内容
|
||||
*/
|
||||
"Content": string;
|
||||
|
||||
/** Creates a new Edit instance. */
|
||||
constructor($$source: Partial<Edit> = {}) {
|
||||
if (!("Type" in $$source)) {
|
||||
this["Type"] = (0 as EditType);
|
||||
}
|
||||
if (!("Content" in $$source)) {
|
||||
this["Content"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Edit instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Edit {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Edit($$parsedSource as Partial<Edit>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit 表示编辑操作类型
|
||||
*/
|
||||
export enum EditType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = 0,
|
||||
|
||||
/**
|
||||
* EditInsert 插入操作
|
||||
*/
|
||||
EditInsert = 0,
|
||||
|
||||
/**
|
||||
* EditDelete 删除操作
|
||||
*/
|
||||
EditDelete = 1,
|
||||
|
||||
/**
|
||||
* EditEqual 相等部分
|
||||
*/
|
||||
EditEqual = 2,
|
||||
};
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = Edit.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
@@ -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);
|
||||
|
117
frontend/src/editor/extensions/autoSaveExtension.ts
Normal file
117
frontend/src/editor/extensions/autoSaveExtension.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
// 统一导出所有扩展
|
||||
export * from './tabExtension';
|
||||
export * from './wheelZoomExtension';
|
||||
export * from './statsExtension';
|
||||
export * from './statsExtension';
|
||||
export * from './autoSaveExtension';
|
@@ -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'
|
||||
}
|
||||
}
|
||||
};
|
@@ -31,5 +31,17 @@ export default {
|
||||
languages: {
|
||||
'zh-CN': '简体中文',
|
||||
'en-US': 'English'
|
||||
},
|
||||
document: {
|
||||
loadSuccess: '文档加载成功',
|
||||
loadFailed: '文档加载失败',
|
||||
saveSuccess: '文档保存成功',
|
||||
saveFailed: '文档保存失败',
|
||||
manualSaveSuccess: '手动保存成功',
|
||||
settings: {
|
||||
loadFailed: '加载保存设置失败',
|
||||
saveSuccess: '保存设置已更新',
|
||||
saveFailed: '保存设置更新失败'
|
||||
}
|
||||
}
|
||||
};
|
123
frontend/src/stores/documentStore.ts
Normal file
123
frontend/src/stores/documentStore.ts
Normal 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
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user