Vue预览Excel文件的完整指南:从零开始实现
Vue预览Excel文件的完整指南:从零开始实现
大家好,我是你们的前端老司机。今天我们来聊聊一个让无数前端开发者头疼的问题——Vue中如何预览Excel文件。
你是否也遇到过这些场景:
- 产品经理说:"用户上传Excel文件后,要在页面上直接预览,不要下载"
- 用户抱怨:"我上传的Excel文件怎么看不到内容?"
- 后端同事问:"前端能不能直接展示Excel,我返回二进制流就行"
- 老板质疑:"为什么别人家的系统能预览Excel,我们的不行?"
别急,今天我就把这套Vue预览Excel文件的完整实现方案全掏出来,手把手教你从零开始实现Excel文件预览功能!
为什么Excel预览这么难搞?
在开始正题之前,先聊聊为什么Excel预览这么复杂:
- 格式多样:.xls、.xlsx、.csv等多种格式
- 功能复杂:合并单元格、公式计算、样式渲染
- 兼容性差:不同版本的Excel文件格式差异大
- 性能要求高:大文件预览不能卡顿
- 浏览器限制:原生不支持Excel格式解析
实现方案对比
方案一:使用第三方库(推荐)
优点:
- 功能强大,支持多种Excel特性
- 社区活跃,文档完善
- 开箱即用,开发效率高
缺点:
- 包体积较大
- 需要学习成本
方案二:服务端转换
优点:
- 前端实现简单
- 兼容性好
缺点:
- 增加服务端压力
- 需要网络传输
- 实时性差
方案三:纯前端实现
优点:
- 无服务端依赖
- 响应速度快
缺点:
- 实现复杂
- 功能有限
今天我们就重点介绍方案一:使用第三方库的实现方式。
核心实现:基于xlsx.js的Excel预览组件
1. 安装依赖
npm install xlsx
# 如果需要公式计算功能
npm install hot-formula-parser
2. 核心组件实现
<template>
<div class="excel-preview-container">
<!-- 文件上传区域 -->
<div v-if="!fileData" class="upload-area">
<el-upload
class="upload-demo"
drag
action=""
:http-request="handleFileUpload"
:auto-upload="true"
accept=".xls,.xlsx,.csv"
>
<el-icon class="el-icon--upload">
<upload-filled />
</el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传 xls/xlsx/csv 文件,且不超过 10MB
</div>
</template>
</el-upload>
</div>
<!-- Excel预览区域 -->
<div v-else class="preview-area">
<!-- 工具栏 -->
<div class="toolbar">
<el-button @click="resetPreview">重新选择</el-button>
<el-checkbox
v-model="showFormulas"
@change="refreshPreview"
>
显示公式
</el-checkbox>
<el-select
v-model="currentSheet"
@change="switchSheet"
placeholder="选择工作表"
>
<el-option
v-for="sheet in sheetNames"
:key="sheet"
:label="sheet"
:value="sheet"
/>
</el-select>
</div>
<!-- 表格预览 -->
<div class="table-container" ref="tableContainer">
<table class="excel-table" v-if="tableData.length > 0">
<tbody>
<tr v-for="(row, rowIndex) in tableData" :key="rowIndex">
<template v-for="(cell, colIndex) in row" :key="colIndex">
<td
v-if="!isCellMerged(rowIndex, colIndex)"
:colspan="getColspan(rowIndex, colIndex)"
:rowspan="getRowspan(rowIndex, colIndex)"
:class="getCellClass(rowIndex, colIndex, cell)"
>
<div class="cell-content">
<div
v-if="cellFormulas[`${rowIndex},${colIndex}`] && showFormulas"
class="formula-display"
>
<span class="formula-icon">ƒ</span>
<span class="formula-text">
{{ cellFormulas[`${rowIndex},${colIndex}`] }}
</span>
</div>
<span v-else>
{{ formatCellValue(cell, rowIndex, colIndex) }}
</span>
</div>
</td>
</template>
</tr>
</tbody>
</table>
<!-- 空数据提示 -->
<div v-else class="empty-data">
<el-empty description="暂无数据" />
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<el-spinner />
<p>正在解析文件...</p>
</div>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import { Parser } from 'hot-formula-parser';
export default {
name: 'ExcelPreview',
props: {
// 支持传入文件对象或ArrayBuffer
file: {
type: [File, ArrayBuffer, Blob],
default: null
},
// 是否显示公式
showFormulas: {
type: Boolean,
default: false
}
},
data() {
return {
fileData: null, // 文件数据
tableData: [], // 表格数据
sheetNames: [], // 工作表名称列表
currentSheet: '', // 当前工作表
mergedCells: {}, // 合并单元格信息
cellFormulas: {}, // 单元格公式
cellFormats: {}, // 单元格格式
loading: false, // 加载状态
workbook: null // 工作簿对象
};
},
watch: {
// 监听外部传入的文件
file: {
immediate: true,
handler(newFile) {
if (newFile) {
this.processFile(newFile);
}
}
},
// 监听显示公式选项变化
showFormulas() {
this.refreshPreview();
}
},
methods: {
// 处理文件上传
async handleFileUpload({ file }) {
try {
this.loading = true;
await this.processFile(file);
this.$emit('file-loaded', file);
} catch (error) {
this.$message.error('文件解析失败:' + error.message);
} finally {
this.loading = false;
}
},
// 处理文件数据
async processFile(file) {
try {
let arrayBuffer;
// 根据文件类型处理
if (file instanceof ArrayBuffer) {
arrayBuffer = file;
} else if (file instanceof Blob) {
arrayBuffer = await this.blobToArrayBuffer(file);
} else {
// File对象
arrayBuffer = await this.fileToArrayBuffer(file);
}
// 解析Excel文件
this.parseExcelFile(arrayBuffer);
} catch (error) {
throw new Error('文件处理失败:' + error.message);
}
},
// File转ArrayBuffer
fileToArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = event => resolve(event.target.result);
reader.onerror = error => reject(error);
reader.readAsArrayBuffer(file);
});
},
// Blob转ArrayBuffer
blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = event => resolve(event.target.result);
reader.onerror = error => reject(error);
reader.readAsArrayBuffer(blob);
});
},
// 解析Excel文件
parseExcelFile(arrayBuffer) {
try {
// 读取工作簿
const workbook = XLSX.read(arrayBuffer, {
type: 'array',
cellFormula: true, // 读取公式
cellHTML: false, // 不读取HTML
cellDates: true, // 日期格式化
sheetStubs: true, // 读取空单元格
WTF: false // 不显示警告
});
this.workbook = workbook;
this.sheetNames = workbook.SheetNames;
// 默认显示第一个工作表
if (this.sheetNames.length > 0) {
this.currentSheet = this.sheetNames[0];
this.renderSheet(this.currentSheet);
}
this.fileData = arrayBuffer;
} catch (error) {
throw new Error('Excel文件解析失败:' + error.message);
}
},
// 渲染工作表
renderSheet(sheetName) {
try {
const worksheet = this.workbook.Sheets[sheetName];
if (!worksheet) {
throw new Error('工作表不存在');
}
// 获取工作表范围
const range = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } };
// 解析合并单元格
this.parseMergedCells(worksheet);
// 解析公式
this.parseFormulas(worksheet);
// 解析单元格格式
this.parseCellFormats(worksheet);
// 转换为表格数据
this.convertToTableData(worksheet, range);
} catch (error) {
this.$message.error('工作表渲染失败:' + error.message);
}
},
// 解析合并单元格
parseMergedCells(worksheet) {
this.mergedCells = {};
if (worksheet['!merges']) {
worksheet['!merges'].forEach(merge => {
const startRow = merge.s.r;
const startCol = merge.s.c;
const endRow = merge.e.r;
const endCol = merge.e.c;
// 记录合并单元格的起始位置和跨度
this.mergedCells[`${startRow},${startCol}`] = {
rowspan: endRow - startRow + 1,
colspan: endCol - startCol + 1
};
// 标记被合并的单元格
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
if (r !== startRow || c !== startCol) {
this.mergedCells[`${r},${c}`] = { hidden: true };
}
}
}
});
}
},
// 解析公式
parseFormulas(worksheet) {
this.cellFormulas = {};
// 遍历所有单元格
for (const cellRef in worksheet) {
if (cellRef[0] === '!') continue; // 跳过特殊属性
const cell = worksheet[cellRef];
if (cell && cell.f) { // 有公式
const { r: row, c: col } = XLSX.utils.decode_cell(cellRef);
this.cellFormulas[`${row},${col}`] = cell.f;
}
}
},
// 解析单元格格式
parseCellFormats(worksheet) {
this.cellFormats = {};
for (const cellRef in worksheet) {
if (cellRef[0] === '!') continue;
const cell = worksheet[cellRef];
if (cell && cell.z) { // 有格式
const { r: row, c: col } = XLSX.utils.decode_cell(cellRef);
this.cellFormats[`${row},${col}`] = cell.z;
}
}
},
// 转换为表格数据
convertToTableData(worksheet, range) {
const data = [];
// 遍历行
for (let r = range.s.r; r <= range.e.r; r++) {
const row = [];
// 遍历列
for (let c = range.s.c; c <= range.e.c; c++) {
const cellRef = XLSX.utils.encode_cell({ r, c });
const cell = worksheet[cellRef];
if (cell && cell.v !== undefined) {
row.push(cell.v);
} else {
row.push('');
}
}
data.push(row);
}
this.tableData = data;
},
// 判断是否为合并单元格
isCellMerged(row, col) {
const key = `${row},${col}`;
return this.mergedCells[key] && this.mergedCells[key].hidden;
},
// 获取colspan
getColspan(row, col) {
const key = `${row},${col}`;
return this.mergedCells[key] ? this.mergedCells[key].colspan || 1 : 1;
},
// 获取rowspan
getRowspan(row, col) {
const key = `${row},${col}`;
return this.mergedCells[key] ? this.mergedCells[key].rowspan || 1 : 1;
},
// 获取单元格样式类
getCellClass(row, col, cell) {
const classes = [];
// 表头样式
if (row === 0) {
classes.push('header-cell');
}
// 隔行变色
if (row % 2 === 1) {
classes.push('odd-row');
}
// 公式单元格
if (this.cellFormulas[`${row},${col}`]) {
classes.push('formula-cell');
}
// 空单元格
if (cell === '' || cell === null || cell === undefined) {
classes.push('empty-cell');
}
return classes.join(' ');
},
// 格式化单元格值
formatCellValue(value, row, col) {
if (value === null || value === undefined) {
return '';
}
// 处理日期
if (value instanceof Date) {
return value.toLocaleDateString();
}
// 处理数字格式
const format = this.cellFormats[`${row},${col}`];
if (format) {
try {
return XLSX.SSF.format(format, value);
} catch (e) {
// 格式化失败,返回原始值
}
}
return String(value);
},
// 切换工作表
switchSheet(sheetName) {
this.renderSheet(sheetName);
},
// 刷新预览
refreshPreview() {
if (this.currentSheet) {
this.renderSheet(this.currentSheet);
}
},
// 重置预览
resetPreview() {
this.fileData = null;
this.tableData = [];
this.sheetNames = [];
this.currentSheet = '';
this.mergedCells = {};
this.cellFormulas = {};
this.cellFormats = {};
this.workbook = null;
this.$emit('reset');
}
}
};
</script>
<style scoped>
.excel-preview-container {
position: relative;
width: 100%;
height: 100%;
}
.upload-area {
padding: 20px;
text-align: center;
}
.preview-area {
padding: 20px;
}
.toolbar {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
}
.table-container {
overflow: auto;
max-height: 600px;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.excel-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.excel-table td {
border: 1px solid #ebeef5;
padding: 8px 12px;
min-width: 80px;
vertical-align: middle;
position: relative;
}
.header-cell {
background-color: #f5f7fa;
font-weight: bold;
}
.odd-row {
background-color: #fafafa;
}
.formula-cell {
background-color: #fff7e6;
}
.empty-cell {
color: #c0c4cc;
}
.formula-display {
display: flex;
align-items: center;
gap: 4px;
}
.formula-icon {
color: #409eff;
font-weight: bold;
}
.formula-text {
color: #606266;
font-family: monospace;
}
.cell-content {
word-break: break-all;
line-height: 1.4;
}
.empty-data {
text-align: center;
padding: 40px 0;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-overlay p {
margin-top: 10px;
color: #606266;
}
</style>
3. 使用示例
<template>
<div class="app-container">
<h2>Excel文件预览示例</h2>
<!-- 基础使用 -->
<ExcelPreview
:file="selectedFile"
:show-formulas="showFormulas"
@file-loaded="onFileLoaded"
@reset="onReset"
/>
<!-- 后端返回的二进制流处理 -->
<div class="backend-example">
<h3>后端文件预览示例</h3>
<el-button @click="loadBackendFile" :loading="loading">
加载后端Excel文件
</el-button>
<ExcelPreview
v-if="backendFileData"
:file="backendFileData"
/>
</div>
</div>
</template>
<script>
import ExcelPreview from './components/ExcelPreview.vue';
import axios from 'axios';
export default {
name: 'App',
components: {
ExcelPreview
},
data() {
return {
selectedFile: null,
showFormulas: false,
backendFileData: null,
loading: false
};
},
methods: {
// 处理文件加载完成
onFileLoaded(file) {
console.log('文件加载完成:', file);
this.$message.success('Excel文件加载成功');
},
// 处理重置
onReset() {
console.log('预览已重置');
this.selectedFile = null;
},
// 加载后端文件
async loadBackendFile() {
try {
this.loading = true;
// 模拟后端API调用
const response = await axios.get('/api/excel-file', {
responseType: 'arraybuffer'
});
// 直接将ArrayBuffer传递给组件
this.backendFileData = response.data;
this.$message.success('后端文件加载成功');
} catch (error) {
this.$message.error('文件加载失败:' + error.message);
} finally {
this.loading = false;
}
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.backend-example {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
</style>
4. 高级功能扩展
公式计算支持
// 在ExcelPreview组件中添加公式计算功能
import { Parser } from 'hot-formula-parser';
// 在data中添加
data() {
return {
formulaParser: new Parser(),
// ... 其他数据
};
},
// 初始化公式解析器
mounted() {
this.initFormulaParser();
},
methods: {
initFormulaParser() {
// 设置单元格值获取回调
this.formulaParser.on('callCellValue', (cellCoord, done) => {
const sheet = cellCoord.sheet || this.currentSheet;
const row = cellCoord.row.index;
const col = cellCoord.column.index;
// 从工作表数据中获取值
const value = this.getCellValue(sheet, row, col);
done(value !== undefined ? value : null);
});
// 设置范围值获取回调
this.formulaParser.on('callRangeValue', (startCellCoord, endCellCoord, done) => {
const sheet = startCellCoord.sheet || this.currentSheet;
const startRow = startCellCoord.row.index;
const endRow = endCellCoord.row.index;
const startCol = startCellCoord.column.index;
const endCol = endCellCoord.column.index;
const values = [];
for (let r = startRow; r <= endRow; r++) {
const row = [];
for (let c = startCol; c <= endCol; c++) {
const value = this.getCellValue(sheet, r, c);
row.push(value !== undefined ? value : null);
}
values.push(row);
}
done(values);
});
},
// 计算公式值
calculateFormula(formula, sheetName) {
try {
const result = this.formulaParser.parse(formula);
return result.result;
} catch (error) {
console.error('公式计算错误:', error);
return '#ERROR!';
}
},
// 获取单元格值
getCellValue(sheetName, row, col) {
// 实现获取指定工作表中指定单元格值的逻辑
// 这里需要根据实际的数据结构来实现
}
}
样式美化增强
/* 增强的样式 */
.excel-table td {
border: 1px solid #ebeef5;
padding: 8px 12px;
min-width: 80px;
vertical-align: middle;
position: relative;
transition: all 0.2s ease;
}
.excel-table td:hover {
background-color: #f0f9eb;
box-shadow: inset 0 0 0 1px #67c23a;
}
.header-cell {
background: linear-gradient(180deg, #409eff, #337ecc);
color: white;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.odd-row {
background-color: #fafafa;
}
.even-row {
background-color: white;
}
.formula-cell {
background: linear-gradient(180deg, #fff7e6, #ffe7ba);
position: relative;
}
.formula-cell::before {
content: "ƒ";
position: absolute;
top: 2px;
right: 2px;
font-size: 10px;
color: #409eff;
}
.empty-cell {
color: #c0c4cc;
background-color: #f8f8f8;
}
.error-cell {
background-color: #fef0f0;
color: #f56c6c;
border-color: #fbc4c4;
}
.cell-content {
word-break: break-all;
line-height: 1.4;
min-height: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.excel-table td {
padding: 6px 8px;
font-size: 12px;
min-width: 60px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.table-container {
max-height: 400px;
}
}
5个核心优化技巧
1. 大文件性能优化
// 虚拟滚动实现
methods: {
// 限制渲染的行数
limitRenderRows(data, maxRows = 1000) {
if (data.length > maxRows) {
this.$message.warning(`文件行数过多,仅显示前${maxRows}行`);
return data.slice(0, maxRows);
}
return data;
},
// 分页渲染
renderWithPagination(data, pageSize = 100) {
this.totalPages = Math.ceil(data.length / pageSize);
this.currentPage = 1;
this.paginatedData = data.slice(0, pageSize);
}
}
2. 内存管理
// 及时释放资源
beforeDestroy() {
// 清理工作簿
if (this.workbook) {
this.workbook = null;
}
// 清理文件数据
if (this.fileData) {
this.fileData = null;
}
// 清理缓存数据
this.tableData = [];
this.sheetNames = [];
this.mergedCells = {};
this.cellFormulas = {};
this.cellFormats = {};
}
3. 错误处理
// 完善的错误处理
methods: {
async safeParseFile(file) {
try {
this.loading = true;
await this.processFile(file);
this.$emit('success', file);
} catch (error) {
this.$emit('error', error);
this.handleError(error);
} finally {
this.loading = false;
}
},
handleError(error) {
const errorMessage = this.getErrorMessage(error);
this.$message.error(errorMessage);
// 记录错误日志
console.error('Excel预览错误:', error);
},
getErrorMessage(error) {
if (error.message.includes('password')) {
return '文件已加密,请先解密';
}
if (error.message.includes('format')) {
return '文件格式不支持';
}
if (error.message.includes('size')) {
return '文件过大,请压缩后重试';
}
return '文件解析失败,请检查文件是否损坏';
}
}
4. 用户体验优化
// 加载进度提示
methods: {
showProgress(percent) {
this.$message.info(`文件解析中... ${percent}%`);
},
// 拖拽上传优化
handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
this.isDragging = true;
},
handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
this.isDragging = false;
}
}
5. 兼容性处理
// 浏览器兼容性检查
mounted() {
this.checkBrowserCompatibility();
},
methods: {
checkBrowserCompatibility() {
if (!window.FileReader) {
this.$message.error('当前浏览器不支持文件读取功能');
return false;
}
if (!window.ArrayBuffer) {
this.$message.error('当前浏览器不支持ArrayBuffer');
return false;
}
return true;
},
// 文件类型检查
validateFileType(file) {
const allowedTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
const allowedExtensions = ['.xls', '.xlsx', '.csv'];
const isValidType = allowedTypes.includes(file.type);
const isValidExtension = allowedExtensions.some(ext =>
file.name.toLowerCase().endsWith(ext)
);
return isValidType || isValidExtension;
}
}
实战案例:某企业管理系统Excel预览功能
需求分析
某企业管理系统需要支持员工上传Excel文件进行数据导入,要求:
- 支持.xls、.xlsx、.csv格式
- 预览前500行数据
- 显示工作表切换
- 支持公式显示
- 大文件提示优化
实现代码
<template>
<div class="enterprise-excel-preview">
<div class="preview-header">
<h3>数据预览</h3>
<div class="header-actions">
<el-tag v-if="fileInfo" type="info">
{{ fileInfo.name }} ({{ fileInfo.size }})
</el-tag>
<el-button @click="confirmImport" type="primary" size="small">
确认导入
</el-button>
</div>
</div>
<ExcelPreview
:file="excelFile"
:show-formulas="showFormulas"
@file-loaded="onFileLoaded"
@error="onError"
/>
<div v-if="warningMessage" class="warning-message">
<el-alert
:title="warningMessage"
type="warning"
show-icon
:closable="false"
/>
</div>
</div>
</template>
<script>
import ExcelPreview from './ExcelPreview.vue';
export default {
name: 'EnterpriseExcelPreview',
components: {
ExcelPreview
},
props: {
excelFile: {
type: [File, ArrayBuffer],
required: true
}
},
data() {
return {
showFormulas: false,
fileInfo: null,
warningMessage: '',
tableStats: {
rows: 0,
cols: 0,
sheets: 0
}
};
},
methods: {
onFileLoaded(file) {
this.fileInfo = {
name: file.name,
size: this.formatFileSize(file.size),
type: file.type
};
// 分析文件统计信息
this.analyzeFileStats(file);
this.$emit('loaded', file);
},
onError(error) {
this.$emit('error', error);
},
analyzeFileStats(file) {
// 这里可以添加文件统计分析逻辑
// 比如行数、列数、工作表数量等
},
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
confirmImport() {
this.$emit('confirm-import', {
fileInfo: this.fileInfo,
stats: this.tableStats
});
}
}
};
</script>
<style scoped>
.enterprise-excel-preview {
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
}
.preview-header h3 {
margin: 0;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.warning-message {
padding: 15px 20px;
background-color: #fdf6ec;
border-top: 1px solid #ebeef5;
}
</style>
结语
通过今天的学习,我们掌握了Vue中实现Excel文件预览的完整方案:
- 核心技术:使用xlsx.js库解析Excel文件
- 核心功能:文件上传、表格渲染、公式显示、合并单元格处理
- 优化技巧:性能优化、内存管理、错误处理、用户体验优化
- 实战应用:企业级应用中的完整实现
记住这几个关键点:
- 选择合适的第三方库是成功的一半
- 合理处理大文件和性能问题是关键
- 完善的错误处理提升用户体验
- 样式美化让预览效果更专业
Excel预览功能虽然看似简单,但要做好却需要考虑很多细节。希望今天的分享能帮助大家在项目中轻松实现这个功能!
如果你觉得这篇文章对你有帮助,欢迎点赞、在看、转发三连,你的支持是我们持续创作的最大动力!
前端技术精选 | 专注分享实用的前端技术干货
标题:Vue预览Excel文件的完整指南:从零开始实现
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304277222.html
0 评论