📊 数据导出
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"></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"></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
}
}
}前端处理流程
- 自动分页加载: Excel 插件会自动处理分页,通过
output=json¬_cache_limit=1&limit=100&page=1等参数获取所有数据 - 数据转换: 将对象数组转换为二维数组格式
- 添加表头: 在第一行添加表头
- 样式设置: 应用表格样式
- 生成文件: 生成 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;
}, '导出文件名');
});📋 注意事项
后端注意事项
- 返回格式: 必须返回
code: 1和data.list格式的 JSON - 分页处理: 导出时会自动分页获取数据,后端需要支持
output=json&limit=100&page=1参数 - 数据量: 大数据量导出时,建议后端进行数据限制或分批处理
- 性能优化: 导出时可以使用缓存或优化查询,提升性能
前端注意事项
- 数据格式: 确保数据格式正确,表头和数据行数量匹配
- 样式设置: 样式设置是可选的,但建议设置以提高可读性
- 文件大小: 大数据量导出可能生成较大的 Excel 文件
- 浏览器兼容: 确保浏览器支持文件下载功能
最佳实践
- 数据验证: 导出前验证数据是否有效
- 进度提示: 大数据量导出时显示进度提示
- 错误处理: 完善的错误处理机制
- 权限控制: 使用
auth()函数控制导出权限
📋 常见问题
Q: 导出时如何包含搜索条件? A: data-form-export 会自动获取表单的搜索条件,并附加到导出 URL 中。
Q: 如何导出当前表格显示的数据? A: 使用 data-form-export 时,会自动使用表单的搜索条件,确保导出的数据与表格显示一致。
Q: 大数据量导出如何处理? A: Excel 插件会自动分页获取数据,但建议后端对导出数据进行限制,避免导出过多数据。
Q: 如何自定义导出列? A: 在 excel.bind 的回调函数中,只处理需要导出的字段即可。
提示: 更多详细用法请参考 layui.excel 官方文档 或查看系统日志模块的实际实现。
