📊 Excel 导出

ThinkAdmin 后台的 Excel 导出由前端静态模块 plugs/admin/excel.js 实现。它会在浏览器端分页请求 JSON 数据,合并结果后调用 layui.excel.exportExcel() 生成并下载 .xlsx 文件,服务端不需要生成 Excel 文件。

think\admin\extend\ExcelExtend 是已废弃的 CSV 导出辅助类,不是当前后台页面使用的 XLSX 导出路径。

基本流程

  1. 页面按钮通过 data-form-exportdata-excel 指定导出地址。
  2. excel.bind() 绑定点击事件。
  3. 点击导出时读取按钮所在表单的序列化数据。
  4. 前端自动追加 output=json&not_cache_limit=1&limit=100&page=N 分页请求数据。
  5. 后端返回 code=1,并在 data.listdata.page 中提供当前页数据和分页信息。
  6. 前端把每页 data.list 合并为一个数组,交给 excel.bind() 的回调转换成 Excel 数据。
  7. 回调返回二维数组或多工作表对象后,前端下载 .xlsx 文件。

按钮绑定

最常用写法是在搜索表单中放置 data-form-export 按钮:

<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" method="get">
    <div class="layui-form-item layui-inline">
        <label class="layui-form-label">关键词</label>
        <label class="layui-input-inline">
            <input name="keyword" value="{$get.keyword|default=''}" placeholder="请输入关键词" class="layui-input">
        </label>
    </div>

    <div class="layui-form-item layui-inline">
        <button type="submit" class="layui-btn layui-btn-primary">
            <i class="layui-icon">&#xe615;</i> 搜 索
        </button>
        <button type="button" data-form-export="{:url('index')}" class="layui-btn layui-btn-primary">
            <i class="layui-icon layui-icon-export"></i> 导 出
        </button>
    </div>
</form>

data-form-export 的取值规则:

写法说明
data-form-export使用按钮所在表单的 action 作为请求地址
data-form-export="{:url('index')}"使用指定地址请求导出数据
data-method="post"覆盖表单 method,默认取表单 method,再默认 get
data-filename="用户数据"覆盖 excel.bind() 传入的文件名

如果列表使用 layTable 并发生排序,admin.js 会把排序字段写到导出按钮的 data-sort-fielddata-sort-type,导出请求会追加 _field__order_

前端示例

实际页面通常只需要把后端对象数组转换成二维数组,并在第一行插入表头:

<script>
require(['excel'], function (excel) {
    excel.bind(function (data) {
        data.forEach(function (item, index) {
            data[index] = [
                item.id || 0,
                item.username || '',
                item.node || '',
                item.geoip || '',
                item.geoisp || '',
                item.action || '',
                item.content || '',
                item.create_at || ''
            ];
        });

        data.unshift(['ID', '操作账号', '操作节点', '访问地址', '网络服务商', '操作行为', '操作内容', '创建时间']);

        return this.withStyle(data, {A: 60, B: 80, C: 99, E: 120, G: 120});
    }, '操作日志' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'));
});
</script>

excel.bind(done, filename, selector, options) 参数说明:

参数说明
done数据转换回调,第 1 个参数是合并后的 data.list 数组,第 2 个参数是源码传入的空数组
filename默认文件名,源码当前会追加 .xlsx 后缀
selector自定义触发选择器,默认 [data-form-export]
options合并到导出配置,默认包含 writeOpt: {bookSST: false}

自定义触发

自定义导出按钮使用 data-excel 指定请求地址,再把选择器传给 excel.bind()

<button type="button" id="exportOrderBtn" data-excel="{:url('index')}?type=order" class="layui-btn layui-btn-primary">
    <i class="layui-icon layui-icon-export"></i> 导出订单
</button>

<script>
require(['excel'], function (excel) {
    excel.bind(function (data) {
        data.forEach(function (item, index) {
            data[index] = [
                item.id || 0,
                item.order_no || '',
                item.order_amount || '',
                item.create_time || ''
            ];
        });
        data.unshift(['ID', '订单编号', '订单金额', '创建时间']);
        return this.withStyle(data, {C: 120, D: 170});
    }, '订单数据' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'), '#exportOrderBtn');
});
</script>

Excel.push() 属于导入流程:它会读取本地 Excel 文件并逐行 POST 到后端,不能用于导出。

直接导出

如果数据已经在前端准备好,可以直接调用 excel.export(data, filename, options)

require(['excel'], function (excel) {
    var data = [
        ['名称', '数量'],
        ['已支付订单', 128],
        ['待支付订单', 16]
    ];

    excel.export(data, '订单统计');
});

源码当前的后缀判断使用 name.substring(0, -5),实际会为文件名追加 .xlsx 后缀。建议传入不带扩展名的文件名,避免出现 订单统计.xlsx.xlsx

后端返回

导出请求使用 output=json,后端需要返回根级 code=1,并在 data 中包含 listpage。使用 QueryHelper::layTable() 时,这个结构由 PageHelper 自动处理:

<?php
declare(strict_types=1);

namespace app\admin\controller;

use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemOplog;

class Oplog extends Controller
{
    /**
     * 系统日志管理
     * @auth true
     * @menu true
     */
    public function index()
    {
        SystemOplog::mQuery()->layTable(function () {
            $this->title = '系统日志管理';
        }, static function (QueryHelper $query) {
            $query->dateBetween('create_at')->equal('username,action')->like('content,geoip,node');
        });
    }

    /**
     * 列表数据处理
     */
    protected function _index_page_filter(array &$data)
    {
        foreach ($data as &$vo) {
            $vo['geoisp'] = $vo['geoisp'] ?? '';
        }
    }
}

返回结构示例:

{
    "code": 1,
    "info": "JSON-DATA",
    "data": {
        "page": {
            "limit": 100,
            "total": 120,
            "pages": 2,
            "current": 1
        },
        "list": [
            {
                "id": 1,
                "username": "admin",
                "node": "/admin/oplog/index",
                "geoip": "127.0.0.1",
                "geoisp": "内网IP",
                "action": "查询",
                "content": "查看系统日志",
                "create_at": "2026-01-01 12:00:00"
            }
        ]
    }
}

若不使用 layTable(),也可以自行调用分页并返回相同结构:

if ($this->request->get('output') === 'json') {
    $result = SystemOplog::mQuery()
        ->dateBetween('create_at')
        ->equal('username,action')
        ->like('content,geoip,node')
        ->order('id desc')
        ->page(true, false);

    $this->success('JSON-DATA', $result);
}

前端 excel.load() 只在收到 ret.data.page 后继续调度下一页。正常导出接口必须返回 data.page.currentdata.page.pages

样式设置

withStyle(data, colsWidth, defaultWidth, defaultHeight) 是当前封装提供的快捷样式方法:

return this.withStyle(data, {
    A: 60,
    B: 100,
    C: 170
}, 99, 28);

实际效果:

  • 根据 data[0] 计算列范围。
  • 表头行使用绿色背景、白色加粗字体和居中对齐。
  • 从第 2 行开始设置交替行背景色。
  • 默认列宽为 defaultWidth || 99,A 列默认 60,最后一列默认 160。
  • 第 1 行高度为 33,最后一行高度为 defaultHeight || 28
  • 通过 this.options.extend 写入 layui.excel!cols!rows 配置。

需要更复杂样式时,可以直接调用 layui.excel.setExportCellStyle()layui.excel.makeColConfig()layui.excel.makeRowConfig(),再把配置合并到 this.options.extend

多工作表

layui.excel.exportExcel() 支持对象形式的多工作表数据,excel.bind() 的回调可以直接返回对象:

require(['excel'], function (excel) {
    excel.bind(function (data) {
        var rows = [['ID', '账号', '创建时间']];
        data.forEach(function (item) {
            rows.push([item.id || 0, item.username || '', item.create_at || '']);
        });

        return {
            '用户列表': rows,
            '统计信息': [
                ['项目', '数量'],
                ['导出数量', rows.length - 1]
            ]
        };
    }, '用户数据');
});

注意事项

  • 导出是浏览器端生成文件,大数据量会占用浏览器内存。
  • 前端固定按 limit=100 分页拉取数据,后端应支持 pagelimit 参数。
  • not_cache_limit=1 用于避免本次 limit=100 写入后台分页条数 Cookie。
  • 失败响应只会提示 数据加载异常数据加载失败,复杂错误信息应在后端日志中记录。
  • data-form-export 会序列化按钮所在表单;导出按钮应放在搜索表单内,或使用自定义选择器自行组织请求。

常见问题

为什么导出没有开始下载?

优先检查后端是否返回了 code=1data.listdata.page。当前实现收到 data.page 后才会继续分页并最终完成导出。

搜索条件会一起导出吗?

会。点击按钮时会读取按钮所在表单,并通过 form.serialize() 把搜索条件一起提交给导出接口。

表格排序会影响导出吗?

使用 layTable 且导出按钮能被表格关联到时,会同步当前排序字段。请求中会包含 _field__order_

能否在 PHP 中直接生成 Excel?

当前后台封装没有内置 PHP XLSX 导出器。需要服务端生成文件时,应自行接入 phpoffice/phpspreadsheet 等库,或改用业务自定义下载接口。

最近更新:
Contributors: 邹景立, Anyon