亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

搞定 XLSX 預覽?別瞎找了,這幾個庫(尤其最后一個)真香!

標簽:
Html5
  • Hey, 我是 沉浸式趣谈

  • 本文首发于【沉浸式趣谈】,我的个人博客 https://yaolifeng.com 也同步更新。

  • 转载请在文章开头注明出处和版权信息。

  • 如果本文对您有所帮助,请 点赞评论转发,支持一下,谢谢!

  • 该平台创作会佛系一点,更多文章在我的个人博客上更新,欢迎访问我的个人博客。

做前端的经常碰到这种需求:用户哗啦一下传个 Excel 上来,你得在网页上给它弄个像模像样的预览?有时候还要编辑,还挺折腾人的。

我踩了不少坑,也试了市面上挺多库,今天就聊聊几个比较主流的选择,特别是最后那个,我个人是强推!

在线预览 Demo

第一个选手:老牌劲旅 xlsx

提起处理 Excel,xlsx 这库估计是绕不过去的。GitHub 上 35k 的 star,简直是元老级别的存在了。

安装?老规矩:


npm install xlsx

用起来嘛,也挺直接。看段代码感受下:


<template>

<input type="file" @change="readExcel" />

</template>

  

<script setup>

import { ref } from 'vue';

import * as XLSX from 'xlsx';

  

// 读取Excel文件

const readExcel  =  event  => {

const file  = event.target.files[0];

const reader  = new FileReader();

reader.onload  =  e  => {

const data  = new Uint8Array(e.target.result);

const workbook  = XLSX.read(data, { type: 'array' });

  

// 获取第一个工作表

const firstSheet  = workbook.Sheets[workbook.SheetNames[0]];

  

// 转换为JSON

const jsonData  = XLSX.utils.sheet_to_json(firstSheet);

console.log('喏,JSON 数据到手:', jsonData);

};

reader.readAsArrayBuffer(file);

};

</script>

alt text

上面就是读个文件,拿到第一个 sheet 转成 JSON。很简单粗暴,对吧?

搞个带文件选择器的预览 Demo 也不复杂:


<template>

<div>

<input type="file"  accept=".xlsx,.xls" @change="handleFile" />

  

<div v-if="data.length > 0"  style="overflow-x: auto; margin-top: 20px">

<table border="1"  cellPadding="5"  style="border-collapse: collapse">

<thead>

<tr>

<th v-for="(column, index) in columns" :key="index">

{{ column.title }}

</th>

</tr>

</thead>

<tbody>

<tr v-for="(row, rowIndex) in data" :key="rowIndex">

<td v-for="(column, colIndex) in columns" :key="colIndex">

{{ row[column.title] }}

</td>

</tr>

</tbody>

</table>

</div>

</div>

</template>

  

<script setup>

import { ref } from 'vue';

import * as XLSX from 'xlsx';

  

const data  = ref([]);

const columns  = ref([]);

  

const handleFile  = (e: Event) => {

const file  = (e.target as HTMLInputElement).files?.[0];

if (!file) return;

  

const reader  = new FileReader();

reader.onload  = (event) => {

try {

// 修改变量名避免与外部响应式变量冲突

const fileData  = new Uint8Array(event.target?.result as ArrayBuffer);

const workbook  = XLSX.read(fileData, { type: 'array' });

const worksheet  = workbook.Sheets[workbook.SheetNames[0]];

  

// 使用 header: 1 来获取原始数组格式

const jsonData  = XLSX.utils.sheet_to_json(worksheet, { header: 1 });

  

if (jsonData.length > 0) {

// 第一行作为列标题

columns.value  = jsonData[0] as string[];

// 其余行作为数据

data.value  = jsonData.slice(1);

console.log('数据已加载:', { 列数: columns.value.length, 行数: data.value.length });

}

} catch (error) {

console.error('Excel解析失败:', error);

alert('文件解析失败,请检查文件格式');

}

};

  

reader.readAsArrayBuffer(file);

};

</script>

alt text

xlsx 这家伙吧,优点很明显: 轻、快!核心库体积不大,解析速度嗖嗖的,兼容性也不错,老格式新格式基本都能吃。社区也活跃,遇到问题谷歌一下大多有解。

但缺点也得说说: 它的 API 设计感觉有点…嗯…老派?或者说比较底层,不太直观。想拿到的数据结构,经常得自己再加工一道(就像上面 Demo 里那样)。而且,如果你想连着样式一起搞,比如单元格颜色、字体啥的,那 xlsx 就有点力不从心了,样式处理能力基本等于没有。

我个人觉得,如果你的需求就是简单读写数据,不关心样式,那 xlsx 绝对够用,效率杠杠的。但凡需求复杂一点,比如要高度还原 Excel 样式,或者处理复杂公式,那用它就有点“小马拉大车”的感觉了。

第二个选手:重量级嘉宾 Handsontable

聊完基础款,我们来看个重量级的:Handsontable。这家伙最大的卖点,就是直接给你一个长得、用起来都跟 Excel 贼像的在线表格!

安装要多装个 Vue 的适配包:


npm install handsontable

npm install @handsontable/vue3 # Vue3 专用包

别忘了还有 CSS:


import 'handsontable/dist/handsontable.full.css';

基础用法,它是在一个 DOM 容器里初始化:


<template>

<div id="excel-preview"></div>

</template>

  

<script setup>

import { onMounted } from 'vue';

import Handsontable from 'handsontable';

import 'handsontable/dist/handsontable.full.css';

  

onMounted(() => {

// 初始化表格

const container  = document.getElementById('excel-preview');

const hot  = new Handsontable(container, {

data: [

['姓名', '年龄', '城市'],

['张三', 28, '北京'],

['李四', 32, '上海'],

['王五', 25, '广州'],

],

rowHeaders: true,

colHeaders: true,

contextMenu: true,

licenseKey: 'non-commercial-and-evaluation', // 注意:商用要钱!这很关键!

});

});

</script>

alt text

搞个可编辑的 Demo 看看?这才是它的强项:


<template>

<div class="handsontable-container">

<h2>Handsontable 数据分析工具</h2>

  

<div class="toolbar">

<div class="filter-section">

<label>部门过滤:</label>

<select v-model="selectedDepartment" @change="applyFilters">

<option value="all">所有部门</option>

<option value="销售">销售</option>

<option value="市场">市场</option>

<option value="技术">技术</option>

</select>

</div>

  

<div class="toolbar-actions">

<button @click="addNewRow">添加员工</button>

<button @click="saveData">保存数据</button>

<button @click="exportToExcel">导出Excel</button>

</div>

</div>

  

<hot-table

ref="hotTableRef"

:data="filteredData"

:colHeaders="headers"

:rowHeaders="true"

:width="'100%'"

:height="500"

:contextMenu="contextMenuOptions"

:columns="columnDefinitions"

:nestedHeaders="nestedHeaders"

:manualColumnResize="true"

:manualRowResize="true"

:colWidths="colWidths"

:beforeChange="beforeChangeHandler"

:afterChange="afterChangeHandler"

:cells="cellsRenderer"

licenseKey="non-commercial-and-evaluation"

></hot-table>

  

<div class="summary-section">

<h3>数据统计</h3>

<div class="summary-items">

<div class="summary-item"> <strong>员工总数:</strong> {{ totalEmployees }} </div>

<div class="summary-item"> <strong>平均绩效分:</strong> {{ averagePerformance }} </div>

<div class="summary-item"> <strong>总薪资支出:</strong> {{ totalSalary }} </div>

</div>

</div>

</div>

</template>

  

<script setup lang="ts">

import { ref, computed, onMounted } from 'vue';

import { HotTable } from '@handsontable/vue3';

import { registerAllModules } from 'handsontable/registry';

import 'handsontable/dist/handsontable.full.css';

import * as XLSX from 'xlsx';

import Handsontable from 'handsontable';

  

// 注册所有模块

registerAllModules();

  

// 表头定义

const headers  = ['ID', '姓名', '部门', '职位', '入职日期', '薪资', '绩效评分', '状态'];

  

// 嵌套表头

const nestedHeaders  = [['员工基本信息', '', '', '', '员工绩效数据', '', '', ''], headers];

  

// 列宽设置

const colWidths  = [60, 100, 100, 120, 120, 100, 100, 120];

  

// 列定义

const columnDefinitions  = [

{ data: 'id', type: 'numeric', readOnly: true },

{ data: 'name', type: 'text' },

{

data: 'department',

type: 'dropdown',

source: ['销售', '市场', '技术', '人事', '财务'],

},

{ data: 'position', type: 'text' },

{

data: 'joinDate',

type: 'date',

dateFormat: 'YYYY-MM-DD',

correctFormat: true,

},

{

data: 'salary',

type: 'numeric',

numericFormat: {

pattern: '¥ 0,0.00',

culture: 'zh-CN',

},

},

{

data: 'performance',

type: 'numeric',

numericFormat: {

pattern: '0.0',

},

},

{

data: 'status',

type: 'dropdown',

source: ['在职', '离职', '休假'],

},

];

  

// 右键菜单选项

const contextMenuOptions  = {

items: {

row_above: { name: '上方插入行' },

row_below: { name: '下方插入行' },

remove_row: { name: '删除行' },

separator1: Handsontable.plugins.ContextMenu.SEPARATOR,

copy: { name: '复制' },

cut: { name: '剪切' },

separator2: Handsontable.plugins.ContextMenu.SEPARATOR,

columns_resize: { name: '调整列宽' },

alignment: { name: '对齐' },

},

};

  

// 初始数据

const initialData  = [

{

id: 1,

name: '张三',

department: '销售',

position: '销售经理',

joinDate: '2022-01-15',

salary: 15000,

performance: 4.5,

status: '在职',

},

{

id: 2,

name: '李四',

department: '技术',

position: '高级开发',

joinDate: '2021-05-20',

salary: 18000,

performance: 4.7,

status: '在职',

},

{

id: 3,

name: '王五',

department: '市场',

position: '市场专员',

joinDate: '2022-03-10',

salary: 12000,

performance: 3.8,

status: '在职',

},

{

id: 4,

name: '赵六',

department: '技术',

position: '开发工程师',

joinDate: '2020-11-05',

salary: 16500,

performance: 4.2,

status: '在职',

},

{

id: 5,

name: '钱七',

department: '销售',

position: '销售代表',

joinDate: '2022-07-18',

salary: 10000,

performance: 3.5,

status: '休假',

},

{

id: 6,

name: '孙八',

department: '市场',

position: '市场总监',

joinDate: '2019-02-28',

salary: 25000,

performance: 4.8,

status: '在职',

},

{

id: 7,

name: '周九',

department: '技术',

position: '测试工程师',

joinDate: '2021-09-15',

salary: 14000,

performance: 4.0,

status: '在职',

},

{

id: 8,

name: '吴十',

department: '销售',

position: '销售代表',

joinDate: '2022-04-01',

salary: 11000,

performance: 3.6,

status: '离职',

},

];

  

// 表格引用

const hotTableRef  = ref(null);

const data  = ref([...initialData]);

const selectedDepartment  = ref('all');

  

// 过滤后的数据

const filteredData  = computed(() => {

if (selectedDepartment.value  === 'all') {

return data.value;

}

return data.value.filter(item  => item.department  === selectedDepartment.value);

});

  

// 数据统计

const totalEmployees  = computed(() => data.value.filter(emp  => emp.status  === '在职' || emp.status  === '休假').length);

  

const averagePerformance  = computed(() => {

const activeEmployees  = data.value.filter(emp  => emp.status  === '在职');

if (activeEmployees.length  === 0) return 0;

  

const sum  = activeEmployees.reduce((acc, emp) => acc + emp.performance, 0);

return (sum / activeEmployees.length).toFixed(1);

});

  

const totalSalary  = computed(() => {

const activeEmployees  = data.value.filter(emp  => emp.status  === '在职' || emp.status  === '休假');

const sum  = activeEmployees.reduce((acc, emp) => acc + emp.salary, 0);

return `¥ ${sum.toLocaleString('zh-CN')}`;

});

  

// 单元格渲染器 - 条件格式

const cellsRenderer  = (row, col, prop) => {

const cellProperties  = {};

  

// 绩效评分条件格式

if (prop  === 'performance') {

const value  = filteredData.value[row]?.performance;

  

if (value >= 4.5) {

cellProperties.className  =  'bg-green';

} else if (value >= 4.0) {

cellProperties.className  =  'bg-light-green';

} else if (value < 3.5) {

cellProperties.className  =  'bg-red';

}

}

  

// 状态条件格式

if (prop  === 'status') {

const status  = filteredData.value[row]?.status;

  

if (status  === '在职') {

cellProperties.className  =  'status-active';

} else if (status  === '离职') {

cellProperties.className  =  'status-inactive';

} else if (status  === '休假') {

cellProperties.className  =  'status-vacation';

}

}

  

return cellProperties;

};

  

// 数据验证

const beforeChangeHandler  = (changes, source) => {

if (source  === 'edit') {

for (let i  = 0; i < changes.length; i++) {

const [row, prop, oldValue, newValue] = changes[i];

  

// 薪资验证:不能小于0

if (prop  === 'salary' && newValue < 0) {

changes[i][3] = oldValue;

}

  

// 绩效验证:范围1-5

if (prop  === 'performance') {

if (newValue < 1) changes[i][3] = 1;

if (newValue > 5) changes[i][3] = 5;

}

}

}

return true;

};

  

// 在数据更改后的处理

const afterChangeHandler  = (changes, source) => {

if (!changes) return;

  

setTimeout(() => {

if (hotTableRef.value?.hotInstance) {

hotTableRef.value.hotInstance.render();

}

}, 0);

};

  

// 应用过滤器

const applyFilters  = () => {

if (hotTableRef.value?.hotInstance) {

hotTableRef.value.hotInstance.render();

}

};

  

// 添加新行

const addNewRow  = () => {

const newId  = Math.max(...data.value.map(item  => item.id), 0) + 1;

data.value.push({

id: newId,

name: '',

department: '',

position: '',

joinDate: new Date().toISOString().split('T')[0],

salary: 0,

performance: 3.0,

status: '在职',

});

  

if (hotTableRef.value?.hotInstance) {

setTimeout(() => {

hotTableRef.value.hotInstance.render();

}, 0);

}

};

  

// 保存数据

const saveData  = () => {

// 这里可以添加API保存逻辑

alert('数据已保存');

};

  

// 导出为Excel

const exportToExcel  = () => {

const currentData  = data.value;

const ws  = XLSX.utils.json_to_sheet(currentData);

const wb  = XLSX.utils.book_new();

XLSX.utils.book_append_sheet(wb, ws, '员工数据');

XLSX.writeFile(wb, '员工数据报表.xlsx');

};

  

// 确保组件挂载后正确渲染

onMounted(() => {

setTimeout(() => {

if (hotTableRef.value?.hotInstance) {

hotTableRef.value.hotInstance.render();

}

}, 100);

});

</script>

  

<style>

.handsontable-container {

padding: 20px;

font-family: Arial, sans-serif;

}

  

.toolbar {

display: flex;

justify-content: space-between;

margin-bottom: 20px;

align-items: center;

}

  

.filter-section {

display: flex;

align-items: center;

gap: 10px;

}

  

.toolbar-actions {

display: flex;

gap: 10px;

}

  

button {

padding: 8px 16px;

background-color: #4285f4;

color: white;

border: none;

border-radius: 4px;

cursor: pointer;

font-size: 14px;

transition: background-color 0.3s;

}

  

button:hover {

background-color: #3367d6;

}

  

select {

padding: 6px;

border-radius: 4px;

border: 1px solid #ccc;

}

  

.summary-section {

margin-top: 20px;

padding: 15px;

background-color: #f9f9f9;

border-radius: 6px;

}

  

.summary-items {

display: flex;

gap: 30px;

margin-top: 10px;

}

  

.summary-item {

padding: 10px;

background-color: white;

border-radius: 4px;

box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

}

  

/* 条件格式样式 */

.bg-green {

background-color: rgba(76, 175, 80, 0.3) !important;

}

  

.bg-light-green {

background-color: rgba(139, 195, 74, 0.2) !important;

}

  

.bg-red {

background-color: rgba(244, 67, 54, 0.2) !important;

}

  

.status-active {

font-weight: bold;

color: #2e7d32;

}

  

.status-inactive {

font-weight: bold;

color: #d32f2f;

}

  

.status-vacation {

font-weight: bold;

color: #f57c00;

}

</style>

alt text

Handsontable 的牛逼之处: 界面无敌!

用户体验几乎无缝对接 Excel,什么排序、筛选、合并单元格、公式计算、右键菜单、拖拽调整行列,花里胡哨的功能一大堆。

定制性也强,事件钩子多得很。官方还贴心地提供了 Vue、React 这些框架的集成包。

但(总有个但是,对吧?):

贵! 商用许可不便宜,对不少项目来说是个门槛。虽然有非商用许可,但你懂的。

重! 功能全的代价就是体积大,加载可能慢一丢丢,尤其对性能敏感的页面。

大数据量有压力: 行列一多,性能可能会有点吃紧。

学习曲线: 配置项多如牛毛,想玩溜需要花点时间看文档。

我个人感觉,Handsontable 就像是你去了一家装修豪华、菜品精致的高档餐厅,体验一级棒,但结账时钱包会疼。

如果项目预算充足,而且用户强烈要求“就要 Excel 那样的体验”,那它确实是王炸。

压轴出场:我的心头好 ExcelJS

前面说了两个,一个轻快但简陋,一个豪华但贵重。

那有没有折中点的,功能强又免费的?

ExcelJS 登场!这家伙给我的感觉就是:现代化、全能型选手,而且 API 设计得相当舒服。

老规矩,安装


npm install exceljs

基本用法,注意它用了 async/await,很现代:


<template>

<input type="file" @change="readExcel" />

</template>

  

<script setup>

import { ref } from 'vue';

import ExcelJS from 'exceljs';

  

const readExcel  = async event  => {

const file  = event.target.files[0];

if (!file) return;

  

// 最好加个 try...catch

try {

const workbook  = new ExcelJS.Workbook();

const arrayBuffer  = await file.arrayBuffer(); // 直接读 ArrayBuffer,省事儿

await workbook.xlsx.load(arrayBuffer);

  

const worksheet  = workbook.getWorksheet(1); // 获取第一个 worksheet

const data  = [];

  

worksheet.eachRow((row, rowNumber) => {

const rowData  = [];

row.eachCell((cell, colNumber) => {

rowData.push(cell.value);

});

// 它的 API 遍历起来就挺顺手

data.push(rowData);

});

  

console.log(data);

return data; // 返回解析好的数据

} catch (error) {

console.error('用 ExcelJS 解析失败了,检查下文件?', error);

alert('文件好像有点问题,解析不了哦');

}

};

</script>

alt text

来个带劲的 Demo:把 Excel 样式也给你扒下来!


<template>

<div>

<button @click="exportAdvancedExcel">导出进阶Excel</button>

</div>

</template>

  

<script setup lang="ts">

import ExcelJS from 'exceljs';

  

// 高级数据类型

interface AdvancedData {

id: number;

name: string;

department: string;

salary: number;

joinDate: Date;

performance: number;

}

  

// 生成示例数据

const generateData  = () => {

const data: AdvancedData[] = [];

for (let i  = 1; i <= 5; i++) {

data.push({

id: i,

name: `员工${i}`,

department: ['技术部', '市场部', '财务部'][i % 3],

salary: 10000 + i * 1000,

joinDate: new Date(2020 + i, i % 12, i),

performance: Math.random() * 100,

});

}

return data;

};

  

const exportAdvancedExcel  = async () => {

const workbook  = new ExcelJS.Workbook();

const worksheet  = workbook.addWorksheet('员工报表');

  

// 设置文档属性

workbook.creator  =  '企业管理系统';

workbook.lastModifiedBy  =  '管理员';

workbook.created  = new Date();

  

// 设置页面布局

worksheet.pageSetup  = {

orientation: 'landscape',

margins: { left: 0.7, right: 0.7, top: 0.75, bottom: 0.75 },

};

  

// 创建自定义样式

const headerStyle  = {

font: { bold: true, color: { argb: 'FFFFFFFF' } },

fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4F81BD' } },

border: {

top: { style: 'thin' },

left: { style: 'thin' },

bottom: { style: 'thin' },

right: { style: 'thin' },

},

alignment: { vertical: 'middle', horizontal: 'center' },

};

  

const moneyFormat  =  '"¥"#,##0.00';

const dateFormat  =  'yyyy-mm-dd';

const percentFormat  =  '0.00%';

  

// 合并标题行

worksheet.mergeCells('A1:F1');

const titleCell  = worksheet.getCell('A1');

titleCell.value  =  '2023年度员工数据报表';

titleCell.style  = {

font: { size: 18, bold: true, color: { argb: 'FF2E75B5' } },

alignment: { vertical: 'middle', horizontal: 'center' },

};

  

// 设置列定义

worksheet.columns  = [

{ header: '工号', key: 'id', width: 10 },

{ header: '姓名', key: 'name', width: 15 },

{ header: '部门', key: 'department', width: 15 },

{

header: '薪资',

key: 'salary',

width: 15,

style: { numFmt: moneyFormat },

},

{

header: '入职日期',

key: 'joinDate',

width: 15,

style: { numFmt: dateFormat },

},

{

header: '绩效',

key: 'performance',

width: 15,

style: { numFmt: percentFormat },

},

];

  

// 应用表头样式

worksheet.getRow(2).eachCell(cell  => {

cell.style  = headerStyle;

});

  

// 添加数据

const data  = generateData();

worksheet.addRows(data);

  

// 添加公式行

const totalRow  = worksheet.addRow({

id: '总计',

salary: { formula: 'SUM(D3:D7)' },

performance: { formula: 'AVERAGE(F3:F7)' },

});

  

// 设置总计行样式

totalRow.eachCell(cell  => {

cell.style  = {

font: { bold: true },

fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFCE4D6' } },

};

});

  

// 添加条件格式

worksheet.addConditionalFormatting({

ref: 'F3:F7',

rules: [

{

type: 'cellIs',

operator: 'greaterThan',

formulae: [0.8],

style: { fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC6EFCE' } } },

},

],

});

  

// 生成Blob并下载

const buffer  = await workbook.xlsx.writeBuffer();

const blob  = new Blob([buffer], {

type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',

});

  

// 使用原生API下载

const url  = URL.createObjectURL(blob);

const link  = document.createElement('a');

link.href  = url;

link.download  =  '员工报表.xlsx';

document.body.appendChild(link);

link.click();

document.body.removeChild(link);

URL.revokeObjectURL(url);

};

</script>

alt text

为啥我偏爱 ExcelJS?

API 友好: Promise 风格,链式调用,写起来舒服,代码也更易读。感觉就是为现代 JS 开发设计的。

功能全面: 不仅仅是读写数据,样式、公式、合并单元格、图片、表单控件… 它支持的 Excel 特性相当多。特别是读取和修改样式,这对于需要“还原”Excel 样貌的场景太重要了!

免费开源! 这点太香了,没有商业使用的后顾之忧。

文档清晰: 官方文档写得挺明白,示例也足。

当然,没啥是完美的:

体积比 xlsx 大点: 但功能也强得多嘛,可以接受。

复杂公式支持可能有限: 极其复杂的嵌套公式或者宏,可能还是搞不定(不过大部分场景够用了)。

超大文件性能: 几十上百兆的 Excel,解析起来可能会慢,或者内存占用高点(老实说,哪个库处理这种文件不头疼呢)。

我之前用 xlsx 时,老是要自己写一堆转换逻辑,数据结构处理起来烦得很。换了 ExcelJS 后,感觉世界清净了不少。尤其是它能把单元格的背景色、字体、边框这些信息都读出来,这对做预览太有用了!

实战中怎么选?或者…全都要?

其实吧,这三个库也不是非得“你死我活”。在真实项目中,完全可以根据情况搭配使用:

简单快速的导入导出: 用户上传个模板,或者导出一份简单数据,用 xlsx 就行,轻快好省。

需要精确保留样式或复杂解析: 用户传了个带格式的报表,你想尽可能还原预览,那 ExcelJS 就是主力。

需要在线编辑、强交互: 如果你做的不是预览,而是个在线的类 Excel 编辑器,那砸钱上 Handsontable 可能是最接近目标的(如果预算允许的话)。

我甚至见过有项目是这样搞的:先用 xlsx 快速读取基本数据和 Sheet 名称做个“秒开”预览,然后后台或者异步再用 ExcelJS 做详细的、带样式的解析。

这样既快,又能保证最终效果。

下面这个(伪)代码片段,大概是这个思路:


<template>

<div class="excel-viewer">

<div class="controls">

<input type="file" @change="e => detailedParse(e.target.files[0])"  accept=".xlsx,.xls" />

<button @click="exportToExcel">导出Excel</button>

</div>

  

<div v-if="isLoading">加载中...</div>

  

<template v-else>

<div v-if="sheetNames.length > 0"  class="sheet-tabs">

<button

v-for="(name, index) in sheetNames"

:key="index"

:class="{ active: activeSheet === index }"

@click="handleSheetChange(index)"

>

{{ name }}

</button>

</div>

  

<hot-table

v-if="data.length > 0"

ref="hotTableRef"

:data="data"

:rowHeaders="true"

:colHeaders="true"

:width="'100%'"

:height="400"

licenseKey="non-commercial-and-evaluation"

></hot-table>

</template>

</div>

</template>

  

<script setup>

import { ref } from 'vue';

import * as XLSX from 'xlsx'; // 用于快速预览 & 导出

import ExcelJS from 'exceljs'; // 用于详细解析

import { HotTable } from '@handsontable/vue3'; // 用于展示 & 编辑

import 'handsontable/dist/handsontable.full.css';

  

const data  = ref([]);

const isLoading  = ref(false);

const sheetNames  = ref([]);

const activeSheet  = ref(0);

const hotTableRef  = ref(null);

  

// 快速预览(可选,或者直接用 detailedParse)

const quickPreview  =  file  => {

isLoading.value  = true;

const reader  = new FileReader();

reader.onload  =  e  => {

try {

const data  = new Uint8Array(e.target.result);

const workbook  = XLSX.read(data, { type: 'array' });

sheetNames.value  = workbook.SheetNames;

  

const firstSheet  = workbook.Sheets[workbook.SheetNames[0]];

const jsonData  = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });

data.value  = jsonData;

activeSheet.value  = 0;

} catch (error) {

console.error('预览失败:', error);

alert('文件预览失败');

} finally {

isLoading.value  = false;

}

};

reader.readAsArrayBuffer(file);

};

  

// 使用ExcelJS详细解析

const detailedParse  = async file  => {

isLoading.value  = true;

try {

const workbook  = new ExcelJS.Workbook();

const arrayBuffer  = await file.arrayBuffer();

await workbook.xlsx.load(arrayBuffer);

  

// 也许这里还可以把 ExcelJS 解析到的样式信息存起来,以后可能用得到

// 比如,导出时尝试用 ExcelJS 写回样式?那就更高级了

const names  = workbook.worksheets.map(sheet  => sheet.name);

sheetNames.value  = names;

  

// 解析第一个 sheet

parseWorksheet(workbook.worksheets[0]);

activeSheet.value  = 0;

} catch (error) {

console.error('解析失败:', error);

alert('文件解析失败');

} finally {

isLoading.value  = false;

}

};

  

// 解析某个 worksheet 并更新 Handsontable 数据

const parseWorksheet  =  worksheet  => {

const sheetData  = [];

worksheet.eachRow((row, rowNumber) => {

const rowData  = [];

row.eachCell((cell, colNumber) => {

let value  = cell.value;

// 处理日期等特殊类型

if (value instanceof Date) {

value  = value.toLocaleDateString();

}

rowData.push(value);

});

sheetData.push(rowData);

});

// 这里的 data 结构要适配 Handsontable,通常是二维数组

data.value  = sheetData;

};

  

// 切换 Sheet (需要重新调用 parseWorksheet)

const handleSheetChange  = async index  => {

activeSheet.value  = index;

// 重新加载并解析对应 Sheet 的数据... 这需要保存 workbook 实例

// 或者在 detailedParse 时就把所有 sheet 数据都解析缓存起来?看内存消耗

};

  

// 导出 (简单起见,用 xlsx 快速导出当前 Handsontable 的数据)

const exportToExcel  = () => {

const ws  = XLSX.utils.aoa_to_sheet(data.value);

const wb  = XLSX.utils.book_new();

XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');

XLSX.writeFile(wb, '导出数据.xlsx');

// 如果想导出带样式的,那得用 ExcelJS 来写,会复杂不少

};

</script>

  

<style scoped>

.excel-viewer {

margin: 20px;

}

.controls {

margin-bottom: 15px;

}

.sheet-tabs {

display: flex;

margin-bottom: 10px;

}

.sheet-tabs button {

padding: 5px 10px;

margin-right: 5px;

border: 1px solid #ccc;

background: #f5f5f5;

cursor: pointer;

}

.sheet-tabs button.active {

background: #e0e0e0;

border-bottom: 2px solid #1890ff;

}

</style>

总结一下我的个人看法:

折腾下来,这几个库真是各有千秋:

xlsx (SheetJS): 老司机,适合追求极致性能和体积的简单场景。代码写得少,跑得快,但不怎么讲究“内饰”(样式)。

Handsontable: 豪华座驾,提供近乎完美的 Excel 编辑体验。功能强大没得说,但得看你口袋里的银子够不够。

ExcelJS: 可靠的全能伙伴。API 现代,功能均衡,对样式支持好,关键还免费!能帮你解决绝大多数问题。

说真的,没有银弹。选哪个,最终还是看你的具体需求和项目限制。

但如果非要我推荐一个,我绝对站 ExcelJS。

在功能、易用性和成本(免费!)之间,它平衡得太好了。

对于大部分需要精细处理 Excel 文件(尤其是带样式预览)的场景,它就是那个最香的选择!

好了,就叨叨这么多,希望能帮到你!赶紧去试试吧!

其他好文推荐

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
Web前端工程師
手記
粉絲
6
獲贊與收藏
41

關注作者,訂閱最新文章

閱讀免費教程

  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消