动态文件夹+动态表格+动态表单 vue

512 阅读6分钟

动态文件夹+动态表格+表单(DynaActionForm)

使用技术 :Vue2 + element UI


01动态文件夹

过滤查询文件夹,动态添加文件夹,可修改文件名,添加子文件夹,删除文件夹

template
<el-input placeholder="输入关键字进行过滤" v-model="filterText">
      </el-input>
      <button class="icon-btn add-btn" @click="handlerAppend">
        <div class="add-icon"></div>
        <div class="btn-txt">添加 文件</div>
      </button>
      <el-tree class="filter-tree" :data="treeList" show-checkbox :props="defaultProps" default-expand-all
        highlight-current :expand-on-click-node="showTree" @node-click="handleNodeClick" :filter-node-method="filterNode"
        @check-change="handleCheckChange" ref="tree">
        <template slot-scope="{ node, data }">
          <div class="flex-row treeSortList">
            <el-input v-model="data.label" placeholder="请输入文件名称" v-if="showEdit[data.id]" size="mini"></el-input>
            <el-tooltip v-else class="item" effect="light" content="点击进行操作" placement="top">
              <span> {{ node.label }} </span>
            </el-tooltip>
​
            <div class="doBtn" v-if="showEdit[data.id]">
              <el-button type="text" size="mini" title="保存修改" @click.stop="() => save(node, data)">保存
              </el-button>
              <el-button type="text" size="mini" title="取消修改" @click.stop="() => cancel(node, data)">取消
              </el-button>
            </div>
            <div class="doBtn" v-if="showBtn[data.id] && !showEdit[data.id]">
              <el-tooltip class="item" effect="light" content="添加子项目" placement="top">
                <el-button type="text" size="mini" title="添加" @click.stop="() => append(data)" icon="el-icon-plus">
                </el-button>
              </el-tooltip>
              <el-tooltip class="item" effect="light" content="编辑文件名" placement="top">
                <el-button type="text" size="mini" title="编辑" @click.stop="() => edit(data)" icon="el-icon-edit-outline">
                </el-button>
              </el-tooltip>
              <el-tooltip class="item" effect="light" content="删除文件" placement="top">
                <el-button type="text" size="mini" title="删除" @click.stop="() => remove(node, data)"
                  icon="el-icon-delete">
                </el-button>
              </el-tooltip>
            </div>
          </div>
        </template>
      </el-tree>
css
::v-deep .el-tree-node__content {
  padding: 10px 0;
  height: auto;
}
​
.treeSortList {
  width: calc(100% - 30px);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
​
.treeSortList .doBtn {
  padding-right: 10px;
}
​
.treeSortList .doBtn .el-button--text {
  color: rgb(61, 61, 61);
  padding: 5px;
}
​
.treeSortList .doBtn .el-button--text i {
  color: rgb(80, 80, 80);
}
​
.treeSortList .doBtn .el-button--text i.el-icon-delete {
  color: #f3a68e;
}
js
data(){
return{
filterText: "",
treeList: [],
defaultProps: {
children: "children",
label: "label",
},
showTree: false, //是否点击节点展开树,false 只能点前面三角图标展开
showBtn: [],
showEdit: [],
editData: [],
newAddData: false,
}
}
 watch: {
    filterText(val) {
      this.$refs.tree.filter(val);
    },
  },
 methods:{
 filterNode(value, data) {
      if (!value) return true;
      return data.label.indexOf(value) !== -1;
    },
    handleCheckChange(data, checked, indeterminate) {
      console.log(data, checked, indeterminate);
    },
    handlerAppend() {
      let i = 0;
      if (this.data) {
        const newChild = {
          id: id++,
          label: "文件" + (Number(this.data.length) + 1),
          children: [],
        };
        this.treeList.push(newChild);
      } else {
        const newChild = {
          id: id++,
          label: "文件" + (i + 1),
          children: [],
        };
        this.treeList.push(newChild);
      }
      this.$message({
        message: "添加成功,开始编写吧~",
        type: "success",
      });
    },
    //点击树节点
    handleNodeClick(data) {
      if (!this.ifEdit()) {
        return;
      }
      this.showBtn = [];
      this.$set(this.showBtn, data.id, true);
    },
    append(data) {
      if (!this.ifEdit()) {
        return;
      }
      const newChild = { id: id++, label: "", children: [] };
      if (!data.children) {
        this.$set(data, "children", []);
      }
      data.children.push(newChild);
​
      this.newAddData = true;
      this.$set(this.showEdit, newChild.id, true);
    },
    edit(data) {
      var localEdit = localStorage.getItem("treeEdit");
      var id = data.id;
      var newlocalData = { [data.id]: data.label };
      if (localEdit) {
        var localData = JSON.parse(localEdit);
        Object.assign(localData, newlocalData);
        localData = JSON.stringify(localData);
        localStorage.setItem("treeEdit", localData);
      } else {
        newlocalData = JSON.stringify(newlocalData);
        localStorage.setItem("treeEdit", newlocalData);
      }
      // console.log(localStorage.getItem("treeEdit"));
      this.showEdit = [];
      this.$set(this.showEdit, data.id, true);
    },
    remove(node, data) {
      const parent = node.parent;
      const children = parent.data.children || parent.data;
      const index = children.findIndex((d) => d.id === data.id);
      children.splice(index, 1);
    },
    //保存
    save(node, data) {
      if (data.label == "") {
        this.$message({
          type: "error",
          message: "请输入完整数据",
          offset: 70,
        });
        return;
      }
      this.newAddData = "";
      var localEdit = localStorage.getItem("treeEdit");
      if (localEdit) {
        var localData = JSON.parse(localEdit);
        delete localData[data.id]; //删除已经取消项
        localData = JSON.stringify(localData);
        localStorage.setItem("treeEdit", localData); //重置缓存
      }
      this.$set(this.showEdit, data.id, false);
    },
    cancel(node, data) {
      if (this.newAddData) {
        this.remove(node, data);
      }
      var localEdit = localStorage.getItem("treeEdit");
      if (localEdit) {
        var localData = JSON.parse(localEdit);
        data.label = localData[data.id];
        delete localData[data.id]; //删除已经取消项
        localData = JSON.stringify(localData);
        localStorage.setItem("treeEdit", localData); //重置缓存
      }
      this.$set(this.showEdit, data.id, false);
    },
    //判断是否有编辑或增加的项
    ifEdit() {
      if (this.showEdit.indexOf(true) != -1) {
        this.$message({
          type: "error",
          message: "先保存正在编辑的行",
          offset: 70,
        });
        return false;
      } else {
        return true;
      }
    },
 }
效果展示

image.png


02动态表格 - Table/DynamicTable.vue

参考网址:blog.csdn.net/coralime/ar…

02-1使用展示
  • tableHeader 表头的数据
  • tableData 表格的数据
  • height 表格的高度
  • isSelection 是否添加勾选
  • isIndex 是否需要添加序号列
  • loading 加载
<DynamicTable :tableHeader="tableHeader" :tableData="tableData" :isIndex="false" :isSelection="true"
          @selection-change="handlerSelectionChange" :loading="loading" />

tableHeader 表头的数据

--普通模式
 tableHeader: [
        {
          label: '姓名',
          prop: 'name',
          module: 'text',
          show: true
        }, {
          label: '地址',
          prop: 'address',
          module: 'text',
          show: true,
        }
      ],

image.png

--多级表头
tableHeader: [
        {
          label: '姓名',
          prop: 'name',
          module: 'text',
          show: true
        }, {
          label: '地址',
          prop: 'address',
          module: 'text',
          show: true,
          children: [
            {
              label: '地址1',
              prop: 'address1',
              module: 'text',
              show: true,
              children: [
                {
                  label: '地址1',
                  prop: 'address1',
                  module: 'text',
                  show: true,
                },
                {
                  label: '地址2',
                  prop: 'address2',
                  module: 'text',
                  show: true,
                }
              ]
            },
            {
              label: '地址2',
              prop: 'address2',
              module: 'text',
              show: true,
            }
          ]
        }
      ],

image.png

02-2动态表文件 - componebts/Table/DynamicTable.vue
<template>
    <!-- 动态展示表格 -->
    <el-table :key="tableRefresh" id="multipleTable" ref="multipleTable" v-loading="loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
        element-loading-background="rgba(0, 0, 0, 0.8)" :data="tableData" border stripe :height="height"
        @row-click="handleRowClick" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" v-if="isSelection" align="center">
        </el-table-column>
        <el-table-column v-if="isIndex" type="index" width="100" label="序号" :index="hIndex" align="center" />
        <!-- v-for 循环取表头数据 -->
        <template v-for="(item, index) in bindTableColumns">
            <table-column v-if="item.children && item.children.length" :key="index" :column-header="item" />
            <el-table-column v-else :key="index" :label="item.label" :prop="item.prop" align="center" />
        </template>
        <!-- <el-table-column prop="onlineStatus" label="操作" width="140" align="center">
            <template slot-scope="scope">
                <el-button @click="handleClick(scope.row)" type="info" size="small" icon="el-icon-document">详情</el-button>
            </template>
        </el-table-column> -->
    </el-table>
</template>
<script>
import TableColumn from '@/components/Table/TableColumn'export default {
    name: 'DynamicTable',
    components: {
        TableColumn
    },
    props: {
        // 表格的数据
        tableData: {
            type: Array,
            required: true
        },
        // 多级表头的数据
        tableHeader: {
            type: Array,
            required: true
        },
        // 表格的高度
        height: {
            type: String,
            default: '300'
        },
        // 是否需要添加序号列
        isIndex: {
            type: Boolean
        },
        // 是否添加勾选
        isSelection: {
            type: Boolean
        },
        loading: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            tableRefresh: 0
        }
    },
    computed: {
        bindTableColumns() {
            this.tableRefresh++
            return this.tableHeader.filter((column) => column.show);
        },
    },
    methods: {
        // 详情
        handleClick(row) {
            this.$emit('json-click', row)
            //数据是string类型的需要用到JSON.parse(object)将string类型转换为JSON类型
            //row.jsonData的jsonData是后台接口数据所提供的,this.jsonData是容器,用来实现数据绑定显示的:value="jsonData"
        },
        // 行点击事件
        handleRowClick(row, column, event) {
            // 通知调用父组件的row-click事件
            // row作为参数传递过去
            this.$emit('row-click', row)
        },
        handleSelectionChange(val) {
            this.$emit('selection-change', val)
        },
        hIndex(index) {
            // index索引从零开始,index +1即为当前数据序号
            this.$options.parent.queryParams.pageNum <= 0
                ? (this.$options.parent.queryParams.pageNum = 1)
                : this.$options.parent.queryParams.pageNum;
            // 如果当前不是第一页数据
            if (this.$options.parent.queryParams.pageNum != 1) {
                // index + 1 + (( 当前页 - 1) * 每页展示条数)
                // 比如是第二页数据 1 + ((2 - 1)*5) = 6,第二页数据也就是从序号6开始
                return (
                    index + 1 + (this.$options.parent.queryParams.pageNum - 1) * this.$options.parent.queryParams.pageSize
                );
            }
            // 否则直接返回索引+1作为序号
            return index + 1;
        },
    }
}
</script>
复杂表头文件 - componebts/Table/TableColumn.vue
<template>
    <el-table-column
      :label="columnHeader.label"
      align="center"
    >
      <!--columnHeader对应:column-header-->
      <template v-for="(item,index) in columnHeader.children">
        <tableColumn
          v-if="item.children && item.children.length"
          :key="index"
          :column-header="item"
        />
        <el-table-column
          v-else
          :key="index"
          :label="item.label"
          :prop="item.prop"
          align="center"
        />
      </template>
    </el-table-column>
  </template>
   
  <script>
    export default {
      name: 'tableColumn',
      props: {
        columnHeader: {
          type: Object,
          required: true
        }
      },
    }
  </script>
   
  <style scoped>
   
  </style>

03动态表单

<template>
    <el-dialog :title="title" :visible.sync="dialogFormVisible" width="25%">
        <el-form :model="form" ref="ruleForm">
            <el-form-item v-for="(item, index) in headerData" :key="index" :label="item.label">
                <el-input v-if="item.module == 'text'" :placeholder="`请输入${item.label}`" v-model="form[item.prop]"
                autocomplete="off"></el-input>
            <el-input v-if="item.module == 'password'" :placeholder="`请输入${item.label}`" v-model="form[item.prop]"
                show-password></el-input>
            <el-input v-if="item.module == 'textarea'" :placeholder="`请输入${item.label}`" type="textarea"
                :autosize="{ minRows: 2, maxRows: 4 }" v-model="form[item.prop]">
                </el-input>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="resetForm('ruleForm')">取 消</el-button>
            <el-button type="primary" @click="submitForm('ruleForm')">确 定</el-button>
        </div>
    </el-dialog>
</template>
​
<script>
export default {
    props: {
        parentThat: {}
    },
    data() {
        return {
            // 表格添加
            title: '',
            dialogFormVisible: false,
            form: {},
            headerData: null,
            editKeys: null,
        }
    },
    mounted() {
    },
    methods: {
        init(title, headerData, keys) {
            this.dialogFormVisible = true;
            this.title = title
            this.headerData = headerData
            this.editKeys = null
            headerData.forEach(item => {
                this.form = {
                    [item.prop]: ''
                }
            })
            if (keys) {
                this.editKeys = keys
                this.form = JSON.parse(JSON.stringify(keys))
            }
        },
        submitForm(formName) {
            this.$refs[formName].validate((valid) => {
                if (valid) {
                    if (this.editKeys) {
                        this.$emit('handlerConfirm', this.form, this.editKeys)
                    } else {
                        this.$emit('handlerConfirm', this.form)
                    }
                    this.parentThat.$refs.TableList.$refs.multipleTable.clearSelection()
                    this.form = this.$options.data.call(this).form
                    this.dialogFormVisible = false;
                } else {
                    console.log('error submit!!');
                    return false;
                }
            });
        },
        resetForm(formName) {
            this.form = this.$options.data.call(this).form
            this.dialogFormVisible = false;
            this.$refs[formName].resetFields();
        },
    }
}
</script>
​
<style></style>

导入+导出 -- 前端版

下载

npm install -S file-saver xlsx
npm install -D script-loader
npm install xlsx

main.js

import  * as XLSX from 'xlsx'
Vue.use(XLSX)

上传获取数据参考网址:blog.csdn.net/weixin_4528…

导入导出参考网址:www.jianshu.com/p/02d615d60…

多级表头导出参考网址:blog.csdn.net/weixin_4268…

01导入

<el-button type="info" icon="el-icon-upload2" size="mini" @click="handlerLeadingIn" plain>导入</el-button>
// 导入表格
    handlerLeadingIn() {
      this.$refs.LeadingIn.init('导入', this.tableHeader);
    },
​
//新建文件 - LeadingIn.vue
import LeadingIn from "./LeadingIn.vue"
​
<template>
    <el-dialog :title="title" :visible.sync="dialogFormVisible" width="400px">
        <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :action="upload.url" :on-change="handleChange"
            :auto-upload="false" drag>
            <i class="el-icon-upload"></i>
            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
            <div class="el-upload__tip" slot="tip">
                <div><el-checkbox v-model="upload.isUploading" />是否更新已经存在的数据</div>
                <div>仅允许导入xls、xlsx格式文件。<a style="color:dodgerblue;" @click="importTemplate">下载模板</a> </div>
            </div>
        </el-upload>
        <div slot="footer" class="dialog-footer">
            <el-button type="primary" @click="submitForm('uploadRef')">确 定</el-button>
            <el-button @click="resetForm('uploadRef')">取 消</el-button>
        </div>
    </el-dialog>
</template><script>
export default {
    props: {
        parentThat: {},
    },
    data() {
        return {
            // 表格添加
            title: '',
            dialogFormVisible: false,
            upload: {
                // 是否更新已数据
                isUploading: false,
                // 上传的地址
                url: process.env.VUE_APP_BASE_API +
                    "/efarmcloud-open-file/api/v1/files/upload"
            },
            XlsxData: [],
            fileContent: null,
            headerTable: [],
        }
    },
    methods: {
        init(title, headerTable) {
            this.dialogFormVisible = true;
            this.title = title
            this.headerTable = headerTable
        },
          setDesc() {
            // JS-获取到26个英文大写字母(A-Z)
            const letterArr = []
            Array(26).fill('').map((item, index) => {
                letterArr.push(String.fromCharCode(index + 65))
            })
            return letterArr
        },
       /** 下载模板操作 多级表头和单个表头 表头测试只有2级一次类推 */
        importTemplate() {
            let tHeader = [], multiHeader = [], header1 = [], filterPop = [], merges = [], merges1 = [], merges3 = [], merges2 = [], headerRowLength = 1;
            // this.handleExcel('user_')
            // 有69个元素是空的,所以直接进行了截取
            let flag = this.headerTable.some(column => {
                if (column.children && column.children.length > 0) {
                    return true
                }
                return false
            })
            if (flag) {
                const randomAbc = this.setDesc()
                this.headerTable.map((column, i) => {
                    if (column.children && column.children.length > 0) {
                        headerRowLength++
                        header1.push(column.label)
                        column.children.map((childColumn, j) => {
                            merges2.push(randomAbc[j] + headerRowLength)
                            tHeader.push(childColumn.label)
                            filterPop.push(childColumn.prop)
                            header1.push('')
​
                        })
                    } else {
                        merges1.push(randomAbc[i] + headerRowLength)
                        header1.push(column.label)
                        tHeader.push('')
                        filterPop.push(column.prop)
                    }
                })
                // 二维数组依次递增 表格头部依次递进 // tHeader表格头部最后一级 merges合并列或行编号自行调节
                multiHeader.push(header1)
                // 合并的行
                merges1.forEach(i => {
                    merges2.forEach(j => {
                        if (i.substring(0, 1) == j.substring(0, 1)) {
                            merges.push(i + ':' + j)
                        }
                    })
                })
                // 合并的列
                let len = merges1.length
                tHeader.forEach((item, o) => {
                    merges3.push(randomAbc[o])
                })
                merges3.splice(0, len)
                merges.push(merges3.slice(0, 1)[0] + (headerRowLength - 1) + ':' + merges3.slice(-1)[0] + (headerRowLength - 1))
                this.getXlsxData_many(tHeader, multiHeader, filterPop, merges)
            } else {
                this.headerTable.map((column, i) => {
                    tHeader.push(column.label)
                    filterPop.push(column.prop)
                })
                this.getXlsxData(tHeader, filterPop)
            }
        },
        // 多行表头下载
        getXlsxData_many(tHeader, multiHeader, filterPop, merges, dataT = []) {
            require.ensure([], () => {
                const { export_json_to_excel_headerMany } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
                const data = this.formatJson(filterPop, dataT);//格式化
                // console.log(tHeader, multiHeader, filterPop, merges, dataT);
                //这个得说明一下:网上得博客每个不一样,你那我的直接用也是没啥用得,你的理解这个合并是怎么写的:根据你的多级表头,如果没有合并得从上往下写,遇到开始合并单元格的,从左往右得单行写,从上到下,直到写完整
                export_json_to_excel_headerMany({
                    multiHeader,
                    header: tHeader,
                    data,
                    filename: `user_${new Date().getTime()}`, merges,
                    autoWidth: true,
                })
            })
        },
        // 单行表头下载
        getXlsxData(tHeader = [], filterVal = [], dataT = []) {
            require.ensure([], () => {
                const { export_json_to_excel } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
                // const tHeader = []; //对应表格输出的title
                // const filterVal = []; // 对应表格输出的数据
                const data = this.formatJson(filterVal, dataT);
                console.log(tHeader, data, filterVal, '下载');
                export_json_to_excel(tHeader, data, `user_${new Date().getTime()}`); //对应下载文件的名字
            })
        },
        formatJson(filterVal, jsonData) {
            return jsonData.map(v => filterVal.map(j => v[j]))
        },
        // 核心部分代码(handleChange 和 importfile)
        handleChange(file) {
            this.fileContent = file.raw
            const fileName = file.name
            const fileType = fileName.substring(fileName.lastIndexOf('.') + 1)
            if (this.fileContent) {
                if (fileType === 'xlsx' || fileType === 'xls') {
                   // 判断是否有子项
                    let flag = this.headerTable.some(column => {
                        if (column.children && column.children.length > 0) {
                            return true
                        }
                        return false
                    })
                    this.loading = this.$loading({
                        lock: true,
                        text: 'Loading',
                        spinner: 'el-icon-loading',
                        background: 'rgba(0, 0, 0, 0.7)'
                    });
                    if (flag) {
                        this.importFile(this.fileContent, flag)
                        this.loading.close();
                    } else {
                        this.importFile(this.fileContent, flag)
                        this.loading.close();
                    }
                } else {
                    this.$message({
                        type: 'warning',
                        message: '附件格式错误,请重新上传!'
                    })
                }
            } else {
                this.$message({
                    type: 'warning',
                    message: '请上传附件!'
                })
            }
        },
        importfile(obj) {
            const reader = new FileReader()
            const _this = this
            reader.readAsArrayBuffer(obj)
            reader.onload = function () {
                const buffer = reader.result
                const bytes = new Uint8Array(buffer)
                const length = bytes.byteLength
                let binary = ''
                for (let i = 0; i < length; i++) {
                    binary += String.fromCharCode(bytes[i])
                }
                const XLSX = require('xlsx')
                const wb = XLSX.read(binary, {
                    type: 'binary'
                })
                const outData = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]])
                const arr = [...outData], newArr = []
                arr.map((v) => {
                    //***** 重点重点数据 list ******
                    let list = _this.headerTable.map((s, i) => {
                        let key = Object.keys(v)[i]
                        if (key == s.label) {
                            return { id: Math.random(),[s.prop]: v[key] }
                        }
                    })
                    newArr.push(Object.assign(...list))
                    //表头数据多或数据不齐全请使用一对一获取  如:
                     // newArr.push({
                    //     group: v.组别,
                    //     farmer_name: v.农户姓名,
                    //     ID_number: v.身份证号,
                    //     age: v.年龄,
                    //     phone: v.电话,
                    //     remark: v.备注,
                    //     public_security_system_list: v.公安系统名单中无此人原因,
                    // })
                })
                _this.XlsxData = newArr
            }
        },
        submitForm(uploadRef) {
            // console.log(this.XlsxData, this.parentThat._data.tableData, this.upload.isUploading);
            if (this.upload.isUploading) {
                this.parentThat._data.tableData = this.XlsxData
            } else {
                this.parentThat._data.tableData = this.parentThat._data.tableData.concat(this.XlsxData)
            }
            this.parentThat._data.total =  this.parentThat._data.tableData.length //数量
            this.dialogFormVisible = false;
            this.$refs[uploadRef].clearFiles();
        },
        resetForm(uploadRef) {
            this.dialogFormVisible = false;
            this.$refs[uploadRef].clearFiles();
        },
    }
}
</script><style lang="scss" scoped>
.el-upload__tip {
    display: flex;
    flex-direction: column;
    align-items: center;
}
</style>

02导出

<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport" plain>导出</el-button>
​
  setDesc() {
      // JS-获取到26个英文大写字母(A-Z)
      const letterArr = []
      Array(26).fill('').map((item, index) => {
        letterArr.push(String.fromCharCode(index + 65))
      })
      return letterArr
    },
    /** 导出按钮操作 */
    handleExport() {
      let tHeader = [], multiHeader = [], header1 = [], filterPop = [], merges = [], merges1 = [], merges3 = [], merges2 = [], headerRowLength = 1;
      let flag = this.tableHeader.some(column => {
        if (column.children && column.children.length > 0) {
          return true
        }
        return false
      })
      if (flag) {
        const randomAbc = this.setDesc()
        this.headerTable.map((column, i) => {
                    if (column.show) {
                        if (column.children && column.children.length > 0) {
                            headerRowLength++
                            header1.push(column.label)
                            column.children.map((childColumn, j) => {
                                merges2.push(randomAbc[j] + headerRowLength)
                                tHeader.push(childColumn.label)
                                filterPop.push(childColumn.prop)
                                header1.push('')
​
                            })
                        } else {
                            merges1.push(randomAbc[i] + headerRowLength)
                            header1.push(column.label)
                            tHeader.push('')
                            filterPop.push(column.prop)
                        }
                    }
​
                })
        // 二维数组依次递增 表格头部 // tHeader 表格头部最后一级
        multiHeader.push(header1)
        // 合并的行
        merges1.forEach(i => {
          merges2.forEach(j => {
            if (i.substring(0, 1) == j.substring(0, 1)) {
              merges.push(i + ':' + j)
            }
          })
        })
        // 合并的列
        let len = merges1.length
        tHeader.forEach((item, o) => {
          merges3.push(randomAbc[o])
        })
        merges3.splice(0, len)
        merges.push(merges3.slice(0, 1)[0] + (headerRowLength - 1) + ':' + merges3.slice(-1)[0] + (headerRowLength - 1))
​
        this.$confirm('确定导出全部数据么?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        })
          .then(() => {
            // 确定
            this.getXlsxData_many(tHeader, multiHeader, filterPop, merges, this.tableData)
            this.$message({
              showClose: true,
              message: '下载成功'
            });
          })
          .catch(() => {
            // 取消
            this.$message({
              showClose: true,
              message: '取消下载'
            });
          })
      } else {
        this.tableHeader.map((column, i) => {
                    if (column.show) {
                        tHeader.push(column.label)
                        filterPop.push(column.prop)
                    }
                })
        this.$confirm('确定导出全部数据么?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        })
          .then(() => {
            // 确定
            this.getXlsxData(tHeader, filterPop, this.tableData)
            this.$message({
              showClose: true,
              message: '下载成功'
            });
          })
          .catch(() => {
            // 取消
            this.$message({
              showClose: true,
              message: '取消下载'
            });
          })
​
      }
​
    },
    // 多行表头下载
    getXlsxData_many(tHeader, multiHeader, filterPop, merges, dataT = []) {
      require.ensure([], () => {
        const { export_json_to_excel_headerMany } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
        const data = this.formatJson(filterPop, dataT);//格式化
        // console.log(tHeader, multiHeader, filterPop, merges, dataT);
        //这个得说明一下:网上得博客每个不一样,你那我的直接用也是没啥用得,你的理解这个合并是怎么写的:根据你的多级表头,如果没有合并得从上往下写,遇到开始合并单元格的,从左往右得单行写,从上到下,直到写完整
        export_json_to_excel_headerMany({
          multiHeader,
          header: tHeader,
          data,
          filename: `user_${new Date().getTime()}`, merges,
          autoWidth: true,
        })
      })
    },
    // 单行表头下载
    getXlsxData(tHeader = [], filterVal = [], dataT = []) {
      require.ensure([], () => {
        const { export_json_to_excel } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
        // const tHeader = []; //对应表格输出的title
        // const filterVal = []; // 对应表格输出的数据
        const data = this.formatJson(filterVal, dataT);
        export_json_to_excel(tHeader, data, `user_${new Date().getTime()}`); //对应下载文件的名字
      })
    },
    formatJson(filterVal, jsonData) {
      return jsonData.map(v => filterVal.map(j => v[j]))
    },
execl/Export2Excel.js
/* eslint-disable */
require('script-loader!file-saver');
require('./Blob.js');
require('script-loader!xlsx/dist/xlsx.core.min');
​
function generateArray(table) {
  var out = [];
  var rows = table.querySelectorAll('tr');
  var ranges = [];
  for (var R = 0; R < rows.length; ++R) {
    var outRow = [];
    var row = rows[R];
    var columns = row.querySelectorAll('td');
    for (var C = 0; C < columns.length; ++C) {
      var cell = columns[C];
      var colspan = cell.getAttribute('colspan');
      var rowspan = cell.getAttribute('rowspan');
      var cellValue = cell.innerText;
      if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
​
      //Skip ranges
      ranges.forEach(function (range) {
        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
        }
      });
​
      //Handle Row Span
      if (rowspan || colspan) {
        rowspan = rowspan || 1;
        colspan = colspan || 1;
        ranges.push({
          s: {
            r: R,
            c: outRow.length
          },
          e: {
            r: R + rowspan - 1,
            c: outRow.length + colspan - 1
          }
        });
      };
​
      //Handle Value
      outRow.push(cellValue !== "" ? cellValue : null);
​
      //Handle Colspan
      if (colspan)
        for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
    }
    out.push(outRow);
  }
  return [out, ranges];
};
​
function datenum(v, date1904) {
  if (date1904) v += 1462;
  var epoch = Date.parse(v);
  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
}
​
function sheet_from_array_of_arrays(data, opts) {
  var ws = {};
  var range = {
    s: {
      c: 10000000,
      r: 10000000
    },
    e: {
      c: 0,
      r: 0
    }
  };
  for (var R = 0; R != data.length; ++R) {
​
    for (var C = 0; C != data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R;
      if (range.s.c > C) range.s.c = C;
      if (range.e.r < R) range.e.r = R;
      if (range.e.c < C) range.e.c = C;
      var cell = {
        v: data[R][C]
      };
      if (cell.v == null) continue;
      var cell_ref = XLSX.utils.encode_cell({
        c: C,
        r: R
      });
​
      if (typeof cell.v === 'number') cell.t = 'n';
      else if (typeof cell.v === 'boolean') cell.t = 'b';
      else if (cell.v instanceof Date) {
        cell.t = 'n';
        cell.z = XLSX.SSF._table[14];
        cell.v = datenum(cell.v);
      } else cell.t = 's';
      ws[cell_ref] = cell;
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
  return ws;
}
​
function Workbook() {
  if (!(this instanceof Workbook)) return new Workbook();
  this.SheetNames = [];
  this.Sheets = {};
}
​
function s2ab(s) {
  var buf = new ArrayBuffer(s.length);
  var view = new Uint8Array(buf);
  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
  return buf;
}
​
export function export_table_to_excel(id) {
  var theTable = document.getElementById(id);
  var oo = generateArray(theTable);
  var ranges = oo[1];
​
  /* original data */
  var data = oo[0];
  var ws_name = "SheetJS";
​
  var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);
​
  /* add ranges to worksheet */
  // ws['!cols'] = ['apple', 'banan'];
  ws['!merges'] = ranges;
​
  /* add worksheet to workbook */
  wb.SheetNames.push(ws_name);
  wb.Sheets[ws_name] = ws;
​
  var wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    bookSST: false,
    type: 'binary'
  });
​
  saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
  }), "test.xlsx")
}
​
function formatJson(jsonData) {
  console.log(jsonData)
}
export function export_json_to_excel(th, jsonData, defaultTitle) {
  /* original data */
  var data = jsonData;
  data.unshift(th);
  var ws_name = "SheetJS";
​
  var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);
  /* add worksheet to workbook */
  wb.SheetNames.push(ws_name);
  wb.Sheets[ws_name] = ws;
​
  var wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    bookSST: false,
    type: 'binary'
  });
  var title = defaultTitle || '列表'
  saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
  }), title + ".xlsx")
}
export function export_json_to_excel_headerMany({
  multiHeader  = [], // 第二行表头
  header,
  data,
  filename, //文件名
  merges = [], // 合并
  autoWidth = true,
  bookType = 'xlsx'
} = {}) {
  filename = filename || '列表';
  data = [...data]
  data.unshift(header);
​
  for (let i = multiHeader.length - 1; i > -1; i--) {
    data.unshift(multiHeader [i])
  }
  console.log(data);
​
​
  var ws_name = "SheetJS";
  var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);
​
  if (merges.length > 0) {
    if (!ws['!merges']) ws['!merges'] = [];
    merges.forEach(item => {
      ws['!merges'].push(XLSX.utils.decode_range(item))
    })
  }
​
  if (autoWidth) {
    /*设置worksheet每列的最大宽度*/
    const colWidth = data.map(row => row.map(val => {
      /*先判断是否为null/undefined*/
      if (val == null) {
        return {
          'wch': 10
        };
      }
      /*再判断是否为中文*/
      else if (val.toString().charCodeAt(0) > 255) {
        return {
          'wch': val.toString().length * 2
        };
      } else {
        return {
          'wch': val.toString().length
        };
      }
    }))
    /*以第一行为初始值*/
    let result = colWidth[0];
    for (let i = 1; i < colWidth.length; i++) {
      for (let j = 0; j < colWidth[i].length; j++) {
        if (result[j]['wch'] < colWidth[i][j]['wch']) {
          result[j]['wch'] = colWidth[i][j]['wch'];
        }
      }
    }
    ws['!cols'] = result;
  }
​
  /* add worksheet to workbook */
  wb.SheetNames.push(ws_name);
  wb.Sheets[ws_name] = ws;
​
  var wbout = XLSX.write(wb, {
    bookType: bookType,
    bookSST: false,
    type: 'binary'
  });
  saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
  }), `${filename}.${bookType}`);
}
​
execl/Blob.js
​
(function (view) {
    "use strict";
​
    view.URL = view.URL || view.webkitURL;
​
    if (view.Blob && view.URL) {
        try {
            new Blob;
            return;
        } catch (e) {}
    }
​
    // Internally we use a BlobBuilder implementation to base Blob off of
    // in order to support older browsers that only have BlobBuilder
    var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
            var
                get_class = function(object) {
                    return Object.prototype.toString.call(object).match(/^[object\s(.*)]$/)[1];
                }
                , FakeBlobBuilder = function BlobBuilder() {
                    this.data = [];
                }
                , FakeBlob = function Blob(data, type, encoding) {
                    this.data = data;
                    this.size = data.length;
                    this.type = type;
                    this.encoding = encoding;
                }
                , FBB_proto = FakeBlobBuilder.prototype
                , FB_proto = FakeBlob.prototype
                , FileReaderSync = view.FileReaderSync
                , FileException = function(type) {
                    this.code = this[this.name = type];
                }
                , file_ex_codes = (
                    "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
                    + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
                ).split(" ")
                , file_ex_code = file_ex_codes.length
                , real_URL = view.URL || view.webkitURL || view
                , real_create_object_URL = real_URL.createObjectURL
                , real_revoke_object_URL = real_URL.revokeObjectURL
                , URL = real_URL
                , btoa = view.btoa
                , atob = view.atob
​
                , ArrayBuffer = view.ArrayBuffer
                , Uint8Array = view.Uint8Array
                ;
            FakeBlob.fake = FB_proto.fake = true;
            while (file_ex_code--) {
                FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
            }
            if (!real_URL.createObjectURL) {
                URL = view.URL = {};
            }
            URL.createObjectURL = function(blob) {
                var
                    type = blob.type
                    , data_URI_header
                    ;
                if (type === null) {
                    type = "application/octet-stream";
                }
                if (blob instanceof FakeBlob) {
                    data_URI_header = "data:" + type;
                    if (blob.encoding === "base64") {
                        return data_URI_header + ";base64," + blob.data;
                    } else if (blob.encoding === "URI") {
                        return data_URI_header + "," + decodeURIComponent(blob.data);
                    } if (btoa) {
                        return data_URI_header + ";base64," + btoa(blob.data);
                    } else {
                        return data_URI_header + "," + encodeURIComponent(blob.data);
                    }
                } else if (real_create_object_URL) {
                    return real_create_object_URL.call(real_URL, blob);
                }
            };
            URL.revokeObjectURL = function(object_URL) {
                if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
                    real_revoke_object_URL.call(real_URL, object_URL);
                }
            };
            FBB_proto.append = function(data/*, endings*/) {
                var bb = this.data;
                // decode data to a binary string
                if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
                    var
                        str = ""
                        , buf = new Uint8Array(data)
                        , i = 0
                        , buf_len = buf.length
                        ;
                    for (; i < buf_len; i++) {
                        str += String.fromCharCode(buf[i]);
                    }
                    bb.push(str);
                } else if (get_class(data) === "Blob" || get_class(data) === "File") {
                    if (FileReaderSync) {
                        var fr = new FileReaderSync;
                        bb.push(fr.readAsBinaryString(data));
                    } else {
                        // async FileReader won't work as BlobBuilder is sync
                        throw new FileException("NOT_READABLE_ERR");
                    }
                } else if (data instanceof FakeBlob) {
                    if (data.encoding === "base64" && atob) {
                        bb.push(atob(data.data));
                    } else if (data.encoding === "URI") {
                        bb.push(decodeURIComponent(data.data));
                    } else if (data.encoding === "raw") {
                        bb.push(data.data);
                    }
                } else {
                    if (typeof data !== "string") {
                        data += ""; // convert unsupported types to strings
                    }
                    // decode UTF-16 to binary string
                    bb.push(unescape(encodeURIComponent(data)));
                }
            };
            FBB_proto.getBlob = function(type) {
                if (!arguments.length) {
                    type = null;
                }
                return new FakeBlob(this.data.join(""), type, "raw");
            };
            FBB_proto.toString = function() {
                return "[object BlobBuilder]";
            };
            FB_proto.slice = function(start, end, type) {
                var args = arguments.length;
                if (args < 3) {
                    type = null;
                }
                return new FakeBlob(
                    this.data.slice(start, args > 1 ? end : this.data.length)
                    , type
                    , this.encoding
                );
            };
            FB_proto.toString = function() {
                return "[object Blob]";
            };
            FB_proto.close = function() {
                this.size = this.data.length = 0;
            };
            return FakeBlobBuilder;
        }(view));
​
    view.Blob = function Blob(blobParts, options) {
        var type = options ? (options.type || "") : "";
        var builder = new BlobBuilder();
        if (blobParts) {
            for (var i = 0, len = blobParts.length; i < len; i++) {
                builder.append(blobParts[i]);
            }
        }
        return builder.getBlob(type);
    };
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

最终效效果展示

普通表头

image.png 多级表头

image.png

终极代码

<template>
  <div class="container">
    <div class="left-con">
      <el-input placeholder="输入关键字进行过滤" v-model="filterText">
      </el-input>
      <button class="icon-btn add-btn" @click="handlerAppend">
        <div class="add-icon"></div>
        <div class="btn-txt">添加 文件</div>
      </button>
      <el-tree class="filter-tree" :data="treeList" :props="defaultProps" default-expand-all highlight-current
        :expand-on-click-node="showTree" @node-click="handleNodeClick" :filter-node-method="filterNode" ref="tree">
        <template slot-scope="{ node, data }">
          <div class="flex-row treeSortList">
            <el-input v-model="data.label" placeholder="请输入文件名称" v-if="showEdit[data.id]" size="mini"></el-input>
            <el-tooltip v-else class="item" effect="light" content="点击进行操作" placement="top">
              <span> {{ node.label }} </span>
            </el-tooltip>
​
            <div class="doBtn" v-if="showEdit[data.id]">
              <el-button type="text" size="mini" title="保存修改" @click.stop="() => save(node, data)">保存
              </el-button>
              <el-button type="text" size="mini" title="取消修改" @click.stop="() => cancel(node, data)">取消
              </el-button>
            </div>
            <div class="doBtn" v-if="showBtn[data.id] && !showEdit[data.id]">
              <el-tooltip class="item" effect="light" content="添加子项目" placement="top">
                <el-button type="text" size="mini" title="添加" @click.stop="() => append(data)" icon="el-icon-plus">
                </el-button>
              </el-tooltip>
              <el-tooltip class="item" effect="light" content="编辑文件名" placement="top">
                <el-button type="text" size="mini" title="编辑" @click.stop="() => edit(data)" icon="el-icon-edit-outline">
                </el-button>
              </el-tooltip>
              <el-tooltip class="item" effect="light" content="删除文件" placement="top">
                <el-button type="text" size="mini" title="删除" @click.stop="() => remove(node, data)"
                  icon="el-icon-delete">
                </el-button>
              </el-tooltip>
            </div>
          </div>
        </template>
      </el-tree>
    </div>
    <div class="right-con">
      <div class="header-con">
​
        <el-popover placement="right" width="600" v-model="visible">
          <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
            <el-form-item label="字段(label)" prop="label">
              <el-input type="text" v-model="ruleForm.label" autocomplete="off"></el-input>
            </el-form-item>
            <el-form-item label="索引(prop)" prop="props">
              <el-input type="text" v-model="ruleForm.props" autocomplete="off"></el-input>
            </el-form-item>
            <el-form-item label="类型" :required="true">
              <el-select v-model="ruleForm.value" placeholder="请选择" style="width:100%" @change="changeModule">
                <el-option v-for="item in modules" :key="item.value" :label="item.label" :value="item.value">
                  <span style="float: left">{{ item.label }}</span>
                  <span style="float: right; color: #8492a6; font-size: 13px">{{ item.value }}</span>
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="是否有子项">
              <el-radio-group v-model="ruleForm.radio" @input="inputChildren">
                <el-radio :label="true"></el-radio>
                <el-radio :label="false"></el-radio>
              </el-radio-group>
              <el-button style="margin-left:30px;" v-if="ruleForm.radio" @click="addDomain">新增子项</el-button>
            </el-form-item>
            <el-form-item label="子项" v-if="ruleForm.childrenArr.length != 0" :required="true">
              <div style="display:flex;" v-for="(domain, index) in ruleForm.childrenArr" :key="domain.key">
                <el-input placeholder="字段(label)" type="text" v-model="domain.label" autocomplete="off"></el-input>
                <el-input placeholder="索引(prop)" type="text" v-model="domain.prop" autocomplete="off"></el-input>
                <el-select style="width:100%;" v-model="domain.value" placeholder="请选择类型">
                  <el-option v-for="item in modules" :key="item.value" :label="item.label" :value="item.value">
                    <span style="float: left">{{ item.label }}</span>
                    <span style="float: right; color: #8492a6; font-size: 13px">{{ item.value }}</span>
                  </el-option>
                </el-select>
                <el-button @click.prevent="removeDomain(index)">删除</el-button>
              </div>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
              <el-button @click="resetForm('ruleForm')">重置</el-button>
            </el-form-item>
          </el-form>
          <el-button slot="reference" icon="el-icon-plus">添加表头</el-button>
        </el-popover>
        <el-form v-if="showQueryParams" :model="queryParams" ref="ruleForm" class="fromCss" :inline="true">
          <el-form-item v-for="(item, index) in tableHeader" :key="index" :label="item.label">
            <el-input v-if="!item.children" :placeholder="`请输入${item.label}`" v-model="queryParams[item.prop]"
              autocomplete="off" @change="changeQueryParams"></el-input>
            <div v-if="item.children && item.children.length != 0" style="display: flex;
                  flex-wrap: wrap;">
              <div style="width:20%;margin:5px" v-for="domain in item.children" :key="domain.key">
                <el-input style="width:100%" v-if="domain.module == 'text'" :placeholder="`请输入${domain.label}`"
                  v-model="queryParams[domain.prop]" autocomplete="off"></el-input>
              </div>
            </div>
          </el-form-item>
​
        </el-form>
      </div>
      <div class="content-con" v-if="tableHeader.length != 0">
        <div class="content-con-top">
          <div class="content-con-l">
            <el-button type="primary" icon="el-icon-plus" size="mini" @click="handlerAddTable" plain>新增</el-button>
            <el-button type="success" icon="el-icon-edit" size="mini" :disabled="selectionKeys.length == 1 ? false : true"
              @click="handlerEditTable" plain>修改</el-button>
            <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="selectionKeys.length > 0 ? false : true"
              @click="handlerDelTable" plain>删除</el-button>
            <el-button type="info" icon="el-icon-upload2" size="mini" @click="handlerLeadingIn" plain>导入</el-button>
            <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport" plain>导出</el-button>
          </div>
          <div class="content-con-r">
            <el-tooltip class="item" effect="dark" content="隐藏搜索" placement="top">
              <el-button icon="el-icon-search" size="mini" @click="() => { this.showQueryParams = !this.showQueryParams }"
                circle></el-button>
            </el-tooltip>
            <el-tooltip class="item" effect="dark" size="mini" content="刷新" placement="top">
              <el-button style="margin:0;" icon="el-icon-refresh" size="mini" @click="handlerUpdateTable"
                circle></el-button>
            </el-tooltip>
            <el-tooltip class="item" effect="dark" content="显隐列" placement="top">
              <el-popover placement="bottom" width="200" trigger="click">
                <el-checkbox-group style="display: flex;flex-direction: column;" v-model="checkedTableColumns"
                  @change="handleCheckedCitiesChange">
                  <el-checkbox v-for="heads in tableHeader" :label="heads.prop" :key="heads.prop">{{ heads.label
                  }}</el-checkbox>
                </el-checkbox-group>
                <el-button slot="reference" icon="el-icon-menu" size="mini" circle></el-button>
              </el-popover>
​
            </el-tooltip>
          </div>
        </div>
        <!-- .slice((queryParams.pageNum - 1) * queryParams.pageSize, queryParams.pageNum * queryParams.pageSize) -->
        <DynamicTable :tableHeader="tableHeader" :tableData="tableDataList" :isIndex="false" :isSelection="true"
          @selection-change="handlerSelectionChange" @json-click="handlerJsonClick" :loading="loading" ref="TableList"
          height="700" />
        <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
          :page-sizes="[20, 50, 300, 400]" :page-size="20" layout="total, sizes, prev, pager, next, jumper"
          :total="total">
        </el-pagination>
      </div>
      <el-empty v-else description="创建一个新表吧~"></el-empty>
    </div>
    <!-- 新增 -->
    <TrendsFrom ref="TrendsFrom" :parentThat="this" @handlerConfirm="handlerConfirm" />
    <!-- 导入 -->
    <LeadingIn ref="LeadingIn" :parentThat="this" />
    <el-dialog title="设备日志" :visible.sync="showJson" width="40%">
      <json-viewer :value="jsonData" :expand-depth=5 copyable boxed sort></json-viewer>
    </el-dialog>
  </div>
</template><script>
import DynamicTable from "@/components/Table/DynamicTable.vue"
import TrendsFrom from "./TrendsFrom.vue"
import LeadingIn from "./LeadingIn.vue"let id = 20;
​
export default {
  components: { DynamicTable, TrendsFrom, LeadingIn },
  data() {
    return {
      filterText: "",
      treeList: [],
      defaultProps: {
        children: "children",
        label: "label",
      },
      showTree: false, //是否点击节点展开树,false 只能点前面三角图标展开
      showBtn: [],
      showEdit: [],
      editData: [],
      newAddData: false,
      // 表格数据
      showQueryParams: true,
      tableHeader: [],
      loading: false,
      tableData: [],
      queryParams: {
        pageNum: 1,
        pageSize: 20
      },
      // 表头添加
      ruleForm: {
        label: '',
        props: '',
        value: '',
        radio: false,
        childrenArr: [],
      },
      selectShow: false,
      modules: [{
        value: 'text',
        label: '文字'
      }, {
        value: 'textarea',
        label: '文本域'
      }, {
        value: 'password',
        label: '密码'
      },
        // {
        //   value:'select',
        //   label:'下拉框'
        // }, {
        //   value: 'image',
        //   label: '图片'
        // }
      ],
      rules: {
        label: [
          { required: true, message: '请填写表头字段', trigger: 'blur' }
        ],
        props: [
          {
            required: true,
            validator: (rule, value, callback) => {
              var rul = /(?![A-Z]*$)||(?![a-z]*$)/
              if (!rul.test(value)) {
                callback(
                  new Error(
                    '索引必须是大写字母或小写字母以上类型组成!'
                  )
                )
              } else {
                callback()
              }
            }, trigger: 'blur'
          }
        ],
      },
      visible: false,
      selectionKeys: [],
      checkedTableColumns: [],
      total: 0,
      showJson: false,
      jsonData: ''
    };
  },
  watch: {
    filterText(val) {
      this.$refs.tree.filter(val);
    },
  },
  computed: {
    tableDataList() {
      return this.tableData.slice((this.queryParams.pageNum - 1) * this.queryParams.pageSize, this.queryParams.pageNum * this.queryParams.pageSize)
    }
  },
  mounted() {
​
    this.treeList = [{
      id: 1,
      label: "文件1",
      children: [
        {
          id: 11,
          label: "文件1-01",
          children: [],
        },
        {
          id: 12,
          label: "文件1-02",
          children: [],
        }
      ],
    }, {
      id: 2,
      label: "文件2",
      children: [],
    }, {
      id: 3,
      label: "文件3",
      children: [],
    }]
​
  },
  methods: {
    filterNode(value, data) {
      if (!value) return true;
      return data.label.indexOf(value) !== -1;
    },
    handlerAppend() {
      let i = 0;
      if (this.data) {
        const newChild = {
          id: id++,
          label: "文件" + (Number(this.data.length) + 1),
          children: [],
        };
        this.treeList.push(newChild);
      } else {
        const newChild = {
          id: id++,
          label: "文件" + (i + 1),
          children: [],
        };
        this.treeList.push(newChild);
      }
      this.$message({
        message: "添加成功,开始编写吧~",
        type: "success",
      });
    },
    //点击树节点
    handleNodeClick(data) {
      if (!this.ifEdit()) {
        return;
      }
      this.showBtn = [];
      // 数组:第一个参数是要修改的数组, 第二个值是修改的下标或字段,第三个是要修改成什么值
      // 对象:第一个参数是要修改的对象, 第二个值是修改属性字段,第三个是要修改成什么值
      this.$set(this.showBtn, data.id, true);
      this.getTableData(data.id)
      // console.log(data)
    },
    append(data) {
      if (!this.ifEdit()) {
        return;
      }
      const newChild = { id: id++, label: "", children: [] };
      if (!data.children) {
        this.$set(data, "children", []);
      }
      data.children.push(newChild);
​
      this.newAddData = true;
      this.$set(this.showEdit, newChild.id, true);
    },
    edit(data) {
      var localEdit = localStorage.getItem("treeEdit");
      var id = data.id;
      var newlocalData = { [data.id]: data.label };
      if (localEdit) {
        var localData = JSON.parse(localEdit);
        Object.assign(localData, newlocalData);
        localData = JSON.stringify(localData);
        localStorage.setItem("treeEdit", localData);
      } else {
        newlocalData = JSON.stringify(newlocalData);
        localStorage.setItem("treeEdit", newlocalData);
      }
      // console.log(localStorage.getItem("treeEdit"));
      this.showEdit = [];
      this.$set(this.showEdit, data.id, true);
    },
    remove(node, data) {
      const parent = node.parent;
      const children = parent.data.children || parent.data;
      const index = children.findIndex((d) => d.id === data.id);
      children.splice(index, 1);
    },
    //保存
    save(node, data) {
      if (data.label == "") {
        this.$message({
          type: "error",
          message: "请输入完整数据",
          offset: 70,
        });
        return;
      }
      this.newAddData = "";
      var localEdit = localStorage.getItem("treeEdit");
      if (localEdit) {
        var localData = JSON.parse(localEdit);
        delete localData[data.id]; //删除已经取消项
        localData = JSON.stringify(localData);
        localStorage.setItem("treeEdit", localData); //重置缓存
      }
      this.$set(this.showEdit, data.id, false);
    },
    cancel(node, data) {
      if (this.newAddData) {
        this.remove(node, data);
      }
      var localEdit = localStorage.getItem("treeEdit");
      if (localEdit) {
        var localData = JSON.parse(localEdit);
        data.label = localData[data.id];
        delete localData[data.id]; //删除已经取消项
        localData = JSON.stringify(localData);
        localStorage.setItem("treeEdit", localData); //重置缓存
      }
      this.$set(this.showEdit, data.id, false);
    },
    //判断是否有编辑或增加的项
    ifEdit() {
      if (this.showEdit.indexOf(true) != -1) {
        this.$message({
          type: "error",
          message: "先保存正在编辑的行",
          offset: 70,
        });
        return false;
      } else {
        return true;
      }
    },
    getTableData(id) {
      if (id == 11) {
        this.tableHeader = [
          {
            id: Math.random(),
            label: '数量(家)',
            prop: 'number',
            module: 'text',
            show: true
          }, {
            id: Math.random(),
            label: '名称',
            prop: 'name',
            module: 'text',
            show: true,
          }, {
            id: Math.random(),
            label: '详细地址',
            prop: 'address',
            module: 'textarea',
            show: true,
          }, {
            id: Math.random(),
            label: '负责人姓名、电话',
            prop: 'name_phone',
            module: 'text',
            show: true,
          }
        ]
        this.checkedTableColumns = this.tableHeader.map(column => column.prop)
      } else if (id == 12) {
        this.tableHeader = [
          {
            id: Math.random(),
            label: '姓名',
            prop: 'name',
            module: 'text',
            show: true,
          }, {
            id: Math.random(),
            label: '账号',
            prop: 'account',
            module: 'text',
            show: true,
          }, {
            id: Math.random(),
            label: '部门',
            prop: 'department',
            module: 'text',
            show: true,
          }, {
            id: Math.random(),
            label: '职务',
            prop: 'job',
            module: 'text',
            show: true,
          }, {
            id: Math.random(),
            label: '所属规则',
            prop: 'Owning_rule',
            module: 'text',
            show: true,
          }, {
            id: Math.random(),
            label: '概括',
            prop: 'generalize',
            // module: 'text',
            show: true,
            children: [
              {
                id: Math.random(),
                label: '应打卡天数(天)',
                prop: 'day1',
                module: 'text',
                show: true,
              }, {
                id: Math.random(),
                label: '实际打卡天数(天)',
                prop: 'day2',
                module: 'text',
                show: true,
              }, {
                id: Math.random(),
                label: '正常天数(天)',
                prop: 'day3',
                module: 'text',
                show: true,
              }, {
                id: Math.random(),
                label: '异常天数(天)',
                prop: 'day4',
                module: 'text',
                show: true,
              }, {
                id: Math.random(),
                label: '标准工作时长(小时)',
                prop: 'day5',
                module: 'text',
                show: true,
              }, {
                id: Math.random(),
                label: '实际工作时长(小时)',
                prop: 'day6',
                module: 'text',
                show: true,
              },
            ]
          }
        ]
        this.checkedTableColumns = this.tableHeader.map(column => column.prop)
        this.tableData = [{
          id: Math.random(),
          name: '张三',
          account: '23523',
          department: '897',
          job: 'nalvnl',
          Owning_rule: '23',
          generalize: '890',
          day1: '1',
          day2: '2',
          day3: '3',
          day4: '4',
          day5: '5',
          day6: '6',
        }, {
          id: Math.random(),
          name: '李四',
          account: '23523',
          department: '897',
          job: 'nalvnl',
          Owning_rule: '23',
          generalize: '890',
          day1: '1',
          day2: '2',
          day3: '3',
          day4: '4',
          day5: '5',
          day6: '6',
        }]
      } else {
        this.tableHeader = []
        this.tableData = []
        this.checkedTableColumns = this.tableHeader.map(column => column.prop)
      }
    },
    // 搜索
    changeQueryParams(val) {
​
    },
    inputChildren(val) {
      if (val) {
        this.ruleForm.childrenArr.push({
          key: Date.now(),
          label: '',
          prop: '',
          value: '',
        })
      } else {
        this.ruleForm.childrenArr = []
      }
    },
    // 添加子项
    removeDomain(index) {
      // var index = this.ruleForm.childrenArr.indexOf(item => item.key == domain.key)
      if (index !== 0) {
        this.ruleForm.childrenArr.splice(index, 1)
      }
    },
    addDomain() {
      this.ruleForm.childrenArr.push({
        key: Date.now(),
        label: '',
        prop: '',
        value: '',
      });
    },
    // 添加表头
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          if (this.tableHeader.length == 0) {
            if (this.ruleForm.label && this.ruleForm.props && this.ruleForm.value) {
              this.tableHeader.push({
                label: this.ruleForm.label,
                prop: this.ruleForm.props,
                module: this.ruleForm.value,
                show: true,
                radio: this.ruleForm.radio,
                children: this.ruleForm.childrenArr
              })
              this.ruleForm = this.$options.data.call(this).ruleForm
              this.visible = false
            }
          } else {
            this.tableHeader.forEach(item => {
              if (item.label == this.ruleForm.label || item.prop == this.ruleForm.props) {
                if (item.prop == this.ruleForm.props) {
                  this.$message.error('索引(prop)已存在,换一个索引(prop)填写!');
                } else {
                  this.$message.error('字段(label)已存在,换一个字段(label)填写!');
                }
              } else {
                if (this.ruleForm.label && this.ruleForm.props && this.ruleForm.value) {
                  this.tableHeader.push({
                    label: this.ruleForm.label,
                    prop: this.ruleForm.props,
                    module: this.ruleForm.value,
                    show: true,
                    radio: this.ruleForm.radio,
                    children: this.ruleForm.childrenArr
                  })
                  this.ruleForm = this.$options.data.call(this).ruleForm
                  this.visible = false
​
                }
              }
​
            })
          }
          this.checkedTableColumns = this.tableHeader.map(column => column.prop)
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    changeModule(val) {
      console.log(val)
      if (val == 'select') {
        this.selectShow = true
      } else {
        this.selectShow = false
      }
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
    // 添加列表
    handlerAddTable() {
      this.$refs.TrendsFrom.init('添加', this.tableHeader);
    },
    // 修改列表
    handlerEditTable() {
      this.$refs.TrendsFrom.init('修改', this.tableHeader, this.selectionKeys[0]);
    },
    // 删除列表
    handlerDelTable() {
      this.$confirm('此操作将删除该条数据,是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
        .then(() => {
          // 确定
          this.selectionKeys.forEach((sItem, j) => {
            let key = Object.keys(sItem)[0]
            let indexKey = this.tableData.findIndex((el) => el[key] == sItem[key]);//找到下标
            this.tableData.splice(indexKey, this.selectionKeys.length)
          })
          this.$refs.TableList.$refs.multipleTable.clearSelection()
          this.total = this.tableData.length
          // this.queryParams.pageNum = 1
          this.$message({
            showClose: true,
            message: '成功'
          });
        })
        .catch(() => {
          // 取消
          this.$message({
            showClose: true,
            message: '取消'
          });
        })
    },
    // 导入表格
    handlerLeadingIn() {
      this.$refs.LeadingIn.init('导入', this.tableHeader, this.tableData);
    },
    setDesc() {
      // JS-获取到26个英文大写字母(A-Z)
      const letterArr = []
      Array(26).fill('').map((item, index) => {
        letterArr.push(String.fromCharCode(index + 65))
      })
      return letterArr
    },
    /** 导出按钮操作 */
    handleExport() {
      let tHeader = [], multiHeader = [], header1 = [], filterPop = [], merges = [], merges1 = [], merges3 = [], merges2 = [], headerRowLength = 1;
      let flag = this.tableHeader.some(column => {
        if (column.children && column.children.length > 0) {
          return true
        }
        return false
      })
      if (flag) {
        const randomAbc = this.setDesc()
        this.tableHeader.map((column, i) => {
          if (column.show) {
            if (column.children && column.children.length > 0) {
              headerRowLength++
              header1.push(column.label)
              column.children.map((childColumn, j) => {
                merges2.push(randomAbc[j] + headerRowLength)
                tHeader.push(childColumn.label)
                filterPop.push(childColumn.prop)
                header1.push('')
​
              })
            } else {
              merges1.push(randomAbc[i] + headerRowLength)
              header1.push(column.label)
              tHeader.push('')
              filterPop.push(column.prop)
            }
          }
​
        })
        // 二维数组依次递增 表格头部 // tHeader 表格头部最后一级
        multiHeader.push(header1)
        // 合并的行
        merges1.forEach(i => {
          merges2.forEach(j => {
            if (i.substring(0, 1) == j.substring(0, 1)) {
              merges.push(i + ':' + j)
            }
          })
        })
        // 合并的列
        let len = merges1.length
        tHeader.forEach((item, o) => {
          merges3.push(randomAbc[o])
        })
        merges3.splice(0, len)
        merges.push(merges3.slice(0, 1)[0] + (headerRowLength - 1) + ':' + merges3.slice(-1)[0] + (headerRowLength - 1))
​
        this.$confirm('确定导出全部数据么?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        })
          .then(() => {
            // 确定
            this.getXlsxData_many(tHeader, multiHeader, filterPop, merges, this.tableData)
            this.$message({
              showClose: true,
              message: '下载成功'
            });
          })
          .catch(() => {
            // 取消
            this.$message({
              showClose: true,
              message: '取消下载'
            });
          })
      } else {
        this.tableHeader.map((column, i) => {
          if (column.show) {
            tHeader.push(column.label)
            filterPop.push(column.prop)
          }
        })
        this.$confirm('确定导出全部数据么?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        })
          .then(() => {
            // 确定
            this.getXlsxData(tHeader, filterPop, this.tableData)
            this.$message({
              showClose: true,
              message: '下载成功'
            });
          })
          .catch(() => {
            // 取消
            this.$message({
              showClose: true,
              message: '取消下载'
            });
          })
​
      }
​
    },
    // 多行表头下载
    getXlsxData_many(tHeader, multiHeader, filterPop, merges, dataT = []) {
      require.ensure([], () => {
        const { export_json_to_excel_headerMany } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
        const data = this.formatJson(filterPop, dataT);//格式化
        // console.log(tHeader, multiHeader, filterPop, merges, dataT);
        //这个得说明一下:网上得博客每个不一样,你那我的直接用也是没啥用得,你的理解这个合并是怎么写的:根据你的多级表头,如果没有合并得从上往下写,遇到开始合并单元格的,从左往右得单行写,从上到下,直到写完整
        export_json_to_excel_headerMany({
          multiHeader,
          header: tHeader,
          data,
          filename: `user_${new Date().getTime()}`, merges,
          autoWidth: true,
        })
      })
    },
    // 单行表头下载
    getXlsxData(tHeader = [], filterVal = [], dataT = []) {
      require.ensure([], () => {
        const { export_json_to_excel } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
        // const tHeader = []; //对应表格输出的title
        // const filterVal = []; // 对应表格输出的数据
        const data = this.formatJson(filterVal, dataT);
        export_json_to_excel(tHeader, data, `user_${new Date().getTime()}`); //对应下载文件的名字
      })
    },
    formatJson(filterVal, jsonData) {
      return jsonData.map(v => filterVal.map(j => v[j]))
    },
    // 刷新列表
    handlerUpdateTable() {
      this.loading = true
      setTimeout(() => {
        this.tableData = this.$options.data.call(this).tableData
        this.loading = false
      }, 200);
    },
    // 显示隐藏列
    handleCheckedCitiesChange(value) {
      this.tableHeader.forEach(column => {
        // 如果选中,则设置列显示
        if (value.includes(column.prop)) {
          column.show = true;
        } else {
          // 如果未选中,则设置列隐藏
          column.show = false;
        }
      })
    },
    // 表格数据
    handlerConfirm(data, keys) {
      if (keys) {
        let key = Object.keys(keys)[0]
        let indexKey = this.tableData.findIndex((el) => el[key] == keys[key]);//找到下标
        this.tableData[indexKey] = data //替换数据
        this.$message({
          message: '修改成功!',
          type: 'success'
        });
      } else {
        this.tableData.push(data)
        this.$message({
          message: '添加成功!',
          type: 'success'
        });
      }
      this.total = this.tableData.length
    },
    // 勾选
    handlerSelectionChange(val) {
      // console.log(val)
      this.selectionKeys = val
    },
    // 分页
    handleSizeChange(val) {
      this.queryParams.pageNum = 1;
      this.queryParams.pageSize = val;
    },
    handleCurrentChange(val) {
      this.queryParams.pageNum = val;
    },
    // 日志
    handlerJsonClick(row) {
      this.showJson = true
      this.jsonData = row
    },
  },
};
</script><style lang="scss" scoped>
::v-deep .el-tree-node__content {
  padding: 10px 0;
  height: auto;
}
​
.treeSortList {
  width: calc(100% - 30px);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
​
.treeSortList .doBtn {
  padding-right: 10px;
}
​
.treeSortList .doBtn .el-button--text {
  color: rgb(61, 61, 61);
  padding: 5px;
}
​
.treeSortList .doBtn .el-button--text i {
  color: rgb(80, 80, 80);
}
​
.treeSortList .doBtn .el-button--text i.el-icon-delete {
  color: #f3a68e;
}
​
.container {
  height: 100vh;
  overflow: hidden;
  display: flex;
  box-sizing: border-box;
​
  .left-con {
    background-color: #d3dce6;
    color: #333;
    // text-align: center;
    // line-height: 200px;
    width: 320px;
    box-sizing: border-box;
    padding: 20px;
    overflow-y: scroll;
  }
​
  .right-con {
    flex: 1;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
​
    .header-con {
      position: sticky;
      top: 0;
      margin-bottom: 10px;
      padding: 10px;
    }
​
    .content-con {
      flex: 1;
      overflow-y: scroll;
      padding: 10px;
​
      .content-con-top {
        display: flex;
        margin: 10px 0;
        justify-content: space-between;
​
        .content-con-l {}
​
        .content-con-r {
          display: flex;
          width: 10%;
          justify-content: space-between;
        }
      }
    }
  }
}
​
.header-con,
.content-con {
  background-color: #b3c0d1;
  color: #333;
  // text-align: center;
  // line-height: 60px;
}
​
.fromCss {
  .el-form-item {
    margin-bottom: 0;
    margin-top: 10px;
  }
}
​
.icon-btn {
  width: 50px;
  height: 50px;
  border: 1px solid #cdcdcd;
  background: white;
  border-radius: 25px;
  overflow: hidden;
  position: relative;
  transition: width 0.2s ease-in-out;
  font-weight: 500;
  font-family: inherit;
  margin: 10px auto;
}
​
.add-btn:hover {
  width: 120px;
}
​
.add-btn::before,
.add-btn::after {
  transition: width 0.2s ease-in-out, border-radius 0.2s ease-in-out;
  content: "";
  position: absolute;
  height: 4px;
  width: 10px;
  top: calc(50% - 2px);
  background: seagreen;
}
​
.add-btn::after {
  right: 14px;
  overflow: hidden;
  border-top-right-radius: 2px;
  border-bottom-right-radius: 2px;
}
​
.add-btn::before {
  left: 14px;
  border-top-left-radius: 2px;
  border-bottom-left-radius: 2px;
}
​
.icon-btn:focus {
  outline: none;
}
​
.btn-txt {
  opacity: 0;
  transition: opacity 0.2s;
}
​
.add-btn:hover::before,
.add-btn:hover::after {
  width: 4px;
  border-radius: 2px;
}
​
.add-btn:hover .btn-txt {
  opacity: 1;
}
​
.add-icon::after,
.add-icon::before {
  transition: all 0.2s ease-in-out;
  content: "";
  position: absolute;
  height: 20px;
  width: 2px;
  top: calc(50% - 10px);
  background: seagreen;
  overflow: hidden;
}
​
.add-icon::before {
  left: 22px;
  border-top-left-radius: 2px;
  border-bottom-left-radius: 2px;
}
​
.add-icon::after {
  right: 22px;
  border-top-right-radius: 2px;
  border-bottom-right-radius: 2px;
}
​
.add-btn:hover .add-icon::before {
  left: 15px;
  height: 4px;
  top: calc(50% - 2px);
}
​
.add-btn:hover .add-icon::after {
  right: 15px;
  height: 4px;
  top: calc(50% - 2px);
}
</style>

TrendsFrom.vue 表单新增或修改

<template>
    <el-dialog :title="title" :visible.sync="dialogFormVisible" width="45%">
        <el-form :model="form" ref="ruleForm" class="fromCss">
            <el-form-item v-for="(item, index) in headerData" :key="index" :label="item.label">
                <el-input v-if="item.module == 'text'" :placeholder="`请输入${item.label}`" v-model="form[item.prop]"
                    autocomplete="off"></el-input>
                <el-input v-if="item.module == 'password'" :placeholder="`请输入${item.label}`" v-model="form[item.prop]"
                    show-password></el-input>
                <el-input v-if="item.module == 'textarea'" :placeholder="`请输入${item.label}`" type="textarea"
                    :autosize="{ minRows: 2, maxRows: 4 }" v-model="form[item.prop]">
                </el-input>
                <div v-if="item.children && item.children.length != 0" style="display: flex;
            flex-wrap: wrap;">
                    <div style="width:50%;margin:5px 0" v-for="domain in item.children" :key="domain.key">
                        <el-input style="width:100%" v-if="domain.module == 'text'" :placeholder="`请输入${domain.label}`"
                            v-model="form[domain.prop]" autocomplete="off"></el-input>
                    </div>
                </div>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="resetForm('ruleForm')">取 消</el-button>
            <el-button type="primary" @click="submitForm('ruleForm')">确 定</el-button>
        </div>
    </el-dialog>
</template><script>
export default {
    props: {
        parentThat: {}
    },
    data() {
        return {
            // 表格添加
            title: '',
            dialogFormVisible: false,
            form: {},
            headerData: null,
            editKeys: null,
        }
    },
    mounted() {
    },
    methods: {
        init(title, headerData, keys) {
            this.dialogFormVisible = true;
            this.title = title
            this.headerData = headerData
            this.editKeys = null
            // console.log(headerData);
            if (keys) {
                this.editKeys = keys
                this.form = JSON.parse(JSON.stringify(keys))
            }
        },
        submitForm(formName) {
            this.$refs[formName].validate((valid) => {
                if (valid) {
                    if (this.editKeys) {
                        this.$emit('handlerConfirm', this.form, this.editKeys)
                    } else {
                        this.$emit('handlerConfirm', this.form)
                    }
                    this.parentThat.$refs.TableList.$refs.multipleTable.clearSelection()
                    this.form = this.$options.data.call(this).form
                    this.dialogFormVisible = false;
                } else {
                    console.log('error submit!!');
                    return false;
                }
            });
        },
        resetForm(formName) {
            this.form = this.$options.data.call(this).form
            this.dialogFormVisible = false;
            this.$refs[formName].resetFields();
        },
    }
}
</script><style lang="scss" scoped>
.fromCss {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
</style>

LeadingIn.vue 导入

<template>
    <el-dialog :title="title" :visible.sync="dialogFormVisible" width="400px">
        <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :action="upload.url" :on-change="handleChange"
            :auto-upload="false" drag>
            <i class="el-icon-upload"></i>
            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
            <div class="el-upload__tip" slot="tip">
                <div><el-checkbox v-model="upload.isUploading" />是否更新已经存在的数据</div>
                <div>仅允许导入xls、xlsx格式文件。<a style="color:dodgerblue;" @click="importTemplate">下载模板</a> </div>
            </div>
        </el-upload>
        <div slot="footer" class="dialog-footer">
            <el-button type="primary" @click="submitForm('uploadRef')">确 定</el-button>
            <el-button @click="resetForm('uploadRef')">取 消</el-button>
        </div>
    </el-dialog>
</template><script>
export default {
    props: {
        parentThat: {},
    },
    data() {
        return {
            // 表格添加
            title: '',
            dialogFormVisible: false,
            upload: {
                // 是否更新已数据
                isUploading: false,
                // 上传的地址
                url: process.env.VUE_APP_BASE_API +
                    "/efarmcloud-open-file/api/v1/files/upload"
            },
            XlsxData: [],
            fileContent: null,
            headerTable: [],
            tableData: [],
            loading:null,
        }
    },
    methods: {
        init(title, headerTable, tableData) {
            this.dialogFormVisible = true;
            this.title = title
            this.headerTable = headerTable
            this.tableData = tableData
        },
        setDesc() {
            // JS-获取到26个英文大写字母(A-Z)
            const letterArr = []
            Array(26).fill('').map((item, index) => {
                letterArr.push(String.fromCharCode(index + 65))
            })
            return letterArr
        },
        /** 下载模板操作 */
        importTemplate() {
            let tHeader = [], multiHeader = [], header1 = [], filterPop = [], merges = [], merges1 = [], merges3 = [], merges2 = [], headerRowLength = 1;
            // 有69个元素是空的,所以直接进行了截取
            let flag = this.headerTable.some(column => {
                if (column.children && column.children.length > 0) {
                    return true
                }
                return false
            })
            if (flag) {
                const randomAbc = this.setDesc()
               this.headerTable.map((column, i) => {
                    if (column.show) {
                        if (column.children && column.children.length > 0) {
                            headerRowLength++
                            header1.push(column.label)
                            column.children.map((childColumn, j) => {
                                merges2.push(randomAbc[j] + headerRowLength)
                                tHeader.push(childColumn.label)
                                filterPop.push(childColumn.prop)
                                header1.push('')
​
                            })
                        } else {
                            merges1.push(randomAbc[i] + headerRowLength)
                            header1.push(column.label)
                            tHeader.push('')
                            filterPop.push(column.prop)
                        }
                    }
​
                })
                // 二维数组依次递增 表格头部 // tHeader 表格头部最后一级
                multiHeader.push(header1)
                // 合并的行
                merges1.forEach(i => {
                    merges2.forEach(j => {
                        if (i.substring(0, 1) == j.substring(0, 1)) {
                            merges.push(i + ':' + j)
                        }
                    })
                })
                // 合并的列
                let len = merges1.length
                tHeader.forEach((item, o) => {
                    merges3.push(randomAbc[o])
                })
                merges3.splice(0, len)
                merges.push(merges3.slice(0, 1)[0] + (headerRowLength - 1) + ':' + merges3.slice(-1)[0] + (headerRowLength - 1))
                // console.log(filterPop, tHeader, multiHeader, headerRowLength, randomAbc, merges1, merges2, merges, merges3);
                this.getXlsxData_many(tHeader, multiHeader, filterPop, merges)
            } else {
                 this.tableHeader.map((column, i) => {
                    if (column.show) {
                        tHeader.push(column.label)
                        filterPop.push(column.prop)
                    }
                })
                this.getXlsxData(tHeader, filterPop)
            }
        },
        // 多行表头下载
        getXlsxData_many(tHeader, multiHeader, filterPop, merges, dataT = []) {
            require.ensure([], () => {
                const { export_json_to_excel_headerMany } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
                const data = this.formatJson(filterPop, dataT);//格式化
                // console.log(tHeader, multiHeader, filterPop, merges, dataT);
                //这个得说明一下:网上得博客每个不一样,你那我的直接用也是没啥用得,你的理解这个合并是怎么写的:根据你的多级表头,如果没有合并得从上往下写,遇到开始合并单元格的,从左往右得单行写,从上到下,直到写完整
                export_json_to_excel_headerMany({
                    multiHeader,
                    header: tHeader,
                    data,
                    filename: `user_${new Date().getTime()}`, merges,
                    autoWidth: true,
                })
            })
        },
        // 单行表头下载
        getXlsxData(tHeader = [], filterVal = [], dataT = []) {
            require.ensure([], () => {
                const { export_json_to_excel } = require('@/excel/Export2Excel.js');// 这里 require 写你的Export2Excel.js的绝对地址
                // const tHeader = []; //对应表格输出的title
                // const filterVal = []; // 对应表格输出的数据
                const data = this.formatJson(filterVal, dataT);
                console.log(tHeader, data, filterVal, '下载');
                export_json_to_excel(tHeader, data, `user_${new Date().getTime()}`); //对应下载文件的名字
            })
        },
        formatJson(filterVal, jsonData) {
            return jsonData.map(v => filterVal.map(j => v[j]))
        },
        // 核心部分代码(handleChange 和 importfile)
        handleChange(file) {
            this.fileContent = file.raw
            const fileName = file.name
            const fileType = fileName.substring(fileName.lastIndexOf('.') + 1)
            if (this.fileContent) {
                if (fileType === 'xlsx' || fileType === 'xls') {
                    // 判断是否有子项
                    let flag = this.headerTable.some(column => {
                        if (column.children && column.children.length > 0) {
                            return true
                        }
                        return false
                    })
                    this.loading = this.$loading({
                        lock: true,
                        text: 'Loading',
                        spinner: 'el-icon-loading',
                        background: 'rgba(0, 0, 0, 0.7)'
                    });
                    if (flag) {
                        this.importFile(this.fileContent, flag)
                        this.loading.close();
                    } else {
                        this.importFile(this.fileContent, flag)
                        this.loading.close();
                    }
​
                } else {
                    this.$message({
                        type: 'warning',
                        message: '附件格式错误,请重新上传!'
                    })
                }
            } else {
                this.$message({
                    type: 'warning',
                    message: '请上传附件!'
                })
            }
        },
        importFile(obj, bol) {
            const reader = new FileReader()
            const _this = this
            reader.readAsArrayBuffer(obj)
            reader.onload = function () {
                const buffer = reader.result
                const bytes = new Uint8Array(buffer)
                const length = bytes.byteLength
                let binary = ''
                for (let i = 0; i < length; i++) {
                    binary += String.fromCharCode(bytes[i])
                }
                const XLSX = require('xlsx')
                const wb = XLSX.read(binary, {
                    type: 'binary'
                })
                const outData = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]])
                // console.log(outData);
                const arr = [...outData], newArr = []
                arr.map((v, j) => {
                    let list = _this.headerTable.map((s, i) => {
                        let key = Object.keys(v)[i]
                        if (key == s.label) {
                            return { id: Math.random(), [s.prop]: v[key] }
                        }
                    })
                    let newList = []
                    list.forEach(item => {
                        if (item) {
                            if (bol) {
                                // 手动填写子项的prop值与对应xlsx 对应的字段
                                newList.push(item, {
                                    day1: v.概括,
                                    day2: v.__EMPTY,
                                    day3: v.__EMPTY_1,
                                    day4: v.__EMPTY_2,
                                    day5: v.__EMPTY_3,
                                    day6: v.__EMPTY_4,
                                })
                            } else {
                                newList.push(item)
                            }
                        }
                    })
                    if (newList.length != 0) {
                        newArr.push(Object.assign(...newList))
                    } else {
                        if (j < 1 && !bol) {
                            _this.$notify.error({
                                title: '错误',
                                message: '上传的数据格式不匹配,请重新上传!'
                            });
                            _this.$refs.uploadRef.clearFiles();
                        }
                    }
                })
                _this.XlsxData = newArr
            }
        },
        submitForm(uploadRef) {
            // console.log(this.XlsxData, this.parentThat._data.tableData, this.upload.isUploading);
            if (this.upload.isUploading) {
                this.parentThat._data.tableData = this.XlsxData
            } else {
                this.parentThat._data.tableData = this.parentThat._data.tableData.concat(this.XlsxData)
            }
            this.parentThat._data.total = this.parentThat._data.tableData.length //数量
            this.dialogFormVisible = false;
            this.$refs[uploadRef].clearFiles();
        },
        resetForm(uploadRef) {
            this.dialogFormVisible = false;
            this.$refs[uploadRef].clearFiles();
        },
    }
}
</script><style lang="scss" scoped>
.el-upload__tip {
    display: flex;
    flex-direction: column;
    align-items: center;
}
</style>
技术难点:
  1. 在已有表头已有数据的情况下,再次添加表头修改数据回显慢,切换页面数据才会显示(不明原因)。

  2. 使用前端分页,会导致表格勾选错乱或不显示(解决方案使用computed)

     <DynamicTable :tableHeader="tableHeader" :tableData="tableDataList" :isIndex="true" :isSelection="true"
              @selection-change="handlerSelectionChange" :loading="loading" ref="TableList" height="700" />
    ​
    computed: {
        tableDataList() {
          return this.tableData.slice((this.queryParams.pageNum - 1) * this.queryParams.pageSize, this.queryParams.pageNum * this.queryParams.pageSize)
        }
      },
    
  3. 删除删掉的永远是首条(由于没有id值,拿到的永远是第一条)(已修改)

  4. 表头过多的情况下加载会很慢,导入字符串建议一对一填写,数据缺失或表头样式复杂数据不准确

  5. 动态表单或动态表格内用使用或展示根据使用情况进行增减

弹窗样式(居中显示不超过屏幕内含滚动条)
  // el-dialog高度自适应,内容超出时中间出现滚动条
.common-dialog {
  display: flex;
  justify-content: center;
  align-items: Center;
  overflow: hidden;
​
​
  .el-dialog:not(.is-fullscreen) {
    margin-top: 0 !important;
  }
​
  .el-dialog {
    margin: 0 auto !important;
    position: relative;
​
​
    .el-dialog__header {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      width: 100%;
      height: 60px;
      z-index: 1;
      background-color: #fff;
    }
​
    .el-dialog__body {
      width: 100%;
      overflow: hidden;
      overflow-y: auto;
      max-height: 100vh; //最大高度为视口高度的90%
      padding-top: 60px;
      padding-bottom: 100px;
      z-index: 1;
    }
​
    .el-dialog__footer {
      position: absolute;
      left: 0;
      bottom: 0;
      right: 0;
      width: 100%;
      height: 80px;
      z-index: 1;
      background-color: #fff;
    }
  }
}