使用npm
下载:npm install wangeditor
(注意 wangeditor
全部是小写字母)
<template> <div> <Template :titleName="titleName" :className="className"> <!-- Form --> <div slot="tool"> <Form :model="form" inline> <FormItem prop="articleTitle"> <Input v-model="form.articleTitle" placeholder="请输入文章标题"></Input> </FormItem> <FormItem prop="articleAuther"> <Input v-model="form.articleAuther" placeholder="请输入文章作者"></Input> </FormItem> <FormItem prop="articleStatus"> <Select v-model="form.articleStatus" placeholder="请选择文章状态" clearable style="width:200px" > <Option v-for="item in articleStatusList" :value="item.value" :key="item.value" >{{ item.label }}</Option> </Select> </FormItem> <FormItem> <DatePicker type="daterange" split-panels placeholder="请输入文章创建时间" style="width: 200px" format="yyyy-MM-dd HH:mm:ss" @on-change="time" ></DatePicker> </FormItem> <FormItem> <Button type="primary" icon="ios-search" @click="searchHandle">搜 索</Button> </FormItem> <FormItem> <Button type="success" icon="md-brush" @click="createArticleHandle" v-if="access.includes('article:create')" >新增文章</Button> </FormItem> </Form> </div> <!-- Table --> <div slot="content"> <Table border :columns="tableColumn" :height="tableHeight" :highlight-row="true" :data="tableData" :loading="tableLoading" > <template slot-scope="{row}" slot="businessPriceSlot" v-if="access.includes('article:viewPrice')" > <span v-text="row.businessVo.business4PriceVo.businessPrice===undefined?'':'¥'+row.businessVo.business4PriceVo.businessPrice.toFixed(3)*1000/1000" ></span> </template> <template slot-scope="{row}" slot="totalBusinessPriceSlot" v-if="access.includes('article:viewPrice')" > <span v-text="row.businessVo.business4PriceVo.businessPrice===undefined || row.articleChannelPageViews===undefined?'': '¥'+(row.businessVo.business4PriceVo.businessPrice.toFixed(3)*row.articleChannelPageViews.toFixed(3)).toFixed(3)*1000/1000" ></span> </template> <template slot-scope="{row}" slot="businessNameSlot" >{{row.businessVo.business4PriceVo.businessName}}</template> <template slot-scope="{row}" slot="articleLink"> <a :href="host+row.articleId" target="_blank">{{row.articleTitle}}</a> </template> <template slot-scope="{ row }" slot="articleStatus"> <Tag v-if="0 === row.articleStatus" color="green">草稿</Tag> <Tag v-if="1 === row.articleStatus" color="magenta">已发布</Tag> <Tag v-if="2 === row.articleStatus" color="volcano">已撤销</Tag> </template> <template slot-scope="{ index, row }" slot="action"> <Tooltip content="编辑" placement="top" v-if="access.includes('article:update')"> <Button type="info" icon="ios-create-outline" size="small" @click="updateArticleHandle(index, row)" ></Button> </Tooltip> <Tooltip content="预览文章" placement="top" v-if="access.includes('article:view')"> <Button type="primary" size="small" icon="md-eye" ghost @click="lookArticleHandle(index, row)" ></Button> </Tooltip> <Tooltip v-if="0!==row.articleStatus && access.includes('article:updateStatus')" :content="2!==row.articleStatus?'撤销':'恢复'" placement="top" > <Button :type="2!==row.articleStatus?'warning':'primary'" size="small" :icon="2!==row.articleStatus?'md-eye-off':'md-refresh'" @click="isWithdrawArticle(index, row)" ></Button> </Tooltip> <Tooltip content="删除" placement="top" v-if="access.includes('article:delete')"> <Button type="error" size="small" icon="md-trash" @click="removeArticleHandle(index, row)" ></Button> </Tooltip> <Tooltip content="设置浏览量" placement="top" v-if="access.includes('article:setPageViews')"> <Button type="success" ghost size="small" icon="md-create" @click="setArticleViewsHandle(index, row)" ></Button> </Tooltip> <Tooltip content="修改文章投放渠道" placement="top" v-if="access.includes('article:updateChannel')" > <Button type="success" size="small" icon="ios-add-circle-outline" @click="setArticleChannelHandle(index, row)" ></Button> </Tooltip> </template> </Table> </div> <!-- Tool --> <div slot="foot"> <Page :total="total" show-elevator :page-size="form.size" :page-size-opts="pageSizeOpts" show-total @on-change="pageChangeHandle" /> </div> </Template> <ArticleModal ref="articleModal" @create="createArticleSuccess" @update="updateArticleSuccess"></ArticleModal> <ArticlePreview ref="articlePreview"></ArticlePreview> <ArticleViewsModal ref="articleViewsModal" @success="setArticleViewsSuccessHandle" /> <ArticleChannelModal ref="articleChannelModal" @success="setArticleChannelSuccessHandle" /> </div> </template> <script> import Template from "@/components/page/page-templete"; import ArticleModal from "./article-modal"; import expandRow from "./table-expand.vue"; import ArticlePreview from "./article-preview"; import ArticleViewsModal from "./article-view-set-modal"; import ArticleChannelModal from "./article-add-channel-modal"; import { queryArticleList, deleteArticle, withdrawArticle, queryBusinessList } from "@/api/article"; import { responseHandle } from "@/libs/util"; import { listChannelsApi } from "@/api/channel"; export default { components: { Template, ArticleModal, expandRow, ArticlePreview, ArticleViewsModal, ArticleChannelModal }, data() { return { host: "http://" + window.location.host + "/article?id=", titleName: "文章管理", className: "article", businessList: [], channelList: [], viewForm: { articleId: "", articlePageViews: "" }, form: { articleTitle: "", articleAuther: "", articleStatus: "", current: 1, size: 15 }, tableHeight: 550, tableLoading: true, tableColumn: [ { type: "expand", width: 50, render: (h, params) => { return h(expandRow, { props: { row: params.row } }); } }, // { title: "编号", key: "articleId" }, { title: "标题", slot: "articleLink" }, { title: "作者", key: "articleAuther" }, { title: "商户名称", slot: "businessNameSlot" }, { title: "标签", key: "articleLalbel" }, // { // title: "浏览量", // slot: "articlePageViewsSlot", // align: "center" // }, { title: "浏览量", key: "articlePageViews" }, { title: "渠道总浏览量", key: "articleChannelPageViews" }, { title: "单价", slot: "businessPriceSlot" }, { title: "总价", slot: "totalBusinessPriceSlot" }, // { // title: "渠道总浏览量", // slot: "articleChannelPageViewsSlot", // align: "center" // }, { title: "状态", slot: "articleStatus" }, { title: "操 作", slot: "action", width: 330, align: "left" } ], tableData: [], total: 0, pageSizeOpts: [15, 30, 50], rowIndex: -1, balanceFormModalStatus: false, balanceFormLoading: true, articleStatusList: [ { value: "0", label: "草稿" }, { value: "1", label: "已发布" }, { value: "2", label: "已撤销" } ], articleStatus: "" }; }, computed: { access() { return this.$store.state.user.access; } }, mounted() { this.hideTableColumnHeader4Business() this.tableHeight = window.innerHeight - 315; this.loadArticleList(false); this.loadBusinessList(); this.loadChannelList(); }, methods: { hideTableColumnHeader4Business() { if (!this.access.includes("article:viewPrice")) { for (var i in this.tableColumn) { if (this.tableColumn[i].title === "单价") { this.tableColumn.splice(i, 1); } if (this.tableColumn[i].title === "总价") { this.tableColumn.splice(i, 1); } } } }, loadChannelList() { responseHandle(this, listChannelsApi, null, false) .then(({ status, data }) => { if (status === 200) { this.channelList = data; } }) .catch(e => { this.resetLoading(); }); }, setArticleChannelHandle(index, row) { this.$refs.articleChannelModal.open(index, row, this.channelList); }, setArticleChannelSuccessHandle(index, data) { // this.$set(this.tableData, index, data); this.loadArticleList(false); }, setArticleViewsHandle(index, row) { this.$refs.articleViewsModal.open(index, row); }, setArticleViewsSuccessHandle(index, data) { // var oldData = { ...this.tableData[index] }; // this.$set(data, "channelVo", oldData.channelVo); // this.$set(this.tableData, index, data); this.loadArticleList(false); }, loadBusinessList() { responseHandle(this, queryBusinessList, null, false) .then(({ status, data }) => { if (status === 200) { this.businessList = data; } }) .catch(() => {}); }, time(val) { this.form.startTime = val[0]; this.form.endTime = val[1]; }, loadArticleList(param) { var showMessage = param ? true : param; this.tableLoading = true; responseHandle(this, queryArticleList, this.form, showMessage) .then(({ status, data }) => { if (status === 200) { this.tableData = data.records; this.total = data.total; } this.tableLoading = false; }) .catch(() => { this.tableLoading = false; }); }, searchHandle() { this.form.current = 1; this.loadArticleList(); }, //新增文章 createArticleHandle() { this.rowIndex = -1; this.$refs.articleModal.open(0, "", this.businessList); }, createArticleSuccess(data) { this.tableData.unshift(data); // this.loadArticleList(); }, updateArticleSuccess(data) { // var oldRowData = { ...this.tableData[this.rowIndex] }; // this.$set(data, "channelVo", oldRowData.channelVo); // this.$set(data, "businessVo", oldRowData.businessVo); // this.tableData.splice(this.rowIndex, 1, data); this.loadArticleList(false); }, //修改文章 updateArticleHandle(index, row) { this.rowIndex = index; this.$refs.articleModal.open(1, row, this.businessList); }, //查看文章详情 lookArticleHandle(index, row) { this.rowIndex = index; this.$refs.articlePreview.open(row); }, pageChangeHandle(page) { this.form.current = page; this.loadArticleList(); }, isWithdrawArticle(index, row) { this.$Modal.confirm({ title: "警 告", content: `确认要${ row.articleStatus === 2 ? "恢复" : "撤销" } <span class="confirm-keyword">${row.articleTitle}</span> 吗?`, onOk: () => { withdrawArticle(row.articleId) .then(({ status, message, data }) => { if (status === 200) { // this.loadArticleList(); this.$set( data, "articleChannelPageViews", row.articleChannelPageViews ); this.$set(data, "businessVo", row.businessVo); this.$set(data, "channelVo", row.channelVo); this.tableData.splice(index, 1, data); this.$Message["success"]({ background: true, content: message || "操作成功", duration: 2.5 }); } else { this.$Notice.error({ title: message, desc: data ? `• ${data}` : "• 操作失败" }); } }) .catch(e => { this.$Message["error"]({ background: true, content: e && e.message ? e.message : "操作失败", duration: 2.5 }); }); } }); }, removeArticleHandle(index, row) { this.$Modal.confirm({ title: "警 告", content: `确认要删除 <span class="confirm-keyword">${"文章:" + row.articleTitle}</span> 吗?`, onOk: () => { deleteArticle(row.articleId) .then(({ status, message, data }) => { if (status === 200) { this.loadArticleList(); // this.$Message["success"]({ // background: true, // content: message, // duration: 2.5 // }); } else { this.$Notice.error({ title: message, desc: `• ${data}` }); } }) .catch(e => { this.$Message["error"]({ background: true, content: e && e.message ? e.message : "操作失败", duration: 2.5 }); }); } }); } } }; </script> <style> .confirm-keyword { color: red; font-weight: bold; } .ivu-tooltip { margin-left: 5px; } </style>
<template> <Modal v-model="show" :title="title" :mask-closable="false" :loading="modalLoading" :width="50" @on-ok="ok" @on-cancel="cancel" > <Form ref="articleForm" :model="form" :label-width="80" :rules="ruleValidate" class="article-form" :disabled="onlyRead" :label-colon="true" > <FormItem label="商户" prop="businessId"> <Select v-model="form.businessId" placeholder="请选择与文章关联的商户" filterable> <Option v-for="item in businessList" :value="item.businessId" :key="item.businessId" >{{ item.businessName }}</Option> </Select> </FormItem> <FormItem label="标题" prop="articleTitle"> <Input v-model="form.articleTitle" placeholder="请输入标题"></Input> </FormItem> <FormItem label="作者" prop="articleAuther"> <Input v-model="form.articleAuther" placeholder="请输入作者"></Input> </FormItem> <FormItem label="标签" prop="labelTextArr"> <Tag v-for="item in labelTextArr" :key="item" :name="item" closable @on-close="handleClose2" >{{item}}</Tag> <Button icon="ios-add" type="dashed" size="small" @click="handleAdd">添加标签</Button> </FormItem> <FormItem label="内容" prop="articleContent"> <weang-editor :onlyRead="onlyRead" :content="text" :catchData="catchData"></weang-editor> </FormItem> <FormItem> <Button v-if="this.type===0 || this.type===1 " @click="saveToDraft">保存为草稿</Button> </FormItem> </Form> <Modal v-model="isShow" title="请输入文章标签" @on-ok="labelOk" @on-cancel="labelCancel"> <Input v-model="articleLalbel" placeholder="请输入一个文章标签"></Input> </Modal> </Modal> </template> <script> import { saveArticle } from "@/api/article"; import { responseHandle } from "@/libs/util"; import weangEditor from "./wangEditor"; export default { components: { "weang-editor": weangEditor }, data() { return { type: 0, onlyRead: false, articleLalbel: null, isShow: false, labelTextArr: [], show: false, title: "", text: "", modalLoading: true, businessList: [], form: { articleId: "", articleTitle: "", articleAuther: "", articleLalbel: "", articleContent: "", articleStatus: "", businessId: "" }, ruleValidate: { businessId: [ { required: true, message: "与文章关联的商户不能为空", trigger: "blur" } ], articleTitle: [ { required: true, message: "标题不能为空", trigger: "blur" }, { type: "string", min: 1, max: 100, message: "标题长度需要在1和100之间", trigger: "blur" } ], articleAuther: [ { required: true, message: "作者不能为空", trigger: "blur" }, { type: "string", min: 1, max: 20, message: "作者长度需要在1和20之间", trigger: "blur" } ], articleContent: [ { required: true, message: "文章内容不能为空", trigger: "blur" } ] } }; }, methods: { saveToDraft() { this.form.articleStatus = 0; this.ok(); }, catchData(html) { this.form.articleContent = html; }, labelOk() { if (this.articleLalbel !== null) { const index = this.labelTextArr.indexOf(this.articleLalbel); if (this.labelTextArr.length > 4) { this.$Message.warning("标签数量不能超过5个"); return; } if (this.articleLalbel.length > 10) { this.$Message.warning("标签长度不能超过10"); this.articleLalbel = null; return; } if (index > -1) { this.$Message.warning("标签【" + this.articleLalbel + "】已存在"); } else { this.labelTextArr.push(this.articleLalbel); } this.articleLalbel = null; } // this.$Message.info("Clicked ok"); }, labelCancel() { // this.$Message.info("Clicked cancel"); }, handleAdd() { this.isShow = true; }, handleClose2(event, name) { if (this.onlyRead) { return; } const index = this.labelTextArr.indexOf(name); this.labelTextArr.splice(index, 1); }, open(type, article, businessList) { this.onlyRead = false; this.form.articleStatus = ""; this.type = type; if (0 === type) { this.title = "新增文章"; this.text = "<p></p>"; this.businessList = businessList; } else if (1 === type) { this.title = "修改文章"; const { articleTitle, articleAuther, articleContent, articleId } = article; const { businessId } = article.businessVo; this.businessList = businessList; this.text = article.articleContent; this.form = { articleTitle, articleAuther, articleContent, articleId, businessId }; //回显标签 if (article.articleLalbel !== "") { this.labelTextArr = article.articleLalbel.split(","); } } else { //查看文章详情 this.title = "文章详情"; this.onlyRead = true; this.text = article.articleContent; const { articleTitle, articleAuther, articleContent } = article; this.form = { articleTitle, articleAuther, articleContent }; //回显标签 if (article.articleLalbel !== "") { this.labelTextArr = article.articleLalbel.split(","); } } this.show = true; }, close() { this.onlyRead = false; this.$refs["articleForm"].resetFields(); this.show = false; this.labelTextArr = []; }, ok() { this.$refs["articleForm"].validate(valid => { if (this.type === 0) { //新增 if (valid) { //数组转字符串 this.form.articleLalbel = this.labelTextArr.toString(); responseHandle(this, saveArticle, this.form) .then(({ status, data }) => { if (200 === status) { this.$emit("create", data); this.close(); } this.resetLoading(); }) .catch(() => { this.resetLoading(); }); } else { this.$Message.error("表单填写有误"); this.resetLoading(); } } if (this.type === 1) { //编辑 if (valid) { //数组转字符串 this.form.articleLalbel = this.labelTextArr.toString(); responseHandle(this, saveArticle, this.form) .then(({ status, data }) => { if (200 === status) { this.$emit("update", data); this.close(); } this.resetLoading(); }) .catch(() => { this.resetLoading(); }); } else { this.$Message.error("表单填写有误"); this.resetLoading(); } } if (this.type === 2) { //查看 this.close(); } }); }, cancel() { this.close(); }, resetLoading() { this.modalLoading = false; this.$nextTick(() => { this.modalLoading = true; }); } } }; </script> <style> .article-form { padding-right: 12px; } .ivu-input[disabled], fieldset[disabled] .ivu-input { color: #aaacb1; } .ivu-select-selected-value { color: #aaacb1; } </style>
<template> <div id="wangeditor"> <div ref="editorElem" style="text-align:left;"></div> </div> </template> <script> import E from "wangeditor"; export default { name: "Editor", data() { return { editor: null }; }, // catchData是一个类似回调函数,来自父组件,当然也可以自己写一个函数,主要是用来获取富文本编辑器中的html内容用来传递给服务端 props: ["catchData", "onlyRead", "content"], // 接收父组件的方法 mounted() { this.editor = new E(this.$refs.editorElem); // 编辑器的事件,每次改变会获取其html内容 this.editor.customConfig.onchange = html => { this.catchData(html); // 把这个html通过catchData的方法传入父组件 }; this.editor.customConfig.menus = [ // 菜单配置 "head", // 标题 "bold", // 粗体 "fontSize", // 字号 "fontName", // 字体 "italic", // 斜体 "underline", // 下划线 "strikeThrough", // 删除线 "foreColor", // 文字颜色 "backColor", // 背景颜色 "link", // 插入链接 "list", // 列表 "justify", // 对齐方式 // "quote", // 引用 // "emoticon", // 表情 "image", // 插入图片 "table", // 表格 "code", // 插入代码 "undo", // 撤销 "redo" // 重复 ]; //显示本地上传图片Tab // this.editor.customConfig.uploadImgShowBase64 = true; //隐藏网络图片Tab this.editor.customConfig.showLinkImg = false; //限制一次最多能传几张图片 this.editor.customConfig.uploadImgMaxLength = 10; // this.editor.customConfig.onchange = function (html) { // // html 即变化之后的内容 // console.log(html) // this.$emit('catchData',html) // } // this.editor.customConfig.uploadImgShowBase64 = false this.editor.customConfig.uploadImgServer = "/api/pictrues"; // 将图片大小限制为 3M this.editor.customConfig.uploadImgMaxSize = 3 * 1024 * 1024; // 跨域上传中如果需要传递 cookie 需设置 withCredentials this.editor.customConfig.withCredentials = true; // 将 timeout 时间改为 3s // this.editor.customConfig.uploadImgTimeout = 3000; this.editor.customConfig.debug = true; this.editor.customConfig.uploadFileName = "file"; //上传参数 自定义 this.editor.customConfig.uploadImgHooks = { before: function(xhr, editor, files) { // 图片上传之前触发 // xhr 是 XMLHttpRequst 对象,editor 是编辑器对象,files 是选择的图片文件 // 如果返回的结果是 {prevent: true, msg: 'xxxx'} 则表示用户放弃上传 // return { // prevent: true, // msg: '放弃上传' // } }, success: function(xhr, editor, result) { // 图片上传并返回结果,图片插入成功之后触发 // xhr 是 XMLHttpRequst 对象,editor 是编辑器对象,result 是服务器端返回的结果 }, fail: function(xhr, editor, result) { // 图片上传并返回结果,但图片插入错误时触发 this.$message("图片插入错误"); // xhr 是 XMLHttpRequst 对象,editor 是编辑器对象,result 是服务器端返回的结果 }, error: function(xhr, editor) { // 图片上传出错时触发 this.$message("图片上传出错"); // xhr 是 XMLHttpRequst 对象,editor 是编辑器对象 }, timeout: function(xhr, editor) { // 图片上传超时时触发 this.$message("上传图片超时"); // xhr 是 XMLHttpRequst 对象,editor 是编辑器对象 } // 如果服务器端返回的不是 {errno:0, data: [...]} 这种格式,可使用该配置 // (但是,服务器端返回的必须是一个 JSON 格式字符串!!!否则会报错) // customInsert: function(insertImg, result, editor) { // // 图片上传并返回结果,自定义插入图片的事件(而不是编辑器自动插入图片!!!) // // insertImg 是插入图片的函数,editor 是编辑器对象,result 是服务器端返回的结果 // // 举例:假如上传图片成功后,服务器端返回的是 {url:'....'} 这种格式,即可这样插入图片: // for (var i = 0; i < result.data.length; i++) { // debugger // insertImg(result.data[i]); // } // result 必须是一个 JSON 格式字符串!!!否则报错 // } }; this.editor.create(); // 创建富文本实例 //禁用编辑器 // this.editor.$textElem.attr('contenteditable',false); }, watch: { onlyRead(val, oldVal) { val ? this.editor.$textElem.attr("contenteditable", false) : this.editor.$textElem.attr("contenteditable", true); }, content(val) { this.editor.txt.html(val); } } }; </script>
更多Api以及使用方法请访问wangEditor文档:https://www.kancloud.cn/wangfupeng/wangeditor3/332599