Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/views/editor/components/header/header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import { useFile } from './hook'
import Contact from '@/components/contact.vue'
import ExportTotal from '@/components/exportTotal.vue'

defineProps<{
localMarkdownFileName: string
localMarkdownSyncStatus: 'idle' | 'syncing' | 'synced' | 'error'
}>()

const emit = defineEmits([
'download-dynamic',
'download-native',
'download-md',
'import-md',
'download-picture'
'download-picture',
'sync-local-markdown'
])

const { exportFile, importFile, fileName } = useFile(emit)
Expand All @@ -35,6 +41,21 @@ const { open, toggle } = useSwitch()
<Reward />
<button class="exporter server-export btn" @click="exportFile('dynamic')">导出PDF</button>
<button class="exporter local-export btn" @click="exportFile('native')">备用导出</button>
<el-tooltip
:content="
localMarkdownFileName ? `自动保存到 ${localMarkdownFileName}` : '关联本地Markdown文件'
"
>
<button class="exporter local-markdown-sync btn" @click="$emit('sync-local-markdown')">
{{
localMarkdownSyncStatus === 'syncing'
? '同步中...'
: localMarkdownFileName
? '本地MD已同步'
: '同步本地MD'
}}
</button>
</el-tooltip>
<div class="operator">
<i
class="iconfont icon-github github font-25"
Expand Down
14 changes: 13 additions & 1 deletion src/views/editor/editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,36 @@
import Header from './components/header/header.vue'
import MarkdownRender from '@/views/editor/components/preview/render.vue'
import Editor from '@/views/editor/components/editor/editorContainer.vue'
import { useResumeType, useDownLoad, useImportMD, useAvatar, useShowExport } from './hook'
import {
useResumeType,
useDownLoad,
useImportMD,
useLocalMarkdownSync,
useAvatar,
useShowExport
} from './hook'
import { startGuide } from './components/guide/guide'

const { resumeType } = useResumeType()
const { downloadDynamic, downloadNative, downloadMD } = useDownLoad(resumeType)
const { importMD } = useImportMD(resumeType.value)
const { localMarkdownFileName, localMarkdownSyncStatus, syncLocalMarkdown } =
useLocalMarkdownSync(resumeType)
const { setAvatar } = useAvatar(resumeType.value)
const { showExport } = useShowExport()
startGuide()
</script>

<template>
<Header
:local-markdown-file-name="localMarkdownFileName"
:local-markdown-sync-status="localMarkdownSyncStatus"
@download-dynamic="(filename: string) => downloadDynamic(true, filename)"
@download-picture="(filename: string) => downloadDynamic(false, filename)"
@download-native="downloadNative"
@download-md="downloadMD"
@import-md="importMD"
@sync-local-markdown="syncLocalMarkdown"
/>
<div id="editor">
<Editor />
Expand Down
117 changes: 117 additions & 0 deletions src/views/editor/hook.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { onActivated, onDeactivated, Ref, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
import { ElMessageBox } from 'element-plus'
import 'element-plus/es/components/message-box/style/css'

import { getLocalStorage } from '@/common/localstorage'
import { errorMessage, successMessage, warningMessage } from '@/common/message'
Expand Down Expand Up @@ -172,6 +174,121 @@ export function useImportMD(resumeType: string) {
}
}

type LocalMarkdownFileHandle = {
name: string
getFile: () => Promise<File>
createWritable: () => Promise<{
write: (content: string) => Promise<void>
close: () => Promise<void>
abort?: () => Promise<void>
}>
}

type FilePickerWindow = Window & {
showOpenFilePicker?: (options: {
multiple: boolean
types: Array<{ description: string; accept: Record<string, string[]> }>
}) => Promise<LocalMarkdownFileHandle[]>
}

export function useLocalMarkdownSync(resumeType: Ref<string>) {
const editorStore = useEditorStore()
const localMarkdownFileName = ref('')
const localMarkdownSyncStatus = ref<'idle' | 'syncing' | 'synced' | 'error'>('idle')
let fileHandle: LocalMarkdownFileHandle | null = null
let writeQueue = Promise.resolve()

async function writeFile(handle: LocalMarkdownFileHandle, content: string) {
const writable = await handle.createWritable()
try {
await writable.write(content)
await writable.close()
} catch (error) {
await writable.abort?.()
throw error
}
}

const writeChanges = useDebounceFn((content: string, currentHandle: LocalMarkdownFileHandle) => {
if (fileHandle !== currentHandle) return
localMarkdownSyncStatus.value = 'syncing'
writeQueue = writeQueue
.then(() => writeFile(currentHandle, content))
.then(() => {
if (fileHandle === currentHandle) localMarkdownSyncStatus.value = 'synced'
})
.catch(() => {
if (fileHandle !== currentHandle) return
fileHandle = null
localMarkdownFileName.value = ''
localMarkdownSyncStatus.value = 'error'
errorMessage('本地Markdown同步失败,请重新授权文件')
})
}, 500)

watch(
() => editorStore.MDContent,
content => fileHandle && writeChanges(content, fileHandle)
)

async function syncLocalMarkdown() {
if (editorStore.writable) return warningMessage('请先切换到Markdown模式')
const picker = (window as FilePickerWindow).showOpenFilePicker
if (!picker) return warningMessage('当前浏览器不支持本地文件同步,请继续使用导入/导出MD')

try {
const [handle] = await picker.call(window, {
multiple: false,
types: [{ description: 'Markdown', accept: { 'text/markdown': ['.md'] } }]
})
if (!handle) return
const fileContent = await (await handle.getFile()).text()
let loadFileContent = false

if (fileContent !== editorStore.MDContent) {
try {
await ElMessageBox.confirm(
`本地文件 ${handle.name} 与编辑器内容不同,请选择要保留的版本。`,
'内容不一致',
{
confirmButtonText: '用编辑器内容覆盖文件',
cancelButtonText: '载入本地文件',
distinguishCancelAndClose: true,
closeOnClickModal: false,
type: 'warning'
}
)
} catch (action) {
if (action !== 'cancel') return
loadFileContent = true
}
}

fileHandle = handle
localMarkdownFileName.value = handle.name
if (loadFileContent) {
editorStore.setMDContent(fileContent, resumeType.value)
} else if (fileContent !== editorStore.MDContent) {
await writeFile(handle, editorStore.MDContent)
}
localMarkdownSyncStatus.value = 'synced'
successMessage(`已同步 ${handle.name},后续修改将自动保存`)
} catch (error) {
if ((error as DOMException).name === 'AbortError') return
fileHandle = null
localMarkdownFileName.value = ''
localMarkdownSyncStatus.value = 'error'
errorMessage('无法同步本地Markdown文件')
}
}

return {
localMarkdownFileName,
localMarkdownSyncStatus,
syncLocalMarkdown
}
}

export function useAvatar(resumeType: string) {
const matchAvatarSlot = /!\[个人头像\]\(.*\)/
function setAvatar(path: string) {
Expand Down