@guandata/guanvis 0.1.31 → 0.1.33

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## @guandata/guanvis 0.1.33 - 2026-07-23
4
+
5
+ - 指标图表支持主次指标组和数字分组,可构建层次更清晰的指标展示。
6
+ - 增强页面发布前检查,可提前发现无效资源 ID、未挂载到页面的卡片和资源生成错误。
7
+ - 优化页面检出、指标初始化和全局参数读取效率,复杂页面编辑等待更少。
8
+ - 大型资源导入支持更灵活的等待时间,并能更快返回明确失败原因。
9
+
10
+ ## @guandata/guanvis 0.1.32 - 2026-07-20
11
+
12
+ - 新增复杂报表 Pro 的创建、编辑、检视、反编译与发布完整链路。
13
+ - 支持页面另存为,并可将自定义图表脚本、样式和资源还原为可编辑工程。
14
+ - 支持全局参数和导出配置,增强线上页面 checkout、修改和无损回写的稳定性。
15
+ - 补全图表类型校验,提前阻止无法正常展示的卡片配置。
16
+ - 全局安装或升级后自动刷新 AI Skill,并随包提供完整使用说明和参考资料。
17
+
3
18
  ## @guandata/guanvis 0.1.31 - 2026-07-15
4
19
 
5
20
  - 新增仪表板筛选栏布局配置,支持网格或流式排列、间距、内边距、标签位置和操作区位置。
package/README.md CHANGED
@@ -51,6 +51,21 @@ guanvis publish ./my_dashboard/ --allow-overwrite
51
51
 
52
52
  ## 版本更新
53
53
 
54
+ ### @guandata/guanvis 0.1.33
55
+
56
+ - 指标图表支持主次指标组和数字分组,可构建层次更清晰的指标展示。
57
+ - 增强页面发布前检查,可提前发现无效资源 ID、未挂载到页面的卡片和资源生成错误。
58
+ - 优化页面检出、指标初始化和全局参数读取效率,复杂页面编辑等待更少。
59
+ - 大型资源导入支持更灵活的等待时间,并能更快返回明确失败原因。
60
+
61
+ ### @guandata/guanvis 0.1.32
62
+
63
+ - 新增复杂报表 Pro 的创建、编辑、检视、反编译与发布完整链路。
64
+ - 支持页面另存为,并可将自定义图表脚本、样式和资源还原为可编辑工程。
65
+ - 支持全局参数和导出配置,增强线上页面 checkout、修改和无损回写的稳定性。
66
+ - 补全图表类型校验,提前阻止无法正常展示的卡片配置。
67
+ - 全局安装或升级后自动刷新 AI Skill,并随包提供完整使用说明和参考资料。
68
+
54
69
  ### @guandata/guanvis 0.1.31
55
70
 
56
71
  - 新增仪表板筛选栏布局配置,支持网格或流式排列、间距、内边距、标签位置和操作区位置。
@@ -178,6 +193,8 @@ registerPage(page.build());
178
193
  guanvis install-skill
179
194
  ```
180
195
 
196
+ > `npm install -g` / `npm link` 全局安装时会通过 postinstall 自动执行一次 skill 安装/刷新;上述命令用于手动重装或排查。CI 等无需 skill 的环境可设 `GUAN_SKIP_INSTALL_SKILL=1` 跳过。
197
+
181
198
  ## 卸载
182
199
 
183
200
  ```bash
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Best-effort post-install hook: refresh the installed AI skill right after
5
+ * a global npm install/upgrade, so users cannot forget to run
6
+ * `guanvis install-skill` and end up with a stale SKILL.md.
7
+ *
8
+ * Constraints:
9
+ * - MUST never fail the npm install: every failure path still exits 0.
10
+ * - Only runs for global installs (`npm install -g` / `npm link`); a local
11
+ * `npm install` inside a project never touches the user's home dirs.
12
+ * - Escape hatch: GUAN_SKIP_INSTALL_SKILL=1 skips entirely (e.g. CI images
13
+ * that install the CLI but never run an AI agent are skipped by default).
14
+ */
15
+
16
+ "use strict";
17
+
18
+ const { spawnSync } = require("child_process");
19
+ const path = require("path");
20
+
21
+ const CLI_NAME = "guanvis";
22
+
23
+ function skipReason() {
24
+ if (process.env.GUAN_SKIP_INSTALL_SKILL === "1") return "GUAN_SKIP_INSTALL_SKILL=1";
25
+ if (process.env.npm_config_global !== "true") return "not a global install";
26
+ if (process.env.CI) return "CI environment";
27
+ return "";
28
+ }
29
+
30
+ function main() {
31
+ const reason = skipReason();
32
+ if (reason) {
33
+ console.log(`[${CLI_NAME}] postinstall: skipping automatic install-skill (${reason}).`);
34
+ return;
35
+ }
36
+ console.log(`[${CLI_NAME}] postinstall: refreshing AI skill (${CLI_NAME} install-skill)...`);
37
+ const result = spawnSync(
38
+ process.execPath,
39
+ [path.join(__dirname, "run.js"), "install-skill"],
40
+ { stdio: "inherit", env: process.env, timeout: 180000 }
41
+ );
42
+ if (result.error || result.status !== 0) {
43
+ console.warn(
44
+ `[${CLI_NAME}] postinstall: automatic skill install did not complete` +
45
+ (result.error ? ` (${result.error.message})` : ` (exit ${result.status})`) +
46
+ `; run \`${CLI_NAME} install-skill\` manually to refresh SKILL.md.`
47
+ );
48
+ }
49
+ }
50
+
51
+ try {
52
+ main();
53
+ } catch (err) {
54
+ console.warn(
55
+ `[${CLI_NAME}] postinstall: ${err.message}; run \`${CLI_NAME} install-skill\` manually.`
56
+ );
57
+ }
58
+ process.exit(0);
package/bin/run.js CHANGED
@@ -86,10 +86,36 @@ if (process.argv[2] === "version" && process.argv.length === 3) {
86
86
  process.exit(0);
87
87
  }
88
88
 
89
+
90
+ // 定位 npm 自带的 npx-cli.js 并用 node 直接执行(与 guanskill 保持一致),
91
+ // 避免 Windows 上 spawn npx.cmd 的两类问题:shell:true 不做参数 quoting
92
+ // (全局安装路径含空格时会被拆参),以及新版 Node 禁止无 shell 的 .cmd
93
+ // spawn(CVE-2024-27980)。postinstall 场景下 npm_execpath 必然可用。
94
+ function resolveNpxInvocation() {
95
+ const executableDir = path.dirname(process.execPath);
96
+ const candidates = [];
97
+ if (process.env.npm_execpath) {
98
+ candidates.push(path.join(path.dirname(process.env.npm_execpath), "npx-cli.js"));
99
+ }
100
+ candidates.push(path.join(executableDir, "node_modules", "npm", "bin", "npx-cli.js"));
101
+ candidates.push(path.resolve(executableDir, "..", "node_modules", "npm", "bin", "npx-cli.js"));
102
+ candidates.push(path.resolve(executableDir, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js"));
103
+ const npxCliPath = candidates.find((candidate) => fs.existsSync(candidate));
104
+ if (npxCliPath) {
105
+ return { command: process.execPath, argsPrefix: [npxCliPath] };
106
+ }
107
+ if (process.platform === "win32") {
108
+ throw new Error("Cannot locate npm/bin/npx-cli.js for safe Windows execution");
109
+ }
110
+ return { command: "npx", argsPrefix: [] };
111
+ }
112
+
89
113
  if (process.argv[2] === "install-skill") {
90
114
  const pkgRoot = path.join(__dirname, "..");
91
115
  const extraArgs = process.argv.slice(3);
92
116
  const args = [
117
+ // --yes: postinstall 等非交互环境下 npx 需要免确认下载 skills CLI
118
+ "--yes",
93
119
  "skills",
94
120
  "add",
95
121
  pkgRoot,
@@ -100,7 +126,12 @@ if (process.argv[2] === "install-skill") {
100
126
  ...extraArgs,
101
127
  ];
102
128
  console.log("Installing guanvis to AI coding assistants...");
103
- const result = spawnSync("npx", args, { stdio: "inherit", env: process.env, shell: true });
129
+ const npxInvocation = resolveNpxInvocation();
130
+ const result = spawnSync(npxInvocation.command, [...npxInvocation.argsPrefix, ...args], {
131
+ stdio: "inherit",
132
+ env: process.env,
133
+ shell: false,
134
+ });
104
135
  if (result.error) throw result.error;
105
136
  const status = result.status || 0;
106
137
  if (status === 0) installBuddySkills(pkgRoot, "guanvis");
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@guandata/guanvis",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "观远 BI Card/Page 生成工具 - 通过 JS DSL 创建图表和仪表板",
5
5
  "bin": {
6
6
  "guanvis": "bin/run.js"
7
7
  },
8
8
  "scripts": {
9
+ "postinstall": "node bin/postinstall.js",
9
10
  "build": "node scripts/build.js && node scripts/sync-skill.js",
10
11
  "test:build-script": "node scripts/build.test.js",
11
12
  "changelog": "node ../../scripts/generate-release-changelog.js .",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: guanvis
3
- description: 当用户要新建、修改、组装观远 BI / Guandata 的 Card(图表/报表卡片)、用指标平台指标创建的指标卡片、文本卡片、图片卡片、筛选器(selector,含日历/时间宏/区间/离散值)或仪表板(Page),或给出 Card ID、数据集 ID、指标 ID、card.js/page.js、图表类型关键词(柱状/折线/饼/KPI/表格/漏斗/地图/散点等 30+ 种)时,优先使用这个 skill。即使用户只说"做个销售仪表板""用这个指标做张卡片""加个 KPI 卡片""新建一个区域筛选器联动所有图""帮我改一下这个图的图例""把这几个 card 拼成一个 page""加一个本月/近 7 天的快捷日期筛选",也要主动使用。它通过 AI 编写简洁的 JS 脚本(card_*.js / selector_*.js / page.js)定义卡片、筛选器和页面布局,再 pack/publish 上传到目标 BI。认证复用 guancli 共享配置。只想查现有 Card/Page 内容走 guancli
3
+ description: 当用户要新建、修改、组装观远 BI / Guandata 的 Card(图表/报表卡片)、复杂报表 Pro(COMPLEX_REPORT_PRO)、用指标平台指标创建的指标卡片、文本卡片、图片卡片、筛选器(selector,含日历/时间宏/区间/离散值)或仪表板(Page),或给出 Card ID、数据集 ID、指标 ID、card.js/page.js、图表类型关键词(柱状/折线/饼/KPI/表格/漏斗/地图/散点等 30+ 种)时,优先使用这个 skill。即使用户只说"做个销售仪表板""创建一个复杂报表 Pro""用这个指标做张卡片""加个 KPI 卡片""新建一个区域筛选器联动所有图""帮我改一下这个图的图例""把这几个 card 拼成一个 page""加一个本月/近 7 天的快捷日期筛选",也要主动使用。它通过 AI 编写简洁的 JS 脚本(card_*.js / selector_*.js / page.js)定义卡片、筛选器和页面布局,再 pack/publish 上传到目标 BI。认证复用 guancli 共享配置。只想查现有 Card/Page 内容走 guancli。旧版复杂报表不支持创建或编辑。
4
4
  compatibility: "Requires Node.js 14+. Install via npm link (local) or npm install -g @guandata/guanvis (from internal Nexus registry). CLI command: guanvis."
5
5
  ---
6
6
 
@@ -18,25 +18,29 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
18
18
  把可视化生成当作源文件驱动的构建流程:本地 JS DSL 是可编辑事实源,payload、ZIP、线上 Card/Page 都是从源文件生成的派生产物。
19
19
 
20
20
  - `schema.js` 是由 `init` / `checkout` 生成的数据集事实快照,不手改;数据集字段变化时重新运行对应命令。`metrics.js` 是由 `metric-init` 生成的指标事实快照,不手改;指标口径、适用维度或格式变化时重新运行 `metric-init`。
21
+ - 全局参数先用 `guanvis parameter` 创建或复用,再通过 `dynamic-parameters.js` 按 `dpId` 引用;`pack`/`publish` 不会隐式写入参数。创建前必须按名称查询;若系统已有同名有效参数,立即停止并让用户明确选择“更新已有参数”或“换名创建”,不得自动复用或修改。只有用户明确选择更新后,才可按该参数的 `dpId` 执行 `update`。
21
22
  - `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
+ - **Page 归属红线**:任何包含 Card Selector 的资源包都必须同时包含 Page,且包内每个 Card/Selector 都必须被至少一个 Page 引用。禁止发布只有 Card/Selector、没有 Page 的资源包,也禁止留下未放入任何 Page Card/Selector;否则后端会产生 `pg_id` 为空、无法访问且可能阻塞后续页面发布的孤儿卡片。校验失败时在 `page.js` 中放置对应资源,再将 Page Card/Selector 同包 `pack`/`publish`。
24
+ - 已有线上仪表板用 `guanvis checkout <pgId> -d <dir>` 拉成可编辑工程:checkout 会尽量生成 `schema.js`,但数据集无权限/已删除/跨环境残留时只 warning 并继续生成可编辑工程;普通 Card 用 `attachCard(cdId, ".guanvis/base/cards/<cdId>.json")` 接受线上 JSON 作为基线;自定义图表(SDK/ECHARTS_LITE)会把脚本/HTML/CSS 反编译到 `charts/<cdId>.js|.html|.css`,超长内联资源(GeoJSON、base64 图片)抽取回 `charts/<cdId>_assets/` + `__asset_text/__asset_base64` 占位符(逐字节往返验证,未编辑时 diff 恒为空),card JS 用 `attachCard(...).loadContent("charts/<cdId>")` 引用,直接编辑内容文件即可改图表;复杂报表 Pro 会额外下载可编辑 xlsx 到 `templates/`,生成 `createComplexReportPro()` 父卡和嵌套的 attached data view,并使用下一模板版本,旧版复杂报表 checkout 直接拒绝;Page 生成 `createPage(...).setBasePath(...).placeCard("cdId", ...)` 脚本,尽量反编译根布局里的 Tab/CardGroup/AreaTitle/SelGroup 和快捷筛选组,并保留 parentDirId。checkout 工程只用于修改指定 Page,不用于复制新 Page;遇到嵌套布局组件等当前 DSL 不能安全表达的结构,checkout 会直接失败,不生成会清结构的工程。只支持普通仪表板(pgType=PAGE),自助取数/数据大屏/清单页等特殊页面会在 checkout 入口直接报错。
25
+ - **Checkout 账号要求**:checkout 读到的是"当前账号视角"的卡片定义,publish 会把它整体回写。必须用对页面涉及数据集拥有**完整列权限、无脱敏限制**的账号(推荐资源 owner 或管理员)执行 checkout/publish,否则读接口会按该账号权限裁剪 zone 字段/脱敏字段,回写后这些字段会从线上卡片中永久丢失。租户启用多语言时,操作账号语言应与卡片原始语言一致,避免把翻译后的字段显示名固化回卡片。
23
26
  - **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()` 等工厂函数。
24
27
  - Card/Page 描述也是 JS 源文件的一部分:新建看板或改版时,在 `card_*.js` / `page.js` 中写 `.setDescription(...)`,再通过 `preview`/`pack`/`publish` 从 JS 源文件生成并发布资源。
25
28
  - 写脚本前先形成 dashboard contract:目标用户、业务问题、使用的数据集、核心指标、维度拆解、筛选器、页面结构和验证方式。
26
29
  - 每次生成都针对明确目录或明确子目录;不要把 unrelated 示例、旧包或临时 ZIP 混入同一个发布目标。
27
- - Card/Page/Selector 的 `.setId(...)` 建议使用 `guanvis genid` 生成;如果生成或填写的 24 位 ID 以数字开头,重新生成一组,避免 BI 前端把 ID 拼成 CSS selector(如 `#5bef...`)时触发 `querySelector` 语法错误。
30
+ - Card/Page/Selector 的 `.setId(...)` 建议使用 `guanvis genid` 生成(保证字母开头)。数字开头的 24 位 ID 会让 BI 前端把 ID 拼成 CSS selector(如 `#5bef...`)时触发 `querySelector` 语法错误,`preview`/`pack`/`publish` 会对**新建资源**直接报 error 拦截;checkout/attach 的已有线上资源不受此限制(保留原 ID 原地更新)。
28
31
  - 先 `preview/pack` 做本地结构验证,再 `publish/upload`;修改已发布资源前先判断变更类型:仅修复 Card/Page 描述时应保留原资源 ID,使用 `card/page set-description` 并同步维护 JS 中的 `.setDescription(...)`;调整图表、布局、筛选器、字段等看板内容时,按线上仪表板更新策略处理。
32
+ - **编辑红线(编辑 ≠ 删除重建)**:用户要求修改/改名已发布 Page 或 Card 时,必须保留原 pgId/cdId 原地覆盖发布,先按工程来源分流。**本地有源工程**(该 Page 就是当前 guanvis 工程创建并发布的,`card_*.js` / `page.js` 还在,且线上没有在发布后被网页端改动过):直接修改本地 JS 源文件(改名即改 `createCard(...)` / `createPage(...)` 标题)重新 publish 同 ID 覆盖。**线上在发布后被网页端修改过、本地没有原始 JS 工程、或不确定线上是否已被改动**:用 `checkout` 拉取线上版本编辑——Page 改 checkout 生成的 `createPage(...)` 标题,已 checkout 的 Card 用 `attachCard(...).setName(...)`,**不得**把 `attachCard` 改写成 `createCard()`(那会绕过 base JSON,未被 DSL 表达的线上配置会在覆盖发布时丢失,`createCard()` 只用于新增卡片)。**禁止**用"新建一个新 Page/Card + 删除或废弃旧的"来模拟编辑——资源 ID 变化会让收藏、分享链接、订阅推送、门户菜单引用和页面权限配置全部失效,这些状态 guanvis 无法迁移。需要"保留旧页面、出一个新版本"时用 `guanvis page save-as`。全局参数编辑同理:只能 `parameter update <dpId>` 原地更新,禁止 delete 后重建同名参数(dpId 变化会让所有引用它的卡片/筛选器断链);`parameter delete` 只用于用户明确要求删除参数本身。
29
33
  - **资源包安全红线**:Agent 只编辑 DSL 源文件;资源包 ZIP 是 `guanvis pack/publish` 的派生产物,不手工生成、解包修改或重打包。`guanvis upload` 只允许上传 `guanvis pack` 原样生成的 ZIP。若用户要求批量重绑资源、迁移已有页面或复用线上页面结构,先停下来说明风险并确认方案,不要直接改 ZIP。
30
34
  - 发布后优先用 `guancli page get/card get` 回读结构与配置。只有在明确需要视觉质量判断、且当前大模型支持图像理解时,才使用 `guanvis screenshot <pageId>` 生成 PNG 并交给模型分析;不要把截图作为默认闭环步骤,因为图像理解会额外消耗 token/费用。
31
35
 
32
36
  ## AI Quick Reference(速查,详细说明见按需参考资料)
33
37
 
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` 局部调整;筛选栏布局使用 `setFilterPanelLayout()`;颜色、字体和背景继续由主题管理。筛选器在筛选栏和画布间移动优先用动作级 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。
38
+ **Checkout/attachCard 速记**:`attachCard(cardId, jsonPath)` 是“base JSON + 链式 DSL 操作 = 目标 JSON”。它不会修改 base JSON,重复执行同一组 JS 操作应产出相同 payload。已有卡片可继续串接常用 `createCard` 后续操作,包括标题/描述、`setRawSettings`、图例/标签/坐标轴/表格/拆分等视觉设置。已有自定义图表**仅限 subType 为 SDK / ECHARTS_LITE** 支持内容编辑:checkout 自动反编译出 `charts/<cdId>.js`(含内嵌资源抽取),card JS 里 `attachCard(...).loadContent("charts/<cdId>")`;也可用 `.setScript()/.setHtml()/.setCss()/.setLibs()/.addLib()` 内联修改,编辑 `charts/` 下源文件即可改图表脚本。COMPLEX_REPORT(走 Pro 流程)、PLUGIN/PLUGIN_LITE(事实源是插件市场资源)、REPORT_FORM(填报模板配置)调用这些内容编辑 API 会直接报错,不要对它们生成此类修改;详见 `references/builder-reference.md` 自定义图表章节。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` 局部调整;筛选栏布局使用 `setFilterPanelLayout()`;颜色、字体和背景继续由主题管理。筛选器在筛选栏和画布间移动优先用动作级 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
39
 
36
40
  **字段显示名与 Card 标题**:`createCard()` 第二参数是 Card 标题,与字段显示名相互独立。所有数据图表的图例、轴标题、表头、Tooltip 或指标标签都可能使用字段显示名;需要与物理字段名或 calcField 内部名分开时设置 `alias`,如 `f("营收", { alias: "本月营收" })`、`calcField("整体毛利率_kpi", formula, { alias: "整体毛利率" })`。其中 `SINGLE_VALUE`、`KPI_CARD`、`KPI_TREND` 和仪表盘/进度类图表尤其明显。
37
41
 
38
- 1. **工厂函数**:数据集图表用 `createCard()`;已有线上卡片用 `attachCard(cardId, jsonPath)`;指标平台指标卡片用 `createMetricChart()`;筛选器/筛选器组/文本/图片/杜邦/Tab/页面用 `createSelector()` / `createSelectorGroup()` / `createTextCard()` / `createImageCard()` / `createDuPontChart()` / `createAreaTitle()` / `createCardGroup()` / `createTab()` / `createPage()`,不要 `new XxxBuilder()`;checkout JSON 不可复制改造,新卡片必须 create,老卡片才 attach
39
- 2. **注册函数**:`registerCard(card.build())` / `registerMetricChart(card.build())` / `registerSelector(sel.build())` / `registerTextCard(text.build())` / `registerImageCard(image.build())` / `registerDuPontChart(dupont.build())` / `registerPage(page.build())`
42
+ 1. **工厂函数**:数据集图表用 `createCard()`;复杂报表 Pro `createComplexReportPro()` + `createReportWorkbook()` `.setTemplate(xlsx)`;已有线上普通卡片用 `attachCard(cardId, jsonPath)`;指标平台指标卡片用 `createMetricChart()`;筛选器/筛选器组/文本/图片/杜邦/Tab/页面用对应工厂函数,不要 `new XxxBuilder()`。旧版复杂报表拒绝,Pro 父卡也不能用泛化 `attachCard()`
43
+ 2. **注册函数**:`registerCard(card.build())` / `registerComplexReportPro(report.build())` / `registerMetricChart(card.build())` / `registerSelector(sel.build())` / `registerTextCard(text.build())` / `registerImageCard(image.build())` / `registerDuPontChart(dupont.build())` / `registerPage(page.build())`
40
44
  3. **字段引用**:单数据集用 `f("字段名")`,多数据集用 `field(DS, "字段名")`
41
45
  4. **zone maxCount**:`BASIC_COLUMN/BAR/LINE` metric=1;`GROUPED_*/STACKED_*` metric=∞;`KPI_CARD` metric=1;组合图 `*_WITH_LINE` row=1
42
46
  5. **column vs colorBy**:column 放维度(按类别分组着色),colorBy 放度量(按值渐变着色)
@@ -49,7 +53,7 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
49
53
  12. **同环比默认**:用户说同比/环比/同环比/年同比/月环比且未指定输出值时,默认用增长率;未指定模式时默认按日期筛选模式(`ComparativeMode.FILTER_BASED`),普通模式需显式指定 `ComparativeMode.NORMAL`。如果日期字段已经是预聚合周期字段(如 `周开始日期` / `月开始日期`),必须把日期字段声明为 `f("周开始日期", { granularity: Granularity.NONE })`,builder 会生成不带筛选窗口和 `mode` 的同环比,避免按 DAY 筛选窗口计算为空
50
54
  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)`。
51
55
  14. **publish 环境**:认证由底层 CLI 负责——guancli 需先 `guancli auth use <profile>`,guancli-lite 需设置环境变量
52
- 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
56
+ 15. **更新线上仪表板**:checkout 工程默认表示“修改指定线上 Page”,保留原 Page/Card/Selector ID;发布时如 CLI 检测到同 ID Page,必须先向用户说明将覆盖哪些线上资源并取得明确确认,确认后才可加 `--allow-overwrite`,由现有覆盖备份机制兜底。不要把 checkout JSON 复制后改 ID 来做“新版本”。若用户明确要保留原页面并生成新版本,用 `guanvis page save-as <pgId> --suffix _guanvis`(或 `--name <新名>`)做 BI 原生整页另存为——服务端事务复制全部卡片并重映射联动/下钻/父子/指标关系等 ID,命令自动 release 发布副本(7.2.0 之前无草稿态会自动跳过),再基于副本页面 checkout/edit;不要在 guanvis CLI 里手写复杂 ID map,也不要用浏览器手工复制页面。
53
57
  16. **描述维护**:仅修复已发布 Card/Page 的描述时,保留原资源 ID,使用 `guanvis card/page set-description` 更新线上描述;如果本地有对应 JS 工程,也同步更新 `.setDescription(...)`,让源文件与线上描述一致
54
58
  17. **主题切换**:用户描述风格(深色科技风/科技蓝/蓝色简约等)→ 在工程目录里 `guanvis theme preference --keywords "..." --sync`(`[dir]` 可省,默认当前目录);不要选择租户默认的“浅色”/“深色”主题,找不到合适主题时保持/清空偏好,让 preview/pack/publish 自动使用内置“简约”兜底。普通数据集图表与指标平台 MetricChart 都会自动应用主题视觉配置;改版未提风格时 `.applied.json` 会自动继承上次主题,preview/pack/publish 不需要重复指定。**多子目录工程**:每个子目录是独立工程,主题要在子目录里配置 + preview/pack/publish 也必须 `cd` 进对应子目录运行(在根目录直接跑会被命令显式拒绝并给出 cd 提示);详见 `references/theme.md`
55
59
  18. **设计规则**:`preview`/`pack`/`publish` 会自动应用内置设计规则;需要自定义静态卡片默认规则或主题已开放配置时,在工程目录新建 `design-rule.json`,不要手改 `themes/<themeId>.json`;详见 `references/theme.md`
@@ -58,6 +62,19 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
58
62
  21. **布局组件**:支持 `小标题(AreaTitle)` / `卡片组(CardGroup)` / `筛选器组(SelGroup)` / `标签页(Tab)`。布局组件本身只支持放在画布根布局,不支持嵌套组合使用;SelGroup 内只能放 selector;checkout 会反编译根布局组件,无法安全表达的嵌套结构会失败;内部布局能力详见 `references/builder-reference.md`。
59
63
  22. **资源包/checkout JSON 禁止手改**:只改 DSL,不改 ZIP 内部文件,也不改 `.guanvis/raw` / `.guanvis/base` / `.guanvis/manifest.json`;`upload` 只用于上传 `guanvis pack` 原样生成的包。批量重绑、迁移页面、补缺失操作符等需求先讨论方案,必要时扩展 DSL 操作,不手工改生成物。
60
64
  23. **动态字段**:普通卡支持动态维度/动态数值,指标平台卡支持动态维度/动态指标;用户明确需要字段切换时使用 `.addDynamicRow()` / `.addDynamicMetric()` 等 API,细节见 `references/builder-reference.md`。
65
+ 24. **复杂报表 Pro 红线速记**(工作流程见下文「复杂报表 Pro 工作方式」,配方与报错修复先读 `references/complex-report-pro-patterns.md`):
66
+ - **Pro 不是默认功能,需要单独授权**:只有用户明确说了"复杂报表 / 复杂报表 Pro",或修改的卡片本身已是 Pro(checkout 保持原类型),才选 Pro;用户只说"表格/报表/透视表"一律用原生卡片(DATA_GRID/PIVOT_TABLE 等),版式做不到时告知用户"Pro 可实现但需授权"由用户决定。
67
+ - 只支持 `COMPLEX_REPORT_PRO`;旧版复杂报表拒绝,Pro 父卡不能用泛化 `attachCard()`。从零创建优先 Workbook DSL,拿到现成 xlsx 才用 `.setTemplate()`。
68
+ - 同一数据视图只写 `context`,绝不加同源 `filterBy`(嵌套 LP 后端 500);`filterBy` 仅用于跨数据视图父格映射。
69
+ - 左父格(纵向)允许在其列或左侧的**任意行**(阶梯布局、"汇总在上"总计/小计行挂下方维度均合法);上父格(横向)必须同列上方;各最多一个,双父格写 `A4*B3`。
70
+ - 小计三件套:维度父格 → `setContextText` 标签 → `bindSubtotal`(标签同行左侧);总计 `bindGrandTotal` 禁止 context;小计行公式聚合用 `setDynamicFormula("SUM(D2)", { context: 标签格 })`;不要写老版 `G_SUBTOTAL/G_GRANDTOTAL`。
71
+ - 聚合白名单 `SUM/COUNT/AVERAGE/MAX/MIN/PRODUCT/STDDEV/STDDEVP/VAR/VARP`(`AVG`→`AVERAGE`,`COUNTA` 拒绝);`bindDimension` 字段必须 DIM,数值列不能做维度。
72
+ - 分页:单 Sheet、全模板禁 `filter/filterBy`、唯一 `countPerPage` 放无 context 顶层维度。段落块用 `sheet.beginBlock()`。多源 JOIN 用 `.setDataSourceRelations()`。
73
+ - 模板引用的每个字段都必须进对应子视图查询区;子视图 `.setLimit()` 不限制导出行数。
74
+ - 排序放数据视图 `.addSort()`(决定模板展开顺序);行级链接用 `HYPERLINK()` 动态公式(模板格超链接不随扩展复制);涨跌着色可直接用条件数字格式 `[Red]0"↑";[Green]0"↓"`。
75
+ - **计算 BI 优先**:行级公式→guands 普通计算字段;比率/均值等非可加指标→聚合计算字段(`--calc-type aggregation`,引用时不写 aggrType),其小计/总计=每个粒度一个数据视图(禁用 bindSubtotal,会把比率按明细求和);GcExcel `setDynamicFormula` 只做排版级算术(序号/HYPERLINK/单格换算)。模板 `aggregate: COUNT` 作用于视图行不是明细行,计数放视图 zone 模板用 SUM 透传。
76
+ - 新建 Pro 必须与引用它的 Page 同包发布,ID 用 `guanvis genid` 生成;pack/preview 通过≠语义正确,发布后必须 `guancli card preview <proCardId> -o result.xlsx` 检查无 `{{...}}` 残留。发布失败后重试若持续报"找不到相关卡片",直接 `genid` 换新 ID 重发。
77
+ - 编辑已有 Pro 模板:统一走 `.setWorkbook()`。`report decompile` 全量可表达时输出 `createReportWorkbook()` 从零形态;含图片/数据验证等 lossy 部件时输出 `createReportWorkbook("templates/<cdId>.xlsx").editSheet(...)` 基底编辑形态(增量操作,未触及部件原样保留;覆盖模板格需 `{ overwrite: true }`,删除用 `clearCell`/`unmerge`)。publish 成功后 CLI 自动 bump JS 里的 `.setTemplateVersion()`;若提示未找到该调用,下轮改模板前必须手动 +1,否则命中服务端模板缓存静默不生效。
61
78
 
62
79
  ## 何时使用
63
80
 
@@ -66,6 +83,7 @@ compatibility: "Requires Node.js 14+. Install via npm link (local) or npm instal
66
83
  - 新建观远 BI Card(柱形图、折线图、饼图、表格、KPI、散点图、漏斗、地图等 30+ 种)。
67
84
  - 基于指标平台已有指标创建指标卡片(MetricChart,后端 `cdType=13`),支持指标自身格式和适用维度。
68
85
  - 创建文本卡片(含内嵌指标引用)或图片卡片(外链/本地图片)。
86
+ - 创建或 checkout/edit 复杂报表 Pro——**仅当用户明确点名"复杂报表 / 复杂报表 Pro",或被修改的卡片已是 Pro**(Pro 需单独授权,不是默认功能,泛化的"做个表格/报表"需求用原生卡片);旧版复杂报表则明确拒绝并建议外部迁移后新建 Pro。
69
87
  - 创建筛选器并配置联动关系(选择筛选器、日历筛选器等)。
70
88
  - 组装多个 Card 和筛选器为一个仪表板/Page,含 grid layout 布局和筛选器面板。
71
89
  - 批量生成一组 Card 并上传到 BI 系统。
@@ -126,7 +144,7 @@ var card = createMetricChart(ChartType.PIVOT_TABLE, "销售指标分析")
126
144
  registerMetricChart(card.build());
127
145
  ```
128
146
 
129
- `metrics.js` 必须在 `card_*.js` 前加载;目录模式会自动按 `schema.js` → `metrics.js` → `card_*.js` → `selector_*.js` → `page.js` 的顺序执行。
147
+ `metrics.js` 和 `dynamic-parameters.js` 必须在 `card_*.js` 前加载;目录模式会自动按 `schema.js` → `metrics.js` → `dynamic-parameters.js` → `card_*.js` → `selector_*.js` → `page.js` 的顺序执行。
130
148
 
131
149
  ### 3. 设置仪表板主题(按需)
132
150
 
@@ -207,7 +225,7 @@ var card = createCard(ChartType.PIVOT_TABLE, "区域产品交叉分析")
207
225
  registerCard(card.build());
208
226
  ```
209
227
 
210
- ### 5. 编写 page.js(可选)
228
+ ### 5. 编写 page.js(发布 Card/Selector 时必需)
211
229
 
212
230
  ```javascript
213
231
  var page = createPage("销售仪表板")
@@ -215,13 +233,6 @@ var page = createPage("销售仪表板")
215
233
  .setDescription("综合销售分析")
216
234
  .setBackgroundColor("#f5f5f5")
217
235
  .setCardMargin(8)
218
- .setFilterPanelLayout({
219
- type: FilterPanelLayoutType.GRID,
220
- spacing: FilterPanelSpacing.MIDDLE,
221
- padding: { top: 8, right: 12, bottom: 8, left: 12 },
222
- labelPosition: "top",
223
- actionOrder: "right"
224
- })
225
236
  .addRow([{ card: 0, w: 4 }, { card: 1, w: 8 }], 4)
226
237
  .addFullWidthCard(2, 6)
227
238
  .addHalfWidthCards(3, 4, 6);
@@ -229,8 +240,6 @@ var page = createPage("销售仪表板")
229
240
  registerPage(page.build());
230
241
  ```
231
242
 
232
- `setFilterPanelLayout()` 只配置筛选栏的纯布局字段。颜色、字体、背景和控件视觉样式不在该接口范围内,继续跟随主题;字段和值详见 `references/builder-reference.md` 的“筛选栏布局”。
233
-
234
243
  **仪表板主题**:项目目录下没配 `themes/` 时,`preview`/`pack`/`publish` 自动使用 skill 自带的简约主题(embed 在二进制里的 `simple.json`,不依赖租户线上主题列表)。如需切换租户自定义主题或带明确风格的默认主题,请参考 `references/theme.md`,通过 `guanvis theme preference` 写入 `themes/.preference.json`;租户默认“浅色”/“深色”没有特殊样式,不作为可选主题。**JS DSL 不再提供主题相关接口**——任何 `setDashboardTheme(...)` 或 `page.setTheme(...)` 调用都会因函数未定义而报错。
235
244
 
236
245
  指标平台 MetricChart 也会在 `preview`/`pack`/`publish` 阶段自动应用主题视觉配置:透视表会补表格/合计样式,柱线饼等会补坐标轴、图例、数据标签和主题色。主题只写 `settings` 与 `meta.chartMain.props`,不改 `zoneData`、`dsInfo`、`defaultView` 等影响指标查询的字段。
@@ -245,6 +254,7 @@ registerPage(page.build());
245
254
  project/
246
255
  ├── overview/
247
256
  │ ├── schema.js
257
+ │ ├── dynamic-parameters.js ← 全局参数快照(按需)
248
258
  │ ├── themes/ ← 主题偏好与缓存(按需,仅当跑过 theme preference / sync)
249
259
  │ │ ├── .preference.json
250
260
  │ │ ├── .applied.json
@@ -279,7 +289,7 @@ registerPage(p.build());
279
289
  # 生成资源 ID(用于 .setId() 调用)
280
290
  guanvis genid # 生成 1 个
281
291
  guanvis genid 5 # 生成 5 个
282
- # 如果生成或填写的 ID 以数字开头,重新生成一组,避免 BI 前端 querySelector("#<id>") 报非法 selector
292
+ # genid 保证字母开头;手写数字开头 ID 会被 preview/pack/publish 对新建资源直接报错拦截
283
293
 
284
294
  # 生成布局组件 ID
285
295
  guanvis gen-layout-id tab # 生成 1 个 tab_ + 默认 6 位字母
@@ -291,6 +301,7 @@ guanvis gen-layout-id selGroup # 生成 1 个 selGroup_ + 默认 6
291
301
  # 拉取已有线上仪表板为可编辑工程(只读 BI,不发布;尽量生成 schema.js)
292
302
  guanvis checkout <pageId> -d ./existing_dashboard
293
303
  # checkout 后只编辑 schema 外的 card_*.js / selector_*.js / page.js;不要修改或复制 .guanvis/raw、.guanvis/base、.guanvis/manifest.json
304
+ # 自定义图表的脚本/HTML/CSS/内嵌资源会反编译到 charts/ 下(可编辑源文件),改完 pack/publish 自动回填
294
305
  # 若 schema.js 未生成或缺字段,后续可手动 guanvis init <dsId> -d ./existing_dashboard --force 补齐
295
306
  # checkout --overwrite 会清理输出目录下所有根级 .js 和旧 .guanvis,避免 schema/selector/metrics/other.js 残留混入运行
296
307
 
@@ -355,23 +366,97 @@ guanvis pack ./project
355
366
 
356
367
  `publish` 和 `upload` 使用 BI 的 **transfer API**(`/api/manual/template/transfer`),特点:
357
368
  - `needIdMapping=false`:保持资源 ID 不变,重复导入会覆盖同 ID 资源
358
- - 发布前在线覆盖检查只探测 Page ID,不探测 Card/Selector ID;如果资源包不包含 Page,则不会执行在线覆盖检查,避免部分 BI 版本在导入前缓存"找不到相关卡片"
369
+ - 发布前强制校验 Page 归属:资源包包含 Card/Selector 时必须同时包含 Page,且每个 Card/Selector 都必须被至少一个 Page 引用;Card-only 和孤儿 Card/Selector 包会在上传前被拒绝,并提示在 `page.js` 中放置资源后同包发布
370
+ - 在线覆盖检查只探测 Page ID,不探测 Card/Selector ID,避免部分 BI 版本在导入前缓存"找不到相关卡片"
359
371
  - 认证方式:随底层 `guancli fetch` 使用 `Cookie: uIdToken=...`
360
372
  - 需要 `raw-backend-response: TRUE` header 绕过前端代理层
361
373
  - 不需要目标系统开启"一键迁移"开关,所有环境通用
362
374
 
363
375
  **资源包安全约束**:`upload` 只是上传器,不是制作自定义资源包的入口。除非用户明确批准,否则不得上传手工生成、解包修改、重打包或批量替换内部内容后的 ZIP。需要批量重绑数据集、字段、卡片或页面 ID 时,先讨论方案,不要直接改 ZIP。
364
376
 
365
- **线上仪表板更新策略**:新建工程发布的是新 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 发起资源包导出备份并等待导出成功;备份包含 Page 及其组成资源,但不会沿血缘额外导出数据集、数据账户等上游资源,避免普通用户因缺少上游资源所有者权限而无法备份。备份未成功则中止覆盖。CLI 只记录备份导出记录和 packageId,不自动下载资源包;需要回滚时,到 BI 资源迁移导出记录中手动下载该资源包后再导入覆盖回去。
377
+ **线上仪表板更新策略**:新建工程发布的是新 Page;checkout 工程发布的是对 checkout 指定 Page 的覆盖式修改,不承担“复制新版本”职责。需要保留原页面并生成新版本时,不要在 guanvis CLI 内手工复制 JSON 或改 ID map;用 `guanvis page save-as <pgId> --suffix _guanvis`(或 `--name <新名>`,可加 `--parent-dir <dirId>`)做 BI 原生整页另存为,服务端会重映射全部卡片间引用并由命令自动 release 发布副本,然后 checkout 副本 Page 继续编辑。命令会回读副本核对卡片数(无指标平台 license 时指标卡会被服务端过滤)并扫描是否残留源页卡片 ID 引用,出现 ⚠ 警告时先排查再继续。对 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 发起资源包导出备份并等待导出成功;备份包含 Page 及其组成资源,但不会沿血缘额外导出数据集、数据账户等上游资源,避免普通用户因缺少上游资源所有者权限而无法备份。备份未成功则中止覆盖。CLI 只记录备份导出记录和 packageId,不自动下载资源包;需要回滚时,到 BI 资源迁移导出记录中手动下载该资源包后再导入覆盖回去。
378
+
379
+ ## 复杂报表 Pro 工作方式
380
+
381
+ **进入本节的前提**:用户明确点名了"复杂报表 / 复杂报表 Pro",或 checkout 的目标卡片已是 Pro。Pro 是需要单独授权的非默认功能——未点名的表格/报表需求回到「固定工作方式」用原生卡片实现。
382
+
383
+ 与「固定工作方式」同体系(auth → init → 写脚本 → pack → publish),差异只在脚本形态和验证闭环。**动手前先读 `references/complex-report-pro-patterns.md`**:§0 选型门槛与判定(平面表/透视表不要用 Pro)、§2 结构模式配方(分组/交叉/小计/分页/块/多源等 10 个套路直接套用)、§4 报错修复表。
384
+
385
+ ### Pro-1. 新建(init → genid → 数据视图 + workbook → page → pack/publish)
386
+
387
+ ```bash
388
+ guanvis init <dsId> -d ./pro_report/ # 单数据集
389
+ guanvis init <ds1> <ds2> -d ./x/ --alias ORDERS,SALES # 多数据集必须用 --alias 命名全局变量
390
+ cd ./pro_report && guanvis genid 3 # 父卡 + 每个数据视图 + page 各一个 ID
391
+ ```
392
+
393
+ 多数据集时**不要**手写 `var X = defineDataset(...)`(schema.js 的顶层 var 不会进入其他脚本的作用域),一律用 `--alias`;单数据集裸引用可用 `f("字段")`,多数据集用 `field(SALES, "字段")` 指明归属(`f()` 永远取第一个数据集)。
394
+
395
+ `card_01_xxx.js` 最小骨架(分组小计形态;其他结构套 patterns §2 配方):
396
+
397
+ ```javascript
398
+ // 数据视图:模板引用到的字段都必须出现在 row/metric 查询区
399
+ var orders = createCard(ChartType.DATA_GRID, "orders")
400
+ .setId("<genid>")
401
+ .bindDataset(DS)
402
+ .addRow(f("区域"))
403
+ .addMetric(f("金额", { aggrType: AggrType.SUM }));
404
+
405
+ var workbook = createReportWorkbook().addSheet("月报", function (sheet) {
406
+ sheet.merge("A1:B1")
407
+ .setValue("A1", "销售月报", { fontSize: 16, bold: true, horizontalAlignment: "center", verticalAlignment: "center" })
408
+ .setRowHeight(1, 34)
409
+ .setValue("A2", "区域", { bold: true }).setValue("B2", "金额", { bold: true })
410
+ .bindDimension("A3", reportField("orders", "区域"))
411
+ .bindMetric("B3", reportField("orders", "金额"), { aggregate: "SUM", context: "A3", numberFormat: "#,##0.00" })
412
+ .setContextText("A4", "小计", { context: "A3" })
413
+ .bindSubtotal("B4", reportField("orders", "金额"), { aggregate: "SUM", context: "A4" })
414
+ .bindGrandTotal("B5", reportField("orders", "金额"), { aggregate: "SUM" });
415
+ });
416
+
417
+ registerComplexReportPro(createComplexReportPro("销售月报")
418
+ .setId("<genid>")
419
+ .setWorkbook(workbook)
420
+ .addDataView("orders", orders)
421
+ .setColumnWidthStretch(true)
422
+ .build());
423
+ ```
424
+
425
+ `page.js` 正常 `registerPage`(Pro 卡按注册顺序参与 card index,或用 ID 字符串);**所有 Card/Selector 都必须与引用它们的 Page 同包发布**,Pro 也不例外。之后与普通流程一致:`pack` → 确认 → `publish`。
426
+
427
+ ### Pro-2. 编辑已有 Pro(checkout → inspect/decompile → 改 → diff → publish)
428
+
429
+ ```bash
430
+ guanvis checkout <pageId> -d ./work # 模板下载到 templates/,templateVersion 自增
431
+ guanvis report inspect ./work/templates/<cdId>.xlsx # 不开 Excel 看模板格/父格链/环检测
432
+ guanvis report decompile ./work/templates/<cdId>.xlsx -o wb.js # 反编译为 Workbook DSL 或 patch 脚手架
433
+ ```
434
+
435
+ 创建与编辑共用**一套 Workbook DSL**:`createReportWorkbook()` 从零构建(`addSheet`),`createReportWorkbook("templates/<cdId>.xlsx")` 以已有模板为基底做增量编辑(`editSheet`),挂载入口统一是 `.setWorkbook(workbook)`。decompile 有两种产物:模板完全可表达时输出从零构建形态(冻结窗格/条件格式/序号列/阶梯布局都能还原);含 DSL 无法表达部件(内嵌图片、数据验证等)时自动输出基底编辑脚手架 `createReportWorkbook(path).editSheet(...)`——只写要改的操作,未触及部件原样保留,现有模板格以注释列在脚手架里供参考。`editSheet` 内方法与 `addSheet` 完全同名,另有仅编辑模式可用的原语:`clearCell`/`unmerge` 删除,`insertRows`/`removeRows`/`insertCols`/`removeCols` 结构编辑(自动重写模板表达式引用与父格,删除被引用模板格会拒绝;插入后坐标按移位后位置书写),`setCellStyle(cellOrRange, style)` 合并式改样式(只覆盖给出的属性);落点已有模板表达式时须在 options 加 `{ overwrite: true }`。改完 `guanvis diff` 看影响面(基底编辑的指令流会逐条出现在 changeSummary),发布走覆盖确认流程(`--allow-overwrite` 前必须征得用户确认)。
436
+
437
+ **templateVersion 生命周期**:checkout 生成线上版本 +1;`publish` 成功后 CLI 自动把工程 JS 里的 `.setTemplateVersion(N)` 改写为 N+1(防止下一轮模板编辑与线上同版本、命中服务端模板缓存静默不生效)。看到 "Bumped .setTemplateVersion" 输出属正常;若提示找不到调用,须手动在 JS 中补 `.setTemplateVersion(N+1)` 再改模板。
438
+
439
+ **多轮基底编辑防护**:基底编辑(`createReportWorkbook(templatePath)`)的发布结果 = 本地 templates/ 快照 + JS 全部操作。多轮修改必须累积追加操作;只保留新一轮操作会让上一轮修改从线上静默消失。preview/diff/pack/publish 检测到基底落后(发布过后未刷新)会打印 stale-base warning;此时要么确认操作已累积,要么 `guanvis checkout <pgId> -d <dir> --refresh-base` 刷新基底(不动 card_*.js/page.js),刷新后删除已发布的旧操作再写新增量。
440
+
441
+ ### Pro-3. 验证闭环(发布后必做)
442
+
443
+ `pack`/`preview` 只验证结构,**只有 GcExcel 能验证模板语义**:
444
+
445
+ ```bash
446
+ guancli card preview <proCardId> -o result.xlsx # 1. 真实渲染导出
447
+ # 2. 检查 result.xlsx:无 {{...}} 残留、行数/分组符合预期、抽查小计值
448
+ guancli card preview <proCardId> --filter "字段 EQ 值" -o f.xlsx # 3. 有筛选联动时验证 childFilters
449
+ ```
366
450
 
367
451
  ## 文件结构约定
368
452
 
369
453
  目录模式下文件加载顺序:
370
454
  1. `schema.js` — 数据集定义(init 生成;checkout 尽量生成,不可修改)
371
455
  2. `metrics.js` — 指标定义(仅指标卡片需要,自动生成,不可修改)
372
- 3. `card_01_xxx.js` ~ `card_NN_xxx.js` Card 定义(按文件名排序)
373
- 4. `selector_01_xxx.js` ~ `selector_NN_xxx.js` — 筛选器定义(在 card 之后执行,因为联动需要引用 card 索引)
374
- 5. `page.js` — Page/仪表板组装
456
+ 3. `dynamic-parameters.js` 项目全局参数快照(按需,由 parameter 命令或 checkout 生成)
457
+ 4. `card_01_xxx.js` ~ `card_NN_xxx.js` — Card 定义(按文件名排序)
458
+ 5. `selector_01_xxx.js` ~ `selector_NN_xxx.js` 筛选器定义(在 card 之后执行,因为联动需要引用 card 索引)
459
+ 6. `page.js` — Page/仪表板组装
375
460
 
376
461
  自定义图表时,图表内容文件(如 ECharts 脚本)放在 **子目录**(如 `charts/`),避免被当作 card 脚本执行。
377
462
 
@@ -384,6 +469,7 @@ guanvis pack ./project
384
469
  | 基础仪表板 | `evals/sales_dashboard/` | 普通卡片(柱状图、折线图、KPI、饼图)+ 筛选器 + 页面布局 |
385
470
  | 拆分图 | `evals/split_charts/` | 柱形等图表按字段拆分 |
386
471
  | 自定义图表 | `evals/custom_chart_echarts/` | ECharts Lite 自定义图表:柱状图 + 饼图,使用 `loadContent()` 文件模式 |
472
+ | 复杂报表 Pro | `evals/complex_report_pro/` | 最小 Workbook DSL 工程;其余结构形态(小计/交叉/分页/块/多源等)的配方以 `references/complex-report-pro-patterns.md` §2 的代码片段为准 |
387
473
  | Tab 布局 | `evals/tab_layout/` | 单页面 tab 示例,含根布局指标卡、图表卡片、文本卡片、筛选器和 panel 内卡片布局 |
388
474
 
389
475
  生成脚本前先查阅对应示例中的文件结构和写法。
@@ -394,6 +480,8 @@ guanvis pack ./project
394
480
  |------|------|----------|
395
481
  | 字段、计算字段、NumberFormat、高级计算和卡片筛选器 API | `references/api-reference.md` | 编写字段、指标、公式或筛选条件时 |
396
482
  | Card/Page/Tab/Selector/Text/Image/CustomChart Builder API 与枚举 | `references/builder-reference.md` | 编写或修改 JS DSL builder 调用时 |
483
+ | 复杂报表 Pro 选型/配方/报错修复 | `references/complex-report-pro-patterns.md` | 创建或修改 Pro 前先读;pack/publish 报 Pro 相关错误时查 §4 |
484
+ | 复杂报表 Pro Builder API 权威定义 | `references/builder-reference.md` 的 ComplexReportProBuilder | 需要完整方法签名、options 取值或协议细节时 |
397
485
  | zone 校验、图表选型、地图/拆分图、字段对象细节 | `references/validation-and-chart-patterns.md` | pack 报错、选型不确定或需要特殊图表行为时 |
398
486
  | 仪表板主题机制与 theme 命令排错 | `references/theme.md` | 用户指定视觉风格、改版继承主题或主题异常时 |
399
487
  | 在线同步、认证、ID 管理和硬约束 | `references/publish-and-constraints.md` | publish/upload、更新线上资源或确认生成边界时 |
@@ -0,0 +1,63 @@
1
+ var salesView = createCard(ChartType.DATA_GRID, "sales")
2
+ .setId("crpchildsales00000000001")
3
+ .bindDataset(DS)
4
+ .addRow(f("区域"))
5
+ .addRow(f("门店"))
6
+ .addMetric(f("销售额", { aggrType: AggrType.SUM }))
7
+ .addMetric(f("利润", { aggrType: AggrType.SUM }));
8
+
9
+ var workbook = createReportWorkbook()
10
+ .addSheet("经营月报", function(sheet) {
11
+ sheet.setColumnWidth("A", 18)
12
+ .setColumnWidth("B", 18)
13
+ .setColumnWidth("C", 16)
14
+ .setColumnWidth("D", 16)
15
+ .setRowHeight(1, 28)
16
+ .merge("A1:D1")
17
+ .setValue("A1", "门店经营月报", {
18
+ fontSize: 16,
19
+ bold: true,
20
+ horizontalAlignment: "center",
21
+ verticalAlignment: "center",
22
+ backgroundColor: "#D9EAF7",
23
+ borderColor: "#8AA8BF"
24
+ })
25
+ .setValue("A2", "区域", { bold: true, backgroundColor: "#EEF5FA", borderColor: "#8AA8BF" })
26
+ .setValue("B2", "门店", { bold: true, backgroundColor: "#EEF5FA", borderColor: "#8AA8BF" })
27
+ .setValue("C2", "销售额", { bold: true, backgroundColor: "#EEF5FA", borderColor: "#8AA8BF" })
28
+ .setValue("D2", "利润", { bold: true, backgroundColor: "#EEF5FA", borderColor: "#8AA8BF" })
29
+ .bindDimension("A3", reportField("sales", "区域"), {
30
+ expansion: "vertical",
31
+ group: "merge",
32
+ style: { borderColor: "#D0D7DE" }
33
+ })
34
+ .bindDimension("B3", reportField("sales", "门店"), {
35
+ expansion: "vertical",
36
+ context: "A3",
37
+ style: { borderColor: "#D0D7DE" }
38
+ })
39
+ .bindMetric("C3", reportField("sales", "销售额"), {
40
+ aggregate: "SUM",
41
+ expansion: "vertical",
42
+ context: "B3",
43
+ numberFormat: "#,##0.00",
44
+ style: { borderColor: "#D0D7DE" }
45
+ })
46
+ .bindMetric("D3", reportField("sales", "利润"), {
47
+ aggregate: "SUM",
48
+ expansion: "vertical",
49
+ context: "B3",
50
+ numberFormat: "#,##0.00",
51
+ style: { borderColor: "#D0D7DE" }
52
+ });
53
+ });
54
+
55
+ var report = createComplexReportPro("门店经营月报 Pro")
56
+ .setId("crpparentmonthly00000001")
57
+ .setDescription("GuanVis 结构化工作簿 DSL 生成的复杂报表 Pro")
58
+ .setWorkbook(workbook)
59
+ .addDataView("sales", salesView)
60
+ .setPagination(false)
61
+ .setColumnWidthStretch(true);
62
+
63
+ registerComplexReportPro(report.build());
@@ -0,0 +1,6 @@
1
+ var page = createPage("复杂报表 Pro DSL 示例")
2
+ .setId("crppagemonthly0000000001")
3
+ .setDescription("结构化 xlsx、字段绑定、样式和复杂报表 Pro 原生附件协议")
4
+ .addFullWidthCard(0, 12);
5
+
6
+ registerPage(page.build());
@@ -0,0 +1,6 @@
1
+ defineDataset("complex_report_sales", [
2
+ { fdId: "region_fd", name: "区域", fdType: "STRING", metaType: "DIM" },
3
+ { fdId: "store_fd", name: "门店", fdType: "STRING", metaType: "DIM" },
4
+ { fdId: "sales_fd", name: "销售额", fdType: "DOUBLE", metaType: "METRIC" },
5
+ { fdId: "profit_fd", name: "利润", fdType: "DOUBLE", metaType: "METRIC" }
6
+ ], { displayType: "CSV" });
@@ -14,7 +14,7 @@ It covers:
14
14
  Local payload preview from `guanvis/`:
15
15
 
16
16
  ```bash
17
- go run ./cmd/guanvis preview ./evals/percentage_advcalc_roundtrip
17
+ guanvis preview ./evals/percentage_advcalc_roundtrip
18
18
  ```
19
19
 
20
20
  Default-environment publish/readback commands intentionally live in the project golden-answer note rather than this PR, because they publish fixed resource IDs to the shared `default` environment. The manual verification checks:
@@ -11,6 +11,16 @@
11
11
  | `filterField(dsId, fieldName, filterType, filterValue?, opts?)` | 创建筛选字段(带 filterType/filterValue),传给 `.addFilter()` 使用 |
12
12
  | `getDataset(dsId)` | 获取已定义的数据集对象(返回对象,**不要**传给 `field()` 和 `bindDataset()`) |
13
13
 
14
+ ### 全局参数
15
+
16
+ | 函数 | 说明 |
17
+ |------|------|
18
+ | `param(dpId)` | 从 `dynamic-parameters.js` 按稳定 ID 获取参数引用 |
19
+ | `param(dpId).expression()` | 生成计算字段可用的参数表达式 |
20
+ | `selector.bindParameter(paramRef, options?)` | 创建参数筛选器;`options.inheritParent` 默认 `true`,设为 `false` 时可传 `defaultValue` |
21
+
22
+ 参数创建和同步使用 `guanvis parameter`;具体参数运行 `guanvis parameter <command> --help` 查看。
23
+
14
24
  **`field()` vs `calcField()` vs `filterField()` 选择**:
15
25
  - `field()` — 引用数据集中已有的字段,用于维度/指标/排序等;单字段聚合优先在这里设置 `aggrType`,如 `SUM`、`AVG`、`COUNT`、`COUNT_DISTINCT`、`MIN`、`MAX`
16
26
  - `calcField()` — 创建公式计算字段(如利润率 = 利润/销售额),用于必须组合多个字段、多个聚合或数据库函数的指标