在后台项目的实际开发过程中,涉及到表单的部分通常会使用动态渲染的方案进行实现,由后端接口返回表单配置,前端进行遍历渲染。考虑到通用后台需要具备的功能,除了基础的表单项如输入、下拉、多选、开关、时间、日期等,还需要具备上传、富文本框等功能。
首先导入一个百度来的富文本框插件: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>
页面效果:
