📊 数据导出

ThinkAdmin 提供完善的数据导出功能,基于 layui.excel 工具实现 Excel 文件的前端导出,支持前端直接生成和下载,无需服务器生成文件。

🚀 主要功能

  • Excel 导出: 支持 Excel 文件的前端导出
  • JSON 数据: 仅需返回 JSON 内容,无需服务器生成文件
  • 前端生成: 由前端 JS 生成并下载到本地
  • 性能优化: 减少服务器负载,提升导出性能
  • 灵活配置: 支持多种导出配置和格式
  • 样式定制: 支持自定义表头和内容样式
  • 用户友好: 提供友好的导出界面和操作

📋 技术特点

基于 layui.excel

  • 官方工具: 基于 layui.excel 官方工具
  • 文档支持: 详细文档参考 http://excel.wj2015.com
  • 功能完整: 支持完整的 Excel 导出功能
  • 兼容性好: 与 LayUI 框架完美兼容

前端处理

  • 无需服务器: 不需要服务器生成 Excel 文件
  • JSON 数据: 仅需返回 JSON 格式的数据
  • 前端生成: 由前端 JavaScript 生成文件
  • 本地下载: 直接下载到用户本地
  • 自动分页: 自动处理分页数据,支持大数据量导出

⚙️ 使用场景

数据报表

  • 业务数据: 导出业务相关的数据报表
  • 统计信息: 导出统计和分析数据
  • 用户数据: 导出用户相关的数据信息
  • 财务数据: 导出财务和交易数据

系统管理

  • 日志导出: 导出系统操作日志
  • 配置导出: 导出系统配置信息
  • 备份数据: 导出重要数据备份
  • 审计数据: 导出审计和合规数据

业务应用

  • 订单导出: 导出订单和交易数据
  • 用户导出: 导出用户信息和数据
  • 内容导出: 导出内容管理数据
  • 营销数据: 导出营销活动数据

🔧 实现方式

方式一:使用 data-form-export 属性(推荐)

这是最简单的方式,只需要在按钮上添加 data-form-export 属性,并绑定 Excel 处理函数。

1. HTML 代码

<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>
        <div class="layui-input-inline">
            <input name="keyword" value="{$get.keyword|default=''}" placeholder="请输入关键词" class="layui-input">
        </div>
    </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>

2. JavaScript 代码

<script>
require(['excel'], function (excel) {
    // 绑定导出处理函数
    excel.bind(function (data) {
        // data 是从服务器获取的 JSON 数据数组
        
        // 1. 转换数据格式:将对象数组转换为二维数组
        data.forEach(function (item, index) {
            data[index] = [
                item.id,           // ID
                item.username,     // 用户名
                item.nickname,     // 昵称
                item.phone,        // 手机号
                item.email,        // 邮箱
                item.status,       // 状态
                item.create_at     // 创建时间
            ];
        });
        
        // 2. 添加表头(必须在第一行)
        data.unshift([
            'ID', 
            '用户名', 
            '昵称', 
            '手机号', 
            '邮箱', 
            '状态', 
            '创建时间'
        ]);
        
        // 3. 应用样式(可选)
        return this.withStyle(data, {
            A: 60,   // A列宽度
            B: 100,  // B列宽度
            C: 120,  // C列宽度
            D: 120,  // D列宽度
            E: 150,  // E列宽度
            F: 80,   // F列宽度
            G: 170   // G列宽度
        });
        
    }, '用户数据_' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'));
});
</script>

3. 后端代码

后端需要返回 JSON 格式的数据:

<?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 index()
    {
        // 如果是导出请求,返回 JSON 数据
        if ($this->request->get('output') === 'json') {
            $query = SystemUser::mQuery();
            $query->where(['is_deleted' => 0]);
            $query->like('username,nickname,phone,email');
            $query->equal('status');
            $query->dateBetween('create_at');
            
            // 导出时不分页,获取所有数据
            $list = $query->order('id desc')->page(true, false);
            
            $this->success('success', [
                'list' => $list,
                'page' => ['current' => 1, 'pages' => 1, 'total' => count($list)]
            ]);
        }
        
        // 正常列表显示
        SystemUser::mQuery()->layTable(function () {
            $this->title = '用户管理';
        }, function ($query) {
            $query->where(['is_deleted' => 0]);
            $query->like('username,nickname,phone,email');
            $query->equal('status');
            $query->dateBetween('create_at');
        });
    }
}

方式二:使用 Excel.push 方法(自定义导出)

适用于需要自定义导出逻辑的场景。

<script>
require(['excel'], function (excel) {
    // 自定义导出按钮
    $('#customExportBtn').on('click', function() {
        excel.bind(function (data) {
            // 处理数据
            data.forEach(function (item, index) {
                data[index] = [item.id, item.name, item.value];
            });
            data.unshift(['ID', '名称', '值']);
            
            // 应用样式
            return this.withStyle(data, {A: 60, B: 120, C: 100});
        }, '自定义导出_' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'));
        
        // 触发导出(需要先绑定 data-form-export 的按钮)
        $('[data-form-export]').trigger('click');
    });
});
</script>

📋 样式设置

使用 withStyle 方法(推荐)

withStyle 是 Excel 插件提供的快捷方法,可以快速设置表格样式。

// 基本用法
return this.withStyle(data, {
    A: 60,   // A列宽度
    B: 100,  // B列宽度
    C: 120   // C列宽度
});

// 指定默认宽度和高度
return this.withStyle(data, {
    A: 60,
    B: 100
}, 99,  // 默认列宽
28);    // 默认行高

手动设置样式

如果需要更精细的样式控制,可以手动设置:

require(['excel'], function (excel) {
    excel.bind(function (data) {
        // 转换数据
        data.forEach(function (item, index) {
            data[index] = [item.id, item.username, item.create_at];
        });
        data.unshift(['ID', '用户名', '创建时间']);
        
        // 计算最后一列
        var lastCol = layui.excel.numToTitle((function (count, idx) {
            for (idx in data[0]) count++;
            return count;
        })(0));
        
        // 设置表头样式
        layui.excel.setExportCellStyle(data, 'A1:' + lastCol + '1', {
            s: {
                font: {
                    sz: 14,           // 字体大小
                    bold: true,        // 粗体
                    color: {rgb: "FFFFFF"},  // 字体颜色(白色)
                    shadow: true,      // 阴影
                    name: '微软雅黑'   // 字体名称
                },
                fill: {
                    bgColor: {indexed: 64},
                    fgColor: {rgb: "5FB878"}  // 背景颜色(绿色)
                },
                alignment: {
                    vertical: 'center',   // 垂直居中
                    horizontal: 'center'  // 水平居中
                }
            }
        });
        
        // 设置内容样式(交替行颜色)
        var style1 = {
            fill: {bgColor: {indexed: 64}, fgColor: {rgb: "EAEAEA"}},  // 灰色
            alignment: {vertical: 'center', horizontal: 'center'}
        };
        var style2 = {
            fill: {bgColor: {indexed: 64}, fgColor: {rgb: "FFFFFF"}},  // 白色
            alignment: {vertical: 'center', horizontal: 'center'}
        };
        
        layui.excel.setExportCellStyle(data, 'A2:' + lastCol + data.length, {
            s: style1
        }, function (rawCell, newCell, row, config, currentRow, currentCol, fieldKey) {
            // 判断并转换单元格数据为对象
            typeof rawCell !== 'object' && (rawCell = {v: rawCell});
            rawCell.s = Object.assign({}, style2, rawCell.s || {});
            // 偶数行使用 style1,奇数行使用 style2
            return (currentRow % 2 === 0) ? newCell : rawCell;
        });
        
        // 设置行高和列宽
        var rowsC = {1: 40};  // 第1行(表头)高度为40
        var colsC = {A: 60, B: 100, C: 170};  // 列宽设置
        rowsC[data.length] = 33;  // 最后一行高度为33
        colsC[lastCol] = 160;     // 最后一列宽度为160
        
        this.options.extend = {
            '!rows': layui.excel.makeRowConfig(rowsC, 33),  // 默认行高33
            '!cols': layui.excel.makeRowConfig(colsC, 160)  // 默认列宽160
        };
        
        return data;
    }, '导出文件名');
});

📋 完整示例

参考系统日志模块的实现,这是一个完整的导出示例。

1. 前端HTML代码

{extend name="admin@public/container" /}

{block name="content"}
<fieldset>
    <legend>条件搜索</legend>
    <form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
        
        <div class="layui-form-item layui-inline">
            <label class="layui-form-label">操作账号</label>
            <div class="layui-input-inline">
                <select name='username' lay-search class="layui-select">
                    <option value=''>-- 全部账号 --</option>
                    {foreach $users as $user}
                    <option value="{$user}" {if $user eq input('get.username')}selected{/if}>{$user}</option>
                    {/foreach}
                </select>
            </div>
        </div>
        
        <div class="layui-form-item layui-inline">
            <label class="layui-form-label">操作节点</label>
            <div class="layui-input-inline">
                <input name="node" value="{$get.node|default=''}" placeholder="请输入操作节点" class="layui-input">
            </div>
        </div>
        
        <div class="layui-form-item layui-inline">
            <label class="layui-form-label">操作时间</label>
            <div class="layui-input-inline">
                <input data-date-range name="create_at" value="{$get.create_at|default=''}" placeholder="请选择操作时间" class="layui-input">
            </div>
        </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')}?type={$type|default=''}" 
                    class="layui-btn layui-btn-primary">
                <i class="layui-icon layui-icon-export"></i> 导 出
            </button>
        </div>
    </form>
</fieldset>

<!-- 数据表格 -->
<table id="OplogTable" data-url="{:request()->url()}" data-target-search="form.form-search"></table>
{/block}

{block name='script'}
<script>
$(function () {
    // 初始化表格
    $('#OplogTable').layTable({
        even: true, 
        height: 'full',
        sort: {field: 'id', type: 'desc'},
        cols: [[
            {checkbox: true},
            {field: 'id', title: 'ID', width: 80, sort: true, align: 'center'},
            {field: 'username', title: '操作账号', minWidth: 100, sort: true},
            {field: 'node', title: '操作节点', minWidth: 120},
            {field: 'action', title: '操作行为', minWidth: 120},
            {field: 'content', title: '操作内容', minWidth: 150},
            {field: 'geoip', title: '访问地址', minWidth: 100},
            {field: 'geoisp', title: '网络服务商', minWidth: 100},
            {field: 'create_at', title: '创建时间', minWidth: 170, align: 'center', sort: true}
        ]]
    });
});

// Excel 导出配置
require(['excel'], function (excel) {
    excel.bind(function (data) {
        // 1. 转换数据格式:将对象数组转换为二维数组
        data.forEach(function (item, index) {
            data[index] = [
                item.id,           // ID
                item.username,     // 操作账号
                item.node,         // 操作节点
                item.geoip,        // 访问地址
                item.geoisp,       // 网络服务商
                item.action,       // 操作行为
                item.content,      // 操作内容
                item.create_at     // 创建时间
            ];
        });
        
        // 2. 添加表头
        data.unshift([
            'ID', 
            '操作账号', 
            '操作节点', 
            '访问地址', 
            '网络服务商', 
            '操作行为', 
            '操作内容', 
            '创建时间'
        ]);
        
        // 3. 应用样式(使用 withStyle 快捷方法)
        return this.withStyle(data, {
            A: 60,   // ID列
            B: 80,   // 操作账号列
            C: 99,   // 操作节点列
            D: 120,  // 访问地址列
            E: 120,  // 网络服务商列
            F: 100,  // 操作行为列
            G: 120,  // 操作内容列
            H: 170   // 创建时间列
        });
        
    }, '操作日志_' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'));
});
</script>
{/block}

2. 后端PHP代码

<?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
 * @package app\admin\controller
 */
class Oplog extends Controller
{
    /**
     * 系统日志管理
     * @auth true
     * @menu true
     */
    public function index()
    {
        // 如果是导出请求,返回 JSON 数据
        if ($this->request->get('output') === 'json') {
            $query = SystemOplog::mQuery();
            $query->dateBetween('create_at');
            $query->equal('username,action');
            $query->like('content,geoip,node');
            
            // 导出时不分页,获取所有数据
            $list = $query->order('id desc')->page(true, false);
            
            // 处理IP地址信息
            $region = new \Ip2Region();
            foreach ($list as &$vo) {
                try {
                    $vo['geoisp'] = $region->simple($vo['geoip']);
                } catch (\Exception $exception) {
                    $vo['geoisp'] = '';
                }
            }
            
            $this->success('success', [
                'list' => $list,
                'page' => ['current' => 1, 'pages' => 1, 'total' => count($list)]
            ]);
        }
        
        // 正常列表显示
        SystemOplog::mQuery()->layTable(function () {
            $this->title = '系统日志管理';
            $columns = SystemOplog::mk()->column('action,username', 'id');
            $this->users = array_unique(array_column($columns, 'username'));
            $this->actions = array_unique(array_column($columns, 'action'));
        }, static function (QueryHelper $query) {
            $query->dateBetween('create_at');
            $query->equal('username,action');
            $query->like('content,geoip,node');
        });
    }
}

📋 数据格式说明

后端返回格式

后端需要返回以下格式的 JSON 数据:

{
    "code": 1,
    "info": "success",
    "data": {
        "list": [
            {
                "id": 1,
                "username": "admin",
                "node": "/admin/user/index",
                "geoip": "127.0.0.1",
                "geoisp": "内网IP",
                "action": "查询",
                "content": "查看用户列表",
                "create_at": "2024-01-01 12:00:00"
            },
            // ... 更多数据
        ],
        "page": {
            "current": 1,
            "pages": 1,
            "total": 100
        }
    }
}

前端处理流程

  1. 自动分页加载: Excel 插件会自动处理分页,通过 output=json&not_cache_limit=1&limit=100&page=1 等参数获取所有数据
  2. 数据转换: 将对象数组转换为二维数组格式
  3. 添加表头: 在第一行添加表头
  4. 样式设置: 应用表格样式
  5. 生成文件: 生成 Excel 文件并下载

📋 高级用法

自定义导出文件名

require(['excel'], function (excel) {
    excel.bind(function (data) {
        // 处理数据
        // ...
        return data;
    }, '自定义文件名_' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'));
});

数据过滤和处理

require(['excel'], function (excel) {
    excel.bind(function (data) {
        // 过滤数据
        data = data.filter(function(item) {
            return item.status === 1; // 只导出状态为1的数据
        });
        
        // 转换数据
        data.forEach(function (item, index) {
            // 处理状态值
            var statusText = item.status === 1 ? '正常' : '禁用';
            
            data[index] = [
                item.id,
                item.username,
                statusText,  // 转换后的状态
                item.create_at
            ];
        });
        
        data.unshift(['ID', '用户名', '状态', '创建时间']);
        return this.withStyle(data, {A: 60, B: 100, C: 80, D: 170});
    }, '用户数据导出');
});

多工作表导出

require(['excel'], function (excel) {
    excel.bind(function (data) {
        // 处理数据
        data.forEach(function (item, index) {
            data[index] = [item.id, item.username, item.create_at];
        });
        data.unshift(['ID', '用户名', '创建时间']);
        
        // 创建多个工作表
        var sheet1 = data;
        var sheet2 = [['统计', '数量'], ['总数', data.length - 1]];
        
        // 返回工作表对象
        return {
            '用户列表': sheet1,
            '统计信息': sheet2
        };
    }, '多工作表导出');
});

自定义导出选项

require(['excel'], function (excel) {
    // 设置导出选项
    excel.options = {
        writeOpt: {bookSST: false},
        extend: {
            // 自定义选项
        }
    };
    
    excel.bind(function (data) {
        // 处理数据
        // ...
        return data;
    }, '导出文件名');
});

📋 注意事项

后端注意事项

  1. 返回格式: 必须返回 code: 1data.list 格式的 JSON
  2. 分页处理: 导出时会自动分页获取数据,后端需要支持 output=json&limit=100&page=1 参数
  3. 数据量: 大数据量导出时,建议后端进行数据限制或分批处理
  4. 性能优化: 导出时可以使用缓存或优化查询,提升性能

前端注意事项

  1. 数据格式: 确保数据格式正确,表头和数据行数量匹配
  2. 样式设置: 样式设置是可选的,但建议设置以提高可读性
  3. 文件大小: 大数据量导出可能生成较大的 Excel 文件
  4. 浏览器兼容: 确保浏览器支持文件下载功能

最佳实践

  1. 数据验证: 导出前验证数据是否有效
  2. 进度提示: 大数据量导出时显示进度提示
  3. 错误处理: 完善的错误处理机制
  4. 权限控制: 使用 auth() 函数控制导出权限

📋 常见问题

Q: 导出时如何包含搜索条件? A: data-form-export 会自动获取表单的搜索条件,并附加到导出 URL 中。

Q: 如何导出当前表格显示的数据? A: 使用 data-form-export 时,会自动使用表单的搜索条件,确保导出的数据与表格显示一致。

Q: 大数据量导出如何处理? A: Excel 插件会自动分页获取数据,但建议后端对导出数据进行限制,避免导出过多数据。

Q: 如何自定义导出列? A: 在 excel.bind 的回调函数中,只处理需要导出的字段即可。


提示: 更多详细用法请参考 layui.excel 官方文档 或查看系统日志模块的实际实现。

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