@guandata/guanvis 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## @guandata/guanvis 0.1.30 - 2026-07-08
4
+
5
+ - 支持 checkout 线上页面到本地工程,并增强已有仪表板的编辑、差异查看和回写流程。
6
+ - 支持动态维度、动态指标和拆分图表相关构建能力,提升复杂图表生成覆盖面。
7
+ - 新增筛选器分组和开关类检查能力,页面交互配置更容易验证。
8
+ - `pack` / `preview` / `lint` 诊断增强,可提前发现资源 ID、布局、联动和覆盖相关问题。
9
+
3
10
  ## @guandata/guanvis 0.1.29 - 2026-07-01
4
11
 
5
12
  - 支持筛选器级联联动,页面中的筛选器可以按上游筛选结果继续约束下游筛选项。
package/README.md CHANGED
@@ -51,6 +51,13 @@ guanvis publish ./my_dashboard/ --allow-overwrite
51
51
 
52
52
  ## 版本更新
53
53
 
54
+ ### @guandata/guanvis 0.1.30
55
+
56
+ - 支持 checkout 线上页面到本地工程,并增强已有仪表板的编辑、差异查看和回写流程。
57
+ - 支持动态维度、动态指标和拆分图表相关构建能力,提升复杂图表生成覆盖面。
58
+ - 新增筛选器分组和开关类检查能力,页面交互配置更容易验证。
59
+ - `pack` / `preview` / `lint` 诊断增强,可提前发现资源 ID、布局、联动和覆盖相关问题。
60
+
54
61
  ### @guandata/guanvis 0.1.29
55
62
 
56
63
  - 支持筛选器级联联动,页面中的筛选器可以按上游筛选结果继续约束下游筛选项。
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guandata/guanvis",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "观远 BI Card/Page 生成工具 - 通过 JS DSL 创建图表和仪表板",
5
5
  "bin": {
6
6
  "guanvis": "bin/run.js"
@@ -9,7 +9,7 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
9
9
  这是一个执行型 skill,用于通过 AI 生成的 JS 脚本创建 BI Card 和 Page 资源。
10
10
 
11
11
  - AI 生成 `card_*.js` 定义各个 Card 图表;`page.js` 将多个 Card 组装成仪表板。
12
- - `schema.js` 由 `init` 命令自动生成,定义数据集字段信息(不允许 AI 修改);指标卡片使用 `metric-init` 生成 `metrics.js`。
12
+ - `schema.js` 由 `init` `checkout` 命令生成,定义数据集字段信息(不允许 AI 修改);checkout 生成 schema 是 best-effort,失败不会阻断 checkout。指标卡片使用 `metric-init` 生成 `metrics.js`。
13
13
  - 框架内置验证规则,在 pack/publish 时检查字段数量、zone 兼容性、必填字段等。
14
14
  - 认证通过 `guancli` 共享配置自动获取。
15
15
 
@@ -17,8 +17,10 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
17
17
 
18
18
  把可视化生成当作源文件驱动的构建流程:本地 JS DSL 是可编辑事实源,payload、ZIP、线上 Card/Page 都是从源文件生成的派生产物。
19
19
 
20
- - `schema.js` 是由 `init` 生成的数据集事实快照,不手改;数据集字段变化时重新运行 `init`。`metrics.js` 是由 `metric-init` 生成的指标事实快照,不手改;指标口径、适用维度或格式变化时重新运行 `metric-init`。
20
+ - `schema.js` 是由 `init` / `checkout` 生成的数据集事实快照,不手改;数据集字段变化时重新运行对应命令。`metrics.js` 是由 `metric-init` 生成的指标事实快照,不手改;指标口径、适用维度或格式变化时重新运行 `metric-init`。
21
21
  - `card_*.js`、`selector_*.js`、`page.js` 是可编辑源文件;`*_package.zip`、preview JSON、publish 后线上资源都是派生产物。
22
+ - 已有线上仪表板用 `guanvis checkout <pgId> -d <dir>` 拉成可编辑工程:checkout 会尽量生成 `schema.js`,但数据集无权限/已删除/跨环境残留时只 warning 并继续生成可编辑工程;Card 用 `attachCard(cdId, ".guanvis/base/cards/<cdId>.json")` 接受线上 JSON 作为基线;Page 生成 `createPage(...).setBasePath(...).placeCard("cdId", ...)` 脚本,尽量反编译根布局里的 Tab/CardGroup/AreaTitle/SelGroup 和快捷筛选组,并保留 parentDirId。checkout 工程只用于修改指定 Page,不用于复制新 Page;遇到嵌套布局组件等当前 DSL 不能安全表达的结构,checkout 会直接失败,不生成会清结构的工程。
23
+ - **Checkout JSON 红线**:`.guanvis/raw/**`、`.guanvis/base/**`、`.guanvis/manifest.json` 是 checkout 生成物和只读快照,不是可编辑源文件。Agent 不得修改这些 JSON,也不得复制一份 checkout JSON 后改副本来创建新资源;`attachCard(cardId, jsonPath)` 只能引用 manifest 记录的原始 base-card,且 `cardId` 必须与该 JSON 的资源 ID 一致。现有卡片修改必须通过 `attachCard(...).setName()/updateMetric()/addMetric()/removeMetric()/addLink()` 等 DSL 操作表达;字段整体重建才用 `setRows()` / `setMetrics()`,这些整 zone 替换会触发 warning,默认应优先用 `update*/patch*/add*/remove*/move*`。Page 快捷筛选区修改用 `createPage(...).setBasePath(...).setFilterLayout()/clearFilterLayout()/addFilterLayoutItem()/insertFilterSelector()/removeFilterSelector()/moveFilterSelector()` 表达;筛选器在快捷筛选区和画布之间移动时优先用 `moveFilterSelectorToCanvas()` / `moveCanvasSelectorToFilter()`,筛选器组用 `moveSelectorGroupToCanvas()` / `moveCanvasSelectorGroupToFilter()`。新增卡片必须使用 `createCard()` / `createSelector()` 等工厂函数。
22
24
  - Card/Page 描述也是 JS 源文件的一部分:新建看板或改版时,在 `card_*.js` / `page.js` 中写 `.setDescription(...)`,再通过 `preview`/`pack`/`publish` 从 JS 源文件生成并发布资源。
23
25
  - 写脚本前先形成 dashboard contract:目标用户、业务问题、使用的数据集、核心指标、维度拆解、筛选器、页面结构和验证方式。
24
26
  - 每次生成都针对明确目录或明确子目录;不要把 unrelated 示例、旧包或临时 ZIP 混入同一个发布目标。
@@ -29,7 +31,9 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
29
31
 
30
32
  ## AI Quick Reference(速查,详细说明见按需参考资料)
31
33
 
32
- 1. **工厂函数**:数据集图表用 `createCard()`;指标平台指标卡片用 `createMetricChart()`;筛选器/文本/图片/杜邦/Tab/页面用 `createSelector()` / `createTextCard()` / `createImageCard()` / `createDuPontChart()` / `createAreaTitle()` / `createCardGroup()` / `createTab()` / `createPage()`,不要 `new XxxBuilder()`
34
+ **Checkout/attachCard 速记**:`attachCard(cardId, jsonPath)` 是“base JSON + 链式 DSL 操作 = 目标 JSON”。它不会修改 base JSON,重复执行同一组 JS 操作应产出相同 payload。已有卡片可继续串接常用 `createCard` 后续操作,包括标题/描述、`setRawSettings`、图例/标签/坐标轴/表格/拆分等视觉设置。Zone 操作按链式顺序真实执行:`addRow/addMetric/...` 追加字段,`insertMetric(index, field)` 插入字段,`removeMetric(selector)` 删除字段并保守清理其它 zone 中同字段引用,`moveMetric(selector, index)` 调整顺序,`updateMetric(selector, patch)` / `patchMetric(...)` 修改已有字段属性并保留未设置字段配置;`setRows/setMetrics/...` `clearRows/clearMetrics/...` 是整 zone 重建,不会继承被替换字段的格式,并会在验证时提示 warning。已有 selector 额外用 `.addLink(cardIdOrIndex, targetFieldName?)` / `.removeLink(cardIdOrIndex)` / `.clearLinks()` 修改联动。Page 筛选区也按有序操作执行:`setFilterLayout` 整体设置,`clearFilterLayout` 清空,`addFilterLayoutItem` 追加去重,`insert/remove/moveFilterSelector` 局部调整;筛选器在快捷筛选区和画布间移动优先用动作级 API:`moveFilterSelectorToCanvas(selectorId, x, y, w, h)` / `moveCanvasSelectorToFilter(selectorId, index?)`,筛选器组用 `moveSelectorGroupToCanvas(group, x, y, w, h)` / `moveCanvasSelectorGroupToFilter(group, index?)`。checkout 生成的 page 布局默认用 card/selector ID 字符串;发布前可用 `guanvis diff <dir>` 或 `preview` 输出里的 `changeSummary` 查看 base JSON 到最终 payload 的路径级影响面;没有 DSL 操作覆盖的需求,先扩展 DSL,不要改 `.guanvis` JSON。
35
+
36
+ 1. **工厂函数**:数据集图表用 `createCard()`;已有线上卡片用 `attachCard(cardId, jsonPath)`;指标平台指标卡片用 `createMetricChart()`;筛选器/筛选器组/文本/图片/杜邦/Tab/页面用 `createSelector()` / `createSelectorGroup()` / `createTextCard()` / `createImageCard()` / `createDuPontChart()` / `createAreaTitle()` / `createCardGroup()` / `createTab()` / `createPage()`,不要 `new XxxBuilder()`;checkout JSON 不可复制改造,新卡片必须 create,老卡片才 attach
33
37
  2. **注册函数**:`registerCard(card.build())` / `registerMetricChart(card.build())` / `registerSelector(sel.build())` / `registerTextCard(text.build())` / `registerImageCard(image.build())` / `registerDuPontChart(dupont.build())` / `registerPage(page.build())`
34
38
  3. **字段引用**:单数据集用 `f("字段名")`,多数据集用 `field(DS, "字段名")`
35
39
  4. **zone maxCount**:`BASIC_COLUMN/BAR/LINE` metric=1;`GROUPED_*/STACKED_*` metric=∞;`KPI_CARD` metric=1;组合图 `*_WITH_LINE` row=1
@@ -38,19 +42,20 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
38
42
  7. **calcField 命名**:不能与数据集物理字段同名,否则 BI 默认取数据集字段
39
43
  8. **calcField 类型**:`aggregation`(默认)公式必须含聚合函数;纯算术用 `{ calculationType: "normal" }`;窗口函数用 `{ calculationType: "window" }`
40
44
  9. **明细/滚动表 calcField**:`DETAIL_TABLE` / `SCROLL_TABLE` 只做逐行展示;如需行级计算,必须写 `{ calculationType: "normal" }`,且不要写任何 SQL 聚合函数或窗口函数。汇总需求改用非明细图表 `aggrType` / aggregation calcField,或 ETL 预计算
41
- 10. **联动/下钻**:筛选器联动图表必须调用 `.linkToAll()` 或 `.linkTo(cardIndex)`;筛选器级联筛选器用 `.linkToSelector(selectorId, targetFieldName?)`,目标为 ALL/空默认值时会自动启用 FIRST_PICK + firstPickLink,固定默认值会保留;普通图表卡片联动普通图表用 `card.linkTo(layoutCardIndex, { fields: [{ source, target }] })`;固定路径下钻用 `registerDrillPath(parentCardIndex, [child.build()], { position: DrillPathPosition.BOTTOM })`;详细规则见 `references/builder-reference.md`
45
+ 10. **联动/下钻**:新建筛选器联动图表必须调用 `.linkToAll()` 或 `.linkTo(cardIndex)`;checkout attach 回来的已有筛选器用 `attachCard(selectorId, jsonPath).addLink(cardIdOrIndex, targetFieldName?)` / `.removeLink(cardIdOrIndex)` / `.clearLinks()` 叠加修改现有 `settings.asFilter`,不要反向改 JSON;筛选器级联筛选器用 `.linkToSelector(selectorId, targetFieldName?)`,目标为 ALL/空默认值时会自动启用 FIRST_PICK + firstPickLink,固定默认值会保留;普通图表卡片联动普通图表用 `card.linkTo(layoutCardIndex, { fields: [{ source, target }] })`;固定路径下钻用 `registerDrillPath(parentCardIndex, [child.build()], { position: DrillPathPosition.BOTTOM })`;详细规则见 `references/builder-reference.md`
42
46
  11. **selector 类型选择**:离散值(区域/类别)→ `DS_ELEMENTS`(默认);连续数值(利润率/金额)→ `.setSelectorType(SelectorType.DS_INTERVAL)`;日期 → `CALENDAR`;快捷日期区间(本月/近7天等)→ `.setTimeMacroOptions(options)`
43
47
  12. **同环比默认**:用户说同比/环比/同环比/年同比/月环比且未指定输出值时,默认用增长率;未指定模式时默认按日期筛选模式(`ComparativeMode.FILTER_BASED`),普通模式需显式指定 `ComparativeMode.NORMAL`。如果日期字段已经是预聚合周期字段(如 `周开始日期` / `月开始日期`),必须把日期字段声明为 `f("周开始日期", { granularity: Granularity.NONE })`,builder 会生成不带筛选窗口和 `mode` 的同环比,避免按 DAY 筛选窗口计算为空
44
- 13. **placeCard 入参**:数字 index `registerCard` / `registerMetricChart` / `registerTextCard` / `registerImageCard` / `registerCustomChart` / `registerDuPontChart` 等可布局资源的注册顺序累加,文件按文件名排序加载。**registerSelector 不参与 card index 计数**;要把 selector 放进画布或者布局组件时,使用 selector 字符串 ID,如 `placeCard("selectorId", x, y, w, h)` 或 `addFullWidthCard("selectorId", h)`。
48
+ 13. **placeCard 入参**:优先使用 card/selector ID 字符串,尤其是 checkout 工程和子目录工程,如 `placeCard("cardId", x, y, w, h)`。数字 index 仍可用于新建工程,按 `registerCard` / `registerMetricChart` / `registerTextCard` / `registerImageCard` / `registerCustomChart` / `registerDuPontChart` 等可布局资源的注册顺序累加,文件按文件名排序加载;**registerSelector 不参与 card index 计数**。要把 selector 放进画布或者布局组件时,使用 selector 字符串 ID,如 `placeCard("selectorId", x, y, w, h)` 或 `addFullWidthCard("selectorId", h)`。
45
49
  14. **publish 环境**:认证由底层 CLI 负责——guancli 需先 `guancli auth use <profile>`,guancli-lite 需设置环境变量
46
- 15. **更新线上仪表板**:已发布过或线上正在使用的仪表板,若调整图表、布局、筛选器、字段等看板内容,默认创建新版本仪表板,不覆盖原仪表板;新版本使用新的 Page/Card/Selector ID,名称追加版本号(如 `销售仪表板 v2` / `销售仪表板 20260430`),保留多个版本。只有用户明确要求覆盖时,才复用原 ID;遇到覆盖提示时,Agent 必须先向用户说明将覆盖哪些线上资源并取得明确确认,确认前不得自行加 `--allow-overwrite`
50
+ 15. **更新线上仪表板**:checkout 工程默认表示“修改指定线上 Page”,保留原 Page/Card/Selector ID;发布时如 CLI 检测到同 ID Page,必须先向用户说明将覆盖哪些线上资源并取得明确确认,确认后才可加 `--allow-overwrite`,由现有覆盖备份机制兜底。不要把 checkout JSON 复制后改 ID 来做“新版本”。若用户明确要保留原页面并生成新版本,应优先调用 BI 自身 Page Save/复制能力让 BI 处理 ID 映射,再基于新页面 checkout/edit;不要在 guanvis CLI 里手写复杂 ID map。
47
51
  16. **描述维护**:仅修复已发布 Card/Page 的描述时,保留原资源 ID,使用 `guanvis card/page set-description` 更新线上描述;如果本地有对应 JS 工程,也同步更新 `.setDescription(...)`,让源文件与线上描述一致
48
52
  17. **主题切换**:用户描述风格(深色科技风/科技蓝/蓝色简约等)→ 在工程目录里 `guanvis theme preference --keywords "..." --sync`(`[dir]` 可省,默认当前目录);不要选择租户默认的“浅色”/“深色”主题,找不到合适主题时保持/清空偏好,让 preview/pack/publish 自动使用内置“简约”兜底。普通数据集图表与指标平台 MetricChart 都会自动应用主题视觉配置;改版未提风格时 `.applied.json` 会自动继承上次主题,preview/pack/publish 不需要重复指定。**多子目录工程**:每个子目录是独立工程,主题要在子目录里配置 + preview/pack/publish 也必须 `cd` 进对应子目录运行(在根目录直接跑会被命令显式拒绝并给出 cd 提示);详见 `references/theme.md`
49
53
  18. **设计规则**:`preview`/`pack`/`publish` 会自动应用内置设计规则;需要自定义静态卡片默认规则或主题已开放配置时,在工程目录新建 `design-rule.json`,不要手改 `themes/<themeId>.json`;详见 `references/theme.md`
50
54
  19. **图表选型**:用户说“指标卡片”时先区分语义:如果是数据集字段做单值/KPI,用 `SINGLE_VALUE` / `KPI_CARD`;如果是“用指标平台已有指标创建卡片”,必须先 `guanvis metric-init <metricId>`,再用 `createMetricChart()` + `metric()` / `metricDim()`,生成后端 `CARD_TYPE.METRIC_CHART`。复杂指标卡片参数先读 `references/metric-chart-reference.md`
51
55
  20. **杜邦分析图**:杜邦不是普通 `ChartType`,用 `createDuPontChart()` 创建 `LAYOUT` 卡片;节点通常放 `KPI_CARD` 子卡片,并通过 `.setRoot()` / `.addChild()` 组织树。页面布局只放杜邦父卡片,不单独放子卡片;筛选器 `linkToAll()` 会覆盖杜邦子卡片。
52
- 21. **布局组件**:支持 `小标题(AreaTitle)` / `卡片组(CardGroup)` / `标签页(Tab)`。布局组件本身只支持放在画布根布局,不支持嵌套组合使用;内部布局能力详见 `references/builder-reference.md`。
53
- 22. **资源包禁止手改**:只改 DSL,不改 ZIP 内部文件;`upload` 只用于上传 `guanvis pack` 原样生成的包。批量重绑、迁移页面等需求先讨论方案。
56
+ 21. **布局组件**:支持 `小标题(AreaTitle)` / `卡片组(CardGroup)` / `筛选器组(SelGroup)` / `标签页(Tab)`。布局组件本身只支持放在画布根布局,不支持嵌套组合使用;SelGroup 内只能放 selector;checkout 会反编译根布局组件,无法安全表达的嵌套结构会失败;内部布局能力详见 `references/builder-reference.md`。
57
+ 22. **资源包/checkout JSON 禁止手改**:只改 DSL,不改 ZIP 内部文件,也不改 `.guanvis/raw` / `.guanvis/base` / `.guanvis/manifest.json`;`upload` 只用于上传 `guanvis pack` 原样生成的包。批量重绑、迁移页面、补缺失操作符等需求先讨论方案,必要时扩展 DSL 操作,不手工改生成物。
58
+ 23. **动态字段**:普通卡支持动态维度/动态数值,指标平台卡支持动态维度/动态指标;用户明确需要字段切换时使用 `.addDynamicRow()` / `.addDynamicMetric()` 等 API,细节见 `references/builder-reference.md`。
54
59
 
55
60
  ## 何时使用
56
61
 
@@ -270,9 +275,18 @@ guanvis gen-layout-id tab # 生成 1 个 tab_ + 默认 6 位字
270
275
  guanvis gen-layout-id panel 3 --length 8 # 生成 3 个 panel_ + 8 位字母;length 只计算下划线后的随机字母,超出 6~10 时自动收敛
271
276
  guanvis gen-layout-id areaTitle # 生成 1 个 areaTitle_ + 默认 6 位字母
272
277
  guanvis gen-layout-id cardGroup # 生成 1 个 cardGroup_ + 默认 6 位字母
278
+ guanvis gen-layout-id selGroup # 生成 1 个 selGroup_ + 默认 6 位字母
279
+
280
+ # 拉取已有线上仪表板为可编辑工程(只读 BI,不发布;尽量生成 schema.js)
281
+ guanvis checkout <pageId> -d ./existing_dashboard
282
+ # checkout 后只编辑 schema 外的 card_*.js / selector_*.js / page.js;不要修改或复制 .guanvis/raw、.guanvis/base、.guanvis/manifest.json
283
+ # 若 schema.js 未生成或缺字段,后续可手动 guanvis init <dsId> -d ./existing_dashboard --force 补齐
284
+ # checkout --overwrite 会清理输出目录下所有根级 .js 和旧 .guanvis,避免 schema/selector/metrics/other.js 残留混入运行
273
285
 
274
286
  # 预览生成结果(JSON 输出到 stdout,含 payload 验证,用于调试)
275
287
  guanvis preview ./my_dashboard/
288
+ # checkout/attach 工程的路径级变更摘要(也会出现在 preview JSON 的 changeSummary 中)
289
+ guanvis diff ./existing_dashboard/
276
290
 
277
291
  # 打包为 ZIP 资源包
278
292
  guanvis pack ./my_dashboard/
@@ -298,6 +312,12 @@ guanvis screenshot <pageId> # 截图页面 P
298
312
  guanvis screenshot <pageId> -o /tmp/dashboard.png # 指定输出路径
299
313
  guanvis screenshot <pageId> --orientation horizontal # 横向截图
300
314
 
315
+ # 数据集/指标切换后的检查(仅梳理)
316
+ guanvis check-dataset-usage . --ds <dsId> # 盘点某个 dsId 在工程中的引用
317
+ guanvis check-dataset-switch . --from <oldDs> --to <newDs> --mode full # 整页/整包数据集切换后检查
318
+ guanvis check-dataset-switch . --from <oldDs> --to <newDs> --mode linked --changed-cards <cdId> # 局部切数据集后检查一跳关联资源
319
+ guanvis check-metric-switch . --from <oldMetric> --to <newMetric> --mode linked --changed-cards <cdId> # 局部切指标后检查一跳关联资源
320
+
301
321
  # 仪表板主题(详见 `references/theme.md`,[dir] 缺省为当前目录)
302
322
  guanvis theme preference --keywords "深色 科技" --sync # 在当前目录写入偏好并同步主题列表
303
323
  guanvis theme preference ./my_dashboard --theme-id custom_blue # 显式指定工程目录
@@ -331,12 +351,12 @@ guanvis pack ./project
331
351
 
332
352
  **资源包安全约束**:`upload` 只是上传器,不是制作自定义资源包的入口。除非用户明确批准,否则不得上传手工生成、解包修改、重打包或批量替换内部内容后的 ZIP。需要批量重绑数据集、字段、卡片或页面 ID 时,先讨论方案,不要直接改 ZIP。
333
353
 
334
- **线上仪表板更新策略**:为了保护已经发布过的仪表板和线上仪表板,默认不要复用原 Page/Card/Selector ID 做覆盖更新。需要调整线上看板时,应先生成一组新的 ID,复制并修改 DSL/JS,给 Page 名称追加版本号(例如 `v2`、`v20260430` 或业务约定版本),再 `publish` 到目标目录。旧版本保留用于回滚和对比。仅当用户明确要求“覆盖原仪表板/复用原 ID”时,才允许同 ID 发布,并且命令必须显式加 `--allow-overwrite` 允许同 ID Page 覆盖;发布前可用 `--dry-run` 查看会覆盖哪些线上 Page。Card/Selector ID 不做在线覆盖检查。**Agent 禁止在未确认的情况下自行加 `--allow-overwrite`**:当 CLI 提示将覆盖线上 Page 时,必须先停止发布,向用户说明将覆盖的 Page ID、名称和覆盖后可能替换原页面布局,等用户明确确认“覆盖”后才可以重跑并加 `--allow-overwrite`。使用 `--allow-overwrite` 时,CLI 会先为冲突 Page 发起资源包导出备份并等待导出成功;备份未成功则中止覆盖。CLI 只记录备份导出记录和 packageId,不自动下载资源包;需要回滚时,到 BI 资源迁移导出记录中手动下载该资源包后再导入覆盖回去。
354
+ **线上仪表板更新策略**:新建工程发布的是新 Page;checkout 工程发布的是对 checkout 指定 Page 的覆盖式修改,不承担“复制新版本”职责。需要保留原页面并生成新版本时,不要在 guanvis CLI 内手工复制 JSON 或改 ID map;应先使用 BI 自身 Page Save/复制能力生成新 Page,让 BI 处理 ID 映射,再 checkout 新 Page 继续编辑。对 checkout 工程同 ID 发布时,命令必须显式加 `--allow-overwrite` 允许同 ID Page 覆盖;发布前可用 `--dry-run` 查看会覆盖哪些线上 Page。Card/Selector ID 不做在线覆盖检查。**Agent 禁止在未确认的情况下自行加 `--allow-overwrite`**:当 CLI 提示将覆盖线上 Page 时,必须先停止发布,向用户说明将覆盖的 Page ID、名称和覆盖后可能替换原页面布局,等用户明确确认“覆盖”后才可以重跑并加 `--allow-overwrite`。使用 `--allow-overwrite` 时,CLI 会先为冲突 Page 发起资源包导出备份并等待导出成功;备份未成功则中止覆盖。CLI 只记录备份导出记录和 packageId,不自动下载资源包;需要回滚时,到 BI 资源迁移导出记录中手动下载该资源包后再导入覆盖回去。
335
355
 
336
356
  ## 文件结构约定
337
357
 
338
358
  目录模式下文件加载顺序:
339
- 1. `schema.js` — 数据集定义(自动生成,不可修改)
359
+ 1. `schema.js` — 数据集定义(init 生成;checkout 尽量生成,不可修改)
340
360
  2. `metrics.js` — 指标定义(仅指标卡片需要,自动生成,不可修改)
341
361
  3. `card_01_xxx.js` ~ `card_NN_xxx.js` — Card 定义(按文件名排序)
342
362
  4. `selector_01_xxx.js` ~ `selector_NN_xxx.js` — 筛选器定义(在 card 之后执行,因为联动需要引用 card 索引)
@@ -351,6 +371,7 @@ guanvis pack ./project
351
371
  | 示例 | 路径 | 说明 |
352
372
  |------|------|------|
353
373
  | 基础仪表板 | `evals/sales_dashboard/` | 普通卡片(柱状图、折线图、KPI、饼图)+ 筛选器 + 页面布局 |
374
+ | 拆分图 | `evals/split_charts/` | 柱形等图表按字段拆分 |
354
375
  | 自定义图表 | `evals/custom_chart_echarts/` | ECharts Lite 自定义图表:柱状图 + 饼图,使用 `loadContent()` 文件模式 |
355
376
  | Tab 布局 | `evals/tab_layout/` | 单页面 tab 示例,含根布局指标卡、图表卡片、文本卡片、筛选器和 panel 内卡片布局 |
356
377
 
@@ -0,0 +1,20 @@
1
+ var region = f("区域");
2
+ var city = f("城市");
3
+ var sales = f("销售额", { aggrType: AggrType.SUM });
4
+ var profit = f("利润", { aggrType: AggrType.SUM });
5
+
6
+ var card = createCard(ChartType.GROUPED_COLUMN, "动态维度与动态数值")
7
+ .setId("dfcarddataset00000000000")
8
+ .bindDataset(DS)
9
+ .addDynamicRow("分析维度", [region, city], {
10
+ defaultValue: [region],
11
+ multiSelect: false
12
+ })
13
+ .addDynamicMetric("分析数值", [sales, profit], {
14
+ defaultValue: [sales],
15
+ multiSelect: true,
16
+ orderType: DynamicFieldOrder.CLICK
17
+ })
18
+ .setShowLegend(true, "bottom");
19
+
20
+ registerCard(card.build());
@@ -0,0 +1,16 @@
1
+ var region = metricDim("销售额", "区域");
2
+ var city = metricDim("销售额", "城市");
3
+ var sales = metric("销售额");
4
+ var profit = metric("利润");
5
+
6
+ var card = createMetricChart(ChartType.PIVOT_TABLE, "指标平台动态维度与动态指标")
7
+ .setId("dfmetriccard000000000000")
8
+ .addDynamicRow("分析维度", [region, city], {
9
+ defaultValue: [region]
10
+ })
11
+ .addDynamicMetric("分析指标", [sales, profit], {
12
+ defaultValue: [sales],
13
+ multiSelect: true
14
+ });
15
+
16
+ registerMetricChart(card.build());
@@ -0,0 +1,19 @@
1
+ defineMetric({
2
+ id: "metric_sales_eval_000001",
3
+ name: "销售额",
4
+ subType: "ATOMIC",
5
+ applicableDims: [
6
+ { fdId: "region_fd", dsId: "ds_dynamic_fields_eval", name: "区域", fdType: "STRING", metaType: "DIM" },
7
+ { fdId: "city_fd", dsId: "ds_dynamic_fields_eval", name: "城市", fdType: "STRING", metaType: "DIM" }
8
+ ]
9
+ });
10
+
11
+ defineMetric({
12
+ id: "metric_profit_eval_00001",
13
+ name: "利润",
14
+ subType: "ATOMIC",
15
+ applicableDims: [
16
+ { fdId: "region_fd", dsId: "ds_dynamic_fields_eval", name: "区域", fdType: "STRING", metaType: "DIM" },
17
+ { fdId: "city_fd", dsId: "ds_dynamic_fields_eval", name: "城市", fdType: "STRING", metaType: "DIM" }
18
+ ]
19
+ });
@@ -0,0 +1,9 @@
1
+ var page = createPage("动态字段示例")
2
+ .setId("dfpage000000000000000000")
3
+ .setDescription("验证动态维度、动态数值和指标平台动态指标的资源包结构。")
4
+ .setBackgroundColor("#f5f5f5")
5
+ .setCardMargin(8)
6
+ .addFullWidthCard(0, 7)
7
+ .addFullWidthCard(1, 7);
8
+
9
+ registerPage(page.build());
@@ -0,0 +1,7 @@
1
+ defineDataset("ds_dynamic_fields_eval", [
2
+ { fdId: "region_fd", name: "区域", fdType: "STRING", metaType: "DIM" },
3
+ { fdId: "city_fd", name: "城市", fdType: "STRING", metaType: "DIM" },
4
+ { fdId: "month_fd", name: "月份", fdType: "DATE", metaType: "DIM" },
5
+ { fdId: "sales_fd", name: "销售额", fdType: "DOUBLE", metaType: "METRIC" },
6
+ { fdId: "profit_fd", name: "利润", fdType: "DOUBLE", metaType: "METRIC" }
7
+ ], { displayType: "CSV" });
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+ EVAL_DIR="$ROOT_DIR/evals/dynamic_fields"
6
+ OUT_DIR="$EVAL_DIR/.roundtrip"
7
+
8
+ mkdir -p "$OUT_DIR"
9
+
10
+ cd "$ROOT_DIR"
11
+
12
+ go run ./cmd/guanvis preview "$EVAL_DIR" > "$OUT_DIR/preview_payload.json"
13
+ go run ./cmd/guanvis pack "$EVAL_DIR" -o "$OUT_DIR/dynamic_fields_package.zip" > "$OUT_DIR/pack.log"
14
+
15
+ if [[ "${PUBLISH:-0}" == "1" ]]; then
16
+ go run ./cmd/guanvis publish "$EVAL_DIR" > "$OUT_DIR/publish.log"
17
+ guancli card get dfcarddataset00000000000 --raw > "$OUT_DIR/raw_dataset_card.json"
18
+ guancli card get dfmetriccard000000000000 --raw > "$OUT_DIR/raw_metric_card.json"
19
+ fi
20
+
21
+ python3 - "$OUT_DIR" <<'PY'
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ out = Path(sys.argv[1])
27
+
28
+ expected = {
29
+ "dfcarddataset00000000000": {
30
+ "name": "动态维度与动态数值",
31
+ "mappings": {
32
+ "分析维度": {"zoneId": "row", "metaType": "DIM", "multiSelect": False},
33
+ "分析数值": {"zoneId": "metric", "metaType": "METRIC", "multiSelect": True},
34
+ },
35
+ },
36
+ "dfmetriccard000000000000": {
37
+ "name": "指标平台动态维度与动态指标",
38
+ "mappings": {
39
+ "分析维度": {"zoneId": "row", "metaType": "DIM", "multiSelect": False},
40
+ "分析指标": {"zoneId": "metric", "metaType": "METRIC", "multiSelect": True},
41
+ },
42
+ },
43
+ }
44
+
45
+ def load_json(path):
46
+ text = path.read_text(encoding="utf-8")
47
+ start = text.find("{")
48
+ if start > 0:
49
+ text = text[start:]
50
+ return json.loads(text)
51
+
52
+ def normalize_card(raw):
53
+ obj = raw.get("response") or raw.get("data") or raw
54
+ if "card" in obj:
55
+ card = obj["card"]
56
+ meta = json.loads(card["meta"]) if isinstance(card.get("meta"), str) else card.get("meta")
57
+ return {"id": card.get("cdId"), "name": card.get("name"), "meta": meta}
58
+ content = obj.get("content") or obj
59
+ return {"id": obj.get("cdId") or obj.get("id"), "name": obj.get("name"), "meta": content.get("meta")}
60
+
61
+ def preview_cards():
62
+ payload = load_json(out / "preview_payload.json")
63
+ cards = []
64
+ for item in payload.get("cards", []):
65
+ card = item.get("card", {})
66
+ meta = json.loads(card["meta"]) if isinstance(card.get("meta"), str) else card.get("meta")
67
+ cards.append({"id": card.get("cdId"), "name": card.get("name"), "meta": meta})
68
+ return cards
69
+
70
+ def assert_dynamic_card(card):
71
+ card_id = card["id"]
72
+ if card_id not in expected:
73
+ return None
74
+ spec = expected[card_id]
75
+ chart_main = (card.get("meta") or {}).get("chartMain") or {}
76
+ zone_data = chart_main.get("zoneData") or {}
77
+ dz_info = chart_main.get("dynamicZoneInfo") or {}
78
+ if dz_info.get("hidden") is not False:
79
+ raise SystemExit(f"{card_id}: dynamicZoneInfo.hidden={dz_info.get('hidden')!r}, want false")
80
+ mappings = dz_info.get("dzMappings") or []
81
+ by_name = {m.get("name"): m for m in mappings}
82
+ if set(by_name) != set(spec["mappings"]):
83
+ raise SystemExit(f"{card_id}: dzMappings names={sorted(by_name)}, want {sorted(spec['mappings'])}")
84
+
85
+ summary = {"cardId": card_id, "name": card.get("name"), "mappings": {}}
86
+ for name, want in spec["mappings"].items():
87
+ mapping = by_name[name]
88
+ for key, want_value in want.items():
89
+ got = mapping.get(key)
90
+ if got != want_value:
91
+ raise SystemExit(f"{card_id}: mapping {name} {key}={got!r}, want {want_value!r}")
92
+ if mapping.get("selectType") != "SEARCHBOX":
93
+ raise SystemExit(f"{card_id}: mapping {name} selectType={mapping.get('selectType')!r}, want SEARCHBOX")
94
+ if mapping.get("orderType") not in ("PRESET", "CLICK"):
95
+ raise SystemExit(f"{card_id}: mapping {name} invalid orderType={mapping.get('orderType')!r}")
96
+ dz_id = mapping.get("dzId")
97
+ defaults = mapping.get("defaultValue") or []
98
+ if not dz_id:
99
+ raise SystemExit(f"{card_id}: mapping {name} missing dzId")
100
+ if not defaults:
101
+ raise SystemExit(f"{card_id}: mapping {name} defaultValue is empty")
102
+ zone_fields = zone_data.get(mapping["zoneId"]) or []
103
+ candidate_keys = {f.get("key") for f in zone_fields if f.get("dzId") == dz_id}
104
+ if not candidate_keys:
105
+ raise SystemExit(f"{card_id}: mapping {name} has no zoneData candidates for dzId={dz_id}")
106
+ missing = [key for key in defaults if key not in candidate_keys]
107
+ if missing:
108
+ raise SystemExit(f"{card_id}: mapping {name} defaultValue keys {missing} not found in zoneData.{mapping['zoneId']}")
109
+ summary["mappings"][name] = {
110
+ "dzId": dz_id,
111
+ "zoneId": mapping["zoneId"],
112
+ "metaType": mapping["metaType"],
113
+ "multiSelect": mapping["multiSelect"],
114
+ "defaultValue": defaults,
115
+ "candidateCount": len(candidate_keys),
116
+ }
117
+ return summary
118
+
119
+ summaries = []
120
+ for card in preview_cards():
121
+ result = assert_dynamic_card(card)
122
+ if result:
123
+ summaries.append(result)
124
+
125
+ if len(summaries) != len(expected):
126
+ got = sorted(item["cardId"] for item in summaries)
127
+ raise SystemExit(f"preview dynamic card count={len(summaries)}, got {got}, want {sorted(expected)}")
128
+
129
+ if (out / "raw_dataset_card.json").exists() and (out / "raw_metric_card.json").exists():
130
+ for raw_name in ("raw_dataset_card.json", "raw_metric_card.json"):
131
+ raw_summary = assert_dynamic_card(normalize_card(load_json(out / raw_name)))
132
+ summaries.append({**raw_summary, "source": raw_name})
133
+
134
+ summary = {"cards": summaries}
135
+ (out / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
136
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
137
+ PY
@@ -0,0 +1,10 @@
1
+ var card = createCard(ChartType.BASIC_COLUMN, "按产品拆分区域营收")
2
+ .setId("spcolsplitdemoaabbccddee")
3
+ .bindDataset(DS)
4
+ .addRow(f("区域"))
5
+ .addMetric(f("营收", { aggrType: AggrType.SUM, numberFormat: NumberFormat.currency("¥", 0) }))
6
+ .addSplit(f("产品"))
7
+ .setSplitSetting({ rows: 2, columns: 3 })
8
+ .setDataLabel({ show: true, showNumber: true });
9
+
10
+ registerCard(card.build());
@@ -0,0 +1,10 @@
1
+ var card = createCard(ChartType.BASIC_LINE, "按产品拆分月营收趋势")
2
+ .setId("splinesplitdemoaabbccdde")
3
+ .bindDataset(DS)
4
+ .addRow(f("月份", { granularity: "MONTH" }))
5
+ .addMetric(f("营收", { aggrType: AggrType.SUM, numberFormat: NumberFormat.currency("¥", 0) }))
6
+ .addSplit(f("产品"))
7
+ .setSplitSetting({ rows: 2, columns: 3 })
8
+ .setShowLegend(false);
9
+
10
+ registerCard(card.build());
@@ -0,0 +1,11 @@
1
+ var card = createCard(ChartType.GROUPED_COLUMN_WITH_LINE, "按产品拆分营收成本趋势")
2
+ .setId("spcombosplitdemoaabbccdd")
3
+ .bindDataset(DS)
4
+ .addRow(f("月份", { granularity: "MONTH" }))
5
+ .addMetric(f("营收", { aggrType: AggrType.SUM, numberFormat: NumberFormat.currency("¥", 0) }))
6
+ .addMetricAdditional(f("成本", { aggrType: AggrType.SUM, numberFormat: NumberFormat.currency("¥", 0) }))
7
+ .addSplit(f("产品"))
8
+ .setSplitSetting({ rows: 2, columns: 2 })
9
+ .setShowLegend(true, "bottom");
10
+
11
+ registerCard(card.build());
@@ -0,0 +1,10 @@
1
+ var page = createPage("拆分图示例")
2
+ .setId("sppagesplitdemoaabbccdde")
3
+ .setDescription("展示柱形、折线和柱线组合图的 addSplit 用法。")
4
+ .setBackgroundColor("#f5f5f5")
5
+ .setCardMargin(8)
6
+ .addFullWidthCard(0, 6)
7
+ .addFullWidthCard(1, 6)
8
+ .addFullWidthCard(2, 6);
9
+
10
+ registerPage(page.build());
@@ -0,0 +1,14 @@
1
+ // Auto-generated by guanvis schema command
2
+ // DO NOT EDIT — regenerate with: guanvis schema s9ded2338f43807b095fbb4f
3
+ // This file defines dataset schemas for field() references in card scripts.
4
+
5
+ // Dataset: 销售数据 (example, 7 fields)
6
+ defineDataset("s9ded2338f43807b095fbb4f", [
7
+ { fdId: "jb16f22f6c1c21c3f6e8e293", name: "区域", fdType: "STRING", metaType: "DIM" },
8
+ { fdId: "o7093aaf2b2f32c9e964c41a", name: "产品", fdType: "STRING", metaType: "DIM" },
9
+ { fdId: "p2670dd6b74fc78c3e9c57ad", name: "月份", fdType: "DATE", metaType: "DIM" },
10
+ { fdId: "lb1c31b4dc86511a27049a98", name: "营收", fdType: "DOUBLE", metaType: "METRIC" },
11
+ { fdId: "aece10cd6806966be56f52ff", name: "成本", fdType: "DOUBLE", metaType: "METRIC" },
12
+ { fdId: "g1b16b52a16eb10c5e26d4bb", name: "数量", fdType: "INT", metaType: "METRIC" },
13
+ { fdId: "eae7cae6fbdcde68acdf5d1f", name: "利润率", fdType: "DOUBLE", metaType: "METRIC" }
14
+ ], { displayType: "EXCEL" });
@@ -11,12 +11,18 @@
11
11
  | `.addColumn(field)` | 添加列维度(按维度分组着色,如按地区/类别分色)。仅 `STACKED_COLUMN`、`GROUPED_COLUMN`、`GROUPED_BAR` 等多指标图表支持 |
12
12
  | `.addMetric(field)` | 添加度量(Y 轴,主轴) |
13
13
  | `.addMetricAdditional(field)` | 添加副轴度量(仅组合图 `*_WITH_LINE`/`*_WITH_SYMBOL`),默认绑定副 Y 轴(`plotOn: secondary`) |
14
+ | `.addDynamicRow(name, fields, options?)` | 添加动态维度组,候选字段写入 row |
15
+ | `.addDynamicColumn(name, fields, options?)` | 添加动态维度组,候选字段写入 column |
16
+ | `.addDynamicMetric(name, fields, options?)` | 添加动态数值组,候选字段写入 metric |
17
+ | `.addDynamicMetricAdditional(name, fields, options?)` | 添加动态数值组,候选字段写入副轴 |
14
18
  | `.addColorBy(field)` | 按指标值渐变着色(接受度量字段,不是维度)|
15
19
  | `.addTooltip(field)` / `.addFilter(field)` | 提示/筛选 |
16
20
  | `.addSort(field)` / `.addSplit(field)` / `.addSize(field)` | 排序/拆分/大小 |
17
21
  | `.addLocation(field)` / `.addTarget(field)` / `.addCompare(field)` | 位置/目标/对比 |
18
22
  | `.setSplitSetting({ rows, columns })` | 拆分行列数(默认各 3),需配合 `.addSplit(field)` |
19
23
  | `.setColorByColors(preset_or_config)` | colorBy 渐变色。传 `ColorByPreset.RedGreen` 等预设名称,或 `{ startColor, endColor, middleColor?, steps? }` 自定义 hex 颜色 |
24
+ | `.setShapeColorType(type)` | 图形填充方式。默认不调用为纯色;支持 `ShapeColorType.TransparentGradient`,仅用于前端已支持的柱/条类图表 |
25
+ | `.setShowTitle(show)` | 是否显示卡片标题;隐藏标题用 `.setShowTitle(false)` |
20
26
  | `.setShowLegend(show, position)` | 图例 |
21
27
  | `.setDataLabel(config)` | 数据标签 |
22
28
  | `.setAxis(config)` | 轴配置(见下方 Axis Config 详解) |
@@ -29,6 +35,19 @@
29
35
  | `.setRawSettings(key, value)` | 原始设置 |
30
36
  | `.build()` | 构建(触发验证) |
31
37
 
38
+ 动态字段是显式能力,不会根据同一区域多个字段自动推断。只有调用 `.addDynamicRow()` / `.addDynamicColumn()` / `.addDynamicMetric()` / `.addDynamicMetricAdditional()` 时,才会生成 `zoneData.*[].dzId` 和 `meta.chartMain.dynamicZoneInfo`。
39
+
40
+ 动态字段 `options`:
41
+
42
+ | 字段 | 默认值 | 说明 |
43
+ |---|---|---|
44
+ | `defaultValue` | 第一个候选字段 | 推荐传候选数组里的字段对象;字符串只在唯一命中 `name` / `alias` / `fdId` / `id` 时可用 |
45
+ | `multiSelect` | 根据 zone `maxCount` 推断 | `maxCount=1` 时强制单选;维度切换常建议显式写 `false` |
46
+ | `orderType` | `DynamicFieldOrder.PRESET` | `PRESET` 按预置顺序,`CLICK` 按用户点击顺序 |
47
+ | `id` | 自动生成 | 可选,显式指定 `dzId`,用于稳定 diff |
48
+
49
+ `hidden` 和 `selectType` 不开放配置;资源包固定写 `hidden: false`、`selectType: "SEARCHBOX"`。动态字段不写 `stateValue.dzFieldValues`,前端加载卡片时会根据 `dynamicZoneInfo.dzMappings.defaultValue` 初始化默认选中值。
50
+
32
51
  ### MetricChartBuilder(指标平台指标卡片)
33
52
 
34
53
  用于“用指标平台已有指标创建卡片”,生成后端 `CARD_TYPE.METRIC_CHART`(`cdType=13`)。不要用普通 `createCard()` 伪造,也不要把指标 ID 当作数据集字段。
@@ -45,8 +64,10 @@
45
64
  | `.setId(cardId)` | 设置 24 位资源 ID,用于重复导入覆盖 |
46
65
  | `.addRow(field)` / `.addColumn(field)` | 添加指标适用维度 |
47
66
  | `.addMetric(metric)` | 添加指标平台指标;至少一个 |
67
+ | `.addDynamicRow(name, fields, options?)` / `.addDynamicColumn(name, fields, options?)` | 添加指标平台动态维度组 |
68
+ | `.addDynamicMetric(name, fields, options?)` / `.addDynamicMetricAdditional(name, fields, options?)` | 添加指标平台动态指标组 |
48
69
  | `.addFilter(field, filterType, filterValue)` / `.addSort(field)` | 添加筛选/排序 |
49
- | `.setShowLegend()` / `.setDataLabel()` / `.setAxis()` / `.setTableSetting()` / `.setRawSettings()` | 常用图表设置,和普通 CardBuilder 一致 |
70
+ | `.setShowTitle()` / `.setShowLegend()` / `.setDataLabel()` / `.setAxis()` / `.setTableSetting()` / `.setRawSettings()` | 常用图表设置,和普通 CardBuilder 一致 |
50
71
  | `.setProps(obj)` / `.setRawProps(key, value)` | 设置指标卡片 `meta.chartMain.props` |
51
72
  | `.setConfig(obj)` / `.setRawConfig(key, value)` | 设置指标卡片 `meta.chartMain.config` |
52
73
  | `.setSummary(obj)` | 设置指标卡片 `meta.summary` |
@@ -66,6 +87,21 @@ var card = createMetricChart(ChartType.PIVOT_TABLE, "销售指标分析")
66
87
  registerMetricChart(card.build());
67
88
  ```
68
89
 
90
+ #### Shape Color Type(图形填充方式)
91
+
92
+ `setShapeColorType()` 控制柱子/条形等图形本身的填充方式,和 `.addColorBy()` / `.setColorByColors()` 的“按度量值渐变着色”不同。默认不调用时保持 BI 默认纯色。
93
+
94
+ ```javascript
95
+ var card = createCard(ChartType.BASIC_COLUMN, "区域销售额")
96
+ .setId("card12345678901234567890")
97
+ .bindDataset(DS)
98
+ .addRow(f("区域"))
99
+ .addMetric(f("销售额", { aggrType: AggrType.SUM }))
100
+ .setShapeColorType(ShapeColorType.TransparentGradient);
101
+ ```
102
+
103
+ 支持的图表:`BASIC_COLUMN`、`BASIC_BAR`、`GROUPED_COLUMN`、`GROUPED_BAR`、`GROUPED_COLUMN_WITH_LINE`、`GROUPED_COLUMN_WITH_SYMBOL`、`BULLET_BAR`。
104
+
69
105
  多指标示例:
70
106
 
71
107
  ```javascript
@@ -289,11 +325,13 @@ overview.linkTo("bbbbbbbbbbbbbbbbbbbbbbbb", {
289
325
 
290
326
  **布局单位**:
291
327
  1. 默认横向使用 12 列栅格;开启精细模式后使用 60 列栅格。
292
- 2. 推荐通过 `.setFineMode(true)` 设置为精细模式,且必须在任何 `addRow()` / `placeCard()` / `addTab()` / `addAreaTitle()` / `addCardGroup()` 等布局方法之前调用。
328
+ 2. 推荐通过 `.setFineMode(true)` 设置为精细模式,且必须在任何 `addRow()` / `placeCard()` / `addTab()` / `addAreaTitle()` / `addCardGroup()` / `addSelectorGroup()` 等布局方法之前调用。
293
329
  3. `addRow()` 及其快捷方法的 `height` 省略或为 0 时使用默认行高(普通 6,精细 18)。
294
330
  4. 区域小标题用 `createAreaTitle(...).setId(...).build()` 定义,并通过 `page.addAreaTitle(title)` 放在 Page 根布局;不要放进 tab panel。
295
331
  5. 卡片组用 `createCardGroup(...).setId(...)` 定义,并通过 `page.addCardGroup(group)` 放在 Page 根布局。
296
- 6. 下文布局 API 中的 `cardRef` 表示:非 selector 卡片的数字 index,或已注册 selector 的字符串 ID。`registerSelector()` 不参与数字 index 计数;被布局引用的 selector 不再进入同页筛选器栏。
332
+ 6. 筛选器组用 `createSelectorGroup(...).setId(...)` 定义;画布组通过 `page.addSelectorGroup(group)` 放在 Page 根布局,筛选栏组通过 `page.addFilterSelectorGroup(group)` 加入筛选栏,未分组 selector 可通过 `page.addFilterSelector(selectorId)` 显式控制顺序;组内只能放 selector。checkout 后修改已有快捷筛选区时,优先用 `setFilterLayout()` / `clearFilterLayout()` / `addFilterLayoutItem()` / `insertFilterSelector()` / `removeFilterSelector()` / `moveFilterSelector()` 表达增量操作;筛选器或筛选器组需要在快捷筛选区和画布之间移动时,优先用动作级 `moveFilterSelectorToCanvas()` / `moveCanvasSelectorToFilter()` / `moveSelectorGroupToCanvas()` / `moveCanvasSelectorGroupToFilter()`,不要手改 base JSON。
333
+ 7. checkout 生成的根布局组件会使用 `page.placeTab()` / `placeAreaTitle()` / `placeCardGroup()` / `placeSelectorGroup()` 保留线上 x/y/w/h;新建工程通常继续用 `addTab()` / `addAreaTitle()` / `addCardGroup()` / `addSelectorGroup()` 自动满宽布局。
334
+ 8. 下文布局 API 中的 `cardRef` 表示:推荐使用已注册卡片或 selector 的字符串 ID;数字 index 仍可用于非 selector 卡片,按可布局资源注册顺序计数。`registerSelector()` 不参与数字 index 计数;被布局引用的 selector 不再进入同页筛选器栏。
297
335
 
298
336
  | 方法 | 说明 |
299
337
  |------|------|
@@ -309,6 +347,24 @@ overview.linkTo("bbbbbbbbbbbbbbbbbbbbbbbb", {
309
347
  | `.addTab(tab, height?)` | 添加一个满宽 tab 容器;不传 height 时按第一个 panel 内容自动推导 |
310
348
  | `.addAreaTitle(areaTitle, height?)` | 添加一个满宽区域小标题|
311
349
  | `.addCardGroup(group, height?)` | 添加一个满宽卡片组;不传 height 时按标题和组内布局自动推导 |
350
+ | `.addSelectorGroup(group, height?)` | 添加一个满宽画布筛选器组;不传 height 时按展示模式、标题和组内布局自动推导 |
351
+ | `.placeTab(tab, x, y, w, h)` | checkout/精确布局用:注册 tab 并按显式坐标放入 Page 根布局 |
352
+ | `.placeAreaTitle(areaTitle, x, y, w, h)` | checkout/精确布局用:注册区域小标题并按显式坐标放入 Page 根布局 |
353
+ | `.placeCardGroup(group, x, y, w, h)` | checkout/精确布局用:注册卡片组并按显式坐标放入 Page 根布局 |
354
+ | `.placeSelectorGroup(group, x, y, w, h)` | checkout/精确布局用:注册画布筛选器组并按显式坐标放入 Page 根布局 |
355
+ | `.removeLayoutItem(cardRef)` | 从当前 Page 根布局移除已放置的 card/selector/layout component;主要给动作级移动 API 使用 |
356
+ | `.addFilterSelectorGroup(group)` | 添加一个筛选栏筛选器组 |
357
+ | `.addFilterSelector(selectorId)` | 显式添加一个未分组的筛选栏 selector,并控制其与筛选器组的顺序 |
358
+ | `.setFilterLayout(items)` | 整体设置快捷筛选区的 selector / filter selectorGroup ID 列表;checkout 场景会覆盖 base `filterLayout` |
359
+ | `.clearFilterLayout()` | 清空快捷筛选区;常用于把已有快捷筛选器改成画布内普通筛选器卡片 |
360
+ | `.addFilterLayoutItem(items)` | 向快捷筛选区追加 selector / filter selectorGroup ID,已存在则跳过 |
361
+ | `.insertFilterLayoutItem(index, items)` / `.insertFilterSelector(index, selectorId)` | 在快捷筛选区指定位置插入 |
362
+ | `.removeFilterLayoutItem(id)` / `.removeFilterSelector(selectorId)` | 从快捷筛选区移除已有 selector / filter selectorGroup ID |
363
+ | `.moveFilterLayoutItem(id, index)` / `.moveFilterSelector(selectorId, index)` | 调整快捷筛选区已有项顺序 |
364
+ | `.moveFilterSelectorToCanvas(selectorId, x, y, w, h)` | 动作级:从快捷筛选区移除 selector,并按坐标放到 Page 画布,联动配置保留在 selector 卡片本身 |
365
+ | `.moveCanvasSelectorToFilter(selectorId, index?)` | 动作级:从 Page 根布局移除 selector,并加入快捷筛选区;传 `index` 时插入指定位置,否则追加 |
366
+ | `.moveSelectorGroupToCanvas(group, x, y, w, h)` / `.moveFilterSelectorGroupToCanvas(group, x, y, w, h)` | 动作级:把筛选栏 SelGroup 转换成画布 SelGroup,并从快捷筛选区移除 |
367
+ | `.moveCanvasSelectorGroupToFilter(group, index?)` | 动作级:把画布 SelGroup 转换成筛选栏 SelGroup,并从 Page 根布局移除;组内布局必须引用 selector ID 字符串 |
312
368
  | `.setBackgroundColor(color)` | 页面背景色 |
313
369
  | `.setCardMargin(margin)` | 卡片间距 |
314
370
  | `.setFineMode(enabled)` | 开启/关闭精细模式 |
@@ -324,6 +380,7 @@ overview.linkTo("bbbbbbbbbbbbbbbbbbbbbbbb", {
324
380
  |------|------|
325
381
  | `createAreaTitle(name)` | 创建区域小标题,`name` 即为标题的内容,会被写入该组件的 style 中,必须要传 name |
326
382
  | `.setId(areaTitleId)` | **必填**。设置区域小标题 ID,必须以 `areaTitle_` 开头 |
383
+ | `.setRawStyle(style)` | checkout 保留线上 style 用;新建工程优先用下列语义化方法 |
327
384
  | `.setShowTitle(boolean)` | 是否显示标题 |
328
385
  | `.setFontSize(number)` | 字号 |
329
386
  | `.setColor(color)` | 字体颜色 |
@@ -358,6 +415,7 @@ registerPage(page.build());
358
415
  |------|------|
359
416
  | `createCardGroup(name)` | 创建卡片组,`name` 写入分组标题 |
360
417
  | `.setId(cardGroupId)` | **必填**。设置卡片组 ID,必须以 `cardGroup_` 开头 |
418
+ | `.setRawStyle(style)` | checkout 保留线上 style 用;新建工程优先用 `.setShowTitle()` 等语义化方法 |
361
419
  | `.setShowTitle(boolean)` | 是否显示标题,默认 `true` |
362
420
  | `.addRow(specs, height?)` | 在组内按行放置卡片,写法同 `PageBuilder.addRow()` |
363
421
  | `.addFullWidthCard(cardRef, height?)` | 在组内放一张满宽卡片 |
@@ -378,6 +436,57 @@ var page = createPage("销售仪表板")
378
436
  registerPage(page.build());
379
437
  ```
380
438
 
439
+ ### SelectorGroupBuilder
440
+
441
+ `SelGroup` 是原生筛选器组,不是 Card,不绑定数据集,也不会生成独立 page-card relation。ID 必须以 `selGroup_` 开头,使用 `guanvis gen-layout-id selGroup` 生成。画布 SelGroup 只能放入 Page 根布局;筛选栏 SelGroup 只能放入 `filterLayout`;组内只能包含已注册 selector。
442
+
443
+ 默认值:`displayMode = "tiled"`、`showTitle = false`、`titleStyle.fontSize = 14`、`titleStyle.bold = true`。筛选栏组名始终来自 `name`,不受 `showTitle` 控制。
444
+
445
+ | 方法 | 说明 |
446
+ |------|------|
447
+ | `createSelectorGroup(name)` | 创建筛选器组,`name` 必填 |
448
+ | `.setId(selGroupId)` | **必填**。设置筛选器组 ID,必须以 `selGroup_` 开头 |
449
+ | `.setRawStyle(style)` | checkout 保留线上 style 用;新建工程优先用 `.setDisplayMode()` / `.setShowTitle()` / `.setFpWidth()` |
450
+ | `.setDisplayMode(mode)` | 展示模式:`SelectorGroupDisplayMode.TILED`(默认)或 `SelectorGroupDisplayMode.DROPDOWN` |
451
+ | `.setShowTitle(boolean)` | 是否显示画布组标题;默认 `false` |
452
+ | `.setFpWidth(width)` | 筛选栏非栅格模式宽度,仅筛选栏组有效 |
453
+ | `.setFpGrid(span)` | 筛选栏栅格模式宽度,仅筛选栏组有效 |
454
+ | `.addSelector(selectorId)` / `.addSelectors(selectorIds)` | 添加筛选栏组内 selector;使用后只能传给 `page.addFilterSelectorGroup()` |
455
+ | `.addRow(specs, height?)` | 添加画布组内 selector 布局,写法同 `PageBuilder.addRow()`;height 省略或为 0 时 selector 高度默认普通 1、精细 3;使用后只能传给 `page.addSelectorGroup()` |
456
+ | `.addFullWidthCard(cardRef, height?)` | 在画布组内放一个满宽 selector |
457
+ | `.placeCard(cardRef, x, y, w, h)` | 在画布组内精确放置 selector |
458
+
459
+ 画布下拉筛选器组:
460
+
461
+ ```javascript
462
+ var advanced = createSelectorGroup("高级筛选")
463
+ .setId("selGroup_AbCdEf")
464
+ .setDisplayMode(SelectorGroupDisplayMode.DROPDOWN)
465
+ .setShowTitle(true)
466
+ .addRow([
467
+ { card: "ssssssssssssssssssssssss", w: 6 },
468
+ { card: "tttttttttttttttttttttttt", w: 6 }
469
+ ]);
470
+
471
+ var page = createPage("销售仪表板")
472
+ .setId("p184352b7a76776db5f534df")
473
+ .addSelectorGroup(advanced);
474
+ ```
475
+
476
+ 筛选栏筛选器组与未分组筛选器混排:
477
+
478
+ ```javascript
479
+ var globalFilters = createSelectorGroup("全局筛选")
480
+ .setId("selGroup_GhIjKl")
481
+ .addSelector("ssssssssssssssssssssssss")
482
+ .addSelector("tttttttttttttttttttttttt");
483
+
484
+ var page = createPage("销售仪表板")
485
+ .setId("p184352b7a76776db5f534df")
486
+ .addFilterSelectorGroup(globalFilters)
487
+ .addFilterSelector("uuuuuuuuuuuuuuuuuuuuuuuu");
488
+ ```
489
+
381
490
  #### 页面密度尺寸建议
382
491
 
383
492
  以下尺寸建议以非精细模式的 12 列布局单位为基准,用于生成页面时选择页面卡片间距和常见卡片高度。精细模式目前只定义横向 60 列栅格与显式 `x/y/w/h` 放置规则,暂不提供独立的密度尺寸换算规则。
@@ -404,6 +513,9 @@ tab 用于把页面中的卡片分到多个 panel。适合同一主题下多组
404
513
  |------|------|
405
514
  | `createTab(name)` | 创建 tab 容器;`name` 用于脚本可读性,页面上显示的是各 panel 的 name |
406
515
  | `.setId(tabId)` | **必填**。设置 tab ID,必须以 `tab_` 开头且同一页面内唯一。建议用 `guanvis gen-layout-id tab` 生成 |
516
+ | `.setRawStyle(style)` | checkout 保留线上 tab style 用;新建工程优先用下列语义化方法 |
517
+ | `.setRawPanelStyle(panelStyle)` | checkout 保留线上 panelStyle 用 |
518
+ | `.setRawLayoutItemMap(layoutItemMap)` | checkout 保留线上 tab 内 layoutItemMap 用 |
407
519
  | `.addPanel(name, callback)` | 添加 panel;callback 接收 `panel` 配置对象,必须在其中放入至少一张卡片 |
408
520
  | `.setLabelStyle(style)` | 设置标签样式:`TabLabelStyle.UNDERLINE`(默认)、`CARD`、`CAPSULE`、`TRAPEZOID` |
409
521
  | `.setAlignment(alignment)` | 设置标签对齐:`TabAlignment.LEFT`(默认)、`CENTER`、`RIGHT` |
@@ -1080,6 +1192,7 @@ option = {
1080
1192
  | `FieldType` | `STRING`, `INT`, `LONG`, `DOUBLE`, `FLOAT`, `DATE`, `BOOL`, `DECIMAL` | 字段数据类型 |
1081
1193
  | `SortOrder` | `ASC`, `DESC` | 排序方向 |
1082
1194
  | `Granularity` | `NONE`, `YEAR`, `QUARTER`, `MONTH`, `WEEK`, `DAYOFWEEK`, `DAY`, `HOUR`, `MINUTE`, `SECOND` | 日期粒度 |
1195
+ | `DynamicFieldOrder` | `PRESET`, `CLICK` | 动态字段默认顺序;`PRESET` 按候选顺序,`CLICK` 按用户选择顺序 |
1083
1196
  | `FilterType` | `IN`, `NOT_IN`, `GT`, `GE`, `LT`, `LE`, `EQ`, `NE`, `BT`(区间), `CONTAINS`, `NOT_CONTAINS`, `STARTSWITH`, `ENDSWITH`, `IS_NULL`, `NOT_NULL` | 筛选条件类型 |
1084
1197
  | `FilterLevel` | `DETAIL`(明细), `AGGREGATION`(聚合), `RESULT`(结果) | 筛选级别 |
1085
1198
  | `NumberFormat` | `.number()`, `.currency()`, `.percentage()`, `.auto()`, `.custom()` | 数值格式化工厂 |
@@ -160,6 +160,10 @@ registerMetricChart(card.build());
160
160
  | `.addMetricAdditional(metric(...))` | 添加副轴指标 |
161
161
  | `.addRow(metricDim(...))` | 添加行/类目维度 |
162
162
  | `.addColumn(metricDim(...))` | 添加列/对比维度 |
163
+ | `.addDynamicRow(name, fields, options?)` | 添加动态维度组,候选字段写入 row |
164
+ | `.addDynamicColumn(name, fields, options?)` | 添加动态维度组,候选字段写入 column |
165
+ | `.addDynamicMetric(name, fields, options?)` | 添加动态指标组,候选指标写入 metric |
166
+ | `.addDynamicMetricAdditional(name, fields, options?)` | 添加动态指标组,候选指标写入副轴 |
163
167
  | `.addFilter(field, type, values)` | 添加筛选 |
164
168
  | `.addSort(field)` | 添加排序 |
165
169
  | `.addColorBy(metric(...))` | 添加颜色指标 |
@@ -174,6 +178,42 @@ registerMetricChart(card.build());
174
178
  | `.setFreeDrill(enabled, position)` | 设置 `config.freeDrillConfig` |
175
179
  | `.setRowThreshold(thresholds)` | 设置 `config.rowThreshold` |
176
180
 
181
+ ## 动态维度 / 动态指标
182
+
183
+ 动态字段是显式能力。只有调用 `.addDynamicRow()` / `.addDynamicColumn()` / `.addDynamicMetric()` / `.addDynamicMetricAdditional()` 时,guanvis 才会生成候选字段的 `dzId` 和 `meta.chartMain.dynamicZoneInfo`;普通 `.addRow()` / `.addMetric()` 仍生成静态字段。
184
+
185
+ ```javascript
186
+ var region = metricDim("销售额", "区域");
187
+ var city = metricDim("销售额", "城市");
188
+ var sales = metric("销售额");
189
+ var profit = metric("利润");
190
+
191
+ var card = createMetricChart(ChartType.PIVOT_TABLE, "指标动态分析")
192
+ .setId("metriccard12345678901234")
193
+ .addDynamicRow("分析维度", [region, city], {
194
+ defaultValue: [region],
195
+ multiSelect: false
196
+ })
197
+ .addDynamicMetric("分析指标", [sales, profit], {
198
+ defaultValue: [sales],
199
+ multiSelect: true,
200
+ orderType: DynamicFieldOrder.CLICK
201
+ });
202
+
203
+ registerMetricChart(card.build());
204
+ ```
205
+
206
+ `options`:
207
+
208
+ | 字段 | 默认值 | 说明 |
209
+ |---|---|---|
210
+ | `defaultValue` | 第一个候选字段 | 推荐传候选数组里的字段对象;字符串只在唯一命中 `name` / `alias` / `fdId` / `id` 时可用 |
211
+ | `multiSelect` | 根据 zone `maxCount` 推断 | `maxCount=1` 时强制单选;指标卡 row 默认单选 |
212
+ | `orderType` | `DynamicFieldOrder.PRESET` | `PRESET` 按候选顺序,`CLICK` 按用户点击顺序 |
213
+ | `id` | 自动生成 | 可选,显式指定 `dzId`,用于稳定 diff |
214
+
215
+ `hidden` 和 `selectType` 不开放配置;资源包固定写 `hidden: false`、`selectType: "SEARCHBOX"`。动态字段不写 `stateValue.dzFieldValues`,前端加载卡片时会根据 `dynamicZoneInfo.dzMappings.defaultValue` 初始化默认选中值。
216
+
177
217
  复杂配置可直接放到 `metric()` / `metricDim()` 的 overrides 中:
178
218
 
179
219
  ```javascript
@@ -5,7 +5,7 @@
5
5
  - **接口**:`POST /api/manual/template/transfer`(标准 multipart/form-data,表单字段名 `new-file`)
6
6
  - **认证**:随底层 `guancli fetch` 使用 `Cookie: uIdToken=...`
7
7
  - **关键 header**:`raw-backend-response: TRUE`(绕过前端代理层,直达后端)
8
- - **ID 策略**:`needIdMapping=false`,保持资源 ID 不变。同 ID 资源会被覆盖更新。为避免部分 BI 版本在导入前探测 Card 时缓存"找不到相关卡片",`guanvis publish/upload` 的在线覆盖检查只探测目标环境已有 Page ID;检测到同 ID Page 时默认拒绝上传,只有明确加 `--allow-overwrite` 才允许覆盖。加 `--allow-overwrite` 后,CLI 会先调用资源包导出为冲突 Page 生成备份记录,并等待导出成功;备份失败或超时则中止上传。Card/Selector ID 不做在线探测;如果资源包不包含 Page,则不会执行在线覆盖检查。更新已有仪表板时应按新版本策略生成新的 Page/Card/Selector ID
8
+ - **ID 策略**:`needIdMapping=false`,保持资源 ID 不变。同 ID 资源会被覆盖更新。为避免部分 BI 版本在导入前探测 Card 时缓存"找不到相关卡片",`guanvis publish/upload` 的在线覆盖检查只探测目标环境已有 Page ID;检测到同 ID Page 时默认拒绝上传,只有明确加 `--allow-overwrite` 才允许覆盖。加 `--allow-overwrite` 后,CLI 会先调用资源包导出为冲突 Page 生成备份记录,并等待导出成功;备份失败或超时则中止上传。Card/Selector ID 不做在线探测;如果资源包不包含 Page,则不会执行在线覆盖检查。checkout 工程表示修改指定 Page,不在 CLI 内复制新版本或手写 ID 映射。
9
9
  - **通用性**:不需要目标系统开启"一键迁移"开关,所有客户环境可用
10
10
  - **异步执行**:上传成功后返回 `taskId`,后端异步完成导入
11
11
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  若用户要求批量重绑资源、迁移已有页面或复用线上页面结构,先说明风险并确认方案;不要直接修改 ZIP 内部文件后上传。
15
15
 
16
- 修改或重新发布已存在的仪表板资源时,默认按“新版本仪表板”处理:先从目标 BI 线上环境同步最新资源状态,再基于同步后的内容生成新的 Page/Card/Selector ID,并给仪表板名称追加版本号后发布。用户可能已经在 BI 上手工改过页面布局、卡片配置、筛选器或说明文本;如果只按本地旧文件 `publish`,同 ID 资源会被覆盖,导致线上改动丢失。只有用户明确要求覆盖原资源时,才允许保留原 ID 并同 ID 发布。
16
+ checkout 工程表示“基于线上快照修改指定 Page”,不会新建 Page,也不要通过复制 checkout JSON、改 ID 或自建 ID map 来生成新版本。若用户要求保留原页面并另存新版本,应先调用 BI 自身 Page Save/复制能力,由 BI 处理资源 ID 映射,再对新 Page 执行 checkout/edit。用户可能已经在 BI 上手工改过页面布局、卡片配置、筛选器或说明文本;如果按旧 checkout 工程同 ID `publish`,线上改动会被覆盖。发布前应使用 `--dry-run` 检查覆盖对象,只有用户明确要求覆盖原资源时,才允许加 `--allow-overwrite` ID 发布。
17
17
 
18
18
  ## 认证与 CLI 集成
19
19
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  ## 硬约束
25
25
 
26
- - `schema.js` 由 `init` 命令自动生成,不允许 AI 或人工修改。
26
+ - `schema.js` 由 `init` 命令生成;`checkout` 会尽量生成 schema,但数据集无权限、已删除或跨环境残留时只提示 warning 并继续生成可编辑工程。不允许 AI 或人工修改 `schema.js`,缺失时后续用 `guanvis init <dsId> -d <dir> --force` 补齐。
27
27
  - card 脚本必须调用 `registerCard(card.build())`(目录模式)或返回 `card.build()`(单文件模式)。
28
28
  - metric chart 脚本必须调用 `registerMetricChart(card.build())`。
29
29
  - text card 脚本必须调用 `registerTextCard(textCard.build())`。
@@ -48,6 +48,7 @@
48
48
 
49
49
  - `.setId(id)` 的 ID **必须**为严格 24 位字母数字字符串(正则:`^[a-zA-Z0-9]{24}$`),与后端 `RandUtil.uuid` 格式一致。若生成或手填的 ID 以数字开头,建议重新生成一组,避免 BI 前端某些路径把 ID 拼成 CSS selector(如 `#5bef...`)时触发 `querySelector` 语法错误。
50
50
  - **生成 ID**:先运行 `guanvis genid <数量>` 生成足够的 ID,在编写脚本时直接填入每个 card/selector/page 的 `.setId()` 调用中。
51
- - **线上更新默认策略**:已发布过或线上正在使用的仪表板,后续修改默认生成新的 Page/Card/Selector ID,并给 Page 名称追加版本号后发布,保留旧版本不覆盖。
51
+ - **布局组件 ID 例外**:Tab、Panel、AreaTitle、CardGroup、SelGroup 使用 `guanvis gen-layout-id <prefix>` 生成,形如 `tab_AbCdEf`、`panel_AbCdEf`、`areaTitle_AbCdEf`、`cardGroup_AbCdEf`、`selGroup_AbCdEf`,不适用 24 位 `genid` 规则。
52
+ - **线上更新默认策略**:新建工程发布新 Page;checkout 工程只修改 checkout 指定的 Page,不负责复制新版本。
52
53
  - **覆盖前检查**:发布前可先运行 `guanvis publish <dir> --dry-run` 或 `guanvis upload <zip> --dry-run`,只构建/解析资源并列出将被覆盖的线上 Page,不提交 transfer 任务。
53
- - **显式覆盖场景**:只有用户明确要求覆盖原仪表板时,才保持 `.setId()` 不变,并在 `publish/upload` 时加 `--allow-overwrite` 允许同 ID Page 覆盖;多次同 ID 发布会覆盖资源(因为 transfer API 的 `needIdMapping=false`),其中 Card/Selector ID 不做在线覆盖检查。覆盖前备份只创建资源迁移导出记录并打印 packageId,不自动下载资源包;需要回滚时,用户应到 BI 资源迁移导出记录中下载该资源包,再手动导入覆盖回去。
54
+ - **显式覆盖场景**:checkout 工程发布时保持 `.setId()` 不变,并在用户确认覆盖后加 `--allow-overwrite` 允许同 ID Page 覆盖;多次同 ID 发布会覆盖资源(因为 transfer API 的 `needIdMapping=false`),其中 Card/Selector ID 不做在线覆盖检查。覆盖前备份只创建资源迁移导出记录并打印 packageId,不自动下载资源包;需要回滚时,用户应到 BI 资源迁移导出记录中下载该资源包,再手动导入覆盖回去。