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.
 
 
 
 
 

183 lines
4.8 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 toast = useToast()
const mountEl = ref<HTMLElement | null>(null)
const isMobileViewport = ref(false)
const isEditorReady = ref(false)
const editorContainerStyle = computed(() => {
if (isMobileViewport.value) {
return {
height: 'min(62vh, 560px)',
minHeight: '360px',
}
}
return {
height: 'min(78vh, 900px)',
minHeight: '520px',
}
})
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,
onUploadError: () => {
toast.add({ title: '图片上传失败', color: 'warning' })
},
}))
},
})
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
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)
isEditorReady.value = true
}
},
onError: (error) => {
console.error('Failed to initialize Vditor editor', error)
},
})
})
watch(() => props.modelValue, () => {
bridge.syncFromProps()
})
onBeforeUnmount(() => {
unmounted = true
isEditorReady.value = false
if (viewportMql && onViewportChange) {
viewportMql.removeEventListener('change', onViewportChange)
}
onViewportChange = null
viewportMql = null
bridge.unmount()
})
</script>
<template>
<ClientOnly>
<div class="relative">
<div
ref="mountEl"
:style="editorContainerStyle"
class="w-full min-w-0 rounded-lg overflow-hidden ring ring-default"
/>
<div
v-if="!isEditorReady"
class="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 rounded-lg bg-default/80 backdrop-blur-[1px] text-muted ring ring-default"
aria-live="polite"
aria-busy="true"
>
<UIcon name="i-lucide-loader-2" class="size-5 animate-spin text-primary" />
<span class="text-sm">编辑器加载中…</span>
</div>
</div>
<template #fallback>
<div class="text-muted text-sm py-12 text-center border border-default rounded-lg">
编辑器加载中
</div>
</template>
</ClientOnly>
</template>
<style scoped>
@media (max-width: 767px) {
:deep(.vditor) {
min-width: 0;
width: 100%;
}
:deep(.vditor-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
padding: 4px 6px;
}
:deep(.vditor-toolbar__item) {
float: none;
flex: 0 0 auto;
padding: 0 !important;
}
:deep(.vditor-toolbar__item .vditor-tooltipped) {
width: 30px;
height: 30px;
min-width: 30px;
padding: 7px;
border-radius: 6px;
}
:deep(.vditor-toolbar__divider) {
display: none;
}
:deep(.vditor-content),
:deep(.vditor-ir),
:deep(.vditor-wysiwyg) {
min-width: 0;
}
}
</style>