Compare commits

...

82 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
df67f63137 优化 2026-03-13 09:41:00 +08:00
zc
0f7efec0e1 优化 2026-03-12 17:18:56 +08:00
zc
2171c83d55 优化 2026-03-12 16:30:30 +08:00
zc
db40b1733b 优化 2026-03-12 16:12:30 +08:00
zc
fe56422336 优化 2026-03-12 15:41:52 +08:00
zc
dc395406c3 优化 2026-03-11 18:38:45 +08:00
zc
41db731e40 优化视频样式 2026-03-11 17:40:24 +08:00
zc
35fcd40bd3 优化视频流 2026-03-11 11:09:19 +08:00
zc
e27e44a3c3 优化 2026-03-11 10:56:33 +08:00
zc
47463c9b14 优化 2026-03-09 20:35:02 +08:00
zc
3f8b6e4695 优化 2026-03-09 20:34:20 +08:00
2bf7b6872a 抓取优化 2026-03-09 16:22:17 +08:00
zc
a3cb2263fd 优化 2026-03-09 16:06:00 +08:00
zc
d8273464c5 优化 2026-03-09 15:24:02 +08:00
zc
630a98af65 优化 2026-03-06 18:25:24 +08:00
zc
31178da436 Merge remote-tracking branch 'refs/remotes/origin/master' into dev
# Conflicts:
#	src/views/weightManage/index.vue
2026-03-06 18:15:58 +08:00
zc
323a641fa3 Merge branch 'refs/heads/master' into dev
# Conflicts:
#	src/views/system/user/index.vue
2026-03-06 18:06:34 +08:00
zc
c858ace541 优化 2026-03-06 18:04:40 +08:00
f35d64bd91 称重抓取 2026-03-06 18:03:28 +08:00
zc
006130b4b7 优化 2026-03-06 16:29:31 +08:00
zc
b4ebebe2a8 优化 2026-03-06 15:54:36 +08:00
zc
60933ee1a6 优化 2026-03-06 14:17:19 +08:00
zc
d014699b95 优化 2026-03-05 18:14:25 +08:00
15ce1cfa13 Merge branch 'refs/heads/dev' into master_lz
# Conflicts:
#	src/views/system/user/index.vue
2026-03-05 16:35:28 +08:00
zc
0c40b20df7 优化追溯 2026-03-04 18:04:32 +08:00
zc
1e64e776ce 优化追溯 2026-03-04 16:46:52 +08:00
zc
31b7d3237a Merge branch 'refs/heads/master' into dev 2026-03-04 10:34:25 +08:00
zc
85dd8102a3 优化称重页面 2026-03-03 17:59:37 +08:00
45 changed files with 6018 additions and 367 deletions

View File

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

View File

@@ -1,22 +1,21 @@
# 环境变量 (命名必须以 VITE_ 开头) # 环境变量 (命名必须以 VITE_ 开头)
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = true
# 接口前缀 # 接口前缀
VITE_API_PREFIX = '/test-api' VITE_API_PREFIX = '/dev-api'
# 接口地址 # 接口地址
VITE_API_BASE_URL = 'http://localhost:6609' 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 # 终端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a' VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

309
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.4",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"flv.js": "^1.6.2",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lint-staged": "^15.2.10", "lint-staged": "^15.2.10",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -36,6 +37,7 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.16", "pinia": "^2.0.16",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"qrcode": "^1.5.4",
"qs": "^6.11.2", "qs": "^6.11.2",
"query-string": "^9.0.0", "query-string": "^9.0.0",
"v-viewer": "^3.0.10", "v-viewer": "^3.0.10",
@@ -60,6 +62,7 @@
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
"@types/qrcode": "^1.5.6",
"@types/query-string": "^6.3.0", "@types/query-string": "^6.3.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -765,7 +768,6 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3272,6 +3274,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/query-string": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmmirror.com/@types/query-string/-/query-string-6.3.0.tgz", "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": { "node_modules/decode-uri-component": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz", "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" "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": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
@@ -6560,6 +6587,12 @@
"es6-symbol": "^3.1.1" "es6-symbol": "^3.1.1"
} }
}, },
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/es6-symbol": { "node_modules/es6-symbol": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz", "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
@@ -8254,6 +8287,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.9", "version": "1.15.9",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -8431,7 +8474,6 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
@@ -11220,7 +11262,6 @@
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -11397,7 +11438,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -11539,6 +11579,15 @@
"node": ">=4" "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": { "node_modules/posix-character-classes": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmmirror.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "resolved": "https://registry.npmmirror.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -11925,6 +11974,233 @@
"node": ">=6" "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": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
@@ -12319,7 +12595,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -12336,6 +12611,12 @@
"node": ">=0.10.0" "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": { "node_modules/resize-detector": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmmirror.com/resize-detector/-/resize-detector-0.3.0.tgz", "resolved": "https://registry.npmmirror.com/resize-detector/-/resize-detector-0.3.0.tgz",
@@ -12859,6 +13140,12 @@
"randombytes": "^2.1.0" "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": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -16043,6 +16330,12 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz",
"integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
@@ -16132,6 +16425,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/which-typed-array": {
"version": "1.1.19", "version": "1.1.19",
"resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",

View File

@@ -34,6 +34,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.4",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"flv.js": "^1.6.2",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lint-staged": "^15.2.10", "lint-staged": "^15.2.10",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -42,6 +43,7 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.16", "pinia": "^2.0.16",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"qrcode": "^1.5.4",
"qs": "^6.11.2", "qs": "^6.11.2",
"query-string": "^9.0.0", "query-string": "^9.0.0",
"v-viewer": "^3.0.10", "v-viewer": "^3.0.10",
@@ -66,6 +68,7 @@
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
"@types/qrcode": "^1.5.6",
"@types/query-string": "^6.3.0", "@types/query-string": "^6.3.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",

View File

@@ -25,6 +25,11 @@ export function socialLogin(req: any) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req) return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
} }
/** @desc 三方账号登录 */
export function cardLogin(req: any) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
}
/** @desc 三方账号登录授权 */ /** @desc 三方账号登录授权 */
export function socialAuth(source: string) { export function socialAuth(source: string) {
return http.get<T.SocialAuthAuthorizeResp>(`${BASE_URL}/${source}`) return http.get<T.SocialAuthAuthorizeResp>(`${BASE_URL}/${source}`)

View File

@@ -42,13 +42,14 @@ export interface RouteItem {
} }
/** 认证类型 */ /** 认证类型 */
export type AuthType = 'ACCOUNT' | 'PHONE' | 'EMAIL' | 'SOCIAL' export type AuthType = 'ACCOUNT' | 'PHONE' | 'EMAIL' | 'SOCIAL' | 'CARD'
export const AuthTypeConstants = { export const AuthTypeConstants = {
ACCOUNT: 'ACCOUNT', ACCOUNT: 'ACCOUNT',
PHONE: 'PHONE', PHONE: 'PHONE',
EMAIL: 'EMAIL', EMAIL: 'EMAIL',
SOCIAL: 'SOCIAL', SOCIAL: 'SOCIAL',
CARD: 'CARD',
} as const } as const
/** 基础认证请求接口 */ /** 基础认证请求接口 */
@@ -77,6 +78,11 @@ export interface EmailLoginReq extends AuthReq {
captcha: string captcha: string
} }
/** 刷卡登录请求参数 */
export interface CardLoginReq extends AuthReq {
cardNumber: string
}
/** 登录响应类型 */ /** 登录响应类型 */
export interface LoginResp { export interface LoginResp {
token: string 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' const BASE_URL = '/admin/materialInfo'
export interface MaterialInfoResp { export interface MaterialInfoResp {
id: string
materialName: string materialName: string
encoding: string encoding: string
encodingPrint: string
unitWeight: string unitWeight: string
materialSpec: string materialSpec: string
photoUrl: string photoUrl: string
@@ -12,11 +14,18 @@ export interface MaterialInfoResp {
createTime: string createTime: string
createUserString: string createUserString: string
updateUserString: string updateUserString: string
lightLevel: number
batch: string
mark: string
disabled: boolean disabled: boolean
photoLoadError: boolean
} }
export interface MaterialInfoQuery { export interface MaterialInfoQuery {
materialName: string | undefined materialName: string | undefined
encoding: string | undefined encoding: string | undefined
encodingPrint: string | undefined
batch: string | undefined
mark: string | undefined
sort: Array<string> sort: Array<string>
} }
@@ -35,9 +44,6 @@ export function listMaterialInfo(query: MaterialInfoPageQuery) {
return http.get<PageRes<MaterialInfoResp[]>>(`${BASE_URL}`, query) return http.get<PageRes<MaterialInfoResp[]>>(`${BASE_URL}`, query)
} }
interface MaterialInfoDetailResp {
}
/** @desc 下载物料信息导入模板 */ /** @desc 下载物料信息导入模板 */
export function downloadMaterialInfoImportTemplate() { export function downloadMaterialInfoImportTemplate() {
return http.download(`${BASE_URL}/import/template`) return http.download(`${BASE_URL}/import/template`)
@@ -45,7 +51,7 @@ export function downloadMaterialInfoImportTemplate() {
/** @desc 查询物料信息详情 */ /** @desc 查询物料信息详情 */
export function getMaterialInfo(id: string) { export function getMaterialInfo(id: string) {
return http.get<MaterialInfoDetailResp>(`${BASE_URL}/${id}`) return http.get<MaterialInfoResp>(`${BASE_URL}/${id}`)
} }
/** @desc 新增物料信息 */ /** @desc 新增物料信息 */
@@ -59,7 +65,7 @@ export function updateMaterialInfo(data: any, id: string) {
} }
/** @desc 删除物料信息 */ /** @desc 删除物料信息 */
export function deleteMaterialInfo(id: string) { export function deleteMaterialInfo(id: string | Array<string>) {
return http.del(`${BASE_URL}/${id}`) return http.del(`${BASE_URL}/${id}`)
} }
@@ -78,7 +84,35 @@ export function importMaterial(data: any) {
return http.post(`${BASE_URL}/import`, data) return http.post(`${BASE_URL}/import`, data)
} }
/** @desc 解析物料信息导入数据 */ /** @desc 物料照片批量导入数据 */
export function uploadMaterialPhotos(data: FormData) { export function uploadMaterialPhotos(data: FormData) {
return http.post(`${BASE_URL}/import/uploadMaterialPhotos`, data) return http.post(`${BASE_URL}/import/uploadMaterialPhotos`, data)
} }
/** @desc 物料照片抓取数据 */
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

@@ -1,27 +1,58 @@
import http from '@/utils/http' import http from '@/utils/http'
const BASE_URL = '/weighManage/material' const BASE_URL = '/weighManage/workOrder'
export interface WeighManageResp { export interface WeighManageResp {
id: string id: string
materialCode: string encoding: string
encodingPrint: string
materialName: string materialName: string
materialSpec: string materialSpec: string
weight: number unitWeight: number
imageUrl: string photoUrl: string
batch: string
mark: string
materialProcess: string
matchResult: string matchResult: string
downFloatRatio: string
upFloatRatio: string
} }
export interface WeighManageQuery { export interface WeighManageQuery {
materialCode: string encoding: string
} }
/** @desc 查询物料信息 */ /** @desc 查询物料信息 */
export function getMaterialDetail(query: WeighManageQuery) { export function getMaterialDetail(code: string) {
return http.get<WeighManageResp>(`${BASE_URL}/detail`, query) return http.get<WeighManageResp>(`/admin/materialInfo/code/${code}`)
} }
/** @desc 新增人员管理 */ /** @desc 校验称重信息 */
export function addPeople(data: any) { export function validateWeighing(data: any) {
return http.post(`${BASE_URL}`, data) return http.post(`${BASE_URL}/validateWeighing`, data)
}
/** @desc 校验物料是否一致 */
export function vmSend(materialCode: string) {
return http.post<string>(`/vm/send`, { materialCode })
}
/** @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

@@ -0,0 +1,86 @@
import http from '@/utils/http'
const BASE_URL = '/weighManage/workOrder'
export interface WorkOrderResp {
id: string
title: string
orderNo: string
materialName: string
encoding: string
unitWeight: string
materialSpec: string
photoUrl: string
batch: string
totalWeight: string
totalCalculatedWeight: string
totalCount: string
createUserString: string
updateUserString: string
matchResult: string
workOrderInfos: Array<WorkOrderInfoResp>
qrCodeData: string
mark: string
}
export interface WorkOrderInfoResp {
id: string
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
startDate: string | undefined
endDate: string | undefined
sort: Array<string>
}
export interface WorkOrderPageQuery extends WorkOrderQuery, PageQuery {}
/** @desc 查询工作订单列表 */
export function listWorkOrder(query: WorkOrderPageQuery) {
return http.get<PageRes<WorkOrderResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询工作订单详情 */
export function getWorkOrderInfos(id: string) {
return http.get<Array<WorkOrderInfoResp>>(`${BASE_URL}/info/${id}`)
}
/** @desc 查询工作订单详情 */
export function getWorkOrder(id: string) {
return http.get<WorkOrderResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增工作订单 */
export function addWorkOrder(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 删除工作订单 */
export function deleteWorkOrder(ids: string | Array<string>) {
return http.del(`${BASE_URL}/${ids}`)
}
/** @desc 导出工作订单 */
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,7 +38,7 @@ export const FileIcon: FileExtendNameIconMap = {
} }
/** 图片类型 */ /** 图片类型 */
export const ImageTypes = ['jpg', 'png', 'gif', 'jpeg'] export const ImageTypes = ['jpg', 'png', 'gif', 'jpeg', 'bmp']
/** WPS、Office文件类型 */ /** WPS、Office文件类型 */
export const OfficeTypes = ['ppt', 'pptx', 'doc', 'docx', 'xls', 'xlsx', 'pdf'] export const OfficeTypes = ['ppt', 'pptx', 'doc', 'docx', 'xls', 'xlsx', 'pdf']

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

@@ -4,10 +4,12 @@ import { resetRouter } from '@/router'
import { import {
type AccountLoginReq, type AccountLoginReq,
AuthTypeConstants, AuthTypeConstants,
type CardLoginReq,
type EmailLoginReq, type EmailLoginReq,
type PhoneLoginReq, type PhoneLoginReq,
type UserInfo, type UserInfo,
accountLogin as accountLoginApi, accountLogin as accountLoginApi,
cardLogin as cardLoginApi,
emailLogin as emailLoginApi, emailLogin as emailLoginApi,
getUserInfo as getUserInfoApi, getUserInfo as getUserInfoApi,
logout as logoutApi, logout as logoutApi,
@@ -70,6 +72,13 @@ const storeSetup = () => {
token.value = res.data.token token.value = res.data.token
} }
// 刷卡登录
const cardLogin = async (req: CardLoginReq) => {
const res = await cardLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.CARD })
setToken(res.data.token)
token.value = res.data.token
}
// 三方账号登录 // 三方账号登录
const socialLogin = async (source: string, req: any) => { const socialLogin = async (source: string, req: any) => {
const res = await socialLoginApi({ ...req, source, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.SOCIAL }) const res = await socialLoginApi({ ...req, source, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.SOCIAL })
@@ -120,6 +129,7 @@ const storeSetup = () => {
accountLogin, accountLogin,
emailLogin, emailLogin,
phoneLogin, phoneLogin,
cardLogin,
socialLogin, socialLogin,
logout, logout,
logoutCallBack, logoutCallBack,

View File

@@ -13,11 +13,17 @@ declare module 'vue' {
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button'] AButton: typeof import('@arco-design/web-vue')['Button']
AButtonGroup: typeof import('@arco-design/web-vue')['ButtonGroup']
ACard: typeof import('@arco-design/web-vue')['Card'] 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'] ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup'] ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
ACol: typeof import('@arco-design/web-vue')['Col'] ACol: typeof import('@arco-design/web-vue')['Col']
AColorPicker: typeof import('@arco-design/web-vue')['ColorPicker']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADescriptions: typeof import('@arco-design/web-vue')['Descriptions'] ADescriptions: typeof import('@arco-design/web-vue')['Descriptions']
ADescriptionsItem: typeof import('@arco-design/web-vue')['DescriptionsItem'] ADescriptionsItem: typeof import('@arco-design/web-vue')['DescriptionsItem']
ADivider: typeof import('@arco-design/web-vue')['Divider'] ADivider: typeof import('@arco-design/web-vue')['Divider']
@@ -29,9 +35,9 @@ declare module 'vue' {
AFormItem: typeof import('@arco-design/web-vue')['FormItem'] AFormItem: typeof import('@arco-design/web-vue')['FormItem']
AGrid: typeof import('@arco-design/web-vue')['Grid'] AGrid: typeof import('@arco-design/web-vue')['Grid']
AGridItem: typeof import('@arco-design/web-vue')['GridItem'] AGridItem: typeof import('@arco-design/web-vue')['GridItem']
AIcon: typeof import('@arco-design/web-vue')['Icon']
AImage: typeof import('@arco-design/web-vue')['Image'] AImage: typeof import('@arco-design/web-vue')['Image']
AInput: typeof import('@arco-design/web-vue')['Input'] AInput: typeof import('@arco-design/web-vue')['Input']
AInputGroup: typeof import('@arco-design/web-vue')['InputGroup']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'] AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword'] AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
@@ -55,11 +61,15 @@ declare module 'vue' {
ARow: typeof import('@arco-design/web-vue')['Row'] ARow: typeof import('@arco-design/web-vue')['Row']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar'] AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASelect: typeof import('@arco-design/web-vue')['Select'] 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'] ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
AStatistic: typeof import('@arco-design/web-vue')['Statistic'] AStatistic: typeof import('@arco-design/web-vue')['Statistic']
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu'] ASubMenu: typeof import('@arco-design/web-vue')['SubMenu']
ASwitch: typeof import('@arco-design/web-vue')['Switch'] ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATable: typeof import('@arco-design/web-vue')['Table'] ATable: typeof import('@arco-design/web-vue')['Table']
ATableColumn: typeof import('@arco-design/web-vue')['TableColumn']
ATabPane: typeof import('@arco-design/web-vue')['TabPane'] ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs'] ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag'] ATag: typeof import('@arco-design/web-vue')['Tag']
@@ -69,6 +79,7 @@ declare module 'vue' {
ATreeSelect: typeof import('@arco-design/web-vue')['TreeSelect'] ATreeSelect: typeof import('@arco-design/web-vue')['TreeSelect']
ATrigger: typeof import('@arco-design/web-vue')['Trigger'] ATrigger: typeof import('@arco-design/web-vue')['Trigger']
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph'] ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph']
ATypographyText: typeof import('@arco-design/web-vue')['TypographyText']
ATypographyTitle: typeof import('@arco-design/web-vue')['TypographyTitle'] ATypographyTitle: typeof import('@arco-design/web-vue')['TypographyTitle']
AUpload: typeof import('@arco-design/web-vue')['Upload'] AUpload: typeof import('@arco-design/web-vue')['Upload']
Avatar: typeof import('./../components/Avatar/index.vue')['default'] Avatar: typeof import('./../components/Avatar/index.vue')['default']

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

@@ -0,0 +1,776 @@
<template>
<div class="gi_page">
<div class="container">
<h2 class="page-title">标签打印</h2>
<!-- 标签参数设置 -->
<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 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>
</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="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>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
import {getWorkOrder, savePrintRecord, type WorkOrderInfoResp} from "@/apis/workOrder/workOrder"
import QRCode from 'qrcode';
import {getMaterialDetail} from "@/apis/weightManage/weightManage";
const route = useRoute()
// 表单数据
const formData = reactive({
workerOrderId: '',
encoding: '',
encodingPrint: '',
materialNamePrint: '',
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 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) {
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')
// 为每个工单明细生成一个标签
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.materialNamePrint || formData.materialName || '',
partNumber: formData.encodingPrint || 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('生成标签失败')
}
}
// 组件挂载时,检查是否有参数传递
onMounted(() => {
// 从路由参数中获取数据
const workerOrderId = route.query.workerOrderId as string
// 如果有参数传递,设置表单数据并标记为不可修改
if (workerOrderId) {
formData.workerOrderId = workerOrderId
getWorkOrder(workerOrderId).then(res => {
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('获取详情失败')
}
});
}
})
// 打印标签
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]
// 生成标签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>
@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>
<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>
`
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(() => {
printWindow.focus()
printWindow.print()
printWindow.close()
}, 500)
}
}
}
}
defineOptions({ name: 'print' })
</script>
<style scoped lang="scss">
.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%;
}
.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: 12px;
font-weight: bold;
margin-right: 10px;
min-width: 60px;
}
.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;
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 {
font-size: 12px;
margin-top: 5px;
}
.label-actions {
display: flex;
justify-content: center;
margin-top: 20px;
}
/* 确保输入框宽度一致 */
:deep(.arco-input-wrapper) {
width: 100%;
height: 35px;
}
:deep(.arco-input) {
font-size: 16px;
}
</style>

View File

@@ -32,13 +32,11 @@
</div> </div>
</a-card> </a-card>
<NoticeDetailModal ref="NoticeDetailModalRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { type DashboardNoticeResp, listDashboardNotice } from '@/apis' import { type DashboardNoticeResp, listDashboardNotice } from '@/apis'
import { useDict } from '@/hooks/app' import { useDict } from '@/hooks/app'
import NoticeDetailModal from '@/views/system/notice/NoticeDetailModal.vue'
const router = useRouter() const router = useRouter()
const { notice_type } = useDict('notice_type') 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(() => { onMounted(() => {
getDataList() 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

@@ -0,0 +1,137 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="cardNumber" hide-label>
<a-input v-model="form.cardNumber" placeholder="请刷卡" allow-clear @keyup.enter="handleLogin" />
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<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>()
const form = reactive({
cardNumber: '',
})
const rules: FormInstance['rules'] = {
cardNumber: [
{ required: true, message: '请刷卡或输入卡号' },
],
}
const userStore = useUserStore()
const tabsStore = useTabsStore()
const router = useRouter()
const loading = ref(false)
// 登录
const handleLogin = async () => {
console.log("卡号登录handleLogin")
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
try {
console.log("卡号-校验通过")
loading.value = true
// 调用后端接口校验卡号
await userStore.cardLogin(form)
// 登录成功后移除事件监听器
if (keyDownListener) {
document.removeEventListener('keydown', keyDownListener)
keyDownListener = null
}
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
await router.push({
path: (redirect as string) || '/',
query: {
...othersQuery,
},
})
Message.success('欢迎使用')
} catch (error) {
// 登录失败处理
console.error('刷卡登录失败:', error)
Message.error( '登录失败,请刷新页面')
} finally {
loading.value = false
}
}
// 键盘事件监听器引用
let keyDownListener: ((e: KeyboardEvent) => void) | null = null
// 监听键盘输入,实现自动识别卡号
onMounted(() => {
// 只监听Enter键用于自动登录
keyDownListener = (e) => {
// 刷卡器通常会快速输入卡号并以Enter键结束
if (e.key === 'Enter') {
// 遇到Enter键尝试登录
if (form.cardNumber.trim()) {
handleLogin()
}
}
}
document.addEventListener('keydown', keyDownListener)
})
// 组件卸载时移除事件监听器
onUnmounted(() => {
if (keyDownListener) {
document.removeEventListener('keydown', keyDownListener)
}
})
</script>
<style scoped lang="scss">
.arco-input-wrapper,
:deep(.arco-select-view-single) {
height: 40px;
border-radius: 4px;
font-size: 13px;
}
.arco-input-wrapper.arco-input-error {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-3));
}
.arco-input-wrapper.arco-input-error:hover {
background-color: rgb(var(--danger-1));
border-color: rgb(var(--danger-6));
}
.arco-input-wrapper :deep(.arco-input) {
font-size: 13px;
color: var(--color-text-1);
}
.arco-input-wrapper:hover {
border-color: rgb(var(--arcoblue-6));
}
.btn {
height: 40px;
}
</style>

View File

@@ -15,28 +15,14 @@
<a-col :xs="24" :sm="12" :md="11"> <a-col :xs="24" :sm="12" :md="11">
<div class="login-right"> <div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3> <h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form"> <a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录"> <a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" /> <component :is="AccountLogin" v-if="activeTab === '1'" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" title="手机号登录"> <a-tab-pane key="2" title="刷卡">
<component :is="PhoneLogin" v-if="activeTab === '2'" /> <component :is="CardLogin" v-if="activeTab === '2'" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<!-- <div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>-->
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@@ -51,51 +37,13 @@
<Background /> <Background />
</div> </div>
<div v-else class="login h5">
<div class="login-logo">
<img v-if="logo" :src="logo" alt="logo" />
<img v-else src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div>
<a-row align="stretch" class="login-box">
<a-col :xs="24" :sm="12" :md="11">
<div class="login-right">
<h3 v-if="isEmailLogin" class="login-right__title">邮箱登录</h3>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else v-model:activeKey="activeTab" class="login-right__form">
<a-tab-pane key="1" title="账号登录">
<component :is="AccountLogin" v-if="activeTab === '1'" />
</a-tab-pane>
<a-tab-pane key="2" title="手机号登录">
<component :is="PhoneLogin" v-if="activeTab === '2'" />
</a-tab-pane>
</a-tabs>
</div>
</a-col>
</a-row>
<div class="login-right__oauth">
<a-divider orientation="center">其他登录方式</a-divider>
<div class="list">
<div v-if="isEmailLogin" class="mode item" @click="toggleLoginMode"><icon-user /> 账号/手机号登录</div>
<div v-else class="mode item" @click="toggleLoginMode"><icon-email /> 邮箱登录</div>
<a class="item" title="使用 Gitee 账号登录" @click="onOauth('gitee')">
<GiSvgIcon name="gitee" :size="24" />
</a>
<a class="item" title="使用 GitHub 账号登录" @click="onOauth('github')">
<GiSvgIcon name="github" :size="24" />
</a>
</div>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import Background from './components/background/index.vue' import Background from './components/background/index.vue'
import AccountLogin from './components/account/index.vue' import AccountLogin from './components/account/index.vue'
import PhoneLogin from './components/phone/index.vue' import CardLogin from './components/card/index.vue'
import EmailLogin from './components/email/index.vue'
import { socialAuth } from '@/apis/auth'
import { useAppStore } from '@/stores' import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks' import { useDevice } from '@/hooks'
@@ -109,16 +57,6 @@ const logo = computed(() => appStore.getLogo())
const isEmailLogin = ref(false) const isEmailLogin = ref(false)
const activeTab = ref('1') const activeTab = ref('1')
// 切换登录模式
const toggleLoginMode = () => {
isEmailLogin.value = !isEmailLogin.value
}
// 第三方登录授权
const onOauth = async (source: string) => {
const { data } = await socialAuth(source)
window.location.href = data.authorizeUrl
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

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 { addMaterialInfo, getMaterialInfo, updateMaterialInfo } from '@/apis/material/materialInfo'
import { type ColumnItem, GiForm } from '@/components/GiForm' import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks' import { useResetReactive } from '@/hooks'
import { materialType } from "@/hooks/app/materialType";
import {materialProcess} from "@/hooks/app/materialProcess";
import {useDict} from "@/hooks/app";
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'save-success'): void (e: 'save-success'): void
@@ -32,6 +35,11 @@ const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改物料管理' : '新增物料管理')) const title = computed(() => (isUpdate.value ? '修改物料管理' : '新增物料管理'))
const formRef = ref<InstanceType<typeof GiForm>>() const formRef = ref<InstanceType<typeof GiForm>>()
const { materialTypeList, getMaterialTypeSelect } = materialType()
const { light_level } = useDict('light_level')
const [form, resetForm] = useResetReactive({ const [form, resetForm] = useResetReactive({
// todo 待补充 // todo 待补充
}) })
@@ -52,14 +60,63 @@ const columns: ColumnItem[] = reactive([
required: true, required: true,
}, },
{ {
label: '物料单位重量,单位(g)', label: '物料单位重量(g)',
field: 'unitWeight', field: 'unitWeight',
type: 'input-number', type: 'input-number',
span: 24, 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', field: 'materialSpec',
type: 'input-number',
props: {
min: 0,
mode: 'button',
step: 0.001,
},
span: 24,
},
{
label: '物料颜色',
field: 'color',
type: 'input', type: 'input',
span: 24, span: 24,
}, },
@@ -101,6 +158,9 @@ const save = async () => {
const onAdd = async () => { const onAdd = async () => {
reset() reset()
dataId.value = '' dataId.value = ''
if (!materialTypeList.value.length) {
await getMaterialTypeSelect()
}
visible.value = true visible.value = true
} }
@@ -109,6 +169,9 @@ const onUpdate = async (id: string) => {
reset() reset()
dataId.value = id dataId.value = id
const { data } = await getMaterialInfo(id) const { data } = await getMaterialInfo(id)
if (!materialTypeList.value.length) {
await getMaterialTypeSelect()
}
Object.assign(form, data) Object.assign(form, data)
visible.value = true visible.value = true
} }

View File

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

View File

@@ -10,17 +10,27 @@
:pagination="pagination" :pagination="pagination"
:disabled-tools="['size']" :disabled-tools="['size']"
:disabled-column-keys="['name']" :disabled-column-keys="['name']"
:selected-keys="selectedKeys"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select-all="selectAll"
@select="select"
@refresh="search" @refresh="search"
> >
<!-- toolbar 部分保持不变 -->
<template #toolbar-left> <template #toolbar-left>
<a-input-search v-model="queryForm.materialName" placeholder="请输入物料名称" allow-clear @search="search" /> <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.encoding" placeholder="请输入物料编码" allow-clear @search="search" />
<a-input-search v-model="queryForm.batch" placeholder="请输入批次" allow-clear @search="search" />
<a-button @click="reset"> <a-button @click="reset">
<template #icon><icon-refresh /></template> <template #icon><icon-refresh /></template>
<template #default>重置</template> <template #default>重置</template>
</a-button> </a-button>
</template> </template>
<template #toolbar-right> <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"> <a-button v-permission="['admin:materialInfo:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template> <template #icon><icon-plus /></template>
<template #default>新增</template> <template #default>新增</template>
@@ -35,21 +45,21 @@
</a-button> </a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onPhotosImport"> <a-button v-permission="['admin:materialInfo:import']" @click="onPhotosImport">
<template #icon><icon-upload /></template> <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> </a-button>
</template> </template>
<template #photoUrl="{ record }"> <template #photoUrl="{ record }">
<div class="photo-container"> <a-image
<img width="60"
v-if="record.photoUrl"
:src="record.photoUrl" :src="record.photoUrl"
alt="物料照片"
class="material-photo"
@error="handleImgError($event)"
/> />
<span v-else class="no-photo">暂无照片</span>
</div>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
<a-link v-permission="['admin:materialInfo:update']" title="修改" @click="onUpdate(record)">修改</a-link> <a-link v-permission="['admin:materialInfo:update']" title="修改" @click="onUpdate(record)">修改</a-link>
@@ -58,7 +68,7 @@
status="danger" status="danger"
:disabled="record.disabled" :disabled="record.disabled"
:title="record.disabled ? '不可删除' : '删除'" :title="record.disabled ? '不可删除' : '删除'"
@click="onDelete(record)" @click="onDeleteOne(record)"
> >
删除 删除
</a-link> </a-link>
@@ -69,6 +79,7 @@
<MaterialInfoAddModal ref="MaterialInfoAddModalRef" @save-success="search" /> <MaterialInfoAddModal ref="MaterialInfoAddModalRef" @save-success="search" />
<MaterialInfoImportDrawer ref="MaterialInfoImportDrawerRef" @save-success="search" /> <MaterialInfoImportDrawer ref="MaterialInfoImportDrawerRef" @save-success="search" />
<PhotosImport ref="PhotosImportRef" @save-success="search"/> <PhotosImport ref="PhotosImportRef" @save-success="search"/>
<BatchImport ref="BatchImportRef" @save-success="search"/>
</div> </div>
</template> </template>
@@ -76,33 +87,46 @@
import MaterialInfoAddModal from './MaterialInfoAddModal.vue' import MaterialInfoAddModal from './MaterialInfoAddModal.vue'
import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue' import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue'
import PhotosImport from '@/views/material/PhotosImport.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 MaterialInfoQuery, type MaterialInfoResp, deleteMaterialInfo, exportMaterialInfo, listMaterialInfo } from '@/apis/material/materialInfo'
import type { TableInstanceColumns } from '@/components/GiTable/type' import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks' import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils' import { isMobile } from '@/utils'
import has from '@/utils/has' import has from '@/utils/has'
import {Message} from "@arco-design/web-vue";
defineOptions({ name: 'MaterialInfo' }) defineOptions({ name: 'MaterialInfo' })
const queryForm = reactive<MaterialInfoQuery>({ const queryForm = reactive<MaterialInfoQuery>({
materialName: undefined, materialName: undefined,
encoding: undefined, encoding: undefined,
sort: ['id,desc'], batch: undefined,
mark: undefined,
sort: ['mi.id,desc'],
}) })
const { const {
tableData: dataList, tableData: dataList,
loading, loading,
pagination, pagination,
selectedKeys,
select,
selectAll,
search, search,
handleDelete, handleDelete,
} = useTable((page) => listMaterialInfo({ ...queryForm, ...page }), { immediate: true }) } = useTable((page) => listMaterialInfo({ ...queryForm, ...page }), { immediate: true })
const columns = ref<TableInstanceColumns[]>([ const columns = ref<TableInstanceColumns[]>([
{ title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' }, { title: '物料名称', dataIndex: 'materialName', slotName: 'materialName' },
{ title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' }, { 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: '物料照片', 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: 'createUserString', slotName: 'createUser' },
{ title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' }, { title: '创建时间', dataIndex: 'createTime', slotName: 'createTime' },
{ {
@@ -116,40 +140,39 @@ const columns = ref<TableInstanceColumns[]>([
}, },
]) ])
// 重置
const reset = () => { const reset = () => {
queryForm.materialName = undefined queryForm.materialName = undefined
queryForm.encoding = undefined queryForm.encoding = undefined
search() search()
} }
// 删除 const onDeleteOne = (record: MaterialInfoResp) => {
const onDelete = (record: MaterialInfoResp) => {
return handleDelete(() => deleteMaterialInfo(record.id), { return handleDelete(() => deleteMaterialInfo(record.id), {
content: `是否确定删除该条数据?`, content: `是否确定删除该条数据?`,
showModal: true, showModal: true,
}) })
} }
// 导出 // 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteMaterialInfo(selectedKeys.value), {
content: `是否确定删除选中的 ${selectedKeys.value.length} 条数据?`,
showModal: true
})
}
const onExport = () => { const onExport = () => {
useDownload(() => exportMaterialInfo(queryForm)) useDownload(() => exportMaterialInfo(queryForm))
} }
// 图片加载失败处理函数(新增)
const handleImgError = (e: Event) => {
// 替换为你的默认图片地址(建议放在/static目录下
const target = e.target as HTMLImageElement
target.src = '/static/images/default-material.png'
}
const MaterialInfoAddModalRef = ref<InstanceType<typeof MaterialInfoAddModal>>() const MaterialInfoAddModalRef = ref<InstanceType<typeof MaterialInfoAddModal>>()
// 新增
const onAdd = () => { const onAdd = () => {
MaterialInfoAddModalRef.value?.onAdd() MaterialInfoAddModalRef.value?.onAdd()
} }
// 修改
const onUpdate = (record: MaterialInfoResp) => { const onUpdate = (record: MaterialInfoResp) => {
MaterialInfoAddModalRef.value?.onUpdate(record.id) MaterialInfoAddModalRef.value?.onUpdate(record.id)
} }
@@ -163,26 +186,52 @@ const PhotosImportRef = ref<InstanceType<typeof PhotosImport>>()
const onPhotosImport = () => { const onPhotosImport = () => {
PhotosImportRef.value?.onOpen() PhotosImportRef.value?.onOpen()
} }
const BatchImportRef = ref<InstanceType<typeof BatchImport>>()
const onBatchImport = () => {
BatchImportRef.value?.onOpen()
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 物料照片样式(新增)
.photo-container { .photo-container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 80px; height: 80px;
width: 100%;
overflow: hidden;
} }
.material-photo { .material-photo {
width: 80px; width: 100%;
height: 80px; height: 100%;
object-fit: cover; // 保持图片比例,避免拉伸 object-fit: cover;
border-radius: 4px; border-radius: 4px;
transition: opacity 0.3s;
} }
.no-photo { .no-photo {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
text-align: center;
}
// 新增样式
.photo-error {
color: #ff4d4f; // 阿里红/危险色
font-size: 12px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.4;
.error-icon {
font-size: 18px;
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

@@ -49,7 +49,7 @@ const columns: ColumnItem[] = reactive([
field: 'configKey', field: 'configKey',
type: 'input', type: 'input',
span: 24, span: 24,
disabled: true, disabled: isUpdate.value,
}, },
{ {
label: '参数键值', label: '参数键值',

View File

@@ -102,7 +102,7 @@ import { DisEnableStatusList } from '@/constant/common'
import { useDownload, useResetReactive, useTable } from '@/hooks' import { useDownload, useResetReactive, useTable } from '@/hooks'
import { isMobile } from '@/utils' import { isMobile } from '@/utils'
import has from '@/utils/has' import has from '@/utils/has'
import type { ColumnItem } from '@/components/GiForm' import type {ColumnItem} from "@/components/GiForm";
defineOptions({ name: 'SystemUser' }) defineOptions({ name: 'SystemUser' })
@@ -204,7 +204,7 @@ const reset = () => {
const onDelete = (record: UserResp) => { const onDelete = (record: UserResp) => {
return handleDelete(() => deleteUser(record.id), { return handleDelete(() => deleteUser(record.id), {
content: `是否确定删除用户「${record.username}」?`, content: `是否确定删除用户「${record.nickname}(${record.username})」?`,
showModal: true, showModal: true,
}) })
} }

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

@@ -0,0 +1,290 @@
<template>
<div class="gi_table_page">
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1800 }"
: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.userName" placeholder="请输入姓名" allow-clear @search="search" />
<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"
placeholder="请选择开始时间"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="height: 32px"
@change="search"
/>
<a-date-picker
v-model="queryForm.endDate"
placeholder="请选择结束时间"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="height: 32px"
@change="search"
/>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<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="['weighManage:workOrder:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #photoUrl="{ record }">
<a-image :src="record.photoUrl" width="55" />
</template>
<template #unitWeight="{ record }">
{{ record.unitWeight + 'g' }}
</template>
<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="['weighManage:workOrder:delete']" status="danger" :title="'删除'" @click="onDeleteOne(record.id)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="称重详情"
width="800px"
:loading="detailLoading"
>
<GiTable
v-if="detailData.length > 0"
row-key="id"
:data="detailData"
:columns="detailColumns"
:scroll="{ x: '100%', y: '100%', width: 800 }"
>
<template #imgUrl="{ record }">
<a-image :src="record.imgUrl" width="50" />
</template>
<template #calculatedWeight="{ record }">
{{ record.calculatedWeight + 'g' }}
</template>
<template #weight="{ record }">
{{ record.weight + 'g' }}
</template>
</GiTable>
<div v-else class="no-data">
<icon-loading style="font-size: 48px; color: rgb(var(--arcoblue-6));" />
<p>暂无称重数据</p>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import type GiTable from "@/components/GiTable/index.vue";
import {ref, reactive} from "vue";
import {Message} from "@arco-design/web-vue";
import { useRouter } from 'vue-router';
import {
deleteWorkOrder,
exportWorkOrder, getWorkOrderInfos,
listWorkOrder,
type WorkOrderQuery,
type WorkOrderResp
} from "@/apis/workOrder/workOrder";
defineOptions({ name: 'Record' })
const router = useRouter()
const queryForm = reactive<WorkOrderQuery>({
orderNo: undefined,
materialName: undefined,
encoding: undefined,
userName: undefined,
carNo: undefined,
batch: undefined,
startDate: undefined,
endDate: undefined,
sort: ['w.id,desc']
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete,
} = useTable((page) => listWorkOrder({ ...queryForm, ...page }), { immediate: true })
// 创建工具函数统一处理列配置
const processColumns = (columns: TableInstanceColumns[]): TableInstanceColumns[] => {
return columns.map(column => {
const defaultConfig = {
ellipsis: true,
tooltip: true
};
return { ...defaultConfig, ...column };
});
};
// 定义列配置,使用工具函数处理
const columns = ref<TableInstanceColumns[]>(processColumns([
{ title: '标题', dataIndex: 'title', width: 180 },
{ 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: 'totalCount' },
{ title: '标准总重量', dataIndex: 'totalCalculatedWeight' ,slotName: 'totalCalculatedWeight'},
{ title: '实际总重量', dataIndex: 'totalWeight' ,slotName: 'totalWeight'},
{ title: '人员卡号', dataIndex: 'cardNo' },
{ title: '创建人', dataIndex: 'createUserString' },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['workOrder:record:detail', 'workOrder:record:update', 'weighManage:workOrder:delete'])
}
]))
// 重置
const reset = () => {
queryForm.orderNo = undefined
queryForm.materialName = undefined
queryForm.encoding = undefined
queryForm.userName = undefined
queryForm.carNo = undefined
queryForm.startDate = undefined
queryForm.endDate = undefined
search()
}
// 详情弹窗状态
const detailModalVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<any[]>([])
// 详情列配置
const detailColumns = ref<TableInstanceColumns[]>([
{ title: '称重次数', dataIndex: 'weightTime' },
{ title: '物料名称', dataIndex: 'materialName' },
{ title: '输入数量', dataIndex: 'quantity' },
{ title: '标准重量(g)', dataIndex: 'calculatedWeight', slotName: 'calculatedWeight' },
{ title: '称重数量', dataIndex: 'weightQuantity' },
{ title: '称重重量(g)', dataIndex: 'weight', slotName: 'weight' },
{ title: '抓拍图片', dataIndex: 'imgUrl', slotName: 'imgUrl' }
])
// 详情
const onDetail = async (record: WorkOrderResp) => {
detailLoading.value = true
detailModalVisible.value = true
detailData.value = []
getWorkOrderInfos(record.id).then(res => {
if (res.code == '0') {
detailData.value = res.data;
detailLoading.value = false
} else {
Message.error('获取详情失败')
}
});
};
// 删除
const onDeleteOne = (id) => {
return handleDelete(() => deleteWorkOrder(id), {
content: `是否确定删除该条数据?`,
showModal: true
})
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteWorkOrder(selectedKeys.value), {
content: `是否确定删除选中的 ${selectedKeys.value.length} 条数据?`,
showModal: true
})
}
// 导出
const onExport = () => {
useDownload(() => exportWorkOrder(queryForm))
}
// 打印
const onPrint = (record: WorkOrderResp) => {
// 跳转到标签打印页面,并传递数据
router.push({
path: '/print',
query: {
workerOrderId: record.id,
}
})
}
</script>
<style scoped lang="scss">
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: var(--color-text-4);
p {
margin-top: 16px;
font-size: 14px;
}
}
</style>