feat(editor): add markdown presets, heading folding and external file refresh
This commit is contained in:
@@ -3,10 +3,13 @@ import { ref, computed } from 'vue'
|
||||
import type { SaveStatus } from '@/contracts'
|
||||
import * as workspaceService from '@/services/workspaceService'
|
||||
import { t } from '@/i18n'
|
||||
import { ApiErrorClass } from '@/services/apiClient'
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
const mode = ref<'wysiwyg' | 'source'>('wysiwyg')
|
||||
const content = ref('')
|
||||
const contentRevision = ref(0)
|
||||
let diskContent: string | undefined
|
||||
const saveStatus = ref<SaveStatus>('idle')
|
||||
const lastSavedAt = ref<string | null>(null)
|
||||
const currentNoteId = ref<string | null>(null)
|
||||
@@ -35,13 +38,14 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
function updateContent(newContent: string) {
|
||||
content.value = newContent
|
||||
saveStatus.value = 'dirty'
|
||||
if (saveStatus.value !== 'conflict' && saveStatus.value !== 'external_changed') saveStatus.value = 'dirty'
|
||||
}
|
||||
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pendingSave: Promise<void> | null = null
|
||||
|
||||
function scheduleAutoSave(delay = 1500) {
|
||||
if (saveStatus.value === 'conflict' || saveStatus.value === 'external_changed') return
|
||||
if (saveTimer) clearTimeout(saveTimer)
|
||||
saveTimer = setTimeout(() => {
|
||||
saveTimer = null
|
||||
@@ -51,20 +55,23 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
async function save() {
|
||||
if (!currentFilePath.value) return
|
||||
if (saveStatus.value === 'conflict' || saveStatus.value === 'external_changed') return
|
||||
if (pendingSave) return pendingSave
|
||||
// 保存路径与正文都取快照;请求完成时用户可能已继续输入或切换文件。
|
||||
const targetPath = currentFilePath.value
|
||||
const snapshot = content.value
|
||||
const baseline = diskContent
|
||||
saveStatus.value = 'saving'
|
||||
pendingSave = (async () => {
|
||||
try {
|
||||
await workspaceService.saveFileContent(targetPath, snapshot)
|
||||
await workspaceService.saveFileContent(targetPath, snapshot, baseline)
|
||||
if (currentFilePath.value === targetPath) {
|
||||
diskContent = snapshot
|
||||
saveStatus.value = content.value === snapshot ? 'saved' : 'dirty'
|
||||
lastSavedAt.value = new Date().toISOString()
|
||||
}
|
||||
} catch {
|
||||
if (currentFilePath.value === targetPath) saveStatus.value = 'save_failed'
|
||||
} catch (error) {
|
||||
if (currentFilePath.value === targetPath) saveStatus.value = error instanceof ApiErrorClass && error.code === 'NOTE_CONTENT_CONFLICT' ? 'conflict' : 'save_failed'
|
||||
} finally {
|
||||
pendingSave = null
|
||||
if (currentFilePath.value === targetPath && saveStatus.value === 'dirty') scheduleAutoSave()
|
||||
@@ -86,7 +93,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
if (pendingSave) await pendingSave
|
||||
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') await save()
|
||||
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') {
|
||||
if (['dirty', 'save_failed', 'conflict', 'external_changed'].includes(saveStatus.value)) {
|
||||
throw new Error(t('当前文件保存失败,已阻止切换以避免内容丢失。', 'The current file could not be saved. Switching was blocked to prevent data loss.'))
|
||||
}
|
||||
// 版本号使较慢的旧读取不能覆盖用户后选择的新文件。
|
||||
@@ -102,6 +109,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
currentFilePath.value = filePath
|
||||
currentNoteId.value = loadedNoteId
|
||||
content.value = loadedContent
|
||||
diskContent = loadedContent
|
||||
saveStatus.value = 'saved'
|
||||
lastSavedAt.value = new Date().toISOString()
|
||||
} catch (error) {
|
||||
@@ -122,13 +130,37 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
|
||||
function setExternalChanged() {
|
||||
if (saveStatus.value === 'dirty') {
|
||||
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null }
|
||||
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed' || saveStatus.value === 'conflict') {
|
||||
saveStatus.value = 'conflict'
|
||||
} else {
|
||||
saveStatus.value = 'external_changed'
|
||||
}
|
||||
}
|
||||
|
||||
async function checkExternalFile() {
|
||||
if (!currentFilePath.value || pendingSave || diskContent === undefined || saveStatus.value === 'conflict') return
|
||||
const path = currentFilePath.value, baseline = diskContent
|
||||
try {
|
||||
const latest = await workspaceService.readFileContent(path)
|
||||
if (path !== currentFilePath.value || pendingSave || diskContent !== baseline || latest === baseline) return
|
||||
if (content.value === baseline && saveStatus.value === 'saved') {
|
||||
content.value = latest; diskContent = latest; contentRevision.value++
|
||||
} else {
|
||||
setExternalChanged(); saveStatus.value = 'conflict'
|
||||
}
|
||||
} catch { /* Tree polling reports missing files; transient network errors retain edits. */ }
|
||||
}
|
||||
|
||||
async function reloadExternalFile() {
|
||||
const path = currentFilePath.value, snapshot = content.value
|
||||
if (!path || pendingSave) return
|
||||
const latest = await workspaceService.readFileContent(path)
|
||||
if (path !== currentFilePath.value || content.value !== snapshot || pendingSave) return
|
||||
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null }
|
||||
content.value = latest; diskContent = latest; saveStatus.value = 'saved'; contentRevision.value++
|
||||
}
|
||||
|
||||
// TODO(editor): 桌面文件监听接入后提供冲突对比/合并界面,而非只阻止切换。
|
||||
|
||||
function closeFile() {
|
||||
@@ -137,6 +169,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
currentFilePath.value = null
|
||||
currentNoteId.value = null
|
||||
content.value = ''
|
||||
diskContent = undefined
|
||||
saveStatus.value = 'idle'
|
||||
lastSavedAt.value = null
|
||||
highlightBlockId.value = null
|
||||
@@ -153,6 +186,9 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
jumpToHeading,
|
||||
mode,
|
||||
content,
|
||||
contentRevision,
|
||||
checkExternalFile,
|
||||
reloadExternalFile,
|
||||
saveStatus,
|
||||
lastSavedAt,
|
||||
currentNoteId,
|
||||
|
||||
Reference in New Issue
Block a user