手动合并多张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文档的方法。有时问题就在那里,就看我们是否想用更有效率的方式去处理。

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

相关推荐
hdsoft_huge4 小时前
VUE npm ERR! code ERESOLVE, npm ERR! ERESOLVE could not resolve, 错误有效解决
vue.js·rust·npm
agenIT5 小时前
vue3 getcurrentinstance 用法
javascript·vue.js·ecmascript
代码老y5 小时前
基于springboot的校园商铺管理系统的设计与实现
java·vue.js·spring boot·后端·毕业设计·课程设计·个人开发
码农捻旧5 小时前
JavaScript 性能优化按层次逐步分析
开发语言·前端·javascript·性能优化
小辉懂编程5 小时前
2025年最新基于Vue基础项目Todolist任务编辑器【适合新手入手】【有这一片足够了】【附源码】
前端·vue.js·编辑器
我是哈哈hh5 小时前
【Vue3】生命周期 & hook函数 & toRef
开发语言·前端·javascript·vue.js·前端框架·生命周期·proxy模式
Gazer_S7 小时前
【Vue Vapor Mode :技术突破与性能优化的可能性】
vue.js·性能优化
前端_学习之路8 小时前
React与Vue的内置指令对比
前端·vue.js·react.js
会飞的鱼先生8 小时前
vue3自定义指令来实现 v-focus 功能
前端·javascript·vue.js
_abab8 小时前
Nginx 性能优化全解析:从进程到安全的深度实践
nginx·安全·性能优化