前端富文本编译器使用总结:
UEditor:百度前端的开源项目,功能强大,基于 jQuery,但已经没有再维护,而且限定了后端代码,修改起来比较费劲
bootstrap-wysiwyg:微型,易用,小而美,只是 Bootstrap + jQuery...
kindEditor:功能强大,代码简洁,需要配置后台,而且好久没见更新了
wangEditor:轻量、简洁、易用,但是升级到 3.x 之后,不便于定制化开发。不过作者很勤奋,广义上和我是一家人,打个call
quill:本身功能不多,不过可以自行扩展,api 也很好懂,如果能看懂英文的话...
summernote:没深入研究,UI挺漂亮,也是一款小而美的编辑器,可是我需要大的
在这里着重说一下这个 tinymce这个插件,
优势有三:
1\. GitHub 上星星很多,功能也齐全;
2\. 唯一一个从 word 粘贴过来还能保持绝大部分格式的编辑器;
3\. 不需要找后端人员扫码改接口,前后端分离;
上代码(vue中使用)
1.引入(两个都得引入)
npm install @tinymce/tinymce-vue -S
npm install tinymce -S
2.在 node_modules 中找到 tinymce/skins 目录,然后将 skins 目录拷贝到 static 目录下
// 如果是使用 vue-cli 3.x 构建的 typescript 项目,就放到 public 目录下,文中所有 static 目录相关都这样处理
3.给你们个语言包(https://www.tiny.cloud/download/language-packages/)
4.然后将这个语言包放到 static 目录下,为了结构清晰,我包了一层 tinymce 目录
5.import
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/modern/theme'
//import "tinymce/themes/silver";
//如果有报错的话可以把import 'tinymce/themes/modern/theme'换成import "tinymce/themes/silver";
//如果控制台提示:icons.js文件中Uncaught SyntaxError: Unexpected token <:报错
//就加上import 'tinymce/icons/default';
// 如果还是不好使的话就改成
// import 'tinymce/icons/default/icons'
import 'tinymce/icons/default';
import Editor from '@tinymce/tinymce-vue'
tinymce-vue 是一个组件,需要在 components 中注册,然后直接使用
<editor id="tinymce" v-model="tinymceHTML" :init="tinymceInit"></editor>
这里的 init 是 tinymce 初始化配置项,后面会讲到一些关键的 api,完整 api 可以参考https://www.tiny.cloud/docs/configure/
编辑器需要一个 skin 才能正常工作,所以要设置一个 skin_url 指向之前复制出来的 skin 文件
data () {
return {
tinymceHtml: '请输入内容',
init: {
language_url: '/static/langs/tinymce/zh_CN.js',
language: 'zh_CN',
skin_url: '/static/tinymce/skins/ui/oxide',
height: 300,
plugins: 'link lists image code table colorpicker textcolor wordcount contextmenu',
toolbar: 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo | link unlink image code | removeformat',
branding: false
}
}
},
6.同时在 mounted 中也需要初始化一次:
mounted(){
tinymce.init({}) // 特别注意这个空对象的存在,如果这个初始化空对象不存在依旧会报错
}
效果如下图:
富文本框上传图片
PS:images_upload_handler自定义上传图片函数能成功调用,automatic_uploads必须设置为 true,我也是踩了坑才知道的,晕死😵
automatic_uploads: true,
images_upload_handler: (blobInfo, success, failure)=> {
let files = {};
// 此处我是上传了阿里云oss
files = new window.File([blobInfo.blob()], blobInfo.blob().name, {type: blobInfo.blob().type});
aliupload().upload(files, blobInfo.blob().name ? blobInfo.blob().name : "").then(res=>{
if (res.res.statusCode == '200') {
success(res.res.requestUrls[0].split('?')[0]);
failure('上传失败')
} else {
failure('上传失败')
this.$message.warning('上传图片失败!')
}
})
}
完整代码如下:
<template>
<div>
<MenuPanel></MenuPanel>
<DataPanel>
<div>
<el-form
label-width="100%"
class="demo-ruleForm"
:label-position="labelPosition"
v-model="searchValue"
>
<el-row>
<el-col :span="5">
<el-row>
<el-col :span="8">
<el-form-item label="二级分类名称" prop="name"></el-form-item>
</el-col>
<el-col :span="16">
<el-input
v-model="searchValue.name"
placeholder="请输入二级分类名称"
></el-input>
</el-col>
</el-row>
</el-col>
</el-row>
</el-form>
</div>
<editor id="tinymce" v-model="tinymceHtml" :init="init"></editor>
<div class="anniu">
<el-row>
<el-col :span="15">
<el-button type="primary" size="small" icon="el-icon-view" @click="preview">预览</el-button>
<el-button
type="primary"
size="small"
class="line-btn"
icon="el-icon-document"
@click="save"
>保存</el-button>
</el-col>
</el-row>
</div>
<el-dialog
:title="searchValue.name"
:visible.sync="dialogVisible"
width="50%"
:show-close="false"
custom-class="dialogVisible"
:close-on-click-modal="false"
top="5vh"
>
<div v-html="tinymceHtml"></div>
<div slot="footer" class="dialog-footer">
<el-button
type="primary"
plain
icon="el-icon-close"
class="line-btn"
@click="closeTinymceHtml"
>关 闭</el-button>
</div>
</el-dialog>
</DataPanel>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit, Watch } from "vue-property-decorator";
import DataPanel from "../../components/DataPanel.vue";
import MenuPanel from "./component/MenuPanel.vue";
import * as carManage from '../../store/modules/carManage'
import { default as aliupload } from '../../common/util/ossUploadService'
import tinymce from "tinymce/tinymce";
//import "tinymce/themes/silver";
import "tinymce/themes/silver/theme";
import 'tinymce/icons/default';
import Editor from "@tinymce/tinymce-vue";
import "tinymce/plugins/code";
import "tinymce/plugins/table";
import "tinymce/plugins/lists";
import "tinymce/plugins/contextmenu";
import "tinymce/plugins/wordcount";
import "tinymce/plugins/colorpicker";
import "tinymce/plugins/textcolor";
import 'tinymce/plugins/image'
import 'tinymce/plugins/imagetools'
import 'tinymce/plugins/importcss'
import 'tinymce/plugins/paste'
@Component({
components: {
DataPanel,
Editor,
MenuPanel
}
})
export default class InstructionsEditPanel extends Vue {
public labelPosition: string = "right";
public searchValue = {
id: "",
name: ""
};
public dialogVisible: boolean = false;
public Editortext: string = "";
public tinymceHtml: string = "";
public init = {
language_url: "/static/tinymce/langs/zh_CN.js",
language: "zh_CN",
skin_url: "/static/tinymce/skins/ui/oxide",
height: 600,
menubar: false, //顶部菜单栏显示
plugins: "lists image imagetools importcss code table colorpicker textcolor wordcount contextmenu paste",
toolbar: "bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo | removeformat | table | image",
branding: false,
automatic_uploads: true,
paste_data_images: true,
paste_retain_style_properties: 'color',
paste_word_valid_elements: "table[width|border|border-collapse],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody,h1,h2,h3,h4,h5,h6,span,strong,p,div",
images_upload_handler: (blobInfo, success, failure)=> {
let files = {};
files = new window.File([blobInfo.blob()], blobInfo.blob().name, {type: blobInfo.blob().type});
aliupload().upload(files, blobInfo.blob().name ? blobInfo.blob().name : "").then(res=>{
if (res.res.statusCode == '200') {
success(res.res.requestUrls[0].split('?')[0]);
failure('上传失败')
} else {
failure('上传失败')
this.$message.warning('上传图片失败!')
}
})
}
};
@carManage.Action
public getTwoLevelDetail: (payload: carManage.deleteVehicleTypePayload) => Promise<any>;
@carManage.Action
public editTwoLevelcation: (payload: carManage.getTwoLevelDetailPayload) => Promise<any>;
// 预览
public preview() {
this.dialogVisible = true;
if(this.tinymceHtml.indexOf('#000000') > -1){
this.tinymceHtml = this.tinymceHtml.replace(/#000000/g, "#ffffff")
}
}
public closeTinymceHtml(){
this.dialogVisible = false;
if(this.tinymceHtml.indexOf('#fffff') > -1){
this.tinymceHtml = this.tinymceHtml.replace(/#ffffff/g, "#000000")
}
}
public encode(str) {
// 对字符串进行编码
var encode = encodeURI(str);
// 对编码的字符串转化base64
var base64 = btoa(encode);
return base64;
}
// base64转字符串
public decode(base64) {
// 对base64转编码
var decode = atob(base64);
// 编码转字符串
var str = decodeURI(decode);
return str;
}
// 保存
public save() {
this.$confirm('是否保存当前内容?', {
confirmButtonText: '确认',
cancelButtonText: '取消',
center: true
}).then(() => {
if(this.tinymceHtml.indexOf('<img') > -1 || this.tinymceHtml.indexOf('#000000') > -1){
this.tinymceHtml = this.tinymceHtml.replace(/<img/g, "<img style='max-width:100%;'").replace(/#000000/g, "#ffffff")
}
this.editTwoLevelcation({
id: this.searchValue.id,
name: this.searchValue.name,
twoLevelRemark: this.tinymceHtml
}).then(res=>{
this.$message({ message: '保存成功', type: 'success' });
this.loadData();
this.$root.$emit('updateDendrogram');
}).catch(()=>{})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消保存'
});
});
}
public mounted() {
this.searchValue.id = this.$route.query.id;
tinymce.init({});
this.loadData();
}
public loadData() {
this.getTwoLevelDetail({id: this.searchValue.id}).then(res=>{
this.searchValue.name = res ? res.name : '';
// this.tinymceHtml = res ? this.decode(res.twoLevelRemark) : '';
this.tinymceHtml = res ? res.twoLevelRemark : '';
if(this.tinymceHtml.indexOf('#fffff') > -1){
this.tinymceHtml = this.tinymceHtml.replace(/#ffffff/g, "#000000")
}
})
}
@Watch("$route")
routechange(to: any, from: any) {
this.searchValue.id = this.$route.query.id;
this.loadData();
}
}
</script>
<style lang="scss" scoped>
.line-btn {
background-color: #3563c5;
border-color: #3563c5;
color: #fff;
}
.line-btn:hover,
.line-btn:focus {
background-color: #3563c5;
border-color: #3563c5;
opacity: 0.7;
color: #fff;
}
/deep/ .anniu {
padding: 10px 0;
}
/deep/ .dialogVisible {
background: #152025;
.el-dialog__header {
border-bottom: 0;
text-align: center;
.el-dialog__title {
color: #fff;
font-weight: normal;
}
}
}
/deep/ .el-dialog__body{
color: #fff;
}
/deep/ img{
max-width: 100%;
}
</style>
其中的带plugins为扩展性操作,如果不需要,可以不引入。