做前端的经常碰到这种需求:用户哗啦一下传个 Excel 上来,你得在网页上给它弄个像模像样的预览?有时候还要编辑,还挺折腾人的。
我踩了不少坑,也试了市面上挺多库,今天就聊聊几个比较主流的选择,特别是最后那个,我个人是强推!
提起处理 Excel,xlsx 这库估计是绕不过去的。GitHub 上 35k 的 star,简直是元老级别的存在了。
npm install xlsx
<template>
<input type="file" @user1="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>
上面就是读个文件,拿到第一个 sheet 转成 JSON。很简单粗暴,对吧?
<template>
<div>
<input type="file" accept=".xlsx,.xls" @user2="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>
xlsx 这家伙吧,优点很明显: 轻、快!核心库体积不大,解析速度嗖嗖的,兼容性也不错,老格式新格式基本都能吃。社区也活跃,遇到问题谷歌一下大多有解。
但缺点也得说说: 它的 API 设计感觉有点…嗯…老派?或者说比较底层,不太直观。想拿到的数据结构,经常得自己再加工一道(就像上面 Demo 里那样)。而且,如果你想连着样式一起搞,比如单元格颜色、字体啥的,那 xlsx 就有点力不从心了,样式处理能力基本等于没有。
我个人觉得,如果你的需求就是简单读写数据,不关心样式,那 xlsx 绝对够用,效率杠杠的。但凡需求复杂一点,比如要高度还原 Excel 样式,或者处理复杂公式,那用它就有点 “小马拉大车” 的感觉了。
聊完基础款,我们来看个重量级的:Handsontable。这家伙最大的卖点,就是直接给你一个长得、用起来都跟 Excel 贼像的在线表格!
npm install handsontable
npm install @handsontable/vue3 # Vue3 专用包
别忘了还有 CSS:
import 'handsontable/dist/handsontable.full.css';
<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>
<template>
<div class="handsontable-container">
<h2>Handsontable 数据分析工具</h2>
<div class="toolbar">
<div class="filter-section">
<label>部门过滤:</label>
<select v-model="selectedDepartment" @user4="applyFilters">
<option value="all">所有部门</option>
<option value="销售">销售</option>
<option value="市场">市场</option>
<option value="技术">技术</option>
</select>
</div>
<div class="toolbar-actions">
<button @user5="addNewRow">添加员工</button>
<button @user6="saveData">保存数据</button>
<button @user7="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>
Handsontable 的牛逼之处: 界面无敌!
用户体验几乎无缝对接 Excel,什么排序、筛选、合并单元格、公式计算、右键菜单、拖拽调整行列,花里胡哨的功能一大堆。
定制性也强,事件钩子多得很。官方还贴心地提供了 Vue、React 这些框架的集成包。
但(总有个但是,对吧?):
贵! 商用许可不便宜,对不少项目来说是个门槛。虽然有非商用许可,但你懂的。
重! 功能全的代价就是体积大,加载可能慢一丢丢,尤其对性能敏感的页面。
大数据量有压力: 行列一多,性能可能会有点吃紧。
学习曲线: 配置项多如牛毛,想玩溜需要花点时间看文档。
我个人感觉,Handsontable 就像是你去了一家装修豪华、菜品精致的高档餐厅,体验一级棒,但结账时钱包会疼。
如果项目预算充足,而且用户强烈要求 “就要 Excel 那样的体验”,那它确实是王炸。
前面说了两个,一个轻快但简陋,一个豪华但贵重。
那有没有折中点的,功能强又免费的?
ExcelJS 登场!这家伙给我的感觉就是:现代化、全能型选手,而且 API 设计得相当舒服。
npm install exceljs
<template>
<input type="file" @user9="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>
<template>
<div>
<button @user10="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>
为啥我偏爱 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" @user11="e => detailedParse(e.target.files[0])" accept=".xlsx,.xls" />
<button @user12="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 文件(尤其是带样式预览)的场景,它就是那个最香的选择!
好了,就叨叨这么多,希望能帮到你!赶紧去试试吧!