在后台项目的实际开发过程中,涉及到表单的部分通常会使用动态渲染的方案进行实现,由后端接口返回表单配置,前端进行遍历渲染。考虑到通用后台需要具备的功能,除了基础的表单项如输入、下拉、多选、开关、时间、日期等,还需要具备上传、富文本框等功能。
首先导入一个百度来的富文本框插件: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" 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'" > <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'" > <el-checkbox v-for="item in item.opts" :key="item.value" :label="item.value" >{{ item.label }}</el-checkbox > </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]" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" type="daterange" placeholder="选择日期" v-if="item.type === 'dateRange'" value-format="yyyy-MM-dd" > </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> <!-- todo: 文件上传 --> <UploadFile v-if="item.type === 'upload'" v-model="form[item.model]" fieldName="cardUpload" prefix="cardUpload" /> <!-- todo: 富文本框 --> <quill-editor ref="myQuillEditor" v-if="item.type === 'content'" v-model="form[item.model]" class="editor" :options="editorOption" style="height: 265px" /> </el-form-item> </div> <!-- 行内时样式【常用于搜索栏 --> <div class="btnGroup" v-if="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> <el-form-item> <el-button type="primary" @click="search">{{ submitText }}</el-button> <el-button @click="reset">重置</el-button> </el-form-item> </div> </el-form> <!-- 富文本编辑器中的上传图片控件 --> <el-upload class="avatar-uploader-img" :action="'uploadUrl'" :show-file-list="false" :on-success="uploadImgSuccess" :before-upload="beforeUploadImg" :on-error="uploadImgError" :data="{ pathName: '' }" /> <el-upload class="avatar-uploader-video" :action="'uploadUrl'" :show-file-list="false" :on-success="uploadVideoSuccess" :before-upload="beforeUploadVideo" :on-error="uploadVideoError" :data="{ pathName: '' }" /> </div> </template> <script> import UploadFile from "./UploadFile.vue"; // 工具栏配置 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"] }], // 字体大小 [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题 [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色 [{ font: [] }], // 字体种类 [{ align: [] }], // 对齐方式 ["clean"], // 清除文本格式 ["link", "image", "video"], // 链接、图片、视频 ]; 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: "提交", }, formLabel: Array, rules: Object, }, watch: { formLabel: { handler(newVal) { if (newVal) { newVal.forEach((item) => { this.$set(this.form, item.model, item.default || ""); }); } }, immediate: true, }, }, data() { return { form: {}, editorOption: { // 编辑框操作事件 theme: "snow", // or 'bubble' placeholder: "请输入想发布的内容", 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); } }, }, }, }, }, }; }, 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.$emit("confirm", this.form); // Bus.$emit('getParam', this.form);//给Table传查询参数 }, 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; }, //富文本图片上传前 beforeUploadImg(file) { const isJPG = file.type === "image/jpeg" || file.type === "image/png" || file.type === "image/gif"; if (!isJPG) { this.$message.error("上传图片只能是 JPG,PNG, GIF 格式!"); } else { // 显示loading动画 this.quillUpdate = true; } return isJPG; }, // 富文本视频上传前 beforeUploadVideo(file) { const fileSize = file.size / 1024 / 1024 < 50; 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("视频大小不能超过50MB"); return false; } this.isShowUploadVideo = false; // const isVideo = file.type === "video/mp4"; // if (!isVideo) { // this.$message.error("上传视频只能是 mp4 格式!"); // } else { // // 显示loading动画 // this.quillUpdate = true; // } // return isVideo; }, uploadImgSuccess(res, file) { console.log(res, file, "===uploadImgSuccess"); //富文本图片上传成功 // res为图片服务器返回的数据 // 获取富文本组件实例 const quill = this.$refs.myQuillEditor.quill; // 这里需要注意自己文件上传接口返回内容,我这里code=0表示上传成功,返回的文件地址:res.data.src if (res.code !== 0) { this.$message.error(res.msg); //this.$message.error('图片插入失败!') } else { console.info(res); // 获取光标所在位置 const length = quill.getSelection().index; // 插入图片 res.info为服务器返回的图片地址 quill.insertEmbed(length, "image", res.data.src); // 调整光标到最后 quill.setSelection(length + 1); } // loading动画消失 this.quillUpdate = false; }, uploadImgError() { //富文本图片上传失败 // loading动画消失 this.quillUpdate = false; this.$message.error("图片插入失败!"); }, uploadVideoSuccess(res, file) { console.log(res, file, "===uploadVideoSuccess"); // res为图片服务器返回的数据 // 获取富文本组件实例 const quill = this.$refs.myQuillEditor.quill; // 如果上传成功 if (res.code == "200" && res.data.url != null) { // 获取光标所在位置 const length = quill.getSelection().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("视频插入失败"); }, }, components: { UploadFile, }, }; </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> <!-- 富文本编辑器 --> <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: "等宽字体"; } /************************************** 富文本编辑器 **************************************/ </style>
需要注意的是上传功能因为还没实现,所以暂时是假的
接口返回的数据格式:
/** * demo表单接口 */ export function demoInit() { return { code: 10000, msg: "请求成功", data: { searchList: [ { label: "输入文本", placeholder: "输入文本", model: "text", type: "input", props: { showWordLimit: false, }, }, { label: "请选择日期", model: "date", type: "date", }, { label: "请选择日期范围", model: "dateRange", type: "dateRange", }, { label: "请选择时间范围", model: "time", type: "dateTimeRange", }, { label: "请选择游戏", placeholder: "请选择游戏", model: "game", type: "select", opts: [ { label: "游戏1", value: "game1", }, { label: "游戏2", value: "game2", }, ], }, { label: "状态", model: "status", type: "switch", }, { label: "复选框", model: "type", type: "checkbox", opts: [ { label: "平台订单号", value: "orderId", }, { label: "游戏订单号", value: "gameOrderId", }, ], }, { label: "单选框", model: "type_s", type: "radio", opts: [ { label: "平台订单号", }, { label: "游戏订单号", }, ], }, { label: "请输入文本", placeholder: "输入文本", model: "textarea", type: "textarea", props: { rows: 3, showWordLimit: true, maxLength: 50, }, }, { label: "上传", model: "files", type: "upload", }, { label: "富文本框", model: "news", type: "content", }, ], searchRules: { textarea: [{ required: true, message: "请输入文本", trigger: "blur" }], }, }, }; }
页面中使用:
<template> <div> <h2>动态表单demo</h2> <el-card class="box-card search-card"> <CustomForm :formLabel="searchList" :rules="searchRules" @confirm="handleConfirm" :inline="false" :label-width="'160px'" /> </el-card> </div> </template> <script> import CustomForm from "@/components/CustomForm.vue"; import { demoInit } from "@/api/demo"; import { showPageLoading, hidePageLoading } from "@/utils/loading"; export default { name: "FirstView", data() { return { searchList: [], searchRules: {}, }; }, mounted() { this.init(); }, methods: { // 初始化 async init() { try { showPageLoading(); // 开启 const res = await demoInit(); if (res.code !== 10000) this.$message.error(res.msg); if (res.code === 10000 && res.data) { this.searchList = res.data.searchList; this.searchRules = res.data.searchRules; } } finally { setTimeout(() => { hidePageLoading(); }, 500); } }, // 提交 async handleConfirm(data) { console.log(data, "===提交"); }, }, components: { CustomForm, }, }; </script> <style lang="scss" scoped> .search-card { margin-bottom: 16px; } </style>
页面效果:
标签:picker,snow,label,文本框,含富,value,element,data,ql From: https://www.cnblogs.com/nangras/p/17711483.html