手动合并多张Excel后,我做了一个合并生成Excel的小工具

年前正准备休假。同事火急火燎的让我帮个忙,我还没反应过来,她已经给我发来了20个Excel文件,打看一看,这是一份统计数据,几张表的数据总量在5万内。同事告诉我,做如下的操作:

  1. 将20张表的数据,汇总到一个表格中。
  2. 为每个表格增加一列,这一列标注来源。
  3. 替换表格内的表头数据,该表头数据根据用户指定的含义设置。

我看了看这20张表,再看看同事紧急的样子,没想太多就搞起来。挨个打开文件,复制粘贴,增加列,修改表头;有的数据列由于信息缺失,在粘贴的时候竟然粘错了位置,于是撤销重来。如此操作,中午的一个半小时就过去了。下午我好奇的问同事,这个工作你做了多久,她回答几乎每月会统计好几次。我说每次都是这样操作吗?她说是的,繁琐的操作让她眼花缭乱,手工操作下来,有时还会丢失数据,她被这种统计工作,折腾的精疲力竭。当即,我就在想能不能做个小的工具来解决这个问题呢?于是我立即罗列了一下涉及到的问题。

  1. 多个文件打开、复制。 -> 读取excel文件。
  2. 对文件增加列 -> 读取文件前的配置操作。
  3. 表头设置 -> 读取文件后,生成文件前的标题配置。
  4. 粘贴、汇总到一张表内 -> 合并数据,并生成excel文件。

整理出来后,其实就是个简单的excel数据读取、合并、生成过程。复工后,用空闲时间把这个小工具搞了下,同事在年后的统计工作中,正式使用了这个小工具,表示很好用,因为将之前的一两小时时间缩短到了一两分钟。那来看下目前的实现效果吧。

项目实现,基于VUE3框架,插件主要使用了xlsx,VUE3创建项目过程不再赘述,根据官网即可完成,下面主要讲一下文件处理方面的。

一、读取文件

读取文件过程,主要涉及三方面内容,文件选择容器、文件读取插件、文件虚拟滚动。

1.1 文件选择容器

面向用户的操作中,需要给用户提供操作入口,HTML中的input type="file">元素允许用户从当前设备中选择文件。在聊聊web中关于文件的使用,及大文件分片上传的实践一文中对该元素做了详细说明。这里我直接使用Element-Plus的Upload组件给用户提供选择文件的入口。注:该组件本质也是对input type="file">做了封装。

html 复制代码
<el-upload
        class="upload-wrapper"
        ref="upload"
        action
        multiple
        accept=".xlsx,.xls"
        :auto-upload="false"
        :on-change="handleFiles"
        :show-file-list="false"
>
    <template #trigger>
        <el-button ref="uploadRef" type="primary">选择文件</el-button>
    </template>
</el-upload>

1.2 xlsx读取文件

当用户选择多个文件后,需要对选择文件依次读取。在讲解具体处理方法前,大家可通过官网来了解excel中的基础概念如何抽象成xlsx的数据模型的。有了前置知识,在使用xlsx的API时,可以有更好的理解。

  1. 读取文件:XLSX.read(data, options)
  2. 读取数据转化为json对象数组:XLSX.utils.sheet_to_json(worksheet, opts)

根据官网API指引,在读取文件操作上,主要需要使用到上面列出的两个API。第一个read接口,可根据数据类型将读取的data数据,解析为对应的工作簿对象。这里的data由FileReader 来读取,fileReader提供不同的read方式,不同read接口返回的数据类型不同,如FileReader.readAsArrayBuffer()返回的是读取文件的 ArrayBuffer数据对象,FileReader.readAsDataURL()返回URL格式的 Base64 字符串。XLSX的read接口则根据fileReader的data类型,来选择type。

js 复制代码
function readLocalFile(file) {
    const reader = new FileReader()
    reader.readAsArrayBuffer(file.raw) // 按照arraybuffer格式读取,这里的读取方式决定了XLSX read接口的type选择
}

1.3 文件的虚拟滚动

文件在读取过程中,通常是一次性读取整个excel当中的数据,当数据量过大时,会出现页面卡顿、空白的现象,这样会导致页面假死,用户体验差。因此虚拟滚动需要考虑到。虚拟滚动,提供一种优化一次性读取大数据量的方式,通过只展示视窗内的数据,而非全部数据,来提高页面性能。这里选择 vue3-virtual-scroller实现虚拟滚动效果。 注意,该插件在实现虚拟滚动上要求数据提供唯一id,因此在读取数据过程中,会自动生成uuid,来满足该插件的要求。

js 复制代码
<div class="table-container">
    <table-head :header="columnDatas" v-if="columnDatas.length" class="header-wrapper"/>
    <RecycleScroller
            class="virtual-table-container"
            :items="tableDatas"
            :item-size="30"
            key-field="EXCEL_READ_UUID" // 唯一UUID
    >
        <template #default="{ item }">
            <table-row :item="item"></table-row>
        </template>
    </RecycleScroller>

二、合并文件

文件读取之后,对文件进行合并。合并相对简单,只需要对文件进行concat即可。

ini 复制代码
tableDatas.value = tableDatas.value.concat(datas)

三、配置表头

由于在生成文件时,可能会根据用户要求,更改excel表头数据,因此在生成文件前,提供了表头的配置功能。满足个性化的表头配置工作。这里提供的是,根据表头生成的可编辑表格弹窗组件。使用el-form和el-table结合。这里需要做表头数据的转化,稍后把代码都贴上。

js 复制代码
<el-form ref="cfgForm">
    <el-table :data="tableResults" border height="560">
        <template v-for="(column, idx) in tableColumns" :key="idx">
            <el-table-column :label="column.name" :prop="column.id">
                <template #header>
                    <span class="required-symbol" v-if="idx === 1">*</span>
                    <span>{{ column.name }}</span>
                </template>
                <template #default="scope">
                    <el-form-item :label="tableResults[scope.$index][column.id]" v-if="!idx"/>
                    <el-form-item :prop="tableResults[scope.$index][column.id]" v-else>
                        <el-input v-model="modelResults[scope.$index][column.id]"></el-input>
                    </el-form-item>
                </template>
            </el-table-column>
        </template>
    </el-table>
</el-form>

四、生成文件

文件读取、表头配置完成后,可以生成文件了,生成文件时,按照excel的文档结构进行构造。

  1. 使用XLSX.utils.json_to_sheet生成工作表。
  2. 使用XLSX.utils.book_new生成工作簿。
  3. 使用XLSX.utils.book_append_sheet将工作表附加到工作簿。
  4. 使用writeFileXLSX将数据导出。

导出的数据会生成配置之后的excel表格。

js 复制代码
function exportFile(res) {
    const ws = utils.json_to_sheet(res);
    const wb = utils.book_new();
    utils.book_append_sheet(wb, ws, "Data");
    writeFileXLSX(wb, "导出数据.xlsx");
    tableRes.value = []
}

部分重要代码如下

js 复制代码
<!--页面内容-->
<template>
    <el-upload
            class="upload-wrapper"
            ref="upload"
            action
            multiple
            accept=".xlsx,.xls"
            :auto-upload="false"
            :on-change="handleFiles"
            :show-file-list="false">
        <template #trigger>
            <el-button ref="uploadRef" type="primary">选择文件</el-button>
        </template>
        <el-button class="ml-30" type="success" @click="submitUpload" :disabled="!tableDatas.length"> 生成文件</el-button>
        <el-button class="ml-30" type="warning" @click="reset">清空选择</el-button>
        <span class="tip-wrapper ml-30">当前已合并<span>{{ tableDatas.length }}</span>条数据</span>
    </el-upload>
    <div class="table-container">
        <table-head :header="columnDatas" v-if="columnDatas.length" class="header-wrapper"/>
        <RecycleScroller
                class="virtual-table-container"
                :items="tableDatas"
                :item-size="30"
                key-field="EXCEL_READ_UUID">
            <template #default="{ item }">
                <table-row :item="item"></table-row>
            </template>
        </RecycleScroller>
    </div>
    <HeaderCfg :visible="cfg" :tableResults="tableTitleRes" @close="closeCfg"/>
</template>
js 复制代码
<!--逻辑处理-->
// 读取当前文件
function readLocalFile(file) {
    const reader = new FileReader()
    reader.onload = (event) => {
        tLoading.value = false
        extractFileDatas(event, file)
    }
    reader.onprogress = (event) => {
        if (event.lengthComputable) {
            tLoading.value = true
        }
    };
    reader.readAsArrayBuffer(file.raw)
}

// 处理文件数据
function extractFileDatas(event, file) {
    const data = new Uint8Array(event.target.result);
    const workbook = read(data, {type: 'array'});
    const worksheetName = workbook.SheetNames[0] // 读取第一个sheet内容
    const worksheet = workbook.Sheets[worksheetName]; // 获取指定工作表的数据
    const header = utils.sheet_to_json(worksheet, {header: 1}); // 读取表头
    const fileDatas = utils.sheet_to_json(worksheet, {defval: ""}); // 读取表格内容
    const fileName = file.name.split(".")[0]

    const combineFileDatas = webpackDatas(fileDatas, header, fileName) // 包装合并数据
    return combineData(header, combineFileDatas, file)
}

// 包装数据
function webpackDatas(fileDatas, header, fileName ) {
    const combineFileDatas =
        fileDatas.map(fileData => {
            return {
                'EXCEL_READ_UUID': uuidv4(),
                ...fileData,
            }
        })
    header[0].unshift('EXCEL_READ_UUID')
    return combineFileDatas
}

// 合并
function combineData(header, datas, file) {
    if (datas.length) {
        if (columnDatas.value.length &&
            !arraysEqual(columnDatas.value, header[0])) {
            ElMessage.error(`合并文件表头需一致,请检查文件[${ file.name }]!`)
            return
        }
        columnDatas.value = header[0]
        tableDatas.value = tableDatas.value.concat(datas)
    }
}

// 转化表头
function transferHeader(datas) {
    cfg.value = true
    tableTitleRes.value = Object.keys(datas[0]).map(data => {
        return {
            column: data,
            value: data
        }
    })
    tableTitleRes.value.shift()
}
// 生成文件
function generateExcel(titleDatas) {
    const res = tableDatas.value.map(data => {
        const obj = {}
        titleDatas.forEach(title => {
            const key = Object.keys(data).filter(key => key === title.column)[0]
            if (key) {
                obj[title.value] = data[key]
            }
        })
        return obj
    })
    exportFile(res)
}
// 导出excel文件
function exportFile(res) {
    const ws = utils.json_to_sheet(res);
    const wb = utils.book_new();
    utils.book_append_sheet(wb, ws, "Data");
    writeFileXLSX(wb, "导出数据.xlsx");
    tableRes.value = []
}
js 复制代码
<!--表头配置页面-->
<template>
    <el-dialog title="标题配置" :model-value="visible" @close="closeDialog(false)" class="header-cfg-dialog" :top="'10vh'">
        <el-form ref="cfgForm">
            <el-table :data="tableResults" border height="560">
                <template v-for="(column, idx) in tableColumns" :key="idx">
                    <el-table-column :label="column.name" :prop="column.id">
                        <template #header>
                            <span class="required-symbol" v-if="idx === 1">*</span>
                            <span>{{ column.name }}</span>
                        </template>
                        <template #default="scope">
                            <el-form-item :label="tableResults[scope.$index][column.id]" v-if="!idx"/>
                            <el-form-item :prop="tableResults[scope.$index][column.id]" v-else>
                                <el-input v-model="modelResults[scope.$index][column.id]"></el-input>
                            </el-form-item>
                        </template>
                    </el-table-column>
                </template>
            </el-table>
        </el-form>
    </el-dialog>
</template>

<script setup>
    const tableColumns = [
        { id: 'column', name: '列名' },
        { id: 'value', name: '替换名' }
    ]
    const props = defineProps({
        visible: {
            type: Boolean,
            default: false
        },
        tableResults: {
            type: Array,
            default: () => []
        }
    })

    const modelResults = computed(() => props.tableResults)
    const emit = defineEmits(['close'])
    function closeDialog(flag) {
        emit('close', flag, modelResults.value)
    }
</script>

一次帮同事的小忙,促使我做了这个小工具,从时间成本上来说,帮同事节省了时间。从自身来说,探索了一种新的处理excel文档的方法。有时问题就在那里,就看我们是否想用更有效率的方式去处理。

当然,该小工具还存在不够完善的地方,后续的思路是,完善读取前后的配置工作,比如是否生成特殊列,读取文件后,如何支持数据的过滤操作;针对明显错误的数据,能否高亮识别等。

相关推荐
EterNity_TiMe_11 分钟前
【论文复现】(CLIP)文本也能和图像配对
python·学习·算法·性能优化·数据分析·clip
星星会笑滴1 小时前
vue+node+Express+xlsx+emements-plus实现导入excel,并且将数据保存到数据库
vue.js·excel·express
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
临枫5411 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
RAY_CHEN.1 小时前
vue3 pinia 中actions修改状态不生效
vue.js·typescript·npm
酷酷的威朗普1 小时前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
_Legend_King1 小时前
vue3 + elementPlus 日期时间选择器禁用未来及过去时间
javascript·vue.js·elementui
景天科技苑2 小时前
【vue3+vite】新一代vue脚手架工具vite,助力前端开发更快捷更高效
前端·javascript·vue.js·vite·vue项目·脚手架工具
小行星1252 小时前
前端预览pdf文件流
前端·javascript·vue.js