From 28c6a76ead9b65a0b5861d70f0838ef2a46f5c45 Mon Sep 17 00:00:00 2001
From: zhou zhou <zozhouo3o@gmail.com>
Date: 星期二, 14 四月 2026 10:58:10 +0800
Subject: [PATCH] #barcode
---
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-dialog.vue | 694 +++++++
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/MatnrPrintTemplateMapper.java | 11
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/MatnrPrintTemplateController.java | 101 +
rsf-design/src/views/basic-info/wh-mat/matnrPrintTemplate.helpers.js | 827 ++++++++
rsf-server/src/main/resources/sql/20260413_matnr_print_template_menu.sql | 380 ++++
rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-property-panel.vue | 859 +++++++++
rsf-server/src/main/resources/sql/20260413_matnr_print_template.sql | 19
rsf-design/src/api/wh-mat.js | 45
rsf-design/src/views/basic-info/matnr-print-template/index.vue | 87
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MatnrPrintTemplateServiceImpl.java | 376 ++++
rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-canvas.vue | 571 ++++++
rsf-design/pnpm-lock.yaml | 81
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java | 2
rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-field-panel.vue | 88
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java | 2
rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-toolbar.vue | 86
rsf-design/src/locales/langs/en.json | 167 +
rsf-design/src/locales/langs/zh.json | 167 +
rsf-design/package.json | 1
rsf-design/src/router/adapters/backendMenuAdapter.js | 5
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/MatnrPrintTemplateService.java | 23
rsf-design/src/utils/backend-menu-title.js | 5
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MatnrPrintTemplate.java | 82
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-template-workspace.vue | 749 ++++++++
rsf-design/src/views/basic-info/wh-mat/index.vue | 140
25 files changed, 5,440 insertions(+), 128 deletions(-)
diff --git a/rsf-design/package.json b/rsf-design/package.json
index 8e22c1e..3473115 100644
--- a/rsf-design/package.json
+++ b/rsf-design/package.json
@@ -42,6 +42,7 @@
"element-plus": "^2.11.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.10.0",
+ "jsbarcode": "^3.12.3",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"ohash": "^2.0.11",
diff --git a/rsf-design/pnpm-lock.yaml b/rsf-design/pnpm-lock.yaml
index 6c53174..c146d17 100644
--- a/rsf-design/pnpm-lock.yaml
+++ b/rsf-design/pnpm-lock.yaml
@@ -77,6 +77,9 @@
highlight.js:
specifier: ^11.10.0
version: 11.11.1
+ jsbarcode:
+ specifier: ^3.12.3
+ version: 3.12.3
mitt:
specifier: ^3.0.1
version: 3.0.1
@@ -114,8 +117,8 @@
specifier: ^3.0.20
version: 3.0.23(core-js@3.45.1)
xlsx:
- specifier: ^0.18.5
- version: 0.18.5
+ specifier: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz
+ version: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz
devDependencies:
'@eslint/js':
specifier: ^9.9.1
@@ -1278,10 +1281,6 @@
engines: {node: '>=0.4.0'}
hasBin: true
- adler-32@1.3.1:
- resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
- engines: {node: '>=0.8'}
-
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1373,10 +1372,6 @@
caniuse-lite@1.0.30001745:
resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==}
- cfb@1.2.2:
- resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
- engines: {node: '>=0.8'}
-
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -1396,10 +1391,6 @@
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
-
- codepage@1.15.0:
- resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
- engines: {node: '>=0.8'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
@@ -1448,11 +1439,6 @@
peerDependenciesMeta:
typescript:
optional: true
-
- crc-32@1.2.2:
- resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
- engines: {node: '>=0.8'}
- hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
@@ -1831,10 +1817,6 @@
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
- frac@1.1.2:
- resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
- engines: {node: '>=0.8'}
-
fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
@@ -2077,6 +2059,9 @@
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
+
+ jsbarcode@3.12.3:
+ resolution: {integrity: sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==}
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
@@ -2691,10 +2676,6 @@
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
- ssf@0.11.2:
- resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
- engines: {node: '>=0.8'}
-
ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
@@ -3063,17 +3044,9 @@
wildcard@1.1.2:
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
- wmf@1.0.2:
- resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
- engines: {node: '>=0.8'}
-
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
-
- word@0.3.0:
- resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
- engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
@@ -3097,8 +3070,9 @@
peerDependencies:
core-js: '>=3.12.1'
- xlsx@0.18.5:
- resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
+ xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz:
+ resolution: {integrity: sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA==, tarball: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz}
+ version: 0.20.2
engines: {node: '>=0.8'}
hasBin: true
@@ -4166,8 +4140,6 @@
acorn@8.15.0: {}
- adler-32@1.3.1: {}
-
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -4263,11 +4235,6 @@
caniuse-lite@1.0.30001745: {}
- cfb@1.2.2:
- dependencies:
- adler-32: 1.3.1
- crc-32: 1.2.2
-
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -4296,8 +4263,6 @@
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
-
- codepage@1.15.0: {}
color-convert@2.0.1:
dependencies:
@@ -4337,8 +4302,6 @@
parse-json: 5.2.0
optionalDependencies:
typescript: 5.6.3
-
- crc-32@1.2.2: {}
cross-spawn@7.0.6:
dependencies:
@@ -4775,8 +4738,6 @@
hasown: 2.0.2
mime-types: 2.1.35
- frac@1.1.2: {}
-
fs-extra@10.1.0:
dependencies:
graceful-fs: 4.2.11
@@ -4979,6 +4940,8 @@
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
+
+ jsbarcode@3.12.3: {}
jsesc@3.1.0: {}
@@ -5497,10 +5460,6 @@
speakingurl@14.0.1: {}
- ssf@0.11.2:
- dependencies:
- frac: 1.1.2
-
ssr-window@3.0.0: {}
string-width@4.2.3:
@@ -5932,11 +5891,7 @@
wildcard@1.1.2: {}
- wmf@1.0.2: {}
-
word-wrap@1.2.5: {}
-
- word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
@@ -5967,15 +5922,7 @@
eventemitter3: 4.0.7
xgplayer-subtitles: 3.0.23(core-js@3.45.1)
- xlsx@0.18.5:
- dependencies:
- adler-32: 1.3.1
- cfb: 1.2.2
- codepage: 1.15.0
- crc-32: 1.2.2
- ssf: 0.11.2
- wmf: 1.0.2
- word: 0.3.0
+ xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz: {}
xml-name-validator@4.0.0: {}
diff --git a/rsf-design/src/api/wh-mat.js b/rsf-design/src/api/wh-mat.js
index 2bc5a50..ef7e9fd 100644
--- a/rsf-design/src/api/wh-mat.js
+++ b/rsf-design/src/api/wh-mat.js
@@ -195,3 +195,48 @@
}
})
}
+
+export function fetchMatnrPrintTemplateList(params = {}) {
+ return request.post({
+ url: '/matnrPrintTemplate/list',
+ params
+ })
+}
+
+export function fetchGetMatnrPrintTemplateDetail(id) {
+ return request.get({
+ url: `/matnrPrintTemplate/${id}`
+ })
+}
+
+export function fetchGetDefaultMatnrPrintTemplate() {
+ return request.get({
+ url: '/matnrPrintTemplate/default'
+ })
+}
+
+export function fetchSaveMatnrPrintTemplate(params = {}) {
+ return request.post({
+ url: '/matnrPrintTemplate/save',
+ params
+ })
+}
+
+export function fetchUpdateMatnrPrintTemplate(params = {}) {
+ return request.post({
+ url: '/matnrPrintTemplate/update',
+ params
+ })
+}
+
+export function fetchRemoveMatnrPrintTemplate(ids) {
+ return request.post({
+ url: `/matnrPrintTemplate/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSetDefaultMatnrPrintTemplate(id) {
+ return request.post({
+ url: `/matnrPrintTemplate/default/${id}`
+ })
+}
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
index 48b53ed..6d3187d 100644
--- a/rsf-design/src/locales/langs/en.json
+++ b/rsf-design/src/locales/langs/en.json
@@ -480,6 +480,7 @@
"customer": "Customer",
"shipper": "shipper",
"matnr": "Matnr",
+ "matnrPrintTemplate": "Material Print Templates",
"matnrGroup": "MatnrGroup",
"warehouse": "Warehouse",
"warehouseAreas": "WarehouseAreas",
@@ -2844,6 +2845,172 @@
"groupId": "Please select material group"
}
},
+ "printTemplate": {
+ "page": {
+ "messages": {
+ "enabledFieldsTimeout": "Dynamic fields loading timed out",
+ "previewTimeout": "Sample material loading timed out",
+ "initFailed": "Failed to initialize the print template page"
+ }
+ },
+ "workspace": {
+ "sidebarTitle": "Template List",
+ "sidebarSubtitle": "Tenant-shared multi-template management",
+ "defaultTag": "Default",
+ "unnamedTemplate": "Untitled Template",
+ "previewRecord": "Preview Record: {name}",
+ "noPreviewRecord": "No sample data",
+ "emptyTemplate": "No templates yet. Please create one first.",
+ "actions": {
+ "copy": "Copy",
+ "setDefault": "Set Default",
+ "saveTemplate": "Save Template",
+ "delete": "Delete"
+ },
+ "messages": {
+ "loadFailed": "Failed to load templates",
+ "deleteConfirm": "Delete template \"{name}\"?",
+ "deleteSuccess": "Template deleted",
+ "deleteFailed": "Failed to delete template",
+ "defaultSuccess": "Default template updated",
+ "defaultFailed": "Failed to set default template",
+ "saveSuccess": "Template saved",
+ "saveFailed": "Failed to save template"
+ }
+ },
+ "toolbar": {
+ "zoomOut": "Zoom Out",
+ "zoomIn": "Zoom In",
+ "zoomFit": "Auto Fit",
+ "elements": {
+ "text": "Text",
+ "barcode": "Barcode",
+ "qrcode": "QR Code",
+ "line": "Line",
+ "rect": "Rectangle",
+ "table": "Field Table"
+ }
+ },
+ "fieldPanel": {
+ "title": "Available Fields",
+ "fixedCategory": "Fixed Fields",
+ "extendCategory": "Extended Fields"
+ },
+ "propertyPanel": {
+ "sections": {
+ "templateInfo": "Template Info",
+ "canvas": "Canvas Settings",
+ "element": "Element Properties",
+ "fieldList": "Field List"
+ },
+ "fields": {
+ "templateName": "Template Name",
+ "templateCode": "Template Code",
+ "defaultTemplate": "Default Template",
+ "widthMm": "Width (mm)",
+ "heightMm": "Height (mm)",
+ "backgroundColor": "Background Color",
+ "gridSizeMm": "Grid (mm)",
+ "elementType": "Element Type",
+ "visible": "Visible",
+ "zIndex": "Z-Index",
+ "contentMode": "Content Mode",
+ "textContent": "Text Content",
+ "fontSizeMm": "Font Size (mm)",
+ "fontWeight": "Font Weight",
+ "textAlign": "Alignment",
+ "textColor": "Text Color",
+ "valueTemplate": "Value Template",
+ "symbology": "Symbology",
+ "showText": "Show Text",
+ "direction": "Direction",
+ "lineWidthMm": "Line Width (mm)",
+ "lineColor": "Line Color",
+ "borderWidthMm": "Border Width (mm)",
+ "radiusMm": "Radius (mm)",
+ "borderColor": "Border Color",
+ "fillColor": "Fill Color",
+ "leftMode": "Left Column Mode",
+ "rightMode": "Right Column Mode",
+ "rowHeightMm": "Row Height (mm)",
+ "leftContent": "Left Content",
+ "rightContent": "Right Content"
+ },
+ "options": {
+ "staticText": "Static Text",
+ "templateText": "Template Placeholder",
+ "template": "Template",
+ "alignLeft": "Left",
+ "alignCenter": "Center",
+ "alignRight": "Right",
+ "horizontal": "Horizontal",
+ "vertical": "Vertical"
+ },
+ "actions": {
+ "removeElement": "Delete Element",
+ "addRow": "Add Row"
+ },
+ "empty": "Select an element on the canvas to edit its position, size, and content here.",
+ "rowTitle": "Row {index}",
+ "defaultFieldLabel": "Field Label"
+ },
+ "dialog": {
+ "title": "Print",
+ "templatePlaceholder": "Please select a template",
+ "copyCount": "Copies",
+ "labelCount": "{count} labels",
+ "noTemplate": "No print templates are available for the current tenant. Please create one from the left-side menu \"Material Print Templates\" first.",
+ "noData": "No printable data",
+ "canvasOverflow": "Some elements extend beyond the canvas bounds",
+ "canvasOverflowDescription": "Printing still uses the template canvas size as the source of truth. Anything outside the canvas will be clipped, so please adjust the layout in the template editor first.",
+ "confirmPrint": "Print",
+ "printDocumentTitle": "Material Print",
+ "messages": {
+ "loadFailed": "Failed to load templates",
+ "detailFailed": "Failed to load template details",
+ "popupBlocked": "The browser blocked the print window. Please check the popup settings."
+ }
+ },
+ "helpers": {
+ "defaultTemplateName": "Material Label Template",
+ "copySuffix": " - Copy",
+ "defaultText": {
+ "productCode": "Product Code",
+ "productName": "Product",
+ "staticText": "Static Text"
+ },
+ "tableLabels": {
+ "code": "Material Code",
+ "name": "Material Name",
+ "spec": "Specification",
+ "model": "Model"
+ },
+ "fixedFields": {
+ "id": "Material ID",
+ "code": "Material Code",
+ "name": "Material Name",
+ "groupName": "Material Group",
+ "barcode": "Barcode",
+ "spec": "Specification",
+ "model": "Model",
+ "color": "Color",
+ "size": "Size",
+ "unit": "Unit",
+ "purUnit": "Purchase Unit",
+ "stockUnit": "Stock Unit",
+ "stockLevelText": "Stock Level",
+ "flagLabelManageText": "Label Management",
+ "flagCheckText": "Exempt Inspection",
+ "statusText": "Status",
+ "describle": "Description",
+ "memo": "Memo",
+ "createByText": "Created By",
+ "createTimeText": "Created At",
+ "updateByText": "Updated By",
+ "updateTimeText": "Updated At"
+ }
+ }
+ },
"batchDialog": {
"titles": {
"status": "Batch Update Status",
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index 5af6421..611f044 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -480,6 +480,7 @@
"customer": "瀹㈡埛琛�",
"shipper": "璐т富淇℃伅",
"matnr": "鐗╂枡",
+ "matnrPrintTemplate": "鐗╂枡鎵撳嵃妯℃澘",
"matnrGroup": "鐗╂枡鍒嗙粍",
"warehouse": "浠撳簱",
"warehouseAreas": "搴撳尯",
@@ -2852,6 +2853,172 @@
"groupId": "璇烽�夋嫨鐗╂枡鍒嗙粍"
}
},
+ "printTemplate": {
+ "page": {
+ "messages": {
+ "enabledFieldsTimeout": "鎵╁睍瀛楁鍔犺浇瓒呮椂",
+ "previewTimeout": "鐗╂枡绀轰緥鏁版嵁鍔犺浇瓒呮椂",
+ "initFailed": "鎵撳嵃妯℃澘椤甸潰鍒濆鍖栧け璐�"
+ }
+ },
+ "workspace": {
+ "sidebarTitle": "妯℃澘鍒楄〃",
+ "sidebarSubtitle": "绉熸埛鍏变韩锛屽妯℃澘绠$悊",
+ "defaultTag": "榛樿",
+ "unnamedTemplate": "鏈懡鍚嶆ā鏉�",
+ "previewRecord": "棰勮璁板綍锛歿name}",
+ "noPreviewRecord": "鏆傛棤绀轰緥鏁版嵁",
+ "emptyTemplate": "鏆傛棤妯℃澘锛岃鍏堟柊澧炴ā鏉�",
+ "actions": {
+ "copy": "澶嶅埗",
+ "setDefault": "璁句负榛樿",
+ "saveTemplate": "淇濆瓨妯℃澘",
+ "delete": "鍒犻櫎"
+ },
+ "messages": {
+ "loadFailed": "妯℃澘鍔犺浇澶辫触",
+ "deleteConfirm": "纭鍒犻櫎妯℃澘銆寋name}銆嶅悧锛�",
+ "deleteSuccess": "妯℃澘宸插垹闄�",
+ "deleteFailed": "妯℃澘鍒犻櫎澶辫触",
+ "defaultSuccess": "榛樿妯℃澘宸叉洿鏂�",
+ "defaultFailed": "榛樿妯℃澘璁剧疆澶辫触",
+ "saveSuccess": "妯℃澘淇濆瓨鎴愬姛",
+ "saveFailed": "妯℃澘淇濆瓨澶辫触"
+ }
+ },
+ "toolbar": {
+ "zoomOut": "缂╁皬",
+ "zoomIn": "鏀惧ぇ",
+ "zoomFit": "鑷�傚簲",
+ "elements": {
+ "text": "鏂囨湰",
+ "barcode": "涓�缁寸爜",
+ "qrcode": "浜岀淮鐮�",
+ "line": "鐩寸嚎",
+ "rect": "鐭╁舰",
+ "table": "瀛楁琛ㄦ牸"
+ }
+ },
+ "fieldPanel": {
+ "title": "鍙敤瀛楁",
+ "fixedCategory": "鍥哄畾瀛楁",
+ "extendCategory": "鎵╁睍瀛楁"
+ },
+ "propertyPanel": {
+ "sections": {
+ "templateInfo": "妯℃澘淇℃伅",
+ "canvas": "鐢诲竷璁剧疆",
+ "element": "鍏冪礌灞炴��",
+ "fieldList": "瀛楁娓呭崟"
+ },
+ "fields": {
+ "templateName": "妯℃澘鍚嶇О",
+ "templateCode": "妯℃澘缂栫爜",
+ "defaultTemplate": "榛樿妯℃澘",
+ "widthMm": "瀹藉害(mm)",
+ "heightMm": "楂樺害(mm)",
+ "backgroundColor": "鑳屾櫙鑹�",
+ "gridSizeMm": "缃戞牸(mm)",
+ "elementType": "鍏冪礌绫诲瀷",
+ "visible": "鏄剧ず",
+ "zIndex": "灞傜骇",
+ "contentMode": "鍐呭妯″紡",
+ "textContent": "鏂囨湰鍐呭",
+ "fontSizeMm": "瀛楀彿(mm)",
+ "fontWeight": "瀛楅噸",
+ "textAlign": "瀵归綈",
+ "textColor": "鏂囧瓧棰滆壊",
+ "valueTemplate": "鐮佸�兼ā鏉�",
+ "symbology": "缂栫爜鍒跺紡",
+ "showText": "鏄剧ず鏂囧瓧",
+ "direction": "鏂瑰悜",
+ "lineWidthMm": "绮楃粏(mm)",
+ "lineColor": "绾挎潯棰滆壊",
+ "borderWidthMm": "杈规瀹藉害(mm)",
+ "radiusMm": "鍦嗚(mm)",
+ "borderColor": "杈规棰滆壊",
+ "fillColor": "濉厖棰滆壊",
+ "leftMode": "宸﹀垪妯″紡",
+ "rightMode": "鍙冲垪妯″紡",
+ "rowHeightMm": "琛岄珮(mm)",
+ "leftContent": "宸﹀垪鍐呭",
+ "rightContent": "鍙冲垪鍐呭"
+ },
+ "options": {
+ "staticText": "闈欐�佹枃鏈�",
+ "templateText": "鍗犱綅绗︽ā鏉�",
+ "template": "妯℃澘",
+ "alignLeft": "宸﹀榻�",
+ "alignCenter": "灞呬腑",
+ "alignRight": "鍙冲榻�",
+ "horizontal": "妯嚎",
+ "vertical": "绔栫嚎"
+ },
+ "actions": {
+ "removeElement": "鍒犻櫎鍏冪礌",
+ "addRow": "鏂板涓�琛�"
+ },
+ "empty": "閫夋嫨鐢诲竷涓殑鍏冪礌鍚庯紝鍦ㄨ繖閲岀紪杈戜綅缃�佸昂瀵稿拰瀛楁鍐呭銆�",
+ "rowTitle": "绗� {index} 琛�",
+ "defaultFieldLabel": "瀛楁鍚�"
+ },
+ "dialog": {
+ "title": "鎵撳嵃",
+ "templatePlaceholder": "璇烽�夋嫨妯℃澘",
+ "copyCount": "鎵撳嵃浠芥暟",
+ "labelCount": "鍏� {count} 鏉℃爣绛�",
+ "noTemplate": "褰撳墠绉熸埛杩樻病鏈夋墦鍗版ā鏉匡紝璇峰厛鍒板乏渚ц彍鍗曗�滅墿鏂欐墦鍗版ā鏉库�濋噷鍒涘缓銆�",
+ "noData": "鏆傛棤鍙墦鍗版暟鎹�",
+ "canvasOverflow": "褰撳墠妯℃澘鏈夊厓绱犺秴鍑轰簡鐢诲竷杈圭晫",
+ "canvasOverflowDescription": "鎵撳嵃浠嶄細涓ユ牸鎸夋ā鏉跨敾甯冨昂瀵歌緭鍑猴紝瓒呭嚭閮ㄥ垎浼氳瑁佸垏锛岃鍏堝洖鍒版ā鏉块〉璋冩暣甯冨眬銆�",
+ "confirmPrint": "纭鎵撳嵃",
+ "printDocumentTitle": "鐗╂枡鎵撳嵃",
+ "messages": {
+ "loadFailed": "妯℃澘鍔犺浇澶辫触",
+ "detailFailed": "妯℃澘璇︽儏鍔犺浇澶辫触",
+ "popupBlocked": "娴忚鍣ㄩ樆姝簡鎵撳嵃绐楀彛锛岃妫�鏌ュ脊绐楄缃�"
+ }
+ },
+ "helpers": {
+ "defaultTemplateName": "鐗╂枡鏍囩妯℃澘",
+ "copySuffix": "-鍓湰",
+ "defaultText": {
+ "productCode": "鍟嗗搧缂栫爜",
+ "productName": "鍟嗗搧",
+ "staticText": "闈欐�佹枃鏈�"
+ },
+ "tableLabels": {
+ "code": "鐗╂枡缂栫爜",
+ "name": "鐗╂枡鍚嶇О",
+ "spec": "瑙勬牸",
+ "model": "鍨嬪彿"
+ },
+ "fixedFields": {
+ "id": "鐗╂枡ID",
+ "code": "鐗╂枡缂栫爜",
+ "name": "鐗╂枡鍚嶇О",
+ "groupName": "鐗╂枡鍒嗙粍",
+ "barcode": "鏉$爜",
+ "spec": "瑙勬牸",
+ "model": "鍨嬪彿",
+ "color": "棰滆壊",
+ "size": "灏哄",
+ "unit": "鍗曚綅",
+ "purUnit": "閲囪喘鍗曚綅",
+ "stockUnit": "搴撲綅鍗曚綅",
+ "stockLevelText": "搴撳瓨绛夌骇",
+ "flagLabelManageText": "鏍囩绠$悊",
+ "flagCheckText": "鍏嶆",
+ "statusText": "鐘舵��",
+ "describle": "鎻忚堪",
+ "memo": "澶囨敞",
+ "createByText": "鍒涘缓浜�",
+ "createTimeText": "鍒涘缓鏃堕棿",
+ "updateByText": "鏇存柊浜�",
+ "updateTimeText": "鏇存柊鏃堕棿"
+ }
+ }
+ },
"batchDialog": {
"titles": {
"status": "鎵归噺淇敼鐘舵��",
diff --git a/rsf-design/src/router/adapters/backendMenuAdapter.js b/rsf-design/src/router/adapters/backendMenuAdapter.js
index 5778434..211c1a4 100644
--- a/rsf-design/src/router/adapters/backendMenuAdapter.js
+++ b/rsf-design/src/router/adapters/backendMenuAdapter.js
@@ -19,6 +19,7 @@
fieldsItem: '/system/fields-item',
whMat: '/basic-info/wh-mat',
matnr: '/basic-info/wh-mat',
+ matnrPrintTemplate: '/basic-info/matnr-print-template',
matnrGroup: '/basic-info/matnr-group',
locType: '/basic-info/loc-type',
taskPathTemplate: '/basic-info/task-path-template',
@@ -171,7 +172,7 @@
return null
}
- const path = resolvePath(node, { component, isFirstLevel, hasChildren, rawRoute })
+ const path = resolvePath(node, { component, isFirstLevel, rawRoute })
const meta = buildMeta(node)
const adapted = {
id: normalizeId(node.id),
@@ -211,7 +212,7 @@
return PHASE_1_COMPONENTS[normalizedKey] || normalizeComponentPath(fullRoutePath)
}
-function resolvePath(node, { component, isFirstLevel, hasChildren, rawRoute }) {
+function resolvePath(node, { component, isFirstLevel, rawRoute }) {
if (rawRoute) {
if (!isFirstLevel && rawRoute.startsWith('/')) {
return normalizeComponentPath(rawRoute)
diff --git a/rsf-design/src/utils/backend-menu-title.js b/rsf-design/src/utils/backend-menu-title.js
index 0f363bc..fe5e7c3 100644
--- a/rsf-design/src/utils/backend-menu-title.js
+++ b/rsf-design/src/utils/backend-menu-title.js
@@ -55,6 +55,7 @@
'menu.locType': '搴撲綅绫诲瀷(搴�)',
'menu.logs': '鏃ュ織',
'menu.matnr': '鐗╂枡',
+ 'menu.matnrPrintTemplate': '鐗╂枡鎵撳嵃妯℃澘',
'menu.matnrGroup': '鐗╂枡鍒嗙粍',
'menu.matnrRoleMenu': '鐗╂枡鏉冮檺',
'menu.menu': '鑿滃崟绠$悊',
@@ -133,8 +134,8 @@
return accumulator
},
{
- '浠〃鐩�': 'menus.dashboard.title',
- '宸ヤ綔鍙�': 'menus.dashboard.console',
+ 浠〃鐩�: 'menus.dashboard.title',
+ 宸ヤ綔鍙�: 'menus.dashboard.console',
'menus.dashboard.title': 'menus.dashboard.title',
'menus.dashboard.console': 'menus.dashboard.console'
}
diff --git a/rsf-design/src/views/basic-info/matnr-print-template/index.vue b/rsf-design/src/views/basic-info/matnr-print-template/index.vue
new file mode 100644
index 0000000..19c1347
--- /dev/null
+++ b/rsf-design/src/views/basic-info/matnr-print-template/index.vue
@@ -0,0 +1,87 @@
+<template>
+ <div class="matnr-print-template-page art-full-height">
+ <WhMatPrintTemplateWorkspace
+ :enabled-fields="enabledFields"
+ :preview-record="previewRecord"
+ :auto-load="true"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { ElMessage } from 'element-plus'
+ import { fetchEnabledFields, fetchMatnrPage } from '@/api/wh-mat'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ buildMatnrPageQueryParams,
+ normalizeMatnrRow,
+ normalizeWhMatEnabledFields
+ } from '../wh-mat/whMatPage.helpers'
+ import WhMatPrintTemplateWorkspace from '../wh-mat/modules/wh-mat-print-template-workspace.vue'
+
+ defineOptions({ name: 'MatnrPrintTemplate' })
+
+ const { t } = useI18n()
+ const enabledFields = ref([])
+ const previewRecord = ref({})
+
+ async function loadEnabledFields() {
+ const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+ timeoutMessage: t('pages.basicInfo.whMat.printTemplate.page.messages.enabledFieldsTimeout')
+ })
+ enabledFields.value = normalizeWhMatEnabledFields(fields)
+ }
+
+ async function loadPreviewRecord() {
+ const response = await guardRequestWithMessage(
+ fetchMatnrPage(
+ buildMatnrPageQueryParams({
+ current: 1,
+ pageSize: 1,
+ orderBy: 'update_time desc'
+ })
+ ),
+ {
+ records: []
+ },
+ {
+ timeoutMessage: t('pages.basicInfo.whMat.printTemplate.page.messages.previewTimeout')
+ }
+ )
+
+ const firstRecord = Array.isArray(response?.records) ? response.records[0] : null
+ previewRecord.value = firstRecord
+ ? normalizeMatnrRow(firstRecord, () => '', enabledFields.value)
+ : {}
+ }
+
+ onMounted(async () => {
+ try {
+ await loadEnabledFields()
+ await loadPreviewRecord()
+ } catch (error) {
+ ElMessage.error(
+ error?.message || t('pages.basicInfo.whMat.printTemplate.page.messages.initFailed')
+ )
+ }
+ })
+</script>
+
+<style scoped>
+ .matnr-print-template-page {
+ height: calc(var(--art-full-height) - 40px);
+ max-height: calc(var(--art-full-height) - 40px);
+ min-height: 0;
+ overflow: hidden;
+ }
+
+ @media (max-width: 640px) {
+ .matnr-print-template-page {
+ height: auto;
+ max-height: none;
+ overflow: auto;
+ }
+ }
+</style>
diff --git a/rsf-design/src/views/basic-info/wh-mat/index.vue b/rsf-design/src/views/basic-info/wh-mat/index.vue
index 6d28ea1..ff0043a 100644
--- a/rsf-design/src/views/basic-info/wh-mat/index.vue
+++ b/rsf-design/src/views/basic-info/wh-mat/index.vue
@@ -155,21 +155,18 @@
<ElButton :loading="templateDownloading" @click="handleDownloadTemplate" v-ripple>
{{ t('pages.basicInfo.whMat.actions.downloadTemplate') }}
</ElButton>
- <ListExportPrint
- class="inline-flex"
- :preview-visible="previewVisible"
- @update:previewVisible="handlePreviewVisibleChange"
- :report-title="reportTitle"
- :selected-rows="selectedRows"
- :query-params="reportQueryParams"
- :columns="columns"
- :preview-rows="previewRows"
- :preview-meta="resolvedPreviewMeta"
- :total="pagination.total"
- :disabled="loading"
- @export="handleExport"
- @print="handlePrint"
- />
+ <ElButton :loading="exporting" :disabled="loading" @click="handleExport" v-ripple>
+ {{ t('common.actions.export') }}
+ </ElButton>
+ <ElButton
+ v-auth="'manager:matnrPrintTemplate:list'"
+ :loading="printLoading"
+ :disabled="loading || selectedRows.length === 0"
+ @click="handlePrint()"
+ v-ripple
+ >
+ {{ t('common.actions.print') }}
+ </ElButton>
</ElSpace>
</template>
</ArtTableHeader>
@@ -222,6 +219,8 @@
:loading="detailLoading"
:detail="detailData"
/>
+
+ <WhMatPrintDialog v-model:visible="printDialogVisible" :rows="printRows" />
</div>
</template>
@@ -229,7 +228,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
- import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { buildListExportPayload } from '@/components/biz/list-export-print/list-export-print.helpers.js'
import { useAuth } from '@/hooks/core/useAuth'
import { useTableColumns } from '@/hooks/core/useTableColumns'
import { useUserStore } from '@/store/modules/user'
@@ -241,7 +240,6 @@
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
import { useCrudPage } from '@/views/system/common/useCrudPage'
- import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
import {
fetchBatchUpdateMatnr,
fetchBindMatnrGroup,
@@ -262,6 +260,7 @@
import WhMatBindLocDialog from './modules/wh-mat-bind-loc-dialog.vue'
import WhMatDialog from './modules/wh-mat-dialog.vue'
import WhMatDetailDrawer from './modules/wh-mat-detail-drawer.vue'
+ import WhMatPrintDialog from './modules/wh-mat-print-dialog.vue'
import { createWhMatTableColumns } from './whMatTable.columns'
import {
WH_MAT_REPORT_STYLE,
@@ -269,8 +268,6 @@
buildMatnrGroupTreeQueryParams,
buildMatnrPageQueryParams,
buildWhMatDialogModel,
- buildWhMatPrintRows,
- buildWhMatReportMeta,
buildWhMatSavePayload,
createWhMatSearchState,
getWhMatDynamicFieldKey,
@@ -292,6 +289,7 @@
const { t } = useI18n()
const { hasAuth } = useAuth()
const userStore = useUserStore()
+ const canPrintMatnr = hasAuth('manager:matnrPrintTemplate:list')
const showBatchActionButtons = false
const loading = ref(false)
@@ -303,10 +301,14 @@
const bindLocDialogVisible = ref(false)
const bindLocOptionsLoading = ref(false)
const importing = ref(false)
+ const exporting = ref(false)
+ const printLoading = ref(false)
+ const printDialogVisible = ref(false)
const templateDownloading = ref(false)
const tableData = ref([])
const groupTreeData = ref([])
const detailData = ref({})
+ const printRows = ref([])
const enabledFields = ref([])
const serialRuleOptions = ref([])
const areaOptions = ref([])
@@ -643,7 +645,7 @@
handleViewDetail: openDetailDrawer,
handleEdit: hasAuth('update') ? openEditDialog : null,
handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
- handlePrint: (row) => handlePrint({ ids: [row.id] }),
+ handlePrint: canPrintMatnr ? (row) => handlePrint({ ids: [row.id] }) : null,
canEdit: hasAuth('update'),
canDelete: hasAuth('delete'),
t
@@ -1038,41 +1040,6 @@
}
}
- const resolvePrintRecords = async (payload) => {
- if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
- return defaultResponseAdapter(await fetchGetMatnrMany(payload.ids)).records
- }
- return tableData.value
- }
-
- const {
- previewVisible,
- previewRows,
- previewMeta,
- handlePreviewVisibleChange,
- handleExport,
- handlePrint
- } = usePrintExportPage({
- downloadFileName: 'matnr.xlsx',
- requestExport: (payload) =>
- fetchExportMatnrReport(payload, {
- headers: {
- Authorization: userStore.accessToken || ''
- }
- }),
- resolvePrintRecords,
- buildPreviewRows: (records) => buildWhMatPrintRows(records, t),
- buildPreviewMeta
- })
-
- const resolvedPreviewMeta = computed(() =>
- buildWhMatReportMeta({
- previewMeta: previewMeta.value,
- count: previewRows.value.length,
- orientation: previewMeta.value?.reportStyle?.orientation || WH_MAT_REPORT_STYLE.orientation
- })
- )
-
async function downloadFile(response, fallbackName) {
if (!response?.ok) {
throw new Error(
@@ -1169,6 +1136,69 @@
}
}
+ async function handleExport() {
+ exporting.value = true
+ try {
+ const payload = buildListExportPayload({
+ reportTitle,
+ selectedRows: selectedRows.value,
+ queryParams: reportQueryParams.value,
+ columns,
+ meta: buildPreviewMeta(tableData.value)
+ })
+ const response = await guardRequestWithMessage(
+ fetchExportMatnrReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ null,
+ {
+ timeoutMessage: t('message.exportTimeoutStopped')
+ }
+ )
+ if (!response) {
+ return
+ }
+ await downloadFile(response, 'matnr.xlsx')
+ ElMessage.success(t('crud.messages.exportSuccess'))
+ } catch (error) {
+ ElMessage.error(error?.message || t('crud.messages.exportFailed'))
+ } finally {
+ exporting.value = false
+ }
+ }
+
+ async function handlePrint(payload = {}) {
+ const ids =
+ Array.isArray(payload?.ids) && payload.ids.length > 0 ? payload.ids : getSelectedIds()
+
+ if (!ids.length) {
+ ElMessage.warning(t('pages.basicInfo.whMat.messages.selectAtLeastOne'))
+ return
+ }
+
+ printLoading.value = true
+ try {
+ const records = await guardRequestWithMessage(fetchGetMatnrMany(ids), [], {
+ timeoutMessage: t('message.printTimeoutStopped')
+ })
+ const normalizedRecords = defaultResponseAdapter(records).records
+ if (!normalizedRecords.length) {
+ ElMessage.warning(t('print.noData'))
+ return
+ }
+ printRows.value = normalizedRecords.map((record) =>
+ normalizeMatnrRow(record, t, enabledFields.value)
+ )
+ printDialogVisible.value = true
+ } catch (error) {
+ ElMessage.error(error?.message || t('crud.messages.printFailed'))
+ } finally {
+ printLoading.value = false
+ }
+ }
+
async function handleDownloadTemplate() {
templateDownloading.value = true
try {
diff --git a/rsf-design/src/views/basic-info/wh-mat/matnrPrintTemplate.helpers.js b/rsf-design/src/views/basic-info/wh-mat/matnrPrintTemplate.helpers.js
new file mode 100644
index 0000000..df53418
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/matnrPrintTemplate.helpers.js
@@ -0,0 +1,827 @@
+import JsBarcode from 'jsbarcode'
+import { $t } from '@/locales'
+
+const MM_UNIT = 'mm'
+const DEFAULT_CANVAS = {
+ width: 70,
+ height: 40,
+ unit: MM_UNIT,
+ backgroundColor: '#FFFFFF',
+ gridSize: 1
+}
+
+const TEXT_STYLE_DEFAULTS = {
+ fontSize: 3.2,
+ fontWeight: 400,
+ textAlign: 'left',
+ color: '#111111',
+ lineHeight: 1.25
+}
+
+const TABLE_STYLE_DEFAULTS = {
+ fontSize: 2.8,
+ fontWeight: 400,
+ textAlign: 'left',
+ color: '#111111',
+ borderColor: '#111111',
+ borderWidth: 0.2,
+ backgroundColor: '#FFFFFF'
+}
+
+const FIXED_FIELD_OPTIONS = [
+ 'id',
+ 'code',
+ 'name',
+ 'groupName',
+ 'barcode',
+ 'spec',
+ 'model',
+ 'color',
+ 'size',
+ 'unit',
+ 'purUnit',
+ 'stockUnit',
+ 'stockLevelText',
+ 'flagLabelManageText',
+ 'flagCheckText',
+ 'statusText',
+ 'describle',
+ 'memo',
+ 'createByText',
+ 'createTimeText',
+ 'updateByText',
+ 'updateTimeText'
+]
+
+function cloneDeep(value) {
+ return JSON.parse(JSON.stringify(value ?? null))
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = 0) {
+ const nextValue = Number(value)
+ return Number.isFinite(nextValue) ? nextValue : fallback
+}
+
+function normalizeInteger(value, fallback = 0) {
+ return Math.round(normalizeNumber(value, fallback))
+}
+
+function normalizeColor(value, fallback = '#111111') {
+ const color = normalizeText(value)
+ return color || fallback
+}
+
+function createElementId(prefix = 'el') {
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
+}
+
+export function mmToPx(mm, scale = 4) {
+ return `${normalizeNumber(mm) * scale}px`
+}
+
+export function buildMatnrPrintFieldOptions(enabledFields = []) {
+ const fixedFields = FIXED_FIELD_OPTIONS.map((field) => ({
+ label: $t(`pages.basicInfo.whMat.printTemplate.helpers.fixedFields.${field}`),
+ path: field,
+ placeholder: `{${field}}`,
+ category: $t('pages.basicInfo.whMat.printTemplate.fieldPanel.fixedCategory')
+ }))
+
+ const extendFields = (Array.isArray(enabledFields) ? enabledFields : [])
+ .map((field) => {
+ const key = normalizeText(field?.fields)
+ if (!key) {
+ return null
+ }
+ return {
+ label: normalizeText(field?.fieldsAlise || field?.fieldsAlias || key),
+ path: `extendFields.${key}`,
+ placeholder: `{extendFields.${key}}`,
+ category: $t('pages.basicInfo.whMat.printTemplate.fieldPanel.extendCategory')
+ }
+ })
+ .filter(Boolean)
+
+ return [...fixedFields, ...extendFields]
+}
+
+export function resolveTemplatePlaceholders(template, record = {}) {
+ const source = typeof template === 'string' ? template : String(template ?? '')
+ return source.replace(/\{([^{}]+)\}/g, (_, rawPath) => {
+ const value = getValueByPath(record, normalizeText(rawPath))
+ if (value === null || value === undefined) {
+ return ''
+ }
+ if (typeof value === 'object') {
+ return ''
+ }
+ return String(value)
+ })
+}
+
+export function renderMatnrPrintTemplate(template = {}, record = {}) {
+ const normalizedTemplate = getNormalizedTemplate(template)
+ const elements = normalizedTemplate.elements
+ .filter((element) => element.visible !== false)
+ .map((element) => resolveElement(element, record))
+ .sort((left, right) => normalizeNumber(left?.zIndex) - normalizeNumber(right?.zIndex))
+
+ return {
+ version: normalizedTemplate.version,
+ canvas: normalizedTemplate.canvas,
+ elements
+ }
+}
+
+export function measureMatnrPrintTemplateBounds(template = {}) {
+ return measureNormalizedTemplateBounds(getNormalizedTemplate(template))
+}
+
+export function detectMatnrPrintTemplateOverflow(template = {}) {
+ const normalizedTemplate = getNormalizedTemplate(template)
+ const bounds = measureNormalizedTemplateBounds(normalizedTemplate)
+ const canvasWidth = Math.max(10, normalizeNumber(normalizedTemplate?.canvas?.width, 70))
+ const canvasHeight = Math.max(10, normalizeNumber(normalizedTemplate?.canvas?.height, 40))
+ return {
+ hasOverflow: bounds.width > canvasWidth || bounds.height > canvasHeight,
+ overflowX: bounds.width > canvasWidth,
+ overflowY: bounds.height > canvasHeight,
+ bounds,
+ canvas: {
+ width: canvasWidth,
+ height: canvasHeight
+ }
+ }
+}
+
+function resolveElement(element, record) {
+ const base = {
+ ...cloneDeep(element),
+ x: normalizeNumber(element?.x),
+ y: normalizeNumber(element?.y),
+ w: Math.max(0.5, normalizeNumber(element?.w, 10)),
+ h: Math.max(0.5, normalizeNumber(element?.h, 6)),
+ zIndex: normalizeInteger(element?.zIndex, 1),
+ visible: element?.visible !== false
+ }
+
+ if (base.type === 'text') {
+ const contentTemplate = normalizeText(base.contentTemplate)
+ base.resolvedText =
+ base.contentMode === 'template'
+ ? resolveTemplatePlaceholders(contentTemplate, record)
+ : contentTemplate
+ base.style = {
+ ...TEXT_STYLE_DEFAULTS,
+ ...(base.style || {})
+ }
+ return base
+ }
+
+ if (base.type === 'barcode') {
+ const value = resolveTemplatePlaceholders(base.valueTemplate, record)
+ base.resolvedValue = value
+ base.showText = base.showText !== false
+ base.symbology = 'CODE128'
+ base.svgMarkup = buildBarcodeSvgMarkup(value, {
+ showText: base.showText,
+ widthMm: base.w,
+ heightMm: base.h
+ })
+ return base
+ }
+
+ if (base.type === 'qrcode') {
+ base.resolvedValue = resolveTemplatePlaceholders(base.valueTemplate, record)
+ return base
+ }
+
+ if (base.type === 'line') {
+ base.direction = base.direction === 'vertical' ? 'vertical' : 'horizontal'
+ base.borderWidth = Math.max(0.2, normalizeNumber(base.borderWidth, 0.4))
+ base.color = normalizeColor(base.color, '#111111')
+ return base
+ }
+
+ if (base.type === 'rect') {
+ base.borderWidth = Math.max(0.2, normalizeNumber(base.borderWidth, 0.4))
+ base.borderColor = normalizeColor(base.borderColor, '#111111')
+ base.backgroundColor = normalizeColor(base.backgroundColor, '#FFFFFF')
+ base.radius = Math.max(0, normalizeNumber(base.radius, 0))
+ return base
+ }
+
+ if (base.type === 'table') {
+ base.style = {
+ ...TABLE_STYLE_DEFAULTS,
+ ...(base.style || {})
+ }
+ base.columns = Array.isArray(base.columns) ? base.columns : []
+ base.rows = Array.isArray(base.rows) ? base.rows : []
+ base.cells = Array.isArray(base.cells) ? base.cells : []
+ base.resolvedCells = base.cells.map((cell) => ({
+ ...cell,
+ row: normalizeInteger(cell?.row),
+ col: normalizeInteger(cell?.col),
+ rowspan: Math.max(1, normalizeInteger(cell?.rowspan, 1)),
+ colspan: Math.max(1, normalizeInteger(cell?.colspan, 1)),
+ resolvedText:
+ cell?.contentMode === 'template'
+ ? resolveTemplatePlaceholders(cell?.contentTemplate, record)
+ : normalizeText(cell?.contentTemplate),
+ style: {
+ ...TABLE_STYLE_DEFAULTS,
+ ...(base.style || {}),
+ ...(cell?.style || {})
+ }
+ }))
+ return base
+ }
+
+ return base
+}
+
+function getNormalizedTemplate(template = {}) {
+ if (
+ template &&
+ typeof template === 'object' &&
+ template.canvas &&
+ template.canvas.unit === MM_UNIT &&
+ Array.isArray(template.elements)
+ ) {
+ return template
+ }
+ return normalizeMatnrPrintTemplate(template)
+}
+
+function measureNormalizedTemplateBounds(template = {}) {
+ const canvasWidth = Math.max(10, normalizeNumber(template?.canvas?.width, 70))
+ const canvasHeight = Math.max(10, normalizeNumber(template?.canvas?.height, 40))
+ const bounds = (Array.isArray(template?.elements) ? template.elements : []).reduce(
+ (result, element) => {
+ if (element?.visible === false) {
+ return result
+ }
+ const right = normalizeNumber(element?.x) + normalizeNumber(element?.w)
+ const bottom = normalizeNumber(element?.y) + normalizeNumber(element?.h)
+ return {
+ width: Math.max(result.width, right),
+ height: Math.max(result.height, bottom)
+ }
+ },
+ {
+ width: canvasWidth,
+ height: canvasHeight
+ }
+ )
+ return {
+ width: Number(bounds.width.toFixed(2)),
+ height: Number(bounds.height.toFixed(2))
+ }
+}
+
+export function normalizeMatnrPrintTemplate(template = {}) {
+ const canvasSource =
+ template?.canvas || template?.canvasJson?.canvas
+ ? template?.canvas || template?.canvasJson?.canvas
+ : {}
+ const rawCanvasJson =
+ template?.canvasJson &&
+ typeof template.canvasJson === 'object' &&
+ !Array.isArray(template.canvasJson)
+ ? template.canvasJson
+ : null
+ const source =
+ rawCanvasJson && Array.isArray(rawCanvasJson.elements)
+ ? rawCanvasJson
+ : {
+ version: template?.version,
+ canvas: canvasSource,
+ elements: Array.isArray(template?.elements) ? template.elements : []
+ }
+
+ return {
+ id: template?.id,
+ tenantId: template?.tenantId,
+ name:
+ normalizeText(template?.name) ||
+ $t('pages.basicInfo.whMat.printTemplate.workspace.unnamedTemplate'),
+ code: normalizeText(template?.code) || `MATNR_${Date.now()}`,
+ isDefault: normalizeInteger(template?.isDefault, 0),
+ status: normalizeInteger(template?.status, 1),
+ memo: normalizeText(template?.memo),
+ version: normalizeInteger(source?.version, 1),
+ canvas: {
+ ...DEFAULT_CANVAS,
+ ...(source?.canvas || {}),
+ width: Math.max(10, normalizeNumber(source?.canvas?.width, DEFAULT_CANVAS.width)),
+ height: Math.max(10, normalizeNumber(source?.canvas?.height, DEFAULT_CANVAS.height)),
+ unit: MM_UNIT,
+ backgroundColor: normalizeColor(
+ source?.canvas?.backgroundColor,
+ DEFAULT_CANVAS.backgroundColor
+ ),
+ gridSize: Math.max(1, normalizeNumber(source?.canvas?.gridSize, DEFAULT_CANVAS.gridSize))
+ },
+ elements: (Array.isArray(source?.elements) ? source.elements : []).map((element, index) =>
+ normalizeElement(element, index)
+ )
+ }
+}
+
+function normalizeElement(element = {}, index = 0) {
+ const normalized = {
+ id: normalizeText(element?.id) || createElementId(),
+ type: normalizeText(element?.type) || 'text',
+ x: normalizeNumber(element?.x, 2 + index),
+ y: normalizeNumber(element?.y, 2 + index),
+ w: Math.max(1, normalizeNumber(element?.w, 20)),
+ h: Math.max(1, normalizeNumber(element?.h, 8)),
+ zIndex: normalizeInteger(element?.zIndex, index + 1),
+ visible: element?.visible !== false
+ }
+
+ if (normalized.type === 'text') {
+ normalized.contentMode = element?.contentMode === 'template' ? 'template' : 'static'
+ normalized.contentTemplate = normalizeText(element?.contentTemplate)
+ normalized.style = {
+ ...TEXT_STYLE_DEFAULTS,
+ ...(element?.style || {}),
+ fontSize: Math.max(
+ 1.8,
+ normalizeNumber(element?.style?.fontSize, TEXT_STYLE_DEFAULTS.fontSize)
+ ),
+ fontWeight: normalizeInteger(element?.style?.fontWeight, TEXT_STYLE_DEFAULTS.fontWeight),
+ textAlign: ['left', 'center', 'right'].includes(element?.style?.textAlign)
+ ? element.style.textAlign
+ : TEXT_STYLE_DEFAULTS.textAlign,
+ color: normalizeColor(element?.style?.color, TEXT_STYLE_DEFAULTS.color),
+ lineHeight: Math.max(
+ 1,
+ normalizeNumber(element?.style?.lineHeight, TEXT_STYLE_DEFAULTS.lineHeight)
+ )
+ }
+ return normalized
+ }
+
+ if (normalized.type === 'barcode') {
+ normalized.valueTemplate = normalizeText(element?.valueTemplate)
+ normalized.showText = element?.showText !== false
+ normalized.symbology = 'CODE128'
+ normalized.h = Math.max(8, normalizeNumber(element?.h, 14))
+ return normalized
+ }
+
+ if (normalized.type === 'qrcode') {
+ normalized.valueTemplate = normalizeText(element?.valueTemplate)
+ normalized.w = Math.max(8, normalizeNumber(element?.w, 18))
+ normalized.h = Math.max(8, normalizeNumber(element?.h, 18))
+ return normalized
+ }
+
+ if (normalized.type === 'line') {
+ normalized.direction = element?.direction === 'vertical' ? 'vertical' : 'horizontal'
+ normalized.borderWidth = Math.max(0.2, normalizeNumber(element?.borderWidth, 0.4))
+ normalized.color = normalizeColor(element?.color, '#111111')
+ normalized.w =
+ normalized.direction === 'horizontal'
+ ? Math.max(6, normalizeNumber(element?.w, 20))
+ : Math.max(0.4, normalizeNumber(element?.w, 0.4))
+ normalized.h =
+ normalized.direction === 'vertical'
+ ? Math.max(6, normalizeNumber(element?.h, 20))
+ : Math.max(0.4, normalizeNumber(element?.h, 0.4))
+ return normalized
+ }
+
+ if (normalized.type === 'rect') {
+ normalized.borderWidth = Math.max(0.2, normalizeNumber(element?.borderWidth, 0.4))
+ normalized.borderColor = normalizeColor(element?.borderColor, '#111111')
+ normalized.backgroundColor = normalizeColor(element?.backgroundColor, '#FFFFFF')
+ normalized.radius = Math.max(0, normalizeNumber(element?.radius, 0))
+ return normalized
+ }
+
+ if (normalized.type === 'table') {
+ normalized.style = {
+ ...TABLE_STYLE_DEFAULTS,
+ ...(element?.style || {}),
+ fontSize: Math.max(
+ 1.8,
+ normalizeNumber(element?.style?.fontSize, TABLE_STYLE_DEFAULTS.fontSize)
+ ),
+ fontWeight: normalizeInteger(element?.style?.fontWeight, TABLE_STYLE_DEFAULTS.fontWeight),
+ textAlign: ['left', 'center', 'right'].includes(element?.style?.textAlign)
+ ? element.style.textAlign
+ : TABLE_STYLE_DEFAULTS.textAlign,
+ color: normalizeColor(element?.style?.color, TABLE_STYLE_DEFAULTS.color),
+ borderColor: normalizeColor(element?.style?.borderColor, TABLE_STYLE_DEFAULTS.borderColor),
+ borderWidth: Math.max(
+ 0.2,
+ normalizeNumber(element?.style?.borderWidth, TABLE_STYLE_DEFAULTS.borderWidth)
+ ),
+ backgroundColor: normalizeColor(
+ element?.style?.backgroundColor,
+ TABLE_STYLE_DEFAULTS.backgroundColor
+ )
+ }
+ normalized.columns = normalizeTableColumns(element?.columns, normalized.w)
+ normalized.rows = normalizeTableRows(element?.rows)
+ normalized.h = Math.max(
+ normalizeNumber(normalized.h, 0),
+ normalized.rows.reduce((total, row) => total + normalizeNumber(row?.height, 0), 0)
+ )
+ normalized.cells = normalizeTableCells(element?.cells)
+ return normalized
+ }
+
+ return normalized
+}
+
+function normalizeTableColumns(columns = [], totalWidth = 40) {
+ const source =
+ Array.isArray(columns) && columns.length
+ ? columns
+ : [{ width: totalWidth * 0.36 }, { width: totalWidth * 0.64 }]
+ return source.map((column) => ({
+ width: Math.max(6, normalizeNumber(column?.width, 18))
+ }))
+}
+
+function normalizeTableRows(rows = []) {
+ const source = Array.isArray(rows) && rows.length ? rows : [{ height: 6 }]
+ return source.map((row) => ({
+ height: Math.max(4, normalizeNumber(row?.height, 6))
+ }))
+}
+
+function normalizeTableCells(cells = []) {
+ return (Array.isArray(cells) ? cells : []).map((cell) => ({
+ row: normalizeInteger(cell?.row),
+ col: normalizeInteger(cell?.col),
+ rowspan: Math.max(1, normalizeInteger(cell?.rowspan, 1)),
+ colspan: Math.max(1, normalizeInteger(cell?.colspan, 1)),
+ contentMode: cell?.contentMode === 'template' ? 'template' : 'static',
+ contentTemplate: normalizeText(cell?.contentTemplate),
+ style: {
+ ...TABLE_STYLE_DEFAULTS,
+ ...(cell?.style || {})
+ }
+ }))
+}
+
+export function buildMatnrPrintTemplatePayload(template = {}) {
+ const normalized = normalizeMatnrPrintTemplate(template)
+ return {
+ ...(normalized.id ? { id: normalized.id } : {}),
+ name: normalized.name,
+ code: normalized.code,
+ isDefault: normalized.isDefault,
+ status: normalized.status,
+ memo: normalized.memo,
+ canvasJson: {
+ version: normalized.version,
+ canvas: normalized.canvas,
+ elements: normalized.elements
+ }
+ }
+}
+
+export function createDefaultMatnrPrintTemplate(seed = {}) {
+ const name =
+ normalizeText(seed?.name) ||
+ $t('pages.basicInfo.whMat.printTemplate.helpers.defaultTemplateName')
+ const code = normalizeText(seed?.code) || `MATNR_${Date.now()}`
+ return normalizeMatnrPrintTemplate({
+ name,
+ code,
+ status: 1,
+ isDefault: seed?.isDefault ? 1 : 0,
+ memo: '',
+ canvasJson: {
+ version: 1,
+ canvas: {
+ ...DEFAULT_CANVAS
+ },
+ elements: [
+ {
+ id: createElementId(),
+ type: 'text',
+ x: 4,
+ y: 3,
+ w: 18,
+ h: 5,
+ zIndex: 1,
+ visible: true,
+ contentMode: 'static',
+ contentTemplate: $t(
+ 'pages.basicInfo.whMat.printTemplate.helpers.defaultText.productCode'
+ ),
+ style: {
+ ...TEXT_STYLE_DEFAULTS,
+ fontSize: 3.1,
+ fontWeight: 600,
+ textAlign: 'center'
+ }
+ },
+ {
+ id: createElementId(),
+ type: 'barcode',
+ x: 24,
+ y: 3,
+ w: 40,
+ h: 14,
+ zIndex: 2,
+ visible: true,
+ valueTemplate: '{code}',
+ showText: true,
+ symbology: 'CODE128'
+ },
+ {
+ id: createElementId(),
+ type: 'text',
+ x: 4,
+ y: 20,
+ w: 12,
+ h: 5,
+ zIndex: 3,
+ visible: true,
+ contentMode: 'static',
+ contentTemplate: $t(
+ 'pages.basicInfo.whMat.printTemplate.helpers.defaultText.productName'
+ ),
+ style: {
+ ...TEXT_STYLE_DEFAULTS,
+ fontSize: 3,
+ fontWeight: 600
+ }
+ },
+ {
+ id: createElementId(),
+ type: 'text',
+ x: 17,
+ y: 20,
+ w: 47,
+ h: 5,
+ zIndex: 4,
+ visible: true,
+ contentMode: 'template',
+ contentTemplate: '{name}',
+ style: {
+ ...TEXT_STYLE_DEFAULTS,
+ fontSize: 3,
+ fontWeight: 500
+ }
+ }
+ ]
+ }
+ })
+}
+
+export function duplicateMatnrPrintTemplate(template = {}) {
+ const normalized = normalizeMatnrPrintTemplate(template)
+ return normalizeMatnrPrintTemplate({
+ ...cloneDeep(normalized),
+ id: null,
+ name: `${normalized.name}${$t('pages.basicInfo.whMat.printTemplate.helpers.copySuffix')}`,
+ code: `${normalized.code}_COPY_${Date.now()}`,
+ isDefault: 0,
+ canvasJson: {
+ version: normalized.version,
+ canvas: normalized.canvas,
+ elements: normalized.elements.map((element) => ({
+ ...cloneDeep(element),
+ id: createElementId()
+ }))
+ }
+ })
+}
+
+export function createElementByType(type, offsetIndex = 0) {
+ const offset = offsetIndex * 2
+ const common = {
+ id: createElementId(),
+ type,
+ x: 4 + offset,
+ y: 4 + offset,
+ zIndex: offsetIndex + 1,
+ visible: true
+ }
+
+ if (type === 'text') {
+ return normalizeElement({
+ ...common,
+ w: 24,
+ h: 6,
+ contentMode: 'static',
+ contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.defaultText.staticText'),
+ style: {
+ ...TEXT_STYLE_DEFAULTS
+ }
+ })
+ }
+
+ if (type === 'barcode') {
+ return normalizeElement({
+ ...common,
+ w: 34,
+ h: 14,
+ valueTemplate: '{code}',
+ showText: true,
+ symbology: 'CODE128'
+ })
+ }
+
+ if (type === 'qrcode') {
+ return normalizeElement({
+ ...common,
+ w: 20,
+ h: 20,
+ valueTemplate: '{barcode}'
+ })
+ }
+
+ if (type === 'line') {
+ return normalizeElement({
+ ...common,
+ w: 24,
+ h: 0.4,
+ direction: 'horizontal',
+ borderWidth: 0.4,
+ color: '#111111'
+ })
+ }
+
+ if (type === 'rect') {
+ return normalizeElement({
+ ...common,
+ w: 24,
+ h: 12,
+ borderWidth: 0.4,
+ borderColor: '#111111',
+ backgroundColor: '#FFFFFF',
+ radius: 0
+ })
+ }
+
+ if (type === 'table') {
+ return createFieldListTableElement(offsetIndex)
+ }
+
+ return normalizeElement(common)
+}
+
+export function createFieldListTableElement(offsetIndex = 0) {
+ const x = 4 + offsetIndex * 2
+ const y = 4 + offsetIndex * 2
+ return normalizeElement({
+ id: createElementId(),
+ type: 'table',
+ x,
+ y,
+ w: 58,
+ h: 24,
+ zIndex: offsetIndex + 1,
+ visible: true,
+ style: {
+ ...TABLE_STYLE_DEFAULTS,
+ fontSize: 2.8
+ },
+ columns: [{ width: 18 }, { width: 40 }],
+ rows: [{ height: 6 }, { height: 6 }, { height: 6 }, { height: 6 }],
+ cells: [
+ {
+ row: 0,
+ col: 0,
+ contentMode: 'static',
+ contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.code')
+ },
+ { row: 0, col: 1, contentMode: 'template', contentTemplate: '{code}' },
+ {
+ row: 1,
+ col: 0,
+ contentMode: 'static',
+ contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.name')
+ },
+ { row: 1, col: 1, contentMode: 'template', contentTemplate: '{name}' },
+ {
+ row: 2,
+ col: 0,
+ contentMode: 'static',
+ contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.spec')
+ },
+ { row: 2, col: 1, contentMode: 'template', contentTemplate: '{spec}' },
+ {
+ row: 3,
+ col: 0,
+ contentMode: 'static',
+ contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.model')
+ },
+ { row: 3, col: 1, contentMode: 'template', contentTemplate: '{model}' }
+ ]
+ })
+}
+
+export function getFieldListTableRows(element = {}) {
+ if (element?.type !== 'table') {
+ return []
+ }
+ const rows = Array.isArray(element?.rows) ? element.rows : []
+ const cells = Array.isArray(element?.cells) ? element.cells : []
+ return rows.map((row, rowIndex) => {
+ const labelCell = cells.find(
+ (cell) => normalizeInteger(cell?.row) === rowIndex && normalizeInteger(cell?.col) === 0
+ )
+ const valueCell = cells.find(
+ (cell) => normalizeInteger(cell?.row) === rowIndex && normalizeInteger(cell?.col) === 1
+ )
+ return {
+ id: `${element.id}_row_${rowIndex}`,
+ height: Math.max(4, normalizeNumber(row?.height, 6)),
+ labelTemplate: normalizeText(labelCell?.contentTemplate),
+ labelMode: labelCell?.contentMode === 'template' ? 'template' : 'static',
+ valueTemplate: normalizeText(valueCell?.contentTemplate),
+ valueMode: valueCell?.contentMode === 'static' ? 'static' : 'template'
+ }
+ })
+}
+
+export function updateFieldListTableRows(element = {}, rowConfigs = []) {
+ const sourceRows = Array.isArray(rowConfigs) && rowConfigs.length ? rowConfigs : []
+ return normalizeElement({
+ ...cloneDeep(element),
+ rows: sourceRows.map((row) => ({
+ height: Math.max(4, normalizeNumber(row?.height, 6))
+ })),
+ columns: normalizeTableColumns(element?.columns, element?.w),
+ cells: sourceRows.flatMap((row, index) => [
+ {
+ row: index,
+ col: 0,
+ contentMode: row?.labelMode === 'template' ? 'template' : 'static',
+ contentTemplate: normalizeText(row?.labelTemplate)
+ },
+ {
+ row: index,
+ col: 1,
+ contentMode: row?.valueMode === 'static' ? 'static' : 'template',
+ contentTemplate: normalizeText(row?.valueTemplate)
+ }
+ ])
+ })
+}
+
+export function appendFieldPlaceholder(originValue, placeholder) {
+ const currentValue = String(originValue ?? '')
+ return `${currentValue}${placeholder}`
+}
+
+function getValueByPath(source, path) {
+ if (!path) {
+ return ''
+ }
+ return path.split('.').reduce((current, key) => {
+ if (current === null || current === undefined) {
+ return ''
+ }
+ return current[key]
+ }, source)
+}
+
+export function buildBarcodeSvgMarkup(value, options = {}) {
+ const codeValue = normalizeText(value)
+ if (!codeValue || typeof document === 'undefined') {
+ return ''
+ }
+ try {
+ const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
+ const widthMm = Math.max(8, normalizeNumber(options?.widthMm, 20))
+ const heightMm = Math.max(8, normalizeNumber(options?.heightMm, 12))
+ const fontSize = Math.max(12, normalizeNumber(options?.fontSize, 12))
+ const targetHeightPx = Math.max(32, heightMm * 4)
+ const textReservePx = options?.showText !== false ? Math.max(fontSize + 8, 18) : 0
+ JsBarcode(svgNode, codeValue, {
+ format: 'CODE128',
+ displayValue: options?.showText !== false,
+ width: Math.max(1, normalizeNumber(options?.lineWidth, 1.4)),
+ height: Math.max(12, targetHeightPx - textReservePx),
+ margin: 0,
+ background: '#FFFFFF',
+ lineColor: '#111111',
+ font: 'Arial',
+ fontSize,
+ textMargin: 2
+ })
+ svgNode.setAttribute('width', `${widthMm}mm`)
+ svgNode.setAttribute('height', `${heightMm}mm`)
+ svgNode.setAttribute('preserveAspectRatio', 'none')
+ return svgNode.outerHTML
+ } catch (error) {
+ console.warn('[matnr-print] barcode render failed', error)
+ return ''
+ }
+}
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-canvas.vue b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-canvas.vue
new file mode 100644
index 0000000..9a3f530
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-canvas.vue
@@ -0,0 +1,571 @@
+<template>
+ <div class="matnr-print-canvas-shell">
+ <div
+ ref="canvasRef"
+ class="matnr-print-canvas"
+ :class="{
+ 'matnr-print-canvas--editor': editorMode,
+ 'matnr-print-canvas--interactive': interactive,
+ 'matnr-print-canvas--grid': showGrid && editorMode
+ }"
+ :style="canvasStyle"
+ @mousedown.self="handleCanvasMouseDown"
+ >
+ <div
+ v-for="element in renderedTemplate.elements"
+ :key="element.id"
+ class="matnr-print-element"
+ :class="{
+ 'is-selected': interactive && selectedElementId === element.id,
+ 'is-hidden': element.visible === false
+ }"
+ :style="getElementBoxStyle(element)"
+ @mousedown.stop="handleElementMouseDown($event, element)"
+ >
+ <div
+ v-if="element.type === 'text'"
+ class="matnr-print-element__text"
+ :style="getTextStyle(element)"
+ >
+ {{ element.resolvedText }}
+ </div>
+
+ <div
+ v-else-if="element.type === 'barcode'"
+ class="matnr-print-element__barcode"
+ v-html="element.svgMarkup"
+ ></div>
+
+ <div v-else-if="element.type === 'qrcode'" class="matnr-print-element__qrcode">
+ <QrcodeVue
+ :value="element.resolvedValue || ' '"
+ render-as="svg"
+ level="M"
+ :size="getQrcodeSize(element)"
+ :margin="0"
+ />
+ </div>
+
+ <div
+ v-else-if="element.type === 'line'"
+ class="matnr-print-element__line"
+ :style="getLineStyle(element)"
+ ></div>
+
+ <div
+ v-else-if="element.type === 'rect'"
+ class="matnr-print-element__rect"
+ :style="getRectStyle(element)"
+ ></div>
+
+ <table
+ v-else-if="element.type === 'table'"
+ class="matnr-print-element__table"
+ :style="getTableStyle(element)"
+ >
+ <colgroup>
+ <col
+ v-for="(column, columnIndex) in element.columns"
+ :key="`${element.id}_col_${columnIndex}`"
+ :style="{ width: getUnitValue(column.width || 10) }"
+ />
+ </colgroup>
+ <tbody>
+ <tr
+ v-for="(row, rowIndex) in element.rows"
+ :key="`${element.id}_row_${rowIndex}`"
+ :style="{ height: getUnitValue(row.height || 6) }"
+ >
+ <template
+ v-for="cell in getTableCellsForRow(element, rowIndex)"
+ :key="`${element.id}_${rowIndex}_${cell.col}`"
+ >
+ <td
+ :colspan="cell.colspan || 1"
+ :rowspan="cell.rowspan || 1"
+ :style="getTableCellStyle(cell, element)"
+ >
+ <div
+ class="matnr-print-element__table-cell-content"
+ :style="getTableCellContentStyle(cell, element)"
+ >
+ {{ cell.resolvedText }}
+ </div>
+ </td>
+ </template>
+ </tr>
+ </tbody>
+ </table>
+
+ <template v-if="interactive && selectedElementId === element.id && element.type !== 'line'">
+ <span
+ v-for="handle in resizeHandles"
+ :key="handle.direction"
+ class="matnr-print-element__handle"
+ :class="`is-${handle.direction}`"
+ @mousedown.stop="handleResizeMouseDown($event, element, handle.direction)"
+ ></span>
+ </template>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onBeforeUnmount, ref } from 'vue'
+ import QrcodeVue from 'qrcode.vue'
+ import {
+ mmToPx,
+ normalizeMatnrPrintTemplate,
+ renderMatnrPrintTemplate
+ } from '../matnrPrintTemplate.helpers'
+
+ defineOptions({ name: 'MatnrPrintCanvas' })
+
+ const props = defineProps({
+ template: {
+ type: Object,
+ default: () => ({})
+ },
+ activeRecord: {
+ type: Object,
+ default: () => ({})
+ },
+ selectedElementId: {
+ type: String,
+ default: ''
+ },
+ mode: {
+ type: String,
+ default: 'editor'
+ },
+ scale: {
+ type: Number,
+ default: 4
+ },
+ interactive: {
+ type: Boolean,
+ default: false
+ },
+ showGrid: {
+ type: Boolean,
+ default: false
+ }
+ })
+
+ const emit = defineEmits(['select-element', 'update-element'])
+
+ const canvasRef = ref(null)
+ const interactionState = ref(null)
+ const resizeHandles = [
+ { direction: 'top-left' },
+ { direction: 'top' },
+ { direction: 'top-right' },
+ { direction: 'right' },
+ { direction: 'bottom-right' },
+ { direction: 'bottom' },
+ { direction: 'bottom-left' },
+ { direction: 'left' }
+ ]
+
+ const editorMode = computed(() => props.mode === 'editor')
+ const normalizedTemplate = computed(() => normalizeMatnrPrintTemplate(props.template))
+ const renderedTemplate = computed(() =>
+ renderMatnrPrintTemplate(normalizedTemplate.value, props.activeRecord || {})
+ )
+
+ const canvasStyle = computed(() => {
+ const canvas = renderedTemplate.value.canvas
+ const width = editorMode.value ? mmToPx(canvas.width, props.scale) : `${canvas.width}mm`
+ const height = editorMode.value ? mmToPx(canvas.height, props.scale) : `${canvas.height}mm`
+ const gridSize = `${canvas.gridSize * props.scale}px`
+ return {
+ width,
+ height,
+ backgroundColor: canvas.backgroundColor || '#FFFFFF',
+ '--matnr-print-grid-size': gridSize
+ }
+ })
+
+ function getUnitValue(value) {
+ return editorMode.value ? mmToPx(value, props.scale) : `${value}mm`
+ }
+
+ function getElementBoxStyle(element) {
+ return {
+ left: getUnitValue(element.x),
+ top: getUnitValue(element.y),
+ width: getUnitValue(element.w),
+ height: getUnitValue(element.h),
+ zIndex: element.zIndex
+ }
+ }
+
+ function getTextStyle(element) {
+ const style = element.style || {}
+ return {
+ fontSize: editorMode.value ? mmToPx(style.fontSize, props.scale) : `${style.fontSize}mm`,
+ fontWeight: style.fontWeight,
+ color: style.color,
+ textAlign: style.textAlign,
+ lineHeight: style.lineHeight
+ }
+ }
+
+ function getLineStyle(element) {
+ const borderSize = editorMode.value
+ ? mmToPx(element.borderWidth || 0.4, props.scale)
+ : `${element.borderWidth || 0.4}mm`
+ return {
+ backgroundColor: element.color || '#111111',
+ height: element.direction === 'horizontal' ? borderSize : editorMode.value ? '100%' : '100%',
+ width: element.direction === 'vertical' ? borderSize : editorMode.value ? '100%' : '100%'
+ }
+ }
+
+ function getRectStyle(element) {
+ return {
+ borderWidth: editorMode.value
+ ? mmToPx(element.borderWidth || 0.4, props.scale)
+ : `${element.borderWidth || 0.4}mm`,
+ borderStyle: 'solid',
+ borderColor: element.borderColor || '#111111',
+ backgroundColor: element.backgroundColor || '#FFFFFF',
+ borderRadius: editorMode.value
+ ? mmToPx(element.radius || 0, props.scale)
+ : `${element.radius || 0}mm`
+ }
+ }
+
+ function getTableStyle(element) {
+ const style = element.style || {}
+ return {
+ fontSize: editorMode.value ? mmToPx(style.fontSize, props.scale) : `${style.fontSize}mm`,
+ color: style.color,
+ tableLayout: 'fixed'
+ }
+ }
+
+ function getTableCellSpanHeight(element, cell) {
+ const rows = Array.isArray(element?.rows) ? element.rows : []
+ const startRow = Number(cell?.row) || 0
+ const rowspan = Math.max(1, Number(cell?.rowspan) || 1)
+ return rows
+ .slice(startRow, startRow + rowspan)
+ .reduce((total, row) => total + (Number(row?.height) || 6), 0)
+ }
+
+ function getTableCellStyle(cell, element) {
+ const style = cell.style || {}
+ return {
+ border: `${editorMode.value ? mmToPx(style.borderWidth || 0.2, props.scale) : `${style.borderWidth || 0.2}mm`} solid ${style.borderColor || '#111111'}`,
+ backgroundColor: style.backgroundColor || '#FFFFFF',
+ textAlign: style.textAlign || 'left',
+ fontWeight: style.fontWeight || 400,
+ height: getUnitValue(getTableCellSpanHeight(element, cell)),
+ padding: 0,
+ overflow: 'hidden'
+ }
+ }
+
+ function getTableCellContentStyle(cell, element) {
+ const paddingY = editorMode.value ? `${0.4 * props.scale}px` : '0.4mm'
+ const paddingX = editorMode.value ? `${0.8 * props.scale}px` : '0.8mm'
+ return {
+ width: '100%',
+ height: getUnitValue(getTableCellSpanHeight(element, cell)),
+ boxSizing: 'border-box',
+ padding: `${paddingY} ${paddingX}`,
+ overflow: 'hidden',
+ display: 'flex',
+ alignItems: 'center',
+ whiteSpace: 'pre-wrap',
+ wordBreak: 'break-word'
+ }
+ }
+
+ function getTableCellsForRow(element, rowIndex) {
+ return (Array.isArray(element?.resolvedCells) ? element.resolvedCells : []).filter(
+ (cell) => Number(cell?.row) === rowIndex
+ )
+ }
+
+ function getQrcodeSize(element) {
+ const sizeMm = Math.max(element.w || 12, element.h || 12)
+ return Math.max(48, sizeMm * (editorMode.value ? props.scale : 4))
+ }
+
+ function handleCanvasMouseDown() {
+ if (!props.interactive) {
+ return
+ }
+ emit('select-element', '')
+ }
+
+ function handleElementMouseDown(event, element) {
+ if (!props.interactive) {
+ return
+ }
+ emit('select-element', element.id)
+ if (element.type === 'line') {
+ return startInteraction(event, element, 'drag')
+ }
+ if (event.target?.classList?.contains('matnr-print-element__handle')) {
+ return
+ }
+ startInteraction(event, element, 'drag')
+ }
+
+ function handleResizeMouseDown(event, element, direction) {
+ if (!props.interactive) {
+ return
+ }
+ emit('select-element', element.id)
+ startInteraction(event, element, 'resize', direction)
+ }
+
+ function startInteraction(event, element, type, direction = '') {
+ interactionState.value = {
+ type,
+ direction,
+ elementId: element.id,
+ startX: event.clientX,
+ startY: event.clientY,
+ origin: {
+ x: Number(element.x) || 0,
+ y: Number(element.y) || 0,
+ w: Number(element.w) || 0,
+ h: Number(element.h) || 0
+ }
+ }
+ window.addEventListener('mousemove', handleWindowMouseMove)
+ window.addEventListener('mouseup', stopInteraction)
+ }
+
+ function handleWindowMouseMove(event) {
+ if (!interactionState.value) {
+ return
+ }
+ const dx = snapMm((event.clientX - interactionState.value.startX) / props.scale)
+ const dy = snapMm((event.clientY - interactionState.value.startY) / props.scale)
+ const origin = interactionState.value.origin
+ let patch = {}
+
+ if (interactionState.value.type === 'drag') {
+ patch = {
+ x: Math.max(0, origin.x + dx),
+ y: Math.max(0, origin.y + dy)
+ }
+ } else if (interactionState.value.type === 'resize') {
+ patch = buildResizePatch(origin, dx, dy, interactionState.value.direction)
+ }
+
+ emit('update-element', {
+ id: interactionState.value.elementId,
+ patch
+ })
+ }
+
+ function buildResizePatch(origin, dx, dy, direction) {
+ const next = {
+ x: origin.x,
+ y: origin.y,
+ w: origin.w,
+ h: origin.h
+ }
+
+ if (direction.includes('left')) {
+ next.x = Math.max(0, origin.x + dx)
+ next.w = Math.max(2, origin.w - dx)
+ }
+ if (direction.includes('right')) {
+ next.w = Math.max(2, origin.w + dx)
+ }
+ if (direction.includes('top')) {
+ next.y = Math.max(0, origin.y + dy)
+ next.h = Math.max(2, origin.h - dy)
+ }
+ if (direction.includes('bottom')) {
+ next.h = Math.max(2, origin.h + dy)
+ }
+
+ if (next.w === 2 && direction.includes('left')) {
+ next.x = origin.x + origin.w - 2
+ }
+ if (next.h === 2 && direction.includes('top')) {
+ next.y = origin.y + origin.h - 2
+ }
+
+ return next
+ }
+
+ function stopInteraction() {
+ interactionState.value = null
+ window.removeEventListener('mousemove', handleWindowMouseMove)
+ window.removeEventListener('mouseup', stopInteraction)
+ }
+
+ function snapMm(value) {
+ return Math.round(Number(value || 0))
+ }
+
+ onBeforeUnmount(() => {
+ stopInteraction()
+ })
+</script>
+
+<style scoped>
+ .matnr-print-canvas-shell {
+ width: 100%;
+ min-width: 100%;
+ min-height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+ }
+
+ .matnr-print-canvas {
+ position: relative;
+ box-sizing: border-box;
+ flex: none;
+ margin: 0 auto;
+ box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
+ }
+
+ .matnr-print-canvas--editor {
+ border: 1px solid rgba(148, 163, 184, 0.45);
+ }
+
+ .matnr-print-canvas--grid {
+ background-image:
+ linear-gradient(to right, rgba(148, 163, 184, 0.2) 1px, transparent 1px),
+ linear-gradient(to bottom, rgba(148, 163, 184, 0.2) 1px, transparent 1px);
+ background-size: var(--matnr-print-grid-size) var(--matnr-print-grid-size);
+ }
+
+ .matnr-print-element {
+ position: absolute;
+ box-sizing: border-box;
+ overflow: hidden;
+ user-select: none;
+ }
+
+ .matnr-print-element.is-selected {
+ outline: 1px solid #2563eb;
+ outline-offset: 0;
+ }
+
+ .matnr-print-element__text,
+ .matnr-print-element__barcode,
+ .matnr-print-element__qrcode,
+ .matnr-print-element__rect,
+ .matnr-print-element__table {
+ width: 100%;
+ height: 100%;
+ }
+
+ .matnr-print-element__text {
+ display: flex;
+ align-items: center;
+ word-break: break-word;
+ white-space: pre-wrap;
+ }
+
+ .matnr-print-element__barcode :deep(svg) {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+
+ .matnr-print-element__qrcode {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .matnr-print-element__qrcode :deep(svg) {
+ width: 100%;
+ height: 100%;
+ }
+
+ .matnr-print-element__line {
+ position: absolute;
+ inset: 0;
+ }
+
+ .matnr-print-element__rect {
+ box-sizing: border-box;
+ }
+
+ .matnr-print-element__table {
+ border-collapse: collapse;
+ }
+
+ .matnr-print-element__table td {
+ box-sizing: border-box;
+ vertical-align: middle;
+ }
+
+ .matnr-print-element__table-cell-content {
+ box-sizing: border-box;
+ }
+
+ .matnr-print-element__handle {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ border: 1px solid #2563eb;
+ background: #ffffff;
+ border-radius: 50%;
+ z-index: 3;
+ }
+
+ .matnr-print-element__handle.is-top-left {
+ top: -5px;
+ left: -5px;
+ cursor: nwse-resize;
+ }
+
+ .matnr-print-element__handle.is-top {
+ top: -5px;
+ left: calc(50% - 5px);
+ cursor: ns-resize;
+ }
+
+ .matnr-print-element__handle.is-top-right {
+ top: -5px;
+ right: -5px;
+ cursor: nesw-resize;
+ }
+
+ .matnr-print-element__handle.is-right {
+ top: calc(50% - 5px);
+ right: -5px;
+ cursor: ew-resize;
+ }
+
+ .matnr-print-element__handle.is-bottom-right {
+ right: -5px;
+ bottom: -5px;
+ cursor: nwse-resize;
+ }
+
+ .matnr-print-element__handle.is-bottom {
+ left: calc(50% - 5px);
+ bottom: -5px;
+ cursor: ns-resize;
+ }
+
+ .matnr-print-element__handle.is-bottom-left {
+ left: -5px;
+ bottom: -5px;
+ cursor: nesw-resize;
+ }
+
+ .matnr-print-element__handle.is-left {
+ top: calc(50% - 5px);
+ left: -5px;
+ cursor: ew-resize;
+ }
+</style>
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-field-panel.vue b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-field-panel.vue
new file mode 100644
index 0000000..3f5315a
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-field-panel.vue
@@ -0,0 +1,88 @@
+<template>
+ <div class="matnr-print-field-panel">
+ <div class="matnr-print-field-panel__title">{{
+ t('pages.basicInfo.whMat.printTemplate.fieldPanel.title')
+ }}</div>
+ <ElScrollbar max-height="220px">
+ <div class="matnr-print-field-panel__list">
+ <button
+ v-for="field in fields"
+ :key="field.path"
+ type="button"
+ class="matnr-print-field-panel__item"
+ @click="$emit('insert-field', field.placeholder)"
+ >
+ <span>{{ field.label }}</span>
+ <small>{{ field.placeholder }}</small>
+ </button>
+ </div>
+ </ElScrollbar>
+ </div>
+</template>
+
+<script setup>
+ import { useI18n } from 'vue-i18n'
+
+ defineOptions({ name: 'MatnrPrintFieldPanel' })
+
+ const { t } = useI18n()
+
+ defineProps({
+ fields: {
+ type: Array,
+ default: () => []
+ }
+ })
+
+ defineEmits(['insert-field'])
+</script>
+
+<style scoped>
+ .matnr-print-field-panel {
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ gap: 10px;
+ padding: 12px;
+ border-top: 1px solid rgba(148, 163, 184, 0.18);
+ }
+
+ .matnr-print-field-panel__title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--art-text-primary);
+ }
+
+ .matnr-print-field-panel__list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .matnr-print-field-panel__item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid rgba(148, 163, 184, 0.24);
+ border-radius: 10px;
+ background: #ffffff;
+ color: var(--art-text-primary);
+ cursor: pointer;
+ transition:
+ border-color 0.2s ease,
+ transform 0.2s ease;
+ }
+
+ .matnr-print-field-panel__item:hover {
+ border-color: rgba(37, 99, 235, 0.45);
+ transform: translateY(-1px);
+ }
+
+ .matnr-print-field-panel__item small {
+ color: var(--art-text-secondary);
+ font-size: 12px;
+ }
+</style>
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-property-panel.vue b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-property-panel.vue
new file mode 100644
index 0000000..cec4625
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-property-panel.vue
@@ -0,0 +1,859 @@
+<template>
+ <div class="matnr-print-property-panel">
+ <ElScrollbar height="100%">
+ <div class="matnr-print-property-panel__content">
+ <section class="matnr-print-property-panel__section">
+ <div class="matnr-print-property-panel__section-title">
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.sections.templateInfo') }}
+ </div>
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.templateName')
+ }}</span>
+ <ElInput
+ :model-value="template.name"
+ @update:model-value="updateTemplateMeta('name', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.templateCode')
+ }}</span>
+ <ElInput
+ :model-value="template.code"
+ @update:model-value="updateTemplateMeta('code', $event)"
+ />
+ </label>
+ <label>
+ <span>{{ t('table.status') }}</span>
+ <ElSelect
+ :model-value="template.status"
+ @update:model-value="updateTemplateMeta('status', Number($event))"
+ >
+ <ElOption :value="1" :label="t('common.status.normal')" />
+ <ElOption :value="0" :label="t('common.status.frozen')" />
+ </ElSelect>
+ </label>
+ <label class="matnr-print-property-panel__switch">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.defaultTemplate')
+ }}</span>
+ <ElSwitch
+ :model-value="template.isDefault === 1"
+ @update:model-value="updateTemplateMeta('isDefault', $event ? 1 : 0)"
+ />
+ </label>
+ <label class="matnr-print-property-panel__span-2">
+ <span>{{ t('table.memo') }}</span>
+ <ElInput
+ type="textarea"
+ :rows="2"
+ :model-value="template.memo"
+ @update:model-value="updateTemplateMeta('memo', $event)"
+ />
+ </label>
+ </div>
+ </section>
+
+ <section class="matnr-print-property-panel__section">
+ <div class="matnr-print-property-panel__section-title">
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.sections.canvas') }}
+ </div>
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.widthMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="template.canvas?.width"
+ :min="10"
+ :step="1"
+ controls-position="right"
+ @update:model-value="updateCanvas('width', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.heightMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="template.canvas?.height"
+ :min="10"
+ :step="1"
+ controls-position="right"
+ @update:model-value="updateCanvas('height', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.backgroundColor')
+ }}</span>
+ <ElColorPicker
+ :model-value="template.canvas?.backgroundColor"
+ @change="updateCanvas('backgroundColor', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.gridSizeMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="template.canvas?.gridSize"
+ :min="1"
+ :step="1"
+ controls-position="right"
+ @update:model-value="updateCanvas('gridSize', $event)"
+ />
+ </label>
+ </div>
+ </section>
+
+ <section class="matnr-print-property-panel__section">
+ <div class="matnr-print-property-panel__section-header">
+ <div class="matnr-print-property-panel__section-title">
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.sections.element') }}
+ </div>
+ <ElButton
+ v-if="selectedElement"
+ type="danger"
+ text
+ @click="$emit('remove-element', selectedElement.id)"
+ >
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.actions.removeElement') }}
+ </ElButton>
+ </div>
+
+ <div v-if="!selectedElement" class="matnr-print-property-panel__empty">
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.empty') }}
+ </div>
+
+ <template v-else>
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.elementType')
+ }}</span>
+ <ElInput :model-value="selectedElement.type" disabled />
+ </label>
+ <label class="matnr-print-property-panel__switch">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.visible')
+ }}</span>
+ <ElSwitch
+ :model-value="selectedElement.visible !== false"
+ @update:model-value="updateElement('visible', $event)"
+ />
+ </label>
+ <label>
+ <span>X(mm)</span>
+ <ElInputNumber
+ :model-value="selectedElement.x"
+ :min="0"
+ controls-position="right"
+ @update:model-value="updateElement('x', $event)"
+ />
+ </label>
+ <label>
+ <span>Y(mm)</span>
+ <ElInputNumber
+ :model-value="selectedElement.y"
+ :min="0"
+ controls-position="right"
+ @update:model-value="updateElement('y', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.widthMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.w"
+ :min="1"
+ controls-position="right"
+ @update:model-value="updateElement('w', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.heightMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.h"
+ :min="0.4"
+ :step="0.2"
+ controls-position="right"
+ @update:model-value="updateElement('h', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.zIndex')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.zIndex"
+ :min="1"
+ controls-position="right"
+ @update:model-value="updateElement('zIndex', $event)"
+ />
+ </label>
+ </div>
+
+ <template v-if="selectedElement.type === 'text'">
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.contentMode')
+ }}</span>
+ <ElSelect
+ :model-value="selectedElement.contentMode"
+ @update:model-value="updateElement('contentMode', $event)"
+ >
+ <ElOption
+ value="static"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.staticText')
+ "
+ />
+ <ElOption
+ value="template"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.templateText')
+ "
+ />
+ </ElSelect>
+ </label>
+ <label class="matnr-print-property-panel__span-2">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.textContent')
+ }}</span>
+ <ElInput
+ type="textarea"
+ :rows="3"
+ :model-value="selectedElement.contentTemplate"
+ @focus="setPlaceholderTarget('contentTemplate')"
+ @update:model-value="updateElement('contentTemplate', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.fontSizeMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.style?.fontSize"
+ :min="1.8"
+ :step="0.2"
+ controls-position="right"
+ @update:model-value="updateElementStyle('fontSize', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.fontWeight')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.style?.fontWeight"
+ :min="100"
+ :max="900"
+ :step="100"
+ controls-position="right"
+ @update:model-value="updateElementStyle('fontWeight', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.textAlign')
+ }}</span>
+ <ElSelect
+ :model-value="selectedElement.style?.textAlign"
+ @update:model-value="updateElementStyle('textAlign', $event)"
+ >
+ <ElOption
+ value="left"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.alignLeft')
+ "
+ />
+ <ElOption
+ value="center"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.alignCenter')
+ "
+ />
+ <ElOption
+ value="right"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.alignRight')
+ "
+ />
+ </ElSelect>
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.textColor')
+ }}</span>
+ <ElColorPicker
+ :model-value="selectedElement.style?.color"
+ @change="updateElementStyle('color', $event)"
+ />
+ </label>
+ </div>
+ </template>
+
+ <template v-else-if="selectedElement.type === 'barcode'">
+ <div class="matnr-print-property-panel__grid">
+ <label class="matnr-print-property-panel__span-2">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.valueTemplate')
+ }}</span>
+ <ElInput
+ :model-value="selectedElement.valueTemplate"
+ @focus="setPlaceholderTarget('valueTemplate')"
+ @update:model-value="updateElement('valueTemplate', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.symbology')
+ }}</span>
+ <ElInput model-value="CODE128" disabled />
+ </label>
+ <label class="matnr-print-property-panel__switch">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.showText')
+ }}</span>
+ <ElSwitch
+ :model-value="selectedElement.showText !== false"
+ @update:model-value="updateElement('showText', $event)"
+ />
+ </label>
+ </div>
+ </template>
+
+ <template v-else-if="selectedElement.type === 'qrcode'">
+ <div class="matnr-print-property-panel__grid">
+ <label class="matnr-print-property-panel__span-2">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.valueTemplate')
+ }}</span>
+ <ElInput
+ :model-value="selectedElement.valueTemplate"
+ @focus="setPlaceholderTarget('valueTemplate')"
+ @update:model-value="updateElement('valueTemplate', $event)"
+ />
+ </label>
+ </div>
+ </template>
+
+ <template v-else-if="selectedElement.type === 'line'">
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.direction')
+ }}</span>
+ <ElSelect
+ :model-value="selectedElement.direction"
+ @update:model-value="updateLineDirection($event)"
+ >
+ <ElOption
+ value="horizontal"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.horizontal')
+ "
+ />
+ <ElOption
+ value="vertical"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.vertical')
+ "
+ />
+ </ElSelect>
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.lineWidthMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.borderWidth"
+ :min="0.2"
+ :step="0.2"
+ controls-position="right"
+ @update:model-value="updateElement('borderWidth', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.lineColor')
+ }}</span>
+ <ElColorPicker
+ :model-value="selectedElement.color"
+ @change="updateElement('color', $event)"
+ />
+ </label>
+ </div>
+ </template>
+
+ <template v-else-if="selectedElement.type === 'rect'">
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.borderWidthMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.borderWidth"
+ :min="0.2"
+ :step="0.2"
+ controls-position="right"
+ @update:model-value="updateElement('borderWidth', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.radiusMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.radius"
+ :min="0"
+ :step="0.5"
+ controls-position="right"
+ @update:model-value="updateElement('radius', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.borderColor')
+ }}</span>
+ <ElColorPicker
+ :model-value="selectedElement.borderColor"
+ @change="updateElement('borderColor', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.fillColor')
+ }}</span>
+ <ElColorPicker
+ :model-value="selectedElement.backgroundColor"
+ @change="updateElement('backgroundColor', $event)"
+ />
+ </label>
+ </div>
+ </template>
+
+ <template v-else-if="selectedElement.type === 'table'">
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.fontSizeMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.style?.fontSize"
+ :min="1.8"
+ :step="0.2"
+ controls-position="right"
+ @update:model-value="updateElementStyle('fontSize', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.borderWidthMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="selectedElement.style?.borderWidth"
+ :min="0.2"
+ :step="0.2"
+ controls-position="right"
+ @update:model-value="updateElementStyle('borderWidth', $event)"
+ />
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.textAlign')
+ }}</span>
+ <ElSelect
+ :model-value="selectedElement.style?.textAlign"
+ @update:model-value="updateElementStyle('textAlign', $event)"
+ >
+ <ElOption
+ value="left"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.alignLeft')
+ "
+ />
+ <ElOption
+ value="center"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.alignCenter')
+ "
+ />
+ <ElOption
+ value="right"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.alignRight')
+ "
+ />
+ </ElSelect>
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.borderColor')
+ }}</span>
+ <ElColorPicker
+ :model-value="selectedElement.style?.borderColor"
+ @change="updateElementStyle('borderColor', $event)"
+ />
+ </label>
+ </div>
+
+ <div class="matnr-print-property-panel__section-subtitle">
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.sections.fieldList') }}
+ </div>
+ <div class="matnr-print-property-panel__table-rows">
+ <div
+ v-for="(row, rowIndex) in tableRows"
+ :key="row.id"
+ class="matnr-print-property-panel__table-row"
+ >
+ <div class="matnr-print-property-panel__table-row-header">
+ <span>
+ {{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.rowTitle', {
+ index: rowIndex + 1
+ })
+ }}
+ </span>
+ <ElButton type="danger" text @click="removeTableRow(rowIndex)">
+ {{ t('common.actions.delete') }}
+ </ElButton>
+ </div>
+ <div class="matnr-print-property-panel__grid">
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.leftMode')
+ }}</span>
+ <ElSelect
+ :model-value="row.labelMode"
+ @update:model-value="updateTableRow(rowIndex, 'labelMode', $event)"
+ >
+ <ElOption
+ value="static"
+ :label="
+ t(
+ 'pages.basicInfo.whMat.printTemplate.propertyPanel.options.staticText'
+ )
+ "
+ />
+ <ElOption
+ value="template"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.template')
+ "
+ />
+ </ElSelect>
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.rightMode')
+ }}</span>
+ <ElSelect
+ :model-value="row.valueMode"
+ @update:model-value="updateTableRow(rowIndex, 'valueMode', $event)"
+ >
+ <ElOption
+ value="template"
+ :label="
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.options.template')
+ "
+ />
+ <ElOption
+ value="static"
+ :label="
+ t(
+ 'pages.basicInfo.whMat.printTemplate.propertyPanel.options.staticText'
+ )
+ "
+ />
+ </ElSelect>
+ </label>
+ <label>
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.rowHeightMm')
+ }}</span>
+ <ElInputNumber
+ :model-value="row.height"
+ :min="4"
+ :step="1"
+ controls-position="right"
+ @update:model-value="updateTableRow(rowIndex, 'height', $event)"
+ />
+ </label>
+ <label class="matnr-print-property-panel__span-2">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.leftContent')
+ }}</span>
+ <ElInput
+ :model-value="row.labelTemplate"
+ @focus="setPlaceholderTarget('table', rowIndex, 'labelTemplate')"
+ @update:model-value="updateTableRow(rowIndex, 'labelTemplate', $event)"
+ />
+ </label>
+ <label class="matnr-print-property-panel__span-2">
+ <span>{{
+ t('pages.basicInfo.whMat.printTemplate.propertyPanel.fields.rightContent')
+ }}</span>
+ <ElInput
+ :model-value="row.valueTemplate"
+ @focus="setPlaceholderTarget('table', rowIndex, 'valueTemplate')"
+ @update:model-value="updateTableRow(rowIndex, 'valueTemplate', $event)"
+ />
+ </label>
+ </div>
+ </div>
+ </div>
+ <ElButton size="small" @click="appendTableRow">
+ {{ t('pages.basicInfo.whMat.printTemplate.propertyPanel.actions.addRow') }}
+ </ElButton>
+ </template>
+ </template>
+ </section>
+ </div>
+ </ElScrollbar>
+ </div>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { getFieldListTableRows, updateFieldListTableRows } from '../matnrPrintTemplate.helpers'
+
+ defineOptions({ name: 'MatnrPrintPropertyPanel' })
+
+ const { t } = useI18n()
+
+ const props = defineProps({
+ template: {
+ type: Object,
+ default: () => ({})
+ },
+ selectedElement: {
+ type: Object,
+ default: null
+ }
+ })
+
+ const emit = defineEmits([
+ 'update-template-meta',
+ 'update-canvas',
+ 'update-element',
+ 'remove-element',
+ 'set-placeholder-target'
+ ])
+
+ const tableRows = computed(() => getFieldListTableRows(props.selectedElement))
+
+ function updateTemplateMeta(field, value) {
+ emit('update-template-meta', { field, value })
+ }
+
+ function updateCanvas(field, value) {
+ emit('update-canvas', { field, value })
+ }
+
+ function updateElement(field, value) {
+ emit('update-element', {
+ id: props.selectedElement?.id,
+ patch: {
+ [field]: value
+ }
+ })
+ }
+
+ function updateElementStyle(field, value) {
+ emit('update-element', {
+ id: props.selectedElement?.id,
+ patch: {
+ style: {
+ ...(props.selectedElement?.style || {}),
+ [field]: value
+ }
+ }
+ })
+ }
+
+ function updateLineDirection(direction) {
+ const patch =
+ direction === 'vertical'
+ ? {
+ direction,
+ w: Math.max(0.4, Number(props.selectedElement?.borderWidth || 0.4))
+ }
+ : {
+ direction,
+ h: Math.max(0.4, Number(props.selectedElement?.borderWidth || 0.4))
+ }
+ emit('update-element', {
+ id: props.selectedElement?.id,
+ patch
+ })
+ }
+
+ function setPlaceholderTarget(field, rowIndex, rowField) {
+ emit('set-placeholder-target', {
+ elementId: props.selectedElement?.id,
+ field,
+ rowIndex,
+ rowField
+ })
+ }
+
+ function appendTableRow() {
+ const nextRows = [
+ ...tableRows.value,
+ {
+ height: 6,
+ labelTemplate: t('pages.basicInfo.whMat.printTemplate.propertyPanel.defaultFieldLabel'),
+ labelMode: 'static',
+ valueTemplate: '{code}',
+ valueMode: 'template'
+ }
+ ]
+ syncTableRows(nextRows)
+ }
+
+ function removeTableRow(index) {
+ const nextRows = tableRows.value.filter((_, rowIndex) => rowIndex !== index)
+ syncTableRows(nextRows)
+ }
+
+ function updateTableRow(index, field, value) {
+ const nextRows = tableRows.value.map((row, rowIndex) =>
+ rowIndex === index
+ ? {
+ ...row,
+ [field]: value
+ }
+ : row
+ )
+ syncTableRows(nextRows)
+ }
+
+ function syncTableRows(rows) {
+ emit('update-element', {
+ id: props.selectedElement?.id,
+ patch: updateFieldListTableRows(props.selectedElement, rows)
+ })
+ }
+</script>
+
+<style scoped>
+ .matnr-print-property-panel {
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ background: #ffffff;
+ }
+
+ .matnr-print-property-panel__content {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
+ }
+
+ .matnr-print-property-panel__section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 14px;
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: 16px;
+ background: linear-gradient(180deg, #ffffff, #f8fafc);
+ }
+
+ .matnr-print-property-panel__section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ }
+
+ .matnr-print-property-panel__section-title {
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--art-text-primary);
+ }
+
+ .matnr-print-property-panel__section-subtitle {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--art-text-primary);
+ }
+
+ .matnr-print-property-panel__grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+ }
+
+ .matnr-print-property-panel__grid label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 0;
+ font-size: 12px;
+ color: var(--art-text-secondary);
+ }
+
+ .matnr-print-property-panel__span-2 {
+ grid-column: span 2;
+ }
+
+ .matnr-print-property-panel__switch {
+ justify-content: space-between;
+ }
+
+ .matnr-print-property-panel__empty {
+ padding: 18px 14px;
+ border-radius: 12px;
+ background: rgba(241, 245, 249, 0.85);
+ color: var(--art-text-secondary);
+ font-size: 13px;
+ line-height: 1.6;
+ }
+
+ .matnr-print-property-panel__table-rows {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .matnr-print-property-panel__table-row {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.9);
+ }
+
+ .matnr-print-property-panel__table-row-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--art-text-primary);
+ }
+
+ @media (max-width: 1280px) {
+ .matnr-print-property-panel__grid {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .matnr-print-property-panel__span-2 {
+ grid-column: span 1;
+ }
+ }
+</style>
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-toolbar.vue b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-toolbar.vue
new file mode 100644
index 0000000..aa9d9fd
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/matnr-print-toolbar.vue
@@ -0,0 +1,86 @@
+<template>
+ <div class="matnr-print-toolbar">
+ <ElSpace wrap>
+ <ElButton
+ v-for="item in items"
+ :key="item.type"
+ size="small"
+ @click="$emit('add-element', item.type)"
+ >
+ {{ item.label }}
+ </ElButton>
+ </ElSpace>
+
+ <ElSpace wrap>
+ <ElButton size="small" :disabled="!canZoomOut" @click="$emit('zoom-out')">
+ {{ t('pages.basicInfo.whMat.printTemplate.toolbar.zoomOut') }}
+ </ElButton>
+ <ElButton size="small" @click="$emit('zoom-reset')">{{ zoomPercent }}%</ElButton>
+ <ElButton size="small" :disabled="!canZoomIn" @click="$emit('zoom-in')">
+ {{ t('pages.basicInfo.whMat.printTemplate.toolbar.zoomIn') }}
+ </ElButton>
+ <ElButton
+ size="small"
+ :type="autoFitActive ? 'primary' : 'default'"
+ @click="$emit('zoom-fit')"
+ >
+ {{ t('pages.basicInfo.whMat.printTemplate.toolbar.zoomFit') }}
+ </ElButton>
+ </ElSpace>
+ </div>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { useI18n } from 'vue-i18n'
+
+ defineOptions({ name: 'MatnrPrintToolbar' })
+
+ defineProps({
+ zoomPercent: {
+ type: Number,
+ default: 100
+ },
+ canZoomIn: {
+ type: Boolean,
+ default: true
+ },
+ canZoomOut: {
+ type: Boolean,
+ default: true
+ },
+ autoFitActive: {
+ type: Boolean,
+ default: false
+ }
+ })
+
+ defineEmits(['add-element', 'zoom-in', 'zoom-out', 'zoom-reset', 'zoom-fit'])
+
+ const { t, locale } = useI18n()
+
+ const items = computed(() => {
+ locale.value
+ return [
+ { type: 'text', label: t('pages.basicInfo.whMat.printTemplate.toolbar.elements.text') },
+ { type: 'barcode', label: t('pages.basicInfo.whMat.printTemplate.toolbar.elements.barcode') },
+ { type: 'qrcode', label: t('pages.basicInfo.whMat.printTemplate.toolbar.elements.qrcode') },
+ { type: 'line', label: t('pages.basicInfo.whMat.printTemplate.toolbar.elements.line') },
+ { type: 'rect', label: t('pages.basicInfo.whMat.printTemplate.toolbar.elements.rect') },
+ { type: 'table', label: t('pages.basicInfo.whMat.printTemplate.toolbar.elements.table') }
+ ]
+ })
+</script>
+
+<style scoped>
+ .matnr-print-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ padding: 12px 16px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
+ background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.9));
+ }
+</style>
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-dialog.vue b/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-dialog.vue
new file mode 100644
index 0000000..5d348ab
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-dialog.vue
@@ -0,0 +1,694 @@
+<template>
+ <ElDialog
+ v-model="visibleModel"
+ :title="t('pages.basicInfo.whMat.printTemplate.dialog.title')"
+ width="min(96vw, 1180px)"
+ top="4vh"
+ class="wh-mat-print-preview-dialog"
+ destroy-on-close
+ >
+ <div class="wh-mat-print-dialog">
+ <div class="wh-mat-print-dialog__toolbar">
+ <ElSelect
+ v-model="selectedTemplateId"
+ class="wh-mat-print-dialog__template-select"
+ :placeholder="t('pages.basicInfo.whMat.printTemplate.dialog.templatePlaceholder')"
+ :disabled="loading || !templateOptions.length"
+ @change="handleTemplateChange"
+ >
+ <ElOption
+ v-for="item in templateOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </ElSelect>
+ <div class="wh-mat-print-dialog__toolbar-right">
+ <div class="wh-mat-print-dialog__copies">
+ <span class="wh-mat-print-dialog__copies-label">
+ {{ t('pages.basicInfo.whMat.printTemplate.dialog.copyCount') }}
+ </span>
+ <ElInputNumber
+ v-model="copyCount"
+ :min="1"
+ :max="99"
+ :step="1"
+ :precision="0"
+ size="small"
+ class="wh-mat-print-dialog__copies-input"
+ />
+ </div>
+ <span class="wh-mat-print-dialog__summary">
+ {{
+ t('pages.basicInfo.whMat.printTemplate.dialog.labelCount', {
+ count: previewRows.length
+ })
+ }}
+ </span>
+ <div class="wh-mat-print-dialog__zoom">
+ <ElButton size="small" :disabled="!canZoomOut" @click="handleZoomOut">
+ {{ t('pages.basicInfo.whMat.printTemplate.toolbar.zoomOut') }}
+ </ElButton>
+ <ElButton size="small" @click="handleZoomReset"> {{ previewZoomPercent }}% </ElButton>
+ <ElButton size="small" :disabled="!canZoomIn" @click="handleZoomIn">
+ {{ t('pages.basicInfo.whMat.printTemplate.toolbar.zoomIn') }}
+ </ElButton>
+ <ElButton
+ size="small"
+ :type="autoFitEnabled ? 'primary' : 'default'"
+ @click="handleZoomFit"
+ >
+ {{ t('pages.basicInfo.whMat.printTemplate.toolbar.zoomFit') }}
+ </ElButton>
+ </div>
+ </div>
+ </div>
+
+ <ElEmpty
+ v-if="!templateOptions.length"
+ :description="t('pages.basicInfo.whMat.printTemplate.dialog.noTemplate')"
+ />
+
+ <ElEmpty
+ v-else-if="!normalizedRows.length"
+ :description="t('pages.basicInfo.whMat.printTemplate.dialog.noData')"
+ />
+
+ <template v-else>
+ <ElAlert
+ v-if="templateOverflow.hasOverflow"
+ class="wh-mat-print-dialog__overflow-alert"
+ type="warning"
+ :closable="false"
+ show-icon
+ :title="t('pages.basicInfo.whMat.printTemplate.dialog.canvasOverflow')"
+ :description="t('pages.basicInfo.whMat.printTemplate.dialog.canvasOverflowDescription')"
+ />
+
+ <div ref="previewViewportRef" class="wh-mat-print-dialog__preview-list">
+ <div
+ v-for="item in previewItems"
+ :key="item.key"
+ class="wh-mat-print-dialog__preview-item"
+ :style="item.previewStyle"
+ >
+ <div class="wh-mat-print-dialog__preview-canvas" :style="item.previewCanvasStyle">
+ <MatnrPrintCanvas
+ v-if="activeTemplate"
+ :template="activeTemplate"
+ :active-record="item.row"
+ mode="preview"
+ :interactive="false"
+ :show-grid="false"
+ :scale="4"
+ />
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <div
+ v-if="printSourceVisible && activeTemplate && normalizedRows.length"
+ ref="printBodyRef"
+ class="wh-mat-print-dialog__print-source"
+ >
+ <div
+ v-for="item in previewItems"
+ :key="`${item.key}_print`"
+ class="wh-mat-print-dialog__preview-item wh-mat-print-dialog__preview-item--print"
+ :style="item.printStyle"
+ >
+ <MatnrPrintCanvas
+ :template="activeTemplate"
+ :active-record="item.row"
+ mode="preview"
+ :interactive="false"
+ :show-grid="false"
+ :scale="4"
+ />
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="wh-mat-print-dialog__footer">
+ <ElButton @click="visibleModel = false">{{ t('common.actions.close') }}</ElButton>
+ <ElButton
+ type="primary"
+ :disabled="!activeTemplate || !normalizedRows.length"
+ @click="handlePrint"
+ >
+ {{ t('pages.basicInfo.whMat.printTemplate.dialog.confirmPrint') }}
+ </ElButton>
+ </div>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { ElMessage } from 'element-plus'
+ import {
+ fetchGetDefaultMatnrPrintTemplate,
+ fetchGetMatnrPrintTemplateDetail,
+ fetchMatnrPrintTemplateList
+ } from '@/api/wh-mat'
+ import {
+ detectMatnrPrintTemplateOverflow,
+ normalizeMatnrPrintTemplate
+ } from '../matnrPrintTemplate.helpers'
+ import MatnrPrintCanvas from './matnr-print-canvas.vue'
+
+ defineOptions({ name: 'WhMatPrintDialog' })
+
+ const { t, locale } = useI18n()
+
+ const visibleModel = defineModel('visible', {
+ type: Boolean,
+ default: false
+ })
+
+ const props = defineProps({
+ rows: {
+ type: Array,
+ default: () => []
+ }
+ })
+
+ const previewViewportRef = ref(null)
+ const printBodyRef = ref(null)
+ const loading = ref(false)
+ const templateOptions = ref([])
+ const selectedTemplateId = ref(null)
+ const activeTemplate = ref(null)
+ const printSourceVisible = ref(false)
+ const copyCount = ref(1)
+ const SCREEN_MM_TO_PX = 96 / 25.4
+ const DEFAULT_PREVIEW_SCALE = 4
+ const MIN_PREVIEW_SCALE = 0.5
+ const MAX_PREVIEW_SCALE = 24
+ const PREVIEW_SCALE_STEPS = [0.5, 0.75, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20, 24]
+ const PREVIEW_PANEL_GAP = 24
+ const previewScale = ref(DEFAULT_PREVIEW_SCALE)
+ const autoFitEnabled = ref(true)
+ let resizeObserver = null
+
+ const normalizedRows = computed(() => (Array.isArray(props.rows) ? props.rows : []))
+ const previewRows = computed(() =>
+ normalizedRows.value.flatMap((row, rowIndex) =>
+ Array.from({ length: copyCount.value }, (_, copyIndex) => ({
+ key: `${row.id || row.code || rowIndex}_copy_${copyIndex + 1}`,
+ row
+ }))
+ )
+ )
+ const activeCanvas = computed(() => ({
+ width: Number(activeTemplate.value?.canvas?.width) || 70,
+ height: Number(activeTemplate.value?.canvas?.height) || 40
+ }))
+ const previewZoomRatio = computed(() =>
+ Number((previewScale.value / DEFAULT_PREVIEW_SCALE).toFixed(4))
+ )
+ const previewZoomPercent = computed(() =>
+ Math.round((previewScale.value / DEFAULT_PREVIEW_SCALE) * 100)
+ )
+ const canZoomIn = computed(() => previewScale.value < MAX_PREVIEW_SCALE)
+ const canZoomOut = computed(() => previewScale.value > MIN_PREVIEW_SCALE)
+ const templateOverflow = computed(() => detectMatnrPrintTemplateOverflow(activeTemplate.value))
+ const previewItems = computed(() =>
+ previewRows.value.map((item) => {
+ const canvas = activeCanvas.value
+ const zoomRatio = previewZoomRatio.value
+ return {
+ key: item.key,
+ row: item.row,
+ previewStyle: {
+ width: `${canvas.width * SCREEN_MM_TO_PX * zoomRatio}px`,
+ height: `${canvas.height * SCREEN_MM_TO_PX * zoomRatio}px`
+ },
+ previewCanvasStyle: {
+ width: `${canvas.width}mm`,
+ height: `${canvas.height}mm`,
+ transform: `scale(${zoomRatio})`
+ },
+ printStyle: {
+ width: `${canvas.width}mm`,
+ height: `${canvas.height}mm`
+ }
+ }
+ })
+ )
+
+ watch(
+ () => visibleModel.value,
+ async (visible) => {
+ if (visible) {
+ copyCount.value = 1
+ await loadTemplates()
+ await nextTick()
+ observePreviewViewport()
+ fitPreviewToViewport()
+ return
+ }
+ printSourceVisible.value = false
+ copyCount.value = 1
+ }
+ )
+
+ watch(
+ () => [activeTemplate.value?.id, normalizedRows.value.length],
+ async () => {
+ if (!visibleModel.value) {
+ return
+ }
+ await nextTick()
+ if (autoFitEnabled.value) {
+ fitPreviewToViewport()
+ }
+ }
+ )
+
+ async function loadTemplates() {
+ loading.value = true
+ try {
+ const [templates, defaultTemplate] = await Promise.all([
+ fetchMatnrPrintTemplateList({}),
+ fetchGetDefaultMatnrPrintTemplate()
+ ])
+ const nextTemplates = (Array.isArray(templates) ? templates : [])
+ .map((item) => normalizeMatnrPrintTemplate(item))
+ .filter((item) => item.status === 1)
+ templateOptions.value = nextTemplates
+ if (!nextTemplates.length) {
+ selectedTemplateId.value = null
+ activeTemplate.value = null
+ return
+ }
+
+ const defaultId =
+ defaultTemplate && typeof defaultTemplate === 'object'
+ ? Number(defaultTemplate.id) || null
+ : null
+ const nextTemplateId =
+ nextTemplates.find((item) => item.id === selectedTemplateId.value)?.id ||
+ nextTemplates.find((item) => item.id === defaultId)?.id ||
+ nextTemplates[0]?.id
+ selectedTemplateId.value = nextTemplateId
+ await handleTemplateChange(nextTemplateId)
+ } catch (error) {
+ ElMessage.error(
+ error?.message || t('pages.basicInfo.whMat.printTemplate.dialog.messages.loadFailed')
+ )
+ templateOptions.value = []
+ activeTemplate.value = null
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function handleTemplateChange(templateId) {
+ if (!templateId) {
+ activeTemplate.value = null
+ return
+ }
+ try {
+ const detail = await fetchGetMatnrPrintTemplateDetail(templateId)
+ activeTemplate.value = normalizeMatnrPrintTemplate(detail)
+ } catch (error) {
+ ElMessage.error(
+ error?.message || t('pages.basicInfo.whMat.printTemplate.dialog.messages.detailFailed')
+ )
+ activeTemplate.value = null
+ }
+ }
+
+ async function handlePrint() {
+ if (!activeTemplate.value) {
+ return
+ }
+
+ if (!printSourceVisible.value || !printBodyRef.value) {
+ printSourceVisible.value = true
+ await nextTick()
+ await waitForPaint()
+ }
+
+ if (!printBodyRef.value) {
+ printSourceVisible.value = false
+ return
+ }
+
+ const printWindow = window.open('', '_blank')
+ if (!printWindow) {
+ printSourceVisible.value = false
+ ElMessage.warning(t('pages.basicInfo.whMat.printTemplate.dialog.messages.popupBlocked'))
+ return
+ }
+
+ const content = printBodyRef.value.innerHTML
+ printSourceVisible.value = false
+ printWindow.document.open()
+ printWindow.document.write(buildPrintHtml(content, activeCanvas.value))
+ printWindow.document.close()
+ }
+
+ function buildPrintHtml(content, canvas) {
+ const printCanvas = canvas || { width: 70, height: 40 }
+ return (
+ `<!DOCTYPE html>
+<html lang="${locale.value === 'en' ? 'en' : 'zh-CN'}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>${t('pages.basicInfo.whMat.printTemplate.dialog.printDocumentTitle')}</title>
+ <style>
+ :root {
+ color-scheme: light;
+ }
+ * {
+ box-sizing: border-box;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
+ @page {
+ size: ${printCanvas.width}mm ${printCanvas.height}mm;
+ margin: 0;
+ }
+ html,
+ body {
+ margin: 0;
+ padding: 0;
+ background: #ffffff;
+ color: #111827;
+ font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
+ }
+ body {
+ display: block;
+ }
+ .wh-mat-print-dialog__print-pages {
+ display: block;
+ }
+ .wh-mat-print-dialog__preview-list {
+ display: block;
+ }
+ .wh-mat-print-dialog__preview-item {
+ display: block;
+ width: ${printCanvas.width}mm;
+ height: ${printCanvas.height}mm;
+ margin: 0;
+ overflow: hidden;
+ }
+ .wh-mat-print-dialog__preview-item + .wh-mat-print-dialog__preview-item {
+ page-break-before: always;
+ break-before: page;
+ }
+ .matnr-print-canvas-shell {
+ width: ${printCanvas.width}mm;
+ height: ${printCanvas.height}mm;
+ display: block;
+ overflow: hidden;
+ }
+ .matnr-print-canvas {
+ position: relative;
+ box-sizing: border-box;
+ display: block;
+ margin: 0;
+ box-shadow: none;
+ overflow: hidden;
+ }
+ .matnr-print-element {
+ position: absolute;
+ overflow: hidden;
+ }
+ .matnr-print-element__text,
+ .matnr-print-element__barcode,
+ .matnr-print-element__qrcode,
+ .matnr-print-element__rect,
+ .matnr-print-element__table {
+ width: 100%;
+ height: 100%;
+ }
+ .matnr-print-element__text {
+ display: flex;
+ align-items: center;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+ .matnr-print-element__barcode svg,
+ .matnr-print-element__qrcode svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+ .matnr-print-element__qrcode {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .matnr-print-element__table {
+ border-collapse: collapse;
+ table-layout: fixed;
+ }
+ .matnr-print-element__table td {
+ padding: 0.4mm 0.8mm;
+ vertical-align: middle;
+ word-break: break-word;
+ }
+ </style>
+ <scr` +
+ `ipt>
+ window.addEventListener('load', () => {
+ window.focus();
+ setTimeout(() => window.print(), 60);
+ });
+ window.addEventListener('afterprint', () => {
+ window.close();
+ });
+ </scr` +
+ `ipt>
+ </head>
+ <body><div class="wh-mat-print-dialog__print-pages">${content}</div></body>
+</html>`
+ )
+ }
+
+ function waitForPaint() {
+ return new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+
+ function resolveNextScale(direction) {
+ if (direction > 0) {
+ return (
+ PREVIEW_SCALE_STEPS.find((step) => step > previewScale.value + 0.001) || previewScale.value
+ )
+ }
+ return (
+ [...PREVIEW_SCALE_STEPS].reverse().find((step) => step < previewScale.value - 0.001) ||
+ previewScale.value
+ )
+ }
+
+ function getPreviewViewportAvailableSize() {
+ const viewportElement = previewViewportRef.value
+ if (!viewportElement) {
+ return null
+ }
+ const viewportStyle = window.getComputedStyle(viewportElement)
+ const width =
+ viewportElement.clientWidth -
+ Number.parseFloat(viewportStyle.paddingLeft || '0') -
+ Number.parseFloat(viewportStyle.paddingRight || '0') -
+ PREVIEW_PANEL_GAP
+ const height =
+ viewportElement.clientHeight -
+ Number.parseFloat(viewportStyle.paddingTop || '0') -
+ Number.parseFloat(viewportStyle.paddingBottom || '0') -
+ PREVIEW_PANEL_GAP
+ if (width <= 0 || height <= 0) {
+ return null
+ }
+ return { width, height }
+ }
+
+ function fitPreviewToViewport() {
+ const viewportSize = getPreviewViewportAvailableSize()
+ if (!viewportSize || !activeCanvas.value.width || !activeCanvas.value.height) {
+ return
+ }
+ const widthScale =
+ viewportSize.width / Math.max(Number(activeCanvas.value.width) * SCREEN_MM_TO_PX || 1, 1)
+ const heightScale =
+ viewportSize.height / Math.max(Number(activeCanvas.value.height) * SCREEN_MM_TO_PX || 1, 1)
+ const nextZoomRatio = Math.min(
+ widthScale,
+ heightScale,
+ MAX_PREVIEW_SCALE / DEFAULT_PREVIEW_SCALE
+ )
+ const nextScale = DEFAULT_PREVIEW_SCALE * nextZoomRatio
+ previewScale.value = Number(Math.max(MIN_PREVIEW_SCALE, nextScale).toFixed(2))
+ }
+
+ function observePreviewViewport() {
+ if (!resizeObserver || !previewViewportRef.value) {
+ return
+ }
+ resizeObserver.disconnect()
+ resizeObserver.observe(previewViewportRef.value)
+ }
+
+ function handleZoomIn() {
+ autoFitEnabled.value = false
+ previewScale.value = resolveNextScale(1)
+ }
+
+ function handleZoomOut() {
+ autoFitEnabled.value = false
+ previewScale.value = resolveNextScale(-1)
+ }
+
+ function handleZoomReset() {
+ autoFitEnabled.value = false
+ previewScale.value = DEFAULT_PREVIEW_SCALE
+ }
+
+ async function handleZoomFit() {
+ autoFitEnabled.value = true
+ await nextTick()
+ fitPreviewToViewport()
+ }
+
+ onMounted(() => {
+ resizeObserver = new ResizeObserver(() => {
+ if (autoFitEnabled.value) {
+ fitPreviewToViewport()
+ }
+ })
+ observePreviewViewport()
+ })
+
+ onBeforeUnmount(() => {
+ resizeObserver?.disconnect()
+ resizeObserver = null
+ })
+</script>
+
+<style scoped>
+ .wh-mat-print-dialog {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ height: min(70vh, 760px);
+ min-height: 420px;
+ }
+
+ .wh-mat-print-dialog__toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ }
+
+ .wh-mat-print-dialog__toolbar-right {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-left: auto;
+ }
+
+ .wh-mat-print-dialog__zoom {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .wh-mat-print-dialog__copies {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .wh-mat-print-dialog__copies-label {
+ font-size: 13px;
+ color: var(--art-text-secondary);
+ white-space: nowrap;
+ }
+
+ .wh-mat-print-dialog__copies-input {
+ width: 110px;
+ }
+
+ .wh-mat-print-dialog__template-select {
+ width: 320px;
+ max-width: 100%;
+ }
+
+ .wh-mat-print-dialog__summary {
+ font-size: 13px;
+ color: var(--art-text-secondary);
+ }
+
+ .wh-mat-print-dialog__overflow-alert {
+ margin-bottom: -4px;
+ }
+
+ .wh-mat-print-dialog__preview-list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+ min-height: 0;
+ gap: 16px;
+ overflow: auto;
+ padding: 12px;
+ background:
+ radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 28%),
+ linear-gradient(180deg, #eef2f7 0%, #f8fafc 100%);
+ border-radius: 16px;
+ }
+
+ .wh-mat-print-dialog__preview-item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ flex: none;
+ padding: 0;
+ border-radius: 0;
+ background: transparent;
+ box-shadow: none;
+ overflow: visible;
+ }
+
+ .wh-mat-print-dialog__preview-item :deep(.matnr-print-canvas-shell) {
+ width: auto;
+ min-width: 0;
+ min-height: 0;
+ display: block;
+ overflow: visible;
+ }
+
+ .wh-mat-print-dialog__preview-canvas {
+ transform-origin: center top;
+ }
+
+ .wh-mat-print-dialog__preview-canvas :deep(.matnr-print-canvas) {
+ box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
+ }
+
+ .wh-mat-print-dialog__print-source {
+ position: fixed;
+ left: -99999px;
+ top: 0;
+ visibility: hidden;
+ pointer-events: none;
+ }
+
+ .wh-mat-print-dialog__footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 12px;
+ }
+</style>
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-template-workspace.vue b/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-template-workspace.vue
new file mode 100644
index 0000000..e509ebd
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-print-template-workspace.vue
@@ -0,0 +1,749 @@
+<template>
+ <div class="wh-mat-print-template-manager__layout">
+ <aside class="wh-mat-print-template-manager__sidebar">
+ <div class="wh-mat-print-template-manager__sidebar-header">
+ <div>
+ <div class="wh-mat-print-template-manager__title">
+ {{ t('pages.basicInfo.whMat.printTemplate.workspace.sidebarTitle') }}
+ </div>
+ <div class="wh-mat-print-template-manager__subtitle">
+ {{ t('pages.basicInfo.whMat.printTemplate.workspace.sidebarSubtitle') }}
+ </div>
+ </div>
+ <ElButton size="small" type="primary" @click="handleCreateTemplate">
+ {{ t('common.actions.add') }}
+ </ElButton>
+ </div>
+
+ <ElScrollbar class="wh-mat-print-template-manager__sidebar-scroll">
+ <div class="wh-mat-print-template-manager__template-list">
+ <button
+ v-for="template in templates"
+ :key="template.id || template.code"
+ type="button"
+ class="wh-mat-print-template-manager__template-card"
+ :class="{ 'is-active': template.code === activeTemplateCode }"
+ @click="selectTemplate(template.code)"
+ >
+ <div class="wh-mat-print-template-manager__template-card-header">
+ <span>{{ template.name }}</span>
+ <ElTag v-if="template.isDefault === 1" size="small" type="success">
+ {{ t('pages.basicInfo.whMat.printTemplate.workspace.defaultTag') }}
+ </ElTag>
+ </div>
+ <div class="wh-mat-print-template-manager__template-code">{{ template.code }}</div>
+ <div class="wh-mat-print-template-manager__template-actions">
+ <ElButton size="small" text @click.stop="handleCopyTemplate(template.code)">{{
+ t('pages.basicInfo.whMat.printTemplate.workspace.actions.copy')
+ }}</ElButton>
+ <ElButton size="small" text @click.stop="handleMarkDefault(template.code)">
+ {{ t('pages.basicInfo.whMat.printTemplate.workspace.actions.setDefault') }}
+ </ElButton>
+ <ElButton
+ size="small"
+ text
+ type="danger"
+ @click.stop="handleDeleteTemplate(template.code)"
+ >
+ {{ t('common.actions.delete') }}
+ </ElButton>
+ </div>
+ </button>
+ </div>
+ </ElScrollbar>
+ </aside>
+
+ <main class="wh-mat-print-template-manager__main">
+ <div class="wh-mat-print-template-manager__main-header">
+ <div>
+ <div class="wh-mat-print-template-manager__title">
+ {{
+ activeTemplate?.name ||
+ t('pages.basicInfo.whMat.printTemplate.workspace.unnamedTemplate')
+ }}
+ </div>
+ <div class="wh-mat-print-template-manager__subtitle">
+ {{
+ t('pages.basicInfo.whMat.printTemplate.workspace.previewRecord', {
+ name:
+ previewRecord?.code ||
+ previewRecord?.name ||
+ t('pages.basicInfo.whMat.printTemplate.workspace.noPreviewRecord')
+ })
+ }}
+ </div>
+ </div>
+ <ElSpace wrap>
+ <ElButton :loading="loading" @click="handleReload">
+ {{ t('common.actions.refresh') }}
+ </ElButton>
+ <ElButton type="primary" :loading="saving" @click="handleSaveTemplate">
+ {{ t('pages.basicInfo.whMat.printTemplate.workspace.actions.saveTemplate') }}
+ </ElButton>
+ </ElSpace>
+ </div>
+
+ <MatnrPrintToolbar
+ :zoom-percent="zoomPercent"
+ :can-zoom-in="canZoomIn"
+ :can-zoom-out="canZoomOut"
+ :auto-fit-active="autoFitEnabled"
+ @add-element="handleAddElement"
+ @zoom-in="handleZoomIn"
+ @zoom-out="handleZoomOut"
+ @zoom-reset="handleZoomReset"
+ @zoom-fit="handleZoomFit"
+ />
+
+ <div ref="canvasPanelRef" class="wh-mat-print-template-manager__canvas-panel">
+ <ElEmpty
+ v-if="!activeTemplate"
+ :description="t('pages.basicInfo.whMat.printTemplate.workspace.emptyTemplate')"
+ />
+ <MatnrPrintCanvas
+ v-else
+ :template="activeTemplate"
+ :active-record="previewRecord"
+ :selected-element-id="selectedElementId"
+ mode="editor"
+ :scale="canvasScale"
+ :interactive="true"
+ :show-grid="true"
+ @select-element="selectedElementId = $event"
+ @update-element="handleElementPatch"
+ />
+ </div>
+ </main>
+
+ <aside class="wh-mat-print-template-manager__property">
+ <MatnrPrintPropertyPanel
+ v-if="activeTemplate"
+ :template="activeTemplate"
+ :selected-element="selectedElement"
+ @update-template-meta="handleTemplateMetaPatch"
+ @update-canvas="handleCanvasPatch"
+ @update-element="handleElementPatch"
+ @remove-element="handleRemoveElement"
+ @set-placeholder-target="placeholderTarget = $event"
+ />
+ <MatnrPrintFieldPanel
+ v-if="activeTemplate"
+ :fields="fieldOptions"
+ @insert-field="handleInsertPlaceholder"
+ />
+ </aside>
+ </div>
+</template>
+
+<script setup>
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import {
+ appendFieldPlaceholder,
+ buildMatnrPrintFieldOptions,
+ buildMatnrPrintTemplatePayload,
+ createDefaultMatnrPrintTemplate,
+ createElementByType,
+ duplicateMatnrPrintTemplate,
+ getFieldListTableRows,
+ normalizeMatnrPrintTemplate,
+ updateFieldListTableRows
+ } from '../matnrPrintTemplate.helpers'
+ import {
+ fetchGetDefaultMatnrPrintTemplate,
+ fetchGetMatnrPrintTemplateDetail,
+ fetchMatnrPrintTemplateList,
+ fetchRemoveMatnrPrintTemplate,
+ fetchSaveMatnrPrintTemplate,
+ fetchSetDefaultMatnrPrintTemplate,
+ fetchUpdateMatnrPrintTemplate
+ } from '@/api/wh-mat'
+ import MatnrPrintCanvas from './matnr-print-canvas.vue'
+ import MatnrPrintFieldPanel from './matnr-print-field-panel.vue'
+ import MatnrPrintPropertyPanel from './matnr-print-property-panel.vue'
+ import MatnrPrintToolbar from './matnr-print-toolbar.vue'
+
+ defineOptions({ name: 'WhMatPrintTemplateWorkspace' })
+
+ const { t, locale } = useI18n()
+
+ const props = defineProps({
+ enabledFields: {
+ type: Array,
+ default: () => []
+ },
+ previewRecord: {
+ type: Object,
+ default: () => ({})
+ },
+ autoLoad: {
+ type: Boolean,
+ default: true
+ }
+ })
+
+ const DEFAULT_CANVAS_SCALE = 4
+ const MIN_CANVAS_SCALE = 0.5
+ const MAX_CANVAS_SCALE = 24
+ const CANVAS_SCALE_STEPS = [0.5, 0.75, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20, 24]
+ const FIT_PANEL_GAP = 16
+
+ const templates = ref([])
+ const activeTemplateCode = ref('')
+ const selectedElementId = ref('')
+ const placeholderTarget = ref(null)
+ const loading = ref(false)
+ const saving = ref(false)
+ const canvasScale = ref(DEFAULT_CANVAS_SCALE)
+ const autoFitEnabled = ref(true)
+ const canvasPanelRef = ref(null)
+ let resizeObserver = null
+
+ const fieldOptions = computed(() => {
+ locale.value
+ return buildMatnrPrintFieldOptions(props.enabledFields)
+ })
+ const zoomPercent = computed(() => Math.round((canvasScale.value / DEFAULT_CANVAS_SCALE) * 100))
+ const canZoomIn = computed(() => canvasScale.value < MAX_CANVAS_SCALE)
+ const canZoomOut = computed(() => canvasScale.value > MIN_CANVAS_SCALE)
+ const activeTemplate = computed(
+ () => templates.value.find((template) => template.code === activeTemplateCode.value) || null
+ )
+ const selectedElement = computed(
+ () =>
+ activeTemplate.value?.elements?.find((element) => element.id === selectedElementId.value) ||
+ null
+ )
+
+ watch(
+ () => props.autoLoad,
+ async (autoLoad, previousValue) => {
+ if (autoLoad && !previousValue) {
+ await loadTemplates()
+ }
+ },
+ {
+ immediate: true
+ }
+ )
+
+ watch(
+ () =>
+ `${activeTemplate.value?.canvas?.width || 0}_${activeTemplate.value?.canvas?.height || 0}`,
+ async () => {
+ if (!autoFitEnabled.value || !activeTemplate.value) {
+ return
+ }
+ await nextTick()
+ fitCanvasToViewport()
+ }
+ )
+
+ async function loadTemplates() {
+ loading.value = true
+ try {
+ const [list, defaultTemplate] = await Promise.all([
+ fetchMatnrPrintTemplateList({}),
+ fetchGetDefaultMatnrPrintTemplate()
+ ])
+ const nextTemplates = (Array.isArray(list) ? list : [])
+ .map((item) => normalizeMatnrPrintTemplate(item))
+ .sort((left, right) => Number(right.isDefault) - Number(left.isDefault))
+
+ templates.value = nextTemplates
+ if (!templates.value.length) {
+ handleCreateTemplate()
+ return
+ }
+
+ const defaultCode = normalizeMatnrPrintTemplate(defaultTemplate || {}).code
+ const nextActiveCode =
+ templates.value.find((item) => item.code === activeTemplateCode.value)?.code ||
+ templates.value.find((item) => item.code === defaultCode)?.code ||
+ templates.value[0]?.code ||
+ ''
+ selectTemplate(nextActiveCode)
+ } catch (error) {
+ ElMessage.error(
+ error?.message || t('pages.basicInfo.whMat.printTemplate.workspace.messages.loadFailed')
+ )
+ if (!templates.value.length) {
+ handleCreateTemplate()
+ }
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function handleReload() {
+ loadTemplates()
+ }
+
+ function resolveNextCanvasScale(direction) {
+ if (direction > 0) {
+ return (
+ CANVAS_SCALE_STEPS.find((step) => step > canvasScale.value + 0.001) || canvasScale.value
+ )
+ }
+ return (
+ [...CANVAS_SCALE_STEPS].reverse().find((step) => step < canvasScale.value - 0.001) ||
+ canvasScale.value
+ )
+ }
+
+ function handleZoomIn() {
+ autoFitEnabled.value = false
+ canvasScale.value = resolveNextCanvasScale(1)
+ }
+
+ function handleZoomOut() {
+ autoFitEnabled.value = false
+ canvasScale.value = resolveNextCanvasScale(-1)
+ }
+
+ function handleZoomReset() {
+ autoFitEnabled.value = false
+ canvasScale.value = DEFAULT_CANVAS_SCALE
+ }
+
+ function getCanvasPanelAvailableSize() {
+ const panelElement = canvasPanelRef.value
+ if (!panelElement) {
+ return null
+ }
+ const panelStyle = window.getComputedStyle(panelElement)
+ const width =
+ panelElement.clientWidth -
+ Number.parseFloat(panelStyle.paddingLeft || '0') -
+ Number.parseFloat(panelStyle.paddingRight || '0') -
+ FIT_PANEL_GAP
+ const height =
+ panelElement.clientHeight -
+ Number.parseFloat(panelStyle.paddingTop || '0') -
+ Number.parseFloat(panelStyle.paddingBottom || '0') -
+ FIT_PANEL_GAP
+ if (width <= 0 || height <= 0) {
+ return null
+ }
+ return { width, height }
+ }
+
+ function fitCanvasToViewport() {
+ if (!activeTemplate.value) {
+ return
+ }
+ const panelSize = getCanvasPanelAvailableSize()
+ if (!panelSize) {
+ return
+ }
+ const canvas = activeTemplate.value.canvas || {}
+ const widthScale = panelSize.width / Math.max(Number(canvas.width) || 1, 1)
+ const heightScale = panelSize.height / Math.max(Number(canvas.height) || 1, 1)
+ const nextScale = Math.min(widthScale, heightScale, MAX_CANVAS_SCALE)
+ canvasScale.value = Number(Math.max(MIN_CANVAS_SCALE, nextScale).toFixed(2))
+ }
+
+ async function handleZoomFit() {
+ autoFitEnabled.value = true
+ await nextTick()
+ fitCanvasToViewport()
+ }
+
+ function selectTemplate(code) {
+ activeTemplateCode.value = code
+ selectedElementId.value = ''
+ placeholderTarget.value = null
+ if (autoFitEnabled.value) {
+ nextTick(() => {
+ fitCanvasToViewport()
+ })
+ }
+ }
+
+ function replaceActiveTemplate(patch) {
+ templates.value = templates.value.map((template) =>
+ template.code === activeTemplateCode.value
+ ? normalizeMatnrPrintTemplate({ ...template, ...patch })
+ : template
+ )
+ }
+
+ function handleTemplateMetaPatch({ field, value }) {
+ if (!activeTemplate.value) {
+ return
+ }
+ if (field === 'isDefault' && value === 1) {
+ templates.value = templates.value.map((template) => ({
+ ...template,
+ isDefault: template.code === activeTemplateCode.value ? 1 : 0
+ }))
+ return
+ }
+ replaceActiveTemplate({
+ [field]: value
+ })
+ }
+
+ function handleCanvasPatch({ field, value }) {
+ if (!activeTemplate.value) {
+ return
+ }
+ replaceActiveTemplate({
+ canvas: {
+ ...(activeTemplate.value.canvas || {}),
+ [field]: value
+ }
+ })
+ }
+
+ function handleElementPatch({ id, patch }) {
+ if (!activeTemplate.value || !id) {
+ return
+ }
+ const nextElements = activeTemplate.value.elements.map((element) =>
+ element.id === id
+ ? normalizeMatnrPrintTemplate({ elements: [{ ...element, ...patch }] }).elements[0]
+ : element
+ )
+ replaceActiveTemplate({
+ elements: nextElements
+ })
+ }
+
+ function handleAddElement(type) {
+ if (!activeTemplate.value) {
+ return
+ }
+ const newElement = createElementByType(type, activeTemplate.value.elements.length)
+ replaceActiveTemplate({
+ elements: [...activeTemplate.value.elements, newElement]
+ })
+ selectedElementId.value = newElement.id
+ }
+
+ function handleRemoveElement(id) {
+ if (!activeTemplate.value) {
+ return
+ }
+ replaceActiveTemplate({
+ elements: activeTemplate.value.elements.filter((element) => element.id !== id)
+ })
+ if (selectedElementId.value === id) {
+ selectedElementId.value = ''
+ }
+ }
+
+ function handleCreateTemplate() {
+ const newTemplate = createDefaultMatnrPrintTemplate({
+ name: `${t('pages.basicInfo.whMat.printTemplate.helpers.defaultTemplateName')}${templates.value.length + 1}`,
+ code: `MATNR_${Date.now()}_${templates.value.length + 1}`
+ })
+ templates.value = [
+ ...templates.value.map((item) => ({ ...item, isDefault: item.isDefault })),
+ newTemplate
+ ]
+ selectTemplate(newTemplate.code)
+ }
+
+ function handleCopyTemplate(code) {
+ const template = templates.value.find((item) => item.code === code)
+ if (!template) {
+ return
+ }
+ const copyTemplate = duplicateMatnrPrintTemplate(template)
+ templates.value = [...templates.value, copyTemplate]
+ selectTemplate(copyTemplate.code)
+ }
+
+ async function handleDeleteTemplate(code) {
+ const template = templates.value.find((item) => item.code === code)
+ if (!template) {
+ return
+ }
+ try {
+ await ElMessageBox.confirm(
+ t('pages.basicInfo.whMat.printTemplate.workspace.messages.deleteConfirm', {
+ name: template.name
+ }),
+ t('pages.basicInfo.whMat.printTemplate.workspace.actions.delete'),
+ {
+ type: 'warning'
+ }
+ )
+ if (template.id) {
+ await fetchRemoveMatnrPrintTemplate(template.id)
+ }
+ templates.value = templates.value.filter((item) => item.code !== code)
+ if (!templates.value.length) {
+ handleCreateTemplate()
+ } else if (activeTemplateCode.value === code) {
+ selectTemplate(templates.value[0].code)
+ }
+ ElMessage.success(t('pages.basicInfo.whMat.printTemplate.workspace.messages.deleteSuccess'))
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(
+ error?.message || t('pages.basicInfo.whMat.printTemplate.workspace.messages.deleteFailed')
+ )
+ }
+ }
+ }
+
+ async function handleMarkDefault(code) {
+ const template = templates.value.find((item) => item.code === code)
+ if (!template) {
+ return
+ }
+ templates.value = templates.value.map((item) => ({
+ ...item,
+ isDefault: item.code === code ? 1 : 0
+ }))
+ if (template.id) {
+ try {
+ await fetchSetDefaultMatnrPrintTemplate(template.id)
+ ElMessage.success(
+ t('pages.basicInfo.whMat.printTemplate.workspace.messages.defaultSuccess')
+ )
+ } catch (error) {
+ ElMessage.error(
+ error?.message ||
+ t('pages.basicInfo.whMat.printTemplate.workspace.messages.defaultFailed')
+ )
+ }
+ }
+ }
+
+ async function handleSaveTemplate() {
+ if (!activeTemplate.value) {
+ return
+ }
+ saving.value = true
+ try {
+ const payload = buildMatnrPrintTemplatePayload(activeTemplate.value)
+ const saved = activeTemplate.value.id
+ ? await fetchUpdateMatnrPrintTemplate(payload)
+ : await fetchSaveMatnrPrintTemplate(payload)
+ const normalized = normalizeMatnrPrintTemplate(saved)
+ templates.value = templates.value.map((template) =>
+ template.code === activeTemplateCode.value
+ ? normalized
+ : { ...template, isDefault: normalized.isDefault === 1 ? 0 : template.isDefault }
+ )
+ if (!templates.value.find((template) => template.code === normalized.code)) {
+ templates.value = [...templates.value, normalized]
+ }
+ if (normalized.isDefault === 1) {
+ templates.value = templates.value.map((template) => ({
+ ...template,
+ isDefault: template.id === normalized.id ? 1 : 0
+ }))
+ }
+ activeTemplateCode.value = normalized.code
+ if (normalized.id) {
+ const detail = await fetchGetMatnrPrintTemplateDetail(normalized.id)
+ const nextTemplate = normalizeMatnrPrintTemplate(detail)
+ templates.value = templates.value.map((template) =>
+ template.code === normalized.code ? nextTemplate : template
+ )
+ }
+ ElMessage.success(t('pages.basicInfo.whMat.printTemplate.workspace.messages.saveSuccess'))
+ } catch (error) {
+ ElMessage.error(
+ error?.message || t('pages.basicInfo.whMat.printTemplate.workspace.messages.saveFailed')
+ )
+ } finally {
+ saving.value = false
+ }
+ }
+
+ function handleInsertPlaceholder(placeholder) {
+ if (!activeTemplate.value || !placeholderTarget.value || !selectedElement.value) {
+ return
+ }
+ const target = placeholderTarget.value
+ if (target.field === 'contentTemplate' || target.field === 'valueTemplate') {
+ handleElementPatch({
+ id: selectedElement.value.id,
+ patch: {
+ [target.field]: appendFieldPlaceholder(selectedElement.value[target.field], placeholder)
+ }
+ })
+ return
+ }
+ if (target.field === 'table') {
+ const rows = getFieldListTableRows(selectedElement.value).map((row, rowIndex) =>
+ rowIndex === target.rowIndex
+ ? {
+ ...row,
+ [target.rowField]: appendFieldPlaceholder(row[target.rowField], placeholder)
+ }
+ : row
+ )
+ handleElementPatch({
+ id: selectedElement.value.id,
+ patch: updateFieldListTableRows(selectedElement.value, rows)
+ })
+ }
+ }
+
+ defineExpose({
+ loadTemplates
+ })
+
+ onMounted(() => {
+ resizeObserver = new ResizeObserver(() => {
+ if (autoFitEnabled.value) {
+ fitCanvasToViewport()
+ }
+ })
+ if (canvasPanelRef.value) {
+ resizeObserver.observe(canvasPanelRef.value)
+ }
+ })
+
+ onBeforeUnmount(() => {
+ resizeObserver?.disconnect()
+ resizeObserver = null
+ })
+</script>
+
+<style scoped>
+ .wh-mat-print-template-manager__layout {
+ display: grid;
+ grid-template-columns: 280px minmax(0, 1fr) 360px;
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ background:
+ radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 26%),
+ linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
+ }
+
+ .wh-mat-print-template-manager__sidebar,
+ .wh-mat-print-template-manager__property {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.94);
+ border-right: 1px solid rgba(148, 163, 184, 0.18);
+ }
+
+ .wh-mat-print-template-manager__property {
+ border-right: none;
+ border-left: 1px solid rgba(148, 163, 184, 0.18);
+ }
+
+ .wh-mat-print-template-manager__sidebar-header,
+ .wh-mat-print-template-manager__main-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 16px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
+ }
+
+ .wh-mat-print-template-manager__sidebar-scroll {
+ flex: 1;
+ min-height: 0;
+ }
+
+ .wh-mat-print-template-manager__template-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px;
+ }
+
+ .wh-mat-print-template-manager__template-card {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 14px;
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: 16px;
+ background: #ffffff;
+ text-align: left;
+ cursor: pointer;
+ transition:
+ transform 0.2s ease,
+ border-color 0.2s ease,
+ box-shadow 0.2s ease;
+ }
+
+ .wh-mat-print-template-manager__template-card:hover,
+ .wh-mat-print-template-manager__template-card.is-active {
+ border-color: rgba(37, 99, 235, 0.45);
+ transform: translateY(-1px);
+ box-shadow: 0 12px 28px rgba(37, 99, 235, 0.08);
+ }
+
+ .wh-mat-print-template-manager__template-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--art-text-primary);
+ }
+
+ .wh-mat-print-template-manager__template-code,
+ .wh-mat-print-template-manager__subtitle {
+ font-size: 12px;
+ color: var(--art-text-secondary);
+ }
+
+ .wh-mat-print-template-manager__template-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+ }
+
+ .wh-mat-print-template-manager__main {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+ }
+
+ .wh-mat-print-template-manager__canvas-panel {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ padding: 16px;
+ overflow: auto;
+ }
+
+ .wh-mat-print-template-manager__title {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--art-text-primary);
+ }
+
+ @media (max-width: 1440px) {
+ .wh-mat-print-template-manager__layout {
+ grid-template-columns: 240px minmax(0, 1fr) 320px;
+ }
+ }
+
+ @media (max-width: 1180px) {
+ .wh-mat-print-template-manager__layout {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: minmax(0, 0.82fr) minmax(0, 1.45fr) minmax(0, 1fr);
+ }
+
+ .wh-mat-print-template-manager__sidebar,
+ .wh-mat-print-template-manager__property {
+ border-left: none;
+ border-right: none;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
+ }
+ }
+</style>
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java
index 7959f0b..b16c1c5 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java
@@ -41,7 +41,7 @@
@Autowired
private CompanysService companysService;
- @PreAuthorize("hasAuthority('manager:locItem:list')")
+ @PreAuthorize("hasAnyAuthority('manager:locItem:list','manager:locPreview:list')")
@PostMapping("/locItem/page")
public R page(@RequestBody Map<String, Object> map) {
BaseParam baseParam = buildParam(map, BaseParam.class);
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/MatnrPrintTemplateController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/MatnrPrintTemplateController.java
new file mode 100644
index 0000000..1794bd8
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/MatnrPrintTemplateController.java
@@ -0,0 +1,101 @@
+package com.vincent.rsf.server.manager.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.framework.exception.CoolException;
+import com.vincent.rsf.server.common.annotation.OperationLog;
+import com.vincent.rsf.server.manager.entity.MatnrPrintTemplate;
+import com.vincent.rsf.server.manager.service.MatnrPrintTemplateService;
+import com.vincent.rsf.server.manager.utils.buildPageRowsUtils;
+import com.vincent.rsf.server.system.controller.BaseController;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@Api(tags = "鐗╂枡鎵撳嵃妯℃澘鎺ュ彛")
+public class MatnrPrintTemplateController extends BaseController {
+
+ private final MatnrPrintTemplateService matnrPrintTemplateService;
+
+ public MatnrPrintTemplateController(MatnrPrintTemplateService matnrPrintTemplateService) {
+ this.matnrPrintTemplateService = matnrPrintTemplateService;
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:list')")
+ @ApiOperation("鏌ヨ鐗╂枡鎵撳嵃妯℃澘鍒楄〃")
+ @PostMapping("/matnrPrintTemplate/list")
+ public R list(@RequestBody(required = false) Map<String, Object> params) {
+ List<MatnrPrintTemplate> templates = matnrPrintTemplateService.listCurrentTenantTemplates();
+ return R.ok().add(buildPageRowsUtils.rowsMap(templates));
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:list')")
+ @ApiOperation("鑾峰彇鐗╂枡鎵撳嵃妯℃澘璇︽儏")
+ @GetMapping("/matnrPrintTemplate/{id}")
+ public R get(@PathVariable("id") Long id) {
+ return R.ok(buildPageRowsUtils.rowsMap(matnrPrintTemplateService.getCurrentTenantTemplate(id)));
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:list')")
+ @ApiOperation("鑾峰彇榛樿鐗╂枡鎵撳嵃妯℃澘")
+ @GetMapping("/matnrPrintTemplate/default")
+ public R getDefaultTemplate() {
+ MatnrPrintTemplate template = matnrPrintTemplateService.getCurrentTenantDefaultTemplate();
+ if (template == null) {
+ return R.ok();
+ }
+ return R.ok(buildPageRowsUtils.rowsMap(template));
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:save')")
+ @OperationLog("Create 鐗╂枡鎵撳嵃妯℃澘")
+ @ApiOperation("淇濆瓨鐗╂枡鎵撳嵃妯℃澘")
+ @PostMapping("/matnrPrintTemplate/save")
+ public R save(@RequestBody Map<String, Object> params) {
+ MatnrPrintTemplate template = JSONObject.parseObject(JSONObject.toJSONString(params), MatnrPrintTemplate.class);
+ if (template == null) {
+ throw new CoolException("妯℃澘鍙傛暟涓嶈兘涓虹┖");
+ }
+ MatnrPrintTemplate saved = matnrPrintTemplateService.saveTemplate(template);
+ return R.ok("Create Success").add(buildPageRowsUtils.rowsMap(saved));
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:update')")
+ @OperationLog("Update 鐗╂枡鎵撳嵃妯℃澘")
+ @ApiOperation("鏇存柊鐗╂枡鎵撳嵃妯℃澘")
+ @PostMapping("/matnrPrintTemplate/update")
+ @Transactional(rollbackFor = Exception.class)
+ public R update(@RequestBody Map<String, Object> params) {
+ MatnrPrintTemplate template = JSONObject.parseObject(JSONObject.toJSONString(params), MatnrPrintTemplate.class);
+ if (template == null || template.getId() == null) {
+ throw new CoolException("妯℃澘ID涓嶈兘涓虹┖");
+ }
+ MatnrPrintTemplate updated = matnrPrintTemplateService.updateTemplate(template);
+ return R.ok("Update Success").add(buildPageRowsUtils.rowsMap(updated));
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:remove')")
+ @OperationLog("Delete 鐗╂枡鎵撳嵃妯℃澘")
+ @ApiOperation("鍒犻櫎鐗╂枡鎵撳嵃妯℃澘")
+ @PostMapping("/matnrPrintTemplate/remove/{ids}")
+ public R remove(@PathVariable Long[] ids) {
+ matnrPrintTemplateService.removeTemplates(Arrays.asList(ids));
+ return R.ok("Delete Success").add(buildPageRowsUtils.rowsMap(ids));
+ }
+
+ @PreAuthorize("hasAuthority('manager:matnrPrintTemplate:update')")
+ @OperationLog("Set Default 鐗╂枡鎵撳嵃妯℃澘")
+ @ApiOperation("璁剧疆榛樿鐗╂枡鎵撳嵃妯℃澘")
+ @PostMapping("/matnrPrintTemplate/default/{id}")
+ public R setDefault(@PathVariable("id") Long id) {
+ matnrPrintTemplateService.setDefaultTemplate(id);
+ return R.ok("Update Success");
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MatnrPrintTemplate.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MatnrPrintTemplate.java
new file mode 100644
index 0000000..7046a76
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MatnrPrintTemplate.java
@@ -0,0 +1,82 @@
+package com.vincent.rsf.server.manager.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+
+@Data
+@Accessors(chain = true)
+@TableName(value = "man_matnr_print_template", autoResultMap = true)
+@ApiModel(value = "MatnrPrintTemplate", description = "鐗╂枡鎵撳嵃妯℃澘")
+public class MatnrPrintTemplate implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty(value = "ID")
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @ApiModelProperty(value = "绉熸埛")
+ private Long tenantId;
+
+ @ApiModelProperty(value = "妯℃澘鍚嶇О")
+ private String name;
+
+ @ApiModelProperty(value = "妯℃澘缂栫爜")
+ private String code;
+
+ @ApiModelProperty(value = "鏄惁榛樿妯℃澘")
+ private Integer isDefault;
+
+ @ApiModelProperty(value = "鐘舵�� 1: 姝e父 0: 鍐荤粨")
+ private Integer status;
+
+ @ApiModelProperty(value = "妯℃澘鐢诲竷 JSON")
+ @TableField(value = "canvas_json", typeHandler = JacksonTypeHandler.class)
+ private Map<String, Object> canvasJson;
+
+ @ApiModelProperty(value = "鏄惁鍒犻櫎 1: 鏄� 0: 鍚�")
+ @TableLogic
+ private Integer deleted;
+
+ @ApiModelProperty(value = "娣诲姞浜哄憳")
+ private Long createBy;
+
+ @ApiModelProperty(value = "娣诲姞鏃堕棿")
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date createTime;
+
+ @ApiModelProperty(value = "淇敼浜哄憳")
+ private Long updateBy;
+
+ @ApiModelProperty(value = "淇敼鏃堕棿")
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date updateTime;
+
+ @ApiModelProperty(value = "澶囨敞")
+ private String memo;
+
+ @TableField(exist = false)
+ private String tenantId$;
+
+ @TableField(exist = false)
+ private String createBy$;
+
+ @TableField(exist = false)
+ private String updateBy$;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/MatnrPrintTemplateMapper.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/MatnrPrintTemplateMapper.java
new file mode 100644
index 0000000..2814bcd
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/MatnrPrintTemplateMapper.java
@@ -0,0 +1,11 @@
+package com.vincent.rsf.server.manager.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.vincent.rsf.server.manager.entity.MatnrPrintTemplate;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface MatnrPrintTemplateMapper extends BaseMapper<MatnrPrintTemplate> {
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/MatnrPrintTemplateService.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/MatnrPrintTemplateService.java
new file mode 100644
index 0000000..10a2687
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/MatnrPrintTemplateService.java
@@ -0,0 +1,23 @@
+package com.vincent.rsf.server.manager.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.server.manager.entity.MatnrPrintTemplate;
+
+import java.util.List;
+
+public interface MatnrPrintTemplateService extends IService<MatnrPrintTemplate> {
+
+ List<MatnrPrintTemplate> listCurrentTenantTemplates();
+
+ MatnrPrintTemplate getCurrentTenantTemplate(Long id);
+
+ MatnrPrintTemplate getCurrentTenantDefaultTemplate();
+
+ MatnrPrintTemplate saveTemplate(MatnrPrintTemplate template);
+
+ MatnrPrintTemplate updateTemplate(MatnrPrintTemplate template);
+
+ boolean removeTemplates(List<Long> ids);
+
+ boolean setDefaultTemplate(Long id);
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MatnrPrintTemplateServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MatnrPrintTemplateServiceImpl.java
new file mode 100644
index 0000000..d69d567
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MatnrPrintTemplateServiceImpl.java
@@ -0,0 +1,376 @@
+package com.vincent.rsf.server.manager.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.vincent.rsf.framework.exception.CoolException;
+import com.vincent.rsf.server.manager.entity.MatnrPrintTemplate;
+import com.vincent.rsf.server.manager.mapper.MatnrPrintTemplateMapper;
+import com.vincent.rsf.server.manager.service.MatnrPrintTemplateService;
+import com.vincent.rsf.server.system.entity.User;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@Service("matnrPrintTemplateService")
+public class MatnrPrintTemplateServiceImpl
+ extends ServiceImpl<MatnrPrintTemplateMapper, MatnrPrintTemplate>
+ implements MatnrPrintTemplateService {
+
+ private static final Set<String> SUPPORTED_ELEMENT_TYPES = Collections.unmodifiableSet(
+ new LinkedHashSet<>(Arrays.asList("text", "barcode", "qrcode", "line", "rect", "table"))
+ );
+
+ @Override
+ public List<MatnrPrintTemplate> listCurrentTenantTemplates() {
+ List<MatnrPrintTemplate> templates = this.list(new LambdaQueryWrapper<MatnrPrintTemplate>()
+ .orderByDesc(MatnrPrintTemplate::getIsDefault)
+ .orderByDesc(MatnrPrintTemplate::getUpdateTime)
+ .orderByDesc(MatnrPrintTemplate::getCreateTime)
+ );
+ return templates == null ? new ArrayList<>() : templates;
+ }
+
+ @Override
+ public MatnrPrintTemplate getCurrentTenantTemplate(Long id) {
+ if (id == null) {
+ throw new CoolException("妯℃澘ID涓嶈兘涓虹┖");
+ }
+ MatnrPrintTemplate template = this.getById(id);
+ if (template == null) {
+ throw new CoolException("妯℃澘涓嶅瓨鍦ㄦ垨宸茶鍒犻櫎");
+ }
+ return template;
+ }
+
+ @Override
+ public MatnrPrintTemplate getCurrentTenantDefaultTemplate() {
+ MatnrPrintTemplate template = this.getOne(new LambdaQueryWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getStatus, 1)
+ .eq(MatnrPrintTemplate::getIsDefault, 1)
+ .orderByDesc(MatnrPrintTemplate::getUpdateTime)
+ .last("limit 1")
+ );
+ if (template != null) {
+ return template;
+ }
+ return this.getOne(new LambdaQueryWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getStatus, 1)
+ .orderByDesc(MatnrPrintTemplate::getUpdateTime)
+ .orderByDesc(MatnrPrintTemplate::getCreateTime)
+ .last("limit 1")
+ );
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public MatnrPrintTemplate saveTemplate(MatnrPrintTemplate template) {
+ MatnrPrintTemplate normalized = prepareTemplateForSave(template, false);
+ long currentCount = this.count();
+ boolean shouldDefault = Objects.equals(normalized.getIsDefault(), 1) || currentCount == 0;
+ normalized.setIsDefault(shouldDefault ? 1 : 0);
+ if (shouldDefault) {
+ clearCurrentTenantDefaults();
+ }
+ if (!this.save(normalized)) {
+ throw new CoolException("妯℃澘淇濆瓨澶辫触");
+ }
+ return this.getCurrentTenantTemplate(normalized.getId());
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public MatnrPrintTemplate updateTemplate(MatnrPrintTemplate template) {
+ if (template == null || template.getId() == null) {
+ throw new CoolException("妯℃澘ID涓嶈兘涓虹┖");
+ }
+ MatnrPrintTemplate existing = getCurrentTenantTemplate(template.getId());
+ MatnrPrintTemplate normalized = prepareTemplateForSave(template, true);
+ normalized.setTenantId(existing.getTenantId());
+ normalized.setCreateBy(existing.getCreateBy());
+ normalized.setCreateTime(existing.getCreateTime());
+ normalized.setDeleted(existing.getDeleted());
+ boolean shouldDefault = Objects.equals(normalized.getIsDefault(), 1);
+ if (shouldDefault) {
+ clearCurrentTenantDefaults();
+ } else if (Objects.equals(existing.getIsDefault(), 1)) {
+ normalized.setIsDefault(1);
+ }
+ if (!this.updateById(normalized)) {
+ throw new CoolException("妯℃澘鏇存柊澶辫触");
+ }
+ ensureOneDefaultTemplate();
+ return this.getCurrentTenantTemplate(normalized.getId());
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean removeTemplates(List<Long> ids) {
+ if (ids == null || ids.isEmpty()) {
+ throw new CoolException("璇烽�夋嫨瑕佸垹闄ょ殑妯℃澘");
+ }
+ List<MatnrPrintTemplate> templates = this.listByIds(ids);
+ if (templates == null || templates.isEmpty()) {
+ return true;
+ }
+ boolean removedDefault = templates.stream().anyMatch(item -> Objects.equals(item.getIsDefault(), 1));
+ if (!this.removeByIds(ids)) {
+ throw new CoolException("妯℃澘鍒犻櫎澶辫触");
+ }
+ if (removedDefault) {
+ ensureOneDefaultTemplate();
+ }
+ return true;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean setDefaultTemplate(Long id) {
+ MatnrPrintTemplate template = getCurrentTenantTemplate(id);
+ clearCurrentTenantDefaults();
+ boolean updated = this.update(new LambdaUpdateWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getId, template.getId())
+ .set(MatnrPrintTemplate::getIsDefault, 1)
+ .set(MatnrPrintTemplate::getUpdateBy, resolveCurrentUserId())
+ .set(MatnrPrintTemplate::getUpdateTime, new Date())
+ );
+ if (!updated) {
+ throw new CoolException("榛樿妯℃澘璁剧疆澶辫触");
+ }
+ return true;
+ }
+
+ private MatnrPrintTemplate prepareTemplateForSave(MatnrPrintTemplate template, boolean updating) {
+ if (template == null) {
+ throw new CoolException("妯℃澘鍙傛暟涓嶈兘涓虹┖");
+ }
+ Long currentTenantId = resolveCurrentTenantId();
+ Long currentUserId = resolveCurrentUserId();
+ if (currentTenantId == null) {
+ throw new CoolException("褰撳墠绉熸埛淇℃伅缂哄け");
+ }
+ String name = normalizeText(template.getName());
+ String code = normalizeText(template.getCode());
+ if (name.isEmpty()) {
+ throw new CoolException("妯℃澘鍚嶇О涓嶈兘涓虹┖");
+ }
+ if (code.isEmpty()) {
+ throw new CoolException("妯℃澘缂栫爜涓嶈兘涓虹┖");
+ }
+ Map<String, Object> canvasJson = template.getCanvasJson();
+ if (canvasJson == null || canvasJson.isEmpty()) {
+ throw new CoolException("妯℃澘鐢诲竷涓嶈兘涓虹┖");
+ }
+ validateCanvasJson(canvasJson);
+ ensureTemplateCodeUnique(code, updating ? template.getId() : null);
+
+ Date now = new Date();
+ template.setTenantId(currentTenantId)
+ .setName(name)
+ .setCode(code)
+ .setStatus(template.getStatus() == null ? 1 : template.getStatus())
+ .setIsDefault(Objects.equals(template.getIsDefault(), 1) ? 1 : 0)
+ .setMemo(normalizeText(template.getMemo()))
+ .setUpdateBy(currentUserId)
+ .setUpdateTime(now);
+ if (!updating) {
+ template.setCreateBy(currentUserId);
+ template.setCreateTime(now);
+ }
+ return template;
+ }
+
+ private void ensureTemplateCodeUnique(String code, Long excludeId) {
+ long duplicateCount = this.count(new LambdaQueryWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getCode, code)
+ .ne(excludeId != null, MatnrPrintTemplate::getId, excludeId)
+ );
+ if (duplicateCount > 0) {
+ throw new CoolException("妯℃澘缂栫爜宸插瓨鍦紝璇锋洿鎹㈠悗閲嶈瘯");
+ }
+ }
+
+ private void clearCurrentTenantDefaults() {
+ this.update(new LambdaUpdateWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getIsDefault, 1)
+ .set(MatnrPrintTemplate::getIsDefault, 0)
+ .set(MatnrPrintTemplate::getUpdateBy, resolveCurrentUserId())
+ .set(MatnrPrintTemplate::getUpdateTime, new Date())
+ );
+ }
+
+ private void ensureOneDefaultTemplate() {
+ long defaultCount = this.count(new LambdaQueryWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getIsDefault, 1)
+ );
+ if (defaultCount > 0) {
+ return;
+ }
+ MatnrPrintTemplate newest = this.getOne(new LambdaQueryWrapper<MatnrPrintTemplate>()
+ .orderByDesc(MatnrPrintTemplate::getUpdateTime)
+ .orderByDesc(MatnrPrintTemplate::getCreateTime)
+ .last("limit 1")
+ );
+ if (newest == null) {
+ return;
+ }
+ this.update(new LambdaUpdateWrapper<MatnrPrintTemplate>()
+ .eq(MatnrPrintTemplate::getId, newest.getId())
+ .set(MatnrPrintTemplate::getIsDefault, 1)
+ .set(MatnrPrintTemplate::getUpdateBy, resolveCurrentUserId())
+ .set(MatnrPrintTemplate::getUpdateTime, new Date())
+ );
+ }
+
+ private void validateCanvasJson(Map<String, Object> canvasJson) {
+ JSONObject root = JSONObject.parseObject(JSON.toJSONString(canvasJson));
+ if (root == null) {
+ throw new CoolException("妯℃澘鐢诲竷鏍煎紡涓嶆纭�");
+ }
+ if (root.getInteger("version") == null) {
+ throw new CoolException("妯℃澘鐗堟湰涓嶈兘涓虹┖");
+ }
+ JSONObject canvas = root.getJSONObject("canvas");
+ if (canvas == null) {
+ throw new CoolException("妯℃澘鐢诲竷閰嶇疆涓嶈兘涓虹┖");
+ }
+ double width = getPositiveNumber(canvas, "width", "鐢诲竷瀹藉害");
+ double height = getPositiveNumber(canvas, "height", "鐢诲竷楂樺害");
+ if (width <= 0 || height <= 0) {
+ throw new CoolException("鐢诲竷灏哄蹇呴』澶т簬0");
+ }
+ String unit = normalizeText(canvas.getString("unit"));
+ if (!"mm".equals(unit)) {
+ throw new CoolException("鐢诲竷鍗曚綅浠呮敮鎸� mm");
+ }
+ JSONArray elements = root.getJSONArray("elements");
+ if (elements == null) {
+ throw new CoolException("妯℃澘鍏冪礌涓嶈兘涓虹┖");
+ }
+ for (int index = 0; index < elements.size(); index++) {
+ JSONObject element = elements.getJSONObject(index);
+ if (element == null) {
+ throw new CoolException("妯℃澘鍏冪礌鏍煎紡涓嶆纭�");
+ }
+ validateElement(element, index);
+ }
+ }
+
+ private void validateElement(JSONObject element, int index) {
+ String type = normalizeText(element.getString("type"));
+ if (!SUPPORTED_ELEMENT_TYPES.contains(type)) {
+ throw new CoolException("绗�" + (index + 1) + "涓厓绱犵被鍨嬩笉鏀寔");
+ }
+ if (normalizeText(element.getString("id")).isEmpty()) {
+ throw new CoolException("绗�" + (index + 1) + "涓厓绱犵己灏� ID");
+ }
+ ensureNumber(element, "x", "鍏冪礌 X 鍧愭爣");
+ ensureNumber(element, "y", "鍏冪礌 Y 鍧愭爣");
+ if (!"line".equals(type)) {
+ getPositiveNumber(element, "w", "鍏冪礌瀹藉害");
+ getPositiveNumber(element, "h", "鍏冪礌楂樺害");
+ } else {
+ String direction = normalizeText(element.getString("direction"));
+ if (!Arrays.asList("horizontal", "vertical").contains(direction)) {
+ throw new CoolException("绾挎潯鍏冪礌鏂瑰悜浠呮敮鎸� horizontal 鎴� vertical");
+ }
+ getPositiveNumber(element, "w", "绾挎潯闀垮害");
+ getPositiveNumber(element, "h", "绾挎潯绮楃粏");
+ }
+
+ switch (type) {
+ case "text":
+ String contentMode = normalizeText(element.getString("contentMode"));
+ if (!Arrays.asList("static", "template").contains(contentMode)) {
+ throw new CoolException("鏂囨湰鍏冪礌鍐呭妯″紡涓嶆敮鎸�");
+ }
+ if (normalizeText(element.getString("contentTemplate")).isEmpty()) {
+ throw new CoolException("鏂囨湰鍏冪礌鍐呭涓嶈兘涓虹┖");
+ }
+ break;
+ case "barcode":
+ if (normalizeText(element.getString("valueTemplate")).isEmpty()) {
+ throw new CoolException("鏉$爜鍏冪礌鍊兼ā鏉夸笉鑳戒负绌�");
+ }
+ String symbology = normalizeText(element.getString("symbology"));
+ if (!symbology.isEmpty() && !"CODE128".equals(symbology)) {
+ throw new CoolException("涓�缁寸爜浠呮敮鎸� CODE128");
+ }
+ break;
+ case "qrcode":
+ if (normalizeText(element.getString("valueTemplate")).isEmpty()) {
+ throw new CoolException("浜岀淮鐮佸厓绱犲�兼ā鏉夸笉鑳戒负绌�");
+ }
+ break;
+ case "table":
+ if (element.getJSONArray("columns") == null) {
+ throw new CoolException("琛ㄦ牸鍏冪礌 columns 涓嶈兘涓虹┖");
+ }
+ if (element.getJSONArray("rows") == null) {
+ throw new CoolException("琛ㄦ牸鍏冪礌 rows 涓嶈兘涓虹┖");
+ }
+ if (element.getJSONArray("cells") == null) {
+ throw new CoolException("琛ㄦ牸鍏冪礌 cells 涓嶈兘涓虹┖");
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void ensureNumber(JSONObject object, String key, String label) {
+ if (object.getBigDecimal(key) == null) {
+ throw new CoolException(label + "涓嶈兘涓虹┖");
+ }
+ }
+
+ private double getPositiveNumber(JSONObject object, String key, String label) {
+ if (object.getBigDecimal(key) == null) {
+ throw new CoolException(label + "涓嶈兘涓虹┖");
+ }
+ double value = object.getBigDecimal(key).doubleValue();
+ if (value <= 0) {
+ throw new CoolException(label + "蹇呴』澶т簬0");
+ }
+ return value;
+ }
+
+ private String normalizeText(String value) {
+ return value == null ? "" : value.trim();
+ }
+
+ private Long resolveCurrentTenantId() {
+ User loginUser = getCurrentUser();
+ return loginUser == null ? null : loginUser.getTenantId();
+ }
+
+ private Long resolveCurrentUserId() {
+ User loginUser = getCurrentUser();
+ return loginUser == null ? null : loginUser.getId();
+ }
+
+ private User getCurrentUser() {
+ try {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null && authentication.getPrincipal() instanceof User) {
+ return (User) authentication.getPrincipal();
+ }
+ } catch (Exception ignored) {
+ }
+ return null;
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java
index adb7030..9a47288 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java
@@ -294,7 +294,7 @@
if (menu.getParentId() == null || menu.getParentId() == 0L) {
return "/index/index";
}
- return "/"+menu.getComponent();
+ return menu.getRoute();
}
private String resolveRoutePath(Menu menu) {
diff --git a/rsf-server/src/main/resources/sql/20260413_matnr_print_template.sql b/rsf-server/src/main/resources/sql/20260413_matnr_print_template.sql
new file mode 100644
index 0000000..7d08e14
--- /dev/null
+++ b/rsf-server/src/main/resources/sql/20260413_matnr_print_template.sql
@@ -0,0 +1,19 @@
+CREATE TABLE IF NOT EXISTS `man_matnr_print_template` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `tenant_id` bigint NOT NULL COMMENT '绉熸埛ID',
+ `name` varchar(100) NOT NULL COMMENT '妯℃澘鍚嶇О',
+ `code` varchar(100) NOT NULL COMMENT '妯℃澘缂栫爜',
+ `is_default` tinyint NOT NULL DEFAULT 0 COMMENT '鏄惁榛樿妯℃澘 1鏄� 0鍚�',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '鐘舵�� 1姝e父 0鍐荤粨',
+ `canvas_json` longtext NOT NULL COMMENT '鐢诲竷JSON',
+ `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '鏄惁鍒犻櫎 1鏄� 0鍚�',
+ `create_by` bigint DEFAULT NULL COMMENT '鍒涘缓浜�',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_by` bigint DEFAULT NULL COMMENT '鏇存柊浜�',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `memo` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_matnr_print_template_tenant_code_deleted` (`tenant_id`, `code`, `deleted`),
+ KEY `idx_matnr_print_template_tenant_default` (`tenant_id`, `is_default`),
+ KEY `idx_matnr_print_template_tenant_status` (`tenant_id`, `status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鐗╂枡鎵撳嵃妯℃澘';
diff --git a/rsf-server/src/main/resources/sql/20260413_matnr_print_template_menu.sql b/rsf-server/src/main/resources/sql/20260413_matnr_print_template_menu.sql
new file mode 100644
index 0000000..59a636d
--- /dev/null
+++ b/rsf-server/src/main/resources/sql/20260413_matnr_print_template_menu.sql
@@ -0,0 +1,380 @@
+-- 鐗╂枡鎵撳嵃妯℃澘宸︿晶鐙珛鑿滃崟 + 鏉冮檺鎸夐挳
+-- 璇存槑锛�
+-- 1. 鍦ㄢ�滃熀纭�淇℃伅鈥濅笅鏂板鈥滅墿鏂欐墦鍗版ā鏉库�濈嫭绔嬭彍鍗�
+-- 2. 鑿滃崟璺敱涓� /basic-info/matnr-print-template锛岀粍浠堕敭涓� matnrPrintTemplate
+-- 3. 鑻ユ鍓嶅凡鎵ц鏃х増鈥滄寕鍦ㄧ墿鏂欓〉涓嬬殑鎸夐挳鏉冮檺鈥濊剼鏈紝閲嶅鎵ц鏈剼鏈細鑷姩杩佺Щ鎸夐挳鍒版柊鑿滃崟涓�
+
+SET @tenant_id := 1;
+
+SET @basic_info_menu_id := COALESCE(
+ (
+ SELECT parent_id
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND type = 0
+ AND (
+ route = '/basic-info/wh-mat'
+ OR component IN ('matnr', 'whMat')
+ OR name = 'menu.matnr'
+ )
+ ORDER BY id
+ LIMIT 1
+ ),
+ (
+ SELECT id
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND type = 0
+ AND (
+ route = '/basic-info'
+ OR component = 'basicInfo'
+ OR name = 'menu.basicInfo'
+ )
+ ORDER BY id
+ LIMIT 1
+ )
+);
+
+INSERT INTO sys_menu (
+ name,
+ parent_id,
+ parent_name,
+ path,
+ path_name,
+ route,
+ component,
+ brief,
+ code,
+ type,
+ authority,
+ icon,
+ sort,
+ meta,
+ tenant_id,
+ status,
+ deleted,
+ create_time,
+ create_by,
+ update_time,
+ update_by,
+ memo
+)
+SELECT
+ 'menu.matnrPrintTemplate',
+ @basic_info_menu_id,
+ 'menu.basicInfo',
+ 'matnr-print-template',
+ 'matnrPrintTemplate',
+ '/basic-info/matnr-print-template',
+ 'matnrPrintTemplate',
+ '鐗╂枡鎵撳嵃妯℃澘鐙珛鑿滃崟',
+ NULL,
+ 0,
+ NULL,
+ 'ri:price-tag-3-line',
+ 96,
+ NULL,
+ @tenant_id,
+ 1,
+ 0,
+ NOW(),
+ 1,
+ NOW(),
+ 1,
+ '鐗╂枡鎵撳嵃妯℃澘宸︿晶鑿滃崟'
+FROM dual
+WHERE @basic_info_menu_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND type = 0
+ AND (
+ route = '/basic-info/matnr-print-template'
+ OR component = 'matnrPrintTemplate'
+ OR name = 'menu.matnrPrintTemplate'
+ )
+ );
+
+SET @template_menu_id := (
+ SELECT id
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND type = 0
+ AND (
+ route = '/basic-info/matnr-print-template'
+ OR component = 'matnrPrintTemplate'
+ OR name = 'menu.matnrPrintTemplate'
+ )
+ ORDER BY id
+ LIMIT 1
+);
+
+UPDATE sys_menu
+SET
+ name = 'menu.matnrPrintTemplate',
+ parent_id = @basic_info_menu_id,
+ parent_name = 'menu.basicInfo',
+ path = 'matnr-print-template',
+ path_name = 'matnrPrintTemplate',
+ route = '/basic-info/matnr-print-template',
+ component = 'matnrPrintTemplate',
+ brief = '鐗╂枡鎵撳嵃妯℃澘鐙珛鑿滃崟',
+ type = 0,
+ authority = NULL,
+ icon = 'ri:price-tag-3-line',
+ sort = 96,
+ status = 1,
+ update_time = NOW(),
+ update_by = 1,
+ memo = '鐗╂枡鎵撳嵃妯℃澘宸︿晶鑿滃崟'
+WHERE @template_menu_id IS NOT NULL
+ AND id = @template_menu_id;
+
+UPDATE sys_menu
+SET
+ parent_id = @template_menu_id,
+ parent_name = 'menu.matnrPrintTemplate',
+ update_time = NOW(),
+ update_by = 1
+WHERE @template_menu_id IS NOT NULL
+ AND deleted = 0
+ AND tenant_id = @tenant_id
+ AND authority IN (
+ 'manager:matnrPrintTemplate:list',
+ 'manager:matnrPrintTemplate:save',
+ 'manager:matnrPrintTemplate:update',
+ 'manager:matnrPrintTemplate:remove'
+ );
+
+INSERT INTO sys_menu (
+ name,
+ parent_id,
+ parent_name,
+ path,
+ path_name,
+ route,
+ component,
+ brief,
+ code,
+ type,
+ authority,
+ icon,
+ sort,
+ meta,
+ tenant_id,
+ status,
+ deleted,
+ create_time,
+ create_by,
+ update_time,
+ update_by,
+ memo
+)
+SELECT
+ 'Query 鐗╂枡鎵撳嵃妯℃澘',
+ @template_menu_id,
+ 'menu.matnrPrintTemplate',
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ '鐗╂枡鎵撳嵃妯℃澘璇诲彇鏉冮檺',
+ NULL,
+ 1,
+ 'manager:matnrPrintTemplate:list',
+ NULL,
+ 10,
+ NULL,
+ @tenant_id,
+ 1,
+ 0,
+ NOW(),
+ 1,
+ NOW(),
+ 1,
+ '鐗╂枡鎵撳嵃妯℃澘鍒楄〃銆佽鎯呫�侀粯璁ゆā鏉裤�佹墦鍗拌鍙�'
+FROM dual
+WHERE @template_menu_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND authority = 'manager:matnrPrintTemplate:list'
+ );
+
+INSERT INTO sys_menu (
+ name,
+ parent_id,
+ parent_name,
+ path,
+ path_name,
+ route,
+ component,
+ brief,
+ code,
+ type,
+ authority,
+ icon,
+ sort,
+ meta,
+ tenant_id,
+ status,
+ deleted,
+ create_time,
+ create_by,
+ update_time,
+ update_by,
+ memo
+)
+SELECT
+ 'Create 鐗╂枡鎵撳嵃妯℃澘',
+ @template_menu_id,
+ 'menu.matnrPrintTemplate',
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ '鐗╂枡鎵撳嵃妯℃澘鏂板鏉冮檺',
+ NULL,
+ 1,
+ 'manager:matnrPrintTemplate:save',
+ NULL,
+ 11,
+ NULL,
+ @tenant_id,
+ 1,
+ 0,
+ NOW(),
+ 1,
+ NOW(),
+ 1,
+ '鐗╂枡鎵撳嵃妯℃澘鏂板'
+FROM dual
+WHERE @template_menu_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND authority = 'manager:matnrPrintTemplate:save'
+ );
+
+INSERT INTO sys_menu (
+ name,
+ parent_id,
+ parent_name,
+ path,
+ path_name,
+ route,
+ component,
+ brief,
+ code,
+ type,
+ authority,
+ icon,
+ sort,
+ meta,
+ tenant_id,
+ status,
+ deleted,
+ create_time,
+ create_by,
+ update_time,
+ update_by,
+ memo
+)
+SELECT
+ 'Update 鐗╂枡鎵撳嵃妯℃澘',
+ @template_menu_id,
+ 'menu.matnrPrintTemplate',
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ '鐗╂枡鎵撳嵃妯℃澘淇敼鏉冮檺',
+ NULL,
+ 1,
+ 'manager:matnrPrintTemplate:update',
+ NULL,
+ 12,
+ NULL,
+ @tenant_id,
+ 1,
+ 0,
+ NOW(),
+ 1,
+ NOW(),
+ 1,
+ '鐗╂枡鎵撳嵃妯℃澘缂栬緫銆佽榛樿'
+FROM dual
+WHERE @template_menu_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND authority = 'manager:matnrPrintTemplate:update'
+ );
+
+INSERT INTO sys_menu (
+ name,
+ parent_id,
+ parent_name,
+ path,
+ path_name,
+ route,
+ component,
+ brief,
+ code,
+ type,
+ authority,
+ icon,
+ sort,
+ meta,
+ tenant_id,
+ status,
+ deleted,
+ create_time,
+ create_by,
+ update_time,
+ update_by,
+ memo
+)
+SELECT
+ 'Delete 鐗╂枡鎵撳嵃妯℃澘',
+ @template_menu_id,
+ 'menu.matnrPrintTemplate',
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ '鐗╂枡鎵撳嵃妯℃澘鍒犻櫎鏉冮檺',
+ NULL,
+ 1,
+ 'manager:matnrPrintTemplate:remove',
+ NULL,
+ 13,
+ NULL,
+ @tenant_id,
+ 1,
+ 0,
+ NOW(),
+ 1,
+ NOW(),
+ 1,
+ '鐗╂枡鎵撳嵃妯℃澘鍒犻櫎'
+FROM dual
+WHERE @template_menu_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_menu
+ WHERE deleted = 0
+ AND tenant_id = @tenant_id
+ AND authority = 'manager:matnrPrintTemplate:remove'
+ );
--
Gitblit v1.9.1