36 KiB
name, description
| name | description |
|---|---|
| jimureport | Use when user asks to create/edit JiMu reports (积木报表), visual Excel-style reports, or says "创建积木报表", "积木报表", "jmreport", "做一个可视化报表", "Excel报表", "数据填报", "积木设计器", "create jimureport", "visual report". Also triggers when user describes report requirements involving Excel-like layouts, data binding with |
JeecgBoot 积木报表 (JiMu Report) AI 自动生成器
将自然语言的报表需求描述转换为积木报表配置,并通过 API 在 JeecgBoot 系统中自动创建/编辑报表。
重要:本 skill 处理「积木报表」(可视化 Excel 风格报表设计器),不涉及「Online 报表」(SQL 驱动的 cgreport)或「Online 表单」(cgform)。
与 Online 报表的区别
| 特性 | 积木报表 (jimureport) | Online 报表 (cgreport) |
|---|---|---|
| 设计方式 | 可视化 Excel 设计器 | 配置式(字段列表) |
| 布局能力 | 自由布局、合并单元格、多sheet | 固定表格列 |
| 数据绑定 | #{数据集编码.字段名} |
自动列映射 |
| 填报功能 | 支持(submitForm=1) | 不支持 |
| 打印配置 | 精细控制(纸张/边距/方向) | 基础打印 |
| 增强能力 | CSS/JS/Python 增强 | 无 |
前置条件
用户必须提供以下信息(或由 AI 引导确认):
- API 地址:JeecgBoot 后端地址(如
https://boot3.jeecg.com/jeecgboot) - X-Access-Token:JWT 登录令牌(从浏览器 F12 获取)
如果用户未提供,提示:
请提供 JeecgBoot 后端地址和 X-Access-Token(从浏览器 F12 → Network → 任意请求的 Request Headers 中复制)。
接口签名机制
部分接口标记了 @JimuSignature 注解,调用时必须在 Header 中携带 X-Sign 和 X-TIMESTAMP,否则返回 code: 1001 签名校验失败。
需要签名的接口: queryFieldBySql、executeSelectApi、loadTableData、testConnection、download/image、dictCodeSearch、getDataSourceByPage、getDataSourceById
不需要签名的接口: save、saveDb、get/{id}、field/tree/{reportId}、loadDbData/{dbId}
签名算法
1. 收集所有请求参数(URL query + POST body)
2. 所有值转为字符串(数字→str, 布尔→"true"/"false", 对象→JSON字符串)
3. 按 key 字母升序排序
4. 转为紧凑 JSON 字符串(无空格): json.dumps(sorted_dict, separators=(',', ':'))
5. 拼接密钥: jsonStr + "dd05f1c54d63749eda95f9fa6d49v442a"
6. MD5 并转大写: hashlib.md5(拼接结果.encode()).hexdigest().upper()
默认签名密钥:
dd05f1c54d63749eda95f9fa6d49v442a(注意第29位是字母v不是数字4) 可通过jeecg.signatureSecret配置覆盖。 时间戳有效期:5 分钟。
详细文档见 references/signature.md。
更多参考文档:
references/template-analysis.md- 模板报表分析(46个模板结构、displayConfig条码、循环报表)references/components.md- 组件配置(图表、图片、条码、二维码)references/chart-templates.md- 图表模板与ECharts配置references/chart-config.md- 图表配置详解
交互流程
Step 0: 判断操作类型
| 用户意图关键词 | 操作类型 |
|---|---|
| 创建/新建/做一个积木报表 | 新增报表 → Step 1 |
| 修改积木报表/改字段/加数据集/加查询条件 | 编辑报表 → 需要报表ID,走编辑流程 |
编辑报表流程:
GET /jmreport/field/tree/{reportId}→ 获取所有数据集的dbCode和dbIdGET /jmreport/loadDbData/{dbId}?reportId={reportId}→ 获取数据集详情(含 fieldList)POST /jmreport/saveDb(传 id = 更新,不传 id = 新增)→ 更新 SQL / 参数GET /jmreport/get/{reportId}→ 获取当前 jsonStr- 修改 jsonStr 内容后,
POST /jmreport/save→ 保存报表设计
详见 references/dataset-skills.md 中的"查询已有数据集"章节。
Step 1: 解析需求
从用户描述中提取:
| 信息 | 默认值 | 示例 |
|---|---|---|
| 报表名称 (name) | 用户指定 | "销售统计报表" |
| SQL 语句 | 从需求推导或用户提供 | SELECT ... FROM ... |
| 数据源 (dbSource) | 空(默认数据源) | second_db |
| 是否分页 (isPage) | "1" | "0"=不分页 |
| 是否填报 (submitForm) | 0 | 1=填报模式 |
Step 2: 调用 SQL 解析接口获取字段
POST /jmreport/queryFieldBySql
{
"sql": "select * from demo",
"dbSource": "",
"type": "0"
}
返回结构:
{
"success": true,
"result": {
"paramList": [],
"fieldList": [
{
"fieldName": "id",
"fieldText": "id",
"widgetType": "String",
"orderNum": 1
}
]
}
}
Step 3: 调用数据集保存接口
POST /jmreport/saveDb
{
"izSharedSource": 0,
"jimuReportId": "报表ID",
"dbCode": "数据集编码",
"dbChName": "数据集中文名",
"dbType": "0",
"dbSource": "",
"jsonData": "",
"apiConvert": "",
"isList": "1",
"isPage": "1",
"dbDynSql": "SQL语句",
"fieldList": [],
"paramList": []
}
关键字段说明:
| 字段 | 说明 | 示例 |
|---|---|---|
jimuReportId |
关联的报表ID | "1193766682428530688" |
dbCode |
数据集编码,在jsonStr中通过 #{dbCode.fieldName} 引用 |
"sales" |
dbChName |
数据集中文名称 | "销售数据" |
dbType |
数据源类型:"0"=SQL, "1"=API, "2"=JavaBean, "3"=JSON, "4"=共享, "5"=多文件, "6"=单文件 | "0" |
dbSource |
数据源标识,空=默认 | "" |
isList |
"1"=列表数据 | "1" |
isPage |
"1"=分页 | "1" |
dbDynSql |
SQL语句 | "select * from demo" |
fieldList 每个字段的结构:
{
"fieldName": "id",
"fieldText": "id",
"widgetType": "String",
"orderNum": 0,
"tableIndex": 0,
"extJson": "",
"dictCode": ""
}
Step 4: 构造报表 jsonStr
jsonStr 是积木报表的核心设计数据,定义了 Excel 风格的布局。
4.1 jsonStr 完整结构
{
"loopBlockList": [],
"querySetting": {
"izOpenQueryBar": false,
"izDefaultQuery": true
},
"recordSubTableOrCollection": { "group": [], "record": [], "range": [] },
"printConfig": {
"paper": "A4",
"width": 210,
"height": 297,
"definition": 1,
"isBackend": false,
"marginX": 10,
"marginY": 10,
"layout": "portrait",
"printCallBackUrl": ""
},
"hidden": { "rows": [], "cols": [], "conditions": { "rows": {}, "cols": {} } },
"queryFormSetting": { "useQueryForm": false, "dbKey": "", "idField": "" },
"dbexps": [],
"dicts": [],
"fillFormToolbar": {
"show": true,
"btnList": ["save","subTable_add","verify","subTable_del","print","close","first","prev","next","paging","total","last","exportPDF","exportExcel","exportWord"]
},
"freeze": "A1",
"dataRectWidth": 700,
"isViewContentHorizontalCenter": false,
"autofilter": {},
"validations": [],
"cols": { "len": 100 },
"area": { "sri": 0, "sci": 0, "eri": 0, "eci": 0, "width": 100, "height": 25 },
"pyGroupEngine": false,
"submitHandlers": [],
"hiddenCells": [],
"zonedEditionList": [],
"rows": {
"1": {
"cells": {
"1": { "text": "表头1", "style": 4 },
"2": { "text": "表头2", "style": 4 }
},
"height": 34
},
"2": {
"cells": { "name": "sheet1",
"1": { "text": "#{数据集编码.字段1}", "style": 2 },
"2": { "text": "#{数据集编码.字段2}", "style": 2 }
}
},
"len": 200
},
"rpbar": { "show": true, "pageSize": "", "btnList": [] },
"fixedPrintHeadRows": [],
"fixedPrintTailRows": [],
"displayConfig": {},
"fillFormInfo": { "layout": { "direction": "horizontal", "width": 200, "height": 45 } },
"background": false,
"styles": [],
"fillFormStyle": "default",
"freezeLineColor": "rgb(185, 185, 185)",
"merges": []
}
4.2 行列数据 (rows)
行和列的索引从 1 开始(0行通常为空)。
"rows": {
"1": {
"cells": {
"1": { "text": "ID", "style": 4 },
"2": { "text": "名称", "style": 4 },
"3": { "text": "金额", "style": 4 }
},
"height": 34
},
"2": {
"cells": {
"1": { "text": "#{ds.id}", "style": 2 },
"2": { "text": "#{ds.name}", "style": 2 },
"3": { "text": "#{ds.amount}", "style": 2 }
}
},
"len": 200
}
- 第1行:表头行(通常用 style 4,蓝底白字)
- 第2行:数据绑定行(用
#{数据集编码.字段名}语法) height:行高(像素)len:总行数(默认200)
4.3 数据绑定语法
| 语法 | 说明 | 示例 |
|---|---|---|
#{dbCode.fieldName} |
普通字段绑定 | #{sales.amount} |
=SUM(#{dbCode.fieldName}) |
聚合函数 | =SUM(#{sales.amount}) |
=COUNT(#{dbCode.fieldName}) |
计数 | =COUNT(#{sales.id}) |
4.4 样式 (styles)
样式数组,通过索引在 cells 中引用:
"styles": [
{
"border": { "bottom": ["thin","#000"], "top": ["thin","#000"], "left": ["thin","#000"], "right": ["thin","#000"] }
},
{
"border": { "bottom": ["thin","#000"], "top": ["thin","#000"], "left": ["thin","#000"], "right": ["thin","#000"] },
"align": "center"
},
{
"border": { "bottom": ["thin","#000"], "top": ["thin","#000"], "left": ["thin","#000"], "right": ["thin","#000"] },
"align": "center",
"valign": "middle"
},
{
"border": { "bottom": ["thin","#000"], "top": ["thin","#000"], "left": ["thin","#000"], "right": ["thin","#000"] },
"align": "center",
"valign": "middle",
"bgcolor": "#01b0f1"
},
{
"border": { "bottom": ["thin","#000"], "top": ["thin","#000"], "left": ["thin","#000"], "right": ["thin","#000"] },
"align": "center",
"valign": "middle",
"bgcolor": "#01b0f1",
"color": "#ffffff"
}
]
常用样式索引:
| 索引 | 效果 | 用途 |
|---|---|---|
| 0 | 边框 | 基础单元格 |
| 1 | 边框+居中 | 文本居中 |
| 2 | 边框+居中+垂直居中 | 数据行 |
| 3 | 边框+居中+垂直居中+蓝底 | 表头(无白字) |
| 4 | 边框+居中+垂直居中+蓝底白字 | 表头(推荐) |
4.5 单元格合并 (merges)
"merges": [
"B1:F1"
]
格式为 Excel 风格的范围表示,如 B1:F1 表示合并 B1 到 F1。
4.6 打印配置 (printConfig)
| 属性 | 说明 | 可选值 |
|---|---|---|
| paper | 纸张大小 | "A4", "A3", "B5", "letter" |
| width/height | 纸张宽高(mm) | A4: 210×297 |
| layout | 方向 | "portrait"(纵向), "landscape"(横向) |
| marginX/marginY | 边距(mm) | 默认10 |
| isBackend | 后端打印 | true/false |
4.7 查询条件 (querySetting)
"querySetting": {
"izOpenQueryBar": true,
"izDefaultQuery": true
}
izOpenQueryBar: 是否显示查询栏izDefaultQuery: 是否默认查询
Step 5: 调用报表保存接口
POST /jmreport/save
关键格式要求:
designerObj是 JSON 字符串(不是对象)- 所有 jsonStr 字段(
rows、cols、styles、merges、chartList等)都放在请求体顶层,每个值都是 JSON 字符串(不是对象)- 必须包含
sheetId、sheetName、sheetOrder字段- 后端
saveReport逻辑:json.remove("designerObj")后,剩余的顶层 JSON 直接作为 jsonStr 存入数据库
请求体结构(只有 designerObj 是字符串,其他都是原始对象):
{
"designerObj": "{\"id\":\"报表ID\",\"name\":\"报表名称\",\"type\":\"0\",\"template\":0,\"delFlag\":0,\"submitForm\":0,\"reportName\":\"报表名称\"}",
"name": "sheet1",
"freeze": "A1",
"freezeLineColor": "rgb(185, 185, 185)",
"rows": {"1": {"cells": {"1": {"text": "表头", "style": 4}}, "height": 34}, "len": 200},
"cols": {"len": 100},
"styles": [],
"merges": [],
"validations": [],
"autofilter": {},
"dbexps": [],
"dicts": [],
"loopBlockList": [],
"zonedEditionList": [],
"fixedPrintHeadRows": [],
"fixedPrintTailRows": [],
"rpbar": {"show": true, "pageSize": "", "btnList": []},
"fillFormToolbar": {"show": true, "btnList": ["save","subTable_add","verify","subTable_del","print","close","first","prev","next","paging","total","last","exportPDF","exportExcel","exportWord"]},
"hiddenCells": [],
"hidden": {"rows": [], "cols": [], "conditions": {"rows": {}, "cols": {}}},
"fillFormInfo": {"layout": {"direction": "horizontal", "width": 200, "height": 45}},
"recordSubTableOrCollection": {"group": [], "record": [], "range": []},
"displayConfig": {},
"printConfig": {"paper": "A4", "width": 210, "height": 297, "definition": 1, "isBackend": false, "marginX": 10, "marginY": 10, "layout": "portrait", "printCallBackUrl": ""},
"querySetting": {"izOpenQueryBar": false, "izDefaultQuery": true},
"queryFormSetting": {"useQueryForm": false, "dbKey": "", "idField": ""},
"area": {"sri": 0, "sci": 0, "eri": 0, "eci": 0, "width": 100, "height": 25},
"submitHandlers": [],
"chartList": [],
"background": false,
"dataRectWidth": 700,
"excel_config_id": "报表ID",
"pyGroupEngine": false,
"isViewContentHorizontalCenter": false,
"fillFormStyle": "default",
"sheetId": "default",
"sheetName": "默认Sheet",
"sheetOrder": "0"
}
Python 构造示例:
save_data = {
# 只有 designerObj 是字符串
"designerObj": json.dumps(designer_obj, ensure_ascii=False),
# 其他所有字段都是原始对象/数组,不要 json.dumps
"name": "sheet1",
"freeze": "A1",
"freezeLineColor": "rgb(185, 185, 185)",
"rows": rows_data, # dict, 不是字符串
"cols": cols_data, # dict
"styles": styles_list, # list
"merges": merges_list, # list
"chartList": chart_list, # list
"loopBlockList": [], # list
"querySetting": {"izOpenQueryBar": False, "izDefaultQuery": True}, # dict
# ... 其他配置字段同理
"sheetId": "default",
"sheetName": "默认Sheet",
"sheetOrder": "0",
"background": False, # 布尔值
"dataRectWidth": 700, # 数字
"excel_config_id": report_id,
"pyGroupEngine": False,
"isViewContentHorizontalCenter": False,
"fillFormStyle": "default"
}
关键:只有
designerObj用json.dumps()转字符串,其他所有字段(rows、cols、styles、merges、chartList、loopBlockList等)都保持原始 Python 对象。如果把它们也 json.dumps 转成字符串,会导致双重序列化,前端解析报错。
designerObj 关键字段(JSON 字符串内的对象结构):
| 字段 | 说明 | 必填 |
|---|---|---|
id |
报表唯一ID | 是 |
code |
报表编码(如时间戳格式) | 是 |
name / reportName |
报表名称 | 是 |
type |
报表分类,默认 "0" |
是 |
template |
是否为模板(0否) | 是 |
cssStr |
CSS增强代码 | 否 |
jsStr |
JS增强代码 | 否 |
pyStr |
Python增强代码 | 否 |
tenantId |
租户ID | 否 |
submitForm |
是否填报(0否,1是) | 否 |
注意事项:
- 只有
designerObj是字符串(json.dumps(obj)),其他所有字段保持原始对象/数组 rows、cols、styles、chartList、loopBlockList等都是 原始对象/数组,禁止 json.dumpsbackground、pyGroupEngine、isViewContentHorizontalCenter是布尔值FalsedataRectWidth是数字(如700)- 必须传
sheetId: "default"、sheetName: "默认Sheet"、sheetOrder: "0"
Step 6: 展示摘要并确认
必须展示以下内容,等待用户确认后再执行:
## 积木报表配置摘要
- 报表名称:销售统计报表
- 数据源:默认
- 目标环境:https://boot3.jeecg.com/jeecgboot
### 数据集配置
| 编码 | 名称 | SQL | 分页 |
|------|------|-----|------|
| sales | 销售数据 | SELECT id, name, amount FROM biz_sales | 是 |
### 表头设计
| 列 | 表头文本 | 数据绑定 |
|----|---------|---------|
| B | ID | #{sales.id} |
| C | 名称 | #{sales.name} |
| D | 金额 | #{sales.amount} |
确认以上配置?(y/n)
Step 7: 使用 Python 调用 API
重要限制:
- Windows 环境下 curl 发送中文/长 JSON 会出错,必须使用 Python
- 禁止使用
python3 -c "..."内联方式 - 必须先用 Write 工具写入
.py临时文件,再用 Bash 执行,最后删除临时文件
完整 Python 脚本模板:
import urllib.request
import json
import time
import random
import ssl
import hashlib
API_BASE = '{用户提供的后端地址}'
TOKEN = '{用户提供的 X-Access-Token}'
SIGNATURE_SECRET = 'dd05f1c54d63749eda95f9fa6d49v442a'
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# 需要签名的接口列表
SIGNED_ENDPOINTS = [
'/jmreport/queryFieldBySql',
'/jmreport/executeSelectApi',
'/jmreport/loadTableData',
'/jmreport/testConnection',
'/jmreport/download/image',
'/jmreport/dictCodeSearch',
'/jmreport/getDataSourceByPage',
'/jmreport/getDataSourceById',
]
def compute_sign(params_dict):
"""计算积木报表接口签名"""
str_params = {}
for k, v in params_dict.items():
if v is None:
continue
if isinstance(v, bool):
str_params[k] = str(v).lower()
elif isinstance(v, (int, float)):
str_params[k] = str(v)
elif isinstance(v, (dict, list)):
str_params[k] = json.dumps(v, ensure_ascii=False, separators=(',', ':'))
else:
str_params[k] = str(v)
sorted_params = dict(sorted(str_params.items()))
params_json = json.dumps(sorted_params, ensure_ascii=False, separators=(',', ':'))
sign_str = params_json + SIGNATURE_SECRET
return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
def api_request(path, data=None, method=None):
url = f'{API_BASE}{path}'
headers = {
'X-Access-Token': TOKEN,
'Content-Type': 'application/json; charset=UTF-8'
}
# 自动判断是否需要签名
need_sign = any(path.rstrip('/').endswith(ep.rstrip('/')) for ep in SIGNED_ENDPOINTS)
if need_sign:
sign_params = data if data else {}
headers['X-TIMESTAMP'] = str(int(time.time() * 1000))
headers['X-Sign'] = compute_sign(sign_params)
if data is not None:
json_data = json.dumps(data, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(url, data=json_data, headers=headers, method=method or 'POST')
else:
req = urllib.request.Request(url, headers=headers, method=method or 'GET')
resp = urllib.request.urlopen(req, context=ctx)
return json.loads(resp.read().decode('utf-8'))
def gen_id():
return str(int(time.time() * 1000) * 1000000 + random.randint(100000, 999999))
# ====== Step 1: 解析SQL获取字段 ======
parse_result = api_request('/jmreport/queryFieldBySql', {
"sql": "select * from demo",
"dbSource": "",
"type": "0"
})
print('SQL解析结果:', json.dumps(parse_result, ensure_ascii=False, indent=2))
# ====== Step 2: 保存数据集 ======
db_data = {
"izSharedSource": 0,
"jimuReportId": "报表ID",
"dbCode": "demo",
"dbChName": "示例数据",
"dbType": "0",
"dbSource": "",
"jsonData": "",
"apiConvert": "",
"isList": "1",
"isPage": "1",
"dbDynSql": "select * from demo",
"fieldList": parse_result['result']['fieldList'],
"paramList": []
}
db_result = api_request('/jmreport/saveDb', db_data)
print('数据集保存结果:', json.dumps(db_result, ensure_ascii=False, indent=2))
# ====== Step 3: 构造请求体并保存报表 ======
# 关键: designerObj 是字符串, 所有 jsonStr 字段也是字符串
# 后端逻辑: json.remove("designerObj") 后, 剩余的顶层字段就是 jsonStr
designer_obj = {
"id": report_id, "name": "报表名称", "type": "0",
"template": 0, "delFlag": 0, "viewCount": 0, "updateCount": 0,
"submitForm": 0, "reportName": "报表名称"
}
rows_data = {
"1": {"cells": {"1": {"text": "ID", "style": 4}, "2": {"text": "名称", "style": 4}}, "height": 34},
"2": {"cells": {"1": {"text": "#{demo.id}", "style": 2}, "2": {"text": "#{demo.name}", "style": 2}}},
"len": 200
}
styles_list = [
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}},
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center"},
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle"},
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#01b0f1"},
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#01b0f1", "color": "#ffffff"}
]
# 所有对象/数组字段用 json.dumps 转为字符串
save_data = {
"designerObj": json.dumps(designer_obj, ensure_ascii=False),
"name": "sheet1",
"freeze": "A1",
"freezeLineColor": "rgb(185, 185, 185)",
"rows": json.dumps(rows_data, ensure_ascii=False),
"cols": json.dumps({"len": 100}, ensure_ascii=False),
"styles": json.dumps(styles_list, ensure_ascii=False),
"merges": json.dumps([], ensure_ascii=False),
"validations": "[]",
"autofilter": "{}",
"dbexps": "[]",
"dicts": "[]",
"loopBlockList": "[]",
"zonedEditionList": "[]",
"fixedPrintHeadRows": "[]",
"fixedPrintTailRows": "[]",
"hiddenCells": "[]",
"submitHandlers": "[]",
"rpbar": json.dumps({"show": True, "pageSize": "", "btnList": []}, ensure_ascii=False),
"fillFormToolbar": json.dumps({"show": True, "btnList": ["save", "subTable_add", "verify", "subTable_del", "print", "close", "first", "prev", "next", "paging", "total", "last", "exportPDF", "exportExcel", "exportWord"]}, ensure_ascii=False),
"hidden": json.dumps({"rows": [], "cols": [], "conditions": {"rows": {}, "cols": {}}}, ensure_ascii=False),
"fillFormInfo": json.dumps({"layout": {"direction": "horizontal", "width": 200, "height": 45}}, ensure_ascii=False),
"recordSubTableOrCollection": json.dumps({"group": [], "record": [], "range": []}, ensure_ascii=False),
"displayConfig": "{}",
"printConfig": json.dumps({"paper": "A4", "width": 210, "height": 297, "definition": 1, "isBackend": False, "marginX": 10, "marginY": 10, "layout": "portrait", "printCallBackUrl": ""}, ensure_ascii=False),
"querySetting": json.dumps({"izOpenQueryBar": False, "izDefaultQuery": True}, ensure_ascii=False),
"queryFormSetting": json.dumps({"useQueryForm": False, "dbKey": "", "idField": ""}, ensure_ascii=False),
"area": json.dumps({"sri": 0, "sci": 0, "eri": 0, "eci": 0, "width": 100, "height": 25}, ensure_ascii=False),
"chartList": "[]",
"background": "false",
"dataRectWidth": "700",
"excel_config_id": report_id,
"pyGroupEngine": "false",
"isViewContentHorizontalCenter": "false",
"fillFormStyle": "default",
"sheetId": "default",
"sheetName": "默认Sheet",
"sheetOrder": "0"
}
save_result = api_request('/jmreport/save', save_data)
print('报表保存结果:', json.dumps(save_result, ensure_ascii=False, indent=2))
典型工作流总结
1. save (空报表) → 先创建空报表,获取报表ID
2. queryFieldBySql → 解析SQL,获取字段列表
3. saveDb → 保存数据集(含字段映射、分页配置),关联报表ID
4. save (完整设计) → jsonStr内容放请求体顶层,保存完整报表设计
关键注意事项:
- Step 1 创建空报表时,
save接口首次调用会返回isRefresh: true,此时报表已创建 - Step 4 的
save请求体格式:designerObj(元数据)+ jsonStr 内容(rows/cols/styles 等)放在同一层级 - 禁止将 jsonStr 嵌套在
designerObj.jsonStr字符串中,否则后端会清空 rows 数据 designerObj.type默认值为"0",不要传分类名称字符串(如 "demo")
智能字段配置
字段显示名称推导
| 字段名模式 | 推导中文名 |
|---|---|
| id | ID/主键 |
| name / title | 名称/标题 |
| code / no | 编码/编号 |
| status | 状态 |
| amount / money / price / salary | 金额/费用/价格/薪资 |
| count / qty / num / age | 数量/年龄 |
| date / time / birthday | 日期/时间/生日 |
| create_by / update_by | 创建人/更新人 |
| create_time / update_time | 创建时间/更新时间 |
| sex | 性别 |
| 邮箱 | |
| phone / mobile / tel | 电话/手机号 |
| content / remark | 内容/备注 |
| sys_org_code | 组织编码 |
| tenant_id | 租户ID |
是否在报表中显示
| 规则 | 是否显示 |
|---|---|
| 业务字段(默认) | 显示 |
| id / 主键字段 | 通常隐藏 |
| create_by / update_by | 通常隐藏 |
| sys_org_code / tenant_id | 隐藏 |
高级功能
SQL 参数化与动态条件
积木报表支持在SQL中使用参数和FreeMarker动态条件:
-- 基础参数
SELECT * FROM demo WHERE name like '%${name}%'
-- FreeMarker动态条件(参数为空时自动跳过)
select * from demo where 1=1
<#if isNotEmpty(name)> and name = '${name}'</#if>
<#if isNotEmpty(age)> and age = '${age}'</#if>
-- IN查询(v1.6.2+)
select * from demo where sex in(${DaoFormat.in('${sex}')})
select * from demo where age in(${DaoFormat.inNumber('${age}')})
查询配置
报表支持丰富的查询控件(文本、下拉单选/多选、范围、模糊、下拉树),详见 references/query-config.md。
关键配置点:
- querySetting:
izOpenQueryBar(展开查询栏) /izDefaultQuery(自动查询) - 控件默认值:静态值 /
=dateStr('yyyy-MM-dd')/#{sysUserCode} - JS增强:级联下拉
updateSelectOptions()/ 监听变化onSearchFormChange() - 参数优先级:查询条件值 > URL参数 > 默认值
分组报表(纵向分组)
当用户要求"分组报表"、"按XX分组"、"按XX统计"时,必须使用分组语法,不要用普通的汇总SQL+明细SQL拆分方式。
核心配置(3个必须项)
- jsonStr 顶层添加分组标记:
{
"isGroup": true,
"groupField": "数据集编码.分组字段名"
}
- save 请求体中也要传这两个字段(与 rows/cols 同级):
save_data = {
...
"isGroup": True,
"groupField": "users.sex_name",
...
}
- 分组列单元格使用
#{db.group(field)}语法,并配置聚合属性:
{
"text": "#{users.group(sex_name)}",
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "合计"
}
分组单元格属性
| 属性 | 值 | 说明 |
|---|---|---|
text |
#{dbCode.group(fieldName)} |
分组绑定,相同值自动合并单元格 |
aggregate |
"group" |
标记为分组聚合列 |
subtotal |
"groupField" |
启用小计/合计行 |
funcname |
"-1" / "SUM" / "COUNT" / "AVG" |
聚合函数,"-1"=不计算 |
subtotalText |
"合计" / "小计" |
小计行显示的文本 |
多级分组
从左到右为高到低级别,每级用不同的 subtotalText 区分:
- 一级分组(如起始站):
subtotalText: "合计"— 一级分组切换时显示 - 二级分组(如终止站):
subtotalText: "小计"— 二级分组切换时显示 groupField始终指向一级(最高级)分组字段
多级分组示例(按起始站+终止站分组):
// save 请求体
{
"isGroup": true,
"groupField": "jp.kaishi", // 指向一级分组字段
...
}
// 数据绑定行 cells
"1": {
"text": "#{jp.group(kaishi)}", // 一级分组
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "合计"
},
"2": {
"text": "#{jp.group(jieshu)}", // 二级分组
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "小计"
},
"3": {"text": "#{jp.bnum}"}, // 普通字段
分组报表布局示例
第1行: 标题(合并单元格)
第2行: 表头(起始站 | 终止站 | 班次号 | 发车时间 | ...)
第3行: 数据绑定行(#{db.group(kaishi)} | #{db.group(jieshu)} | #{db.bnum} | ...)
预览效果:
┌────────┬────────┬────────┬──────────┐
│ 起始站 │ 终止站 │ 班次号 │ 发车时间 │
├────────┼────────┼────────┼──────────┤
│ │ │ K7725 │ 21:13 │
│ │ 邯郸 ├────────┼──────────┤
│ 北京西 │ │ 小计 │
│ ├────────┼────────┼──────────┤
│ │ 深圳 │ G101 │ 06:44 │
│ │ │ 小计 │
├────────┼────────┼────────┼──────────┤
│ │ │ 合计 │
└────────┴────────┴────────┴──────────┘
注意事项
- SQL 中必须按分组字段
ORDER BY,确保相同值相邻(多级分组时按一级、二级顺序排序) - 数据集
isPage设为"0"(不分页),否则分组合并可能不完整 pyGroupEngine保持false(标准分组不需要 Python 引擎)- 列数较多时(>6列),考虑将
printConfig.layout设为"landscape"(横向打印) - 完整示例见
examples/vertical-group-subtotal-example.md
数据探查
queryFieldBySql 只返回字段元数据,不返回实际数据行。当需要了解数据内容以判断分组字段时:
- 优先通过 pymysql 连接本地数据库查看实际数据(
SELECT DISTINCT、GROUP BY等) - 本地数据库名通常为
jeecgboot3(可通过SHOW DATABASES LIKE '%jeecg%'确认) - 查看数据后再确定哪些字段适合作为分组依据
CSS/JS/Python 增强
通过 designerObj 的 cssStr、jsStr、pyStr 字段传入增强代码。
多 Sheet
设置 isMultiSheet 为 1,通过 sheets 字段管理多个 sheet 页。
填报模式
设置 submitForm 为 1,启用数据填报功能,允许用户在报表中录入数据。
错误处理
| 错误 | 解决方案 |
|---|---|
| Token 过期(401/认证失败) | 提示用户重新获取 X-Access-Token |
code:1001 签名验证失败 |
接口需要签名,需在 Header 添加 X-Sign 和 X-TIMESTAMP,详见签名机制章节 |
签名验证失败:X-TIMESTAMP已过期 |
客户端与服务器时间差超过5分钟,检查系统时间 |
签名校验失败,参数有误! |
签名计算不匹配,检查参数排序、JSON无空格格式、密钥是否正确 |
| SQL 解析失败 | 检查 SQL 语法是否正确,表是否存在 |
| 数据集编码重复 | 换一个 dbCode |
| jsonStr 格式错误 | 检查 JSON 字符串转义是否正确 |
| 中文乱码 | 确认使用 Python urllib(不要用 curl) |
与其他 Skill 的区别
| Skill | 产出物 | 适用场景 |
|---|---|---|
jeecg-jimureport |
积木报表(可视化Excel设计器) | 复杂布局报表、合并单元格、打印、填报 |
jeecg-onlreport |
Online 报表(SQL 驱动列表) | 简单数据查询报表 |
jeecg-onlform |
Online 表单(元数据CRUD) | 数据录入管理 |
jeecg-desform |
设计器表单 JSON | 数据采集、审批表单 |
jeecg-codegen |
Java + Vue3 代码 + SQL | 自定义业务逻辑模块 |
图表与数据表格布局实战经验
chart_bottom 布局(表格在上,图表在下)
核心问题: 积木报表的数据绑定行在预览时会展开显示多页数据,导致图表位置被推后。
解决方案:
- 图表虚拟单元格需要放在数据展开区域之后
- 图表开始行 = 数据绑定行 + pageSize + gap
- 示例:数据绑定行=3, pageSize=10, gap=1 → 图表从第14行开始
# 布局计算公式
page_size = config.get('pageSize', 10)
gap = config.get('gap', 1) # 默认1行间距,负值可减少间距
data_binding_row = 3 # 标题行(1) + 表头行(2) + 数据绑定行(3)
chart_start = data_binding_row + page_size + gap # 14
虚拟单元格行数
关键发现: 图表的 virtualCellRange 只需要 1行(不是多行)。
错误做法(早期版本):
row_count = (chart_height // 25) + 2 # 300px高度 = 14行
正确做法:
row_count = 1 # 只用1行作为锚点,图表大小由width/height控制
设计器保存后的实际结构:
- 图表虚拟单元格只有1行
- 图表位置由
chartList[].row和chartList[].width/height决定
area 和 dataRectWidth 设置
为确保预览正确显示,需要设置正确的 area 和 dataRectWidth:
# 计算列宽总和
total_width = sum(col.get('width', 100) for col in cols.values() if isinstance(col, dict))
# area 定义内容边界(告诉前端报表的实际范围)
# 注意:设计器保存后会重新计算 area,建议在图表底部添加2-3行空行来确保滚动正常
area = {
"sri": 1, # 起始行(UI行号)
"sci": 1, # 起始列
"eri": chart_start, # 结束行(图表开始的行)
"eci": col_count, # 结束列
"width": total_width,
"height": title_h + header_h + (chart_start - 3) * row_h + chart_h
}
滚动问题的解决方案(已自动化):
脚本已自动处理滚动条问题,无需手动操作:
- 设置
area = False,让系统自动计算滚动高度 - 在图表底部自动添加分页符行(位置 = chart_start + pageSize + 3)
# 在图表下方添加分页符行(使用空格避免显示"1")
pagination_row = chart_start + pageSize + 3
all_rows[str(pagination_row)] = {"cells": {"1": {"text": " "}}}
这样系统就能正确识别滚动区域,滚动条自动正常工作。
合并单元格行号
重要: 合并单元格使用 UI 行号(不是代码行号)。
- 代码行号从 0 开始(但 rows 中的 key 从 "1" 开始)
- UI 行号从 1 开始
- 公式:
ui_row = code_row + 1
示例:
# 标题在代码第1行,合并 C1:H1
ui_row = 1 + 1 # = 2
merges.append(f"C{ui_row}:H{ui_row}") # "C2:H2"
预览地址带 Token
报表预览地址需要携带 token 参数:
https://api3.boot.jeecg.com/jmreport/view/{report_id}?token={X-Access-Token}
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 表格和图表间距过大 | 图表虚拟单元格放在了数据展开区域内 | 图表从 data_binding_row + pageSize + gap 开始 |
| 图表与数据重叠 | 虚拟单元格行数过多 | 虚拟单元格只用1行 |
| 设计器与预览效果不一致 | area 设置不正确 | 设置正确的 area.sri/eri |
| 滚动条不显示 | area 范围计算错误 | 确保 area.eri 等于图表实际开始的行 |
| 间距仍偏大 | gap 默认值过大 | 将 gap 改为负值(如 -5)可以减少间距 |
| 滚动幅度太小 | 内容总高度不够 | 在图表底部添加2-3行空行或分页符,增加总高度 |