用最通俗的语言,教会你如何实现这个超实用的功能!保证看完就会写!
🤔 这功能是干嘛的?
想象一下这个场景:
没有批量导入时的痛苦:
需要录入 100 条数据...
复制... 粘贴... 复制... 粘贴...
手指快断了!😭
有了批量导入之后:
1. 点击"下载模版" → 得到一个 Excel 📥
2. 在 Excel 里批量填写(复制粘贴随便来)✍️
3. 点击"批量导入" → 一键导入 100 条!🚀
简单来说:
- 下载模版 = 给用户一张空白表格
- 批量导入 = 把用户填好的表格读进系统
就这么简单!🎯
🏗️ 整体架构
这个功能分为两个核心部分:
┌─────────────────────────────┐
│ 📥 TemplateDownload │ 生成 Excel 模版
│ - 创建空白表格 │
│ - 设置格式 │
│ - 触发下载 │
└─────────────────────────────┘
┌─────────────────────────────┐
│ 📤 BatchImport │ 读取 Excel 文件
│ - 读取文件 │
│ - 解析数据 │
│ - 返回结果 │
└─────────────────────────────┘
核心思想:
- 一个管"生"(下载),一个管"读"(导入)
- 各干各的活,互不干扰
- 简单、清晰、好维护!✨
📥 下载模版:如何生成 Excel?
第一步:安装依赖
bash
npm install xlsx
第二步:核心工具函数
typescript
import xlsx from 'xlsx'
export const exportMould = (headers: IHeader[], fileName = '模板.xlsx') => {
// 🎯 步骤1:准备表头
const headerRow = headers.map((item) => item.title)
// 比如:["姓名", "年龄", "邮箱"]
// 🎯 步骤2:创建工作表
const worksheet = xlsx.utils.aoa_to_sheet([headerRow])
// aoa = Array of Arrays(数组的数组)
// 🎯 步骤3:设置单元格为文本格式(重要!)
const maxRows = 100 // 预留 100 行
const maxCols = headers.length - 1
for (let row = 0; row <= maxRows; row++) {
for (let col = 0; col <= maxCols; col++) {
const cellAddress = xlsx.utils.encode_cell({ r: row, c: col })
// 生成单元格地址:A1, B1, C1...
if (!worksheet[cellAddress]) {
worksheet[cellAddress] = { t: 's', v: '' }
}
worksheet[cellAddress].t = 's' // t = type (文本类型)
worksheet[cellAddress].z = '@' // z = format (文本格式)
}
}
// 🎯 步骤4:更新工作表范围
worksheet['!ref'] = xlsx.utils.encode_range({
s: { r: 0, c: 0 }, // start: A1
e: { r: maxRows, c: maxCols }, // end: 最后一个单元格
})
// 🎯 步骤5:设置列宽
worksheet['!cols'] = headers.map(() => ({ wpx: 250 }))
// wpx = width in pixels
// 🎯 步骤6:创建工作簿
const workbook = xlsx.utils.book_new()
xlsx.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 🎯 步骤7:保存文件(浏览器自动下载)
xlsx.writeFile(workbook, fileName)
}
第三步:React 组件封装
typescript
interface ITemplateDownloadProps {
fieldLabel: string // 表头文本(比如"姓名")
fieldKey: string // 字段键名(比如"name")
children: React.ReactNode // 触发下载的元素(按钮)
}
const TemplateDownload: React.FC<ITemplateDownloadProps> = (props) => {
const { fieldLabel, fieldKey, children } = props
const handleDownloadTemplate = () => {
const headers = [
{ title: fieldLabel, key: fieldKey },
]
const fileName = `${fieldLabel}导入模版.xlsx`
exportMould(headers, fileName)
}
return (
<div onClick={handleDownloadTemplate}>
{children}
</div>
)
}
使用示例
typescript
<TemplateDownload fieldLabel="姓名" fieldKey="name">
<Button icon={<DownloadOutlined />}>
下载模版
</Button>
</TemplateDownload>
点击后会下载一个 Excel 文件:
┌────────┐
│ 姓名 │ ← 表头
├────────┤
│ │ ← 空行(等待填写)
│ │
│ │
└────────┘
🔑 核心知识点:为什么要设置文本格式?
问题演示
javascript
// Excel 的"智能"识别
用户输入:001234
Excel 识别:这是数字吧?→ 1234 (前导零没了!)
用户输入:123456789012345
Excel 识别:太长了!→ 1.23456789012345e+14 (科学计数法!)
用户输入:1-2
Excel 识别:日期吧?→ 1月2日 (完全变了!)
解决方案:
typescript
worksheet[cellAddress].t = 's' // 类型 = 字符串
worksheet[cellAddress].z = '@' // 格式 = 文本
// 现在 Excel 不敢乱改了!
用户输入:001234 → 保存为:001234 ✅
用户输入:123456789012345 → 保存为:123456789012345 ✅
记住: 涉及 ID、编号、长数字的模版,必须设置文本格式!
📤 批量导入:如何读取 Excel?
第一步:文件读取工具
typescript
/**
* 读取文件为二进制字符串
* @param file - File 对象
* @returns Promise<string> - 二进制字符串
*/
export const readFile = async (file: File) => new Promise((resolve) => {
const reader = new FileReader() // 浏览器提供的文件读取器
reader.readAsBinaryString(file) // 读取为二进制字符串
reader.onload = (e) => {
resolve(e.target?.result) // 读取完成,返回结果
}
})
为什么用二进制字符串?
- xlsx 库需要这种格式
- 可以保留 Excel 的所有原始信息
第二步:React 组件封装
typescript
interface IBatchImportProps {
onImport: (data: any[]) => Promise<void> // 导入成功后的回调
loading?: boolean // 加载状态
children: React.ReactNode // 触发元素
}
const BatchImport: React.FC<IBatchImportProps> = (props) => {
const { onImport, loading = false, children } = props
const handleFileUpload = async (file: any) => {
try {
// 🎯 步骤1:读取文件
const metaData = await readFile(file.file as File)
// 🎯 步骤2:解析 Excel 工作簿
const workbook = xlsx.read(metaData, { type: 'binary' })
// 🎯 步骤3:获取所有工作表
const sheets = Object.keys(workbook.Sheets)
const excelData: any[] = []
// 🎯 步骤4:读取每个工作表的数据
sheets.forEach((sheet) => {
const current = xlsx.utils.sheet_to_json(workbook.Sheets[sheet], {
raw: true, // 保持原始值
})
excelData.push(...current)
})
// 🎯 步骤5:空文件检查
if (excelData.length === 0) {
message.warning('导入的文件为空')
return
}
// 🎯 步骤6:回调给父组件
await onImport(excelData)
} catch (err) {
console.error('导入失败:', err)
message.error('导入失败,请检查文件格式')
}
}
return (
<Upload
accept=".xls, .xlsx" // 只接受 Excel 文件
fileList={[]} // 不显示文件列表
customRequest={handleFileUpload} // 自定义上传逻辑
showUploadList={false} // 隐藏上传列表
disabled={loading} // 加载中禁用
>
{children}
</Upload>
)
}
使用示例
typescript
<BatchImport
onImport={async (data) => {
console.log('导入的数据:', data)
// 在这里处理数据
}}
>
<Button icon={<UploadOutlined />}>
批量导入
</Button>
</BatchImport>
🔍 数据解析流程详解
Excel → JavaScript 数组
Excel 文件内容:
| 姓名 | 年龄 | 邮箱 |
|--------|------|-------------------|
| 张三 | 25 | zhangsan@qq.com |
| 李四 | 30 | lisi@qq.com |
解析后的 JavaScript 数组:
javascript
[
{
"姓名": "张三",
"年龄": "25",
"邮箱": "zhangsan@qq.com"
},
{
"姓名": "李四",
"年龄": "30",
"邮箱": "lisi@qq.com"
}
]
核心 API:
typescript
xlsx.utils.sheet_to_json(worksheet, { raw: true })
这个函数做了什么?
- 第一行作为 key(对象的属性名)
- 其他行作为 value(对象的值)
- 每一行数据变成一个对象
- 所有对象组成一个数组
超级方便! 🎉
🎯 关键参数详解
raw: true 的作用
javascript
// raw: false(默认)
Excel 单元格: "123.456"
解析结果: 123.456 (number 类型)
问题: 可能丢失精度、前导零
// raw: true
Excel 单元格: "123.456"
解析结果: "123.456" (string 类型)
优势: 完全保持原始值 ✅
什么时候用 raw: true?
- ID、编号、序列号等(必须保持原样)
- 长数字(超过 15 位会丢失精度)
- 带前导零的数据(如 001、002)
- 任何需要"原汁原味"的数据
什么时候用 raw: false?
- 真正的数字计算(如金额、数量)
- 需要数值类型的场景
🎨 完整示例:实现一个用户导入功能
需求
实现一个通用的用户批量导入功能,支持:
- 下载标准模版
- 批量导入用户信息
- 数据格式校验
实现代码
typescript
import React, { useState } from 'react'
import { Button, message, Form, Input } from 'antd'
import { DownloadOutlined, UploadOutlined } from '@ant-design/icons'
import BatchImport from '@/components/batch-import'
import TemplateDownload from '@/components/template-download'
const UserBatchImport: React.FC = () => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
// 处理导入的数据
const handleImport = async (rawData: any[]) => {
setLoading(true)
try {
// 提取数据(假设表头是"姓名"和"邮箱")
const users = rawData.map((row) => ({
name: row['姓名'],
email: row['邮箱'],
}))
// 简单校验
const invalidUsers = users.filter((user) => !user.name || !user.email)
if (invalidUsers.length > 0) {
message.error(`发现 ${invalidUsers.length} 条数据不完整`)
return
}
// 填充到表单(或者调用接口保存)
console.log('导入的用户:', users)
message.success(`成功导入 ${users.length} 条数据`)
} finally {
setLoading(false)
}
}
return (
<div>
<Form form={form}>
{/* 表单其他字段 */}
</Form>
<div style={{ display: 'flex', gap: 16 }}>
{/* 下载模版按钮 */}
<TemplateDownload
fieldLabel="姓名"
fieldKey="name"
>
<Button icon={<DownloadOutlined />}>
下载模版
</Button>
</TemplateDownload>
{/* 批量导入按钮 */}
<BatchImport onImport={handleImport} loading={loading}>
<Button icon={<UploadOutlined />} loading={loading}>
批量导入
</Button>
</BatchImport>
</div>
</div>
)
}
就这么简单! 两个组件,搞定批量导入!🎊
🧪 深入理解:xlsx 库核心 API
1. aoa_to_sheet:数组转工作表
javascript
const data = [
["姓名", "年龄"], // 第一行(表头)
["张三", 25], // 第二行
["李四", 30] // 第三行
]
const worksheet = xlsx.utils.aoa_to_sheet(data)
生成的 Excel:
┌────┬────┐
│姓名│年龄 │
├────┼────┤
│张三│ 25 │
│李四│ 30 │
└────┴────┘
2. sheet_to_json:工作表转数组
javascript
const worksheet = /* Excel 工作表 */
const jsonData = xlsx.utils.sheet_to_json(worksheet)
// 结果:
[
{ "姓名": "张三", "年龄": 25 },
{ "姓名": "李四", "年龄": 30 }
]
自动把第一行当作 key! 超智能!🤖
3. encode_cell:生成单元格地址
javascript
xlsx.utils.encode_cell({ r: 0, c: 0 }) // "A1"
xlsx.utils.encode_cell({ r: 0, c: 1 }) // "B1"
xlsx.utils.encode_cell({ r: 1, c: 0 }) // "A2"
// r = row (行)
// c = column (列)
4. encode_range:生成范围地址
javascript
xlsx.utils.encode_range({
s: { r: 0, c: 0 }, // start
e: { r: 2, c: 1 } // end
})
// 结果: "A1:B3"
🎭 customRequest vs 默认上传
默认上传(action)
typescript
<Upload action="/api/upload">
<Button>上传</Button>
</Upload>
流程:
文件 → 发送到服务器 → 服务器处理 → 返回结果
优点: 简单
缺点: 需要服务器接口、慢
自定义上传(customRequest)
typescript
<Upload
customRequest={(file) => {
// 在浏览器本地处理
const data = processFile(file)
callback(data)
}}
>
<Button>上传</Button>
</Upload>
流程:
文件 → 浏览器本地处理 → 立即返回结果
优点: 快、不依赖服务器
缺点: 需要自己实现逻辑
批量导入场景: 只需要读取数据,不需要上传到服务器,所以用 customRequest 完美!✨
💡 进阶技巧
技巧1:多字段模版
typescript
const headers = [
{ title: '姓名', key: 'name' },
{ title: '年龄', key: 'age' },
{ title: '邮箱', key: 'email' },
]
exportMould(headers, '用户信息导入模版.xlsx')
生成的模版:
┌────┬────┬─────────────┐
│姓名│年龄│ 邮箱 │
├────┼────┼─────────────┤
│ │ │ │
└────┴────┴─────────────┘
技巧2:带示例数据的模版
typescript
const data = [
["姓名", "年龄", "邮箱"], // 表头
["张三", "25", "zhangsan@qq.com"], // 示例数据
]
const worksheet = xlsx.utils.aoa_to_sheet(data)
效果:
┌────┬────┬─────────────────┐
│姓名│年龄│ 邮箱 │
├────┼────┼─────────────────┤
│张三│ 25 │zhangsan@qq.com │ ← 示例数据
│ │ │ │ ← 空行
└────┴────┴─────────────────┘
提示: 有示例数据,用户更容易理解格式!👍
技巧3:设置单元格样式
typescript
// 设置表头加粗
worksheet['A1'].s = {
font: { bold: true },
fill: { fgColor: { rgb: "FFFF00" } }, // 黄色背景
}
技巧4:多工作表模版
typescript
const workbook = xlsx.utils.book_new()
// 添加多个工作表
xlsx.utils.book_append_sheet(workbook, worksheet1, '用户信息')
xlsx.utils.book_append_sheet(workbook, worksheet2, '订单信息')
xlsx.utils.book_append_sheet(workbook, worksheet3, '备注说明')
xlsx.writeFile(workbook, '批量导入模版.xlsx')
🛡️ 数据校验实战
虽然说不涉及业务,但校验是通用逻辑,简单讲一下:
基本校验模式
typescript
const handleImport = async (rawData: any[]) => {
const validData: any[] = []
const invalidData: any[] = []
rawData.forEach((row) => {
// 提取数据
const value = row['字段名']
// 校验规则(根据实际需求修改)
if (isValid(value)) {
validData.push(value)
} else {
invalidData.push(value)
}
})
// 错误处理
if (invalidData.length > 0) {
message.error(`发现 ${invalidData.length} 条无效数据`)
return
}
// 成功
onSuccess(validData)
}
去重处理
typescript
// 使用 Set 去重
const uniqueData = Array.from(new Set(validData))
// 或者根据特定字段去重
const uniqueData = Array.from(
new Map(validData.map(item => [item.id, item])).values()
)
🚀 完整数据流
下载模版流程
用户点击"下载模版"
↓
触发 handleDownloadTemplate
↓
准备表头数据: [{ title: "字段名", key: "field_key" }]
↓
调用 exportMould(headers, fileName)
↓
创建工作表: xlsx.utils.aoa_to_sheet([headerRow])
↓
设置单元格格式: t='s', z='@'
↓
创建工作簿: xlsx.utils.book_new()
↓
添加工作表: xlsx.utils.book_append_sheet()
↓
保存文件: xlsx.writeFile(workbook, fileName)
↓
浏览器弹出下载框 📥
批量导入流程
用户点击"批量导入"
↓
Upload 组件打开文件选择器
↓
用户选择 Excel 文件
↓
触发 customRequest (handleFileUpload)
↓
readFile: 读取文件为二进制字符串
↓
xlsx.read: 解析为工作簿对象
↓
获取所有工作表名称: Object.keys(workbook.Sheets)
↓
遍历每个工作表
↓
sheet_to_json: 工作表转 JSON
↓
合并所有工作表数据: excelData.push(...current)
↓
回调 onImport(excelData)
↓
父组件处理数据 ✅
🔧 常见问题排查
Q1: 文件上传后没反应?
可能原因:
- 文件格式不对(不是 .xlsx 或 .xls)
- 文件损坏
- 回调函数报错
排查方法:
typescript
const handleFileUpload = async (file: any) => {
console.log('收到文件:', file) // ← 加这行
try {
const metaData = await readFile(file.file)
console.log('读取成功:', metaData) // ← 加这行
const workbook = xlsx.read(metaData, { type: 'binary' })
console.log('解析成功:', workbook) // ← 加这行
} catch (err) {
console.error('具体错误:', err) // ← 看这里
}
}
Q2: 导入的数据全是 undefined?
原因: 表头名称不匹配。
javascript
// Excel 表头: "用户名"
// 代码中取值: row['姓名'] ← 错了!
// 正确做法
const value = row['用户名'] // 必须和 Excel 表头一致
解决方案:
typescript
// 方案1:统一约定表头名称
const FIELD_NAMES = {
NAME: '姓名',
EMAIL: '邮箱',
}
const value = row[FIELD_NAMES.NAME]
// 方案2:容错处理
const value = row['姓名'] || row['用户名'] || row['name']
Q3: 中文乱码问题?
原因: 文件编码不是 UTF-8。
解决方案:
typescript
const reader = new FileReader()
reader.readAsBinaryString(file) // 二进制读取,避免编码问题
// 或者明确指定编码
reader.readAsText(file, 'UTF-8')
Q4: 大文件导入卡顿?
原因: 一次性处理大量数据阻塞主线程。
解决方案1:分批处理
typescript
const processBatch = async (data: any[], batchSize = 100) => {
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize)
await processBatchData(batch)
// 更新进度
const progress = Math.round((i + batchSize) / data.length * 100)
setProgress(progress)
// 让出主线程
await new Promise(resolve => setTimeout(resolve, 0))
}
}
解决方案2:使用 Web Worker
typescript
// 在后台线程处理
const worker = new Worker('/excel-processor.js')
worker.postMessage({ data: rawData })
worker.onmessage = (e) => {
const processedData = e.data
onSuccess(processedData)
}
🎁 实用工具函数集合
1. 读取多种文件格式
typescript
export const readFileAsText = async (file: File) => new Promise((resolve) => {
const reader = new FileReader()
reader.readAsText(file)
reader.onload = (e) => resolve(e.target?.result)
})
export const readFileAsBinary = async (file: File) => new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = (e) => resolve(e.target?.result)
})
2. 格式化单元格
typescript
// 设置数字格式(千分位)
worksheet['A1'].z = '#,##0.00'
// 设置日期格式
worksheet['B1'].z = 'yyyy-mm-dd'
// 设置百分比格式
worksheet['C1'].z = '0.00%'
3. 设置列宽(自适应)
typescript
const getColumnWidth = (data: any[], key: string) => {
const maxLength = Math.max(
key.length, // 表头长度
...data.map(row => String(row[key] || '').length) // 数据长度
)
return { wpx: maxLength * 10 + 20 } // 字符宽度 * 10 + 边距
}
worksheet['!cols'] = headers.map(header =>
getColumnWidth(data, header.key)
)
4. 添加数据验证(下拉列表)
typescript
// 在 Excel 中添加下拉列表
worksheet['!dataValidation'] = {
'A2:A100': {
type: 'list',
formula1: '"男,女"', // 下拉选项
}
}
🎨 UI 设计建议
按钮样式
typescript
<div className="import-actions">
<TemplateDownload fieldLabel="数据" fieldKey="data">
<div className="import-action-item">
<IconDownload />
<span>下载模版</span>
</div>
</TemplateDownload>
<BatchImport onImport={handleImport}>
<div className="import-action-item">
<IconUpload />
<span>批量导入</span>
</div>
</BatchImport>
</div>
CSS:
less
.import-actions {
display: flex;
gap: 12px;
}
.import-action-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #1890ff;
border-color: #1890ff;
}
}
🌟 最佳实践
1. 组件职责单一
typescript
// ✅ 好的设计
<BatchImport onImport={handleImport}> // 只负责读文件
<Button>导入</Button>
</BatchImport>
// ❌ 不好的设计
<BatchImport
onImport={handleImport}
onValidate={handleValidate} // 校验不应该在这里
onTransform={handleTransform} // 转换不应该在这里
onSave={handleSave} // 保存不应该在这里
/>
原则: 一个组件只做一件事!
2. Props 设计合理
typescript
// ✅ 好的设计
interface IProps {
onImport: (data: any[]) => void // 必需的
loading?: boolean // 可选的
children: React.ReactNode // 自定义UI
}
// ❌ 不好的设计
interface IProps {
onImport: (data: any[]) => void
buttonText: string // 限制了 UI,不灵活
buttonStyle: CSSProperties // 太细节了
iconType: string // 不需要组件管
}
3. 错误处理完善
typescript
try {
await handleImport(data)
} catch (err) {
// 不同错误,不同提示
if (err.message.includes('格式')) {
message.error('文件格式错误,请检查是否为 Excel 文件')
} else if (err.message.includes('空')) {
message.warning('文件内容为空')
} else {
message.error('导入失败,请重试')
}
}
4. 给用户反馈
typescript
// 导入前
message.loading('正在导入,请稍候...', 0)
// 导入中(大文件)
setProgress(50) // 显示进度条
// 导入后
message.destroy() // 关闭 loading
message.success(`成功导入 ${count} 条数据`)
📦 完整代码模版(拿来即用)
工具函数(utils/excel.ts)
typescript
import xlsx from 'xlsx'
export interface IHeader {
title: string
key: string
}
// 生成 Excel 模版
export const exportExcelTemplate = (
headers: IHeader[],
fileName = '导入模版.xlsx'
) => {
const headerRow = headers.map(item => item.title)
const worksheet = xlsx.utils.aoa_to_sheet([headerRow])
// 设置文本格式
const maxRows = 100
for (let row = 0; row <= maxRows; row++) {
for (let col = 0; col < headers.length; col++) {
const cellAddress = xlsx.utils.encode_cell({ r: row, c: col })
if (!worksheet[cellAddress]) {
worksheet[cellAddress] = { t: 's', v: '' }
}
worksheet[cellAddress].t = 's'
worksheet[cellAddress].z = '@'
}
}
worksheet['!ref'] = xlsx.utils.encode_range({
s: { r: 0, c: 0 },
e: { r: maxRows, c: headers.length - 1 },
})
worksheet['!cols'] = headers.map(() => ({ wpx: 250 }))
const workbook = xlsx.utils.book_new()
xlsx.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
xlsx.writeFile(workbook, fileName)
}
// 读取 Excel 文件
export const readExcelFile = async (file: File): Promise<any[]> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = (e) => {
try {
const data = e.target?.result
const workbook = xlsx.read(data, { type: 'binary' })
const sheets = Object.keys(workbook.Sheets)
const jsonData: any[] = []
sheets.forEach(sheet => {
const current = xlsx.utils.sheet_to_json(workbook.Sheets[sheet], {
raw: true,
})
jsonData.push(...current)
})
resolve(jsonData)
} catch (err) {
reject(err)
}
}
reader.onerror = () => {
reject(new Error('文件读取失败'))
}
})
}
下载组件(components/TemplateDownload.tsx)
typescript
import React from 'react'
import { exportExcelTemplate, IHeader } from '@/utils/excel'
interface IProps {
headers: IHeader[]
fileName?: string
children: React.ReactNode
}
const TemplateDownload: React.FC<IProps> = (props) => {
const { headers, fileName = '导入模版.xlsx', children } = props
const handleClick = () => {
exportExcelTemplate(headers, fileName)
}
return <div onClick={handleClick}>{children}</div>
}
export default TemplateDownload
导入组件(components/BatchImport.tsx)
typescript
import React from 'react'
import { Upload, message } from 'antd'
import { readExcelFile } from '@/utils/excel'
interface IProps {
onImport: (data: any[]) => Promise<void>
loading?: boolean
children: React.ReactNode
}
const BatchImport: React.FC<IProps> = (props) => {
const { onImport, loading = false, children } = props
const handleUpload = async (file: any) => {
try {
const data = await readExcelFile(file.file)
if (data.length === 0) {
message.warning('文件内容为空')
return
}
await onImport(data)
} catch (err) {
console.error('导入失败:', err)
message.error('导入失败,请检查文件格式')
}
}
return (
<Upload
accept=".xls, .xlsx"
fileList={[]}
customRequest={handleUpload}
showUploadList={false}
disabled={loading}
>
{children}
</Upload>
)
}
export default BatchImport
使用示例
typescript
import React from 'react'
import { Button } from 'antd'
import BatchImport from '@/components/BatchImport'
import TemplateDownload from '@/components/TemplateDownload'
const MyPage: React.FC = () => {
const headers = [
{ title: '姓名', key: 'name' },
{ title: '邮箱', key: 'email' },
]
const handleImport = async (data: any[]) => {
console.log('导入的数据:', data)
// 在这里处理数据
}
return (
<div>
<TemplateDownload headers={headers} fileName="用户导入模版.xlsx">
<Button>下载模版</Button>
</TemplateDownload>
<BatchImport onImport={handleImport}>
<Button>批量导入</Button>
</BatchImport>
</div>
)
}
三步搞定! 复制、粘贴、改字段名!🎊
🎪 有趣的实现细节
为什么 Upload 的 fileList 设为空数组?
typescript
<Upload fileList={[]}>
原因: 阻止 Upload 组件显示已上传文件列表。
fileList={undefined} → 组件自己管理列表(会显示)
fileList={[]} → 手动控制为空(不显示)✅
效果:
- 用户选文件 → 立即处理 → 不留痕迹
- 适合"一次性"操作的场景
Excel 单元格地址是怎么编码的?
javascript
// Excel 的列名规则
A, B, C, ..., Z, // 1-26
AA, AB, AC, ..., AZ, // 27-52
BA, BB, BC, ..., ZZ, // ...
AAA, AAB, ... // 更多
// xlsx 库的编码
{ r: 0, c: 0 } → "A1"
{ r: 0, c: 26 } → "AA1"
{ r: 0, c: 52 } → "BA1"
就像车牌号: 一位不够用,就加一位!🚗
为什么要设置 !ref?
typescript
worksheet['!ref'] = 'A1:C100'
!ref 告诉 Excel:"这个表格的有效范围是 A1 到 C100"
没设置 !ref:
Excel 只能看到有内容的单元格
空白单元格会被忽略 ❌
设置了 !ref:
Excel 知道完整的表格范围
空白单元格也保留格式 ✅
特别是设置了文本格式,必须用 !ref 才能生效!
🚦 性能对比
| 操作 | 小文件(< 100行) | 中文件(100-1000行) | 大文件(> 1000行) |
|---|---|---|---|
| 读取 | 瞬间 ⚡ | 很快 🚀 | 稍慢(1-2秒)⏱️ |
| 解析 | 瞬间 ⚡ | 很快 🚀 | 需要时间(2-5秒)⏱️ |
| 建议 | 直接处理 | 直接处理 | 使用 Worker 或分批 |
🎓 学习路径
入门级(必学)
- ✅ 掌握
xlsx库基本 API - ✅ 理解 FileReader 的使用
- ✅ 掌握 Upload 组件的 customRequest
进阶级(推荐)
- ✅ 数据校验与去重
- ✅ 错误处理与用户提示
- ✅ 性能优化(防抖、分批)
高级级(锦上添花)
- ✅ Web Worker 处理大文件
- ✅ 虚拟滚动展示大量数据
- ✅ 支持拖拽上传
- ✅ 导入进度条
🎯 总结
核心概念
下载模版 = 用 xlsx 生成 Excel 文件
批量导入 = 用 xlsx 读取 Excel 文件
就这么简单! 其他的都是细节和优化。
最重要的三个 API
typescript
// 1. 数组转工作表
xlsx.utils.aoa_to_sheet(data)
// 2. 工作表转数组
xlsx.utils.sheet_to_json(worksheet, { raw: true })
// 3. 保存文件
xlsx.writeFile(workbook, fileName)
记住这三个,就能实现 80% 的功能! 🎉
设计精髓
- 组件职责单一 - 一个组件只做一件事
- 接口设计灵活 - 通过 Props 传递配置
- 错误处理完善 - 各种边界情况都要考虑
- 用户体验优先 - 即时反馈,友好提示
🎁 最后的话
批量导入和下载模版 是一个非常实用的功能!
掌握了这个,你可以:
- 让用户批量录入数据 📝
- 导出数据供用户编辑 📤
- 实现数据的批量迁移 🚚
- 生成各种报表和模版 📊
核心思想:
把 Excel 当作"用户和系统之间的桥梁"
下载模版 = 给用户一个桥 🌉
批量导入 = 把数据搬过桥 🚚
最后一句话送给你:
"代码不难,难的是把复杂的事情变简单。"
"现在你学会了,快去实现一个属于自己的批量导入功能吧!" 💪
P.S. 如果觉得有用,给个 点赞和收藏⭐ 吧!(开玩笑的 😄)
祝你编码愉快! 🎊