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