从零开始使用vue2+element搭建后台管理系统(动态表单实现(含富文本框【vue-quill-editor】【解决vue-quill-editor视频的显示问题】))[待完善]
在后台项目的实际开发过程中,涉及到表单的部分通常会使用动态渲染的方案进行实现,由后端接口返回表单配置,前端进行遍历渲染。考虑到通用后台需要具备的功能,除了基础的表单项如输入、下拉、多选、开关、时间、日期等,还需要具备上传、富文本框等功能。
首先导入一个百度来的富文本框插件:npm install vue-quill-editor --save
(官方文档:https://www.kancloud.cn/liuwave/quill/1434140)
然后在main.js中进行引入:
// 引入富文本组件 import QuillEditor from "vue-quill-editor"; // 引入富文本组件样式 import "quill/dist/quill.core.css"; import "quill/dist/quill.snow.css"; import "quill/dist/quill.bubble.css"; Vue.use(QuillEditor);
然后就可以在components文件夹下新建动态表单组件了:
<template> <div class="filterPanel"> <!--是否行内表单--> <el-form :class="!inline ? 'form' : ' form form-inline'" :inline="inline" :model="form" :rules="rules" :label-width="labelWidth" :label-position="!inline ? 'top' : 'left'" ref="form" > <!--标签显示名称--> <div class="labelGroup"> <slot></slot> <el-form-item v-for="item in formLabel" :key="item.model" :label="item.label" :prop="item.model" > <!--根据type来显示是什么标签--> <!-- 默认输入框 --> <el-input v-model="form[item.model]" v-if="item.type === 'input'" :placeholder="item.placeholder || '请输入' + item.label" :maxlength="item.props?.maxLength" :show-word-limit="item.props?.showWordLimit || false" > </el-input> <!-- 区域输入框 --> <el-input v-model="form[item.model]" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" :show-word-limit="item.props?.showWordLimit || true" v-if="item.type === 'textarea'" :rows="item.props?.rows || 2" :maxlength="item.props?.maxLength" :placeholder="item.placeholder || '请输入' + item.label" ></el-input> <!-- 数字输入框 --> <el-input v-model="form[item.model]" :min="0" type="number" :placeholder="item.placeholder || '请输入' + item.label" v-if="item.type === 'number'" > </el-input> <!-- 动态搜索框 --> <el-autocomplete class="inline-input" v-model="form[item.model]" v-if="item.type === 'searchInput'" :fetch-suggestions=" (queryString, cb) => { searchOptionName(queryString, cb, item.opts); } " :placeholder="item.placeholder || '请输入' + item.label" :trigger-on-focus="false" ></el-autocomplete> <!-- 下拉框 --> <el-select v-model="form[item.model]" :placeholder="item.placeholder || '请选择' + item.label" v-if="item.type === 'select'" :multiple="item.props?.multiple || false" > <el-option v-for="item in item.opts" :key="item.value" v-show="item.label" :label="item.label" :value="item.value" ></el-option> </el-select> <!-- 开关 --> <el-switch v-model="form[item.model]" v-if="item.type === 'switch'" ></el-switch> <!-- 单选框 --> <el-radio-group v-model="form[item.model]" v-if="item.type === 'radio'" > <el-radio v-for="item in item.opts" :key="item.value" :label="item.label" ></el-radio> </el-radio-group> <!-- 复选框 --> <el-checkbox-group v-model="form[item.model]" v-if="item.type === 'checkbox'" size="small" > <el-checkbox-button v-for="child in item.opts" :label="child.value" :key="child.value" >{{ child.label }}</el-checkbox-button > </el-checkbox-group> <!-- 单个日期选择器 --> <el-date-picker v-model="form[item.model]" type="date" placeholder="选择日期" v-if="item.type === 'date'" value-format="yyyy-MM-dd" > </el-date-picker> <!-- 单个日期时间选择器 --> <el-date-picker v-model="form[item.model]" type="datetime" placeholder="选择日期时间" v-if="item.type === 'dateRangeTime'" value-format="yyyy-MM-dd HH:mm:ss" > </el-date-picker> <!-- 日期范围选择器 --> <el-date-picker v-model="form[item.model]" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" type="daterange" placeholder="选择日期" v-if="item.type === 'dateRange'" value-format="yyyy-MM-dd" :picker-options="item.props?.pickerOptions || null" > </el-date-picker> <!-- 日期时间选择器 --> <el-date-picker v-model="form[item.model]" type="datetimerange" range-separator="至" v-if="item.type === 'dateTimeRange'" start-placeholder="开始日期及时间" end-placeholder="结束日期及时间" value-format="yyyy-MM-dd HH:mm:ss" > </el-date-picker> <!-- 级联选择 --> <el-cascader v-model="form[item.model]" :placeholder="item.placeholder || '请选择' + item.label" v-if="item.type === 'cascader'" :options="item.opts" :props="item.props" clearable > </el-cascader> <!-- 图片地址上传 --> <UploadImgUrl v-if="item.type === 'uploadImgUrl'" v-model="form[item.model]" /> <!-- 文件上传 【ps 目前后端接口只做了单个上传】 --> <UploadFile v-if="item.type === 'upload'" v-model="form[item.model]" fieldName="cardUpload" prefix="cardUpload" :uploadProps="item.props || {}" /> <!-- 富文本框 --> <ArticleEditor v-if="item.type === 'content'" v-model="form[item.model]" /> </el-form-item> </div> <!-- 行内时样式【常用于搜索栏 --> <div class="btnGroup" v-if="!hiddenBtn && inline === true"> <el-form-item> <el-button type="primary" @click="search">{{ searchText }}</el-button> <el-button @click="reset">重置</el-button> </el-form-item> </div> <!-- 纵向时样式【常用于新增编辑表单 --> <div class="btnGroup" v-else-if="!hiddenBtn && !inline"> <el-form-item> <el-button type="primary" @click="search">{{ submitText }}</el-button> <el-button @click="reset">重置</el-button> </el-form-item> </div> </el-form> </div> </template> <script> import { getToken } from "@/utils/auth"; import UploadFile from "./UploadFile.vue"; import UploadImgUrl from "./UploadImgUrl.vue"; import ArticleEditor from "./ArticleEditor.vue"; export default { name: "CustomForm", //inline 行内表单域 //form 表单数据 formLabel 标签数据 props: { inline: { type: Boolean, default: true, }, labelWidth: { type: String, default: "", }, searchText: { type: String, default: "搜索", }, submitText: { type: String, default: "提交", }, hiddenBtn: { type: Boolean, default: false, }, formLabel: Array, rules: Object, formValue: Object, }, watch: { formLabel: { handler(newVal) { if (newVal) { newVal.forEach((item) => { this.$set(this.form, item.model, item.default || ""); }); } }, immediate: true, }, formValue: { handler(newVal) { if (newVal) { this.formLabel.forEach((item) => { let defaultValue = ""; if (item.type === "upload") defaultValue = []; this.$set( this.form, item.model, newVal[item.model] || item.default || defaultValue ); }); } }, immediate: true, }, }, data() { return { form: {}, // 设置请求头 headers: { "Access-Control-Allow-Origin": "*", "Admin-Token": getToken() || sessionStorage.getItem("token"), }, }; }, mounted() { let obj = {}; this.formLabel.forEach(async (item, index) => { if (item.optsConfig) { //获取动态下拉选项 let val = await this.getOpts(item); this.$set(this.formLabel[index], "opts", val); obj[item.model] = val; this.$emit("getSelect", obj); } }); }, methods: { searchOptionName(queryString, cb, data) { var restaurants = data; var results = queryString ? restaurants.filter(this.createFilter(queryString)) : restaurants; cb(results); }, createFilter(queryString) { return (restaurant) => { return ( restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) != -1 ); }; }, reset() { this.form.pageNum = 1; this.$refs["form"].resetFields(); this.$emit("confirm", this.form); // Bus.$emit('getParam', this.form);//给Table传查询参数 }, search() { this.form.pageNum = 1; this.$refs["form"].validate(async (valid) => { if (valid) { this.$emit("confirm", this.form); // Bus.$emit('getParam', this.form);//给Table传查询参数 } }); }, getValues() { this.$refs["form"].validate(async (valid) => { if (valid) { return this.form; } else { return false; } }); }, async getOpts(oData) { let { api, param, labelKey, valueKey } = oData.optsConfig; let opts = []; const res = await api(param); if (res.code === 1) { opts = res.data.map((item) => { if (oData.model === "goodsSku" && item["goodsSku"] != "") { //SKU特殊处理 const itemObj = JSON.parse(item.goodsSku); return { label: itemObj[labelKey].join("-"), value: itemObj[valueKey], ...item, }; } else { return { label: item[labelKey], value: item[valueKey], ...item, }; } }); } return opts; }, }, components: { UploadFile, UploadImgUrl, ArticleEditor, }, }; </script> <style lang="scss"> .filterPanel { margin-bottom: 20px; padding: 20px 20px 0 20px; .form { .btnGroup { min-width: 150px; display: flex; flex-direction: row; flex-wrap: nowrap; } } .form-inline { display: flex; justify-content: space-between; } .el-input__inner { background-color: #fff; height: 33px; line-height: 33px; } .filterPanel { width: 100%; border-radius: 4px; background: #f7f8fa; padding-top: 20px; padding-right: 20px; } .btnContainer { margin-bottom: 20px; } .el-button { height: 32px !important; padding: 0 16px; } .el-date-editor .el-range__icon, .el-range-separator { line-height: 26px; } } </style>
上传组件:(这里只具体实现了单个图片上传)
<template> <div> <el-upload class="upload-demo" :headers="headers" :data="uploadData" :action="action" :on-remove="handleRemove" :on-preview="handlePreview" :on-success="handleSuccess" :before-upload="beforeUpload" :multiple="uploadProps.multiple" :limit="uploadProps.limit" :on-exceed="handleExceed" :file-list="fileList" > <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip" style="font-weight: 700"> 仅支持上传<span v-for="(v, i) in fileProps.typeList" :key="i" >.{{ v }}<i v-if="i + 1 < fileProps.typeList.length">、</i></span >等格式文件。<span v-if="uploadProps.width && uploadProps.height"> 尺寸<span>{{ uploadProps.width }}<i>×</i></span >{{ uploadProps.height }},大小在{{ uploadProps.size || 10 }}M内 </span> </div> </el-upload> <el-dialog title="预览图片" :visible.sync="showDialog" center append-to-body > <img :src="imgUrl" style="width: 100%" /> </el-dialog> </div> </template> <script> import { getToken } from "@/utils/auth"; export default { name: "UploadFile", model: { prop: "value", event: "inputFun", }, // list:文件列表,fieldName 字段名, prefix 是 prefix对应的 参数名 props: { value: { type: Array, }, uploadProps: { fieldName: { type: String, default: "", }, prefix: { type: String, default: "", }, limit: { type: Number, default: 1, }, multiple: { type: Boolean, default: false, }, width: { type: Number, default: 1, }, height: { type: Number, default: 1, }, size: { type: Number, default: 10, }, // 允许上传的文件类型 typeList: { type: Array, default: [ "png", "jpg", "jpeg", "gif", "slx", "xlsx", "doc", "docx", "pdf", "mp4", ], }, }, }, data() { return { fileList: [], // 传给后端的参数 uploadData: {}, // 设置请求头 headers: { "Access-Control-Allow-Origin": "*", "Admin-Token": getToken() || sessionStorage.getItem("token"), }, // 设置请求地址 http://10.2.48.241:9413/ [测试环境] action: process.env.NODE_ENV === "production" ? `${process.env.VUE_APP_BASE_API}/admin_api/uploadFile.php` : "http://10.2.48.241:9413/admin_api/uploadFile.php", currentImg: null, fileUrl: null, fileProps: this.uploadProps || {}, showDialog: false, imgUrl: "", }; }, created() { this.fileUrl = "/admin_api/uploadFile.php"; this.uploadData = { prefix: this.uploadProps.prefix || this.fileProps.prefix, game: "ppjt", }; }, watch: { //深度监听 value: { handler(n) { if (n) { if (n && typeof n === "string") { const valueArrList = this.value.split("/"); const dataList = [ { url: this.value, name: valueArrList[valueArrList.length - 1], uid: "0", }, ]; this.fileList = dataList; } else { this.fileList = n; } } else { this.fileList = []; } }, immediate: false, }, uploadProps() { this.fileProps = this.uploadProps; }, }, mounted() { if (this.value && typeof this.value === "string") { const valueArrList = this.value.split("/"); const dataList = [ { url: this.value, name: valueArrList[valueArrList.length - 1], uid: "0", }, ]; this.fileList = dataList; } else { this.fileList = this.value || []; } }, methods: { //删除文件方法 handleRemove(_, fileList) { const dataList = fileList?.map((item) => { return { url: item.response.data.url, name: item.name, uid: item.uid, }; }); this.fileList = dataList; this.$emit("inputFun", dataList); }, handleExceed(files, fileList) { this.$message.warning( `当前限制选择 ${this.uploadProps.limit || 1} 个文件,本次选择了 ${ files.length } 个文件,共选择了 ${files.length + fileList.length} 个文件` ); }, //上传之前做文件类型和大小判断 beforeUpload(file) { if (this.fileList.length >= (this.uploadProps.limit || 1)) { this.$message.warning( `当前限制选择 ${this.uploadProps.limit || 1} 个文件` ); return false; } let arr = file.name.split("."); let istrue = this.uploadProps.typeList.indexOf(arr[arr.length - 1]); if (istrue == -1) { this.$message.warning("文件格式不支持"); return false; } if (file.size > (this.uploadProps.size * 1024 * 1024 || 10485760)) { this.$message.warning( `文件大小不能超过${this.uploadProps.size || 10}MB` ); return false; } // if (this.uploadProps.width && this.uploadProps.height) { // let _this = this; // const isSize = new Promise(function (resolve, reject) { // let _URL = window.URL || window.webkitURL; // let img = new Image(); // img.onload = function () { // let valid = // img.width === this.uploadProps.width && // img.height === this.uploadProps.height; // valid ? resolve() : reject(); // }; // img.src = _URL.createObjectURL(file); // }).then( // () => { // return file; // }, // () => { // _this.$message.warning(`文件宽高不符合要求`); // return Promise.reject(); // } // ); // return isSize; // } }, //完成上传 handleSuccess(response, _, fileList) { // console.log(response, file, fileList, "===完成上传事件"); if (response.code === 0) { const dataList = fileList?.map((item) => { return { url: item.response.data.url, name: item.name, uid: item.uid, }; }); this.fileList = dataList; this.$emit("inputFun", dataList); } else { this.$message.error("上传失败"); return false; } }, handlePreview(file) { this.imgUrl = file.url; this.showDialog = true; }, }, }; </script>
图片链接插入组件:
<template> <div class="inputBox"> <el-input class="inputBoxIpt" v-bind:value="value" :placeholder="placeholder" @input="inputFun" @blur="blurFun" /> <div class="imgSmall"> <el-image style="height: 80px; width: auto" :src="fileUrl" :preview-src-list="[fileUrl]" > </el-image> </div> </div> </template> <script> export default { name: "UploadImgUrl", model: { prop: "value", event: "inputFun", }, props: { value: { type: String, require: false, }, placeholder: { type: String, require: false, default: "请输入", }, RegExpFlag: { type: String, require: false, default: "", }, }, data() { return { fileUrl: "", }; }, watch: { value: { handler(newVal) { if (newVal) { this.fileUrl = newVal; } }, immediate: true, }, }, methods: { inputFun(e) { if (this.RegExpFlag) { this.$emit("inputFun", e.replace(eval(this.RegExpFlag), "")); return; } this.$emit("inputFun", e); }, blurFun(e) { this.$emit("blurFun", e.target.value); this.fileUrl = e.target.value; }, }, }; </script> <style lang="scss" scoped> .inputBox { margin-bottom: 10px; width: 100%; .inputBoxIpt { width: 100%; margin-bottom: 8px; } } </style>
富文本框组件:
<template> <div class=""> <!-- 富文本框 --> <quill-editor ref="myQuillEditor" v-bind:value="value" :placeholder="placeholder" @input="inputFun" class="editor" :options="editorOption" /> <!-- 富文本编辑器中的上传图片控件 --> <el-upload class="avatar-uploader-img" :action="action" :show-file-list="false" :on-success="uploadImgSuccess" :before-upload="beforeUploadImg" :on-error="uploadImgError" :data="{ game: 'ppjt' }" :headers="headers" /> <el-upload class="avatar-uploader-video" :action="action" :show-file-list="false" :on-success="uploadVideoSuccess" :before-upload="beforeUploadVideo" :on-error="uploadVideoError" :data="{ game: 'ppjt' }" :headers="headers" /> </div> </template> <script> import { getToken } from "@/utils/auth"; // import { Quill } from "vue-quill-editor"; // 自定义字体大小 // const Size = Quill.import("attributors/style/size"); // Size.whitelist = [false, "14px", "16px", "18px", "20px", "32px"]; // Quill.register(Size, true); // 工具栏配置 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", "normal", "large", "huge"] }], // 字体大小 // [{ size: Size.whitelist }], // 字体大小 [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题 [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色 [{ font: [] }], // 字体种类 [{ align: [] }], // 对齐方式 ["clean"], // 清除文本格式 ["link", "image", "video"], // 链接、图片、视频 ]; export default { name: "ArticleEditor", model: { prop: "value", event: "inputFun", }, props: { value: { type: String, require: false, default: "", }, placeholder: { type: String, require: false, default: "请输入", }, }, data() { return { headers: { "Access-Control-Allow-Origin": "*", "Admin-Token": getToken() || sessionStorage.getItem("token"), }, editorOption: { // 编辑框操作事件 theme: "snow", // or 'bubble' placeholder: "请输入想发布的内容", imageDrop: true, modules: { toolbar: { container: toolbarOptions, handlers: { image: function (value) { // 上传图片 if (value) { document.querySelector(".avatar-uploader-img input").click(); // 触发input框选择文件 } else { this.quill.format("image", false); } }, link: function (value) { // 添加链接 if (value) { var href = prompt("请输入url"); this.quill.format("link", href); } else { this.quill.format("link", false); } }, video: function (value) { // 上传视频 if (value) { document .querySelector(".avatar-uploader-video input") .click(); // 触发input框选择文件 } else { this.quill.format("video", false); } }, }, }, }, }, action: process.env.NODE_ENV === "production" ? `${process.env.VUE_APP_BASE_API}/admin_api/uploadFile.php` : "http://10.2.48.241:9413/admin_api/uploadFile.php", }; }, watch: { value: { handler(newVal) { if (newVal) { this.fileUrl = newVal; } }, immediate: true, }, }, methods: { //富文本图片上传前 beforeUploadImg(file) { const isJPG = file.type === "image/jpeg" || file.type === "image/png" || file.type === "image/gif" || file.type === "image/webp"; if (!isJPG) { this.$message.error("上传图片只能是 JPG,PNG, GIF 格式!"); } else { // 显示loading动画 this.quillUpdate = true; } return isJPG; }, // 富文本视频上传前 beforeUploadVideo(file) { const fileSize = file.size / 1024 / 1024 < 500; if ( [ "video/mp4", "video/ogg", "video/flv", "video/avi", "video/wmv", "video/rmvb", "video/mov", ].indexOf(file.type) == -1 ) { this.$message.error("请上传正确的视频格式"); return false; } if (!fileSize) { this.$message.error("视频大小不能超过500MB"); return false; } // 富文本框视频上传限制最小宽高均为480px this.isShowUploadVideo = false; // const isVideo = file.type === "video/mp4"; // if (!isVideo) { // this.$message.error("上传视频只能是 mp4 格式!"); // } else { // // 显示loading动画 // this.quillUpdate = true; // } // return isVideo; }, uploadImgSuccess(res) { //富文本图片上传成功 // res为图片服务器返回的数据 // 获取富文本组件实例 const quill = this.$refs.myQuillEditor.quill; // 这里需要注意自己文件上传接口返回内容,code=0表示上传成功,返回的文件地址:res.data.src if (res.code !== 0) { this.$message.error("图片插入失败"); } else { // const range = quill.getSelection(true); // quill.insertEmbed(range.index, "image", res.data.url); // 获取光标所在位置 const length = quill.getSelection(true).index; // // 插入图片 quill.insertEmbed(length, "image", res.data.url); // // 调整光标到最后 quill.setSelection(length + 1); } // loading动画消失 this.quillUpdate = false; }, uploadImgError() { //富文本图片上传失败 // loading动画消失 this.quillUpdate = false; this.$message.error("图片插入失败!"); }, uploadVideoSuccess(res) { // res为图片服务器返回的数据 // 获取富文本组件实例 const quill = this.$refs.myQuillEditor.quill; // 如果上传成功 if (res.code == 0 && res.data.url != null) { // 获取光标所在位置 const length = quill.getSelection(true).index; // 插入图片 res.info为服务器返回的图片地址 quill.insertEmbed(length, "video", res.data.url); // 调整光标到最后 quill.setSelection(length + 1); } else { this.$message.error("视频插入失败"); } // loading动画消失 this.quillUpdate = false; }, uploadVideoError() { // loading动画消失 this.quillUpdate = false; this.$message.error("视频插入失败"); }, inputFun(e) { this.$emit("inputFun", e); }, }, }; </script> <!-- 富文本编辑器 --> <style lang="scss" scoped> .editor { line-height: normal !important; height: 730px; margin-bottom: 30px; } .ql-container { height: 700px !important; } .avatar-uploader-img { height: 0; } .avatar-uploader-video { height: 0; } ::v-deep .ql-snow .ql-tooltip[data-mode="link"]::before { content: "请输入链接地址:"; } ::v-deep .ql-snow .ql-tooltip.ql-editing a.ql-action::after { border-right: 0px; content: "保存"; padding-right: 0px; } ::v-deep .ql-snow .ql-tooltip[data-mode="video"]::before { content: "请输入视频地址:"; } ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-label::before, ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-item::before { content: "14px"; } ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before, ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before { content: "10px"; } ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before, ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before { content: "18px"; } ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before, ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before { content: "32px"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item::before { content: "文本"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { content: "标题1"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { content: "标题2"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { content: "标题3"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { content: "标题4"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { content: "标题5"; } ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { content: "标题6"; } ::v-deep .ql-snow .ql-picker.ql-font .ql-picker-label::before, ::v-deep .ql-snow .ql-picker.ql-font .ql-picker-item::before { content: "标准字体"; } ::v-deep .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before, ::v-deep .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before { content: "衬线字体"; } ::v-deep .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before, ::v-deep .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before { content: "等宽字体"; } /************************************** 富文本编辑器 **************************************/ // ::v-deep .ql-snow .ql-picker { // line-height: 24px; // } // ::v-deep .ql-container { // height: 400px; // overflow: auto; // } // ::v-deep .ql-snow .ql-picker.ql-size .ql-picker-label::before, // ::v-deep.ql-snow .ql-picker.ql-size .ql-picker-item::before { // content: "字号"; // } // ::v-deep.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before, // ::v-deep.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before { // content: "12px"; // } // ::v-deep.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before, // ::v-deep.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before { // content: "14px"; // } // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-label[data-value="16px"]::before, // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-item[data-value="16px"]::before { // content: "16px"; // } // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-label[data-value="18px"]::before, // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-item[data-value="18px"]::before { // content: "18px"; // } // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-label[data-value="20px"]::before, // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-item[data-value="20px"]::before { // content: "20px"; // } // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-label[data-value="32px"]::before, // ::v-deep // .ql-snow // .ql-picker.ql-size // .ql-picker-item[data-value="32px"]::before { // content: "32px"; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label::before, // ::v-deep.ql-snow .ql-picker.ql-header .ql-picker-item::before { // content: "正文" !important; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, // ::v-deep.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { // content: "标题1" !important; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, // ::v-deep.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { // content: "标题2" !important; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, // ::v-deep.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { // content: "标题3" !important; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { // content: "标题4" !important; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, // ::v-deep.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { // content: "标题5" !important; // } // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, // ::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { // content: "标题6" !important; // } </style>
接口返回的数据格式:
searchList: [
{
label: "输入文本",
placeholder: "输入文本",
model: "text",
type: "input",
props: {
showWordLimit: false,
},
},
{
label: "请选择日期",
model: "date",
type: "date",
},
{
label: "请选择日期时间",
placeholder: "请选择日期时间",
model: "dateRangeTime",
type: "dateRangeTime",
},
{
label: "请选择日期范围",
model: "dateRange",
type: "dateRange",
props: {
pickerOptions: {
shortcuts: [
{
text: "今日",
onClick(picker) {
const end = new Date();
const start = new Date();
picker.$emit("pick", [start, end]);
},
},
{
text: "昨日",
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 1);
end.setTime(end.getTime() - 3600 * 1000 * 24 * 1);
picker.$emit("pick", [start, end]);
},
},
{
text: "最近7天",
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit("pick", [start, end]);
},
},
{
text: "最近30天",
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit("pick", [start, end]);
},
},
],
},
},
},
{
label: "请选择时间范围",
model: "time",
type: "dateTimeRange",
},
{
label: "请选择游戏【单选】",
placeholder: "请选择游戏",
model: "game",
type: "select",
opts: [
{
label: "游戏1",
value: "game1",
},
{
label: "游戏2",
value: "game2",
},
],
},
{
label: "请选择游戏【多选】",
placeholder: "请选择游戏",
model: "game_m",
type: "select",
opts: [
{
label: "游戏1",
value: "game1",
},
{
label: "游戏2",
value: "game2",
},
],
props: {
multiple: true,
},
},
{
label: "状态",
model: "status",
type: "switch",
},
{
label: "复选框",
model: "checkbox_type",
type: "checkbox",
default: [], // 当复选框存在多个候选项时需要传默认值为空数组
opts: [
{
label: "平台订单号",
value: 1,
},
{
label: "游戏订单号",
value: 2,
},
],
},
{
label: "单选框",
model: "type_s",
type: "radio",
opts: [
{
label: "平台订单号",
},
{
label: "游戏订单号",
},
],
},
{
label: "请选择地区",
placeholder: "请选择地区",
model: "cascader",
type: "cascader",
opts: [
{
value: 1,
label: "东南",
children: [
{
value: 2,
label: "上海",
children: [
{ value: 3, label: "普陀" },
{ value: 4, label: "黄埔" },
{ value: 5, label: "徐汇" },
],
},
{
value: 7,
label: "江苏",
children: [
{ value: 8, label: "南京" },
{ value: 9, label: "苏州" },
{ value: 10, label: "无锡" },
],
},
{
value: 12,
label: "浙江",
children: [
{ value: 13, label: "杭州" },
{ value: 14, label: "宁波" },
{ value: 15, label: "嘉兴" },
],
},
],
},
{
value: 17,
label: "西北",
children: [
{
value: 18,
label: "陕西",
children: [
{ value: 19, label: "西安" },
{ value: 20, label: "延安" },
],
},
{
value: 21,
label: "新疆维吾尔自治区",
children: [
{ value: 22, label: "乌鲁木齐" },
{ value: 23, label: "克拉玛依" },
],
},
],
},
],
props: {
multiple: true,
},
},
{
label: "请输入文本",
placeholder: "输入文本",
model: "textarea",
type: "textarea",
props: {
rows: 3,
showWordLimit: true,
maxLength: 50,
},
},
{
label: "上传图片",
model: "files",
type: "upload",
props: {
width: 1280,
height: 720,
size: 5,
limit: 1,
multiple: false,
typeList: ["png", "jpg", "jpeg", "webp"],
},
},
{
label: "富文本框",
model: "news",
placeholder: "请输入文章内容",
type: "content",
},
],
searchRules: {
textarea: [{ required: true, message: "请输入文本", trigger: "blur" }],
},
页面中使用:(抽屉中创建表单)
<template>
<el-drawer
:visible.sync="showDrawer"
size="100%"
:title="title"
@open="handleOpen"
@close="handleSave"
>
<div class="drawer-body">
<div class="drawer-form">
<CustomForm
ref="form"
:inline="false"
:formLabel="formList"
:rules="formRules"
:formValue="defaultInfo"
@confirm="handleConfirm"
:hiddenBtn="true"
/>
</div>
<div class="drawer-footer">
<el-button @click="handleCancel" v-if="!rowInfo || !rowInfo.id"
>取消</el-button
>
<el-button @click="handlePreview">发布预览</el-button>
<el-button
type="primary"
@click="handleSave"
v-if="!rowInfo || !rowInfo.id"
>保存草稿</el-button
>
<el-popconfirm
title="确认发布吗?"
@confirm="handleConfirm"
:loading="loading"
>
<el-button type="primary" slot="reference" style="margin-left: 8px"
>确认发布</el-button
>
</el-popconfirm>
</div>
</div>
<PagePreview
:previewInfo="previewInfo"
:visible="previewVisible"
@preview="preview"
/>
</el-drawer>
</template>
页面效果:
页面中使用:(搜索表单)
<el-card class="box-card search-card"> <CustomForm :formLabel="searchList" :rules="searchRules" @confirm="handleSearch" /> </el-card>
接口返回格式:
searchList: [ { label: "标题", placeholder: "输入标题", model: "name", type: "input", }, { label: "发布时间", model: "time", type: "dateRange", props: { pickerOptions: { shortcuts: [ { text: "今日", onClick(picker) { const end = new Date(); const start = new Date(); picker.$emit("pick", [start, end]); }, }, { text: "昨日", onClick(picker) { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 1); end.setTime(end.getTime() - 3600 * 1000 * 24 * 1); picker.$emit("pick", [start, end]); }, }, { text: "最近7天", onClick(picker) { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); picker.$emit("pick", [start, end]); }, }, { text: "最近30天", onClick(picker) { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); picker.$emit("pick", [start, end]); }, }, ], }, }, }, ], searchRules: {},
页面效果:
附上vue-quill-editor文章显示:
<!-- 文章内容 --> <div :class="$style.detailBox"> <div :class="$style.detailTitle"> {{ title }} </div> <div :class="$style.detailTime">{{ time }}</div> <div :class="$style.detailBody"> <div class="ql-container ql-snow"> <div class="ql-editor"> <div :class="$style.detailArticle" v-html="change(detailInfo)" ></div> </div> </div> </div> </div>
多一层处理是因为vue-quill-editor插入的视频最终会变成iframe包裹的代码,无法控制样式,因此用video替换掉了:
export default { name: "NewsDetail", components: {}, data() { return { title: "-", time: "-", detailInfo: "", }; }, mounted() { const newsId = this.$route.query.id; if (newsId) this.fetchDetail(newsId); }, methods: { // 根据路由中的id获取新闻详细信息 async fetchDetail(id) { try { const { code, msg, data } = await api.newsDetail({ id, }); if (code === 0 && data) { this.title = data.title; this.time = data.releaseTime ? moment(data.releaseTime).format("YYYY-MM-DD") : "-"; this.detailInfo = data.content; } else { this.$toast(msg || "新闻详细信息获取失败"); } } catch (err) { // } finally { this.isSubmit = false; } }, goBack() { this.$router.back(); // window.history.back() // this.$router.go(-1) }, change(content) { let t = content .replaceAll( "<iframe", `<video style="width:100%;outline:none;" controls="" autoplay=""` ) .replaceAll("</iframe>", "</video>"); return t; }, }, };
<style> .ql-container.ql-snow { border: none; } </style>