📥 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) 的实际流程:

  1. 创建隐藏文件选择器,限制选择 Excel MIME 类型。
  2. 使用 layui.excel.importExcel() 在浏览器读取文件。
  3. 从指定工作表中读取表头行。
  4. cols 映射把每一行转换成对象。
  5. 可选执行 filter(item)
  6. 使用 $.form.load(url, item, 'post', ...) 逐行提交。
  7. 根据每行响应 ret.code 统计成功和失败数量。
  8. 完成后自动 $.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">&#xe67c;</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 控制,默认包含 xlsxlsx
  • data-size 只是前端大小限制。
  • 安全上传会返回本地安全文件键,不是公开 URL。
  • 云存储 URL 不一定能直接转成本地路径,服务端解析更适合本地上传或先下载到本地临时文件。

常见问题

为什么导入后字段都是空?

优先检查 cols 映射方向。实际写法应为:

{
    username: '用户名'
}

而不是:

{
    '用户名': 'username'
}

为什么提示未读取到工作表?

第二个参数必须和 Excel 文件中的工作表名称一致。常见默认值是 Sheet1,中文表格可能是 用户信息题库 等自定义名称。

跳过的行会算成功吗?

不会。filter(item) 返回 false 时,源码会增加失败计数并继续下一行。

可以自定义进度提示吗?

Excel.push() 的提示文案写在模块内部,不能通过参数配置。需要完全自定义时,应使用 xlsx 模块自行实现读取和提交。

支持哪些文件?

文件选择器限制了 Excel MIME 类型,底层依赖 layui.excel.importExcel() 解析。实际兼容性取决于浏览器和 layui.excel/xlsx 能力。

最近更新:
Contributors: Anyon