|
@@ -0,0 +1,290 @@
|
|
|
|
+<template>
|
|
|
|
+ <div style="border: 1px solid #ccc;">
|
|
|
|
+ <Toolbar
|
|
|
|
+ style="border-bottom: 1px solid #ccc"
|
|
|
|
+ :editor="editor"
|
|
|
|
+ :mode="mode"
|
|
|
|
+ :default-config="toolbarConfig"
|
|
|
|
+ />
|
|
|
|
+ <Editor
|
|
|
|
+ style="height: 500px; overflow-y: hidden;"
|
|
|
|
+ v-model="content"
|
|
|
|
+ :mode="mode"
|
|
|
|
+ :default-config="editorConfig"
|
|
|
|
+ @onCreated="onCreated"
|
|
|
|
+ @onChange="onChange"
|
|
|
|
+ @onDestroyed="onDestroyed"
|
|
|
|
+ @onMaxLength="onMaxLength"
|
|
|
|
+ @onFocus="onFocus"
|
|
|
|
+ @onBlur="onBlur"
|
|
|
|
+ @customAlert="customAlert"
|
|
|
|
+ @customPaste="customPaste"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script>
|
|
|
|
+import Vue from 'vue'
|
|
|
|
+import '@wangeditor/editor/dist/css/style.css'
|
|
|
|
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
|
|
+import * as API_Upload from "@/api/upload";
|
|
|
|
+
|
|
|
|
+const serverUrl = domain.serverUrl
|
|
|
|
+export default Vue.extend({
|
|
|
|
+ name: "wangEditor",
|
|
|
|
+ components: { Editor, Toolbar },
|
|
|
|
+ model: {
|
|
|
|
+ prop: 'value',
|
|
|
|
+ event: 'input'
|
|
|
|
+ },
|
|
|
|
+ props: {
|
|
|
|
+ value: {
|
|
|
|
+ type: String,
|
|
|
|
+ default: ''
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ data() {
|
|
|
|
+ return {
|
|
|
|
+ editor: null,
|
|
|
|
+ mode: 'simple', // or 'default'
|
|
|
|
+ content: '请输入内容...',
|
|
|
|
+ toolbarConfig: { },
|
|
|
|
+ editorConfig: {
|
|
|
|
+ placeholder: '请输入内容...',
|
|
|
|
+ withCredentials: true,
|
|
|
|
+ MENU_CONF: {
|
|
|
|
+ // 配置上传图片
|
|
|
|
+ uploadImage: {
|
|
|
|
+ // // 自定义上传图片的方法
|
|
|
|
+ customUpload: this.uploadImg,
|
|
|
|
+ // // 自定义插入图片的方法
|
|
|
|
+ customInsert: this.insertImg,
|
|
|
|
+ //server必须要配置正确
|
|
|
|
+ // server: serverUrl + '/ncs/upload/uploadFile',
|
|
|
|
+ // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
|
|
|
|
+ fieldName: "file",
|
|
|
|
+ headers: {
|
|
|
|
+ // 'Content-Type': 'multipart/form-data;'
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ maxFileSize: 4 * 1024 * 1024, // 1M
|
|
|
|
+ // 最多可上传几个文件,默认为 100
|
|
|
|
+ maxNumberOfFiles: 100,
|
|
|
|
+ // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
|
|
|
|
+ allowedFileTypes: [],
|
|
|
|
+ meta: {
|
|
|
|
+ //官网中把token放到了这里,但是请求的时候会看不到token
|
|
|
|
+ },
|
|
|
|
+ // 将 meta 拼接到 url 参数中,默认 false
|
|
|
|
+ metaWithUrl: false,
|
|
|
|
+ // 跨域是否传递 cookie ,默认为 false
|
|
|
|
+ withCredentials: false,
|
|
|
|
+ // 超时时间,默认为 10 秒
|
|
|
|
+ timeout: 5 * 1000, // 5 秒
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ watch: {
|
|
|
|
+ value(val) {
|
|
|
|
+ this.html = val
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ beforeDestroy() {
|
|
|
|
+ const editor = this.editor
|
|
|
|
+ if (editor == null) return
|
|
|
|
+ editor.destroy() // 组件销毁时,及时销毁编辑器
|
|
|
|
+ },
|
|
|
|
+ methods: {
|
|
|
|
+ onCreated(editor) {
|
|
|
|
+ this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
|
|
|
|
+ },
|
|
|
|
+ onChange(editor) {
|
|
|
|
+ this.$emit('input', editor.getHtml())
|
|
|
|
+ },
|
|
|
|
+ onDestroyed(editor) {
|
|
|
|
+ console.log("onDestroyed", editor);
|
|
|
|
+ },
|
|
|
|
+ onMaxLength(editor) {
|
|
|
|
+ console.log("onMaxLength", editor);
|
|
|
|
+ },
|
|
|
|
+ onFocus(editor) {
|
|
|
|
+ console.log("onFocus", editor);
|
|
|
|
+ },
|
|
|
|
+ onBlur(editor) {
|
|
|
|
+ console.log("onBlur", editor);
|
|
|
|
+ },
|
|
|
|
+ customAlert(info, type) {
|
|
|
|
+ window.alert(`customAlert in Vue demo\n${type}:\n${info}`);
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ //重点来了: 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑。(可以实现复制粘贴 word ,有图片)
|
|
|
|
+ customPaste(editor, event, callback) {
|
|
|
|
+ console.log("ClipboardEvent 粘贴事件对象", event);
|
|
|
|
+ let html = event.clipboardData.getData("text/html"); // 获取粘贴的 html
|
|
|
|
+ // let text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
|
|
|
|
+ let rtf = event.clipboardData.getData("text/rtf"); // 获取 rtf 数据(如从 word wsp 复制粘贴)
|
|
|
|
+ var that = this;
|
|
|
|
+ if (html && rtf) {
|
|
|
|
+
|
|
|
|
+ // 列表缩进会超出边框,直接过滤掉
|
|
|
|
+ html = html.replace(/text\-indent:\-(.*?)pt/gi, "");
|
|
|
|
+
|
|
|
|
+ // 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
|
|
|
|
+ const imgSrcs = that.findAllImgSrcsFromHtml(html);
|
|
|
|
+
|
|
|
|
+ // 如果有
|
|
|
|
+ if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
|
|
|
|
+ // 从rtf内容中查找图片数据
|
|
|
|
+ const rtfImageData = that.extractImageDataFromRtf(rtf);
|
|
|
|
+
|
|
|
|
+ // 如果找到
|
|
|
|
+ if (rtfImageData.length) {
|
|
|
|
+ // TODO:此处可以将图片上传到自己的服务器上
|
|
|
|
+ this.uploadImg(rtfImageData, this.editor)
|
|
|
|
+
|
|
|
|
+ // 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
|
|
|
|
+ html = that.replaceImagesFileSourceWithInlineRepresentation(
|
|
|
|
+ html,
|
|
|
|
+ imgSrcs,
|
|
|
|
+ rtfImageData
|
|
|
|
+ );
|
|
|
|
+ editor.dangerouslyInsertHtml(html);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 阻止默认的粘贴行为
|
|
|
|
+ event.preventDefault();
|
|
|
|
+ return false;
|
|
|
|
+ } else {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ //自定义上传图片
|
|
|
|
+ uploadImg(file, insertFn) {
|
|
|
|
+ let imgData = new FormData();
|
|
|
|
+ imgData.append("file", file);
|
|
|
|
+ //调用上传图片接口,上传图片
|
|
|
|
+ API_Upload.upload(imgData).then((res) => {
|
|
|
|
+ // 插入后端返回的url
|
|
|
|
+ insertFn(serverUrl + '/' + res);
|
|
|
|
+ this.$message({
|
|
|
|
+ type: "success",
|
|
|
|
+ message: "上传成功",
|
|
|
|
+ });
|
|
|
|
+ }).catch((error) => {
|
|
|
|
+ this.$message("上传失败,请重新上传");
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 自定义插入图片
|
|
|
|
+ insertImg(file) {
|
|
|
|
+ console.log(file);
|
|
|
|
+ },
|
|
|
|
+ /**
|
|
|
|
+ * 从html代码中匹配返回图片标签img的属性src的值的集合
|
|
|
|
+ * @param htmlData
|
|
|
|
+ * @return Array
|
|
|
|
+ */
|
|
|
|
+ findAllImgSrcsFromHtml(htmlData) {
|
|
|
|
+ let imgReg = /<img.*?(?:>|\/>)/gi; //匹配图片中的img标签
|
|
|
|
+ let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i; // 匹配图片中的src
|
|
|
|
+
|
|
|
|
+ let arr = htmlData.match(imgReg); //筛选出所有的img
|
|
|
|
+ if (!arr || (Array.isArray(arr) && !arr.length)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let srcArr = [];
|
|
|
|
+ for (let i = 0; i < arr.length; i++) {
|
|
|
|
+ let src = arr[i].match(srcReg);
|
|
|
|
+ // 获取图片地址
|
|
|
|
+ srcArr.push(src[1]);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return srcArr;
|
|
|
|
+ },
|
|
|
|
+ /**
|
|
|
|
+ * 从rtf内容中匹配返回图片数据的集合
|
|
|
|
+ * @param rtfData
|
|
|
|
+ * @return Array
|
|
|
|
+ */
|
|
|
|
+ extractImageDataFromRtf(rtfData) {
|
|
|
|
+ if (!rtfData) {
|
|
|
|
+ return [];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const regexPictureHeader =
|
|
|
|
+ /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/;
|
|
|
|
+ const regexPicture = new RegExp(
|
|
|
|
+ "(?:(" + regexPictureHeader.source + "))([\\da-fA-F\\s]+)\\}",
|
|
|
|
+ "g"
|
|
|
|
+ );
|
|
|
|
+ const images = rtfData.match(regexPicture);
|
|
|
|
+ const result = [];
|
|
|
|
+
|
|
|
|
+ if (images) {
|
|
|
|
+ for (const image of images) {
|
|
|
|
+ let imageType = false;
|
|
|
|
+
|
|
|
|
+ if (image.includes("\\pngblip")) {
|
|
|
|
+ imageType = "image/png";
|
|
|
|
+ } else if (image.includes("\\jpegblip")) {
|
|
|
|
+ imageType = "image/jpeg";
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (imageType) {
|
|
|
|
+ result.push({
|
|
|
|
+ hex: image
|
|
|
|
+ .replace(regexPictureHeader, "")
|
|
|
|
+ .replace(/[^\da-fA-F]/g, ""),
|
|
|
|
+ type: imageType,
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return result;
|
|
|
|
+ },
|
|
|
|
+ /**
|
|
|
|
+ * 将html内容中img标签的属性值替换
|
|
|
|
+ * @param htmlData html内容
|
|
|
|
+ * @param imageSrcs html中img的属性src的值的集合
|
|
|
|
+ * @param imagesHexSources rtf中图片数据的集合,与html内容中的img标签对应
|
|
|
|
+ * @param isBase64Data 是否是Base64的图片数据
|
|
|
|
+ * @return String
|
|
|
|
+ */
|
|
|
|
+ replaceImagesFileSourceWithInlineRepresentation(
|
|
|
|
+ htmlData,
|
|
|
|
+ imageSrcs,
|
|
|
|
+ imagesHexSources,
|
|
|
|
+ isBase64Data = true
|
|
|
|
+ ) {
|
|
|
|
+ if (imageSrcs.length === imagesHexSources.length) {
|
|
|
|
+ for (let i = 0; i < imageSrcs.length; i++) {
|
|
|
|
+ const newSrc = isBase64Data
|
|
|
|
+ ? `data:${
|
|
|
|
+ imagesHexSources[i].type
|
|
|
|
+ };base64,${this._convertHexToBase64(imagesHexSources[i].hex)}`
|
|
|
|
+ : imagesHexSources[i];
|
|
|
|
+
|
|
|
|
+ htmlData = htmlData.replace(imageSrcs[i], newSrc);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return htmlData;
|
|
|
|
+ },
|
|
|
|
+ // 销毁富文本
|
|
|
|
+ beforeDestroy() {
|
|
|
|
+ const editor = this.editor;
|
|
|
|
+ if (editor == null) return;
|
|
|
|
+ editor.destroy(); // 组件销毁时,及时销毁编辑器
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+})
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style>
|
|
|
|
+</style>
|