name: jimubi-dashboard description: Use when user asks to create/design a dashboard (仪表盘/看板), data kanban, or says "创建仪表盘", "生成仪表盘", "做一个仪表盘", "数据看板", "做一个看板", "创建看板", "数据面板", "统计看板", "运营看板", "create dashboard", "generate dashboard", "design dashboard", "data kanban", "KPI dashboard". Also triggers for QQY/敲敲云 mode dashboards: "敲敲云仪表盘", "低代码应用仪表盘", "应用内仪表盘", "给应用添加图表", mentions appId+tenantId in dashboard context. Also triggers when user describes dashboard/kanban requirements like "做一个运营数据看板" or mentions grid-layout data display like "统计系统数据". Make sure to use this skill for dashboards (仪表盘/看板) — NOT big screens (大屏), which use completely different positioning, styling, and component configurations.
JeecgBoot 仪表盘 AI 自动生成器
将自然语言的仪表盘需求转换为 drag page 配置,并通过 API 自动创建。
本 skill 专门处理仪表盘(default)模式:网格布局(24列栅格),亮色主题,带卡片头,适用于日常数据看板。 大屏请使用
jimubi-bigscreenskill。
⚠️ 强制规则:所有仪表盘相关操作必须优先通过本 skill 处理(无任何例外)
触发范围:凡涉及仪表盘的任何操作,包括但不限于:
- 创建/删除/修改仪表盘页面
- 添加/编辑/删除组件
- 数据集(SQL/API/文件/WebSocket)的创建与绑定
- 数据源的创建、编辑、测试(包括修改用户名、密码、连接参数等)
- 模板复制、页面配置修改
- 组件联动、钻取、外部链接
禁止行为:
- 未调用本 skill,直接读 memory 找凭据自行执行
- 未调用本 skill,自己探索 API 路径后直接调用
- 以"操作太简单不需要 skill"为由跳过
- 用 curl/bash/Agent 子代理探测仪表盘 API 端点(正确做法:先调用本 skill,再在 skill 上下文内执行)
正确执行顺序:
- 用户提出仪表盘相关需求
- 第一步必须调用本 skill(
Skill jimubi-dashboard) - 在 skill 上下文中读取凭据、选择脚本、执行操作
按需加载指南
本 skill 采用分层加载:核心规则始终在上下文中,专题文档按需读取。
| 场景 | 读取文件 |
|---|---|
| 敲敲云(QQY)低代码应用仪表盘 | 核心规则已内联(识别条件/初始化/必填字段/工作流);完整 config 模板/UI组件配置/批量生成/按钮操作 → 读取 references/qqy-guide.md |
| QQY全组件仪表盘(30统计图表+7UI,一次生成) | 直接用 gen_qqy_all_comps.py(无需 Write 脚本):SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"; PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/gen_qqy_all_comps.py" API_BASE TOKEN --page-id PAGE_ID --app-id APP_ID --tenant-id TENANT_ID --form-code FORM_CODE [--form-name 表单名称] [--form-type design|online] |
| 需要示例/演示数据(用户未提供数据源) | references/api-dataset-examples.md(92条公开 mock API,按行业分类,直接用 dataset_ops.py create-api 创建) |
| 创建/绑定/修改数据集(SQL/API/文件) | references/dataset-guide.md(仅自定义脚本时需要;使用预置脚本时无需读取) |
| 多文件数据集(FILES)+ 图表 | 直接用 files_ops.py create-bind(无需 Write 脚本) |
| 创建 WebSocket 数据集 | references/dataset-guide.md「创建 WebSocket 数据集」章节 |
| 创建 JSON 数据集 + 图表 | 无需读文档;规则已内联:dataType:'json',数据放 querySql(禁止放 content),无需 dbSource/queryFieldBySql,直接 _request POST /add + batch-add --specs 绑定 |
| 多图表+联动批量生成(≥2个图表且需要联动) | 直接用 multi_chart_linkage.py |
| 从模板复制创建仪表盘 | 直接用 template_ops.py copy |
| 模板复制遇到问题时 | references/template-copy-guide.md |
| 地图组件(JAreaMap 等) | references/map-guide.md + 静态数据用 references/map-static-data.md |
| 创建数据源 + SQL数据集 + 图表 | datasource_ops.py create 创建数据源 → dataset_ops.py create-sql 创建数据集 → comp_ops.py batch-add --specs 绑定图表(在每个 spec 的 "config" 中传入 dataType:2/dataSetId/dataMapping 即可,视觉配置由 default_configs.json 自动提供)。仅当 SQL 含 FreeMarker / 需要 queryFieldBySql 回写时,才需写全流程自定义脚本。 |
| 自写 Java API 接口 + API 数据集 + 批量图表 | 参考 references/pitfalls.md「完整工作流:自写API接口」章节 |
| YApi Mock 系统 + API 数据集 | 直接用 yapi_ops.py create-mock(固定项目:proj_id=57,catid=1157,basepath=/claude) |
| 签名接口 / 数据源管理 / NoSQL 数据源 | references/signing-datasource-guide.md |
| 组件联动 / 钻取 | 直接用 linkage_ops.py |
| 组件外部链接跳转 | 直接用 link_ops.py |
| 字典翻译(jimu_dict) | references/dict-guide.md |
| 修改页面配置(背景色/背景图/风格/组件主题) | references/page-config-guide.md(仪表盘无水印功能,水印仅大屏专有) |
| 遇到奇怪问题时查阅 | references/pitfalls.md |
| 组件样式配置路径 | references/bi-comp-option-config.md(仅当 skill.md 中未列出目标组件时才读取;JStatsSummary/JCapsuleChart/JGauge/JProgress/JScrollBoard/JNumber 已内联在「常用组件配置路径速查」章节) |
| 完整组件类型清单 | references/bi-component-types.md(已内联在「图表查询与推荐」章节,一般无需再读取) |
| 新增组件默认尺寸/数据/option | references/core-configs/component-defaults.md |
| Online表单/设计器表单生成图表(dataType:4) | references/online-design-form-chart-guide.md |
| bi_utils 初始化 / 字段访问规则 | 已内联在「bi_utils 使用规则(强制)」章节,无需读取外部文件 |
| comp_ops.py 参数与数据绑定格式 | 已内联在「快捷操作:comp_ops.py」章节,无需读取外部文件 |
| linkage_ops.py 联动/钻取命令 | 已内联在「快捷操作:linkage_ops.py」章节,无需读取外部文件 |
| link_ops.py 外部链接 + 自定义JS | 已内联在「快捷操作:link_ops.py」章节,无需读取外部文件 |
| 踩坑速查 | 已内联在「核心踩坑速查」章节(~45条),优先查此处;极端复杂场景再读 references/pitfalls.md |
| 图库(图标/图片)管理 | 已内联在「图库管理」章节,无需读取外部文件 |
SQL数据集创建标准流程(强制)
触发条件:用户说"使用SQL数据集"、"增加SQL数据集"、"统计 xxx 表"、"生成图表"等涉及 SQL 数据集的任何场景,必须严格按以下四步执行,不得跳过第1步。
第1步:确认数据源(必须询问,禁止擅自选择,无任何例外)
⚠️ 无论使用任何方式创建 SQL 数据集,都必须先询问数据源,禁止直接执行。
执行步骤(强制):
- 先运行以下命令列出所有可用数据源
- 向用户展示列表,询问"请问使用哪个数据源?"
- 等待用户选择后,用选定的数据源 ID 继续执行
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/datasource_ops.py" list "<api_base>" "TOKEN"
第2步:根据业务场景自行编写SQL
- 用户指定数据源后,根据用户描述的业务场景,自行设计并编写合适的 SQL 语句
- 🚨 comp_ops.py add/batch-add 不支持
--create-sql/--sql-file/--ds-name/--db-source,这些参数不存在,使用会报unrecognized arguments - 推荐方式(普通SQL,无 FreeMarker 动态条件):
dataset_ops.py create-sql创建数据集,再用comp_ops.py batch-add --specs绑定图表;每个 spec 的"config"字段只需传dataType/dataSetId/dataMapping等数据绑定字段,视觉配置自动从default_configs.json取 - 全流程自定义脚本(仅限以下场景):SQL 含 FreeMarker 动态参数(
<#if>/${})、需要 queryFieldBySql 自动回写字段、或需要在同一脚本内串联复杂逻辑时,才用 Write 工具写入 Python 脚本执行(详见下方"全流程自定义脚本模板"章节) - ⚠️ "singleFile" 是文件数据集的 dataType 值(
dataType: 'singleFile',上传 Excel/CSV),与 SQL 数据集脚本模式无关,禁止把 SQL 场景的自定义脚本称为"singleFile 脚本"
第3步:创建SQL数据集
- 分组必须使用 "示例数据集"(
dataset_ops.py create-sql已内置--group "示例数据集"默认值)
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/dataset_ops.py" create-sql $API_BASE $TOKEN \
--name "数据集名称" --db-source "数据源ID" \
--sql "SELECT name, value FROM table GROUP BY name" \
--fields "name:String,value:Integer"
- 数据集创建完成后,必须执行查询解析验证确认数据正常返回
第4步:后续绑定操作(按需)
- 询问用户是否需要将数据集绑定到图表组件
- 如需要,优先使用
dataset_ops.py create-sql+comp_ops.py batch-add --specs组合;仅当场景复杂(FreeMarker/需字段回写)时才写全流程自定义脚本
执行效率规则(强制)
简单操作直接执行,禁止多余探索
对所有仪表盘操作,必须跳过以下步骤直接执行:
- 禁止启动 Explore 子代理去探索源码
- 禁止启动子代理去读 data.ts 默认配置(skill 文档已包含完整信息)
- 禁止读取 template-copy-guide.md(template_ops.py copy 已实现全部流程)
- 禁止使用预置脚本时读取 dataset-guide.md(
dataset_ops.py/comp_ops.py --dataset-name已封装全部逻辑) - 禁止执行预置脚本前先
--help查看用法(skill 文档已包含完整参数说明) - 禁止展示设计摘要等待确认(除非用户明确要求确认)
耗时目标:
| 操作类型 | 目标耗时 | 做法 |
|---|---|---|
| 单组件增/删/改/查 | ≤30s | comp_ops.py 一条命令(SKILL_REFS + 全路径执行,1轮完成) |
| 数据集 + 单组件 | ≤60s | singleFile 脚本(7步完整流程:/add → queryFieldBySql → /edit → getAllChartData → config → append → save_page) |
| 复合操作(数据集 + 多组件) | ≤60s | 并行 Bash 调用 |
| 模板复制创建仪表盘 | ≤60s | template_ops.py copy |
| 多图表+联动(≥2图+联动) | ≤10s | multi_chart_linkage.py 单脚本 |
反模式检查清单(出现任何一条就说明在浪费时间)
- 🚨 用
py -c或ls探索已知信息(default_configs.json 的路径、可用键名、组件类型等在 skill.md 和 map-guide.md 中已完整列出,禁止用探索命令去"验证",直接写脚本执行。探索命令还会引入$HOMEUnix 路径格式问题导致FileNotFoundError。) - 🚨 添加≥2个组件时用单独的
add并行执行(并行导致乐观锁冲突丢失组件!必须用batch-add --specs '[...]'一次保存) - 🚨
add命令后 chartData 为[](comp_ops.py 已从 default_configs.json 加载完整默认数据,出现空数据说明 default_configs.json 未被复制到工作目录) - 🚨 静态 chartData 禁止使用 comp_ops.py 兜底数据(default_configs.json 为空时 comp_ops.py 回落到内置占位数据:JBar→A/B/C/D/E、JStackBar→收入/支出,这些是虚构数据。必须从前端源码
data.ts(位于packages/dragEngine/components/jeecgComponents/data.ts)中读取各组件compConfig.chartData的真实值,通过每个 spec 加"config":{"chartData":[...]}字段,或 singleFile 脚本中调用comp_ops._build_comp_config(comp_type, title, {"chartData": json.dumps(real_data)})覆盖) - ⚠️ 创建 SQL 数据集时跳过询问数据源直接执行(无论使用哪种脚本,执行前必须先
datasource_ops.py list列出数据源,询问用户) - ⚠️ 用户已给出 API 地址时先用
--dataset-name探测或先添加静态组件(直接dataset_ops.py create-api→comp_ops.py add --dataset-name,2 轮完成) - ⚠️ singleFile 全流程脚本第4步(add_component)前漏写 query_page + 缓存 template(漏掉两行缓存代码时,
save_page将仪表盘所有已有组件永久清空) - ⚠️ singleFile 场景将"建数据集脚本"和"添加图表脚本"拆成两个(必须一个脚本完成全部流程)
- ⚠️ singleFile 场景用
comp_ops.py --dataset-name绑定图表(按字段数组顺序自动映射,导致图表显示错误数据) - ⚠️ 执行
py script.py时不加PYTHONIOENCODING=utf-8(Windows 默认 GBK 编码,中文必定乱码) - ⚠️ 自定义脚本用
cp bi_utils.py而非sys.path.insert(cp 路径硬编码,换机器失效,且需要清理。必须用sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))) - ⚠️ 预置脚本用
py comp_ops.py短名调用(PYTHONPATH 只解决import bi_utils,不解决脚本文件查找!py comp_ops.py在当前目录找脚本,必然报"No such file"。必须用全路径:SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"; PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/comp_ops.py" ...) - 🚨 create_page 后紧接 save_page(create_page 内部已保存一次,updateCount 变为1;随即 save_page 携带 updateCount=0 触发乐观锁报"仪表盘内容不是最新"。save_page 只在添加/修改组件后调用)
- ⚠️ Write 脚本时写占位符 TOKEN/API_BASE 再单独 Edit 更新(凭据已在上下文中,必须 Write 时直接填入最终值)
- ⚠️ 多图表+联动场景逐个调用 comp_ops.py + linkage_ops.py(用
multi_chart_linkage.py单脚本,节省约80%耗时) - ⚠️ 直接调用 bi_utils.add_xxx() + save_page() 添加组件到已有页面(会覆盖已有组件!必须先 query_page + 缓存 template)
- ⚠️ 写自定义脚本用 bash heredoc 而非 Write 工具(heredoc 含单引号必报错)
- ⚠️ 脚本中用拼音/英文替代中文字段名、组件名(如把"基础柱形图"写成
JiChu-ZhuXingTu,用户无法识别) - ⚠️ 用户未指定数据来源时擅自使用公开 mock API(必须先执行 Step 0.1 询问数据来源)
- ⚠️ 批量绑定数据集时 dataMapping.filed 写成字段名(
filed是语义槽位标签"维度"/"数值"/"分组",mapping才是字段名) - ⚠️ dataMapping 按数组索引顺序映射而非语义映射(必须按语义显式指定:单系列
[{维度→name},{数值→value}],多系列[{分组→type},{维度→name},{数值→value}]) - ⚠️ 仪表盘 size 字段用栅格单位(
config.size.width/height必须是像素:width = w×75, height = h×11) - 🚨 QQY全组件仪表盘需要从头 Write 脚本(直接用
gen_qqy_all_comps.py预置脚本,参数:--page-id --app-id --tenant-id --form-code,1轮完成,耗时<2s) - 🚨 QQY全组件生成前跳过字段确认直接执行脚本(禁止!必须先查询表单字段,以表格列出并给出推荐配置(维度/数值/分组),等用户确认后再执行 gen_qqy_all_comps.py)
- ⚠️ QQY dataType=4 组件缺少 compStyleConfig 或 analysis(前端
useChartBiz.ts读取这两个字段,缺少任意一个则 TypeError 白屏;必须包含compStyleConfig: {'summary': {'showTotal': False, 'showY': False, 'decimals': 0}}+analysis: {}) - ⚠️ QQY filter 缺少 conditionFields 字段(
filter对象必须包含conditionFields: [],否则前端"设置"弹窗报错) - 🚨 QQY seriesType 作用域:只有 JPivotTable + 4个地图 需要非空数组,其余26个统计图表必须是
[](✅正确:非分组图表seriesType:[];JPivotTable/JAreaMap/JBubbleMap/JHeatMap/JBarMap 用[{"series":"1","type":"bar"},...];❌错误:所有图表统一填非空数组,会导致前端.map is not a function崩溃) - ⚠️ QQY 仪表盘类(JGauge/JColorGauge/JAntvGauge)nameFields 放了字符串字段(仪表盘类 nameFields 必须为
[],只有 valueFields) - ⚠️ QQY 散点图 nameFields 用字符串维度字段(JScatter/JBubble 的 nameFields 必须是数值类型字段,否则散点图坐标轴无法渲染)
- 🚨 QQY commonOption 作用域:只有 4 个地图类型需要,其余26个统计图表禁止包含(✅正确:JAreaMap/JBubbleMap/JHeatMap/JBarMap 加 commonOption;❌错误:所有统计图表都加 commonOption,会引入不必要字段干扰渲染)
- 🚨 QQY JHeatMap commonOption 正确值(来自参考JSON):
heat:{blurSize:20,pointSize:15,maxOpacity:1},breadcrumb.textColor:'#000000',areaColor:{color1:'#f7f7f7',color2:'#fcc02e'},barColor:'#fff176',barColor2:'#fcc02e',inRange:{color:['#04387b','#467bc0']};❌错误:blurSize:13/pointSize:6/textColor:'#ffffff'/不同inRange配色 - 🚨 QQY JHeatMap 四项强制要求(①
visualMap.show:true——false 时报Heatmap must use with visualMap;②visualMap.seriesIndex:[1]——不是 [0];③commonOption必须含heat字段,blurSize:20,pointSize:15;④geo.roam:true) - ⚠️ QQY 地图 visualMap.seriesIndex 搞错(JAreaMap→[0]show:false;JBubbleMap→[1]show:false;JHeatMap→[1]show:TRUE;JBarMap→[0]show:false)
- 🚨 QQY option 坐标轴颜色禁用 #EEF1FA(大屏暗色在白底仪表盘看不清;禁止写 axisLabel.color/textStyle.color 覆盖,用默认色)
- 🚨 QQY JWordCloud/JTotalProgress option 必须为
{title,card}只需 title+card,无坐标轴(加坐标轴/series 反而崩溃);JRankingList 需要完整横向条形图 option(yAxis:{data:[],type:'category'}+xAxis:{type:'value'}+series:[{type:'bar'}]+grid:{containLabel:true},不能是{}) - 🚨 QQY DoubleLineBar yAxis 必须是双数组(
yAxis:[{type:'value'},{type:'value'}],单对象则第二轴缺失) - 🚨 QQY HorizontalBar 系 category 必须是 'HorizontalBar'(JHorizontalBar/JRankingList/JTotalProgress 的 category 写 'Bar' 则方向/样式全错)
- 🚨 QQY JPivotTable isGroup 必须为 True(False 时透视表不渲染行列分组)
- 🚨 QQY compStyleConfig showField 取值:
'all'=全部字段(用户选"全部"时),'fieldName'=指定字段,''=默认未选;columnFreeze 必须为 False(参考JSON权威);headerFreeze/unilineShow/lineFreeze 为 True - 🚨 QQY option.card 必须含 headColor:'#FFFFFF';option.title.text 必须设为组件显示名称(缺 headColor 导致卡片头色异常;title.text 为空则图表无标题)
- 🚨 QQY assistYFields/assistTypeFields 作用域:只有 JPivotTable + 4个地图 需要填充,其余26个统计图表必须是
[](✅正确:普通图表assistYFields:[];❌错误:所有图表统一填 [数值字段]) - 🚨 QQY JGauge 与 JColorGauge/JAntvGauge option 结构不同:JGauge 需要
series:[{min:0,data:[],max:100,axisTick:{lineStyle:{color:'#eee'},show:true},detail:{formatter:'{value}'},type:'gauge'}];JColorGauge/JAntvGauge 只需{title, card}(无 series) - 🚨 QQY JBarMap geo 必须含 aspectScale:0.96 + areaColor:'#37805B' + roam:true(来自参考JSON;其余地图 areaColor 为空字符串,JHeatMap/JBarMap roam:true,JAreaMap/JBubbleMap roam:false)
- 🚨 QQY JPivotTable pivotTable 子配置必须动态包含所有 num_fields(
controlList/unitList必须对每个数值字段建一个条目;只用 VAL[0] 则多值字段的汇总列缺失) - 🚨 QQY filterField 必须在表单字段前预置5个系统字段(create_by/update_by/update_time/create_time/bpm_status,缺失则筛选面板不完整)
仪表盘特征
- 布局:24 列栅格,坐标和尺寸单位为栅格单位(如 x=0, y=0, w=6, h=17)
- 背景色:支持,字段
backgroundColor(DragEngineDef.vue第68行) - 背景图:支持,字段
backgroundImage - 风格(style):支持
transparent/light/dark/default(DragEngineDef.vue第50行) - 组件主题(theme):支持
default/gray/green/red/blue/dark(DragEngineDef.vue第58行) - 水印:❌ 不支持(水印仅大屏专有,仪表盘无此功能)
- 卡片头:图表组件的
card.title应留空(标题由 EChartsoption.title显示),避免标题重复 - 颜色体系:白底
#FFFFFF、深灰标题#464646、浅灰轴标签#909198、浅灰网格#F3F3F3
仪表盘栅格布局规则
| 组件类型 | 推荐 w | 推荐 h | 说明 |
|---|---|---|---|
| JNumber | 6 | 17 | 数字卡片,4 个一行正好 24 列 |
| JGrowCard/JSimpleCard | 6-8 | 17-22 | 统计增长卡片 |
| JLine/JBar/JSmoothLine | 12-14 | 28-35 | 图表,通常半宽或更宽 |
| JPie/JRing/JRose | 10-12 | 28-35 | 饼图/环形图 |
| JHorizontalBar | 12 | 28-35 | 横向柱状图 |
| JCommonTable | 12 | 30-40 | 数据表格 |
| JList | 12 | 30-40 | 数据列表 |
| JGauge | 6-8 | 25-30 | 仪表盘表盘 |
| JProgress/JCustomProgress | 8-12 | 20-28 | 进度条/进度图 |
布局原则:
- 总宽度 24 列,组件 w 之和不要超过 24
- 第一行通常放 4 个 JNumber(w=6×4=24)
- 第二行放图表组合(如 JLine w=14 + JPie w=10 = 24)
- 第三行放表格/排行等
前置条件
用户必须提供:
- API 地址:JeecgBoot 后端地址(如
https://api3.boot.jeecg.com) - X-Access-Token:JWT 登录令牌(从浏览器 F12 获取)
敲敲云(QQY)仪表盘模式专题
本章节专门处理低代码应用(敲敲云)模式下的仪表盘。 如果用户只是做普通仪表盘,跳过本章节。
识别条件(满足任一即进入 QQY 模式)
- 用户提及"敲敲云"、"低代码应用"、"应用仪表盘"、"应用内仪表盘"
- 用户提供了
appId(低代码应用 ID)和tenantId(租户 ID) - 操作上下文是在低代码应用(
/myapp/{appId}/...路由)内的仪表盘 - 用户说"给某应用创建仪表盘"、"在应用里加一个图表"
QQY 模式 vs 标准仪表盘核心区别
| 特性 | 标准仪表盘 | QQY 仪表盘 |
|---|---|---|
isLowApp |
前端标识,不存库 | 前端标识,不存库(禁止写入数据库,仅前端引擎据此切换至 DragEngineQqyun) |
| 组件库来源 | menuData |
qqyMenuData(不含 JBreakRing 等) |
| 主要数据来源 | SQL/API 数据集(dataType=2) | 设计器/Online 表单(dataType=4) |
| 额外前置条件 | 无 | appId(应用ID)+ tenantId(租户ID) |
| HTTP 附加头 | 无 | X-Low-App-ID: {appId} + X-Tenant-Id: {tenantId} |
| 仪表盘归属 | 系统级,无应用关联 | 应用级,lowAppId 字段关联到具体应用 |
| 数据查询接口 | getAllChartData |
getTotalData(QQY 统计表单数据) |
| 数据集管理 | 前端可见 | 隐藏,用户不感知 |
| 按钮操作 | 无特殊绑定 | 支持创建记录/打开视图/调用业务流程等 5 种 |
QQY 模式额外前置条件
用户在标准前置条件基础上,还必须提供:
3. appId(低代码应用 ID):从页面 URL /myapp/{appId}/... 或应用管理中获取
4. tenantId(租户 ID):从系统设置→租户管理中获取,或询问用户
若用户未提供 appId/tenantId,必须先询问,不得用占位符代替。
QQY 模式脚本初始化(强制)
QQY 模式下所有脚本必须在 init_api 后立即设置额外请求头,同时创建页面时必须在 body 中显式传入 lowAppId,确保应用归属正确保存到数据库:
import json, time
import bi_utils
API_BASE = '<api_base>'
TOKEN = 'your-token'
APP_ID = '应用ID' # 低代码应用ID(必填)
TENANT_ID = '1' # 租户ID(必填)
PAGE_ID = '已有页面ID' # 或稍后调用 create_page 获取
# QQY 模式初始化(必须设置 extra_headers)
bi_utils.init_api(API_BASE, TOKEN, extra_headers={
'X-Low-App-ID': APP_ID,
'X-Tenant-Id': str(TENANT_ID),
})
⚠️ 创建页面时必须在 body 中传
lowAppId(强制):page_resp = bi_utils._request('POST', '/drag/page/add', data={ 'name': '仪表盘名称', 'style': 'default', 'lowAppId': APP_ID, # 必须显式传入,确保存库 # ❌ 禁止传 isLowApp:这是前端标识,不存数据库 })标准仪表盘创建时不传
lowAppId。
证据:
DragEngine.vueonMounted 中执行localStorage.setItem(ConfigEnum.DRAG_APP_ID, props.lowAppId),请求拦截器request.js中:config.headers[ConfigEnum.LOW_APP_ID] = localStorage.getItem(ConfigEnum.DRAG_APP_ID)后端OnlDragPageController.java:String lowAppId = TokenUtils.getLowAppIdByRequest(request)→ 写入onlDragPage.setLowAppId(lowAppId)
QQY 仪表盘列表查询
🚨 强制规则:用户未提供 appId 时,必须先询问,禁止自行猜测或遍历已知 appId
正确流程:
- 用户说"在某仪表盘中操作"但未给 appId → 先问:"请提供该应用的 appId(可从浏览器 URL
/myapp/{appId}/...获取)"- 拿到 appId 后,优先通过应用菜单接口查找仪表盘(可按名称精确定位 pageId)
- 确认 pageId 后再执行操作
✅ 推荐方式:通过应用菜单查找仪表盘(按名称定位 pageId)
# 查询应用菜单,按名称找到仪表盘的 pageId(menuUrl 字段)
resp = bi_utils._request('GET', '/online/lowAppMenu/list',
params={'appId': APP_ID, 'pageSize': 100})
records = resp.get('result', {}).get('records', []) or []
for m in records:
# type='drag' 为仪表盘菜单项,menuUrl 即为 pageId
print(m['id'], m.get('type'), m.get('menuName'), m.get('menuUrl'))
# 示例输出:
# 2047251681335025666 | drag | 销量分析 | 1207230587321589760
# ↑名称 ↑这就是 PAGE_ID
备用方式:通过 page/list 过滤(结果需二次验证 lowAppId)
# ⚠️ page/list 接口不精确过滤,返回结果混有其他应用页面,需手动校验 lowAppId
result = bi_utils._request('GET', '/drag/page/list', params={
'lowAppId': APP_ID,
'pageNo': 1,
'pageSize': 50
})
pages = result.get('result', {}).get('records', [])
# 必须二次过滤,排除 lowAppId 不匹配的页面
pages = [p for p in pages if p.get('lowAppId') == APP_ID]
for p in pages:
print(p['id'], p['name'])
QQY 可用组件(快速参考)
统计图表(30个):JBar, JStackBar, JMultipleBar, JNegativeBar, JHorizontalBar, JRankingList, JTotalProgress, JLine, JArea, JMultipleLine, DoubleLineBar, JWordCloud, JPie, JRing, JRose, JFunnel, JPyramidFunnel, JRadar, JCircleRadar, JColorGauge, JGauge, JAntvGauge, JNumber, JScatter, JBubble, JPivotTable, JAreaMap, JBubbleMap, JHeatMap, JBarMap
❌ 禁止添加:JDynamicBar, JMixLineBar, JSmoothLine, JProgress, JCommonTable, JList, JGrowCard, JFlyLineMap 等(不在 qqyMenuData 中)
UI/功能组件(7个):JCustomButton(按钮), JText(文本), JFilterQuery(查询条件), JCarousel(轮播图,需绑定imgupload字段), JDragEditor(富文本), JIframe(嵌入URL), JCurrentTime(实时日期)
❌ 禁止添加:JTabs, JGrid, JImg, JCalendar, JWaitMatter, JRadioButton 等
dataType=4 必填字段(QQY 统计图表核心)
每个 QQY 统计图表 config 必须包含:
dataType: 4+formType/formId/formName/tableName/appId/appTypenameFields/valueFields/typeFields/sorts/filter/filterField(含filter.conditionMode:"and"+filter.conditionFields:[])compStyleConfig(含summary/showUnit/assist完整结构)analysis(含showData:1, isRawData:True, showMode:1, trendType:'1')- 笛卡尔坐标图:
option.series:[{type:'bar/line/scatter'}]+ xAxis/yAxis + grid chart:{category,subclass,isGroup}+seriesType:[](JPivotTable/地图除外)
完整 config 模板、UI组件配置、批量生成流程、按钮操作类型:见
references/qqy-guide.md
QQY 仪表盘创建完整工作流
Step 1: 确认 appId + tenantId(必须询问用户)
Step 2: 确认仪表盘名称
Step 3: 在 bi_utils.init_api 中设置 extra_headers
Step 4: 调用 /drag/page/add 创建页面,body 中必须显式传 lowAppId: APP_ID(不传 isLowApp)
Step 5: 添加每个统计图表前,必须执行【四步询问流程】(见下方)
Step 6: 将仪表盘菜单归入目标分组(见下方「QQY 仪表盘菜单归组」章节)
Step 7: 创建完成后输出仪表盘 ID 和分享地址(格式:{前端域名}:{端口}/drag/share/{appId}/{pageId})
QQY 仪表盘菜单归组(创建后必须执行)
QQY 仪表盘页面创建后,其对应的应用菜单项 parentId 默认为空(不在任何分组下),必须手动调用接口将其归入目标分组,否则在低代码应用侧边栏中无法在分组下看到该仪表盘。
Step 1:查询应用菜单,找到目标分组 ID
⚠️ 必须用
appId参数过滤,用lowAppId参数无效(会返回所有应用的菜单)
r = requests.get(f'{API_BASE}/online/lowAppMenu/list', headers=HEADERS,
params={'appId': APP_ID, 'pageSize': 100})
records = r.json().get('result', {}).get('records', []) or []
# 找 type='group' 的分组,以及 type='drag' 的仪表盘菜单项(parentId 为空即是待归组的)
for m in records:
if m.get('appId') == APP_ID:
print(m['id'], m['type'], m['menuName'], m.get('parentId'))
Step 2:调用 edit 接口设置 parentId
body = {
'id': MENU_ID, # 仪表盘菜单项 ID(type='drag' 的那条)
'parentId': GROUP_ID, # 目标分组 ID(type='group')
'menuName': '仪表盘名称',
'type': 'drag',
'menuUrl': PAGE_ID, # 仪表盘页面 ID
'appId': APP_ID,
'orderNum': 4,
}
r = requests.put(f'{API_BASE}/online/lowAppMenu/edit', headers=HEADERS, json=body)
# {"success":true,"message":"编辑成功!"} 即为成功
注意:请求头必须包含
X-Low-App-ID和X-Tenant-Id,否则鉴权失败。
🚨 QQY 统计图表四步询问流程(强制,每个统计图表都必须执行)
每次在 QQY 仪表盘中添加任意一个统计图表(30个范围内),必须严格执行以下四步,禁止自行假设表单或字段:
Step 0:询问使用当前应用还是其他应用的表单
询问用户:"请问使用当前应用下的表单,还是其他应用下的表单?"
- 当前应用 → 用当前 APP_ID 继续 Step A
- 其他应用 → 询问"请提供应用名称或应用ID",等待用户提供后继续
Step A:同时查询普通表单和聚合表,分两组展示 → 询问用户选择
# 同时调用两个接口(携带 X-Low-App-ID 头):
GET /desform/api/list/options?appId={APP_ID} # 普通设计器表单
GET /drag/onlDragTableRelation/list?pageSize=20 # 聚合表
# 向用户分两组展示(对应前端 FormSelectModal 两个 Tab):
# 【表单(普通)】
# | formCode | 表单名称 | type |
# | ding_dan_guan_li_oaf0 | 订单管理 | design |
#
# 【聚合表】
# | id | 聚合表名称 | 类型标签 |
# | 1207232765004226560 | [聚合] 测试 | aggregation |
# (类型标签判断:relationForms.formType=='aggregation' → '[聚合工厂]',否则 '[聚合]')
#
# 询问:"请问使用哪个表单?(请指明普通表单 / 聚合表)"
# 等待用户选择后继续
Step B:查询并展示字段 → 询问用户选择维度/数值字段
# 根据用户选择分两种情况:
# ① 普通表单(type=design):
GET /desform/api/fields/{formCode}
# result 是 dict,字段列表在 result['fields']
# 跳过 file-upload 类型字段
# ② 聚合表(type=aggregation):
GET /drag/onlDragTableRelation/getFields/{aggregationId}
# result 直接是字段数组,计算字段格式:{"title":"总额","type":"number","value":"总额fc37c"}
# 跳过 file-upload/imgupload/location 类型字段
# 向用户展示字段列表(字段名 + 显示名 + 控件类型),询问:
# "请选择要显示的字段:
# - 维度字段(nameFields,文字/选项类)
# - 数值字段(valueFields,数字/金额类)"
# 等待用户确认后继续
Step C:按用户选定的表单 + 字段,构建 dataType=4 完整 config 创建图表
普通表单与聚合表的 config 关键字段差异:
| 字段 | 普通表单(design) | 聚合表(aggregation) |
|---|---|---|
type |
'design' |
'aggregation' |
formType |
'design' |
'design'(保持不变) |
formId |
formCode(如 ding_dan_guan_li_oaf0) |
聚合表 id(如 1207232765004226560) |
tableName |
formCode | 聚合表 id(与 formId 相同) |
formName |
表单显示名 | [聚合] 聚合表名(如 [聚合] 测试) |
| filterField 来源 | /desform/api/fields/{formCode} |
/drag/onlDragTableRelation/getFields/{id} |
聚合表 config 示例(以 JBar 为例):
comp_config = {
'dataType': 4,
'formType': 'design', # 聚合表 formType 仍为 'design'
'formId': '1207232765004226560', # 聚合表 id
'formName': '[聚合] 测试',
'tableName': '1207232765004226560',# 与 formId 相同
'type': 'aggregation', # 🚨 关键区别:type='aggregation'
'appId': APP_ID,
'appType': 'current',
'nameFields': [{'fieldName': 'input_xxx', 'fieldTxt': '名称', 'fieldType': 'string',
'widgetType': 'input', 'fieldShow': True, 'options': [], 'customDateType': ''}],
'valueFields': [{'fieldName': '总额fc37c', 'fieldTxt': '总额', 'fieldType': 'number',
'widgetType': 'number', 'fieldShow': True, 'groupField': '', 'options': [], 'customDateType': ''}],
'typeFields': [], 'assistYFields': [], 'assistTypeFields': [], 'calcFields': [],
'seriesType': [],
'sorts': {'name': '', 'type': ''},
'filter': {'queryField': 'create_time', 'queryRange': 'all',
'conditionMode': 'and', 'conditionFields': [], 'customTime': []},
'filterField': SYSTEM_FIELDS + form_filter_fields, # 系统字段 + 聚合表字段
'chart': {'category': 'Bar', 'subclass': 'JBar', 'isGroup': False},
'turnConfig': {'url': ''}, 'jsConfig': '', 'drillData': [],
'authFieldShowResult': [], 'timeOut': 0, 'chartData': '[]',
'background': '#FFFFFF', 'borderColor': '#E8E8E8',
'size': {'height': 385},
'compStyleConfig': DEFAULT_COMP_STYLE_CONFIG,
'analysis': DEFAULT_ANALYSIS,
'option': {
'card': {'title': '', 'size': 'default', 'headColor': '#FFFFFF',
'textStyle': {'color': '#464646', 'fontSize': 16, 'fontWeight': 'bold'},
'extra': '', 'rightHref': ''},
'title': {'show': True, 'text': '基础柱形图'},
'series': [{'type': 'bar'}],
'xAxis': {'type': 'category'},
'yAxis': {'type': 'value'},
'grid': {'top': 70, 'bottom': 60, 'left': 50, 'right': 30, 'containLabel': True},
'tooltip': {'trigger': 'axis'},
'legend': {'show': True},
},
}
禁止行为:
- ❌ 禁止跳过 Step 0,不询问应用来源直接查询当前应用表单
- ❌ 禁止只展示普通表单,忽略聚合表(前端有两个 Tab,AI 流程必须还原)
- ❌ 禁止自行推断"复用页面已有组件的表单"
- ❌ 禁止跳过询问、直接用某个表单或字段
- ❌ 即使应用只有一个表单,也要展示让用户确认
- ❌ 禁止使用 dataType=1 静态数据兜底
- ❌ 聚合表 filterField 禁止调用 /desform/api/fields,必须用 /drag/onlDragTableRelation/getFields/{id}
QQY 也支持 dataType=2(SQL/API 数据集),只需额外携带 appId/tenantId 头。
QQY 模式不支持的功能
- ❌ 水印(仅大屏专有)
- ❌ JBreakRing、JPyramid3D 等大屏专属组件(不在 qqyMenuData 中)
交互流程
Step 0: 解析用户需求
| 信息 | 默认值 | 示例 |
|---|---|---|
| 页面名称 | 用户指定 | "运营数据看板" |
| 主题 | default | default |
| 组件列表 | 从描述中解析 | 用户总数(数字)、增长趋势(折线)、来源分布(饼图) |
Step 0.1: 数据来源确认(强制,用户未明确指定时必须询问)
触发条件: 用户没有明确说明数据来自哪里(没有给出接口地址、没有指定数据集、没有说用 SQL/mock/自己写代码),则必须先问用户以下两个问题,不得擅自假设或跳过:
问题一:接口来源 使用 mock 系统 还是 自己编写代码?
- mock 系统:请提供 mock 服务地址 + 账号密码(如 YApi)
- 自己编写代码:请提供代码存放路径(Java Controller 文件全路径)
问题二:接口需要实现什么业务需求? 描述各组件要展示的数据内容
可跳过询问、直接执行的情况:
- 用户已明确说"使用 mock 系统"并提供了地址和账号
- 用户已明确说"写接口"并提供了文件路径
- 用户指定了已有数据集名称或 SQL 数据源
- 任务不涉及数据集创建(如纯样式修改、组件位移、删除等)
Step 0.5: 模板匹配(优先使用模板布局)
生成整个仪表盘时,必须先匹配模板,复用已有布局。 这是最优先的步骤,能确保生成的仪表盘布局专业、美观。
模板目录:references/templates/default/(29 个仪表盘模板 JSON)
匹配流程:
- 根据用户需求关键词搜索模板:将用户描述的行业/场景与模板名称进行语义匹配
| 用户需求关键词 | 推荐模板 |
|---|---|
| 销售/订单/电商/运营 | 产品销售数据、某电商公司销售运营看板、某连锁饮品销售看板 |
| 招聘/HR/人事 | 公司年度招聘看板 |
| 金融/银行/封控 | 金融封控数据展示、示例_乡村振兴普惠金融服务 |
| 仓储/库存/物料 | 库存管理可视化大屏 |
| 医院/医疗/医美 | 示例_医院综合数据统计、医美行业网络关注度 |
| 旅游/景区/客流 | 示例_旅游数据监控 |
| 社区/物业/消防 | 示例_智慧社区、物业消防巡检状态 |
| 生产/制造/车间 | 车间生产管理 |
| 门户/首页/工作台 | 企业门户、流程门户、示例_首页 |
| 消费者/权益/投诉 | 消费者权益保护 |
| 数据分析/统计/报表 | 示例_数据分析、示例_数据表格、示例_统计近十日的登陆次数 |
| 查询/联动/筛选 | 示例_查询_联动、示例_日期范围查询、示例_钻取 |
| 通用/综合/看板 | 示例_智能大数据、示例_全组件、示例_首页 |
找到匹配模板 → 使用「模板复制方式」创建仪表盘(参见下方"备选方式:从模板复制创建仪表盘"章节),保留模板的布局和装饰,仅替换业务数据和标题文字
找不到匹配模板 → 随机选择一个通用模板作为布局基础(推荐选择:
示例_智能大数据、示例_首页、示例_全组件),同样保留布局和装饰,替换业务数据
重要:只有在用户明确要求"不使用模板"或"从零创建"时,才跳过模板匹配,直接使用 bi_utils 默认组件函数逐个添加。
图表查询与推荐(用户询问或需求不明确时)
场景一:用户询问"可以使用什么图表"
触发条件:用户问"有哪些图表"、"支持什么图表"、"可以用什么图表"、"图表有哪些类型"等。
处理方式:直接输出以下完整图表分类表格(无需读取任何文档,无需执行任何脚本):
| 分类 | 图表名称 | compType |
|---|---|---|
| 柱形图 | 基础柱形图 | JBar |
| 堆叠柱形图 | JStackBar | |
| 动态柱形图 | JDynamicBar | |
| 象形图 | JPictorialBar | |
| 基础条形图 | JHorizontalBar | |
| 背景柱形图 | JBackgroundBar | |
| 对比柱形图 | JMultipleBar | |
| 正负条形图 | JNegativeBar | |
| 折柱图 | JMixLineBar | |
| 双轴图 | DoubleLineBar | |
| 饼状图 | 饼图 | JPie |
| 南丁格尔玫瑰图 | JRose | |
| 折线图 | 基础折线图 | JLine |
| 平滑曲线图 | JSmoothLine | |
| 阶梯折线图 | JStepLine | |
| 面积图 | JArea | |
| 对比折线图 | JMultipleLine | |
| 进度图 | 基础进度图 | JCustomProgress |
| 进度图 | JProgress | |
| 仪表盘 | 基础仪表盘 | JGauge |
| 多色仪表盘 | JColorGauge | |
| 环形图 | 饼状环形图 | JRing |
| 散点图 | 普通散点图 | JScatter |
| 气泡图 | JBubble | |
| 漏斗图 | 普通漏斗图 | JFunnel |
| 金字塔漏斗图 | JPyramidFunnel | |
| 雷达图 | 普通雷达图 | JRadar |
| 圆形雷达图 | JCircleRadar | |
| 地图 | 区域地图 | JAreaMap |
| 散点地图 | JBubbleMap | |
| 飞线地图 | JFlyLineMap | |
| 柱形地图 | JBarMap | |
| 热力地图 | JHeatMap | |
| 柱形排名地图 | JTotalBarMap | |
| 时间轴飞线地图 | JTotalFlyLineMap | |
| 表格/列表 | 数据表格 | JCommonTable |
| 透视表 | JPivotTable | |
| 数据列表 | JList | |
| 统计/数字 | 数值 | JNumber |
| 统计卡片(增长) | JGrowCard | |
| 简洁卡片 | JSimpleCard | |
| 首页功能 | 待办事项 | JWaitMatter |
| 项目列表 | JProjectCard | |
| 快捷导航 | JQuickNav | |
| 最新动态 | JDynamicInfo | |
| 交互 | 查询条件 | JFilterQuery |
| 自定义按钮 | JCustomButton | |
| 选项卡切换 | JTabs | |
| 按钮组 | JRadioButton | |
| 辅助 | 图片 | JImg |
| 文本 | JText | |
| 轮播图 | JCarousel | |
| 富文本 | JDragEditor | |
| Iframe | JIframe | |
| 日历 | JCalendar | |
| 多视图日历 | JMultiViewCalendar | |
| 当前时间 | JCurrentTime | |
| 栅格布局 | JGrid | |
| 自定义ECharts | JCustomEchart | |
| Online/表单 | Online表单 | online |
| 设计器表单 | design |
场景二:用户图表需求不明确时给出推荐
处理方式:根据用户数据类型/业务场景,给出 3-5 个建议:
| 数据类型/业务场景 | 推荐图表 | 理由 |
|---|---|---|
| 占比/构成分析 | JPie、JRing、JRose | 直观展示各部分在整体中的比例 |
| 趋势/时序变化 | JLine、JSmoothLine、JArea | 反映随时间变化的走势 |
| 分类对比 | JBar、JHorizontalBar、JMultipleBar | 比较不同类别的数值大小 |
| 多系列对比 | JMultipleLine、JMultipleBar、JStackBar | 同时展示多个维度的数据 |
| 完成率/进度 | JProgress、JGauge、JLiquid、JRingProgress | 展示目标达成程度 |
| 排行/Top N | JScrollRankingBoard、JCapsuleChart、JDynamicBar | 突出排名先后顺序 |
| KPI/核心指标 | JNumber、JStatsSummary、JCountTo | 大字号展示关键数字 |
| 地理分布 | JAreaMap、JBubbleMap、JBarMap | 展示地理位置相关数据 |
| 转化漏斗 | JFunnel、JPyramidFunnel | 展示各环节转化率递减 |
| 多维综合评估 | JRadar、JBubble | 多维度综合打分或分布 |
| 数据列表/明细 | JScrollBoard、JCommonTable、JScrollTable | 展示多条明细数据 |
推荐话术: 列出3-5个图表名+compType+一句话原因,末尾"请选择,我将立即创建"。
Step 1: 识别组件并选择类型
用户说组件名时直接查上方「图表查询与推荐」章节的表格获取 compType,禁止 Grep 搜索源码。
常用仪表盘组件速查:
| 用户描述关键词 | 组件 component | 说明 |
|---|---|---|
| 数字/KPI/指标/总数 | JNumber |
数字指标卡(带卡片头) |
| 统计卡片/增长卡片 | JGrowCard |
带增长率的统计卡片 |
| 柱状图 | JBar |
基础柱状图 |
| 横向柱状图 | JHorizontalBar |
水平柱状图 |
| 折线图/趋势 | JLine |
折线图 |
| 曲线图 | JSmoothLine |
平滑曲线 |
| 柱线混合 | JMixLineBar |
柱状+折线混合 |
| 饼图 | JPie |
饼图 |
| 环形图 | JRing |
环形图 |
| 玫瑰图 | JRose |
南丁格尔玫瑰图 |
| 表盘 | JGauge |
仪表盘表盘 |
| 进度条 | JProgress |
进度条 |
| 雷达图 | JRadar |
雷达图 |
| 漏斗图 | JFunnel |
漏斗图 |
| 地图 | JAreaMap |
区域地图 |
| 数据表格 | JCommonTable |
数据表格 |
| 数据列表 | JList |
数据列表 |
| 日历 | JCalendar |
日历组件 |
| 待办/工作台 | JWaitMatter |
待办事项列表 |
| 查询/筛选 | JFilterQuery |
查询条件组件 |
Step 2: 展示设计摘要并确认
跳过确认: 用户说「直接生成」/「不用确认」,或模板精确匹配,或同会话中已确认过。
需要确认时,展示如下摘要:
## 仪表盘设计摘要
- 页面名称:运营数据看板
- 主题:default
### 组件列表
| 序号 | 组件名称 | 组件类型 | 位置(x,y) | 尺寸(w×h) | 数据源 |
|------|---------|---------|-----------|----------|--------|
| 1 | 总用户数 | JNumber | (0,0) | 6×17 | 静态数据 |
| 2 | 今日活跃 | JNumber | (6,0) | 6×17 | 静态数据 |
| 3 | 用户增长趋势 | JLine | (0,17) | 14×35 | 静态数据 |
| 4 | 用户来源 | JPie | (14,17) | 10×35 | 静态数据 |
确认以上信息正确?(y/n)
快捷操作:全部预置脚本一览
脚本目录:<skill_base_dir>\references\scripts\(<skill_base_dir> 为 skill 加载时显示的 Base directory for this skill 路径)
| 脚本 | 功能 | 常用命令 |
|---|---|---|
comp_ops.py |
组件增删改查 | list, delete, edit, add, batch-add, move, switch-type |
page_ops.py |
页面配置(背景/主题) | info, set-bg, set-bgimg, set-theme, rename。rename 参数格式: py page_ops.py rename API_BASE TOKEN PAGE_ID --name "新名称"(--name 是命名参数)。delete: bi_utils._request('DELETE','/drag/page/delete',params={'id':PAGE_ID})。⚠️ 水印仅大屏有,仪表盘无此命令 |
dataset_ops.py |
数据集管理 | list, create-sql, create-api, test, delete, bind |
template_ops.py |
模板操作 | list, preview, search, copy |
linkage_ops.py |
联动/钻取配置 | show, add-linkage, remove-linkage, add-drill, remove-drill |
map_ops.py |
地图数据管理 | list, check, upload, add-map |
style_ops.py |
批量样式修改 | show-colors, set-title-color, set-palette, batch-edit |
datasource_ops.py |
数据源管理(含签名) | list, detail, create, test, delete, parse-sql。create 参数: --db(非 --db-name)、--user(非 --username);test 支持 --id 或 --name |
dict_ops.py |
字典管理 | list(无过滤参数), items --code <编码>(不是 --dict-code), create --name --code [--items "1=男,2=女"], add-item --code --value --text [--sort], deletebind。list 不支持按编码过滤,需过滤时直接调 /jmreport/dict/list?dictCode=xxx;delete 改用直接 API(见 dict-guide.md) |
files_ops.py |
文件数据集(singleFile/FILES) | create-bind(上传→建数据集→绑图表一体化,支持 --single 参数创建单文件数据集)。JOIN 模式必须传 --group-by <列名> --join-on <关联列> --agg <聚合列>;列名未知时先问用户 |
link_ops.py |
外部链接/自定义JS | set(外部链接), set-js(自定义JS), show(查看), remove(删除链接), remove-js(删除JS) |
yapi_ops.py |
YApi Mock 管理 | create-mock(必填 --title,不是 --name;--template single/multi/pie/gauge/table/bar_multi),list,delete。⚠️ 创建前必须先 list 查已有接口,复用勿重建 |
multi_chart_linkage.py |
批量图表+联动 | 单脚本完成多图+联动,节省80%耗时 |
proc_ops.py |
存储过程绑定 | bindcomp(创建SP+数据集+绑组件一体化)。前置:py -m pip install pymysql |
default_configs.json |
组件默认配置 | 所有组件默认 w/h/chartData/option,自定义脚本时加载 |
使用前准备(所有脚本通用):
# PYTHONPATH 让脚本内部 import bi_utils 成功;脚本文件本身必须用全路径,py 不会在 PYTHONPATH 中搜索脚本
# $HOME 在 Windows Git Bash / Mac / Linux 均可自动解析当前用户目录
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/脚本名.py" ...
快捷操作:comp_ops.py(增删改查)
⚠️ 添加/编辑/删除组件必须使用 comp_ops.py,严禁直接调用 bi_utils.add_xxx() + save_page()。 原因:bi_utils.add_component() 内部将
_page_components[page_id]初始化为空列表,save_page 时会用空列表覆盖页面已有的全部组件,造成不可恢复的数据丢失。
使用前准备:
# PYTHONPATH 让 import bi_utils 成功;脚本文件必须用全路径(py 不在 PYTHONPATH 中搜索脚本文件)
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
COMP_OPS="$SKILL_REFS/scripts/comp_ops.py"
# add/batch-add 命令自动从 SKILL_REFS/scripts/default_configs.json 读取默认配置
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" ...
核心命令(坐标为栅格单位,w 之和≤24):
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
COMP_OPS="$SKILL_REFS/scripts/comp_ops.py"
# 查看组件
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" list $API_BASE $TOKEN $PAGE_ID
# 删除组件
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" delete $API_BASE $TOKEN $PAGE_ID --name "组件名"
# 编辑组件属性(单属性)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" edit $API_BASE $TOKEN $PAGE_ID --name "组件名" --set "option.title.text=新标题"
# 编辑组件属性(多属性:每个属性一个 --set)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" edit $API_BASE $TOKEN $PAGE_ID --name "组件名" --set "option.showValue=true" --set "option.unit=个"
# 添加单个组件(静态数据,栅格坐标)⚠️ 严禁并行调用多个 add,会因乐观锁丢失组件
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" add $API_BASE $TOKEN $PAGE_ID --comp "JBar" --title "柱形图" --x 0 --y 0 --w 12 --h 30
# 批量添加多个组件(一次 save,彻底消除并发锁冲突)——添加≥2个组件时必须用此命令
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" batch-add $API_BASE $TOKEN $PAGE_ID --specs '[
{"comp":"JBar","title":"柱形图","x":0,"y":0,"w":12,"h":30},
{"comp":"JPie","title":"饼图","x":12,"y":0,"w":12,"h":30},
{"comp":"JProgress","title":"进度图","x":0,"y":30,"w":12,"h":28},
{"comp":"JCustomProgress","title":"自定义进度图","x":12,"y":30,"w":12,"h":28}
]'
# 创建SQL数据集+绑定图表:comp_ops.py add/batch-add 不支持直接创建数据集
# 推荐:先 dataset_ops.py create-sql,再 batch-add 传 "config" 绑定;复杂场景用全流程自定义脚本
# 移动/缩放组件
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" move $API_BASE $TOKEN $PAGE_ID --name "组件名" --x 0 --y 17
# 切换组件类型(保留公共配置:数据绑定/title/card/customColor/tooltip/legend/grid/xAxis/yAxis)
# --title 可选,不填则沿用原名;笛卡尔图互换时 xAxis/yAxis 结构自动保留(data 清空)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" switch-type $API_BASE $TOKEN $PAGE_ID --name "基础柱形图" --to "JLine"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" switch-type $API_BASE $TOKEN $PAGE_ID --id "组件i值" --to "JPie" --title "新标题"
四种数据模式:
| 模式 | 参数 | 说明 |
|---|---|---|
| 静态数据(默认) | 无额外参数 | 从 default_configs.json 加载默认配置 |
| 绑定已有数据集 | --config '{"dataType":2,"dataSetId":"...","dataMapping":[...]}' |
手动传完整 config JSON |
| 创建SQL数据集+绑定 | 不支持,先 dataset_ops.py create-sql 建集,再 batch-add 的 spec "config" 中传绑定字段 |
--create-sql 等参数均不存在 |
SQL数据集 + 批量图表推荐工作流(dataset_ops + batch-add)
适用场景:普通 SQL(无 FreeMarker 动态参数),字段手动已知。视觉配置由
default_configs.json自动提供,无需手写 option。
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
# Step 1: 创建 SQL 数据集(获取 DS_ID)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/dataset_ops.py" create-sql $API_BASE $TOKEN \
--name "每年大屏创建数量" --code "yearly_bigscreen_count" \
--db-source "数据源ID" \
--sql "SELECT YEAR(create_time) AS name, COUNT(*) AS value FROM onl_drag_page WHERE del_flag=0 GROUP BY YEAR(create_time)" \
--fields "name:String,value:Integer"
# Step 2: 验证数据集
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/dataset_ops.py" test $API_BASE $TOKEN --id "DS_ID"
# Step 3: 批量绑定图表(视觉配置自动取 default_configs.json,只需传数据绑定字段)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/comp_ops.py" batch-add $API_BASE $TOKEN $PAGE_ID --specs '[
{"comp":"JBar","title":"基础柱形图","x":0,"y":0,"w":12,"h":35,
"config":{"dataType":2,"dataSetId":"DS_ID","dataSetName":"每年大屏创建数量",
"dataSetType":"sql","dataSetApi":"SELECT ...","dataSetMethod":"GET","dataSetIzAgent":"1",
"dataMapping":[{"filed":"维度","mapping":"name"},{"filed":"数值","mapping":"value"}],
"fieldOption":[{"fieldName":"name","fieldTxt":"年份","fieldType":"String"},{"fieldName":"value","fieldTxt":"数量","fieldType":"Integer"}],
"dictOptions":{},"paramOption":[],"chartData":"[]"}},
{"comp":"JPie","title":"饼图","x":12,"y":0,"w":12,"h":35,
"config":{"dataType":2,"dataSetId":"DS_ID",...}}
]'
config 只需传数据绑定字段:
dataType/dataSetId/dataSetName/dataSetType/dataSetApi/dataSetMethod/dataSetIzAgent/dataMapping/fieldOption/dictOptions/paramOption/chartData。option/background/borderColor/size/w/h等视觉字段全部由default_configs.json自动补全,不要手写。
全流程自定义脚本模板(仅限复杂场景:FreeMarker SQL / 需 queryFieldBySql 自动回写)
⚠️ "singleFile" 是文件数据集的 dataType 值,SQL 场景的自定义脚本不要用此名称。 SQL 含特殊字符(
%、<、>、${})时必须通过 Write 工具写入 Python 脚本,禁止 bash 命令行传递。
# 全流程自定义脚本(SQL数据集 + 图表绑定,适用于含 FreeMarker 的复杂 SQL)
import sys, os
# 动态导入 bi_utils,跨机器兼容($HOME 自动解析当前用户目录)
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))
import json, time, random, hashlib, urllib.request
import bi_utils, copy
t0 = time.time()
API_BASE = '<api_base>'
TOKEN = '...'
PAGE_ID = '...'
DB_SOURCE_ID = '...'
bi_utils.API_BASE = API_BASE
bi_utils.TOKEN = TOKEN
# ===== Step 1: 缓存已有组件(防止覆盖) =====
page = bi_utils.query_page(PAGE_ID)
tmpl = page.get('template', [])
if isinstance(tmpl, str): tmpl = json.loads(tmpl)
bi_utils._page_components[PAGE_ID] = tmpl
# ===== Step 2: 获取或创建"示例数据集"分组 =====
groups_resp = bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup')
parent_id = '0'
for g in groups_resp.get('result', []):
if g.get('name') == '示例数据集' and g.get('dataType') is None:
parent_id = g.get('id', '0'); break
if parent_id == '0':
bi_utils._request('POST', '/drag/onlDragDatasetHead/addGroup', data={'groupName': '示例数据集'})
groups2 = bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup')
for g in groups2.get('result', []):
if g.get('name') == '示例数据集' and g.get('dataType') is None:
parent_id = g.get('id', '0'); break
# ===== Step 3: 创建SQL数据集 =====
SQL = "SELECT ... FROM ... GROUP BY ..."
FIELD_LIST = [
{'fieldName': 'name', 'fieldTxt': '维度', 'fieldType': 'String', 'izShow': 'Y', 'orderNum': 0},
{'fieldName': 'value', 'fieldTxt': '数值', 'fieldType': 'Integer', 'izShow': 'Y', 'orderNum': 1},
]
ds_resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '数据集名称', 'code': 'dataset_code',
'dataType': 'sql', 'dbSource': DB_SOURCE_ID,
'querySql': SQL, 'apiMethod': 'GET',
'parentId': parent_id,
'datasetItemList': FIELD_LIST, 'datasetParamList': [] # ⚠️ datasetItemList,不是 onlDragDatasetItemList
})
result = ds_resp.get('result', {})
DS_ID = result.get('id') if isinstance(result, dict) else result
# ===== Step 4: queryFieldBySql 解析字段并 edit 回写(需签名)=====
SECRET = 'dd05f1c54d63749eda95f9fa6d49v442a'
def get_sign(path):
"""X-Sign:URL参数 + SECRET(不含时间戳)"""
json_obj = {}
if '?' in path:
for kv in path.split('?', 1)[1].split('&'):
if '=' in kv: k, v = kv.split('=', 1); json_obj[k] = v
json_obj.pop('_t', None)
s = json.dumps(dict(sorted(json_obj.items())), ensure_ascii=False, separators=(',', ':')) + SECRET
return hashlib.md5(s.encode('utf-8')).hexdigest().upper()
def get_vsign(data, sign):
"""V-Sign:Body中的String字段 + sign + SECRET(不含时间戳)"""
j = dict(data) if data else {}; j['sign'] = sign
sp = {k: v for k, v in j.items() if v and isinstance(v, str)}
s = json.dumps(dict(sorted(sp.items())), ensure_ascii=False, separators=(',', ':')) + SECRET
return hashlib.md5(s.encode('utf-8')).hexdigest().upper()
def signed_post(path, data):
ts = str(int(time.time() * 1000))
xsign = get_sign(path); vsign = get_vsign(data, xsign)
body = json.dumps(data, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(API_BASE + path, data=body, headers={
'Content-Type': 'application/json;charset=UTF-8', 'X-Access-Token': TOKEN,
'X-TIMESTAMP': ts, 'X-Sign': xsign, 'V-Sign': vsign,
}, method='POST')
return json.loads(urllib.request.urlopen(req, timeout=30).read().decode('utf-8'))
fields_resp = signed_post('/drag/onlDragDatasetHead/queryFieldBySql',
{'sql': SQL, 'dbCode': DB_SOURCE_ID, 'paramArray': []})
# ⚠️ result 是 dict({fieldList:[...], paramList:[...]}),不是直接的列表
raw_fields = (fields_resp.get('result') or {})
if isinstance(raw_fields, dict): raw_fields = raw_fields.get('fieldList', [])
parsed_fields = [
{'fieldName': f['fieldName'], 'fieldTxt': f.get('fieldTxt', f['fieldName']),
'fieldType': f.get('fieldType', 'String'), 'izShow': 'Y', 'izWhere': 'N', 'izTotal': 'N', 'orderNum': i}
for i, f in enumerate(raw_fields)
]
if parsed_fields:
ds_full = bi_utils._request('GET', '/drag/onlDragDatasetHead/queryById', params={'id': DS_ID})
ds_obj = ds_full.get('result', {})
ds_obj['datasetItemList'] = parsed_fields
bi_utils._request('POST', '/drag/onlDragDatasetHead/edit', data=ds_obj)
# ===== Step 5: getAllChartData 验证 + 取 dictOptions =====
chart_data_resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/getAllChartData', data={'id': DS_ID})
data_list = chart_data_resp.get('result', {}).get('data', [])
dict_options = chart_data_resp.get('result', {}).get('dictOptions', {})
print(f'数据集返回 {len(data_list)} 条,示例: {data_list[:2]}')
field_option = [{'fieldName': f['fieldName'], 'fieldTxt': f['fieldTxt'], 'fieldType': f['fieldType']}
for f in (parsed_fields or FIELD_LIST)]
# ===== Step 6: 加载 default_configs.json,用业务数据覆盖(禁止手写 option)=====
import os
_cfg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'default_configs.json')
if not os.path.exists(_cfg_path): _cfg_path = os.path.join(os.getcwd(), 'default_configs.json')
with open(_cfg_path, encoding='utf-8') as f: defaults = json.load(f)
def build_chart_config(comp_type, ds_id, mapping, field_opt, dict_opts, sql):
cfg = copy.deepcopy(defaults.get(comp_type, {}))
cfg.update({
'dataType': 2, 'dataSetId': ds_id, 'dataSetName': '数据集名称',
'dataSetType': 'sql', 'dataSetApi': sql, 'dataSetMethod': 'GET', 'dataSetIzAgent': '1',
'dataMapping': mapping, 'fieldOption': field_opt,
'dictOptions': dict_opts, 'paramOption': [], 'chartData': '[]',
})
return cfg
single_mapping = [{'filed': '维度', 'mapping': 'name'}, {'filed': '数值', 'mapping': 'value'}]
cfg = build_chart_config('JBar', DS_ID, single_mapping, field_option, dict_options, SQL)
comp = {
'component': 'JBar', 'componentName': '基础柱形图',
'visible': True, 'i': f'{int(time.time()*1000)}_{random.randint(100000,999999)}',
'x': 0, 'y': 0, 'w': 12, 'h': 35, 'orderNum': 10, 'config': cfg
}
# ===== Step 7: 追加并保存 =====
bi_utils._page_components[PAGE_ID].append(comp)
bi_utils.save_page(PAGE_ID)
print(f'完成!耗时: {time.time()-t0:.1f}s')
print(f'预览: {API_BASE}/drag/page/view/{PAGE_ID}')
FreeMarker 语法规则(强制):
| 规则 | 正确写法 | 错误写法 |
|---|---|---|
| 参数判空 | <#if isNotEmpty(age)> |
<#if age?? && age?length gt 0> |
| 参数占位 | '${age}' |
#{age}#{} 是系统变量专用) |
| 系统变量 | #{sys.login_user} |
${sys.login_user} |
--sql-params 格式:paramName:paramTxt:defaultValue:dictCode(后三项可省略,多个逗号分隔)
自定义脚本添加图表的强制规则:
- 图表 config 必须从
default_configs.json深拷贝:json.loads(json.dumps(defaults['JBar'])),再覆盖动态数据字段 - 字典翻译用 jimu_dict:
/jmreport/dict/*API,不是/sys/dict/* - dictOptions 从
getAllChartData获取:创建数据集后调getAllChartData,将返回的dictOptions写入组件 config
快捷操作:linkage_ops.py(组件联动/钻取)
组件联动 = 点击源组件,将参数传递给目标组件的数据集查询参数,目标组件自动刷新数据。
使用前准备:
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
LINK_OPS="$SKILL_REFS/scripts/linkage_ops.py"
# 脚本文件必须用全路径;PYTHONPATH 只解决 import bi_utils,不解决脚本文件查找
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" ...
核心命令:
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
LINK_OPS="$SKILL_REFS/scripts/linkage_ops.py"
# 查看页面所有联动配置
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" show $API_BASE $TOKEN $PAGE_ID
# 添加联动(--mapping 格式:src=tgt,多个逗号分隔)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" add-linkage $API_BASE $TOKEN $PAGE_ID --source "源组件名" --target "目标组件名" --mapping "value=age"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" add-linkage $API_BASE $TOKEN $PAGE_ID --source "柱形图" --target "饼图" --mapping "name=name,value=keyword"
# 删除联动
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" remove-linkage $API_BASE $TOKEN $PAGE_ID --source "源组件名" --target "目标组件名"
# 添加钻取(自刷新下钻,--comp 为源组件,无 --target)
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" add-drill $API_BASE $TOKEN $PAGE_ID --comp "组件名" --mapping "name=year"
# 删除钻取
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" remove-drill $API_BASE $TOKEN $PAGE_ID --comp "组件名"
联动 vs 钻取核心区别:
| 特性 | 联动(add-linkage) | 钻取(add-drill) |
|---|---|---|
| 刷新对象 | 其他组件 | 自身(递归查询) |
| 参数 | --source + --target |
--comp(只有自己) |
| 支持回退 | 不支持 | 支持(图表左上角回退按钮) |
⚠️ 易错点(强制记忆):
| 错误写法 | 正确写法 | 说明 |
|---|---|---|
add-drill --source "A" --target "B" |
add-drill --comp "A" |
钻取无 --target,是自刷新不是跨组件 |
--mapping "value:age" |
--mapping "value=age" |
映射用 = 分隔,不是 : |
--mapping "a=b c=d" |
--mapping "a=b,c=d" |
多个映射用逗号分隔 |
QQY dataType=4 图表钻取配置(标准流程,1轮完成)
触发场景:用户说"给敲敲云/应用仪表盘中的某图表增加钻取配置"
核心结论(验证来源:2026-04-17 实操):
- QQY dataType=4 图表同样使用
drillData存钻取映射,与 dataType=2 机制一致 --mapping的 target 必须是 nameFields[].fieldName(表单字段的 model 值),不是 SQL 参数名- 点击图表后,前端用
params.name(ECharts 点击事件的 name)匹配 drillData,过滤到只显示对应 nameField 值的数据
标准执行步骤(共2轮):
步骤1:查询图表配置,取 nameFields[0].fieldName
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/comp_ops.py" list API_BASE TOKEN PAGE_ID
# 再用 py -c 取 config 中的 nameFields,读取 fieldName
步骤2:写入钻取配置
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/linkage_ops.py" add-drill API_BASE TOKEN PAGE_ID \
--comp "图表名" --mapping "name=<nameFields[0].fieldName>"
快速参考(直接取 nameFields[0].fieldName 写入 mapping 的命令):
# 一次性取 nameFields 字段名 + 写入钻取(推荐:合并为同一命令链)
PYTHONIOENCODING=utf-8 py -c "
import sys, json
sys.path.insert(0, '.')
import bi_utils
bi_utils.init_api('API_BASE', 'TOKEN')
page = bi_utils.query_page('PAGE_ID')
tmpl = page.get('template', [])
if isinstance(tmpl, str): tmpl = json.loads(tmpl)
for comp in tmpl:
if comp.get('componentName') == '目标图表名':
cfg = comp.get('config', {})
if isinstance(cfg, str): cfg = json.loads(cfg)
nf = cfg.get('nameFields', [])
if nf: print('nameField:', nf[0]['fieldName'])
break
"
# 输出 nameField: input_xxxx_xxxx 后,直接执行:
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/linkage_ops.py" add-drill API_BASE TOKEN PAGE_ID --comp "目标图表名" --mapping "name=input_xxxx_xxxx"
⚠️ QQY 钻取的 mapping target 是表单字段名(不是语义名):
| 图表数据类型 | mapping target 填什么 | 示例 |
|---|---|---|
| dataType=2(SQL数据集) | SQL FreeMarker 参数名(如 year、category) |
name=year |
| dataType=4(QQY表单) | nameFields[0].fieldName(表单字段 model) | name=input_1772159072450_604010 |
快捷操作:link_ops.py(外部链接/自定义JS)
使用前准备:
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
LINK_OPS="$SKILL_REFS/scripts/link_ops.py"
# 脚本文件必须用全路径;PYTHONPATH 只解决 import bi_utils,不解决脚本文件查找
核心命令:
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
LINK_OPS="$SKILL_REFS/scripts/link_ops.py"
# 查看页面所有链接配置
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" show $API_BASE $TOKEN $PAGE_ID
# 设置外部链接(按名称/类型/ID 定位组件)——已验证:2026-04-24,JBar 点击跳转 jeecg.com
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" set $API_BASE $TOKEN $PAGE_ID --name "基础柱形图" --url "https://www.jeecg.com"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" set $API_BASE $TOKEN $PAGE_ID --name "饼图名" --url "https://example.com/detail?category=\${name}"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" set $API_BASE $TOKEN $PAGE_ID --type "JPie" --url "https://www.baidu.com/s?wd=\${name}&value=\${value}"
# 删除外部链接
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" remove $API_BASE $TOKEN $PAGE_ID --name "饼图名"
# 设置自定义JS脚本
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" set-js $API_BASE $TOKEN $PAGE_ID --name "基础柱形图" --js 'window.open("http://example.com");return false;'
# 从文件读取复杂脚本
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" set-js $API_BASE $TOKEN $PAGE_ID --type "JBar" --js-file script.js
# 删除自定义JS脚本
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$LINK_OPS" remove-js $API_BASE $TOKEN $PAGE_ID --name "基础柱形图"
URL 参数占位符(来自 ECharts 点击事件 params):
| 占位符 | 含义 |
|---|---|
${name} |
维度名称(饼图扇区名、柱子x轴标签) |
${value} |
数值(饼图扇区值、柱子高度) |
${type} |
系列名称(多系列图表标识) |
打开方式(--target): _blank(新窗口,默认)、_self(当前窗口)
快捷操作:自定义JS脚本(config.jsConfig)
自定义JS脚本存储在
config.jsConfig(字符串)。执行顺序:jsConfig → (return true?) → 外部链接 → 联动 → 钻取。return false 阻断后续。
脚本参数 params 常用属性(ECharts 图表):
| 属性 | 含义 |
|---|---|
params.name |
维度名称(柱子x轴标签、饼图扇区名) |
params.value |
数值 |
params.data |
原始数据对象 {name:'北京', value:100} |
params.dataIndex |
数据索引 |
params.seriesName |
系列名称 |
常用脚本示例:
// 跳转到外部网站(带点击参数)
window.open("https://example.com/detail?name=" + params.name + "&value=" + params.value);
return false;
// 条件跳转
if (params.value > 100) {
window.open("https://example.com/high?name=" + params.name);
} else {
window.open("https://example.com/low?name=" + params.name);
}
return false;
也可用 comp_ops.py edit 快速设置:
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/comp_ops.py" edit $API_BASE $TOKEN $PAGE_ID --name "基础柱形图" --set "jsConfig=window.open(\"http://example.com\");return false;"
组件 dataMapping 槽位配置(SLOT_CONFIGS)速查
绑定数据集时 dataMapping.filed 必须使用以下语义槽位标签(源自 data.ts 定义,禁止自行创造不存在的槽位):
SLOT_CONFIGS = {
# 单系列图表(2槽位:维度+数值)
'JBar': ['维度', '数值'], 'JDynamicBar': ['维度', '数值'], 'JCapsuleChart': ['维度', '数值'],
'JHorizontalBar': ['维度', '数值'], 'JBackgroundBar': ['维度', '数值'],
'JPie': ['维度', '数值'], 'JRose': ['维度', '数值'], 'JRotatePie': ['维度', '数值'],
'JLine': ['维度', '数值'], 'JSmoothLine': ['维度', '数值'], 'JStepLine': ['维度', '数值'], 'JArea': ['维度', '数值'],
'JCustomProgress': ['维度', '数值'], 'JProgress': ['维度', '数值'], 'JListProgress': ['维度', '数值'],
'JPictorialBar': ['维度', '数值'], 'JPictorial': ['维度', '数值'],
'JScatter': ['维度', '数值'], 'JFunnel': ['维度', '数值'], 'JPyramidFunnel': ['维度', '数值'],
'JRadar': ['维度', '数值'], 'JRing': ['维度', '数值'], 'JRingProgress': ['维度', '数值'],
'JActiveRing': ['维度', '数值'], 'JRadialBar': ['维度', '数值'],
'JWordCloud': ['维度', '数值'], 'JAreaMap': ['维度', '数值'], 'JBubbleMap': ['维度', '数值'],
'JBarMap': ['维度', '数值'], 'JHeatMap': ['维度', '数值'],
# 仪表盘(总计+已用)
'JGauge': ['总计', '已用'], 'JColorGauge': ['总计', '已用'], 'JAntvGauge': ['总计', '已用'],
# 多系列图表(3槽位:分组+维度+数值)
'JStackBar': ['分组', '维度', '数值'], 'JMultipleBar': ['分组', '维度', '数值'],
'JNegativeBar': ['分组', '维度', '数值'], 'JPercentBar': ['分组', '维度', '数值'],
'JMixLineBar': ['分组', '维度', '数值'], 'JMultipleLine': ['分组', '维度', '数值'],
'DoubleLineBar': ['分组', '维度', '数值'],
# 表格/列表(名称+数值)
'JTable': ['名称', '数值'], 'JCommonTable': ['名称', '数值'],
'JScrollTable': ['名称', '数值'], 'JScrollRankingBoard': ['名称', '数值'],
# 数字指标(数值)
'JNumber': ['数值'],
# 水波图(总量+当前)
'JLiquid': ['总量', '当前'],
}
# UI-only 组件不绑数据集
NO_BIND = {'JImg', 'JText', 'JCurrentTime', 'JIframe', 'JDragEditor',
'JRadioButton', 'JForm', 'JSelectRadio', 'JTabToggle'}
Step 3: 调用 API 创建仪表盘
优先使用共通工具库 bi_utils.py(从 Skills 目录复制到后端项目根目录使用):
执行步骤:
1. Write 工具 → 写入业务脚本(开头用 sys.path.insert 动态导入 bi_utils,无需 cp)
2. Bash 工具 → PYTHONIOENCODING=utf-8 py create_xxx_dashboard.py
3. Bash 工具 → rm create_xxx_dashboard.py(清理临时脚本)
仪表盘创建示例:
import sys, os
# 动态导入 bi_utils,无需 cp,跨机器兼容
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))
from bi_utils import *
init_api('<api_base>', '<token>')
# 创建仪表盘(style='default',栅格坐标)
page_id = create_page('运营数据看板', style='default', theme='default')
# ⚠️ create_page 内部已保存一次,不要再调 save_page(否则 updateCount 不一致报乐观锁错误)
# 第一行:4 个数字卡片(w=6×4=24,h=17)
add_number(page_id, '总用户数', x=0, y=0, w=6, h=17, value=15890, suffix='人')
add_number(page_id, '今日活跃', x=6, y=0, w=6, h=17, value=3256, suffix='人')
add_number(page_id, '今日收入', x=12, y=0, w=6, h=17, value=89600, prefix='¥')
add_number(page_id, '转化率', x=18, y=0, w=6, h=17, value=23.5, suffix='%')
# 第二行:折线图 + 饼图
add_chart(page_id, 'JLine', '用户增长趋势', x=0, y=17, w=14, h=35,
categories=['周一','周二','周三','周四','周五','周六','周日'],
series=[{'name':'新增用户', 'data':[120,200,150,80,70,110,130]}])
add_chart(page_id, 'JPie', '用户来源', x=14, y=17, w=10, h=35,
pie_data=[
{'name':'微信','value':40},
{'name':'APP','value':30},
{'name':'网页','value':20},
{'name':'其他','value':10},
])
save_page(page_id) # 添加了组件后调一次 save_page
print(f'仪表盘创建成功!ID: {page_id}')
仪表盘样式特点(bi_utils.py 自动应用):
- 背景:白色
#FFFFFF - 边框:浅灰
#E8E8E8 - 标题颜色:深灰
#464646 - 轴标签:
#909198 - 网格线:
#F3F3F3 - 卡片头:白色背景 + 深灰粗体标题(
headColor: '#FFFFFF') - 图例:深灰色文字
仪表盘标题规则(重要)
图表组件:card.title 留空,用 option.title 显示
根据真实模板验证,图表组件(JBar/JLine/JPie/JRing 等)在仪表盘模式下 card.title 应为空字符串,标题通过 ECharts option.title.text 显示。如果两者都设置,标题会重复出现(卡片头一次 + 图表内部一次)。
bi_utils.py 的 add_chart() 已自动处理:调用 _make_card(mode, '') 传入空标题。
JNumber 等非图表组件可以使用 card.title 显示标题。
大屏 vs 仪表盘标题对比
| 特征 | 大屏(bigScreen) | 仪表盘(default) |
|---|---|---|
| 图表标题 | option.title.text(ECharts 内部) |
option.title.text(ECharts 内部) |
| card.title(图表) | 必须为空 '' |
必须为空 ''(避免重复) |
| card.title(JNumber等) | 为空 '' |
可填标题 |
| 页面主标题 | JText 组件(fontSize 40+) | 不需要 |
JText 正确的 config 格式
如果仪表盘中需要使用 JText(少见),config 结构为:
config = {
'dataType': 1,
'chartData': {'value': '显示文本'}, # dict 格式,不是字符串
'option': {
'body': {
'color': '#464646',
'fontSize': 16,
'fontWeight': 'normal',
'letterSpacing': 0,
'text': '',
'marginTop': 0,
'marginLeft': 0,
},
'textAlign': 'left',
'card': {'title': '', ...},
},
}
手动构建组件(用于高级定制,需直接操作 config):
当 add_chart 等快捷函数无法满足需求时(如需要多系列 chartData、自定义 customColor),可直接构建组件 config:
import json, time, random
import bi_utils
def _key():
return f'{int(time.time()*1000)}_{random.randint(100000,999999)}'
# 仪表盘亮色主题通用样式
CARD = {
'size': 'default',
'headColor': '#FFFFFF',
'textStyle': {'color': '#464646', 'fontSize': 16, 'fontWeight': 'bold'},
'extra': '', 'rightHref': ''
}
# 直接构建折线图组件
line_data = [
{'name': '1月', 'value': 120, 'type': '新增'},
{'name': '1月', 'value': 80, 'type': '流失'},
# ...
]
comp = {
'component': 'JLine',
'x': 0, 'y': 17, 'w': 14, 'h': 35,
'i': _key(),
'config': json.dumps({
'dataType': 1,
'chartData': json.dumps(line_data, ensure_ascii=False),
'background': '#FFFFFF',
'borderColor': '#E8E8E8',
'size': {'width': 700, 'height': 375},
'option': {
'customColor': [
{'color': '#1890FF', 'color1': '#1890FF'},
{'color': '#52C41A', 'color1': '#52C41A'},
],
'title': {'show': True, 'text': '用户变化趋势',
'textStyle': {'color': '#464646'}},
'tooltip': {'show': True},
'legend': {'show': True, 'textStyle': {'fontSize': 12}},
'xAxis': {
'type': 'category',
'axisLabel': {'color': '#909198'},
'axisLine': {'lineStyle': {'color': '#F3F3F3'}},
},
'yAxis': {
'axisLabel': {'color': '#909198'},
'splitLine': {'lineStyle': {'color': '#F3F3F3'}},
},
'grid': {'top': 70, 'left': 60, 'right': 30, 'bottom': 40},
'card': {**CARD, 'title': '用户变化趋势'},
}
}, ensure_ascii=False)
}
bi_utils._page_components[page_id].append(comp)
Step 4: 输出结果
必须将预览地址作为单独一行返回,并用 clip.exe 复制到剪贴板:
⚠️ 每次任务完成后必须输出总耗时(强制):
- 脚本中:开头记录
import time; t0 = time.time(),末尾输出print(f'耗时: {time.time()-t0:.1f}s') - 多轮调用/纯API操作:在最终回复文字末尾补充一行
耗时:约 Xs - 禁止输出每个步骤的耗时,只输出整个任务从开始到结束的总耗时
## 仪表盘创建成功
- 页面ID:{id}
- 页面名称:{name}
- 模式:仪表盘(default)
- 组件数量:{count} 个
预览地址(标准仪表盘):
{API_BASE}/drag/page/view/{id}
分享地址(QQY 低代码应用仪表盘):
http://{前端域名}:{端口}/drag/share/{appId}/{pageId}
echo -n "{完整URL}" | clip.exe
⚠️ 写了 Java 接口时,脚本末尾必须额外输出(强制):
print("\n" + "="*60)
print("仪表盘组件已生成完成!")
print("="*60)
print("\n【API 接口地址】(需重启后端后生效):")
print(f" {API_BASE}/drag/mock/xxxFlow")
print("\n【重要提示】请重启 Spring Boot 后端服务!")
print(f"\n【仪表盘预览地址】")
print(f" {API_BASE}/drag/page/view/{PAGE_ID}")
print("="*60)
数据集管理(动态数据源)
config.dataType:1=静态数据(chartData直写);2=动态数据(SQL/API/文件数据集);4=表单数据(Online/设计器表单,无需数据集)
推荐工作流(无需读此节):
- SQL 数据集:
dataset_ops.py create-sql→comp_ops.py batch-add --specs(spec 的 config 传 dataType/dataSetId/dataMapping) - API 数据集:
dataset_ops.py create-api→comp_ops.py add --dataset-name(2轮完成) - 文件数据集:
files_ops.py create-bind(一体化)
数据集 API 端点
| 端点 | 方法 | 说明 |
|---|---|---|
/drag/onlDragDatasetHead/add |
POST | 创建数据集 |
/drag/onlDragDatasetHead/edit |
POST | 编辑数据集 |
/drag/onlDragDatasetHead/delete?id=xxx |
DELETE | 删除 |
/drag/onlDragDatasetHead/list |
GET | 查询列表 |
/drag/onlDragDatasetHead/getAllChartData |
POST | 执行查询/取数据 |
/drag/onlDragDatasetHead/queryFieldBySql |
POST | 解析SQL字段(需签名) |
/drag/onlDragDatasetHead/queryFieldByApi |
POST | 解析API字段 |
组件绑定数据集(dataType=2)
config = {
'dataType': 2,
'dataSetId': 'dataset_id',
'dataSetName': '数据集名称',
'dataSetType': 'sql', # sql / api
'dataSetApi': 'SELECT ...', # SQL语句或API地址
'dataSetMethod': 'GET',
'dataSetIzAgent': '', # SQL='', API直连='0', API代理='1'
'dataMapping': [
{'filed': '维度', 'mapping': 'name'}, # filed=语义槽位,mapping=字段名
{'filed': '数值', 'mapping': 'value'},
],
'fieldOption': [...], # 字段列表(getAllChartData后可获取)
'dictOptions': {}, # 字典翻译(getAllChartData返回)
'paramOption': [], # 参数列表(FreeMarker条件参数)
'chartData': '[]',
}
dataMapping 语义槽位:单系列 [维度, 数值];多系列 [分组, 维度, 数值]
filed 拼写:filed(少一个 d,不是 field)
完整 SQL/API 端到端流程、FreeMarker 语法、全流程自定义脚本 → 见
references/datasource-dataset-chart-guide.md和references/dataset-guide.md
API 接口签名机制
queryFieldBySql 等接口带 @SignatureValidation,需要签名头。bi_utils 的 signed_request() 函数已封装签名逻辑,直接调用即可。
签名算法、Python 实现、需签名接口清单 → 见
references/signing-datasource-guide.md
数据源管理
使用 datasource_ops.py 管理数据源:list, detail, create, test, delete
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/datasource_ops.py" list "API_BASE" "TOKEN"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/datasource_ops.py" create "API_BASE" "TOKEN" --name "名称" --db "MYSQL5.7" \
--url "jdbc:mysql://..." --user "root" --password "root"
已适配 18 种数据库类型(MySQL5.7/8.0, PostgreSQL, Oracle, SQLServer, DM等)。NoSQL/签名数据源 → 见
references/signing-datasource-guide.md
SQL 数据集动态查询条件(FreeMarker)
SQL 支持 FreeMarker 动态条件:<#if isNotEmpty(sex)> AND sex = '${sex}' </#if>
参数配置在 datasetParamList;组件 config.paramOption 中传参值。
FreeMarker 语法规则、参数配置详情 → 见
references/dataset-guide.md
SQL/API 数据集绑定图表完整端到端流程
推荐方式(普通SQL,无FreeMarker):
SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"
# 1. 创建SQL数据集
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/dataset_ops.py" create-sql $API_BASE $TOKEN \
--name "数据集名" --db-source "数据源ID" \
--sql "SELECT name, value FROM t GROUP BY name" \
--fields "name:String,value:Integer"
# 2. 批量添加图表并绑定
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/comp_ops.py" batch-add $API_BASE $TOKEN $PAGE_ID --specs '[
{"comp":"JBar","title":"柱形图","x":0,"y":0,"w":12,"h":35,
"config":{"dataType":2,"dataSetId":"<DS_ID>",
"dataMapping":[{"filed":"维度","mapping":"name"},{"filed":"数值","mapping":"value"}]}}
]'
全流程自定义脚本(含FreeMarker SQL / 需queryFieldBySql回写):见 references/datasource-dataset-chart-guide.md
API 数据集:dataset_ops.py create-api + comp_ops.py add --dataset-name(2轮完成)
JSON 数据集 + 图表(内联静态 JSON,无需外部数据源)
适用场景:演示/示例数据,数据量小,不需要数据库或外部 API。
关键规则(强制):
dataType: 'json',数据必须放在querySql字段(JSON 数组字符串)- 禁止放
content字段:content对 JSON 类型无效,getAllChartData不读取content,会返回data: null - 不需要
dbSource(无数据库) - 不需要
queryFieldBySql(字段在创建时手动指定) - 绑定组件时
dataSetType: 'json',dataSetIzAgent: ''
完整流程(自定义脚本):
import sys, os, json, time, random, copy
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references', 'scripts'))
import bi_utils
bi_utils.API_BASE = '<api_base>'
bi_utils.TOKEN = '<token>'
# Step 1: 获取/创建"示例数据集"分组
groups_resp = bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup')
parent_id = '0'
for g in (groups_resp.get('result') or []):
if g.get('name') == '示例数据集' and g.get('dataType') is None:
parent_id = g.get('id', '0'); break
if parent_id == '0':
bi_utils._request('POST', '/drag/onlDragDatasetHead/addGroup', data={'groupName': '示例数据集'})
for g in (bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup').get('result') or []):
if g.get('name') == '示例数据集' and g.get('dataType') is None:
parent_id = g.get('id', '0'); break
# Step 2: 创建 JSON 数据集(数据放 querySql,不是 content)
json_data = [
{'name': '类别A', 'value': 3200},
{'name': '类别B', 'value': 5800},
{'name': '类别C', 'value': 1900},
]
resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '数据集名称',
'code': 'dataset_code',
'dataType': 'json',
'querySql': json.dumps(json_data, ensure_ascii=False), # ✅ 数据在 querySql
'content': '', # ❌ content 无效,留空
'apiMethod': 'GET',
'parentId': parent_id,
'datasetItemList': [
{'fieldName': 'name', 'fieldTxt': '名称', 'fieldType': 'String', 'izShow': 'Y', 'orderNum': 0},
{'fieldName': 'value', 'fieldTxt': '数值', 'fieldType': 'Integer', 'izShow': 'Y', 'orderNum': 1},
],
'datasetParamList': [],
})
result = resp.get('result', {})
DS_ID = result.get('id') if isinstance(result, dict) else result
# Step 3: 绑定图表(batch-add,config 中传数据绑定字段)
# comp_ops.py batch-add specs 中:
spec_config = {
'dataType': 2,
'dataSetId': DS_ID,
'dataSetName': '数据集名称',
'dataSetType': 'json', # 对应 dataType:'json' 的数据集
'dataSetApi': '',
'dataSetMethod': 'GET',
'dataSetIzAgent': '', # JSON 类型留空
'dataMapping': [
{'filed': '维度', 'mapping': 'name'},
{'filed': '数值', 'mapping': 'value'},
],
'fieldOption': [
{'fieldName': 'name', 'fieldTxt': '名称', 'fieldType': 'String'},
{'fieldName': 'value', 'fieldTxt': '数值', 'fieldType': 'Integer'},
],
'dictOptions': {}, 'paramOption': [], 'chartData': '[]',
}
数据集踩坑:
| 问题 | 原因 | 解决 |
|---|---|---|
getAllChartData 返回 data: null |
JSON 数据放在 content 而非 querySql |
把 JSON 数组字符串放 querySql,content 留空 |
| 数据集管理 UI 无数据预览 | 同上 | 同上 |
文件数据集(单文件 singleFile / 多文件 FILES)
文件数据集通过上传 Excel/CSV/JSON 文件作为数据源,无需外部数据库连接。
文件数据集 vs SQL/API 数据集的关键差异
| 项目 | 单文件 (singleFile) | 多文件 (FILES) | SQL 数据集 | API 数据集 |
|---|---|---|---|---|
dataType(数据集) |
'singleFile' |
'FILES' |
'sql' |
'api' |
dbSource |
reportId(页面 ID) | reportId(页面 ID) | 数据库源 ID | None |
querySql |
select * from {tableName} |
可跨表 SQL 查询 | SQL 语句 | API URL |
dataSetIzAgent(组件config) |
''(空字符串) |
'1'(后端代理) |
'0' |
'0'/'1' |
| 文件上传 | 1 个文件(isSingle=true) |
多个文件 | 不需要 | 不需要 |
content |
JSON.stringify(fileList) |
不需要 | 不需要 | 不需要 |
| 字段解析 API | queryFileFieldBySql(非 queryFieldBySql) |
queryFileFieldBySql(非 queryFieldBySql) |
queryFieldBySql |
queryFieldByApi |
| 支持格式 | .csv .xls .xlsx .json |
.csv .xls .xlsx .json |
— | — |
🚨
dataSetIzAgent区别:FILES 必须设'1'(走后端代理读文件),singleFile 必须设''(空字符串,非'0')。写错会导致图表无数据或请求失败。
文件上传 API
| 端点 | 方法 | 说明 |
|---|---|---|
/jmreport/source/datasource/files/add |
POST (multipart) | 上传文件 |
/jmreport/source/datasource/files/get |
GET | 获取文件列表 ?reportId=xxx |
/jmreport/source/datasource/files/preview |
GET | 预览文件数据 |
/jmreport/source/datasource/files/del |
DELETE | 删除数据源 |
/jmreport/source/datasource/files/del/file |
DELETE | 删除单个文件 |
上传参数(multipart/form-data):
| 参数 | 类型 | 说明 |
|---|---|---|
file |
File | 上传的文件 |
reportId |
String | 页面 ID(大屏/仪表盘 ID) |
isSingle |
Boolean | 单文件数据集设为 true,多文件不传 |
X-Access-Token |
Header | JWT 令牌 |
上传返回结构:
{
"success": true,
"message": "filesDataSet/PAGE_ID/default.xls",
"result": {
"id": "xxx",
"dbUrl": "[{\"fileName\":\"default.xls\",\"name\":\"jmf.Sheet1_default_excel\"}]"
}
}
表名命名规则:
- XLS/XLSX:
jmf.{SheetName}_{fileName}_excel(取第一个 Sheet 名),如jmf.Sheet1_default_excel- CSV:
jmf.{fileName}_csv,如jmf.sales_csv- JSON:
jmf.{fileName}_json,如jmf.orders_json⚠️ 文件字段名必须为英文:H2/Calcite 引擎不支持中文列名,上传含中文列名的 Excel 会导致字段解析异常。上传前将列名改为英文。
Python 文件上传函数
def upload_file(file_path, report_id, is_single=False):
url = f'{API_BASE}/jmreport/source/datasource/files/add'
boundary = f'----WebKitFormBoundary{int(time.time()*1000)}'
file_name = os.path.basename(file_path)
with open(file_path, 'rb') as f:
file_data = f.read()
body_parts = []
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="reportId"\r\n\r\n{report_id}\r\n'.encode())
if is_single:
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="isSingle"\r\n\r\ntrue\r\n'.encode())
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{file_name}"\r\nContent-Type: application/octet-stream\r\n\r\n'.encode())
body_parts.append(file_data)
body_parts.append(f'\r\n--{boundary}--\r\n'.encode())
body = b''.join(body_parts)
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}', 'X-Access-Token': TOKEN}
req = urllib.request.Request(url, data=body, headers=headers, method='POST')
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode('utf-8'))
创建单文件数据集(singleFile)
# 1. 上传文件(isSingle=True)
result = upload_file(FILE_PATH, PAGE_ID, is_single=True)
file_list = json.loads(result['result']['dbUrl'])
table_name = file_list[0]['name'] # 如 jmf.Sheet1_default_excel
# 2. 解析字段(必须用 queryFileFieldBySql,非 queryFieldBySql)
fields_resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/queryFileFieldBySql', data={
'sql': f'select * from {table_name}',
'dbCode': PAGE_ID, # 注意:参数名是 dbCode,值是页面ID(非数据库ID)
})
fields = fields_resp.get('result', []) # [{fieldName, fieldTxt, fieldType}, ...]
# 3. 创建数据集(🚨 code 必须等于 table_name,不能自造随机字符串)
ds = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '销售数据(单文件)',
'code': table_name, # 🚨 必须等于 table_name(如 jmf.Sheet1_default_excel)
'dataType': 'singleFile', # 关键
'dbSource': PAGE_ID, # 关键:页面ID(非数据库连接ID)
'querySql': f'select * from {table_name}',
'content': json.dumps(file_list, ensure_ascii=False), # 文件列表 JSON
'apiMethod': 'GET',
'datasetItemList': fields, 'datasetParamList': [],
})
# 4. 组件 config 绑定(dataSetIzAgent 必须是空字符串,非 '0')
config = {
'dataType': 2, 'dataSetType': 'singleFile',
'dataSetId': ds['result']['id'],
'dataSetApi': f'select * from {table_name}',
'dataSetIzAgent': '', # 🚨 单文件必须是空字符串(不是 '0')
'dataMapping': [{'filed': '维度', 'mapping': 'col1'}, {'filed': '数值', 'mapping': 'col2'}],
}
创建多文件数据集(FILES)
# 1. 上传多个文件
upload_file(r'<file_path>', PAGE_ID)
upload_file(r'<file_path>', PAGE_ID)
# 2. 获取文件列表
files = bi_utils._request('GET', '/jmreport/source/datasource/files/get', params={'reportId': PAGE_ID})
file_list = json.loads(files['result']['dbUrl'])
# 3. 创建数据集(可跨文件表 SQL)
ds = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '多文件数据集', 'code': 'multi_files',
'dataType': 'FILES', # 关键:大写 FILES
'dbSource': PAGE_ID, # 关键:页面ID
'querySql': f'select name, value from {table_name} order by value desc',
'datasetItemList': [...], 'datasetParamList': []
})
# 4. 组件 config 绑定(dataSetIzAgent 必须是 '1',走后端代理读文件)
config = {
'dataType': 2, 'dataSetType': 'FILES',
'dataSetId': ds['result']['id'],
'dataSetApi': f'select * from {table_name}',
'dataSetIzAgent': '1', # 🚨 多文件必须是 '1'(后端代理),不能是 '' 或 '0'
'dataMapping': [{'filed': '维度', 'mapping': 'col1'}, {'filed': '数值', 'mapping': 'col2'}],
}
数据集踩坑记录
| 问题 | 原因 | 解决方案 |
|---|---|---|
| "数据源不存在" | SQL 数据集未设置 dbSource |
必须指定 dbSource(如 707437208002265088) |
| 字段列表不生效 | 用了 onlDragDatasetItemList |
正确字段名是 datasetItemList |
| 编辑数据集 510 权限错误 | 缺少 sign 字段 |
编辑时需传 sign: 'E19D6243CB1945AB4F7202A1B00F77D5' |
| dataMapping 的 filed 拼写 | 系统中 filed 不是 field |
必须用 filed(少一个 d),这是系统设计 |
| API 类型跨域 | 前端直连外部 API 遇到 CORS | 设置 izAgent: '1' 走后端代理 |
| SQL 参数替换 | 需要动态参数 | SQL 中用 #{paramName}(系统变量)或 ${paramName}(FreeMarker) |
| SQL 最大返回 1000 条 | 后端限制 | getChartData 方法限制最大 1000 条记录 |
| queryFieldBySql 签名验证失败 | 该接口带 @SignatureValidation |
必须用 signed_request() 携带 X-Sign/V-Sign/X-TIMESTAMP |
| SQL 注入检测拦截 | 查询 information_schema 被拦截 | 后端 SqlInjectionUtil 会拦截敏感关键词,直接用已知表名 |
| API 地址存在 querySql 字段 | API 数据集没有独立的 url 字段 | querySql 对 SQL 类型存 SQL,对 API 类型存 API URL |
| API 数据集不需要 dbSource | API 类型直接访问外部接口 | dbSource 设为 None,否则可能报错 |
| 漏斗图数据过多显示拥挤 | 漏斗图层级太多影响视觉效果 | 使用 dataFilterNum 限制前 N 条(建议 3-7 条) |
| API 数据集 izAgent 选择 | mock API 无跨域问题,外部 API 可能有 | 同域/mock 用 '0'(直连),跨域用 '1'(后端代理) |
| 文件数据集 dbSource 不是数据库ID | singleFile/FILES 的 dbSource 是页面 ID |
dbSource = reportId(页面 ID),不是数据库连接 ID |
| 单文件数据集需要 content 字段 | 单文件的 content 存文件列表 JSON |
content = JSON.stringify([{fileName, name}]),多文件不需要 |
| 多文件字段解析用 queryFileFieldBySql | 多文件的字段解析 API 不同于 SQL 数据集 | 用 queryFileFieldBySql(非 queryFieldBySql),参数 dbCode = reportId |
| XLS 文件表名含 Sheet 名 | 系统从 Excel 的 Sheet 名生成表名 | 表名格式 jmf.{SheetName}_{fileName}_{ext},如 jmf.Sheet1_default_excel |
| CSV 编码问题 | UTF-8 BOM 头导致字段名乱码 | 上传前确保文件为纯 UTF-8(无 BOM),或系统会自动处理 |
| 文件上传 isSingle 参数 | 单文件和多文件的区别标志 | 单文件上传传 isSingle=true,多文件不传此参数 |
| 🚨 文件数据集 dataSetIzAgent 值不同 | FILES 需要后端代理读文件,singleFile 不需要 | FILES:dataSetIzAgent='1';singleFile:dataSetIzAgent=''(空字符串,非 '0');填错导致图表无数据 |
| 🚨 文件列名必须为英文 | H2/Calcite 引擎不支持中文列名 | 上传含中文列名的 Excel/CSV 会导致字段解析异常,上传前将列名改为英文 |
| 🚨 文件数据集字段解析必须用 queryFileFieldBySql | 文件引擎与 SQL 数据库引擎不同 | 单/多文件数据集的字段解析必须调用 /queryFileFieldBySql,参数 dbCode=PAGE_ID;用 queryFieldBySql 会失败 |
Online表单 / 设计器表单 生成图表(dataType=4)
config.dataType=4 直接绑定 Online表单(cgform)或设计器表单(desform),无需创建数据集。
| 类型 | formType | 查询API |
|---|---|---|
| Online表单(cgform) | 'online' |
GET /online/cgform/head/list |
| 设计器表单(desform) | 'design' |
GET /desform/api/list/options |
字段角色:nameFields=维度(String类型);valueFields=指标(数值类型或 record_count);typeFields=分组
完整字段查询、config 构建模板、字段类型映射 → 见
references/online-design-form-chart-guide.md
编辑已有仪表盘
from bi_utils import *
init_api('<api_base>', '<token>')
page = query_page(page_id)
print(page['name'], page['updateCount'])
add_chart(page_id, 'JBar', '新增图表', x=0, y=52, w=12, h=35,
categories=['A','B','C'], series=[{'name':'值','data':[10,20,30]}])
save_page(page_id)
删除仪表盘
from bi_utils import *
init_api('<api_base>', '<token>')
delete_page(page_id) # 软删除
delete_page(page_id, physical=True) # 硬删除
修改组件样式
阅读 references/bi-comp-option-config.md 获取每种组件的完整配置项路径。
仪表盘样式修改关键规则:
- 颜色使用色值(
#000000),不用英文单词 - customColor 格式:
[{color1:'#xxx',color:'#xxx'}] - 卡片头样式:
option.card.textStyle.color、option.card.headColor - 背景色:
config.background(仪表盘默认#FFFFFF,禁止设置透明色#FFFFFF00或transparent) - 边框色:
config.borderColor(仪表盘默认#E8E8E8)
import json
from bi_utils import *
import bi_utils
init_api('<api_base>', '<token>')
page_id = 'xxx'
page = query_page(page_id)
tmpl = page.get('template', [])
if isinstance(tmpl, str):
tmpl = json.loads(tmpl)
for comp in tmpl:
config_str = comp.get('config', '{}')
config = json.loads(config_str) if isinstance(config_str, str) else config_str
if comp.get('component') == 'JBar':
option = config.get('option', {})
option['series'][0]['itemStyle'] = {'color': '#1890FF'}
config['option'] = option
comp['config'] = json.dumps(config, ensure_ascii=False)
bi_utils._page_components[page_id] = tmpl
save_page(page_id)
bi_utils 使用规则(强制)
初始化方式
# 正确:直接赋值模块级全局变量
import bi_utils
bi_utils.API_BASE = '<api_base>'
bi_utils.TOKEN = '...'
# 也可以用 init_api(封装了赋值)
from bi_utils import *
init_api('<api_base>', '<token>')
# 错误:没有 init() 方法
# bi_utils.init(API_BASE, TOKEN) # ← AttributeError
页面数据与组件字段映射(query_page 返回值)
| 正确字段 | 常见误猜 | 说明 |
|---|---|---|
page['template'] |
page['pageTemplate'] |
组件列表,已经是 list,无需 json.loads |
comp['i'] |
comp['id'] |
组件唯一标识(UUID) |
comp['componentName'] |
comp['label']comp['name'] |
组件显示名称(中文) |
comp['component'] |
- | 组件类型(JBar, JText 等) |
comp['pageCompId'] |
- | 后端数据库 ID |
comp['isLock'] |
- | 锁定状态(true/false) |
自定义脚本操作模板的正确模式
import bi_utils
bi_utils.API_BASE = '...'
bi_utils.TOKEN = '...'
PAGE_ID = '...'
page = bi_utils.query_page(PAGE_ID)
tmpl = page.get('template', []) # 已经是 list,不需要 json.loads
# 按组件名查找(字段是 componentName,不是 label/name)
target_idx = next(i for i, c in enumerate(tmpl) if c.get('componentName') == '目标名称')
# 修改后保存
bi_utils._page_components[PAGE_ID] = tmpl
bi_utils.save_page(PAGE_ID)
Windows Python 命令
- 用
py不是python(Git Bash 下python找不到) - 必须加
PYTHONIOENCODING=utf-8前缀(Windows 默认 GBK 编码,脚本中含中文输出时报UnicodeEncodeError) - 所有
py script.py调用必须写成PYTHONIOENCODING=utf-8 py script.py - 脚本内部
print禁止使用 emoji 字符
常用组件配置路径速查(内联)
以下组件的 option 路径已内联,修改时直接使用,无需读取
bi-comp-option-config.md。
JStatsSummary(统计概览)
| 说明 | 配置路径 | 示例值 |
|---|---|---|
| 卡片最小宽度 | option.card.minWidth |
250 |
| 卡片圆角 | option.card.borderRadius |
16 |
| 卡片边框颜色 | option.card.borderColor |
#1890FF59 |
| 卡片阴影 | option.card.shadow |
0 4px 12px #00000020 |
| 卡片填充颜色 | option.card.fill.color |
#F7F7F7 |
| 数值字号 | option.sections.top.value.fontSize |
28 |
| 数值颜色 | option.sections.top.value.fontColor |
#464646 |
| 单位字号 | option.sections.top.value.unit.fontSize |
14 |
| 标签字号 | option.sections.bottom.label.fontSize |
14 |
| 标签颜色 | option.sections.bottom.label.fontColor |
#909198 |
JCapsuleChart(胶囊图)
| 说明 | 配置路径 | 示例值 |
|---|---|---|
| 显示数值 | option.showValue |
true/false |
| X轴名称/单位 | option.unit |
个 |
JGauge(仪表盘表盘)
| 说明 | 配置路径 |
|---|---|
| 刻度值显隐 | option.series[0].axisLabel.show |
| 刻度值颜色 | option.series[0].axisLabel.color |
| 刻度线显隐 | option.series[0].axisTick.show |
| 分割线显隐 | option.series[0].splitLine.show |
| 指标字号 | option.series[0].detail.fontSize |
JProgress(进度条)
| 说明 | 配置路径 |
|---|---|
| 显示标题 | option.yAxis.axisLabel.show |
| 标题字体颜色 | option.yAxis.axisLabel.color |
JScrollBoard(轮播表)
| 说明 | 配置路径 |
|---|---|
| 悬浮暂停 | option.hoverPause |
| 等待时间 | option.waitTime |
JNumber(数字指标卡)
| 说明 | 配置路径 |
|---|---|
| 数值字号 | option.valueStyle.fontSize |
| 数值颜色 | option.valueStyle.color |
| 前缀 | option.prefix |
| 后缀/单位 | option.suffix |
| 卡片头颜色 | option.card.textStyle.color |
布局组件使用指南(JTabs / JGrid)
证据来源:
Tabs.vue、Grid.vue、data.ts(第3640-3687行)
JTabs(选项卡)
用途:在同一空间内放多个 Tab,每个 Tab 内嵌一个图表/组件,切换显示。
默认尺寸:w=12, h=40
数据结构(API创建时完整格式):
jtabs_i = bi_utils._gen_uuid()
jtabs_comp = {
'component': 'JTabs',
'componentName': '选项卡', # 必须中文
'i': jtabs_i,
'x': 0, 'y': 0, 'w': 24, 'h': 40,
'orderNum': 10,
'visible': True,
'config': {
'w': 1800, 'h': 440,
'size': {'width': 1800, 'height': 440},
'option': {
'title': '选项卡',
# 右上角文字按钮(可选)
'rightText': '更多', # 显示文字
'rightHref': '/page/detail', # 点击跳转链接
'rightTextColor': '#4A90E2', # 按钮颜色(默认#4A90E2)
},
'child': [
{
'title': 'Tab1', # Tab 标签文字
'i': bi_utils._gen_uuid(), # 每个 tab 必须有独立 uuid
'parentId': jtabs_i, # 必须是 JTabs 的 i
'component': 'JBar', # 嵌套的组件类型
'config': { ... } # 嵌套组件的完整 config
},
{
'title': 'Tab2',
'i': bi_utils._gen_uuid(),
'parentId': jtabs_i,
'component': '', # 空字符串 = 未放组件
'config': {}
}
]
}
}
关键规则(来源:Tabs.vue 第90-98行):
config.child读取的是props.config.child(不是props.child),必须放在config下- 每个 Tab 只能嵌套 一个 组件
- 禁止在 JTabs 内再嵌套 JTabs 或 customForm(
excludeComp = ['JTabs','customForm']) - Tab 内组件 resize 时会触发重新渲染(watch size → reloadKey++,解决组件尺寸失配问题)
rightText/rightHref不填时不显示右上角按钮
⚠️ JTabs 不能用 comp_ops.py batch-add 直接创建(child 嵌套结构含动态 parentId,CLI 无法表达)。必须用自定义 Python 脚本。
JTabs 完整实操脚本(含 SQL 数据集 + API 数据集,已验证可用)
验证日期:2026-04-24;场景:Tab1=散点地图(SQL数据集),Tab2=饼状图(API数据集)
# -*- coding: utf-8 -*-
import sys, os, json, time, random
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references', 'scripts'))
import bi_utils
API_BASE = '<api_base>'
TOKEN = '<token>'
PAGE_ID = '<page_id>'
DB_SOURCE = '<datasource_id>' # SQL 数据集所用数据源 ID
bi_utils.API_BASE = API_BASE
bi_utils.TOKEN = TOKEN
def _key():
return f'{int(time.time()*1000)}_{random.randint(100000,999999)}'
# Step 1: 缓存模板(必须!防止 save_page 覆盖已有组件)
page = bi_utils.query_page(PAGE_ID)
tmpl = page.get('template') or []
if isinstance(tmpl, str): tmpl = json.loads(tmpl)
# 若要替换已有 JTabs,先过滤掉旧的
tmpl = [c for c in tmpl if c.get('component') != 'JTabs']
# Step 2: 获取/创建"示例数据集"分组
groups = (bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup').get('result') or [])
parent_id = next((g['id'] for g in groups if g.get('name') == '示例数据集' and g.get('dataType') is None), '0')
if parent_id == '0':
bi_utils._request('POST', '/drag/onlDragDatasetHead/addGroup', data={'groupName': '示例数据集'})
groups = (bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup').get('result') or [])
parent_id = next((g['id'] for g in groups if g.get('name') == '示例数据集' and g.get('dataType') is None), '0')
# Step 3: 创建 SQL 数据集(Tab1 使用)
SQL = "SELECT '北京' AS name, 320 AS value UNION ALL SELECT '上海', 285 ..."
ds_resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '城市散点分布', 'code': 'city_scatter_dist',
'dataType': 'sql', 'dbSource': DB_SOURCE, 'querySql': SQL, 'apiMethod': 'GET',
'parentId': parent_id,
'datasetItemList': [
{'fieldName': 'name', 'fieldTxt': '城市名称', 'fieldType': 'String', 'izShow': 'Y', 'orderNum': 0},
{'fieldName': 'value', 'fieldTxt': '数值', 'fieldType': 'Integer', 'izShow': 'Y', 'orderNum': 1},
],
'datasetParamList': []
})
result = ds_resp.get('result', {})
SQL_DS_ID = result.get('id') if isinstance(result, dict) else result
# Step 4: 构建 JTabs——tabs_i 必须先生成,child 的 parentId 指向它
tabs_i = _key()
map_i = _key()
pie_i = _key()
jtabs_comp = {
'component': 'JTabs', 'componentName': '选项卡',
'visible': True, 'i': tabs_i,
'x': 0, 'y': <Y>, 'w': 24, 'h': 45,
'pcX': 0, 'pcY': <Y>, 'pcW': 24, 'orderNum': 100,
'config': {
'w': 1800, 'h': 495, # 像素:w*75, h*11
'size': {'width': 1800, 'height': 495},
'option': {'title': '选项卡'},
'child': [
{
'title': '散点地图', # Tab 标签文字
'i': map_i,
'parentId': tabs_i, # 🚨 必须是父 JTabs 的 i
'component': 'JBubbleMap',
'w': 24, 'x': 0, 'h': 40,
'config': {
'dataType': 2, 'dataSetId': SQL_DS_ID,
'dataSetName': '城市散点分布', 'dataSetType': 'sql',
'dataSetApi': SQL, 'dataSetMethod': 'GET', 'dataSetIzAgent': '',
'dataMapping': [{'filed': '维度', 'mapping': 'name'}, {'filed': '数值', 'mapping': 'value'}],
'fieldOption': [
{'fieldName': 'name', 'fieldTxt': '城市名称', 'fieldType': 'String'},
{'fieldName': 'value', 'fieldTxt': '数值', 'fieldType': 'Integer'},
],
'dictOptions': {}, 'paramOption': [], 'chartData': '[]',
'chart': {'subclass': 'JBubbleMap', 'category': 'Map', 'isGroup': False},
'commonOption': {
'barSize': 10, 'gradientColor': False,
'breadcrumb': {'drillDown': False, 'textColor': '#000000'},
'areaColor': {'color1': '#f7f7f7', 'color2': '#fcc02e'},
'barColor': '#fff176', 'barColor2': '#fcc02e',
'inRange': {'color': ['#04387b', '#467bc0']},
},
'filter': {'conditionFields': [], 'conditionMode': 'and', 'queryRange': 'all'},
'timeOut': 0, 'size': {'width': 1800, 'height': 440},
'background': '#FFFFFF', 'turnConfig': {'url': ''},
'option': {
'drillDown': False,
'area': {
'markerColor': '#DDE330', 'shadowBlur': 10, 'markerCount': 5,
'markerOpacity': 1, 'name': ['中国'], 'scatterLabelShow': False,
'shadowColor': '#DDE330', 'value': ['china'], 'markerType': 'effectScatter',
},
'geo': {
'top': 30, 'zoom': 1, 'roam': False,
'itemStyle': {
'normal': {'borderColor': '#a9a9a9', 'areaColor': '',
'borderWidth': 1, 'shadowColor': '#80d9f8',
'shadowBlur': 0, 'shadowOffsetX': 0, 'shadowOffsetY': 0},
'emphasis': {'areaColor': '#fff59c', 'borderWidth': 0},
},
'label': {'emphasis': {'color': '#fff', 'show': False}},
},
'series': [], 'grid': {'bottom': 115, 'show': False},
'legend': {'data': []}, 'graphic': [],
'title': {'left': 10, 'show': True, 'text': '城市散点地图',
'textStyle': {'fontWeight': 'normal'}},
'card': {'rightHref': '', 'size': 'default', 'extra': '',
'headColor': '#FFFFFF', 'title': ''},
'visualMap': {
'min': 0, 'top': 'bottom', 'max': 400, 'left': '5%',
'calculable': True, 'show': False, 'type': 'continuous',
'seriesIndex': [1], # JBubbleMap 固定 [1]
},
},
},
},
{
'title': '饼状图',
'i': pie_i,
'parentId': tabs_i, # 🚨 同样必须指向父 JTabs 的 i
'component': 'JPie',
'w': 24, 'x': 0, 'h': 40,
'config': {
'dataType': 2, 'dataSetId': '<API_DATASET_ID>',
'dataSetName': '<数据集名>', 'dataSetType': 'api',
'dataSetApi': '<API_URL>', 'dataSetMethod': 'GET', 'dataSetIzAgent': '1',
'dataMapping': [{'filed': '维度', 'mapping': 'name'}, {'filed': '数值', 'mapping': 'value'}],
'fieldOption': [
{'fieldName': 'name', 'fieldTxt': 'name', 'fieldType': 'String'},
{'fieldName': 'value', 'fieldTxt': 'value', 'fieldType': 'Integer'},
],
'dictOptions': {}, 'paramOption': [], 'chartData': '[]',
'timeOut': 0, 'size': {'width': 1800, 'height': 440},
'background': '#FFFFFF', 'turnConfig': {'url': ''},
'option': {
'series': [{'data': [], 'type': 'pie', 'radius': '55%'}],
'tooltip': {'trigger': 'item'},
'legend': {'orient': 'vertical', 'left': 'left'},
'title': {'show': True, 'text': '饼状图标题',
'textStyle': {'fontWeight': 'normal', 'color': '#464646'}},
'card': {'rightHref': '', 'size': 'default', 'extra': '',
'headColor': '#FFFFFF', 'title': ''},
},
},
},
],
},
}
# Step 5: 追加并保存(一次 save,避免乐观锁)
tmpl.append(jtabs_comp)
bi_utils._page_components[PAGE_ID] = tmpl
bi_utils.save_page(PAGE_ID)
JGrid(栅格布局)
用途:横向并排展示多个组件,可设置各列宽度比例,支持移动端自适应。
默认尺寸:w=12, h=40
数据结构(API创建时完整格式):
jgrid_i = bi_utils._gen_uuid()
jgrid_comp = {
'component': 'JGrid',
'componentName': '栅格布局', # 必须中文
'i': jgrid_i,
'x': 0, 'y': 0, 'w': 24, 'h': 40,
'orderNum': 10,
'visible': True,
'config': {
'w': 1800, 'h': 440,
'size': {'width': 1800, 'height': 440},
'option': {
'card': {
'title': '', # 整体卡片标题(留空则不显示)
'extra': '', # 卡片右侧附加文字
'rightHref': '', # 附加文字跳转链接
'size': 'default', # 卡片尺寸
}
},
'child': [
{
'i': bi_utils._gen_uuid(),
'parentId': jgrid_i, # 必须是 JGrid 的 i
'span': 12, # 列宽(24列制,多列 span 之和 = 24)
'component': 'JPie', # 该列放置的组件类型
'config': { ... } # 该列组件的完整 config
},
{
'i': bi_utils._gen_uuid(),
'parentId': jgrid_i,
'span': 12,
'component': 'JBar',
'config': { ... }
}
]
}
}
关键规则(来源:Grid.vue 第146-151行):
- 子组件实际渲染宽度 =
(parent.size.width / 24) * item.span(自动计算,不需手动传 size) span之和推荐 = 24(等于总宽),否则最后一列会截断或留白- 禁止在 JGrid 内嵌套 layout 类或 customForm(
excludeComp = ['layout','customForm']) - 可以 JGrid 嵌套在 JTabs 内(支持
pid参数传递父 Tab ID) - 移动端响应式(来源:Grid.vue 第156-162行):屏幕宽度 < 500px 时所有列
span自动改为 24(全宽堆叠) child默认为空数组,需在 API 创建时直接填充child才能预设内容- 🚨 child 每项必须含
w/x/h字段(仅有 span/i/parentId/component/config 不够,缺少 w/x/h 时子组件高度为 0,内容不可见) - 🚨 JGrid 不能用
comp_ops.py batch-add创建(child 含动态 parentId 及完整子组件 config,CLI 无法表达;必须写自定义脚本) - 🚨 JGrid 顶层组件需要
pcX/pcY/pcW字段(与普通组件对齐,缺少时移动端布局异常) config.w/config.h必须与像素 size 一致:config.w = w*75,config.h = h*11
常见 span 分配方案:
| 列数 | span 分配 | 说明 |
|---|---|---|
| 2列 | 12 + 12 | 各半 |
| 3列 | 8 + 8 + 8 | 三等分 |
| 4列 | 6 + 6 + 6 + 6 | 四等分 |
| 左宽右窄 | 16 + 8 | 左图右说明 |
| 左窄右宽 | 8 + 16 | 左说明右图 |
JGrid 完整实操脚本(含 JSON 数据集 + API 数据集,已验证可用)
验证日期:2026-04-24;场景:栅格1=热力地图(JSON数据集),栅格2=柱形图(API数据集)
# -*- coding: utf-8 -*-
import sys, os, json, time, random, copy
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references', 'scripts'))
import bi_utils
API_BASE = '<api_base>'
TOKEN = '<token>'
PAGE_ID = '<page_id>'
API_DS_ID = '<已有API数据集ID>' # 可选:已有 API 数据集直接复用
bi_utils.API_BASE = API_BASE
bi_utils.TOKEN = TOKEN
def _key():
return f'{int(time.time()*1000)}_{random.randint(100000,999999)}'
# Step 1: 缓存已有组件(必须!防止 save_page 覆盖)
page = bi_utils.query_page(PAGE_ID)
tmpl = page.get('template') or []
if isinstance(tmpl, str): tmpl = json.loads(tmpl)
bi_utils._page_components[PAGE_ID] = tmpl
# Step 2: 获取/创建"示例数据集"分组
groups = (bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup').get('result') or [])
parent_id = next((g['id'] for g in groups if g.get('name') == '示例数据集' and g.get('dataType') is None), '0')
if parent_id == '0':
bi_utils._request('POST', '/drag/onlDragDatasetHead/addGroup', data={'groupName': '示例数据集'})
groups = (bi_utils._request('GET', '/drag/onlDragDatasetHead/getAllGroup').get('result') or [])
parent_id = next((g['id'] for g in groups if g.get('name') == '示例数据集' and g.get('dataType') is None), '0')
# Step 3: 创建 JSON 数据集(栅格1用)——数据放 querySql,禁止放 content
json_data = [
{'name': '北京', 'value': 180}, {'name': '上海', 'value': 250},
{'name': '广州', 'value': 138}, {'name': '深圳', 'value': 200},
{'name': '成都', 'value': 120}, {'name': '杭州', 'value': 160},
]
ds_resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '城市热力数据', 'code': f'city_heatmap_{int(time.time())}',
'dataType': 'json',
'querySql': json.dumps(json_data, ensure_ascii=False), # ✅ 数据放 querySql
'content': '', # ❌ content 无效,留空
'apiMethod': 'GET', 'parentId': parent_id,
'datasetItemList': [
{'fieldName': 'name', 'fieldTxt': '城市名称', 'fieldType': 'String', 'izShow': 'Y', 'orderNum': 0},
{'fieldName': 'value', 'fieldTxt': '热力值', 'fieldType': 'Integer', 'izShow': 'Y', 'orderNum': 1},
],
'datasetParamList': [],
})
result = ds_resp.get('result', {})
JSON_DS_ID = result.get('id') if isinstance(result, dict) else result
# Step 4: 加载 default_configs.json,深拷贝构建子组件 config
_cfg_path = os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references', 'scripts', 'default_configs.json')
with open(_cfg_path, encoding='utf-8') as f:
defaults = json.load(f)
GRID_H = 50 # 栅格高度(栅格单位)
# 栅格1: JHeatMap + JSON 数据集
cell1_cfg = copy.deepcopy(defaults.get('JHeatMap', {}))
cell1_cfg.update({
'dataType': 2, 'dataSetId': JSON_DS_ID,
'dataSetName': '城市热力数据', 'dataSetType': 'json',
'dataSetApi': '', 'dataSetMethod': 'GET', 'dataSetIzAgent': '',
'dataMapping': [{'filed': '维度', 'mapping': 'name'}, {'filed': '数值', 'mapping': 'value'}],
'fieldOption': [
{'fieldName': 'name', 'fieldTxt': '城市名称', 'fieldType': 'String'},
{'fieldName': 'value', 'fieldTxt': '热力值', 'fieldType': 'Integer'},
],
'dictOptions': {}, 'paramOption': [], 'chartData': '[]',
'background': '#FFFFFF', 'borderColor': '#E8E8E8',
'size': {'width': 900, 'height': GRID_H * 11},
})
cell1_cfg.setdefault('option', {}).setdefault('title', {})['text'] = '城市热力分布'
# 栅格2: JBar + API 数据集
cell2_cfg = copy.deepcopy(defaults.get('JBar', {}))
cell2_cfg.update({
'dataType': 2, 'dataSetId': API_DS_ID,
'dataSetName': '柱形图数据', 'dataSetType': 'api',
'dataSetApi': '<API_URL>', 'dataSetMethod': 'GET', 'dataSetIzAgent': '0',
'dataMapping': [{'filed': '维度', 'mapping': 'name'}, {'filed': '数值', 'mapping': 'value'}],
'fieldOption': [
{'fieldName': 'name', 'fieldTxt': '名称', 'fieldType': 'String'},
{'fieldName': 'value', 'fieldTxt': '数值', 'fieldType': 'Integer'},
],
'dictOptions': {}, 'paramOption': [], 'chartData': '[]',
'background': '#FFFFFF', 'borderColor': '#E8E8E8',
'size': {'width': 900, 'height': GRID_H * 11},
})
cell2_cfg.setdefault('option', {}).setdefault('title', {})['text'] = '数据统计柱形图'
# Step 5: 计算 Y 坐标(追加在最底部)
max_y = max((c.get('y', 0) + c.get('h', 0) for c in tmpl), default=0)
# Step 6: 构建 JGrid——🚨 child 每项必须含 w/x/h,否则子组件高度为 0 不可见
GRID_W = 24
grid_i = _key()
cell1_i = _key()
cell2_i = _key()
jgrid_comp = {
'component': 'JGrid', 'componentName': '栅格布局',
'visible': True, 'i': grid_i,
'x': 0, 'y': max_y, 'w': GRID_W, 'h': GRID_H,
'pcX': 0, 'pcY': max_y, 'pcW': GRID_W, # 🚨 必须填 pcX/pcY/pcW
'orderNum': len(tmpl) * 10 + 10,
'config': {
'w': GRID_W * 75, # 🚨 config.w = w*75(像素)
'h': GRID_H * 11, # 🚨 config.h = h*11(像素)
'size': {'width': GRID_W * 75, 'height': GRID_H * 11},
'option': {'card': {'title': '', 'extra': '', 'rightHref': '', 'size': 'default'}},
'child': [
{
'i': cell1_i, 'parentId': grid_i, # 🚨 parentId 必须是父 JGrid 的 i
'span': 12,
'w': 12, 'x': 0, 'h': GRID_H, # 🚨 必须含 w/x/h
'component': 'JHeatMap',
'config': cell1_cfg,
},
{
'i': cell2_i, 'parentId': grid_i,
'span': 12,
'w': 12, 'x': 12, 'h': GRID_H, # 🚨 必须含 w/x/h
'component': 'JBar',
'config': cell2_cfg,
},
],
},
}
# Step 7: 追加并保存(一次 save,避免乐观锁)
tmpl.append(jgrid_comp)
bi_utils._page_components[PAGE_ID] = tmpl
bi_utils.save_page(PAGE_ID)
print(f'JGrid 添加成功,预览: {API_BASE}/drag/page/view/{PAGE_ID}')
JTabs 嵌套 JGrid 示例
JTabs 的每个 Tab 内可放 JGrid(一个 Tab 内并排多列):
grid_i = bi_utils._gen_uuid()
tabs_i = bi_utils._gen_uuid()
jgrid_config = {
'w': 1800, 'h': 440,
'size': {'width': 1800, 'height': 440},
'option': {'card': {'title': '', 'extra': '', 'rightHref': '', 'size': 'default'}},
'child': [
{'i': bi_utils._gen_uuid(), 'parentId': grid_i, 'span': 12, 'component': 'JBar', 'config': {...}},
{'i': bi_utils._gen_uuid(), 'parentId': grid_i, 'span': 12, 'component': 'JPie', 'config': {...}},
]
}
jtabs_comp = {
'component': 'JTabs',
'componentName': '销售分析',
'i': tabs_i, 'x': 0, 'y': 40, 'w': 24, 'h': 40,
'orderNum': 20, 'visible': True,
'config': {
'w': 1800, 'h': 440,
'size': {'width': 1800, 'height': 440},
'option': {'title': '销售分析'},
'child': [
{'title': '柱图+饼图', 'i': grid_i, 'parentId': tabs_i,
'component': 'JGrid', 'config': jgrid_config},
{'title': 'Tab2', 'i': bi_utils._gen_uuid(), 'parentId': tabs_i,
'component': '', 'config': {}},
]
}
}
可用的快捷函数
API 初始化:
init_api(api_base, token)— 初始化 API 地址和 Token
页面管理:
create_page(name, style='default', theme='default')— 创建仪表盘query_page(page_id)— 查询页面详情list_pages(style='default')— 列表查询save_page(page_id)— 保存设计delete_page(page_id, physical)— 删除copy_page(page_id)— 复制
添加组件(栅格坐标):
add_number(page_id, title, x, y, w, h, value, prefix, suffix)— 数字指标add_chart(page_id, chart_type, title, x, y, w, h, categories, series, pie_data)— 图表add_table(page_id, title, x, y, w, h, columns, data)— 数据表格add_scroll_table(page_id, title, x, y, w, h, columns, data)— 滚动表格add_ranking(page_id, title, x, y, w, h, data)— 排行榜add_text(page_id, title, x, y, w, h, content, font_size, color)— 文本add_image(page_id, title, x, y, w, h, src)— 图片add_gauge(page_id, title, x, y, w, h, value, max_val, unit, color)— 仪表盘表盘add_liquid(page_id, title, x, y, w, h, value, color)— 水球图add_component(page_id, component, title, x, y, w, h, config)— 通用组件
核心踩坑速查
| 问题 | 核心规则 |
|---|---|
| 🚨 严禁 bi_utils.add_xxx + save_page | add_component 初始化空列表,save_page 覆盖已有组件。必须用 comp_ops.py add |
| 🚨 添加≥2个组件时严禁并行 add | 并行两个 add 进程读到相同 updateCount,第二个保存必被后端拒绝,组件永久丢失。必须用 batch-add --specs '[...]' 一次保存 |
| 🚨 add 命令后 chartData 为 [] | comp_ops.py 已改为从 default_configs.json 加载完整默认数据;出现空数据说明 default_configs.json 未 cp 到工作目录,cp 时必须一并复制 default_configs.json |
| 🚨 静态 chartData 禁止使用兜底数据 | default_configs.json 为空时 comp_ops.py 回落到内置占位数据(JBar→A/B/C/D/E)。必须从 data.ts 读取真实 chartData,通过 spec "config":{"chartData":[...]} 或 singleFile 脚本 _build_comp_config(comp_type, title, {"chartData": json.dumps(real_data)}) 覆盖 |
| 🚨 add_component 前必须缓存 template | 任何场景调用 add_component 前,必须先:page=bi_utils.query_page(PAGE_ID) → tmpl=page.get('template') or [](必须用 or [],不能用 .get('template',[])——空页面时 key 存在但值为 None,.get 默认值失效)→ bi_utils._page_components[PAGE_ID]=tmpl → 再调 add_component。漏写则所有已有组件被永久清空 |
| 🚨 删除前必须询问用户确认 | 除非用户明确说"删除/去掉/移除",禁止自行执行任何 delete 操作。删除不可逆 |
| POST /drag/page/edit 乐观锁 | 必须传 updateCount |
| chartData 必须是 JSON 字符串 | json.dumps(...) 后传入,不能是原生 list/dict |
| dataMapping 的 filed 拼写 | filed(不是 field,少一个 d) |
| 图表标题去重 | 图表组件 card.title='',只用 option.title.text |
| size 字段必须是像素 | config.size.width/height 是像素值。栅格转像素:w*75, h*11 |
| 组件 ID/名称字段 | ID 是 i(不是 id),名称是 componentName(不是 label/name) |
| template 字段 | query_page 返回组件列表在 template(已是 list),不是 pageTemplate。空页面时 template 为 None(非缺失键),必须用 page.get('template') or [],禁止 .get('template',[]) |
| bi_utils 初始化 | 直接赋值 bi_utils.API_BASE/TOKEN,无 init() 方法(有 init_api 封装) |
| Windows 命令 | 用 py 不是 python;所有脚本必须加 PYTHONIOENCODING=utf-8;脚本 print 禁用 emoji |
⚠️ comp_ops.py list 不是查页面列表 |
comp_ops.py list 是查某页面内的组件列表,page_id 必填,不传则报 error: page_id required。查所有页面:bi_utils._request('GET', '/drag/page/list', params={'pageNo':1,'pageSize':50}) |
⚠️ py -c 内 sys.path.insert(0, '$HOME/...') 无效 |
Git Bash $HOME 展开为 /c/Users/xxx,Windows Python 不认 Unix 格式路径,import bi_utils 报 ModuleNotFoundError。必须改用 PYTHONPATH 环境变量:SKILL_REFS="$HOME/.claude/skills/jimubi-dashboard/references"; PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py -c "import bi_utils; ..." |
| cp 目标路径格式 | Git Bash 必须用 Unix 格式 /c/Users/<用户名>/,不能用 C:/Users/ |
| cp 后必须 ls 验证 | cp 可能静默失败,必须 && ls *.py *.json 验证,否则 ModuleNotFoundError |
| cp 与 py 必须同轮 | cp 依赖文件和 py 执行必须在同一命令链,不能拆成两轮 |
| 写脚本用 Write 工具 | 禁止 bash heredoc(含单引号必报错) |
| FreeMarker SQL 禁止 bash 传递 | ${age} 被 shell 消费,</#if> 中的 > 被解释为重定向,必须用 --sql-file |
| 带 FreeMarker 的参数判空 | <#if isNotEmpty(age)>,禁止 age?? && age?length gt 0 |
| 🚨 comp_ops.py add 不支持 --create-sql | --create-sql/--sql-file/--ds-name/--db-source 均不存在,报 unrecognized arguments。应先 dataset_ops.py create-sql,再 batch-add --specs 传 "config" 绑定 |
| 🚨 SQL+批量图表禁止手写 option | 图表 option/background/size 等视觉配置必须来自 default_configs.json(深拷贝),禁止在 config 里手写 option。只需传 dataType/dataSetId/dataMapping/fieldOption 等数据绑定字段 |
| 🚨 queryFieldBySql result 是 dict 不是 list | fields_resp.get('result', []) 会得到 None 或 dict,报 TypeError: 'NoneType' object is not iterable。正确写法:(fields_resp.get('result') or {}).get('fieldList', []) |
| 🚨 签名算法不含时间戳 | X-Sign/V-Sign 的 MD5 字符串 = json_str + SECRET(无 + ts)。加了时间戳必报"签名不匹配"。时间戳只放在请求头 X-TIMESTAMP,不参与 MD5 计算 |
| 🚨 getAllGroup 字段是 name 不是 groupName | item.get('groupName') 永远 None → 无限触发 addGroup。必须用 item.get('name');addGroup 返回 null,必须重查取 id 作 parentId |
| 🚨 SQL数据集创建后必须: queryFieldBySql+edit+getAllChartData+dictOptions | 图表不渲染的根本原因:①缺少字段解析回写(fieldOption空);②config缺dictOptions。全流程自定义脚本4步不可少;推荐方式(dataset_ops+batch-add)手动指定字段可跳过queryFieldBySql |
| 🚨 JSON 数据集数据必须放 querySql,禁止放 content | dataType:'json' 类型数据集,后端 getAllChartData 读取 querySql 字段(JSON 数组字符串),content 字段完全无效——放 content 则预览和图表均返回 data:null。正确:'querySql': json.dumps(data_list);❌ 错误:'content': json.dumps(data_list) |
| 🚨 JSON 数据集不需要 dbSource/queryFieldBySql | dataType:'json' 无需数据库连接(dbSource 不传或留空),也无需调 queryFieldBySql(字段在 datasetItemList 中手动指定即可),getAllChartData 直接返回 querySql 中的 JSON 数据 |
| 🚨 "singleFile脚本"术语禁用于SQL场景 | singleFile 是文件数据集的 dataType 值(dataType:'singleFile',上传 Excel/CSV)。SQL 场景的自定义脚本应称为"全流程自定义脚本",以免混淆 |
| SQL 数据集无 dbSource | SQL 类型数据集必须指定 dbSource(数据库源 ID),否则报"数据源不存在" |
| datasetItemList 字段名 | 创建数据集用 datasetItemList,不是 onlDragDatasetItemList |
| API 数据集不需要 dbSource | API 类型 dbSource 设为 None,否则可能报错 |
| dataSetIzAgent 取值 | SQL 类型设 ''(留空);API 直连 '0';API 代理 '1' |
| 多系列 chartData 格式 | 需 type 字段区分系列:[{"name":"1月","value":10,"type":"系列A"}] |
| 字典翻译用 jimu_dict | 必须用 /jmreport/dict/*,禁止 /sys/dict/* |
| SQL 注入检测拦截 | information_schema/SHOW TABLES 会被后端 SqlInjectionUtil 拦截 |
| SQL 最大返回 1000 条 | getChartData 方法限制最大 1000 条记录 |
| queryFieldBySql 需签名 | 该接口带 @SignatureValidation,需要 X-Sign/V-Sign/X-TIMESTAMP |
| API 数据集 /add 字段名 | datasetItemList(不是 onlDragDatasetItemList);/add 后无需再 queryById+edit 回写 |
| 数据源 /add result 类型 | 数据源 /add 的 result 是字符串 ID;数据集 /add 的 result 是完整 dict(用 .get('id') 取) |
| response null 处理 | result=null 时 .get('result',{}) 返回 None,必须用 (resp.get('result') or {}) |
| componentName 必须用中文名 | 批量生成时图层名用中文(如"基础柱形图"),禁止用 compType(JBar/JPie) |
| singleFile 场景禁止 comp_ops.py --dataset-name | 按索引映射导致字段错乱,必须在同一脚本内用 bi_utils.add_component() 显式语义映射 |
| 🚨 singleFile code 必须等于 table_name(jmf.xxx) | chart GROUP BY 查询使用 code 作为表名,code 与 querySql 中表名不一致则查询错误表。正确:code=table_name(如 jmf.Sheet1_default_excel),querySql=f'select * from {table_name}'。❌ 错误:code='sf_1778313720' → 生成 FROM sf_1778313720 而非正确表 |
| 🚨 singleFile 删除数据集会清空 dbUrl + H2 表 | 删除任意 singleFile 数据集后,后端同步清空该 page 对应文件数据源的 dbUrl(置为 [])并销毁 H2 表,导致同 page 其他 singleFile 数据集查询报 Object not found。files_ops.py 的回退路径(queryFileFieldBySql 返回空时创建 __tmp__ 临时数据集再 DELETE)踩中此坑。修复:临时数据集不删除,改用 edit API 重命名+更新字段后直接复用;自定义脚本中同理,禁止在有其他 singleFile 数据集存活时执行 DELETE |
| singleFile 文件上传用 requests.post | bi_utils._request() 不支持 files 参数 |
| 🚨 文件数据集 dataSetIzAgent 必须区分 | FILES:'1'(后端代理);singleFile:''(空字符串,非 '0')。填错导致图表无数据 |
| 🚨 文件列名必须英文 | H2/Calcite 不支持中文列名,上传前将 Excel/CSV 列名改为英文 |
| 🚨 singleFile datasetItemList 的 fieldName 必须是表中真实列名,禁止用 SQL 别名(2026-05-09) | 后端会用 datasetItemList[].fieldName 重建执行 SQL(SELECT {f1},SUM({f2}) FROM table GROUP BY {f1})。若 fieldName 是 SQL 别名(如 name/cnt),H2 表中不存在这些列,必报 Column 'name' not found。正确:fieldName 必须是 Excel 真实列名(如 bsl_pi_name/bsl_amount);SQL 用 select * from {table_name},不用 GROUP BY 聚合 |
| 🚨 singleFile 禁止在 SQL 中使用无别名聚合函数(count(*) 等)(2026-05-09) | count(*) 无别名时 H2 引擎返回 EXPR$1 作为列名,这是 H2 内部名,不是表中真实列。若把 EXPR$1 写入 fieldName,后端重建 SQL 变成 SUM(EXPR$1) 再报错。singleFile 数据集禁止 GROUP BY,直接 select * from {table_name} 取全量数据 |
| 🚨 文件字段解析必须用 queryFileFieldBySql | 单/多文件数据集字段解析调用 /queryFileFieldBySql,参数 dbCode=PAGE_ID;用 queryFieldBySql 会失败 |
| FILES result 类型 | files/get 的 result 是 dict,json.loads(result.get('dbUrl','[]')) 取文件列表 |
| yapi_ops.py create-mock 参数 | 接口标题参数是 --title,不是 --name |
| subprocess 调用 yapi_ops.py | 子命令必须在 YAPI_BASE 之前:['py','yapi_ops.py', cmd, YAPI_BASE, EMAIL, PWD] |
| YApi mock 前先 list | 直接创建会重复,先 yapi_ops.py list 查已有接口,复用已存在的 |
| 🚨 YApi mock 自定义 res_body 必须是纯数组 | 手写 mock body 时禁止包装成 {"result":{"data":[...]}} 格式,必须直接是 [{"name":"...","value":...}] 纯数组。包装格式导致 getAllChartData 返回 data: null,所有图表显示"暂无数据"(2026-04-24 实踩) |
🚨 YApi /api/interface/up 用 id 字段不是 _id |
更新 mock 时 payload 中接口 ID 必须是 id(无下划线),用 _id 报 errcode=400: 请求参数 data 应当有必需属性 id(2026-04-24 实踩) |
🚨 API 数据集 izAgent='0' 时 getAllChartData 返回 data: null 是正常行为 |
前端直连模式下后端不代理外部 URL,服务端 getAllChartData 返回 null 是预期结果。数据由浏览器直接从 mock URL 获取。禁止因此修改 izAgent 或删除重建数据集(2026-04-24 实踩) |
| 🚨 仪表盘需求必须先调用 jimubi-dashboard skill | 收到任何仪表盘相关请求,第一步必须 Skill jimubi-dashboard,再在 skill 上下文中执行。禁止读 memory 凭据后直接用 curl/bash/Agent 子代理探测 API 端点,无论操作多简单(2026-04-24 实踩:联动配置需求未调 skill,自行探索接口路径浪费大量时间) |
| 写 Java 接口 | 禁止自行 Grep 搜索 Controller 文件,必须问用户路径;完成后脚本末尾必须输出接口 URL + 重启提示 |
| lockd/解锁组件 | 锁定字段是顶层 disabled,解锁必须用自定义脚本操作顶层:comp['disabled']=False; comp['selected']=False |
| 多图表+联动场景 | 必须用 multi_chart_linkage.py,禁止逐个调 comp_ops.py |
| 🚨 联动源图表 fieldOption 必须用前端 DataSource.vue 格式 | 配置联动时 config.fieldOption 必须用 {value:fieldName, label:fieldTxt, text:fieldTxt, show:'Y', type:fieldType}。用 {fieldName,fieldTxt,fieldType} 时,联动设置弹窗"映射字段"列显示为空(LinkConfig.vue 第322行:field.value==item.mapping,fieldName 键不等于 value 键 → filter 返回 undefined)。联动功能本身仍可运行,只是 UI 弹窗看不到映射字段。❌ {'fieldName':'name','fieldTxt':'性别','fieldType':'String'};✅ {'value':'name','label':'性别','text':'性别','show':'Y','type':'String'} |
| 🚨 联动目标数据集必须同时满足三个条件才能真正刷新 | ① SQL 含 FreeMarker:<#if isNotEmpty(name)>WHERE f='${name}'</#if>;② 数据集 datasetParamList 声明参数:{paramName:'name',...};③ 目标图表 config.paramOption 包含同名参数。三者缺一,点击源图表后目标图表不刷新(联动配置看起来正常,但无效果) |
| 🚨 SQL GROUP BY 禁止用 SELECT 别名 | SELECT DATE_FORMAT(x,'%Y') as name ... GROUP BY name 在 MySQL 某些模式下报 bad SQL grammar [...](2026-04-24 实踩)。必须用实际表达式:GROUP BY DATE_FORMAT(x,'%Y'),禁止用别名 name 做分组列 |
| 🚨 SQL 中使用 IFNULL/COALESCE 被后端拦截导致报错 | IFNULL(col,'default') / COALESCE(col,'default') 等函数写在 SELECT 中会被后端 SqlInjectionUtil 检测拦截(2026-04-24 实踩)。应改用 WHERE col IS NOT NULL 在 WHERE 子句过滤空值,避免在 SELECT 中使用空值包装函数。❌ SELECT IFNULL(style,'default') AS name;✅ SELECT style AS name ... WHERE style IS NOT NULL |
| 🚨 联动端到端完整配置三要素(缺一不可) | 创建带联动的图表对时,必须同时满足:① 联动源图表 fieldOption 用 {value, label, text, show:'Y', type} 格式(否则设置 UI 映射字段列为空);② 目标数据集 SQL 含 FreeMarker:<#if isNotEmpty(name)>AND col='${name}'</#if>,且 datasetParamList 声明参数;③ 目标图表 config.paramOption 包含同名参数 {paramName:'name',paramTxt:'...',paramType:'String',paramValue:''}。三者不会互相检测,全部缺失时联动外观正常但点击无效果 |
| 设计器表单端点 | 固定端点 /desform/api/list/options、/desform/api/fields/{tableName},禁止盲猜 |
| Online表单 dataType=4 | 最易漏!漏写则 dataType=0,读不到表单数据 |
| 🚨 X-Low-App-ID 必须是应用 ID,不是仪表盘页面 ID | QQY URL 格式:/myapp/{appId}/drag/{pageId},appId ≠ pageId!X-Low-App-ID 填 appId,query_page 传 pageId。用错后 /desform/api/list/options 返回空列表,极易误判为"无表单"。若用户只给一个 ID,必须问清是应用 ID 还是页面 ID,或请提供完整 URL |
| 🚨 表单列表返回空时第一反应是检查 appId,禁止从现有组件推断表单 | 表单接口返回 [] 最常见原因:X-Low-App-ID 用了页面 ID。排查:① 验证 appId 是否正确 → ② 确认无误后告知用户。任何情况禁止自行决定使用哪个表单 |
| 🚨 QQY dataType=4 必须包含 compStyleConfig + analysis | 缺少或为 {} 时前端 useEChartsNew.ts 访问 .summary.showTotal / .showUnit.position 抛 TypeError 白屏。禁止写 'compStyleConfig': {},必须用完整默认值对象(见"组件 config 结构(dataType=4)"章节) |
🚨 修复 compStyleConfig 时用 not cfg.get() 而非 not in |
空对象 {} 是 truthy 的 key,'compStyleConfig' not in cfg 为 False(不覆盖)。必须用 if not cfg.get('compStyleConfig'): 检测空值 |
| 仪表盘组件颜色设置 | JPie/JRose/JLine/JArea/JMixLineBar 等组件颜色必须用 option.customColor,格式 [{"color1":"#FF","color":"#FF"}],option.color 无效 |
| 🚨 饼图/折线图等颜色禁止写在 config 顶层 | cfg['customColor'](config 顶层)前端不读取,颜色不生效。必须写在 option.customColor:cfg['option']['customColor'] = [{"color":"#FF0000","color1":"#FF0000"},...]。❌ 错误:cfg['customColor'] = [...];✅ 正确:cfg['option']['customColor'] = [...] |
| api.jeecg.com 是 YApi 服务器 | 禁止对其尝试 JeecgBoot /sys/login,YApi 登录路径是 /api/user/login |
| 组件默认背景色 | config.background 必须为 #FFFFFF(白色),禁止使用 #FFFFFF00(透明)或 transparent |
| 坐标单位 | 仪表盘用栅格坐标(24列),不是像素 |
| 总宽度限制 | 同行组件 w 之和 ≤ 24 |
| 🚨 QQY dataType=4 filter 必须含 conditionFields | 缺少 filter.conditionFields 导致 common.ts:1657 抛 TypeError: Cannot read properties of undefined (reading 'forEach'),同时设置弹窗无法打开。所有 dataType=4 组件 filter 必须写:{'queryField': '', 'queryRange': 'all', 'conditionFields': []} |
| 🚨 QQY JBar/折线/散点等笛卡尔坐标图必须在 option 显式写 series 类型 | QQY dataType=4 的柱形/折线/面积/散点/条形图,若 option 中没有 series: [{type: 'bar/line/scatter'}],ECharts 报 Unknown series undefined,图表只显示坐标轴不渲染数据。必须补充:'series': [{'type': 'bar'}](或 line/scatter)+ xAxis + yAxis + grid |
| 🚨 QQY isGroup 图表分组字段是 typeFields 而非 groupFields | dataType=4 多系列/分组图表(JStackBar/JMultipleBar/JRadar/JPivotTable 等)的分组字段键名是 typeFields,不是 groupFields,写错则分组无效 |
| 🚨 isLowApp 禁止写入数据库 | isLowApp: True 是前端引擎切换标识(DragEngineQqyun.vue 判断),不存数据库,创建/保存页面的 body 中禁止传此字段。lowAppId 才是数据库字段,必须在 body 中传 |
| 🚨 QQY analysis 字段默认值错误 | 正确默认值:{'showData': 1, 'isRawData': True, 'showMode': 1, 'isCompare': False, 'izTimeOut': False, 'showFields': [], 'trendType': '1', 'timeOut': 0}。❌ 旧错误值:isRawData=False, showMode=0, showData=0, trendType='mom'——这些值会导致数据展示模式异常 |
| 🚨 QQY filter 必须含 conditionMode:"and" | filter 缺少 conditionMode 字段会导致筛选条件模式未定义,设置弹窗行为异常。正确完整结构:{'queryField': 'create_time', 'queryRange': 'all', 'conditionMode': 'and', 'conditionFields': [], 'customTime': []} |
| 🚨 conditionFields 条目必须同时含 val + fieldValue + condition | 通过 API 写入筛选条件时,conditionFields 每条必须包含:val(显示值)、fieldValue(后端实际查询值,缺少时条件不生效)、condition(条件类型枚举,如 '4'=包含/LIKE)。仅有 val 无 fieldValue 则查询不执行过滤。完整结构:{'fieldName':'xxx','fieldTxt':'订单名称','fieldType':'string','widgetType':'input','rule':'LIKE','condition':'4','val':'华为','fieldValue':'华为','options':[],'fieldShow':True,'customDateType':''} |
| 🚨 QQY JFilterQuery 添加前必须询问用户三项信息 | 禁止直接 comp_ops.py add JFilterQuery 后结束。必须先询问:①联动哪几个图表(列出当前页面图表供选择);②添加几个查询条件;③每个条件关联哪个字段。收集完信息后一次性配置完整 config。详见 references/qqy-guide.md 「QQY 查询条件完整配置流程」 |
| 🚨 QQY JFilterQuery config 缺少 4 个必填字段 | conditionFields(顶层)/ filter / linkageConfig / chartData(JSON字符串)四个字段缺一不可。缺少任意一个:条件不显示 / 筛选面板报错 / 查询不触发刷新 / 前端解析失败。完整结构见 references/qqy-guide.md |
| 🚨 QQY JFilterQuery chartData 必须是 JSON 字符串 | chartData 存储的是 json.dumps([...]) 的字符串,不能是 Python list。写成 list 则前端 JSON.parse 失败,查询条件无法渲染 |
| 🚨 QQY JFilterQuery conditionFields 必须在顶层和 filter 内各写一份 | config.conditionFields 和 config.filter.conditionFields 必须同时存在且内容相同。只写其中一处则另一处报 TypeError |
| 🚨 QQY JFilterQuery 禁止修改目标图表 | 联动完全由 JFilterQuery 自身的 linkageConfig 驱动,目标图表(JBar/JLine等)无需添加 drillData 或任何修改 |
| 🚨 QQY JFilterQuery relationChartList.options 需过滤类型 + 去除 dataType:null | /desform/api/fields 返回的字段需过滤 SKIP_TYPES(file-upload/imgupload等),且删除 options.dataType: null(与前端参考JSON一致,否则字段对比异常) |
| 🚨 QQY filterField 不能为空数组 | filterField: [] 导致图表设置面板无可选筛选字段。必须填入表单所有字段,每条包含 fieldShow: True、完整 options 对象;系统字段(create_by/update_by/create_time/update_time/bpm_status)也要包含;日期字段加 customDateType: '1',人员字段加 customDateType: '3' |
| 🚨 QQY nameFields/typeFields 必须含 fieldShow:True | 字段条目缺少 fieldShow 属性会导致字段在设置面板中不可见/不可操作。每个 nameFields/typeFields 条目必须加 'fieldShow': True |
| 🚨 QQY valueFields 必须含 fieldShow:True + groupField:"" | valueFields 条目除 fieldShow: True 外还必须含 'groupField': '',缺少 groupField 导致聚合分组配置失效 |
| 🚨 QQY sorts 必须含 type:"" 字段 | sorts: {'name': ''} 不完整,必须写 sorts: {'name': '', 'type': ''},缺少 type 字段导致排序设置面板异常 |
| 🚨 QQY JPivotTable 缺 pivotTable 子配置 → "暂无数据" | JPivotTable 没有 pivotTable 顶层配置对象时,透视表始终显示"暂无数据"。必须在 config 顶层加:'pivotTable': {'columnSummary': {'controlList': [{'showName':'','show':True,'totalType':'sum','position':'2','key':'<值字段名>'}], 'name':'列汇总','location':'right'}, 'lineSummary': {'controlList':[...],'name':'行汇总','location':'bottom'}, 'unitList':[{'showName':'','unit':'','key':'<值字段名>'}], 'showLineCount':0,'showColumnCount':0,'showColumnTotal':False,'showLineTotal':False} |
| 🚨 QQY 地图组件缺 commonOption → 地图不加载 | JAreaMap/JBubbleMap/JHeatMap/JBarMap 的 config 顶层必须含 commonOption,缺失导致地图样式/颜色完全失效。必须加:'commonOption': {'barSize':10,'gradientColor':False,'breadcrumb':{'drillDown':False,'textColor':'#000000'},'areaColor':{'color1':'#f7f7f7','color2':'#fcc02e'},'barColor':'#fff176','barColor2':'#fcc02e','inRange':{'color':['#04387b','#467bc0']}} |
| 🚨 QQY 地图 option geo 必须用旧版 ECharts 格式 | handleMapWarn(useEChartsMap.ts:1145)处理 itemStyle.normal/emphasis 嵌套格式;使用新版 itemStyle.areaColor 直接写法样式失效。正确格式:'geo':{'top':30,'zoom':1,'roam':False,'itemStyle':{'normal':{'areaColor':'#f7f7f7','borderColor':'#b0b5c1','borderWidth':0.5},'emphasis':{'areaColor':'#fcc02e'}},'label':{'emphasis':{'show':True,'color':'#000'}}} |
| 🚨 QQY 地图 visualMap 必须含 seriesIndex | ECharts 要求 heatmap series 必须有 visualMap 明确引用其 seriesIndex,否则报 Heatmap must use with visualMap。各类型 seriesIndex:JAreaMap→[0](show:False),JBubbleMap→[1](show:False),JHeatMap→[1](show:True),JBarMap→[0](show:False)。缺 seriesIndex 或值错误→热力图崩溃 |
| 🚨 QQY 所有4种地图 option 都必须含 area 字段 | 不仅 JBubbleMap,全部4种地图的 config.option?.area?.markerType 都会被读取作为 series[0].type;缺失→type=undefined→[ECharts] Unknown series undefined。必须加:'area':{'markerType':'effectScatter','markerColor':'#DDE330','shadowBlur':10,'markerCount':5,'markerOpacity':1,'scatterLabelShow':False,'value':['china'],'name':['中国']} |
| 🚨 QQY 地图和表格 config 需要 seriesType/assistTypeFields/assistYFields | JPivotTable、JAreaMap、JBubbleMap、JHeatMap、JBarMap 的 config 顶层缺少这3个字段会导致多系列/辅助轴配置失效。必须加:seriesType:[{series:'1',type:'bar'},{series:'2',type:'bar'},{series:'',type:'bar'}];assistTypeFields:[{fieldName:'create_time',fieldTxt:'创建时间',options:{},fieldType:'date',widgetType:'date',customDateType:'3'}];assistYFields:[val_field_obj] |
| 🚨 QQY 地图 valueFields 必须用表单实际数值字段,非固定 record_count | record_count 只是"计数"的一种指标,不是地图 valueFields 的固定值。地图 valueFields 应使用用户表单中的数值类型字段(num_fields 中的字段);仅在表单无数值字段时才以 record_count 兜底 |
| 🚨 QQY JPivotTable 透视表必须含 pivotTable 子配置 | 缺少 pivotTable 顶层配置时透视表始终"暂无数据"。必须动态从 valueFields 构建:{'columnSummary':{'controlList':[{'showName':'','show':True,'totalType':'sum','position':'2','key':k}],'name':'列汇总','location':'right'},'lineSummary':{'controlList':[{'showName':'','show':True,'totalType':'sum','key':k}],'name':'行汇总','location':'bottom'},'unitList':[{'unit':'','numberLevel':'','position':'suffix','decimal':0,'key':k}],'showLineCount':0,'showColumnCount':0,'showColumnTotal':False,'showLineTotal':False};analysis 必须加 compareType:'' |
| 🚨 QQY 地图 option 不能为空 {} | 地图 option 必须包含完整 geo/area/series/visualMap 结构,否则地图不渲染。最小可用结构(以 JAreaMap 为例):`{'drillDown':False,'area':{'name':['中国'],'value':['china'],'markerType':'effectScatter','markerColor':'#DDE330','shadowBlur':10,'markerCount':5,'markerOpacity':1,'scatterLabelShow':False,'shadowColor':'#DDE330'},'geo':{'top':30,'zoom':1,'roam':False,'itemStyle':{'normal':{'areaColor':'#f7f7f7','borderColor':'#b0b5c1','borderWidth':0.5},'emphasis':{'areaColor':'#fcc02e'}},'label':{'emphasis':{'show':T |
Content truncated for page performance. Open the source repository for the full SKILL.md file.