Files
workspace/code/link56/link56-frontend/src/components/wang-editor/index.vue
T
2025-12-11 17:31:05 +08:00

178 lines
4.1 KiB
Vue

<template>
<a-spin :loading="false" class="w-full h-full editor-container">
<div class="main">
<Toolbar :class="toolbarClass" class="toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" v-if="props.displayMode === 'edit'" />
<Editor :class="editorClass" class="editor" @onChange="handleChange" v-model="valueHtml" :defaultConfig="editorConfig" @onCreated="handleCreated" key="main" />
</div>
<Direcotry class="directory" v-model:content="valueHtml" v-if="showDirecotry" />
</a-spin>
<Preview v-model:visible="showPreview" :content="previewHtml" />
</template>
<script setup>
import Preview from "./modules/preview.vue";
import Direcotry from "./modules/directory.vue";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import { uploadVideo, uploadImage, uploadToAliyun } from "@/utils/file";
import { Message } from "@arco-design/web-vue";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { ref, computed, onBeforeUnmount, shallowRef, watch, nextTick, defineExpose, onMounted } from "vue";
import { registerPreviewMenu, toolbarConfig } from "./config";
const props = defineProps({
displayMode: {
type: String,
default: "edit" // 'edit' 表示编辑器模式,'view' 表示只读/阅读模式
},
modelValue: {
type: String,
default: ""
},
showDirecotry: {
type: Boolean,
default: false
}
});
const emit = defineEmits(["update:modelValue"]);
const previewHtml = ref(""); // 只读副本
const showPreview = ref(false);
onMounted(() => {
registerPreviewMenu(() => {
previewHtml.value = valueHtml.value;
showPreview.value = true;
});
});
const toolbarClass = computed(() => {
return props.displayMode === "view" ? "toolbar-view" : "toolbar-edit";
});
const editorClass = computed(() => {
return props.displayMode === "view" ? "editor-view" : "editor-edit";
});
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
defineExpose({
getText: () => {
if (editorRef.value) {
return editorRef.value.getText();
} else {
console.error("Editor not initialized");
return "";
}
}
});
// 内容 HTML
const valueHtml = ref("");
// 目录
watch(
() => props.modelValue,
(newVal) => {
valueHtml.value = newVal;
},
{ immediate: true }
);
const handleChange = () => {
emit("update:modelValue", valueHtml);
};
const editorConfig = {
placeholder: props.displayMode === "edit" ? "请输入内容..." : "",
readOnly: props.displayMode === "view",
MENU_CONF: {} // ✅ 这一步不能漏!
};
editorConfig.MENU_CONF["uploadImage"] = {
// 自定义选择图片
customBrowseAndUpload(insertFn) {
uploadImage().then((data) => {
if (data) {
uploadToAliyun(data.blob, data.name, data.size).then((res) => {
insertFn(res.url, res.fileName, "");
});
}
});
}
};
editorConfig.MENU_CONF["uploadVideo"] = {
customBrowseAndUpload(insertFn) {
uploadVideo().then((data) => {
if (data) {
uploadToAliyun(data.blob, data.name, data.size).then((res) => {
insertFn(res.url, res.fileName, "");
});
} else {
Message.error("上传失败,请稍后再试!");
}
});
}
};
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
const handleCreated = (editor) => {
editorRef.value = editor;
nextTick(() => {});
};
</script>
<style lang="scss" scoped>
.toolbar {
}
.toolbar-view {
}
.toolbar-edit {
border: 1px solid #ccc;
}
.editor {
overflow-y: hidden;
:deep(.w-e-text-container video) {
width: 100% !important;
}
}
.editor-view {
height: auto;
:deep(.w-e-text-container) {
background-color: transparent;
}
:deep(.w-e-text-container .w-e-scroll [data-slate-editor]) {
overflow-y: auto;
padding: 0 0px !important;
}
}
.editor-edit {
height: 500px !important;
border: 1px solid #ccc;
border-top: transparent;
}
.editor-container {
display: flex;
gap: 44px;
.directory {
width: 200px;
height: 450px;
}
.main {
flex: 1;
}
}
</style>