54 Commits

Author SHA1 Message Date
zc
f571704614 工单打印记录 2026-06-17 15:42:46 +08:00
zc
c3da8055ec 工单打印根据打印编码查询打印物料名称 2026-06-15 16:15:43 +08:00
zc
852b446901 打印编码需求变更 2026-06-15 15:34:21 +08:00
zc
225a7a1932 刷卡登录 2026-06-15 14:06:14 +08:00
zc
466ac71f80 打印样式修改 2026-06-10 18:25:14 +08:00
zc
e7ac5f3bbf 打印样式修改 2026-06-10 17:34:09 +08:00
zc
a173cf7044 打印编码 2026-06-10 10:36:14 +08:00
zc
809c07264f 优化整箱 2026-04-28 17:40:09 +08:00
zc
a01a36e1b7 优化整箱 2026-04-27 17:14:24 +08:00
zc
79f9124c9a 优化整箱 2026-04-27 16:14:26 +08:00
zc
848cabd0fa 优化 2026-04-21 14:17:28 +08:00
zc
86a75d5c28 优化标记号 2026-04-17 17:04:06 +08:00
zc
660a95c9ed 标签字体加粗 2026-04-17 11:02:37 +08:00
zc
ebe08f0c4f 手动输入物料 2026-04-17 10:35:41 +08:00
zc
e5d9cea7b6 权限标识 2026-04-16 11:40:34 +08:00
zc
4d52bceea5 优化 2026-04-16 00:47:58 +08:00
zc
3c03907e69 优化 2026-04-13 11:02:00 +08:00
zc
fce1099b38 优化 2026-04-12 23:18:33 +08:00
zc
9927382054 优化宇视图片地址 2026-04-12 19:57:14 +08:00
zc
69c1430836 优化 2026-04-12 18:26:48 +08:00
zc
d1aca2113d 优化 2026-04-11 23:42:28 +08:00
zc
a062c996c7 优化 2026-04-11 22:57:34 +08:00
zc
d34a501df9 优化 2026-04-09 16:46:36 +08:00
zc
947a8a27d3 优化 2026-04-09 16:02:27 +08:00
zc
cf269a2f10 优化 2026-04-09 15:07:31 +08:00
zc
da317b9460 优化 2026-04-09 14:49:59 +08:00
zc
c360d00cac 优化 2026-04-09 14:36:56 +08:00
zc
60f01ac6d4 优化 2026-04-09 11:06:37 +08:00
zc
e9ed63646a 优化打包 2026-04-08 10:59:39 +08:00
zc
5d5996b915 优化称重代码 2026-04-07 17:40:19 +08:00
zc
a54a56865a 优化称重连接 2026-04-07 16:37:51 +08:00
zc
45bd3bc8f5 灯光调整 2026-04-07 14:59:06 +08:00
zc
6f3613133b 图片定时任务 2026-04-07 11:00:45 +08:00
zc
7238aabd0a 物料流程整合 2026-04-03 17:29:42 +08:00
zc
3eb98be172 还原测试 2026-04-03 16:06:04 +08:00
zc
63013bbd67 优化 2026-03-27 13:44:16 +08:00
zc
ad1cd77a48 优化 2026-03-25 17:37:30 +08:00
zc
57ba3a72b8 优化宇视摄像头 2026-03-24 14:58:30 +08:00
zc
443ee0038b 优化宇视摄像头 2026-03-23 18:02:11 +08:00
zc
33db8595ec 优化 2026-03-20 18:07:07 +08:00
zc
0dee240c6e 优化 2026-03-20 16:33:39 +08:00
zc
69f97a740d 优化 2026-03-20 11:00:23 +08:00
zc
c4303b4d5f 优化物料编码无需点击 2026-03-19 11:30:10 +08:00
zc
7b57bb9b05 优化 2026-03-19 11:16:13 +08:00
zc
feab964397 优化 2026-03-18 17:42:14 +08:00
zc
0320c8d8bc 优化 2026-03-18 09:50:13 +08:00
zc
2dc2d4887d 优化称重首页图每秒刷新 2026-03-17 16:28:54 +08:00
zc
05c6cf6b1c 优化 2026-03-17 16:14:34 +08:00
zc
722fbd988c 优化 2026-03-17 10:47:57 +08:00
zc
5554cf1548 优化 2026-03-13 10:44:12 +08:00
zc
37369a8e9a 优化 2026-03-13 10:43:33 +08:00
zc
22747f7c87 Merge remote-tracking branch 'refs/remotes/origin/master_lz' 2026-03-13 10:41:46 +08:00
zc
5aec2d7d1f 优化 2026-03-13 10:40:40 +08:00
zc
1a1be2f9d3 优化 2026-03-13 10:24:18 +08:00
40 changed files with 4886 additions and 966 deletions

View File

@@ -1,15 +1,19 @@
# 环境变量 (命名必须以 VITE_ 开头)
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = false
# 接口前缀
VITE_API_PREFIX = '/dev-api'
# 接口地址
VITE_API_BASE_URL = 'https://api.continew.top'
VITE_API_WS_URL = 'wss://api.continew.top'
VITE_API_BASE_URL = 'http://localhost:6609'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:6609'
# 地址前缀
VITE_BASE = '/'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
# 应用配置面板
VITE_APP_SETTING = true

View File

@@ -1,22 +1,21 @@
# 环境变量 (命名必须以 VITE_ 开头)
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = true
# 接口前缀
VITE_API_PREFIX = '/test-api'
VITE_API_PREFIX = '/dev-api'
# 接口地址
VITE_API_BASE_URL = 'http://localhost:6609'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:6609'
# 地址前缀
VITE_BASE = '/test'
VITE_BASE = '/'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = true
VITE_OPEN_DEVTOOLS = false
# 应用配置面板
VITE_APP_SETTING = false
VITE_APP_SETTING = true
# 终端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

285
package-lock.json generated
View File

@@ -37,6 +37,7 @@
"nprogress": "^0.2.0",
"pinia": "^2.0.16",
"pinia-plugin-persistedstate": "^3.1.0",
"qrcode": "^1.5.4",
"qs": "^6.11.2",
"query-string": "^9.0.0",
"v-viewer": "^3.0.10",
@@ -61,6 +62,7 @@
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5",
"@types/qrcode": "^1.5.6",
"@types/query-string": "^6.3.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -3272,6 +3274,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/query-string": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/@types/query-string/-/query-string-6.3.0.tgz",
@@ -6005,6 +6017,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decode-uri-component": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
@@ -6154,6 +6175,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
@@ -8447,7 +8474,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@@ -11236,7 +11262,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -11413,7 +11438,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11555,6 +11579,15 @@
"node": ">=4"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/posix-character-classes": {
"version": "0.1.1",
"resolved": "https://registry.npmmirror.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -11941,6 +11974,233 @@
"node": ">=6"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/qrcode/node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/qrcode/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/qrcode/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/qrcode/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/qrcode/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
@@ -12335,7 +12595,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -12352,6 +12611,12 @@
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resize-detector": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/resize-detector/-/resize-detector-0.3.0.tgz",
@@ -12875,6 +13140,12 @@
"randombytes": "^2.1.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -16154,6 +16425,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",

View File

@@ -43,6 +43,7 @@
"nprogress": "^0.2.0",
"pinia": "^2.0.16",
"pinia-plugin-persistedstate": "^3.1.0",
"qrcode": "^1.5.4",
"qs": "^6.11.2",
"query-string": "^9.0.0",
"v-viewer": "^3.0.10",
@@ -67,6 +68,7 @@
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5",
"@types/qrcode": "^1.5.6",
"@types/query-string": "^6.3.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0",

View File

@@ -83,11 +83,6 @@ export interface CardLoginReq extends AuthReq {
cardNumber: string
}
/** 邮箱登录请求参数 */
export interface CardLoginReq extends AuthReq {
card: string
}
/** 登录响应类型 */
export interface LoginResp {
token: string

View File

@@ -0,0 +1,62 @@
import http from '@/utils/http'
const BASE_URL = '/fullWorkOrder/fullWorkOrder'
export interface FullWorkOrderResp {
id: string
title: string
orderNo: string
materialCode: string
encodingPrint: string
materialName: string
batch: string
mark: string
count: number
imgUrl: string
createUser: string
createTime: string
updateUser: string
updateTime: string
createUserString: string
updateUserString: string
disabled: boolean
}
export interface FullWorkOrderQuery {
orderNo: string | undefined
materialCode: string | undefined
materialName: string | undefined
batch: string | undefined
createTime: Array<string> | undefined
sort: Array<string>
}
export interface FullWorkOrderPageQuery extends FullWorkOrderQuery, PageQuery {}
/** @desc 查询整箱领取记录列表 */
export function listFullWorkOrder(query: FullWorkOrderPageQuery) {
return http.get<PageRes<FullWorkOrderResp[]>>(`${BASE_URL}`, query)
}
/** @desc 新增整箱领取记录 */
export function addFullWorkOrder(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 删除整箱领取记录 */
export function deleteFullWorkOrder(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 导出整箱领取记录 */
export function exportFullWorkOrder(query: FullWorkOrderQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 保存原材料详情 */
export function saveFullWorkOrderDetail(data: any) {
return http.post(`${BASE_URL}/saveInfo`, data)
}
/** @desc 获取原材料详情列表 */
export function getFullWorkOrderDetailList(id: string) {
return http.get(`${BASE_URL}/infos/${id}`)
}

View File

@@ -3,8 +3,10 @@ import http from '@/utils/http'
const BASE_URL = '/admin/materialInfo'
export interface MaterialInfoResp {
id: string
materialName: string
encoding: string
encodingPrint: string
unitWeight: string
materialSpec: string
photoUrl: string
@@ -12,11 +14,18 @@ export interface MaterialInfoResp {
createTime: string
createUserString: string
updateUserString: string
lightLevel: number
batch: string
mark: string
disabled: boolean
photoLoadError: boolean
}
export interface MaterialInfoQuery {
materialName: string | undefined
encoding: string | undefined
encodingPrint: string | undefined
batch: string | undefined
mark: string | undefined
sort: Array<string>
}
@@ -35,9 +44,6 @@ export function listMaterialInfo(query: MaterialInfoPageQuery) {
return http.get<PageRes<MaterialInfoResp[]>>(`${BASE_URL}`, query)
}
interface MaterialInfoDetailResp {
}
/** @desc 下载物料信息导入模板 */
export function downloadMaterialInfoImportTemplate() {
return http.download(`${BASE_URL}/import/template`)
@@ -45,7 +51,7 @@ export function downloadMaterialInfoImportTemplate() {
/** @desc 查询物料信息详情 */
export function getMaterialInfo(id: string) {
return http.get<MaterialInfoDetailResp>(`${BASE_URL}/${id}`)
return http.get<MaterialInfoResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增物料信息 */
@@ -59,7 +65,7 @@ export function updateMaterialInfo(data: any, id: string) {
}
/** @desc 删除物料信息 */
export function deleteMaterialInfo(id: string) {
export function deleteMaterialInfo(id: string | Array<string>) {
return http.del(`${BASE_URL}/${id}`)
}
@@ -87,3 +93,26 @@ export function uploadMaterialPhotos(data: FormData) {
export function catchPhoto(data: FormData) {
return http.post(`${BASE_URL}/import/catch`, data)
}
/* 批次导入结果类型 */
export interface BatchImportResp {
importKey: string
totalRows: number
validRows: number
duplicateRows: number
}
/** @desc 下载批次导入模板 */
export function downloadBatchImportTemplate() {
return http.download(`${BASE_URL}/batch/import/template`)
}
/** @desc 解析批次导入数据 */
export function parseBatchImport(data: FormData) {
return http.post(`${BASE_URL}/batch/import/parse`, data)
}
/** @desc 批次导入 */
export function batchImport(data: any) {
return http.post(`${BASE_URL}/batch/import`, data)
}

View File

@@ -0,0 +1,68 @@
import http from '@/utils/http'
import type {LabelValueState} from "@/types/global";
const BASE_URL = '/materialProcess/materialProcess'
export interface MaterialProcessResp {
id: string
processName: string
processCode: string
createTime: string
updateTime: string
createUser: string
updateUser: string
createUserString: string
updateUserString: string
disabled: boolean
}
export interface MaterialProcessQuery {
processName: string | undefined
processCode: string | undefined
sort: Array<string>
}
export interface MaterialProcessPageQuery extends MaterialProcessQuery, PageQuery {}
/** @desc 查询海康物料流程列表 */
export function listMaterialProcess(query: MaterialProcessPageQuery) {
return http.get<PageRes<MaterialProcessResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询海康物料流程详情 */
export function selectList() {
return http.get(`${BASE_URL}/selectList`)
}
/** @desc 新增海康物料流程 */
export function addMaterialProcess(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 修改海康物料流程 */
export function updateMaterialProcess(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除海康物料流程 */
export function deleteMaterialProcess(id: string | Array<string>) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 导出海康物料流程 */
export function exportMaterialProcess(query: MaterialProcessQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 下载物料流程导入模板 */
export function downloadMaterialProcessImportTemplate() {
return http.download(`${BASE_URL}/import/template`)
}
/** @desc 解析物料流程导入数据 */
export function parseImportMaterialProcess(data: FormData) {
return http.post(`${BASE_URL}/import/parse`, data)
}
/** @desc 导入物料流程 */
export function importMaterialProcess(data: any) {
return http.post(`${BASE_URL}/import`, data)
}

View File

@@ -0,0 +1,80 @@
import http from '@/utils/http'
const BASE_URL = '/materialType/materialType'
export interface MaterialTypeResp {
id: string
typeName: string
upFloatRatio: string
downFloatRatio: string
createTime: string
updateTime: string
createUser: string
updateUser: string
createUserString: string
updateUserString: string
disabled: boolean
}
export interface MaterialTypeQuery {
typeName: string | undefined
sort: Array<string>
}
export interface MaterialTypePageQuery extends MaterialTypeQuery, PageQuery {}
export interface MaterialTypeImportResp {
importKey: string
totalRows: number
validRows: number
duplicateNameRows: number
}
export interface MaterialTypeImportResult {
insertRows: number
updateRows: number
totalRows: number
}
/** @desc 查询物料品类列表 */
export function listMaterialType(query: MaterialTypePageQuery) {
return http.get<PageRes<MaterialTypeResp[]>>(`${BASE_URL}`, query)
}
/** @desc 新增物料品类 */
export function addMaterialType(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 查询物料品类 */
export function selectList() {
return http.get(`${BASE_URL}/selectList`)
}
/** @desc 修改物料品类 */
export function updateMaterialType(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除物料品类 */
export function deleteMaterialType(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 导出物料品类 */
export function exportMaterialType(query: MaterialTypeQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 下载物料品类导入模板 */
export function downloadMaterialTypeImportTemplate() {
return http.download(`${BASE_URL}/importTemplate`)
}
/** @desc 解析物料品类导入数据 */
export function parseImportMaterialType(data: FormData) {
return http.post<MaterialTypeImportResp>(`${BASE_URL}/parseImport`, data)
}
/** @desc 导入物料品类 */
export function importMaterialType(data: any) {
return http.post<MaterialTypeImportResult>(`${BASE_URL}/import`, data)
}

View File

@@ -0,0 +1,23 @@
import http from '@/utils/http'
const BASE_URL = '/api/light'
/** @desc 连接灯光 */
export function connect() {
return http.post<any>(`${BASE_URL}/connect`)
}
/** @desc 断开灯光连接 */
export function disconnect() {
return http.post<any>(`${BASE_URL}/disconnect`)
}
/** @desc 设置灯光亮度 */
export function brightness(materialId: string) {
return http.post<any>(`${BASE_URL}/brightness`, { materialId: Number(materialId) })
}
/** @desc 检查灯光状态 */
export function status() {
return http.get<any>(`${BASE_URL}/status`)
}

View File

@@ -5,11 +5,17 @@ const BASE_URL = '/weighManage/workOrder'
export interface WeighManageResp {
id: string
encoding: string
encodingPrint: string
materialName: string
materialSpec: string
unitWeight: number
photoUrl: string
batch: string
mark: string
materialProcess: string
matchResult: string
downFloatRatio: string
upFloatRatio: string
}
export interface WeighManageQuery {
@@ -26,12 +32,27 @@ export function validateWeighing(data: any) {
return http.post(`${BASE_URL}/validateWeighing`, data)
}
/** @desc 校验称重信息 */
export function vmSend(code: string) {
return http.get<WeighManageResp>(`/vm/send?msg=${code}`)
/** @desc 校验物料是否一致 */
export function vmSend(materialCode: string) {
return http.post<string>(`/vm/send`, { materialCode })
}
/** @desc 获取图片 */
export function getImg() {
return http.get<string>(`/vm/latest-photo`)
/** @desc vm定时任务保存图片 */
export function getVmSaveImageTask() {
return http.post<any>(`/vm/start`)
}
/** @desc vm关闭定时任务 */
export function getVmCloseTask() {
return http.post<any>(`/vm/stop`)
}
/** @desc 启动电子称连接线程 */
export function weighAHStart() {
return http.post<any>(`/api/weigh/ah/init`)
}
/** @desc 终止电子称连接线程 */
export function weighAHStop() {
return http.post<any>(`/api/weigh/ah/destroy`)
}

View File

@@ -0,0 +1,23 @@
import http from '@/utils/http'
const BASE_URL = '/api/ys'
/** @desc 启动宇视SDK */
export function getEnterWeighPage() {
return http.get<any>(`${BASE_URL}/enter-weigh-page`)
}
/** @desc 退出宇视SDK */
export function getLeaveWeighPage() {
return http.get<any>(`${BASE_URL}/leave-weigh-page`)
}
/** @desc 抓拍图片 */
export function getCaptureImage(data: any) {
return http.get<any>(`${BASE_URL}/capture-image`, data)
}
/** @desc 检查宇视SDK状态 */
export function getCheckStatus() {
return http.get<any>(`${BASE_URL}/status`)
}

View File

@@ -11,6 +11,7 @@ export interface WorkOrderResp {
unitWeight: string
materialSpec: string
photoUrl: string
batch: string
totalWeight: string
totalCalculatedWeight: string
totalCount: string
@@ -19,6 +20,7 @@ export interface WorkOrderResp {
matchResult: string
workOrderInfos: Array<WorkOrderInfoResp>
qrCodeData: string
mark: string
}
export interface WorkOrderInfoResp {
@@ -26,15 +28,19 @@ export interface WorkOrderInfoResp {
workOrderId: string
materialId: string
weightTime: string
batch: string
quantity: string
weight: string
imgUrl: string
calculatedWeight: string
weightQuantity: string
mark: string
}
export interface WorkOrderQuery {
orderNo: string | undefined
materialName: string | undefined
batch: string | undefined
encoding: string | undefined
userName: string | undefined
carNo: string | undefined
@@ -73,3 +79,8 @@ export function deleteWorkOrder(ids: string | Array<string>) {
export function exportWorkOrder(query: WorkOrderQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 保存打印记录 */
export function savePrintRecord(data: any) {
return http.post(`/print/print`, data)
}

BIN
src/assets/wav/tooLess.wav Normal file

Binary file not shown.

BIN
src/assets/wav/tooMany.wav Normal file

Binary file not shown.

View File

@@ -38,11 +38,11 @@ export const FileIcon: FileExtendNameIconMap = {
}
/** 图片类型 */
export const ImageTypes = ['jpg', 'png', 'gif', 'jpeg']
export const ImageTypes = ['jpg', 'png', 'gif', 'jpeg', 'bmp']
/** WPS、Office文件类型 */
export const OfficeTypes = ['ppt', 'pptx', 'doc', 'docx', 'xls', 'xlsx', 'pdf']
export const WordTypes = ['doc', 'docx']
export const ExcelTypes = ['xls', 'xlsx']
export const ExcelTypes = ['xls', 'xlsx']

View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import { selectList } from '@/apis/materialProcess/materialProcess'
import type { LabelValueState } from '@/types/global'
/** 物料品类模块 */
export function materialProcess(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const materialProcessList = ref<LabelValueState[]>([])
const getMaterialProcessSelect = async () => {
try {
loading.value = true
const res = await selectList()
materialProcessList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { materialProcessList, getMaterialProcessSelect, loading }
}

View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import { selectList } from '@/apis/materialType/materialType'
import type { LabelValueState } from '@/types/global'
/** 物料品类模块 */
export function materialType(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const materialTypeList = ref<LabelValueState[]>([])
const getMaterialTypeSelect = async () => {
try {
loading.value = true
const res = await selectList()
materialTypeList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { materialTypeList, getMaterialTypeSelect, loading }
}

View File

@@ -13,8 +13,15 @@ declare module 'vue' {
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button']
AButtonGroup: typeof import('@arco-design/web-vue')['ButtonGroup']
ACard: typeof import('@arco-design/web-vue')['Card']
ACardMeta: typeof import('@arco-design/web-vue')['CardMeta']
ACarousel: typeof import('@arco-design/web-vue')['Carousel']
ACarouselItem: typeof import('@arco-design/web-vue')['CarouselItem']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
ACol: typeof import('@arco-design/web-vue')['Col']
AColorPicker: typeof import('@arco-design/web-vue')['ColorPicker']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADescriptions: typeof import('@arco-design/web-vue')['Descriptions']
@@ -28,21 +35,24 @@ declare module 'vue' {
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
AGrid: typeof import('@arco-design/web-vue')['Grid']
AGridItem: typeof import('@arco-design/web-vue')['GridItem']
AIcon: typeof import('@arco-design/web-vue')['Icon']
AImage: typeof import('@arco-design/web-vue')['Image']
AInput: typeof import('@arco-design/web-vue')['Input']
AInputGroup: typeof import('@arco-design/web-vue')['InputGroup']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
ALayout: typeof import('@arco-design/web-vue')['Layout']
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link']
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal']
AOption: typeof import('@arco-design/web-vue')['Option']
AOverflowList: typeof import('@arco-design/web-vue')['OverflowList']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopconfirm: typeof import('@arco-design/web-vue')['Popconfirm']
APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress']
ARadio: typeof import('@arco-design/web-vue')['Radio']
@@ -51,18 +61,26 @@ declare module 'vue' {
ARow: typeof import('@arco-design/web-vue')['Row']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASkeleton: typeof import('@arco-design/web-vue')['Skeleton']
ASkeletonLine: typeof import('@arco-design/web-vue')['SkeletonLine']
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
AStatistic: typeof import('@arco-design/web-vue')['Statistic']
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATable: typeof import('@arco-design/web-vue')['Table']
ATableColumn: typeof import('@arco-design/web-vue')['TableColumn']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
ATree: typeof import('@arco-design/web-vue')['Tree']
ATreeSelect: typeof import('@arco-design/web-vue')['TreeSelect']
ATrigger: typeof import('@arco-design/web-vue')['Trigger']
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph']
ATypographyText: typeof import('@arco-design/web-vue')['TypographyText']
ATypographyTitle: typeof import('@arco-design/web-vue')['TypographyTitle']
AUpload: typeof import('@arco-design/web-vue')['Upload']
Avatar: typeof import('./../components/Avatar/index.vue')['default']
AWatermark: typeof import('@arco-design/web-vue')['Watermark']

View File

@@ -0,0 +1,25 @@
import QRCode from 'qrcode'
/**
* 生成二维码Base64图片
* @param {string} data - 要编码的二维码数据
* @param {Object} options - 二维码选项
* @returns {Promise<string>} Base64编码的二维码图片
*/
export async function generateQRCode(data, options = {}) {
try {
const defaultOptions = {
width: 120,
margin: 1,
color: {
dark: '#000000',
light: '#FFFFFF',
},
}
const finalOptions = { ...defaultOptions, ...options }
return await QRCode.toDataURL(data, finalOptions)
} catch (error) {
console.error('生成二维码失败:', error)
return ''
}
}

View File

@@ -23,87 +23,171 @@
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="生产批次" required>
<a-input v-model="formData.productionBatch" placeholder="请输入生产批次" />
<a-form-item label="生产批次">
<a-input v-model="formData.batch" placeholder="未获取到生产批次" :disabled="true" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="打印编码(国产替代)">
<a-input v-model="formData.encodingPrint" allow-clear placeholder="请输入打印编码" @change="getMaterialName"/>
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="打印物料名称(国产替代)">
<a-input v-model="formData.materialNamePrint" allow-clear placeholder="请输入打印物料名称"/>
</a-form-item>
</div>
</div>
<div class="form-actions">
<a-button type="primary" @click="generateLabel" :disabled="!formData.productionBatch">生成标签</a-button>
</div>
</a-form>
<div class="form-actions">
<a-button type="primary" @click="generateDetailLabel">明细标签</a-button>
<a-button type="primary" @click="generateOverallLabel">整体标签</a-button>
</div>
</div>
<!-- 标签预览 -->
<div v-if="labelData.partName" class="label-preview-section">
<!-- <div class="label-preview-section">-->
<div v-if="labelDataList.length > 0 || labelData.partName" class="label-preview-section">
<h3>标签预览</h3>
<div class="label-container" ref="labelContainer">
<div class="label" v-for="index in 1" :key="index">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">{{ labelData.partName }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">{{ labelData.productionDate }}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img :src="`https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(labelData.qrCodeData)}`" alt="QR Code" />
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">{{ labelData.partNumber }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">{{ labelData.totalCount }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(kg)</div>
<div class="label-value">{{ labelData.totalCalculatedWeight }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">{{ labelData.packingSignature || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(kg)</div>
<div class="label-value">{{ labelData.totalWeight || '' }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">{{ labelData.inspectionSignature || '' }}</div>
</div>
</td>
</tr>
</table>
</div>
<!-- 明细标签显示多个标签 -->
<template v-if="labelDataList.length > 0">
<div class="label" v-for="(item, index) in labelDataList" :key="index">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">{{ item.partName }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">{{ item.productionDate }}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img v-if="item.qrCodeImage" :src="item.qrCodeImage" alt="QR Code" />
<div class="mark-number">{{ item.mark || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">{{ item.partNumber }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">{{ item.totalCount }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value">{{ item.totalCalculatedWeight }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">{{ item.packingSignature || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value">{{ item.totalWeight || '' }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">{{ item.inspectionSignature || '' }}</div>
</div>
</td>
</tr>
</table>
</div>
</template>
<!-- 整体标签显示单个标签 -->
<template v-else>
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">{{ labelData.partName }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">{{ labelData.productionDate }}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img v-if="labelData.qrCodeImage" :src="labelData.qrCodeImage" alt="QR Code" />
<div class="mark-number">{{ labelData.mark || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">{{ labelData.partNumber }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">{{ labelData.totalCount }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value">{{ labelData.totalCalculatedWeight }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">{{ labelData.packingSignature || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value">{{ labelData.totalWeight || '' }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">{{ labelData.inspectionSignature || '' }}</div>
</div>
</td>
</tr>
</table>
</div>
</template>
</div>
<div class="label-actions">
<a-button type="primary" @click="printLabel">打印标签</a-button>
@@ -117,7 +201,9 @@
import { ref, reactive, nextTick, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
import {getWorkOrder} from "@/apis/workOrder/workOrder";
import {getWorkOrder, savePrintRecord, type WorkOrderInfoResp} from "@/apis/workOrder/workOrder"
import QRCode from 'qrcode';
import {getMaterialDetail} from "@/apis/weightManage/weightManage";
const route = useRoute()
@@ -125,16 +211,35 @@ const route = useRoute()
const formData = reactive({
workerOrderId: '',
encoding: '',
encodingPrint: '',
materialNamePrint: '',
materialName: '',
orderNo: '',
totalCalculatedWeight: '',
totalWeight: '',
totalCount: '',
productionBatch: '',
batch: '',
mark: '',
qrCodeData: '',
workOrderInfos: Array<WorkOrderInfoResp>(),
})
// 标签数据
// 标签数据数组
const labelDataList = reactive<Array<{
partName: string
partNumber: string
totalCalculatedWeight: string
totalWeight: string
productionDate: string
totalCount: string
packingSignature: string
inspectionSignature: string
qrCodeData: string
qrCodeImage: string
mark: string
}>>([])
// 标签数据(用于整体标签)
const labelData = reactive({
partName: '',
partNumber: '',
@@ -145,28 +250,54 @@ const labelData = reactive({
packingSignature: '',
inspectionSignature: '',
qrCodeData: '',
qrCodeImage: '',
mark: '',
})
// 标签数据
// const labelData = reactive({
// partName: '物料3',
// partNumber: '1',
// totalCalculatedWeight: '100',
// totalWeight: '100',
// productionDate: '202401010000',
// totalCount: '100',
// packingSignature: '',
// inspectionSignature: '',
// qrCodeData: '10#$$DY',
// })
// 标签容器引用
const labelContainer = ref<HTMLElement | null>(null)
// 生成标签
const generateLabel = async () => {
if (!formData.productionBatch) {
Message.error('请输入生产批次')
const getMaterialName = async (value: string) => {
try {
const res = await getMaterialDetail(value)
if (res.code == '0') {
formData.materialNamePrint = res.data.materialName || ''
} else {
formData.materialNamePrint = '';
Message.error('获取物料信息失败')
}
} catch (error) {
formData.materialNamePrint = '';
}
};
// 生成二维码
const generateQRCode = async (data: string) => {
try {
return await QRCode.toDataURL(data, {
width: 120,
margin: 1,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
} catch (error) {
console.error('生成二维码失败:', error)
return ''
}
}
// 生成明细标签
const generateDetailLabel = async () => {
if (!formData.workOrderInfos || formData.workOrderInfos.length === 0) {
Message.error('未获取到工单明细信息')
return
}
if (!formData.batch) {
Message.error('未获取到批次信息')
return
}
if (!formData.materialName) {
@@ -175,6 +306,9 @@ const generateLabel = async () => {
}
try {
// 清空之前的标签数据
labelDataList.length = 0
// 格式化生产日期为 yyyyMMddHHmm 格式
const now = new Date()
const formattedDate = now.getFullYear().toString() +
@@ -185,20 +319,86 @@ const generateLabel = async () => {
const formattedDate2 = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0');
String(now.getDate()).padStart(2, '0')
// 为每个工单明细生成一个标签
for (const workOrderInfo of formData.workOrderInfos) {
// 计算二维码数据
const orderNo = formData.orderNo + workOrderInfo.id;
const qrCodeData = `10#${formData.encodingPrint || formData.encoding}$11#9DP$12#${formData.batch}$17#${workOrderInfo.quantity}$20#${formattedDate2}$31#${orderNo}$DY`;
// 生成二维码图片
const qrCodeImage = await generateQRCode(qrCodeData)
// 添加标签数据
labelDataList.push({
partName: formData.materialNamePrint || formData.materialName || '',
partNumber: formData.encodingPrint || formData.encoding || '',
totalCalculatedWeight: workOrderInfo.calculatedWeight || '',
totalWeight: workOrderInfo.weight || '',
productionDate: formattedDate,
totalCount: workOrderInfo.quantity || '',
packingSignature: '',
inspectionSignature: '',
qrCodeData: qrCodeData,
qrCodeImage: qrCodeImage,
mark: workOrderInfo.mark || '',
})
}
Message.success(`成功生成 ${labelDataList.length} 个明细标签`)
} catch (error) {
console.error('生成明细标签失败:', error)
Message.error('生成明细标签失败')
}
}
// 生成整体标签
const generateOverallLabel = async () => {
if (!formData.batch) {
Message.error('未获取到批次信息')
return
}
if (!formData.materialName) {
Message.error('未获取到物料信息')
return
}
try {
// 清空明细标签数据
labelDataList.length = 0
// 格式化生产日期为 yyyyMMddHHmm 格式
const now = new Date()
const formattedDate = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0')
const formattedDate2 = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0')
// 计算二维码数据
const qrCodeData = `10#${formData.encodingPrint || formData.encoding}$11#9DP$12#${formData.batch}$17#${formData.totalCount}$20#${formattedDate2}$31#${formData.orderNo}$DY`
// 生成二维码图片
const qrCodeImage = await generateQRCode(qrCodeData)
// 直接从 formData 中获取数据
Object.assign(labelData, {
partName: formData.materialName || '',
partNumber: formData.encoding || '',
partName: formData.materialNamePrint || formData.materialName || '',
partNumber: formData.encodingPrint || formData.encoding || '',
totalCalculatedWeight: formData.totalCalculatedWeight || '',
totalWeight: formData.totalWeight || '',
productionDate: formattedDate,
totalCount: formData.totalCount || '',
packingSignature: '',
inspectionSignature: '',
//10#零件号$11#供应商代码$12#生产批次$17#数量$20#包装日期$31#唯一号$DY
qrCodeData: `10#${formData.materialName}$11#9DP$12#${formData.productionBatch}$17#${formData.totalCount}$20#${formattedDate2}$31#${formData.orderNo}$DY`
qrCodeData: qrCodeData,
qrCodeImage: qrCodeImage,
mark: formData.mark || '',
})
Message.success('标签生成成功')
@@ -220,16 +420,16 @@ onMounted(() => {
formData.encoding = res.data.encoding
formData.materialName = res.data.materialName
formData.orderNo = res.data.orderNo
formData.batch = res.data.batch
formData.totalCalculatedWeight = res.data.totalCalculatedWeight
formData.totalWeight = res.data.totalWeight
formData.totalCount = res.data.totalCount
formData.workOrderInfos = res.data.workOrderInfos
formData.mark = res.data.mark
} else {
Message.error('获取详情失败')
}
});
// 自动生成标签
generateLabel()
}
})
@@ -240,93 +440,125 @@ const printLabel = async () => {
// 创建打印窗口
const printWindow = window.open('', '_blank')
if (printWindow) {
// 直接构建打印内容,确保二维码图片正确生成
// 判断是打印明细标签还是整体标签
const isDetailLabels = labelDataList.length > 0
const labelsToPrint = isDetailLabels ? labelDataList : [labelData]
// 生成标签HTML
const labelsHTML = labelsToPrint.map(item => `
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">${item.partName}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">${item.productionDate}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img src="${item.qrCodeImage}" alt="QR Code" />
<div class="mark-number">${item.mark || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">${item.partNumber}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">${item.totalCount}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value">${item.totalCalculatedWeight}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">${item.packingSignature || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value">${item.totalWeight || ''}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">${item.inspectionSignature || ''}</div>
</div>
</td>
</tr>
</table>
</div>
`).join('')
// 构建打印内容,通过 matchMedia 监听打印确认事件(仅在确认打印时保存记录)
const printHTML = `
<html>
<head>
<title>标签打印</title>
<style>
body { margin: 0; padding: 10mm; font-family: Arial, sans-serif; }
.label-container { display: flex; flex-wrap: wrap; gap: 10mm; justify-content: center; }
.label { width: 90mm; height: 38.5mm; border: 1px solid #000; padding: 0; box-sizing: border-box; page-break-inside: avoid; }
.label-table { width: 100%; height: 100%; border-collapse: collapse; }
.label-cell { border: 1px solid #000; padding: 1mm; vertical-align: top; }
.qr-cell { width: 24mm; text-align: center; vertical-align: middle; border: 1px solid #000; }
@page { size: 80mm 50mm; margin: 0; }
body { margin: 0; padding: 1mm; font-family: Arial, sans-serif; }
.label-container { display: flex; flex-wrap: wrap; gap: 1mm; justify-content: center; align-items: center; transform-origin: center; }
.label { width: 75mm; height: 45mm; border: 1px solid #000; padding: 0; box-sizing: border-box; page-break-inside: avoid; overflow: hidden; margin: 0 auto; }
.label-table { width: 100%; height: 100%; border-collapse: collapse; table-layout: fixed; }
.label-cell { border: 1px solid #000; padding: 0.8mm; vertical-align: top; }
.qr-cell { width: 22mm; text-align: center; vertical-align: middle; border: 1px solid #000; }
.label-row { display: flex; align-items: center; }
.label-field { font-size: 7pt; font-weight: bold; margin-right: 2mm; min-width: 25pt; }
.label-value { font-size: 7pt; flex: 1; }
.qr-code img { width: 20mm; height: 20mm; margin: 1mm 0; }
.serial-number { font-size: 7pt; margin-top: 1mm; }
.label-field { font-size: 7.5pt; font-weight: bold; margin-right: 1mm; min-width: 22pt; white-space: nowrap; }
.label-value { font-size: 7.5pt; font-weight: bold; flex: 1; overflow-wrap: break-word; word-break: break-word; }
.qr-code img { width: 22mm; height: 22mm; margin: 0.5mm 0; }
.mark-number { font-size: 7pt; font-weight: bold; margin-top: 0.5mm; text-align: center; }
.serial-number { font-size: 7.5pt; font-weight: bold; margin-top: 0.5mm; }
</style>
</head>
<body>
<div class="label-container">
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">${labelData.partName}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">${labelData.productionDate}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(labelData.qrCodeData)}" alt="QR Code" />
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">${labelData.partNumber}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">${labelData.totalCount}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(kg)</div>
<div class="label-value">${labelData.totalCalculatedWeight}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">${labelData.packingSignature || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(kg)</div>
<div class="label-value">${labelData.totalWeight || ''}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">${labelData.inspectionSignature || ''}</div>
</div>
</td>
</tr>
</table>
</div>
${labelsHTML}
</div>
<script>
// 标记是否已保存打印记录(避免重复触发)
var printRecordSaved = false;
// 通过 matchMedia 监听打印状态变化
// mediaQuery.matches === true 表示浏览器进入"打印"渲染状态(用户点击了"打印/确认"按钮)
// 取消或关闭窗口不会触发此状态切换
var mediaQuery = window.matchMedia('print');
var handlePrintChange = function(mql) {
if (mql.matches && !printRecordSaved) {
printRecordSaved = true;
// 向父窗口发送消息,通知保存打印记录
window.opener && window.opener.postMessage({ type: 'PRINT_CONFIRMED' }, '*');
}
};
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handlePrintChange);
} else if (mediaQuery.addListener) {
mediaQuery.addListener(handlePrintChange);
}
<\/script>
</body>
</html>
`
@@ -334,6 +566,46 @@ const printLabel = async () => {
printWindow.document.write(printHTML)
printWindow.document.close()
// 监听子窗口发来的打印确认消息,保存打印记录
const onPrintConfirmed = async (event: MessageEvent) => {
if (event.data && event.data.type === 'PRINT_CONFIRMED') {
try {
const printRecords = labelsToPrint.map(item => ({
workerOrderId: formData.workerOrderId,
materialNamePrint: item.partName,
encodingPrint: item.partNumber,
totalCount: item.totalCount,
totalCalculatedWeight: item.totalCalculatedWeight,
totalWeight: item.totalWeight,
qrCodeData: item.qrCodeData,
batch: formData.batch,
mark: item.mark,
productionDate: item.productionDate,
packingSignature: item.packingSignature == '' ? null : item.packingSignature,
inspectionSignature: item.inspectionSignature == '' ? null : item.inspectionSignature,
}))
await savePrintRecord({
workerOrderId: formData.workerOrderId,
orderNo: formData.orderNo,
encoding: formData.encoding,
materialName: formData.materialName,
materialNamePrint: formData.materialNamePrint,
encodingPrint: formData.encodingPrint,
batch: formData.batch,
labelType: isDetailLabels ? 0 : 1,
labelCount: labelsToPrint.length,
printInfoList: printRecords,
})
Message.success('打印记录已保存')
} catch (err) {
console.error('保存打印记录失败:', err)
Message.error('保存打印记录失败')
}
window.removeEventListener('message', onPrintConfirmed)
}
}
window.addEventListener('message', onPrintConfirmed)
// 等待图片加载完成后再打印
printWindow.onload = () => {
setTimeout(() => {
@@ -346,7 +618,7 @@ const printLabel = async () => {
}
}
defineOptions({ name: 'BarcodePrint' })
defineOptions({ name: 'print' })
</script>
<style scoped lang="scss">
@@ -386,6 +658,7 @@ defineOptions({ name: 'BarcodePrint' })
margin-top: 20px;
display: flex;
justify-content: center;
gap: 20px;
}
/* 标签预览 */
@@ -405,9 +678,9 @@ defineOptions({ name: 'BarcodePrint' })
.label-container {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 20px;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
@@ -452,13 +725,32 @@ defineOptions({ name: 'BarcodePrint' })
.label-value {
font-size: 12px;
font-weight: bold;
flex: 1;
word-break: break-word;
}
.qr-code img {
width: 115px;
height: 120px;
margin: 0 0;
}
.qr-code .loading {
width: 100px;
height: 100px;
margin: 5px 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #666;
}
.mark-number {
font-size: 10px;
margin-top: 5px;
text-align: center;
font-weight: bold;
}
.serial-number {

View File

@@ -32,13 +32,11 @@
</div>
</a-card>
<NoticeDetailModal ref="NoticeDetailModalRef" />
</template>
<script setup lang="ts">
import { type DashboardNoticeResp, listDashboardNotice } from '@/apis'
import { useDict } from '@/hooks/app'
import NoticeDetailModal from '@/views/system/notice/NoticeDetailModal.vue'
const router = useRouter()
const { notice_type } = useDict('notice_type')
@@ -56,11 +54,6 @@ const getDataList = async () => {
}
}
const NoticeDetailModalRef = ref<InstanceType<typeof NoticeDetailModal>>()
// 详情
const onDetail = (id: string) => {
NoticeDetailModalRef.value?.onDetail(id)
}
onMounted(() => {
getDataList()

View File

@@ -0,0 +1,431 @@
<template>
<a-modal
v-model:visible="visible"
title="新增整箱领取记录"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 800 ? 800 : '90%'"
:style="{ height: '90vh', maxHeight: '900px' }"
draggable
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" v-model="form">
<a-row :gutter="22">
<a-col :span="22">
<a-form-item label="物料编码" label-col-flex="100px">
<a-input
ref="materialCodeInput"
v-model="form.inputMaterialCode"
placeholder="请点击此处确保光标闪烁,且输入法为英文状态,使用扫码枪扫描物料编码"
@keydown="handleKeyDown"
@input="handleMaterialCodeChange"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="22">
<a-col :span="11">
<a-form-item label="手动输入编码" label-col-flex="100px">
<a-input
ref="inputMaterialCode"
v-model="form.inputMaterialCode2"
placeholder="请输入物料编码"
@change="handleMaterialCodeChange2"
/>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item label="序号">
<a-input-number
v-model="form.mark"
:min="1"
placeholder="请输入序号"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="22">
<a-col :span="11">
<a-form-item label="物料名称" label-col-flex="100px">
<a-input
ref="materialName"
v-model="form.materialName"
placeholder="-"
disabled
/>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item label="批次">
<a-input
ref="batch"
v-model="form.batch"
placeholder="-"
disabled
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="22">
<a-col :span="11">
<a-form-item label="数量" label-col-flex="100px">
<a-input-number
v-model="form.count"
placeholder="整箱数量"
:min="0"
:disabled = "disableCount"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="22">
<a-col :span="22">
<a-form-item label="图片" label-col-flex="100px">
<div class="image-container">
<img
:src="imgData.imgUrl"
alt="图片"
style="width: 100%; height: 100%; object-fit: cover; border-radius: 4px;"
/>
<!-- 错误状态 -->
<div v-if="weighingPageStatus === 'error'" class="video-overlay error">
<icon-close-circle-fill style="color: #ff4d4f; font-size: 24px;" />
<span>连接异常</span>
<Button size="small" type="primary" @click="enterWeighPage">重试</Button>
</div>
<!-- 加载状态 -->
<div v-if="weighingPageStatus === 'entering'" class="video-overlay">
<Spin />
<span style="margin-left: 8px;">加载中...</span>
</div>
</div>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import {type FormInstance, Message, Spin, Button, Icon} from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addFullWorkOrder } from '@/apis/fullWorkOrder/fullWorkOrder'
import {getCaptureImage, getEnterWeighPage, getLeaveWeighPage} from '@/apis/weightManage/ys'
import { useResetReactive } from '@/hooks'
import {getMaterialDetail} from "@/apis/weightManage/weightManage";
import {number} from "echarts";
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width, height } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const disableCount = ref(false)
const materialCodeInput = ref<any>(null)
// 称重页面状态
const weighingPageStatus = ref<'idle' | 'entering' | 'entered' | 'error'>('idle')
const [form, resetForm] = useResetReactive({
materialCode: '',
encodingPrint: '',
imgUrl: '',
materialName: '',
batch: '',
count: undefined,
inputMaterialCode: '',
inputMaterialCode2: '',
mark: undefined,
})
const imgData = reactive({
imgUrl: 'http://localhost:6609/file/ys/carousel.jpg', // 称重页面图片URL
baseUrl: 'http://localhost:6609/file/ys/carousel.jpg' // 基础URL
})
// 图片刷新定时器
let imageRefreshTimer: any = null
// 重置
const reset = () => {
resetForm()
}
// 防抖函数
const debounce = <T extends (...args: any[]) => any>(func: T, delay: number): ((...args: Parameters<T>) => void) => {
let timer: ReturnType<typeof setTimeout> | null = null
return function (...args: Parameters<T>) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func(...args)
}, delay)
}
}
// 原始的物料编码变化处理函数
const originalHandleMaterialCodeChange = async () => {
// 确保输入框内容是完整的物料编码
// todo
const materialCode = form.inputMaterialCode?.trim()
// const materialCode = "831002839562,1,0.12,KP,0A2005,0A200520260325";
// 无论是否有输入,先重置所有物料相关字段,确保新数据能完全覆盖旧数据
form.materialCode = ''
form.materialName = ''
form.inputMaterialCode2 = ''
form.batch = ''
form.encodingPrint = ''
form.count = undefined
form.mark = undefined
// 如果有物料编码输入,获取物料数据
if (materialCode) {
try {
disableCount.value = true
const parts = materialCode.split(',');
form.count = parseFloat(parts[2]) * 1000;
console.log("form.count", form.count);
await fetchMaterialData(materialCode)
} catch (error) {
console.error('获取物料数据失败:', error)
// 即使获取失败,也保持字段为空,避免显示旧数据
}
}
}
// 带防抖的物料编码变化处理函数
const handleMaterialCodeChange = debounce(originalHandleMaterialCodeChange, 500) // 500ms防抖延迟
// 扫码核验获取物料信息
const fetchMaterialData = async (code: string) => {
const res = await getMaterialDetail(code);
if (res.code === '0') {
// 更新表单数据
form.materialCode = res.data?.encoding || ''
form.encodingPrint = res.data?.encodingPrint || ''
form.materialName = res.data?.materialName || ''
form.batch = res.data?.batch || ''
}
}
const handleMaterialCodeChange2 = async (code: string) => {
if (!code || code?.trim()=== '') {
form.materialCode = ''
form.encodingPrint = ''
form.materialName = ''
form.batch = ''
form.mark = undefined
form.count = undefined
return
}
disableCount.value = false
form.inputMaterialCode = ''
form.mark = undefined
form.count = undefined
const res = await getMaterialDetail(code);
if (res.code === '0') {
// 更新表单数据
form.materialCode = res.data?.encoding || ''
form.encodingPrint = res.data?.encodingPrint || ''
form.materialName = res.data?.materialName || ''
form.batch = res.data?.batch || ''
}
};
// 保存
const save = async () => {
if (!form.materialCode || form.materialCode.trim() === '') {
Message.error('未找到物料信息');
return false
}
if (!form.count) {
Message.error('数量不能为空');
return false
}
if (!form.batch || form.batch.trim() === '') {
Message.error('未找到批次');
return false
}
if (!form.mark) {
Message.error('请输入序号');
return false
}
try {
//手动抓图
const data = {
type: 2,
}
// todo
const response = await getCaptureImage(data);
// const response = {'data': 'http://localhost:6609/file/ys/carousel.jpg'};
if (response) {
form.imgUrl = response.data;
} else {
Message.error('抓图失败');
return false;
}
await addFullWorkOrder(form)
Message.success('新增成功')
emit('save-success')
return true
} catch (error) {
return false
}
}
// 启动宇视SDK
const enterWeighPage = async () => {
weighingPageStatus.value = 'entering'
try {
await getEnterWeighPage()
weighingPageStatus.value = 'entered'
} catch (error) {
console.error('启动宇视SDK失败:', error)
weighingPageStatus.value = 'error'
}
}
// 退出宇视SDK
const leaveWeighPage = async () => {
try {
await getLeaveWeighPage()
} catch (error) {
console.error('退出宇视SDK失败:', error)
}
}
// 新增
const onAdd = async () => {
reset()
dataId.value = ''
visible.value = true
// 启动宇视SDK
await enterWeighPage()
// 聚焦到物料编码输入框
nextTick(() => {
if (materialCodeInput.value) {
materialCodeInput.value.focus()
}
})
}
// 记录上次输入时间,用于判断是否是扫码枪输入
let lastInputTime = 0
// 记录是否正在接收扫码输入
let isScanning = false
// 处理键盘按下事件
const handleKeyDown = (event: KeyboardEvent) => {
// 获取当前时间
const currentTime = Date.now()
// 计算时间差
const timeDiff = currentTime - lastInputTime
// 检查是否是回车键(扫码枪通常以回车键结束)
if (event.key === 'Enter') {
// 扫码结束,标记为不在扫描中
isScanning = false
// 不阻止默认行为,让表单可以正常提交
}
// 检查是否是新的扫码开始
// 当时间间隔大于300ms且不是修饰键且不是功能键时认为是新的扫码开始
else if (timeDiff > 300 && !event.ctrlKey && !event.altKey && !event.metaKey) {
form.inputMaterialCode = ''
// 标记为开始扫描
isScanning = true
}
// 更新上次输入时间
lastInputTime = currentTime
}
// 组件挂载时
onMounted(() => {
// 初始时不启动图片刷新
})
// 组件卸载时
onBeforeUnmount(() => {
// 清除图片刷新定时器
if (imageRefreshTimer) {
clearInterval(imageRefreshTimer)
imageRefreshTimer = null
}
// 退出宇视SDK
leaveWeighPage()
})
// 监听visible变化
watch(visible, async (newVal) => {
if (newVal) {
// 当弹窗打开时启动图片自动刷新每1.5秒更新一次
imageRefreshTimer = setInterval(() => {
// 添加时间戳参数,避免浏览器缓存
imgData.imgUrl = `${imgData.baseUrl}?t=${Date.now()}`
}, 1500)
} else {
// 当弹窗关闭时退出宇视SDK并停止图片刷新
await leaveWeighPage()
if (imageRefreshTimer) {
clearInterval(imageRefreshTimer)
imageRefreshTimer = null
}
}
})
defineExpose({ onAdd })
</script>
<style scoped lang="scss">
.image-container {
position: relative;
width: 100%;
height: 100%;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* 视频覆盖层样式(用于错误和加载状态) */
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
gap: 10px;
}
.video-overlay.error {
background: rgba(255, 77, 79, 0.2);
}
/* 禁用输入框样式 - 黑色加粗字体 */
:deep(.arco-input-wrapper.arco-input-disabled) {
background-color: #ffffff !important;
border-color: #d9d9d9 !important;
}
:deep(.arco-input-wrapper.arco-input-disabled .arco-input) {
color: #000000 !important;
background-color: transparent !important;
opacity: 1 !important;
-webkit-text-fill-color: #000000 !important;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<a-modal
v-model:visible="visible"
title="原材料详情"
:width="800"
:footer="false"
@cancel="handleClose"
>
<a-table
:columns="columns"
:data="detailList"
:loading="loading"
:pagination="false"
bordered
>
<template #imgUrl="{ record }">
<a-image
width="80"
height="60"
:src="record.imgUrl"
fit="cover"
/>
</template>
</a-table>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getFullWorkOrderDetailList } from '@/apis/fullWorkOrder/fullWorkOrder'
const visible = ref(false)
const loading = ref(false)
const detailList = ref<any[]>([])
const columns = [
{ title: '称重重量(g)', dataIndex: 'weight', key: 'weight' },
{ title: '截图', dataIndex: 'imgUrl', key: 'imgUrl', slotName: 'imgUrl' }
]
const onOpen = async (id: string) => {
visible.value = true
loading.value = true
try {
const res = await getFullWorkOrderDetailList(id)
if (res.code === '0') {
detailList.value = res.data || []
} else {
Message.error(res.msg || '获取详情失败')
}
} catch (error) {
console.error('获取详情失败:', error)
Message.error('获取详情失败')
} finally {
loading.value = false
}
}
const handleClose = () => {
visible.value = false
detailList.value = []
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,368 @@
<template>
<a-modal
v-model:visible="visible"
title="新增原材料详情"
:mask-closable="false"
:esc-to-close="false"
:width="1200"
:style="{ height: '80vh' }"
draggable
@before-ok="save"
@close="reset"
>
<div class="detail-container">
<div class="left-section">
<div class="weight-input">
<label>称重重量(g)</label>
<a-input v-model="weightValue" placeholder="等待电子秤数据..." disabled />
</div>
<div class="camera-section">
<div class="image-container">
<img
:src="imgData.imgUrl"
alt="宇视摄像头实时画面"
style="width: 100%; height: 100%; object-fit: cover; border-radius: 4px;"
/>
<div v-if="cameraStatus === 'error'" class="video-overlay error">
<icon-close-circle-fill style="color: #ff4d4f; font-size: 24px;" />
<span>连接异常</span>
<a-button size="small" type="primary" @click="enterCamera">重试</a-button>
</div>
<div v-if="cameraStatus === 'entering'" class="video-overlay">
<a-spin />
<span style="margin-left: 8px;">加载中...</span>
</div>
</div>
</div>
<div class="confirm-button">
<a-button type="primary" @click="handleConfirm">确定</a-button>
</div>
</div>
<div class="right-section">
<div class="detail-list">
<a-table :columns="detailColumns" :data="detailList" :pagination="false" bordered>
<template #imgUrl="{ record }">
<a-image width="80" height="60" :src="record.imgUrl" />
</template>
<template #action="{ record }">
<a-button type="text" status="danger" @click="handleDeleteDetail(record)">
删除
</a-button>
</template>
</a-table>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getCaptureImage, getEnterWeighPage, getLeaveWeighPage } from '@/apis/weightManage/ys'
import { weighAHStart, weighAHStop } from '@/apis/weightManage/weightManage'
import { saveFullWorkOrderDetail } from '@/apis/fullWorkOrder/fullWorkOrder'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const visible = ref(false)
const weightValue = ref('')
const cameraStatus = ref<'idle' | 'entering' | 'entered' | 'error'>('idle')
const fullWorkOrder = ref('')
const imgData = reactive({
imgUrl: 'http://localhost:6609/file/ys/carousel.jpg',
baseUrl: 'http://localhost:6609/file/ys/carousel.jpg'
})
let imageRefreshTimer: any = null
const detailList = ref<any[]>([])
const detailColumns = [
{ title: '称重重量(g)', dataIndex: 'weight', key: 'weight' },
{ title: '截图', dataIndex: 'imgUrl', key: 'imgUrl', slotName: 'imgUrl' },
{ title: '操作', dataIndex: 'action', key: 'action', slotName: 'action', width: 80 }
]
// WebSocket连接
const ws = ref<WebSocket | null>(null)
const wsConnected = ref(false)
// 建立WebSocket连接电子称
const establishWebSocket = () => {
try {
const wsUrl = 'ws://localhost:6609/ws/scale'
ws.value = new WebSocket(wsUrl)
ws.value.onopen = () => {
wsConnected.value = true
}
ws.value.onmessage = (event) => {
try {
if (event.data) {
weightValue.value = event.data
}
} catch (error) {
console.error('WebSocket消息解析失败:', error)
}
}
ws.value.onclose = () => {
wsConnected.value = false
}
ws.value.onerror = (error) => {
console.error('WebSocket错误:', error)
wsConnected.value = false
Message.error('称重连接失败')
}
} catch (error) {
console.error('建立WebSocket连接失败:', error)
Message.error('无法建立称重连接')
}
}
// 关闭WebSocket连接电子称
const closeWebSocket = () => {
if (ws.value) {
ws.value.close()
ws.value = null
wsConnected.value = false
}
}
// 连接电子称
const connectScale = async () => {
try {
await weighAHStart()
establishWebSocket()
} catch (error) {
console.error('连接电子称失败:', error)
Message.error('与电子称的连接建立失败')
}
}
// 断开电子称
const disconnectScale = async () => {
try {
await weighAHStop()
} catch (error) {
console.error('断开电子称失败:', error)
}
closeWebSocket()
}
const reset = () => {
weightValue.value = ''
detailList.value = []
fullWorkOrder.value = ''
}
const enterCamera = async () => {
cameraStatus.value = 'entering'
try {
await getEnterWeighPage()
cameraStatus.value = 'entered'
} catch (error) {
console.error('进入摄像头页面失败:', error)
cameraStatus.value = 'error'
}
}
const leaveCamera = async () => {
try {
await getLeaveWeighPage()
} catch (error) {
console.error('离开摄像头页面失败:', error)
}
}
const onAdd = async (id: string) => {
reset()
fullWorkOrder.value = id
visible.value = true
// 独立连接电子称和摄像头
await Promise.all([
enterCamera(),
connectScale()
])
}
const handleConfirm = async () => {
if (!weightValue.value || weightValue.value.trim() === '') {
Message.error('电子秤称重结果为空!')
return
}
try {
const data = {
type: 2,
}
// todo
const response = await getCaptureImage(data)
// const response = {
// data: 'http://localhost:6609/file/ys/workOrder/fullOrder_1774322914630.jpg',
// }
if (response && response.data) {
const newItem = {
key: Date.now().toString(),
weight: weightValue.value,
imgUrl: response.data
}
detailList.value.push(newItem)
weightValue.value = ''
Message.success('添加成功')
} else {
Message.error('抓图失败')
}
} catch (error) {
console.error('抓图失败:', error)
Message.error('抓图失败')
}
}
const handleDeleteDetail = (record: any) => {
detailList.value = detailList.value.filter(item => item.key !== record.key)
Message.success('删除成功')
}
const save = async () => {
if (detailList.value.length === 0) {
Message.error('请至少添加一条详情记录')
return false
}
try {
const data = detailList.value.map(item => ({
weight: item.weight,
imgUrl: item.imgUrl,
fullWorkOrderId: fullWorkOrder.value,
}))
const res = await saveFullWorkOrderDetail(data)
if (res.code === '0') {
Message.success('保存成功')
emit('save-success')
return true
} else {
Message.error(res.msg || '保存失败')
return false
}
} catch (error) {
console.error('保存失败:', error)
Message.error('保存失败')
return false
}
}
onMounted(() => {
})
onBeforeUnmount(() => {
if (imageRefreshTimer) {
clearInterval(imageRefreshTimer)
imageRefreshTimer = null
}
leaveCamera()
})
watch(visible, async (newVal) => {
if (newVal) {
imageRefreshTimer = setInterval(() => {
imgData.imgUrl = `${imgData.baseUrl}?t=${Date.now()}`
}, 1500)
} else {
// 独立断开电子称和摄像头
await Promise.all([
leaveCamera(),
disconnectScale()
])
if (imageRefreshTimer) {
clearInterval(imageRefreshTimer)
imageRefreshTimer = null
}
}
})
defineExpose({ onAdd })
</script>
<style scoped lang="scss">
.detail-container {
display: flex;
height: 100%;
gap: 20px;
}
.left-section {
width: 45%;
display: flex;
flex-direction: column;
gap: 16px;
}
.weight-input {
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
}
.camera-section {
flex: 1;
min-height: 300px;
}
.image-container {
position: relative;
width: 100%;
height: 100%;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
gap: 10px;
}
.video-overlay.error {
background: rgba(255, 77, 79, 0.2);
}
.confirm-button {
display: flex;
justify-content: center;
}
.right-section {
width: 55%;
display: flex;
flex-direction: column;
}
.detail-list {
flex: 1;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,351 @@
<template>
<div class="gi_table_page">
<GiTable
title="整箱领取记录管理"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.orderNo" placeholder="请输入任务工单号" allow-clear @search="search" />
<a-input-search v-model="queryForm.materialCode" placeholder="请输入物料编码" allow-clear @search="search" />
<a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" />
<a-input-search v-model="queryForm.batch" placeholder="请输入批次号" allow-clear @search="search" />
<a-range-picker
v-model="queryForm.createTime"
:show-time="true"
format="YYYY-MM-DD HH:mm:ss"
style="height: 32px"
:allow-clear="true"
@change="search"
/>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['fullWorkOrder:fullWorkOrder:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
<a-button v-permission="['fullWorkOrder:fullWorkOrder:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #imgUrl="{ record }">
<a-image
width="60"
:src="record.imgUrl"
/>
</template>
<template #action="{ record }">
<a-space>
<a-link
@click="onAddDetail(record.id)"
>
新增
</a-link>
<a-link
@click="onViewDetail(record)"
>
详情
</a-link>
<a-link
@click="onPrint(record)"
>
打印
</a-link>
<a-link
v-permission="['fullWorkOrder:fullWorkOrder:delete']"
status="danger"
:disabled="record.disabled"
:title="record.disabled ? '不可删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
</a-space>
</template>
</GiTable>
<FullWorkOrderAddModal ref="FullWorkOrderAddModalRef" @save-success="search" />
<FullWorkOrderDetailModal ref="FullWorkOrderDetailModalRef" @save-success="search" />
<FullWorkOrderDetailListModal ref="FullWorkOrderDetailListModalRef" />
</div>
</template>
<script setup lang="ts">
import FullWorkOrderAddModal from './FullWorkOrderAddModal.vue'
import FullWorkOrderDetailModal from './FullWorkOrderDetailModal.vue'
import FullWorkOrderDetailListModal from './FullWorkOrderDetailListModal.vue'
import { type FullWorkOrderResp, type FullWorkOrderQuery, deleteFullWorkOrder, exportFullWorkOrder, listFullWorkOrder } from '@/apis/fullWorkOrder/fullWorkOrder'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import type {WorkOrderResp} from "@/apis/workOrder/workOrder";
import QRCode from 'qrcode';
import {Message} from "@arco-design/web-vue";
defineOptions({ name: 'FullWorkOrder' })
const queryForm = reactive<FullWorkOrderQuery>({
orderNo: undefined,
materialCode: undefined,
materialName: undefined,
batch: undefined,
createTime: undefined,
sort: ['f.id,desc']
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete
} = useTable((page) => listFullWorkOrder({ ...queryForm, ...page }), { immediate: true })
const columns = ref<TableInstanceColumns[]>([
{ title: '标题', dataIndex: 'title', slotName: 'title' },
{ title: '任务工单号', dataIndex: 'orderNo', slotName: 'orderNo' },
{ title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' },
{ title: '物料编码', dataIndex: 'materialCode', slotName: 'materialCode' },
{ title: '批次号', dataIndex: 'batch', slotName: 'batch' },
{ title: '数量', dataIndex: 'count', slotName: 'count' },
{ title: '标记号', dataIndex: 'mark', slotName: 'mark' },
{ title: '抓拍图', dataIndex: 'imgUrl', slotName: 'imgUrl' },
{ title: '创建人', dataIndex: 'createUserString', slotName: 'createUser' },
{ title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' },
{ title: '修改人', dataIndex: 'updateUserString', slotName: 'updateUser', show: false },
{ title: '修改时间', dataIndex: 'updateTime', slotName: 'updateTime', show: false },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 220,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['fullWorkOrder:fullWorkOrder:detail', 'fullWorkOrder:fullWorkOrder:update', 'fullWorkOrder:fullWorkOrder:delete'])
}
]);
// 重置
const reset = () => {
queryForm.orderNo = undefined
queryForm.materialCode = undefined
queryForm.materialName = undefined
queryForm.batch = undefined
queryForm.createTime = undefined
search()
}
// 删除
const onDelete = (record: FullWorkOrderResp) => {
return handleDelete(() => deleteFullWorkOrder(record.id), {
content: `是否确定删除该条数据?`,
showModal: true
})
}
// 生成二维码
const generateQRCode = async (data: string) => {
try {
return await QRCode.toDataURL(data, {
width: 120,
margin: 1,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
} catch (error) {
console.error('生成二维码失败:', error)
return ''
}
}
const onPrint = async (record: FullWorkOrderResp) => {
if (!record.batch || record.batch.trim() === '') {
Message.error('该条记录批次号为空,无法打印!')
return
}
if (!record.mark || record.mark.trim() === '') {
Message.error('该条记录标记号为空,无法打印!')
return
}
if (!record.count) {
Message.error('该条记录数量为空,无法打印!')
return
}
try {
// 格式化生产日期为 yyyyMMddHHmm 格式
const now = new Date()
const formattedDate = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0')
const formattedDate2 = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0')
// 计算二维码数据
const qrCodeData = `10#${record.materialCode || ''}$11#9DP$12#${record.batch || ''}$17#${record.count || ''}$20#${formattedDate2}$31#${record.orderNo || ''}$DY`
// 生成二维码图片
const qrCodeImage = await generateQRCode(qrCodeData)
// 直接生成打印标签
const printWindow = window.open('', '_blank');
if (!printWindow) return;
// 构建打印内容
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>标签打印</title>
<style>
@page { size: 80mm 50mm; margin: 0; }
body { margin: 0; padding: 1mm; font-family: Arial, sans-serif; }
.label-container { display: flex; flex-wrap: wrap; gap: 1mm; justify-content: center; align-items: center; transform-origin: center; }
.label { width: 75mm; height: 45mm; border: 1px solid #000; padding: 0; box-sizing: border-box; page-break-inside: avoid; overflow: hidden; margin: 0 auto; }
.label-table { width: 100%; height: 100%; border-collapse: collapse; table-layout: fixed; }
.label-cell { border: 1px solid #000; padding: 0.8mm; vertical-align: top; }
.qr-cell { width: 22mm; text-align: center; vertical-align: middle; border: 1px solid #000; }
.label-row { display: flex; align-items: center; }
.label-field { font-size: 7.5pt; font-weight: bold; margin-right: 1mm; min-width: 22pt; white-space: nowrap; }
.label-value { font-size: 7.5pt; font-weight: bold; flex: 1; overflow-wrap: break-word; word-break: break-word; }
.qr-code img { width: 22mm; height: 22mm; margin: 0.5mm 0; }
.mark-number { font-size: 7pt; font-weight: bold; margin-top: 0.5mm; text-align: center; }
.serial-number { font-size: 7.5pt; font-weight: bold; margin-top: 0.5mm; }
</style>
</head>
<body>
<div class="label-container">
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">${record.materialName || ''}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">${formattedDate}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img src="${qrCodeImage}" alt="QR Code" />
<div class="mark-number">${record.mark || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">${record.materialCode || ''}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">${record.count || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value"></div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value"></div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value"></div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value"></div>
</div>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
// 等待页面加载完成后打印
printWindow.onload = function() {
setTimeout(() => {
printWindow.focus();
printWindow.print();
printWindow.close();
}, 500);
};
} catch (error) {
console.error('打印标签失败:', error);
}
}
// 导出
const onExport = () => {
useDownload(() => exportFullWorkOrder(queryForm))
}
const FullWorkOrderAddModalRef = ref<InstanceType<typeof FullWorkOrderAddModal>>()
const FullWorkOrderDetailModalRef = ref<InstanceType<typeof FullWorkOrderDetailModal>>()
const FullWorkOrderDetailListModalRef = ref<InstanceType<typeof FullWorkOrderDetailListModal>>()
// 新增
const onAdd = () => {
FullWorkOrderAddModalRef.value?.onAdd()
}
// 新增原材料详情
const onAddDetail = (id: string) => {
FullWorkOrderDetailModalRef.value?.onAdd(id)
}
// 查看原材料详情
const onViewDetail = (record: FullWorkOrderResp) => {
FullWorkOrderDetailListModalRef.value?.onOpen(record.id)
}
</script>
<style scoped lang="scss"></style>

View File

@@ -22,6 +22,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import { useTabsStore, useUserStore } from '@/stores'
const formRef = ref<FormInstance>()
@@ -42,14 +43,12 @@ const loading = ref(false)
// 登录
const handleLogin = async () => {
// 检查用户是否已经登录
if (userStore.token) {
return
}
console.log("卡号登录handleLogin")
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
try {
console.log("卡号-校验通过")
loading.value = true
// 调用后端接口校验卡号
await userStore.cardLogin(form)
@@ -71,6 +70,8 @@ const handleLogin = async () => {
Message.success('欢迎使用')
} catch (error) {
// 登录失败处理
console.error('刷卡登录失败:', error)
Message.error( '登录失败,请刷新页面')
} finally {
loading.value = false
}

View File

@@ -0,0 +1,163 @@
<template>
<a-drawer
v-model:visible="visible"
title="批次导入"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<a-alert v-if="!form.disabled" style="margin-bottom: 15px">
请按照模板要求填写数据填写完毕后请先上传并进行解析
<template #action>
<a-link @click="downloadTemplate">
<template #icon><GiSvgIcon name="file-excel" :size="16" /></template>
<template #default>下载模板</template>
</a-link>
</template>
</a-alert>
<fieldset>
<legend>1.解析数据</legend>
<div class="file-box">
<a-upload
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</div>
<div v-if="dataResult.importKey">
<div class="file-box">
<a-space size="large">
<a-statistic title="总计行数" :value="dataResult.totalRows" />
<a-statistic title="正常行数" :value="dataResult.validRows" />
</a-space>
</div>
<div class="file-box">
<a-space size="large">
<a-statistic title="已存在批次" :value="dataResult.duplicateRows" />
</a-space>
</div>
</div>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import {
type BatchImportResp,
downloadBatchImportTemplate,
batchImport,
parseBatchImport,
} from '@/apis/material/materialInfo'
import { useDownload, useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const [form, resetForm] = useResetReactive({
importKey: '',
})
const dataResult = ref<BatchImportResp>({
importKey: '',
totalRows: 0,
validRows: 0,
duplicateRows: 0,
})
const reset = () => {
formRef.value?.resetFields()
dataResult.value.importKey = ''
uploadFile.value = []
resetForm()
}
const downloadTemplate = () => {
useDownload(() => downloadBatchImportTemplate())
}
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await parseBatchImport(formData)
dataResult.value = res.data
Message.success('上传解析成功')
onSuccess(res)
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
},
}
}
const save = async () => {
try {
if (!dataResult.value.importKey) {
Message.warning('请先上传文件,解析导入数据')
return false
}
form.importKey = dataResult.value.importKey
const res = await batchImport(form)
Message.success(`导入成功! 表格总数${res.data.totalRows} 修改${res.data.updateRows}`)
emit('save-success')
return true
} catch (error) {
return false
}
}
const onOpen = () => {
reset()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -19,6 +19,9 @@ import { useWindowSize } from '@vueuse/core'
import { addMaterialInfo, getMaterialInfo, updateMaterialInfo } from '@/apis/material/materialInfo'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { materialType } from "@/hooks/app/materialType";
import {materialProcess} from "@/hooks/app/materialProcess";
import {useDict} from "@/hooks/app";
const emit = defineEmits<{
(e: 'save-success'): void
@@ -32,6 +35,11 @@ const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改物料管理' : '新增物料管理'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { materialTypeList, getMaterialTypeSelect } = materialType()
const { light_level } = useDict('light_level')
const [form, resetForm] = useResetReactive({
// todo 待补充
})
@@ -52,14 +60,63 @@ const columns: ColumnItem[] = reactive([
required: true,
},
{
label: '物料单位重量,单位(g)',
label: '物料单位重量(g)',
field: 'unitWeight',
type: 'input-number',
span: 24,
required: true,
},
{
label: '物料规格',
label: '物料品类',
field: 'materialTypeId',
type: 'select',
span: 24,
required: true,
props: {
options: materialTypeList,
allowClear: true,
allowSearch: true,
},
},
{
label: '物料流程编码',
field: 'materialProcess',
type: 'input',
span: 24,
required: true,
},
{
label: '批次',
field: 'batch',
type: 'input',
span: 24,
},
{
label: '灯光等级',
field: 'lightLevel',
type: 'input-number',
props: {
mode: 'button',
min: 1,
max: 300,
step: 1,
},
span: 24,
},
{
label: '物料直径',
field: 'materialSpec',
type: 'input-number',
props: {
min: 0,
mode: 'button',
step: 0.001,
},
span: 24,
},
{
label: '物料颜色',
field: 'color',
type: 'input',
span: 24,
},
@@ -101,6 +158,9 @@ const save = async () => {
const onAdd = async () => {
reset()
dataId.value = ''
if (!materialTypeList.value.length) {
await getMaterialTypeSelect()
}
visible.value = true
}
@@ -109,6 +169,9 @@ const onUpdate = async (id: string) => {
reset()
dataId.value = id
const { data } = await getMaterialInfo(id)
if (!materialTypeList.value.length) {
await getMaterialTypeSelect()
}
Object.assign(form, data)
visible.value = true
}
@@ -116,4 +179,4 @@ const onUpdate = async (id: string) => {
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss"></style>

View File

@@ -20,7 +20,7 @@
:limit="1"
:show-retry-button="false"
:show-cancel-button="false"
tip="仅支持zip格式"
tip="仅支持zip格式,请确保图片名称和物料编码一致"
:file-list="uploadFile"
accept=".zip"
/>

View File

@@ -10,18 +10,27 @@
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
:selected-keys="selectedKeys"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<!-- toolbar 部分保持不变 -->
<template #toolbar-left>
<a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" />
<a-input-search v-model="queryForm.encoding" placeholder="请输入物料编码" allow-clear @search="search" />
<a-input-search v-model="queryForm.batch" placeholder="请输入批次" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['admin:materialInfo:delete']" type="outline" status="danger" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button v-permission="['admin:materialInfo:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
@@ -36,32 +45,19 @@
</a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onPhotosImport">
<template #icon><icon-upload /></template>
<template #default>照片批量导入</template>
<template #default>照片导入</template>
</a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onBatchImport">
<template #icon><icon-upload /></template>
<template #default>批次导入</template>
</a-button>
</template>
<!-- 修改点照片列插槽 -->
<template #photoUrl="{ record }">
<div class="photo-container">
<!-- 1. 正常显示图片有地址 未标记为错误 -->
<img
v-if="record.photoUrl && !record.photoLoadError"
:src="record.photoUrl"
alt="物料照片"
class="material-photo"
@load="handleImgLoad(record)"
@error="handleImgError(record, $event)"
/>
<!-- 2. 无地址 -->
<span v-else-if="!record.photoUrl" class="no-photo">暂无照片</span>
<!-- 3. 地址存在但加载失败 -->
<span v-else class="photo-error">
<icon-exclamation-circle-fill class="error-icon" />
照片异常
</span>
</div>
<a-image
width="60"
:src="record.photoUrl"
/>
</template>
<template #action="{ record }">
@@ -72,7 +68,7 @@
status="danger"
:disabled="record.disabled"
:title="record.disabled ? '不可删除' : '删除'"
@click="onDelete(record)"
@click="onDeleteOne(record)"
>
删除
</a-link>
@@ -83,38 +79,39 @@
<MaterialInfoAddModal ref="MaterialInfoAddModalRef" @save-success="search" />
<MaterialInfoImportDrawer ref="MaterialInfoImportDrawerRef" @save-success="search" />
<PhotosImport ref="PhotosImportRef" @save-success="search"/>
<BatchImport ref="BatchImportRef" @save-success="search"/>
</div>
</template>
<script setup lang="ts">
// ... 保持原有的 import 不变
import MaterialInfoAddModal from './MaterialInfoAddModal.vue'
import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue'
import PhotosImport from '@/views/material/PhotosImport.vue';
import BatchImport from '@/views/material/BatchImport.vue';
import { type MaterialInfoQuery, type MaterialInfoResp, deleteMaterialInfo, exportMaterialInfo, listMaterialInfo } from '@/apis/material/materialInfo'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import {Message} from "@arco-design/web-vue";
defineOptions({ name: 'MaterialInfo' })
// 扩展类型定义,允许动态添加 photoLoadError 属性
// 如果 TS 报错,可以在 MaterialInfoResp 接口定义中添加 photoLoadError?: boolean;
interface MaterialInfoRespWithStatus extends MaterialInfoResp {
photoLoadError?: boolean;
}
const queryForm = reactive<MaterialInfoQuery>({
materialName: undefined,
encoding: undefined,
sort: ['id,desc'],
batch: undefined,
mark: undefined,
sort: ['mi.id,desc'],
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete,
} = useTable((page) => listMaterialInfo({ ...queryForm, ...page }), { immediate: true })
@@ -122,9 +119,14 @@ const {
const columns = ref<TableInstanceColumns[]>([
{ title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' },
{ title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' },
{ title: '物料单位重量(g)', dataIndex: 'unitWeight', slotName: 'unitWeight' },
{ title: '物料规格', dataIndex: 'materialSpec', slotName: 'materialSpec' },
{ title: '物料照片', dataIndex: 'photoUrl', slotName: 'photoUrl', width: 120, align: 'center' },
{ title: '物料单位重量(g)', dataIndex: 'unitWeight', slotName: 'unitWeight' },
{ title: '物料品类', dataIndex: 'typeName' },
{ title: '批次', dataIndex: 'batch' },
{ title: '物料直径', dataIndex: 'materialSpec', slotName: 'materialSpec', show: false },
{ title: '物料颜色', dataIndex: 'color', slotName: 'color', show: false },
{ title: '物料流程', dataIndex: 'materialProcess', slotName: 'materialProcess', show: false },
{ title: '灯光等级', dataIndex: 'lightLevel', slotName: 'lightLevel', show: false },
{ title: '创建人', dataIndex: 'createUserString', slotName: 'createUser' },
{ title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' },
{
@@ -144,28 +146,28 @@ const reset = () => {
search()
}
const onDelete = (record: MaterialInfoResp) => {
const onDeleteOne = (record: MaterialInfoResp) => {
return handleDelete(() => deleteMaterialInfo(record.id), {
content: `是否确定删除该条数据?`,
showModal: true,
})
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteMaterialInfo(selectedKeys.value), {
content: `是否确定删除选中的 ${selectedKeys.value.length} 条数据?`,
showModal: true
})
}
const onExport = () => {
useDownload(() => exportMaterialInfo(queryForm))
}
// 【修改点】图片加载处理逻辑
const handleImgLoad = (record: MaterialInfoRespWithStatus) => {
record.photoLoadError = false
}
const handleImgError = (record: MaterialInfoRespWithStatus, e: Event) => {
// 标记为加载错误,触发模板渲染 "照片异常"
record.photoLoadError = true
console.warn(`物料照片加载失败: ${record.photoUrl}`)
}
const MaterialInfoAddModalRef = ref<InstanceType<typeof MaterialInfoAddModal>>()
const onAdd = () => {
MaterialInfoAddModalRef.value?.onAdd()
@@ -184,6 +186,12 @@ const PhotosImportRef = ref<InstanceType<typeof PhotosImport>>()
const onPhotosImport = () => {
PhotosImportRef.value?.onOpen()
}
const BatchImportRef = ref<InstanceType<typeof BatchImport>>()
const onBatchImport = () => {
BatchImportRef.value?.onOpen()
}
</script>
<style scoped lang="scss">
@@ -226,4 +234,4 @@ const onPhotosImport = () => {
margin-bottom: 2px;
}
}
</style>
</style>

View File

@@ -0,0 +1,103 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import {
addMaterialProcess,
updateMaterialProcess,
type MaterialProcessResp
} from '@/apis/materialProcess/materialProcess'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改海康物料流程' : '新增海康物料流程'))
const formRef = ref<InstanceType<typeof GiForm>>()
const [form, resetForm] = useResetReactive({
// todo 待补充
})
const columns: ColumnItem[] = reactive([
{
label: '流程名称',
field: 'processName',
type: 'input',
span: 24,
required: true,
},
{
label: '流程编码',
field: 'processCode',
type: 'input',
span: 24,
required: true,
},
])
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateMaterialProcess(form, dataId.value)
Message.success('修改成功')
} else {
await addMaterialProcess(form)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 新增
const onAdd = async () => {
reset()
dataId.value = ''
visible.value = true
}
// 修改
const onUpdate = async (record: MaterialProcessResp) => {
reset()
dataId.value = record.id
Object.assign(form, record)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,185 @@
<template>
<a-drawer
v-model:visible="visible"
title="导入物料流程"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<a-alert v-if="!form.disabled" style="margin-bottom: 15px">
请按照模板要求填写数据填写完毕后请先上传并进行解析
<template #action>
<a-link @click="downloadTemplate">
<template #icon><icon-download /></template>
<template #default>下载模板</template>
</a-link>
</template>
</a-alert>
<fieldset>
<legend>1.解析数据</legend>
<div class="file-box">
<a-upload
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</div>
<div v-if="dataResult.importKey">
<div class="file-box">
<a-space size="large">
<a-statistic title="总计行数" :value="dataResult.totalRows" />
<a-statistic title="正常行数" :value="dataResult.validRows" />
</a-space>
</div>
<div class="file-box">
<a-space size="large">
<a-statistic title="已存在流程名称" :value="dataResult.duplicateNameRows" />
<a-statistic title="已存在流程编码" :value="dataResult.duplicateCodeRows" />
</a-space>
</div>
</div>
</fieldset>
<fieldset>
<legend>2.导入策略</legend>
<a-form-item label="流程名称已存在" field="duplicateName">
<a-radio-group v-model="form.duplicateName" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="2">修改数据</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="流程编码已存在" field="duplicateCode">
<a-radio-group v-model="form.duplicateCode" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="2">修改数据</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { downloadMaterialProcessImportTemplate, importMaterialProcess, parseImportMaterialProcess } from '@/apis/materialProcess/materialProcess'
import { useDownload, useResetReactive } from '@/hooks'
import { IconDownload as iconDownload } from '@arco-design/web-vue/es/icon'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const [form, resetForm] = useResetReactive({
importKey: '',
duplicateName: 1,
duplicateCode: 1,
})
const dataResult = ref({
importKey: '',
totalRows: 0,
validRows: 0,
duplicateNameRows: 0,
duplicateCodeRows: 0,
})
// 重置
const reset = () => {
formRef.value?.resetFields()
dataResult.value.importKey = ''
uploadFile.value = []
resetForm()
}
// 下载模板
const downloadTemplate = () => {
useDownload(() => downloadMaterialProcessImportTemplate())
}
// 上传解析导入数据
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await parseImportMaterialProcess(formData)
dataResult.value = res.data
Message.success('上传解析成功')
onSuccess(res)
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
},
}
}
// 执行导入
const save = async () => {
try {
if (!dataResult.value.importKey) {
Message.warning('请先上传文件,解析导入数据')
return false
}
form.importKey = dataResult.value.importKey
const res = await importMaterialProcess(form)
Message.success(`导入成功! 新增${res.data.insertCount}, 修改${res.data.updateCount},总计处理${res.data.totalHandleCount}`)
emit('save-success')
return true
} catch (error) {
return false
}
}
// 打开
const onOpen = () => {
reset()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<div class="gi_table_page">
<GiTable
title="物料流程管理"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
:selected-keys="selectedKeys"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.processName" placeholder="请输入流程名称" allow-clear @search="search" />
<a-input-search v-model="queryForm.processCode" placeholder="请输入流程编码" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['materialProcess:materialProcess:delete']" type="outline" status="danger" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button v-permission="['materialProcess:materialProcess:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
<a-button v-permission="['materialProcess:materialProcess:import']" @click="onImport">
<template #icon><icon-upload /></template>
<template #default>导入</template>
</a-button>
<a-button v-permission="['materialProcess:materialProcess:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['materialProcess:materialProcess:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['materialProcess:materialProcess:delete']"
status="danger"
:disabled="record.disabled"
:title="record.disabled ? '不可删除' : '删除'"
@click="onDeleteOne(record)"
>
删除
</a-link>
</a-space>
</template>
</GiTable>
<MaterialProcessAddModal ref="MaterialProcessAddModalRef" @save-success="search" />
<MaterialProcessImportDrawer ref="MaterialProcessImportDrawerRef" @save-success="search" />
</div>
</template>
<script setup lang="ts">
import MaterialProcessAddModal from './MaterialProcessAddModal.vue'
import MaterialProcessImportDrawer from './MaterialProcessImportDrawer.vue'
import { type MaterialProcessResp, type MaterialProcessQuery, deleteMaterialProcess, exportMaterialProcess, listMaterialProcess } from '@/apis/materialProcess/materialProcess'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import { IconUpload as iconUpload, IconDownload as iconDownload } from '@arco-design/web-vue/es/icon'
import {Message} from "@arco-design/web-vue";
defineOptions({ name: 'MaterialProcess' })
const queryForm = reactive<MaterialProcessQuery>({
processName: undefined,
processCode: undefined,
sort: ['id,desc']
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
selectAll,
select,
search,
handleDelete
} = useTable((page) => listMaterialProcess({ ...queryForm, ...page }), { immediate: true })
const columns = ref<TableInstanceColumns[]>([
{ title: '流程名称', dataIndex: 'processName', slotName: 'processName' },
{ title: '流程编码', dataIndex: 'processCode', slotName: 'processCode' },
{ title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' },
{ title: '更新时间', dataIndex: 'updateTime', slotName: 'updateTime', show: false },
{ title: '创建人', dataIndex: 'createUserString', slotName: 'createUser' },
{ title: '更新人', dataIndex: 'updateUserString', slotName: 'updateUser', show: false },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['materialProcess:materialProcess:detail', 'materialProcess:materialProcess:update', 'materialProcess:materialProcess:delete'])
}
]);
// 重置
const reset = () => {
queryForm.processName = undefined
queryForm.processCode = undefined
search()
}
// 删除
const onDeleteOne = (record: MaterialProcessResp) => {
return handleDelete(() => deleteMaterialProcess(record.id), {
content: `是否确定删除该条数据?`,
showModal: true
})
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteMaterialProcess(selectedKeys.value), {
content: `是否确定删除选中的 ${selectedKeys.value.length} 条数据?`,
showModal: true
})
}
// 导出
const onExport = () => {
useDownload(() => exportMaterialProcess(queryForm))
}
const MaterialProcessAddModalRef = ref<InstanceType<typeof MaterialProcessAddModal>>()
const MaterialProcessImportDrawerRef = ref<InstanceType<typeof MaterialProcessImportDrawer>>()
// 新增
const onAdd = () => {
MaterialProcessAddModalRef.value?.onAdd()
}
// 修改
const onUpdate = (record: MaterialProcessResp) => {
MaterialProcessAddModalRef.value?.onUpdate(record)
}
// 导入
const onImport = () => {
MaterialProcessImportDrawerRef.value?.onOpen()
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,122 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #floatRatio>
<div style="display: flex; align-items: center; gap: 12px;">
<a-input-number
v-model="form.downFloatRatio"
style="width: 45%"
:min="-100"
:max="100"
:mode="'button'"
placeholder="最小值"
required: true,
/>
<span style="color: #999;"></span>
<a-input-number
v-model="form.upFloatRatio"
style="width: 45%"
:min="0"
:max="100"
:mode="'button'"
placeholder="最大值"
required: true,
/>
</div>
</template>
</GiForm>
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addMaterialType, type MaterialTypeResp, updateMaterialType} from '@/apis/materialType/materialType'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改物料品类' : '新增物料品类'))
const formRef = ref<InstanceType<typeof GiForm>>()
const [form, resetForm] = useResetReactive({
})
const columns: ColumnItem[] = reactive([
{
label: '品类名称',
field: 'typeName',
type: 'input',
span: 24,
required: true,
},
{
label: '浮动范围(%',
field: 'floatRatio',
type: 'custom',
span: 24,
},
])
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateMaterialType(form, dataId.value)
Message.success('修改成功')
} else {
await addMaterialType(form)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 新增
const onAdd = async () => {
reset()
dataId.value = ''
visible.value = true
}
// 修改
const onUpdate = async (data: MaterialTypeResp) => {
reset()
dataId.value = data.id
Object.assign(form, data)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,179 @@
<template>
<a-drawer
v-model:visible="visible"
title="导入物料品类"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<a-alert v-if="!form.disabled" style="margin-bottom: 15px">
请按照模板要求填写数据填写完毕后请先上传并进行解析
<template #action>
<a-link @click="downloadTemplate">
<template #icon><GiSvgIcon name="file-excel" :size="16" /></template>
<template #default>下载模板</template>
</a-link>
</template>
</a-alert>
<fieldset>
<legend>1.解析数据</legend>
<div class="file-box">
<a-upload
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</div>
<div v-if="dataResult.importKey">
<div class="file-box">
<a-space size="large">
<a-statistic title="总计行数" :value="dataResult.totalRows" />
<a-statistic title="正常行数" :value="dataResult.validRows" />
</a-space>
</div>
<div class="file-box">
<a-space size="large">
<a-statistic title="已存在品类名" :value="dataResult.duplicateNameRows" />
</a-space>
</div>
</div>
</fieldset>
<fieldset>
<legend>2.导入策略</legend>
<a-form-item label="品类名已存在" field="duplicateTypeName">
<a-radio-group v-model="form.duplicateTypeName" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="2">修改数据</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import {
type MaterialTypeImportResp,
downloadMaterialTypeImportTemplate,
importMaterialType,
parseImportMaterialType,
} from '@/apis/materialType/materialType'
import { useDownload, useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const [form, resetForm] = useResetReactive({
importKey: '',
duplicateTypeName: 1,
})
const dataResult = ref<MaterialTypeImportResp>({
importKey: '',
totalRows: 0,
validRows: 0,
duplicateNameRows: 0,
})
// 重置
const reset = () => {
formRef.value?.resetFields()
dataResult.value.importKey = ''
uploadFile.value = []
resetForm()
}
// 下载模板
const downloadTemplate = () => {
useDownload(() => downloadMaterialTypeImportTemplate())
}
// 上传解析导入数据
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await parseImportMaterialType(formData)
dataResult.value = res.data
Message.success('上传解析成功')
onSuccess(res)
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
},
}
}
// 执行导入
const save = async () => {
try {
if (!dataResult.value.importKey) {
Message.warning('请先上传文件,解析导入数据')
return false
}
form.importKey = dataResult.value.importKey
const res = await importMaterialType(form)
Message.success(`导入成功! 新增${res.data.insertRows}, 修改${res.data.updateRows},总计处理${res.data.totalRows}`)
emit('save-success')
return true
} catch (error) {
return false
}
}
// 打开
const onOpen = () => {
reset()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div class="gi_table_page">
<GiTable
title="物料品类管理"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
:selected-keys="selectedKeys"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.typeName" placeholder="请输入品类名称" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['materialType:materialType:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
<a-button v-permission="['materialType:materialType:import']" @click="onImport">
<template #icon><icon-upload /></template>
<template #default>导入</template>
</a-button>
<a-button v-permission="['materialType:materialType:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #floatRatio="{ record }">
{{ record.downFloatRatio + '% ~ ' + record.upFloatRatio + '%' }}
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['materialType:materialType:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['materialType:materialType:delete']"
status="danger"
@click="onDelete(record.id)"
>
删除
</a-link>
</a-space>
</template>
</GiTable>
<MaterialTypeAddModal ref="MaterialTypeAddModalRef" @save-success="search" />
<MaterialTypeImportDrawer ref="MaterialTypeImportDrawerRef" @save-success="search" />
</div>
</template>
<script setup lang="ts">
import { h } from 'vue'
import MaterialTypeAddModal from './MaterialTypeAddModal.vue'
import MaterialTypeImportDrawer from './MaterialTypeImportDrawer.vue'
import { type MaterialTypeResp, type MaterialTypeQuery, deleteMaterialType, exportMaterialType, listMaterialType } from '@/apis/materialType/materialType'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'MaterialType' })
const queryForm = reactive<MaterialTypeQuery>({
typeName: undefined,
sort: ['id,desc']
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete
} = useTable((page) => listMaterialType({ ...queryForm, ...page }), { immediate: true })
const columns = ref<TableInstanceColumns[]>([
{ title: '品类名称', dataIndex: 'typeName', slotName: 'typeName' },
{ title: '品类上下浮动范围(%', dataIndex: 'upFloatRatio', slotName: 'floatRatio' },
{ title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' },
{ title: '更新时间', dataIndex: 'updateTime', slotName: 'updateTime', show: false },
{ title: '创建人', dataIndex: 'createUserString', slotName: 'createUser' },
{ title: '更新人', dataIndex: 'updateUserString', slotName: 'updateUser', show: false },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['materialType:materialType:detail', 'materialType:materialType:update', 'materialType:materialType:delete'])
}
]);
// 重置
const reset = () => {
queryForm.typeName = undefined
search()
}
// 删除
const onDelete = (id) => {
return handleDelete(() => deleteMaterialType(id), {
content: `是否确定删除该条数据?`,
showModal: true
})
}
// 导出
const onExport = () => {
useDownload(() => exportMaterialType(queryForm))
}
const MaterialTypeAddModalRef = ref<InstanceType<typeof MaterialTypeAddModal>>()
const MaterialTypeImportDrawerRef = ref<InstanceType<typeof MaterialTypeImportDrawer>>()
// 新增
const onAdd = () => {
MaterialTypeAddModalRef.value?.onAdd()
}
// 修改
const onUpdate = (record: MaterialTypeResp) => {
MaterialTypeAddModalRef.value?.onUpdate(record)
}
// 导入
const onImport = () => {
MaterialTypeImportDrawerRef.value?.onOpen()
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,667 @@
<template>
<div class="label-print-container">
<div class="form-section">
<a-form :model="formData" layout="vertical">
<div class="form-grid">
<div class="form-grid-item">
<a-form-item label="物料名称">
<a-input v-model="formData.materialName" placeholder="未获取到物料名称" :disabled="true" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="物料编码">
<a-input v-model="formData.encoding" placeholder="未获取到物料编码" :disabled="true" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="工单编号">
<a-input v-model="formData.orderNo" placeholder="未获取到工单编号" :disabled="true" />
</a-form-item>
</div>
<div class="form-grid-item">
<a-form-item label="生产批次">
<a-input v-model="formData.batch" placeholder="未获取到生产批次" :disabled="true" />
</a-form-item>
</div>
</div>
</a-form>
<div class="form-actions">
<a-button @click="handlePrevious">继续称重</a-button>
<a-button type="primary" @click="generateDetailLabel">明细标签</a-button>
<a-button type="primary" @click="generateOverallLabel">整体标签</a-button>
</div>
</div>
<div v-if="labelDataList.length > 0 || labelData.partName" class="label-preview-section">
<h3>标签预览</h3>
<div class="label-container" ref="labelContainer">
<template v-if="labelDataList.length > 0">
<div class="label" v-for="(item, index) in labelDataList" :key="index">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">{{ item.partName }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">{{ item.productionDate }}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img v-if="item.qrCodeImage" :src="item.qrCodeImage" alt="QR Code" />
<div class="mark-number">{{ item.mark || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">{{ item.partNumber }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">{{ item.totalCount }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value">{{ item.totalCalculatedWeight }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">{{ item.packingSignature || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value">{{ item.totalWeight || '' }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">{{ item.inspectionSignature || '' }}</div>
</div>
</td>
</tr>
</table>
</div>
</template>
<template v-else>
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">{{ labelData.partName }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">{{ labelData.productionDate }}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img v-if="labelData.qrCodeImage" :src="labelData.qrCodeImage" alt="QR Code" />
<div class="mark-number">{{ labelData.mark || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">{{ labelData.partNumber }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">{{ labelData.totalCount }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value">{{ labelData.totalCalculatedWeight }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">{{ labelData.packingSignature || '' }}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value">{{ labelData.totalWeight || '' }}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">{{ labelData.inspectionSignature || '' }}</div>
</div>
</td>
</tr>
</table>
</div>
</template>
</div>
<div class="label-actions">
<a-button type="primary" @click="printLabel">打印标签</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, onMounted, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getWorkOrder, type WorkOrderInfoResp } from "@/apis/workOrder/workOrder"
import QRCode from 'qrcode';
const emit = defineEmits<{
previous: []
}>()
interface LabelPrintProps {
workerOrderId?: string
}
const props = withDefaults(defineProps<LabelPrintProps>(), {
workerOrderId: ''
})
const formData = reactive({
workerOrderId: '',
encoding: '',
materialName: '',
orderNo: '',
totalCalculatedWeight: '',
totalWeight: '',
totalCount: '',
batch: '',
mark: '',
qrCodeData: '',
workOrderInfos: Array<WorkOrderInfoResp>(),
})
const labelDataList = reactive<Array<{
partName: string
partNumber: string
totalCalculatedWeight: string
totalWeight: string
productionDate: string
totalCount: string
packingSignature: string
inspectionSignature: string
qrCodeData: string
qrCodeImage: string
mark: string
}>>([])
const labelData = reactive({
partName: '',
partNumber: '',
totalCalculatedWeight: '',
totalWeight: '',
productionDate: '',
totalCount: '',
packingSignature: '',
inspectionSignature: '',
qrCodeData: '',
qrCodeImage: '',
mark: '',
})
const labelContainer = ref<HTMLElement | null>(null)
const handlePrevious = () => {
emit('previous')
}
const fetchWorkOrderData = async (workerOrderId: string) => {
if (!workerOrderId) {
return
}
try {
const res = await getWorkOrder(workerOrderId)
if (res.code == '0') {
formData.encoding = res.data.encoding
formData.materialName = res.data.materialName
formData.orderNo = res.data.orderNo
formData.batch = res.data.batch
formData.totalCalculatedWeight = res.data.totalCalculatedWeight
formData.totalWeight = res.data.totalWeight
formData.totalCount = res.data.totalCount
formData.workOrderInfos = res.data.workOrderInfos
formData.mark = res.data.mark
} else {
Message.error('获取工单详情失败')
}
} catch (error) {
console.error('获取工单详情失败:', error)
Message.error('获取工单详情失败')
}
}
const generateQRCode = async (data: string) => {
try {
return await QRCode.toDataURL(data, {
width: 120,
margin: 1,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
} catch (error) {
console.error('生成二维码失败:', error)
return ''
}
}
const generateDetailLabel = async () => {
if (!formData.workOrderInfos || formData.workOrderInfos.length === 0) {
Message.error('未获取到工单明细信息')
return
}
if (!formData.batch) {
Message.error('未获取到批次信息')
return
}
if (!formData.materialName) {
Message.error('未获取到物料信息')
return
}
try {
labelDataList.length = 0
const now = new Date()
const formattedDate = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0')
const formattedDate2 = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0')
for (const workOrderInfo of formData.workOrderInfos) {
const orderNo = formData.orderNo + workOrderInfo.id;
const qrCodeData = `10#${formData.encoding}$11#9DP$12#${formData.batch}$17#${workOrderInfo.quantity}$20#${formattedDate2}$31#${orderNo}$DY`
const qrCodeImage = await generateQRCode(qrCodeData)
console.log("========", workOrderInfo.mark);
labelDataList.push({
partName: formData.materialName || '',
partNumber: formData.encoding || '',
totalCalculatedWeight: workOrderInfo.calculatedWeight || '',
totalWeight: workOrderInfo.weight || '',
productionDate: formattedDate,
totalCount: workOrderInfo.quantity || '',
packingSignature: '',
inspectionSignature: '',
qrCodeData: qrCodeData,
qrCodeImage: qrCodeImage,
mark: workOrderInfo.mark || ''
})
}
Message.success(`成功生成 ${labelDataList.length} 个明细标签`)
} catch (error) {
console.error('生成明细标签失败:', error)
Message.error('生成明细标签失败')
}
}
const generateOverallLabel = async () => {
if (!formData.batch) {
Message.error('未获取到批次信息')
return
}
if (!formData.materialName) {
Message.error('未获取到物料信息')
return
}
try {
labelDataList.length = 0
const now = new Date()
const formattedDate = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0')
const formattedDate2 = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0')
const qrCodeData = `10#${formData.encoding}$11#9DP$12#${formData.batch}$17#${formData.totalCount}$20#${formattedDate2}$31#${formData.orderNo}$DY`
const qrCodeImage = await generateQRCode(qrCodeData)
Object.assign(labelData, {
partName: formData.materialName || '',
partNumber: formData.encoding || '',
totalCalculatedWeight: formData.totalCalculatedWeight || '',
totalWeight: formData.totalWeight || '',
productionDate: formattedDate,
totalCount: formData.totalCount || '',
packingSignature: '',
inspectionSignature: '',
qrCodeData: qrCodeData,
qrCodeImage: qrCodeImage,
mark: formData.mark || ''
})
Message.success('标签生成成功')
} catch (error) {
console.error('生成标签失败:', error)
Message.error('生成标签失败')
}
}
const printLabel = async () => {
await nextTick()
if (labelContainer.value) {
const printWindow = window.open('', '_blank')
if (printWindow) {
const isDetailLabels = labelDataList.length > 0
const labelsToPrint = isDetailLabels ? labelDataList : [labelData]
const labelsHTML = labelsToPrint.map(item => `
<div class="label">
<table class="label-table">
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件名称</div>
<div class="label-value">${item.partName}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">生产日期</div>
<div class="label-value">${item.productionDate}</div>
</div>
</td>
<td class="label-cell qr-cell" rowspan="4">
<div class="qr-code">
<img src="${item.qrCodeImage}" alt="QR Code" />
<div class="mark-number">${item.mark || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">零件号</div>
<div class="label-value">${item.partNumber}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">数量</div>
<div class="label-value">${item.totalCount}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">标重(g)</div>
<div class="label-value">${item.totalCalculatedWeight}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">包装签字</div>
<div class="label-value">${item.packingSignature || ''}</div>
</div>
</td>
</tr>
<tr>
<td class="label-cell">
<div class="label-row">
<div class="label-field">实重(g)</div>
<div class="label-value">${item.totalWeight || ''}</div>
</div>
</td>
<td class="label-cell">
<div class="label-row">
<div class="label-field">检验签字</div>
<div class="label-value">${item.inspectionSignature || ''}</div>
</div>
</td>
</tr>
</table>
</div>
`).join('')
const printHTML = `
<html>
<head>
<title>标签打印</title>
<style>
@page { size: 80mm 50mm; margin: 0; }
body { margin: 0; padding: 1mm; font-family: Arial, sans-serif; }
.label-container { display: flex; flex-wrap: wrap; gap: 1mm; justify-content: center; align-items: center; transform-origin: center; }
.label { width: 75mm; height: 45mm; border: 1px solid #000; padding: 0; box-sizing: border-box; page-break-inside: avoid; overflow: hidden; margin: 0 auto; }
.label-table { width: 100%; height: 100%; border-collapse: collapse; table-layout: fixed; }
.label-cell { border: 1px solid #000; padding: 0.8mm; vertical-align: top; }
.qr-cell { width: 22mm; text-align: center; vertical-align: middle; border: 1px solid #000; }
.label-row { display: flex; align-items: center; }
.label-field { font-size: 7.5pt; font-weight: bold; margin-right: 1mm; min-width: 22pt; white-space: nowrap; }
.label-value { font-size: 7.5pt; font-weight: bold; flex: 1; overflow-wrap: break-word; word-break: break-word; }
.qr-code img { width: 22mm; height: 22mm; margin: 0.5mm 0; }
.mark-number { font-size: 7pt; font-weight: bold; margin-top: 0.5mm; text-align: center; }
.serial-number { font-size: 7.5pt; font-weight: bold; margin-top: 0.5mm; }
</style>
</head>
<body>
<div class="label-container">
${labelsHTML}
</div>
</body>
</html>
`
printWindow.document.write(printHTML)
printWindow.document.close()
printWindow.onload = () => {
setTimeout(() => {
printWindow.focus()
printWindow.print()
printWindow.close()
}, 500)
}
}
}
}
onMounted(() => {
if (props.workerOrderId) {
fetchWorkOrderData(props.workerOrderId)
}
})
watch(() => props.workerOrderId, (newWorkerOrderId) => {
if (newWorkerOrderId) {
fetchWorkOrderData(newWorkerOrderId)
}
})
defineExpose({
generateDetailLabel,
generateOverallLabel
})
defineOptions({ name: 'LabelPrint' })
</script>
<style scoped lang="scss">
.label-print-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-title {
margin-bottom: 30px;
text-align: center;
color: #333;
}
.form-section {
background-color: var(--color-bg-2);
padding: 20px;
border-radius: 4px;
margin-bottom: 30px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.form-grid-item {
width: 100%;
}
.mark-number {
font-size: 10px;
margin-top: 5px;
text-align: center;
font-weight: bold;
}
.form-actions {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 20px;
}
.label-preview-section {
margin-top: 30px;
padding: 20px;
background-color: var(--color-bg-2);
border-radius: 4px;
}
.label-preview-section h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: bold;
text-align: center;
}
.label-container {
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
margin-bottom: 20px;
}
.label {
width: 450px;
height: 180px;
border: 1px solid #000;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
.label-table {
width: 100%;
height: 100%;
border-collapse: collapse;
}
.label-cell {
border: 1px solid #000;
padding: 5px;
vertical-align: top;
}
.qr-cell {
width: 120px;
text-align: center;
vertical-align: middle;
}
.label-row {
display: flex;
align-items: center;
}
.label-field {
font-size: 10pt;
font-weight: bold;
margin-right: 8px;
min-width: 35px;
}
.label-value {
font-size: 10pt;
flex: 1;
font-weight: bold;
word-break: break-word;
}
.qr-code img {
width: 115px;
height: 120px;
margin: 0 0;
}
.loading {
color: #999;
font-size: 12px;
}
.label-actions {
display: flex;
justify-content: center;
gap: 20px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:scroll="{ x: '100%', y: '100%', minWidth: 1800 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@@ -20,6 +20,7 @@
<a-input-search v-model="queryForm.carNo" placeholder="请输入人员卡号" allow-clear @search="search" />
<a-input-search v-model="queryForm.orderNo" placeholder="请输入任务工单号" allow-clear @search="search" />
<a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" />
<a-input-search v-model="queryForm.batch" placeholder="请输入批次" allow-clear @search="search" />
<a-input-search v-model="queryForm.encoding" placeholder="请输入物料编码" allow-clear @search="search" />
<a-date-picker
v-model="queryForm.startDate"
@@ -44,11 +45,11 @@
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['workOrder:record:delete']" type="outline" status="danger" @click="onDelete">
<a-button v-permission="['weighManage:workOrder:delete']" type="outline" status="danger" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button v-permission="['workOrder:record:export']" @click="onExport">
<a-button v-permission="['weighManage:workOrder:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
@@ -63,12 +64,15 @@
<template #totalWeight="{ record }">
{{ record.totalWeight + 'g' }}
</template>
<template #totalCalculatedWeight="{ record }">
{{ record.totalCalculatedWeight + 'g' }}
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['workOrder:record:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['workOrder:record:print']" title="打印" @click="onPrint(record)">打印</a-link>
<a-link v-permission="['workOrder:record:delete']" status="danger" :title="'删除'" @click="onDeleteOne(record.id)">删除</a-link>
<a-link v-permission="['weighManage:workOrder:delete']" status="danger" :title="'删除'" @click="onDeleteOne(record.id)">删除</a-link>
</a-space>
</template>
</GiTable>
@@ -85,7 +89,7 @@
row-key="id"
:data="detailData"
:columns="detailColumns"
:scroll="{ x: '100%', y: '100%', minWidth: 700 }"
:scroll="{ x: '100%', y: '100%', width: 800 }"
>
<template #imgUrl="{ record }">
<a-image :src="record.imgUrl" width="50" />
@@ -134,6 +138,7 @@ const queryForm = reactive<WorkOrderQuery>({
encoding: undefined,
userName: undefined,
carNo: undefined,
batch: undefined,
startDate: undefined,
endDate: undefined,
sort: ['w.id,desc']
@@ -164,15 +169,17 @@ const processColumns = (columns: TableInstanceColumns[]): TableInstanceColumns[]
// 定义列配置,使用工具函数处理
const columns = ref<TableInstanceColumns[]>(processColumns([
{ title: '标题', dataIndex: 'title', width: 180 },
{ title: '创建人', dataIndex: 'createUserString' },
{ title: '人员卡号', dataIndex: 'cardNo' },
{ title: '任务工单号', dataIndex: 'orderNo' },
{ title: '批次', dataIndex: 'batch' },
{ title: '物料图片', dataIndex: 'photoUrl', slotName: 'photoUrl', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '物料名称', dataIndex: 'materialName' },
{ title: '物料编码', dataIndex: 'encoding' },
{ title: '单位克重', dataIndex: 'unitWeight' ,slotName: 'unitWeight'},
{ title: '总重量', dataIndex: 'totalWeight' ,slotName: 'totalWeight'},
{ title: '总数量', dataIndex: 'totalCount' },
{ title: '标准总重量', dataIndex: 'totalCalculatedWeight' ,slotName: 'totalCalculatedWeight'},
{ title: '实际总重量', dataIndex: 'totalWeight' ,slotName: 'totalWeight'},
{ title: '人员卡号', dataIndex: 'cardNo' },
{ title: '创建人', dataIndex: 'createUserString' },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{
title: '操作',
@@ -181,7 +188,7 @@ const columns = ref<TableInstanceColumns[]>(processColumns([
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['workOrder:record:detail', 'workOrder:record:update', 'workOrder:record:delete'])
show: has.hasPermOr(['workOrder:record:detail', 'workOrder:record:update', 'weighManage:workOrder:delete'])
}
]))
@@ -206,9 +213,10 @@ const detailData = ref<any[]>([])
const detailColumns = ref<TableInstanceColumns[]>([
{ title: '称重次数', dataIndex: 'weightTime' },
{ title: '物料名称', dataIndex: 'materialName' },
{ title: '数量', dataIndex: 'quantity' },
{ title: '重量', dataIndex: 'weight', slotName: 'weight' },
{ title: '计算重量', dataIndex: 'calculatedWeight', slotName: 'calculatedWeight' },
{ title: '输入数量', dataIndex: 'quantity' },
{ title: '标准重量(g)', dataIndex: 'calculatedWeight', slotName: 'calculatedWeight' },
{ title: '称重数量', dataIndex: 'weightQuantity' },
{ title: '称重重量(g)', dataIndex: 'weight', slotName: 'weight' },
{ title: '抓拍图片', dataIndex: 'imgUrl', slotName: 'imgUrl' }
])