@veltra/sheet 2.1.1 → 2.2.0

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.
Files changed (202) hide show
  1. package/dist/components/report/binding-badge.js +76 -0
  2. package/dist/components/report/designer/cell-coords.js +46 -0
  3. package/dist/components/report/designer/col-widths.js +15 -0
  4. package/dist/components/report/designer/conditional-rules/helpers.js +139 -0
  5. package/dist/components/report/designer/drop-highlight-overlay.js +76 -0
  6. package/dist/components/report/designer/float-panel.js +385 -0
  7. package/dist/components/report/designer/role.js +17 -0
  8. package/dist/components/report/designer/rule-preview.js +33 -0
  9. package/dist/components/report/designer/rule-row.js +269 -0
  10. package/dist/components/report/designer/rules-dialog.js +135 -0
  11. package/dist/components/report/designer/topology-overlay.js +122 -0
  12. package/dist/components/report/designer/topology.js +64 -0
  13. package/dist/components/report/designer/use-grid-overlay.js +75 -0
  14. package/dist/components/report/designer-hub.js +177 -0
  15. package/dist/components/report/field-panel-helpers.js +33 -0
  16. package/dist/components/report/field-panel.js +132 -0
  17. package/dist/components/report/filter-bar.js +74 -0
  18. package/dist/components/report/hub-connection-form.js +145 -0
  19. package/dist/components/report/hub-dataset-editor.js +394 -0
  20. package/dist/components/report/report-designer.js +343 -0
  21. package/dist/components/report/report-viewer.js +111 -0
  22. package/dist/components/report/style.d.ts +17 -0
  23. package/dist/components/report/style.js +18 -0
  24. package/dist/components/report/style2.css +869 -0
  25. package/dist/components/report/use-report-designer.js +360 -0
  26. package/dist/components/report/use-report-viewer.js +50 -0
  27. package/dist/{vue → components/sheet}/formula-bar.js +1 -1
  28. package/dist/{vue → components/sheet}/formula-suggest-list.js +1 -1
  29. package/dist/{vue → components/sheet}/import-file.js +1 -1
  30. package/dist/{vue → components/sheet}/insert-count-menu-item.js +1 -1
  31. package/dist/{vue → components/sheet}/insert-image.js +1 -1
  32. package/dist/{vue → components/sheet}/popup-helpers.js +1 -1
  33. package/dist/{vue → components/sheet}/popups/border-popup.js +1 -1
  34. package/dist/{vue → components/sheet}/popups/border-preset-glyph.js +1 -1
  35. package/dist/{vue → components/sheet}/popups/export-popup.js +2 -2
  36. package/dist/{vue → components/sheet}/popups/fill-color-popup.js +1 -1
  37. package/dist/{vue → components/sheet}/popups/find-popup.js +1 -1
  38. package/dist/{vue → components/sheet}/popups/font-color-popup.js +1 -1
  39. package/dist/{vue → components/sheet}/popups/font-size-popup.js +1 -1
  40. package/dist/{vue → components/sheet}/popups/import.worker.d.ts +1 -1
  41. package/dist/{vue → components/sheet}/popups/import.worker.js +1 -1
  42. package/dist/{vue → components/sheet}/popups/insert-image-popup.js +1 -1
  43. package/dist/{vue → components/sheet}/sheet-context-menu.js +1 -1
  44. package/dist/{vue → components/sheet}/sheet-tabs.js +1 -1
  45. package/dist/{vue → components/sheet}/sheet-toolbar.js +1 -1
  46. package/dist/{vue → components/sheet}/sheet.js +8 -1
  47. package/dist/{vue → components/sheet}/use-find-replace.js +1 -1
  48. package/dist/{vue → components/sheet}/use-formula-ref-select.js +1 -1
  49. package/dist/{vue → components/sheet}/use-sheet-grid.js +10 -2
  50. package/dist/{vue → components/sheet}/use-sheet-state.js +3 -3
  51. package/dist/{vue → components/sheet}/use-sheet-tabs-bar.js +1 -1
  52. package/dist/{vue → components/sheet}/use-tool-groups.js +2 -2
  53. package/dist/{vue → components/sheet}/use-tool-popup.js +2 -2
  54. package/dist/{vue → components/sheet}/use-toolbar-scroll.js +1 -1
  55. package/dist/index.d.ts +17 -3
  56. package/dist/index.js +13 -3
  57. package/dist/report/binding.d.ts +45 -0
  58. package/dist/report/binding.js +175 -0
  59. package/dist/report/connector.d.ts +66 -0
  60. package/dist/report/connector.js +159 -0
  61. package/dist/report/export-xlsx.d.ts +19 -0
  62. package/dist/report/export-xlsx.js +95 -0
  63. package/dist/report/filter-bar.d.ts +10 -0
  64. package/dist/report/filter-bar.js +31 -0
  65. package/dist/report/index.d.ts +12 -0
  66. package/dist/report/params.d.ts +13 -0
  67. package/dist/report/params.js +61 -0
  68. package/dist/report/render/aggregate.js +157 -0
  69. package/dist/report/render/builder.d.ts +12 -0
  70. package/dist/report/render/builder.js +139 -0
  71. package/dist/report/render/coordinate.d.ts +25 -0
  72. package/dist/report/render/coordinate.js +733 -0
  73. package/dist/report/render/index.d.ts +14 -0
  74. package/dist/report/render/index.js +21 -0
  75. package/dist/report/render/style-resolver.js +50 -0
  76. package/dist/report/render/template-index.d.ts +24 -0
  77. package/dist/report/render/template-index.js +137 -0
  78. package/dist/report/rules/evaluate.d.ts +25 -0
  79. package/dist/report/rules/evaluate.js +82 -0
  80. package/dist/report/rules/index.d.ts +2 -0
  81. package/dist/report/template.d.ts +59 -0
  82. package/dist/report/template.js +80 -0
  83. package/dist/report/types.d.ts +85 -0
  84. package/dist/tools/context.d.ts +1 -1
  85. package/dist/types/index.d.ts +3 -2
  86. package/dist/types/report-designer.d.ts +36 -0
  87. package/dist/types/report-viewer.d.ts +38 -0
  88. package/dist/types/sheet.d.ts +17 -1
  89. package/package.json +3 -5
  90. package/src/components/report/__test__/conditional-rules-helpers.test.ts +87 -0
  91. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  92. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  93. package/src/components/report/__test__/report-designer.test.ts +388 -0
  94. package/src/components/report/__test__/report-viewer.test.ts +334 -0
  95. package/src/components/report/__test__/topology.test.ts +143 -0
  96. package/src/components/report/__test__/use-report-designer.test.ts +565 -0
  97. package/src/components/report/binding-badge.ts +73 -0
  98. package/src/components/report/designer/cell-coords.ts +94 -0
  99. package/src/components/report/designer/col-widths.ts +33 -0
  100. package/src/components/report/designer/conditional-rules/helpers.ts +186 -0
  101. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  102. package/src/components/report/designer/float-panel.vue +387 -0
  103. package/src/components/report/designer/role.ts +32 -0
  104. package/src/components/report/designer/rule-preview.vue +40 -0
  105. package/src/components/report/designer/rule-row.vue +252 -0
  106. package/src/components/report/designer/rules-dialog.vue +126 -0
  107. package/src/components/report/designer/topology-overlay.vue +127 -0
  108. package/src/components/report/designer/topology.ts +124 -0
  109. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  110. package/src/components/report/designer-hub.vue +198 -0
  111. package/src/components/report/field-panel-helpers.ts +44 -0
  112. package/src/components/report/field-panel.vue +136 -0
  113. package/src/components/report/filter-bar.vue +77 -0
  114. package/src/components/report/hub-connection-form.vue +124 -0
  115. package/src/components/report/hub-dataset-editor.vue +369 -0
  116. package/src/components/report/index.ts +2 -0
  117. package/src/components/report/report-designer.vue +356 -0
  118. package/src/components/report/report-viewer.vue +128 -0
  119. package/src/components/report/style-designer.scss +583 -0
  120. package/src/components/report/style-hub.scss +292 -0
  121. package/src/components/report/style-viewer.scss +104 -0
  122. package/src/components/report/style.scss +3 -0
  123. package/src/components/report/style.ts +20 -0
  124. package/src/components/report/use-report-designer.ts +547 -0
  125. package/src/components/report/use-report-viewer.ts +79 -0
  126. package/src/{vue → components/sheet}/__test__/formula-bar.test.ts +37 -18
  127. package/src/{vue → components/sheet}/__test__/insert-image-popup.test.ts +1 -1
  128. package/src/{vue → components/sheet}/__test__/insert-image.test.ts +1 -1
  129. package/src/{vue → components/sheet}/__test__/sheet-component.test.ts +40 -3
  130. package/src/{vue → components/sheet}/__test__/sheet-context-menu.test.ts +1 -1
  131. package/src/{vue → components/sheet}/formula-bar.vue +1 -1
  132. package/src/{vue → components/sheet}/insert-image.ts +1 -1
  133. package/src/{vue → components/sheet}/popup-helpers.ts +1 -1
  134. package/src/{vue → components/sheet}/popups/border-popup.vue +1 -1
  135. package/src/{vue → components/sheet}/popups/export-popup.vue +2 -2
  136. package/src/{vue → components/sheet}/popups/fill-color-popup.vue +1 -1
  137. package/src/{vue → components/sheet}/popups/find-popup.vue +1 -1
  138. package/src/{vue → components/sheet}/popups/font-color-popup.vue +1 -1
  139. package/src/{vue → components/sheet}/popups/font-size-popup.vue +1 -1
  140. package/src/{vue → components/sheet}/popups/insert-image-popup.vue +1 -1
  141. package/src/{vue → components/sheet}/sheet-context-menu.ts +2 -2
  142. package/src/{vue → components/sheet}/sheet-toolbar.vue +2 -2
  143. package/src/{vue → components/sheet}/sheet.vue +4 -3
  144. package/src/{vue → components/sheet}/use-find-replace.ts +1 -1
  145. package/src/{vue → components/sheet}/use-sheet-grid.ts +7 -3
  146. package/src/{vue → components/sheet}/use-sheet-state.ts +3 -3
  147. package/src/{vue → components/sheet}/use-tool-groups.ts +2 -2
  148. package/src/{vue → components/sheet}/use-tool-popup.ts +2 -2
  149. package/src/index.ts +24 -188
  150. package/src/report/__test__/binding.test.ts +218 -0
  151. package/src/report/__test__/connector.test.ts +218 -0
  152. package/src/report/__test__/export-xlsx.test.ts +118 -0
  153. package/src/report/__test__/filter-bar.test.ts +58 -0
  154. package/src/report/__test__/params.test.ts +156 -0
  155. package/src/report/__test__/render.test.ts +728 -0
  156. package/src/report/__test__/rules.test.ts +131 -0
  157. package/src/report/__test__/template.test.ts +355 -0
  158. package/src/report/binding.ts +243 -0
  159. package/src/report/connector.ts +205 -0
  160. package/src/report/export-xlsx.ts +118 -0
  161. package/src/report/filter-bar.ts +36 -0
  162. package/src/report/index.ts +13 -0
  163. package/src/report/params.ts +96 -0
  164. package/src/report/render/__test__/aggregate.test.ts +16 -0
  165. package/src/report/render/__test__/coordinate.test.ts +405 -0
  166. package/src/report/render/aggregate.ts +240 -0
  167. package/src/report/render/builder.ts +211 -0
  168. package/src/report/render/coordinate.ts +1239 -0
  169. package/src/report/render/index.ts +30 -0
  170. package/src/report/render/style-resolver.ts +68 -0
  171. package/src/report/render/template-index.ts +200 -0
  172. package/src/report/rules/evaluate.ts +113 -0
  173. package/src/report/rules/index.ts +7 -0
  174. package/src/report/template.ts +141 -0
  175. package/src/report/types.ts +96 -0
  176. package/src/tools/builtin.ts +2 -2
  177. package/src/types/index.ts +2 -6
  178. package/src/types/report-designer.ts +36 -0
  179. package/src/types/report-viewer.ts +37 -0
  180. package/src/types/sheet.ts +22 -1
  181. /package/dist/{vue → components/sheet}/style.d.ts +0 -0
  182. /package/dist/{vue → components/sheet}/style.js +0 -0
  183. /package/dist/{vue → components/sheet}/style2.css +0 -0
  184. /package/dist/{vue → components/sheet}/use-formula-suggest.js +0 -0
  185. /package/src/{vue → components/sheet}/__test__/import-file.test.ts +0 -0
  186. /package/src/{vue → components/sheet}/__test__/insert-count-menu-item.test.ts +0 -0
  187. /package/src/{vue → components/sheet}/__test__/use-formula-ref-select.test.ts +0 -0
  188. /package/src/{vue → components/sheet}/__test__/use-formula-suggest.test.ts +0 -0
  189. /package/src/{vue → components/sheet}/formula-suggest-list.vue +0 -0
  190. /package/src/{vue → components/sheet}/import-file.ts +0 -0
  191. /package/src/{vue → components/sheet}/index.ts +0 -0
  192. /package/src/{vue → components/sheet}/insert-count-menu-item.vue +0 -0
  193. /package/src/{vue → components/sheet}/parsing.ts +0 -0
  194. /package/src/{vue → components/sheet}/popups/border-preset-glyph.vue +0 -0
  195. /package/src/{vue → components/sheet}/popups/import.worker.ts +0 -0
  196. /package/src/{vue → components/sheet}/sheet-tabs.vue +0 -0
  197. /package/src/{vue → components/sheet}/style.scss +0 -0
  198. /package/src/{vue → components/sheet}/style.ts +0 -0
  199. /package/src/{vue → components/sheet}/use-formula-ref-select.ts +0 -0
  200. /package/src/{vue → components/sheet}/use-formula-suggest.ts +0 -0
  201. /package/src/{vue → components/sheet}/use-sheet-tabs-bar.ts +0 -0
  202. /package/src/{vue → components/sheet}/use-toolbar-scroll.ts +0 -0
@@ -0,0 +1,38 @@
1
+ import { ReportColWidthEntry } from "../report/export-xlsx.js";
2
+ import { DataConnector } from "../report/connector.js";
3
+ import { ReportTemplate } from "../report/template.js";
4
+ import "../report/index.js";
5
+ import { DeconstructValue } from "@veltra/utils";
6
+ import { Workbook } from "@veltra/sheet-core/core/workbook";
7
+ //#region src/types/report-viewer.d.ts
8
+ /** 报表查看器组件属性(ADR-0003 决策 2) */
9
+ interface ReportViewerProps {
10
+ /** 数据连接器(如 `createHttpConnector({ endpoint })`) */
11
+ connector: DataConnector;
12
+ /**
13
+ * 自包含 Report Template(SheetSnapshot + 内嵌数据集定义,见 `ReportTemplate`):
14
+ * 查看器据此提取实际绑定数据集的查询参数并集生成 Filter Bar,并经连接器取数展开渲染
15
+ */
16
+ template: ReportTemplate;
17
+ /** 承载填充报表的工作簿(USheet 先例:缺省内部自建单 sheet 工作簿) */
18
+ workbook?: Workbook;
19
+ /**
20
+ * 列宽(sheet-core 列宽未进 SheetSnapshot):载入填充结果后写入 VTable 运行时,
21
+ * 供 `exportXlsx()` 读取保真列宽(设计器预览态传入设计态捕获值)
22
+ */
23
+ colWidths?: ReadonlyArray<ReportColWidthEntry>;
24
+ }
25
+ /** 在组件内部引用 */
26
+ interface _ReportViewerExposed {
27
+ /** 重新取数并展开渲染(外部状态变化后主动刷新报表) */
28
+ refresh: () => Promise<void>;
29
+ /**
30
+ * 导出填充报表为 XLSX(取数完成前拒绝;内部读取填充 sheet 与运行时列宽)。
31
+ * 不内置导出按钮,工具栏由下游决定。
32
+ */
33
+ exportXlsx: () => Promise<void>;
34
+ }
35
+ /** 报表查看器组件暴露的方法 */
36
+ type ReportViewerExposed = DeconstructValue<_ReportViewerExposed>;
37
+ //#endregion
38
+ export { ReportViewerExposed, ReportViewerProps, _ReportViewerExposed };
@@ -1,7 +1,7 @@
1
1
  import { SheetContext } from "../tools/context.js";
2
2
  import { ComputedRef } from "vue";
3
3
  import { DeconstructValue } from "@veltra/utils";
4
- import { SheetGrid } from "@veltra/sheet-core/grid/sheet-grid";
4
+ import { ResolveCellRenderer, ResolveCellStyleHook, ResolveDisplayValue, SheetGrid } from "@veltra/sheet-core/grid/sheet-grid";
5
5
  import { Workbook } from "@veltra/sheet-core/core/workbook";
6
6
  import { Sheet } from "@veltra/sheet-core/core/sheet";
7
7
  //#region src/types/sheet.d.ts
@@ -13,12 +13,28 @@ interface SheetProps {
13
13
  rows?: number;
14
14
  /** 渲染列数,默认 26(A..Z) */
15
15
  cols?: number;
16
+ /**
17
+ * 显示值覆盖(设计态 Binding Placeholder 等):覆盖 VTable record,不写 CellData.v
18
+ */
19
+ resolveDisplayValue?: ResolveDisplayValue;
20
+ /**
21
+ * 动态单元格样式:视口渲染时叠加条件样式补丁,不写 CellData.s / StylePool
22
+ */
23
+ resolveCellStyle?: ResolveCellStyleHook;
24
+ /**
25
+ * 动态单元格渲染(ADR-0004):视口布局时按格自定义渲染形态(VTable
26
+ * customLayout,布局构建用 sheet-core 导出的 CustomLayout),返回 undefined
27
+ * 回落默认渲染;不写模型、不进快照
28
+ */
29
+ resolveCellRenderer?: ResolveCellRenderer;
16
30
  /** 是否显示工具栏,默认 true */
17
31
  showToolbar?: boolean;
18
32
  /** 是否显示顶部公式栏(名称框 + fx 输入栏),默认 true */
19
33
  showFormulaBar?: boolean;
20
34
  /** 是否显示底部 sheet 标签栏,默认 true */
21
35
  showTabs?: boolean;
36
+ /** 只读预览(关闭编辑回写、填充柄等写入口) */
37
+ readonly?: boolean;
22
38
  }
23
39
  interface SheetEmits {
24
40
  /** 激活 sheet 切换(点击 tab 或宿主调用 workbook.activateSheet) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/sheet",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -30,14 +30,12 @@
30
30
  "peerDependencies": {
31
31
  "@cat-kit/core": ">=1.1.8",
32
32
  "@cat-kit/fe": ">=1.1.8",
33
+ "@veltra/compositions": "^1.5.0",
33
34
  "@veltra/desktop": "^1.5.0",
34
35
  "@veltra/icons": "^1.5.0",
35
- "@veltra/sheet-core": "^2.1.1",
36
+ "@veltra/sheet-core": "^2.2.0",
36
37
  "@veltra/styles": "^1.5.0",
37
38
  "@veltra/utils": "^1.5.0",
38
39
  "vue": ">=3.5.41"
39
- },
40
- "devDependencies": {
41
- "@visactor/vtable": "^1.26.6"
42
40
  }
43
41
  }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { ConditionalRule } from '../../../report/types'
4
+ import {
5
+ cloneRulesFromDraft,
6
+ coerceValueForOperator,
7
+ createDraftItem,
8
+ defaultRuleValue,
9
+ initDraftFromRules,
10
+ operatorsForFieldType,
11
+ readBetweenValue,
12
+ resolveEvalFieldType,
13
+ ruleEvalFieldOptions,
14
+ RULE_SCOPE_OPTIONS,
15
+ writeBetweenValue
16
+ } from '../designer/conditional-rules/helpers'
17
+
18
+ describe('conditional-rules helpers', () => {
19
+ it('按字段类型返回运算符列表', () => {
20
+ expect(operatorsForFieldType('number').map((o) => o.value)).toEqual([
21
+ 'gt',
22
+ 'gte',
23
+ 'lt',
24
+ 'lte',
25
+ 'eq',
26
+ 'between'
27
+ ])
28
+ expect(operatorsForFieldType('string').map((o) => o.value)).toEqual(['eq', 'contains'])
29
+ })
30
+
31
+ it('运算符切换时重置比较值', () => {
32
+ expect(coerceValueForOperator('between', 'number', 42)).toEqual([0, 100])
33
+ expect(coerceValueForOperator('contains', 'string', 42)).toBe('')
34
+ expect(coerceValueForOperator('gt', 'number', '12')).toBe(12)
35
+ })
36
+
37
+ it('between 读写辅助', () => {
38
+ const value = defaultRuleValue('between', 'number')
39
+ expect(readBetweenValue(value, 0, 'number')).toBe(0)
40
+ expect(readBetweenValue(value, 1, 'number')).toBe(100)
41
+ expect(writeBetweenValue(value, 1, 250, 'number')).toEqual([0, 250])
42
+ })
43
+
44
+ it('草稿与规则互转保持深拷贝', () => {
45
+ const rules: ConditionalRule[] = [
46
+ { operator: 'gt', value: 10, style: { fill: { color: '#FFCCCC' }, font: { bold: true } } }
47
+ ]
48
+ const draft = initDraftFromRules(rules, 'number')
49
+ expect(draft).toHaveLength(1)
50
+
51
+ const cloned = cloneRulesFromDraft(draft)
52
+ expect(cloned[0]).toEqual(rules[0])
53
+ expect(cloned[0]).not.toBe(rules[0])
54
+
55
+ draft[0]!.rule.style.fill!.color = '#000000'
56
+ expect(rules[0]!.style.fill!.color).toBe('#FFCCCC')
57
+ })
58
+
59
+ it('新建草稿项带默认样式', () => {
60
+ const item = createDraftItem(undefined, 'string')
61
+ expect(item.rule.operator).toBe('eq')
62
+ expect(item.rule.value).toBe('')
63
+ expect(item.rule.style.fill?.color).toBeTruthy()
64
+ })
65
+
66
+ it('求值字段与作用范围选项', () => {
67
+ const fields = [
68
+ { name: 'amount', label: '金额', type: 'number' as const },
69
+ { name: 'region', label: '地区', type: 'string' as const }
70
+ ]
71
+ expect(ruleEvalFieldOptions('amount', fields).map((item) => item.label)).toEqual([
72
+ '本格字段',
73
+ '地区'
74
+ ])
75
+ expect(RULE_SCOPE_OPTIONS.map((item) => item.label)).toEqual(['本格', '整行'])
76
+ expect(resolveEvalFieldType({ operator: 'eq', value: '', style: {} }, 'amount', fields)).toBe(
77
+ 'number'
78
+ )
79
+ expect(
80
+ resolveEvalFieldType(
81
+ { operator: 'eq', value: '', style: {}, field: 'region' },
82
+ 'amount',
83
+ fields
84
+ )
85
+ ).toBe('string')
86
+ })
87
+ })
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { DatasetCatalogItem } from '../../../report/types'
4
+ import {
5
+ fieldTypeGlyph,
6
+ filterDatasetsByQuery,
7
+ formatFieldDragPayload,
8
+ parseFieldDragPayload
9
+ } from '../field-panel-helpers'
10
+
11
+ // ---- 内联 fixtures ----
12
+
13
+ const ORDERS: DatasetCatalogItem = {
14
+ id: 'orders',
15
+ label: '销售明细',
16
+ fields: [
17
+ { name: 'customer', label: '客户', type: 'string' },
18
+ { name: 'amount', label: '金额', type: 'number' }
19
+ ]
20
+ }
21
+
22
+ const INVENTORY: DatasetCatalogItem = {
23
+ id: 'inventory',
24
+ label: '库存',
25
+ fields: [
26
+ { name: 'sku', label: 'SKU', type: 'string' },
27
+ { name: 'qty', label: '数量', type: 'number' }
28
+ ]
29
+ }
30
+
31
+ describe('field-panel helpers', () => {
32
+ it('fieldTypeGlyph 按字段类型返回短标识', () => {
33
+ expect(fieldTypeGlyph('string')).toBe('文')
34
+ expect(fieldTypeGlyph('number')).toBe('#')
35
+ expect(fieldTypeGlyph('date')).toBe('日')
36
+ })
37
+
38
+ it('filterDatasetsByQuery 按中文名或英文字段名过滤', () => {
39
+ const datasets = [ORDERS, INVENTORY]
40
+
41
+ expect(filterDatasetsByQuery(datasets, '客户')).toEqual([
42
+ { ...ORDERS, fields: [ORDERS.fields[0]!] }
43
+ ])
44
+
45
+ expect(filterDatasetsByQuery(datasets, 'qty')).toEqual([
46
+ { ...INVENTORY, fields: [INVENTORY.fields[1]!] }
47
+ ])
48
+
49
+ expect(filterDatasetsByQuery(datasets, ' ')).toEqual(datasets)
50
+ expect(filterDatasetsByQuery(datasets, '不存在')).toEqual([])
51
+ })
52
+
53
+ it('字段拖拽负载编解码往返;非法负载返回 null', () => {
54
+ const payload = formatFieldDragPayload('orders', 'customer')
55
+ expect(payload).toBe('orders:customer')
56
+ expect(parseFieldDragPayload(payload)).toEqual({ datasetId: 'orders', fieldName: 'customer' })
57
+
58
+ // 字段名含冒号时按首个分隔符切分
59
+ expect(parseFieldDragPayload('orders:a:b')).toEqual({ datasetId: 'orders', fieldName: 'a:b' })
60
+
61
+ expect(parseFieldDragPayload('')).toBeNull()
62
+ expect(parseFieldDragPayload(':customer')).toBeNull()
63
+ expect(parseFieldDragPayload('orders:')).toBeNull()
64
+ })
65
+ })
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { CellOverlayRect, GridOverlayLayout } from '../designer/cell-coords'
4
+ import { resolveBindingFloatPanelPosition } from '../designer/use-grid-overlay'
5
+
6
+ function cellRect(top: number, height = 24): CellOverlayRect {
7
+ const bottom = top + height
8
+ return {
9
+ left: 100,
10
+ top,
11
+ right: 180,
12
+ bottom,
13
+ width: 80,
14
+ height,
15
+ centerX: 140,
16
+ centerY: top + height / 2
17
+ }
18
+ }
19
+
20
+ const layout: GridOverlayLayout = { offsetX: 0, offsetY: 77, viewW: 640, viewH: 400 }
21
+
22
+ describe('resolveBindingFloatPanelPosition', () => {
23
+ it('顶行格空间不足时改在单元格下方', () => {
24
+ const rect = cellRect(101)
25
+ const position = resolveBindingFloatPanelPosition(rect, layout, 320, 68, 640)
26
+
27
+ expect(position.placement).toBe('below')
28
+ expect(position.top).toBe(rect.bottom + 8)
29
+ })
30
+
31
+ it('中部格优先在单元格上方', () => {
32
+ const rect = cellRect(200)
33
+ const position = resolveBindingFloatPanelPosition(rect, layout, 320, 68, 640)
34
+
35
+ expect(position.placement).toBe('above')
36
+ expect(position.top).toBe(rect.top - 8)
37
+ })
38
+
39
+ it('水平居中并限制在宿主宽度内', () => {
40
+ const rect = cellRect(200)
41
+ const position = resolveBindingFloatPanelPosition(rect, layout, 320, 68, 340)
42
+
43
+ expect(position.left).toBe(170)
44
+ })
45
+ })
@@ -0,0 +1,388 @@
1
+ import { Workbook } from '@veltra/sheet-core'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { createApp, h, nextTick, ref, type App } from 'vue'
4
+
5
+ import { UReportDesigner } from '../../../index'
6
+ import { REPORT_META_NAMESPACE } from '../../../report/binding'
7
+ import type { DataConnection, DataConnector } from '../../../report/connector'
8
+ import type { ReportTemplate } from '../../../report/template'
9
+ import type { ParamValues, ReportBinding } from '../../../report/types'
10
+ import type { ReportDesignerExposed } from '../../../types'
11
+ import * as cellCoords from '../designer/cell-coords'
12
+ import * as gridOverlay from '../designer/use-grid-overlay'
13
+ import { FIELD_DRAG_MIME } from '../field-panel-helpers'
14
+
15
+ // ---- 内联 fixtures:stub connector(实现 DataConnector 接口的内存测试夹具)----
16
+
17
+ function createStubConnector() {
18
+ const calls = {
19
+ test: [] as DataConnection[],
20
+ describe: [] as Array<{ connection: DataConnection; sql: string }>,
21
+ query: [] as Array<{ connection: DataConnection; sql: string; values: ParamValues }>
22
+ }
23
+ /** test 结果可变(失败路径用例切换为业务错误) */
24
+ const state = {
25
+ testOutcome: { ok: true, data: undefined } as Awaited<ReturnType<DataConnector['test']>>
26
+ }
27
+ const connector: DataConnector = {
28
+ test: (connection) => {
29
+ calls.test.push(connection)
30
+ return Promise.resolve(state.testOutcome)
31
+ },
32
+ describe: (connection, sql) => {
33
+ calls.describe.push({ connection, sql })
34
+ return Promise.resolve({
35
+ ok: true,
36
+ data: [
37
+ { name: 'customer', label: '客户', type: 'string' as const },
38
+ { name: 'amount', label: '金额', type: 'number' as const }
39
+ ]
40
+ })
41
+ },
42
+ query: (connection, sql, values) => {
43
+ calls.query.push({ connection, sql, values })
44
+ return Promise.resolve({
45
+ ok: true,
46
+ data: {
47
+ fields: [
48
+ { name: 'customer', label: '客户', type: 'string' as const },
49
+ { name: 'amount', label: '金额', type: 'number' as const }
50
+ ],
51
+ rows: [
52
+ { customer: '甲公司', amount: 100 },
53
+ { customer: '乙公司', amount: 400 }
54
+ ]
55
+ }
56
+ })
57
+ }
58
+ }
59
+ return { connector, calls, state }
60
+ }
61
+
62
+ // ---- 挂载与等待 ----
63
+
64
+ const apps: App[] = []
65
+ const containers: HTMLElement[] = []
66
+
67
+ function mountDesigner(workbook: Workbook, options?: { template?: ReportTemplate }) {
68
+ const { connector, calls, state } = createStubConnector()
69
+ const el = document.createElement('div')
70
+ el.style.width = '960px'
71
+ el.style.height = '600px'
72
+ document.body.appendChild(el)
73
+ containers.push(el)
74
+ const connections = ref<DataConnection[]>([])
75
+ const exposedRef = { value: undefined as ReportDesignerExposed | undefined }
76
+ const app = createApp({
77
+ render: () =>
78
+ h(UReportDesigner, {
79
+ connector,
80
+ workbook,
81
+ connections: connections.value,
82
+ template: options?.template,
83
+ 'onUpdate:connections': (value: DataConnection[]) => {
84
+ connections.value = value
85
+ },
86
+ ref: (value: unknown) => {
87
+ exposedRef.value = value as ReportDesignerExposed | undefined
88
+ }
89
+ })
90
+ })
91
+ app.mount(el)
92
+ apps.push(app)
93
+ return { app, el, connections, exposedRef, calls, state }
94
+ }
95
+
96
+ /** 等待抽屉(teleport + transition)与连接器微任务落定 */
97
+ async function flush(): Promise<void> {
98
+ await nextTick()
99
+ await new Promise((resolve) => setTimeout(resolve, 0))
100
+ await nextTick()
101
+ await nextTick()
102
+ }
103
+
104
+ /** 按文本定位按钮(drawer 经 Teleport 挂到 body,统一在 document 范围找) */
105
+ function findButton(text: string): HTMLButtonElement | undefined {
106
+ return [...document.querySelectorAll<HTMLButtonElement>('button')].find((button) =>
107
+ button.textContent?.includes(text)
108
+ )
109
+ }
110
+
111
+ async function click(button: HTMLElement): Promise<void> {
112
+ button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
113
+ await flush()
114
+ }
115
+
116
+ /** 构造字段拖拽 drop 事件(happy-dom 无 DataTransfer 构造器,注入最小桩) */
117
+ function dispatchFieldDrop(target: HTMLElement, payload: string): void {
118
+ const event = new Event('drop', { bubbles: true, cancelable: true })
119
+ Object.defineProperty(event, 'dataTransfer', {
120
+ value: {
121
+ types: [FIELD_DRAG_MIME],
122
+ getData: (type: string) => (type === FIELD_DRAG_MIME ? payload : '')
123
+ }
124
+ })
125
+ // 落点超出网格视口 → hit-test 落空,回退当前选区(确定性断言)
126
+ Object.defineProperty(event, 'clientX', { value: 99999 })
127
+ Object.defineProperty(event, 'clientY', { value: 99999 })
128
+ target.dispatchEvent(event)
129
+ }
130
+
131
+ /** 构造字段拖拽 dragover 事件 */
132
+ function dispatchFieldDragOver(target: HTMLElement, clientX = 100, clientY = 100): void {
133
+ const event = new Event('dragover', { bubbles: true, cancelable: true })
134
+ Object.defineProperty(event, 'dataTransfer', {
135
+ value: { types: [FIELD_DRAG_MIME], dropEffect: 'copy' }
136
+ })
137
+ Object.defineProperty(event, 'clientX', { value: clientX })
138
+ Object.defineProperty(event, 'clientY', { value: clientY })
139
+ target.dispatchEvent(event)
140
+ }
141
+
142
+ afterEach(() => {
143
+ while (apps.length) apps.pop()!.unmount()
144
+ while (containers.length) containers.pop()!.remove()
145
+ // drawer Teleport 到 body,卸载后清理残留
146
+ document.querySelectorAll('.u-drawer-overlay').forEach((node) => node.remove())
147
+ })
148
+
149
+ describe('UReportDesigner 最小闭环(数据中枢 + 拖拽绑定 + getTemplate)', () => {
150
+ it('连接 CRUD / 真实测试连接走 v-model:connections 与连接器;落格写 meta;getTemplate 吐出快照', async () => {
151
+ const workbook = new Workbook()
152
+ const { el, connections, exposedRef, calls } = mountDesigner(workbook)
153
+ await flush()
154
+
155
+ // 打开数据中枢 drawer(Teleport 到 body)
156
+ const hubTrigger = el.querySelector<HTMLButtonElement>('.u-report-designer__toolbar button')!
157
+ expect(hubTrigger.textContent).toContain('数据中枢')
158
+ await click(hubTrigger)
159
+ expect(document.querySelector('.u-report-hub')).toBeTruthy()
160
+
161
+ // 新建连接:表单出现 → 真实测试连接(经 stub connector)→ 保存
162
+ await click(findButton('新建连接')!)
163
+ expect(document.querySelector('.u-report-hub-connection-form')).toBeTruthy()
164
+
165
+ await click(findButton('测试连接')!)
166
+ expect(calls.test).toHaveLength(1)
167
+ expect(calls.test[0]).toMatchObject({ label: '新连接', type: 'mysql' })
168
+ expect(document.querySelector('.u-report-hub-connection-form__test')?.textContent).toContain(
169
+ '连接成功'
170
+ )
171
+
172
+ await click(findButton('保存')!)
173
+ expect(connections.value).toHaveLength(1)
174
+ expect(connections.value[0]).toMatchObject({ label: '新连接', type: 'mysql' })
175
+ const connectionId = connections.value[0]!.id
176
+
177
+ // 在该连接下新建数据集(编辑器出现,含 SQL 编辑与 schema/preview tabs)
178
+ await click(findButton('+ 新建数据集')!)
179
+ expect(document.querySelector('.u-report-hub-dataset-editor')).toBeTruthy()
180
+ const datasetId = document.querySelector('.u-report-hub__dataset-id')!.textContent!
181
+
182
+ // 关闭 drawer,回到设计态
183
+ await click(findButton('完成')!)
184
+
185
+ // 拖拽字段落格 → 写入 Cell Meta 绑定(落点 hit-test 落空 → 回退当前选区 A1)
186
+ const gridHost = el.querySelector<HTMLElement>('.u-report-designer__grid')!
187
+ dispatchFieldDrop(gridHost, `${datasetId}:customer`)
188
+ await flush()
189
+
190
+ const metas = [...workbook.activeSheet.entriesCellMeta()].filter(
191
+ ([, namespace]) => namespace === REPORT_META_NAMESPACE
192
+ )
193
+ expect(metas).toHaveLength(1)
194
+ const [addr, , payload] = metas[0]!
195
+ expect(addr).toEqual({ row: 0, col: 0 })
196
+ expect(payload as ReportBinding).toMatchObject({
197
+ dataset: datasetId,
198
+ field: 'customer',
199
+ preset: 'detail',
200
+ aggregate: 'list',
201
+ expand: 'down'
202
+ })
203
+
204
+ // getTemplate() 返回含 meta 绑定与内嵌数据集定义(连接对象内嵌,凭据随模板流转)
205
+ const template = exposedRef.value!.getTemplate()
206
+ expect(template.version).toBe(1)
207
+ expect(template.meta).toHaveLength(1)
208
+ expect(template.meta![0]).toMatchObject({ row: 0, col: 0, namespace: 'report' })
209
+ expect(template.datasets).toEqual([
210
+ {
211
+ id: datasetId,
212
+ label: '新数据集',
213
+ connection: { ...connections.value[0]!, id: connectionId },
214
+ sql: ''
215
+ }
216
+ ])
217
+ })
218
+
219
+ it('测试连接业务失败(ok:false)时展示可读错误提示', async () => {
220
+ const workbook = new Workbook()
221
+ const { el, state } = mountDesigner(workbook)
222
+ state.testOutcome = {
223
+ ok: false,
224
+ error: { code: 'CONNECTION_FAILED', message: '数据库连接失败' }
225
+ }
226
+ await flush()
227
+
228
+ await click(el.querySelector<HTMLButtonElement>('.u-report-designer__toolbar button')!)
229
+ await click(findButton('新建连接')!)
230
+ await click(findButton('测试连接')!)
231
+
232
+ const banner = document.querySelector('.u-report-hub-connection-form__test')
233
+ expect(banner?.textContent).toContain('数据库连接失败')
234
+ expect(banner?.classList.contains('is-ok')).toBe(false)
235
+ })
236
+ })
237
+
238
+ describe('UReportDesigner 全量:template 载入与预览模式(内嵌查看器路径)', () => {
239
+ const TEMPLATE_CONNECTION: DataConnection = {
240
+ id: 'conn-template',
241
+ label: '模板连接',
242
+ type: 'mysql',
243
+ host: '127.0.0.1',
244
+ port: 3306,
245
+ database: 'demo',
246
+ username: 'root',
247
+ password: ''
248
+ }
249
+
250
+ function buildTemplate(): ReportTemplate {
251
+ const group: ReportBinding = {
252
+ dataset: 'ds-template',
253
+ field: 'customer',
254
+ preset: 'groupHeader',
255
+ aggregate: 'group',
256
+ expand: 'down',
257
+ sort: 'none',
258
+ conditionalRules: []
259
+ }
260
+ return {
261
+ version: 1,
262
+ cells: [{ row: 0, col: 0, v: '客户' }],
263
+ styles: [],
264
+ merges: [],
265
+ frozen: { rows: 0, cols: 0 },
266
+ rows: 24,
267
+ cols: 10,
268
+ meta: [{ row: 1, col: 0, namespace: REPORT_META_NAMESPACE, payload: group }],
269
+ datasets: [
270
+ {
271
+ id: 'ds-template',
272
+ label: '订单',
273
+ connection: TEMPLATE_CONNECTION,
274
+ sql: 'SELECT customer, amount FROM orders'
275
+ }
276
+ ]
277
+ }
278
+ }
279
+
280
+ function reportMetas(workbook: Workbook) {
281
+ return [...workbook.activeSheet.entriesCellMeta()].filter(
282
+ ([, namespace]) => namespace === REPORT_META_NAMESPACE
283
+ )
284
+ }
285
+
286
+ it('template prop 载入恢复绑定与连接;预览切换经内嵌查看器取数展开;切回设计态绑定不丢', async () => {
287
+ const workbook = new Workbook()
288
+ const { el, connections, exposedRef, calls } = mountDesigner(workbook, {
289
+ template: buildTemplate()
290
+ })
291
+ await flush()
292
+
293
+ // 模板载入:绑定恢复 + 内嵌连接合并进 v-model 列表
294
+ expect(reportMetas(workbook)).toHaveLength(1)
295
+ expect(connections.value.map((item) => item.id)).toEqual(['conn-template'])
296
+
297
+ // 切预览:内嵌查看器按模板内嵌数据集取数(含 Filter Bar 参数默认值)
298
+ await click(findButton('预览模式')!)
299
+ expect(el.querySelector('.u-report-viewer')).toBeTruthy()
300
+ expect(calls.query).toHaveLength(1)
301
+ expect(calls.query[0]!.connection.id).toBe('conn-template')
302
+ expect(calls.query[0]!.sql).toBe('SELECT customer, amount FROM orders')
303
+ // 取数成功:无业务错误 banner;设计格线已卸载
304
+ expect(el.querySelector('.u-report-viewer__error')).toBeFalsy()
305
+ expect(el.querySelector('.u-report-designer__grid')).toBeFalsy()
306
+
307
+ // 切回设计态:绑定不丢,可继续设计
308
+ await click(findButton('设计模式')!)
309
+ expect(el.querySelector('.u-report-viewer')).toBeFalsy()
310
+ expect(el.querySelector('.u-report-designer__grid')).toBeTruthy()
311
+ expect(reportMetas(workbook)).toHaveLength(1)
312
+
313
+ // getTemplate 往返:绑定与内嵌数据集定义完整
314
+ const roundTrip = exposedRef.value!.getTemplate()
315
+ expect(roundTrip.meta).toHaveLength(1)
316
+ expect(roundTrip.datasets).toHaveLength(1)
317
+ expect(roundTrip.datasets![0]).toMatchObject({ id: 'ds-template', label: '订单' })
318
+ expect(roundTrip.datasets![0]!.connection.id).toBe('conn-template')
319
+ })
320
+
321
+ it('预览态导出 XLSX 按钮可见且转调查看器 exportXlsx()', async () => {
322
+ const workbook = new Workbook()
323
+ const { el } = mountDesigner(workbook, { template: buildTemplate() })
324
+ await flush()
325
+
326
+ // 设计态无导出按钮
327
+ expect(findButton('导出 XLSX')).toBeUndefined()
328
+
329
+ await click(findButton('预览模式')!)
330
+ const exportButton = findButton('导出 XLSX')
331
+ expect(exportButton).toBeTruthy()
332
+ await flush()
333
+
334
+ const exportModule = await import('../../../report/export-xlsx')
335
+ const catKitFe = await import('@cat-kit/fe')
336
+ const exportSpy = vi
337
+ .spyOn(exportModule, 'exportFilledReportXlsx')
338
+ .mockResolvedValue(new Uint8Array([80, 75, 3, 4]))
339
+ const saveBlobSpy = vi.spyOn(catKitFe, 'saveBlob').mockImplementation(() => {})
340
+
341
+ await click(exportButton!)
342
+ expect(exportSpy).toHaveBeenCalled()
343
+ expect(saveBlobSpy).toHaveBeenCalled()
344
+ expect(el.querySelector('.u-report-viewer')).toBeTruthy()
345
+
346
+ exportSpy.mockRestore()
347
+ saveBlobSpy.mockRestore()
348
+ })
349
+
350
+ it('字段拖拽 dragover 时落点高亮随 hit-test 更新;落空时提示回退当前选区', async () => {
351
+ const workbook = new Workbook()
352
+ workbook.activeSheet.selectCell({ row: 2, col: 1 })
353
+ const { el } = mountDesigner(workbook)
354
+ await flush()
355
+
356
+ const gridHost = el.querySelector<HTMLElement>('.u-report-designer__grid')!
357
+ const overlayRect = {
358
+ left: 10,
359
+ top: 20,
360
+ right: 90,
361
+ bottom: 44,
362
+ width: 80,
363
+ height: 24,
364
+ centerX: 50,
365
+ centerY: 32
366
+ }
367
+ const hitTestSpy = vi
368
+ .spyOn(cellCoords, 'resolveGridDropAddress')
369
+ .mockReturnValueOnce({ row: 1, col: 2 })
370
+ .mockReturnValue(null)
371
+ const overlaySpy = vi.spyOn(gridOverlay, 'readCellOverlayRect').mockReturnValue(overlayRect)
372
+
373
+ dispatchFieldDragOver(gridHost, 120, 140)
374
+ await flush()
375
+ expect(el.querySelector('.u-report-drop-highlight')).toBeTruthy()
376
+ expect(el.querySelector('.u-report-drop-highlight__fallback')).toBeNull()
377
+
378
+ dispatchFieldDragOver(gridHost, 99999, 99999)
379
+ await flush()
380
+ expect(el.querySelector('.u-report-drop-highlight')).toBeNull()
381
+ const fallback = el.querySelector('.u-report-drop-highlight__fallback')
382
+ expect(fallback).toBeTruthy()
383
+ expect(fallback!.textContent).toContain('B3')
384
+
385
+ hitTestSpy.mockRestore()
386
+ overlaySpy.mockRestore()
387
+ })
388
+ })