| 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-bigscreen skill。
⚠️ 强制规则:所有仪表盘相关操作必须优先通过本 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 中已完整列出,禁止用探索命令去"验证",直接写脚本执行。探索命令还会引入 $HOME Unix 路径格式问题导致 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 应留空(标题由 ECharts option.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'
TENANT_ID = '1'
PAGE_ID = '已有页面ID'
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,
})
标准仪表盘创建时不传 lowAppId。
证据:DragEngine.vue onMounted 中执行 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)
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:
print(m['id'], m.get('type'), m.get('menuName'), m.get('menuUrl'))
备用方式:通过 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', [])
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/appType
nameFields/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 []
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,
'parentId': GROUP_ID,
'menuName': '仪表盘名称',
'type': 'drag',
'menuUrl': PAGE_ID,
'appId': APP_ID,
'orderNum': 4,
}
r = requests.put(f'{API_BASE}/online/lowAppMenu/edit', headers=HEADERS, json=body)
注意:请求头必须包含 X-Low-App-ID 和 X-Tenant-Id,否则鉴权失败。
🚨 QQY 统计图表四步询问流程(强制,每个统计图表都必须执行)
每次在 QQY 仪表盘中添加任意一个统计图表(30个范围内),必须严格执行以下四步,禁止自行假设表单或字段:
Step 0:询问使用当前应用还是其他应用的表单
询问用户:"请问使用当前应用下的表单,还是其他应用下的表单?"
- 当前应用 → 用当前 APP_ID 继续 Step A
- 其他应用 → 询问"请提供应用名称或应用ID",等待用户提供后继续
Step A:同时查询普通表单和聚合表,分两组展示 → 询问用户选择
GET /desform/api/list/options?appId={APP_ID}
GET /drag/onlDragTableRelation/list?pageSize=20
Step B:查询并展示字段 → 询问用户选择维度/数值字段
GET /desform/api/fields/{formCode}
GET /drag/onlDragTableRelation/getFields/{aggregationId}
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',
'formId': '1207232765004226560',
'formName': '[聚合] 测试',
'tableName': '1207232765004226560',
'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], delete(有 bug,禁用), bind。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,自定义脚本时加载 |
使用前准备(所有脚本通用):
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 时会用空列表覆盖页面已有的全部组件,造成不可恢复的数据丢失。
使用前准备:
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" ...
核心命令(坐标为栅格单位,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=新标题"
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=个"
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
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}
]'
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$COMP_OPS" move $API_BASE $TOKEN $PAGE_ID --name "组件名" --x 0 --y 17
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"
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"
PYTHONIOENCODING=utf-8 PYTHONPATH="$SKILL_REFS:$SKILL_REFS/scripts" py "$SKILL_REFS/scripts/dataset_ops.py" test $API_BASE $TOKEN --id "DS_ID"
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 命令行传递。
import sys, os
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
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
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
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': []
})
result = ds_resp.get('result', {})
DS_ID = result.get('id') if isinstance(result, dict) else result
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': []})
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)
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)]
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
}
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"
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
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 "目标组件名"
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 的命令):
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
"
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"
核心命令:
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
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 "饼图名"
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
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 = {
'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': ['总计', '已用'],
'JStackBar': ['分组', '维度', '数值'], 'JMultipleBar': ['分组', '维度', '数值'],
'JNegativeBar': ['分组', '维度', '数值'], 'JPercentBar': ['分组', '维度', '数值'],
'JMixLineBar': ['分组', '维度', '数值'], 'JMultipleLine': ['分组', '维度', '数值'],
'DoubleLineBar': ['分组', '维度', '数值'],
'JTable': ['名称', '数值'], 'JCommonTable': ['名称', '数值'],
'JScrollTable': ['名称', '数值'], 'JScrollRankingBoard': ['名称', '数值'],
'JNumber': ['数值'],
'JLiquid': ['总量', '当前'],
}
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
sys.path.insert(0, os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'jimubi-dashboard', 'references'))
from bi_utils import *
init_api('<api_base>', '<token>')
page_id = create_page('运营数据看板', style='default', theme='default')
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)
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': '显示文本'},
'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',
'dataSetApi': 'SELECT ...',
'dataSetMethod': 'GET',
'dataSetIzAgent': '',
'dataMapping': [
{'filed': '维度', 'mapping': 'name'},
{'filed': '数值', 'mapping': 'value'},
],
'fieldOption': [...],
'dictOptions': {},
'paramOption': [],
'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"
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"
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>'
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
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),
'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
spec_config = {
'dataType': 2,
'dataSetId': 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': '[]',
}
数据集踩坑:
| 问题 | 原因 | 解决 |
|---|
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)
result = upload_file(FILE_PATH, PAGE_ID, is_single=True)
file_list = json.loads(result['result']['dbUrl'])
table_name = file_list[0]['name']
fields_resp = bi_utils._request('POST', '/drag/onlDragDatasetHead/queryFileFieldBySql', data={
'sql': f'select * from {table_name}',
'dbCode': PAGE_ID,
})
fields = fields_resp.get('result', [])
ds = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '销售数据(单文件)',
'code': table_name,
'dataType': 'singleFile',
'dbSource': PAGE_ID,
'querySql': f'select * from {table_name}',
'content': json.dumps(file_list, ensure_ascii=False),
'apiMethod': 'GET',
'datasetItemList': fields, 'datasetParamList': [],
})
config = {
'dataType': 2, 'dataSetType': 'singleFile',
'dataSetId': ds['result']['id'],
'dataSetApi': f'select * from {table_name}',
'dataSetIzAgent': '',
'dataMapping': [{'filed': '维度', 'mapping': 'col1'}, {'filed': '数值', 'mapping': 'col2'}],
}
创建多文件数据集(FILES)
upload_file(r'<file_path>', PAGE_ID)
upload_file(r'<file_path>', PAGE_ID)
files = bi_utils._request('GET', '/jmreport/source/datasource/files/get', params={'reportId': PAGE_ID})
file_list = json.loads(files['result']['dbUrl'])
ds = bi_utils._request('POST', '/drag/onlDragDatasetHead/add', data={
'name': '多文件数据集', 'code': 'multi_files',
'dataType': 'FILES',
'dbSource': PAGE_ID,
'querySql': f'select name, value from {table_name} order by value desc',
'datasetItemList': [...], 'datasetParamList': []
})
config = {
'dataType': 2, 'dataSetType': 'FILES',
'dataSetId': ds['result']['id'],
'dataSetApi': f'select * from {table_name}',
'dataSetIzAgent': '1',
'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 = '...'
from bi_utils import *
init_api('<api_base>', '<token>')
页面数据与组件字段映射(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', [])
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',
},
'child': [
{
'title': 'Tab1',
'i': bi_utils._gen_uuid(),
'parentId': jtabs_i,
'component': 'JBar',
'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数据集)
import sys, os, json, time, random