Bootstrap

vue excel文件上传预览和修改组件封装

b站演示地址—下周有空再录制

前言

  • 本组件的封装使用了部分elemenu-ui的组件,使用时,需要注意导入
  • 上传的组件使用了我自己制作的kl-upload ,大家也可以直接使用elementui的上传,并不影响功能,这儿也附上这个组件的制作地址 kl-upload

示例

  1. 上传文件
    在这里插入图片描述
  2. table展示
    在这里插入图片描述

组件功能描述

  • 提供单个或多个excel文件的上传(简单的行列容易对应的表格)
  • 前端解析文件,生成表格(el-table)两份,一份的table1,一份冻结的table2,都支持下载为excel和收集为json数据上传
  • 表格分页展示
  • 支持在线修改和删除

使用方式

 <kl-excel @uploadJson="uploadJson" :options="options"></kl-excel>
 data() {
    return {
      options: [
        {
          name: "id",
          prop: "id",
        },
        {
          name: "姓名",
          prop: "username",
        },
        {
          name: "年龄",
          prop: "age",
        },
        {
          name: "性别",
          prop: "sex",
        },
      ],
    };
  },
  methods: {
    uploadJson(jsonData) {
      console.log("最终的上传数据--", jsonData);
    },
  },

实现

安装依赖

cnpm i file-saver xlsx script-loader -S

目录结构

在这里插入图片描述

以下依次为每个文件的内容

create_id.js

export function createId() {
  return (
    Math.floor(Math.random() * 100000) + Math.random().toString(36).substr(2)
  );
}

Export2Excel.js

require("script-loader!file-saver");
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);
  console.log("a");
  var oo = generateArray(theTable);
  var ranges = oo[1];

  /* original data */
  var data = oo[0];
  var ws_name = "SheetJS";
  console.log(data);

  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"
  );
}

index.js

var file = null;

// 将导入的excel转为 json
function importfxx(obj, excelToJson) {
  return new Promise((res, rej) => {
    let _this = this;
    // 通过DOM取文件数据
    file = obj;
    var rABS = false; //是否将文件读取为二进制字符串
    var f = file;
    var reader = new FileReader();
    //if (!FileReader.prototype.readAsBinaryString) {
    FileReader.prototype.readAsBinaryString = function (f) {
      var binary = "";
      var rABS = false; //是否将文件读取为二进制字符串
      var pt = this;
      var wb; //读取完成的数据
      var outdata;
      var reader = new FileReader();
      reader.onload = function (e) {
        var bytes = new Uint8Array(reader.result);
        var length = bytes.byteLength;
        for (var i = 0; i < length; i++) {
          binary += String.fromCharCode(bytes[i]);
        }
        var XLSX = require("xlsx");
        if (rABS) {
          wb = XLSX.read(btoa(fixdata(binary)), {
            //手动转化
            type: "base64",
          });
        } else {
          wb = XLSX.read(binary, {
            type: "binary",
          });
        }
        outdata = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); //outdata就是你想要的东西
        let arr = [];
        let length1 = outdata.length;
        let length2 = excelToJson.length;
        for (let i = 0; i < length1; i++) {
          let obj = {};
          for (let item in outdata[i]) {
            for (let j = 0; j < length2; j++) {
              if (excelToJson[j].name == item) {
                obj[excelToJson[j].prop] = outdata[i][item];
              }
            }
          }

          arr.push(obj);
        }
        res(arr);
      };
      reader.readAsArrayBuffer(f);
    };

    if (rABS) {
      reader.readAsArrayBuffer(f);
    } else {
      reader.readAsBinaryString(f);
    }
  });
}

function getExcel(res, excelToJson) {
  require.ensure([], () => {
    const {
      export_json_to_excel,
    } = require("@/mixins/components/kl-excel/tool/Export2Excel.js");
    // const tHeader = ['id', '姓名', '年龄', '性别']
    const tHeader = excelToJson.map((item) => {
      return item.name;
    });
    const filterVal = excelToJson.map((item) => {
      return item.prop;
    });
    // const filterVal = ['id', 'name', 'age', 'sex']
    const list = res;
    const data = formatJson(filterVal, list);
    export_json_to_excel(tHeader, data, "导出列表名称");
  });
}

function formatJson(filterVal, jsonData) {
  return jsonData.map((v) => filterVal.map((j) => v[j]));
}

export { importfxx, getExcel };

index.vue

<template>
  <div class="kl-excel">
    <h1>excel文件上传预览,及修改</h1>
    <component
      :navName="navName"
      :tableData="tableData"
      @putInfoToTableData="putInfoToTableData"
      @changeCom="changeCom"
      @changeData="changeData"
      @deleteFormData="deleteFormData"
      @removeToFreeze="removeToFreeze"
      @switchNav="switchNav"
      @uploadJson="uploadJson"
      :is="componentId"
      :options="options"
    ></component>
  </div>
</template>

<script>
import Table from "./table.vue";
import Upload from "./upload.vue";
export default {
  components: {
    Table,
    Upload,
  },
  props: {
    options: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      tableData: [],
      freezeData: [],
      componentId: Upload,
      defaultData: [],
      navName: "default",
      key: "",
    };
  },
  methods: {
    // 获取key
    getKey() {
      if (this.key) {
        return;
      }
      let obj = JSON.parse(window.localStorage.getItem("key") || null);

      if (!obj) {
        return this.$message.error("内部错误,请清除数据后重新上传");
      }

      this.key = obj.key;
    },
    // 确定修改/提交新项
    putInfoToTableData(option) {
      if (option.type === "add") {
        this.tableData.unshift(option.info);
        return;
      }

      this.getKey();

      // 修改
      this.tableData = this.tableData.map((item) => {
        if (item[this.key] === option.info[this.key]) {
          item = option.info;
        }

        return item;
      });
    },
    // 将最终的数据导出
    uploadJson(data) {
      // 到处前需要删除key
      let data_copy = JSON.parse(JSON.stringify(data));
      data_copy.forEach((item) => {
        delete item[this.key];
      });
      this.$emit("uploadJson", data_copy);
    },
    // 切换顶部
    switchNav(navName) {
      this.navName = navName;
      if (navName === "default") {
        // 先需要保存原来的信息
        this.freezeData = this.tableData;
        this.tableData = this.defaultData;
        return;
      }

      this.defaultData = this.tableData;
      this.tableData = this.freezeData;
    },
    // 冻结 --- 解冻
    removeToFreeze(id) {
      // console.log(id);
      // console.log(this.tableData);
      this.getKey();
      let info = this.tableData.find((item) => {
        return item[this.key] === id;
      });
      // console.log(info);

      if (!info) {
        return this.$message.error("内部错误,请重试");
      }

      if (this.navName !== "default") {
        // 解冻
        this.defaultData.unshift(info);
      } else {
        // 冻结
        this.freezeData.unshift(info);
      }

      this.tableData = this.tableData.filter((item) => {
        return item[this.key] !== id;
      });
    },

    // 删除
    deleteFormData(id) {
      this.getKey();
      this.tableData = this.tableData.filter((item) => {
        return item[this.key] !== id;
      });
    },

    changeData(val) {
      // console.log("父组件tabledata", val);
      this.tableData = val;
      this.defaultData = val;
    },
    changeCom(val) {
      this.componentId = val;
    },
  },
};
</script>

<style lang="scss" scoped>
.item {
  display: flex;
  span {
    display: block;
    min-width: 80px;
  }
}
</style>

table.vue

<template>
  <div class="table">
    <ul class="header-nav">
      <li
        @click="switchNav('default')"
        :class="navName === 'default' ? 'active' : ''"
      >
        表单数据
      </li>
      <li
        @click="switchNav('freeze')"
        :class="navName === 'freeze' ? 'active' : ''"
      >
        冻结数据
      </li>
    </ul>
    <div class="tool">
      <el-button @click="exportExcel" type="primary">导出表单</el-button>
      <el-button @click="uploadJson" type="success">确定上传</el-button>
      <el-button @click="postInfo"> 新增 </el-button>
    </div>

    <!-- 切换数据展示 --- 需要上传的数据,冻结数据 -->

    <el-table
      v-if="tableData.length > 0"
      :data="
        tableData.slice(
          (page.currentPage - 1) * page.pageSize,
          page.currentPage * page.pageSize
        )
      "
      style="width: 100%"
      border
    >
      <el-table-column
        align="center"
        header-align="center"
        v-for="(item, index) in this.options"
        :key="index"
        :prop="item.prop"
        :label="item.name"
      >
      </el-table-column>
      <el-table-column
        align="center"
        header-align="center"
        fixed="right"
        label="操作"
        width="250"
      >
        <template slot-scope="scope">
          <el-button
            @click="removeToFreeze(scope.row)"
            type="warning"
            size="small"
            >{{ navName === "default" ? "冻结" : "解冻" }}</el-button
          >
          <el-button @click="handleClick(scope.row)" type="primary" size="small"
            >修改</el-button
          >
          <el-button
            @click="deleteFormData(scope.row)"
            type="danger"
            size="small"
            >删除</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <div class="page">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="page.currentPage"
        :page-sizes="page.pageSizes"
        :page-size="page.pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="page.total"
      >
      </el-pagination>
    </div>

    <el-dialog :title="title" :visible.sync="dialogVisible" width="450px">
      <div class="container">
        <ul>
          <li v-for="(val, key1, index) in Info" :key="index">
            <div v-if="key1 !== key">
              <span> {{ key1 | filterKey(that) }}: </span>
              <input type="text" :value="val" @input="input(key1, $event)" />
            </div>
          </li>
        </ul>
      </div>
      <div class="flex-center">
        <el-button @click="cancel">取 消</el-button>
        <el-button type="primary" @click="putInfoToTableData">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
// import { this.options } from "@/mixins/components/kl-excel/tool/config.js";
import { getExcel } from "@/mixins/components/kl-excel/tool/index.js";
import { createId } from "@/mixins/components/kl-excel/tool/create_id.js";

export default {
  name: "table",
  components: {},
  props: {
    tableData: [],
    freezeData: [],
    options: [],
    navName: "default",
  },
  data() {
    return {
      page: {
        pageSizes: [5, 10, 15, 20],
        pageSize: 5,
        total: 0,
        currentPage: 1,
      },
      Info: null,
      dialogVisible: false,
      title: "",
      that: this,
      key: "",
    };
  },

  watch: {
    tableData: {
      handler(val) {
        // console.log("table组件接收到的tableData ---", val);
        this.tableData = val;
        this.page.total = val.length;
      },
      immediate: true,
    },
  },
  filters: {
    filterKey(val, that) {
      // console.log(that.options);
      let info = that.options.find((item) => {
        return item.prop === val;
      });
      // console.log(info);
      if (!info) {
        return "";
      }
      return info.name;
    },
  },

  methods: {
    // 获取key
    getKey() {
      if (this.key) {
        return;
      }
      let obj = JSON.parse(window.localStorage.getItem("key") || null);

      if (!obj) {
        return this.$message.error("内部错误,请清除数据后重新上传");
      }

      this.key = obj.key;
    },

    switchNav(navName) {
      // console.log(nav);
      this.navName = navName;
      this.page = {
        pageSizes: [5, 10, 15, 20],
        pageSize: 5,
        total: 0,
        currentPage: 1,
      };

      this.$emit("switchNav", navName);
    },

    // 冻结
    removeToFreeze(info) {
      this.getKey();
      this.$emit("removeToFreeze", info[this.key]);
    },

    // 删除
    deleteFormData(info) {
      this.getKey();
      this.$emit("deleteFormData", info[this.key]);
    },
    handleSizeChange(val) {
      // console.log(`每页 ${val} 条`);
      this.page.pageSize = val;
    },
    handleCurrentChange(val) {
      // console.log(`当前页: ${val}`);
      this.page.currentPage = val;
    },
    // 添加一项新的
    postInfo() {
      this.title = "新增";
      // console.log(this.options);

      this.Info = {};
      this.options.forEach((item) => {
        this.Info[item.prop] = "";
      });

      // console.log(this.Info);

      this.dialogVisible = true;
    },
    // 确定修改/提交新项
    putInfoToTableData() {
      this.getKey();
      if (this.title === "新增") {
        this.Info[this.key] = createId();
        this.$emit("putInfoToTableData", {
          type: "add",
          info: this.Info,
        });
        this.Info = null;
        this.dialogVisible = false;
        return;
      }

      // 修改
      this.$emit("putInfoToTableData", {
        type: "put",
        info: this.Info,
      });

      this.dialogVisible = false;
    },
    // 关闭弹窗
    cancel() {
      this.Info = null;
      this.dialogVisible = false;
    },
    // 收集变化后的数据
    input(key, e) {
      this.Info[key] = e.target.value;
    },
    // 向后端上传json 数据
    uploadJson() {
      // console.log(this.tableData);
      this.$emit("uploadJson", this.tableData);
    },
    // 修改
    handleClick(data) {
      this.getKey();
      this.title = "修改";
      this.Info = JSON.parse(JSON.stringify(data));
      // console.log(data);
      this.dialogVisible = true;
    },

    // 导出
    exportExcel() {
      getExcel(this.tableData, this.options);
    },
  },
};
</script>

<style lang="scss" scoped>
.header-nav {
  display: flex;
  li {
    font-weight: 600;
    cursor: pointer;
    margin-right: 30px;
    height: 40px !important;
    display: flex;
    align-items: center;
    padding: 0 5px;
  }
}
.tool {
  height: 80px;
  display: flex;
  align-items: center;
  padding-left: 20px;
}

.container {
  li {
    display: flex;
    align-items: center;
    margin-bottom: 20px;
    /* &:nth-last-of-type(1) {
      margin-bottom: 0;
    } */
    span {
      text-align: right;
      display: inline-block;
      width: 80px;
      margin-right: 10px;
    }
    input {
      height: 40px;
      width: 250px;
      border: 1px solid #369;
      line-height: 40px;
      padding-left: 10px;
      border-radius: 3px;
    }
  }
}

.page {
  height: 60px;
  display: flex;
  justify-content: center;
  align-items: center;
}
.active {
  color: #369 !important;
  border-bottom: 1px solid #369;
}
.flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

upload.vue

<template>
  <div class="kl-excel">
    <div class="excel">
      <kl-upload :limit="8" @getFiles="getFiles" :allowTypes="allowTypes">
      </kl-upload>
    </div>
  </div>
</template>

<script>
import { importfxx } from "@/mixins/components/kl-excel/tool/index.js";
import { createId } from "@/mixins/components/kl-excel/tool/create_id.js";
export default {
  props: {
    options: [],
  },
  data() {
    return {
      allowTypes: ["xls", "xlsx", "csv"],
    };
  },
  methods: {
    // 将file转化为json
    formToJson(formData) {
      return new Promise(async (res, rej) => {
        let result = await importfxx(formData, this.options);
        if (result) {
          return res(result);
        }

        res([]);
      });
    },
    // 文件上传成功
    async getFiles(val) {
      let result = [];
      for (let i = 0; i < val.length; i++) {
        let res = await this.formToJson(val[i].file);
        result = [...result, ...res];
      }

      if (!result) {
        return this.$message.error("请重新检查文件后再上传");
      }

      // 需要生成全局的唯一key项
      let key = "wxj" + (Math.ceil(Math.random() * 100000) + 1000);

      window.localStorage.setItem(
        "key",
        JSON.stringify({
          key,
        })
      );

      result.forEach((item) => {
        item[key] = createId();
      });

      this.$emit("changeData", result);
      this.$emit("changeCom", "Table");
    },
  },
};
</script>

<style lang="scss" scoped>
.item {
  display: flex;
  span {
    display: block;
    min-width: 80px;
  }
}
</style>
;