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

<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>