160 lines
4.4 KiB
Vue
160 lines
4.4 KiB
Vue
<template>
|
||
<div class="rich-editor">
|
||
<Toolbar class="rich-editor__toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
|
||
<Editor
|
||
class="rich-editor__body"
|
||
v-model="model"
|
||
:defaultConfig="editorConfig"
|
||
mode="default"
|
||
@onCreated="handleCreated"
|
||
@onChange="handleChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, shallowRef, onBeforeUnmount, watch } from "vue"
|
||
import { Editor, Toolbar } from "@wangeditor/editor-for-vue"
|
||
import "@wangeditor/editor/dist/css/style.css"
|
||
|
||
const model = defineModel<string>({ default: "" })
|
||
|
||
const editorRef = shallowRef<any>(null)
|
||
const valueHtml = ref(model.value || "")
|
||
|
||
// === 单文件上传 (admin_new: /api 前缀 + access_token, 5MB) ===
|
||
async function uploadFile(file: File): Promise<string> {
|
||
if (file.size > 5 * 1024 * 1024) throw new Error("图片大小不能超过5MB,请压缩后重试")
|
||
const fd = new FormData()
|
||
fd.append("file", file)
|
||
const token = localStorage.getItem("access_token") || ""
|
||
let resp: Response
|
||
try {
|
||
resp = await fetch("/api/sys/file/upload", {
|
||
method: "POST",
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
body: fd,
|
||
})
|
||
} catch {
|
||
throw new Error("图片上传失败,请检查网络后重试")
|
||
}
|
||
if (resp.status === 413) throw new Error("图片超过大小限制(5MB),请压缩后重试")
|
||
const data = await resp.json()
|
||
if (data.code === 0) return data?.data?.url || data?.url || ""
|
||
throw new Error(data.msg || "图片上传失败")
|
||
}
|
||
|
||
// === 图片上传配置 (工具栏选择/拖拽/粘贴位图统一走 customUpload) ===
|
||
const editorConfig = {
|
||
placeholder: "请输入资讯内容…支持直接粘贴公众号图文(保留排版,图片自动转存平台)",
|
||
MENU_CONF: {
|
||
uploadImage: {
|
||
maxFileSize: 5 * 1024 * 1024,
|
||
async customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
|
||
try {
|
||
const url = await uploadFile(file)
|
||
insertFn(url, "", "")
|
||
} catch (e: any) {
|
||
alert(e?.message || "图片上传失败")
|
||
}
|
||
},
|
||
},
|
||
},
|
||
// 自定义粘贴: 处理剪贴板中的位图(截图) -> 上传平台后插入; 排版 HTML 交给编辑器默认逻辑
|
||
customPaste: (editor: any, event: ClipboardEvent, callback: (v: boolean) => void) => {
|
||
const items = event.clipboardData?.items
|
||
if (!items) return callback(true) // 无剪贴板数据, 走默认
|
||
const imageFiles: File[] = []
|
||
for (const it of Array.from(items)) {
|
||
if (it.type.startsWith("image/")) {
|
||
const f = it.getAsFile()
|
||
if (f) imageFiles.push(f)
|
||
}
|
||
}
|
||
// 无位图: 交给默认粘贴 (HTML 排版由 wangEditor 保留)
|
||
if (!imageFiles.length) return callback(true)
|
||
|
||
// 有位图: 阻止默认, 上传后插入
|
||
event.preventDefault()
|
||
;(async () => {
|
||
for (const f of imageFiles) {
|
||
try {
|
||
const url = await uploadFile(f)
|
||
editor.insertNode({ type: "image", src: url, alt: "", href: "" })
|
||
editor.insertNode({ type: "paragraph", children: [{ text: "" }] })
|
||
} catch (e: any) {
|
||
alert(e?.message || "图片上传失败")
|
||
}
|
||
}
|
||
})()
|
||
callback(false)
|
||
},
|
||
}
|
||
|
||
const toolbarConfig = {
|
||
excludeKeys: ["group-video", "fullScreen"],
|
||
toolbarKeys: [
|
||
"headerSelect",
|
||
"bold",
|
||
"italic",
|
||
"underline",
|
||
"through",
|
||
"color",
|
||
"bgColor",
|
||
"fontSize",
|
||
"fontFamily",
|
||
"lineHeight",
|
||
"justifyLeft",
|
||
"justifyCenter",
|
||
"justifyRight",
|
||
"bulletedList",
|
||
"numberedList",
|
||
"blockquote",
|
||
"codeBlock",
|
||
"insertLink",
|
||
"uploadImage",
|
||
"insertTable",
|
||
"undo",
|
||
"redo",
|
||
],
|
||
}
|
||
|
||
function handleCreated(editor: any) {
|
||
editorRef.value = editor
|
||
}
|
||
|
||
function handleChange(editor: any) {
|
||
valueHtml.value = editor.getHtml()
|
||
model.value = valueHtml.value
|
||
}
|
||
|
||
// 外部 v-model 变化时同步 (编辑回显)
|
||
watch(model, (v) => {
|
||
if (v !== valueHtml.value && editorRef.value) {
|
||
editorRef.value.setHtml(v || "")
|
||
}
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
if (editorRef.value) editorRef.value.destroy()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.rich-editor {
|
||
border: 1px solid var(--el-border-color, #dcdfe6);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
.rich-editor__toolbar {
|
||
border-bottom: 1px solid var(--el-border-color, #dcdfe6);
|
||
}
|
||
.rich-editor__body {
|
||
min-height: 320px;
|
||
z-index: 100;
|
||
}
|
||
.rich-editor__body :deep(.w-e-text-container) {
|
||
min-height: 320px;
|
||
}
|
||
</style>
|