从零开始使用vue2+element搭建后台管理系统(动态表单实现(含富文本框))[待完善]

发布时间 2023-09-18 11:44:59作者: 芝麻小仙女

在后台项目的实际开发过程中,涉及到表单的部分通常会使用动态渲染的方案进行实现,由后端接口返回表单配置,前端进行遍历渲染。考虑到通用后台需要具备的功能,除了基础的表单项如输入、下拉、多选、开关、时间、日期等,还需要具备上传、富文本框等功能。

首先导入一个百度来的富文本框插件: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>

  

页面效果: