前言
- 本组件的封装使用了部分elemenu-ui的组件,使用时,需要注意导入
- 上传的组件使用了我自己制作的kl-upload ,大家也可以直接使用elementui的上传,并不影响功能,这儿也附上这个组件的制作地址 kl-upload
示例
- 上传文件
- 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>