Vue开发中常用的自定义组件总结

1,270 阅读8分钟

Vue开发过程中经常使用大量相似的组件,比如弹窗、表格、表单、菜单栏、面包屑...等等,每个人也有每个人的一套代码逻辑,为了提高效率也会总结自己的组件逻辑,因此在这里进行自己的自定义组件的整理和总结,力求做出好用,易用的自定义组件

1.弹窗组件

弹窗dialog是Vue PC开发中使用最频繁的组件,但是弹窗其实除了窗体的内容略有不同之外,窗头和窗尾的样式和内容都是相似的,因此这个也是最值得被重用的一个组件。

  • 弹窗组件页面 dialog.vue:
<template>
  <div class="dialog">
    <el-dialog
      v-if="dialogData"
      :title="dialogData.title"
      :visible.sync="visible"
      :before-close="cancelClick"
      :center="false"
    >
      <div class="dialog-content">
        <h1>这里是根据dialogData内容的不同形成的不同的弹窗内容</h1>
      </div>
      <slot name="fixContent">
        <!-- 这里可以放置一些无需处理数据的固定内容 -->
      </slot>
      <span slot="footer" class="dialog-footer">
        <el-button size="medium" @click="cancelClick">取 消</el-button>
        <el-button
          type="primary"
          size="medium"
          v-resetButton="1000"
          @click="confirmClick"
          >确 定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "dialog",
  data() {
    return {
      dialogData: null,
      visible: false,
    };
  },
  props: {
    dialogVal: {
      //父级传递过来的弹窗内容
      type: Object,
      default: () => {
        return null;
      },
    },
    dialogDefaultVal: {
      //编辑弹窗时,弹窗内容上要呈现的默认值
      type: Object,
      default: () => {
        return null;
      },
    },
    dialogVisible: {
      //弹窗打开关闭开关
      type: Boolean,
      default: false,
    },
  },
  watch: {
    dialogVisible(newVal) {
      //监控弹窗关闭
      if (newVal) {
        //弹窗打开时,进行弹窗的初始化
        this.initDialogView();
      } else {
        //弹窗关闭时,对弹窗进行相应的格式化
        this.resetDialogView();
      }
      this.visible = newVal;
    },
  },
  methods: {
    initDialogView() {
      this.dialogData = this.dialogVal; //初始化页面的数据和相对应的赋值
      if (this.dialogDefaultVal) {
        //当有默认值时要进行赋值操作
      }
    },
    resetDialogView() {
      //重置页面的数据
      this.dialogData = null;
      this.cancelClick();
    },
    cancelClick() {
      //点击取消或者X弹窗消失的操作
      this.$emit("cancelClick");
    },
    confirmClick() {
      //点击确定时,发送弹窗的内容数据的操作
    },
  },
};
</script>
<style lang="scss" scoped>
/deep/.el-dialog__header {
  display: flex;
  align-items: center;
}
</style>
  • 具体调用页面 view.vue
<template>
  <div class="view-box">
    <el-button @click="btnClick">弹窗按钮</el-button>
    <dialogCus
      :dialogVal="dialogVal"
      :dialogVisible="dialogVisible"
      :dialogDefaultVal="dialogDefaultVal"
      @cancelClick="cancelClick"
    >
      <template v-slot:fixContent>
        <h1>这里可以放置一些无需处理数据的固定内容</h1>
      </template>
    </dialogCus>
  </div>
</template>
<script>
import dialogCus from "./dialog.vue";
export default {
  name: "view",
  data() {
    return {
      dialogVisible: false, //弹窗是否显示
      dialogDefaultVal: null, //弹窗默认值
      dialogVal: null, //弹窗结构
    };
  },
  components: {
    dialogCus,
  },
  methods: {
    cancelClick() {
      //弹窗关闭,对数据进行初始化
      this.dialogVisible = false;
      this.dialogDefaultVal = null;
      this.dialogVal = null;
    },
    btnClick() {
      this.dialogVal = {
        title: "自定义弹窗表头",
        contents: [], // 这里可以编写弹窗的具体内容
      };
      this.dialogVisible = true;
    },
  },
};
</script>

2.批量添加/修改组件

可以利用表格的特性,制作一个批量添加/修改组件,然后再结合VeeVaildate校验和el-table的slot-scope="scope"的特性,就可以模拟成一个可以批量添加删除修改的表单。

<template>
 <div class="view-box">
  <h1>多选表单</h1>
  <el-button type="primary" icon="el-icon-plus" @click="newAdd()">新增</el-button>
  <p></p>
   <ValidationObserver ref="observer">
      <el-table max-height="445px" :data="tableData" border>
        <el-table-column type="index" label="序号" width="50"></el-table-column>
        <el-table-column label="名称" prop="name">
          <template slot-scope="scope">
            <ValidationProvider rules="required" v-slot="{ errors }">
              <el-input
                :maxlength="30"
                placeholder="请输入名字"
                v-model="scope.row.name"
                :class="{ 'error-txt': errors[0] }"
              >
              </el-input>
              <span class="validate-span">{{ errors[0] }}</span>
            </ValidationProvider>
          </template>
        </el-table-column>
        <el-table-column label="性别" prop="gender">
          <template slot-scope="scope">
            <ValidationProvider rules="required" v-slot="{ errors }">
              <el-input
                :maxlength="30"
                placeholder="请输入性别"
                v-model="scope.row.gender"
                :class="{ 'error-txt': errors[0] }"
              >
              </el-input>
              <span class="validate-span">{{ errors[0] }}</span>
            </ValidationProvider>
          </template>
        </el-table-column>
        <el-table-column label="年龄" prop="age">
          <template slot-scope="scope">
            <ValidationProvider rules="required" v-slot="{ errors }">
              <el-input
                :maxlength="30"
                placeholder="请输入年龄"
                v-model="scope.row.age"
                :class="{ 'error-txt': errors[0] }"
              >
              </el-input>
              <span class="validate-span">{{ errors[0] }}</span>
            </ValidationProvider>
          </template>
        </el-table-column>
        <el-table-column label="操作" prop="operation">
          <template slot-scope="scope">
            <span @click="delClick(scope.row)"> 删除 </span>
          </template>
        </el-table-column>
      </el-table>
    </ValidationObserver>
    <p></p>
    <el-button type="primary" @click="submit()">提交</el-button>
 </div>
</template>
<script>
import { required } from "vee-validate/dist/rules";
import { ValidationProvider, ValidationObserver, extend } from "vee-validate"; //自定义校验的方法
extend("required", {
  ...required,
  message: "此项必填",
});
export default {
  name: "view",
  data() {
    return {
      tableData: [],
    };
  },
  components: {
    ValidationProvider,
    ValidationObserver,
  },
  methods: {
    newAdd() {
      let tempObj = {
        id: Date.now(), //给手动添加的表单,赋值id
        isExised: false, //为了区分,自己手动添加的还是数据库查询出来的数据
        name: "",
        gender: "",
        age: "",
      };
      this.tableData.push(tempObj);
    },
    delClick(row) {
      this.tableData.splice(
        this.tableData.findIndex((t) => t.id == row.id),
        1
      );
    },
  },
  submit() {
    this.$refs.observer.validate().then((vali) => {
      if (vali) {
        console.log("发送请求的信息", this.tableData);
      }
    });
  },
};
</script>
<style lang="scss" scoped>
.view-box {
  text-align: left;
  .validate-span {
    font-size: 14px;
    color: tomato;
  }
  /deep/.el-input.error-txt .el-input__inner {
    border: 1px solid tomato !important;
  }
}
</style>
  • 实现效果如图: image.png
  • 校验效果如图: image.png

3.自定义form表单的实现

自定义表单也是非常常用的一个组件,但是经常需要花费大量的时间来编写表单本身,所以这里就把自己常用的表单做成一个公共页面,然后使用数据并结合el-row的colspan来控制表单的展示,最终获取相应的formData,将前端的精力聚焦数据本身即可。实现页面如下:

  1. data.js
//这是自定义的表单内容的数据,可以根据自己的需要进行编写,之后控制表单就可以按照数据来处理表单
let formContents = [
  {
    label: "基本信息", //名称
    type: "titlebox", //类型,自己定义的一般加box,表示不是formItem,而是一个box的自定义内容
    prop: "essentialInformation", //标识,在box自定义内容中,仅仅是一个标识
    colspan: 24, //该自定义内容再el-row中所占的大小,24表示是一整行
  },
  {
    label: "会议名称", //名称,在formItem中还是label名
    type: "input", //类型,表示是formItem中的编辑内容,比如这个就是一个input
    prop: "meetingName", //标识,再formItem中还是formData的属性名,且与表单校验相关联
    disabled: true, //表示表单该内容是否禁用
    colspan: 8, //代表该表单内容占三分之一宽度的大小
    rules: [
      {
        required: true,
        message: "会议名称不能为空",
        trigger: "blur",
      },
    ], //代表该表单内容的校验规则
  },
  {
    label: "开始时间",
    type: "datetimepicker",
    prop: "startTime",
    disabled: true,
    colspan: 8,
    rules: [
      {
        type: "date",
        required: true,
        message: "开始时间不能为空",
        trigger: "change",
      },
    ],
  },
  {
    label: "结束时间",
    type: "datetimepicker",
    prop: "endTime",
    disabled: true,
    colspan: 8,
    rules: [
      {
        type: "date",
        required: true,
        message: "开始时间不能为空",
        trigger: "change",
      },
    ],
  },
  {
    label: "会议地点",
    type: "input",
    prop: "address",
    disabled: true,
    colspan: 8,
    rules: [
      {
        required: true,
        message: "会议地点不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "参会人员",
    type: "input",
    prop: "participants",
    disabled: true,
    colspan: 8,
    rules: [
      {
        required: true,
        message: "参会人员不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "会议类型",
    type: "input",
    prop: "meetingtype",
    disabled: true,
    colspan: 8,
    rules: [
      {
        required: true,
        message: "会议类型不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "会议密级",
    type: "input",
    prop: "meetingsecuritylevel",
    disabled: true,
    colspan: 8,
    rules: [
      {
        required: true,
        message: "会议密级不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "主讲人",
    type: "input",
    prop: "speaker",
    disabled: true,
    colspan: 8,
    rules: [
      {
        required: true,
        message: "主讲人不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "记录人",
    type: "input",
    prop: "noteTaker",
    disabled: true,
    colspan: 8,
    rules: [
      {
        required: true,
        message: "记录人不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "会议纪要",
    type: "buttonbox",
    prop: "meetingMinutes",
    disabled: false,
    colspan: 24,
  },
  {
    label: "会议议程",
    type: "input",
    prop: "meetingAgenda",
    disabled: false,
    colspan: 24,
    rules: [
      {
        required: true,
        message: "会议议程不能为空",
        trigger: "blur",
      },
    ],
  },
  {
    label: "会议结论",
    type: "textarea",
    prop: "meetingConclusions",
    disabled: false,
    colspan: 24,
    rules: [
      {
        required: true,
        message: "会议结论不能为空",
        trigger: "blur",
      },
    ],
  },
];
export { formContents };
  1. form.vue
<template>
  <div class="meetingminutes">
    <el-form
      v-if="formData"
      :model="formData"
      ref="ruleForm"
      label-width="130px"
      label-suffix=":"
      label-position="left"
    >
      <el-row :gutter="20">
        <el-col
          v-for="item in formDataContents"
          :key="item.prop"
          :span="item.colspan"
        >
          <template v-if="item.type == 'titlebox'">
            <div class="title-box">{{ item.label }}:</div>
          </template>
          <template v-else-if="item.type == 'buttonbox'">
            <div class="button-box">
              <span>{{ item.label }}:</span>
              <el-button type="primary" @click="exportMeetingMinutes(formData)">
                导出会议纪要
              </el-button>
            </div>
          </template>
          <template v-else>
            <el-form-item
              :label="item.label"
              :prop="item.prop"
              :rules="!item.disabled ? item.rules : []"
            >
              <template v-if="item.type == 'input'">
                <el-input
                  v-model="formData[item.prop]"
                  :disabled="item.disabled"
                ></el-input>
              </template>
              <template v-else-if="item.type == 'datetimepicker'">
                <el-date-picker
                  :disabled="item.disabled"
                  style="width: 100%"
                  v-model="formData[item.prop]"
                  type="datetime"
                  placeholder="选择日期时间"
                >
                </el-date-picker>
              </template>
              <template v-else-if="item.type == 'textarea'">
                <el-input
                  :disabled="item.disabled"
                  type="textarea"
                  :autosize="{ minRows: 4, maxRows: 10 }"
                  placeholder="请输入内容"
                  v-model="formData[item.prop]"
                ></el-input>
              </template>
            </el-form-item>
          </template>
        </el-col>
      </el-row>
    </el-form>
    <el-button @click="cancel()"> 取消</el-button>
    <el-button type="primary" @click="confirm()">提交</el-button>
  </div>
</template>
<script>
import { formContents } from "./data";
export default {
  name: "meetingminutes",
  data() {
    return {
      formDataContents: formContents, //表单内容数据
      formData: null, //表单数据,该页面最终要用来保存的数据
    };
  },
  created() {
    this.formData = {};
    this.formDataContents.forEach((f) => {
      if (["input", "datetimepicker", "textarea"].includes(f.type)) {
        //给this.formData赋初始值。根据表单内容数据,给单表数据赋初始值,一般是新增操作为空,编辑操作有值,如果有值,则根据情况自行判断并赋值。
        this.$set(this.formData, f.prop, ""); //使用数据动态生成的表单,得使用this.$set,才能完成双向绑定,否则无法初始化
      }
    });
  },
  methods: {
    cancel() {},
    confirm() {
      this.$refs.ruleForm
        .validate()
        .then((res) => {
          //校验无误后提交
          console.log(res);
        })
        .catch((err) => {
          console.log(err);
        });
    },
    exportMeetingMinutes(formData) {
      console.log("exportMeetingMinutes formData", formData); //从这里拿到相应得数据,进行相应得请求以导出数据
    },
  },
};
</script>
<style lang="scss" scoped>
.meetingminutes {
  padding: 30px;
  box-sizing: border-box;
  .el-row {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap; //el-row本身不会让el-col进行换行,所以需要结合flex的换行机制,使其换行。
    .button-box {
      font-family: "Microsoft YaHei", sans-serif;
      font-size: 16px;
      font-weight: 700;
      margin-bottom: 22px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .title-box {
      font-family: "Microsoft YaHei", sans-serif;
      font-size: 16px;
      text-align: left;
      font-weight: 700;
      margin-bottom: 22px;
    }
  }
}
</style>
  • 效果图如下:

image.png

自定义form表单的优化

上面的示例可以做出一个简单的没有自定义DOM的组件,但是如果业务需要做一些elementUI上没有的DOM元素,那就比较麻烦了,如果使用v-if来渲染自定义组件也可以做到,但是随着项目越来越大,组件就会越来越臃肿,越来越难以维护,无法做到重用的目的,失去了组件的意义。因此,需要使用mixins属性把相应的数据分离开,使用函数式组件和JSX结合的方式,使组件尽量做到重用。当然这样的作法也可以运用的到所有的组件中,让组件尽量有可复用性。

  • view.vue
<template>
  <div class="formData">
    <formDataView
      ref="formView"
      :formDataContents="formDataContents"
      v-model="formData"
      @validateConfirm="validateConfirm"
    ></formDataView>
    <div class="box">
      <el-button type="primary" @click="confirmClick">确定</el-button>
    </div>
  </div>
</template>

<script>
import { formDataMixins } from "./formDataMixins.js";
import formDataView from "./formDataView.vue";
export default {
  name: "formData",
  mixins: [formDataMixins],
  components: {
    formDataView,
  },
  methods: {
    validateConfirm(vail) {
      console.log("confirmDataValidate vail", vail);
    },
    confirmClick() {
      this.$refs.formView.confirmValidate();
    },
  },
};
</script>
  • formDataMixins.js
export const formDataMixins = {
  data() {
    return {
      pickerTime: "",
      address: "",
      formDataContents: [
        {
          label: "基本信息", //名称
          type: "titlebox", //类型,自己定义的一般加box,表示不是formItem,而是一个box的自定义内容
          prop: "essentialInformation", //标识,在box自定义内容中,仅仅是一个标识
          colspan: 24, //该自定义内容再el-row中所占的大小,24表示是一整行
          render: (h, item) => {
            return <div class="title-box">{item.label}:</div>;
          },
        },
        {
          label: "会议名称", //名称,在formItem中还是label名
          type: "input", //类型,表示是formItem中的编辑内容,比如这个就是一个input
          prop: "meetingName", //标识,再formItem中还是formData的属性名,且与表单校验相关联
          disabled: false, //表示表单该内容是否禁用
          colspan: 8, //代表该表单内容占三分之一宽度的大小
          rules: [
            {
              required: true,
              message: "会议名称不能为空",
              trigger: "blur",
            },
          ], //代表该表单内容的校验规则
        },
        {
          label: "会议类型",
          type: "select",
          prop: "type",
          disabled: this.$route.params.isEditor == "1",
          colspan: 8,
          inClearable: true,
          rules: [
            {
              required: true,
              message: "会议类型不能为空",
              trigger: "change",
            },
          ],
          selects: [
            {
              value: "A",
              label: "A",
              contents: {
                id: "Aa",
              },
            },
            {
              value: "B",
              label: "B",
              contents: {
                id: "Bb",
              },
            },
            {
              value: "C",
              label: "C",
              contents: {
                id: "Cc",
              },
            },
            {
              value: "D",
              label: "D",
              contents: {
                id: "Dd",
              },
            },
          ],
          selectClick: (item, prop) => this.selectClick(item, prop),
        },
        {
          label: "时间和地址",
          type: "buttonAppendPopover",
          prop: "timeAndAddress",
          disabled: this.$route.params.isEditor == "1",
          colspan: 8,
          placeholder: "请选择时间和地址",
          rules: [
            {
              required: true,
              message: "时间和地址不能为空",
              trigger: "blur",
            },
          ],
          readonly: true,
          render: (h, item) => {
            return (
              <div class="pop-con-box">
                <div class="pop-con-box-item">
                  <span>时间:</span>
                  <el-date-picker
                    vModel={this.pickerTime}
                    type="date"
                    placeholder="选择日期"
                    value-format="yyyy-MM-dd"
                    format="yyyy 年 MM 月 dd 日"
                  ></el-date-picker>
                </div>
                <div class="pop-con-box-item">
                  <span>地址:</span>
                  <el-input size="medium" vModel={this.address}></el-input>
                </div>
                <el-button
                  type="text"
                  on-click={() => this.setAddressAndTimeTxt(item.prop)}
                >
                  确定
                </el-button>
              </div>
            );
          },
        },
        {
          label: "开始时间",
          type: "datetimepicker",
          prop: "startTime",
          disabled: false,
          colspan: 8,
          rules: [
            {
              type: "date",
              required: true,
              message: "开始时间不能为空",
              trigger: "change",
            },
          ],
        },
        {
          label: "结束时间",
          type: "datetimepicker",
          prop: "endTime",
          disabled: false,
          colspan: 8,
          rules: [
            {
              type: "date",
              required: true,
              message: "开始时间不能为空",
              trigger: "change",
            },
          ],
        },
        {
          label: "会议地点",
          type: "input",
          prop: "address",
          disabled: false,
          colspan: 8,
          rules: [
            {
              required: true,
              message: "会议地点不能为空",
              trigger: "blur",
            },
          ],
        },
        {
          label: "会议类型",
          type: "input",
          prop: "meetingtype",
          disabled: false,
          colspan: 8,
          rules: [
            {
              required: true,
              message: "会议类型不能为空",
              trigger: "blur",
            },
          ],
        },
        {
          label: "主讲人",
          type: "input",
          prop: "speaker",
          disabled: false,
          colspan: 8,
          rules: [
            {
              required: true,
              message: "主讲人不能为空",
              trigger: "blur",
            },
          ],
        },
        {
          label: "记录人",
          type: "input",
          prop: "noteTaker",
          disabled: false,
          colspan: 8,
          rules: [
            {
              required: true,
              message: "记录人不能为空",
              trigger: "blur",
            },
          ],
        },
        {
          label: "会议纪要",
          type: "buttonbox",
          prop: "meetingMinutes",
          disabled: false,
          colspan: 24,
          render: (h, item) => {
            return (
              <div class="button-box">
                <span>{item.label}:</span>
                <el-button
                  type="primary"
                  on-click={() => this.exportMeetingMinutes()}
                >
                  导出会议纪要
                </el-button>
              </div>
            );
          },
        },
        {
          label: "是否通过",
          type: "radio",
          prop: "status",
          disabled: false,
          colspan: 8,
          rules: [
            { required: true, message: "请选择评审结果", trigger: "change" },
          ],
          radios: [
            {
              label: "PASS",
              name: "通过",
            },
            {
              label: "FAIL",
              name: "不通过",
            },
          ],
        },
        {
          label: "会议议程",
          type: "input",
          prop: "meetingAgenda",
          disabled: false,
          colspan: 24,
          rules: [
            {
              required: true,
              message: "会议议程不能为空",
              trigger: "blur",
            },
          ],
        },
        {
          label: "会议结论",
          type: "textarea",
          prop: "meetingConclusions",
          disabled: false,
          colspan: 24,
          rules: [
            {
              required: true,
              message: "会议结论不能为空",
              trigger: "blur",
            },
          ],
        },
      ], //表单内容数据
      formData: null, //表单数据,该页面最终要用来保存的数据
    };
  },
  created() {
    this.formData = {};
    this.formDataContents.forEach((f) => {
      if (
        [
          "input",
          "datetimepicker",
          "textarea",
          "radio",
          "select",
          "timeAndAddress",
        ].includes(f.type)
      ) {
        //给this.formData赋初始值。根据表单内容数据,给单表数据赋初始值,一般是新增操作为空,编辑操作有值,如果有值,则根据情况自行判断并赋值。
        this.$set(this.formData, f.prop, ""); //使用数据动态生成的表单,得使用this.$set,才能完成双向绑定,否则无法初始化
      }
    });
  },
  methods: {
    exportMeetingMinutes() {
      console.log("exportMeetingMinutes formData", this.formData); //从这里拿到相应得数据,进行相应得请求以导出数据
    },
    selectClick(item, prop) {
      //下拉框点击事件 item 是option的item,prop 是数组的标识,是唯一的标识
      console.log("selectClick", item, prop);
    },
    setAddressAndTimeTxt(prop) {
      if (this.pickerTime && this.address) {
        this.$set(this.formData, prop, `${this.pickerTime} ${this.address}`);
      } else {
        this.$message.error("时间和地址都不可为空");
      }
    },
  },
};
  • formDataView.vue form表单组件
<template>
  <div class="form-com">
    <el-form
      v-if="value"
      :model="value"
      ref="ruleForm"
      :label-width="labelWidth || '100px'"
      label-suffix=":"
      label-position="left"
    >
      <el-row :gutter="20">
        <el-col
          v-for="item in formDataContents"
          :key="item.prop"
          :span="item.colspan"
        >
          <template v-if="['titlebox', 'buttonbox'].includes(item.type)">
            <!-- 自定义函数式组件 -->
            <cusSlot
              v-if="item.render"
              :itemObj="item"
              :render="item.render"
            ></cusSlot>
          </template>
          <template v-else>
            <el-form-item
              :label="item.label"
              :prop="item.prop"
              :rules="!item.disabled ? item.rules : []"
            >
              <template v-if="item.type == 'input'">
                <el-input
                  :size="item.size || 'medium'"
                  v-model="value[item.prop]"
                  :disabled="item.disabled"
                ></el-input>
              </template>
              <template v-else-if="item.type == 'datetimepicker'">
                <el-date-picker
                  :size="item.size || 'medium'"
                  :disabled="item.disabled"
                  style="width: 100%"
                  v-model="value[item.prop]"
                  type="datetime"
                  placeholder="选择日期时间"
                >
                </el-date-picker>
              </template>
              <template v-else-if="item.type == 'textarea'">
                <el-input
                  :size="item.size || 'medium'"
                  :disabled="item.disabled"
                  type="textarea"
                  :autosize="{ minRows: 4, maxRows: 10 }"
                  placeholder="请输入内容"
                  v-model="value[item.prop]"
                ></el-input>
              </template>
              <template v-else-if="item.type == 'select'">
                <el-select
                  v-model="value[item.prop]"
                  placeholder="请选择"
                  :disabled="item.disabled"
                  :multiple="item.multiple"
                  :size="item.size || 'medium'"
                  filterable
                  :clearable="!item.inClearable"
                  style="width: 100%"
                >
                  <el-option
                    v-for="selectItem in item.selects"
                    :key="selectItem.value"
                    :label="selectItem.label"
                    :value="selectItem.value"
                    @click.native="item.selectClick(selectItem, item.prop)"
                  >
                  </el-option>
                </el-select>
              </template>
              <template v-else-if="item.type == 'radio'">
                <el-radio-group v-model="value[item.prop]">
                  <el-radio
                    :size="item.size || 'medium'"
                    v-for="radio in item.radios"
                    :key="radio.label"
                    :label="radio.label"
                    :disabled="item.disabled"
                  >
                    {{ radio.name }}
                  </el-radio>
                </el-radio-group>
              </template>
              <template v-else-if="item.type == 'buttonAppendPopover'">
                <template v-if="item.prop == 'timeAndAddress'">
                  <div class="btnAppPop">
                    <el-popover
                      placement="bottom"
                      :size="item.size || 'medium'"
                      :popper-class="item.popperClass || ''"
                      :trigger="item.trigger || 'click'"
                    >
                      <cusSlot
                        v-if="item.render"
                        :itemObj="item"
                        :render="item.render"
                      ></cusSlot>
                      <el-input
                        slot="reference"
                        :size="item.size || 'medium'"
                        :placeholder="item.placeholder"
                        v-model="value[item.prop]"
                        :readonly="item.readonly"
                      >
                      </el-input>
                    </el-popover>
                  </div>
                </template>
              </template>
            </el-form-item>
          </template>
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>
<script>
//自定义函数式组件 ==START
let cusSlot = {
  functional: true, //代表这是一个函数式组件
  props: {
    itemObj: Object, //接受父组件传来得数据
    render: Function, //接受父组件传来的回调函数,用于把JSX编写的DOM以及DOM上的事件抽取出来到数据脚本页面,尽量不污染外部组件,使组件更加的灵活轻便可复用。
  },
  render: (h, context) => {
    //函数式组件用来渲染相应的DOM,有两个参数,h:render渲染函数,context:上下文对象,可以获取相应父组件传到props中的数据
    //使用父组件传来的render回调函数,将JSX编写的DOM对象抽取出去,保持组件的灵活性。*****关键*****
    return context.props.render(h, context.props.itemObj);
  },
};
//自定义函数式组件 ==END
export default {
  name: "formView",
  components: {
    cusSlot,
  },
  props: {
    value: {
      /*因为经常性的要处理表单数据,
      所以为了减少跨组建传值的次数,
      就模拟使用v-model来双向绑定表单数据,
      这样可以方便处理数据。*/
      type: Object,
      default: () => null,
    },
    formDataContents: {
      //表单内容DOM数组
      type: Array,
      default: () => [],
    },
    labelWidth: {
      type: String,
      default: "",
    },
  },
  watch: {
    value: {
      handler(newVal) {
        //就模拟使用v-model来双向绑定表单数据
        console.log("watch value", newVal);
        this.$emit("input", newVal);
      },
      deep: true,
      immediate: true,
    },
  },
  methods: {
    confirmValidate() {
      this.$refs.ruleForm
        .validate()
        .then((res) => {
          //校验无误,方可提交数据
          this.$emit("validateConfirm", res);
        })
        .catch((err) => {
          this.$emit("validateConfirm", err);
        });
    },
  },
};
</script>
<style lang="scss" scoped>
.form-com {
  width: 100%;
  padding: 30px;
  box-sizing: border-box;
  .el-row {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap; //el-row本身不会让el-col进行换行,所以需要结合flex的换行机制,使其换行。
    .button-box {
      font-family: "Microsoft YaHei", sans-serif;
      font-size: 16px;
      font-weight: 700;
      margin-bottom: 22px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .title-box {
      font-family: "Microsoft YaHei", sans-serif;
      font-size: 16px;
      text-align: left;
      font-weight: 700;
      margin-bottom: 22px;
    }
    /deep/.el-form-item__content {
      text-align: left;
      .el-radio-group {
        width: 100%;
      }
    }
    .btnAppPop {
      display: flex;
      width: 100%;
      justify-content: flex-start;
      align-items: center;
      .appendBtn {
        flex: 0 0 98px;
        height: 36px;
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
      }
      /deep/.el-input {
        flex: 1;
        .el-input__inner {
          border-top-right-radius: 0;
          border-bottom-right-radius: 0;
        }
      }
      span {
        flex: 1;
        /deep/.el-input__inner {
          border-top-right-radius: 0;
          border-bottom-right-radius: 0;
        }
      }
    }
  }
}
</style>
<style lang="scss">
.pop-con-box {
  width: 514px;
  .pop-con-box-item {
    margin-bottom: 10px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    span {
      flex: 1;
    }
    div {
      flex: 3;
    }
  }
}
</style>

这样在formDataView.vue表单中只进行了表单校验的操作,在view.vue主页面只进行提交交互操作,而处理form表单内部数据的操作都在formDataMixins.js脚本进行,在formDataMixins.js页面声明数据,处理数据,然后双向绑定this.formData避免了大量的父子组件之间的传值,这样可以使formDataView.vue表单组件尽可能的重用,formDataMixins.js脚本的数据在不同页面的相同数据情况下,也可以大量重复使用,从而避免多次声明数据的重复操作,也避免了view.vue主页面代码量过大,从而导致的后期难以维护。

  • 效果如图: image.png

自定义tree组件

  • treeView.vue
<template>
  <div class="treeview">
    <el-tree
      v-if="treeDataObj"
      ref="tree"
      :data="treeDataObj.treeData"
      :default-expanded-keys="treeDataObj.expendNodesItems"
      :current-node-key="treeDataObj.currentNodeKey"
      :props="treeDataObj.defaultProps"
      :node-key="treeDataObj.id"
      :expand-on-click-node="treeDataObj.expand_on_click_node"
      @node-click="treeDataObj.handleNodeClick"
    ></el-tree>
  </div>
</template>

<script>
export default {
  name: "treeview",
  props: {
    treeDataObj: {
      type: Object,
      default: () => null,
    },
  },
  created() {
    console.log(this.treeDataObj);
  },
};
</script>
<style lang="scss" scoped>
.treeview {
  width: 100%;
}
</style>
  • treeViewMixins.js
import sysDocManage from "@/services/sysDocManage.service";
export const treeViewMixins = {
  data() {
    return {
      expendNodesItems: [],
      practiceFieldId: "",
      treeDataObj: {
        treeData: [], //树状数据
        expendNodesItems: [], //展开的数据
        currentNodeKey: "",
        defaultProps: {
          children: "children",
          label: "practiceFieldName",
        },
        id: "practiceFieldId",
        expand_on_click_node: false,
        handleNodeClick: this.handleNodeClick,
      },
    };
  },
  created() {
    this.initTreeDataView();
  },
  methods: {
    initTreeProp(data) {
      this.treeDataObj = {
        treeData: data, //树状数据
        expendNodesItems: this.expendNodesItems.map((m) => m.practiceFieldId), //展开树的数组
        currentNodeKey: this.practiceFieldId, //最开始选中的节点
        defaultProps: {
          children: "children",
          label: "practiceFieldName",
        },
        id: "practiceFieldId",
        expand_on_click_node: false,
        handleNodeClick: this.handleNodeClick,
      };
    },
    initTreeDataView() {
      sysDocManage.getPracticeDictTree().then((res) => {
        let { code, data, message } = res;
        if (code == 200) {
          this.findNodeID(data);
          this.treeDataObj = null; //因为有些属性,需要一起传入,所以要
          this.$nextTick(() => {
            this.initTreeProp(data);
          });
          this.initData();
        } else {
          this.$message.error(message);
        }
      });
    },
    findNodeID(arr, practiceFieldId) {
      let obj = arr.find((a) => {
        if (practiceFieldId) {
          //如果有id,说明要找固定的id的node
        } else {
          //没有传id,说明找到第一个的最后的那个数据
          if (
            Object.prototype.toString.call(a.children).slice(8, -1) == "Array"
          ) {
            if (this.findNodeID(a.children, practiceFieldId)) {
              return true;
            }
          } else {
            this.practiceFieldId = a.practiceFieldId; //找到了第一层的最底层
            return true;
          }
        }
      });
      if (obj) {
        this.expendNodesItems.unshift(obj);
      }
      return obj;
    },
    handleNodeClick(data, node) {
      console.log(data, node);
      if (node.isLeaf) {
        this.practiceFieldId = data.practiceFieldId;
        this.initData();
      }
    },
  },
};
  • viewVue.vue
<template>
  <div>
      <treeView :treeDataObj="treeDataObj"></treeView>
  </div>
 </template>
 import { treeViewMixins } from "./treeViewMixins";
 export default {
  mixins: [treeViewMixins]
  components: {
    treeView,
  },
 }

自定义table组件

  • tableView.vue
<template>
  <div class="table-view">
    <ValidationObserver ref="observer">
      <el-table
        :span-method="arraySpanMethod"
        :height="maxHightNum || '100%'"
        :data="value"
        border
        @selection-change="handleSelectionChange"
      >
        <el-table-column
          v-if="selection"
          type="selection"
          width="50"
        ></el-table-column>
        <el-table-column
          v-if="order"
          type="index"
          label="序号"
          width="50"
        ></el-table-column>
        <el-table-column
          v-for="column in tableColumns"
          :key="column.prop"
          :label="column.label"
          :prop="column.prop"
          :type="column.type"
          :width="column.width"
        >
          <template slot-scope="scope">
            <template v-if="column.render">
              <cusSlot
                :column="column"
                :row="scope.row"
                :index="scope.$index"
                :render="column.render"
              ></cusSlot>
            </template>
            <template v-else-if="column.isEdit">
              <template v-if="column.type == 'input'">
                <ValidationProvider
                  :rules="column.rules.join('|')"
                  v-slot="{ errors }"
                >
                  <el-input
                    :maxlength="30"
                    placeholder="请输入"
                    v-model="scope.row[column.prop]"
                    :class="{ 'error-txt': errors[0] }"
                    clearable
                  >
                  </el-input>
                  <span class="validate-span">{{ errors[0] }}</span>
                </ValidationProvider>
              </template>
              <template v-else-if="column.type == 'select'">
                <ValidationProvider
                  :rules="column.rules.join('|')"
                  v-slot="{ errors }"
                >
                  <el-select
                    v-model="scope.row[column.prop]"
                    :placeholder="column.placeholder"
                    :disabled="column.disabled"
                    :multiple="column.multiple"
                    :size="column.size || 'medium'"
                    filterable
                    :clearable="!column.inClearable"
                    style="width: 100%"
                    :class="{ 'error-txt': errors[0] }"
                  >
                    <el-option
                      v-for="selectItem in column.selects"
                      :key="selectItem.value"
                      :label="selectItem.label"
                      :value="selectItem.value"
                      @click.native="
                        column.selectClick(selectItem, column.prop)
                      "
                    >
                    </el-option>
                  </el-select>
                  <span class="validate-span">{{ errors[0] }}</span>
                </ValidationProvider>
              </template>
            </template>
            <template v-else>
              {{ scope.row[column.prop] }}
            </template>
          </template>
        </el-table-column>
      </el-table>
    </ValidationObserver>
  </div>
</template>

<script>
import Sortable from "sortablejs";//用来进行elementUI拖拽使用
//自定义函数式组件 ==START
let cusSlot = {
  functional: true, //代表这是一个函数式组件
  props: {
    column: Object, //接受父组件传来得数据
    row: Object,
    index: Number,
    render: Function, //接受父组件传来的回调函数,用于把JSX编写的DOM以及DOM上的事件抽取出来到数据脚本页面,尽量不污染外部组件,使组件更加的灵活轻便可复用。
  },
  render: (h, context) => {
    //函数式组件用来渲染相应的DOM,有两个参数,h:render渲染函数,context:上下文对象,可以获取相应父组件传到props中的数据
    let cell = {
      column: context.props.column,
      row: context.props.row,
      index: context.props.index,
    };
    //使用父组件传来的render回调函数,将JSX编写的DOM对象抽取出去,保持组件的灵活性。*****关键*****
    return context.props.render(h, cell);
  },
};
//自定义函数式组件 ==END
//校验组件 ==START
import { required } from "vee-validate/dist/rules";
import { ValidationProvider, ValidationObserver, extend } from "vee-validate"; //自定义校验的方法
// import validate from "@/utils/validateCus.js";
extend("required", {
  ...required,
  message: "此项必填",
});
//校验组件 ==END
export default {
  name: "tableView",
  props: {
    value: {
      type: Array,
      default: () => [],
    },
    tableColumns: {
      type: Array,
      default: () => [],
    },
    order: {
      type: Boolean,
      default: false,
    },
    selection: {
      type: Boolean,
      default: false,
    },
    draggable: {
      type: String,
      default: "",
    },//给elementUI table添加拖拽的属性。
    arraySpanMethod: {
      //如果table要内容折叠,就传入这个函数
      type: Function,
      default: () => {},
    },
    maxHightNum: {
      type: String,
      default: "",
    },
  },
  watch: {
    value: {
      handler(newVal) {
        this.$emit("input", newVal);
      },
      deep: true,
      immediate: true,
    },
  },
  mounted() {
    switch (this.draggable) {
      case "rowDraggy":
        //行拖拽
        this.rowDraggy();
        break;
      case "colDraggy":
        this.colDraggy();
        //列拖拽
        break;

      default:
        break;
    }
  },
  components: {
    cusSlot,
    ValidationProvider,
    ValidationObserver,
  },
  methods: {
    handleSelectionChange(val) {
      this.$emit("selectionItems", val);
    },
    rowDraggy() {
      const tbody = document.querySelector(".el-table__body-wrapper tbody");
      const _this = this;
      Sortable.create(tbody, {
        onEnd({ newIndex, oldIndex }) {
          _this.$emit("rowDraggyEnd", {
            newIndex,
            oldIndex,
          });
        },
      });
    },
    colDraggy() {
      const wrapperTr = document.querySelector(".el-table__header-wrapper tr");
      const _this = this;
      this.sortable = Sortable.create(wrapperTr, {
        animation: 180,
        delay: 0,
        handle: ".allowDrag", // 格式为简单css选择器的字符串,使列表单元中符合选择器的元素成为拖动的手柄,只有按住拖动手柄才能使列表单元进行拖动
        filter: ".noDrag", // 过滤器,不需要进行拖动的元素
        preventOnFilter: true, //  在触发过滤器`filter`的时候调用`event.preventDefault()`
        draggable: ".allowDrag", // 允许拖拽的项目类名
        onEnd: (evt) => {
          _this.$emit("colDraggy", {
            newIndex: evt.newIndex,
            oldIndex: evt.oldIndex,
          });
        },
      });
    },
    validateFun() {
      if (this.$refs.observer) {
        this.$refs.observer
          .validate()
          .then((res) => {
            this.$emit("validateTable", res);
          })
          .catch((err) => {
            this.$emit("validateTable", err);
          });
      }
    },
  },
};
</script>
<style lang="scss" scoped>
.table-view {
  width: 100%;
  .validate-span {
    font-size: $extra-small-font-size;
    color: tomato;
  }
  /deep/.el-select.error-txt .el-input__inner,
  /deep/.el-textarea.error-txt .el-textarea__inner,
  /deep/.el-input.error-txt .el-input__inner,
  /deep/.el-cascader.error-txt .el-input__inner {
    border: 1px solid tomato !important;
  }
}
</style>
  • tableViewMixins.js
import organizeManageService from "@/services/organizeManage.service";
import sysDocManage from "@/services/sysDocManage.service";
export const tableViewMixins = {
  data() {
    return {
      formValidateRes: false,
      tableValidateRes: false,
      paginator: {
        currentPage: 1,
        pageSize: 10,
        totalCount: 0,
      },
      loading: false,
      selection: true,
      selectTableItems: [],
      tableData: [],
      tableDataInit: [],
      tableColumns: [
        {
          label: "体系文件",
          prop: "fileName",
          disabled: false,
          render: (h, tableItem) => {
            return (
              <div>
                <el-button
                  type="text"
                  disabled={tableItem.column.disabled}
                  on-click={() => this.viewSysDoc(tableItem)}
                >
                  {tableItem.row[tableItem.column.prop]}
                </el-button>
              </div>
            );
          },
        },
        {
          label: "分类",
          width: "200",
          prop: "fileTypeId",
          isEdit: true,
          disabled: false,
          type: "select",
          placeholder: "请选择分类",
          size: this.$store.state.settings.formSize,
          selects: this.sysFileTypesSelect || [],
          rules: ["required"],
          selectClick: (item, prop) => this.selectTableClick(item, prop),
        },
        {
          label: "操作",
          prop: "operation",
          width: "200",
          disabled: false,
          render: (h, tableItem) => {
            return (
              <div>
                <el-button
                  type="text"
                  disabled={tableItem.column.disabled}
                  on-click={() => this.deleteClick(tableItem)}
                >
                  删除
                </el-button>
              </div>
            );
          },
        },
      ],
    };
  },
  methods: {
    selectionItems(selectItems) {
      console.log("selectionItems", selectItems);
      this.selectTableItems = selectItems;
    },
    deleteClick(tableItem) {
      console.log("deleteClick", tableItem);
      this.tableData.splice(tableItem.index, 1);
    },
    selectTableClick(item, prop) {
      console.log("selectTableClick", item, prop);
    },
    getTableTypes(formData) {
      console.log("getTableTypes formData", formData, this.tableData);
      let tableDataT = [];
      this.tableData.forEach((t) => {
        if (this.selectTableItems.find((s) => s.fileInfoId == t.fileInfoId)) {
          t.fileTypeId = formData.fileTypeId;
        }
        tableDataT.push(t);
      });
      this.$set(this, "tableData", tableDataT);
      this.dialogVisible = false;
    },
    getFormApprove(formData) {
      console.log("getFormApprove formData", formData);
      let tempArr = this.handleEPGs(formData);
      this.saveAndSubmitFun(this.handleSaveAndSubmitParams(tempArr));
    },
    validateTable(vail) {
      this.formValidateRes = vail;
      if (vail && this.tableValidateRes) {
        this.isNeedApprove();
      }
    },
    validateConfirm(vail) {
      this.tableValidateRes = vail;
      if (vail && this.formValidateRes) {
        this.isNeedApprove();
      }
    },
    isNeedApprove() {
      organizeManageService
        .trainNeedsNeedApprove({
          applyType: "", //待输入
        })
        .then((res) => {
          let { code, message, data } = res;
          if (code == 200) {
            //先按照全部需要审批做,
            if (this.$route.name == "applyview") {
              this.saveAndSubmitFunApplyView(this.handleSaveAndSubmitParams());
            } else {
              this.setFormApprove();
            }
            // if (data) {
            //   // 需要审批,
            //   this.setFormApprove();
            // } else {
            //   //不需要审批的情况,直接请求
            //   this.saveAndSubmitFun(this.handleSaveAndSubmitParams());
            // }
          } else {
            this.$message.error(message);
          }
        });
    },
    saveAndsubmit() {
      if (this.tableData.length == 0) {
        this.$message.error("请上传文件后,再保存");
      } else {
        this.$refs.fromViewRef.confirmValidate();
        this.$refs.tableViewRefs.validateFun();
      }
    },
    handleTime(val) {
      if (val > 10) {
        return val;
      } else {
        return `0${val}`;
      }
    },
    handleSaveAndSubmitParams(FlowRoleList) {
      let params = {
        applicationFlowRoleUserVoList: FlowRoleList || [],
        executeDatetime:
          Object.prototype.toString
            .call(this.formData.executeDatetime)
            .slice(8, -1) == "String"
            ? this.formData.executeDatetime
            : `${this.formData.executeDatetime.getFullYear()}-${this.handleTime(
                this.formData.executeDatetime.getMonth() + 1
              )}-${this.handleTime(this.formData.executeDatetime.getDate())}`,
        publishDatetime:
          Object.prototype.toString
            .call(this.formData.publishDatetime)
            .slice(8, -1) == "String"
            ? this.formData.publishDatetime
            : `${this.formData.publishDatetime.getFullYear()}-${this.handleTime(
                this.formData.publishDatetime.getMonth() + 1
              )}-${this.handleTime(this.formData.publishDatetime.getDate())}`,
        revisedNotes: this.formData.revisedNotes,
        systemFilePvoList: [],
        systemVersion: (this.formData.systemVersion + "").includes(".")
          ? `${this.formData.systemVersion}`
          : `${this.formData.systemVersion}.0`,
        systemVersionId: this.formData.systemVersionId || "",
      };
      this.tableData.forEach((t) => {
        params.systemFilePvoList.push({
          fileInfoId: t.fileInfoId,
          fileName: t.fileName,
          fileType: this.sysFileTypesSelect.find(
            (sys) => sys.value == t.fileTypeId
          ).label,
          fileTypeId: t.fileTypeId,
          sort: 0,
          systemFileId: t.systemFileId || "",
        });
      });
      return params;
    },
    saveAndSubmitFun(params) {
      console.log("saveAndSubmitFun", params);
      sysDocManage.sysFileLibManageSaveAndPublish(params).then((res) => {
        let { code, message } = res;
        if (code == 200) {
          this.dialogVisible = false;
          this.saveContentData();
          this.$router.push({
            path: "/bgManage/sysDocManagement",
          });
        } else {
          this.$message.error(message);
        }
      });
    },
    setFormApprove() {
      //发起审批
      let auditDialogData = {};
      this.auditDialogContents.forEach((r) => {
        auditDialogData[r.prop] = "";
      });
      this.dialogVal = {
        title: "发起审批",
        content: {
          type: "fromData",
          contents: this.auditDialogContents,
          handleType: "setApprove", //处理类型
          defaultVal: auditDialogData, //如果有值的话,传到这里
        }, // 这里可以编写弹窗的具体内容
      };
      this.dialogVisible = true;
    },
    handleEPGs(formData) {
      console.log("this.roleEpgZcs", this.roleEpgZcs, formData);
      let tempArr = [];
      this.roleEpgZcs.forEach((r) => {
        if (this.roleEpgZcs.find((e) => e.value == formData.epgId)) {
          tempArr.push({
            roleId: "ROLE_EPG_ZC",
            roleName: "EPG组长",
            userId: r.value,
            userName: r.label,
          });
        }
      });
      return tempArr;
    },
    batchSetType() {
      console.log(
        "batchDelete",
        this.selectTableItems,
        this.typeDialogContents
      );
      this.setTableType();
    },
    setTableType() {
      //设置分类
      let typeDialogData = {};
      this.typeDialogContents.forEach((r) => {
        typeDialogData[r.prop] = "";
      });
      this.dialogVal = {
        title: "设置分类",
        content: {
          type: "fromData",
          labelWidth: "60px",
          contents: this.typeDialogContents,
          handleType: "setType", //处理类型
          defaultVal: typeDialogData, //如果有值的话,传到这里
        }, // 这里可以编写弹窗的具体内容
      };
      this.dialogVisible = true;
    },
    batchDelete() {
      let selectTableItemsTemp = [];
      this.tableData.forEach((t) => {
        if (!this.selectTableItems.find((s) => s.fileInfoId == t.fileInfoId)) {
          selectTableItemsTemp.push(t);
        }
      });
      this.tableData = selectTableItemsTemp;
    },
    batchUpload() {
      this.initInputFileDom();
    },
    initInputFileDom() {
      if (document.getElementById("inputUploadElCus")) {
        //如果input file标签存在,则先删除该标签,以免出现过多的这样的标签
        this.removeInputFileDom();
      }
      let inputEl = document.createElement("input");
      inputEl.setAttribute("id", "inputUploadElCus");
      inputEl.setAttribute("type", "file");
      inputEl.setAttribute("multiple", "multiple");
      inputEl.style.display = "none";
      document.body.appendChild(inputEl);
      inputEl.addEventListener("change", this.getFileData);
      inputEl.click();
    },
    removeInputFileDom() {
      document
        .getElementById("inputUploadElCus")
        .parentNode.removeChild(document.getElementById("inputUploadElCus"));
    },
    getFileData(e) {
      let formData = new FormData(); //创建一个空对象实例
      for (let i = 0; i < e.target.files.length; i++) {
        formData.append("file", e.target.files[i]);
      }
      sysDocManage.sysFileLibManageUploads(formData).then((res) => {
        let { code, data, message } = res;
        if (code == 200) {
          data.forEach((d) => {
            this.tableData.push(d);
          });
        } else {
          this.$message.error(message);
        }
      });
    },
    handleSizeChange(val) {
      this.paginator.pageSize = val;
      this.initTableData();
    },
    handleCurrentChange(val) {
      this.paginator.currentPage = val;
      this.initTableData();
    },
  },
};
  • viewVue.vue
<template>
  <div>
    <div class="tablebox">
       <tableView
          v-loading="loading"
          ref="tableViewRefs"
          :order="true"
          :selection="true"
          v-model="tableData"
          :tableColumns="tableColumns"
          @selectionItems="selectionItems"
          @validateTable="validateTable"
        ></tableView>
     </div>
  </div>
 </template>
 <script>
 import tableView from "../components/tableView.vue";
 import { tableViewMixins } from "../mixins/tableViewMixinsSDLMAM.js";
 export default {
   mixins: [ tableViewMixins],
   components: {
    tableView,
  },
 }
 </script>

大部分自定义的组件,只要牵涉到数据处理,大概就是这样写的,都大同小异,但是主要时这样的思想,数据从哪里生成在哪里处理(在...mixin.js中生成和处理数据),组件只是展示数据的容器,一个组件用最简单的结构展示最复杂的内容的时候,这个组件才可能被最大程度的重复利用,而要做到这样的效果,就需要让组件结合Vue函数式编程和JSX语言,这样就可以尽可能的做出最容易重复利用的组件

tab与动态组件加载页面结合

  • sysDoc.config.ts
export const componentsItem: any[] = [
  {
    props: "PM",
    component: () => import("./views/pm.vue"), //实践管理
  },
  {
    props: "SDLM",
    component: () => import("./views/sdlm.vue"), //体系文件库管理
  },
  {
    props: "MCRBSDS",
    component: () => import("./views/mcrbsds.vue"), //体系文件与标准对照关系管理
  },
];
  • index.vue
<template>
  <div class="train-box full-height-remedy flex-column-remedy boxHidden">
    <div class="flex justify-between bgc">
      <div>
        <el-button type="primary" size="medium" @click="goBack">返回</el-button>
      </div>
    </div>
    <div
      class="
        app-container
        padding-bottom-none-remedy
        flex-1-remedy
        boxHidden
        flex-cus
      "
    >
      <el-tabs v-model="activeName" type="card" :before-leave="beforeLeave">
        <el-tab-pane
          label="体系文件与标准对照关系管理"
          name="MCRBSDS"
        ></el-tab-pane>
        <el-tab-pane label="体系文件库管理" name="SDLM"></el-tab-pane>
        <el-tab-pane label="实践管理" name="PM"></el-tab-pane>
      </el-tabs>
      <div class="flex flex-1 flex-row boxHidden">
        <component
          v-if="componentObj"
          ref="components"
          :capLevels="capLevels"
          :practiceFields="practiceFields"
          :sysFileTypes="sysFileTypes"
          :versionList="versionList"
          :is="componentObj"
        ></component>
      </div>
    </div>
  </div>
</template>

<script>
import { componentsItem } from "./sysDoc.config";
import assetLibrary from "@/services/assetLibrary.service";
import sysDocManage from "@/services/sysDocManage.service";
import ConfigService from "@/services/config.service";
export default {
  name: "sysDoc",
  data() {
    return {
      activeName: "PM",
      componentObj: null,
      capLevels: [], //能力等级
      practiceFields: [], //实践域
      sysFileTypes: [], //体系文件分类
      versionList: [], //版本下拉框
      roleEpgZcs: [], //epg组成员列表
    };
  },
  created() {
    this.initData();
  },
  beforeRouteEnter(to, form, next) {
    if (["sdlmAdd", "sdlmModify"].includes(form.name)) {
      next((vm) => {
        vm.$nextTick(() => {
          vm.activeName = "SDLM";
        });
      });
    } else {
      next();
    }
  },
  methods: {
    initRoleEpgZcs(res) {
      let { code, data, message } = res;
      if (code == 200) {
        this.roleEpgZcs = data;
      } else {
        this.$message.error(message);
      }
    },
    listDircByType(res) {
      let { data, message, code } = res;
      if (code == 200) {
        this.capLevels = [];
        this.practiceFields = [];
        this.sysFileTypes = [];
        if (data) {
          data.CAPABILITY_LEVEL &&
            data.CAPABILITY_LEVEL.forEach((d) => {
              this.capLevels.push({
                value: d.dircId,
                label: d.dicrName,
              });
            });
          data.CAPABILITY_LEVEL &&
            data.PRACTICE_FIELD.forEach((d) => {
              this.practiceFields.push({
                value: d.dircId,
                label: d.dicrName,
              });
            });
          data.SYSTEM_FILE_TYPE &&
            data.SYSTEM_FILE_TYPE.forEach((d) => {
              this.sysFileTypes.push({
                value: d.dircId,
                label: d.dicrName,
              });
            });
        } else {
          this.$message.error("下拉框数据为空");
        }
      } else {
        this.$message.error(message);
      }
    },

    sysGetVersionNum(res) {
      let { data, message, code } = res;
      if (code == 200) {
        this.versionList = [];
        if (data) {
          data.forEach((d) => {
            this.versionList.push({
              value: d.systemVersionId,
              label: d.systemVersion,
              content: { ...d },
            });
          });
        }
      } else {
        this.$message.error(message);
      }
    },
    initData() {
      Promise.all([
        assetLibrary.listDircByType({
          dictTypeList: [
            "PRACTICE_FIELD",
            "CAPABILITY_LEVEL",
            "SYSTEM_FILE_TYPE",
          ],
        }),
        sysDocManage.sysGetVersionNum({
          type: "ALL",
        }),
        ConfigService.findUserListByRoleId({ roleId: "ROLE_EPG_ZC" }),
      ])
        .then((result) => {
          this.listDircByType(result[0]);
          this.sysGetVersionNum(result[1]);
          this.initRoleEpgZcs(result[2]);
          //必要有这些下拉框数据才能渲染这个component
          this.componentObj = componentsItem.find(
            (c) => c.props == this.activeName
          ).component;
        })
        .catch((err) => {
          console.log(err);
        });
    },
    beforeLeave(activeName) {
      console.log("beforeLeave activeName", activeName);
      this.componentObj = componentsItem.find(
        (c) => c.props == activeName
      ).component;
    },
    goBack() {
      this.$router.go(-1);
    },
  },
};
</script>

<style lang="scss" scoped>
/deep/ .el-tabs {
  .el-tabs__content {
    min-height: 0;
  }
  .el-tabs__header {
    margin-bottom: 0;
    padding: 10px 0;
    box-sizing: border-box;
  }
}
.flex-cus {
  display: flex;
  flex-direction: column;
}
.padding-scroll-style {
  padding-left: 10px;
  box-sizing: border-box;
}
</style>

批量引入自定义组件

  • components.ts
import {
  SearchItem,
  PageFragment,
  DetailItem,
  Pagination,
  MyTable,
  BaseInfo,
  MyPlainButton,
  OperationBtn,
  Breadcrumb,
  DropDown,
  ComCollapse,
  UploadDialog,
  ProcessDialog,
  UploadDialogWithSecLevel,
  RangeNumberInput,
  StaffSelectTree,
  BaseTable,
  BaseNumber,
  ApproverComponent,
  DeleteApproveDialog,
} from ".";

const components: any = {
  SearchItem,
  PageFragment,
  DetailItem,
  Pagination,
  MyTable,
  BaseInfo,
  MyPlainButton,
  OperationBtn,
  Breadcrumb,
  DropDown,
  ComCollapse,
  UploadDialog,
  ProcessDialog,
  UploadDialogWithSecLevel,
  RangeNumberInput,
  StaffSelectTree,
  BaseTable,
  BaseNumber,
  ApproverComponent,
  DeleteApproveDialog,
};

const install = (Vue: any) => {
  Object.keys(components).forEach((key: any) => {
    Vue.component(key, components[key]);
  });
};

const MyPlugin = {
  install,
};
export default MyPlugin;
  • main.ts
import Vue from "vue";
import MyPlugin from "./components/my-plugin";
Vue.use(MyPlugin);