大家好,我是你們的前端老司機。今天我們來聊聊一個讓無數前端開發者頭疼的問題——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預覽功能雖然看似簡單,但要做好卻需要考慮很多細節。希望今天的分享能幫助大家在項目中輕鬆實現這個功能!
如果你覺得這篇文章對你有幫助,歡迎點贊、在看、轉發三連,你的支持是我們持續創作的最大動力!
前端技術精選 | 專注分享實用的前端技術乾貨