从零实现:react&Ts--批量导入 & Excel 模版下载功能

用最通俗的语言,教会你如何实现这个超实用的功能!保证看完就会写!

🤔 这功能是干嘛的?

想象一下这个场景:

没有批量导入时的痛苦:

复制代码
需要录入 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 })

这个函数做了什么?

  1. 第一行作为 key(对象的属性名)
  2. 其他行作为 value(对象的值)
  3. 每一行数据变成一个对象
  4. 所有对象组成一个数组

超级方便! 🎉


🎯 关键参数详解

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: 文件上传后没反应?

可能原因:

  1. 文件格式不对(不是 .xlsx 或 .xls)
  2. 文件损坏
  3. 回调函数报错

排查方法:

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 或分批

🎓 学习路径

入门级(必学)

  1. ✅ 掌握 xlsx 库基本 API
  2. ✅ 理解 FileReader 的使用
  3. ✅ 掌握 Upload 组件的 customRequest

进阶级(推荐)

  1. ✅ 数据校验与去重
  2. ✅ 错误处理与用户提示
  3. ✅ 性能优化(防抖、分批)

高级级(锦上添花)

  1. ✅ Web Worker 处理大文件
  2. ✅ 虚拟滚动展示大量数据
  3. ✅ 支持拖拽上传
  4. ✅ 导入进度条

🎯 总结

核心概念

复制代码
下载模版 = 用 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% 的功能! 🎉

设计精髓

  1. 组件职责单一 - 一个组件只做一件事
  2. 接口设计灵活 - 通过 Props 传递配置
  3. 错误处理完善 - 各种边界情况都要考虑
  4. 用户体验优先 - 即时反馈,友好提示

🎁 最后的话

批量导入和下载模版 是一个非常实用的功能!

掌握了这个,你可以:

  • 让用户批量录入数据 📝
  • 导出数据供用户编辑 📤
  • 实现数据的批量迁移 🚚
  • 生成各种报表和模版 📊

核心思想:

复制代码
把 Excel 当作"用户和系统之间的桥梁"
下载模版 = 给用户一个桥 🌉
批量导入 = 把数据搬过桥 🚚

最后一句话送给你:

"代码不难,难的是把复杂的事情变简单。"
"现在你学会了,快去实现一个属于自己的批量导入功能吧!" 💪

P.S. 如果觉得有用,给个 点赞和收藏⭐ 吧!(开玩笑的 😄)

祝你编码愉快! 🎊

相关推荐
前端sweetGirl1 小时前
Excel 里 XLOOKUP 函数返回日期时找不到值显示 1/0,怎么让他不显示
excel
2503_928411563 小时前
12.4 axios的二次封装-深拷贝
前端·javascript·vue.js
酒尘&11 小时前
Hook学习-上篇
前端·学习·react.js·前端框架·react
Ndmzi11 小时前
Matlab编程技巧:自定义Simulink菜单(理解补充)
前端·javascript·python
勇气要爆发12 小时前
物种起源—JavaScript原型链详解
开发语言·javascript·原型模式
San30.13 小时前
深入理解 JavaScript OOP:从一个「就地编辑组件」看清封装、状态与原型链
开发语言·前端·javascript·ecmascript
AAA阿giao13 小时前
JavaScript 原型与原型链:从零到精通的深度解析
前端·javascript·原型·原型模式·prototype·原型链
0***863313 小时前
SQL Server2019安装步骤+使用+解决部分报错+卸载(超详细 附下载链接)
javascript·数据库·ui
JuneTT14 小时前
【JS】使用内连配置强制引入图片为base64
前端·javascript