@veltra/sheet 2.3.3 → 2.3.4

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.
@@ -46,21 +46,33 @@ function createBindingBadgeStyleResolver(getBindingAt) {
46
46
  };
47
47
  }
48
48
  /**
49
+ * 单元格对齐 → 徽章容器 flex 布局映射(纯函数,便于单测):
50
+ * `horizontal` left / 缺省 → 主轴起点;`vertical` middle / 缺省 → 交叉轴居中。
51
+ */
52
+ function badgeLayoutProps(align) {
53
+ return {
54
+ justifyContent: align?.horizontal === "center" ? "center" : align?.horizontal === "right" ? "flex-end" : "flex-start",
55
+ alignItems: align?.vertical === "top" ? "flex-start" : align?.vertical === "bottom" ? "flex-end" : "center"
56
+ };
57
+ }
58
+ /**
49
59
  * 绑定格富渲染徽章(ADR-0004 `resolveCellRenderer` 首个消费者):
50
- * 绑定单元格渲染为左对齐徽章(强调色聚合标签 + 中性色字段标签),
60
+ * 绑定单元格渲染为徽章布局(强调色聚合标签 + 中性色字段标签),
61
+ * 布局跟随单元格水平 / 垂直对齐(经 `resolveAlign` 读有效样式);
51
62
  * 未绑定格返回 `undefined` 回落默认渲染。
52
63
  */
53
- function createBindingBadgeRenderer(getBindingAt, resolveLabel) {
64
+ function createBindingBadgeRenderer(getBindingAt, resolveLabel, resolveAlign) {
54
65
  return (addr) => {
55
66
  const binding = getBindingAt(addr);
56
67
  if (!binding) return void 0;
57
68
  const { fg } = resolveBadgeColor(binding);
58
69
  const { tag, label } = formatBindingPlaceholderParts(binding, resolveLabel);
70
+ const { justifyContent, alignItems } = badgeLayoutProps(resolveAlign?.(addr));
59
71
  const rootContainer = new CustomLayout.Container({
60
72
  display: "flex",
61
73
  flexDirection: "row",
62
- justifyContent: "flex-start",
63
- alignItems: "center"
74
+ justifyContent,
75
+ alignItems
64
76
  });
65
77
  const tagEl = new CustomLayout.Tag({
66
78
  text: tag,
@@ -97,4 +109,4 @@ function createBindingBadgeRenderer(getBindingAt, resolveLabel) {
97
109
  };
98
110
  }
99
111
  //#endregion
100
- export { REPORT_PRESET_BADGE_COLORS, createBindingBadgeRenderer, createBindingBadgeStyleResolver };
112
+ export { REPORT_PRESET_BADGE_COLORS, badgeLayoutProps, createBindingBadgeRenderer, createBindingBadgeStyleResolver };
@@ -295,7 +295,7 @@ function useReportDesigner(options) {
295
295
  return props.connector.query(connection, dataset.sql, values);
296
296
  }
297
297
  const resolveCellStyle = createBindingBadgeStyleResolver(getBindingAt);
298
- const resolveCellRenderer = createBindingBadgeRenderer(getBindingAt, resolveFieldLabel);
298
+ const resolveCellRenderer = createBindingBadgeRenderer(getBindingAt, resolveFieldLabel, (addr) => workbook.value.activeSheet.getEffectiveStyle(addr)?.align);
299
299
  function getTemplate() {
300
300
  const resolved = datasets.value.flatMap((dataset) => {
301
301
  const connection = resolveDatasetConnection(dataset);
@@ -18,7 +18,7 @@ const EXT_TO_TYPE = {
18
18
  svg: "svg",
19
19
  webp: "webp"
20
20
  };
21
- /** MIME → hucre 图片类型;MIME 为空时回落扩展名 */
21
+ /** MIME → sheet-core 图片类型(core/image 的 SheetImageType);MIME 为空时回落扩展名 */
22
22
  function resolveImageType(file) {
23
23
  const mime = file.type.trim().toLowerCase();
24
24
  if (mime && MIME_TO_TYPE[mime]) return MIME_TO_TYPE[mime];
@@ -3,10 +3,9 @@ self.onmessage = (e) => {
3
3
  const { buffer } = e.data;
4
4
  (async () => {
5
5
  try {
6
- const [{ readXlsx }, { buildWorkbookFromHucre, XLSX_READ_OPTIONS }] = await Promise.all([import("hucre/xlsx"), import("@veltra/sheet-core/core/io/import")]);
7
- const hucreWb = await readXlsx(new Uint8Array(buffer), XLSX_READ_OPTIONS);
6
+ const { importXlsx } = await import("@veltra/sheet-core/core/io/import");
8
7
  let lastPercent = -1;
9
- const wb = buildWorkbookFromHucre(hucreWb, (done, total) => {
8
+ const wb = await importXlsx(new Uint8Array(buffer), (done, total) => {
10
9
  const percent = total > 0 ? Math.floor(done / total * 10) : 10;
11
10
  if (percent !== lastPercent || done === total) {
12
11
  lastPercent = percent;
@@ -78,8 +78,8 @@ function useSheetState(props, emit, hooks) {
78
78
  ];
79
79
  }
80
80
  /**
81
- * 工作簿内容被结构性替换后同步(如导入 xlsx 的 replaceWorkbook):
82
- * replaceWorkbook 未必触发 active-sheet-change(同 index),需显式同步 tabs、
81
+ * 工作簿内容被结构性替换后同步(如导入 xlsx 的 replaceWorkbookWithSnapshots):
82
+ * 整簿替换未必触发 active-sheet-change(同 index),需显式同步 tabs、
83
83
  * 重绑 sheet 事件并重建网格。
84
84
  */
85
85
  function syncFromWorkbook() {
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { ConditionalOperator, ConditionalRule, DatasetCatalogItem, DatasetField,
8
8
  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 "./report/binding.js";
9
9
  import { ConditionalEvalContext, evaluateCondition, evaluateConditionalStyle, resolveRuleCompareValue, toComparableNumber } from "./report/rules/evaluate.js";
10
10
  import { buildParamDefs, extractParamIds, resolveBoundDatasetParams } from "./report/params.js";
11
- import { ReportColWidthEntry, buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth } from "./report/export-xlsx.js";
11
+ import { ReportColWidthEntry, exportFilledReportXlsx } from "./report/export-xlsx.js";
12
12
  import { mapFilledColWidths } from "./report/render/builder.js";
13
13
  import { renderReport, resolveFilledColWidths } from "./report/render/index.js";
14
14
  import { ConnectionType, ConnectorError, CreateHttpConnectorOptions, DEFAULT_CONNECTION_PORTS, DataConnection, DataConnector, QueryResult, Result, createHttpConnector, resolvePortOnTypeChange } from "./report/connector.js";
@@ -18,4 +18,4 @@ import "./report/index.js";
18
18
  import { ReportViewerExposed, ReportViewerProps, _ReportViewerExposed } from "./types/report-viewer.js";
19
19
  import { ReportDesignerEmits, ReportDesignerExposed, ReportDesignerProps, _ReportDesignerExposed } from "./types/report-designer.js";
20
20
  import "./types/index.js";
21
- 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, type ReportDesignerEmits, type ReportDesignerExposed, type ReportDesignerProps, ReportExpand, ReportPreset, ReportSort, ReportTemplate, type ReportViewerExposed, type ReportViewerProps, Result, type SheetContext, type SheetEmits, type SheetExposed, type SheetProps, type SheetTool, type SheetToolGroup, type SheetToolPopupType, _sfc_main as UReportDesigner, _sfc_main$1 as UReportViewer, _sfc_main$2 as USheet, type _ReportDesignerExposed, type _ReportViewerExposed, type _SheetExposed, applyReportPreset, assertCompatibleTemplateVersion, buildColumnDefs, buildParamDefs, createHttpConnector, createReportBinding, createReportTemplate, createSheetContext, defaultToolRegistry, 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, registerTool, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber, unregisterTool };
21
+ 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, type ReportDesignerEmits, type ReportDesignerExposed, type ReportDesignerProps, ReportExpand, ReportPreset, ReportSort, ReportTemplate, type ReportViewerExposed, type ReportViewerProps, Result, type SheetContext, type SheetEmits, type SheetExposed, type SheetProps, type SheetTool, type SheetToolGroup, type SheetToolPopupType, _sfc_main as UReportDesigner, _sfc_main$1 as UReportViewer, _sfc_main$2 as USheet, type _ReportDesignerExposed, type _ReportViewerExposed, type _SheetExposed, applyReportPreset, assertCompatibleTemplateVersion, buildParamDefs, createHttpConnector, createReportBinding, createReportTemplate, createSheetContext, defaultToolRegistry, 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, registerTool, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber, unregisterTool };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { defaultToolRegistry, registerTool, unregisterTool } from "./tools/regis
2
2
  import "./tools/builtin.js";
3
3
  import { createSheetContext } from "./tools/context.js";
4
4
  import _sfc_main$2 from "./components/sheet/sheet.js";
5
- import { buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth } from "./report/export-xlsx.js";
5
+ import { exportFilledReportXlsx } from "./report/export-xlsx.js";
6
6
  import { parseDateRangeValue, patchParamValues, resolveNumberParamValue } from "./report/filter-bar.js";
7
7
  import { 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 "./report/binding.js";
8
8
  import { evaluateCondition, evaluateConditionalStyle, resolveRuleCompareValue, toComparableNumber } from "./report/rules/evaluate.js";
@@ -13,4 +13,4 @@ import { IncompatibleTemplateVersionError, REPORT_TEMPLATE_VERSION, assertCompat
13
13
  import _sfc_main$1 from "./components/report/report-viewer.js";
14
14
  import { DEFAULT_CONNECTION_PORTS, createHttpConnector, resolvePortOnTypeChange } from "./report/connector.js";
15
15
  import _sfc_main from "./components/report/report-designer.js";
16
- export { DEFAULT_CONNECTION_PORTS, IncompatibleTemplateVersionError, REPORT_META_NAMESPACE, REPORT_TEMPLATE_VERSION, _sfc_main as UReportDesigner, _sfc_main$1 as UReportViewer, _sfc_main$2 as USheet, applyReportPreset, assertCompatibleTemplateVersion, buildColumnDefs, buildParamDefs, createHttpConnector, createReportBinding, createReportTemplate, createSheetContext, defaultToolRegistry, 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, registerTool, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber, unregisterTool };
16
+ export { DEFAULT_CONNECTION_PORTS, IncompatibleTemplateVersionError, REPORT_META_NAMESPACE, REPORT_TEMPLATE_VERSION, _sfc_main as UReportDesigner, _sfc_main$1 as UReportViewer, _sfc_main$2 as USheet, applyReportPreset, assertCompatibleTemplateVersion, buildParamDefs, createHttpConnector, createReportBinding, createReportTemplate, createSheetContext, defaultToolRegistry, 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, registerTool, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber, unregisterTool };
@@ -1,17 +1,13 @@
1
1
  import { Sheet } from "@veltra/sheet-core";
2
- import { ColumnDef } from "hucre";
3
2
  //#region src/report/export-xlsx.d.ts
4
3
  /** 列宽条目:模型列索引 → 像素宽(与 SheetSnapshot.colWidths 同构) */
5
4
  type ReportColWidthEntry = readonly [number, number];
6
- /** VTable 像素列宽 → Excel 字符宽度(近似换算) */
7
- declare function pxToExcelColWidth(px: number): number;
8
- /** 从列宽条目构建 hucre ColumnDef */
9
- declare function buildColumnDefs(colWidths: ReadonlyArray<ReportColWidthEntry>): ColumnDef[];
10
5
  /**
11
- * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽)。
6
+ * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽 / 浮动图)。
7
+ * 组装与序列化委托 sheet-core `exportSheetXlsx`(与 `exportWorkbookXlsx` 同一套单表逻辑);
12
8
  * 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
13
9
  * 列宽取自 sheet 模型(`getColWidths`)。
14
10
  */
15
11
  declare function exportFilledReportXlsx(sheet: Sheet): Promise<Uint8Array>;
16
12
  //#endregion
17
- export { ReportColWidthEntry, buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth };
13
+ export { ReportColWidthEntry, exportFilledReportXlsx };
@@ -1,92 +1,13 @@
1
- import { rangeToHucre, styleToHucre } from "@veltra/sheet-core/core/io/export";
2
- import { writeXlsx } from "hucre/xlsx";
1
+ import { exportSheetXlsx } from "@veltra/sheet-core/core/io/export";
3
2
  //#region src/report/export-xlsx.ts
4
- /** 像素 → Excel points */
5
- function pxToPt(px) {
6
- return px * .75;
7
- }
8
- /** VTable 像素列宽 → Excel 字符宽度(近似换算) */
9
- function pxToExcelColWidth(px) {
10
- return Math.max(1, Math.round((px - 5) / 7));
11
- }
12
- /** 从列宽条目构建 hucre ColumnDef */
13
- function buildColumnDefs(colWidths) {
14
- if (colWidths.length === 0) return [];
15
- const maxCol = Math.max(...colWidths.map(([col]) => col));
16
- const columns = Array.from({ length: maxCol + 1 }, () => ({}));
17
- for (const [col, width] of colWidths) columns[col] = { width: pxToExcelColWidth(width) };
18
- return columns;
19
- }
20
3
  /**
21
- * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽)。
4
+ * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽 / 浮动图)。
5
+ * 组装与序列化委托 sheet-core `exportSheetXlsx`(与 `exportWorkbookXlsx` 同一套单表逻辑);
22
6
  * 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
23
7
  * 列宽取自 sheet 模型(`getColWidths`)。
24
8
  */
25
- async function exportFilledReportXlsx(sheet) {
26
- const widths = [...sheet.getColWidths()];
27
- let maxRow = -1;
28
- let maxCol = -1;
29
- for (const [addr] of sheet.store.entries()) {
30
- if (addr.row > maxRow) maxRow = addr.row;
31
- if (addr.col > maxCol) maxCol = addr.col;
32
- }
33
- for (const [row] of sheet.getRowHeights()) if (row > maxRow) maxRow = row;
34
- for (const [col] of widths) if (col > maxCol) maxCol = col;
35
- const rows = maxRow < 0 ? [] : Array.from({ length: maxRow + 1 }, () => Array.from({ length: maxCol + 1 }, () => null));
36
- const cells = /* @__PURE__ */ new Map();
37
- for (const [addr, data] of sheet.store.entries()) {
38
- const row = rows[addr.row];
39
- const key = `${addr.row},${addr.col}`;
40
- const cell = {};
41
- const style = sheet.getEffectiveStyle(addr);
42
- if (style) cell.style = styleToHucre(style);
43
- if (data.f != null && data.f !== "") {
44
- cell.formula = data.f;
45
- if (data.v != null) cell.formulaResult = data.v;
46
- row[addr.col] = data.v ?? null;
47
- cells.set(key, cell);
48
- continue;
49
- }
50
- if (data.t === "e") {
51
- cell.value = data.v;
52
- cell.type = "error";
53
- row[addr.col] = null;
54
- cells.set(key, cell);
55
- continue;
56
- }
57
- if (data.t === "d") {
58
- cell.value = data.v;
59
- cell.type = "number";
60
- cell.style = {
61
- ...cell.style,
62
- numFmt: "yyyy-mm-dd"
63
- };
64
- row[addr.col] = null;
65
- cells.set(key, cell);
66
- continue;
67
- }
68
- row[addr.col] = data.v ?? null;
69
- if (Object.keys(cell).length > 0) cells.set(key, cell);
70
- }
71
- const writeSheet = {
72
- name: sheet.name || "报表",
73
- rows,
74
- ...cells.size > 0 ? { cells } : {},
75
- ...sheet.merges.size > 0 ? { merges: sheet.merges.getMerges().map((range) => rangeToHucre(range)) } : {},
76
- ...widths.length > 0 ? { columns: buildColumnDefs(widths) } : {}
77
- };
78
- const frozen = sheet.frozen;
79
- if (frozen.rows > 0 || frozen.cols > 0) writeSheet.freezePane = {
80
- rows: frozen.rows,
81
- columns: frozen.cols
82
- };
83
- const rowDefs = /* @__PURE__ */ new Map();
84
- for (const [row, height] of sheet.getRowHeights()) rowDefs.set(row, { height: pxToPt(height) });
85
- if (rowDefs.size > 0) writeSheet.rowDefs = rowDefs;
86
- return writeXlsx({
87
- sheets: [writeSheet],
88
- activeSheet: 0
89
- });
9
+ function exportFilledReportXlsx(sheet) {
10
+ return exportSheetXlsx(sheet, { fallbackName: "报表" });
90
11
  }
91
12
  //#endregion
92
- export { buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth };
13
+ export { exportFilledReportXlsx };
@@ -3,10 +3,10 @@ import { InferParentOptions, ListParentCandidateOptions, REPORT_META_NAMESPACE,
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";
6
- import { ReportColWidthEntry, buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth } from "./export-xlsx.js";
6
+ import { ReportColWidthEntry, exportFilledReportXlsx } from "./export-xlsx.js";
7
7
  import { mapFilledColWidths } from "./render/builder.js";
8
8
  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, 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 };
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, 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, renderReport, resolveBoundDatasetParams, resolveFilledColWidths, resolveNumberParamValue, resolveParamDefaults, resolvePortOnTypeChange, resolveRuleCompareValue, resolveTemplateParams, setBindingCatalog, toComparableNumber };
@@ -2,7 +2,7 @@ import { saveBlob } from "@cat-kit/fe";
2
2
  import { exportSheetCsv, exportWorkbookXlsx } from "@veltra/sheet-core/core/io/export";
3
3
  //#region src/tools/download.ts
4
4
  /**
5
- * 大工作簿的 XLSX 导出(hucre 序列化 + ZIP 压缩为秒级同步重活)移入 Web Worker:
5
+ * 大工作簿的 XLSX 导出(sheet-core 序列化 + ZIP 压缩为秒级同步重活)移入 Web Worker:
6
6
  * 主线程只采集快照(结构化克隆友好的纯数据,行高随 SheetSnapshot.rowHeights
7
7
  * 携带),序列化与压缩在 worker 线程进行。worker 不可用(极端环境 / 打包产物
8
8
  * 缺文件)回退主线程导出——与导入侧 parseXlsxAsync 同一模式。
@@ -48,7 +48,7 @@ async function exportWorkbookXlsxAsync(workbook) {
48
48
  /**
49
49
  * 导出当前工作簿为 .xlsx(无 workbook 时空操作)。
50
50
  * 下载动作复用 @cat-kit/fe 的 saveBlob(#28:不再自研 Object URL + a.click)。
51
- * hucre 1.0 起 writeXlsx 会校验 sheet 名(Excel 非法字符 / 超长 / 保留名)并抛
51
+ * 导出侧 sheet-core 的 writeXlsx 会校验 sheet 名(Excel 非法字符 / 超长 / 保留名)并抛
52
52
  * InvalidArgumentError——失败经返回的 Promise 传播,调用方(导出面板)负责提示。
53
53
  */
54
54
  async function exportWorkbookFile(ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/sheet",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -33,16 +33,13 @@
33
33
  "import": "./dist/*"
34
34
  }
35
35
  },
36
- "dependencies": {
37
- "hucre": "^1.1.0"
38
- },
39
36
  "peerDependencies": {
40
37
  "@cat-kit/core": ">=1.1.8",
41
38
  "@cat-kit/fe": ">=1.1.8",
42
39
  "@veltra/compositions": "^1.6.4",
43
40
  "@veltra/desktop": "^1.6.4",
44
41
  "@veltra/icons": "^1.5.0",
45
- "@veltra/sheet-core": "^2.3.3",
42
+ "@veltra/sheet-core": "^2.3.4",
46
43
  "@veltra/styles": "^1.6.4",
47
44
  "@veltra/utils": "^1.6.4",
48
45
  "vue": ">=3.5.41"
@@ -0,0 +1,92 @@
1
+ import { Sheet } from '@veltra/sheet-core'
2
+ import type { CellAddress } from '@veltra/sheet-core'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import { REPORT_META_NAMESPACE, createReportBinding } from '../../../report/binding'
6
+ import type { DatasetCatalogItem, ReportBinding } from '../../../report/types'
7
+ import { badgeLayoutProps, createBindingBadgeRenderer } from '../binding-badge'
8
+
9
+ // ---- 内联 fixtures ----
10
+
11
+ const ORDERS_DATASET: DatasetCatalogItem = {
12
+ id: 'orders',
13
+ label: '销售明细',
14
+ fields: [
15
+ { name: 'customer', label: '客户', type: 'string' },
16
+ { name: 'amount', label: '金额', type: 'number' }
17
+ ]
18
+ }
19
+
20
+ const ADDR = { row: 1, col: 3 }
21
+
22
+ function bindingMap(entries: Array<{ addr: CellAddress; binding: ReportBinding }>) {
23
+ const map = new Map<string, ReportBinding>()
24
+ for (const { addr, binding } of entries) map.set(`${addr.row},${addr.col}`, binding)
25
+ return (addr: CellAddress) => map.get(`${addr.row},${addr.col}`)
26
+ }
27
+
28
+ describe('badgeLayoutProps(单元格对齐 → 徽章容器 flex 映射)', () => {
29
+ it('缺省对齐 → 主轴起点 + 交叉轴居中(向后兼容)', () => {
30
+ expect(badgeLayoutProps(undefined)).toEqual({
31
+ justifyContent: 'flex-start',
32
+ alignItems: 'center'
33
+ })
34
+ expect(badgeLayoutProps({})).toEqual({ justifyContent: 'flex-start', alignItems: 'center' })
35
+ })
36
+
37
+ it('horizontal center / right / left → justifyContent', () => {
38
+ expect(badgeLayoutProps({ horizontal: 'center' }).justifyContent).toBe('center')
39
+ expect(badgeLayoutProps({ horizontal: 'right' }).justifyContent).toBe('flex-end')
40
+ expect(badgeLayoutProps({ horizontal: 'left' }).justifyContent).toBe('flex-start')
41
+ })
42
+
43
+ it('vertical bottom / top / middle → alignItems', () => {
44
+ expect(badgeLayoutProps({ vertical: 'bottom' }).alignItems).toBe('flex-end')
45
+ expect(badgeLayoutProps({ vertical: 'top' }).alignItems).toBe('flex-start')
46
+ expect(badgeLayoutProps({ vertical: 'middle' }).alignItems).toBe('center')
47
+ })
48
+ })
49
+
50
+ describe('createBindingBadgeRenderer', () => {
51
+ it('未绑定格返回 undefined 回落默认渲染', () => {
52
+ const renderer = createBindingBadgeRenderer(bindingMap([]))
53
+ expect(renderer(ADDR, undefined)).toBeUndefined()
54
+ })
55
+
56
+ it('绑定格不传 resolveAlign 保持左对齐徽章(向后兼容)', () => {
57
+ const binding = createReportBinding(ORDERS_DATASET, 'amount')
58
+ const renderer = createBindingBadgeRenderer(bindingMap([{ addr: ADDR, binding }]))
59
+ const layout = renderer(ADDR, undefined)
60
+ expect(layout?.renderDefault).toBe(false)
61
+ expect(layout?.rootContainer.attribute.justifyContent).toBe('flex-start')
62
+ expect(layout?.rootContainer.attribute.alignItems).toBe('center')
63
+ })
64
+
65
+ it('绑定格容器跟随 resolveAlign 的水平 / 垂直对齐', () => {
66
+ const binding = createReportBinding(ORDERS_DATASET, 'amount')
67
+ const renderer = createBindingBadgeRenderer(
68
+ bindingMap([{ addr: ADDR, binding }]),
69
+ undefined,
70
+ () => ({ horizontal: 'center', vertical: 'bottom' })
71
+ )
72
+ const layout = renderer(ADDR, undefined)
73
+ expect(layout?.rootContainer.attribute.justifyContent).toBe('center')
74
+ expect(layout?.rootContainer.attribute.alignItems).toBe('flex-end')
75
+ })
76
+
77
+ it('接线冒烟:经 Sheet.getEffectiveStyle 读取有效样式对齐(含行默认样式叠加)', () => {
78
+ const sheet = new Sheet()
79
+ sheet.setCellMeta(ADDR, REPORT_META_NAMESPACE, createReportBinding(ORDERS_DATASET, 'amount'))
80
+ sheet.setCellStyle({ start: ADDR, end: ADDR }, { align: { horizontal: 'center' } })
81
+ sheet.setRowStyle(ADDR.row, { align: { vertical: 'bottom' } })
82
+
83
+ const renderer = createBindingBadgeRenderer(
84
+ (addr) => sheet.getCellMeta<ReportBinding>(addr, REPORT_META_NAMESPACE),
85
+ undefined,
86
+ (addr) => sheet.getEffectiveStyle(addr)?.align
87
+ )
88
+ const layout = renderer(ADDR, undefined)
89
+ expect(layout?.rootContainer.attribute.justifyContent).toBe('center')
90
+ expect(layout?.rootContainer.attribute.alignItems).toBe('flex-end')
91
+ })
92
+ })
@@ -1,4 +1,4 @@
1
- import type { CellAddress } from '@veltra/sheet-core'
1
+ import type { CellAddress, CellAlign } from '@veltra/sheet-core'
2
2
  import {
3
3
  CustomLayout,
4
4
  type ResolveCellRenderer,
@@ -51,14 +51,38 @@ export function createBindingBadgeStyleResolver(
51
51
  }
52
52
  }
53
53
 
54
+ /** 徽章容器 flex 布局属性(`flexDirection: 'row'` 下主轴 = 水平) */
55
+ interface BadgeLayoutProps {
56
+ justifyContent: 'flex-start' | 'center' | 'flex-end'
57
+ alignItems: 'flex-start' | 'center' | 'flex-end'
58
+ }
59
+
60
+ /**
61
+ * 单元格对齐 → 徽章容器 flex 布局映射(纯函数,便于单测):
62
+ * `horizontal` left / 缺省 → 主轴起点;`vertical` middle / 缺省 → 交叉轴居中。
63
+ */
64
+ export function badgeLayoutProps(align: CellAlign | undefined): BadgeLayoutProps {
65
+ const justifyContent =
66
+ align?.horizontal === 'center'
67
+ ? 'center'
68
+ : align?.horizontal === 'right'
69
+ ? 'flex-end'
70
+ : 'flex-start'
71
+ const alignItems =
72
+ align?.vertical === 'top' ? 'flex-start' : align?.vertical === 'bottom' ? 'flex-end' : 'center'
73
+ return { justifyContent, alignItems }
74
+ }
75
+
54
76
  /**
55
77
  * 绑定格富渲染徽章(ADR-0004 `resolveCellRenderer` 首个消费者):
56
- * 绑定单元格渲染为左对齐徽章(强调色聚合标签 + 中性色字段标签),
78
+ * 绑定单元格渲染为徽章布局(强调色聚合标签 + 中性色字段标签),
79
+ * 布局跟随单元格水平 / 垂直对齐(经 `resolveAlign` 读有效样式);
57
80
  * 未绑定格返回 `undefined` 回落默认渲染。
58
81
  */
59
82
  export function createBindingBadgeRenderer(
60
83
  getBindingAt: (addr: CellAddress) => ReportBinding | undefined,
61
- resolveLabel?: (datasetId: string, fieldName: string) => string
84
+ resolveLabel?: (datasetId: string, fieldName: string) => string,
85
+ resolveAlign?: (addr: CellAddress) => CellAlign | undefined
62
86
  ): ResolveCellRenderer {
63
87
  return (addr) => {
64
88
  const binding = getBindingAt(addr)
@@ -66,12 +90,13 @@ export function createBindingBadgeRenderer(
66
90
 
67
91
  const { fg } = resolveBadgeColor(binding)
68
92
  const { tag, label } = formatBindingPlaceholderParts(binding, resolveLabel)
93
+ const { justifyContent, alignItems } = badgeLayoutProps(resolveAlign?.(addr))
69
94
 
70
95
  const rootContainer = new CustomLayout.Container({
71
96
  display: 'flex',
72
97
  flexDirection: 'row',
73
- justifyContent: 'flex-start',
74
- alignItems: 'center'
98
+ justifyContent,
99
+ alignItems
75
100
  })
76
101
  // VTable 布局层 Tag 组件(vrender-components)与其 INode 声明存在结构性类型偏差,
77
102
  // 运行时完全兼容(VTable 官方 customLayout 徽章用法即如此),此处仅收敛类型
@@ -483,7 +483,13 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
483
483
  // ---- 渲染与模板 ----
484
484
 
485
485
  const resolveCellStyle = createBindingBadgeStyleResolver(getBindingAt)
486
- const resolveCellRenderer = createBindingBadgeRenderer(getBindingAt, resolveFieldLabel)
486
+ // 对齐访问器:hook 为长生命周期闭包而 activeSheet 可变(template 载入 / 宿主换 workbook),
487
+ // 每次调用动态取当前 sheet 的有效样式(getEffectiveStyle 为 O(1) 列→行→格叠加)
488
+ const resolveCellRenderer = createBindingBadgeRenderer(
489
+ getBindingAt,
490
+ resolveFieldLabel,
491
+ (addr) => workbook.value.activeSheet.getEffectiveStyle(addr)?.align
492
+ )
487
493
 
488
494
  function getTemplate(): ReportTemplate {
489
495
  const resolved: ReportDatasetDef[] = datasets.value.flatMap((dataset) => {
@@ -24,7 +24,7 @@ const EXT_TO_TYPE: Record<string, SheetImageType> = {
24
24
  webp: 'webp'
25
25
  }
26
26
 
27
- /** MIME → hucre 图片类型;MIME 为空时回落扩展名 */
27
+ /** MIME → sheet-core 图片类型(core/image 的 SheetImageType);MIME 为空时回落扩展名 */
28
28
  export function resolveImageType(file: File): SheetImageType | undefined {
29
29
  const mime = file.type.trim().toLowerCase()
30
30
  if (mime && MIME_TO_TYPE[mime]) return MIME_TO_TYPE[mime]
@@ -30,7 +30,7 @@ const cls = bem('sheet')
30
30
  function exportXlsx(): void {
31
31
  emit('close')
32
32
  // 大工作簿序列化在 worker 中进行(数秒):给常驻反馈,失败(如 sheet 名含
33
- // Excel 非法字符,hucre 1.0 写入校验抛 InvalidArgumentError)明确报错,
33
+ // Excel 非法字符,sheet-core 写入校验抛 InvalidArgumentError)明确报错,
34
34
  // 绝不静默失败
35
35
  const loading = message({ message: '正在导出…', duration: 0 })
36
36
  exportWorkbookFile(props.context)
@@ -1,15 +1,16 @@
1
1
  /// <reference lib="webworker" />
2
2
  /**
3
- * XLSX 解析 Worker:hucre 解析 + 模型构建(大文件 3~4s 同步重活)
3
+ * XLSX 解析 Worker:xlsx 解析 + 模型构建(大文件 3~4s 同步重活)
4
4
  * 移到独立线程,主线程保持空闲(loading 动画正常、交互不冻结)。
5
5
  *
6
6
  * 协议:主线程 postMessage({ buffer: ArrayBuffer }) → worker 返回
7
- * - { type: 'progress', done, total }(模型分片构建进度,readXlsx 同步解析段无进度)
7
+ * - { type: 'progress', done, total }(模型分片构建进度,xlsx 解析段无进度)
8
8
  * - { type: 'done', ok: true, sheets: [{ name, snapshot }], activeIndex }(快照为
9
9
  * 纯数据,可结构化克隆);失败 { type: 'done', ok: false, error }。
10
10
  *
11
- * 名字唯一化与活动表对齐由 buildWorkbookFromHucre 完成(worker 内),主线程
12
- * 直接把快照替换进目标(无 undo 历史——替换语义由确认后的 replaceWorkbook 负责)。
11
+ * 解析与构建经 sheet-core importXlsx 完成(hucre 仅由 sheet-core 依赖);
12
+ * 名字唯一化与活动表对齐在 worker 内完成,主线程直接把快照替换进目标
13
+ * (无 undo 历史——替换语义由确认后的 replaceWorkbookWithSnapshots 负责)。
13
14
  *
14
15
  * 注意:必须用**运行时动态 import**——worker 顶层静态 import 在 vite dev 的
15
16
  * worker 上下文中会因模块图加载顺序导致 `Workbook is not defined`(实测)。
@@ -30,15 +31,11 @@ self.onmessage = (e: MessageEvent<{ buffer: ArrayBuffer }>): void => {
30
31
  const { buffer } = e.data
31
32
  void (async () => {
32
33
  try {
33
- const [{ readXlsx }, { buildWorkbookFromHucre, XLSX_READ_OPTIONS }] = await Promise.all([
34
- import('hucre/xlsx'),
35
- import('@veltra/sheet-core/core/io/import')
36
- ])
37
- const hucreWb = await readXlsx(new Uint8Array(buffer), XLSX_READ_OPTIONS)
34
+ const { importXlsx } = await import('@veltra/sheet-core/core/io/import')
38
35
  // 分片构建:按 10% 粒度回报进度(避免 196 条/秒的消息风暴与文字跳变过快;
39
36
  // 模型构建段约 1s,10 次更新肉眼可见数字推进)
40
37
  let lastPercent = -1
41
- const wb = buildWorkbookFromHucre(hucreWb, (done, total) => {
38
+ const wb = await importXlsx(new Uint8Array(buffer), (done, total) => {
42
39
  const percent = total > 0 ? Math.floor((done / total) * 10) : 10
43
40
  if (percent !== lastPercent || done === total) {
44
41
  lastPercent = percent
@@ -123,8 +123,8 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
123
123
  }
124
124
 
125
125
  /**
126
- * 工作簿内容被结构性替换后同步(如导入 xlsx 的 replaceWorkbook):
127
- * replaceWorkbook 未必触发 active-sheet-change(同 index),需显式同步 tabs、
126
+ * 工作簿内容被结构性替换后同步(如导入 xlsx 的 replaceWorkbookWithSnapshots):
127
+ * 整簿替换未必触发 active-sheet-change(同 index),需显式同步 tabs、
128
128
  * 重绑 sheet 事件并重建网格。
129
129
  */
130
130
  function syncFromWorkbook(): void {
@@ -1,10 +1,9 @@
1
1
  import { Sheet } from '@veltra/sheet-core'
2
- import { buildWorkbookFromHucre, XLSX_READ_OPTIONS } from '@veltra/sheet-core/core/io/import'
3
- import { readXlsx } from 'hucre/xlsx'
2
+ import { importXlsx } from '@veltra/sheet-core/core/io/import'
4
3
  import { describe, expect, it } from 'vitest'
5
4
 
6
5
  import { REPORT_META_NAMESPACE, createReportBinding } from '../binding'
7
- import { buildColumnDefs, exportFilledReportXlsx, pxToExcelColWidth } from '../export-xlsx'
6
+ import { exportFilledReportXlsx } from '../export-xlsx'
8
7
  import { renderReport } from '../render'
9
8
  import type { DatasetCatalogItem } from '../types'
10
9
 
@@ -69,8 +68,7 @@ describe('exportFilledReportXlsx', () => {
69
68
  expect(buffer[0]).toBe(0x50)
70
69
  expect(buffer[1]).toBe(0x4b)
71
70
 
72
- const hucreWb = await readXlsx(buffer, XLSX_READ_OPTIONS)
73
- const workbook = buildWorkbookFromHucre(hucreWb)
71
+ const workbook = await importXlsx(buffer)
74
72
  const sheet = workbook.getSheets()[0]!
75
73
 
76
74
  // 展开值保真(分组头 + 明细值)
@@ -96,23 +94,3 @@ describe('exportFilledReportXlsx', () => {
96
94
  expect(headerStyle?.fill?.color).toBe('#E8EEF7')
97
95
  })
98
96
  })
99
-
100
- describe('buildColumnDefs / pxToExcelColWidth', () => {
101
- it('像素列宽换算为 Excel 字符宽度并写入对应列', () => {
102
- expect(pxToExcelColWidth(120)).toBe(16)
103
- expect(pxToExcelColWidth(0)).toBe(1)
104
-
105
- const columns = buildColumnDefs([
106
- [0, 120],
107
- [2, 145]
108
- ])
109
- expect(columns).toHaveLength(3)
110
- expect(columns[0]).toEqual({ width: 16 })
111
- expect(columns[1]).toEqual({})
112
- expect(columns[2]).toEqual({ width: 20 })
113
- })
114
-
115
- it('空列宽不产生 columns 定义', () => {
116
- expect(buildColumnDefs([])).toEqual([])
117
- })
118
- })
@@ -325,6 +325,32 @@ describe('renderReport', () => {
325
325
  expect(firstAmount?.s).toBe(styleId)
326
326
  })
327
327
 
328
+ it('模板格对齐样式直通 Filled Report 快照(静态格与绑定扩展格)', () => {
329
+ const sheet = new Sheet()
330
+ seedGroupDetailTemplate(sheet)
331
+ // 静态表头格(resolveStatic 路径)
332
+ sheet.setCellStyle(
333
+ { start: { row: 0, col: 0 }, end: { row: 0, col: 0 } },
334
+ { align: { horizontal: 'right' } }
335
+ )
336
+ // 绑定明细格(resolveCell 路径,样式随扩展实例复制)
337
+ sheet.setCellStyle(
338
+ { start: { row: 1, col: 3 }, end: { row: 1, col: 3 } },
339
+ { align: { horizontal: 'center' } }
340
+ )
341
+
342
+ const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
343
+ const header = filled.cells.find((c) => c.row === 0 && c.col === 0)
344
+ expect(filled.styles[(header!.s ?? 1) - 1]?.align?.horizontal).toBe('right')
345
+
346
+ const firstAmount = filled.cells.find((c) => c.row === 1 && c.col === 3)
347
+ expect(firstAmount?.s).toBeDefined()
348
+ expect(filled.styles[(firstAmount!.s ?? 1) - 1]?.align?.horizontal).toBe('center')
349
+ // 展开带后续实例同样携带对齐
350
+ const secondAmount = filled.cells.find((c) => c.row === 2 && c.col === 3)
351
+ expect(filled.styles[(secondAmount!.s ?? 1) - 1]?.align?.horizontal).toBe('center')
352
+ })
353
+
328
354
  it('subtotal 支持 avg / count 聚合', () => {
329
355
  const sheet = new Sheet()
330
356
  seedGroupDetailTemplate(sheet)
@@ -1,113 +1,15 @@
1
1
  import type { Sheet } from '@veltra/sheet-core'
2
- import { rangeToHucre, styleToHucre } from '@veltra/sheet-core/core/io/export'
3
- import type { Cell, CellValue, ColumnDef, RowDef, WriteSheet } from 'hucre'
4
- import { writeXlsx } from 'hucre/xlsx'
2
+ import { exportSheetXlsx } from '@veltra/sheet-core/core/io/export'
5
3
 
6
4
  /** 列宽条目:模型列索引 → 像素宽(与 SheetSnapshot.colWidths 同构) */
7
5
  export type ReportColWidthEntry = readonly [number, number]
8
6
 
9
- /** 像素 → Excel points */
10
- function pxToPt(px: number): number {
11
- return px * 0.75
12
- }
13
-
14
- /** VTable 像素列宽 → Excel 字符宽度(近似换算) */
15
- export function pxToExcelColWidth(px: number): number {
16
- return Math.max(1, Math.round((px - 5) / 7))
17
- }
18
-
19
- /** 从列宽条目构建 hucre ColumnDef */
20
- export function buildColumnDefs(colWidths: ReadonlyArray<ReportColWidthEntry>): ColumnDef[] {
21
- if (colWidths.length === 0) return []
22
- const maxCol = Math.max(...colWidths.map(([col]) => col))
23
- const columns: ColumnDef[] = Array.from({ length: maxCol + 1 }, () => ({}))
24
- for (const [col, width] of colWidths) {
25
- columns[col] = { width: pxToExcelColWidth(width) }
26
- }
27
- return columns
28
- }
29
-
30
7
  /**
31
- * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽)。
8
+ * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽 / 浮动图)。
9
+ * 组装与序列化委托 sheet-core `exportSheetXlsx`(与 `exportWorkbookXlsx` 同一套单表逻辑);
32
10
  * 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
33
11
  * 列宽取自 sheet 模型(`getColWidths`)。
34
12
  */
35
- export async function exportFilledReportXlsx(sheet: Sheet): Promise<Uint8Array> {
36
- const widths: ReportColWidthEntry[] = [...sheet.getColWidths()]
37
-
38
- let maxRow = -1
39
- let maxCol = -1
40
- for (const [addr] of sheet.store.entries()) {
41
- if (addr.row > maxRow) maxRow = addr.row
42
- if (addr.col > maxCol) maxCol = addr.col
43
- }
44
- for (const [row] of sheet.getRowHeights()) {
45
- if (row > maxRow) maxRow = row
46
- }
47
- for (const [col] of widths) {
48
- if (col > maxCol) maxCol = col
49
- }
50
-
51
- const rows =
52
- maxRow < 0
53
- ? []
54
- : Array.from({ length: maxRow + 1 }, () =>
55
- Array.from({ length: maxCol + 1 }, () => null as CellValue)
56
- )
57
- const cells = new Map<string, Partial<Cell>>()
58
-
59
- for (const [addr, data] of sheet.store.entries()) {
60
- const row = rows[addr.row]!
61
- const key = `${addr.row},${addr.col}`
62
- const cell: Partial<Cell> = {}
63
- const style = sheet.getEffectiveStyle(addr)
64
- if (style) cell.style = styleToHucre(style)
65
- if (data.f != null && data.f !== '') {
66
- cell.formula = data.f
67
- if (data.v != null) cell.formulaResult = data.v as CellValue
68
- row[addr.col] = (data.v as CellValue) ?? null
69
- cells.set(key, cell)
70
- continue
71
- }
72
- if (data.t === 'e') {
73
- cell.value = data.v as string
74
- cell.type = 'error'
75
- row[addr.col] = null
76
- cells.set(key, cell)
77
- continue
78
- }
79
- if (data.t === 'd') {
80
- cell.value = data.v as number
81
- cell.type = 'number'
82
- cell.style = { ...cell.style, numFmt: 'yyyy-mm-dd' }
83
- row[addr.col] = null
84
- cells.set(key, cell)
85
- continue
86
- }
87
- row[addr.col] = (data.v as CellValue) ?? null
88
- if (Object.keys(cell).length > 0) cells.set(key, cell)
89
- }
90
-
91
- const writeSheet: WriteSheet = {
92
- name: sheet.name || '报表',
93
- rows,
94
- ...(cells.size > 0 ? { cells } : {}),
95
- ...(sheet.merges.size > 0
96
- ? { merges: sheet.merges.getMerges().map((range) => rangeToHucre(range)) }
97
- : {}),
98
- ...(widths.length > 0 ? { columns: buildColumnDefs(widths) } : {})
99
- }
100
-
101
- const frozen = sheet.frozen
102
- if (frozen.rows > 0 || frozen.cols > 0) {
103
- writeSheet.freezePane = { rows: frozen.rows, columns: frozen.cols }
104
- }
105
-
106
- const rowDefs = new Map<number, RowDef>()
107
- for (const [row, height] of sheet.getRowHeights()) {
108
- rowDefs.set(row, { height: pxToPt(height) })
109
- }
110
- if (rowDefs.size > 0) writeSheet.rowDefs = rowDefs
111
-
112
- return writeXlsx({ sheets: [writeSheet], activeSheet: 0 })
13
+ export function exportFilledReportXlsx(sheet: Sheet): Promise<Uint8Array> {
14
+ return exportSheetXlsx(sheet, { fallbackName: '报表' })
113
15
  }
@@ -122,7 +122,7 @@ describe('内置工具(dogfood 扩展机制)', () => {
122
122
  }
123
123
  })
124
124
 
125
- it('导出 xlsx:sheet 名含 Excel 非法字符时 reject(hucre 1.0 写入校验,错误传播给调用方)', async () => {
125
+ it('导出 xlsx:sheet 名含 Excel 非法字符时 reject(sheet-core 写入校验,错误传播给调用方)', async () => {
126
126
  const workbook = new Workbook()
127
127
  workbook.renameSheet('Sheet1', 'a:b')
128
128
  const ctx = createSheetContext(workbook.activeSheet, workbook)
@@ -6,7 +6,7 @@ import type { SheetContext } from './context'
6
6
  import type { ExportWorkerPayload, ExportWorkerResponse } from './export.worker'
7
7
 
8
8
  /**
9
- * 大工作簿的 XLSX 导出(hucre 序列化 + ZIP 压缩为秒级同步重活)移入 Web Worker:
9
+ * 大工作簿的 XLSX 导出(sheet-core 序列化 + ZIP 压缩为秒级同步重活)移入 Web Worker:
10
10
  * 主线程只采集快照(结构化克隆友好的纯数据,行高随 SheetSnapshot.rowHeights
11
11
  * 携带),序列化与压缩在 worker 线程进行。worker 不可用(极端环境 / 打包产物
12
12
  * 缺文件)回退主线程导出——与导入侧 parseXlsxAsync 同一模式。
@@ -55,7 +55,7 @@ async function exportWorkbookXlsxAsync(workbook: Workbook): Promise<Uint8Array>
55
55
  /**
56
56
  * 导出当前工作簿为 .xlsx(无 workbook 时空操作)。
57
57
  * 下载动作复用 @cat-kit/fe 的 saveBlob(#28:不再自研 Object URL + a.click)。
58
- * hucre 1.0 起 writeXlsx 会校验 sheet 名(Excel 非法字符 / 超长 / 保留名)并抛
58
+ * 导出侧 sheet-core 的 writeXlsx 会校验 sheet 名(Excel 非法字符 / 超长 / 保留名)并抛
59
59
  * InvalidArgumentError——失败经返回的 Promise 传播,调用方(导出面板)负责提示。
60
60
  */
61
61
  export async function exportWorkbookFile(ctx: SheetContext): Promise<void> {
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="webworker" />
2
2
  /**
3
- * XLSX 导出 Worker:exportWorkbookXlsx(模型 → hucre 序列化 + ZIP 压缩,
3
+ * XLSX 导出 Worker:sheet-core exportWorkbookXlsx(模型序列化 + ZIP 压缩,
4
4
  * 大工作簿为秒级同步重活)移到独立线程,主线程保持空闲。
5
5
  *
6
6
  * 协议:主线程 postMessage({ sheets: [{ name, snapshot }], activeIndex })
@@ -36,7 +36,7 @@ self.onmessage = (e: MessageEvent<ExportWorkerPayload>): void => {
36
36
  for (let i = 0; i < sheets.length; i++) {
37
37
  const { name, snapshot } = sheets[i]!
38
38
  // 首个 sheet 复用 Workbook 自带的默认表:先改名对齐(空名/重名保持原名,
39
- // 导出侧 hucre 1.0 会校验表名合法性)
39
+ // 导出侧 sheet-core 的 writeXlsx 会校验表名合法性)
40
40
  const sheet = i === 0 ? wb.activeSheet : wb.addSheet(name)
41
41
  if (i === 0 && sheet.name !== name) wb.renameSheet(sheet.name, name)
42
42
  sheet.restore(snapshot)