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.
169 lines
4.3 KiB
169 lines
4.3 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 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)
|
|
}
|
|
},
|
|
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="editorContainerStyle"
|
|
class="w-full min-w-0 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>
|
|
|
|
<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>
|
|
|