📥 Excel 导入
ThinkAdmin 提供完善的Excel文件导入功能,支持将Excel文件中的数据导入到数据库中。系统提供三种实现方式:Excel.push(推荐)、PHP后端解析和xlsx.js高级用法。
🚀 主要功能
- Excel 导入: 支持Excel文件的数据导入
- 多种方式: 支持Excel.push、PHP后端解析和xlsx.js三种方式
- 自动处理: Excel.push自动处理文件读取、数据解析和逐行上传
- 数据解析: 自动解析Excel文件内容
- 数据库写入: 将解析的数据写入数据库
- 错误处理: 提供完善的错误处理机制
- 进度显示: 支持导入进度显示和成功/失败统计
📋 支持格式
Excel 格式
- XLSX: 支持新版Excel格式
- XLS: 支持旧版Excel格式
- 数据解析: 自动解析Excel数据
- 格式验证: 支持数据格式验证
⚙️ 实现方式对比
方式一:Excel.push(推荐,最简单)
- 优点: 自动处理文件读取、表头映射、逐行上传,代码简洁
- 缺点: 需要逐行上传,不适合超大数据量
- 适用场景: 中小文件导入、需要逐行验证和处理
方式二:PHP后端解析(推荐用于大文件)
- 优点: 服务器端处理,适合大文件,不占用客户端资源
- 缺点: 需要先上传文件到服务器
- 适用场景: 大文件导入、需要服务器端复杂处理
方式三:xlsx.js高级用法(自定义处理)
- 优点: 完全自定义,可以批量上传或自定义处理逻辑
- 缺点: 需要手动处理文件读取和数据转换
- 适用场景: 需要自定义处理逻辑、批量上传等高级场景
方式一:Excel.push
Excel.push 是 ThinkAdmin 提供的 Excel 导入插件方法,可以自动处理文件选择、读取、表头映射和逐行上传,是最简单易用的方式。
实现流程
- 用户点击导入按钮
- 自动弹出文件选择器
- 自动读取Excel文件内容
- 根据表头映射自动解析数据
- 逐行上传数据到服务器
- 显示导入进度和结果统计
1. 前端HTML代码
{extend name="admin@public/container" /}
{block name="content"}
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-form-item">
<!-- 导入按钮 -->
<!--{if auth("import")}-->
<button class='layui-btn layui-btn-sm layui-btn-active' id="importBtn">
<i class="layui-icon"></i> 导入Excel
</button>
<!--{/if}-->
</div>
</div>
</div>
{/block}2. 前端JavaScript代码
{block name='script'}
<script>
$(function() {
// 点击导入按钮
$('#importBtn').on('click', function() {
require(['excel'], function(Excel) {
// 使用 Excel.push 方法
// 参数1: 上传地址
// 参数2: Excel工作表名称(Sheet名称)
// 参数3: 表头映射配置 { _: 起始行, 'Excel表头': '数据库字段' }
// 参数4: 数据过滤函数(可选)
Excel.push('{:url("import")}', 'Sheet1', {
_: 1, // 数据起始行(1表示第一行是表头,从第二行开始读取数据)
'序号': 'no', // Excel表头 => 数据库字段
'题目': 'name',
'答案': 'answer',
'类型': 'type',
'难度': 'level'
}, function(item) {
// 数据过滤处理函数(可选)
// 参数 item: 当前行的数据对象
// 返回 false: 跳过该条记录,不导入
// 返回 item: 导入该条记录(可以修改item后再返回)
// 示例:跳过题目为空的记录
if (!item.name || item.name.trim() === '') {
return false;
}
// 示例:数据转换
if (item.type === '单选题') {
item.type = 1;
} else if (item.type === '多选题') {
item.type = 2;
}
// 返回处理后的数据
return item;
});
});
});
});
</script>
{/block}3. 后端PHP代码
后端需要接收逐行上传的数据,每次只接收一条记录:
<?php
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\model\SystemUser;
/**
* 用户管理
* @class User
* @package app\admin\controller
*/
class User extends Controller
{
/**
* 导入用户数据(逐行接收)
* @auth true
*/
public function import()
{
// Excel.push 会逐行上传数据,每次只接收一条记录
$data = $this->request->post();
// 数据验证
$this->_vali([
'username.require' => '用户名不能为空!',
'nickname.require' => '昵称不能为空!',
]);
// 检查用户名是否已存在
if (SystemUser::mk()->where('username', $data['username'])->count() > 0) {
$this->error('用户名已存在:' . $data['username']);
}
// 设置默认值
$data['password'] = md5($data['username']); // 默认密码与用户名相同
$data['status'] = 1;
$data['create_at'] = date('Y-m-d H:i:s');
// 保存数据
try {
SystemUser::mk()->save($data);
$this->success('导入成功');
} catch (\Exception $e) {
$this->error('导入失败:' . $e->getMessage());
}
}
}Excel.push 参数说明
Excel.push(url, sheet, cols, filter)参数说明:
url(string, 必需): 数据上传的URL地址sheet(string, 必需): Excel工作表名称,如'Sheet1'、'用户信息'等cols(object, 必需): 表头映射配置_(number): 数据起始行,1表示第一行是表头,从第二行开始读取数据'Excel表头'(string): Excel表头名称 => 数据库字段名
filter(function, 可选): 数据过滤处理函数- 参数:
item- 当前行的数据对象 - 返回:
false跳过该条记录,item导入该条记录
- 参数:
Excel.push 特性
- 自动文件选择: 自动创建文件选择器,无需手动处理
- 自动读取文件: 使用
layui.excel.importExcel自动读取Excel文件 - 自动表头映射: 根据配置自动匹配Excel表头和数据库字段
- 逐行上传: 自动逐行上传数据,显示进度和统计
- 数据过滤: 支持数据过滤和转换
- 进度显示: 自动显示导入进度(读取进度、上传进度、成功/失败统计)
- 日期处理: 自动处理Excel日期格式
完整示例:用户数据导入
{extend name="admin@public/container" /}
{block name="content"}
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-form-item">
<button class='layui-btn layui-btn-sm layui-btn-active' id="importUserBtn">
<i class="layui-icon"></i> 导入用户
</button>
</div>
</div>
</div>
{/block}
{block name='script'}
<script>
$(function() {
$('#importUserBtn').on('click', function() {
require(['excel'], function(Excel) {
Excel.push('{:url("import")}', 'Sheet1', {
_: 1, // 第一行是表头,从第二行开始读取
'用户名': 'username',
'昵称': 'nickname',
'手机号': 'phone',
'邮箱': 'email',
'状态': 'status'
}, function(item) {
// 数据过滤和转换
// 跳过用户名为空的记录
if (!item.username || item.username.trim() === '') {
return false;
}
// 转换状态值
if (item.status === '正常' || item.status === '启用') {
item.status = 1;
} else if (item.status === '禁用' || item.status === '停用') {
item.status = 0;
} else {
item.status = 1; // 默认启用
}
// 设置默认值
if (!item.phone) item.phone = '';
if (!item.email) item.email = '';
return item;
});
});
});
});
</script>
{/block}后端代码:
<?php
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\model\SystemUser;
/**
* 用户管理
* @class User
* @package app\admin\controller
*/
class User extends Controller
{
/**
* 导入用户数据(逐行接收)
* @auth true
*/
public function import()
{
// Excel.push 逐行上传,每次只接收一条记录
$data = $this->request->post();
// 数据验证
$this->_vali([
'username.require' => '用户名不能为空!',
'nickname.require' => '昵称不能为空!',
]);
// 检查用户名是否已存在
if (SystemUser::mk()->where('username', $data['username'])->count() > 0) {
$this->error('用户名已存在:' . $data['username']);
}
// 设置默认值
$data['password'] = md5($data['username']);
$data['status'] = intval($data['status'] ?? 1);
$data['create_at'] = date('Y-m-d H:i:s');
// 保存数据
try {
SystemUser::mk()->save($data);
$this->success('导入成功');
} catch (\Exception $e) {
$this->error('导入失败:' . $e->getMessage());
}
}
}方式二:PHP后端解析
实现流程
- 前端上传Excel文件到服务器
- 后端接收文件并保存
- 后端使用PhpSpreadsheet解析文件
- 后端处理数据并写入数据库
1. 前端HTML代码
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon"></i> 搜 索</button>
<!-- 上传按钮 -->
<!--{if auth("import")}-->
<button class='layui-btn layui-btn-sm layui-btn-active'
data-file
data-type="xlsx,xls"
data-uptype='local'
data-field="topic_excel">上传题库</button>
<!--{/if}-->
</div>说明:
data-uptype设置上传文件存储方式,这里设置的是local,后端读取文件内容会用到(未测试云存储方式)
2. 前端JavaScript代码
{block name='script'}
<script>
window.form.render();
$(function () {
/*!文件上传过程及事件处理 */
$('[data-file]').on('upload.done', function (event, obj) {
// obj.file 当前完成的文件对象
// obj.data 当前文件上传后服务端返回的内容
console.log(obj.file);
console.log(obj.data);
/*! 提交数据并返回结果 */
if(obj.file.xurl) {
$.form.load('{:url("import")}', {file: obj.file.xurl}, 'post');
}
});
});
</script>
{/block}说明: 文件上传js部分可参考:前端-文件上传
3. 后端PHP代码
安装依赖
composer require phpoffice/phpspreadsheet控制器代码
<?php
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use PhpOffice\PhpSpreadsheet\IOFactory;
/**
* 题库管理
* @class Topic
* @package app\admin\controller
*/
class Topic extends Controller
{
/**
* 导入Excel文件
* @auth true
*/
public function import()
{
$file = $this->app->request->post('file');
if (!$file) {
$this->error('文件不能为空');
}
// 处理文件路径
$file = '.' . str_replace($this->app->request->domain(), '', $file);
// 表格字段对应关系(Excel列名 => 数据库字段名)
$cellName = [
'A' => 'no', // 序号
'B' => 'name', // 题目
'C' => 'answer', // 答案
];
// 加载Excel文件
$spreadsheet = IOFactory::load($file);
$sheet = $spreadsheet->getActiveSheet(); // 获取活动工作表
$highestRow = $sheet->getHighestRow(); // 取得总行数
$sheetData = [];
// 从第2行开始读取(第1行通常是表头)
for ($row = 2; $row <= $highestRow; $row++) {
foreach ($cellName as $cell => $field) {
$value = $sheet->getCell($cell . $row)->getValue();
$value = trim($value);
if (!empty($value)) {
$sheetData[$row][$field] = $value;
}
}
}
// 移除空行
$sheetData = array_values(array_filter($sheetData));
$count = count($sheetData);
if ($count < 1) {
$this->error('未读取到有效数据');
}
// 数据入库处理
try {
$this->app->db->name('topic')->data($sheetData)->strict(false)->insertAll();
$this->success("成功导入 {$count} 条记录");
} catch (\Exception $e) {
$this->error('导入失败:' . $e->getMessage());
}
}
}说明: 简单demo仅供参考,多sheet、表格格式等未做校验,可根据自己要求完善
方式三:xlsx.js高级用法
适用于需要完全自定义处理逻辑的场景,如批量上传、数据预处理等。
实现流程
- 前端选择Excel文件
- 前端使用
xlsx.js读取文件内容 - 前端将解析后的数据转换为JSON
- 前端将JSON数据提交到后端(可批量或自定义)
- 后端接收数据并写入数据库
1. 前端HTML代码
<div class="layui-form-item layui-inline">
<button class='layui-btn layui-btn-sm layui-btn-active' id="importExcelBtn">
<i class="layui-icon"></i> 导入Excel
</button>
<!-- 隐藏的文件选择器 -->
<input type="file"
id="excelFileInput"
accept=".xlsx,.xls"
style="display: none;">
</div>2. 前端JavaScript代码
{block name='script'}
<script>
$(function() {
// 点击按钮触发文件选择
$('#importExcelBtn').on('click', function() {
$('#excelFileInput').click();
});
// 文件选择后读取Excel内容
$('#excelFileInput').on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
// 验证文件类型
var fileName = file.name;
var fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
if (fileExt !== 'xlsx' && fileExt !== 'xls') {
$.msg.error('请选择Excel文件(.xlsx 或 .xls)');
return;
}
// 显示加载提示
var loadIdx = $.msg.loading('正在读取Excel文件...');
// 使用 FileReader 读取文件
var reader = new FileReader();
reader.onload = function(e) {
try {
// 加载 xlsx 库
require(['xlsx'], function(XLSX) {
var data = new Uint8Array(e.target.result);
var workbook = XLSX.read(data, {type: 'array'});
// 获取第一个工作表
var firstSheetName = workbook.SheetNames[0];
var worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为JSON(第一行作为表头)
var jsonData = XLSX.utils.sheet_to_json(worksheet, {
header: 1, // 使用数组格式,第一行作为表头
defval: '' // 空单元格默认值
});
// 关闭加载提示
$.msg.close(loadIdx);
if (jsonData.length < 2) {
$.msg.error('Excel文件中没有数据');
return;
}
// 处理数据:第一行是表头,从第二行开始是数据
var headers = jsonData[0]; // 表头
var rows = jsonData.slice(1); // 数据行
// 将数据转换为对象数组
var dataList = [];
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
if (row.every(function(cell) { return cell === ''; })) {
continue; // 跳过空行
}
var item = {};
for (var j = 0; j < headers.length; j++) {
var header = headers[j] || 'col' + j;
item[header] = row[j] || '';
}
dataList.push(item);
}
if (dataList.length < 1) {
$.msg.error('未读取到有效数据');
return;
}
// 显示确认对话框
$.msg.confirm('共读取到 ' + dataList.length + ' 条数据,确认导入吗?', function() {
// 提交数据到后端(批量上传)
var submitIdx = $.msg.loading('正在导入数据...');
$.form.load('{:url("import")}', {
data: JSON.stringify(dataList),
count: dataList.length
}, 'post', function(ret) {
$.msg.close(submitIdx);
if (ret.code > 0) {
$.msg.success(ret.info || '导入成功', 3, function() {
$.layTable.reload('TopicData'); // 刷新表格
});
} else {
$.msg.error(ret.info || '导入失败');
}
}, true);
});
});
} catch (error) {
$.msg.close(loadIdx);
$.msg.error('读取Excel文件失败:' + error.message);
console.error(error);
}
};
reader.onerror = function() {
$.msg.close(loadIdx);
$.msg.error('读取文件失败');
};
// 读取文件为ArrayBuffer
reader.readAsArrayBuffer(file);
});
});
</script>
{/block}3. 后端PHP代码
<?php
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
/**
* 题库管理
* @class Topic
* @package app\admin\controller
*/
class Topic extends Controller
{
/**
* 导入Excel数据(批量接收)
* @auth true
*/
public function import()
{
$data = $this->request->post('data');
$count = intval($this->request->post('count', 0));
if (empty($data)) {
$this->error('数据不能为空');
}
// 解析JSON数据
$dataList = json_decode($data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->error('数据格式错误:' . json_last_error_msg());
}
if (empty($dataList) || !is_array($dataList)) {
$this->error('未读取到有效数据');
}
// 字段映射(Excel表头 => 数据库字段)
$fieldMap = [
'序号' => 'no',
'题目' => 'name',
'答案' => 'answer',
];
// 转换数据格式
$insertData = [];
$errors = [];
foreach ($dataList as $index => $row) {
$item = [];
foreach ($row as $key => $value) {
// 根据字段映射转换
$field = $fieldMap[$key] ?? strtolower($key);
$item[$field] = trim($value);
}
// 数据验证
if (empty($item['name'])) {
$errors[] = "第 " . ($index + 2) . " 行:题目不能为空";
continue;
}
$insertData[] = $item;
}
if (empty($insertData)) {
$this->error('没有有效的数据需要导入' . (!empty($errors) ? ':' . implode(';', array_slice($errors, 0, 5)) : ''));
}
// 数据入库
try {
$this->app->db->name('topic')->data($insertData)->strict(false)->insertAll();
$message = '成功导入 ' . count($insertData) . ' 条记录';
if (!empty($errors)) {
$message .= ',' . count($errors) . ' 条记录导入失败:' . implode(';', array_slice($errors, 0, 5));
}
$this->success($message);
} catch (\Exception $e) {
$this->error('导入失败:' . $e->getMessage());
}
}
}三种方式对比
| 特性 | Excel.push | PHP后端解析 | xlsx.js高级用法 |
|---|---|---|---|
| 实现难度 | ⭐ 最简单 | ⭐⭐ 中等 | ⭐⭐⭐ 较复杂 |
| 代码量 | 最少 | 中等 | 最多 |
| 文件上传 | ❌ 无需上传 | ✅ 需要上传 | ❌ 无需上传 |
| 服务器负载 | 中等(逐行上传) | 较高(处理文件) | 较低(只处理数据) |
| 客户端资源 | 占用较少 | 不占用 | 占用较多 |
| 大文件支持 | ⚠️ 受逐行上传限制 | ✅ 支持 | ⚠️ 受浏览器限制 |
| 实时进度 | ✅ 自动显示 | ❌ 不支持 | ✅ 可自定义 |
| 数据验证 | ✅ 逐行验证 | ✅ 批量验证 | ✅ 可自定义 |
| 错误处理 | ✅ 自动统计 | ⚠️ 需手动处理 | ✅ 可自定义 |
| 适用场景 | 中小文件、逐行处理 | 大文件、批量处理 | 自定义处理逻辑 |
📋 Excel.push 说明
表头映射配置
{
_: 1, // 数据起始行
'Excel表头1': '数据库字段1',
'Excel表头2': '数据库字段2',
// ...
}说明:
_: 数据起始行,1表示第一行是表头,从第二行开始读取数据'Excel表头': Excel文件中的表头名称(必须完全匹配)'数据库字段': 对应的数据库字段名
数据过滤函数
function(item) {
// item 是当前行的数据对象,已根据表头映射转换
// 例如:{ no: '1', name: '题目1', answer: '答案1' }
// 返回 false: 跳过该条记录
if (!item.name) {
return false;
}
// 返回 item: 导入该条记录(可以修改后再返回)
item.create_at = new Date().toISOString();
return item;
}进度显示
Excel.push 会自动显示导入进度:
- 读取阶段: 显示"读取 X.XX%"
- 上传阶段: 显示"更新 X.XX%(成功 X 条, 失败 X 条)"
- 完成: 显示"共处理 X 条记录(成功 X 条, 失败 X 条)"
日期格式处理
Excel.push 会自动处理Excel日期格式,将Excel日期码转换为 YYYY-MM-DD HH:ii:ss 格式。
多工作表支持
如果Excel文件有多个工作表,需要指定工作表名称:
Excel.push('{:url("import")}', '用户信息', {
_: 1,
'用户名': 'username',
'昵称': 'nickname'
});完整示例:题库导入
{extend name="admin@public/container" /}
{block name="content"}
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-form-item">
<button class='layui-btn layui-btn-sm layui-btn-active' id="importTopicBtn">
<i class="layui-icon"></i> 导入题库
</button>
</div>
</div>
</div>
{/block}
{block name='script'}
<script>
$(function() {
$('#importTopicBtn').on('click', function() {
require(['excel'], function(Excel) {
Excel.push('{:url("import")}', 'Sheet1', {
_: 1, // 第一行是表头
'序号': 'no',
'题目': 'name',
'选项A': 'option_a',
'选项B': 'option_b',
'选项C': 'option_c',
'选项D': 'option_d',
'正确答案': 'answer',
'类型': 'type',
'难度': 'level',
'解析': 'analysis'
}, function(item) {
// 数据过滤和转换
// 跳过题目为空的记录
if (!item.name || item.name.trim() === '') {
return false;
}
// 转换类型
if (item.type === '单选题') {
item.type = 1;
} else if (item.type === '多选题') {
item.type = 2;
} else if (item.type === '判断题') {
item.type = 3;
} else {
item.type = 1; // 默认单选题
}
// 转换难度
if (item.level === '简单') {
item.level = 1;
} else if (item.level === '中等') {
item.level = 2;
} else if (item.level === '困难') {
item.level = 3;
} else {
item.level = 1; // 默认简单
}
// 设置默认值
if (!item.option_a) item.option_a = '';
if (!item.option_b) item.option_b = '';
if (!item.option_c) item.option_c = '';
if (!item.option_d) item.option_d = '';
if (!item.analysis) item.analysis = '';
return item;
});
});
});
});
</script>
{/block}后端代码:
<?php
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
/**
* 题库管理
* @class Topic
* @package app\admin\controller
*/
class Topic extends Controller
{
/**
* 导入题目(逐行接收)
* @auth true
*/
public function import()
{
// Excel.push 逐行上传,每次只接收一条记录
$data = $this->request->post();
// 数据验证
$this->_vali([
'name.require' => '题目不能为空!',
'answer.require' => '正确答案不能为空!',
'type.in:1,2,3' => '题目类型不正确!',
]);
// 设置默认值
$data['create_at'] = date('Y-m-d H:i:s');
$data['status'] = 1;
// 保存数据
try {
$this->app->db->name('topic')->save($data);
$this->success('导入成功');
} catch (\Exception $e) {
$this->error('导入失败:' . $e->getMessage());
}
}
}📋 注意事项
Excel.push 注意事项
- 表头匹配: Excel表头必须与配置中的表头名称完全匹配(区分大小写)
- 工作表名称: 确保工作表名称正确,默认是
Sheet1 - 数据起始行:
_参数表示数据起始行,1表示第一行是表头 - 逐行上传: 数据是逐行上传的,后端每次只接收一条记录
- 错误处理: 如果某条记录导入失败,会继续处理下一条记录
- 进度显示: 会自动显示导入进度和成功/失败统计
PHP后端解析注意事项
- 文件存储: 确保服务器有足够的存储空间
- 文件路径: 正确处理文件路径,支持本地和云存储
- 内存限制: 大文件可能需要调整PHP内存限制
- 执行时间: 大文件导入可能需要调整PHP执行时间限制
xlsx.js高级用法注意事项
- 文件大小: 建议文件大小不超过10MB
- 浏览器兼容: 确保浏览器支持FileReader API
- 内存占用: 大文件可能占用较多浏览器内存
- 数据格式: 确保Excel文件格式规范,表头清晰
通用注意事项
- 文件格式: 确保Excel文件格式正确,表头清晰
- 数据验证: 导入前进行数据验证,避免脏数据
- 错误处理: 完善的错误处理机制,提示用户具体错误
- 权限控制: 使用
auth()函数控制导入权限
📋 常见问题
Q: Excel.push 如何指定多个工作表? A: 在 Excel.push 的第二个参数中指定工作表名称即可。
Q: Excel.push 如何处理日期格式? A: Excel.push 会自动处理Excel日期格式,将日期码转换为 YYYY-MM-DD HH:ii:ss 格式。
Q: Excel.push 导入失败怎么办? A: Excel.push 会显示失败统计,后端返回 code: 0 的记录会被计入失败数。可以在后端返回具体的错误信息。
Q: 如何自定义Excel.push的进度显示? A: Excel.push 的进度显示是自动的,无法自定义。如果需要自定义,可以使用 xlsx.js 高级用法。
Q: Excel.push 支持哪些文件格式? A: 支持 .xlsx 和 .xls 格式的Excel文件。
提示:
- 推荐使用 Excel.push,这是最简单易用的方式
- 大文件推荐使用 PHP后端解析
- 需要完全自定义处理逻辑时,使用 xlsx.js 高级用法
