You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
4.2 KiB
149 lines
4.2 KiB
<script setup lang="ts">
|
|
import 'vditor/dist/index.css'
|
|
import { createPostBodyMarkdownEditorBridge, type CreateVditorLikeOptions } from './post-body-markdown-editor-vditor-bridge'
|
|
import { buildPostBodyMarkdownEditorVditorOptions } from './post-body-markdown-editor-vditor-config'
|
|
import { initializePostBodyMarkdownEditorVditor } from './post-body-markdown-editor-vditor-init'
|
|
import { handlePostBodyMarkdownEditorViewportSwitch } from './post-body-markdown-editor-vditor-viewport'
|
|
|
|
const props = defineProps<{
|
|
modelValue: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [string]
|
|
}>()
|
|
|
|
const { fetchData } = useClientApi()
|
|
const toast = useToast()
|
|
const mountEl = ref<HTMLElement | null>(null)
|
|
const isMobileViewport = ref(false)
|
|
let viewportMql: MediaQueryList | null = null
|
|
let onViewportChange: ((event: MediaQueryListEvent) => void) | null = null
|
|
|
|
const bridge = createPostBodyMarkdownEditorBridge({
|
|
getModelValue: () => props.modelValue,
|
|
emitUpdate: (v) => emit('update:modelValue', v),
|
|
createEditor: ({ element, value, onInput }: CreateVditorLikeOptions) => {
|
|
const Vditor = vditorCtor.value
|
|
if (!Vditor) {
|
|
throw new Error('Vditor constructor is not ready')
|
|
}
|
|
return new Vditor(element, buildPostBodyMarkdownEditorVditorOptions({
|
|
value,
|
|
isMobile: isMobileViewport.value,
|
|
onInput,
|
|
uploadHandler: onUploadImg,
|
|
}))
|
|
},
|
|
})
|
|
|
|
const vditorCtor = shallowRef<null | (new (element: HTMLElement, options: Record<string, unknown>) => {
|
|
getValue: () => string
|
|
setValue: (value: string, render?: boolean) => void
|
|
destroy: () => void
|
|
})>(null)
|
|
let unmounted = false
|
|
|
|
async function onUploadImg(files: File[]): Promise<string> {
|
|
const form = new FormData()
|
|
for (const file of files) {
|
|
form.append('file', file)
|
|
}
|
|
try {
|
|
const { files: uploaded } = await fetchData<{ files: { url: string }[] }>('/api/file/upload', {
|
|
method: 'POST',
|
|
body: form,
|
|
})
|
|
const succMap = Object.fromEntries(uploaded.map((item) => [item.url, item.url]))
|
|
toast.add({ title: '图片已上传', color: 'success' })
|
|
return JSON.stringify({
|
|
msg: '',
|
|
code: 0,
|
|
data: {
|
|
errFiles: [] as string[],
|
|
succMap,
|
|
},
|
|
})
|
|
}
|
|
catch {
|
|
toast.add({ title: '图片上传失败', color: 'warning' })
|
|
return JSON.stringify({
|
|
msg: 'upload failed',
|
|
code: 1,
|
|
data: {
|
|
errFiles: [] as string[],
|
|
succMap: {} as Record<string, string>,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!import.meta.client) {
|
|
return
|
|
}
|
|
viewportMql = window.matchMedia('(max-width: 767px)')
|
|
isMobileViewport.value = viewportMql.matches
|
|
onViewportChange = (event: MediaQueryListEvent) => {
|
|
const nextIsMobile = event.matches
|
|
const changed = handlePostBodyMarkdownEditorViewportSwitch({
|
|
currentIsMobile: isMobileViewport.value,
|
|
nextIsMobile,
|
|
hasMountedEditor: Boolean(mountEl.value && bridge.getEditor()),
|
|
remountEditor: () => {
|
|
if (!mountEl.value) {
|
|
return
|
|
}
|
|
bridge.unmount()
|
|
bridge.mount(mountEl.value)
|
|
},
|
|
})
|
|
if (changed) {
|
|
isMobileViewport.value = nextIsMobile
|
|
}
|
|
}
|
|
viewportMql.addEventListener('change', onViewportChange)
|
|
await initializePostBodyMarkdownEditorVditor({
|
|
importVditor: () => import('vditor'),
|
|
isUnmounted: () => unmounted,
|
|
onReady: (ctor) => {
|
|
vditorCtor.value = ctor
|
|
if (mountEl.value) {
|
|
bridge.mount(mountEl.value)
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
console.error('Failed to initialize Vditor editor', error)
|
|
},
|
|
})
|
|
})
|
|
|
|
watch(() => props.modelValue, () => {
|
|
bridge.syncFromProps()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
unmounted = true
|
|
if (viewportMql && onViewportChange) {
|
|
viewportMql.removeEventListener('change', onViewportChange)
|
|
}
|
|
onViewportChange = null
|
|
viewportMql = null
|
|
bridge.unmount()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<ClientOnly>
|
|
<div
|
|
ref="mountEl"
|
|
:style="{ height: 'min(72vh, 720px)' }"
|
|
class="w-full rounded-lg overflow-hidden ring ring-default"
|
|
/>
|
|
<template #fallback>
|
|
<div class="text-muted text-sm py-12 text-center border border-default rounded-lg">
|
|
编辑器加载中…
|
|
</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</template>
|
|
|