@veltra/sheet 2.2.0 → 2.3.1

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 (86) hide show
  1. package/dist/components/report/binding-badge.js +45 -21
  2. package/dist/components/report/designer/col-widths.js +18 -4
  3. package/dist/components/report/designer/conditional-rules/helpers.js +3 -3
  4. package/dist/components/report/designer/float-panel.js +73 -36
  5. package/dist/components/report/designer/rule-row.js +111 -127
  6. package/dist/components/report/designer/rules-dialog.js +5 -22
  7. package/dist/components/report/hub-dataset-editor.js +16 -1
  8. package/dist/components/report/preview-grid-size.js +32 -0
  9. package/dist/components/report/report-designer.js +75 -25
  10. package/dist/components/report/report-viewer.js +38 -25
  11. package/dist/components/report/style2.css +44 -39
  12. package/dist/components/report/use-report-designer.js +34 -12
  13. package/dist/components/report/use-report-viewer.js +6 -1
  14. package/dist/components/sheet/popups/border-popup.js +30 -2
  15. package/dist/components/sheet/popups/fill-color-popup.js +4 -3
  16. package/dist/components/sheet/popups/font-color-popup.js +2 -2
  17. package/dist/components/sheet/popups/font-size-popup.js +2 -2
  18. package/dist/components/sheet/sheet-context-menu.js +6 -3
  19. package/dist/components/sheet/sheet.js +8 -0
  20. package/dist/components/sheet/use-sheet-grid.js +14 -4
  21. package/dist/components/sheet/use-sheet-state.js +3 -2
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +2 -2
  24. package/dist/report/binding.d.ts +49 -11
  25. package/dist/report/binding.js +186 -29
  26. package/dist/report/connector.js +1 -1
  27. package/dist/report/export-xlsx.d.ts +3 -5
  28. package/dist/report/export-xlsx.js +7 -10
  29. package/dist/report/index.d.ts +2 -2
  30. package/dist/report/render/aggregate.js +20 -8
  31. package/dist/report/template.d.ts +8 -4
  32. package/dist/report/template.js +5 -2
  33. package/dist/tools/apply-style.js +29 -0
  34. package/dist/tools/builtin.js +17 -17
  35. package/dist/tools/context.d.ts +38 -5
  36. package/dist/tools/context.js +38 -1
  37. package/dist/types/report-designer.d.ts +2 -0
  38. package/dist/types/report-viewer.d.ts +2 -2
  39. package/dist/types/sheet.d.ts +4 -0
  40. package/package.json +15 -6
  41. package/src/components/report/__test__/conditional-rules-helpers.test.ts +1 -0
  42. package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
  43. package/src/components/report/__test__/report-designer.test.ts +76 -0
  44. package/src/components/report/__test__/report-viewer.test.ts +24 -1
  45. package/src/components/report/__test__/use-report-designer.test.ts +142 -10
  46. package/src/components/report/binding-badge.ts +39 -20
  47. package/src/components/report/designer/col-widths.ts +34 -11
  48. package/src/components/report/designer/conditional-rules/helpers.ts +6 -3
  49. package/src/components/report/designer/float-panel.vue +71 -27
  50. package/src/components/report/designer/rule-row.vue +67 -67
  51. package/src/components/report/designer/rules-dialog.vue +3 -19
  52. package/src/components/report/hub-dataset-editor.vue +19 -1
  53. package/src/components/report/preview-grid-size.ts +33 -0
  54. package/src/components/report/report-designer.vue +79 -15
  55. package/src/components/report/report-viewer.vue +37 -29
  56. package/src/components/report/style-designer.scss +45 -41
  57. package/src/components/report/use-report-designer.ts +63 -14
  58. package/src/components/report/use-report-viewer.ts +11 -2
  59. package/src/components/sheet/__test__/sheet-component.test.ts +37 -0
  60. package/src/components/sheet/__test__/sheet-context-menu.test.ts +3 -2
  61. package/src/components/sheet/popups/border-popup.vue +31 -3
  62. package/src/components/sheet/popups/fill-color-popup.vue +5 -4
  63. package/src/components/sheet/popups/font-color-popup.vue +2 -2
  64. package/src/components/sheet/popups/font-size-popup.vue +2 -2
  65. package/src/components/sheet/sheet-context-menu.ts +8 -2
  66. package/src/components/sheet/sheet.vue +2 -0
  67. package/src/components/sheet/use-sheet-grid.ts +17 -3
  68. package/src/components/sheet/use-sheet-state.ts +8 -3
  69. package/src/report/__test__/binding.test.ts +103 -1
  70. package/src/report/__test__/connector.test.ts +21 -0
  71. package/src/report/__test__/export-xlsx.test.ts +4 -4
  72. package/src/report/__test__/render.test.ts +102 -0
  73. package/src/report/__test__/template.test.ts +11 -0
  74. package/src/report/binding.ts +278 -47
  75. package/src/report/connector.ts +3 -2
  76. package/src/report/export-xlsx.ts +9 -14
  77. package/src/report/render/aggregate.ts +29 -8
  78. package/src/report/template.ts +8 -4
  79. package/src/tools/__test__/apply-style.test.ts +129 -0
  80. package/src/tools/apply-style.ts +41 -0
  81. package/src/tools/builtin.ts +18 -18
  82. package/src/tools/context.ts +67 -4
  83. package/src/tools/index.ts +1 -0
  84. package/src/types/report-designer.ts +2 -0
  85. package/src/types/report-viewer.ts +2 -2
  86. package/src/types/sheet.ts +4 -0
@@ -20,11 +20,10 @@ function buildColumnDefs(colWidths) {
20
20
  /**
21
21
  * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽)。
22
22
  * 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
23
- *
24
- * 与 `exportWorkbookXlsx`(sheet-core)的差异:列宽未进 SheetSnapshot,
25
- * 只能经 VTable 运行时读取,故这里接收显式列宽并写入 columns 定义。
23
+ * 列宽取自 sheet 模型(`getColWidths`)。
26
24
  */
27
- async function exportFilledReportXlsx(sheet, colWidths) {
25
+ async function exportFilledReportXlsx(sheet) {
26
+ const widths = [...sheet.getColWidths()];
28
27
  let maxRow = -1;
29
28
  let maxCol = -1;
30
29
  for (const [addr] of sheet.store.entries()) {
@@ -32,17 +31,15 @@ async function exportFilledReportXlsx(sheet, colWidths) {
32
31
  if (addr.col > maxCol) maxCol = addr.col;
33
32
  }
34
33
  for (const [row] of sheet.getRowHeights()) if (row > maxRow) maxRow = row;
35
- for (const [col] of colWidths) if (col > maxCol) maxCol = col;
34
+ for (const [col] of widths) if (col > maxCol) maxCol = col;
36
35
  const rows = maxRow < 0 ? [] : Array.from({ length: maxRow + 1 }, () => Array.from({ length: maxCol + 1 }, () => null));
37
36
  const cells = /* @__PURE__ */ new Map();
38
37
  for (const [addr, data] of sheet.store.entries()) {
39
38
  const row = rows[addr.row];
40
39
  const key = `${addr.row},${addr.col}`;
41
40
  const cell = {};
42
- if (data.s != null) {
43
- const style = sheet.stylePool.get(data.s);
44
- if (style) cell.style = styleToHucre(style);
45
- }
41
+ const style = sheet.getEffectiveStyle(addr);
42
+ if (style) cell.style = styleToHucre(style);
46
43
  if (data.f != null && data.f !== "") {
47
44
  cell.formula = data.f;
48
45
  if (data.v != null) cell.formulaResult = data.v;
@@ -76,7 +73,7 @@ async function exportFilledReportXlsx(sheet, colWidths) {
76
73
  rows,
77
74
  ...cells.size > 0 ? { cells } : {},
78
75
  ...sheet.merges.size > 0 ? { merges: sheet.merges.getMerges().map((range) => rangeToHucre(range)) } : {},
79
- ...colWidths.length > 0 ? { columns: buildColumnDefs(colWidths) } : {}
76
+ ...widths.length > 0 ? { columns: buildColumnDefs(widths) } : {}
80
77
  };
81
78
  const frozen = sheet.frozen;
82
79
  if (frozen.rows > 0 || frozen.cols > 0) writeSheet.freezePane = {
@@ -1,5 +1,5 @@
1
1
  import { ConditionalOperator, ConditionalRule, DatasetCatalogItem, DatasetField, DatasetRecords, ParamValues, QueryParamDef, QueryParamOption, QueryParamType, ReportAggregate, ReportBinding, ReportExpand, ReportPreset, ReportSort } from "./types.js";
2
- import { REPORT_META_NAMESPACE, applyReportPreset, createReportBinding, formatBindingPlaceholder, formatCellAddress, inferColParentCandidate, inferDropPreset, inferReportPreset, inferRowParentCandidate, isExpansionBandRow, isHorizontalExpandBinding, isVerticalExpandBinding, listColParentCandidates, listRowParentCandidates, parseCellAddress, presetBindingPatch, setBindingCatalog } from "./binding.js";
2
+ import { InferParentOptions, ListParentCandidateOptions, REPORT_META_NAMESPACE, applyReportPreset, createReportBinding, formatBindingPlaceholder, formatBindingPlaceholderParts, formatCellAddress, formatParentCandidateLabel, inferColParentCandidate, inferDropPreset, inferParentsForPreset, inferReportPreset, inferRowParentCandidate, isColGroupBinding, isExpansionBandCol, isExpansionBandRow, isHorizontalExpandBinding, isReductionAggregate, isRowGroupBinding, isVerticalExpandBinding, listColParentCandidates, listRowParentCandidates, parseCellAddress, prefersGroupParent, presetBindingPatch, presetStillApplies, setBindingCatalog } from "./binding.js";
3
3
  import { ConditionalEvalContext, evaluateCondition, evaluateConditionalStyle, resolveRuleCompareValue, toComparableNumber } from "./rules/evaluate.js";
4
4
  import "./rules/index.js";
5
5
  import { buildParamDefs, extractParamIds, resolveBoundDatasetParams } from "./params.js";
@@ -9,4 +9,4 @@ import { renderReport, resolveFilledColWidths } from "./render/index.js";
9
9
  import { ConnectionType, ConnectorError, CreateHttpConnectorOptions, DEFAULT_CONNECTION_PORTS, DataConnection, DataConnector, QueryResult, Result, createHttpConnector, resolvePortOnTypeChange } from "./connector.js";
10
10
  import { IncompatibleTemplateVersionError, REPORT_TEMPLATE_VERSION, ReportDatasetDef, ReportTemplate, assertCompatibleTemplateVersion, createReportTemplate, fetchTemplateRecords, getBoundDatasetIds, getTemplateDatasets, resolveParamDefaults, resolveTemplateParams } from "./template.js";
11
11
  import { parseDateRangeValue, patchParamValues, resolveNumberParamValue } from "./filter-bar.js";
12
- export { type ConditionalEvalContext, ConditionalOperator, ConditionalRule, ConnectionType, ConnectorError, CreateHttpConnectorOptions, DEFAULT_CONNECTION_PORTS, DataConnection, DataConnector, DatasetCatalogItem, DatasetField, DatasetRecords, IncompatibleTemplateVersionError, ParamValues, QueryParamDef, QueryParamOption, QueryParamType, QueryResult, REPORT_META_NAMESPACE, REPORT_TEMPLATE_VERSION, ReportAggregate, ReportBinding, ReportColWidthEntry, ReportDatasetDef, ReportExpand, ReportPreset, ReportSort, ReportTemplate, Result, applyReportPreset, assertCompatibleTemplateVersion, buildColumnDefs, buildParamDefs, createHttpConnector, createReportBinding, createReportTemplate, evaluateCondition, evaluateConditionalStyle, exportFilledReportXlsx, extractParamIds, fetchTemplateRecords, formatBindingPlaceholder, formatCellAddress, getBoundDatasetIds, getTemplateDatasets, inferColParentCandidate, inferDropPreset, inferReportPreset, inferRowParentCandidate, isExpansionBandRow, isHorizontalExpandBinding, isVerticalExpandBinding, listColParentCandidates, listRowParentCandidates, mapFilledColWidths, parseCellAddress, parseDateRangeValue, patchParamValues, presetBindingPatch, pxToExcelColWidth, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber };
12
+ export { type ConditionalEvalContext, ConditionalOperator, ConditionalRule, ConnectionType, ConnectorError, CreateHttpConnectorOptions, DEFAULT_CONNECTION_PORTS, DataConnection, DataConnector, DatasetCatalogItem, DatasetField, DatasetRecords, IncompatibleTemplateVersionError, InferParentOptions, ListParentCandidateOptions, ParamValues, QueryParamDef, QueryParamOption, QueryParamType, QueryResult, REPORT_META_NAMESPACE, REPORT_TEMPLATE_VERSION, ReportAggregate, ReportBinding, ReportColWidthEntry, ReportDatasetDef, ReportExpand, ReportPreset, ReportSort, ReportTemplate, Result, applyReportPreset, assertCompatibleTemplateVersion, buildColumnDefs, buildParamDefs, createHttpConnector, createReportBinding, createReportTemplate, evaluateCondition, evaluateConditionalStyle, exportFilledReportXlsx, extractParamIds, fetchTemplateRecords, formatBindingPlaceholder, formatBindingPlaceholderParts, formatCellAddress, formatParentCandidateLabel, getBoundDatasetIds, getTemplateDatasets, inferColParentCandidate, inferDropPreset, inferParentsForPreset, inferReportPreset, inferRowParentCandidate, isColGroupBinding, isExpansionBandCol, isExpansionBandRow, isHorizontalExpandBinding, isReductionAggregate, isRowGroupBinding, isVerticalExpandBinding, listColParentCandidates, listRowParentCandidates, mapFilledColWidths, parseCellAddress, parseDateRangeValue, patchParamValues, prefersGroupParent, presetBindingPatch, presetStillApplies, pxToExcelColWidth, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber };
@@ -25,7 +25,10 @@ function isRowExpanding(binding) {
25
25
  function isColExpanding(binding) {
26
26
  return binding.expand === "right" && (binding.aggregate === "group" || binding.aggregate === "list");
27
27
  }
28
- function rowExpandingAncestors(binding, index) {
28
+ function isReductionAggregate(aggregate) {
29
+ return aggregate === "sum" || aggregate === "avg" || aggregate === "count" || aggregate === "max" || aggregate === "min";
30
+ }
31
+ function rowExpandingAncestors(binding, index, groupsOnly) {
29
32
  const chain = [];
30
33
  let current = binding.rowParent;
31
34
  while (current) {
@@ -36,9 +39,13 @@ function rowExpandingAncestors(binding, index) {
36
39
  });
37
40
  current = parentBinding?.rowParent;
38
41
  }
39
- return chain.filter((item) => isRowExpanding(item.binding));
42
+ return chain.filter((item) => {
43
+ if (!isRowExpanding(item.binding)) return false;
44
+ if (groupsOnly && item.binding.aggregate !== "group") return false;
45
+ return true;
46
+ });
40
47
  }
41
- function colExpandingAncestors(binding, index) {
48
+ function colExpandingAncestors(binding, index, groupsOnly) {
42
49
  const chain = [];
43
50
  let current = binding.colParent;
44
51
  while (current) {
@@ -49,7 +56,11 @@ function colExpandingAncestors(binding, index) {
49
56
  });
50
57
  current = parentBinding?.colParent;
51
58
  }
52
- return chain.filter((item) => isColExpanding(item.binding));
59
+ return chain.filter((item) => {
60
+ if (!isColExpanding(item.binding)) return false;
61
+ if (groupsOnly && item.binding.aggregate !== "group") return false;
62
+ return true;
63
+ });
53
64
  }
54
65
  function mergeFilters(rowFilter, colFilter) {
55
66
  return {
@@ -61,7 +72,8 @@ function resolveAncestorFilters(binding, placement, index, data) {
61
72
  const rowFilter = {};
62
73
  const colFilter = {};
63
74
  let parentFilter = {};
64
- const rowAncestors = rowExpandingAncestors(binding, index);
75
+ const groupsOnly = isReductionAggregate(binding.aggregate);
76
+ const rowAncestors = rowExpandingAncestors(binding, index, groupsOnly);
65
77
  for (let i = 0; i < rowAncestors.length; i++) {
66
78
  const ancestor = rowAncestors[i];
67
79
  if (ancestor.binding.dataset !== binding.dataset) continue;
@@ -74,7 +86,7 @@ function resolveAncestorFilters(binding, placement, index, data) {
74
86
  parentFilter = instance.filter;
75
87
  }
76
88
  parentFilter = {};
77
- const colAncestors = colExpandingAncestors(binding, index);
89
+ const colAncestors = colExpandingAncestors(binding, index, groupsOnly);
78
90
  for (let i = 0; i < colAncestors.length; i++) {
79
91
  const ancestor = colAncestors[i];
80
92
  if (ancestor.binding.dataset !== binding.dataset) continue;
@@ -89,8 +101,8 @@ function resolveAncestorFilters(binding, placement, index, data) {
89
101
  return mergeFilters(rowFilter, colFilter);
90
102
  }
91
103
  function resolveOwnGroupValue(binding, placement, index, data) {
92
- const rowAncestors = rowExpandingAncestors(binding, index);
93
- const colAncestors = colExpandingAncestors(binding, index);
104
+ const rowAncestors = rowExpandingAncestors(binding, index, false);
105
+ const colAncestors = colExpandingAncestors(binding, index, false);
94
106
  let parentFilter = {};
95
107
  for (let i = 0; i < rowAncestors.length; i++) {
96
108
  const ancestor = rowAncestors[i];
@@ -3,7 +3,7 @@ import { DataConnection, DataConnector, Result } from "./connector.js";
3
3
  import { SheetSnapshot } from "@veltra/sheet-core";
4
4
  //#region src/report/template.d.ts
5
5
  /**
6
- * 模板内嵌的数据集定义(Dataset = Data Connection + SQL,ADR-0002 决策 2)。
6
+ * 模板内嵌的数据集定义
7
7
  * 与 playground DatasetDef 同构,connectionId 替换为内嵌的完整连接对象。
8
8
  */
9
9
  interface ReportDatasetDef {
@@ -16,7 +16,7 @@ interface ReportDatasetDef {
16
16
  /** 字段中文名覆盖(name → label);查看器不消费,随模板序列化往返供设计器还原 */
17
17
  fieldOverrides?: Record<string, Partial<Pick<DatasetField, 'label'>>>;
18
18
  }
19
- /** 当前支持的报表模板版本(ADR-0005 决策 4) */
19
+ /** 当前支持的报表模板版本 */
20
20
  declare const REPORT_TEMPLATE_VERSION = 1;
21
21
  /** 模板版本不兼容时抛出 */
22
22
  declare class IncompatibleTemplateVersionError extends Error {
@@ -25,8 +25,9 @@ declare class IncompatibleTemplateVersionError extends Error {
25
25
  /**
26
26
  * 自包含 Report Template:SheetSnapshot + 内嵌数据集定义 + 版本段。
27
27
  * 查看器仅凭 `template` + `connector` 即可完成「参数提取 → 取数 → 展开渲染」闭环;
28
- * 模板可 JSON 序列化流转(凭据随之流转,持久化与安全存储由下游负责,ADR-0003 决策 4)。
28
+ * 模板可 JSON 序列化流转。
29
29
  *
30
+ * `colWidths` 继承自 `SheetSnapshot.colWidths`(模型列索引 → 像素)。
30
31
  * 注意:`Sheet.snapshot()` 不产生 `datasets` / `version` 字段,经 `restore()`/`snapshot()` 往返会丢失,
31
32
  * 由设计器 `getTemplate()` 在吐出快照时重新附加。
32
33
  */
@@ -37,7 +38,10 @@ interface ReportTemplate extends SheetSnapshot {
37
38
  }
38
39
  /** 载入模板前校验版本:缺失或高于当前支持版本时抛可读错误 */
39
40
  declare function assertCompatibleTemplateVersion(template: ReportTemplate): void;
40
- /** 由工作簿快照构造带版本段的 Report Template */
41
+ /**
42
+ * 由工作簿快照构造带版本段的 Report Template。
43
+ * `colWidths` 随 `snapshot` 展开(已在 `SheetSnapshot.colWidths`)。
44
+ */
41
45
  declare function createReportTemplate(snapshot: SheetSnapshot, datasets?: ReportDatasetDef[]): ReportTemplate;
42
46
  /** 读取模板内嵌的数据集定义(旧模板未内嵌时回退空数组) */
43
47
  declare function getTemplateDatasets(template: ReportTemplate): ReportDatasetDef[];
@@ -1,7 +1,7 @@
1
1
  import "./binding.js";
2
2
  import { buildParamDefs } from "./params.js";
3
3
  //#region src/report/template.ts
4
- /** 当前支持的报表模板版本(ADR-0005 决策 4) */
4
+ /** 当前支持的报表模板版本 */
5
5
  const REPORT_TEMPLATE_VERSION = 1;
6
6
  /** 模板版本不兼容时抛出 */
7
7
  var IncompatibleTemplateVersionError = class extends Error {
@@ -15,7 +15,10 @@ function assertCompatibleTemplateVersion(template) {
15
15
  if (template.version === void 0) throw new IncompatibleTemplateVersionError("报表模板缺少 version 字段,请在设计器中重建模板后重试");
16
16
  if (template.version > 1) throw new IncompatibleTemplateVersionError(`报表模板版本 ${template.version} 高于当前支持的版本 1,请升级组件库后重试`);
17
17
  }
18
- /** 由工作簿快照构造带版本段的 Report Template */
18
+ /**
19
+ * 由工作簿快照构造带版本段的 Report Template。
20
+ * `colWidths` 随 `snapshot` 展开(已在 `SheetSnapshot.colWidths`)。
21
+ */
19
22
  function createReportTemplate(snapshot, datasets) {
20
23
  return {
21
24
  ...snapshot,
@@ -0,0 +1,29 @@
1
+ //#region src/tools/apply-style.ts
2
+ /**
3
+ * 按选区相对渲染网格的跨度判定样式写入目标(对齐 Excel):
4
+ * - 跨满全部列且未跨满全部行 → 行默认样式(rowStyles)
5
+ * - 跨满全部行且未跨满全部列 → 列默认样式(colStyles)
6
+ * - 其余(含全表 / 局部)→ 单元格样式
7
+ */
8
+ function classifySelectionStyleTarget(range, renderRows, renderCols) {
9
+ const cols = Math.max(renderCols, 1);
10
+ const rows = Math.max(renderRows, 1);
11
+ const spansAllCols = range.start.col === 0 && range.end.col >= cols - 1;
12
+ const spansAllRows = range.start.row === 0 && range.end.row >= rows - 1;
13
+ if (spansAllCols && !spansAllRows) return "row";
14
+ if (spansAllRows && !spansAllCols) return "col";
15
+ return "cell";
16
+ }
17
+ /** 整行/整列选区 → set-axis-style 写入项(partial)或清除项(clear) */
18
+ function axisStyleItemsForRange(range, axis, patch) {
19
+ const start = axis === "row" ? range.start.row : range.start.col;
20
+ const end = axis === "row" ? range.end.row : range.end.col;
21
+ const items = [];
22
+ for (let index = start; index <= end; index++) items.push({
23
+ index,
24
+ ...patch
25
+ });
26
+ return items;
27
+ }
28
+ //#endregion
29
+ export { axisStyleItemsForRange, classifySelectionStyleTarget };
@@ -17,15 +17,15 @@ function selectionRange(ctx) {
17
17
  function toggleFontFlag(ctx, field) {
18
18
  const sel = selectionRange(ctx);
19
19
  if (!sel) return;
20
- const on = ctx.getCellStyle(sel.activeCell)?.font?.[field] === true;
21
- ctx.setCellStyle(sel.range, { font: { [field]: on ? null : true } });
20
+ const on = ctx.getEffectiveStyle(sel.activeCell)?.font?.[field] === true;
21
+ ctx.applyStyle(sel.range, { font: { [field]: on ? null : true } });
22
22
  }
23
23
  /** 对齐 toggle:点当前档 = 清除,点其它档 = 切换 */
24
24
  function toggleAlign(ctx, axis, value) {
25
25
  const sel = selectionRange(ctx);
26
26
  if (!sel) return;
27
- if (ctx.getCellStyle(sel.activeCell)?.align?.[axis] === value) ctx.setCellStyle(sel.range, { align: { [axis]: null } });
28
- else ctx.setCellStyle(sel.range, { align: { [axis]: value } });
27
+ if (ctx.getEffectiveStyle(sel.activeCell)?.align?.[axis] === value) ctx.applyStyle(sel.range, { align: { [axis]: null } });
28
+ else ctx.applyStyle(sel.range, { align: { [axis]: value } });
29
29
  }
30
30
  /**
31
31
  * 内置工具(dogfood 扩展机制)。
@@ -129,7 +129,7 @@ registerTool({
129
129
  disabled: (ctx) => !ctx.getSelection().activeCell,
130
130
  active: (ctx) => {
131
131
  const active = ctx.getSelection().activeCell;
132
- return !!active && ctx.getCellStyle(active)?.font?.bold === true;
132
+ return !!active && ctx.getEffectiveStyle(active)?.font?.bold === true;
133
133
  },
134
134
  onClick: (ctx) => toggleFontFlag(ctx, "bold")
135
135
  });
@@ -143,7 +143,7 @@ registerTool({
143
143
  disabled: (ctx) => !ctx.getSelection().activeCell,
144
144
  active: (ctx) => {
145
145
  const active = ctx.getSelection().activeCell;
146
- return !!active && ctx.getCellStyle(active)?.font?.italic === true;
146
+ return !!active && ctx.getEffectiveStyle(active)?.font?.italic === true;
147
147
  },
148
148
  onClick: (ctx) => toggleFontFlag(ctx, "italic")
149
149
  });
@@ -157,7 +157,7 @@ registerTool({
157
157
  disabled: (ctx) => !ctx.getSelection().activeCell,
158
158
  active: (ctx) => {
159
159
  const active = ctx.getSelection().activeCell;
160
- return !!active && ctx.getCellStyle(active)?.font?.underline === true;
160
+ return !!active && ctx.getEffectiveStyle(active)?.font?.underline === true;
161
161
  },
162
162
  onClick: (ctx) => toggleFontFlag(ctx, "underline")
163
163
  });
@@ -171,7 +171,7 @@ registerTool({
171
171
  disabled: (ctx) => !ctx.getSelection().activeCell,
172
172
  active: (ctx) => {
173
173
  const active = ctx.getSelection().activeCell;
174
- return !!active && ctx.getCellStyle(active)?.font?.strikethrough === true;
174
+ return !!active && ctx.getEffectiveStyle(active)?.font?.strikethrough === true;
175
175
  },
176
176
  onClick: (ctx) => toggleFontFlag(ctx, "strikethrough")
177
177
  });
@@ -207,7 +207,7 @@ registerTool({
207
207
  disabled: (ctx) => !ctx.getSelection().activeCell,
208
208
  active: (ctx) => {
209
209
  const active = ctx.getSelection().activeCell;
210
- return !!active && ctx.getCellStyle(active)?.align?.horizontal === "left";
210
+ return !!active && ctx.getEffectiveStyle(active)?.align?.horizontal === "left";
211
211
  },
212
212
  onClick: (ctx) => toggleAlign(ctx, "horizontal", "left")
213
213
  });
@@ -221,7 +221,7 @@ registerTool({
221
221
  disabled: (ctx) => !ctx.getSelection().activeCell,
222
222
  active: (ctx) => {
223
223
  const active = ctx.getSelection().activeCell;
224
- return !!active && ctx.getCellStyle(active)?.align?.horizontal === "center";
224
+ return !!active && ctx.getEffectiveStyle(active)?.align?.horizontal === "center";
225
225
  },
226
226
  onClick: (ctx) => toggleAlign(ctx, "horizontal", "center")
227
227
  });
@@ -235,7 +235,7 @@ registerTool({
235
235
  disabled: (ctx) => !ctx.getSelection().activeCell,
236
236
  active: (ctx) => {
237
237
  const active = ctx.getSelection().activeCell;
238
- return !!active && ctx.getCellStyle(active)?.align?.horizontal === "right";
238
+ return !!active && ctx.getEffectiveStyle(active)?.align?.horizontal === "right";
239
239
  },
240
240
  onClick: (ctx) => toggleAlign(ctx, "horizontal", "right")
241
241
  });
@@ -249,7 +249,7 @@ registerTool({
249
249
  disabled: (ctx) => !ctx.getSelection().activeCell,
250
250
  active: (ctx) => {
251
251
  const active = ctx.getSelection().activeCell;
252
- return !!active && ctx.getCellStyle(active)?.align?.vertical === "top";
252
+ return !!active && ctx.getEffectiveStyle(active)?.align?.vertical === "top";
253
253
  },
254
254
  onClick: (ctx) => toggleAlign(ctx, "vertical", "top")
255
255
  });
@@ -263,7 +263,7 @@ registerTool({
263
263
  disabled: (ctx) => !ctx.getSelection().activeCell,
264
264
  active: (ctx) => {
265
265
  const active = ctx.getSelection().activeCell;
266
- return !!active && ctx.getCellStyle(active)?.align?.vertical === "middle";
266
+ return !!active && ctx.getEffectiveStyle(active)?.align?.vertical === "middle";
267
267
  },
268
268
  onClick: (ctx) => toggleAlign(ctx, "vertical", "middle")
269
269
  });
@@ -277,7 +277,7 @@ registerTool({
277
277
  disabled: (ctx) => !ctx.getSelection().activeCell,
278
278
  active: (ctx) => {
279
279
  const active = ctx.getSelection().activeCell;
280
- return !!active && ctx.getCellStyle(active)?.align?.vertical === "bottom";
280
+ return !!active && ctx.getEffectiveStyle(active)?.align?.vertical === "bottom";
281
281
  },
282
282
  onClick: (ctx) => toggleAlign(ctx, "vertical", "bottom")
283
283
  });
@@ -291,13 +291,13 @@ registerTool({
291
291
  disabled: (ctx) => !ctx.getSelection().activeCell,
292
292
  active: (ctx) => {
293
293
  const active = ctx.getSelection().activeCell;
294
- return !!active && ctx.getCellStyle(active)?.align?.wrap === true;
294
+ return !!active && ctx.getEffectiveStyle(active)?.align?.wrap === true;
295
295
  },
296
296
  onClick: (ctx) => {
297
297
  const sel = selectionRange(ctx);
298
298
  if (!sel) return;
299
- const on = ctx.getCellStyle(sel.activeCell)?.align?.wrap === true;
300
- ctx.setCellStyle(sel.range, { align: { wrap: on ? null : true } });
299
+ const on = ctx.getEffectiveStyle(sel.activeCell)?.align?.wrap === true;
300
+ ctx.applyStyle(sel.range, { align: { wrap: on ? null : true } });
301
301
  }
302
302
  });
303
303
  registerTool({
@@ -37,12 +37,33 @@ interface SheetContext {
37
37
  getDisplayValue(addr: CellAddress): CellValue | undefined;
38
38
  /** 合并语义下的单元格信息(普通格 / 合并锚点 / 被覆盖格) */
39
39
  getCellInfo(addr: CellAddress): CellInfo;
40
- /** 设置区域样式(部分合并语义,见 CellStylePatch);空样式 = 删除 s 字段 */
40
+ /**
41
+ * 按选区形态写样式(Excel 语义):
42
+ * 整行选区 → rowStyles;整列选区 → colStyles;其余 → 逐格。
43
+ * 判定依赖 createSheetContext 注入的渲染尺寸;未注入时一律走 setCellStyle。
44
+ */
45
+ applyStyle(range: CellRange, partial: CellStylePatch): void;
46
+ /**
47
+ * 选区样式目标(与 applyStyle 同一判定);未注入渲染尺寸时恒为 'cell'。
48
+ */
49
+ resolveStyleTarget(range: CellRange): 'row' | 'col' | 'cell';
50
+ /**
51
+ * 按选区形态清除样式:整行/整列清对应默认样式;其余清单元格 s。
52
+ * 未注入渲染尺寸时一律走 clearCellStyle。
53
+ */
54
+ clearStyle(range: CellRange): void;
55
+ /** 设置区域单元格样式(部分合并语义,见 CellStylePatch);空样式 = 删除 s 字段 */
41
56
  setCellStyle(range: CellRange, partial: CellStylePatch): void;
42
- /** 清除区域样式(保留值 / 公式) */
57
+ /** 清除区域单元格样式(保留值 / 公式) */
43
58
  clearCellStyle(range: CellRange): void;
44
- /** 读取单元格样式(原始存储语义:被覆盖格 → undefined) */
59
+ /** 设置行默认样式(部分合并,进 undo) */
60
+ setRowStyle(row: number, partial: CellStylePatch): void;
61
+ /** 设置列默认样式(部分合并,进 undo) */
62
+ setColStyle(col: number, partial: CellStylePatch): void;
63
+ /** 读取单元格样式(原始存储语义:被覆盖格 → undefined;不含行列默认) */
45
64
  getCellStyle(addr: CellAddress): CellStyle | undefined;
65
+ /** 有效样式:列 → 行 → 格字段级叠加(空格可继承行列默认) */
66
+ getEffectiveStyle(addr: CellAddress): CellStyle | undefined;
46
67
  setCellValue(addr: CellAddress, value: CellValue): void;
47
68
  setCellFormula(addr: CellAddress, formula: string): void;
48
69
  /** 批量写入(一次调用 = 一个 undo 单元) */
@@ -86,6 +107,17 @@ interface SheetContext {
86
107
  id?: string;
87
108
  }) => void): () => void;
88
109
  }
110
+ /** createSheetContext 可选配置 */
111
+ interface SheetContextOptions {
112
+ /**
113
+ * 当前渲染网格尺寸(与 SheetGrid / resolveRenderSize 一致)。
114
+ * applyStyle / clearStyle 用其判定整行/整列;缺省时不走行列默认样式。
115
+ */
116
+ resolveGridSize?: () => {
117
+ rows: number;
118
+ cols: number;
119
+ };
120
+ }
89
121
  /**
90
122
  * 创建工具上下文。
91
123
  * @param resolveSheet 活动 sheet 解析器(USheet 传 `() => activeSheet`,
@@ -94,7 +126,8 @@ interface SheetContext {
94
126
  * 支持传解析函数 `() => Workbook`——USheet 在 props.workbook 动态切换后,
95
127
  * 上下文始终解析到**当前**工作簿(#2:旧实现取构造时值快照,宿主切换
96
128
  * workbook prop 后导出工具仍导出旧工作簿)
129
+ * @param options.resolveGridSize 渲染尺寸解析器(整行/整列样式路由)
97
130
  */
98
- declare function createSheetContext(resolveSheet: Sheet | (() => Sheet), workbook?: Workbook | (() => Workbook)): SheetContext;
131
+ declare function createSheetContext(resolveSheet: Sheet | (() => Sheet), workbook?: Workbook | (() => Workbook), options?: SheetContextOptions): SheetContext;
99
132
  //#endregion
100
- export { SheetContext, createSheetContext };
133
+ export { SheetContext, SheetContextOptions, createSheetContext };
@@ -1,3 +1,4 @@
1
+ import { axisStyleItemsForRange, classifySelectionStyleTarget } from "./apply-style.js";
1
2
  //#region src/tools/context.ts
2
3
  /**
3
4
  * 创建工具上下文。
@@ -7,10 +8,16 @@
7
8
  * 支持传解析函数 `() => Workbook`——USheet 在 props.workbook 动态切换后,
8
9
  * 上下文始终解析到**当前**工作簿(#2:旧实现取构造时值快照,宿主切换
9
10
  * workbook prop 后导出工具仍导出旧工作簿)
11
+ * @param options.resolveGridSize 渲染尺寸解析器(整行/整列样式路由)
10
12
  */
11
- function createSheetContext(resolveSheet, workbook) {
13
+ function createSheetContext(resolveSheet, workbook, options) {
12
14
  const sheet = typeof resolveSheet === "function" ? resolveSheet : () => resolveSheet;
13
15
  const resolveWorkbook = typeof workbook === "function" ? workbook : workbook != null ? () => workbook : void 0;
16
+ const resolveGridSize = options?.resolveGridSize;
17
+ const resolveTarget = (range) => {
18
+ const size = resolveGridSize?.();
19
+ return size ? classifySelectionStyleTarget(range, size.rows, size.cols) : "cell";
20
+ };
14
21
  return {
15
22
  get workbook() {
16
23
  return resolveWorkbook?.();
@@ -29,9 +36,39 @@ function createSheetContext(resolveSheet, workbook) {
29
36
  setCells: (items) => sheet().setCells(items),
30
37
  mergeCells: (range) => sheet().mergeCells(range),
31
38
  unmergeCells: (range) => sheet().unmergeCells(range),
39
+ applyStyle: (range, partial) => {
40
+ const target = resolveTarget(range);
41
+ if (target === "row" || target === "col") {
42
+ const items = axisStyleItemsForRange(range, target, { partial });
43
+ if (items.length === 0) return;
44
+ sheet().executeCommand("sheet.command.set-axis-style", {
45
+ axis: target,
46
+ items
47
+ });
48
+ return;
49
+ }
50
+ sheet().setCellStyle(range, partial);
51
+ },
52
+ resolveStyleTarget: (range) => resolveTarget(range),
53
+ clearStyle: (range) => {
54
+ const target = resolveTarget(range);
55
+ if (target === "row" || target === "col") {
56
+ const items = axisStyleItemsForRange(range, target, { clear: true });
57
+ if (items.length === 0) return;
58
+ sheet().executeCommand("sheet.command.set-axis-style", {
59
+ axis: target,
60
+ items
61
+ });
62
+ return;
63
+ }
64
+ sheet().clearCellStyle(range);
65
+ },
32
66
  setCellStyle: (range, partial) => sheet().setCellStyle(range, partial),
33
67
  clearCellStyle: (range) => sheet().clearCellStyle(range),
68
+ setRowStyle: (row, partial) => sheet().setRowStyle(row, partial),
69
+ setColStyle: (col, partial) => sheet().setColStyle(col, partial),
34
70
  getCellStyle: (addr) => sheet().getCellStyle(addr),
71
+ getEffectiveStyle: (addr) => sheet().getEffectiveStyle(addr),
35
72
  executeCommand: (commandId, params) => sheet().executeCommand(commandId, params),
36
73
  get frozen() {
37
74
  return sheet().frozen;
@@ -24,6 +24,8 @@ interface ReportDesignerProps {
24
24
  /** 报表设计器组件定义的事件 */
25
25
  interface ReportDesignerEmits {
26
26
  (e: 'update:connections', value: DataConnection[]): void;
27
+ /** 数据集增删改或数据中枢关闭:宿主可立即持久化工作区 */
28
+ (e: 'datasets-change'): void;
27
29
  }
28
30
  /** 在组件内部引用 */
29
31
  interface _ReportDesignerExposed {
@@ -17,8 +17,8 @@ interface ReportViewerProps {
17
17
  /** 承载填充报表的工作簿(USheet 先例:缺省内部自建单 sheet 工作簿) */
18
18
  workbook?: Workbook;
19
19
  /**
20
- * 列宽(sheet-core 列宽未进 SheetSnapshot):载入填充结果后写入 VTable 运行时,
21
- * 供 `exportXlsx()` 读取保真列宽(设计器预览态传入设计态捕获值)
20
+ * 列宽显式覆盖(可选):优先于模板 `colWidths` / 展开映射结果写入模型与网格。
21
+ * 列宽已进 `SheetSnapshot.colWidths`,通常无需再传。
22
22
  */
23
23
  colWidths?: ReadonlyArray<ReportColWidthEntry>;
24
24
  }
@@ -33,6 +33,10 @@ interface SheetProps {
33
33
  showFormulaBar?: boolean;
34
34
  /** 是否显示底部 sheet 标签栏,默认 true */
35
35
  showTabs?: boolean;
36
+ /** 是否显示行号列,默认 true */
37
+ showRowHeader?: boolean;
38
+ /** 是否显示列字母表头,默认 true */
39
+ showColHeader?: boolean;
36
40
  /** 只读预览(关闭编辑回写、填充柄等写入口) */
37
41
  readonly?: boolean;
38
42
  }
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "@veltra/sheet",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
+ "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
+ "bugs": {
6
+ "url": "https://github.com/cabinet-fe/ultra-ui/issues"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/cabinet-fe/ultra-ui",
11
+ "directory": "packages/sheet"
12
+ },
4
13
  "files": [
5
14
  "dist",
6
15
  "src"
@@ -30,12 +39,12 @@
30
39
  "peerDependencies": {
31
40
  "@cat-kit/core": ">=1.1.8",
32
41
  "@cat-kit/fe": ">=1.1.8",
33
- "@veltra/compositions": "^1.5.0",
34
- "@veltra/desktop": "^1.5.0",
42
+ "@veltra/compositions": "^1.6.2",
43
+ "@veltra/desktop": "^1.6.2",
35
44
  "@veltra/icons": "^1.5.0",
36
- "@veltra/sheet-core": "^2.2.0",
37
- "@veltra/styles": "^1.5.0",
38
- "@veltra/utils": "^1.5.0",
45
+ "@veltra/sheet-core": "^2.3.1",
46
+ "@veltra/styles": "^1.6.2",
47
+ "@veltra/utils": "^1.6.2",
39
48
  "vue": ">=3.5.41"
40
49
  }
41
50
  }
@@ -72,6 +72,7 @@ describe('conditional-rules helpers', () => {
72
72
  '本格字段',
73
73
  '地区'
74
74
  ])
75
+ expect(ruleEvalFieldOptions('amount', fields).map((item) => item.value)).toEqual(['', 'region'])
75
76
  expect(RULE_SCOPE_OPTIONS.map((item) => item.label)).toEqual(['本格', '整行'])
76
77
  expect(resolveEvalFieldType({ operator: 'eq', value: '', style: {} }, 'amount', fields)).toBe(
77
78
  'number'
@@ -0,0 +1,63 @@
1
+ import type { SheetSnapshot } from '@veltra/sheet-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { previewGridSize } from '../preview-grid-size'
5
+
6
+ function snap(
7
+ partial: Partial<SheetSnapshot> & Pick<SheetSnapshot, 'cells'>
8
+ ): Pick<SheetSnapshot, 'cells' | 'merges' | 'meta' | 'rows'> {
9
+ return { merges: [], rows: 0, ...partial }
10
+ }
11
+
12
+ describe('previewGridSize', () => {
13
+ it('template 模式用内容包围盒,忽略设计态 canvas 尺寸', () => {
14
+ expect(
15
+ previewGridSize(
16
+ snap({
17
+ cells: [
18
+ { row: 0, col: 0, v: '客户' },
19
+ { row: 0, col: 1, v: '金额' }
20
+ ],
21
+ rows: 24,
22
+ cols: 10
23
+ }),
24
+ 'template'
25
+ )
26
+ ).toEqual({ rows: 1, cols: 2 })
27
+ })
28
+
29
+ it('filled 模式行保留展开布局尺寸,列按包围盒收敛(去掉设计态撑大的列)', () => {
30
+ expect(
31
+ previewGridSize(
32
+ snap({
33
+ cells: [
34
+ { row: 0, col: 0, v: '客户' },
35
+ { row: 3, col: 1, v: 300 }
36
+ ],
37
+ rows: 4,
38
+ cols: 10
39
+ }),
40
+ 'filled'
41
+ )
42
+ ).toEqual({ rows: 4, cols: 2 })
43
+ })
44
+
45
+ it('空快照至少 1×1(VTable 需要非零网格)', () => {
46
+ expect(previewGridSize(snap({ cells: [] }), 'template')).toEqual({ rows: 1, cols: 1 })
47
+ })
48
+
49
+ it('包围盒计入合并区终点与 meta 绑定格', () => {
50
+ expect(
51
+ previewGridSize(
52
+ snap({
53
+ cells: [{ row: 0, col: 0, v: '标题' }],
54
+ merges: [{ start: { row: 0, col: 0 }, end: { row: 0, col: 2 } }],
55
+ meta: [{ row: 2, col: 1, namespace: 'report', payload: {} }],
56
+ rows: 24,
57
+ cols: 10
58
+ }),
59
+ 'template'
60
+ )
61
+ ).toEqual({ rows: 3, cols: 3 })
62
+ })
63
+ })