新增JeecgBoot BPM流程自动生成器,包含流程创建、修改及审批人配置功能,支持自然语言描述转化为BPMN XML,并通过API与JeecgBoot系统交互。

This commit is contained in:
geht
2026-04-08 16:24:41 +08:00
parent 7c60acd679
commit 67104af7de
168 changed files with 207167 additions and 8 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
# 示例5分栏
**类型:** 分栏报表
**特征:** `loopBlockList` 中加 `"loopTime":2` 实现横向循环2次分2栏
## 关键配置
```json
{"loopBlockList":[{"sci":1,"sri":2,"eci":5,"eri":5,"index":1,"db":"jm","loopTime":2}]}
```
## 报表 JSON
```json
{"loopBlockList":[{"sci":1,"sri":2,"eci":5,"eri":5,"index":1,"db":"jm","loopTime":2}],"querySetting":{"izOpenQueryBar":false,"izDefaultQuery":true},"printConfig":{"paper":"A4","width":210,"height":297,"definition":1,"isBackend":false,"marginX":10,"marginY":10,"layout":"portrait"},"hidden":{"rows":[],"cols":[]},"dbexps":[],"dicts":[],"freeze":"A1","dataRectWidth":817,"autofilter":{},"validations":[],"cols":{"0":{"width":72},"3":{"width":101},"4":{"width":90},"5":{"width":54},"len":50},"pyGroupEngine":false,"submitHandlers":[],"excel_config_id":"590831722099462144","hiddenCells":[],"zonedEditionList":[],"rows":{"1":{"cells":{"5":{"style":15,"text":"分栏示例","merge":[0,1],"height":59},"8":{"style":9,"text":"说明:需要对多行区域进行循环且分栏展示时,则进行循环块设置并指定横向循环次数","merge":[0,2],"height":59}},"height":59},"2":{"cells":{"1":{"text":"","loopBlock":1},"2":{"text":"职员信息","style":2,"merge":[0,1],"height":34,"loopBlock":1},"4":{"text":"","loopBlock":1},"5":{"text":"","loopBlock":1}},"height":51},"3":{"cells":{"1":{"text":"姓名","style":4,"loopBlock":1},"2":{"text":"性别","style":4,"loopBlock":1},"3":{"text":"职务","style":4,"loopBlock":1},"4":{"text":"联系方式","style":4,"loopBlock":1},"5":{"text":"","loopBlock":1}},"height":31},"4":{"cells":{"1":{"text":"#{jm.name}","style":0,"loopBlock":1},"2":{"style":0,"loopBlock":1,"text":"#{jm.sex}"},"3":{"style":0,"loopBlock":1,"text":"#{jm.update_by}"},"4":{"style":0,"loopBlock":1,"text":"#{jm.jphone}"},"5":{"text":"","loopBlock":1}}},"5":{"cells":{"1":{"text":"","loopBlock":1},"5":{"text":"","loopBlock":1}},"height":34},"len":103},"rpbar":{"show":true,"pageSize":"","btnList":[]},"name":"sheet1","merges":["F2:G2","I2:K2","C3:D3"]}
```

View File

@@ -0,0 +1,31 @@
# 示例10员工信息登记带照片
**类型:** 单据模板(带图片占位)
**特征:** `imgList` 图片占位 + `virtual` 虚拟单元格 + `${employee.xxx}` 单值绑定 + 日期格式化
## 数据绑定
`${employee.num}``${employee.name}``${employee.sex}``${employee.birthday}``${employee.nation}``${employee.political}``${employee.native_place}``${employee.height}``${employee.weight}``${employee.health}``${employee.id_card}``${employee.education}``${employee.school}``${employee.major}``${employee.address}``${employee.zip_code}``${employee.email}``${employee.phone}``${employee.foreign_language}``${employee.foreign_language_level}``${employee.computer_level}``${employee.graduation_time}``${employee.arrival_time}``${employee.positional_titles}``${employee.education_experience}``${employee.work_experience}``${employee.create_time}`
## 图片占位配置
```json
{
"imgList": [{
"row": 3, "col": 6, "colspan": 1, "rowspan": 5,
"width": "135", "height": "201",
"src": "https://xxx.png",
"layer_id": "8mRFFslT5d0Hfyos",
"offsetX": 0, "offsetY": 0,
"virtualCellRange": [[3,6]]
}]
}
```
单元格引用图片:`"virtual":"8mRFFslT5d0Hfyos"` + `"merge":[4,0]`
## 报表 JSON
```json
{"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"},"hidden":{"rows":[],"cols":[],"conditions":{"rows":{},"cols":{}}},"dbexps":[],"toolPrintSizeObj":{"printType":"A4","widthPx":718,"heightPx":1047},"dicts":["sex1"],"freeze":"A1","dataRectWidth":710,"autofilter":{},"validations":[],"cols":{"0":{"width":64},"1":{"width":118},"2":{"width":71},"3":{"width":115},"4":{"width":83},"5":{"width":123},"6":{"width":136},"7":{"width":1},"len":50},"excel_config_id":"1316944968992034816","hiddenCells":[],"zonedEditionList":[],"rows":{"1":{"cells":{"0":{"text":"员工信息登记表","merge":[0,6],"style":28}},"height":46},"2":{"cells":{"0":{"text":"编号:","style":29},"1":{"text":"${employee.num}","style":30,"merge":[0,3]},"5":{"text":"填写日期:","style":29},"6":{"text":"${employee.create_time}","style":34}},"isDrag":true,"height":44},"3":{"cells":{"0":{"text":"姓名:","style":29},"1":{"text":"${employee.name}","style":30},"2":{"text":"性别:","style":29},"3":{"text":"${employee.sex}","style":30},"4":{"text":"出生年月:","style":29},"5":{"text":"${employee.birthday}","style":36},"6":{"style":3,"text":" ","merge":[4,0],"virtual":"8mRFFslT5d0Hfyos"}},"isDrag":true,"height":42},"4":{"cells":{"0":{"text":"民族:","style":29},"1":{"text":"${employee.nation}","style":30},"2":{"text":"政治面貌:","style":29},"3":{"text":"${employee.political}","style":30},"4":{"text":"籍贯:","style":29},"5":{"text":"${employee.native_place}","style":30}},"isDrag":true,"height":38},"5":{"cells":{"0":{"text":"身高(cm):","style":29},"1":{"text":"${employee.height}","style":30},"2":{"text":"体重(kg):","style":29},"3":{"text":"${employee.weight}","style":30},"4":{"text":"健康状况:","style":29},"5":{"text":"${employee.health}","style":30}},"isDrag":true,"height":38},"6":{"cells":{"0":{"text":"身份证号:","style":29},"1":{"text":"${employee.id_card}","style":30,"merge":[0,2]},"4":{"text":"学历:","style":29},"5":{"text":"${employee.education}","style":30}},"isDrag":true,"height":40},"7":{"cells":{"0":{"text":"毕业学校:","style":29},"1":{"text":"${employee.school}","style":30,"merge":[0,2]},"4":{"text":"专业:","style":29},"5":{"text":"${employee.major}","style":30}},"isDrag":true,"height":44},"8":{"cells":{"0":{"text":"联系地址:","style":29},"1":{"text":"${employee.address}","style":30,"merge":[0,2]},"4":{"text":"邮编:","style":29},"5":{"text":"${employee.zip_code}","style":30,"merge":[0,1]}},"isDrag":true,"height":45},"9":{"cells":{"0":{"text":"Email:","style":29},"1":{"text":"${employee.email}","style":30,"merge":[0,2]},"4":{"text":"手机号:","style":29},"5":{"text":"${employee.phone}","style":30,"merge":[0,1]}},"isDrag":true,"height":40},"10":{"cells":{"0":{"text":"外语语种:","style":29},"1":{"text":"${employee.foreign_language}","style":30},"2":{"text":"外语水平:","style":29},"3":{"text":"${employee.foreign_language_level}","style":30},"4":{"text":"计算机水平:","style":29},"5":{"text":"${employee.computer_level}","style":30,"merge":[0,1]}},"isDrag":true,"height":41},"11":{"cells":{"0":{"text":"毕业时间:","style":29},"1":{"text":"${employee.graduation_time}","style":34},"2":{"text":"到职时间:","style":29},"3":{"text":"${employee.arrival_time}","style":34},"4":{"text":"职称:","style":29},"5":{"text":"${employee.positional_titles}","style":30,"merge":[0,1]}},"isDrag":true,"height":42},"12":{"cells":{"0":{"text":"教育经历:","style":32},"1":{"text":" ","style":35,"merge":[0,5]}},"isDrag":true,"height":39},"13":{"cells":{"0":{"text":"${employee.education_experience}","style":33,"merge":[0,6]}},"isDrag":true,"height":70},"14":{"cells":{"0":{"text":"工作经历:","style":32},"1":{"merge":[0,5],"style":30,"text":" "}},"height":43},"15":{"cells":{"0":{"text":"${employee.work_experience}","style":30,"merge":[0,6]}},"isDrag":true,"height":61},"len":100},"name":"sheet1","merges":["A2:G2","B3:E3","G4:G8","B7:D7","B8:D8","B9:D9","F9:G9","B10:D10","F10:G10","F11:G11","F12:G12","B13:G13","A14:G14","B15:G15","A16:G16"],"imgList":[{"row":3,"col":6,"colspan":1,"rowspan":5,"width":"135","height":"201","src":"https://static.jeecg.com/designreport/images/QQ截图20210115102648_1610677626114.png","layer_id":"8mRFFslT5d0Hfyos","offsetX":0,"offsetY":0,"virtualCellRange":[[3,6]]}]}
```

View File

@@ -0,0 +1,164 @@
# 常用表达式函数示例
## 场景说明
展示积木报表中可用的表达式函数,包括日期函数、字符串函数、数学函数、条件表达式和行号表达式。表达式以 `=` 开头,可使用常量参数或数据集参数 `${dbCode.field}`
## 表达式语法规则
- 表达式以 `=` 开头:`=函数名(参数)`
- 常量参数直接写值:`=round(341.234, 2)`
- 数据集参数用 `${}` 引用:`=round(${jm_expression.num}, 2)`
- 字符串常量用双引号或单引号:`=date("2021-07-29")`
## 日期函数
| 函数 | 表达式 | 常量参数示例 | 数据集参数示例 |
|------|--------|-------------|---------------|
| `date()` | `date("2021-07-29 12:11:10")` | `=date("2021-07-29 12:11:10")` | `=date("${ds.create_time}")` |
| `time()` | `time("12:11:10")` | `=time("12:11:10")` | `=time("${ds.create_time}")` |
| `now()` | `now()` | `=now()` | `=now()` |
| `year()` | `year("2021-07-29 12:11:10")` | `=year("2021-07-29 12:11:10")` | `=year("${ds.create_time}")` |
| `month()` | `month("2021-07-29 12:11:10")` | `=month("2021-07-29 12:11:10")` | `=month("${ds.create_time}")` |
| `day()` | `day("2021-07-29 12:11:10")` | `=day("2021-07-29 12:11:10")` | `=day("${ds.create_time}")` |
### now() 格式化
通过 style 的 `format` 属性控制 `=now()` 的显示格式:
| format 值 | 输出格式 | 示例 |
|-----------|---------|------|
| `"date"` | yyyy-MM-dd | 2021-07-29 |
| `"date2"` | yyyy/MM/dd | 2021/07/29 |
| `"time"` | HH:mm:ss | 12:11:10 |
| `"datetime"` | yyyy-MM-dd HH:mm:ss | 2021-07-29 12:11:10 |
## 字符串函数
| 函数 | 表达式 | 常量参数示例 | 数据集参数示例 |
|------|--------|-------------|---------------|
| `char()` | `char(22269)` | `=char(22269)` | — |
| `cnmoney()` | `cnmoney(341.234)` | `=cnmoney(341.234)` | `=cnmoney(${ds.num})` |
| `cnmoney("b")` | `cnmoney(341.234,"b")` | `=cnmoney(341.234,"b")` | `=cnmoney(${ds.num},"b")` |
| `cnmoney("bw")` | `cnmoney(341.234,"bw")` | `=cnmoney(341.234,"bw")` | `=cnmoney(${ds.num},"bw")` |
| `concat()` | `concat('hello ','word',' !')` | `=concat('hello ','world',' !')` | `=concat('${ds.upper}',' world',' !')` |
| `lower()` | `lower('HELLOW')` | `=lower('HELLOW')` | `=lower("${ds.upper}")` |
| `upper()` | `upper('world')` | `=upper('world')` | `=upper('${ds.lower}')` |
### cnmoney() 参数说明
| 参数 | 说明 | 示例输出 |
|------|------|---------|
| 无 | 中文大写金额 | 叁佰肆拾壹元贰角叁分肆厘 |
| `"b"` | 简写 | 三四一.二三四 |
| `"bw"` | 简写带万 | 三百四十一.二三四 |
## 数学函数
| 函数 | 表达式 | 常量参数示例 | 数据集参数示例 |
|------|--------|-------------|---------------|
| `rand()` | `rand()` | `=rand()` | — |
| `rand()*N` | `rand()*100` | `=rand()*100` | — |
| `round()` | `round(341.234,2)` | `=round(341.234,2)` | `=round(${ds.num},2)` |
| `round(rand())` | `round(rand(),2)` | `=round(rand(),2)` | — |
| `abs()` | `abs(-341.234)` | `=abs(-341.234)` | `=abs(${ds.num})` |
| `floor()` | `floor(341.234,2)` | `=floor(341.234,2)` | `=floor(${ds.num},2)` |
| `ceil()` | `ceil(341.234,2)` | `=ceil(341.234,2)` | `=ceil(${ds.num},2)` |
| `trunc()` | `trunc(341.234)` | `=trunc(341.234)` | `=trunc(${ds.num})` |
## 条件表达式
### case() — 简单条件
```
=case(条件, 真值, 假值)
```
常量:`=case(1==1,'男','女')`
数据集:`=case('${ds.sex}'=='1','男','女')`
### if() — 多分支条件
```
=(let sex='${ds.sex}';
if(sex== '1'){
return '男';
}elsif(sex== '2'){
return '女';
}else{
return '未知';
})
```
**注意:** 多行 if 表达式需要用 `()` 包裹整个表达式,用 `let` 声明变量,分支用 `elsif`(不是 `else if`)。
## 行号表达式
| 函数 | 说明 | 示例 |
|------|------|------|
| `row()` | 自动行号 | `=row(1)` 从1开始编号 |
## 样式中的 format 属性
styles 数组中可通过 `format` 控制单元格显示格式:
```json
{ "format": "date" }
{ "format": "date2" }
{ "format": "time" }
{ "format": "datetime" }
```
对应的 style 索引(本示例中):
| 索引 | format | 边框 | 用途 |
|------|--------|------|------|
| 0 | date | 无 | 日期格式(无边框) |
| 1 | date2 | 无 | 日期格式2(无边框) |
| 2 | time | 无 | 时间格式(无边框) |
| 3 | datetime | 无 | 日期时间格式(无边框) |
| 5 | date | thin四边 | 日期格式(带边框) |
| 6 | date2 | thin四边 | 日期格式2(带边框) |
| 7 | time | thin四边 | 时间格式(带边框) |
| 8 | datetime | thin四边 | 日期时间格式(带边框) |
## 单元格链接display: link
```json
{
"text": "更多表达式请查看详细文档。",
"linkIds": "580872825561501696,580872825561501696",
"display": "link",
"merge": [0, 1]
}
```
| 属性 | 说明 |
|------|------|
| `display` | `"link"` 表示显示为超链接 |
| `linkIds` | 链接目标的报表ID多个用逗号分隔 |
## 单元格纵向合并(数据行内)
```json
"5": {
"cells": {
"1": {
"text": "now()",
"merge": [3, 0],
"height": 100,
"style": 4
}
}
}
```
`merge: [3, 0]` 表示向下合并3行、向右合并0列即占据第5-8行的第1列。
## 样式方案(绿色主题)
| 索引 | 背景色 | 用途 |
|------|--------|------|
| 12 | #93d051(绿色) | 分类标题行(日期函数/字符串函数等) |
| 17 | #93d051(绿色) | 表头行(函数名称/表达式/常量参数/数据集参数) |
| 4 | — | 数据行thin四边框 |

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
# 示例8固定表头表尾
**类型:** 分组报表 + 固定打印表头表尾
**特征:** `fixedPrintHeadRows`/`fixedPrintTailRows` + 横向分组 `groupRight` + 纵向分组 `group` + 动态聚合 `dynamic`
## 关键配置
```json
{
"fixedPrintHeadRows": [{"sci":1,"eci":3,"sri":1,"eri":2}],
"fixedPrintTailRows": [{"sri":6,"sci":1,"eri":6,"eci":5}],
"isGroup": true,
"groupField": "xs.diqu"
}
```
## 数据绑定语法
- 横向分组(年):`#{xs.groupRight(year)}``direction:"right"``aggregate:"group"``sort:"desc"`
- 横向分组(月):`#{xs.groupRight(mouth)}``aggregate:"group"``direction:"right"`
- 纵向分组(地区):`#{xs.group(diqu)}``aggregate:"group"``subtotal:"groupField"`
- 纵向分组(分类):`#{xs.group(class)}``aggregate:"group"`
- 动态聚合(销量):`#{xs.dynamic(sales)}``aggregate:"dynamic"``funcname:"SUM"`
- 合计行:`=sum(D4)`
- 斜线表头:`lineStart:"lefttop"``text:"地区|销量|时间"`
- 固定表头标记:`fixedHead:1`
- 固定表尾标记:`fixedTail:1`
## 报表 JSON
```json
{"loopBlockList":[],"printConfig":{"layout":"portrait","paper":"A4","isBackend":false,"width":210,"definition":1,"marginX":10,"height":297,"marginY":10},"dbexps":[],"toolPrintSizeObj":{"printType":"A4","widthPx":718,"heightPx":1047},"dicts":[],"freeze":"A1","dataRectWidth":713,"autofilter":{},"validations":[],"cols":{"0":{"width":36},"1":{"width":95},"2":{"width":95},"4":{"width":141},"5":{"width":246},"6":{"width":155},"len":50},"area":{"sri":6,"sci":1,"eri":6,"eci":5,"width":677,"height":25},"excel_config_id":"739738655920574464","zonedEditionList":[],"rows":{"0":{"cells":{"1":{"merge":[0,2],"style":6,"text":"固定表头表尾打印实例"},"5":{"style":30,"text":"说明:本示例在横向分组、纵向分组基础上,添加固定表头表尾。在打印时可显示表头及表尾"}},"height":83},"1":{"cells":{"1":{"lineStart":"lefttop","merge":[1,1],"style":2,"text":"地区|销量|时间","fixedHead":1,"height":74},"2":{"text":"","fixedHead":1},"3":{"style":8,"text":"#{xs.groupRight(year)}年","sort":"desc","fixedHead":1,"aggregate":"group","direction":"right"}},"height":40},"2":{"cells":{"1":{"text":"","fixedHead":1},"2":{"text":"","fixedHead":1},"3":{"style":8,"text":"#{xs.groupRight(mouth)}","sort":"default","fixedHead":1,"aggregate":"group","direction":"right"}},"height":34},"3":{"cells":{"1":{"subtotal":"groupField","style":28,"text":"#{xs.group(diqu)}","aggregate":"group"},"2":{"style":28,"text":"#{xs.group(class)}","aggregate":"group"},"3":{"decimalPlaces":"0","funcname":"SUM","style":29,"text":"#{xs.dynamic(sales)}","aggregate":"dynamic"}},"height":38},"4":{"cells":{"1":{"merge":[0,1],"style":24,"text":"总计"},"3":{"style":25,"text":"=sum(D4)"}},"height":37},"6":{"cells":{"1":{"style":32,"text":"审核:","fixedTail":1},"2":{"style":32,"text":"张三","fixedTail":1},"3":{"style":32,"fixedTail":1},"4":{"style":32,"text":"复审:","fixedTail":1},"5":{"style":32,"text":"李四","fixedTail":1}}},"len":100},"rpbar":{"show":true,"pageSize":"","btnList":[]},"groupField":"xs.diqu","fixedPrintHeadRows":[{"sci":1,"eci":3,"sri":1,"eri":2}],"fixedPrintTailRows":[{"sri":6,"sci":1,"eri":6,"eci":5}],"displayConfig":{},"background":false,"name":"sheet1","isGroup":true,"merges":["B1:D1","B2:C3","B5:C5"]}
```

View File

@@ -0,0 +1,29 @@
# 示例11横向分组
**类型:** 横向分组统计表
**特征:** `customGroup()` + `direction:"right"` 数据横向展开
## 关键语法
所有数据行使用 `#{hex.customGroup(字段名)}` + `direction:"right"` 实现横向展开:
```json
{"text":"#{hex.customGroup(department)}","style":11,"direction":"right"}
```
## 数据字段
| 行 | 标签 | 绑定 |
|---|---|---|
| 2 | 部门 | `#{hex.customGroup(department)}` |
| 3 | 学历 | `#{hex.customGroup(education)}` |
| 4 | 性别 | `#{hex.customGroup(sex)}` |
| 5 | 年龄 | `#{hex.customGroup(age)}` (无 direction纵向 |
| 6 | 姓名 | `#{hex.customGroup(name)}` |
| 7 | 薪水 | `#{hex.customGroup(salary)}` |
## 报表 JSON
```json
{"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},"hidden":{"rows":[],"cols":[],"conditions":{"rows":{},"cols":{}}},"dbexps":[],"toolPrintSizeObj":{"printType":"A4","widthPx":718,"heightPx":1047},"dicts":[],"freeze":"A1","dataRectWidth":204,"isViewContentHorizontalCenter":false,"autofilter":{},"validations":[],"cols":{"0":{"width":44},"1":{"width":79},"2":{"width":81},"len":50},"area":{"sri":7,"sci":5,"eri":7,"eci":5,"width":100,"height":36},"excel_config_id":"1194552262320803840","hiddenCells":[],"zonedEditionList":[],"rows":{"1":{"cells":{"0":{"text":"员工信息横向统计表","style":9,"merge":[0,11]}},"height":97},"2":{"cells":{"1":{"text":"部门","style":7},"2":{"text":"#{hex.customGroup(department)}","style":11,"direction":"right"}},"isDrag":true,"height":40},"3":{"cells":{"1":{"text":"学历","style":7},"2":{"text":"#{hex.customGroup(education)}","style":11,"direction":"right"}},"isDrag":true,"height":39},"4":{"cells":{"1":{"text":"性别","style":7},"2":{"text":"#{hex.customGroup(sex)}","style":11,"direction":"right"}},"isDrag":true,"height":41},"5":{"cells":{"1":{"text":"年龄","style":7},"2":{"text":"#{hex.customGroup(age)}","style":11}},"isDrag":true,"height":39},"6":{"cells":{"1":{"text":"姓名","style":7},"2":{"text":"#{hex.customGroup(name)}","style":11,"direction":"right"}},"isDrag":true,"height":40},"7":{"cells":{"1":{"text":"薪水","style":7},"2":{"text":"#{hex.customGroup(salary)}","style":11,"direction":"right"}},"isDrag":true,"height":36},"len":100},"name":"sheet1","fillFormStyle":"default","merges":["A2:L2"]}
```

View File

@@ -0,0 +1,30 @@
# 示例9实习证明
**类型:** 单据模板(带背景图)
**特征:** `background` 背景图 + `${tt.xxx}` 单值绑定 + 自由布局
## 数据绑定
- `${tt.name}` — 姓名
- `${tt.pingjia}` — 评价内容多行文本merge跨4行5列
- `${tt.lingdao}` — 证明人
- `${tt.shijian}` — 日期
## 关键配置
```json
{
"background": {
"path": "https://static.jeecg.com/designreport/images/11_1611283832037.png",
"repeat": "no-repeat",
"width": "",
"height": ""
}
}
```
## 报表 JSON
```json
{"loopBlockList":[],"area":{"sri":28,"sci":9,"eri":28,"eci":9,"width":100,"height":25},"excel_config_id":"1347373863746539520","printConfig":{"layout":"portrait","paper":"A4","isBackend":false,"width":210,"definition":1,"marginX":10,"height":297,"marginY":10},"hiddenCells":[],"zonedEditionList":[],"rows":{"7":{"cells":{"2":{"merge":[0,4],"style":2,"text":"实习证明"}},"height":41},"10":{"cells":{"2":{"style":11,"text":"${tt.name}"},"3":{"merge":[0,3],"style":19,"text":"同学在我公司与 2020年4月1日 至 2020年5月1日 实习。","height":34}},"height":34},"12":{"cells":{"2":{"merge":[3,4],"style":13,"text":"${tt.pingjia}","height":129}},"height":36},"17":{"cells":{"2":{"style":12,"text":"特此证明!"}}},"22":{"cells":{"4":{"style":11,"text":"证明人:"},"5":{"style":12,"text":"${tt.lingdao}"}}},"23":{"cells":{"5":{"style":15,"text":"${tt.shijian}"}}},"len":100},"dbexps":[],"dicts":[],"freeze":"A1","dataRectWidth":707,"displayConfig":{},"background":{"path":"https://static.jeecg.com/designreport/images/11_1611283832037.png","repeat":"no-repeat","width":"","height":""},"name":"sheet1","autofilter":{},"validations":[],"cols":{"0":{"width":69},"1":{"width":41},"4":{"width":119},"5":{"width":147},"6":{"width":31},"len":50},"merges":["C8:G8","D11:G11","C13:G16"]}
```

View File

@@ -0,0 +1,15 @@
# 示例4循环块明细表员工信息卡片
**类型:** 循环块报表
**特征:** `loopBlockList` 定义循环区域,每条数据渲染一个卡片,支持二维码 `display:"qrcode"`
## 关键配置
- `loopBlockList``[{"sci":1,"sri":2,"eci":7,"eri":5,"index":1,"db":"uiu"}]`
- 二维码:单元格 `"display":"qrcode"` + `displayConfig` 配置宽高颜色
## 报表 JSON
```json
{"loopBlockList":[{"sci":1,"sri":2,"eci":7,"eri":5,"index":1,"db":"uiu"}],"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"},"hidden":{"rows":[],"cols":[],"conditions":{"rows":{},"cols":{}}},"queryFormSetting":{"useQueryForm":false,"dbKey":"","idField":""},"dbexps":[],"toolPrintSizeObj":{"printType":"A4","widthPx":718,"heightPx":1047},"dicts":[],"freeze":"A1","dataRectWidth":688,"isViewContentHorizontalCenter":false,"autofilter":{},"validations":[],"cols":{"0":{"width":30},"1":{"width":94},"2":{"width":96},"3":{"width":81},"4":{"width":93},"5":{"width":88},"6":{"width":90},"7":{"width":116},"8":{"width":22},"len":50},"pyGroupEngine":false,"submitHandlers":[],"excel_config_id":"1176098706643308544","hiddenCells":[],"zonedEditionList":[],"rows":{"1":{"cells":{"1":{"text":"员工信息明细表","merge":[0,5],"style":32}},"height":64},"2":{"cells":{"1":{"text":"姓名:","style":28,"loopBlock":1},"2":{"style":30,"merge":[0,1],"loopBlock":1,"text":"#{uiu.name}"},"4":{"text":"所在部门:","style":29,"loopBlock":1},"5":{"style":30,"merge":[0,1],"loopBlock":1,"text":"#{uiu.department}"},"7":{"merge":[2,0],"height":75,"style":9,"text":"#{uiu.tm}","display":"qrcode","loopBlock":1}},"height":42},"3":{"cells":{"1":{"text":"年龄:","style":28,"loopBlock":1},"2":{"style":30,"merge":[0,1],"loopBlock":1,"text":"#{uiu.age}"},"4":{"text":"学历:","style":29,"loopBlock":1},"5":{"style":30,"merge":[0,1],"loopBlock":1,"text":"#{uiu.education}"},"7":{"text":"","loopBlock":1}},"height":35},"4":{"cells":{"1":{"text":"性别:","style":28,"loopBlock":1},"2":{"style":30,"merge":[0,1],"loopBlock":1,"text":"#{uiu.sex}"},"4":{"text":"薪水:","style":29,"loopBlock":1},"5":{"style":30,"merge":[0,1],"loopBlock":1,"text":"#{uiu.salary}"},"7":{"text":"","loopBlock":1}},"height":35},"5":{"cells":{"1":{"text":"","loopBlock":1},"2":{"text":"","loopBlock":1},"3":{"text":"","loopBlock":1},"4":{"text":"","loopBlock":1},"5":{"text":"","loopBlock":1},"6":{"text":"","loopBlock":1},"7":{"text":"","loopBlock":1}},"height":17},"len":100},"rpbar":{"show":true,"pageSize":"","btnList":[]},"displayConfig":{"11":{"text":"#{uiu.tm}","width":117,"height":117,"colorDark":"#000000","colorLight":"#ffffff"}},"name":"sheet1","merges":["B2:G2","C3:D3","F3:G3","H3:H5","C4:D4","F4:G4","C5:D5","F5:G5"]}
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,304 @@
# 普通列表报表示例
## 场景说明
一个标准的普通列表报表,数据集编码为 `aa`SQL为 `select * from demo`展示6个字段id、name、key_word、punch_time、salary_money、bonus_money。
- 表头行第1行蓝底白字行高34px
- 数据行第2行居中+垂直居中,通过 `#{aa.字段名}` 绑定数据
## 完整 jsonStr
```json
{
"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": 12,
"sci": 3,
"eri": 12,
"eci": 3,
"width": 100,
"height": 25
},
"pyGroupEngine": false,
"submitHandlers": [],
"excel_config_id": "1193766682428530688",
"hiddenCells": [],
"zonedEditionList": [],
"rows": {
"1": {
"cells": {
"1": { "text": "id", "style": 4 },
"2": { "text": "name", "style": 4 },
"3": { "text": "key_word", "style": 4 },
"4": { "text": "punch_time", "style": 4 },
"5": { "text": "salary_money", "style": 4 },
"6": { "text": "bonus_money", "style": 4 }
},
"height": 34
},
"2": {
"cells": {
"1": { "text": "#{aa.id}", "style": 2 },
"2": { "text": "#{aa.name}", "style": 2 },
"3": { "text": "#{aa.key_word}", "style": 2 },
"4": { "text": "#{aa.punch_time}", "style": 2 },
"5": { "text": "#{aa.salary_money}", "style": 2 },
"6": { "text": "#{aa.bonus_money}", "style": 2 }
}
},
"len": 200
},
"rpbar": {
"show": true,
"pageSize": "",
"btnList": []
},
"fixedPrintHeadRows": [],
"fixedPrintTailRows": [],
"displayConfig": {},
"fillFormInfo": {
"layout": {
"direction": "horizontal",
"width": 200,
"height": 45
}
},
"background": false,
"name": "sheet1",
"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"
}
],
"fillFormStyle": "default",
"freezeLineColor": "rgb(185, 185, 185)",
"merges": []
}
```
## 结构要点
### rows 布局
| 行号 | 用途 | style | 说明 |
|------|------|-------|------|
| 1 | 表头行 | 4蓝底白字 | `height: 34`text为字段显示名 |
| 2 | 数据行 | 2居中+垂直居中) | text为 `#{数据集编码.字段名}` |
### styles 索引对照
| 索引 | 边框 | 水平对齐 | 垂直对齐 | 背景色 | 字体色 | 典型用途 |
|------|------|---------|---------|--------|--------|---------|
| 0 | thin #000 | — | — | — | — | 基础单元格 |
| 1 | thin #000 | center | — | — | — | 居中文本 |
| 2 | thin #000 | center | middle | — | — | **数据行** |
| 3 | thin #000 | center | middle | #01b0f1 | — | 蓝底表头(无白字) |
| 4 | thin #000 | center | middle | #01b0f1 | #ffffff | **表头行(推荐)** |
### 数据绑定规则
- 数据集编码 `aa` 对应 saveDb 时的 `dbCode: "aa"`
- 绑定语法: `#{aa.字段名}` — 字段名来自 fieldList 中的 `fieldName`
- 列号从 1 开始0列通常留空
### 对应的数据集配置
```json
{
"jimuReportId": "1193766682428530688",
"dbCode": "aa",
"dbChName": "aa",
"dbType": "0",
"dbSource": "",
"isList": "1",
"isPage": "1",
"dbDynSql": "select * from demo",
"fieldList": [
{ "fieldName": "id", "fieldText": "id", "widgetType": "String", "orderNum": 0 },
{ "fieldName": "name", "fieldText": "name", "widgetType": "String", "orderNum": 1 },
{ "fieldName": "key_word", "fieldText": "key_word", "widgetType": "String", "orderNum": 2 },
{ "fieldName": "punch_time", "fieldText": "punch_time", "widgetType": "String", "orderNum": 3 },
{ "fieldName": "salary_money", "fieldText": "salary_money", "widgetType": "String", "orderNum": 4 },
{ "fieldName": "bonus_money", "fieldText": "bonus_money", "widgetType": "String", "orderNum": 5 }
],
"paramList": []
}
```
### 正确的 /jmreport/save 请求格式
> **关键jsonStr 内容rows、cols、styles 等)必须放在请求体顶层,和 `designerObj` 同级。禁止嵌套在 `designerObj.jsonStr` 中,否则后端会清空 rows 数据。**
>
> 后端 `saveReport` 逻辑:`json.remove("designerObj")` 后,剩余的顶层 JSON 直接作为 jsonStr 存入数据库。
```json
{
"designerObj": {
"id": "1193766682428530688",
"name": "普通列表示例",
"type": "0",
"template": 0,
"delFlag": 0,
"viewCount": 0,
"updateCount": 0,
"submitForm": 0,
"reportName": "普通列表示例"
},
"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": 12, "sci": 3, "eri": 12, "eci": 3, "width": 100, "height": 25 },
"pyGroupEngine": false,
"submitHandlers": [],
"excel_config_id": "1193766682428530688",
"hiddenCells": [],
"zonedEditionList": [],
"rows": {
"1": {
"cells": {
"1": { "text": "id", "style": 4 },
"2": { "text": "name", "style": 4 },
"3": { "text": "key_word", "style": 4 },
"4": { "text": "punch_time", "style": 4 },
"5": { "text": "salary_money", "style": 4 },
"6": { "text": "bonus_money", "style": 4 }
},
"height": 34
},
"2": {
"cells": {
"1": { "text": "#{aa.id}", "style": 2 },
"2": { "text": "#{aa.name}", "style": 2 },
"3": { "text": "#{aa.key_word}", "style": 2 },
"4": { "text": "#{aa.punch_time}", "style": 2 },
"5": { "text": "#{aa.salary_money}", "style": 2 },
"6": { "text": "#{aa.bonus_money}", "style": 2 }
}
},
"len": 200
},
"rpbar": { "show": true, "pageSize": "", "btnList": [] },
"fixedPrintHeadRows": [],
"fixedPrintTailRows": [],
"displayConfig": {},
"fillFormInfo": { "layout": { "direction": "horizontal", "width": 200, "height": 45 } },
"background": false,
"name": "sheet1",
"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" }
],
"fillFormStyle": "default",
"freezeLineColor": "rgb(185, 185, 185)",
"merges": []
}
```

View File

@@ -0,0 +1,180 @@
# 处方笺模板示例(表单+列表混合)
## 场景说明
一个医院处方笺模板,包含:
- **单条数据**(患者信息):使用 `${yonghu.字段名}` 绑定
- **列表数据**(药品明细):使用 `#{yaopin.字段名}` 绑定
- 外边框用 **thick**(粗线),内部用 **thin**(细线)
- 自定义列宽,多处单元格合并
- 隐藏行(`-1` 行)存放辅助字段
## 数据绑定语法对比
| 语法 | 数据类型 | 说明 | 示例 |
|------|---------|------|------|
| `${dbCode.field}` | 单条记录 | 直接取值,不循环 | `${yonghu.yphone}` |
| `#{dbCode.field}` | 列表数据 | 自动循环展开 | `#{yaopin.name}` |
## 布局结构(行分布)
| 行号 | 内容 | 高度 | 说明 |
|------|------|------|------|
| 0 | 顶部留白 | 96px | 预留logo/印章区域 |
| 1 | 上边框线 | 18px | thick边框顶边 |
| 2 | 标题"智能医学院处方笺" | 124px | 合并C3:L3style 3814号加粗居中 |
| 3 | 姓名/性别/年龄 | 默认 | `${yonghu.yphone}` `${yonghu.ysex}` `${yonghu.yage}` |
| 4 | 单位/电话 | 29px | `${yonghu.danwei}` `${yonghu.yphone}` |
| 5 | 初步诊断 | 34px | `${yonghu.yjieguo}` 合并7列 |
| 6 | RP标记 | 79px | 处方开始标志 |
| 7 | **药品列表行** | 37px | `#{yaopin.name}` `#{yaopin.percent}` — 自动循环 |
| 8 | 空行间隔 | 27px | |
| 9 | 医嘱 | 默认 | `${yonghu.yizhu}` 合并8列 |
| 10-12 | 费用明细 | 默认 | 药品费/中成药费/治疗费/检查费等 |
| 13 | 合计 | 默认 | `${yonghu.ytotal}` |
| 14 | 空行 | 17px | |
| 15 | 医师/日期 | 43px | `${yonghu.yishe}` `${yonghu.kdata}` |
| 16 | 空行 | 17px | |
| 17 | 下边框线 | 默认 | thick边框底边 |
| -1 | **隐藏行** | — | `#{yaopin.key1}` `#{yaopin.key2}` 辅助数据 |
## 关键特性
### 1. 隐藏行(-1行
```json
"-1": {
"cells": {
"0": { "text": "#{yaopin.key2}" },
"-1": { "text": "#{yaopin.key1}" }
},
"isDrag": true
}
```
用于存放不需要显示但参与数据处理的字段,行号为 `-1`,列号可以为 `-1`
### 2. 粗细边框方案
外框用 `thick`,内部用 `thin`,通过不同 style 组合实现:
```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ← 行1: thick top + thick left/right
┃ 标题 ┃ ← 行2-16: thick left + thick right
┃ ──────────────────────────── ┃ ← 内部分隔: thin border
┃ 内容 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ← 行17: thick bottom + thick left/right
```
边框样式索引分类:
| 索引范围 | 边框类型 | 用途 |
|---------|---------|------|
| 24-26 | thick top + left/right | 顶部边框行 |
| 27, 31 | thick left | 左边框列 |
| 28 | thick right | 右边框列 |
| 32-34 | thick bottom + left/right | 底部边框行 |
| 6-7 | thin 四边 | 内部费用格子 |
### 3. 自定义列宽
```json
"cols": {
"0": { "width": 35 },
"1": { "width": 14 },
"2": { "width": 56 },
"3": { "width": 54 },
"4": { "width": 156 },
"5": { "width": 41 },
"6": { "width": 31 },
"7": { "width": 113 },
"8": { "width": 58 },
"9": { "width": 20 },
"10": { "width": 23 },
"11": { "width": 81 },
"12": { "width": 12 },
"len": 50
}
```
### 4. 合并单元格
```json
"merges": [
"C3:L3", // 标题横跨10列
"C4:D4", // 姓名标签
"C5:D5", // 单位标签
"C6:D6", // 初步诊断标签
"E6:L6", // 诊断结果跨8列
"B7:D7", // RP标记
"C7:E7", // 药品名称
"H7:I7", // 药品规格
"D10:L10", // 医嘱跨9列
"C11:D11", // 药品费
"F11:G11", // 中成药费
"I11:K11", // 治疗费
"E13:H13", // 合计金额
"J16:L16", // 日期
...
]
```
### 5. isDrag 属性
```json
"3": { "cells": {...}, "isDrag": true }
```
`isDrag: true` 表示该行的高度曾被用户手动拖拽调整过。
### 6. toolPrintSizeObj打印尺寸
```json
"toolPrintSizeObj": {
"printType": "A4",
"widthPx": 718,
"heightPx": 1047
}
```
A4纸张的像素尺寸用于打印预览。
## 对应的数据集配置
### 数据集1yonghu患者信息单条
```json
{
"dbCode": "yonghu",
"dbChName": "患者信息",
"dbType": "0",
"isList": "0",
"isPage": "0",
"dbDynSql": "select yphone, ysex, yage, danwei, yjieguo, yizhu, yprice, yzhenliao, ytotal, yishe, kdata from yonghu_table where id = '${id}'"
}
```
### 数据集2yaopin药品明细列表
```json
{
"dbCode": "yaopin",
"dbChName": "药品明细",
"dbType": "0",
"isList": "1",
"isPage": "0",
"dbDynSql": "select name, percent, key1, key2 from yaopin_table where chufang_id = '${id}'"
}
```
## 与普通列表的区别
| 特性 | 普通列表 | 处方笺(表单混合) |
|------|---------|-------------------|
| 数据集数量 | 通常1个 | 多个yonghu + yaopin |
| 绑定语法 | 全部用 `#{}` | 单条用 `${}`,列表用 `#{}` |
| 布局 | 表头+数据行 | 自由布局,多区域 |
| 边框 | 统一thin | 外粗内细 |
| 列宽 | 默认均匀 | 自定义不等宽 |
| 合并单元格 | 少/无 | 大量合并 |
| 隐藏行 | 无 | `-1` 行存辅助数据 |

View File

@@ -0,0 +1,17 @@
# 示例7处方签
**类型:** 自由布局单据
**特征:** 复杂边框布局thick/thin混用、主表 `${yonghu.xxx}` + 药品列表 `#{yaopin.xxx}`
## 数据绑定
- 主表(单值):`${yonghu.yphone}``${yonghu.ysex}``${yonghu.yage}``${yonghu.danwei}``${yonghu.yjieguo}``${yonghu.yizhu}`
- 药品列表:`#{yaopin.name}``#{yaopin.percent}`
- 费用汇总:`${yonghu.yprice}``${yonghu.yzhenliao}``${yonghu.ytotal}`
- 签名:`${yonghu.yishe}``${yonghu.kdata}`
## 报表 JSON
```json
{"loopBlockList":[],"area":{"sri":10,"sci":5,"eri":10,"eci":6,"width":72,"height":25},"excel_config_id":"6059e405dd9c66a6d38e00841d2e40cc","printConfig":{"paper":"A4","width":210,"height":297,"definition":1,"isBackend":false},"rows":{"0":{"cells":{"3":{"style":80,"text":" "}},"height":96},"1":{"cells":{"1":{"style":24,"text":" "},"2":{"style":25,"text":" "},"3":{"style":25,"text":" "},"4":{"style":25,"text":" "},"5":{"style":25,"text":" "},"6":{"style":25,"text":" "},"7":{"style":25,"text":" "},"8":{"style":25,"text":" "},"9":{"style":25,"text":" "},"10":{"style":25,"text":" "},"11":{"style":25,"text":" "},"12":{"style":26,"text":" "}},"height":18},"2":{"cells":{"1":{"text":" ","style":27},"2":{"merge":[0,9],"text":"智能医学院处方笺","style":38},"12":{"style":28,"text":" "}},"height":124},"3":{"cells":{"1":{"text":" ","style":46},"2":{"merge":[0,1],"text":"姓名:","style":4},"4":{"text":"${yonghu.yphone}"},"5":{"text":"性别:","style":42},"6":{"text":"${yonghu.ysex}","style":42},"7":{"text":"年龄:","style":47},"8":{"text":"${yonghu.yage}"},"11":{"style":69,"text":" ","merge":[0,1]},"12":{"style":43,"text":" "}},"isDrag":true},"4":{"cells":{"1":{"text":" ","style":74},"2":{"style":4,"merge":[0,1],"text":"单位:"},"4":{"text":"${yonghu.danwei}"},"5":{"text":"电话:"},"6":{"text":"${yonghu.yphone}","merge":[0,5]},"12":{"style":28,"text":" "}},"isDrag":true,"height":29},"5":{"cells":{"1":{"style":31,"text":" "},"2":{"merge":[0,1],"text":"初步诊断:","style":4},"4":{"text":"${yonghu.yjieguo}","merge":[0,7]},"12":{"style":28,"text":" "}},"isDrag":true,"height":34},"6":{"cells":{"1":{"text":" RP","merge":[0,2],"style":79},"12":{"style":28,"text":" "}},"height":79},"7":{"cells":{"1":{"text":".","style":48},"3":{"text":"#{yaopin.name}","merge":[0,1]},"7":{"text":"#{yaopin.percent}","merge":[0,1]},"12":{"style":28,"text":" "}},"isDrag":true,"height":37},"9":{"cells":{"1":{"style":31,"text":" "},"2":{"text":"医嘱:","style":76},"3":{"text":"${yonghu.yizhu}","style":6,"merge":[0,8]},"12":{"style":28,"text":" "}},"isDrag":true},"10":{"cells":{"1":{"style":31,"text":" "},"2":{"text":"药品费","style":6,"merge":[0,1]},"4":{"text":"${yonghu.yprice}","style":6},"5":{"merge":[0,1],"text":"中成药费","style":6},"8":{"text":"治疗费","merge":[0,2],"style":6},"12":{"style":28,"text":" "}},"isDrag":true},"13":{"cells":{"1":{"style":31,"text":" "},"2":{"text":"合计","style":6,"merge":[0,1]},"4":{"text":"${yonghu.ytotal}","style":6,"merge":[0,7]},"12":{"style":28,"text":" "}},"isDrag":true},"15":{"cells":{"1":{"style":31,"text":" "},"2":{"text":"医师:","style":4,"merge":[0,1]},"4":{"text":"${yonghu.yishe}","style":80},"8":{"text":"日期:","style":4},"9":{"text":"${yonghu.kdata}","style":80,"merge":[0,2]},"12":{"style":71,"text":" "}},"isDrag":true,"height":43},"len":94},"dbexps":[],"dicts":[],"freeze":"A1","dataRectWidth":694,"displayConfig":{},"background":false,"name":"sheet1","autofilter":{},"validations":[],"cols":{"0":{"width":35},"1":{"width":14},"2":{"width":56},"3":{"width":54},"4":{"width":156},"5":{"width":41},"6":{"width":31},"7":{"width":113},"8":{"width":58},"9":{"width":20},"10":{"width":23},"11":{"width":81},"12":{"width":12},"len":50},"merges":["C3:E3","C7:E7","H3:I3","H7:I7","F11:G11","I11:K11","F12:G12","I12:K12","I13:K13","E13:H13","C11:D11","C12:D12","C13:D13","C14:D14","L4:M4","C3:L3","B7:D7","C4:D4","C5:D5","E14:L14","D10:L10","G5:L5","C6:D6","E6:L6","J16:L16","C16:D16","D8:E8","H8:I8"]}
```

View File

@@ -0,0 +1,287 @@
# 纵向分组小计报表示例
## 场景说明
员工信息登记表,按**部门**分组(一级),按**学历**分组(二级),自动合并相同分组的单元格,并在分组末尾显示小计/合计行。
## 分组效果预览
```
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ 部门 │ 学历 │ 性别 │ 年龄 │ 姓名 │ 薪水 │
├──────┼──────┼──────┼──────┼──────┼──────┤
│ │ │ 男 │ 28 │ 张三 │ 8000 │
│ │ 本科 ├──────┼──────┼──────┼──────┤
│ │ │ 女 │ 25 │ 李四 │ 7500 │
│ 研发 │ ├──────┼──────┼──────┼──────┤
│ 部 │ │ 小计 │15500 │
│ ├──────┼──────┼──────┼──────┼──────┤
│ │ 硕士 │ 男 │ 30 │ 王五 │ 12000│
│ │ ├──────┼──────┼──────┼──────┤
│ │ │ 小计 │12000 │
├──────┼──────┼──────┼──────┼──────┼──────┤
│ │ 合计 │27500 │
├──────┼──────┼──────┼──────┼──────┼──────┤
│ ... │ ... │ ... │ ... │ ... │ ... │
└──────┴──────┴──────┴──────┴──────┴──────┘
```
## 核心配置
### 1. 分组字段声明
jsonStr 顶层需要两个属性:
```json
{
"isGroup": true,
"groupField": "vegvkdueqw.department"
}
```
| 属性 | 说明 |
|------|------|
| `isGroup` | `true` 启用分组模式 |
| `groupField` | 主分组字段,格式 `数据集编码.字段名` |
### 2. 数据行分组绑定
```json
"3": {
"cells": {
"1": {
"style": 17,
"text": "#{vegvkdueqw.group(department)}",
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "合计"
},
"2": {
"style": 17,
"text": "#{vegvkdueqw.group(education)}",
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "小计"
},
"3": { "style": 17, "text": "#{vegvkdueqw.sex}" },
"4": { "style": 17, "text": "#{vegvkdueqw.age}" },
"5": { "style": 17, "text": "#{vegvkdueqw.name}" },
"6": { "style": 17, "text": "#{vegvkdueqw.salary}" }
},
"height": 54
}
```
### 3. 分组单元格属性详解
| 属性 | 值 | 说明 |
|------|-----|------|
| `text` | `#{dbCode.group(fieldName)}` | 分组绑定语法,自动合并相同值的单元格 |
| `aggregate` | `"group"` | 标记为分组聚合列 |
| `subtotal` | `"groupField"` | 启用小计行 |
| `funcname` | `"-1"` | 小计函数:`"-1"`=不计算(仅显示文本),可选 `"SUM"` `"AVG"` `"COUNT"` 等 |
| `subtotalText` | `"合计"` / `"小计"` | 小计行显示的文本 |
### 4. 分组绑定语法
| 语法 | 说明 |
|------|------|
| `#{dbCode.group(field)}` | 分组字段,相同值自动合并单元格 |
| `#{dbCode.field}` | 普通字段,每行独立显示 |
### 5. 多级分组
- **一级分组**部门subtotalText = `"合计"` — 部门切换时显示合计行
- **二级分组**学历subtotalText = `"小计"` — 学历切换时显示小计行
- 分组列从左到右排列,左边为高级别分组
## 完整 jsonStr
```json
{
"loopBlockList": [],
"querySetting": {
"izOpenQueryBar": false,
"izDefaultQuery": true
},
"recordSubTableOrCollection": { "group": [], "record": [], "range": [] },
"printConfig": {
"paper": "A4",
"width": 210,
"height": 297,
"definition": 4,
"isBackend": false,
"marginX": 10,
"marginY": 10,
"layout": "portrait",
"printCallBackUrl": "",
"paginationShow": false,
"paginationLocation": "middle",
"paginationStart": 1,
"headerFooterShow": false,
"headerLocation": "left",
"headerText": "",
"footerLocation": "left",
"footerText": "",
"fontsize": 28,
"rotationAngle": -45,
"watermarkColor": "#246DDE",
"watermarkText": "积木报表",
"watermarkShow": true,
"printFootorFixBottom": false
},
"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": 687,
"isViewContentHorizontalCenter": false,
"autofilter": {},
"validations": [],
"cols": {
"0": { "width": 34 },
"1": { "width": 119 },
"3": { "width": 117 },
"6": { "width": 117 },
"7": { "width": 22 },
"len": 100
},
"area": { "sri": 16, "sci": 4, "eri": 16, "eci": 4, "width": 100, "height": 25 },
"pyGroupEngine": false,
"submitHandlers": [],
"excel_config_id": "1162913845578612736",
"hiddenCells": [],
"zonedEditionList": [],
"rows": {
"1": {
"cells": {
"1": {
"merge": [0, 5],
"style": 2,
"text": "纵向员工信息登记表",
"height": 0
}
},
"height": 40
},
"2": {
"cells": {
"1": { "style": 15, "text": "部门" },
"2": { "style": 15, "text": "学历" },
"3": { "style": 15, "text": "性别" },
"4": { "style": 15, "text": "年龄" },
"5": { "style": 15, "text": "姓名" },
"6": { "style": 15, "text": "薪水" }
},
"height": 34
},
"3": {
"cells": {
"1": {
"style": 17,
"text": "#{vegvkdueqw.group(department)}",
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "合计"
},
"2": {
"style": 17,
"text": "#{vegvkdueqw.group(education)}",
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "小计"
},
"3": { "style": 17, "text": "#{vegvkdueqw.sex}" },
"4": { "style": 17, "text": "#{vegvkdueqw.age}" },
"5": { "style": 17, "text": "#{vegvkdueqw.name}" },
"6": { "style": 17, "text": "#{vegvkdueqw.salary}" }
},
"height": 54
},
"len": 100
},
"rpbar": { "show": true, "pageSize": "", "btnList": [] },
"groupField": "vegvkdueqw.department",
"fixedPrintHeadRows": [],
"fixedPrintTailRows": [],
"displayConfig": {},
"fillFormInfo": { "layout": { "direction": "horizontal", "width": 200, "height": 45 } },
"background": false,
"name": "sheet1",
"styles": [
{ "font": { "bold": true } },
{ "font": { "size": 16, "bold": true } },
{ "align": "center", "font": { "size": 16, "bold": true } },
{ "align": "center" },
{ "bgcolor": "#5b9cd6", "align": "center" },
{ "bgcolor": "#5b9cd6", "color": "#ffffff", "align": "center" },
{ "border": { "top": ["thin", "#000"], "left": ["thin", "#000"], "bottom": ["thin", "#000"], "right": ["thin", "#000"] }, "bgcolor": "#5b9cd6", "color": "#ffffff", "align": "center" },
{ "border": { "top": ["thin", "#000"], "left": ["thin", "#000"], "bottom": ["thin", "#000"], "right": ["thin", "#000"] } },
{ "border": { "top": ["thin", "#bfbfbf"], "left": ["thin", "#bfbfbf"], "bottom": ["thin", "#bfbfbf"], "right": ["thin", "#bfbfbf"] }, "bgcolor": "#5b9cd6", "color": "#ffffff", "align": "center" },
{ "border": { "top": ["thin", "#bfbfbf"], "left": ["thin", "#bfbfbf"], "bottom": ["thin", "#bfbfbf"], "right": ["thin", "#bfbfbf"] } },
{ "border": { "top": ["thin", "#9cc2e6"], "left": ["thin", "#9cc2e6"], "bottom": ["thin", "#9cc2e6"], "right": ["thin", "#9cc2e6"] }, "bgcolor": "#5b9cd6", "color": "#ffffff", "align": "center" },
{ "border": { "top": ["thin", "#9cc2e6"], "left": ["thin", "#9cc2e6"], "bottom": ["thin", "#9cc2e6"], "right": ["thin", "#9cc2e6"] } },
{ "border": { "top": ["thin", "#9cc2e6"], "left": ["thin", "#9cc2e6"], "bottom": ["thin", "#9cc2e6"], "right": ["thin", "#9cc2e6"] }, "bgcolor": "#bdd7ee", "color": "#ffffff", "align": "center" },
{ "border": { "top": ["thin", "#9cc2e6"], "left": ["thin", "#9cc2e6"], "bottom": ["thin", "#9cc2e6"], "right": ["thin", "#9cc2e6"] }, "bgcolor": "#bdd7ee", "color": "#000100", "align": "center" },
{ "border": { "top": ["thin", "#9cc2e6"], "left": ["thin", "#9cc2e6"], "bottom": ["thin", "#9cc2e6"], "right": ["thin", "#9cc2e6"] }, "bgcolor": "#9cc2e6", "color": "#000100", "align": "center" },
{ "border": { "top": ["thin", "#5b9cd6"], "left": ["thin", "#5b9cd6"], "bottom": ["thin", "#5b9cd6"], "right": ["thin", "#5b9cd6"] }, "bgcolor": "#9cc2e6", "color": "#000100", "align": "center" },
{ "border": { "top": ["thin", "#5b9cd6"], "left": ["thin", "#5b9cd6"], "bottom": ["thin", "#5b9cd6"], "right": ["thin", "#5b9cd6"] } },
{ "border": { "top": ["thin", "#5b9cd6"], "left": ["thin", "#5b9cd6"], "bottom": ["thin", "#5b9cd6"], "right": ["thin", "#5b9cd6"] }, "align": "center" }
],
"isGroup": true,
"freezeLineColor": "rgb(185, 185, 185)",
"merges": ["B2:G2"]
}
```
## 样式方案(蓝色主题)
| 索引 | 背景色 | 字体色 | 边框色 | 用途 |
|------|--------|--------|--------|------|
| 2 | — | — | — | 标题16号加粗居中 |
| 15 | #9cc2e6 | #000100 | #5b9cd6 | **表头行**(中蓝底) |
| 17 | — | — | #5b9cd6 | **数据行**(蓝色边框居中) |
三层蓝色渐变:
- 深蓝 `#5b9cd6` — 表头背景/边框色
- 中蓝 `#9cc2e6` — 表头行背景
- 浅蓝 `#bdd7ee` — 交替行/小计行背景
## 打印配置(含水印)
```json
"printConfig": {
"paper": "A4",
"definition": 4,
"watermarkShow": true,
"watermarkText": "积木报表",
"watermarkColor": "#246DDE",
"fontsize": 28,
"rotationAngle": -45,
"paginationShow": false,
"headerFooterShow": false,
"printFootorFixBottom": false
}
```
| 属性 | 说明 |
|------|------|
| `definition` | 打印清晰度1-44最高 |
| `watermarkShow` | 启用水印 |
| `watermarkText` | 水印文字 |
| `watermarkColor` | 水印颜色 |
| `fontsize` | 水印字号 |
| `rotationAngle` | 水印旋转角度(负数=逆时针) |
| `paginationShow` | 是否显示页码 |
| `paginationLocation` | 页码位置left/middle/right |
| `headerFooterShow` | 是否显示页眉页脚 |
| `printFootorFixBottom` | 页脚是否固定在底部 |

View File

@@ -0,0 +1,21 @@
# 示例6分版多表格并排
**类型:** 分版报表
**特征:** `zonedEditionList` 定义多个独立数据区域,单元格标记 `"zonedEdition":N`
## 关键配置
```json
{
"zonedEditionList": [
{"sci":4,"sri":3,"eci":6,"eri":4,"db":"flapi","index":1},
{"sci":8,"sri":4,"eci":9,"eri":5,"db":"flapi","index":2}
]
}
```
## 报表 JSON
```json
{"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"},"hidden":{"rows":[],"cols":[],"conditions":{"rows":{},"cols":{}}},"queryFormSetting":{"useQueryForm":false,"dbKey":"","idField":""},"dbexps":[],"dicts":[],"freeze":"A1","dataRectWidth":930,"isViewContentHorizontalCenter":false,"autofilter":{},"validations":[],"cols":{"3":{"width":69},"7":{"width":61},"len":50},"pyGroupEngine":false,"submitHandlers":[],"excel_config_id":"1193411148792549376","hiddenCells":[],"zonedEditionList":[{"sci":4,"sri":3,"eci":6,"eri":4,"db":"flapi","index":1},{"sci":8,"sri":4,"eci":9,"eri":5,"db":"flapi","index":2}],"rows":{"1":{"cells":{"3":{"style":18,"text":"分版示例","merge":[0,1],"height":65},"5":{"merge":[0,2],"height":65,"text":"说明:当报表左侧已有表格,右侧仍需要展示表格时,需使用分版功能","style":20}},"height":65},"2":{"cells":{"1":{"text":"表1","style":7},"5":{"text":"表2","style":7}},"height":41},"3":{"cells":{"0":{"text":"姓名","style":2},"1":{"text":"性别","style":2},"2":{"text":"年龄","style":2},"4":{"text":"省份","style":2,"zonedEdition":1},"5":{"style":2,"zonedEdition":1,"text":"月份"},"6":{"style":2,"zonedEdition":1,"text":"金额"},"8":{"text":"表3","style":8}},"height":35},"4":{"cells":{"0":{"text":"#{jm.name}","style":0},"1":{"text":"#{jm.sex}","style":0},"2":{"text":"#{jm.age}","style":0},"4":{"text":"#{flapi.dept}","style":0,"zonedEdition":1},"5":{"style":0,"zonedEdition":1,"text":"#{flapi.month}月"},"6":{"style":0,"zonedEdition":1,"text":"#{flapi.amount}"},"8":{"text":"年份","style":2,"zonedEdition":2},"9":{"text":"金额","style":2,"zonedEdition":2}},"height":30},"5":{"cells":{"8":{"text":"#{flapi.year}年","style":0,"zonedEdition":2},"9":{"text":"#{flapi.settleamount}","style":0,"zonedEdition":2}}},"len":103},"rpbar":{"show":true,"pageSize":"","btnList":[]},"name":"sheet1","merges":["D1:E1","D2:E2","F2:H2"]}
```

View File

@@ -0,0 +1,266 @@
# 图表配置参考
## 图表模板文件位置
`src/main/resources/static/jmreport/desreport_/chartjson/`
生成图表报表时,应先读取对应的模板 JSON 文件作为 ECharts 配置基础。
## 可用图表类型
| 文件名 | chartType | 说明 |
|--------|-----------|------|
| `bar.simple.json` | `bar.simple` | 柱状图(单系列) |
| `bar.multi.json` | `bar.multi` | 柱状图(多系列) |
| `bar.stack.json` | `bar.stack` | 堆叠柱状图 |
| `bar.horizontal.json` | `bar.horizontal` | 横向柱状图 |
| `bar.multi.horizontal.json` | `bar.multi.horizontal` | 横向多系列柱状图 |
| `bar.stack.horizontal.json` | `bar.stack.horizontal` | 横向堆叠柱状图 |
| `bar.negative.json` | `bar.negative` | 正负柱状图 |
| `bar.background.json` | `bar.background` | 带背景柱状图 |
| `line.simple.json` | `line.simple` | 折线图(单系列) |
| `line.multi.json` | `line.multi` | 折线图(多系列) |
| `line.smooth.json` | `line.smooth` | 平滑曲线图 |
| `line.area.json` | `line.area` | 面积图 |
| `line.step.json` | `line.step` | 阶梯折线图 |
| `pie.simple.json` | `pie.simple` | 饼图 |
| `pie.doughnut.json` | `pie.doughnut` | 环形图 |
| `pie.rose.json` | `pie.rose` | 玫瑰图 |
| `mixed.linebar.json` | `mixed.linebar` | 柱状+折线混合图 |
| `radar.basic.json` | `radar.basic` | 雷达图 |
| `radar.custom.json` | `radar.custom` | 自定义雷达图 |
| `scatter.simple.json` | `scatter.simple` | 散点图 |
| `scatter.bubble.json` | `scatter.bubble` | 气泡图 |
| `funnel.simple.json` | `funnel.simple` | 漏斗图 |
| `funnel.pyramid.json` | `funnel.pyramid` | 金字塔图 |
| `gauge.simple.json` | `gauge.simple` | 仪表盘 |
| `gauge.simple180.json` | `gauge.simple180` | 半圆仪表盘 |
| `graph.simple.json` | `graph.simple` | 关系图 |
| `map.simple.json` | `map.simple` | 地图 |
| `map.scatter.json` | `map.scatter` | 地图散点 |
| `pictorial.spirits.json` | `pictorial.spirits` | 象形柱图 |
## echartslist.json 主要 key 对照
| key | 对应图表 |
|-----|---------|
| `bar` | 单系列柱状图 |
| `bar2` | dataset 模式柱状图 |
| `bar3` | 多系列柱状图 |
| `line` | 单系列折线图 |
| `line3` | 平滑曲线 |
| `line4` | 多系列折线图 |
| `line5` | 阶梯折线图 |
| `pie` | 饼图 |
| `pie1` | 环形图 |
| `pie2` | 玫瑰图 |
| `linebar` | 柱状+折线混合 |
| `map` | 地图 |
| `scatter` | 散点图 |
## 图表在 jsonStr 中的配置
图表通过**单元格占位 + chartList 配置**实现,不是绝对定位。需要两部分配合:
### 1. chartList 结构
```json
{
"chartList": [
{
"row": 5,
"col": 1,
"colspan": 0,
"rowspan": 0,
"width": "500",
"height": "350",
"config": "ECharts配置JSON字符串",
"url": "",
"extData": {
"chartType": "bar.simple",
"dataType": "sql",
"dataId": "数据集ID",
"dbCode": "数据集编码",
"axisX": "name",
"axisY": "value",
"series": "type",
"xText": "",
"yText": "",
"apiStatus": "1"
},
"layer_id": "唯一层ID",
"offsetX": 0,
"offsetY": 0,
"backgroud": {"enabled": false, "color": "#fff", "image": ""},
"virtualCellRange": [[5,1],[5,2],[5,3],[6,1],[6,2],[6,3]]
}
]
}
```
> **关键字段说明:**
>
> | 字段 | 类型 | 说明 |
> |------|------|------|
> | `row` / `col` | number | 图表起始位置(行号/列号),**不是 left/top 像素值** |
> | `width` / `height` | **string** | 图表宽高像素,**必须是字符串**(如 `"500"`,不是 `500` |
> | `virtualCellRange` | array | 图表占据的所有单元格坐标 `[[row,col], ...]` |
> | `layer_id` | string | 唯一标识,对应 rows 中 cells 的 `virtual` 属性 |
> | `backgroud` | object | 图表背景(注意拼写是 `backgroud` 不是 `background` |
> | `offsetX` / `offsetY` | number | 偏移量,通常为 0 |
### 2. rows 中的 virtual 占位
图表占据的每个单元格必须在 `rows` 中声明 `"virtual": "layer_id"`
```json
"rows": {
"5": {
"cells": {
"1": {"text": " ", "virtual": "chart_xxx"},
"2": {"text": " ", "virtual": "chart_xxx"},
"3": {"text": " ", "virtual": "chart_xxx"}
}
},
"6": {
"cells": {
"1": {"text": " ", "virtual": "chart_xxx"},
"2": {"text": " ", "virtual": "chart_xxx"},
"3": {"text": " ", "virtual": "chart_xxx"}
}
}
}
```
> **注意:**
> - `virtual` 的值必须和 `chartList[].layer_id` 一致
> - `text` 设为 `" "`(一个空格),不能为空字符串
> - 图表区域的行数 × 列数 = `virtualCellRange` 的元素数量
> - 图表区域不能和列表数据行重叠
### extData 关键字段
| 字段 | 说明 |
|------|------|
| `chartType` | 图表类型(如 `bar.simple`, `line.multi`, `pie.simple` |
| `dataType` | 数据来源:`"sql"` / `"api"` / `"json"` / `"javabean"` / `"files"`(前端文本值,与 dbType 数字不同) |
| `dataId` | 数据集IDsaveDb 返回的 id |
| `dbCode` | 数据集编码 |
| `axisX` | X轴/分类字段名,**固定为 `name`** |
| `axisY` | Y轴/数值字段名,**固定为 `value`** |
| `series` | 系列/分组字段名,**固定为 `type`**(单系列也要传 `"type"` |
| `apiStatus` | API 数据集是否启用(`"1"` = 启用) |
| `dataId1` | 第二数据集ID关系图 `graph.simple` 使用) |
| `isCustomPropName` | 是否自定义字段映射(默认不填,使用 name/value/type |
### 图表字段映射规则
> **重要:图表数据绑定使用固定的三个字段名,不是数据集的原始字段名。**
| extData 字段 | 固定值 | 含义 | 示例 |
|-------------|--------|------|------|
| `axisX` | `name` | X轴/分类 | 产品名称 |
| `axisY` | `value` | Y轴/数值 | 销售额 |
| `series` | `type` | 系列/分组(多系列) | 月份、类别 |
前端渲染时会将数据集查询结果按 `name`/`value`/`type` 进行映射:
- 单系列图表:`series` 也传 `"type"`(数据中 type 字段可为空字符串)
- 多系列图表:`series` = `"type"`,按 `type` 值分组生成多条系列
**SQL 数据集示例(需要 AS 别名映射到 name/value**
```sql
SELECT product_name AS name, sales_amount AS value FROM sales_table
```
**多系列 SQL 示例(加 type 字段):**
```sql
SELECT month AS name, amount AS value, category AS type FROM sales_table
```
**JSON 数据集示例:**
```json
{"data": [
{"name": "螺丝钉", "value": 5000, "type": ""},
{"name": "电阻器", "value": 3200, "type": ""}
]}
```
## 使用流程
1. 根据需求确定 `chartType`
2. 读取对应的 `chartjson/{chartType}.json` 文件作为 ECharts 配置模板
3. 修改模板中的 `title.text``series` 等,`data` 留空(由数据集驱动)
4. 将配置 JSON 字符串化后放入 `chartList[].config`
5. 配置 `extData``axisX`=`name``axisY`=`value``series`=`type`
6. 数据集字段必须包含 `name``value`SQL 用 AS 别名JSON 直接命名)
7. 确定图表占位区域(起始 row/col占几行几列
8.`rows` 中为每个占位 cell 添加 `"virtual": "layer_id"`
9. 构造 `virtualCellRange`(所有占位坐标数组)
10.`chartList` 放入 jsonStr 顶层
## 完整示例(列表 + 柱状图)
### 数据集配置
列表和图表使用**两个独立数据集**db_code 唯一):
| 数据集 | dbCode | dbType | 用途 | 字段 |
|--------|--------|--------|------|------|
| 进库列表 | `stocklist` | 3(JSON) | 列表展示 | name, quantity, stock_time |
| 进库图表 | `stockchart` | 3(JSON) | 柱状图 | **name, value** |
### Python 生成图表占位的关键代码
```python
layer_id = "chart_" + gen_id()
# 图表占据 row5~row14, col1~col5
chart_row_start, chart_row_end = 5, 14
chart_col_start, chart_col_end = 1, 5
# 1. 构造 virtualCellRange
virtual_cell_range = []
for r in range(chart_row_start, chart_row_end + 1):
for c in range(chart_col_start, chart_col_end + 1):
virtual_cell_range.append([r, c])
# 2. 构造 rows 中的 virtual 占位 cells
chart_rows = {}
for r in range(chart_row_start, chart_row_end + 1):
cells = {}
for c in range(chart_col_start, chart_col_end + 1):
cells[str(c)] = {"text": " ", "virtual": layer_id}
chart_rows[str(r)] = {"cells": cells}
# 3. 合并到 all_rows
all_rows.update(chart_rows)
# 4. chartList 配置
chart_item = {
"row": chart_row_start,
"col": chart_col_start,
"colspan": 0,
"rowspan": 0,
"width": "500", # 字符串!
"height": "350", # 字符串!
"config": json.dumps(chart_config, ensure_ascii=False),
"url": "",
"extData": {
"chartType": "bar.simple",
"dataType": "json",
"dataId": chart_db_id,
"dbCode": "stockchart",
"axisX": "name",
"axisY": "value",
"series": "type",
"xText": "",
"yText": "",
"apiStatus": "1"
},
"layer_id": layer_id,
"offsetX": 0,
"offsetY": 0,
"backgroud": {"enabled": False, "color": "#fff", "image": ""},
"virtualCellRange": virtual_cell_range
}
```

View File

@@ -0,0 +1,343 @@
# 图表模板快速参考
积木报表内置 30+ 图表模板,文件位于 `static/jmreport/desreport_/chartjson/`
可通过 `GET /jmreport/addChart?chartType=bar.simple` 获取模板配置。
生成图表时,从模板中取 ECharts 配置,修改 `title.text`,清空 `data`(由数据集驱动),然后放入 `chartList[].config`
## 图表分类速查
### 柱状图 (Bar)
| chartType | 说明 | 数据集要求 |
|-----------|------|-----------|
| `bar.simple` | 单系列柱状图 | `name, value` |
| `bar.multi` | 多系列柱状图 | `name, value, type` |
| `bar.stack` | 堆叠柱状图 | `name, value, type` |
| `bar.horizontal` | 横向柱状图 | `name, value` |
| `bar.multi.horizontal` | 横向多系列 | `name, value, type` |
| `bar.stack.horizontal` | 横向堆叠 | `name, value, type` |
| `bar.negative` | 正负柱状图 | `name, value, type` |
| `bar.background` | 带背景柱状图 | `name, value` |
### 折线图 (Line)
| chartType | 说明 | 数据集要求 |
|-----------|------|-----------|
| `line.simple` | 单系列折线图 | `name, value` |
| `line.multi` | 多系列折线图 | `name, value, type` |
| `line.smooth` | 平滑曲线图 | `name, value` |
| `line.area` | 面积图 | `name, value, type` |
| `line.step` | 阶梯折线图 | `name, value` |
### 饼图 (Pie)
| chartType | 说明 | 数据集要求 |
|-----------|------|-----------|
| `pie.simple` | 饼图 | `name, value` |
| `pie.doughnut` | 环形图 | `name, value` |
| `pie.rose` | 玫瑰图(南丁格尔) | `name, value` |
### 混合图
| chartType | 说明 | 数据集要求 |
|-----------|------|-----------|
| `mixed.linebar` | 柱状+折线混合 | `name, value, type` |
### 其他图表
| chartType | 说明 | 数据集要求 |
|-----------|------|-----------|
| `gauge.simple` | 仪表盘 | `name, value` |
| `gauge.simple180` | 半圆仪表盘 | `name, value` |
| `radar.basic` | 雷达图 | 特殊indicator |
| `radar.custom` | 自定义雷达图 | 特殊 |
| `funnel.simple` | 漏斗图 | `name, value` |
| `funnel.pyramid` | 金字塔图 | `name, value` |
| `scatter.simple` | 散点图 | 特殊 |
| `scatter.bubble` | 气泡图 | 特殊 |
| `map.simple` | 地图 | 特殊 |
| `map.scatter` | 地图散点 | 特殊 |
| `graph.simple` | 关系图 | 特殊(需两个数据集) |
| `pictorial.spirits` | 象形柱图 | `name, value` |
## 常用图表 ECharts 配置模板
### bar.simple — 单系列柱状图
```python
{
"title": {"show": True, "text": "标题", "left": "left", "top": "5",
"padding": [5,20,5,20],
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"grid": {"left": 60, "top": 60, "right": 100, "bottom": 60},
"tooltip": {"show": True, "textStyle": {"color": "#fff", "fontSize": 18}},
"xAxis": {"show": True, "name": "", "data": [],
"axisLabel": {"textStyle": {"fontSize": 12, "color": "#333"}},
"axisLine": {"lineStyle": {"color": "#333"}}},
"yAxis": {"show": True, "name": "",
"axisLabel": {"textStyle": {"fontSize": 12, "color": "#333"}},
"axisLine": {"lineStyle": {"color": "#333"}}},
"series": [{"name": "", "type": "bar", "data": [],
"barWidth": 50, "barMinHeight": 2,
"itemStyle": {"barBorderRadius": 0, "color": "#c43632"}}]
}
```
### bar.multi — 多系列柱状图
```python
{
"title": {"show": True, "text": "标题", "left": "left",
"padding": [5,20,5,20],
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"legend": {"show": True, "data": [], "top": "top", "left": "center",
"orient": "horizontal", "padding": [25,20,25,10],
"textStyle": {"color": "#333", "fontSize": 12}},
"grid": {"left": 60, "top": 60, "right": 100, "bottom": 60},
"tooltip": {"show": True, "trigger": "axis",
"axisPointer": {"type": "shadow"},
"textStyle": {"color": "#fff", "fontSize": 18}},
"xAxis": {"show": True, "type": "category", "data": [],
"axisLabel": {"textStyle": {"fontSize": 12, "color": "#333"}},
"axisLine": {"lineStyle": {"color": "#333"}}},
"yAxis": {"show": True,
"axisLabel": {"textStyle": {"fontSize": 12, "color": "#333"}},
"axisLine": {"lineStyle": {"color": "#333"}}},
"series": [
{"name": "系列1", "type": "bar", "data": [], "barWidth": 0, "barMinHeight": 2,
"label": {"show": True, "position": "top", "textStyle": {"color": "black", "fontSize": 12}},
"itemStyle": {"barBorderRadius": 0, "color": ""}},
{"name": "系列2", "type": "bar", "data": [], "barWidth": 0, "barMinHeight": 2,
"label": {"show": True, "position": "top", "textStyle": {"color": "black", "fontSize": 12}},
"itemStyle": {"barBorderRadius": 0, "color": ""}}
]
}
```
### line.simple — 单系列折线图
```python
{
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"grid": {"left": 60, "top": 60, "right": 100, "bottom": 60},
"xAxis": {"show": True, "data": []},
"yAxis": {"show": True, "name": ""},
"series": [{"name": "", "type": "line", "data": [],
"smooth": False, "showSymbol": True, "symbolSize": 5,
"lineStyle": {"width": 2, "color": "#c43632"}}]
}
```
### pie.simple — 饼图
```python
{
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"tooltip": {"show": True, "formatter": "{a} <br/>{b} : {c}"},
"legend": {"show": True, "data": [], "orient": "horizontal",
"textStyle": {"color": "#333", "fontSize": 12}},
"series": [{"name": "数据", "type": "pie",
"radius": "55%", "minAngle": 0,
"center": [320, 180],
"label": {"show": True, "position": "outside"},
"data": []}]
}
```
### pie.doughnut — 环形图
```python
{
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"tooltip": {"show": True, "formatter": "{a} <br/>{b} : {c}"},
"legend": {"show": True, "data": [], "orient": "horizontal",
"textStyle": {"color": "#333", "fontSize": 12}},
"series": [{"name": "数据", "type": "pie",
"isRadius": True,
"radius": ["45%", "55%"], # 内外半径 → 环形
"minAngle": 0, "roseType": "", "isRose": False,
"center": [320, 180],
"label": {"show": True, "position": "outside"},
"data": []}]
}
```
### mixed.linebar — 柱线混合图
```python
{
"chartType": "linebar",
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"legend": {"data": []},
"xAxis": {"type": "category", "data": []},
"yAxis": [
{"type": "value", "name": "左轴"},
{"type": "value", "name": "右轴"}
],
"series": [
{"name": "柱状", "type": "bar", "data": []},
{"name": "折线", "type": "line", "data": []},
{"name": "右轴数据", "type": "bar", "yAxisIndex": 1, "data": []}
]
}
```
### gauge.simple — 仪表盘
```python
{
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"tooltip": {"show": True, "formatter": "{b} : {c}"},
"series": [{"name": "业务指标", "type": "gauge",
"radius": "75%", "center": [330, 200],
"itemStyle": {"color": "#63869E"},
"pointer": {"show": True},
"detail": {"formatter": "{value}%",
"textStyle": {"color": "rgba(0,0,0,1)", "fontSize": 25}},
"axisLine": {"lineStyle": {
"color": [[0.2, "#91c7ae"], [0.8, "#63869E"], [1, "#C23531"]],
"width": 25}},
"data": [{"value": 50, "name": "完成率"}]}]
}
```
### funnel.simple — 漏斗图
```python
{
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"legend": {"show": True, "data": []},
"tooltip": {"show": True, "trigger": "item", "formatter": "{b} : {c}"},
"series": [{"name": "漏斗图", "type": "funnel",
"left": "10%", "top": 60, "bottom": 60, "width": "80%",
"sort": "descending", "gap": 2, "orient": "vertical",
"label": {"show": True, "position": "inside",
"textStyle": {"fontSize": 16}},
"itemStyle": {"borderColor": "#fff", "borderWidth": 1},
"data": []}]
}
```
### radar.basic — 雷达图
```python
{
"title": {"show": True, "text": "标题",
"textStyle": {"fontSize": 18, "fontWeight": "bolder", "color": "#c23531"}},
"legend": {"show": True, "data": []},
"tooltip": {"show": True},
"radar": [{"shape": "polygon", "center": [320, 200],
"name": {"formatter": "{value}",
"textStyle": {"fontSize": 14, "color": "#72ACD1"}},
"indicator": [
{"name": "维度1", "max": 100},
{"name": "维度2", "max": 100},
{"name": "维度3", "max": 100}
]}],
"series": [{"name": "", "type": "radar",
"data": [{"value": [80, 60, 70], "name": "系列1"}]}]
}
```
## 数据集 SQL 映射规则
图表数据集使用固定的 `name`/`value`/`type` 字段映射:
```sql
-- 单系列bar.simple, line.simple, pie.simple 等)
SELECT product_name AS name, sales_amount AS value, '' AS type
FROM sales_table
-- 多系列bar.multi, line.multi, mixed.linebar 等)
SELECT month AS name, amount AS value, category AS type
FROM sales_table
-- 仪表盘gauge
SELECT '完成率' AS name, ROUND(done*100/total) AS value, '' AS type
FROM task_summary
-- 漏斗图funnel
SELECT stage AS name, count AS value, '' AS type
FROM funnel_data ORDER BY count DESC
```
## 快速生成图表的 Python 代码
```python
def create_chart(chart_type, title, db_code, db_id, row_start, col_start,
rows=10, cols=5, width="650", height="350"):
"""快速生成图表配置"""
layer_id = "chart_" + gen_id()
# 根据类型选择基础配置
base_configs = {
"bar.simple": {
"title": {"text": title, "left": "center", "top": "10"},
"tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}},
"grid": {"left": "3%", "right": "4%", "bottom": "3%", "containLabel": True},
"xAxis": [{"type": "category", "data": []}],
"yAxis": [{"type": "value"}],
"series": [{"type": "bar", "data": [], "barWidth": "40%"}]
},
"pie.simple": {
"title": {"text": title, "left": "center"},
"tooltip": {"trigger": "item", "formatter": "{b}: {c} ({d}%)"},
"legend": {"bottom": "5%", "left": "center"},
"series": [{"type": "pie", "radius": "55%", "center": ["50%", "50%"], "data": []}]
},
"line.simple": {
"title": {"text": title, "left": "center"},
"tooltip": {"trigger": "axis"},
"grid": {"left": "3%", "right": "4%", "bottom": "3%", "containLabel": True},
"xAxis": [{"type": "category", "data": []}],
"yAxis": [{"type": "value"}],
"series": [{"type": "line", "smooth": True, "data": []}]
},
"gauge.simple": {
"title": {"text": title, "left": "center"},
"series": [{"type": "gauge", "radius": "75%",
"detail": {"formatter": "{value}%"},
"data": [{"value": 0, "name": ""}]}]
}
}
config = base_configs.get(chart_type, base_configs["bar.simple"])
# virtual cells
row_end = row_start + rows - 1
col_end = col_start + cols - 1
virtual_cells = [[r,c] for r in range(row_start, row_end+1) for c in range(col_start, col_end+1)]
# rows 占位
chart_rows = {}
for r in range(row_start, row_end + 1):
cells = {}
for c in range(col_start, col_end + 1):
cells[str(c)] = {"text": " ", "virtual": layer_id}
chart_rows[str(r)] = {"cells": cells}
chart_item = {
"row": row_start, "col": col_start, "colspan": 0, "rowspan": 0,
"width": width, "height": height,
"config": json.dumps(config, ensure_ascii=False),
"url": "",
"extData": {
"chartType": chart_type, "dataType": "sql",
"dataId": str(db_id), "dbCode": db_code,
"axisX": "name", "axisY": "value", "series": "type",
"xText": "", "yText": "", "apiStatus": "1"
},
"layer_id": layer_id,
"offsetX": 0, "offsetY": 0,
"backgroud": {"enabled": False, "color": "#fff", "image": ""},
"virtualCellRange": virtual_cells
}
return chart_item, chart_rows
```

View File

@@ -0,0 +1,328 @@
# 报表组件参考
积木报表支持 4 种图层组件Layer它们以独立数组存储在 jsonStr 顶层,与 rows/cols 平级。
## 组件类型总览
| 组件 | jsonStr 字段 | 类型标识 | 用途 |
|------|-------------|---------|------|
| 图片 | `imgList` | `img` | 外部图片/Logo/背景图 |
| 图表 | `chartList` | `chart` | ECharts 可视化图表 |
| 条形码 | `barcodeList` | `barcode` | CODE128/EAN 等一维码 |
| 二维码 | `qrcodeList` | `qrcode` | QR Code 二维码 |
所有组件共享以下基础属性:
```python
{
"row": 0, # 起始行号从0开始
"col": 0, # 起始列号从0开始
"colspan": 0, # 列跨度
"rowspan": 0, # 行跨度
"width": 300, # 宽度(像素,图表必须是字符串如"300"
"height": 200, # 高度(像素,图表必须是字符串如"200"
"layer_id": "唯一ID", # 唯一标识,对应 virtual cell
"offsetX": 0, # X偏移
"offsetY": 0, # Y偏移
"virtualCellRange": [[row,col], ...] # 占据的所有单元格坐标
}
```
## 1. 图片组件 (imgList)
### JSON 结构
```python
img_item = {
"row": 0,
"col": 1,
"colspan": 0,
"rowspan": 0,
"width": 315, # 数字
"height": 151, # 数字
"src": "/jmreport/img/upload/xxx.png", # 图片路径(相对或绝对)
"isBackend": False, # 是否作为前置遮罩
"isBackendImg": False, # 是否作为背景图
"commonBackend": None, # 共享遮罩/背景属性
"layer_id": "img_xxx",
"offsetX": 0,
"offsetY": 0,
"virtualCellRange": [[0,1],[0,2],[1,1],[1,2]]
}
```
### 图片路径说明
| 类型 | 示例 |
|------|------|
| 上传图片 | `/jmreport/img/upload/xxx.png` |
| 外部URL | `https://example.com/logo.png` |
| 数据绑定 | `${dbCode.imgField}` — 字段值为图片URL |
## 2. 图表组件 (chartList)
详见 `chart-config.md`,这里列出关键结构。
### JSON 结构
```python
chart_item = {
"row": 5,
"col": 1,
"colspan": 0,
"rowspan": 0,
"width": "650", # 必须是字符串!
"height": "350", # 必须是字符串!
"config": json.dumps(echarts_option), # ECharts 配置 JSON 字符串
"url": "", # 外部数据 URL通常为空
"extData": {
"chartType": "bar.simple", # 图表类型
"dataType": "sql", # 数据来源: "sql"/"api"/"json"/"javabean"/"files"
"dataId": "数据集ID", # saveDb 返回的 id
"dbCode": "数据集编码", # 数据集编码
"axisX": "name", # 固定值
"axisY": "value", # 固定值
"series": "type", # 固定值
"xText": "",
"yText": "",
"apiStatus": "1"
},
"layer_id": "chart_xxx",
"offsetX": 0,
"offsetY": 0,
"backgroud": {"enabled": False, "color": "#fff", "image": ""},
"virtualCellRange": [[5,1],[5,2],...]
}
```
## 3. 条形码组件 (barcodeList)
### JSON 结构
```python
barcode_item = {
"row": 3,
"col": 0,
"colspan": 0,
"rowspan": 0,
"width": 300,
"height": 200,
"layer_id": "barcode_xxx",
"offsetX": 0,
"offsetY": 0,
"jsonString": json.dumps({
"barcodeContent": "jmreport", # 条码内容(支持 ${dbCode.field} 动态绑定)
"format": "CODE128", # 条码格式
"width": 2, # 条线宽度
"height": 100, # 条码高度
"displayValue": False, # 是否显示文字
"text": "jmreport", # 显示文字内容
"fontOptions": "", # 字体选项bold/italic
"font": "monospace", # 字体
"textAlign": "center", # 文字对齐
"textPosition": "bottom", # 文字位置
"textMargin": 2, # 文字间距
"fontSize": 20, # 字体大小
"background": "#fff", # 背景色
"lineColor": "#000", # 条线颜色
"margin": 10 # 边距
}),
"virtualCellRange": [[3,0],[3,1],[4,0],[4,1]]
}
```
### 支持的条码格式
| format | 说明 |
|--------|------|
| `CODE128` | Code 128默认最常用 |
| `CODE39` | Code 39 |
| `EAN13` | EAN-13 |
| `EAN8` | EAN-8 |
| `UPC` | UPC-A |
| `ITF14` | ITF-14 |
### 动态数据绑定
条码内容支持表达式:`${dbCode.fieldName}`,运行时替换为数据集字段值。
## 4. 二维码组件 (qrcodeList)
### JSON 结构
```python
qrcode_item = {
"row": 5,
"col": 0,
"colspan": 0,
"rowspan": 0,
"width": 128,
"height": 128,
"layer_id": "qrcode_xxx",
"offsetX": 0,
"offsetY": 0,
"jsonString": json.dumps({
"text": "http://jimureport.com/", # 二维码内容(支持 ${dbCode.field}
"width": 128,
"height": 128,
"colorDark": "#000000", # 前景色
"colorLight": "#ffffff" # 背景色
}),
"virtualCellRange": [[5,0],[5,1],[6,0],[6,1]]
}
```
## 5. 单元格内嵌组件 (displayConfig)
除了图层组件,单元格本身也可以通过 `display` 属性渲染为特殊组件:
```python
# 在 rows 的 cell 中设置
cell = {
"text": "#{dbCode.imgUrl}",
"display": "img" # 渲染为图片
}
# 或
cell = {
"text": "#{dbCode.code}",
"display": "barcode" # 渲染为条形码
}
# 或
cell = {
"text": "#{dbCode.url}",
"display": "qrcode" # 渲染为二维码
}
```
**display 可选值:**
| 值 | 说明 |
|----|------|
| `normal` | 普通文本(默认) |
| `img` | 图片text 为图片 URL |
| `barcode` | 条形码text 为条码内容) |
| `qrcode` | 二维码text 为二维码内容) |
| `base64Img` | Base64 图片 |
| `richText` | 富文本/HTML |
## Virtual Cell 占位规则
所有图层组件都需要在 `rows` 中声明 virtual 占位:
```python
# 1. 确定组件占据的行列范围
row_start, row_end = 5, 8
col_start, col_end = 1, 4
layer_id = "chart_xxx"
# 2. 构造 virtualCellRange
virtual_cells = []
for r in range(row_start, row_end + 1):
for c in range(col_start, col_end + 1):
virtual_cells.append([r, c])
# 3. 在 rows 中添加 virtual 占位
for r in range(row_start, row_end + 1):
cells = {}
for c in range(col_start, col_end + 1):
cells[str(c)] = {"text": " ", "virtual": layer_id}
rows_data[str(r)] = {"cells": cells}
# 4. 组件中设置 virtualCellRange
component["virtualCellRange"] = virtual_cells
```
**注意事项:**
- `virtual` 值必须和组件的 `layer_id` 一致
- `text` 必须为 `" "`(一个空格),不能为空
- 组件区域不能和数据绑定行重叠
- 一个组件的 virtual cells 不能和其他组件重叠
## Python 构造完整示例
```python
# 构造一个包含 表格 + 柱状图 + 二维码 的报表
layer_chart_id = "chart_" + gen_id()
layer_qr_id = "qrcode_" + gen_id()
rows_data = {
# 标题行
"1": {"cells": {"1": {"text": "销售报表", "style": 5}}, "height": 40},
# 表头
"2": {"cells": {
"1": {"text": "产品", "style": 4},
"2": {"text": "销量", "style": 4},
"3": {"text": "金额", "style": 4}
}, "height": 34},
# 数据行
"3": {"cells": {
"1": {"text": "#{ds.name}", "style": 2},
"2": {"text": "#{ds.qty}", "style": 2},
"3": {"text": "#{ds.amount}", "style": 2}
}},
# 空行
"4": {"cells": {}, "height": 15},
"len": 200
}
# 柱状图占位 (row 5-12, col 1-5)
for r in range(5, 13):
cells = {}
for c in range(1, 6):
cells[str(c)] = {"text": " ", "virtual": layer_chart_id}
rows_data[str(r)] = {"cells": cells}
# 二维码占位 (row 5-8, col 6-7)
for r in range(5, 9):
cells = rows_data.get(str(r), {"cells": {}})["cells"]
for c in range(6, 8):
cells[str(c)] = {"text": " ", "virtual": layer_qr_id}
rows_data[str(r)] = {"cells": cells}
# 柱状图
chart_config = {
"title": {"text": "销量统计", "left": "center"},
"tooltip": {"trigger": "axis"},
"xAxis": [{"type": "category", "data": []}],
"yAxis": [{"type": "value"}],
"series": [{"type": "bar", "data": [], "itemStyle": {"color": "#01b0f1"}}]
}
chart_list = [{
"row": 5, "col": 1, "colspan": 0, "rowspan": 0,
"width": "500", "height": "300",
"config": json.dumps(chart_config, ensure_ascii=False),
"url": "",
"extData": {
"chartType": "bar.simple", "dataType": "sql",
"dataId": chart_db_id, "dbCode": "saleschart",
"axisX": "name", "axisY": "value", "series": "type",
"xText": "", "yText": "", "apiStatus": "1"
},
"layer_id": layer_chart_id,
"offsetX": 0, "offsetY": 0,
"backgroud": {"enabled": False, "color": "#fff", "image": ""},
"virtualCellRange": [[r,c] for r in range(5,13) for c in range(1,6)]
}]
# 二维码
qrcode_list = [{
"row": 5, "col": 6, "colspan": 0, "rowspan": 0,
"width": 128, "height": 128,
"layer_id": layer_qr_id,
"offsetX": 0, "offsetY": 0,
"jsonString": json.dumps({"text": "https://example.com", "width": 128, "height": 128, "colorDark": "#000000", "colorLight": "#ffffff"}),
"virtualCellRange": [[r,c] for r in range(5,9) for c in range(6,8)]
}]
# 最终保存数据中包含
save_data = {
# ... 其他字段
"chartList": chart_list,
"qrcodeList": qrcode_list,
# imgList 和 barcodeList 为空时可省略或传 []
}
```

View File

@@ -0,0 +1,170 @@
# 积木报表重要约束
1. **数据集编码 `db_code` 不能重复且只支持英文字符** — 同一个报表内的多个数据集,每个的 `db_code` 必须唯一。编码只能使用英文字母、数字和下划线,不能包含中文或特殊字符。重复会导致数据覆盖或查询异常。
2. **`is_page` 分页只能有一个** — 一个报表中只能有一个数据集设置 `is_page=1`(启用分页),其余数据集必须为 `is_page=0`。多个数据集同时分页会导致分页冲突。
## 数据绑定语法
| 语法 | 说明 | 场景 |
|------|------|------|
| `${db.field}` | 单值绑定 | 主表字段、固定值 |
| `#{db.field}` | 列表绑定 | 明细行、循环数据 |
| `#{db.group(field)}` | 纵向分组 | 按字段分组汇总 |
| `#{db.groupRight(field)}` | 横向分组 | 按字段横向展开 |
| `#{db.dynamic(field)}` | 动态聚合 | 交叉表数据 |
| `#{db.customGroup(field)}` | 自定义分组 | 横向自定义展开 |
| `=SUM(D7)` | Excel 公式 | 列汇总 |
## 单元格属性
| 属性 | 说明 | 值 |
|------|------|-----|
| `merge` | 合并 | `[行数,列数]`,如 `[0,2]` 向右合并2列 |
| `style` | 样式索引 | 引用 `styles` 数组下标 |
| `loopBlock` | 循环块标记 | 1=属于循环块 |
| `zonedEdition` | 分版标记 | 1/2/... 分版编号 |
| `fixedHead` | 固定表头 | 1=固定 |
| `fixedTail` | 固定表尾 | 1=固定 |
| `aggregate` | 聚合类型 | 见下方分组配置 |
| `subtotal` | 小计配置 | 见下方分组配置 |
| `funcname` | 聚合函数 | 见下方分组配置 |
| `subtotalText` | 小计行文本 | `"合计"` / `"小计"` |
| `direction` | 展开方向 | 见下方分组配置 |
| `sort` | 排序 | 见下方分组配置 |
| `rendered` | 渲染标记 | `""` |
| `config` | 配置标记 | `""` |
| `decimalPlaces` | 小数位 | `"0"`/`"1"`/`"4"` |
| `display` | 显示格式 | 见下方 display 值表 |
| `fillForm` | 填报组件 | 组件配置对象 |
## 分组相关配置
### aggregate 聚合方式polyWayList
| 值 | 说明 |
|-----|------|
| `select` | 列表(普通列,不分组) |
| `group` | 分组(相同值合并单元格) |
### subtotal 是否启用小计
| 值 | 说明 |
|-----|------|
| `"-1"` | 否(不显示小计行) |
| `"groupField"` | 是(分组切换时显示小计/合计行) |
### funcname 聚合函数aggregateList
| 值 | 说明 | 用于 |
|-----|------|------|
| `"-1"` | 无(不计算,仅显示 subtotalText 文本) | 分组字段(地区、销售员等) |
| `"SUM"` | 求和 | 数值字段(金额、数量等) |
| `"MAX"` | 最大值 | 数值字段 |
| `"MIN"` | 最小值 | 数值字段 |
| `"AVERAGE"` | 平均值 | 数值字段 |
| `"COUNT"` | 计数 | 任意字段 |
### direction 展开方向directionList
| 值 | 说明 |
|-----|------|
| `"down"` | 纵向(默认) |
| `"right"` | 横向 |
### sort 排序sortType
| 值 | 说明 |
|-----|------|
| `"default"` | 默认(不排序) |
| `"asc"` | 正序 |
| `"desc"` | 倒序 |
### aggregate 高级模式advancedList
| 值 | 说明 |
|-----|------|
| `"default"` | 普通属性 |
| `"dynamic"` | 动态属性(交叉表) |
### 分组单元格配置示例
**分组字段(地区,一级分组 — 合计):**
```json
{
"text": "#{sales.group(region)}",
"style": 4,
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "合计",
"rendered": "",
"config": ""
}
```
**分组字段(销售员,二级分组 — 小计):**
```json
{
"text": "#{sales.group(salesman)}",
"style": 4,
"aggregate": "group",
"subtotal": "groupField",
"funcname": "-1",
"subtotalText": "小计"
}
```
**数值字段(销售额,小计/合计行自动求和):**
```json
{
"text": "#{sales.amount}",
"style": 4,
"funcname": "SUM",
"subtotal": "-1",
"display": "number",
"rendered": "",
"config": ""
}
```
**注意:** 数值字段的 `subtotal``"-1"`(不是 `"groupField"``funcname``"SUM"`。含义是:该字段不触发分组切换,但在分组切换产生的小计/合计行中自动按 SUM 聚合。
## display 显示格式JmConst.CELL_FORMAT_*
| display 值 | 说明 | 示例 |
|------------|------|------|
| `normal` | 默认文本 | — |
| `number` | 数值(数值类型字段默认) | 58000 |
| `percent` | 百分比 | 85% |
| `rmb` | 人民币 | ¥58,000.00 |
| `usd` | 美元 | $58,000.00 |
| `eur` | 欧元 | €58,000.00 |
| `date` | 日期 | 2026-03-20 |
| `date2` | 日期(斜杠) | 2026/03/20 |
| `time` | 时间 | 12:30:00 |
| `datetime` | 日期时间 | 2026-03-20 12:30:00 |
| `year` | 年 | 2026 |
| `month` | 月 | 03 |
| `base64Img` | Base64图片 | — |
| `img` | 图片 | — |
| `qrcode` | 二维码 | — |
| `barcode` | 条形码 | — |
| `richText` | 富文本 | — |
## 顶层配置
| 字段 | 说明 |
|------|------|
| `loopBlockList` | 循环块定义(含 `loopTime` 分栏次数) |
| `zonedEditionList` | 分版区域定义 |
| `fixedPrintHeadRows` | 固定打印表头 |
| `fixedPrintTailRows` | 固定打印表尾 |
| `groupField` | 分组字段 |
| `isGroup` | 是否启用分组 |
| `submitHandlers` | 填报提交处理器 |
| `background` | 背景图配置 |
| `imgList` | 图片列表 |
| `displayConfig` | 二维码/条码显示配置 |
| `dicts` | 引用的字典编码列表 |
| `printConfig` | 打印配置(纸张/方向/边距) |
| `merges` | 合并单元格列表(如 `"B1:H1"`) |

View File

@@ -0,0 +1,377 @@
# 数据集的用法
所有接口均需要提供token
## 数据源管理
数据集默认使用应用本身的数据库。如需连接外部数据库,需要先添加数据源,再在数据集中通过 `dbSource` 关联。
### 查询已有数据源
- **地址**`GET /jmreport/getDataSourceByPage`
- **返回**:所有数据源列表,每个包含 `id``name`
```python
ds_resp = api_request('/jmreport/getDataSourceByPage')
ds_list = ds_resp.get('result', [])
# 按名称查找数据源 ID
ds_map = {ds['name']: ds['id'] for ds in ds_list}
# 示例: ds_map['ws_mysql'] -> '1010703895600087040'
```
### 添加/编辑数据源
- **地址**`POST /jmreport/addDataSource`
- **新增不传 id编辑传 id**
- **请求参数**
```json
{
"id": "",
"reportId": "报表ID",
"code": "",
"name": "mysql",
"dbType": "MYSQL5.7",
"dbDriver": "com.mysql.cj.jdbc.Driver",
"dbUrl": "jdbc:mysql://127.0.0.1:3306/jimureport?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&tinyInt1isBit=false",
"dbUsername": "root",
"dbPassword": "123456"
}
```
| 字段 | 说明 |
|------|------|
| `id` | 数据源ID新增不传编辑传已有ID |
| `reportId` | 关联的报表ID |
| `name` | 数据源名称 |
| `dbType` | 数据库类型(如 `MYSQL5.7``ORACLE``POSTGRESQL` 等) |
| `dbDriver` | JDBC驱动类名 |
| `dbUrl` | JDBC连接URL |
| `dbUsername` | 数据库用户名 |
| `dbPassword` | 数据库密码 |
### 数据源与数据集的关联
数据集通过 `dbSource` 字段关联数据源,值为数据源的 `id`
```json
{
"dbCode": "aa",
"dbType": "0",
"dbDynSql": "select name from demo",
"dbSource": "1010703895600087040",
"dbSourceType": "mysql"
}
```
| 字段 | 说明 |
|------|------|
| `dbSource` | 数据源ID`addDataSource` 返回或编辑时传入的 `id`)。空字符串 `""` = 使用默认数据源 |
| `dbSourceType` | 数据库类型(`mysql``oracle` 等),后端会自动识别,也可手动指定 |
**注意:** 如果开启了数据源安全模式(`firewall.dataSourceSafe: true`SQL 数据集**必须**指定 `dbSource`,不允许使用默认数据源。
## SQL数据集
### SQL语句用法
- 如果id字段为字符串类型则需要加单引号`select * from table where id='${id}'`
- 您可以编写`${id}`做为一个参数这里id是参数的名称。例如`select * from table where id='${id}'`
- 您可以编写`#{sysUserCode}`做为一个系统变量这里sysUserCode是当前登录人。例如`select * from table where create_by='#{sysUserCode}'`
- 您可以编写存储过程`CALL proc_sys_role(${pageNo}, ${pageSize})`CALL为开启存储过程
- MongoDB和Elasticsearch支持sql语句表名需要增加数据库标识MongoDBmongoElasticsearches。例如`select * from mongo.table`
### SQL解析接口
- **地址**`POST /jmreport/queryFieldBySql`
- **请求参数**
```json
{
"sql": "select * from test_order_product where order_fk_id = ${order_fk_id}",
"type": "0",
"paramArray": "[{\"id\":\"1047395275108601856\",\"jimuReportHeadId\":\"1047395274039054336\",\"paramName\":\"order_fk_id\",\"paramTxt\":\"order_fk_id\",\"paramValue\":\"1\",\"orderNum\":1,\"createBy\":\"admin\",\"createTime\":\"2025-02-06 16:21:43\",\"updateBy\":null,\"updateTime\":null,\"searchFlag\":0,\"widgetType\":null,\"searchMode\":null,\"dictCode\":null,\"searchFormat\":null,\"extJson\":\"\",\"tableIndex\":1,\"_index\":0,\"_rowKey\":105}]"
}
```
- **返回结果**
```json
{
"success": true,
"message": "解析成功",
"code": 200,
"result": {
"paramList": [],
"fieldList": [
{
"fieldName": "id",
"fieldText": "id",
"widgetType": "String",
"orderNum": 1
}
]
}
}
```
## API数据集
### API数据集用法
- 如果id字段为字符串类型则需要加单引号`http://127.0.0.1:8080/jeecg-boot/jimureport/test?id=${id}`
- 您可以编写`#{sysDateTime}`做为一个系统变量这里sysDateTime是当前系统时间。例如`http://127.0.0.1:8080/jeecg-boot/jimureport/test?riqi=#{sysDateTime}`
- 您可以简写访问路径,如:`#{domainURL}/jimureport/test/getList`
### API解析接口
- **地址**`POST /jmreport/executeSelectApi`
- **请求参数**
```json
{
"api": "http://localhost:8085/jimureport/test/getList?pid=&name=",
"method": "0",
"paramArray": "[{\"id\":\"1066517440772788224\",\"jimuReportHeadId\":\"1066517440441438208\",\"paramName\":\"pid\",\"paramTxt\":null,\"paramValue\":\"\",\"orderNum\":1,\"createBy\":\"admin\",\"createTime\":\"2025-03-31 10:46:23\",\"updateBy\":null,\"updateTime\":null,\"searchFlag\":0,\"widgetType\":null,\"searchMode\":null,\"dictCode\":\"\",\"searchFormat\":null,\"extJson\":\"\",\"tableIndex\":1,\"_index\":0,\"_rowKey\":231}]"
}
```
- **返回结果**
```json
{
"success": true,
"message": "",
"code": 200,
"result": [
{
"fieldName": "ctotal",
"fieldText": "ctotal",
"widgetType": "String",
"isShow": true,
"orderNum": 1
}
]
}
```
## JSON数据集
自动将data中的字段解析成fieldName/fieldText/widgetType格式。
- **输入格式**
```json
{
"data": [
{
"ctotal": "125箱",
"cname": "牛奶0",
"cprice": "56",
"riqi": "2022年10月21日",
"id": "1",
"dtotal": "1256箱",
"tp": "7000",
"ztotal": "589箱",
"cnum": "每箱12瓶"
}
]
}
```
## JavaBean数据集
- **地址**`POST /jmreport/queryFieldByBean`
- **请求参数**
```json
{
"javaType": "spring-key",
"javaValue": "testRpSpringBean",
"isPage": false,
"param": {}
}
```
- **返回结果**
```json
{
"success": true,
"message": "解析成功",
"code": 200,
"result": [
{
"fieldName": "name",
"fieldText": "name",
"widgetType": "String",
"orderNum": 1
}
]
}
```
## 查询已有数据集
更新数据集前必须先查到已有数据集的 `dbId`,流程如下:
### Step 1: 获取数据集列表(含 dbId
- **地址**`GET /jmreport/field/tree/{reportId}`
- **返回结构**
```json
{
"result": [
[
{
"code": "userlist",
"dbId": "1194900477760331776",
"title": "用户列表",
"type": "0",
"isList": "1",
"izSharedSource": 0,
"children": [
{"title": "username", "fieldText": "username"},
{"title": "realname", "fieldText": "realname"}
]
}
]
]
}
```
**提取 dbCode → dbId 映射:**
```python
tree = api_request(f'/jmreport/field/tree/{report_id}')
db_map = {} # dbCode -> dbId
for group in tree.get('result', []):
if group and len(group) > 0:
info = group[0]
db_map[info['code']] = info['dbId']
```
### Step 2: 获取单个数据集详情
- **地址**`GET /jmreport/loadDbData/{dbId}?reportId={reportId}`
- **返回结构**`result` 包含三个顶层字段:
```json
{
"result": {
"dbId": "数据集ID",
"reportDb": {
"id": "数据集ID",
"dbCode": "userlist",
"dbDynSql": "SELECT ... FROM ...",
"dbSource": "1010703895600087040",
"dbSourceType": "mysql",
"isPage": "1",
"isList": "1"
},
"fieldList": [...],
"paramList": [...]
}
}
```
> **注意:`dbDynSql` 和 `dbSource` 在 `result.reportDb` 中,不在 `result` 顶层。**
```python
detail = api_request(f'/jmreport/loadDbData/{db_id}?reportId={report_id}').get('result', {})
report_db = detail.get('reportDb', {})
existing_sql = report_db.get('dbDynSql', '')
existing_db_source = report_db.get('dbSource', '')
existing_fields = detail.get('fieldList', [])
existing_params = detail.get('paramList', [])
```
### Step 3: 查询参数列表
- **地址**`GET /jmreport/getListReportDb?reportId={reportId}`
- **返回**:每个 dbCode 对应的参数列表
```json
{"result": {"reportDbParam": {"userlist": [{"paramName": "username", ...}], "userpie": []}}}
```
## 保存或修改数据集
**新增不传 id更新必须传 id。** 后端 `saveOrUpdate` 逻辑:有 id 则更新,无 id 则新增。
- **地址**`POST /jmreport/saveDb`
- **请求参数**
```json
{
"id": "1193767090018410496",
"izSharedSource": 0,
"jimuReportId": "1193766682428530688",
"dbCode": "aa",
"dbChName": "aa",
"dbType": "0",
"dbSource": "",
"jsonData": "",
"apiConvert": "",
"jimuSharedSourceId": null,
"isList": "1",
"isPage": "1",
"dbDynSql": "select * from demo",
"fieldList": [
{
"id": "1193767090198765568",
"jimuReportDbId": "1193767090018410496",
"fieldName": "id",
"fieldText": "id",
"widgetType": "String",
"widgetWidth": null,
"orderNum": 0,
"searchFlag": null,
"searchMode": null,
"searchValue": null,
"dictCode": "",
"createBy": "admin",
"createTime": "2026-03-17 14:11:04",
"updateBy": null,
"updateTime": null,
"searchFormat": null,
"extJson": "",
"fieldNamePhysics": null,
"tableIndex": 1,
"_index": 0,
"_rowKey": 44
}
],
"paramList": []
}
```
- **返回结果**
```json
{
"success": true,
"message": "",
"code": 200,
"result": {
"id": "1193767090018410496",
"jimuReportId": "1193766682428530688",
"dbCode": "aa",
"dbChName": "aa",
"dbType": "0",
"dbDynSql": "select * from demo",
"fieldList": [],
"paramList": [],
"isPage": "1",
"isList": "1",
"dbSource": "",
"dbSourceType": "mysql",
"createBy": "admin",
"updateBy": "admin",
"createTime": "2026-03-19 17:12:34",
"updateTime": "2026-03-19 17:12:34",
"apiConvert": "",
"izSharedSource": 0,
"jimuSharedSourceId": null
}
}
```
### dbType 值说明
| dbType | 类型 | 关键字段 |
|--------|------|----------|
| `"0"` | SQL数据集 | `dbDynSql` |
| `"1"` | API数据集 | `apiUrl` + `apiMethod` |
| `"2"` | JavaBean数据集 | `javaType` + `javaValue` |
| `"3"` | JSON数据集 | `jsonData` |
| `"4"` | 共享数据集 | — |
| `"5"` | 多文件数据集 | — |
| `"6"` | 单文件数据集 | — |

View File

@@ -0,0 +1,369 @@
# 报表查询配置完整指南
## 1. 报表参数配置
### 参数语法
| 语法 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `${paramName}` | 用户参数 | 需要在报表参数中声明,用户可查询输入 | `${id}` `${name}` |
| `#{sysVar}` | 系统变量 | 无需声明,自动解析 | `#{sysUserCode}` `#{sysDate}` |
**注意:** `$``#``{` 之间不能有空格。
### SQL 参数示例
```sql
select * from sys_user where id='${id}' and sex='${sex}' and create_by='#{sysUserCode}'
```
### API 参数示例
```
http://192.168.1.116/jmreport/test/getMessage?name='${name}'&createBy='#{sysUserCode}'
```
### 系统变量列表
| 变量 | 说明 |
|------|------|
| `#{sysUserCode}` | 当前登录用户名 |
| `#{sysDate}` | 当前系统日期 |
| `#{sysDateTime}` | 当前系统日期时间 |
| `#{domainURL}` | 系统域名地址 |
### 参数优先级(高→低)
1. **查询条件值**(用户在查询栏输入的)
2. **URL参数**通过URL传递的
3. **默认值**(配置的默认值)
### 参数合并规则
- 多个数据集中**同名参数**会合并为一个查询控件
- 同名**数据集字段**不会合并
- URL参数会传递给所有匹配的数据集参数
## 2. 查询控件类型
在数据集字段详情中勾选"查询"复选框,即可生成查询控件。
| 控件类型 | 查询模式值 | 说明 |
|---------|-----------|------|
| 文本输入 | 空或"输入框" | 默认类型 |
| 下拉单选 | "下拉单选" | 可搜索默认显示10条 |
| 下拉多选 | "下拉多选" | 可搜索默认显示10条 |
| 范围查询 | "范围查询" | 日期/数值范围;**报表参数不支持** |
| 模糊查询 | "模糊查询" | **报表参数不支持** |
| 下拉树 | 通过配置实现 | 层级树形结构 |
| 自定义下拉 | JS增强实现 | 数据需含 `value``text` 字段 |
### 下拉数据源配置
**方式一:系统字典**
- 配置字典编码(如 `sex`
**方式二SQL字典**
```sql
SELECT username AS value, realname AS text FROM sys_user
```
必须别名为 `value``text`
**方式三API**
- 相对路径:`/jmreport/test/getDictSex?createBy=#{sysUserCode}`
- 绝对路径:`http://127.0.0.1:8080/jeecg-boot/jmreport/test/getDictSex`
- 返回格式:`[{"text":"男","value":"1"},{"text":"女","value":"2"}]`
**方式四JS增强**
```javascript
this.updateSelectOptions('dbCode', 'fieldName', options)
```
### 下拉显示条数配置
在字段的配置设置中:
```json
{"selectSearchPageSize": 20}
```
默认显示10条。
## 3. 查询控件默认值
三种方式:
| 方式 | 示例 |
|------|------|
| 静态值 | 直接输入字符串 |
| 动态表达式 | `=dateStr('yyyy-MM-dd')` |
| 系统变量 | `#{sysUserCode}` |
## 4. 时间控件
### 支持的日期格式
| 格式 | 示例 |
|------|------|
| `yyyy-MM-dd HH:mm:ss` | 2021-07-29 12:11:10 |
| `yyyy-MM-dd` | 2021-07-29 |
| `yyyy-MM` | 2021-07 |
| `yyyy` | 2021 |
| `MM` | 07 |
| `HH:mm:ss` | 12:11:10 |
| `HH:mm` | 12:11 |
**重要:** 日期控件传递的值始终为字符串类型。
### 数据库日期转换
不同数据库需要用对应的日期转换函数作为查询条件字段:
| 数据库 | 转换函数 | 示例 |
|--------|---------|------|
| MySQL | `DATE_FORMAT(field, '%Y')` | `DATE_FORMAT(birthday, '%Y') nian` |
| Oracle | `to_char(field, 'yyyy')` | `to_char(birthday, 'yyyy') nian` |
| SQL Server | `year(field)` | `year(birthday) nian` |
**SQL示例MySQL**
```sql
SELECT name, birthday, DATE_FORMAT(birthday, '%Y') nian FROM demo
```
将转换后的列 `nian` 配置为查询条件,而非原始日期字段。
### 时间默认值函数
#### dateStr(date, format, offset)v1.3.79+
| 参数 | 说明 |
|------|------|
| date | 时间字符串(可选,默认当前时间) |
| format | 格式化模式(默认 `yyyy-MM-dd HH:mm:ss` |
| offset | 数值偏移量 |
**示例(当前时间 2020-08-11 12:00:01**
| 表达式 | 结果 |
|--------|------|
| `=dateStr()` | 2020-08-11 12:00:01 |
| `=dateStr('yyyy-MM-dd')` | 2020-08-11 |
| `=dateStr('MM', 2)` | 10 |
| `=dateStr('dd', -1)` | 10 |
| `=dateStr('2020-08-15 12:00:01', 'yyyy-MM-dd', 1)` | 2020-08-16 |
| `=dateStr('yyyy-MM', -1)` | 2020-07v1.4.0+ |
#### date2Str(date, format, offset)v1.7.5+
`dateStr()` 相同但保留前导零(如 `01` 而非 `1`)。
## 5. SQL条件表达式FreeMarker语法
v1.3.79+ 支持动态SQL条件使用 FreeMarker 模板语法。
### isNotEmpty() 函数
`null` 和空字符串 `""` 都返回 `false`
### 基础示例
```sql
select id, name, age from demo where create_by = '#{sysUserCode}'
<#if isNotEmpty(age)> and age = '${age}'</#if>
<#if isNotEmpty(name)> and name = '${name}'</#if>
```
### LIKE模糊查询
```sql
select * from demo where 1=1
<#if name?? && name?length gt 0>
and name like concat('%', '${name}', '%')
</#if>
```
### 多数据集共享参数
```sql
-- 数据集1
select username, sex, phone, create_time from user where 1=1
<#if isNotEmpty(begin_date)>
and DATE_FORMAT(create_time, '%Y-%m-%d') >= '${begin_date}'
</#if>
<#if isNotEmpty(end_date)>
and DATE_FORMAT(create_time, '%Y-%m-%d') <= '${end_date}'
</#if>
-- 数据集2共享 begin_date 和 end_date 参数)
select count(1) as value, DATE_FORMAT(create_time, '%Y-%m-%d') as name
from user where 1=1
<#if isNotEmpty(begin_date)>
and DATE_FORMAT(create_time, '%Y-%m-%d') >= '${begin_date}'
</#if>
<#if isNotEmpty(end_date)>
and DATE_FORMAT(create_time, '%Y-%m-%d') <= '${end_date}'
</#if>
GROUP BY name
```
## 6. SQL表达式函数DaoFormat
v1.6.2+ 支持在SQL中使用 `DaoFormat` 函数。
### DaoFormat.in() — 字符串IN查询
输入:`male,female` → 输出:`'male','female'`
```sql
select * from demo where sex in(${DaoFormat.in('${sex}')})
```
### DaoFormat.inNumber() — 数字IN查询
输入:`21,22` → 输出:`21,22`
```sql
select * from demo where age in(${DaoFormat.inNumber('${age}')})
```
### DaoFormat.concat() — 字符串拼接
```sql
select * from demo where create_time between
'${DaoFormat.concat('${beginTime}', ' 00:00:00')}'
and '${DaoFormat.concat('${endTime}', ' 23:59:59')}'
```
## 7. 下拉树控件
v1.3.79+ 支持层级树形下拉。
### 配置格式
```json
{"loadTree": "{{ domainURL }}/sys/user/treeTest"}
```
或绝对路径:
```json
{"loadTree": "https://api.jeecg.com/mock/26/queryTree"}
```
### 接口返回格式
```json
[
{"id": "001", "pid": "", "value": "A01", "title": "节点1", "izLeaf": 0},
{"id": "002", "pid": "001", "value": "A02", "title": "子节点1", "izLeaf": 1}
]
```
| 字段 | 说明 |
|------|------|
| `id` | 节点标识 |
| `pid` | 父节点ID空=根节点 |
| `value` | 实际查询值 |
| `title` | 显示文本 |
| `izLeaf` | 1=叶子节点无展开图标0=父节点 |
### 穿透场景v1.5.0+
```json
{
"loadTree": ".../treeTest",
"loadTreeByValue": ".../loadTreeByValue"
}
```
**限制:** 下拉树不支持默认值配置。
## 8. 范围查询默认值
使用管道符 `|` 分隔起止值。
| 场景 | 默认值表达式 |
|------|------------|
| 数字范围 | `16\|22` |
| 固定日期 | `2021-11-01\|2021-11-30` |
| 本月1日到今天 | `=concat(string.substring(dateStr('yyyy-MM-dd'),0,8),'01')\|=dateStr('yyyy-MM-dd')` |
| 最近10天 | `=concat(dateStr('yyyy-MM-dd',-10),' 00:00:00')\|=dateStr('yyyy-MM-dd HH:mm:ss')` |
| 最近3个月 | `=concat(dateStr('yyyy',-1),'-',dateStr('MM',-3),'-',dateStr('dd'))\|=dateStr('yyyy-MM-dd')` |
## 9. JS增强与CSS增强
v1.3.79+ 支持。
### JS API方法
| 方法 | 参数 | 用途 |
|------|------|------|
| `updateSelectOptions(dbCode, fieldName, options)` | 数据集编码, 字段名, 选项数组 | 动态更新下拉选项 |
| `onSearchFormChange(dbCode, fieldName, callback)` | 数据集编码, 字段名, 回调函数 | 监听控件值变化 |
| `updateSearchFormValue(dbCode, fieldName, value)` | 数据集编码, 字段名, 值 | 设置控件初始值 |
| `getSelectOptions(dbCode, fieldName)` | 数据集编码, 字段名 | 获取当前下拉选项 |
| `notLoadDataWhenShow()` | 无 | 预览时不自动加载数据v1.6.7+ |
### 三级联动下拉示例
```javascript
function init(){
// 加载省份
$http.metaGet('http://localhost:8080/jeecg-boot/ces/ai/customSelect')
.then(res => {
this.updateSelectOptions('pca', 'pro', res.data)
})
// 省→市联动
this.onSearchFormChange('pca', 'pro', (value) => {
$http.metaGet('http://localhost:8080/jeecg-boot/ces/ai/customSelect', {pid: value})
.then(res => { this.updateSelectOptions('pca', 'city', res.data) })
})
// 市→区联动
this.onSearchFormChange('pca', 'city', (value) => {
$http.metaGet('http://localhost:8080/jeecg-boot/ces/ai/customSelect', {pid: value})
.then(res => { this.updateSelectOptions('pca', 'area', res.data) })
})
}
```
### 设置下拉默认选中第一项v1.4.0+
```javascript
function init(){
let ops = this.getSelectOptions('de', 'sex');
if(ops && ops.length > 0){
this.updateSearchFormValue('de', 'sex', ops[0].value)
}
}
```
### CSS增强示例
```css
.jm-query-form .ivu-btn-primary {
background-color: red;
border-color: red;
}
```
## 10. 参数配置设置
| 配置项 | 用途 | 适用控件 |
|--------|------|---------|
| `loadTree` | 树结构加载URL | 下拉树 |
| `loadTreeByValue` | 按值检索树URL | 下拉树 |
| `dictSplit` | 字典分隔符(仅英文字符) | 下拉单选/多选 |
| `selectSearchPageSize` | 每页显示条数默认10 | 下拉单选/多选 |
| `order` | SQL排序 | 数据集字段详情(非报表参数) |
| `required` | 必填标记v1.7.9+ | 所有类型 |
必填配置:`{"required": true}`,默认 `false`。v1.9.6+ 支持可视化配置界面。
## 11. 查询设置querySetting
```json
"querySetting": {
"izOpenQueryBar": false,
"izDefaultQuery": true
}
```
| 设置 | 默认值 | 说明 |
|------|--------|------|
| `izDefaultQuery` | true | 是否自动执行查询(关闭后需手动点击查询按钮) |
| `izOpenQueryBar` | false | 是否默认展开查询栏 |

View File

@@ -0,0 +1,211 @@
# 接口签名机制 (JimuSignature)
## 概述
积木报表部分接口使用 `@JimuSignature` 注解标记,调用时需要在请求 Header 中携带签名参数,否则返回 `code: 1001` 签名校验失败错误。
**源码位置:**
- 后端拦截器:`jimureport-spring-boot-starter/.../common/interceptor/JimuReportSignatureInterceptor.java`
- 前端签名工具:`static/jmreport/desreport_/js/biz/SignMd5Util.js`
- 前端请求拦截器:`static/jmreport/desreport_/js/core/request.js`
## 需要签名的接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/jmreport/queryFieldBySql` | POST | SQL 解析获取字段 |
| `/jmreport/executeSelectApi` | POST | API 数据集解析 |
| `/jmreport/loadTableData` | POST | 加载表数据 |
| `/jmreport/testConnection` | POST | 测试数据源连接 |
| `/jmreport/download/image` | GET | 下载图片 |
| `/jmreport/dictCodeSearch` | GET | 字典编码搜索 |
| `/jmreport/getDataSourceByPage` | GET | 分页查询数据源 |
| `/jmreport/getDataSourceById` | GET | 按ID查询数据源 |
**不需要签名的接口(常用):**
- `/jmreport/save` — 保存报表
- `/jmreport/saveDb` — 保存数据集
- `/jmreport/get/{id}` — 获取报表
- `/jmreport/field/tree/{reportId}` — 获取数据集树
- `/jmreport/loadDbData/{dbId}` — 加载数据集详情
## 签名算法
### 请求 Headers
| Header | 值 | 说明 |
|--------|------|------|
| `X-Sign` | MD5 签名(大写) | 见下方计算方法 |
| `X-TIMESTAMP` | 当前时间戳(毫秒) | `int(time.time() * 1000)` |
### 计算步骤
```
1. 收集所有请求参数URL query参数 + POST body参数
2. 按 key 字母升序排序SortedMap / TreeMap
3. 转为 JSON 字符串JSON.stringify(sortedParams) 或 JSONObject.toJSONString(sortedMap)
4. 拼接签名密钥jsonStr + signatureSecret
5. 计算 MD5 并转大写MD5(jsonStr + secret).toUpperCase()
```
### 签名密钥 (signatureSecret)
**默认值:** `dd05f1c54d63749eda95f9fa6d49v442a`
解析优先级:
1. `JmReportBaseConfig.getSignatureSecret()` — 代码配置
2. Spring 属性 `jeecg.signatureSecret` — application.yml 配置
3. 默认值 `dd05f1c54d63749eda95f9fa6d49v442a`
> **注意:** 默认密钥中第29个字符是字母 `v`,不是数字 `4`。
### 时间戳校验
服务端校验时间戳有效期为 **5 分钟300秒**。如果客户端与服务器时间差超过5分钟会返回 "签名验证失败:X-TIMESTAMP已过期"。
### 参数值类型转换规则
前端在签名前会统一类型(后端用 `json.getString(key)` 读取,也是字符串):
- **数字** → 转为字符串(如 `0``"0"`
- **布尔** → 转为字符串(如 `false``"false"`
- **对象/数组** → 转为 JSON 字符串
- **null/空** → 不参与签名
### 后端校验逻辑
```java
// 1. 收集参数到 SortedMap (TreeMap, 自动按key排序)
SortedMap<String, String> map = new TreeMap<>();
// 2. 从 request.getParameterMap() 取 URL/form 参数
// 3. 从 request.getQueryString() 取 GET 参数
// 4. 从 POST body JSON 取参数 (json.getString(key))
// 5. 计算签名
String paramsJsonStr = JSONObject.toJSONString(map); // fastjson
String signValue = DigestUtils.md5DigestAsHex(
(paramsJsonStr + CommonUtils.getSignatureSecret()).getBytes()
).toUpperCase();
// 6. 比对 header 中的 X-Sign
```
> **关键细节:** 后端用 fastjson 的 `JSONObject.toJSONString(map)` 序列化 SortedMap输出格式为 `{"key1":"value1","key2":"value2"}`无空格。Python 端必须用 `json.dumps(sorted_dict, separators=(',', ':'))` 匹配(无空格)。
## Python 实现
```python
import hashlib
import json
import time
SIGNATURE_SECRET = "dd05f1c54d63749eda95f9fa6d49v442a"
def compute_sign(params_dict):
"""
计算积木报表接口签名
params_dict: 请求参数字典POST body 或 GET query 参数)
"""
# 1. 所有值转为字符串(与前端/后端保持一致)
str_params = {}
for k, v in params_dict.items():
if v is None:
continue
if isinstance(v, bool):
str_params[k] = str(v).lower() # True -> "true"
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)
# 2. 按 key 字母升序排序
sorted_params = dict(sorted(str_params.items()))
# 3. 转为 JSON 字符串(无空格,与 fastjson 一致)
params_json = json.dumps(sorted_params, ensure_ascii=False, separators=(',', ':'))
# 4. 拼接密钥并计算 MD5
sign_str = params_json + SIGNATURE_SECRET
sign_value = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
return sign_value
def get_sign_headers(params_dict):
"""获取签名相关的请求头"""
return {
'X-Sign': compute_sign(params_dict),
'X-TIMESTAMP': str(int(time.time() * 1000))
}
```
### 使用示例
```python
# POST /jmreport/queryFieldBySql
body = {"sql": "select * from demo", "dbSource": "", "type": "0"}
sign_headers = get_sign_headers(body)
# sign_headers = {'X-Sign': 'AB12CD34...', 'X-TIMESTAMP': '1774019281912'}
# GET /jmreport/getDataSourceByPage?pageNo=1&pageSize=10
query_params = {"pageNo": "1", "pageSize": "10"}
sign_headers = get_sign_headers(query_params)
```
### 完整的带签名 API 请求函数
```python
def api_request(path, data=None, method=None):
"""发送 API 请求,自动判断是否需要签名"""
url = f'{API_BASE}{path}'
headers = {
'X-Access-Token': TOKEN,
'Content-Type': 'application/json; charset=UTF-8'
}
# 需要签名的接口列表
SIGNED_ENDPOINTS = [
'/jmreport/queryFieldBySql',
'/jmreport/executeSelectApi',
'/jmreport/loadTableData',
'/jmreport/testConnection',
'/jmreport/download/image',
'/jmreport/dictCodeSearch',
'/jmreport/getDataSourceByPage',
'/jmreport/getDataSourceById',
]
# 判断是否需要签名
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'))
```
## 常见错误
| 错误信息 | 原因 | 解决方案 |
|---------|------|---------|
| `签名验证失败: 签名参数不存在` | 未传 X-Sign 或 X-TIMESTAMP Header | 添加签名 Headers |
| `签名验证失败:X-TIMESTAMP已过期` | 客户端与服务器时间差超过5分钟 | 检查系统时间,使用当前时间戳 |
| `签名校验失败,参数有误!` | 签名值不匹配 | 检查参数排序、JSON序列化格式、密钥是否正确 |
| `code: 1001` | 签名相关错误的统一错误码 | 查看 message 详情 |
## 调试技巧
1. **打印签名输入**:输出 `params_json + secret` 字符串,对比前后端是否一致
2. **对比 JSON 格式**:确保使用 `separators=(',', ':')` 无空格格式
3. **检查类型转换**:数字/布尔/对象必须转为字符串
4. **验证密钥**:确认使用的密钥与服务端配置一致(默认 `dd05f1c54d63749eda95f9fa6d49v442a`

View File

@@ -0,0 +1,152 @@
# 积木报表模板分析参考
## 模板报表查询
通过 `getReportByUser` 接口获取模板报表:
```
GET /jmreport/getReportByUser?reportId=&template=1
```
## 46个模板分类统计
| 分类 | 数量 | 示例 |
|------|------|------|
| 基础表格 | 30 | 信息采集表、简单分组报表 |
| 图表报表 | 9 | 全国各大城市化员数据、物业实时监控 |
| 循环报表 | 4 | 订单表循环打印、班级循环套打表 |
| 图片报表 | 4 | 员工信息表、证书打印 |
| 条码/二维码 | 3 | 实习证明、凭证条码报表 |
## 图表数据绑定
### extData 数据类型 (dataType)
**前端使用文本字符串,非数字:**
- `"sql"` - SQL数据集
- `"api"` - API数据集
- `"json"` - JSON数据集
- `"javabean"` - JavaBean数据集
- `"files"` - 文件数据集
- `null` - 静态图表无数据绑定使用ECharts配置中的硬编码数据
### 字段映射规则
固定三个字段名映射:
```python
extData = {
"axisX": "name", # X轴/分类字段
"axisY": "value", # Y轴/数值字段
"series": "type" # 系列/分组字段(多系列图表用)
}
```
SQL查询需要AS别名
```sql
SELECT category AS name, COUNT(*) AS value, '' AS type FROM table GROUP BY category
```
### xText / yText 轴标题
工作正常的模板中这两个字段常常为空字符串轴标题主要通过ECharts配置设置
```python
chart_config = {
"xAxis": {
"name": "表单类型", # 轴标题
"type": "category"
},
"yAxis": {
"name": "数量",
"type": "value"
}
}
```
## displayConfig 单元格组件
用于在普通单元格中渲染条码、二维码、图片。
### 配置结构
```json
{
"displayConfig": {
"1": {"barcodeContent": "#{pop.id}", "format": "CODE128", "width": "50", "height": "100", "displayValue": false},
"11": {"text": "#{uiu.tm}", "width": 227, "height": 227, "colorDark": "#000000", "colorLight": "#ffffff"},
"111": {"barcodeContent": "固定值", "format": "QR", "width": "6", "height": 39}
}
}
```
### 键名规则
- 键名 = `列号`从1开始
- 行号通过cells中的display属性关联
### 条码配置 (barcodeContent)
```json
{
"barcodeContent": "#{字段变量}", // 动态值
"format": "CODE128|CODE39|QR", // 条码格式
"width": "2", // 条码宽度
"height": 80, // 条码高度
"displayValue": false // 是否显示值
}
```
### 二维码配置 (text)
```json
{
"text": "#{字段变量}", // 二维码内容
"width": 112, // 宽度
"height": 112, // 高度
"colorDark": "#000000", // 前景色
"colorLight": "#ffffff" // 背景色
}
```
## 循环报表 (loopBlockList)
### 结构
```json
{
"loopBlockList": [
{
"sci": 1, // 起始列
"sri": 2, // 起始行
"eci": 5, // 结束列
"eri": 5, // 结束行
"index": 1, // 块索引
"db": "jm", // 数据集别名
"loopTime": 3 // 循环次数(可选)
}
]
}
```
### 单元格变量语法
```
#{数据集别名.字段名}
#{jm.name}
#{pop.group(id)}
```
## 常见图表类型数据要求
| 图表类型 | dataType | axisX | axisY | series | 示例数据 |
|---------|----------|-------|-------|--------|---------|
| bar.simple | sql/api | name | value | type | 单系列柱状 |
| bar.multi | sql/api | name | value | type | 多系列柱状 |
| line.simple | sql/api | name | value | type | 单线折线 |
| pie.simple | sql/api | name | value | - | 饼图 |
| gauge.simple | sql/api | name | value | - | 仪表盘 |
| radar.basic | sql/api | name | value | type | 雷达图 |
| map.scatter | sql/api | name | value | - | 地图散点 |
## 模板报表ID参考
| 报表名称 | ID | 特点 |
|---------|---|------|
| 全国各大城市化员数据 | 1339859143477039104 | 9个图表(sql+api混合) |
| 图表数据联动示例 | 1356492523694067712 | 19个图表(静态+动态) |
| 物业实时监控 | 1339478701846433792 | 9个图表+地图 |
| 凭证条码报表 | 1338370016550195200 | displayConfig条码 |
| 实习证明 | 1350035590569136128 | displayConfig二维码 |
| 图片展示平台 | 1334074491629867008 | 8个图片+4个图表 |

View File

@@ -0,0 +1,794 @@
"""
积木报表 (JiMu Report) 创建/编辑工具脚本
用法:
python jimureport_creator.py --api-base <URL> --token <TOKEN> --config <config.json>
config.json 格式见下方示例。
支持的操作:
- 创建报表 (action='create')
- 编辑报表 (action='edit', 需提供 reportId)
config.json 示例(创建):
{
"action": "create",
"reportName": "用户数据统计报表",
"datasets": [
{
"dbCode": "userlist",
"dbChName": "用户列表",
"dbDynSql": "SELECT username, realname, sex FROM sys_user WHERE del_flag = 0",
"isPage": "1"
},
{
"dbCode": "sexchart",
"dbChName": "性别图表",
"dbDynSql": "SELECT sex AS name, COUNT(*) AS value FROM sys_user WHERE del_flag = 0 GROUP BY sex",
"isPage": "0",
"forChart": true
}
],
"layout": "chart_top",
"table": {
"datasetCode": "userlist",
"title": "用户数据列表",
"columns": [
{"field": "username", "title": "用户账号", "width": 120},
{"field": "realname", "title": "姓名", "width": 100},
{"field": "sex", "title": "性别", "width": 80}
]
},
"chart": {
"datasetCode": "sexchart",
"chartType": "pie.doughnut",
"title": "按性别统计",
"width": "650",
"height": "300"
}
}
"""
import urllib.request
import json
import sys
import time
import random
import hashlib
import ssl
import argparse
# 修复 Windows 控制台中文乱码
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
SIGNATURE_SECRET = 'dd05f1c54d63749eda95f9fa6d49v442a'
SIGNED_ENDPOINTS = [
'/jmreport/queryFieldBySql',
'/jmreport/executeSelectApi',
'/jmreport/loadTableData',
'/jmreport/testConnection',
'/jmreport/download/image',
'/jmreport/dictCodeSearch',
'/jmreport/getDataSourceByPage',
'/jmreport/getDataSourceById',
]
# 默认样式列表
DEFAULT_STYLES = [
# 0: 仅边框
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}},
# 1: 边框+居中
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center"},
# 2: 边框+居中+垂直居中(数据行)
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle"},
# 3: 边框+居中+垂直居中+蓝底(表头无白字)
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#01b0f1"},
# 4: 边框+居中+垂直居中+蓝底白字(表头推荐)
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#01b0f1", "color": "#ffffff"},
# 5: 边框+居中+垂直居中+深蓝底白字加粗(大标题)
{"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#4472C4", "color": "#ffffff", "font": {"bold": True}},
]
# 默认图表配色
DEFAULT_CHART_COLORS = ["#5470c6", "#ee6666", "#91cc75", "#fac858", "#73c0de", "#3ba272", "#fc8452", "#9a60b4"]
# ====== 工具函数 ======
def gen_id():
"""生成唯一ID"""
return str(int(time.time() * 1000) * 1000000 + random.randint(100000, 999999))
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 create_ssl_context():
"""创建不验证证书的SSL上下文"""
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
SSL_CTX = create_ssl_context()
def api_request(api_base, token, path, data=None, method=None):
"""发送API请求自动处理签名"""
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=SSL_CTX)
return json.loads(resp.read().decode('utf-8'))
# ====== 数据集相关 ======
def parse_sql_fields(api_base, token, sql, db_source=''):
"""解析SQL获取字段列表"""
result = api_request(api_base, token, '/jmreport/queryFieldBySql', {
"sql": sql, "dbSource": db_source, "type": "0"
})
if not result.get('success'):
print(f' SQL解析失败: {result.get("message")}')
return [], []
field_list = result.get('result', {}).get('fieldList', [])
param_list = result.get('result', {}).get('paramList', [])
return field_list, param_list
def save_dataset(api_base, token, report_id, ds_config, field_list, param_list):
"""保存数据集返回数据集ID"""
db_data = {
"izSharedSource": 0,
"jimuReportId": report_id,
"dbCode": ds_config['dbCode'],
"dbChName": ds_config.get('dbChName', ds_config['dbCode']),
"dbType": ds_config.get('dbType', '0'),
"dbSource": ds_config.get('dbSource', ''),
"jsonData": ds_config.get('jsonData', ''),
"apiConvert": ds_config.get('apiConvert', ''),
"isList": ds_config.get('isList', '1'),
"isPage": ds_config.get('isPage', '1'),
"dbDynSql": ds_config.get('dbDynSql', ''),
"fieldList": field_list,
"paramList": param_list
}
result = api_request(api_base, token, '/jmreport/saveDb', db_data)
if not result.get('success'):
print(f' 数据集保存失败: {result.get("message")}')
return None
return result['result']['id']
# ====== 报表布局构造 ======
def build_cols(columns):
"""根据列配置构造cols对象"""
cols = {"len": 100}
for i, col in enumerate(columns):
if col.get('width'):
cols[str(i + 1)] = {"width": col['width']}
return cols
def build_table_rows(table_config, start_row=1, title_style=5, header_style=4, data_style=2):
"""
构造数据表格的rows、merges。
返回 (rows_dict, merges_list, next_row)
"""
rows = {}
merges = []
columns = table_config.get('columns', [])
ds_code = table_config['datasetCode']
col_count = len(columns)
current_row = start_row
# 标题行
title = table_config.get('title')
if title:
cells = {}
for i in range(col_count):
cells[str(i + 1)] = {"text": title if i == 0 else "", "style": title_style}
rows[str(current_row)] = {"cells": cells, "height": 40}
if col_count > 1:
start_col = chr(ord('A') + 1) # B列开始列索引1对应B
end_col = chr(ord('A') + col_count)
# merge 使用 UI 行号(代码行号+1因为代码row 0 = UI row 1
ui_row = current_row + 1
merges.append(f"{start_col}{ui_row}:{end_col}{ui_row}")
current_row += 1
# 表头行
header_cells = {}
for i, col in enumerate(columns):
header_cells[str(i + 1)] = {"text": col['title'], "style": header_style}
rows[str(current_row)] = {"cells": header_cells, "height": 34}
current_row += 1
# 数据绑定行
data_cells = {}
for i, col in enumerate(columns):
data_cells[str(i + 1)] = {"text": f"#{{{ds_code}.{col['field']}}}", "style": data_style}
rows[str(current_row)] = {"cells": data_cells}
current_row += 1
return rows, merges, current_row
def build_chart_rows(chart_config, chart_db_id, start_row=1, col_start=1, col_end=6, row_count=1):
"""
构造图表的虚拟单元格rows和chartList。
row_count 默认为1行与设计器行为一致图表通过width/height属性控制大小
虚拟单元格仅作为锚点,不需要覆盖整个图表区域)。
返回 (rows_dict, chart_list, next_row)
"""
layer_id = "chart_" + gen_id()
rows = {}
virtual_cell_range = []
for r in range(start_row, start_row + row_count):
cells = {}
for c in range(col_start, col_end + 1):
cells[str(c)] = {"text": " ", "virtual": layer_id}
virtual_cell_range.append([r, c])
rows[str(r)] = {"cells": cells}
# ECharts 配置
chart_type = chart_config.get('chartType', 'bar.simple')
echarts_config = build_echarts_config(chart_type, chart_config)
chart_item = {
"row": start_row,
"col": col_start,
"colspan": 0,
"rowspan": 0,
"width": str(chart_config.get('width', '650')),
"height": str(chart_config.get('height', '350')),
"config": json.dumps(echarts_config, ensure_ascii=False),
"url": "",
"extData": {
"chartType": chart_type,
"dataType": chart_config.get('dataType', 'sql'),
"dataId": chart_db_id,
"dbCode": chart_config['datasetCode'],
"axisX": "name",
"axisY": "value",
"series": "type",
"xText": "",
"yText": "",
"apiStatus": "1"
},
"layer_id": layer_id,
"offsetX": 0,
"offsetY": 0,
"backgroud": {"enabled": False, "color": "#fff", "image": ""},
"virtualCellRange": virtual_cell_range
}
return rows, [chart_item], start_row + row_count
def build_echarts_config(chart_type, chart_config):
"""根据图表类型构造ECharts配置"""
title_text = chart_config.get('title', '')
colors = chart_config.get('colors', DEFAULT_CHART_COLORS)
if chart_type.startswith('pie'):
# 饼图/环形图/玫瑰图
radius = ["40%", "70%"] if 'doughnut' in chart_type else "70%"
if 'rose' in chart_type:
radius = [20, "70%"]
return {
"title": {"text": title_text, "left": "center", "textStyle": {"fontSize": 16}},
"tooltip": {"trigger": "item", "formatter": "{b}: {c} ({d}%)"},
"legend": {"orient": "vertical", "left": "left", "top": "middle"},
"series": [{
"type": "pie",
"radius": radius,
"center": ["55%", "55%"],
"avoidLabelOverlap": True,
"itemStyle": {"borderRadius": 6, "borderColor": "#fff", "borderWidth": 2},
"label": {"show": True, "formatter": "{b}: {c}"},
"emphasis": {"label": {"show": True, "fontSize": 16, "fontWeight": "bold"}},
"data": [],
"roseType": "area" if 'rose' in chart_type else None
}],
"color": colors
}
elif chart_type.startswith('bar'):
# 柱状图
is_horizontal = 'horizontal' in chart_type
return {
"title": {"text": title_text, "left": "center"},
"tooltip": {"trigger": "axis"},
"legend": {"bottom": 0},
"xAxis": [{"type": "value" if is_horizontal else "category", "data": []}],
"yAxis": [{"type": "category" if is_horizontal else "value", "data": []}],
"series": [{"type": "bar", "data": [], "itemStyle": {"color": colors[0]}}],
"color": colors
}
elif chart_type.startswith('line'):
# 折线图
smooth = 'smooth' in chart_type
area_style = {"opacity": 0.3} if 'area' in chart_type else None
return {
"title": {"text": title_text, "left": "center"},
"tooltip": {"trigger": "axis"},
"legend": {"bottom": 0},
"xAxis": [{"type": "category", "data": []}],
"yAxis": [{"type": "value"}],
"series": [{"type": "line", "data": [], "smooth": smooth, "areaStyle": area_style}],
"color": colors
}
elif chart_type.startswith('gauge'):
# 仪表盘
return {
"title": {"text": title_text, "left": "center"},
"tooltip": {"formatter": "{b}: {c}"},
"series": [{"type": "gauge", "data": [], "detail": {"formatter": "{value}"}}]
}
elif chart_type.startswith('radar'):
# 雷达图
return {
"title": {"text": title_text, "left": "center"},
"tooltip": {},
"legend": {"bottom": 0},
"radar": {"indicator": []},
"series": [{"type": "radar", "data": []}],
"color": colors
}
elif chart_type.startswith('funnel'):
# 漏斗图
return {
"title": {"text": title_text, "left": "center"},
"tooltip": {"trigger": "item", "formatter": "{b}: {c}"},
"legend": {"bottom": 0},
"series": [{"type": "funnel", "data": [], "left": "10%", "width": "80%"}],
"color": colors
}
else:
# 通用默认
return {
"title": {"text": title_text, "left": "center"},
"tooltip": {},
"series": [{"type": "bar", "data": []}],
"color": colors
}
# ====== 报表保存 ======
def build_base_save_data(report_id, designer_obj, rows, cols, styles, merges, chart_list=None, page_size=None, area=None, data_rect_width=None):
"""构造报表保存请求体"""
return {
"designerObj": json.dumps(designer_obj, ensure_ascii=False),
"name": "sheet1",
"freeze": "A1",
"freezeLineColor": "rgb(185, 185, 185)",
"rows": rows,
"cols": cols,
"styles": styles,
"merges": merges,
"validations": [],
"autofilter": {},
"dbexps": [],
"dicts": [],
"loopBlockList": [],
"zonedEditionList": [],
"fixedPrintHeadRows": [],
"fixedPrintTailRows": [],
"hiddenCells": [],
"submitHandlers": [],
"rpbar": {"show": True, "pageSize": str(page_size) if page_size else "", "btnList": []},
"fillFormToolbar": {"show": True, "btnList": ["save", "subTable_add", "verify", "subTable_del", "print", "close", "first", "prev", "next", "paging", "total", "last", "exportPDF", "exportExcel", "exportWord"]},
"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": area if area is not None else False,
"chartList": chart_list or [],
"background": False,
"dataRectWidth": data_rect_width if data_rect_width is not None else 700,
"excel_config_id": report_id,
"pyGroupEngine": False,
"isViewContentHorizontalCenter": False,
"fillFormStyle": "default",
"sheetId": "default",
"sheetName": "默认Sheet",
"sheetOrder": "0"
}
def save_report(api_base, token, save_data):
"""调用报表保存接口"""
result = api_request(api_base, token, '/jmreport/save', save_data)
return result
# ====== 主流程 ======
def create_report(api_base, token, config):
"""创建新报表"""
report_id = gen_id()
report_code = str(int(time.time() * 1000))
report_name = config['reportName']
print(f'\n{"=" * 50}')
print(f'创建积木报表: {report_name}')
print(f'{"=" * 50}')
# Step 1: 创建空报表
designer_obj = {
"id": report_id, "code": report_code, "name": report_name,
"type": "0", "template": 0, "delFlag": 0, "viewCount": 0,
"updateCount": 0, "submitForm": config.get('submitForm', 0),
"reportName": report_name
}
empty_save = build_base_save_data(report_id, designer_obj, {"len": 200}, {"len": 100}, [], [])
print('\n[1/4] 创建空报表...')
r = save_report(api_base, token, empty_save)
print(f' 结果: success={r.get("success")}')
if not r.get('success'):
print(f' 失败: {r.get("message")}')
return None
# Step 2: 解析SQL并保存数据集
print('\n[2/4] 解析SQL并保存数据集...')
dataset_ids = {}
for ds in config.get('datasets', []):
db_code = ds['dbCode']
sql = ds.get('dbDynSql', '')
db_source = ds.get('dbSource', '')
print(f' 解析数据集 [{db_code}]: {sql[:60]}...' if len(sql) > 60 else f' 解析数据集 [{db_code}]: {sql}')
field_list, param_list = parse_sql_fields(api_base, token, sql, db_source)
if not field_list:
print(f' 警告: 数据集 [{db_code}] 字段为空')
continue
ds_id = save_dataset(api_base, token, report_id, ds, field_list, param_list)
if ds_id:
dataset_ids[db_code] = ds_id
print(f' 数据集 [{db_code}] 保存成功, id={ds_id}')
else:
print(f' 数据集 [{db_code}] 保存失败')
# Step 3: 构造布局
print('\n[3/4] 构造报表布局...')
layout = config.get('layout', 'table_only')
table_config = config.get('table')
chart_config = config.get('chart')
all_rows = {"len": 200}
all_merges = []
chart_list = []
col_count = len(table_config['columns']) if table_config else 6
if layout == 'chart_top' and chart_config and table_config:
# 图表在上,数据表格在下(避免列表展开与图表冲突)
chart_db_id = dataset_ids.get(chart_config['datasetCode'], '')
chart_rows, chart_list, next_row = build_chart_rows(
chart_config, chart_db_id,
start_row=1, col_start=1, col_end=col_count
)
all_rows.update(chart_rows)
# 分隔行
all_rows[str(next_row)] = {"cells": {}, "height": 10}
next_row += 1
# 数据表格
table_rows, table_merges, _ = build_table_rows(table_config, start_row=next_row)
all_rows.update(table_rows)
all_merges.extend(table_merges)
print(f' 布局: 图表在上(rows 1-10) + 数据表(rows {next_row}+)')
elif layout == 'chart_bottom' and chart_config and table_config:
# 数据表格在上,图表在下
# 图表虚拟行必须在数据展开区域之后,否则预览会重叠
table_rows, table_merges, next_row = build_table_rows(table_config, start_row=1)
all_rows.update(table_rows)
all_merges.extend(table_merges)
# data_binding_row = next_row - 1 (数据绑定行)
# 数据展开后最多占 pageSize 行,图表需在展开区域之后
page_size = config.get('pageSize', 10)
gap = config.get('gap', 1) # 默认1行间距更紧凑
data_binding_row = next_row - 1
chart_start = data_binding_row + page_size + gap
chart_db_id = dataset_ids.get(chart_config['datasetCode'], '')
chart_rows, chart_list, chart_end_row = build_chart_rows(
chart_config, chart_db_id,
start_row=chart_start, col_start=1, col_end=col_count
)
all_rows.update(chart_rows)
# 添加分页符行(自动触发滚动条计算)
# 分页符放在图表下方约3行的位置确保在数据展开区域之外
# 使用多个空格作为分页符,避免显示"1"
pagination_row = chart_start + page_size + 3
all_rows[str(pagination_row)] = {"cells": {"1": {"text": " "}}}
# 确保 len 足够大
if pagination_row > all_rows.get("len", 200):
all_rows["len"] = pagination_row + 10
print(f' 布局: 数据表(rows 1-{next_row - 1}) + 间距({gap}行) + 图表(row {chart_start}+) + 分页符(row {pagination_row})')
print(f' pageSize={page_size}, 图表在数据展开区域之后')
elif layout == 'chart_right' and chart_config and table_config:
# 数据表格在左,图表在右
table_rows, table_merges, next_row = build_table_rows(table_config, start_row=1)
all_rows.update(table_rows)
all_merges.extend(table_merges)
chart_db_id = dataset_ids.get(chart_config['datasetCode'], '')
chart_col_start = col_count + 2 # 留1列间距
chart_col_end = chart_col_start + 5
chart_rows, chart_list, _ = build_chart_rows(
chart_config, chart_db_id,
start_row=1, col_start=chart_col_start, col_end=chart_col_end
)
# 合并chart_rows到all_rows同行不同列
for row_key, row_val in chart_rows.items():
if row_key in all_rows and row_key != "len":
all_rows[row_key]["cells"].update(row_val["cells"])
else:
all_rows[row_key] = row_val
print(f' 布局: 数据表(cols 1-{col_count}) + 图表(cols {chart_col_start}-{chart_col_end})')
elif layout == 'chart_only' and chart_config:
# 仅图表
chart_db_id = dataset_ids.get(chart_config['datasetCode'], '')
chart_rows, chart_list, _ = build_chart_rows(
chart_config, chart_db_id,
start_row=1, col_start=1, col_end=6
)
all_rows.update(chart_rows)
print(f' 布局: 仅图表(row 1)')
elif table_config:
# 仅数据表格(默认)
table_rows, table_merges, _ = build_table_rows(table_config, start_row=1)
all_rows.update(table_rows)
all_merges.extend(table_merges)
print(f' 布局: 仅数据表')
else:
print(' 错误: 未配置 table 或 chart')
return None
# 构造列宽
cols = build_cols(table_config['columns']) if table_config else {"len": 100}
# 计算 dataRectWidth列宽总和
total_width = sum(col.get('width', 100) for col in cols.values() if isinstance(col, dict))
data_rect_width = total_width if total_width > 0 else 700
# 计算 area内容边界区域
# area.sri/eri = 内容区域的起始/结束行UI行号从1开始
# area.sci/eci = 内容区域的起始/结束列
# area.width/height = 内容区域的总像素宽高
title_h = 40
header_h = 34
row_h = 25 # 默认行高
chart_h = int(chart_config.get('height', 350)) if chart_config else 0
if layout == 'chart_bottom' and chart_config and table_config:
# 设置 area 为 false让系统自动计算滚动高度
# 需要在图表底部添加分页符行,系统才能正确计算
area = False
elif layout == 'chart_top' and chart_config and table_config:
area = {
"sri": 1,
"sci": 1,
"eri": next_row - 1,
"eci": col_count,
"width": data_rect_width,
"height": chart_h + 10 + title_h + header_h + row_h * 2
}
elif layout == 'chart_right' and chart_config and table_config:
chart_w = int(chart_config.get('width', 650))
area = {
"sri": 1,
"sci": 1,
"eri": next_row - 1,
"eci": chart_col_end,
"width": data_rect_width + chart_w,
"height": title_h + header_h + row_h * 2
}
elif layout == 'chart_only' and chart_config:
area = {
"sri": 1,
"sci": 1,
"eri": 1,
"eci": col_count,
"width": data_rect_width,
"height": chart_h
}
else:
area = {
"sri": 1,
"sci": 1,
"eri": next_row - 1,
"eci": col_count,
"width": data_rect_width,
"height": title_h + header_h + row_h * 2
}
# Step 4: 保存完整报表
print('\n[4/4] 保存报表设计...')
designer_obj["updateCount"] = 1
save_data = build_base_save_data(
report_id, designer_obj, all_rows, cols,
config.get('styles', DEFAULT_STYLES), all_merges, chart_list,
page_size=config.get('pageSize'),
area=area,
data_rect_width=data_rect_width
)
r = save_report(api_base, token, save_data)
print(f' 结果: success={r.get("success")}')
if r.get('success'):
print(f'\n{"=" * 50}')
print(f'报表创建成功!')
print(f' 报表ID: {report_id}')
print(f' 报表名称: {report_name}')
print(f' 预览地址: {api_base}/jmreport/view/{report_id}')
print(f'{"=" * 50}')
return report_id
else:
print(f' 保存失败: {r.get("message")}')
return None
def edit_report(api_base, token, config):
"""编辑已有报表"""
report_id = config['reportId']
print(f'\n{"=" * 50}')
print(f'编辑积木报表: reportId={report_id}')
print(f'{"=" * 50}')
# 获取现有报表
print('\n[1/3] 获取现有报表...')
r = api_request(api_base, token, f'/jmreport/get/{report_id}', method='GET')
if not r.get('success'):
print(f' 获取失败: {r.get("message")}')
return None
existing = r['result']
print(f' 报表名称: {existing.get("name")}')
# 获取现有数据集
print('\n[2/3] 获取现有数据集...')
tree_r = api_request(api_base, token, f'/jmreport/field/tree/{report_id}', method='GET')
if tree_r.get('success') and tree_r.get('result'):
for ds in tree_r['result']:
print(f' 已有数据集: [{ds.get("dbCode")}] {ds.get("dbChName")}')
# 添加新数据集
dataset_ids = {}
for ds in config.get('addDatasets', []):
db_code = ds['dbCode']
sql = ds.get('dbDynSql', '')
db_source = ds.get('dbSource', '')
print(f' 新增数据集 [{db_code}]...')
field_list, param_list = parse_sql_fields(api_base, token, sql, db_source)
ds_id = save_dataset(api_base, token, report_id, ds, field_list, param_list)
if ds_id:
dataset_ids[db_code] = ds_id
print(f' 数据集 [{db_code}] 保存成功, id={ds_id}')
# 如果需要更新报表设计jsonStr在此处理
if config.get('table') or config.get('chart'):
print('\n[3/3] 更新报表设计...')
# 重新构造完整布局
layout = config.get('layout', 'chart_top')
table_config = config.get('table')
chart_config = config.get('chart')
all_rows = {"len": 200}
all_merges = []
chart_list = []
col_count = len(table_config['columns']) if table_config else 6
if layout == 'chart_top' and chart_config and table_config:
chart_db_id = dataset_ids.get(chart_config['datasetCode'], chart_config.get('dataId', ''))
chart_rows, chart_list, next_row = build_chart_rows(
chart_config, chart_db_id,
start_row=1, col_start=1, col_end=col_count
)
all_rows.update(chart_rows)
all_rows[str(next_row)] = {"cells": {}, "height": 10}
next_row += 1
table_rows, table_merges, _ = build_table_rows(table_config, start_row=next_row)
all_rows.update(table_rows)
all_merges.extend(table_merges)
cols = build_cols(table_config['columns']) if table_config else {"len": 100}
designer_obj = {
"id": report_id,
"code": existing.get('code', ''),
"name": config.get('reportName', existing.get('name', '')),
"type": existing.get('type', '0'),
"template": existing.get('template', 0),
"delFlag": 0,
"viewCount": existing.get('viewCount', 0),
"updateCount": (existing.get('updateCount') or 0) + 1,
"submitForm": existing.get('submitForm', 0),
"reportName": config.get('reportName', existing.get('name', ''))
}
save_data = build_base_save_data(
report_id, designer_obj, all_rows, cols,
config.get('styles', DEFAULT_STYLES), all_merges, chart_list
)
r = save_report(api_base, token, save_data)
print(f' 结果: success={r.get("success")}')
print(f'\n编辑完成!')
print(f' 预览地址: {api_base}/jmreport/view/{report_id}')
return report_id
def main():
parser = argparse.ArgumentParser(description='积木报表 (JiMu Report) 创建/编辑工具')
parser.add_argument('--api-base', required=True, help='JeecgBoot 后端地址')
parser.add_argument('--token', required=True, help='X-Access-Token')
parser.add_argument('--config', required=True, help='配置文件路径 (JSON)')
args = parser.parse_args()
with open(args.config, 'r', encoding='utf-8') as f:
config = json.load(f)
action = config.get('action', 'create')
if action == 'create':
create_report(args.api_base, args.token, config)
elif action == 'edit':
edit_report(args.api_base, args.token, config)
else:
print(f'未知操作类型: {action}')
sys.exit(1)
if __name__ == '__main__':
main()