@veltra/sheet 2.3.2 → 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.
- package/dist/components/report/binding-badge.js +17 -5
- package/dist/components/report/use-report-designer.js +1 -1
- package/dist/components/sheet/insert-image.js +1 -1
- package/dist/components/sheet/popups/import.worker.js +2 -3
- package/dist/components/sheet/use-sheet-grid.js +3 -3
- package/dist/components/sheet/use-sheet-state.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/report/export-xlsx.d.ts +3 -7
- package/dist/report/export-xlsx.js +6 -85
- package/dist/report/index.d.ts +2 -2
- package/dist/tools/download.js +2 -2
- package/package.json +2 -5
- package/src/components/report/__test__/binding-badge.test.ts +92 -0
- package/src/components/report/binding-badge.ts +30 -5
- package/src/components/report/use-report-designer.ts +7 -1
- package/src/components/sheet/insert-image.ts +1 -1
- package/src/components/sheet/popups/export-popup.vue +1 -1
- package/src/components/sheet/popups/import.worker.ts +7 -10
- package/src/components/sheet/use-sheet-grid.ts +3 -3
- package/src/components/sheet/use-sheet-state.ts +2 -2
- package/src/report/__test__/export-xlsx.test.ts +3 -25
- package/src/report/__test__/render.test.ts +26 -0
- package/src/report/export-xlsx.ts +5 -103
- package/src/tools/__test__/builtin.test.ts +1 -1
- package/src/tools/download.ts +2 -2
- package/src/tools/export.worker.ts +2 -2
|
@@ -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
|
|
63
|
-
alignItems
|
|
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 →
|
|
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
|
|
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 =
|
|
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;
|
|
@@ -127,8 +127,9 @@ function useSheetGrid(options) {
|
|
|
127
127
|
* tab 切换:优先复用缓存实例(零重建)。
|
|
128
128
|
* 命中时校验实例是否过期(隐藏期间程序化 structure-change——插入/删除/undo
|
|
129
129
|
* 行列——vue 层只绑定激活 sheet 的 structure-change,隐藏实例靠自持订阅标记
|
|
130
|
-
* dirty
|
|
131
|
-
*
|
|
130
|
+
* dirty);过期则释放重建。命中且未过期只翻可见性:隐藏实例的 cell/merge/
|
|
131
|
+
* content-reset 已在 GridSyncManager 置脏,setVisible(true) 时一次性 flush,
|
|
132
|
+
* 不必每次切 tab 都 syncFromModel(会重放全部列宽)。
|
|
132
133
|
*/
|
|
133
134
|
function activateGrid() {
|
|
134
135
|
const sheet = getActiveSheet();
|
|
@@ -136,7 +137,6 @@ function useSheetGrid(options) {
|
|
|
136
137
|
const cached = cache.get(sheet);
|
|
137
138
|
if (cached && !cached.dirty) {
|
|
138
139
|
cached.lastUsed = ++seq;
|
|
139
|
-
cached.grid.syncFromModel();
|
|
140
140
|
showOnly(cached);
|
|
141
141
|
active = cached;
|
|
142
142
|
bindGridPointerDown(cached.el);
|
|
@@ -78,8 +78,8 @@ function useSheetState(props, emit, hooks) {
|
|
|
78
78
|
];
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
* 工作簿内容被结构性替换后同步(如导入 xlsx 的
|
|
82
|
-
*
|
|
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,
|
|
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,
|
|
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 {
|
|
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,
|
|
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,
|
|
13
|
+
export { ReportColWidthEntry, exportFilledReportXlsx };
|
|
@@ -1,92 +1,13 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
26
|
-
|
|
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 {
|
|
13
|
+
export { exportFilledReportXlsx };
|
package/dist/report/index.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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 };
|
package/dist/tools/download.js
CHANGED
|
@@ -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 导出(
|
|
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
|
-
*
|
|
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
|
+
"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.
|
|
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
|
|
74
|
-
alignItems
|
|
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
|
-
|
|
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 →
|
|
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 非法字符,
|
|
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:
|
|
3
|
+
* XLSX 解析 Worker:xlsx 解析 + 模型构建(大文件 3~4s 同步重活)
|
|
4
4
|
* 移到独立线程,主线程保持空闲(loading 动画正常、交互不冻结)。
|
|
5
5
|
*
|
|
6
6
|
* 协议:主线程 postMessage({ buffer: ArrayBuffer }) → worker 返回
|
|
7
|
-
* - { type: 'progress', done, total }(模型分片构建进度,
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
@@ -189,8 +189,9 @@ export function useSheetGrid(options: UseSheetGridOptions) {
|
|
|
189
189
|
* tab 切换:优先复用缓存实例(零重建)。
|
|
190
190
|
* 命中时校验实例是否过期(隐藏期间程序化 structure-change——插入/删除/undo
|
|
191
191
|
* 行列——vue 层只绑定激活 sheet 的 structure-change,隐藏实例靠自持订阅标记
|
|
192
|
-
* dirty
|
|
193
|
-
*
|
|
192
|
+
* dirty);过期则释放重建。命中且未过期只翻可见性:隐藏实例的 cell/merge/
|
|
193
|
+
* content-reset 已在 GridSyncManager 置脏,setVisible(true) 时一次性 flush,
|
|
194
|
+
* 不必每次切 tab 都 syncFromModel(会重放全部列宽)。
|
|
194
195
|
*/
|
|
195
196
|
function activateGrid(): void {
|
|
196
197
|
const sheet = getActiveSheet()
|
|
@@ -199,7 +200,6 @@ export function useSheetGrid(options: UseSheetGridOptions) {
|
|
|
199
200
|
const cached = cache.get(sheet)
|
|
200
201
|
if (cached && !cached.dirty) {
|
|
201
202
|
cached.lastUsed = ++seq
|
|
202
|
-
cached.grid.syncFromModel()
|
|
203
203
|
showOnly(cached)
|
|
204
204
|
active = cached
|
|
205
205
|
bindGridPointerDown(cached.el)
|
|
@@ -123,8 +123,8 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* 工作簿内容被结构性替换后同步(如导入 xlsx 的
|
|
127
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
36
|
-
|
|
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(
|
|
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)
|
package/src/tools/download.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { SheetContext } from './context'
|
|
|
6
6
|
import type { ExportWorkerPayload, ExportWorkerResponse } from './export.worker'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* 大工作簿的 XLSX 导出(
|
|
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
|
-
*
|
|
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
|
|
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
|
-
// 导出侧
|
|
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)
|