Files
qhmes/.trae/skills/jimureport/SKILL.md

36 KiB
Raw Permalink Blame History

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 引导确认):

  1. API 地址JeecgBoot 后端地址(如 https://boot3.jeecg.com/jeecgboot
  2. X-Access-TokenJWT 登录令牌(从浏览器 F12 获取)

如果用户未提供,提示:

请提供 JeecgBoot 后端地址和 X-Access-Token从浏览器 F12 → Network → 任意请求的 Request Headers 中复制)。

接口签名机制

部分接口标记了 @JimuSignature 注解,调用时必须在 Header 中携带 X-SignX-TIMESTAMP,否则返回 code: 1001 签名校验失败。

需要签名的接口: queryFieldBySqlexecuteSelectApiloadTableDatatestConnectiondownload/imagedictCodeSearchgetDataSourceByPagegetDataSourceById

不需要签名的接口: savesaveDbget/{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走编辑流程

编辑报表流程:

  1. GET /jmreport/field/tree/{reportId} → 获取所有数据集的 dbCodedbId
  2. GET /jmreport/loadDbData/{dbId}?reportId={reportId} → 获取数据集详情(含 fieldList
  3. POST /jmreport/saveDb传 id = 更新,不传 id = 新增)→ 更新 SQL / 参数
  4. GET /jmreport/get/{reportId} → 获取当前 jsonStr
  5. 修改 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

关键格式要求:

  1. designerObjJSON 字符串(不是对象)
  2. 所有 jsonStr 字段(rowscolsstylesmergeschartList 等)都放在请求体顶层,每个值都是 JSON 字符串(不是对象)
  3. 必须包含 sheetIdsheetNamesheetOrder 字段
  4. 后端 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"
}

关键:只有 designerObjjson.dumps() 转字符串,其他所有字段(rowscolsstylesmergeschartListloopBlockList 等)都保持原始 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)),其他所有字段保持原始对象/数组
  • rowscolsstyleschartListloopBlockList 等都是 原始对象/数组,禁止 json.dumps
  • backgroundpyGroupEngineisViewContentHorizontalCenter 是布尔值 False
  • dataRectWidth 是数字(如 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

重要限制:

  1. Windows 环境下 curl 发送中文/长 JSON 会出错,必须使用 Python
  2. 禁止使用 python3 -c "..." 内联方式
  3. 必须先用 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 性别
email 邮箱
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

关键配置点:

  • querySettingizOpenQueryBar(展开查询栏) / izDefaultQuery(自动查询)
  • 控件默认值:静态值 / =dateStr('yyyy-MM-dd') / #{sysUserCode}
  • JS增强:级联下拉 updateSelectOptions() / 监听变化 onSearchFormChange()
  • 参数优先级:查询条件值 > URL参数 > 默认值

分组报表(纵向分组)

当用户要求"分组报表"、"按XX分组"、"按XX统计"时,必须使用分组语法,不要用普通的汇总SQL+明细SQL拆分方式。

核心配置3个必须项

  1. jsonStr 顶层添加分组标记:
{
    "isGroup": true,
    "groupField": "数据集编码.分组字段名"
}
  1. save 请求体中也要传这两个字段(与 rows/cols 同级):
save_data = {
    ...
    "isGroup": True,
    "groupField": "users.sex_name",
    ...
}
  1. 分组列单元格使用 #{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 DISTINCTGROUP BY 等)
  • 本地数据库名通常为 jeecgboot3(可通过 SHOW DATABASES LIKE '%jeecg%' 确认)
  • 查看数据后再确定哪些字段适合作为分组依据

CSS/JS/Python 增强

通过 designerObjcssStrjsStrpyStr 字段传入增强代码。

多 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 布局(表格在上,图表在下)

核心问题: 积木报表的数据绑定行在预览时会展开显示多页数据,导致图表位置被推后。

解决方案:

  1. 图表虚拟单元格需要放在数据展开区域之后
  2. 图表开始行 = 数据绑定行 + pageSize + gap
  3. 示例:数据绑定行=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[].rowchartList[].width/height 决定

area 和 dataRectWidth 设置

为确保预览正确显示,需要设置正确的 areadataRectWidth

# 计算列宽总和
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
}

滚动问题的解决方案(已自动化):

脚本已自动处理滚动条问题,无需手动操作:

  1. 设置 area = False,让系统自动计算滚动高度
  2. 在图表底部自动添加分页符行(位置 = 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行空行或分页符增加总高度