@veltra/sheet 2.3.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/components/report/binding-badge.js +23 -5
  2. package/dist/components/report/designer/drill-dialog.js +258 -0
  3. package/dist/components/report/designer/float-panel.js +26 -14
  4. package/dist/components/report/print.js +39 -0
  5. package/dist/components/report/report-designer.js +56 -21
  6. package/dist/components/report/report-viewer.js +173 -44
  7. package/dist/components/report/style2.css +91 -0
  8. package/dist/components/report/use-report-designer.js +2 -1
  9. package/dist/components/report/use-report-viewer.js +88 -3
  10. package/dist/components/sheet/insert-image.js +1 -1
  11. package/dist/components/sheet/popups/import.worker.js +2 -3
  12. package/dist/components/sheet/use-sheet-state.js +2 -2
  13. package/dist/index.d.ts +5 -3
  14. package/dist/index.js +4 -2
  15. package/dist/report/drill.d.ts +48 -0
  16. package/dist/report/drill.js +78 -0
  17. package/dist/report/export-xlsx.d.ts +3 -7
  18. package/dist/report/export-xlsx.js +6 -85
  19. package/dist/report/index.d.ts +5 -3
  20. package/dist/report/print.d.ts +23 -0
  21. package/dist/report/print.js +197 -0
  22. package/dist/report/render/aggregate.js +25 -1
  23. package/dist/report/types.d.ts +19 -1
  24. package/dist/tools/download.js +2 -2
  25. package/dist/types/report-designer.d.ts +13 -0
  26. package/dist/types/report-viewer.d.ts +19 -0
  27. package/package.json +6 -9
  28. package/src/components/report/__test__/binding-badge.test.ts +92 -0
  29. package/src/components/report/__test__/report-designer.test.ts +98 -3
  30. package/src/components/report/__test__/report-drill.test.ts +692 -0
  31. package/src/components/report/__test__/report-viewer.test.ts +43 -0
  32. package/src/components/report/__test__/use-report-designer.test.ts +108 -1
  33. package/src/components/report/binding-badge.ts +35 -5
  34. package/src/components/report/designer/drill-dialog.vue +245 -0
  35. package/src/components/report/designer/float-panel.vue +12 -0
  36. package/src/components/report/print.ts +44 -0
  37. package/src/components/report/report-designer.vue +35 -2
  38. package/src/components/report/report-viewer.vue +171 -18
  39. package/src/components/report/style-designer.scss +66 -0
  40. package/src/components/report/style-viewer.scss +40 -0
  41. package/src/components/report/use-report-designer.ts +8 -1
  42. package/src/components/report/use-report-viewer.ts +150 -11
  43. package/src/components/sheet/insert-image.ts +1 -1
  44. package/src/components/sheet/popups/export-popup.vue +1 -1
  45. package/src/components/sheet/popups/import.worker.ts +7 -10
  46. package/src/components/sheet/use-sheet-state.ts +2 -2
  47. package/src/report/__test__/drill.test.ts +194 -0
  48. package/src/report/__test__/export-xlsx.test.ts +3 -25
  49. package/src/report/__test__/print.test.ts +170 -0
  50. package/src/report/__test__/render.test.ts +26 -0
  51. package/src/report/drill.ts +112 -0
  52. package/src/report/export-xlsx.ts +5 -103
  53. package/src/report/index.ts +4 -1
  54. package/src/report/print.ts +234 -0
  55. package/src/report/render/aggregate.ts +28 -0
  56. package/src/report/types.ts +20 -0
  57. package/src/tools/__test__/builtin.test.ts +1 -1
  58. package/src/tools/download.ts +2 -2
  59. package/src/tools/export.worker.ts +2 -2
  60. package/src/types/report-designer.ts +18 -1
  61. package/src/types/report-viewer.ts +18 -1
@@ -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,
@@ -90,6 +102,12 @@ function createBindingBadgeRenderer(getBindingAt, resolveLabel) {
90
102
  fontWeight: 500,
91
103
  fill: BADGE_LABEL_COLOR
92
104
  }));
105
+ if (binding.drill) rootContainer.add(new CustomLayout.Text({
106
+ text: " ↗",
107
+ fontSize: 11,
108
+ fontWeight: 700,
109
+ fill: fg
110
+ }));
93
111
  return {
94
112
  rootContainer,
95
113
  renderDefault: false
@@ -97,4 +115,4 @@ function createBindingBadgeRenderer(getBindingAt, resolveLabel) {
97
115
  };
98
116
  }
99
117
  //#endregion
100
- export { REPORT_PRESET_BADGE_COLORS, createBindingBadgeRenderer, createBindingBadgeStyleResolver };
118
+ export { REPORT_PRESET_BADGE_COLORS, badgeLayoutProps, createBindingBadgeRenderer, createBindingBadgeStyleResolver };
@@ -0,0 +1,258 @@
1
+ import { resolveTemplateParams } from "../../../report/template.js";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, watch, withCtx } from "vue";
3
+ import { UButton, UDialog, URadioGroup, USelect } from "@veltra/desktop";
4
+ import { bem } from "@veltra/utils";
5
+ //#region src/components/report/designer/drill-dialog.vue
6
+ const _hoisted_1 = { key: 2 };
7
+ const _sfc_main = /*@__PURE__*/ defineComponent({
8
+ name: "UReportDrillDialog",
9
+ __name: "drill-dialog",
10
+ props: {
11
+ modelValue: { type: Boolean },
12
+ templates: {},
13
+ fields: { default: () => [] },
14
+ drill: { default: void 0 },
15
+ resolveTemplate: {
16
+ type: Function,
17
+ default: void 0
18
+ }
19
+ },
20
+ emits: [
21
+ "update:modelValue",
22
+ "save",
23
+ "remove"
24
+ ],
25
+ setup(__props, { emit: __emit }) {
26
+ /** 映射编辑行草稿(保存时折叠为 Record<字段, 参数>) */
27
+ const props = __props;
28
+ const emit = __emit;
29
+ const cls = bem("report-drill-dialog");
30
+ const visible = computed({
31
+ get: () => props.modelValue,
32
+ set: (value) => emit("update:modelValue", value)
33
+ });
34
+ const openModeItems = [{
35
+ value: "switch",
36
+ label: "查看器内切换"
37
+ }, {
38
+ value: "dialog",
39
+ label: "弹框打开(UDialog)"
40
+ }];
41
+ const target = ref("");
42
+ const openMode = ref("switch");
43
+ const mappings = ref([]);
44
+ watch(() => props.modelValue, (open) => {
45
+ if (!open) return;
46
+ target.value = props.drill?.target ?? "";
47
+ openMode.value = props.drill?.openMode ?? "switch";
48
+ mappings.value = Object.entries(props.drill?.mapping ?? {}).map(([field, param]) => ({
49
+ field,
50
+ param
51
+ }));
52
+ resolveTargetParams(target.value);
53
+ });
54
+ const targetParams = ref([]);
55
+ const paramsLoading = ref(false);
56
+ const paramsError = ref("");
57
+ let paramsSeq = 0;
58
+ async function resolveTargetParams(ref) {
59
+ const seq = ++paramsSeq;
60
+ targetParams.value = [];
61
+ paramsError.value = "";
62
+ if (!ref) return;
63
+ const resolve = props.resolveTemplate;
64
+ if (!resolve) {
65
+ paramsError.value = "宿主未提供 resolveTemplate,无法解析目标模板参数";
66
+ return;
67
+ }
68
+ paramsLoading.value = true;
69
+ try {
70
+ const template = await resolve(ref);
71
+ if (seq !== paramsSeq) return;
72
+ targetParams.value = resolveTemplateParams(template);
73
+ } catch (error) {
74
+ if (seq !== paramsSeq) return;
75
+ paramsError.value = `解析目标模板参数失败:${error instanceof Error ? error.message : String(error)}`;
76
+ } finally {
77
+ if (seq === paramsSeq) paramsLoading.value = false;
78
+ }
79
+ }
80
+ const templateOptions = computed(() => props.templates.map((item) => ({
81
+ value: item.ref,
82
+ label: item.label
83
+ })));
84
+ const fieldOptions = computed(() => props.fields.map((field) => ({
85
+ value: field.name,
86
+ label: field.label
87
+ })));
88
+ const paramOptions = computed(() => targetParams.value.map((param) => ({
89
+ value: param.id,
90
+ label: param.label && param.label !== param.id ? `${param.label}(${param.id})` : param.id
91
+ })));
92
+ /** 用户改选目标:旧映射的参数 id 未必存在于新模板,直接清空重配 */
93
+ function onTarget(value) {
94
+ target.value = value ?? "";
95
+ mappings.value = [];
96
+ resolveTargetParams(target.value);
97
+ }
98
+ function onOpenMode(value) {
99
+ openMode.value = value;
100
+ }
101
+ function addMapping() {
102
+ mappings.value = [...mappings.value, {
103
+ field: "",
104
+ param: ""
105
+ }];
106
+ }
107
+ function removeMapping(index) {
108
+ mappings.value = mappings.value.filter((_, i) => i !== index);
109
+ }
110
+ function confirm() {
111
+ if (!target.value) return;
112
+ const mapping = {};
113
+ for (const row of mappings.value) if (row.field && row.param) mapping[row.field] = row.param;
114
+ emit("save", {
115
+ target: target.value,
116
+ mapping,
117
+ openMode: openMode.value
118
+ });
119
+ visible.value = false;
120
+ }
121
+ function remove() {
122
+ emit("remove");
123
+ visible.value = false;
124
+ }
125
+ function onClose() {
126
+ visible.value = false;
127
+ }
128
+ return (_ctx, _cache) => {
129
+ return openBlock(), createBlock(unref(UDialog), {
130
+ modelValue: visible.value,
131
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => visible.value = $event),
132
+ title: "下钻配置",
133
+ style: {
134
+ "width": "min(560px, 92vw)",
135
+ "max-height": "85vh"
136
+ },
137
+ onClose
138
+ }, {
139
+ footer: withCtx(() => [
140
+ __props.drill ? (openBlock(), createBlock(unref(UButton), {
141
+ key: 0,
142
+ size: "small",
143
+ text: "",
144
+ type: "danger",
145
+ style: { "margin-right": "auto" },
146
+ onClick: remove
147
+ }, {
148
+ default: withCtx(() => [..._cache[4] || (_cache[4] = [createTextVNode(" 移除下钻 ", -1)])]),
149
+ _: 1
150
+ })) : createCommentVNode("v-if", true),
151
+ createVNode(unref(UButton), {
152
+ size: "small",
153
+ onClick: _cache[0] || (_cache[0] = ($event) => visible.value = false)
154
+ }, {
155
+ default: withCtx(() => [..._cache[5] || (_cache[5] = [createTextVNode("取消", -1)])]),
156
+ _: 1
157
+ }),
158
+ createVNode(unref(UButton), {
159
+ size: "small",
160
+ type: "primary",
161
+ disabled: !target.value,
162
+ onClick: confirm
163
+ }, {
164
+ default: withCtx(() => [..._cache[6] || (_cache[6] = [createTextVNode("确定", -1)])]),
165
+ _: 1
166
+ }, 8, ["disabled"])
167
+ ]),
168
+ default: withCtx(() => [createElementVNode("div", { class: normalizeClass(unref(cls).b) }, [
169
+ createElementVNode("section", { class: normalizeClass(unref(cls).e("section")) }, [
170
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("label")) }, "目标模板", 2),
171
+ createVNode(unref(USelect), {
172
+ size: "small",
173
+ placeholder: "从模板列表选择目标",
174
+ "model-value": target.value,
175
+ options: templateOptions.value,
176
+ "onUpdate:modelValue": onTarget
177
+ }, null, 8, ["model-value", "options"]),
178
+ createElementVNode("p", { class: normalizeClass(unref(cls).e("hint")) }, "目标模板仅从宿主提供的列表选择,不支持手填引用。", 2)
179
+ ], 2),
180
+ createElementVNode("section", { class: normalizeClass(unref(cls).e("section")) }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("label")) }, "打开方式", 2), createVNode(unref(URadioGroup), {
181
+ size: "small",
182
+ items: openModeItems,
183
+ "model-value": openMode.value,
184
+ "onUpdate:modelValue": onOpenMode
185
+ }, null, 8, ["model-value"])], 2),
186
+ createElementVNode("section", { class: normalizeClass(unref(cls).e("section")) }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("label")) }, "字段 → 详情报参数映射", 2), !target.value ? (openBlock(), createElementBlock("p", {
187
+ key: 0,
188
+ class: normalizeClass(unref(cls).e("hint"))
189
+ }, "先选择目标模板,再配置映射。", 2)) : paramsLoading.value ? (openBlock(), createElementBlock("p", {
190
+ key: 1,
191
+ class: normalizeClass(unref(cls).e("hint"))
192
+ }, "正在解析目标模板参数…", 2)) : paramsError.value ? (openBlock(), createElementBlock("p", {
193
+ key: 2,
194
+ class: normalizeClass(unref(cls).e("error"))
195
+ }, toDisplayString(paramsError.value), 3)) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [targetParams.value.length === 0 ? (openBlock(), createElementBlock("p", {
196
+ key: 0,
197
+ class: normalizeClass(unref(cls).e("hint"))
198
+ }, " 目标模板没有查询参数,无需配置映射。 ", 2)) : (openBlock(), createElementBlock("ul", {
199
+ key: 1,
200
+ class: normalizeClass(unref(cls).e("list"))
201
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(mappings.value, (row, index) => {
202
+ return openBlock(), createElementBlock("li", {
203
+ key: index,
204
+ class: normalizeClass(unref(cls).e("mapping"))
205
+ }, [
206
+ createVNode(unref(USelect), {
207
+ size: "small",
208
+ class: normalizeClass(unref(cls).e("field")),
209
+ placeholder: "字段",
210
+ "model-value": row.field,
211
+ options: fieldOptions.value,
212
+ "onUpdate:modelValue": ($event) => row.field = $event
213
+ }, null, 8, [
214
+ "class",
215
+ "model-value",
216
+ "options",
217
+ "onUpdate:modelValue"
218
+ ]),
219
+ createElementVNode("span", { class: normalizeClass(unref(cls).e("arrow")) }, "→", 2),
220
+ createVNode(unref(USelect), {
221
+ size: "small",
222
+ class: normalizeClass(unref(cls).e("param")),
223
+ placeholder: "参数",
224
+ "model-value": row.param,
225
+ options: paramOptions.value,
226
+ "onUpdate:modelValue": ($event) => row.param = $event
227
+ }, null, 8, [
228
+ "class",
229
+ "model-value",
230
+ "options",
231
+ "onUpdate:modelValue"
232
+ ]),
233
+ createVNode(unref(UButton), {
234
+ size: "small",
235
+ text: "",
236
+ type: "danger",
237
+ onClick: ($event) => removeMapping(index)
238
+ }, {
239
+ default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode(" 删除 ", -1)])]),
240
+ _: 1
241
+ }, 8, ["onClick"])
242
+ ], 2);
243
+ }), 128))], 2)), targetParams.value.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_1, [createVNode(unref(UButton), {
244
+ size: "small",
245
+ plain: "",
246
+ onClick: addMapping
247
+ }, {
248
+ default: withCtx(() => [..._cache[3] || (_cache[3] = [createTextVNode("添加映射", -1)])]),
249
+ _: 1
250
+ })])) : createCommentVNode("v-if", true)], 64))], 2)
251
+ ], 2)]),
252
+ _: 1
253
+ }, 8, ["modelValue"]);
254
+ };
255
+ }
256
+ });
257
+ //#endregion
258
+ export { _sfc_main as default };
@@ -19,6 +19,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
19
19
  parentPickMode: {},
20
20
  rowParentCandidates: {},
21
21
  colParentCandidates: {},
22
+ showDrill: { type: Boolean },
22
23
  resolveFieldLabel: { type: Function },
23
24
  getBindingAt: { type: Function },
24
25
  hostEl: {},
@@ -28,6 +29,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
28
29
  "patch",
29
30
  "remove",
30
31
  "open-rules",
32
+ "open-drill",
31
33
  "start-parent-pick",
32
34
  "cancel-parent-pick",
33
35
  "clear-parent"
@@ -247,8 +249,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
247
249
  expanded.value ? unref(cls).m("expanded") : ""
248
250
  ]),
249
251
  style: normalizeStyle(panelStyle.value),
250
- onMousedown: _cache[8] || (_cache[8] = withModifiers(() => {}, ["stop"])),
251
- onPointerdown: _cache[9] || (_cache[9] = withModifiers(() => {}, ["stop"]))
252
+ onMousedown: _cache[9] || (_cache[9] = withModifiers(() => {}, ["stop"])),
253
+ onPointerdown: _cache[10] || (_cache[10] = withModifiers(() => {}, ["stop"]))
252
254
  }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("bar")) }, [createElementVNode("div", {
253
255
  class: normalizeClass(unref(cls).e("summary")),
254
256
  title: summaryText.value
@@ -289,16 +291,26 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
289
291
  plain: "",
290
292
  onClick: _cache[0] || (_cache[0] = ($event) => emit("open-rules"))
291
293
  }, {
292
- default: withCtx(() => [..._cache[10] || (_cache[10] = [createTextVNode("条件样式", -1)])]),
294
+ default: withCtx(() => [..._cache[11] || (_cache[11] = [createTextVNode("条件样式", -1)])]),
293
295
  _: 1
294
296
  }),
297
+ __props.showDrill ? (openBlock(), createBlock(unref(UButton), {
298
+ key: 2,
299
+ size: "small",
300
+ plain: "",
301
+ type: __props.binding.drill ? "primary" : void 0,
302
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("open-drill"))
303
+ }, {
304
+ default: withCtx(() => [..._cache[12] || (_cache[12] = [createTextVNode(" 下钻 ", -1)])]),
305
+ _: 1
306
+ }, 8, ["type"])) : createCommentVNode("v-if", true),
295
307
  createVNode(unref(UButton), {
296
308
  size: "small",
297
309
  text: "",
298
310
  type: "danger",
299
- onClick: _cache[1] || (_cache[1] = ($event) => emit("remove"))
311
+ onClick: _cache[2] || (_cache[2] = ($event) => emit("remove"))
300
312
  }, {
301
- default: withCtx(() => [..._cache[11] || (_cache[11] = [createTextVNode("删除绑定", -1)])]),
313
+ default: withCtx(() => [..._cache[13] || (_cache[13] = [createTextVNode("删除绑定", -1)])]),
302
314
  _: 1
303
315
  }),
304
316
  createVNode(unref(UButton), {
@@ -306,7 +318,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
306
318
  text: "",
307
319
  class: normalizeClass(unref(cls).e("expand")),
308
320
  title: expanded.value ? "收起" : "展开更多",
309
- onClick: _cache[2] || (_cache[2] = ($event) => expanded.value = !expanded.value)
321
+ onClick: _cache[3] || (_cache[3] = ($event) => expanded.value = !expanded.value)
310
322
  }, {
311
323
  default: withCtx(() => [createTextVNode(toDisplayString(expanded.value ? "收起" : "展开"), 1)]),
312
324
  _: 1
@@ -335,7 +347,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
335
347
  size: "small",
336
348
  plain: "",
337
349
  type: __props.parentPickMode === "row" ? "primary" : void 0,
338
- onClick: _cache[3] || (_cache[3] = ($event) => emit("start-parent-pick", "row"))
350
+ onClick: _cache[4] || (_cache[4] = ($event) => emit("start-parent-pick", "row"))
339
351
  }, {
340
352
  default: withCtx(() => [createTextVNode(toDisplayString(__props.parentPickMode === "row" ? "点选目标格…" : "设置行方向父格"), 1)]),
341
353
  _: 1
@@ -358,9 +370,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
358
370
  size: "small",
359
371
  text: "",
360
372
  type: "danger",
361
- onClick: _cache[4] || (_cache[4] = ($event) => emit("clear-parent", "row"))
373
+ onClick: _cache[5] || (_cache[5] = ($event) => emit("clear-parent", "row"))
362
374
  }, {
363
- default: withCtx(() => [..._cache[12] || (_cache[12] = [createTextVNode(" 清除 ", -1)])]),
375
+ default: withCtx(() => [..._cache[14] || (_cache[14] = [createTextVNode(" 清除 ", -1)])]),
364
376
  _: 1
365
377
  })) : createCommentVNode("v-if", true)
366
378
  ], 2)
@@ -373,7 +385,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
373
385
  size: "small",
374
386
  plain: "",
375
387
  type: __props.parentPickMode === "col" ? "primary" : void 0,
376
- onClick: _cache[5] || (_cache[5] = ($event) => emit("start-parent-pick", "col"))
388
+ onClick: _cache[6] || (_cache[6] = ($event) => emit("start-parent-pick", "col"))
377
389
  }, {
378
390
  default: withCtx(() => [createTextVNode(toDisplayString(__props.parentPickMode === "col" ? "点选目标格…" : "设置列方向父格"), 1)]),
379
391
  _: 1
@@ -396,9 +408,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
396
408
  size: "small",
397
409
  text: "",
398
410
  type: "danger",
399
- onClick: _cache[6] || (_cache[6] = ($event) => emit("clear-parent", "col"))
411
+ onClick: _cache[7] || (_cache[7] = ($event) => emit("clear-parent", "col"))
400
412
  }, {
401
- default: withCtx(() => [..._cache[13] || (_cache[13] = [createTextVNode(" 清除 ", -1)])]),
413
+ default: withCtx(() => [..._cache[15] || (_cache[15] = [createTextVNode(" 清除 ", -1)])]),
402
414
  _: 1
403
415
  })) : createCommentVNode("v-if", true)
404
416
  ], 2)
@@ -409,9 +421,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
409
421
  }, [createVNode(unref(UButton), {
410
422
  size: "small",
411
423
  plain: "",
412
- onClick: _cache[7] || (_cache[7] = ($event) => emit("cancel-parent-pick"))
424
+ onClick: _cache[8] || (_cache[8] = ($event) => emit("cancel-parent-pick"))
413
425
  }, {
414
- default: withCtx(() => [..._cache[14] || (_cache[14] = [createTextVNode("取消点选", -1)])]),
426
+ default: withCtx(() => [..._cache[16] || (_cache[16] = [createTextVNode("取消点选", -1)])]),
415
427
  _: 1
416
428
  })], 2)) : createCommentVNode("v-if", true)
417
429
  ], 2)) : createCommentVNode("v-if", true)], 38)), [[vShow, visible.value]]) : createCommentVNode("v-if", true);
@@ -0,0 +1,39 @@
1
+ //#region src/components/report/print.ts
2
+ /**
3
+ * 打印 HTML 文档:隐藏 iframe 装载后调起浏览器打印,结束(或兜底超时)销毁。
4
+ *
5
+ * 路线取舍:相对 `window.open` 不被弹窗拦截;相对 `@media print` 直打宿主页,
6
+ * 不侵入应用布局,也不受网格 canvas 虚拟滚动(只绘制视口)限制。
7
+ * iframe 用 0×0 定位隐藏而非 `display:none`(部分浏览器不打印 none 的 iframe)。
8
+ */
9
+ function printHtmlDocument(html) {
10
+ const iframe = document.createElement("iframe");
11
+ iframe.setAttribute("aria-hidden", "true");
12
+ iframe.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
13
+ document.body.appendChild(iframe);
14
+ const win = iframe.contentWindow;
15
+ const doc = iframe.contentDocument;
16
+ if (!win || !doc || typeof win.print !== "function") {
17
+ iframe.remove();
18
+ throw new Error("当前环境不支持打印");
19
+ }
20
+ let cleaned = false;
21
+ const cleanup = () => {
22
+ if (cleaned) return;
23
+ cleaned = true;
24
+ iframe.remove();
25
+ };
26
+ win.addEventListener("afterprint", cleanup);
27
+ doc.open();
28
+ doc.write(html);
29
+ doc.close();
30
+ const decoding = Array.from(doc.images).map((img) => typeof img.decode === "function" ? img.decode().catch(() => void 0) : Promise.resolve());
31
+ Promise.all(decoding).then(() => {
32
+ if (cleaned) return;
33
+ win.focus();
34
+ win.print();
35
+ setTimeout(cleanup, 6e4);
36
+ });
37
+ }
38
+ //#endregion
39
+ export { printHtmlDocument };