📥 Excel 导入
ThinkAdmin 当前没有 PHP 端内置 Excel 解析导入器。后台静态资源提供的是前端 excel 模块,其中 Excel.push() 会在浏览器中读取 Excel 文件,然后逐行把解析后的数据 POST 到后端接口。
如果需要服务端解析 .xlsx / .xls 文件,需要业务自行安装并接入 phpoffice/phpspreadsheet 等库。think\admin\extend\ExcelExtend 只是已废弃的 CSV 导出辅助类,不负责 Excel 导入。
Excel.push 流程
Excel.push(url, sheet, cols, filter) 的实际流程:
- 创建隐藏文件选择器,限制选择 Excel MIME 类型。
- 使用
layui.excel.importExcel()在浏览器读取文件。 - 从指定工作表中读取表头行。
- 按
cols映射把每一行转换成对象。 - 可选执行
filter(item)。 - 使用
$.form.load(url, item, 'post', ...)逐行提交。 - 根据每行响应
ret.code统计成功和失败数量。 - 完成后自动
$.form.reload()。
参数说明
Excel.push(url, sheet, cols, filter)| 参数 | 说明 |
|---|---|
url | 每行数据 POST 的后端地址 |
sheet | 工作表名称,例如 Sheet1、用户信息 |
cols | 字段映射配置 |
filter | 行数据过滤函数,返回 false 时该行不提交并计入失败;其他返回值会作为最终提交数据 |
cols 的方向是“后端字段名 => Excel 表头名”,不是“Excel 表头名 => 后端字段名”:
{
_: 1,
username: '用户名',
nickname: '昵称',
phone: '手机号'
}实际实现会在第 _ 行查找表头。建议保持 _ 为 1,表示第 1 行是表头,从第 2 行开始读取数据。当前源码对 _ > 1 的场景处理不完整,表头前的行也可能被纳入处理并形成空记录,通常需要在 filter 中拦截。
前端示例
<button type="button" class="layui-btn layui-btn-sm" id="importUserBtn">
<i class="layui-icon"></i> 导入用户
</button>
<script>
require(['excel'], function (Excel) {
$('#importUserBtn').on('click', function () {
Excel.push('{:url("import")}', 'Sheet1', {
_: 1,
username: '用户名',
nickname: '昵称',
phone: '手机号',
status: '状态'
}, function (item) {
if (!item.username) {
return false;
}
if (item.status === '正常' || item.status === '启用') {
item.status = 1;
} else if (item.status === '禁用' || item.status === '停用') {
item.status = 0;
} else {
item.status = 1;
}
return item;
});
});
});
</script>上传到后端的单行数据类似:
{
"username": "zhangsan",
"nickname": "张三",
"phone": "13800138000",
"status": 1
}后端示例
后端接口每次只接收一行数据。_vali() 只会返回规则中定义的字段,因此需要显式声明要接收的字段:
<?php
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\model\SystemUser;
use think\exception\HttpResponseException;
class User extends Controller
{
/**
* 导入用户数据
* @auth true
*/
public function import()
{
$data = $this->_vali([
'username.require' => '用户名不能为空!',
'nickname.require' => '昵称不能为空!',
'phone.default' => '',
'status.default' => 1,
]);
if (SystemUser::mk()->where(['username' => $data['username']])->count() > 0) {
$this->error("用户名已存在:{$data['username']}");
}
try {
$data['password'] = md5($data['username']);
$data['status'] = intval($data['status']);
$data['create_at'] = date('Y-m-d H:i:s');
SystemUser::mk()->save($data);
$this->success('导入成功');
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Throwable $exception) {
$this->error('导入失败:' . $exception->getMessage());
}
}
}Excel.push() 不会汇总失败明细,只会按每行响应 code 统计成功和失败。需要记录详细错误时,应在后端写日志,或改用自定义批量提交方案。
数据转换
过滤函数可以修改当前行对象,但必须返回要提交的对象;如果只修改 item 而没有 return item,当前实现会把返回值 undefined 作为提交数据,最终等同于提交空对象。
Excel.push('{:url("import")}', '题库', {
_: 1,
title: '题目',
answer: '答案',
type: '题型'
}, function (item) {
if (!item.title || !item.answer) {
return false;
}
item.type = item.type === '多选题' ? 2 : 1;
item.create_at = layui.util.toDateString(Date.now(), 'yyyy-MM-dd HH:mm:ss');
return item;
});日期处理边界:
- 当单元格值匹配
数字.12位小数时,源码会调用LAY_EXCEL.dateCodeFormat(v, 'YYYY-MM-DD HH:ii:ss')。 - 普通日期文本、自定义日期格式和其他数字格式不会自动转换,需要在
filter中自行处理。
工作表和表头
Excel.push() 只读取传入名称对应的工作表:
Excel.push('{:url("import")}', '用户信息', {
_: 1,
username: '账号',
nickname: '姓名'
});如果文件中不存在该工作表,会提示 未读取到表[用户信息]的数据。
表头匹配使用严格相等比较,表头文字中的空格、换行、大小写差异都会导致字段无法映射。建议导入模板固定表头,或在自定义读取方案中做表头归一化。若业务文件确实存在说明行、标题行等非数据内容,建议使用自定义批量导入方案自行控制起始行。
自定义批量导入
如果不适合逐行提交,可以直接使用前端 xlsx 模块自行读取文件,再批量提交 JSON。该方式不经过 Excel.push(),进度、错误统计和表头映射都需要自行实现。
简化示例:
require(['xlsx'], function (XLSX) {
var reader = new FileReader();
reader.onload = function (event) {
var workbook = XLSX.read(new Uint8Array(event.target.result), {type: 'array'});
var sheet = workbook.Sheets[workbook.SheetNames[0]];
var rows = XLSX.utils.sheet_to_json(sheet, {defval: ''});
$.form.load('{:url("importBatch")}', {
rows: JSON.stringify(rows)
}, 'post');
};
reader.readAsArrayBuffer(file);
});后端示例:
public function importBatch()
{
$rows = json_decode($this->request->post('rows', '[]'), true);
if (empty($rows) || !is_array($rows)) {
$this->error('未读取到有效数据');
}
// 业务自行映射字段、验证、去重、入库。
$this->success('导入成功');
}服务端解析
服务端解析不是 ThinkAdmin 内置能力,需要自行安装库:
composer require phpoffice/phpspreadsheet然后按业务上传文件并读取。若使用后台上传组件上传 Excel 文件,请注意:
- 默认允许后缀由
storage.allow_exts控制,默认包含xls、xlsx。 data-size只是前端大小限制。- 安全上传会返回本地安全文件键,不是公开 URL。
- 云存储 URL 不一定能直接转成本地路径,服务端解析更适合本地上传或先下载到本地临时文件。
常见问题
为什么导入后字段都是空?
优先检查 cols 映射方向。实际写法应为:
{
username: '用户名'
}而不是:
{
'用户名': 'username'
}为什么提示未读取到工作表?
第二个参数必须和 Excel 文件中的工作表名称一致。常见默认值是 Sheet1,中文表格可能是 用户信息、题库 等自定义名称。
跳过的行会算成功吗?
不会。filter(item) 返回 false 时,源码会增加失败计数并继续下一行。
可以自定义进度提示吗?
Excel.push() 的提示文案写在模块内部,不能通过参数配置。需要完全自定义时,应使用 xlsx 模块自行实现读取和提交。
支持哪些文件?
文件选择器限制了 Excel MIME 类型,底层依赖 layui.excel.importExcel() 解析。实际兼容性取决于浏览器和 layui.excel/xlsx 能力。
