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

  

 

posted @ 2023-09-18 11:40  芝麻小仙女  阅读(656)  评论(0编辑  收藏  举报