hcong
2024-10-18 e95ca9ef89c79fbff8f0d1394311f5f18d653cdd
动态表头
已修改4个文件
已添加2个文件
1222 ■■■■■ 文件已修改
src/components.d.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/evaluation/EvalutationEdit.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/evaluation/EvalutationRecordTest.vue 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/evaluation/components/ChangeableColumnTable.vue 585 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts
@@ -19,11 +19,14 @@
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCascader: typeof import('element-plus/es')['ElCascader']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElContainer: typeof import('element-plus/es')['ElContainer']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
    ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -41,7 +44,11 @@
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
@@ -55,6 +62,7 @@
    ElTabs: typeof import('element-plus/es')['ElTabs']
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    Footer: typeof import('./components/core/Footer.vue')['default']
src/constants/menu.js
@@ -72,6 +72,11 @@
  //   name: '整改审核',
  // },
  {
    path: '/fysp/sceneInfo',
    icon: 'Files',
    name: '场景信息'
  },
  {
    icon: 'DocumentChecked',
    name: '自动评估',
    children: [
@@ -84,6 +89,16 @@
        path: '/fysp/evaluation/evalutationRecord',
        icon: 'Tickets',
        name: '评估记录',
      },
      {
        path: '/fysp/evaluation/evalutationRecordRank',
        icon: 'Tickets',
        name: '评估排名',
      },
      {
        path: '/fysp/evaluation/evalutationRecordEdit',
        icon: 'Tickets',
        name: '评分调整',
      },
    ],
  },
@@ -114,6 +129,28 @@
    icon: 'Files',
    name: '对外支持'
  },
  // {
  //   icon: 'Search',
  //   name: '业务分析',
  //   children: [
  //     {
  //       path: '/analysis/profollow',
  //       icon: 'Search',
  //       name: '问题动态跟踪',
  //     },
  //     {
  //       path: '/analysis/proanalysis',
  //       icon: 'Search',
  //       name: '问题整改分析',
  //     },
  //     {
  //       path: '/analysis/standardjudge',
  //       icon: 'Search',
  //       name: '规范性评估',
  //     },
  //   ],
  // },
  // {
  //   path: '/dailyreport',
  //   icon: 'Search',
src/router/index.js
@@ -122,11 +122,17 @@
    component: () => import('@/views/fysp/config/EvalutationRule.vue')
  },
  {
    //评估子规则修改页面
    name: 'fyspEvalutationEdit',
    path: '/fysp/config/evalutationRule/CompEvalutionSubRuleUpd/:subRuleId',
    component: () => import('@/views/fysp/config/components/evalution/CompEvalutionSubRuleUpd.vue'),
  },
  {
    //评估数据源
    name: 'fyspEvalutationTask',
    path: '/fysp/evaluation/evalutationTask',
    component: () => import('@/views/fysp/evaluation/EvalutationTask.vue'),
    meta: { keepAlive: false }
    meta: { keepAlive: true }
  },
  {
    //评估管理
@@ -136,6 +142,20 @@
    meta: { keepAlive: true }
  },
  {
    //评估排名
    name: 'fyspEvalutationRecordRank',
    path: '/fysp/evaluation/evalutationRecordRank',
    component: () => import('@/views/fysp/evaluation/EvalutationRecordTest.vue'),
    meta: { keepAlive: true, readonly: true  }
  },
  {
    //评分调整
    name: 'fyspEvalutationRecordEdit',
    path: '/fysp/evaluation/evalutationRecordEdit',
    component: () => import('@/views/fysp/evaluation/EvalutationRecordTest.vue'),
    meta: { keepAlive: true, readonly: false }
  },
  {
    //评估结果详情
    name: 'fyspEvalutationEdit',
    path: '/fysp/evaluation/evalutationEdit/:subTaskId',
src/views/fysp/evaluation/EvalutationEdit.vue
@@ -2,7 +2,10 @@
  <FYPageHeader title="评估结果详情"></FYPageHeader>
  <el-row v-for="item in evaluation" :key="item.id"> </el-row>
  <div class="btns">
    <el-button type="primary" @click="submit" :disabled="!isUpdated">提交</el-button>
    <!-- <el-button type="primary" @click="submit" :disabled="!isUpdated">提交</el-button> -->
  </div>
  <div class="fixed-div">
    <span>总分: {{ score }}</span>
  </div>
  <el-table
    class="table-style"
@@ -26,6 +29,7 @@
    <el-table-column prop="two_maxScore" label="最大分值" width="90" />
    <el-table-column v-slot="scope" prop="three_title" label="具体问题">
      <el-checkbox
        :disabled="disable"
        v-model="scope.row.three_select"
        @change="(checked) => threeSelectChange(checked, scope.row)"
        >{{ scope.row.three_title }}</el-checkbox
@@ -46,6 +50,8 @@
  },
  data() {
    return {
      disable: false,
      score: '',
      tableData: [],
      evaluation: [],
      subTaskId: '',
@@ -100,27 +106,33 @@
    },
    /** æä»· */
    submit() {
      this.disable = true;
      evaluateApi
        .updateScore({
          subTaskId: this.subTaskId,
          itemList: this.checkedUpdatedList
        })
        .then((res) => {
          if (res.success) {
            ElMessage({
              message: res.message,
              type: 'success'
            });
          }else {
            ElMessage({
              message: res.message,
              type: 'error'
            });
          }
          this.disable = false;
          this.score = res.data.score;
          // if (res.success) {
          //   ElMessage({
          //     message: res.message,
          //     type: 'success'
          //   });
          // }else {
          //   ElMessage({
          //     message: res.message,
          //     type: 'error'
          //   });
          // }
        })
        .catch((e) => {
          this.disable = false;
        });
      setTimeout(() => {
        this.getList();
      }, 1000);
      }, 200);
    },
    /** é€šè¿‡ç¬¬ä¸‰çº§çš„id获取上级以及顶级 */
    getSuperObjByThreeId(threeId, list, path = []) {
@@ -146,6 +158,7 @@
    /** é—®é¢˜é€‰æ‹©æ¡† */
    threeSelectChange(isSelect, row) {
      this.isUpdated = true;
      this.submit();
    },
    /** åˆ—合并 */
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
@@ -241,6 +254,7 @@
      this.subTaskId = this.$route.params.subTaskId;
      evaluateApi.fetchItemEvaluation(this.subTaskId).then((res) => {
        this.isUpdated = false;
        this.score = res.data.score;
        this.tableData = this.genTableData(res.data.details);
      });
    },
@@ -276,4 +290,15 @@
.red-cell {
  background-color: red;
}
/* å›ºå®šå®šä½çš„ div */
.fixed-div {
  position: fixed;
  top: 7%; /* è·ç¦»é¡¶éƒ¨ 20px */
  right: 20px; /* è·ç¦»å³ä¾§ 20px */
  width: 200px; /* div çš„宽度 */
  background-color: #f2f2f2; /* èƒŒæ™¯é¢œè‰² */
  padding: 20px; /* å†…边距 */
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* é˜´å½±æ•ˆæžœ */
  z-index: 1000; /* ç¡®ä¿ div åœ¨é¡µé¢å…¶ä»–内容之上 */
}
</style>
src/views/fysp/evaluation/EvalutationRecordTest.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,519 @@
<template>
  <ChangeableColumnTable
    @search="onSearch"
    :pagination="false"
    :cols="cols"
    ref="tableRef"
    @select-updated="selectUpdated"
    @select-batch-updated="selectBatchUpdated"
  >
    <template #options>
      <!-- åŒºåŽ¿ -->
      <FYOptionLocation
        :allOption="false"
        :level="3"
        :checkStrictly="false"
        v-model:value="formSearch.locations"
      ></FYOptionLocation>
      <!-- åœºæ™¯ç±»åž‹ -->
      <FYOptionScene
        :allOption="false"
        :type="2"
        v-model:value="formSearch.scenetype"
      ></FYOptionScene>
      <!-- æ—¶é—´ -->
      <FYOptionTime :initValue="false" type="month" v-model:value="formSearch.time"></FYOptionTime>
    </template>
    <template #buttons>
      <!-- <el-button icon="Download" size="default" type="success" @click="download"
        >规范性评估与分析报告</el-button
      > -->
      <CompReport
        :locations="formSearch.locations"
        :scenetype="formSearch.scenetype"
        :time="formSearch.time"
      ></CompReport>
    </template>
    <template #options-expand>
      <!-- <div class="flex-div flex-v-center">
        <el-form-item label="部分扣分">
          <el-select
            style="width: 310px"
            collapse-tags
            collapse-tags-tooltip
            v-model="partScoreHeads"
            multiple
            placeholder="部分扣分项"
            clearable
            filterable
            @change="onSelectionChange"
          >
            <el-option
              v-for="item in cols.filter((item) => 'children' in item)"
              :key="item"
              :label="item.label"
              :value="item.name"
            /> </el-select
        ></el-form-item>
        <el-tooltip placement="bottom-start" effect="light">
          <template #content>
            <el-text size="b">选中的条目对应的扣分总和将会显示在表格中的部分扣分列中</el-text><br />
          </template>
          <el-icon class="m-l-8 cursor-p" :size="16" color="var(--el-color-warning)"
            ><QuestionFilled
          /></el-icon>
        </el-tooltip>
      </div>-->
      <el-form :inline="true">
        <CompQuickSet @quick-set="setOptions"></CompQuickSet>
      </el-form>
    </template>
  </ChangeableColumnTable>
</template>
<script>
import ChangeableColumnTable from './components/ChangeableColumnTable.vue';
import CompReport from './components/CompReport.vue';
import evaluateApi from '@/api/fysp/evaluateApi';
import { useCloned } from '@vueuse/core';
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus';
import { useMessageBox, useMessageBoxTip } from '../../../composables/messageBox';
export default {
  components: {
    ChangeableColumnTable,
    CompReport,
  },
  data() {
    return {
      partScoreHeads: [],
      editRecordSwitch: false,
      dataReady: false,
      area: {},
      formSearch: {
        locations: {},
        scenetype: {},
        time: dayjs().add(-1, 'M').date(1).toDate()
      },
      refreshTable: true,
      cols: [],
      tableData: []
    };
  },
  methods: {
    onSelectionChange() {
      for (const tableItem of this.tableData) {
        tableItem.partSum = 0;
        for (const headItem of this.partScoreHeads) {
          if (headItem in tableItem) {
            tableItem.partSum += tableItem[headItem];
          } else {
            tableItem.partSum = '';
          }
        }
      }
    },
    // æŒ‡å®šæ·»åŠ çš„è¡¨å¤´é¡ºåº
    addHead(headObj, index) {
      this.cols.splice(index, 0, headObj);
    },
    async selectBatchUpdated(rows, name) {
      var promises = [];
      for (const row of rows) {
        if (!row.subTaskId || row.subTaskId == null) {
          for (var attr in row) {
            if (attr.endsWith('ScoreSelect')) {
              row[attr] = false;
            }
          }
          continue;
        }
        var checkedUpdatedList = [];
        for (const attr in row) {
          if (attr.endsWith('ScoreSelect') && Boolean(row[attr])) {
            var id = attr.split('ScoreSelect')[0];
            checkedUpdatedList.push(id);
          }
        }
        promises.push(
          evaluateApi
            .updateScore({
              subTaskId: row.subTaskId,
              itemList: checkedUpdatedList
            })
            .then(() => {
              evaluateApi.fetchItemEvaluation(row.subTaskId).then((res) => {
                this.updateChangeableData(res.data.details, res.data.score, row.subTaskId);
              });
            })
        );
      }
      var reSend = true;
      while (promises.length !== 0 && reSend) {
        reSend = false;
        await Promise.allSettled(promises).then((results) => {
          results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
            } else {
              // æˆåŠŸäº†åˆ æŽ‰è¿™ä¸ªpromise
              promises.splice(index, 1);
            }
          });
        });
        ElMessage({
          message: '全部成功',
          type: 'success',
          plain: true
        });
        if (promises.length == 0) {
          // ElMessage({
          //   message: '全部成功',
          //   type: 'success',
          //   plain: true
          // });
        } else {
          // useMessageBoxTip({
          //   confirmMsg: '是否重试',
          //   confirmTitle: '批量更新失败',
          //   onConfirm: async () => {
          //     reSend = true;
          //   }
          // });
        }
      }
    },
    // é€‰ä¸­æ›´æ”¹çš„回调
    selectUpdated(row) {
      if (!row.subTaskId || row.subTaskId == null) {
        for (var attr in row) {
          if (attr.endsWith('ScoreSelect')) {
            row[attr] = false;
          }
        }
        return;
      }
      var checkedUpdatedList = [];
      for (const attr in row) {
        if (attr.endsWith('ScoreSelect') && Boolean(row[attr])) {
          var id = attr.split('ScoreSelect')[0];
          checkedUpdatedList.push(id);
        }
      }
      evaluateApi
        .updateScore({
          subTaskId: row.subTaskId,
          itemList: checkedUpdatedList
        })
        .then(() => {
          evaluateApi.fetchItemEvaluation(row.subTaskId).then((res) => {
            this.updateChangeableData(res.data.details, res.data.score, row.subTaskId);
          });
        });
    },
    _getParam() {
      const { locations, scenetype, time } = this.formSearch;
      return {
        provincecode: locations.pCode,
        provincename: locations.pName,
        citycode: locations.cCode,
        cityname: locations.cName,
        districtcode: locations.dCode,
        districtname: locations.dName,
        starttime: dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
        scensetypeid: scenetype.value
      };
    },
    // æ›´æ–°ä¸€è¡Œæ•°æ®
    updateChangeableData(data, score, subTaskId) {
      for (let index = 0; index < this.tableData.length; index++) {
        const element = this.tableData[index];
        if (element.subTaskId == subTaskId) {
          element.resultscorebef = score;
          this.genChangeableData(data, element);
          // this.tableData[index] = element;
        }
      }
    },
    // ç”Ÿæˆå¾—分条目的数据
    genChangeableData(data, tableItem) {
      for (let index = 0; index < data.length; index++) {
        const element = data[index];
        tableItem[`${element.id}Score`] = element.score;
        if (element.select) {
          tableItem[`${element.id}ScoreSelect`] = element.select;
        }
        var twoLevel = element.subList;
        if (!twoLevel) {
          continue;
        }
        for (let index = 0; index < twoLevel.length; index++) {
          const threeElement = twoLevel[index];
          tableItem[`${threeElement.id}Score`] = threeElement.score;
          if (threeElement.select) {
            tableItem[`${threeElement.id}ScoreSelect`] = threeElement.select;
          }
          var threeLevel = threeElement.subList;
          if (!threeLevel) {
            continue;
          }
          for (let index = 0; index < threeLevel.length; index++) {
            const target = threeLevel[index];
            // å±•示当前子规则的分值
            // tableItem[`${target.id}Score`] = target.maxScore;
            // å±•示当前项的当前得分
            tableItem[`${target.id}Score`] = target.score;
            if (target.select) {
              tableItem[`${target.id}ScoreSelect`] = target.select;
            }
          }
        }
      }
    },
    // ç”Ÿæˆå¾—分条目的表头
    genChangeableHead(data) {
      // ä¸€çº§è¡¨å¤´
      for (let i = 0; i < data.length; i++) {
        const element = data[i];
        const col = {
          fixed: false
        };
        this.cols.push(col);
        col.label = element.title;
        col.name = `${element.id}Score`;
        for (const attr in element) {
          // äºŒçº§è¡¨å¤´
          let children = [];
          col.children = children;
          if (attr === 'subList') {
            for (let j = 0; j < element[attr].length; j++) {
              const twoElement = element[attr][j];
              const child = {
                fixed: false
              };
              children.push(child);
              child.label = twoElement.title;
              child.name = `${twoElement.id}Score`;
              for (const attr2 in twoElement) {
                // ä¸‰çº§æŒ‡æ ‡
                let children2 = [];
                child.children = children2;
                if (attr2 === 'subList') {
                  for (let k = 0; k < twoElement[attr2].length; k++) {
                    const threeElement = twoElement[attr2][k];
                    const child2 = {
                      fixed: false
                    };
                    children2.push(child2);
                    child2.label = threeElement.title;
                    child2.name = `${threeElement.id}Score`;
                    if (this.editRecordSwitch) {
                      child2.needSelect = true;
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    // æŽ’名表头
    genRankHead() {
      var head = {
        name: 'rank',
        label: '总分排名',
        // fixed: 'left'
      };
      this.addHead(head, 0);
    },
    // ç¼–号方法
    makeNumber(array, propName) {
      if (!(array instanceof Array)) {
        return;
      }
      // var copy = [];
      // array.forEach((item) => {
      //   copy.push(item);
      // });
      array.sort((a, b) => b[propName] - a[propName]);
      // var maxIndex = copy.length;
      var minIndex = 1;
      array.forEach((item) => {
        item.rank = minIndex++;
      });
    },
    // æŽ’名数据
    genRankData() {
      this.makeNumber(this.tableData, 'resultscorebef');
    },
    // åŸºæœ¬ä¿¡æ¯è¡¨å¤´
    genTableHeadBaseInfo(data) {
      var cols = [
        {
          name: 'sceneIndex',
          label: '编号',
          fixed: false,
          noncloseable: true
        },
        {
          name: 'createdate',
          label: '巡查日期',
          fixed: 'left',
        },
        {
          name: 'scensename',
          label: '名称',
          fixed: 'left',
        },
        {
          name: 'resultscorebef',
          label: '得分',
          fixed: false
        },
        {
          name: 'districtname',
          label: '区县',
          fixed: false
        },
        {
          name: 'townname',
          label: '街道',
          fixed: false
        },
        {
          name: 'scenseaddress',
          label: '地址',
          fixed: false
        }
      ];
      for (var col of cols) {
        this.cols.push(col);
      }
    },
    // ç”ŸæˆåŸºæœ¬æ•°æ®ä¿¡æ¯
    genTableDataBaseInfo(data, tableItem) {
      try {
        tableItem.sceneIndex = data.sceneIndex;
        tableItem.scensename = data.sceneName;
        tableItem.districtname = data.dname;
        tableItem.townname = data.tname;
        if (!data.evaluation) {
          tableItem.createdate = '';
          tableItem.resultscorebef = '';
          tableItem.scenseaddress = '';
          return;
        }
        tableItem.createdate = data.evaluation.createdate
          ? dayjs(data.evaluation.createdate).format('MM-DD')
          : '';
        tableItem.resultscorebef = data.evaluation.resultscorebef || 0;
        tableItem.scenseaddress = data.evaluation.scenseaddress || '';
      } catch (e) {
        console.log(e.message, data);
      }
    },
    onSearch(page, func) {
      this.dataReady = false;
      this.tableData = [];
      this.cols = [];
      const area = this._getParam();
      var expandHeadOk = false;
      var baseHeadOk = false;
      var showExpand = true;
      evaluateApi
        .fetchAutoEvaluation(area)
        .then((res) => {
          for (const element of res.data) {
            if (element.subTaskId == null) {
              showExpand = false;
              // continue;
              // console.log('subTask为空');
            } else {
              showExpand = true;
            }
            var tableItem = {};
            if (!baseHeadOk) {
              this.genTableHeadBaseInfo(element);
              baseHeadOk = true;
            }
            if (showExpand) {
              evaluateApi
                .fetchItemEvaluation(element.subTaskId)
                .then((res) => {
                  const data = res.data.details;
                  tableItem = {
                    subTaskId: element.subTaskId
                  };
                  this.tableData.push(tableItem);
                  try {
                    if (!expandHeadOk) {
                      this.genChangeableHead(data);
                      expandHeadOk = true;
                    }
                  } catch (e) {
                    console.log('表头准备失败', e.message);
                  }
                  this.genTableDataBaseInfo(element, tableItem);
                  this.genChangeableData(data, tableItem);
                })
                .catch((error) => {
                  console.error('Error fetching item evaluation:', error);
                })
                .finally(() => {});
            } else {
              this.genTableDataBaseInfo(element, tableItem);
              this.tableData.push(tableItem);
            }
          }
        })
        .finally(() => {
          setTimeout(() => {
            this.dataReady = true;
            // ç”ŸæˆæŽ’名表头
            this.genRankHead();
            this.genRankData();
            func({ data: this.tableData });
          }, 1000);
        });
    },
    // åˆå§‹åŒ–调整状态
    initEditRecord() {
      if (!Boolean(this.$route.meta.readonly)) {
        this.editRecordSwitch = true;
      }
    },
    // åˆå§‹åŒ–
    init() {
      this.initEditRecord();
    },
    setOptions(param) {
      this.formSearch.locations = param.locations;
      this.formSearch.scenetype = param.scenetype;
      this.formSearch.sourceType = param.sourceType;
      this.$refs.tableRef.onSearch();
    }
  },
  created() {
    this.init();
  },
  mounted() {
    this.$refs.tableRef.onSearch();
  }
};
</script>
<style scoped>
.flex-div {
  display: flex;
}
.flex-v-center {
  align-items: center;
}
</style>
src/views/fysp/evaluation/components/ChangeableColumnTable.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,585 @@
<template>
  <!-- <el-button type="text" @click="reShow">测试重新加载动态表头组件</el-button> -->
  <el-row ref="searchRef">
    <FYSearchBar @search="onSearch">
      <template #options>
        <slot name="options"></slot>
      </template>
      <template #buttons>
        <slot name="buttons"></slot>
      </template>
    </FYSearchBar>
  </el-row>
  <el-row ref="expandRef" justify="space-between">
    <el-col span="22">
      <slot name="options-expand"></slot>
    </el-col>
    <el-col span="2">
      <el-space :wrap="false">
        <el-text size="small">字体</el-text>
        <el-radio-group v-model="fontSize" size="small">
          <el-radio-button value="small">小</el-radio-button>
          <el-radio-button value="default">中</el-radio-button>
          <el-radio-button value="large">大</el-radio-button>
        </el-radio-group>
        <el-popover placement="bottom" :width="400" trigger="click">
          <template #reference>
            <el-button type="primary" @click="genHeadTreeData">表头修改</el-button>
          </template>
          <!-- åˆ—出表头的树状结构 -->
          <el-tree
            v-if="isOk"
            :props="headTreeProps"
            :data="headTreeData"
            node-key="label"
            show-checkbox
            :default-checked-keys="showHeadLabels"
            @check-change="handleHeadCheckChange"
          />
          <div class="tree-btus">
            <el-button size="small" type="primary" @click="deleteSecondLevelNodes"
              >移除所有二级节点</el-button
            >
            <el-button size="small" type="primary" @click="deleteThirdLevelNodes"
              >移除所有三级节点</el-button
            >
            <!-- <el-button size="small" type="primary" @click="resetCols">复原</el-button> -->
          </div>
        </el-popover>
      </el-space>
    </el-col>
  </el-row>
  <div ref="expand2Ref">
    <slot name="options-expand2"></slot>
  </div>
  <el-table
    :data="tableData"
    v-loading="loading"
    :row-class-name="tableRowClassName"
    table-layout="auto"
    :height="tableHeight"
    :size="fontSize"
    @header-contextmenu="headerContextmenu"
    border
  >
    <el-table-column
      v-for="(item, index) in tableCols"
      :key="item.label"
      :fixed="item.fixed"
      :label="item.label"
      :prop="item.name"
      :ref="item.name"
      :sortable="item.name ? true : false"
    >
      <template v-slot:header v-if="item.needSelect">
        <div>
          {{ item.label }}
          <el-button
            @click="handleHeaderBacthSelectClick(item, true, $event)"
            type="primary"
            size="default"
            circle
          >
            é€‰ä¸­
          </el-button>
          <el-button
            @click="handleHeaderBacthSelectClick(item, false, $event)"
            type="success"
            size="default"
            circle
          >
            å޻除
          </el-button>
        </div>
      </template>
      <template v-slot:default1="scope">
        <el-checkbox
          v-if="item.needSelect"
          v-model="scope.row[`${item.name}Select`]"
          @change="(checked) => selectChange(checked, scope.row)"
          >{{ scope.row[item.name] }}</el-checkbox
        >
      </template>
      <el-table-column
        v-if="item.children && item.children.length > 0"
        v-for="(item2, index2) in item.children"
        :label="item2.label"
        :prop="item2.name"
        :ref="item2.name"
        :sortable="item2.name ? true : false"
      >
        <template v-slot:header v-if="item2.needSelect">
          <div>
            {{ item2.label }}
            <el-button
              @click="handleHeaderBacthSelectClick(item2, true, $event)"
              type="primary"
              size="default"
              circle
            >
              é€‰ä¸­
            </el-button>
            <el-button
              @click="handleHeaderBacthSelectClick(item2, false, $event)"
              type="success"
              size="default"
              circle
            >
              å޻除
            </el-button>
          </div>
        </template>
        <template v-slot:default2="scope2">
          <el-checkbox
            v-if="item2.needSelect"
            v-model="scope2.row[`${item2.name}Select`]"
            @change="(checked) => selectChange(checked, scope2.row)"
            >{{ scope2.row[item2.name] }}</el-checkbox
          >
        </template>
        <el-table-column
          v-if="item2.children && item2.children.length > 0"
          v-for="(item3, index3) in item2.children"
          :label="item3.label"
          :prop="item3.name"
          :ref="item3.name"
          :sortable="item3.name ? true : false"
        >
          <template v-slot:header v-if="item3.needSelect">
            <div>
              {{ item3.label }}
              <el-button
                @click="handleHeaderBacthSelectClick(item3, true, $event)"
                type="primary"
                size="default"
                circle
              >
                é€‰ä¸­
              </el-button>
              <el-button
                @click="handleHeaderBacthSelectClick(item3, false, $event)"
                type="success"
                size="default"
                circle
              >
                å޻除
              </el-button>
            </div>
          </template>
          <template v-slot="scope3">
            <div class="flex-div">
              <!-- <div v-if="scope3.row.loader" class="loader"></div> -->
              <div v-if="item3.needSelect">
                <el-checkbox
                  v-model="scope3.row[`${item3.name}Select`]"
                  @change="(checked) => selectChange(checked, scope3.row)"
                  >{{ scope3.row[item3.name] }}</el-checkbox
                >
              </div>
              <div v-else>
                {{ scope3.row[item3.name] }}
              </div>
            </div>
          </template>
        </el-table-column>
      </el-table-column>
    </el-table-column>
  </el-table>
  <el-pagination
    v-if="pagination"
    ref="paginationRef"
    class="el-pagination"
    v-model:current-page="currentPage"
    v-model:page-size="pageSize"
    :page-sizes="[10, 20, 50, 100]"
    :background="true"
    layout="total, sizes, prev, pager, next, jumper"
    :total="total"
  />
</template>
<script>
import { useCloned } from '@vueuse/core';
export default {
  props: {
    cols: [],
    rowClassName: undefined,
    pagination: {
      type: Boolean,
      default: true
    },
    // '' | 'small' | 'default' | 'large'
    size: {
      type: String,
      default: 'default'
    }
  },
  data() {
    return {
      isOk: false,
      reLoad: true,
      i: 0,
      headTreeData: [],
      headTreeProps: {
        children: 'children',
        label: 'label',
        disabled: 'noncloseable'
      },
      tableHeight: '500',
      tableData: [],
      total: 0,
      currentPage: 1,
      pageSize: 20,
      loading: false,
      fontSize: 'default',
      tableCols: [
        {
          name: '',
          label: '',
          needSelect: false,
          children: []
        }
      ]
    };
  },
  emits: ['search'],
  watch: {
    cols: {
      handler(nValue, oValue) {
        this.tableCols = nValue;
      },
      immediate: true
    },
    currentPage(nValue, oValue) {
      if (nValue != oValue) {
        this.onSearch();
      }
    },
    pageSize(nValue, oValue) {
      if (nValue != oValue) {
        this.onSearch();
      }
    },
    size: {
      handler(nValue, oValue) {
        if (nValue != oValue) {
          this.fontSize = nValue;
        }
      },
      immediate: true
    }
  },
  computed: {
    showHeadLabels() {
      var result = this.collectLabels(this.cols);
      return result;
    },
    cTableHeight() {
      if (this.$refs.searchRef) {
        const h1 = this.$refs.searchRef.$el.offsetHeight;
        const h2 = this.$refs.paginationRef ? this.$refs.paginationRef.$el.offsetHeight : 0;
        const h3 = this.$refs.expandRef.$el.offsetHeight;
        const h4 = this.$refs.expand2Ref.offsetHeight;
        const h = h1 + h2 + h3 + h4;
        // return `calc(100vh - ${h1}px - ${h2}px - var(--el-main-padding) * 2 - var(--el-header-height))`;
        return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
      } else {
        return '500';
      }
    }
  },
  methods: {
    deleteLevelNodes(tree, level) {
      // éåŽ†æ ‘ä¸­çš„æ¯ä¸ªèŠ‚ç‚¹
      tree.forEach((node) => {
        if (node.children && node.children.length > 0) {
          // å¦‚果当前节点的子节点数组不为空,递归调用
          this.deleteLevelNodes(node.children, level - 1);
        }
      });
      // å¦‚果当前级别达到指定级别,清空子节点数组
      if (level === 1) {
        tree.length = 0;
      }
    },
    // åˆ é™¤æ‰€æœ‰äºŒçº§èŠ‚ç‚¹
    deleteSecondLevelNodes() {
      this.loading = true;
      this.deleteLevelNodes(this.tableCols, 2);
      this.loading = false;
      this.genHeadTreeData();
    },
    // åˆ é™¤æ‰€æœ‰ä¸‰çº§èŠ‚ç‚¹
    deleteThirdLevelNodes() {
      this.loading = true;
      this.deleteLevelNodes(this.tableCols, 3);
      this.loading = false;
      this.genHeadTreeData();
    },
    deepCopyCols() {
      return this.deepCopy(this.cols);
    },
    resetCols() {
      this.tableCols = this.deepCopyCols();
    },
    collectLabels(nodes) {
      let labels = []; // ç”¨äºŽå­˜å‚¨ isShow ä¸º true çš„节点的 label
      function traverseNodes(nodes) {
        nodes.forEach((node) => {
          // å¦‚æžœ isShow ä¸º true,将 label æ·»åŠ åˆ°æ•°ç»„ä¸­
          if (!(`isShow` in node) || node.isShow) {
            // labels.push(node);
            labels.push(node.label);
          }
          // å¦‚果当前节点有子节点,递归调用遍历函数
          if (node.children && node.children.length > 0) {
            traverseNodes(node.children);
          }
        });
      }
      traverseNodes(nodes); // å¼€å§‹é€’归遍历
      return labels; // è¿”回收集到的 labels æ•°ç»„
    },
    // æ›´æ–°isShow状态
    updateShowTableHead(labelValue, checked) {
      function traverseNodes(nodes) {
        for (let index = nodes.length - 1; index >= 0; index--) {
          const node = nodes[index];
          if (node.label == labelValue) {
            // node.isShow = !node.isShow;
            // setTimeout(()=>{
            //   nodes.splice(index, 0, node);
            // }, 1000)
            nodes.splice(index, 1);
          }
          // å¦‚果当前节点有子节点,递归调用遍历函数
          if (node.children && node.children.length > 0) {
            traverseNodes(node.children);
          }
        }
      }
      traverseNodes(this.tableCols); // å¼€å§‹é€’归遍历
    },
    // ç”Ÿæˆè¡¨å¤´æ•°æ®
    genHeadTreeData() {
      this.headTreeData = this.tableCols;
    },
    // è¡¨å¤´çŠ¶æ€æ”¹å˜
    handleHeadCheckChange(data, checked, indeterminate) {
      this.updateShowTableHead(data.label, checked);
    },
    // ç”Ÿæˆé€‰ä¸­çŠ¶æ€å±žæ€§å
    genSelectPropName(name) {
      return `${name}Select`;
    },
    // è½¬ä¸­çŠ¶æ€è½¬å˜ ä¼ å…¥ä¸€è¡Œæ•°æ®å’Œéœ€è¦è½¬æ¢é€‰ä¸­çŠ¶æ€çš„åˆ—å
    setSelectStatus(row, name, status) {
      if ((!name) in row) {
        return false;
      }
      row[this.genSelectPropName(name)] = status;
      return true;
    },
    reShow() {
      // setTimeout(() => {
      //   this.isOk = true;
      //   console.log("数据", this.tableData);
      // }, 1000);
    },
    // è¡¨å¤´è¢«å³é”®
    headerContextmenu(column, event) {
      event.preventDefault();
      for (let i = 0; i < this.tableCols.length; i++) {
        var item = this.tableCols[i];
        if (item.label == column.label) {
          this.tableCols.splice(i, 1);
          break;
        }
      }
    },
    handleHeaderBacthSelectClick(prop, isSelect, event) {
      event.stopPropagation();
      this.tableData.forEach((element) => {
        this.setSelectStatus(element, prop.name, isSelect);
      });
      this.selectBatchChange(isSelect, this.tableData, this.genSelectPropName(prop.name));
    },
    initSelectStatus() {
      for (let index = 0; index < this.tableData.length; index++) {
        const element = this.tableData[index];
      }
    },
    initTableData() {
      this.initSelectStatus();
    },
    deepCopy(obj, hash = new WeakMap()) {
      // å¦‚果是null,直接返回null
      if (obj === null) return null;
      // å¦‚果是基本数据类型,直接返回
      if (typeof obj !== 'object') return obj;
      // å¦‚果已拷贝过,直接返回之前拷贝的结果
      if (hash.has(obj)) return hash.get(obj);
      // åˆ›å»ºä¸€ä¸ªæ–°çš„对象或数组
      let cloneObj = Array.isArray(obj) ? [] : {};
      // ç¼“存当前对象的拷贝
      hash.set(obj, cloneObj);
      // é€’归拷贝对象或数组的每个属性
      for (let key in obj) {
        // å¦‚果属性值是数组或对象,则递归拷贝
        cloneObj[key] = this.deepCopy(obj[key], hash);
      }
      return cloneObj;
    },
    initCols() {
      this.i = 0;
      this.tableCols = this.deepCopyCols();
      this.isOk = true;
      for (let index = 0; index < this.tableCols.length; index++) {
        const element = this.tableCols[index];
        element.isShow = true;
      }
    },
    // é€‰ä¸­å›žè°ƒ
    selectChange(isSelect, row) {
      row.loader = true;
      this.$emit('select-updated', row);
      setTimeout(() => {
        row.loader = false;
      }, 1000);
    },
    // æ‰¹é‡é€‰ä¸­å›žè°ƒ
    selectBatchChange(isSelect, rows, name) {
      rows.forEach((element) => {
        element.loader = true;
      });
      this.$emit('select-batch-updated', rows, name);
      setTimeout(() => {
        rows.forEach((element) => {
          element.loader = false;
        });
      }, 1000);
    },
    /**
     * è¡¨æ ¼æ•°æ®æŸ¥è¯¢ï¼Œä¼ é€’两组参数,分页信息和回调函数
     * åˆ†é¡µä¿¡æ¯åŒ…括当前页码currentPage和每页数据量pageSize
     * å›žè°ƒå‡½æ•°æŽ¥æ”¶ä¸€ä¸ªå¯¹è±¡ï¼ŒåŒ…括表格数据数组data和数据总数total
     */
    onSearch() {
      this.loading = true;
      this.$emit(
        'search',
        {
          currentPage: this.currentPage,
          pageSize: this.pageSize
        },
        (res) => {
          this.tableData = res.data;
          this.total = res.total ? res.total : 0;
          this.loading = false;
        }
      );
    },
    calcTableHeight() {
      const h1 = this.$refs.searchRef.$el.offsetHeight;
      const h2 = this.$refs.paginationRef ? this.$refs.paginationRef.$el.offsetHeight : 0;
      const h3 = this.$refs.expandRef.$el.offsetHeight;
      const h4 = this.$refs.expand2Ref.offsetHeight;
      const h = h1 + h2 + h3 + h4;
      // return `calc(100vh - ${h1}px - ${h2}px - var(--el-main-padding) * 2 - var(--el-header-height))`;
      return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
    },
    tableRowClassName({ row }) {
      if (this.rowClassName) {
        if (typeof this.rowClassName == 'string') {
          return this.rowClassName;
        } else if (typeof this.rowClassName == 'function') {
          return this.rowClassName({ row });
        }
      } else {
        return row.extension1 != '0' ? 'online-row' : 'offline-row';
      }
    }
  },
  created() {
    // this.onSearch();
  },
  mounted() {
    this.tableHeight = this.calcTableHeight();
    this.initCols();
    this.initTableData();
    this.isOk = true;
  }
};
</script>
<style>
.l-table .online-row {
  /* background-color: rgb(4, 202, 21); */
}
.el-table .offline-row {
  background-color: var(--el-disabled-bg-color);
  color: var(--el-disabled-text-color);
}
.el-table .cell {
  white-space: nowrap;
}
.el-pagination {
  background-color: var(--el-color-white);
  padding-top: 20px;
  border-top: 1px solid rgba(0, 0, 0, 0.096);
  /* margin-top: 2px; */
}
.no-display {
  display: none;
}
.btns {
  margin-left: 10px;
}
.flex-div {
  display: flex;
}
.loader {
  margin-top: 5px;
  margin-right: 5px;
  border: 4px solid rgba(0, 0, 0, 0.1); /* è½»é¢œè‰²çš„边框 */
  border-radius: 50%; /* åœ†å½¢ */
  border-top: 4px solid #3498db; /* è“è‰²è¾¹æ¡† */
  width: 20px; /* åŠ è½½å™¨çš„å®½åº¦ */
  height: 20px; /* åŠ è½½å™¨çš„é«˜åº¦ */
  animation: spin 2s linear infinite; /* æ— é™å¾ªçŽ¯çš„æ—‹è½¬åŠ¨ç”» */
}
.tree-btus {
  margin-left: 24px;
}
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>