From e846f54f05096d75fa129cf101c13cc955ecc8d1 Mon Sep 17 00:00:00 2001 From: zhouwenxuan <1175765986@qq.com> Date: 星期四, 30 十一月 2023 17:07:44 +0800 Subject: [PATCH] 富文本上传附件功能 --- src/views/safetyReview/notice/components/noticeDialog.vue | 115 +++++++++------ src/main.js | 4 package.json | 8 src/components/WeEditor/index.vue | 248 +++++++++++++++++++++++++++++++++++ 4 files changed, 324 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 2fb9f6a..822d28a 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ }, "dependencies": { "@element-plus/icons-vue": "2.0.10", - "@tinymce/tinymce-vue": "^4.0.5", "@vueup/vue-quill": "1.2.0", "@vueuse/core": "9.5.0", + "@wangeditor/editor": "^5.1.18", + "@wangeditor/editor-for-vue": "^5.1.12", + "@wangeditor/plugin-upload-attachment": "^1.1.0", "axios": "0.27.2", "echarts": "5.4.0", "element-plus": "2.2.27", @@ -30,11 +32,11 @@ "nprogress": "0.2.0", "pinia": "2.0.22", "quill": "^2.0.0-dev.3", - "tinymce": "^5.10.2", "vue": "3.2.45", "vue-cropper": "1.0.3", "vue-quill-editor": "^3.0.6", - "vue-router": "4.1.4" + "vue-router": "4.1.4", + "wangeditor5-for-vue3": "^0.1.0" }, "devDependencies": { "@vitejs/plugin-vue": "3.1.0", diff --git a/src/components/WeEditor/index.vue b/src/components/WeEditor/index.vue new file mode 100644 index 0000000..17eb809 --- /dev/null +++ b/src/components/WeEditor/index.vue @@ -0,0 +1,248 @@ +<template> + <div style="height: 600px"> + <we-editor + toolbar-class="toolbar" + editable-class="editable" + toolbar-style="border: 1px solid #d9d9d9" + editable-style="border: 1px solid #d9d9d9" + :toolbar-option="toolbar" + :editable-option="editable" + :toolbar-reloadbefore="onToolbarReloadBefore" + :editable-reloadbefore="onEditableReloadBefore" + v-model="formData.jarr" + v-model:json="formData.jstr" + v-model:html="formData.html" + /> + </div> +</template> +<script> +import '@wangeditor/editor/dist/css/style.css' + +import { WeEditor, useWangEditor } from 'wangeditor5-for-vue3' +import {defineComponent, onBeforeUnmount, ref, shallowReactive, shallowRef, watch} from 'vue' +import {ElMessage} from "element-plus"; +import {getToken} from "@/utils/auth"; +import {nextTick} from 'vue' +export default defineComponent( { + name: "wangeditor", + components: { WeEditor }, + props: { + propData :String + }, + setup(prop) { + //编辑器配置 + const editableOption = { + config:{ + placeholder:"请在这里输入内容", + hoverbarKeys: { + attachment: { + menuKeys: ['downloadAttachment'], // “下载附件”菜单 + }, + }, + MENU_CONF:{ + // 配置默认字号 + fontSize:{ + fontSizeList: [ + // 元素支持两种形式 + // 1. 字符串; + // 2. { name: 'xxx', value: 'xxx' } + '16px', + '20px', + { name: '26px', value: '26px' }, + '40px', + ] + }, + // 配置上传图片 + uploadImage:{ + // 请求路径 + server: import.meta.env.VITE_APP_BASE_API + "/system/common/uploadFile", + // 后端接收的文件名称 + fieldName: "file", + maxFileSize: 1 * 1024 * 1024, // 1M + // 上传的图片类型 + allowedFileTypes: ["image/*"], + // 小于该值就插入 base64 格式(而不上传),默认为 0 + base64LimitSize: 10 * 1024, // 10MB + // 自定义插入返回格式【后端返回的格式】 + customInsert(res, insertFn) { + if(res.code != 200){ + ElMessage.error("上传文件失败,"+res.message) + return + } + const url = import.meta.env.VITE_APP_BASE_API + "/" +res.data.path + insertFn(url) + }, + // 携带的数据 + meta: { + Authorization: getToken() + }, + // 将 meta 拼接到 url 参数中,默认 false + metaWithUrl: true, + // 单个文件上传成功之后 + onSuccess(file, res) { + if(res.code == 200){ + ElMessage.success(`${file.name} 上传成功`) + return + }else { + ElMessage.warning(`${file.name} 上传出了点异常`) + return + } + // console.log(`${file.name} 上传成功`, res) + //ElMessage.success(`${file.name} 上传成功`, res) + }, + // 单个文件上传失败 + onFailed(file, res) { + console.log(res) + ElMessage.error(`${file.name} 上传失败`) + }, + // 上传错误,或者触发 timeout 超时 + onError(file, err, res) { + console.log(err, res) + ElMessage.error(`${file.name} 上传出错`) + }, + }, + // 上传附件 + uploadAttachment: { + server: import.meta.env.VITE_APP_BASE_API + "/system/common/uploadFile", + fieldName: 'file', + maxFileSize: 100 * 1024 * 1024, // 100M + // 携带的数据 + meta: { + Authorization: getToken() + }, + // 成功回调 + onSuccess(file, res) { + if (res.errno === 0) { + ElMessage.success(`${file}附件上传成功`) + } + }, + // 失败回调 + onFailed(file, res) { + if (res.errno === 1) { + ElMessage.success(`${file}附件上传失败,` + res.message) + } + }, + // 上传成功后,用户自定义插入文件 + customInsert(res, file, insertFn) { + console.log('customInsert', res) + const url = import.meta.env.VITE_APP_BASE_API + "/" +res.data.path + if (!url) throw new Error(`url is empty`) + // 插入附件到编辑器 + insertFn(`${file.name}`, url) + }, + // 插入到编辑器后的回调 + onInsertedAttachment(elem) { + console.log("elem",elem) + } + } + } + } + } + + // 菜单栏配置 + const toolbarOption = { + mode: 'simple', // 指定简介模式 + config:{ + toolbarKeys:[ + "fontSize",'header1', 'header2', 'header3','header4','|', + 'blockquote',"code","codeBlock",'|', + 'bold', 'underline', 'italic', 'through', 'color', 'bgColor', 'clearStyle', '|', + 'bulletedList', 'numberedList', 'todo', 'justifyLeft','justifyCenter', 'justifyRight', '|', + 'insertLink', + { + key: 'group-image', + title: '图片', + iconSvg: "<svg viewBox=\"0 0 1024 1024\"><path d=\"M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z\"></path></svg>", + menuKeys: ['insertImage', 'uploadImage'] + }, + "insertTable", + "|", + "undo","redo" + ], + // 插入哪些菜单 + insertKeys: { + index: 27, // 自定义插入的位置 + keys: ['uploadAttachment'], // “上传附件”菜单 + }, + } + } + + + // 防抖时长。当会触发重载的配置项发生变化 365ms 后,编辑器会重载 + const reloadDelary = 365 + + // 对于上面的三个对象,经过 useWangEditor 处理后,返回的 editable 和 toolbar 分别对应编辑器和菜单栏的配置项 + const { editable, toolbar ,syncContent,clearContent } = useWangEditor( + editableOption, + toolbarOption, + reloadDelary, + { + delay: 3000, // 无操作 3s 后才会同步表单数据 + config: { + placeholder: '表单提交前使用 syncContent API 强制同步数据,确保数据不被丢失', + }, + } + ) + // 获取数据 + const formData = shallowReactive({ html: '' }) + + // 监听父组件传来的参数 + watch(prop, (val) => { + // 监听从父组件接收的所有参数,只要有参数变化就会触发 + console.log('props接收的值', val) + formData.html = prop.propData + }, { + immediate:true + }) + + function submit() { + // 强制同步 v-model 数据 + syncContent() + // 表单验证 + if(formData.html!=''){ + this.$emit('childFn',formData.html) + }else { + // ElMessage.error("请在编辑器内编写内容...") + } + } + function setData(val) { + console.log("val",val) + } + + function clear() { + clearContent() + } + + + // 在可编辑的重新加载之前 + function onEditableReloadBefore(inst) { + console.log(inst) + console.log('editable 即将重载: ' + new Date().toLocaleString()) + } + + // 在工具栏上重新加载之前 + function onToolbarReloadBefore(inst) { + console.log(inst) + console.log('toolbar 即将重载: ' + new Date().toLocaleString()) + } + return { editable, toolbar, formData, submit, clear, prop, onEditableReloadBefore, onToolbarReloadBefore } + }, +}) +</script> +<style> +/*工具栏样式*/ +.toolbar{ + border: 1px solid #d9d9d9;margin-bottom: 10px; +} +/*编辑器样式*/ +.editable{ + margin-top: -11px; + border: 1px solid #d9d9d9; + height: 500px; + //width: 50%; + //margin: 30px auto 150px auto; + background-color: #fff; + box-shadow: 0 2px 10px rgb(0 0 0 / 12%); + //border: 1px solid #e8e8e8; +} +</style> diff --git a/src/main.js b/src/main.js index 5cf8846..f2db41f 100644 --- a/src/main.js +++ b/src/main.js @@ -43,6 +43,10 @@ // 字典标签组件 import DictTag from '@/components/DictTag' +import { Boot } from '@wangeditor/editor' +import attachmentModule from '@wangeditor/plugin-upload-attachment' + +Boot.registerModule(attachmentModule) const app = createApp(App) // 全局方法挂载 diff --git a/src/views/safetyReview/notice/components/noticeDialog.vue b/src/views/safetyReview/notice/components/noticeDialog.vue index 27f66bd..c020262 100644 --- a/src/views/safetyReview/notice/components/noticeDialog.vue +++ b/src/views/safetyReview/notice/components/noticeDialog.vue @@ -11,36 +11,13 @@ <el-input v-model.trim="state.noticeForm.noticeTitle" v-if="!isReview" ></el-input> <span v-else>{{state.noticeForm.noticeTitle}}</span> </el-form-item> - <div style="margin: 0 0 15px 30px" v-if="!isReview"> - <el-upload - v-model:file-list="fileList" - class="upload-demo" - action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" - multiple - :on-preview="handlePreview" - :on-remove="handleRemove" - :before-remove="beforeRemove" - :limit="3" - > - <el-button type="primary">附件上传</el-button> - </el-upload> - </div> - <el-form-item v-else label="附件:" > - <div class="file"> - <el-link v-for="(item,index) in state.noticeForm.fileList" type="primary" :key="index" :underline="false"> - <i class="el-icon-paperclip" style="margin-right: 5px;color: #1e6abc"></i>{{item.fileName}} - </el-link> - </div> - </el-form-item> - <el-form-item v-if="!isReview" style="margin-left: -80px" prop="noticeContent"> -<!-- <tinymce v-model="state.noticeForm.noticeContent"></tinymce>--> - <editor ref="myQuillEditor" v-model="state.noticeForm.noticeContent" :height="300"></editor> - + <el-form-item label="公告内容:" v-if="showEditor" prop="noticeContent"> + <we-editor ref="myEditor" :propData="state.noticeForm.noticeContent" @childFn="getEditorData" /> </el-form-item> <el-form-item label="公告内容:" v-else> - <div class="ql-container ql-snow" style="height: 300px;width: 100%;margin-top: 10px;" > + <div class="ql-container ql-snow" style="height: 500px;width: 100%;margin-top: 10px;" > <div class="ql-editor"> - <div v-html="state.noticeForm.noticeContent"></div> + <div v-html="state.noticeForm.noticeContent" @click="showFile($event)"></div> </div> </div> </el-form-item> @@ -56,23 +33,30 @@ </template> <script setup> import {reactive, ref, toRefs} from 'vue' +import WeEditor from "@/components/WeEditor/index.vue"; import Editor from "@/components/Editor/index.vue"; import {ElMessage} from "element-plus"; import {addNotice, editNotice, getNoticeDetail} from "@/api/backManage/notice"; +import axios from "axios"; +import {getToken} from "@/utils/auth"; + + + + const emit = defineEmits(["getList"]); const dialogVisible = ref(false); const title = ref(""); const noticeRef = ref(); const fileList = ref([]); -const myQuillEditor = ref(); +const myEditor = ref(); const isReview = ref(false); +const showEditor = ref(true); const state = reactive({ noticeForm: { id: '', noticeTitle: '', noticeContent: '', - fileList: [] }, formRules:{ noticeTitle: [{ required: true, message: '请填写公告标题', trigger: 'blur' }], @@ -83,6 +67,7 @@ const openDialog = async (type, value) => { isReview.value = false; + showEditor.value = false title.value = type === 'add' ? '新增' : type ==='edit' ? '编辑' : '查看' ; if(type === 'edit' || type === 'review') { const param = { @@ -96,26 +81,64 @@ }else{ ElMessage.warning(res.message) } + } if(type === 'review') { + showEditor.value = false isReview.value = true; } + if(type === 'edit' || type === 'add') { + showEditor.value = true; + isReview.value = false; + } if(type === 'add'){ - reset() + state.noticeForm.noticeContent = " "; + state.noticeForm.noticeTitle = " "; } dialogVisible.value = true; } +const getEditorData = (val) =>{ + state.noticeForm.noticeContent = val; +} + +const showFile = (e) => { + if(e.target.nodeName === 'A'){ + console.log("e",e) + e.preventDefault(); + const file = { + fileUrl: e.target.href, + fileName: e.target.download + } + axios.get( file.fileUrl,{ + headers: + { + 'Content-Type': 'application/json', + 'Authorization':getToken(), + }, + responseType: 'blob' + } + ).then(res=>{ + if (res) { + const link = document.createElement('a') + let blob = new Blob([res.data],{type: res.data.type}) + link.style.display = "none"; + link.href = URL.createObjectURL(blob); // 创建URL + link.setAttribute("download", file.fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + this.$message.error('获取文件失败') + } + this.handleClose(); + }) + } +} const onSubmit = async () => { + myEditor.value.submit(); const valid = await noticeRef.value.validate(); if(valid){ - if(state.noticeForm.noticeContent === "<p><br></p>"){ - ElMessage({ - type: 'warning', - message: '请输入公告内容' - }); - return; - } if(title.value === '新增'){ const param = { content: state.noticeForm.noticeContent, @@ -132,10 +155,10 @@ } emit("getList") reset(); + // myEditor.value.clear(); noticeRef.value.clearValidate(); dialogVisible.value = false; }else if(title.value === '编辑') { - const nowDate = new Date() const param = { id: state.noticeForm.id, content: state.noticeForm.noticeContent, @@ -152,6 +175,7 @@ } emit("getList") reset(); + // myEditor.value.clear(); noticeRef.value.clearValidate(); dialogVisible.value = false; } @@ -159,6 +183,11 @@ } const handleClose = () => { + if(title ==="新增"|| title ==='编辑'){ + myEditor.value.clear(); + showEditor.value=false + } + noticeRef.value.clearValidate(); dialogVisible.value = false; } @@ -169,16 +198,6 @@ noticeContent: '' } } -const handleRemove = (file, fileList) => { - console.log(file, fileList); -} -const handlePreview = (uploadFile) => { - console.log(uploadFile) -} -const beforeRemove = (file, fileList) => { - return this.$confirm(`确定移除 ${ file.name }?`); -} - defineExpose({ openDialog -- Gitblit v1.9.2