@report-designer/designer 0.1.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 (253) hide show
  1. package/dist/band-metadata.d.ts +8 -0
  2. package/dist/band-metadata.d.ts.map +1 -0
  3. package/dist/band-metadata.js +75 -0
  4. package/dist/band-metadata.js.map +1 -0
  5. package/dist/component-factory.d.ts +9 -0
  6. package/dist/component-factory.d.ts.map +1 -0
  7. package/dist/component-factory.js +141 -0
  8. package/dist/component-factory.js.map +1 -0
  9. package/dist/component-palette-model.d.ts +14 -0
  10. package/dist/component-palette-model.d.ts.map +1 -0
  11. package/dist/component-palette-model.js +21 -0
  12. package/dist/component-palette-model.js.map +1 -0
  13. package/dist/components/Canvas.d.ts +5 -0
  14. package/dist/components/Canvas.d.ts.map +1 -0
  15. package/dist/components/Canvas.js +2428 -0
  16. package/dist/components/Canvas.js.map +1 -0
  17. package/dist/components/ConditionalFormatManager.d.ts +8 -0
  18. package/dist/components/ConditionalFormatManager.d.ts.map +1 -0
  19. package/dist/components/ConditionalFormatManager.js +135 -0
  20. package/dist/components/ConditionalFormatManager.js.map +1 -0
  21. package/dist/components/Designer.d.ts +22 -0
  22. package/dist/components/Designer.d.ts.map +1 -0
  23. package/dist/components/Designer.js +115 -0
  24. package/dist/components/Designer.js.map +1 -0
  25. package/dist/components/ExpressionEditor.d.ts +10 -0
  26. package/dist/components/ExpressionEditor.d.ts.map +1 -0
  27. package/dist/components/ExpressionEditor.js +203 -0
  28. package/dist/components/ExpressionEditor.js.map +1 -0
  29. package/dist/components/LeftPanel.d.ts +11 -0
  30. package/dist/components/LeftPanel.d.ts.map +1 -0
  31. package/dist/components/LeftPanel.js +551 -0
  32. package/dist/components/LeftPanel.js.map +1 -0
  33. package/dist/components/PropertyEditor.d.ts +6 -0
  34. package/dist/components/PropertyEditor.d.ts.map +1 -0
  35. package/dist/components/PropertyEditor.js +1002 -0
  36. package/dist/components/PropertyEditor.js.map +1 -0
  37. package/dist/components/RibbonToolbar.d.ts +3 -0
  38. package/dist/components/RibbonToolbar.d.ts.map +1 -0
  39. package/dist/components/RibbonToolbar.js +179 -0
  40. package/dist/components/RibbonToolbar.js.map +1 -0
  41. package/dist/components/TextFormatEditor.d.ts +13 -0
  42. package/dist/components/TextFormatEditor.d.ts.map +1 -0
  43. package/dist/components/TextFormatEditor.js +199 -0
  44. package/dist/components/TextFormatEditor.js.map +1 -0
  45. package/dist/components/TextStyleLibraryDialog.d.ts +8 -0
  46. package/dist/components/TextStyleLibraryDialog.d.ts.map +1 -0
  47. package/dist/components/TextStyleLibraryDialog.js +367 -0
  48. package/dist/components/TextStyleLibraryDialog.js.map +1 -0
  49. package/dist/components/canvas/DesignerCanvasFrame.d.ts +10 -0
  50. package/dist/components/canvas/DesignerCanvasFrame.d.ts.map +1 -0
  51. package/dist/components/canvas/DesignerCanvasFrame.js +22 -0
  52. package/dist/components/canvas/DesignerCanvasFrame.js.map +1 -0
  53. package/dist/components/chart/ChartAxesPanel.d.ts +12 -0
  54. package/dist/components/chart/ChartAxesPanel.d.ts.map +1 -0
  55. package/dist/components/chart/ChartAxesPanel.js +71 -0
  56. package/dist/components/chart/ChartAxesPanel.js.map +1 -0
  57. package/dist/components/chart/ChartDataPanel.d.ts +21 -0
  58. package/dist/components/chart/ChartDataPanel.d.ts.map +1 -0
  59. package/dist/components/chart/ChartDataPanel.js +80 -0
  60. package/dist/components/chart/ChartDataPanel.js.map +1 -0
  61. package/dist/components/chart/ChartLabelPanel.d.ts +12 -0
  62. package/dist/components/chart/ChartLabelPanel.d.ts.map +1 -0
  63. package/dist/components/chart/ChartLabelPanel.js +34 -0
  64. package/dist/components/chart/ChartLabelPanel.js.map +1 -0
  65. package/dist/components/chart/ChartLegendPanel.d.ts +12 -0
  66. package/dist/components/chart/ChartLegendPanel.d.ts.map +1 -0
  67. package/dist/components/chart/ChartLegendPanel.js +48 -0
  68. package/dist/components/chart/ChartLegendPanel.js.map +1 -0
  69. package/dist/components/chart/ChartPropertyPanel.d.ts +26 -0
  70. package/dist/components/chart/ChartPropertyPanel.d.ts.map +1 -0
  71. package/dist/components/chart/ChartPropertyPanel.js +119 -0
  72. package/dist/components/chart/ChartPropertyPanel.js.map +1 -0
  73. package/dist/components/chart/ChartThemePanel.d.ts +9 -0
  74. package/dist/components/chart/ChartThemePanel.d.ts.map +1 -0
  75. package/dist/components/chart/ChartThemePanel.js +21 -0
  76. package/dist/components/chart/ChartThemePanel.js.map +1 -0
  77. package/dist/components/chart/ChartTitlePanel.d.ts +10 -0
  78. package/dist/components/chart/ChartTitlePanel.d.ts.map +1 -0
  79. package/dist/components/chart/ChartTitlePanel.js +45 -0
  80. package/dist/components/chart/ChartTitlePanel.js.map +1 -0
  81. package/dist/components/chart/ChartTypeStylePanel.d.ts +11 -0
  82. package/dist/components/chart/ChartTypeStylePanel.d.ts.map +1 -0
  83. package/dist/components/chart/ChartTypeStylePanel.js +37 -0
  84. package/dist/components/chart/ChartTypeStylePanel.js.map +1 -0
  85. package/dist/components/chart/ColorPaletteEditor.d.ts +10 -0
  86. package/dist/components/chart/ColorPaletteEditor.d.ts.map +1 -0
  87. package/dist/components/chart/ColorPaletteEditor.js +18 -0
  88. package/dist/components/chart/ColorPaletteEditor.js.map +1 -0
  89. package/dist/components/chart/chart-options.d.ts +119 -0
  90. package/dist/components/chart/chart-options.d.ts.map +1 -0
  91. package/dist/components/chart/chart-options.js +217 -0
  92. package/dist/components/chart/chart-options.js.map +1 -0
  93. package/dist/components/dialogs/BandWizardDialog.d.ts +8 -0
  94. package/dist/components/dialogs/BandWizardDialog.d.ts.map +1 -0
  95. package/dist/components/dialogs/BandWizardDialog.js +54 -0
  96. package/dist/components/dialogs/BandWizardDialog.js.map +1 -0
  97. package/dist/components/dialogs/GroupWizardDialog.d.ts +8 -0
  98. package/dist/components/dialogs/GroupWizardDialog.d.ts.map +1 -0
  99. package/dist/components/dialogs/GroupWizardDialog.js +70 -0
  100. package/dist/components/dialogs/GroupWizardDialog.js.map +1 -0
  101. package/dist/components/dialogs/JsonDataSourceDialog.d.ts +8 -0
  102. package/dist/components/dialogs/JsonDataSourceDialog.d.ts.map +1 -0
  103. package/dist/components/dialogs/JsonDataSourceDialog.js +67 -0
  104. package/dist/components/dialogs/JsonDataSourceDialog.js.map +1 -0
  105. package/dist/components/dialogs/PageSetupDialog.d.ts +8 -0
  106. package/dist/components/dialogs/PageSetupDialog.d.ts.map +1 -0
  107. package/dist/components/dialogs/PageSetupDialog.js +145 -0
  108. package/dist/components/dialogs/PageSetupDialog.js.map +1 -0
  109. package/dist/components/dialogs/dialog-utils.d.ts +6 -0
  110. package/dist/components/dialogs/dialog-utils.d.ts.map +1 -0
  111. package/dist/components/dialogs/dialog-utils.js +37 -0
  112. package/dist/components/dialogs/dialog-utils.js.map +1 -0
  113. package/dist/components/events/EventEditorDialog.d.ts +43 -0
  114. package/dist/components/events/EventEditorDialog.d.ts.map +1 -0
  115. package/dist/components/events/EventEditorDialog.js +271 -0
  116. package/dist/components/events/EventEditorDialog.js.map +1 -0
  117. package/dist/components/events/EventScriptEditor.d.ts +41 -0
  118. package/dist/components/events/EventScriptEditor.d.ts.map +1 -0
  119. package/dist/components/events/EventScriptEditor.js +140 -0
  120. package/dist/components/events/EventScriptEditor.js.map +1 -0
  121. package/dist/components/events/event-editor-utils.d.ts +18 -0
  122. package/dist/components/events/event-editor-utils.d.ts.map +1 -0
  123. package/dist/components/events/event-editor-utils.js +66 -0
  124. package/dist/components/events/event-editor-utils.js.map +1 -0
  125. package/dist/components/events/event-script-monaco.d.ts +74 -0
  126. package/dist/components/events/event-script-monaco.d.ts.map +1 -0
  127. package/dist/components/events/event-script-monaco.js +282 -0
  128. package/dist/components/events/event-script-monaco.js.map +1 -0
  129. package/dist/components/events/event-script-templates.d.ts +7 -0
  130. package/dist/components/events/event-script-templates.d.ts.map +1 -0
  131. package/dist/components/events/event-script-templates.js +100 -0
  132. package/dist/components/events/event-script-templates.js.map +1 -0
  133. package/dist/components/expression/ExpressionMonacoEditor.d.ts +16 -0
  134. package/dist/components/expression/ExpressionMonacoEditor.d.ts.map +1 -0
  135. package/dist/components/expression/ExpressionMonacoEditor.js +63 -0
  136. package/dist/components/expression/ExpressionMonacoEditor.js.map +1 -0
  137. package/dist/components/expression/InlineExpressionEditor.d.ts +10 -0
  138. package/dist/components/expression/InlineExpressionEditor.d.ts.map +1 -0
  139. package/dist/components/expression/InlineExpressionEditor.js +33 -0
  140. package/dist/components/expression/InlineExpressionEditor.js.map +1 -0
  141. package/dist/components/expression/expression-monaco.d.ts +34 -0
  142. package/dist/components/expression/expression-monaco.d.ts.map +1 -0
  143. package/dist/components/expression/expression-monaco.js +87 -0
  144. package/dist/components/expression/expression-monaco.js.map +1 -0
  145. package/dist/components/panels/DesignerLeftPanel.d.ts +11 -0
  146. package/dist/components/panels/DesignerLeftPanel.d.ts.map +1 -0
  147. package/dist/components/panels/DesignerLeftPanel.js +8 -0
  148. package/dist/components/panels/DesignerLeftPanel.js.map +1 -0
  149. package/dist/components/panels/DesignerPropertyPanel.d.ts +6 -0
  150. package/dist/components/panels/DesignerPropertyPanel.d.ts.map +1 -0
  151. package/dist/components/panels/DesignerPropertyPanel.js +441 -0
  152. package/dist/components/panels/DesignerPropertyPanel.js.map +1 -0
  153. package/dist/components/panels/PanelSearchBox.d.ts +9 -0
  154. package/dist/components/panels/PanelSearchBox.d.ts.map +1 -0
  155. package/dist/components/panels/PanelSearchBox.js +5 -0
  156. package/dist/components/panels/PanelSearchBox.js.map +1 -0
  157. package/dist/components/properties/BandPropertyGrid.d.ts +6 -0
  158. package/dist/components/properties/BandPropertyGrid.d.ts.map +1 -0
  159. package/dist/components/properties/BandPropertyGrid.js +504 -0
  160. package/dist/components/properties/BandPropertyGrid.js.map +1 -0
  161. package/dist/components/properties/BoxStyleEditors.d.ts +59 -0
  162. package/dist/components/properties/BoxStyleEditors.d.ts.map +1 -0
  163. package/dist/components/properties/BoxStyleEditors.js +87 -0
  164. package/dist/components/properties/BoxStyleEditors.js.map +1 -0
  165. package/dist/components/properties/FontEditor.d.ts +28 -0
  166. package/dist/components/properties/FontEditor.d.ts.map +1 -0
  167. package/dist/components/properties/FontEditor.js +22 -0
  168. package/dist/components/properties/FontEditor.js.map +1 -0
  169. package/dist/components/ribbon/DesignerRibbon.d.ts +3 -0
  170. package/dist/components/ribbon/DesignerRibbon.d.ts.map +1 -0
  171. package/dist/components/ribbon/DesignerRibbon.js +193 -0
  172. package/dist/components/ribbon/DesignerRibbon.js.map +1 -0
  173. package/dist/components/richtext/RichTextInlineEditor.d.ts +15 -0
  174. package/dist/components/richtext/RichTextInlineEditor.d.ts.map +1 -0
  175. package/dist/components/richtext/RichTextInlineEditor.js +94 -0
  176. package/dist/components/richtext/RichTextInlineEditor.js.map +1 -0
  177. package/dist/components/shell/DesignerShell.d.ts +12 -0
  178. package/dist/components/shell/DesignerShell.d.ts.map +1 -0
  179. package/dist/components/shell/DesignerShell.js +124 -0
  180. package/dist/components/shell/DesignerShell.js.map +1 -0
  181. package/dist/components/shell/DesignerStatusBar.d.ts +3 -0
  182. package/dist/components/shell/DesignerStatusBar.d.ts.map +1 -0
  183. package/dist/components/shell/DesignerStatusBar.js +23 -0
  184. package/dist/components/shell/DesignerStatusBar.js.map +1 -0
  185. package/dist/components/tree/ReportTree.d.ts +3 -0
  186. package/dist/components/tree/ReportTree.d.ts.map +1 -0
  187. package/dist/components/tree/ReportTree.js +17 -0
  188. package/dist/components/tree/ReportTree.js.map +1 -0
  189. package/dist/data-source-fields.d.ts +14 -0
  190. package/dist/data-source-fields.d.ts.map +1 -0
  191. package/dist/data-source-fields.js +49 -0
  192. package/dist/data-source-fields.js.map +1 -0
  193. package/dist/data-source-paths.d.ts +10 -0
  194. package/dist/data-source-paths.d.ts.map +1 -0
  195. package/dist/data-source-paths.js +61 -0
  196. package/dist/data-source-paths.js.map +1 -0
  197. package/dist/expression/expression-catalog.d.ts +39 -0
  198. package/dist/expression/expression-catalog.d.ts.map +1 -0
  199. package/dist/expression/expression-catalog.js +127 -0
  200. package/dist/expression/expression-catalog.js.map +1 -0
  201. package/dist/expression/expression-preview.d.ts +11 -0
  202. package/dist/expression/expression-preview.d.ts.map +1 -0
  203. package/dist/expression/expression-preview.js +58 -0
  204. package/dist/expression/expression-preview.js.map +1 -0
  205. package/dist/expression/expression-validation.d.ts +12 -0
  206. package/dist/expression/expression-validation.d.ts.map +1 -0
  207. package/dist/expression/expression-validation.js +85 -0
  208. package/dist/expression/expression-validation.js.map +1 -0
  209. package/dist/expression/function-catalog.d.ts +21 -0
  210. package/dist/expression/function-catalog.d.ts.map +1 -0
  211. package/dist/expression/function-catalog.js +96 -0
  212. package/dist/expression/function-catalog.js.map +1 -0
  213. package/dist/i18n/DesignerI18nProvider.d.ts +11 -0
  214. package/dist/i18n/DesignerI18nProvider.d.ts.map +1 -0
  215. package/dist/i18n/DesignerI18nProvider.js +32 -0
  216. package/dist/i18n/DesignerI18nProvider.js.map +1 -0
  217. package/dist/i18n/index.d.ts +3 -0
  218. package/dist/i18n/index.d.ts.map +1 -0
  219. package/dist/i18n/index.js +2 -0
  220. package/dist/i18n/index.js.map +1 -0
  221. package/dist/i18n/messages.d.ts +5 -0
  222. package/dist/i18n/messages.d.ts.map +1 -0
  223. package/dist/i18n/messages.js +1383 -0
  224. package/dist/i18n/messages.js.map +1 -0
  225. package/dist/index.d.ts +21 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +18 -0
  228. package/dist/index.js.map +1 -0
  229. package/dist/page-settings.d.ts +21 -0
  230. package/dist/page-settings.d.ts.map +1 -0
  231. package/dist/page-settings.js +66 -0
  232. package/dist/page-settings.js.map +1 -0
  233. package/dist/report-structure.d.ts +20 -0
  234. package/dist/report-structure.d.ts.map +1 -0
  235. package/dist/report-structure.js +219 -0
  236. package/dist/report-structure.js.map +1 -0
  237. package/dist/store/designer-store.d.ts +161 -0
  238. package/dist/store/designer-store.d.ts.map +1 -0
  239. package/dist/store/designer-store.js +1851 -0
  240. package/dist/store/designer-store.js.map +1 -0
  241. package/dist/table/table-structure.d.ts +50 -0
  242. package/dist/table/table-structure.d.ts.map +1 -0
  243. package/dist/table/table-structure.js +251 -0
  244. package/dist/table/table-structure.js.map +1 -0
  245. package/dist/text-style-application.d.ts +11 -0
  246. package/dist/text-style-application.d.ts.map +1 -0
  247. package/dist/text-style-application.js +135 -0
  248. package/dist/text-style-application.js.map +1 -0
  249. package/dist/text-style-bindings.d.ts +16 -0
  250. package/dist/text-style-bindings.d.ts.map +1 -0
  251. package/dist/text-style-bindings.js +549 -0
  252. package/dist/text-style-bindings.js.map +1 -0
  253. package/package.json +70 -0
@@ -0,0 +1,2428 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
3
+ import { Button, Dropdown, Modal, Tooltip } from 'antd';
4
+ import { EditOutlined } from '@ant-design/icons';
5
+ import { sanitizeRichHtml } from '@report-designer/core';
6
+ import { useDesignerStore } from '../store/designer-store';
7
+ import { normalizeTable, resolveCollapsedCellBorder, resolveTableCellStyle, resolveTableRowCellWidths } from '../table/table-structure';
8
+ import { createDefaultComponent, createFieldExpressionComponent, createTextExpressionComponent } from '../component-factory';
9
+ import { formatDataFieldExpression } from '../data-source-fields';
10
+ import { RichTextInlineEditor } from './richtext/RichTextInlineEditor';
11
+ import { useDesignerI18n } from '../i18n';
12
+ import { BAND_COLORS, BAND_LABEL_KEYS } from '../band-metadata';
13
+ import { renderCodeSymbolSvg } from '@report-designer/viewer';
14
+ const MM_TO_PX = 3.78;
15
+ const SNAP_THRESHOLD = 5;
16
+ const HANDLE_SIZE = 8;
17
+ const GRID_MM = 5; // 网格间距 5mm
18
+ const BAND_HEADER_MM = 7;
19
+ const RULER_SIZE = 24;
20
+ const DRAG_THRESHOLD = 3; // px,超过此距离才算真正开始拖拽
21
+ const RESIZE_HANDLES = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
22
+ function mmToPx(mm) {
23
+ const numeric = Number(mm);
24
+ if (!Number.isFinite(numeric)) {
25
+ return 0;
26
+ }
27
+ return Math.round(numeric * MM_TO_PX);
28
+ }
29
+ function safeCssNumber(value) {
30
+ return Number.isFinite(value) ? value : 0;
31
+ }
32
+ function pxToMm(px, zoom = 1) { return Math.round(px / (MM_TO_PX * zoom) * 10) / 10; }
33
+ function elementFromPointSafe(clientX, clientY) {
34
+ if (typeof document.elementFromPoint !== 'function')
35
+ return null;
36
+ return document.elementFromPoint(clientX, clientY);
37
+ }
38
+ function isEditableKeyboardTarget(target) {
39
+ if (!(target instanceof HTMLElement))
40
+ return false;
41
+ const tag = target.tagName;
42
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
43
+ return true;
44
+ if (target.isContentEditable || target.closest('[contenteditable="true"]'))
45
+ return true;
46
+ if (target.closest('[role="textbox"]'))
47
+ return true;
48
+ return Boolean(target.closest('.ant-modal'));
49
+ }
50
+ function computeGuides(compId, xMm, yMm, wMm, hMm, others) {
51
+ const guides = [];
52
+ for (const o of others) {
53
+ if (o.id === compId)
54
+ continue;
55
+ const yChecks = [
56
+ { a: yMm, b: o.y }, { a: yMm + hMm, b: o.y + o.h },
57
+ { a: yMm + hMm / 2, b: o.y + o.h / 2 },
58
+ { a: yMm, b: o.y + o.h }, { a: yMm + hMm, b: o.y },
59
+ ];
60
+ for (const c of yChecks) {
61
+ if (Math.abs(mmToPx(c.a) - mmToPx(c.b)) <= SNAP_THRESHOLD) {
62
+ guides.push({ type: 'horizontal', position: mmToPx(c.a) });
63
+ }
64
+ }
65
+ const xChecks = [
66
+ { a: xMm, b: o.x }, { a: xMm + wMm, b: o.x + o.w },
67
+ { a: xMm + wMm / 2, b: o.x + o.w / 2 },
68
+ { a: xMm, b: o.x + o.w }, { a: xMm + wMm, b: o.x },
69
+ ];
70
+ for (const c of xChecks) {
71
+ if (Math.abs(mmToPx(c.a) - mmToPx(c.b)) <= SNAP_THRESHOLD) {
72
+ guides.push({ type: 'vertical', position: mmToPx(c.a) });
73
+ }
74
+ }
75
+ }
76
+ return guides;
77
+ }
78
+ function getCursorForHandle(handle) {
79
+ return { nw: 'nw-resize', n: 'n-resize', ne: 'ne-resize', w: 'w-resize', e: 'e-resize', sw: 'sw-resize', s: 's-resize', se: 'se-resize' }[handle];
80
+ }
81
+ function getHandlePos(handle, w, h) {
82
+ const half = HANDLE_SIZE / 2;
83
+ switch (handle) {
84
+ case 'nw': return { left: -half, top: -half };
85
+ case 'n': return { left: w / 2 - half, top: -half };
86
+ case 'ne': return { right: -half, top: -half };
87
+ case 'w': return { left: -half, top: h / 2 - half };
88
+ case 'e': return { right: -half, top: h / 2 - half };
89
+ case 'sw': return { left: -half, bottom: -half };
90
+ case 's': return { left: w / 2 - half, bottom: -half };
91
+ case 'se': return { right: -half, bottom: -half };
92
+ }
93
+ }
94
+ function findPanelDropTarget(band, xMm, yMm) {
95
+ const panels = band.components
96
+ .filter((component) => component.type === 'panel')
97
+ .slice()
98
+ .sort((a, b) => (b.zOrder ?? 0) - (a.zOrder ?? 0));
99
+ for (const panel of panels) {
100
+ const insideX = xMm >= panel.x && xMm <= panel.x + panel.width;
101
+ const insideY = yMm >= panel.y && yMm <= panel.y + panel.height;
102
+ if (!insideX || !insideY)
103
+ continue;
104
+ const padding = panel.padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
105
+ const contentWidth = Math.max(0, panel.width - padding.left - padding.right);
106
+ const contentHeight = Math.max(0, panel.height - padding.top - padding.bottom);
107
+ return {
108
+ panelId: panel.id,
109
+ xMm: Math.max(0, Math.min(contentWidth, Math.round((xMm - panel.x - padding.left) * 10) / 10)),
110
+ yMm: Math.max(0, Math.min(contentHeight, Math.round((yMm - panel.y - padding.top) * 10) / 10)),
111
+ };
112
+ }
113
+ return null;
114
+ }
115
+ function hasDragPayload(event, type) {
116
+ const expected = type.toLowerCase();
117
+ return Array.from(event.dataTransfer.types ?? []).some(item => item.toLowerCase() === expected);
118
+ }
119
+ function getDragData(event, type) {
120
+ return event.dataTransfer.getData(type) || event.dataTransfer.getData(type.toLowerCase());
121
+ }
122
+ // ---- Canvas ----
123
+ export const Canvas = ({ className }) => {
124
+ const { t } = useDesignerI18n();
125
+ const template = useDesignerStore(s => s.template);
126
+ const currentPageId = useDesignerStore(s => s.currentPageId);
127
+ const selectedComponentIds = useDesignerStore(s => s.selectedComponentIds);
128
+ const selectedBandId = useDesignerStore(s => s.selectedBandId);
129
+ const pendingBandInsertType = useDesignerStore(s => s.pendingBandInsertType);
130
+ const selectedTableCell = useDesignerStore(s => s.selectedTableCell);
131
+ const storeClipboard = useDesignerStore(s => s.clipboard);
132
+ const selectComponents = useDesignerStore(s => s.selectComponents);
133
+ const selectBand = useDesignerStore(s => s.selectBand);
134
+ const selectTableCell = useDesignerStore(s => s.selectTableCell);
135
+ const moveComponent = useDesignerStore(s => s.moveComponent);
136
+ const moveComponentSilent = useDesignerStore(s => s.moveComponentSilent);
137
+ const updateComponent = useDesignerStore(s => s.updateComponent);
138
+ const addComponent = useDesignerStore(s => s.addComponent);
139
+ const addComponentToPanel = useDesignerStore(s => s.addComponentToPanel);
140
+ const updateComponentSilent = useDesignerStore(s => s.updateComponentSilent);
141
+ const moveComponentToBand = useDesignerStore(s => s.moveComponentToBand);
142
+ const setSelectedTableCellWidth = useDesignerStore(s => s.setSelectedTableCellWidth);
143
+ const resizeBand = useDesignerStore(s => s.resizeBand);
144
+ const resizeBandSilent = useDesignerStore(s => s.resizeBandSilent);
145
+ const moveBand = useDesignerStore(s => s.moveBand);
146
+ const copySelected = useDesignerStore(s => s.copySelected);
147
+ const cutSelected = useDesignerStore(s => s.cutSelected);
148
+ const duplicateSelected = useDesignerStore(s => s.duplicateSelected);
149
+ const pasteClipboard = useDesignerStore(s => s.pasteClipboard);
150
+ const deleteSelected = useDesignerStore(s => s.deleteSelected);
151
+ const clearSelectedTableCell = useDesignerStore(s => s.clearSelectedTableCell);
152
+ const moveSelectedBy = useDesignerStore(s => s.moveSelectedBy);
153
+ const resizeSelectedBy = useDesignerStore(s => s.resizeSelectedBy);
154
+ const toggleSelectedFontStyle = useDesignerStore(s => s.toggleSelectedFontStyle);
155
+ const setTextAlign = useDesignerStore(s => s.setTextAlign);
156
+ const setDesignerMode = useDesignerStore(s => s.setMode);
157
+ const zoom = useDesignerStore(s => s.zoom);
158
+ const setZoom = useDesignerStore(s => s.setZoom);
159
+ const undo = useDesignerStore(s => s.undo);
160
+ const redo = useDesignerStore(s => s.redo);
161
+ const cancelBandInsert = useDesignerStore(s => s.cancelBandInsert);
162
+ const pageRef = useRef(null);
163
+ const modeRef = useRef({ type: 'idle' });
164
+ const [mode, setMode] = useState({ type: 'idle' });
165
+ const [selBox, setSelBox] = useState(null);
166
+ const [guides, setGuides] = useState([]);
167
+ const [contextMenu, setContextMenu] = useState(null);
168
+ const [bandContextMenu, setBandContextMenu] = useState(null);
169
+ const [bandInsertPointer, setBandInsertPointer] = useState(null);
170
+ const [bandReorderTarget, setBandReorderTarget] = useState(null);
171
+ const [bandDragPreview, setBandDragPreview] = useState(null);
172
+ const [componentMoveTargetBandId, setComponentMoveTargetBandId] = useState(null);
173
+ const currentPage = useMemo(() => template.pages.find(p => p.id === currentPageId), [template, currentPageId]);
174
+ const bands = useMemo(() => {
175
+ if (!currentPage)
176
+ return [];
177
+ let contentY = 0;
178
+ let visualY = 0;
179
+ return currentPage.bands.map(band => {
180
+ const r = { band, cumY: contentY, visualY };
181
+ contentY += band.height;
182
+ visualY += band.height + BAND_HEADER_MM;
183
+ return r;
184
+ });
185
+ }, [currentPage]);
186
+ const bandLabelIndexes = useMemo(() => {
187
+ const counters = {};
188
+ const result = {};
189
+ for (const { band } of bands) {
190
+ const key = band.type;
191
+ counters[key] = (counters[key] ?? 0) + 1;
192
+ result[band.id] = counters[key];
193
+ }
194
+ return result;
195
+ }, [bands]);
196
+ const flat = useMemo(() => {
197
+ const items = [];
198
+ for (const { band, cumY, visualY } of bands) {
199
+ for (const comp of band.components) {
200
+ items.push({ comp, bandId: band.id, cumY, visualY });
201
+ }
202
+ }
203
+ return items;
204
+ }, [bands]);
205
+ const others = useMemo(() => flat.map(f => ({ id: f.comp.id, x: f.comp.x, y: f.comp.y, w: f.comp.width, h: f.comp.height, bandId: f.bandId })), [flat]);
206
+ // Refs for stable access during drag (prevent effect re-subscribe)
207
+ const flatRef = useRef(flat);
208
+ flatRef.current = flat;
209
+ const othersRef = useRef(others);
210
+ othersRef.current = others;
211
+ const bandsRef = useRef(bands);
212
+ bandsRef.current = bands;
213
+ const currentPageIdRef = useRef(currentPageId);
214
+ currentPageIdRef.current = currentPageId;
215
+ const selBoxRef = useRef(selBox);
216
+ selBoxRef.current = selBox;
217
+ const getPointerContentY = useCallback((clientY) => {
218
+ if (!pageRef.current)
219
+ return 0;
220
+ const rect = pageRef.current.getBoundingClientRect();
221
+ const marginTop = mmToPx(currentPage?.margins?.top ?? 0);
222
+ return (clientY - rect.top) / zoom - marginTop;
223
+ }, [currentPage, zoom]);
224
+ const findBandAtComponentCenter = useCallback((sourceBandId, componentY, componentHeight) => {
225
+ const sourceLayout = bandsRef.current.find(item => item.band.id === sourceBandId);
226
+ if (!sourceLayout)
227
+ return null;
228
+ const centerY = sourceLayout.visualY + BAND_HEADER_MM + componentY + componentHeight / 2;
229
+ return bandsRef.current.find(item => {
230
+ const bodyTop = item.visualY + BAND_HEADER_MM;
231
+ const bodyBottom = bodyTop + item.band.height;
232
+ return centerY >= bodyTop && centerY <= bodyBottom;
233
+ }) ?? null;
234
+ }, []);
235
+ const convertComponentYToBand = useCallback((sourceBandId, targetBandId, componentY) => {
236
+ const sourceLayout = bandsRef.current.find(item => item.band.id === sourceBandId);
237
+ const targetLayout = bandsRef.current.find(item => item.band.id === targetBandId);
238
+ if (!sourceLayout || !targetLayout)
239
+ return componentY;
240
+ const pageTop = sourceLayout.visualY + BAND_HEADER_MM + componentY;
241
+ const targetBodyTop = targetLayout.visualY + BAND_HEADER_MM;
242
+ return Math.round((pageTop - targetBodyTop) * 10) / 10;
243
+ }, []);
244
+ // ---- Hit tests ----
245
+ const findResizeHandleAtPoint = useCallback((clientX, clientY) => {
246
+ const el = elementFromPointSafe(clientX, clientY);
247
+ if (!el)
248
+ return null;
249
+ const handleEl = el.closest('[data-resize-handle]');
250
+ if (!handleEl)
251
+ return null;
252
+ return {
253
+ compId: handleEl.dataset.compId,
254
+ bandId: handleEl.dataset.bandId,
255
+ handle: handleEl.dataset.handleName,
256
+ };
257
+ }, []);
258
+ const findBandResizeAtPoint = useCallback((clientX, clientY) => {
259
+ const el = elementFromPointSafe(clientX, clientY);
260
+ if (!el)
261
+ return null;
262
+ const handleEl = el.closest('[data-band-resize]');
263
+ if (!handleEl)
264
+ return null;
265
+ return { bandId: handleEl.dataset.bandId };
266
+ }, []);
267
+ const findComponentAtPoint = useCallback((clientX, clientY) => {
268
+ const el = elementFromPointSafe(clientX, clientY);
269
+ if (!el)
270
+ return null;
271
+ const compEl = el.closest('[data-component-id]');
272
+ if (!compEl)
273
+ return null;
274
+ const compId = compEl.dataset.componentId;
275
+ if (!compId)
276
+ return null;
277
+ const f = flat.find(x => x.comp.id === compId);
278
+ if (!f)
279
+ return null;
280
+ return { compId: f.comp.id, bandId: f.bandId };
281
+ }, [flat]);
282
+ const findTableCellAtPoint = useCallback((clientX, clientY) => {
283
+ const el = elementFromPointSafe(clientX, clientY);
284
+ if (!el)
285
+ return null;
286
+ const cellEl = el.closest('[data-table-row][data-table-column]');
287
+ if (!cellEl)
288
+ return null;
289
+ const row = Number(cellEl.dataset.tableRow);
290
+ const column = Number(cellEl.dataset.tableColumn);
291
+ const tableId = cellEl.dataset.tableId;
292
+ const bandId = cellEl.dataset.bandId;
293
+ if (!Number.isInteger(row) || !Number.isInteger(column))
294
+ return null;
295
+ if (!tableId || !bandId)
296
+ return null;
297
+ return { tableId, bandId, row, column };
298
+ }, []);
299
+ const findTableCellResizeAtPoint = useCallback((clientX, clientY) => {
300
+ const el = elementFromPointSafe(clientX, clientY);
301
+ if (!el)
302
+ return null;
303
+ const handleEl = el.closest('[data-table-cell-resize]');
304
+ if (!handleEl)
305
+ return null;
306
+ const row = Number(handleEl.dataset.tableRow);
307
+ const column = Number(handleEl.dataset.tableColumn);
308
+ const width = Number(handleEl.dataset.cellWidth);
309
+ const tableId = handleEl.dataset.tableId;
310
+ const bandId = handleEl.dataset.bandId;
311
+ if (!Number.isInteger(row) || !Number.isInteger(column) || !Number.isFinite(width))
312
+ return null;
313
+ if (!tableId || !bandId)
314
+ return null;
315
+ return { tableId, bandId, row, column, width };
316
+ }, []);
317
+ // ---- Mouse down ----
318
+ const handlePageMouseDown = useCallback((e) => {
319
+ setContextMenu(null);
320
+ setBandContextMenu(null);
321
+ if (e.button === 2) {
322
+ // Right click context menu
323
+ const ch = findComponentAtPoint(e.clientX, e.clientY);
324
+ const tableCell = findTableCellAtPoint(e.clientX, e.clientY);
325
+ if (tableCell) {
326
+ const currentSelection = useDesignerStore.getState().selectedTableCell;
327
+ const isInsideCurrentSelection = currentSelection?.tableId === tableCell.tableId
328
+ && tableCell.row >= currentSelection.startRow
329
+ && tableCell.row <= currentSelection.endRow
330
+ && tableCell.column >= currentSelection.startColumn
331
+ && tableCell.column <= currentSelection.endColumn;
332
+ if (!isInsideCurrentSelection) {
333
+ selectTableCell({
334
+ tableId: tableCell.tableId,
335
+ bandId: tableCell.bandId,
336
+ startRow: tableCell.row,
337
+ startColumn: tableCell.column,
338
+ endRow: tableCell.row,
339
+ endColumn: tableCell.column,
340
+ });
341
+ }
342
+ }
343
+ else if (ch && !selectedComponentIds.includes(ch.compId)) {
344
+ selectComponents([ch.compId]);
345
+ }
346
+ if (pageRef.current) {
347
+ const rect = pageRef.current.getBoundingClientRect();
348
+ setContextMenu({ x: (e.clientX - rect.left) / zoom, y: (e.clientY - rect.top) / zoom, compId: ch?.compId, tableCell: tableCell ?? undefined });
349
+ }
350
+ return;
351
+ }
352
+ if (e.button !== 0)
353
+ return;
354
+ // 1. Resize handle
355
+ const tableResize = findTableCellResizeAtPoint(e.clientX, e.clientY);
356
+ if (tableResize) {
357
+ e.preventDefault();
358
+ e.stopPropagation();
359
+ selectComponents([tableResize.tableId]);
360
+ selectTableCell({
361
+ tableId: tableResize.tableId,
362
+ bandId: tableResize.bandId,
363
+ startRow: tableResize.row,
364
+ startColumn: tableResize.column,
365
+ endRow: tableResize.row,
366
+ endColumn: tableResize.column,
367
+ });
368
+ const m = {
369
+ type: 'table-cell-resize',
370
+ tableId: tableResize.tableId,
371
+ bandId: tableResize.bandId,
372
+ row: tableResize.row,
373
+ column: tableResize.column,
374
+ startX: e.clientX,
375
+ origWidth: tableResize.width,
376
+ };
377
+ modeRef.current = m;
378
+ setMode(m);
379
+ return;
380
+ }
381
+ // 1. Resize handle
382
+ const hr = findResizeHandleAtPoint(e.clientX, e.clientY);
383
+ if (hr && selectedComponentIds.includes(hr.compId)) {
384
+ e.preventDefault();
385
+ e.stopPropagation();
386
+ const f = flat.find(x => x.comp.id === hr.compId);
387
+ if (!f)
388
+ return;
389
+ const m = {
390
+ type: 'resize', compId: hr.compId, bandId: hr.bandId, handle: hr.handle,
391
+ startX: e.clientX, startY: e.clientY,
392
+ origX: f.comp.x, origY: f.comp.y, origW: f.comp.width, origH: f.comp.height,
393
+ };
394
+ modeRef.current = m;
395
+ setMode(m);
396
+ return;
397
+ }
398
+ // 2. Band resize handle
399
+ const br = findBandResizeAtPoint(e.clientX, e.clientY);
400
+ if (br) {
401
+ e.preventDefault();
402
+ e.stopPropagation();
403
+ const bp = bands.find(b => b.band.id === br.bandId);
404
+ if (!bp)
405
+ return;
406
+ const m = {
407
+ type: 'band-resize', bandId: br.bandId,
408
+ startY: e.clientY, origHeight: bp.band.height,
409
+ };
410
+ modeRef.current = m;
411
+ setMode(m);
412
+ return;
413
+ }
414
+ // 3. Component hit
415
+ const ch = findComponentAtPoint(e.clientX, e.clientY);
416
+ if (ch) {
417
+ e.preventDefault();
418
+ e.stopPropagation();
419
+ const tableCell = findTableCellAtPoint(e.clientX, e.clientY);
420
+ if (tableCell && ch.compId === tableCell.tableId) {
421
+ const previous = useDesignerStore.getState().selectedTableCell;
422
+ const nextSelection = e.shiftKey && previous?.tableId === tableCell.tableId
423
+ ? {
424
+ ...previous,
425
+ endRow: tableCell.row,
426
+ endColumn: tableCell.column,
427
+ }
428
+ : {
429
+ tableId: tableCell.tableId,
430
+ bandId: tableCell.bandId,
431
+ startRow: tableCell.row,
432
+ startColumn: tableCell.column,
433
+ endRow: tableCell.row,
434
+ endColumn: tableCell.column,
435
+ };
436
+ selectBand(null);
437
+ if (!selectedComponentIds.includes(ch.compId)) {
438
+ selectComponents([ch.compId]);
439
+ }
440
+ selectTableCell(nextSelection);
441
+ }
442
+ // Clear band selection when selecting a component
443
+ if (!tableCell || ch.compId !== tableCell.tableId) {
444
+ selectBand(null);
445
+ }
446
+ if (tableCell && ch.compId === tableCell.tableId) {
447
+ // Keep the cell selected, but still arm a normal component move so the whole table can be dragged.
448
+ }
449
+ else if (e.ctrlKey || e.metaKey) {
450
+ const cur = [...selectedComponentIds];
451
+ const idx = cur.indexOf(ch.compId);
452
+ if (idx >= 0)
453
+ cur.splice(idx, 1);
454
+ else
455
+ cur.push(ch.compId);
456
+ selectComponents(cur);
457
+ }
458
+ else if (!selectedComponentIds.includes(ch.compId)) {
459
+ selectComponents([ch.compId]);
460
+ }
461
+ // Get latest selection from store (zustand is sync, but React closure is stale)
462
+ const currentSelection = useDesignerStore.getState().selectedComponentIds;
463
+ const ids = currentSelection.length > 0 ? [...currentSelection] : [ch.compId];
464
+ const bandMap = {};
465
+ const origPositions = {};
466
+ for (const id of ids) {
467
+ const item = flat.find(x => x.comp.id === id);
468
+ if (item) {
469
+ bandMap[id] = item.bandId;
470
+ origPositions[id] = { x: item.comp.x, y: item.comp.y };
471
+ }
472
+ }
473
+ const m = {
474
+ type: 'move', compIds: ids, bandMap, origPositions,
475
+ startClientX: e.clientX, startClientY: e.clientY,
476
+ dragStarted: false,
477
+ };
478
+ modeRef.current = m;
479
+ setMode(m);
480
+ return;
481
+ }
482
+ // 4. Empty canvas = selection box
483
+ if (!pageRef.current)
484
+ return;
485
+ const rect = pageRef.current.getBoundingClientRect();
486
+ selectComponents([]);
487
+ selectBand(null);
488
+ const sx = (e.clientX - rect.left) / zoom;
489
+ const sy = (e.clientY - rect.top) / zoom;
490
+ const m = { type: 'select', startX: sx, startY: sy };
491
+ modeRef.current = m;
492
+ setMode(m);
493
+ setSelBox({ x: sx, y: sy, w: 0, h: 0 });
494
+ }, [flat, bands, selectedComponentIds, selectComponents, selectBand, selectTableCell, findComponentAtPoint, findTableCellAtPoint, findTableCellResizeAtPoint, findResizeHandleAtPoint, findBandResizeAtPoint, zoom]);
495
+ // ---- Global mouse move/up ----
496
+ useEffect(() => {
497
+ const handleMouseMove = (e) => {
498
+ const m = modeRef.current;
499
+ if (m.type === 'idle')
500
+ return;
501
+ if (m.type === 'move') {
502
+ const dxPx = e.clientX - m.startClientX;
503
+ const dyPx = e.clientY - m.startClientY;
504
+ const dist = Math.sqrt(dxPx * dxPx + dyPx * dyPx);
505
+ if (!m.dragStarted) {
506
+ if (dist < DRAG_THRESHOLD)
507
+ return; // 未超过阈值,不开始拖拽
508
+ // 超过阈值,激活拖拽模式
509
+ modeRef.current = { ...m, dragStarted: true };
510
+ }
511
+ const dxMm = pxToMm(dxPx, zoom);
512
+ const dyMm = pxToMm(dyPx, zoom);
513
+ // Move all selected components
514
+ const pageId = currentPageIdRef.current;
515
+ for (const compId of m.compIds) {
516
+ const orig = m.origPositions[compId];
517
+ if (!orig)
518
+ continue;
519
+ const currentBand = currentPage?.bands.find(band => band.id === m.bandMap[compId]);
520
+ const currentComponent = flatRef.current.find(item => item.comp.id === compId)?.comp;
521
+ const newX = clampComponentXToFirstColumn(currentPage, currentBand, orig.x + dxMm, currentComponent?.width ?? 0);
522
+ const newY = orig.y + dyMm;
523
+ moveComponentSilent(pageId, m.bandMap[compId], compId, newX, newY);
524
+ }
525
+ // 对齐引导线 (for the first selected component)
526
+ const firstId = m.compIds[0];
527
+ const firstOrig = m.origPositions[firstId];
528
+ const fc = flatRef.current.find(f => f.comp.id === firstId);
529
+ if (fc && firstOrig) {
530
+ const nextY = firstOrig.y + dyMm;
531
+ const targetBand = findBandAtComponentCenter(m.bandMap[firstId], nextY, fc.comp.height);
532
+ setComponentMoveTargetBandId(targetBand && targetBand.band.id !== m.bandMap[firstId] ? targetBand.band.id : null);
533
+ const g = computeGuides(firstId, firstOrig.x + dxMm, nextY, fc.comp.width, fc.comp.height, othersRef.current);
534
+ setGuides(g);
535
+ }
536
+ }
537
+ else if (m.type === 'resize') {
538
+ const dx = pxToMm(e.clientX - m.startX, zoom);
539
+ const dy = pxToMm(e.clientY - m.startY, zoom);
540
+ let nx = m.origX, ny = m.origY, nw = m.origW, nh = m.origH;
541
+ if (m.handle.includes('e'))
542
+ nw = Math.max(5, m.origW + dx);
543
+ if (m.handle.includes('w')) {
544
+ nw = Math.max(5, m.origW - dx);
545
+ nx = m.origX + dx;
546
+ }
547
+ if (m.handle.includes('s'))
548
+ nh = Math.max(5, m.origH + dy);
549
+ if (m.handle.includes('n')) {
550
+ nh = Math.max(5, m.origH - dy);
551
+ ny = m.origY + dy;
552
+ }
553
+ const resizeBand = currentPage?.bands.find(band => band.id === m.bandId);
554
+ const maxRight = getFirstColumnDesignWidth(currentPage, resizeBand);
555
+ if (maxRight !== undefined) {
556
+ nx = Math.max(0, Math.min(nx, maxRight - 5));
557
+ nw = Math.max(5, Math.min(nw, maxRight - nx));
558
+ }
559
+ updateComponentSilent(currentPageIdRef.current, m.bandId, m.compId, { x: nx, y: ny, width: nw, height: nh });
560
+ }
561
+ else if (m.type === 'table-cell-resize') {
562
+ // Width is committed on mouseup to keep the operation a single undoable edit.
563
+ }
564
+ else if (m.type === 'band-resize') {
565
+ const dy = pxToMm(e.clientY - m.startY, zoom);
566
+ const nh = Math.max(5, Math.round((m.origHeight + dy) * 10) / 10);
567
+ resizeBandSilent(currentPageIdRef.current, m.bandId, nh);
568
+ }
569
+ else if (m.type === 'band-sort') {
570
+ const dyPx = Math.abs(e.clientY - m.startClientY);
571
+ if (!m.dragStarted && dyPx < DRAG_THRESHOLD)
572
+ return;
573
+ const pointerContentY = getPointerContentY(e.clientY);
574
+ const targetIndex = getBandReorderTargetIndex(bandsRef.current, m.bandId, pointerContentY);
575
+ const nextMode = { ...m, targetIndex, dragStarted: true };
576
+ modeRef.current = nextMode;
577
+ setMode(nextMode);
578
+ setBandReorderTarget({ bandId: m.bandId, targetIndex });
579
+ setBandDragPreview({
580
+ bandId: m.bandId,
581
+ top: m.startVisualTop + pointerContentY - m.startPointerContentY,
582
+ });
583
+ }
584
+ else if (m.type === 'select') {
585
+ if (!pageRef.current)
586
+ return;
587
+ const rect = pageRef.current.getBoundingClientRect();
588
+ const cx = (e.clientX - rect.left) / zoom;
589
+ const cy = (e.clientY - rect.top) / zoom;
590
+ setSelBox({
591
+ x: Math.min(m.startX, cx), y: Math.min(m.startY, cy),
592
+ w: Math.abs(cx - m.startX), h: Math.abs(cy - m.startY),
593
+ });
594
+ }
595
+ };
596
+ const handleMouseUp = (e) => {
597
+ const m = modeRef.current;
598
+ if (m.type === 'select' && selBoxRef.current) {
599
+ const page = useDesignerStore.getState().template.pages.find(p => p.id === currentPageIdRef.current);
600
+ if (page) {
601
+ const ids = [];
602
+ let visualY = 0;
603
+ const marginLeft = mmToPx(page.margins?.left ?? 0);
604
+ const marginTop = mmToPx(page.margins?.top ?? 0);
605
+ for (const band of page.bands) {
606
+ for (const comp of band.components) {
607
+ const l = marginLeft + mmToPx(comp.x);
608
+ const t = marginTop + mmToPx(visualY) + mmToPx(BAND_HEADER_MM) + mmToPx(comp.y);
609
+ const r = l + mmToPx(comp.width), b = t + mmToPx(comp.height);
610
+ if (selBoxRef.current.x < r && selBoxRef.current.x + selBoxRef.current.w > l && selBoxRef.current.y < b && selBoxRef.current.y + selBoxRef.current.h > t) {
611
+ ids.push(comp.id);
612
+ }
613
+ }
614
+ visualY += band.height + BAND_HEADER_MM;
615
+ }
616
+ if (ids.length > 0)
617
+ selectComponents(ids);
618
+ }
619
+ }
620
+ else if (m.type === 'move') {
621
+ const state = useDesignerStore.getState();
622
+ const page = state.template.pages.find(p => p.id === currentPageIdRef.current);
623
+ for (const compId of m.compIds) {
624
+ const band = page?.bands.find(b => b.id === m.bandMap[compId]);
625
+ const comp = band?.components.find(c => c.id === compId);
626
+ const orig = m.origPositions[compId];
627
+ if (comp && orig && (comp.x !== orig.x || comp.y !== orig.y)) {
628
+ const targetBand = findBandAtComponentCenter(m.bandMap[compId], comp.y, comp.height);
629
+ if (targetBand && targetBand.band.id !== m.bandMap[compId]) {
630
+ const targetY = convertComponentYToBand(m.bandMap[compId], targetBand.band.id, comp.y);
631
+ const targetX = clampComponentXToFirstColumn(page, targetBand.band, comp.x, comp.width);
632
+ moveComponentToBand(currentPageIdRef.current, m.bandMap[compId], targetBand.band.id, compId, targetX, targetY, orig.x, orig.y);
633
+ }
634
+ else {
635
+ const targetX = clampComponentXToFirstColumn(page, band, comp.x, comp.width);
636
+ moveComponent(currentPageIdRef.current, m.bandMap[compId], compId, targetX, comp.y, orig.x, orig.y);
637
+ }
638
+ }
639
+ }
640
+ }
641
+ else if (m.type === 'resize') {
642
+ const state = useDesignerStore.getState();
643
+ const page = state.template.pages.find(p => p.id === currentPageIdRef.current);
644
+ const band = page?.bands.find(b => b.id === m.bandId);
645
+ const comp = band?.components.find(c => c.id === m.compId);
646
+ if (comp) {
647
+ updateComponent(currentPageIdRef.current, m.bandId, m.compId, { x: comp.x, y: comp.y, width: comp.width, height: comp.height }, { x: m.origX, y: m.origY, width: m.origW, height: m.origH });
648
+ }
649
+ }
650
+ else if (m.type === 'table-cell-resize') {
651
+ const width = Math.max(1, Math.round((m.origWidth + pxToMm(e.clientX - m.startX, zoom)) * 10) / 10);
652
+ setSelectedTableCellWidth(m.row, m.column, width);
653
+ }
654
+ else if (m.type === 'band-resize') {
655
+ const state = useDesignerStore.getState();
656
+ const page = state.template.pages.find(p => p.id === currentPageIdRef.current);
657
+ const band = page?.bands.find(b => b.id === m.bandId);
658
+ if (band) {
659
+ if (band.height !== m.origHeight) {
660
+ resizeBand(currentPageIdRef.current, m.bandId, band.height, m.origHeight);
661
+ }
662
+ }
663
+ }
664
+ else if (m.type === 'band-sort') {
665
+ if (m.dragStarted && m.targetIndex !== m.fromIndex) {
666
+ moveBand(currentPageIdRef.current, m.bandId, m.targetIndex);
667
+ }
668
+ }
669
+ modeRef.current = { type: 'idle' };
670
+ setMode({ type: 'idle' });
671
+ setSelBox(null);
672
+ setGuides([]);
673
+ setBandReorderTarget(null);
674
+ setBandDragPreview(null);
675
+ setComponentMoveTargetBandId(null);
676
+ };
677
+ window.addEventListener('mousemove', handleMouseMove);
678
+ window.addEventListener('mouseup', handleMouseUp);
679
+ return () => {
680
+ window.removeEventListener('mousemove', handleMouseMove);
681
+ window.removeEventListener('mouseup', handleMouseUp);
682
+ };
683
+ }, [moveComponent, moveComponentToBand, updateComponent, resizeBand, moveBand, selectComponents, setSelectedTableCellWidth, getPointerContentY, findBandAtComponentCenter, convertComponentYToBand, zoom]);
684
+ // ---- Keyboard shortcuts ----
685
+ useEffect(() => {
686
+ const handleKeyDown = (e) => {
687
+ // 忽略编辑模式
688
+ if (isEditableKeyboardTarget(e.target))
689
+ return;
690
+ const isCtrl = e.ctrlKey || e.metaKey;
691
+ // Delete: 删除
692
+ if (e.key === 'Delete') {
693
+ e.preventDefault();
694
+ if (selectedTableCell) {
695
+ clearSelectedTableCell(selectedTableCell.startRow, selectedTableCell.startColumn);
696
+ return;
697
+ }
698
+ deleteSelected();
699
+ return;
700
+ }
701
+ // Ctrl+C: 复制
702
+ if (isCtrl && e.key === 'c') {
703
+ e.preventDefault();
704
+ copySelected();
705
+ return;
706
+ }
707
+ // Ctrl+X: 剪切
708
+ if (isCtrl && e.key === 'x') {
709
+ e.preventDefault();
710
+ cutSelected();
711
+ return;
712
+ }
713
+ // Ctrl+V: 粘贴
714
+ if (isCtrl && e.key === 'v') {
715
+ e.preventDefault();
716
+ pasteClipboard();
717
+ return;
718
+ }
719
+ // Ctrl+D: 复制一份
720
+ if (isCtrl && e.key === 'd') {
721
+ e.preventDefault();
722
+ duplicateSelected();
723
+ return;
724
+ }
725
+ // Ctrl+Z: 撤销
726
+ if (isCtrl && e.key === 'z') {
727
+ e.preventDefault();
728
+ undo();
729
+ return;
730
+ }
731
+ // Ctrl+Y / Ctrl+Shift+Z: 重做
732
+ if (isCtrl && e.key === 'y') {
733
+ e.preventDefault();
734
+ redo();
735
+ return;
736
+ }
737
+ if (e.key === 'Escape' && useDesignerStore.getState().pendingBandInsertType) {
738
+ e.preventDefault();
739
+ cancelBandInsert();
740
+ return;
741
+ }
742
+ if (isCtrl && e.shiftKey && e.key === 'Z') {
743
+ e.preventDefault();
744
+ redo();
745
+ return;
746
+ }
747
+ // Ctrl+A: 全选
748
+ if (isCtrl && e.key === 'a') {
749
+ e.preventDefault();
750
+ selectComponents(flat.map(f => f.comp.id));
751
+ return;
752
+ }
753
+ // F5 / Ctrl+F5: 预览
754
+ if (e.key === 'F5') {
755
+ e.preventDefault();
756
+ setDesignerMode('preview');
757
+ return;
758
+ }
759
+ // Ctrl+B/I/U/S: 字体样式
760
+ if (isCtrl && ['b', 'i', 'u', 's'].includes(e.key.toLowerCase())) {
761
+ e.preventDefault();
762
+ const key = e.key.toLowerCase();
763
+ if (key === 'b')
764
+ toggleSelectedFontStyle('bold');
765
+ if (key === 'i')
766
+ toggleSelectedFontStyle('italic');
767
+ if (key === 'u')
768
+ toggleSelectedFontStyle('underline');
769
+ if (key === 's')
770
+ toggleSelectedFontStyle('strikethrough');
771
+ return;
772
+ }
773
+ // Ctrl+L/E/R: 文本左/中/右对齐
774
+ if (isCtrl && ['l', 'e', 'r'].includes(e.key.toLowerCase())) {
775
+ e.preventDefault();
776
+ const key = e.key.toLowerCase();
777
+ if (key === 'l')
778
+ setTextAlign('left');
779
+ if (key === 'e')
780
+ setTextAlign('center');
781
+ if (key === 'r')
782
+ setTextAlign('right');
783
+ return;
784
+ }
785
+ // Ctrl+Shift+Arrow: 对齐
786
+ if (isCtrl && e.shiftKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && selectedComponentIds.length >= 2) {
787
+ e.preventDefault();
788
+ if (e.key === 'ArrowLeft')
789
+ useDesignerStore.getState().alignComponents('left');
790
+ if (e.key === 'ArrowRight')
791
+ useDesignerStore.getState().alignComponents('right');
792
+ if (e.key === 'ArrowUp')
793
+ useDesignerStore.getState().alignComponents('top');
794
+ if (e.key === 'ArrowDown')
795
+ useDesignerStore.getState().alignComponents('bottom');
796
+ return;
797
+ }
798
+ // Ctrl+Alt+ArrowUp/Down: 置顶/置底
799
+ if (isCtrl && e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown') && selectedComponentIds.length > 0) {
800
+ e.preventDefault();
801
+ if (e.key === 'ArrowUp')
802
+ useDesignerStore.getState().bringToFront();
803
+ if (e.key === 'ArrowDown')
804
+ useDesignerStore.getState().sendToBack();
805
+ return;
806
+ }
807
+ // Arrow keys: 微移
808
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && selectedComponentIds.length > 0) {
809
+ e.preventDefault();
810
+ const step = e.altKey ? 0.5 : e.ctrlKey || e.metaKey ? GRID_MM : 1;
811
+ const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0;
812
+ const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0;
813
+ if (e.shiftKey) {
814
+ resizeSelectedBy(dx, dy);
815
+ }
816
+ else {
817
+ moveSelectedBy(dx, dy);
818
+ }
819
+ }
820
+ };
821
+ window.addEventListener('keydown', handleKeyDown);
822
+ return () => window.removeEventListener('keydown', handleKeyDown);
823
+ }, [selectedComponentIds, selectedTableCell, flat, undo, redo, selectComponents, copySelected, cutSelected, duplicateSelected, pasteClipboard, deleteSelected, clearSelectedTableCell, moveSelectedBy, resizeSelectedBy, toggleSelectedFontStyle, setTextAlign, setDesignerMode, cancelBandInsert]);
824
+ // ---- Zoom wheel handler ----
825
+ const containerRef = useRef(null);
826
+ useEffect(() => {
827
+ const el = containerRef.current;
828
+ if (!el)
829
+ return;
830
+ const handleWheel = (e) => {
831
+ if (e.ctrlKey || e.metaKey) {
832
+ e.preventDefault();
833
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
834
+ const next = Math.min(4, Math.max(0.25, zoom + delta));
835
+ setZoom(Math.round(next * 100) / 100);
836
+ }
837
+ };
838
+ el.addEventListener('wheel', handleWheel, { passive: false });
839
+ return () => el.removeEventListener('wheel', handleWheel);
840
+ }, [setZoom, zoom]);
841
+ // ---- Click outside to close context menu ----
842
+ useEffect(() => {
843
+ if (!contextMenu && !bandContextMenu)
844
+ return;
845
+ const close = () => {
846
+ setContextMenu(null);
847
+ setBandContextMenu(null);
848
+ };
849
+ window.addEventListener('click', close);
850
+ return () => window.removeEventListener('click', close);
851
+ }, [bandContextMenu, contextMenu]);
852
+ const getDropPosition = useCallback((event) => {
853
+ if (!pageRef.current)
854
+ return null;
855
+ const rect = pageRef.current.getBoundingClientRect();
856
+ const clientX = Number.isFinite(event.clientX) ? event.clientX : rect.left;
857
+ const clientY = Number.isFinite(event.clientY) ? event.clientY : rect.top;
858
+ const pageX = pxToMm(clientX - rect.left, zoom);
859
+ const pageY = pxToMm(clientY - rect.top, zoom);
860
+ const margins = currentPage?.margins ?? { top: 0, right: 0, bottom: 0, left: 0 };
861
+ const printableWidth = currentPage ? Math.max(0, currentPage.width - margins.left - margins.right) : 0;
862
+ const xMm = Math.max(0, Math.min(printableWidth, Math.round((pageX - margins.left) * 10) / 10));
863
+ const yMm = Math.round((pageY - margins.top) * 10) / 10;
864
+ for (const { band, visualY } of bands) {
865
+ const bandTop = visualY;
866
+ const bodyTop = visualY + BAND_HEADER_MM;
867
+ const bandBottom = bodyTop + band.height;
868
+ if (yMm >= bandTop && yMm <= bandBottom) {
869
+ const bandX = clampComponentXToFirstColumn(currentPage, band, xMm, 0);
870
+ const bandY = Math.max(0, Math.min(band.height, Math.round((yMm - bodyTop) * 10) / 10));
871
+ const panelTarget = findPanelDropTarget(band, bandX, bandY);
872
+ if (panelTarget) {
873
+ return {
874
+ targetBandId: band.id,
875
+ targetPanelId: panelTarget.panelId,
876
+ xMm: panelTarget.xMm,
877
+ yMm: panelTarget.yMm,
878
+ };
879
+ }
880
+ return {
881
+ targetBandId: band.id,
882
+ xMm: bandX,
883
+ yMm: bandY,
884
+ };
885
+ }
886
+ }
887
+ const fallbackBand = currentPage?.bands.find(band => band.type === 'data') ?? currentPage?.bands[0];
888
+ return fallbackBand ? { targetBandId: fallbackBand.id, xMm, yMm: 0 } : null;
889
+ }, [bands, currentPage?.bands, zoom]);
890
+ const handleCanvasDragOver = useCallback((event) => {
891
+ const hasSupportedPayload = hasDragPayload(event, 'componentType') || hasDragPayload(event, 'fieldBinding') || hasDragPayload(event, 'expressionBinding');
892
+ if (!hasSupportedPayload)
893
+ return;
894
+ event.preventDefault();
895
+ event.dataTransfer.dropEffect = 'copy';
896
+ }, []);
897
+ const handleCanvasDrop = useCallback((event) => {
898
+ const position = getDropPosition(event);
899
+ if (!position || !currentPageId)
900
+ return;
901
+ const fieldBinding = getDragData(event, 'fieldBinding');
902
+ const expressionBinding = getDragData(event, 'expressionBinding');
903
+ const componentType = getDragData(event, 'componentType');
904
+ if (!fieldBinding && !expressionBinding && !componentType)
905
+ return;
906
+ event.preventDefault();
907
+ const tableCellTarget = findTableCellAtPoint(event.clientX, event.clientY);
908
+ if (fieldBinding) {
909
+ try {
910
+ const field = JSON.parse(fieldBinding);
911
+ if (tableCellTarget) {
912
+ const expression = formatDataFieldExpression(field.dataSourceId, field.fieldName);
913
+ const state = useDesignerStore.getState();
914
+ state.selectComponents([tableCellTarget.tableId]);
915
+ state.selectTableCell({
916
+ tableId: tableCellTarget.tableId,
917
+ bandId: tableCellTarget.bandId,
918
+ startRow: tableCellTarget.row,
919
+ startColumn: tableCellTarget.column,
920
+ endRow: tableCellTarget.row,
921
+ endColumn: tableCellTarget.column,
922
+ });
923
+ state.updateSelectedTableCell({ text: expression });
924
+ return;
925
+ }
926
+ const component = createFieldExpressionComponent(field, position.xMm, position.yMm);
927
+ if ('targetPanelId' in position && position.targetPanelId) {
928
+ addComponentToPanel(currentPageId, position.targetBandId, position.targetPanelId, component);
929
+ }
930
+ else {
931
+ addComponent(currentPageId, position.targetBandId, component);
932
+ }
933
+ return;
934
+ }
935
+ catch {
936
+ return;
937
+ }
938
+ }
939
+ if (expressionBinding) {
940
+ if (tableCellTarget) {
941
+ const state = useDesignerStore.getState();
942
+ state.selectComponents([tableCellTarget.tableId]);
943
+ state.selectTableCell({
944
+ tableId: tableCellTarget.tableId,
945
+ bandId: tableCellTarget.bandId,
946
+ startRow: tableCellTarget.row,
947
+ startColumn: tableCellTarget.column,
948
+ endRow: tableCellTarget.row,
949
+ endColumn: tableCellTarget.column,
950
+ });
951
+ state.updateSelectedTableCell({ text: expressionBinding });
952
+ return;
953
+ }
954
+ const component = createTextExpressionComponent(expressionBinding, position.xMm, position.yMm);
955
+ if ('targetPanelId' in position && position.targetPanelId) {
956
+ addComponentToPanel(currentPageId, position.targetBandId, position.targetPanelId, component);
957
+ }
958
+ else {
959
+ addComponent(currentPageId, position.targetBandId, component);
960
+ }
961
+ return;
962
+ }
963
+ const component = createDefaultComponent(componentType, position.xMm, position.yMm);
964
+ if ('targetPanelId' in position && position.targetPanelId) {
965
+ addComponentToPanel(currentPageId, position.targetBandId, position.targetPanelId, component);
966
+ }
967
+ else {
968
+ addComponent(currentPageId, position.targetBandId, component);
969
+ }
970
+ }, [addComponent, addComponentToPanel, currentPageId, findTableCellAtPoint, getDropPosition]);
971
+ const handlePageMouseMove = useCallback((event) => {
972
+ if (!pendingBandInsertType || !pageRef.current)
973
+ return;
974
+ const rect = pageRef.current.getBoundingClientRect();
975
+ setBandInsertPointer({
976
+ x: (event.clientX - rect.left) / zoom,
977
+ y: (event.clientY - rect.top) / zoom,
978
+ });
979
+ }, [pendingBandInsertType, zoom]);
980
+ const handleStartBandSort = useCallback((bandId, event) => {
981
+ if (pendingBandInsertType)
982
+ return;
983
+ const fromIndex = bands.findIndex(item => item.band.id === bandId);
984
+ if (fromIndex < 0)
985
+ return;
986
+ const bandLayout = bands[fromIndex];
987
+ const startPointerContentY = getPointerContentY(event.clientY);
988
+ const targetIndex = getBandReorderTargetIndex(bands, bandId, getPointerContentY(event.clientY));
989
+ const nextMode = {
990
+ type: 'band-sort',
991
+ bandId,
992
+ startClientY: event.clientY,
993
+ startPointerContentY,
994
+ startVisualTop: mmToPx(bandLayout.visualY),
995
+ fromIndex,
996
+ targetIndex,
997
+ dragStarted: false,
998
+ };
999
+ modeRef.current = nextMode;
1000
+ setMode(nextMode);
1001
+ setBandReorderTarget(null);
1002
+ }, [bands, getPointerContentY, pendingBandInsertType]);
1003
+ const handleBandContextMenu = useCallback((bandId, event) => {
1004
+ event.preventDefault();
1005
+ event.stopPropagation();
1006
+ if (!pageRef.current)
1007
+ return;
1008
+ const rect = pageRef.current.getBoundingClientRect();
1009
+ selectComponents([]);
1010
+ selectBand(bandId);
1011
+ setContextMenu(null);
1012
+ setBandContextMenu({
1013
+ bandId,
1014
+ x: (event.clientX - rect.left) / zoom,
1015
+ y: (event.clientY - rect.top) / zoom,
1016
+ });
1017
+ }, [selectBand, selectComponents, zoom]);
1018
+ if (!currentPage) {
1019
+ return (_jsx("div", { className: className, style: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#999' }, children: t('canvas.noPageSelected') }));
1020
+ }
1021
+ const isBusy = mode.type !== 'idle';
1022
+ const gridPx = mmToPx(GRID_MM);
1023
+ const rawPageWidthPx = mmToPx(currentPage.width);
1024
+ const rawPageHeightPx = mmToPx(currentPage.height);
1025
+ const scaledPageWidthPx = Math.round(rawPageWidthPx * zoom);
1026
+ const scaledPageHeightPx = Math.round(rawPageHeightPx * zoom);
1027
+ const margins = currentPage.margins ?? { top: 0, right: 0, bottom: 0, left: 0 };
1028
+ const rawMarginLeftPx = mmToPx(margins.left);
1029
+ const rawMarginTopPx = mmToPx(margins.top);
1030
+ const rawMarginRightPx = mmToPx(margins.right);
1031
+ const rawMarginBottomPx = mmToPx(margins.bottom);
1032
+ const scaledMarginLeftPx = Math.round(rawMarginLeftPx * zoom);
1033
+ const scaledMarginTopPx = Math.round(rawMarginTopPx * zoom);
1034
+ const printableWidthMm = Math.max(0, currentPage.width - margins.left - margins.right);
1035
+ const printableHeightMm = Math.max(0, currentPage.height - margins.top - margins.bottom);
1036
+ const rawPrintableWidthPx = mmToPx(printableWidthMm);
1037
+ const rawPrintableHeightPx = mmToPx(printableHeightMm);
1038
+ const bandReorderLineTop = bandReorderTarget
1039
+ ? getBandReorderLineTop(bands, bandReorderTarget.bandId, bandReorderTarget.targetIndex)
1040
+ : null;
1041
+ const bandDragPreviewLayout = bandDragPreview
1042
+ ? bands.find(item => item.band.id === bandDragPreview.bandId)
1043
+ : undefined;
1044
+ return (_jsxs("div", { ref: containerRef, className: className, style: { overflow: 'hidden', backgroundColor: '#e8e8e8', height: '100%', userSelect: isBusy ? 'none' : 'auto', position: 'relative' }, children: [_jsx("div", { "data-testid": "designer-canvas-viewport", style: { overflowX: 'auto', overflowY: 'auto', height: '100%', padding: '0 24px 24px 0', display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-start', position: 'relative' }, children: _jsxs("div", { "data-testid": "designer-canvas-page-stack", style: { position: 'relative', width: safeCssNumber(scaledPageWidthPx + RULER_SIZE), height: safeCssNumber(scaledPageHeightPx + RULER_SIZE), margin: 0 }, children: [_jsx(Ruler, { direction: "horizontal", lengthMm: printableWidthMm, lengthPx: scaledPageWidthPx, printableOffsetPx: scaledMarginLeftPx, offsetPx: RULER_SIZE, crossOffsetPx: 0, zoom: zoom }), _jsx(Ruler, { direction: "vertical", lengthMm: printableHeightMm, lengthPx: scaledPageHeightPx, printableOffsetPx: scaledMarginTopPx, offsetPx: RULER_SIZE, crossOffsetPx: 0, zoom: zoom }), _jsxs("div", { ref: pageRef, "data-page": true, "data-testid": "designer-page-sheet", onMouseDown: handlePageMouseDown, onMouseMove: handlePageMouseMove, onDrop: handleCanvasDrop, onDragOver: handleCanvasDragOver, onContextMenu: (e) => e.preventDefault(), style: {
1045
+ width: safeCssNumber(rawPageWidthPx), height: safeCssNumber(rawPageHeightPx),
1046
+ backgroundColor: currentPage.backgroundColor ?? '#ffffff', boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
1047
+ position: 'relative', marginLeft: RULER_SIZE, marginTop: RULER_SIZE, overflow: 'hidden',
1048
+ transform: `scale(${zoom})`,
1049
+ transformOrigin: 'top left',
1050
+ cursor: pendingBandInsertType ? 'copy' : undefined,
1051
+ }, children: [_jsx(PageWatermarkOverlay, { watermark: currentPage.watermark, zIndex: currentPage.watermark?.showBehind === false ? 20 : 0 }), _jsxs("div", { "data-testid": "designer-page-content-area", style: {
1052
+ position: 'absolute',
1053
+ left: rawMarginLeftPx,
1054
+ top: rawMarginTopPx,
1055
+ width: safeCssNumber(rawPrintableWidthPx),
1056
+ height: safeCssNumber(rawPrintableHeightPx),
1057
+ backgroundImage: `
1058
+ linear-gradient(rgba(0,0,0,0.06) 1px, transparent 1px),
1059
+ linear-gradient(90deg, rgba(0,0,0,0.06) 1px, transparent 1px)
1060
+ `,
1061
+ backgroundSize: `${gridPx}px ${gridPx}px`,
1062
+ overflow: 'visible',
1063
+ }, children: [guides.map((g, i) => {
1064
+ const selectedFlat = flat.find(f => f.comp.id === selectedComponentIds[0]);
1065
+ return (_jsx("div", { style: {
1066
+ position: 'absolute',
1067
+ ...(g.type === 'horizontal' ? { left: 0, right: 0, top: safeCssNumber(mmToPx(selectedFlat?.visualY ?? 0) + mmToPx(BAND_HEADER_MM) + g.position - mmToPx((selectedFlat?.comp.y ?? 0))), height: 1 } : { top: 0, bottom: 0, left: safeCssNumber(g.position), width: 1 }),
1068
+ backgroundColor: '#ff4d4f', zIndex: 9998, pointerEvents: 'none',
1069
+ } }, i));
1070
+ }), bands.map(({ band, visualY }) => (_jsx(BandView, { band: band, visualY: visualY, page: currentPage, labelIndex: bandLabelIndexes[band.id] ?? 1, isSelected: band.id === selectedBandId, pendingBandInsertType: pendingBandInsertType, selectedIds: selectedComponentIds, selectedTableCell: selectedTableCell, fonts: template.fonts, isDragging: bandDragPreview?.bandId === band.id, isComponentMoveTarget: componentMoveTargetBandId === band.id, onOpenContextMenu: handleBandContextMenu, onStartBandSort: handleStartBandSort, onUpdateComponent: updateComponent, currentPageId: currentPageId }, band.id))), bandDragPreview && bandDragPreviewLayout ? (_jsx(BandDragPreview, { band: bandDragPreviewLayout.band, labelIndex: bandLabelIndexes[bandDragPreviewLayout.band.id] ?? 1, top: bandDragPreview.top })) : null, bandReorderLineTop !== null ? (_jsx("div", { "data-testid": "designer-band-reorder-line", style: {
1071
+ position: 'absolute',
1072
+ left: 0,
1073
+ right: 0,
1074
+ top: safeCssNumber(bandReorderLineTop),
1075
+ height: 2,
1076
+ backgroundColor: '#1677ff',
1077
+ boxShadow: '0 0 0 1px rgba(22,119,255,0.16)',
1078
+ pointerEvents: 'none',
1079
+ zIndex: 9997,
1080
+ } })) : null] }), pendingBandInsertType ? (_jsx(BandInsertCursor, { type: pendingBandInsertType, label: t(BAND_LABEL_KEYS[pendingBandInsertType]), pointer: bandInsertPointer })) : null, _jsx(PageBorderOverlay, { pageBorder: currentPage.pageBorder }), mode.type === 'select' && selBox && (_jsx("div", { style: {
1081
+ position: 'absolute', left: safeCssNumber(selBox.x), top: safeCssNumber(selBox.y),
1082
+ width: safeCssNumber(selBox.w), height: safeCssNumber(selBox.h),
1083
+ border: '1px dashed #1890ff', backgroundColor: 'rgba(24,144,255,0.08)',
1084
+ pointerEvents: 'none', zIndex: 9999,
1085
+ } })), contextMenu && (_jsx(ContextMenu, { x: contextMenu.x, y: contextMenu.y, hasSelection: selectedComponentIds.length > 0, hasClipboard: storeClipboard.length > 0, selectedType: flat.find(f => f.comp.id === (contextMenu.compId ?? selectedComponentIds[0]))?.comp.type, tableCell: contextMenu.tableCell, onCopy: () => { copySelected(); setContextMenu(null); }, onCut: () => { cutSelected(); setContextMenu(null); }, onPaste: () => { pasteClipboard(); setContextMenu(null); }, onDuplicate: () => { duplicateSelected(); setContextMenu(null); }, onBringToFront: () => { useDesignerStore.getState().bringToFront(); setContextMenu(null); }, onSendToBack: () => { useDesignerStore.getState().sendToBack(); setContextMenu(null); }, onInsertTableColumnLeft: () => { useDesignerStore.getState().insertSelectedTableColumn((contextMenu.tableCell?.column ?? 0) - 1); setContextMenu(null); }, onInsertTableColumnRight: () => { useDesignerStore.getState().insertSelectedTableColumn(contextMenu.tableCell?.column); setContextMenu(null); }, onDeleteTableColumn: () => { useDesignerStore.getState().deleteSelectedTableColumn(contextMenu.tableCell?.column); setContextMenu(null); }, onInsertTableRowAbove: () => { useDesignerStore.getState().insertSelectedTableRow((contextMenu.tableCell?.row ?? 0) - 1); setContextMenu(null); }, onInsertTableRowBelow: () => { useDesignerStore.getState().insertSelectedTableRow(contextMenu.tableCell?.row); setContextMenu(null); }, onDeleteTableRow: () => { useDesignerStore.getState().deleteSelectedTableRow(contextMenu.tableCell?.row); setContextMenu(null); }, onMergeTableCellRight: () => {
1086
+ if (contextMenu.tableCell) {
1087
+ useDesignerStore.getState().mergeSelectedTableCellRight(contextMenu.tableCell.row, contextMenu.tableCell.column);
1088
+ }
1089
+ setContextMenu(null);
1090
+ }, onMergeSelectedTableCells: () => {
1091
+ useDesignerStore.getState().mergeSelectedTableCellRange();
1092
+ setContextMenu(null);
1093
+ }, onSplitTableCell: () => {
1094
+ if (contextMenu.tableCell) {
1095
+ useDesignerStore.getState().splitSelectedTableCell(contextMenu.tableCell.row, contextMenu.tableCell.column);
1096
+ }
1097
+ setContextMenu(null);
1098
+ }, onClearTableCell: () => {
1099
+ if (contextMenu.tableCell) {
1100
+ useDesignerStore.getState().clearSelectedTableCell(contextMenu.tableCell.row, contextMenu.tableCell.column);
1101
+ }
1102
+ setContextMenu(null);
1103
+ }, onClearTableCellStyle: () => {
1104
+ if (contextMenu.tableCell) {
1105
+ useDesignerStore.getState().clearSelectedTableCellStyle(contextMenu.tableCell.row, contextMenu.tableCell.column);
1106
+ }
1107
+ setContextMenu(null);
1108
+ }, onCopyTableCellStyle: () => {
1109
+ if (contextMenu.tableCell) {
1110
+ useDesignerStore.getState().copySelectedTableCellStyle(contextMenu.tableCell.row, contextMenu.tableCell.column);
1111
+ }
1112
+ setContextMenu(null);
1113
+ }, onPasteTableCellStyle: () => {
1114
+ if (contextMenu.tableCell) {
1115
+ useDesignerStore.getState().pasteSelectedTableCellStyle(contextMenu.tableCell.row, contextMenu.tableCell.column);
1116
+ }
1117
+ setContextMenu(null);
1118
+ }, onEqualizeTableColumns: () => { useDesignerStore.getState().equalizeSelectedTableColumns(); setContextMenu(null); }, onEqualizeTableRows: () => { useDesignerStore.getState().equalizeSelectedTableRows(); setContextMenu(null); }, onToggleTableBorder: () => {
1119
+ const table = flat.find(f => f.comp.id === (contextMenu.compId ?? selectedComponentIds[0]))?.comp;
1120
+ if (table?.type === 'table')
1121
+ useDesignerStore.getState().updateSelectedTable({ showBorder: !table.showBorder });
1122
+ setContextMenu(null);
1123
+ }, onDelete: () => {
1124
+ deleteSelected();
1125
+ setContextMenu(null);
1126
+ } })), bandContextMenu && (_jsx(BandContextMenu, { x: bandContextMenu.x, y: bandContextMenu.y, onCopy: () => {
1127
+ useDesignerStore.getState().duplicateBandAfter(currentPageId, bandContextMenu.bandId);
1128
+ setBandContextMenu(null);
1129
+ }, onDelete: () => {
1130
+ useDesignerStore.getState().deleteBand(currentPageId, bandContextMenu.bandId);
1131
+ setBandContextMenu(null);
1132
+ } }))] })] }) }), _jsx(ZoomBar, { zoom: zoom, onZoomIn: () => setZoom(Math.min(4, Math.round((zoom + 0.1) * 100) / 100)), onZoomOut: () => setZoom(Math.max(0.25, Math.round((zoom - 0.1) * 100) / 100)), onReset: () => setZoom(1), onSetZoom: (z) => setZoom(z) })] }));
1133
+ };
1134
+ const BandInsertCursor = ({ label, pointer, type }) => {
1135
+ const color = BAND_COLORS[type] || '#2563eb';
1136
+ const x = safeCssNumber((pointer?.x ?? 16) + 12);
1137
+ const y = safeCssNumber((pointer?.y ?? 16) + 12);
1138
+ return (_jsxs("div", { "data-testid": "designer-band-insert-cursor", style: {
1139
+ position: 'absolute',
1140
+ left: x,
1141
+ top: y,
1142
+ zIndex: 10001,
1143
+ pointerEvents: 'none',
1144
+ display: 'inline-flex',
1145
+ alignItems: 'center',
1146
+ gap: 6,
1147
+ padding: '3px 8px',
1148
+ border: `1px solid ${color}`,
1149
+ backgroundColor: '#ffffff',
1150
+ color: '#111827',
1151
+ borderRadius: 3,
1152
+ boxShadow: '0 2px 8px rgba(15,23,42,0.16)',
1153
+ fontSize: 12,
1154
+ lineHeight: 1.2,
1155
+ whiteSpace: 'nowrap',
1156
+ }, children: [_jsx("span", { "aria-hidden": true, style: {
1157
+ width: 14,
1158
+ height: 10,
1159
+ border: `1px solid ${color}`,
1160
+ borderTopWidth: 3,
1161
+ display: 'inline-block',
1162
+ boxSizing: 'border-box',
1163
+ background: `${color}14`,
1164
+ } }), _jsx("span", { children: label })] }));
1165
+ };
1166
+ // ---- Ruler Component ----
1167
+ const Ruler = ({ direction, lengthMm, lengthPx, printableOffsetPx, offsetPx, crossOffsetPx, zoom }) => {
1168
+ const isHorizontal = direction === 'horizontal';
1169
+ const ticks = useMemo(() => {
1170
+ const result = [];
1171
+ for (let mm = 0; mm <= Math.floor(lengthMm); mm += 1) {
1172
+ const px = printableOffsetPx + mmToPx(mm) * zoom;
1173
+ const isMajor = mm % 10 === 0;
1174
+ result.push({
1175
+ pos: px,
1176
+ major: isMajor,
1177
+ medium: !isMajor && mm % 5 === 0,
1178
+ label: isMajor ? `${mm}` : undefined,
1179
+ });
1180
+ }
1181
+ return result;
1182
+ }, [lengthMm, printableOffsetPx, zoom]);
1183
+ return (_jsx("div", { "data-testid": `designer-ruler-${direction}`, "data-printable-offset-px": Math.round(printableOffsetPx), style: {
1184
+ position: 'absolute',
1185
+ ...(isHorizontal
1186
+ ? { left: `${offsetPx}px`, top: `${crossOffsetPx}px`, width: `${lengthPx}px`, height: `${RULER_SIZE}px` }
1187
+ : { left: `${crossOffsetPx}px`, top: `${offsetPx}px`, width: `${RULER_SIZE}px`, height: `${lengthPx}px` }),
1188
+ overflow: 'hidden',
1189
+ backgroundColor: '#f1f1f1',
1190
+ borderRight: isHorizontal ? undefined : '1px solid #b9b9b9',
1191
+ borderBottom: isHorizontal ? '1px solid #b9b9b9' : undefined,
1192
+ zIndex: 1000,
1193
+ userSelect: 'none',
1194
+ }, children: _jsx("svg", { width: isHorizontal ? lengthPx : RULER_SIZE, height: isHorizontal ? RULER_SIZE : lengthPx, style: { display: 'block' }, children: ticks.map((tick, i) => {
1195
+ if (isHorizontal) {
1196
+ const tickH = tick.major ? 16 : tick.medium ? 11 : 6;
1197
+ return (_jsxs("g", { children: [_jsx("line", { x1: tick.pos, y1: RULER_SIZE, x2: tick.pos, y2: RULER_SIZE - tickH, stroke: tick.major ? '#555' : '#8f8f8f', strokeWidth: tick.major ? 1 : 0.5 }), tick.label && (_jsx("text", { x: tick.pos + 4, y: 11, fontSize: "8", fill: "#444", fontFamily: "Arial", children: tick.label }))] }, i));
1198
+ }
1199
+ else {
1200
+ const tickW = tick.major ? 16 : tick.medium ? 11 : 6;
1201
+ return (_jsxs("g", { children: [_jsx("line", { x1: RULER_SIZE, y1: tick.pos, x2: RULER_SIZE - tickW, y2: tick.pos, stroke: tick.major ? '#555' : '#8f8f8f', strokeWidth: tick.major ? 1 : 0.5 }), tick.label && (_jsx("text", { x: 2, y: tick.pos + 3, fontSize: "8", fill: "#444", fontFamily: "Arial", children: tick.label }))] }, i));
1202
+ }
1203
+ }) }) }));
1204
+ };
1205
+ // ---- Zoom Bar Component ----
1206
+ const ZoomBar = ({ zoom, onZoomIn, onZoomOut, onReset, onSetZoom }) => {
1207
+ const { t } = useDesignerI18n();
1208
+ return (_jsxs("div", { "data-testid": "designer-zoom-bar", style: {
1209
+ position: 'absolute', right: 16, bottom: 16,
1210
+ backgroundColor: '#fff', border: '1px solid #d9d9d9', borderRadius: 4,
1211
+ boxShadow: '0 2px 8px rgba(0,0,0,0.12)', zIndex: 10000,
1212
+ display: 'flex', alignItems: 'center', padding: '2px 4px', gap: 2,
1213
+ }, children: [_jsx("button", { onClick: onZoomOut, title: t('canvas.zoomOut'), style: zoomBtnStyle, children: '−' }), _jsxs("span", { onClick: onReset, title: t('canvas.zoomReset'), style: { fontSize: 11, color: '#555', cursor: 'pointer', padding: '0 6px', minWidth: 40, textAlign: 'center' }, children: [Math.round(zoom * 100), "%"] }), _jsx("button", { onClick: onZoomIn, title: t('canvas.zoomIn'), style: zoomBtnStyle, children: '+' }), _jsx("div", { style: { width: 1, height: 18, backgroundColor: '#d9d9d9', margin: '0 2px' } }), _jsx("button", { onClick: () => onSetZoom(0.5), title: "50%", style: { ...zoomBtnStyle, width: 28, fontSize: 10 }, children: "50%" }), _jsx("button", { onClick: () => onSetZoom(1), title: "100%", style: { ...zoomBtnStyle, width: 34, fontSize: 10 }, children: "100%" }), _jsx("button", { onClick: () => onSetZoom(2), title: "200%", style: { ...zoomBtnStyle, width: 34, fontSize: 10 }, children: "200%" })] }));
1214
+ };
1215
+ const zoomBtnStyle = {
1216
+ width: 24, height: 24, border: '1px solid #d9d9d9', borderRadius: 3,
1217
+ backgroundColor: '#fff', cursor: 'pointer', display: 'flex',
1218
+ alignItems: 'center', justifyContent: 'center', fontSize: 14,
1219
+ padding: 0, lineHeight: 1, color: '#555',
1220
+ };
1221
+ function areMenuKeysEqual(a, b) {
1222
+ return a.length === b.length && a.every((key, index) => key === b[index]);
1223
+ }
1224
+ function divider(key) {
1225
+ return { type: 'divider', key };
1226
+ }
1227
+ function menuLabel(label, shortcut, visible = true, onMouseEnter) {
1228
+ if (!visible)
1229
+ return _jsx("span", { "aria-hidden": true });
1230
+ if (!shortcut) {
1231
+ return (_jsx("span", { style: { display: 'block' }, onMouseEnter: onMouseEnter, children: label }));
1232
+ }
1233
+ return (_jsxs("span", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, minWidth: 150 }, onMouseEnter: onMouseEnter, children: [_jsx("span", { children: label }), _jsx("span", { style: { color: '#999', fontSize: 11 }, children: shortcut })] }));
1234
+ }
1235
+ function menuItem(key, label, options = {}) {
1236
+ return {
1237
+ key,
1238
+ label: menuLabel(label, options.shortcut, options.visible, options.onMouseEnter),
1239
+ disabled: options.disabled,
1240
+ danger: options.danger,
1241
+ };
1242
+ }
1243
+ const ContextMenu = ({ x, y, hasSelection, hasClipboard, selectedType, tableCell, onCopy, onCut, onPaste, onDuplicate, onDelete, onBringToFront, onSendToBack, onInsertTableColumnLeft, onInsertTableColumnRight, onDeleteTableColumn, onInsertTableRowAbove, onInsertTableRowBelow, onDeleteTableRow, onToggleTableBorder, onMergeTableCellRight, onMergeSelectedTableCells, onSplitTableCell, onClearTableCell, onClearTableCellStyle, onCopyTableCellStyle, onPasteTableCellStyle, onEqualizeTableColumns, onEqualizeTableRows, }) => {
1244
+ const { t } = useDesignerI18n();
1245
+ const [openKeys, setOpenKeys] = useState([]);
1246
+ const isTableCellMenu = selectedType === 'table' && !!tableCell;
1247
+ const closeSubmenus = useCallback(() => {
1248
+ setOpenKeys((current) => current.length > 0 ? [] : current);
1249
+ }, []);
1250
+ const openSubmenu = useCallback((keys) => {
1251
+ setOpenKeys((current) => areMenuKeysEqual(current, keys) ? current : keys);
1252
+ }, []);
1253
+ const handleOpenChange = useCallback((keys) => {
1254
+ setOpenKeys((current) => areMenuKeysEqual(current, keys) ? current : keys);
1255
+ }, []);
1256
+ const isSubmenuOpen = useCallback((key) => openKeys.includes(key), [openKeys]);
1257
+ const menuSubmenu = useCallback((key, label, children, disabled, visible = true, openPath = [key]) => ({
1258
+ key,
1259
+ label: (_jsx("span", { style: { display: 'block' }, onMouseEnter: () => openSubmenu(openPath), children: visible ? label : null })),
1260
+ children,
1261
+ disabled,
1262
+ }), [openSubmenu]);
1263
+ const actionMap = {
1264
+ cut: onCut,
1265
+ copy: onCopy,
1266
+ paste: onPaste,
1267
+ duplicate: onDuplicate,
1268
+ delete: onDelete,
1269
+ bringToFront: onBringToFront,
1270
+ sendToBack: onSendToBack,
1271
+ insertColumnLeft: onInsertTableColumnLeft,
1272
+ insertColumnRight: onInsertTableColumnRight,
1273
+ deleteColumn: onDeleteTableColumn,
1274
+ insertRowAbove: onInsertTableRowAbove,
1275
+ insertRowBelow: onInsertTableRowBelow,
1276
+ deleteRow: onDeleteTableRow,
1277
+ toggleBorder: onToggleTableBorder,
1278
+ mergeCells: () => {
1279
+ const selected = useDesignerStore.getState().selectedTableCell;
1280
+ if (selected && (selected.startRow !== selected.endRow || selected.startColumn !== selected.endColumn)) {
1281
+ onMergeSelectedTableCells();
1282
+ return;
1283
+ }
1284
+ onMergeTableCellRight();
1285
+ },
1286
+ splitCell: onSplitTableCell,
1287
+ clearCell: onClearTableCell,
1288
+ clearCellStyle: onClearTableCellStyle,
1289
+ copyCellStyle: onCopyTableCellStyle,
1290
+ pasteCellStyle: onPasteTableCellStyle,
1291
+ equalizeColumns: onEqualizeTableColumns,
1292
+ equalizeRows: onEqualizeTableRows,
1293
+ };
1294
+ const topLevelMenuItem = useCallback((key, label, options = {}) => menuItem(key, label, { ...options, onMouseEnter: closeSubmenus }), [closeSubmenus]);
1295
+ const commonEditItems = [
1296
+ topLevelMenuItem('cut', t('contextMenu.cut'), { shortcut: 'Ctrl+X', disabled: !hasSelection }),
1297
+ topLevelMenuItem('copy', t('contextMenu.copy'), { shortcut: 'Ctrl+C', disabled: !hasSelection }),
1298
+ topLevelMenuItem('paste', t('contextMenu.paste'), { shortcut: 'Ctrl+V', disabled: !hasClipboard }),
1299
+ ];
1300
+ const componentItems = [
1301
+ ...commonEditItems,
1302
+ topLevelMenuItem('duplicate', t('contextMenu.duplicate'), { shortcut: 'Ctrl+D', disabled: !hasSelection }),
1303
+ divider('edit-divider'),
1304
+ menuSubmenu('arrange', t('contextMenu.table.arrange'), [
1305
+ menuItem('bringToFront', t('contextMenu.bringToFront'), { shortcut: 'Ctrl+Alt+↑', disabled: !hasSelection }),
1306
+ menuItem('sendToBack', t('contextMenu.sendToBack'), { shortcut: 'Ctrl+Alt+↓', disabled: !hasSelection }),
1307
+ ], !hasSelection),
1308
+ divider('delete-divider'),
1309
+ topLevelMenuItem('delete', t('contextMenu.delete'), { shortcut: 'Del', disabled: !hasSelection, danger: true }),
1310
+ ];
1311
+ const tableItems = [
1312
+ ...commonEditItems,
1313
+ divider('clear-divider'),
1314
+ topLevelMenuItem('clearCell', t('contextMenu.table.clearContent'), { disabled: !tableCell }),
1315
+ divider('structure-divider'),
1316
+ menuSubmenu('insert', t('contextMenu.table.insert'), [
1317
+ menuItem('insertRowAbove', t('contextMenu.table.insertRowAboveExcel'), { visible: isSubmenuOpen('insert') }),
1318
+ menuItem('insertRowBelow', t('contextMenu.table.insertRowBelowExcel'), { visible: isSubmenuOpen('insert') }),
1319
+ menuItem('insertColumnLeft', t('contextMenu.table.insertColumnLeftExcel'), { visible: isSubmenuOpen('insert') }),
1320
+ menuItem('insertColumnRight', t('contextMenu.table.insertColumnRightExcel'), { visible: isSubmenuOpen('insert') }),
1321
+ ], !tableCell),
1322
+ menuSubmenu('deleteTablePart', t('contextMenu.table.delete'), [
1323
+ menuItem('deleteRow', t('contextMenu.table.deleteCurrentRow'), { visible: isSubmenuOpen('deleteTablePart') }),
1324
+ menuItem('deleteColumn', t('contextMenu.table.deleteCurrentColumn'), { visible: isSubmenuOpen('deleteTablePart') }),
1325
+ ], !tableCell),
1326
+ divider('cell-divider'),
1327
+ topLevelMenuItem('mergeCells', t('contextMenu.table.mergeCells'), { disabled: !tableCell }),
1328
+ topLevelMenuItem('splitCell', t('contextMenu.table.splitCell'), { disabled: !tableCell }),
1329
+ divider('distribution-divider'),
1330
+ topLevelMenuItem('equalizeColumns', t('contextMenu.table.distributeColumns')),
1331
+ topLevelMenuItem('equalizeRows', t('contextMenu.table.distributeRows')),
1332
+ divider('style-divider'),
1333
+ menuSubmenu('cellStyle', t('contextMenu.table.cellStyle'), [
1334
+ menuItem('copyCellStyle', t('contextMenu.table.copyStyle'), { visible: isSubmenuOpen('cellStyle') }),
1335
+ menuItem('pasteCellStyle', t('contextMenu.table.pasteStyle'), { visible: isSubmenuOpen('cellStyle') }),
1336
+ menuItem('clearCellStyle', t('contextMenu.table.clearStyle'), { visible: isSubmenuOpen('cellStyle') }),
1337
+ ], !tableCell),
1338
+ divider('table-divider'),
1339
+ menuSubmenu('table', t('contextMenu.table.table'), [
1340
+ menuItem('toggleBorder', t('contextMenu.table.toggleBorder'), { visible: isSubmenuOpen('table') }),
1341
+ menuSubmenu('tableArrange', t('contextMenu.table.arrange'), [
1342
+ menuItem('bringToFront', t('contextMenu.bringToFront'), { shortcut: 'Ctrl+Alt+↑', disabled: !hasSelection, visible: isSubmenuOpen('tableArrange') }),
1343
+ menuItem('sendToBack', t('contextMenu.sendToBack'), { shortcut: 'Ctrl+Alt+↓', disabled: !hasSelection, visible: isSubmenuOpen('tableArrange') }),
1344
+ ], !hasSelection, isSubmenuOpen('table'), ['table', 'tableArrange']),
1345
+ menuItem('delete', t('contextMenu.table.deleteTable'), { disabled: !hasSelection, danger: true, visible: isSubmenuOpen('table') }),
1346
+ ]),
1347
+ ];
1348
+ const items = isTableCellMenu ? tableItems : componentItems;
1349
+ return (_jsx(Dropdown, { open: true, trigger: [], placement: "bottomLeft", popupRender: (originNode) => (_jsx("div", { onMouseDown: (event) => event.stopPropagation(), onContextMenu: (event) => event.stopPropagation(), onClick: (event) => event.stopPropagation(), children: originNode })), menu: {
1350
+ items,
1351
+ openKeys,
1352
+ forceSubMenuRender: true,
1353
+ subMenuOpenDelay: 0,
1354
+ subMenuCloseDelay: 0,
1355
+ onOpenChange: handleOpenChange,
1356
+ onClick: ({ key }) => {
1357
+ actionMap[key]?.();
1358
+ },
1359
+ }, children: _jsx("span", { "aria-hidden": true, style: { position: 'absolute', left: x, top: y, width: 1, height: 1, pointerEvents: 'none' }, onMouseDown: (event) => event.stopPropagation(), onContextMenu: (event) => event.stopPropagation(), onClick: (event) => event.stopPropagation() }) }));
1360
+ };
1361
+ const BandContextMenu = ({ onCopy, onDelete, x, y }) => {
1362
+ const { t } = useDesignerI18n();
1363
+ return (_jsx(Dropdown, { open: true, trigger: [], placement: "bottomLeft", popupRender: (originNode) => (_jsx("div", { onMouseDown: (event) => event.stopPropagation(), onContextMenu: (event) => event.stopPropagation(), onClick: (event) => event.stopPropagation(), children: originNode })), menu: {
1364
+ items: [
1365
+ menuItem('copy', t('contextMenu.band.copy')),
1366
+ divider('band-divider'),
1367
+ menuItem('delete', t('contextMenu.band.delete'), { danger: true }),
1368
+ ],
1369
+ onClick: ({ key }) => {
1370
+ if (key === 'copy')
1371
+ onCopy();
1372
+ if (key === 'delete')
1373
+ onDelete();
1374
+ },
1375
+ }, children: _jsx("span", { "data-testid": "designer-band-context-menu", "aria-hidden": true, style: { position: 'absolute', left: x, top: y, width: 1, height: 1, pointerEvents: 'none' }, onMouseDown: (event) => event.stopPropagation(), onContextMenu: (event) => event.stopPropagation(), onClick: (event) => event.stopPropagation() }) }));
1376
+ };
1377
+ // ---- Band View ----
1378
+ const BandView = ({ band, visualY, labelIndex, page, isSelected, pendingBandInsertType, selectedIds, selectedTableCell, fonts, isDragging, isComponentMoveTarget, onOpenContextMenu, onStartBandSort, onUpdateComponent, currentPageId }) => {
1379
+ const { t } = useDesignerI18n();
1380
+ const [editId, setEditId] = useState(null);
1381
+ const [editKind, setEditKind] = useState(null);
1382
+ const [editText, setEditText] = useState('');
1383
+ const [editCell, setEditCell] = useState(null);
1384
+ const richTextEditComponent = editKind === 'richtext' && editId
1385
+ ? band.components.find((component) => component.id === editId && component.type === 'richtext')
1386
+ : null;
1387
+ const finishTextEdit = useCallback((text = editText) => {
1388
+ if (!editId || editKind !== 'text')
1389
+ return;
1390
+ const component = band.components.find(item => item.id === editId);
1391
+ if (!component || component.type !== 'text') {
1392
+ setEditId(null);
1393
+ setEditKind(null);
1394
+ return;
1395
+ }
1396
+ onUpdateComponent(currentPageId, band.id, editId, { text }, { text: component.text });
1397
+ setEditId(null);
1398
+ setEditKind(null);
1399
+ }, [band.components, band.id, currentPageId, editId, editKind, editText, onUpdateComponent]);
1400
+ const finishTableCellEdit = useCallback((text = editText) => {
1401
+ if (!editId || editKind !== 'tableCell' || !editCell)
1402
+ return;
1403
+ const table = band.components.find(item => item.id === editId && item.type === 'table');
1404
+ if (!table) {
1405
+ setEditId(null);
1406
+ setEditKind(null);
1407
+ setEditCell(null);
1408
+ return;
1409
+ }
1410
+ const nextTable = updateTableCellText(table, editCell.startRow, editCell.startColumn, text);
1411
+ onUpdateComponent(currentPageId, band.id, table.id, { rows: nextTable.rows }, { rows: table.rows });
1412
+ setEditId(null);
1413
+ setEditKind(null);
1414
+ setEditCell(null);
1415
+ }, [band.components, band.id, currentPageId, editCell, editId, editKind, editText, onUpdateComponent]);
1416
+ useEffect(() => {
1417
+ if (!editId || editKind !== 'text' || selectedIds.includes(editId))
1418
+ return;
1419
+ finishTextEdit(editText);
1420
+ }, [editId, editKind, editText, finishTextEdit, selectedIds]);
1421
+ useEffect(() => {
1422
+ if (!editId || editKind !== 'tableCell' || !editCell)
1423
+ return;
1424
+ const sameCell = Boolean(selectedIds.includes(editCell.tableId)
1425
+ && selectedTableCell
1426
+ && selectedTableCell.tableId === editCell.tableId
1427
+ && selectedTableCell.startRow === editCell.startRow
1428
+ && selectedTableCell.startColumn === editCell.startColumn);
1429
+ if (!sameCell)
1430
+ finishTableCellEdit(editText);
1431
+ }, [editCell, editId, editKind, editText, finishTableCellEdit, selectedIds, selectedTableCell]);
1432
+ const selectBand = useDesignerStore((state) => state.selectBand);
1433
+ const selectComponents = useDesignerStore((state) => state.selectComponents);
1434
+ const selectTableCell = useDesignerStore((state) => state.selectTableCell);
1435
+ const insertBandAfter = useDesignerStore((state) => state.insertBandAfter);
1436
+ const baseColor = BAND_COLORS[band.type] || '#757575';
1437
+ const baseLabel = BAND_LABEL_KEYS[band.type] ? t(BAND_LABEL_KEYS[band.type]) : band.type;
1438
+ const bandLabel = formatBandTitle(baseLabel, labelIndex, band);
1439
+ const headerHeight = mmToPx(BAND_HEADER_MM);
1440
+ const bodyHeight = mmToPx(band.height);
1441
+ const columnGuide = getBandColumnGuide(page, band);
1442
+ const handleBandMouseDown = (event) => {
1443
+ const target = event.target;
1444
+ if (target.closest('[data-component-id]') ||
1445
+ target.closest('[data-resize-handle]') ||
1446
+ target.closest('[data-band-resize]')) {
1447
+ return;
1448
+ }
1449
+ event.stopPropagation();
1450
+ if (pendingBandInsertType) {
1451
+ insertBandAfter(currentPageId, band.id, pendingBandInsertType);
1452
+ return;
1453
+ }
1454
+ selectComponents([]);
1455
+ selectBand(band.id);
1456
+ if (target.closest('[data-band-sort-handle]')) {
1457
+ event.preventDefault();
1458
+ onStartBandSort(band.id, event);
1459
+ }
1460
+ };
1461
+ return (_jsxs("div", { "data-band-id": band.id, "data-testid": `designer-band-frame-${band.type}`, onContextMenu: (event) => onOpenContextMenu(band.id, event), onMouseDown: handleBandMouseDown, style: {
1462
+ position: 'absolute', left: 0, top: safeCssNumber(mmToPx(visualY)), width: '100%', height: safeCssNumber(headerHeight + bodyHeight),
1463
+ border: isComponentMoveTarget ? '2px solid #1677ff' : isSelected ? '1px solid #4d90fe' : '1px solid rgba(0,0,0,0.12)',
1464
+ boxSizing: 'border-box',
1465
+ backgroundColor: isComponentMoveTarget ? 'rgba(22, 119, 255, 0.08)' : `${baseColor}10`,
1466
+ cursor: pendingBandInsertType ? 'copy' : 'default',
1467
+ opacity: isDragging ? 0.35 : 1,
1468
+ }, children: [_jsxs("div", { style: {
1469
+ position: 'absolute', left: 0, right: 0, top: 0, height: safeCssNumber(headerHeight),
1470
+ backgroundColor: `${baseColor}44`,
1471
+ borderBottom: `1px solid ${baseColor}66`,
1472
+ display: 'flex', alignItems: 'center',
1473
+ padding: '0 3px',
1474
+ fontSize: 12, lineHeight: `${headerHeight}px`, color: '#111',
1475
+ cursor: pendingBandInsertType ? 'copy' : 'grab', zIndex: 30, pointerEvents: 'auto',
1476
+ boxSizing: 'border-box',
1477
+ }, "data-band-sort-handle": true, "data-testid": `designer-band-title-${band.type}`, children: [_jsx("span", { children: bandLabel }), _jsx("span", { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clipPath: 'inset(50%)', whiteSpace: 'nowrap' }, children: baseLabel })] }), _jsx(BandResizeHandle, { bandId: band.id }), isComponentMoveTarget ? (_jsxs("div", { "data-testid": "designer-component-move-band-target", style: {
1478
+ position: 'absolute',
1479
+ right: 8,
1480
+ top: headerHeight + 6,
1481
+ zIndex: 1200,
1482
+ padding: '3px 8px',
1483
+ borderRadius: 4,
1484
+ backgroundColor: '#1677ff',
1485
+ color: '#fff',
1486
+ fontSize: 12,
1487
+ lineHeight: '18px',
1488
+ boxShadow: '0 4px 10px rgba(0,0,0,0.18)',
1489
+ pointerEvents: 'none',
1490
+ }, children: ["\u5C06\u79FB\u52A8\u5230\uFF1A", bandLabel] })) : null, _jsxs("div", { "data-testid": `designer-band-body-${band.type}`, style: {
1491
+ position: 'absolute',
1492
+ left: 0,
1493
+ right: 0,
1494
+ top: headerHeight,
1495
+ height: safeCssNumber(bodyHeight),
1496
+ }, children: [columnGuide ? _jsx(BandColumnGuides, { guide: columnGuide, bodyHeight: bodyHeight }) : null, band.components
1497
+ .slice()
1498
+ .sort((a, b) => (a.zOrder ?? 0) - (b.zOrder ?? 0))
1499
+ .map(comp => (_jsx(ComponentView, { component: comp, bandId: band.id, selected: selectedIds.includes(comp.id), editing: editId === comp.id, selectedTableCell: selectedTableCell?.tableId === comp.id ? selectedTableCell : null, editingKind: editId === comp.id ? editKind : null, editText: editText, onStartTextEdit: () => { selectBand(null); selectComponents([comp.id]); setEditId(comp.id); setEditKind('text'); setEditText(comp.text || ''); }, onStartRichTextEdit: () => { setEditId(comp.id); setEditKind('richtext'); }, onFinishEdit: finishTextEdit, editingCell: editId === comp.id && editKind === 'tableCell' ? editCell : null, onStartTableCellEdit: (selection, text) => {
1500
+ selectBand(null);
1501
+ selectComponents([selection.tableId]);
1502
+ selectTableCell(selection);
1503
+ setEditId(selection.tableId);
1504
+ setEditKind('tableCell');
1505
+ setEditCell(selection);
1506
+ setEditText(text);
1507
+ }, onFinishTableCellEdit: finishTableCellEdit, onEditTextChange: setEditText }, comp.id)))] }), _jsx(Modal, { open: Boolean(richTextEditComponent), title: t('richText.edit'), width: 820, footer: null, destroyOnHidden: true, onCancel: () => { setEditId(null); setEditKind(null); }, styles: { body: { paddingTop: 8 } }, children: richTextEditComponent ? (_jsx(RichTextInlineEditor, { html: String(richTextEditComponent.html ?? ''), document: richTextEditComponent.document, fonts: fonts, onSave: (value) => {
1508
+ onUpdateComponent(currentPageId, band.id, richTextEditComponent.id, { html: value.html, document: value.document }, { html: richTextEditComponent.html, document: richTextEditComponent.document });
1509
+ setEditId(null);
1510
+ setEditKind(null);
1511
+ }, onCancel: () => { setEditId(null); setEditKind(null); } })) : null })] }));
1512
+ };
1513
+ const BandColumnGuides = ({ guide, bodyHeight }) => (_jsxs(_Fragment, { children: [Array.from({ length: guide.count - 1 }, (_, index) => {
1514
+ const leftMm = guide.width + index * (guide.width + guide.gap) + guide.gap / 2;
1515
+ return (_jsx("div", { "data-testid": "designer-band-column-guide", style: {
1516
+ position: 'absolute',
1517
+ left: safeCssNumber(mmToPx(leftMm)),
1518
+ top: 0,
1519
+ height: safeCssNumber(bodyHeight),
1520
+ borderLeft: '1px dashed rgba(22, 119, 255, 0.75)',
1521
+ pointerEvents: 'none',
1522
+ zIndex: 6,
1523
+ } }, index));
1524
+ }), _jsx("div", { "data-testid": "designer-band-first-column", style: {
1525
+ position: 'absolute',
1526
+ left: 0,
1527
+ top: 0,
1528
+ width: safeCssNumber(mmToPx(guide.width)),
1529
+ height: safeCssNumber(bodyHeight),
1530
+ boxShadow: 'inset -1px 0 rgba(22, 119, 255, 0.3)',
1531
+ pointerEvents: 'none',
1532
+ zIndex: 5,
1533
+ } })] }));
1534
+ function getBandColumnGuide(page, band) {
1535
+ const width = getFirstColumnDesignWidth(page, band);
1536
+ const columns = getBandColumnSettings(page, band);
1537
+ if (!width || !columns || columns.count <= 1) {
1538
+ return undefined;
1539
+ }
1540
+ return {
1541
+ count: columns.count,
1542
+ gap: columns.gap,
1543
+ width,
1544
+ };
1545
+ }
1546
+ function clampComponentXToFirstColumn(page, band, x, componentWidth) {
1547
+ const maxRight = getFirstColumnDesignWidth(page, band);
1548
+ if (maxRight === undefined) {
1549
+ return Math.max(0, Math.round(x * 10) / 10);
1550
+ }
1551
+ const maxX = Math.max(0, maxRight - componentWidth);
1552
+ return Math.max(0, Math.min(maxX, Math.round(x * 10) / 10));
1553
+ }
1554
+ function getFirstColumnDesignWidth(page, band) {
1555
+ if (!page || !band) {
1556
+ return undefined;
1557
+ }
1558
+ const settings = getBandColumnSettings(page, band);
1559
+ if (!settings || settings.count <= 1) {
1560
+ return undefined;
1561
+ }
1562
+ const printableWidth = page.width - page.margins.left - page.margins.right;
1563
+ return Math.max(1, (printableWidth - settings.gap * (settings.count - 1)) / settings.count);
1564
+ }
1565
+ function getBandColumnSettings(page, band) {
1566
+ if (!page || !band) {
1567
+ return undefined;
1568
+ }
1569
+ const sourceBand = resolveColumnSourceBand(page, band);
1570
+ const count = Math.max(1, Math.floor(sourceBand?.dataBand?.columns?.count ?? 1));
1571
+ if (count <= 1) {
1572
+ return undefined;
1573
+ }
1574
+ return {
1575
+ count,
1576
+ gap: Math.max(0, sourceBand?.dataBand?.columns?.gap ?? 0),
1577
+ };
1578
+ }
1579
+ function resolveColumnSourceBand(page, band) {
1580
+ if (band.type === 'data' && (band.dataBand?.columns?.count ?? 1) > 1) {
1581
+ return band;
1582
+ }
1583
+ const bandIndex = page.bands.findIndex(item => item.id === band.id);
1584
+ if (bandIndex < 0) {
1585
+ return undefined;
1586
+ }
1587
+ if (band.type === 'columnHeader') {
1588
+ return page.bands.slice(bandIndex + 1).find(item => item.type === 'data' && (item.dataBand?.columns?.count ?? 1) > 1);
1589
+ }
1590
+ if (band.type === 'columnFooter') {
1591
+ return [...page.bands.slice(0, bandIndex)].reverse().find(item => item.type === 'data' && (item.dataBand?.columns?.count ?? 1) > 1);
1592
+ }
1593
+ return undefined;
1594
+ }
1595
+ const BandDragPreview = ({ band, labelIndex, top }) => {
1596
+ const { t } = useDesignerI18n();
1597
+ const baseColor = BAND_COLORS[band.type] || '#757575';
1598
+ const baseLabel = BAND_LABEL_KEYS[band.type] ? t(BAND_LABEL_KEYS[band.type]) : band.type;
1599
+ const bandLabel = formatBandTitle(baseLabel, labelIndex, band);
1600
+ const headerHeight = mmToPx(BAND_HEADER_MM);
1601
+ const bodyHeight = mmToPx(band.height);
1602
+ return (_jsx("div", { "data-testid": "designer-band-drag-preview", style: {
1603
+ position: 'absolute',
1604
+ left: 0,
1605
+ right: 0,
1606
+ top: safeCssNumber(top),
1607
+ height: safeCssNumber(headerHeight + bodyHeight),
1608
+ border: `1px solid ${baseColor}`,
1609
+ backgroundColor: `${baseColor}18`,
1610
+ boxShadow: '0 8px 18px rgba(0,0,0,0.22)',
1611
+ boxSizing: 'border-box',
1612
+ opacity: 0.86,
1613
+ pointerEvents: 'none',
1614
+ zIndex: 9998,
1615
+ }, children: _jsx("div", { style: {
1616
+ height: safeCssNumber(headerHeight),
1617
+ backgroundColor: `${baseColor}66`,
1618
+ borderBottom: `1px solid ${baseColor}88`,
1619
+ boxSizing: 'border-box',
1620
+ color: '#111',
1621
+ display: 'flex',
1622
+ alignItems: 'center',
1623
+ fontSize: 12,
1624
+ lineHeight: `${headerHeight}px`,
1625
+ padding: '0 3px',
1626
+ }, children: bandLabel }) }));
1627
+ };
1628
+ function formatBandTitle(baseLabel, labelIndex, band) {
1629
+ const bandLabel = `${baseLabel}${labelIndex}`;
1630
+ if (band.type !== 'data' && band.type !== 'hierarchicalData') {
1631
+ return bandLabel;
1632
+ }
1633
+ const dataSourceId = band.dataBand?.dataSourceId ?? band.dataSource;
1634
+ return dataSourceId ? `${bandLabel}; 数据源: ${dataSourceId}` : bandLabel;
1635
+ }
1636
+ const PageWatermarkOverlay = ({ watermark, zIndex }) => {
1637
+ if (!watermark?.enabled || !watermark.text)
1638
+ return null;
1639
+ return (_jsx("div", { "data-testid": "designer-page-watermark", style: {
1640
+ position: 'absolute',
1641
+ inset: 0,
1642
+ display: 'flex',
1643
+ justifyContent: horizontalAlignToFlex(watermark.horizontalAlign),
1644
+ alignItems: verticalAlignToFlex(watermark.verticalAlign),
1645
+ color: watermark.color,
1646
+ opacity: watermark.opacity,
1647
+ fontFamily: watermark.fontFamily,
1648
+ fontSize: watermark.fontSize * MM_TO_PX,
1649
+ fontWeight: 600,
1650
+ lineHeight: 1,
1651
+ whiteSpace: 'pre-wrap',
1652
+ textAlign: watermark.horizontalAlign,
1653
+ pointerEvents: 'none',
1654
+ userSelect: 'none',
1655
+ zIndex,
1656
+ }, children: _jsx("span", { style: {
1657
+ display: 'inline-block',
1658
+ transform: `rotate(${watermark.angle}deg)`,
1659
+ transformOrigin: 'center',
1660
+ }, children: watermark.text }) }));
1661
+ };
1662
+ const PageBorderOverlay = ({ pageBorder }) => {
1663
+ if (!pageBorder?.enabled || pageBorder.style === 'none' || pageBorder.width <= 0)
1664
+ return null;
1665
+ const border = `${pageBorder.width}mm ${pageBorder.style} ${pageBorder.color}`;
1666
+ return (_jsx("div", { "data-testid": "designer-page-border", style: {
1667
+ position: 'absolute',
1668
+ inset: `${pageBorder.offset ?? 0}mm`,
1669
+ boxSizing: 'border-box',
1670
+ borderTop: pageBorder.sides.top ? border : 'none',
1671
+ borderRight: pageBorder.sides.right ? border : 'none',
1672
+ borderBottom: pageBorder.sides.bottom ? border : 'none',
1673
+ borderLeft: pageBorder.sides.left ? border : 'none',
1674
+ pointerEvents: 'none',
1675
+ zIndex: 25,
1676
+ } }));
1677
+ };
1678
+ function horizontalAlignToFlex(value) {
1679
+ if (value === 'left')
1680
+ return 'flex-start';
1681
+ if (value === 'right')
1682
+ return 'flex-end';
1683
+ return 'center';
1684
+ }
1685
+ // ---- Band Resize Handle ----
1686
+ const BandResizeHandle = ({ bandId }) => (_jsx("div", { "data-band-resize": true, "data-band-id": bandId, style: {
1687
+ position: 'absolute', left: 0, right: 0, bottom: -4, height: 8,
1688
+ cursor: 'ns-resize', backgroundColor: 'transparent', zIndex: 300,
1689
+ }, onMouseEnter: (e) => { e.target.style.backgroundColor = '#1890ff44'; }, onMouseLeave: (e) => { e.target.style.backgroundColor = 'transparent'; } }));
1690
+ const ComponentView = React.memo(function ComponentView({ component, bandId, selected, editing, editText, selectedTableCell, editingKind, editingCell, onStartTextEdit, onStartRichTextEdit, onStartTableCellEdit, onFinishEdit, onFinishTableCellEdit, onEditTextChange, }) {
1691
+ const { t } = useDesignerI18n();
1692
+ const inputRef = useRef(null);
1693
+ React.useEffect(() => { if (editing && editingKind === 'text')
1694
+ setTimeout(() => inputRef.current?.focus(), 0); }, [editing, editingKind]);
1695
+ const x = safeCssNumber(mmToPx(component.x));
1696
+ const y = safeCssNumber(mmToPx(component.y));
1697
+ const w = safeCssNumber(mmToPx(component.width));
1698
+ const h = safeCssNumber(mmToPx(component.height));
1699
+ return (_jsxs("div", { style: {
1700
+ position: 'absolute', left: x, top: y, width: w, height: h,
1701
+ outline: selected ? '2px solid #1890ff' : 'none',
1702
+ borderRadius: 2,
1703
+ }, children: [_jsx("div", { "data-component-id": component.id, onDoubleClick: (e) => {
1704
+ e.stopPropagation();
1705
+ if (component.type === 'text')
1706
+ onStartTextEdit();
1707
+ if (component.type === 'richtext')
1708
+ onStartRichTextEdit();
1709
+ }, onContextMenu: (e) => {
1710
+ e.preventDefault();
1711
+ e.stopPropagation();
1712
+ }, style: {
1713
+ position: 'absolute', inset: 0,
1714
+ boxSizing: 'border-box', cursor: editing ? 'text' : 'grab',
1715
+ overflow: 'hidden',
1716
+ padding: 0,
1717
+ backgroundColor: selected ? 'rgba(24,144,255,0.06)' : 'transparent',
1718
+ zIndex: selected ? 100 : 10,
1719
+ ...getCompStyle(component),
1720
+ }, children: editing && editingKind === 'text' ? (_jsx("input", { ref: inputRef, value: editText, onChange: (e) => onEditTextChange(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter')
1721
+ onFinishEdit(editText); if (e.key === 'Escape')
1722
+ onFinishEdit(editText); }, onBlur: () => onFinishEdit(editText), onPointerDown: (e) => e.stopPropagation(), style: { width: '100%', height: '100%', border: 'none', outline: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 'inherit', color: 'inherit' } })) : getCompContent(component, bandId, selectedTableCell, {
1723
+ imagePlaceholder: t('canvas.imagePlaceholder'),
1724
+ subreportPlaceholder: t('canvas.subreportPlaceholder'),
1725
+ localTemplatePlaceholder: t('canvas.localTemplatePlaceholder'),
1726
+ }, {
1727
+ editingCell,
1728
+ editText,
1729
+ onStartTableCellEdit,
1730
+ onEditTextChange,
1731
+ onFinishTableCellEdit,
1732
+ }) }), _jsx(ComponentBorderOverlay, { component: component, zIndex: 150 }), selected && !editing && RESIZE_HANDLES.map(handle => (_jsx("div", { "data-resize-handle": "", "data-comp-id": component.id, "data-band-id": bandId, "data-handle-name": handle, style: {
1733
+ position: 'absolute', ...getHandlePos(handle, w, h),
1734
+ width: HANDLE_SIZE, height: HANDLE_SIZE,
1735
+ backgroundColor: '#1890ff', border: '1.5px solid #fff', borderRadius: 1,
1736
+ zIndex: 200, cursor: getCursorForHandle(handle),
1737
+ } }, handle))), selected && !editing && component.type === 'richtext' ? (_jsx(Tooltip, { title: t('richText.edit'), children: _jsx(Button, { "aria-label": t('richText.edit'), "data-testid": "designer-richtext-edit-action", size: "small", type: "primary", shape: "circle", icon: _jsx(EditOutlined, {}), onMouseDown: (event) => {
1738
+ event.preventDefault();
1739
+ event.stopPropagation();
1740
+ }, onClick: (event) => {
1741
+ event.preventDefault();
1742
+ event.stopPropagation();
1743
+ onStartRichTextEdit();
1744
+ }, style: {
1745
+ position: 'absolute',
1746
+ right: -12,
1747
+ top: -12,
1748
+ width: 24,
1749
+ height: 24,
1750
+ minWidth: 24,
1751
+ zIndex: 240,
1752
+ boxShadow: '0 2px 8px rgba(0,0,0,0.18)',
1753
+ } }) })) : null] }));
1754
+ }, areComponentViewPropsEqual);
1755
+ function areComponentViewPropsEqual(previous, next) {
1756
+ return previous.component === next.component
1757
+ && previous.bandId === next.bandId
1758
+ && previous.selected === next.selected
1759
+ && previous.editing === next.editing
1760
+ && previous.editText === next.editText
1761
+ && previous.selectedTableCell === next.selectedTableCell
1762
+ && previous.editingKind === next.editingKind
1763
+ && previous.editingCell === next.editingCell;
1764
+ }
1765
+ const ComponentBorderOverlay = ({ component, zIndex }) => {
1766
+ const style = componentBorderToCss(component);
1767
+ if (!style)
1768
+ return null;
1769
+ return (_jsx("div", { "data-component-border-id": component.id, style: {
1770
+ position: 'absolute',
1771
+ inset: 0,
1772
+ boxSizing: 'border-box',
1773
+ pointerEvents: 'none',
1774
+ zIndex,
1775
+ ...style,
1776
+ } }));
1777
+ };
1778
+ // ---- Helpers ----
1779
+ function getCompStyle(comp) {
1780
+ if (comp.type === 'panel') {
1781
+ const pad = comp.padding;
1782
+ return {
1783
+ backgroundColor: comp.backgroundColor || 'transparent',
1784
+ ...(pad ? {
1785
+ paddingTop: `${pad.top * MM_TO_PX}px`,
1786
+ paddingRight: `${pad.right * MM_TO_PX}px`,
1787
+ paddingBottom: `${pad.bottom * MM_TO_PX}px`,
1788
+ paddingLeft: `${pad.left * MM_TO_PX}px`,
1789
+ } : {}),
1790
+ };
1791
+ }
1792
+ if (comp.type === 'text') {
1793
+ const t = comp;
1794
+ const decorations = [];
1795
+ if (t.font?.underline)
1796
+ decorations.push('underline');
1797
+ if (t.font?.strikethrough)
1798
+ decorations.push('line-through');
1799
+ const pad = comp.padding;
1800
+ return {
1801
+ fontFamily: t.font?.family || 'Arial, sans-serif',
1802
+ fontSize: t.font?.size ? `${t.font.size * 1.33}px` : '16px',
1803
+ fontWeight: t.font?.bold ? 'bold' : 'normal',
1804
+ fontStyle: t.font?.italic ? 'italic' : 'normal',
1805
+ color: t.font?.color || '#000',
1806
+ textAlign: t.textAlign || 'left',
1807
+ textDecoration: decorations.length > 0 ? decorations.join(' ') : 'none',
1808
+ backgroundColor: comp.backgroundColor || 'transparent',
1809
+ ...(pad ? {
1810
+ paddingTop: `${pad.top * MM_TO_PX}px`,
1811
+ paddingRight: `${pad.right * MM_TO_PX}px`,
1812
+ paddingBottom: `${pad.bottom * MM_TO_PX}px`,
1813
+ paddingLeft: `${pad.left * MM_TO_PX}px`,
1814
+ } : {}),
1815
+ };
1816
+ }
1817
+ if (comp.type === 'chart') {
1818
+ const pad = comp.padding;
1819
+ return {
1820
+ backgroundColor: comp.backgroundColor || '#ffffff',
1821
+ ...(pad ? {
1822
+ paddingTop: `${pad.top * MM_TO_PX}px`,
1823
+ paddingRight: `${pad.right * MM_TO_PX}px`,
1824
+ paddingBottom: `${pad.bottom * MM_TO_PX}px`,
1825
+ paddingLeft: `${pad.left * MM_TO_PX}px`,
1826
+ } : {}),
1827
+ };
1828
+ }
1829
+ return {};
1830
+ }
1831
+ function componentBorderToCss(component) {
1832
+ const border = component.border;
1833
+ if (!hasVisibleBorder(border))
1834
+ return null;
1835
+ return borderToCss(border);
1836
+ }
1837
+ function hasVisibleBorder(border) {
1838
+ if (!border || border.style === 'none' || !border.width)
1839
+ return false;
1840
+ return Boolean(border.sides?.top || border.sides?.right || border.sides?.bottom || border.sides?.left);
1841
+ }
1842
+ function borderToCss(border) {
1843
+ if (!border || border.style === 'none')
1844
+ return {};
1845
+ const width = border.width ?? 0;
1846
+ const style = border.style ?? 'solid';
1847
+ const color = border.color ?? '#000000';
1848
+ return {
1849
+ borderTop: border.sides?.top ? `${width}mm ${style} ${color}` : 'none',
1850
+ borderRight: border.sides?.right ? `${width}mm ${style} ${color}` : 'none',
1851
+ borderBottom: border.sides?.bottom ? `${width}mm ${style} ${color}` : 'none',
1852
+ borderLeft: border.sides?.left ? `${width}mm ${style} ${color}` : 'none',
1853
+ };
1854
+ }
1855
+ function getFontStyle(font) {
1856
+ if (!font)
1857
+ return {};
1858
+ return {
1859
+ fontFamily: font.family || 'Arial',
1860
+ fontSize: font.size ? `${font.size * 1.33}px` : '16px',
1861
+ fontWeight: font.bold ? 'bold' : 'normal',
1862
+ fontStyle: font.italic ? 'italic' : 'normal',
1863
+ color: font.color || '#000',
1864
+ };
1865
+ }
1866
+ function getCompContent(comp, bandId, selectedTableCell, labels, tableEditing) {
1867
+ switch (comp.type) {
1868
+ case 'text':
1869
+ return _jsx("div", { style: { width: '100%', height: '100%', overflow: 'hidden', lineHeight: 1.2 }, children: comp.text || '' });
1870
+ case 'image': {
1871
+ const src = comp.src || '';
1872
+ const fitMode = comp.fitMode === 'stretch' || comp.fitMode === 'fill'
1873
+ ? 'fill'
1874
+ : comp.fitMode || 'contain';
1875
+ return src
1876
+ ? _jsx("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: fitMode }, draggable: false })
1877
+ : _jsx("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#ccc', fontSize: 10, border: '1px dashed #ddd' }, children: labels.imagePlaceholder });
1878
+ }
1879
+ case 'barcode': {
1880
+ const t = comp;
1881
+ const value = String(t.value || '');
1882
+ const symbol = renderCodeSymbolSvg({ type: 'barcode', value, format: t.format || 'CODE128', foregroundColor: t.foregroundColor });
1883
+ return (_jsxs("div", { "data-testid": "designer-component-barcode-content", style: {
1884
+ width: '100%',
1885
+ height: '100%',
1886
+ display: 'flex',
1887
+ flexDirection: 'column',
1888
+ justifyContent: 'stretch',
1889
+ overflow: 'hidden',
1890
+ backgroundColor: '#fff',
1891
+ }, children: [_jsx("div", { "aria-hidden": "true", style: {
1892
+ flex: 1,
1893
+ minHeight: 0,
1894
+ }, dangerouslySetInnerHTML: { __html: symbol.svg } }), t.showText ? (_jsx("div", { style: { fontSize: 9, lineHeight: '11px', textAlign: 'center', color: '#111', fontFamily: 'monospace' }, children: value })) : null] }));
1895
+ }
1896
+ case 'qrcode': {
1897
+ const t = comp;
1898
+ const symbol = renderCodeSymbolSvg({ type: 'qrcode', value: String(t.value || ''), format: t.format || 'QR_CODE', foregroundColor: t.foregroundColor });
1899
+ return (_jsx("div", { "data-testid": "designer-component-qrcode-content", style: { width: '100%', height: '100%', overflow: 'hidden', backgroundColor: '#fff' }, dangerouslySetInnerHTML: { __html: symbol.svg } }));
1900
+ }
1901
+ case 'checkbox': {
1902
+ const t = comp;
1903
+ const checked = readDesignBoolean(t.checked);
1904
+ return (_jsxs("div", { "data-testid": "designer-component-checkbox-content", style: { display: 'flex', alignItems: 'center', height: '100%', gap: 4, overflow: 'hidden' }, children: [_jsx("span", { style: { width: 13, height: 13, border: '1px solid #333', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, lineHeight: 1, flex: '0 0 auto' }, children: checked ? '✓' : '' }), t.label ? _jsx("span", { style: { fontSize: 11, color: '#111', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: t.label }) : null] }));
1905
+ }
1906
+ case 'table':
1907
+ return _jsx(TablePreview, { table: comp, bandId: bandId, selectedTableCell: selectedTableCell, editing: tableEditing });
1908
+ case 'chart':
1909
+ return _jsx(DesignerChartPreview, { chart: comp });
1910
+ case 'richtext':
1911
+ return (_jsx("div", { "data-testid": "designer-component-richtext-content", style: { width: '100%', height: '100%', overflow: 'hidden' }, dangerouslySetInnerHTML: { __html: sanitizeRichHtml(comp.html || '') } }));
1912
+ case 'subreport':
1913
+ return (_jsx("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#64748b', fontSize: 10, border: '1px dashed #cbd5e1', backgroundColor: '#f8fafc' }, children: `${labels.subreportPlaceholder}: ${comp.templateUrl || labels.localTemplatePlaceholder}` }));
1914
+ case 'panel':
1915
+ return _jsx(PanelChildrenPreview, { panel: comp });
1916
+ case 'line': {
1917
+ const t = comp;
1918
+ const lw = (t.lineWidth ?? 0.2) * MM_TO_PX;
1919
+ const sx = (t.startX ?? 0) * MM_TO_PX;
1920
+ const sy = (t.startY ?? 0) * MM_TO_PX;
1921
+ const ex = (t.endX ?? comp.width) * MM_TO_PX;
1922
+ const ey = (t.endY ?? comp.height) * MM_TO_PX;
1923
+ const dash = t.lineStyle === 'dashed' ? '6,4' : t.lineStyle === 'dotted' ? '2,2' : undefined;
1924
+ return (_jsx("svg", { width: "100%", height: "100%", style: { overflow: 'visible' }, children: _jsx("line", { x1: sx, y1: sy, x2: ex, y2: ey, stroke: t.lineColor || '#000', strokeWidth: lw, strokeDasharray: dash, strokeLinecap: "round" }) }));
1925
+ }
1926
+ case 'shape': {
1927
+ const t = comp;
1928
+ const bw = (t.borderWidth ?? 0) * MM_TO_PX;
1929
+ const bc = t.borderColor || '#000';
1930
+ const fc = t.fillColor || 'transparent';
1931
+ const bs = t.borderStyle || 'solid';
1932
+ const dash = bs === 'dashed' ? '6,4' : bs === 'dotted' ? '2,2' : undefined;
1933
+ const wp = comp.width * MM_TO_PX;
1934
+ const hp = comp.height * MM_TO_PX;
1935
+ if (t.shapeType === 'rectangle') {
1936
+ return _jsx("svg", { width: "100%", height: "100%", children: _jsx("rect", { x: bw / 2, y: bw / 2, width: wp - bw, height: hp - bw, fill: fc, stroke: bc, strokeWidth: bw, strokeDasharray: dash }) });
1937
+ }
1938
+ else if (t.shapeType === 'ellipse') {
1939
+ return _jsx("svg", { width: "100%", height: "100%", children: _jsx("ellipse", { cx: wp / 2, cy: hp / 2, rx: wp / 2 - bw / 2, ry: hp / 2 - bw / 2, fill: fc, stroke: bc, strokeWidth: bw, strokeDasharray: dash }) });
1940
+ }
1941
+ else if (t.shapeType === 'roundRect') {
1942
+ const r = Math.min(wp, hp) * 0.15;
1943
+ return _jsx("svg", { width: "100%", height: "100%", children: _jsx("rect", { x: bw / 2, y: bw / 2, width: wp - bw, height: hp - bw, rx: r, ry: r, fill: fc, stroke: bc, strokeWidth: bw, strokeDasharray: dash }) });
1944
+ }
1945
+ else if (t.shapeType === 'triangle') {
1946
+ return _jsx("svg", { width: "100%", height: "100%", children: _jsx("polygon", { points: `${wp / 2},${bw} ${wp - bw},${hp - bw} ${bw},${hp - bw}`, fill: fc, stroke: bc, strokeWidth: bw, strokeDasharray: dash }) });
1947
+ }
1948
+ return null;
1949
+ }
1950
+ case 'pagenumber': {
1951
+ const t = comp;
1952
+ return _jsx("div", { "data-testid": "designer-component-pagenumber-content", style: { ...textLikePreviewStyle(t, 'center') }, children: designPageNumberText(t.format) });
1953
+ }
1954
+ case 'datetime': {
1955
+ const t = comp;
1956
+ return _jsx("div", { "data-testid": "designer-component-datetime-content", style: { ...textLikePreviewStyle(t, 'left') }, children: formatDesignDateTime(new Date(), t.format || 'yyyy-MM-dd') });
1957
+ }
1958
+ default:
1959
+ return '';
1960
+ }
1961
+ }
1962
+ function getBandReorderTargetIndex(bandLayouts, draggedBandId, pointerContentY) {
1963
+ const withoutDragged = bandLayouts.filter(item => item.band.id !== draggedBandId);
1964
+ let targetIndex = 0;
1965
+ for (const item of withoutDragged) {
1966
+ const centerY = mmToPx(item.visualY + (BAND_HEADER_MM + item.band.height) / 2);
1967
+ if (pointerContentY > centerY) {
1968
+ targetIndex += 1;
1969
+ }
1970
+ }
1971
+ return targetIndex;
1972
+ }
1973
+ function getBandReorderLineTop(bandLayouts, draggedBandId, targetIndex) {
1974
+ const withoutDragged = bandLayouts.filter(item => item.band.id !== draggedBandId);
1975
+ if (withoutDragged.length === 0 || targetIndex <= 0) {
1976
+ return 0;
1977
+ }
1978
+ const previous = withoutDragged[Math.min(targetIndex - 1, withoutDragged.length - 1)];
1979
+ return mmToPx(previous.visualY + BAND_HEADER_MM + previous.band.height);
1980
+ }
1981
+ function textLikePreviewStyle(component, fallbackAlign) {
1982
+ return {
1983
+ width: '100%',
1984
+ height: '100%',
1985
+ overflow: 'hidden',
1986
+ lineHeight: 1.2,
1987
+ textAlign: component.textAlign || fallbackAlign,
1988
+ display: 'flex',
1989
+ alignItems: verticalAlignToFlex(component.verticalAlign),
1990
+ ...getFontStyle(component.font),
1991
+ };
1992
+ }
1993
+ function verticalAlignToFlex(value) {
1994
+ if (value === 'top')
1995
+ return 'flex-start';
1996
+ if (value === 'bottom')
1997
+ return 'flex-end';
1998
+ return 'center';
1999
+ }
2000
+ const PanelChildrenPreview = ({ panel }) => {
2001
+ const children = (panel.components ?? [])
2002
+ .slice()
2003
+ .sort((a, b) => (a.zOrder ?? 0) - (b.zOrder ?? 0));
2004
+ return (_jsx("div", { "data-testid": "designer-panel-content", style: {
2005
+ position: 'relative',
2006
+ width: '100%',
2007
+ height: '100%',
2008
+ overflow: 'hidden',
2009
+ }, children: children.map(child => (_jsx(PanelChildPreview, { component: child }, child.id))) }));
2010
+ };
2011
+ const DesignerChartPreview = ({ chart }) => {
2012
+ const palette = chart.theme?.customPalette?.length
2013
+ ? chart.theme.customPalette
2014
+ : ['#2f6fed', '#16a34a', '#f59e0b', '#ef4444', '#8b5cf6'];
2015
+ const points = chart.data?.length ? chart.data : createFallbackChartPoints(chart);
2016
+ const values = points.map(point => Number(point.value ?? point.y ?? 0)).filter(Number.isFinite);
2017
+ const max = Math.max(1, ...values.map(value => Math.abs(value)));
2018
+ const title = chart.title?.text;
2019
+ const ct = chart.chartType;
2020
+ return (_jsxs("div", { "data-testid": "designer-component-chart-content", style: {
2021
+ width: '100%',
2022
+ height: '100%',
2023
+ display: 'flex',
2024
+ flexDirection: 'column',
2025
+ overflow: 'hidden',
2026
+ background: chart.backgroundColor || '#fff',
2027
+ }, children: [title ? (_jsx("div", { style: { flex: '0 0 auto', textAlign: 'center', fontSize: 10, color: '#1f2937', lineHeight: '14px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: title })) : null, _jsx("div", { style: { flex: 1, minHeight: 0 }, children: (ct === 'pie' || ct === 'donut' || ct === 'rose')
2028
+ ? _jsx(PieChartPreview, { points: points, palette: palette, donut: ct === 'donut', rose: ct === 'rose' })
2029
+ : ct === 'radar'
2030
+ ? _jsx(RadarChartPreview, { points: points, palette: palette, max: max })
2031
+ : ct === 'funnel'
2032
+ ? _jsx(FunnelChartPreview, { points: points, palette: palette, max: max })
2033
+ : ct === 'heatmap'
2034
+ ? _jsx(HeatmapChartPreview, { points: points, palette: palette, max: max })
2035
+ : ct === 'treeMap' || ct === 'sunburst' || ct === 'circlePacking'
2036
+ ? _jsx(TreeMapChartPreview, { points: points, palette: palette, max: max, type: ct })
2037
+ : _jsx(CartesianChartPreview, { chart: chart, points: points, palette: palette, max: max }) })] }));
2038
+ };
2039
+ function createFallbackChartPoints(chart) {
2040
+ const categories = chart.chartType === 'scatter' ? ['A', 'B', 'C', 'D'] : ['Q1', 'Q2', 'Q3', 'Q4'];
2041
+ const values = chart.chartType === 'scatter' ? [22, 46, 33, 62] : [38, 64, 48, 72];
2042
+ return categories.map((category, index) => ({
2043
+ category,
2044
+ value: values[index],
2045
+ x: chart.chartType === 'scatter' ? index + 1 : null,
2046
+ y: values[index],
2047
+ label: category,
2048
+ raw: {},
2049
+ }));
2050
+ }
2051
+ const PieChartPreview = ({ donut, palette, points, rose }) => {
2052
+ const values = points.map(point => Math.max(0, Number(point.value ?? 0))).filter(Number.isFinite);
2053
+ const total = values.reduce((sum, value) => sum + value, 0) || 1;
2054
+ let angle = 0;
2055
+ const stops = values.map((value, index) => {
2056
+ const start = angle;
2057
+ angle += (value / total) * 360;
2058
+ return `${palette[index % palette.length]} ${start}deg ${angle}deg`;
2059
+ });
2060
+ if (rose) {
2061
+ // Rose chart: sectors with varying radius
2062
+ const maxR = 100;
2063
+ let roseAngle = 0;
2064
+ const sectors = values.map((value, index) => {
2065
+ const sweep = (360 / values.length);
2066
+ const startA = roseAngle;
2067
+ roseAngle += sweep;
2068
+ const r = (value / (Math.max(...values) || 1)) * maxR;
2069
+ return { startA, sweep, r, color: palette[index % palette.length] };
2070
+ });
2071
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsx("svg", { viewBox: "-110 -110 220 220", width: "70%", height: "70%", children: sectors.map((s, i) => {
2072
+ const midA = ((s.startA + s.sweep / 2) * Math.PI) / 180;
2073
+ const x1 = Math.cos(((s.startA) * Math.PI) / 180) * s.r;
2074
+ const y1 = Math.sin(((s.startA) * Math.PI) / 180) * s.r;
2075
+ const x2 = Math.cos(((s.startA + s.sweep) * Math.PI) / 180) * s.r;
2076
+ const y2 = Math.sin(((s.startA + s.sweep) * Math.PI) / 180) * s.r;
2077
+ return _jsx("path", { d: `M0,0 L${x1},${y1} A${s.r},${s.r} 0 0,1 ${x2},${y2} Z`, fill: s.color, opacity: 0.8, stroke: "#fff", strokeWidth: 1 }, i);
2078
+ }) }) }));
2079
+ }
2080
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsx("div", { style: {
2081
+ width: '70%',
2082
+ aspectRatio: '1 / 1',
2083
+ borderRadius: '50%',
2084
+ background: `conic-gradient(${stops.join(', ')})`,
2085
+ position: 'relative',
2086
+ boxShadow: 'inset 0 0 0 1px rgba(15,23,42,0.08)',
2087
+ }, children: donut ? (_jsx("div", { style: { position: 'absolute', inset: '30%', borderRadius: '50%', background: '#fff' } })) : null }) }));
2088
+ };
2089
+ const CartesianChartPreview = ({ chart, max, palette, points }) => {
2090
+ const width = 180;
2091
+ const height = 92;
2092
+ const left = 18;
2093
+ const right = 8;
2094
+ const top = 8;
2095
+ const bottom = 18;
2096
+ const plotWidth = width - left - right;
2097
+ const plotHeight = height - top - bottom;
2098
+ const xFor = (index) => left + (points.length <= 1 ? plotWidth / 2 : (index / (points.length - 1)) * plotWidth);
2099
+ const yFor = (value) => top + plotHeight - ((Number(value ?? 0) / max) * plotHeight);
2100
+ const linePoints = points.map((point, index) => `${xFor(index)},${yFor(point.value)}`).join(' ');
2101
+ const areaPoints = `${left},${top + plotHeight} ${linePoints} ${left + plotWidth},${top + plotHeight}`;
2102
+ const barWidth = Math.max(4, plotWidth / Math.max(1, points.length) * 0.58);
2103
+ if (chart.chartType === 'scatter') {
2104
+ return (_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(ChartAxes, { left: left, top: top, plotWidth: plotWidth, plotHeight: plotHeight, showGrid: chart.axes?.x?.gridVisible ?? chart.axes?.y?.gridVisible ?? true }), points.map((point, index) => (_jsx("circle", { cx: xFor(index), cy: yFor(point.value), r: 3, fill: palette[index % palette.length] }, index)))] }));
2105
+ }
2106
+ if (chart.chartType === 'bar' || chart.chartType === 'barParallel' || chart.chartType === 'barPercent') {
2107
+ const rowHeight = plotHeight / Math.max(1, points.length);
2108
+ return (_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(ChartAxes, { left: left, top: top, plotWidth: plotWidth, plotHeight: plotHeight, showGrid: chart.axes?.x?.gridVisible ?? chart.axes?.y?.gridVisible ?? true }), points.map((point, index) => (_jsx("rect", { x: left, y: top + index * rowHeight + rowHeight * 0.22, width: (Number(point.value ?? 0) / max) * plotWidth, height: Math.max(3, rowHeight * 0.56), fill: palette[index % palette.length], rx: 1 }, index)))] }));
2109
+ }
2110
+ if (chart.chartType === 'column' || chart.chartType === 'columnParallel' || chart.chartType === 'columnPercent') {
2111
+ return (_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(ChartAxes, { left: left, top: top, plotWidth: plotWidth, plotHeight: plotHeight, showGrid: chart.axes?.x?.gridVisible ?? chart.axes?.y?.gridVisible ?? true }), points.map((point, index) => {
2112
+ const barHeight = (Number(point.value ?? 0) / max) * plotHeight;
2113
+ return (_jsx("rect", { x: xFor(index) - barWidth / 2, y: top + plotHeight - barHeight, width: barWidth, height: barHeight, fill: palette[index % palette.length], rx: 1 }, index));
2114
+ })] }));
2115
+ }
2116
+ return (_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(ChartAxes, { left: left, top: top, plotWidth: plotWidth, plotHeight: plotHeight, showGrid: chart.axes?.x?.gridVisible ?? chart.axes?.y?.gridVisible ?? true }), chart.chartType === 'area' ? _jsx("polygon", { points: areaPoints, fill: palette[0], opacity: 0.22 }) : null, _jsx("polyline", { points: linePoints, fill: "none", stroke: palette[0], strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round" }), points.map((point, index) => _jsx("circle", { cx: xFor(index), cy: yFor(point.value), r: 2.2, fill: palette[index % palette.length] }, index))] }));
2117
+ };
2118
+ const ChartAxes = ({ left, plotHeight, plotWidth, showGrid, top }) => (_jsxs(_Fragment, { children: [showGrid ? [0.25, 0.5, 0.75].map(ratio => (_jsx("line", { x1: left, y1: top + plotHeight * ratio, x2: left + plotWidth, y2: top + plotHeight * ratio, stroke: "#dbe3ef", strokeWidth: 0.7 }, ratio))) : null, _jsx("line", { x1: left, y1: top, x2: left, y2: top + plotHeight, stroke: "#94a3b8", strokeWidth: 1 }), _jsx("line", { x1: left, y1: top + plotHeight, x2: left + plotWidth, y2: top + plotHeight, stroke: "#94a3b8", strokeWidth: 1 })] }));
2119
+ const RadarChartPreview = ({ points, palette, max }) => {
2120
+ const cx = 100, cy = 100, r = 80;
2121
+ const n = Math.max(3, points.length);
2122
+ const axisAngles = Array.from({ length: n }, (_, i) => (i * 2 * Math.PI) / n - Math.PI / 2);
2123
+ const gridLevels = [0.33, 0.66, 1.0];
2124
+ const dataR = points.map(p => (Number(p.value ?? 0) / max) * r);
2125
+ const dataPath = dataR.map((dr, i) => {
2126
+ const a = axisAngles[i % n];
2127
+ return `${cx + Math.cos(a) * dr},${cy + Math.sin(a) * dr}`;
2128
+ }).join(' ');
2129
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsxs("svg", { viewBox: "0 0 200 200", width: "70%", height: "70%", children: [gridLevels.map(level => (_jsx("polygon", { points: axisAngles.map(a => `${cx + Math.cos(a) * r * level},${cy + Math.sin(a) * r * level}`).join(' '), fill: "none", stroke: "#dbe3ef", strokeWidth: 0.8 }, level))), axisAngles.map((a, i) => (_jsx("line", { x1: cx, y1: cy, x2: cx + Math.cos(a) * r, y2: cy + Math.sin(a) * r, stroke: "#cbd5e1", strokeWidth: 0.6 }, i))), _jsx("polygon", { points: dataPath, fill: palette[0], fillOpacity: 0.25, stroke: palette[0], strokeWidth: 1.5 }), dataR.map((dr, i) => {
2130
+ const a = axisAngles[i % n];
2131
+ return _jsx("circle", { cx: cx + Math.cos(a) * dr, cy: cy + Math.sin(a) * dr, r: 2.5, fill: palette[i % palette.length] }, i);
2132
+ })] }) }));
2133
+ };
2134
+ const FunnelChartPreview = ({ points, palette, max }) => {
2135
+ const w = 180, h = 100;
2136
+ const gap = 3;
2137
+ const rowH = Math.max(6, (h - gap * (points.length - 1)) / Math.max(1, points.length));
2138
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsx("svg", { viewBox: `0 0 ${w} ${h}`, width: "90%", height: "90%", children: points.map((point, i) => {
2139
+ const ratio = Math.max(0.15, Number(point.value ?? 0) / max);
2140
+ const barW = ratio * (w * 0.85);
2141
+ const x = (w - barW) / 2;
2142
+ const y = i * (rowH + gap);
2143
+ return _jsx("rect", { x: x, y: y, width: barW, height: rowH, rx: 2, fill: palette[i % palette.length], opacity: 0.85 }, i);
2144
+ }) }) }));
2145
+ };
2146
+ const HeatmapChartPreview = ({ points, palette }) => {
2147
+ const cols = Math.min(4, points.length);
2148
+ const rows = Math.ceil(points.length / cols);
2149
+ const cellW = 36, cellH = 20, gap = 2;
2150
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsx("svg", { viewBox: `0 0 ${cols * (cellW + gap)} ${rows * (cellH + gap)}`, width: "85%", height: "85%", children: points.map((point, i) => {
2151
+ const col = i % cols;
2152
+ const row = Math.floor(i / cols);
2153
+ const intensity = Math.max(0.15, Number(point.value ?? 0) / (Math.max(...points.map(p => Number(p.value ?? 0))) || 1));
2154
+ return _jsx("rect", { x: col * (cellW + gap), y: row * (cellH + gap), width: cellW, height: cellH, rx: 2, fill: palette[0], opacity: intensity }, i);
2155
+ }) }) }));
2156
+ };
2157
+ const TreeMapChartPreview = ({ points, palette, max, type }) => {
2158
+ if (type === 'sunburst') {
2159
+ // Concentric rings
2160
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsxs("svg", { viewBox: "-110 -110 220 220", width: "70%", height: "70%", children: [_jsx("circle", { cx: 0, cy: 0, r: 90, fill: "#f1f5f9", stroke: "#e2e8f0", strokeWidth: 1 }), _jsx("circle", { cx: 0, cy: 0, r: 55, fill: "#fff", stroke: "#e2e8f0", strokeWidth: 1 }), _jsx("circle", { cx: 0, cy: 0, r: 25, fill: palette[0], opacity: 0.3 }), points.slice(0, 5).map((_, i) => {
2161
+ const a1 = (i / 5) * 2 * Math.PI - Math.PI / 2;
2162
+ const a2 = ((i + 1) / 5) * 2 * Math.PI - Math.PI / 2;
2163
+ const x1 = Math.cos(a1) * 90, y1 = Math.sin(a1) * 90;
2164
+ const x2 = Math.cos(a2) * 90, y2 = Math.sin(a2) * 90;
2165
+ const ix1 = Math.cos(a1) * 55, iy1 = Math.sin(a1) * 55;
2166
+ const ix2 = Math.cos(a2) * 55, iy2 = Math.sin(a2) * 55;
2167
+ return _jsx("path", { d: `M${ix1},${iy1} L${x1},${y1} A90,90 0 0,1 ${x2},${y2} L${ix2},${iy2} A55,55 0 0,0 ${ix1},${iy1}`, fill: palette[i % palette.length], opacity: 0.6, stroke: "#fff", strokeWidth: 1 }, i);
2168
+ })] }) }));
2169
+ }
2170
+ if (type === 'circlePacking') {
2171
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsxs("svg", { viewBox: "0 0 200 200", width: "70%", height: "70%", children: [_jsx("circle", { cx: 100, cy: 100, r: 90, fill: "#f8fafc", stroke: "#e2e8f0", strokeWidth: 1 }), points.slice(0, 5).map((p, i) => {
2172
+ const r = Math.max(8, (Number(p.value ?? 0) / max) * 35);
2173
+ const a = (i / 5) * 2 * Math.PI;
2174
+ const cx = 100 + Math.cos(a) * 45;
2175
+ const cy = 100 + Math.sin(a) * 45;
2176
+ return _jsx("circle", { cx: cx, cy: cy, r: r, fill: palette[i % palette.length], opacity: 0.6, stroke: "#fff", strokeWidth: 1 }, i);
2177
+ })] }) }));
2178
+ }
2179
+ // TreeMap: nested rectangles
2180
+ const w = 180, h = 100;
2181
+ const sorted = [...points].sort((a, b) => Number(b.value ?? 0) - Number(a.value ?? 0));
2182
+ const totalVal = sorted.reduce((s, p) => s + Math.max(0, Number(p.value ?? 0)), 0) || 1;
2183
+ let cursor = 0;
2184
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'grid', placeItems: 'center' }, children: _jsx("svg", { viewBox: `0 0 ${w} ${h}`, width: "90%", height: "90%", children: sorted.map((p, i) => {
2185
+ const frac = Math.max(0, Number(p.value ?? 0)) / totalVal;
2186
+ const rw = frac * w;
2187
+ const x = cursor;
2188
+ cursor += rw;
2189
+ return _jsx("rect", { x: x, y: 0, width: Math.max(2, rw - 1), height: h, rx: 2, fill: palette[i % palette.length], opacity: 0.7, stroke: "#fff", strokeWidth: 1 }, i);
2190
+ }) }) }));
2191
+ };
2192
+ const PanelChildPreview = ({ component }) => {
2193
+ const { t } = useDesignerI18n();
2194
+ return (_jsxs("div", { style: {
2195
+ position: 'absolute',
2196
+ left: safeCssNumber(mmToPx(component.x)),
2197
+ top: safeCssNumber(mmToPx(component.y)),
2198
+ width: safeCssNumber(mmToPx(component.width)),
2199
+ height: safeCssNumber(mmToPx(component.height)),
2200
+ boxSizing: 'border-box',
2201
+ overflow: 'hidden',
2202
+ padding: 0,
2203
+ ...getCompStyle(component),
2204
+ }, children: [getCompContent(component, '', null, {
2205
+ imagePlaceholder: t('canvas.imagePlaceholder'),
2206
+ subreportPlaceholder: t('canvas.subreportPlaceholder'),
2207
+ localTemplatePlaceholder: t('canvas.localTemplatePlaceholder'),
2208
+ }), _jsx(ComponentBorderOverlay, { component: component, zIndex: 150 })] }));
2209
+ };
2210
+ function readDesignBoolean(value) {
2211
+ if (typeof value === 'boolean')
2212
+ return value;
2213
+ const normalized = String(value ?? '').trim().toLowerCase();
2214
+ if (['true', '1', 'yes', 'y'].includes(normalized))
2215
+ return true;
2216
+ if (['false', '0', 'no', 'n', ''].includes(normalized))
2217
+ return false;
2218
+ return Boolean(value);
2219
+ }
2220
+ function designPageNumberText(format) {
2221
+ switch (format) {
2222
+ case '1':
2223
+ return '1';
2224
+ case 'Page 1':
2225
+ return 'Page 1';
2226
+ case 'Page 1 of N':
2227
+ return 'Page 1 of 1';
2228
+ case '1/N':
2229
+ default:
2230
+ return '1/1';
2231
+ }
2232
+ }
2233
+ function formatDesignDateTime(date, pattern) {
2234
+ const parts = {
2235
+ yyyy: String(date.getFullYear()).padStart(4, '0'),
2236
+ MM: String(date.getMonth() + 1).padStart(2, '0'),
2237
+ dd: String(date.getDate()).padStart(2, '0'),
2238
+ HH: String(date.getHours()).padStart(2, '0'),
2239
+ mm: String(date.getMinutes()).padStart(2, '0'),
2240
+ ss: String(date.getSeconds()).padStart(2, '0'),
2241
+ };
2242
+ return pattern.replace(/yyyy|MM|dd|HH|mm|ss/g, token => parts[token] ?? token);
2243
+ }
2244
+ function updateTableCellText(table, rowIndex, columnIndex, text) {
2245
+ const normalized = normalizeTable(table);
2246
+ return {
2247
+ ...normalized,
2248
+ rows: normalized.rows?.map((row, currentRowIndex) => (currentRowIndex !== rowIndex
2249
+ ? row
2250
+ : {
2251
+ ...row,
2252
+ cells: row.cells.map((cell, currentColumnIndex) => (currentColumnIndex === columnIndex ? { ...cell, text } : cell)),
2253
+ })),
2254
+ };
2255
+ }
2256
+ const TablePreview = ({ table, bandId, selectedTableCell, editing }) => {
2257
+ const normalized = normalizeTable(table);
2258
+ const rows = normalized.rows ?? [];
2259
+ const rowCount = rows.length;
2260
+ const rowHeights = rows.map(row => row.height ?? 8);
2261
+ const rowTops = rowHeights.reduce((tops, height, index) => {
2262
+ tops[index + 1] = (tops[index] ?? 0) + height;
2263
+ return tops;
2264
+ }, [0]);
2265
+ const coveredCells = new Set();
2266
+ const cells = [];
2267
+ const borderLines = [];
2268
+ rows.forEach((row, rowIndex) => {
2269
+ const widths = resolveTableRowCellWidths(row, normalized.width);
2270
+ const columnLefts = widths.reduce((lefts, width, index) => {
2271
+ lefts[index + 1] = (lefts[index] ?? 0) + width;
2272
+ return lefts;
2273
+ }, [0]);
2274
+ row.cells.forEach((cell, columnIndex) => {
2275
+ if (coveredCells.has(`${rowIndex}-${columnIndex}`))
2276
+ return;
2277
+ const rowSpan = Math.max(1, Math.min(cell.rowSpan ?? 1, rowCount - rowIndex));
2278
+ const colSpan = Math.max(1, Math.min(cell.colSpan ?? 1, row.cells.length - columnIndex));
2279
+ for (let r = rowIndex; r < rowIndex + rowSpan; r += 1) {
2280
+ for (let c = columnIndex; c < columnIndex + colSpan; c += 1) {
2281
+ if (r === rowIndex && c === columnIndex)
2282
+ continue;
2283
+ coveredCells.add(`${r}-${c}`);
2284
+ }
2285
+ }
2286
+ const isSelected = Boolean(selectedTableCell
2287
+ && selectedTableCell.tableId === normalized.id
2288
+ && rowIndex >= selectedTableCell.startRow
2289
+ && rowIndex <= selectedTableCell.endRow
2290
+ && columnIndex >= selectedTableCell.startColumn
2291
+ && columnIndex <= selectedTableCell.endColumn);
2292
+ const inheritedStyle = resolveTableCellStyle(normalized, row, cell);
2293
+ const border = resolveCollapsedCellBorder(normalized, row, cell, rowIndex, columnIndex);
2294
+ const width = widths.slice(columnIndex, columnIndex + colSpan).reduce((sum, item) => sum + item, 0);
2295
+ const height = rowHeights.slice(rowIndex, rowIndex + rowSpan).reduce((sum, item) => sum + item, 0);
2296
+ const leftPx = safeCssNumber(mmToPx(columnLefts[columnIndex] ?? 0));
2297
+ const topPx = safeCssNumber(mmToPx(rowTops[rowIndex] ?? 0));
2298
+ const widthPx = safeCssNumber(mmToPx(width));
2299
+ const heightPx = safeCssNumber(mmToPx(height));
2300
+ const isEditing = Boolean(editing?.editingCell
2301
+ && editing.editingCell.tableId === normalized.id
2302
+ && editing.editingCell.startRow === rowIndex
2303
+ && editing.editingCell.startColumn === columnIndex);
2304
+ const cellSelection = {
2305
+ tableId: normalized.id,
2306
+ bandId,
2307
+ startRow: rowIndex,
2308
+ startColumn: columnIndex,
2309
+ endRow: rowIndex,
2310
+ endColumn: columnIndex,
2311
+ };
2312
+ if (hasVisibleBorder(border)) {
2313
+ borderLines.push(...tableBorderLinesToNodes(border, {
2314
+ key: `${rowIndex}-${columnIndex}`,
2315
+ left: leftPx,
2316
+ top: topPx,
2317
+ width: widthPx,
2318
+ height: heightPx,
2319
+ }));
2320
+ }
2321
+ cells.push(_jsxs("div", { "data-table-id": normalized.id, "data-band-id": bandId, "data-table-row": rowIndex, "data-table-column": columnIndex, "data-testid": `designer-table-cell-${rowIndex}-${columnIndex}`, onDoubleClick: (event) => {
2322
+ event.preventDefault();
2323
+ event.stopPropagation();
2324
+ editing?.onStartTableCellEdit(cellSelection, cell.text ?? '');
2325
+ }, style: {
2326
+ position: 'absolute',
2327
+ left: leftPx,
2328
+ top: topPx,
2329
+ width: widthPx,
2330
+ height: heightPx,
2331
+ boxSizing: 'border-box',
2332
+ minWidth: 0,
2333
+ minHeight: 0,
2334
+ display: 'flex',
2335
+ justifyContent: tableTextAlignToFlex(inheritedStyle.textAlign),
2336
+ alignItems: verticalAlignToFlex(inheritedStyle.verticalAlign),
2337
+ textAlign: inheritedStyle.textAlign ?? 'left',
2338
+ backgroundColor: isSelected ? '#e6f4ff' : inheritedStyle.backgroundColor ?? 'transparent',
2339
+ padding: tablePaddingToCss(inheritedStyle.padding),
2340
+ color: inheritedStyle.font?.color ?? '#333',
2341
+ outline: isSelected ? '2px solid #1677ff' : undefined,
2342
+ outlineOffset: -2,
2343
+ fontFamily: inheritedStyle.font?.family,
2344
+ fontSize: inheritedStyle.font?.size ?? 10,
2345
+ fontWeight: inheritedStyle.font?.bold ? 700 : 400,
2346
+ fontStyle: inheritedStyle.font?.italic ? 'italic' : undefined,
2347
+ textDecoration: tableFontTextDecoration(inheritedStyle.font),
2348
+ lineHeight: 1.2,
2349
+ overflow: 'hidden',
2350
+ whiteSpace: 'nowrap',
2351
+ textOverflow: 'ellipsis',
2352
+ }, children: [isEditing ? (_jsx("input", { autoFocus: true, value: editing?.editText ?? '', "aria-label": "\u5355\u5143\u683C\u6587\u672C", onChange: event => editing?.onEditTextChange(event.target.value), onKeyDown: (event) => {
2353
+ if (event.key === 'Enter' || event.key === 'Escape') {
2354
+ editing?.onFinishTableCellEdit(editing?.editText ?? '');
2355
+ }
2356
+ }, onBlur: () => editing?.onFinishTableCellEdit(editing?.editText ?? ''), onPointerDown: event => event.stopPropagation(), onMouseDown: event => event.stopPropagation(), style: {
2357
+ width: '100%',
2358
+ height: '100%',
2359
+ border: 'none',
2360
+ outline: 'none',
2361
+ background: 'transparent',
2362
+ font: 'inherit',
2363
+ color: 'inherit',
2364
+ textAlign: 'inherit',
2365
+ padding: 0,
2366
+ minWidth: 0,
2367
+ } })) : (_jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: cell.text ?? '' })), columnIndex < row.cells.length - 1 ? (_jsx("div", { "data-table-cell-resize": true, "data-table-id": normalized.id, "data-band-id": bandId, "data-table-row": rowIndex, "data-table-column": columnIndex, "data-cell-width": widths[columnIndex], style: {
2368
+ position: 'absolute',
2369
+ top: 0,
2370
+ right: -3,
2371
+ width: 6,
2372
+ height: '100%',
2373
+ cursor: 'col-resize',
2374
+ zIndex: 4,
2375
+ } })) : null] }, `${rowIndex}-${columnIndex}`));
2376
+ });
2377
+ });
2378
+ return (_jsxs("div", { "data-testid": "designer-table-grid", style: {
2379
+ width: '100%',
2380
+ height: '100%',
2381
+ position: 'relative',
2382
+ boxSizing: 'border-box',
2383
+ backgroundColor: normalized.backgroundColor ?? 'transparent',
2384
+ }, children: [cells, borderLines] }));
2385
+ };
2386
+ function tableFontTextDecoration(font) {
2387
+ const decorations = [
2388
+ font?.underline ? 'underline' : undefined,
2389
+ font?.strikethrough ? 'line-through' : undefined,
2390
+ ].filter(Boolean);
2391
+ return decorations.length ? decorations.join(' ') : undefined;
2392
+ }
2393
+ function tablePaddingToCss(padding) {
2394
+ if (!padding)
2395
+ return '2px 3px';
2396
+ return `${mmToPx(padding.top)}px ${mmToPx(padding.right)}px ${mmToPx(padding.bottom)}px ${mmToPx(padding.left)}px`;
2397
+ }
2398
+ function tableBorderLinesToNodes(border, rect) {
2399
+ const value = `${border.width}mm ${border.style} ${border.color}`;
2400
+ const base = {
2401
+ position: 'absolute',
2402
+ pointerEvents: 'none',
2403
+ zIndex: 3,
2404
+ boxSizing: 'border-box',
2405
+ };
2406
+ const lines = [];
2407
+ if (border.sides.top) {
2408
+ lines.push(_jsx("div", { "data-testid": `designer-table-border-line-${rect.key}-top`, style: { ...base, left: rect.left, top: rect.top, width: rect.width, height: 0, borderTop: value } }, `${rect.key}-top`));
2409
+ }
2410
+ if (border.sides.right) {
2411
+ lines.push(_jsx("div", { "data-testid": `designer-table-border-line-${rect.key}-right`, style: { ...base, left: rect.left + rect.width, top: rect.top, width: 0, height: rect.height, borderLeft: value } }, `${rect.key}-right`));
2412
+ }
2413
+ if (border.sides.bottom) {
2414
+ lines.push(_jsx("div", { "data-testid": `designer-table-border-line-${rect.key}-bottom`, style: { ...base, left: rect.left, top: rect.top + rect.height, width: rect.width, height: 0, borderTop: value } }, `${rect.key}-bottom`));
2415
+ }
2416
+ if (border.sides.left) {
2417
+ lines.push(_jsx("div", { "data-testid": `designer-table-border-line-${rect.key}-left`, style: { ...base, left: rect.left, top: rect.top, width: 0, height: rect.height, borderLeft: value } }, `${rect.key}-left`));
2418
+ }
2419
+ return lines;
2420
+ }
2421
+ function tableTextAlignToFlex(value) {
2422
+ if (value === 'center')
2423
+ return 'center';
2424
+ if (value === 'right')
2425
+ return 'flex-end';
2426
+ return 'flex-start';
2427
+ }
2428
+ //# sourceMappingURL=Canvas.js.map