从零开始使用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>

浙公网安备 33010602011771号