多种富文本
一、Tinymce富文本
踩坑:disabled和readonly效果问题
解决方法:
【Tinymce富文本】如何实现disabled和readonly效果_tinymce disabled-CSDN博客
二、vue-quill-editor富文本
官方链接: vue-quill-editor · Quill官方中文文档 · 看云
vue-quill-editor 使用教程 (最全)! - 灰信网(软件开发博客聚合)
踩坑:
上传图片时,图片路径会被默认转为base64编码格式,上传多个图片时路径过长数据库存储不下导致报错用富文本(vue-quill-editor)上传图片时,图片路径会被默认转为base64编码格式,上传多个图片时路径过长数据库存储不下导致报错 - 简书
解决方法:
插入图片不采用base64而是从服务器传图片再显示返回url。
搜索关键词:
vue使用vue-quill实现富文本编辑器上传图片
使用步骤:
1.安装依赖
2.全局注册
3.封装组件(注意这是antd工程)
CourseRichText.vue
<template><div><quill-editorclass="editor"ref="myTextEditor"v-model="editorContent":options="editorOption"@change="onEditorChange($event)"@ready="ready($event)":disabled="disabled"></quill-editor><a-uploadclass="ant-my-uploader"style="display:none":action="`${publicName}/proxy/v1/health/commFile/upload`":before-upload="beforeUpload"@change="handleChange"><a-button> <a-icon type="upload" />Upload </a-button></a-upload><!-- <div>剩余可输入<span :style="{ color: 1000 - innerText.length < 0 ? 'red' : 'black' }">{{1000 - innerText.length}}</span>字</div> --></div>
</template><script>
import { quillEditor } from "vue-quill-editor";
import '@/styles/vue-quill-editor/quill.core.css'
import '@/styles/vue-quill-editor/quill.snow.css'
import '@/styles/vue-quill-editor/quill.bubble.css'
import Quill from "quill";
// 图片大小调整
// import ImageResize from "quill-image-resize-module";
// Quill.register("modules/imageResize", ImageResize);
// 自定义文字大小
let fontSizeStyle = Quill.import("attributors/style/size");
fontSizeStyle.whitelist = ["10px","11px","12px","13px","14px","15px","16px","17px","18px","19px","20px","21px","22px","23px","24px","25px","26px"
];
Quill.register(fontSizeStyle, true);
// import { lineHeightStyle } from "@/utils/lineheight";
//工具菜单栏配置
const toolbarOptions = [["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线["blockquote", "code-block"], // 引用 代码块[{ header: 1 }, { header: 2 }], // 1、2 级标题[{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表[{ script: "sub" }, { script: "super" }], // 上标/下标[{ indent: "-1" }, { indent: "+1" }], // 缩进// [{'direction': 'rtl'}], // 文本方向// [{ size: ["small", false, "large", "huge"] }], // 字体大小[{ size: fontSizeStyle.whitelist }], // 字体大小[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色[{ font: [] }], // 字体种类[{ align: [] }], // 对齐方式// [{ lineheight: ["initial", "1", "1.5", "1.75", "2", "3", "4", "5"] }], // 行高["clean"], // 清除文本格式["link", "image"] // 链接、图片// ["video"] // 视频
];
export default {components: {quillEditor},props: {content: String,disabled: Boolean},name:"courseRichText",data() {return {publicName: window.location.pathname,innerText: "",editorContent: null,editorOption: {placeholder: "请在这里输入", //提示readyOnly: false, //是否只读theme: "snow", //主题 snow/bubblesyntax: true, //语法检测modules: {// imageResize: {// //添加// displayStyles: {// //添加// backgroundColor: "black",// border: "none",// color: "white"// },// modules: ["Resize", "DisplaySize", "Toolbar"] //添加// },toolbar: {container: toolbarOptions,handlers: {image: function(value) {if (value) {console.log(value);// 触发input框选择图片文件document.querySelector(".ant-my-uploader input").click();} else {this.quill.format("image", false);}},lineheight: function(value) {if (value) {this.quill.format("lineHeight", value);} else {console.log(value);}}}}}},loading: false};},computed: {editor() {return this.$refs.myTextEditor.quillEditor;}},mounted() {// Quill.register({ "formats/line-height": LineHeight }, true);},watch: {content: {handler: function(val) {this.editorContent = val;// 更新字数},immediate: true}},methods: {ready() {// Quill.register({ "formats/lineHeight": lineHeightStyle }, true);},onEditorChange(editor) {this.editorContent = editor.html;// 不去除空格// this.innerText = editor.text;// 去除空格this.innerText = editor.text.replace(/[\r\n]$/g, "");this.$emit("onChange", {content: editor.html,textLength: this.innerText.length});},// 上传图片uploadSuccess(val) {let quill = this.$refs.myTextEditor.quill;// 获取光标所在位置let length = quill.getSelection().index;// 插入图片 res.url为服务器返回的图片地址quill.insertEmbed(length, "image", val.absolutePath);// 调整光标到最后quill.setSelection(length + 1);},handleChange(info) {switch (info.file.status) {case "uploading":this.loading = true;break;case "done":this.loading = false;// eslint-disable-next-line no-case-declarationsconst { response } = info.file; // 请求返回的数据if (response.code == 200) {this.uploadSuccess(response.data);this.$message.success({content: "上传成功!",key: "uploadPic",duration: 2});} else {this.$message.error({content: response.message[0] || "上传发生错误" + response.code,key: "uploadPic",duration: 2});}break;case "error":this.loading = false;// 错误消息提示this.$message.error({content: "网络错误请稍后再试",key: "uploadPic",duration: 2});break;default:break;}},beforeUpload(file) {return new Promise((resolve, reject) => {this.$message.success({content: "上传中",key: "uploadPic",duration: 2});const isJpgOrPng =file.type === "image/jpeg" ||file.type === "image/png" ||file.type === "image/jpg";if (!isJpgOrPng) {this.$message.error("图片仅支持 jpeg 或 png 或 jpg 格式");return reject(false);}// const isLt300kb = file.size / 1024 < 300;const isLt2M = file.size / 1024 / 1024 < 2;if (!isLt2M) {this.$message.error("图片大于 2M");return reject(false);}return resolve(true);});}}
};
</script><style>
.editor {line-height: normal !important;/* height: 400px; */background-color: #ffffff;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {border-right: 0px;content: "保存";padding-right: 0px;
}
.ql-snow .ql-tooltip a.ql-action::after {content: "编辑";
}
.ql-snow .ql-tooltip a.ql-remove::before {content: "移除";
}
.ql-snow .ql-tooltip[data-mode="video"]::before {content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {content: "32px";
}.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {content: "标题6";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {content: "等宽字体";
}
/* 编辑器内部出现滚动条 */
.ql-container {/* overflow-y: auto; */height: 200px !important;
}
/*滚动条整体样式*/
.ql-container ::-webkit-scrollbar {width: 10px; /*竖向滚动条的宽度*/height: 10px; /*横向滚动条的高度*/
}
.ql-container ::-webkit-scrollbar-thumb {/*滚动条里面的小方块*/background: #666666;border-radius: 5px;
}
.ql-container ::-webkit-scrollbar-track {/*滚动条轨道的样式*/background: #ccc;border-radius: 5px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="11px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="11px"]::before {content: "11px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {content: "12px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="13px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="13px"]::before {content: "13px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="15px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="15px"]::before {content: "15px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {content: "16px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="17px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="17px"]::before {content: "17px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="19px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="19px"]::before {content: "19px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {content: "20px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="21px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="21px"]::before {content: "21px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="22px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="22px"]::before {content: "22px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="23px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="23px"]::before {content: "23px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {content: "24px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="25px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="25px"]::before {content: "25px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="26px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="26px"]::before {content: "26px";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-label::before{content: "行高";
}
.ql-snow.ql-picker.ql-lineheight.ql-picker-item[data-value="initial"]::before {content: "默认";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="1"]::before {content: "1";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="1.5"]::before {content: "1.5";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="1.75"]::before {content: "1.75";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="2"]::before {content: "2";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="3"]::before {content: "3";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="4"]::before {content: "4";
}
.ql-snow .ql-picker.ql-lineheight .ql-picker-item[data-value="5"]::before {content: "5";
}
.ql-snow .ql-picker.ql-lineheight {width: 70px;
}
</style>
4.页面使用组件
<course-rich-text :disabled=" dialogType!='add' && dialogType!='edit'" @onChange="richTextChange" :content="messageData.msgDetail" />
三、Monaco Editor
封装组件:MonacoEditor.vue
<template><div><div id="code-editor" style="width:100%;height:100%;min-height:200px;border:1px solid #DCDFE6"></div></div>
</template><script>
// import * as monaco from 'monaco-editor'
import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
import { checkJsonCode, getJsonPath } from '@/utils/monaco-editor.js'export default {name: 'monacoEditor',model: {prop: 'content',event: 'change'},props: {'content': null,'language': {default: 'javascript'},'readOnly': {default: false}},data: function () {return {editor: null,jsonPath: null}},watch: {content: function (newValue) {let value_ = newValueif (this.editor) {if (newValue !== this.editor.getValue()) {if (this.language == 'json') {value_ = checkJsonCode(value_)}monaco.editor.setModelLanguage(this.editor.getModel(), this.language);this.editor.trigger(this.editor.getValue(), 'editor.action.formatDocument')this.editor.setValue(value_);}}}},mounted: function () {const copyToClipboard = this.copyToClipboardlet value_ = this.contentif (this.language == 'json') {value_ = checkJsonCode(this.content)}this.editor = monaco.editor.create(this.$el.querySelector('#code-editor'),{value: value_,language: this.language,theme: 'vs',readOnly: this.readOnly,automaticLayout: true});this.editor.addAction({id: 'json-path',label: 'Copy JsonPath',keybindings: [monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_J)],precondition: "editorLangId == 'json'",keybindingContext: "editorLangId == 'json'",contextMenuGroupId: '9_cutcopypaste',contextMenuOrder: 2,run: copyToClipboard});this.editor.onDidChangeModelContent(event => {const value = this.editor.getValue()if (this.value !== value) {this.$emit('change', value, event)}})this.editor.onDidChangeCursorPosition(event => {const value = this.editor.getValue()const offSet = this.editor.getModel().getOffsetAt(event.position)const language = this.language;if (this.value !== value && language === 'json') {this.$emit('on-cursor-change', { offSet: offSet })}if (language == 'json' && offSet !== 0) {this.jsonPath = getJsonPath(value, offSet)this.$emit('on-jsonpath-change', { jsonPath: this.jsonPath })}})},methods: {copyToClipboard() {const notification = this.$Noticeif (this.jsonPath) {navigator.clipboard.writeText(this.jsonPath).then(function () { }, function () {notification.error({title: 'jsonpath copy failed.'});});} else {notification.warning({title: 'There is no jsonpath that can be copied.'});}}}
};
</script>
monaco-editor.js
const colType = { Object, Array }export function getJsonPath(text, offSet) {let pos = 0let stack = []let isInKey = falsewhile (pos < offSet) {const startPos = posswitch (text[pos]) {case '"':const { text: s, pos: newPos } = readString(text, pos)if (stack.length) {const frame = stack[stack.length - 1]if (frame.colType === colType.Object && isInKey) {frame.key = sisInKey = false}}pos = newPosbreakcase '{':stack.push({ colType: colType.Object })isInKey = truebreakcase '[':stack.push({ colType: colType.Array, index: 0 })breakcase '}':case ']':stack.pop()breakcase ',':if (stack.length) {const frame = stack[stack.length - 1]if (frame.colType === colType.Object) {isInKey = true} else {frame.index++}}break}if (pos === startPos) {pos++}}return pathToString(stack);
}function pathToString(path) {let s = '$'try {for (const frame of path) {if (frame.colType === colType.Object) {if (!frame.key.match(/^[a-zA-Z$_][a-zA-Z\d$_]*$/)) {const key = frame.key.replace('"', '\\"')s += `["${frame.key}"]`} else {if (s.length) {s += '.'}s += frame.key}} else {s += `[${frame.index}]`}}return s;} catch (ex) {return '';}
}function isEven(n) {return n % 2 === 0;
}function readString(text, pos) {let i = pos + 1i = findEndQuote(text, i)var textpos = {text: text.substring(pos + 1, i),pos: i + 1}return textpos
}// Find the next end quote
function findEndQuote(text, i) {while (i < text.length) {// console.log('findEndQuote: ' + i + ' : ' + text[i])if (text[i] === '"') {var bt = i// Handle backtracking to find if this quote is escaped (or, if the escape is escaping a slash)while (0 <= bt && text[bt] == '\\') {bt--}if (isEven(i - bt)) {break;}}i++}return i
}export function checkJsonCode(strJsonCode) {let res = '';try {for (let i = 0, j = 0, k = 0, ii, ele; i < strJsonCode.length; i++) {ele = strJsonCode.charAt(i);if (j % 2 === 0 && ele === '}') {// eslint-disable-next-line no-plusplusk--;for (ii = 0; ii < k; ii++) ele = ` ${ele}`;ele = `\n${ele}`;} else if (j % 2 === 0 && ele === '{') {ele += '\n';// eslint-disable-next-line no-plusplusk++;for (ii = 0; ii < k; ii++) ele += ' ';} else if (j % 2 === 0 && ele === ',') {ele += '\n';for (ii = 0; ii < k; ii++) ele += ' ';// eslint-disable-next-line no-plusplus} else if (ele === '"') j++;res += ele;}} catch (error) {res = strJsonCode;}return res;}
使用:
<monacoEditor v-model="form.ruleContent" ></monacoEditor>