更新新的流程图

This commit is contained in:
徐星 2026-04-02 16:55:40 +08:00
parent 7f5d01a4d3
commit 0d9e6c27c2
12 changed files with 763 additions and 226 deletions

191
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "device-management-platform", "name": "device-management-platform",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"element-plus": "^2.13.6",
"lucide-vue-next": "^0.487.0", "lucide-vue-next": "^0.487.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
@ -65,6 +66,24 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@ctrl/tinycolor": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
"integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@ -490,6 +509,31 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@isaacs/fs-minipass": { "node_modules/@isaacs/fs-minipass": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "resolved": "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@ -552,6 +596,17 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.8",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
"integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1", "version": "4.60.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
@ -1172,6 +1227,27 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.24",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.4", "version": "5.2.4",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@ -1292,6 +1368,48 @@
"integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vueuse/core": {
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz",
"integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "12.0.0",
"@vueuse/shared": "12.0.0",
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/metadata": {
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz",
"integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz",
"integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
"license": "MIT",
"dependencies": {
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/chownr": { "node_modules/chownr": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz",
@ -1308,6 +1426,12 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
@ -1318,6 +1442,32 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/element-plus": {
"version": "2.13.6",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.6.tgz",
"integrity": "sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.2.0",
"@element-plus/icons-vue": "^2.3.2",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "12.0.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.19",
"lodash": "^4.17.23",
"lodash-es": "^4.17.23",
"lodash-unified": "^1.0.3",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0",
"vue-component-type-helpers": "^3.2.4"
},
"peerDependencies": {
"vue": "^3.3.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.20.1", "version": "5.20.1",
"resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
@ -1698,6 +1848,29 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/lucide-vue-next": { "node_modules/lucide-vue-next": {
"version": "0.487.0", "version": "0.487.0",
"resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.487.0.tgz", "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.487.0.tgz",
@ -1716,6 +1889,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz",
@ -1757,6 +1936,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@ -2023,6 +2208,12 @@
} }
} }
}, },
"node_modules/vue-component-type-helpers": {
"version": "3.2.6",
"resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz",
"integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==",
"license": "MIT"
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.6.4", "version": "4.6.4",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",

View File

@ -9,13 +9,14 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"element-plus": "^2.13.6",
"lucide-vue-next": "^0.487.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0"
"lucide-vue-next": "^0.487.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"@tailwindcss/vite": "4.1.12", "@tailwindcss/vite": "4.1.12",
"@vitejs/plugin-vue": "^5.2.3",
"tailwindcss": "4.1.12", "tailwindcss": "4.1.12",
"vite": "6.3.5" "vite": "6.3.5"
} }

View File

@ -35,7 +35,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Monitor, Settings2, Key, Cpu, FileCode, Gauge, Wrench } from 'lucide-vue-next' import { Monitor, Settings2, Key, Cpu, FileCode, Gauge, Wrench, Recycle } from 'lucide-vue-next'
const route = useRoute() const route = useRoute()
const isActive = (path: string) => const isActive = (path: string) =>
@ -57,7 +57,7 @@ const menuGroups = [
]}, ]},
{ title: '维修', items: [ { title: '维修', items: [
{ path: '/repair', label: '维修工单', icon: Wrench }, { path: '/repair', label: '维修工单', icon: Wrench },
{ path: '/scrap', label: '报废回收', icon: Wrench }, { path: '/scrap', label: '报废回收', icon: Recycle },
]}, ]},
] ]
</script> </script>

View File

@ -93,10 +93,6 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">待校准采集板</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">待校准采集板</div>
<div class="text-3xl font-semibold" style="color: #FAAD14">23</div> <div class="text-3xl font-semibold" style="color: #FAAD14">23</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准中</div>
<div class="text-3xl font-semibold" style="color: #4a7c59">8</div>
</div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准即将到期</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准即将到期</div>
<div class="text-3xl font-semibold" style="color: #FF4D4F">15</div> <div class="text-3xl font-semibold" style="color: #FF4D4F">15</div>
@ -185,7 +181,7 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #4a7c59">详情</button> <button class="text-sm" style="color: #4a7c59">详情</button>
<button class="text-sm" style="color: #4a7c59">下载报告</button> <button class="text-sm" style="color: #4a7c59">校准文件</button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -170,7 +170,6 @@ const getStatusStyle = (status: ConfigFile['status']) => {
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #4a7c59">详情</button> <button class="text-sm" style="color: #4a7c59">详情</button>
<button class="text-sm" style="color: #4a7c59">编辑</button> <button class="text-sm" style="color: #4a7c59">编辑</button>
<button class="text-sm" style="color: #4a7c59">下发</button>
<button class="text-sm" style="color: #4a7c59">下载</button> <button class="text-sm" style="color: #4a7c59">下载</button>
<button class="text-sm" style="color: #FF4D4F">删除</button> <button class="text-sm" style="color: #FF4D4F">删除</button>
</div> </div>

View File

@ -82,7 +82,7 @@ import { TrendingUp, TrendingDown, Server, Wifi, CheckCircle, PackageCheck, Wren
const metrics = [ const metrics = [
{ label: '设备总数', value: '5,234', trend: 'up' as const, trendValue: '+5.2%', color: '#4a7c59', icon: Server, link: '/devices' }, { label: '设备总数', value: '5,234', trend: 'up' as const, trendValue: '+5.2%', color: '#4a7c59', icon: Server, link: '/devices' },
{ label: '装配中', value: '4,856', trend: 'up' as const, trendValue: '+2.8%', color: '#52C41A', icon: Wifi, link: '/devices' }, { label: '装配中', value: '4,856', trend: 'up' as const, trendValue: '+2.8%', color: '#52C41A', icon: Wifi, link: '/devices' },
{ label: '已激活', value: '4,912', trend: 'up' as const, trendValue: '+1.5%', color: '#4a7c59', icon: CheckCircle, link: '/activation' }, { label: '已激活', value: '4,912', trend: 'up' as const, trendValue: '+1.5%', color: '#4a7c59', icon: CheckCircle, link: '/devices' },
{ label: '有新版本', value: '156', color: '#722ED1', icon: PackageCheck, link: '/firmware' }, { label: '有新版本', value: '156', color: '#722ED1', icon: PackageCheck, link: '/firmware' },
{ label: '维修中', value: '23', trend: 'down' as const, trendValue: '-12.3%', color: '#FF4D4F', icon: Wrench, link: '/repair' }, { label: '维修中', value: '23', trend: 'down' as const, trendValue: '-12.3%', color: '#FF4D4F', icon: Wrench, link: '/repair' },
{ label: '报废', value: '56', color: '#FA8C16', icon: Target, link: '/scrap' }, { label: '报废', value: '56', color: '#FA8C16', icon: Target, link: '/scrap' },
@ -100,10 +100,10 @@ const maxStatusValue = computed(() => Math.max(...deviceStatusData.map((d) => d.
const taskGroups = [ const taskGroups = [
{ {
title: '待校准设备', count: 23, link: '/calibration', title: '校准即将到期', count: 23, link: '/calibration',
tasks: [ tasks: [
{ deviceSN: 'AC20240308005', description: '采集板校准即将到期', time: '3天后到期', link: '/calibration' }, { deviceSN: 'AC20240308005', description: '采集板校准即将到期', time: '3天后到期', link: '/calibration' },
{ deviceSN: 'AC20240308006', description: '采集板校准', time: '今天', link: '/calibration' }, { deviceSN: 'AC20240308006', description: '采集板校准即将到期', time: '今天到期', link: '/calibration' },
], ],
}, },
{ {

View File

@ -112,7 +112,15 @@
<!-- Assembly Record Card --> <!-- Assembly Record Card -->
<div class="bg-white p-6 rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<h3 class="text-lg font-semibold mb-6">装配记录</h3> <div style="display: flex; justify-content: space-between;">
<h3 class="text-lg font-semibold mb-6">装配记录</h3>
<button>
<span style="color: #52C41A;">
查看
</span>
</button>
</div>
<div class="grid grid-cols-3 gap-x-12 gap-y-4 mb-6"> <div class="grid grid-cols-3 gap-x-12 gap-y-4 mb-6">
<div> <div>
<div class="text-sm mb-1" style="color: rgba(0,0,0,0.45)">装配工单</div> <div class="text-sm mb-1" style="color: rgba(0,0,0,0.45)">装配工单</div>

View File

@ -170,6 +170,25 @@ const boardVersionData = [
status: 'active', status: 'active',
}, },
] ]
// Checklist template drawer state
const showChecklistDrawer = ref(false)
const newTplModel = ref('GD30')
const newTplItems = ref<{ text: string }[]>([{ text: '' }])
const openChecklistDrawer = () => {
newTplModel.value = 'GD30'
newTplItems.value = [{ text: '' }]
showChecklistDrawer.value = true
}
const addTplItem = () => {
newTplItems.value.push({ text: '' })
}
const removeTplItem = (index: number) => {
newTplItems.value.splice(index, 1)
}
</script> </script>
<template> <template>
@ -291,7 +310,17 @@ const boardVersionData = [
<!-- Assembly Checklist --> <!-- Assembly Checklist -->
<div class="bg-white rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }"> <div class="bg-white rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }">
<div class="p-6 border-b" :style="{ borderColor: '#F0F0F0' }"> <div class="p-6 border-b" :style="{ borderColor: '#F0F0F0' }">
<h3 class="text-lg font-semibold">装配Checklist模板</h3> <div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">装配Checklist模板</h3>
<button
class="px-4 py-2 rounded text-white flex items-center gap-2"
:style="{ backgroundColor: '#4a7c59' }"
@click="openChecklistDrawer"
>
<Plus :size="16" />
新增模版
</button>
</div>
</div> </div>
<!-- Tabs --> <!-- Tabs -->
@ -318,7 +347,6 @@ const boardVersionData = [
<tr> <tr>
<th class="px-3 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)', width: '50px' }">序号</th> <th class="px-3 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)', width: '50px' }">序号</th>
<th class="px-3 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">项目名称</th> <th class="px-3 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">项目名称</th>
<th class="px-3 py-3 text-center text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)', width: '70px' }">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -352,33 +380,9 @@ const boardVersionData = [
{{ item.text }} {{ item.text }}
</span> </span>
</td> </td>
<td class="px-3 py-3 text-center">
<button
@click="deleteChecklistItem(activeTab, item.id)"
class="text-gray-400 hover:text-red-500 transition-colors"
>
<Trash2 :size="15" />
</button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="mt-4 flex items-center justify-between">
<button
@click="addChecklistItem(activeTab)"
class="px-4 py-2 text-sm flex items-center gap-1 rounded transition-colors"
:style="{ color: '#4a7c59', border: '1px dashed #4a7c59' }"
>
<Plus :size="14" />
添加检查项
</button>
<button
class="px-6 py-2 text-sm rounded text-white"
:style="{ backgroundColor: '#4a7c59' }"
>
确认保存
</button>
</div>
</div> </div>
</div> </div>
@ -479,7 +483,7 @@ const boardVersionData = [
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定采集板固件版本</label> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定采集板固件版本</label>
<select v-model="drawerForm.firmwareVersion" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff"> <select v-model="drawerForm.firmwareVersion" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="">请选择采集板固件版本</option> <option value="">请选择采集板固件版本</option>
@ -539,5 +543,78 @@ const boardVersionData = [
</div> </div>
</div> </div>
</div> </div>
<!-- Checklist Template Drawer -->
<div v-if="showChecklistDrawer" class="fixed inset-0 z-50 flex justify-end" style="background-color: rgba(0,0,0,0.45)" @click.self="showChecklistDrawer = false">
<div class="bg-white w-[480px] h-full flex flex-col" style="box-shadow: -4px 0 12px rgba(0,0,0,0.1)">
<!-- Drawer Header -->
<div class="flex items-center justify-between p-5 border-b" style="border-color: #F0F0F0">
<h3 class="text-lg font-semibold">新增Checklist模版</h3>
<button @click="showChecklistDrawer = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.45)">
<X :size="18" />
</button>
</div>
<!-- Drawer Body -->
<div class="flex-1 overflow-y-auto p-6">
<div class="space-y-5">
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">设备型号 <span style="color: #FF4D4F">*</span></label>
<select v-model="newTplModel" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="GD30">GD30</option>
<option value="GT20">GT20</option>
<option value="GM10">GM10</option>
</select>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">检查项列表</label>
<div class="space-y-3">
<div
v-for="(item, index) in newTplItems"
:key="index"
class="flex items-center gap-2"
>
<span class="text-sm w-6 text-center flex-shrink-0" style="color: rgba(0,0,0,0.45)">{{ index + 1 }}</span>
<input
v-model="item.text"
type="text"
class="flex-1 px-3 py-2 border rounded text-sm"
style="border-color: #D9D9D9"
placeholder="请输入检查项名称"
/>
<button
@click="removeTplItem(index)"
class="p-1 rounded hover:bg-red-50 text-gray-400 hover:text-red-500 transition-colors flex-shrink-0"
:disabled="newTplItems.length <= 1"
:style="{ opacity: newTplItems.length <= 1 ? 0.3 : 1 }"
>
<Trash2 :size="15" />
</button>
</div>
</div>
<button
@click="addTplItem"
class="mt-3 px-4 py-2 text-sm flex items-center gap-1 rounded transition-colors w-full justify-center"
:style="{ color: '#4a7c59', border: '1px dashed #4a7c59' }"
>
<Plus :size="14" />
添加检查项
</button>
</div>
</div>
</div>
<!-- Drawer Footer -->
<div class="flex items-center justify-end gap-3 p-5 border-t" style="border-color: #F0F0F0">
<button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showChecklistDrawer = false">取消</button>
<button
class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: '#4a7c59' }"
@click="showChecklistDrawer = false"
>确认生成</button>
</div>
</div>
</div>
</div> </div>
</template> </template>

View File

@ -65,7 +65,7 @@ const checklistItems = ref([
const bomData = [ const bomData = [
{ code: 'MB-2024-001', name: '主控板', sn: 'MB20240308001', spec: 'GD30-MB-V2.3', calibration: '无需校准', quantity: 1 }, { code: 'MB-2024-001', name: '主控板', sn: 'MB20240308001', spec: 'GD30-MB-V2.3', calibration: '无需校准', quantity: 1 },
{ code: 'RX-2024-002', name: '采集板', sn: 'RX20240308002', spec: 'GD30-RX-V1.8', calibration: '已校准', quantity: 2 }, { code: 'RX-2024-002', name: '采集板', sn: 'RX20240308001、RX20240308002', spec: 'GD30-RX-V1.8', calibration: '已校准', quantity: 2 },
{ code: 'MC-2024-003', name: '测控板', sn: 'MC20240308003', spec: 'GD30-MC-V1.5', calibration: '无需校准', quantity: 1 }, { code: 'MC-2024-003', name: '测控板', sn: 'MC20240308003', spec: 'GD30-MC-V1.5', calibration: '无需校准', quantity: 1 },
{ code: 'TX-2024-003', name: '发射板', sn: 'TX20240308003', spec: 'GD30-TX-V1.5', calibration: '无需校准', quantity: 1 }, { code: 'TX-2024-003', name: '发射板', sn: 'TX20240308003', spec: 'GD30-TX-V1.5', calibration: '无需校准', quantity: 1 },
{ code: 'BO-2024-004', name: '升压板', sn: 'BO20240308004', spec: 'BP600', calibration: '无需校准', quantity: 1 }, { code: 'BO-2024-004', name: '升压板', sn: 'BO20240308004', spec: 'BP600', calibration: '无需校准', quantity: 1 },
@ -132,12 +132,6 @@ const toggleChecklistItem = (id: number) => {
placeholder="请输入主板SN号" placeholder="请输入主板SN号"
value="MB20240308001" value="MB20240308001"
/> />
<span
class="px-2 py-1 rounded text-xs whitespace-nowrap"
:style="{ backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }"
>
已绑定
</span>
</div> </div>
</div> </div>
<div> <div>

View File

@ -1,7 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Upload } from 'lucide-vue-next' import { Upload } from 'lucide-vue-next'
import { ref } from 'vue' import { ref, reactive } from 'vue'
import { ElDialog, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElCheckbox, ElMessage, ElUpload, ElButton } from 'element-plus'
import 'element-plus/dist/index.css'
// - firmwareType
interface Firmware { interface Firmware {
version: string version: string
releaseDate: string releaseDate: string
@ -16,6 +19,18 @@ interface Firmware {
signed?: boolean signed?: boolean
notes?: string[] notes?: string[]
expanded: boolean expanded: boolean
firmwareType: '主协板' | '采集板' | '发射板' | '主机固件' | '计算单元固件' //
}
// - firmwareType
interface FirmwareUploadForm {
version: string
hardwareRange: string
upgradeType: '可选' | '强制'
signed: boolean
notes: string
file: File | null
firmwareType: '主协板' | '采集板' | '发射板' | '主机固件' | '计算单元固件' //
} }
const firmwares = ref<Firmware[]>([ const firmwares = ref<Firmware[]>([
@ -38,6 +53,7 @@ const firmwares = ref<Firmware[]>([
'增强安全性,升级前强制验证固件签名', '增强安全性,升级前强制验证固件签名',
], ],
expanded: true, expanded: true,
firmwareType: '主机固件' //
}, },
{ {
version: 'v2.2.0', version: 'v2.2.0',
@ -46,6 +62,7 @@ const firmwares = ref<Firmware[]>([
fileSize: '12.0MB', fileSize: '12.0MB',
downloads: 2456, downloads: 2456,
expanded: false, expanded: false,
firmwareType: '主协板' //
}, },
{ {
version: 'v2.1.0', version: 'v2.1.0',
@ -54,8 +71,102 @@ const firmwares = ref<Firmware[]>([
fileSize: '11.5MB', fileSize: '11.5MB',
downloads: 3587, downloads: 3587,
expanded: false, expanded: false,
firmwareType: '采集板' //
}, },
]) ])
// ============== ==============
const uploadDialogVisible = ref(false)
//
const upgradeTypes = ref<('可选' | '强制')[]>(['可选', '强制'])
// 5
const firmwareTypes = ref<('主协板' | '采集板' | '发射板' | '主机固件' | '计算单元固件')[]>(
['主协板', '采集板', '发射板', '主机固件', '计算单元固件']
)
// - firmwareType
const uploadForm = ref<FirmwareUploadForm>({
version: '',
hardwareRange: '',
upgradeType: '可选',
signed: true,
notes: '',
file: null,
firmwareType: '主机固件' //
})
// - firmwareType
const uploadFormRules = reactive({
version: [{ required: true, message: '请输入固件版本', trigger: 'blur' }],
hardwareRange: [{ required: true, message: '请输入硬件版本范围', trigger: 'blur' }],
upgradeType: [{ required: true, message: '请选择升级类型', trigger: 'change' }],
firmwareType: [{ required: true, message: '请选择固件类型', trigger: 'change' }], //
file: [{ required: true, message: '请上传固件ZIP包', trigger: 'change' }]
})
// ZIP
const beforeUpload = (file: File) => {
const isZip = file.type === 'application/zip' || file.name.endsWith('.zip')
if (!isZip) {
ElMessage.error('只能上传 ZIP 格式的固件包!')
return false
}
uploadForm.value.file = file
return false //
}
//
const resetUploadForm = () => {
uploadForm.value = {
version: '',
hardwareRange: '',
upgradeType: '可选',
signed: true,
notes: '',
file: null,
firmwareType: '主机固件' //
}
}
// ref
const uploadFormRef = ref<any>(null)
//
const handleUploadSubmit = () => {
if (!uploadFormRef.value) return
uploadFormRef.value.validate((valid: boolean) => {
if (!valid) return
// - firmwareType
const newFirmware: Firmware = {
version: uploadForm.value.version,
releaseDate: new Date().toISOString().split('T')[0],
status: '草稿',
fileSize: formatFileSize(uploadForm.value.file!.size),
downloads: 0,
model: 'GD30',
hardwareRange: uploadForm.value.hardwareRange,
upgradeType: uploadForm.value.upgradeType,
signed: uploadForm.value.signed,
notes: uploadForm.value.notes.split('\n').filter(item => item.trim()),
expanded: false,
firmwareType: uploadForm.value.firmwareType //
}
//
firmwares.value.unshift(newFirmware)
ElMessage.success('固件上传成功!')
uploadDialogVisible.value = false
resetUploadForm()
})
}
//
const formatFileSize = (bytes: number): string => {
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
return (bytes / (1024 * 1024)).toFixed(1) + 'MB'
}
</script> </script>
<template> <template>
@ -63,15 +174,12 @@ const firmwares = ref<Firmware[]>([
<!-- Page Header --> <!-- Page Header -->
<div class="mb-6"> <div class="mb-6">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h2 class="text-2xl font-semibold">固件库</h2> <h2 class="text-2xl font-semibold">GD-30 Supreme固件库</h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #4a7c59"> <button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #4a7c59" @click="uploadDialogVisible = true">
<Upload :size="16" /> <Upload :size="16" />
上传固件 上传固件
</button> </button>
<button class="px-4 py-2 rounded flex items-center gap-2" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)">
发布固件
</button>
</div> </div>
</div> </div>
</div> </div>
@ -84,11 +192,15 @@ const firmwares = ref<Firmware[]>([
<div class="divide-y" style="border-color: #F0F0F0"> <div class="divide-y" style="border-color: #F0F0F0">
<div v-for="fw in firmwares" :key="fw.version" class="p-6"> <div v-for="fw in firmwares" :key="fw.version" class="p-6">
<!-- Header row --> <!-- Header row - 新增固件类型展示 -->
<div class="flex items-center gap-4 mb-2"> <div class="flex items-center gap-4 mb-2 flex-wrap">
<span class="text-lg font-semibold">版本: {{ fw.version }}</span> <span class="text-lg font-semibold">版本: {{ fw.version }}</span>
<span class="text-sm" style="color: rgba(0,0,0,0.45)">发布日期: {{ fw.releaseDate }}</span> <span class="text-sm" style="color: rgba(0,0,0,0.45)">发布日期: {{ fw.releaseDate }}</span>
<span class="text-sm" style="color: #52C41A">{{ fw.status }}</span> <span class="text-sm" style="color: #52C41A">{{ fw.status }}</span>
<!-- 新增固件类型标签 -->
<span class="text-sm px-2 py-1 rounded" style="background-color: #f0f9ff; color: #4a7c59">
类型: {{ fw.firmwareType }}
</span>
</div> </div>
<!-- Basic info --> <!-- Basic info -->
@ -123,7 +235,6 @@ const firmwares = ref<Firmware[]>([
<button class="text-sm" style="color: #4a7c59">下载</button> <button class="text-sm" style="color: #4a7c59">下载</button>
<button class="text-sm" style="color: #4a7c59">编辑</button> <button class="text-sm" style="color: #4a7c59">编辑</button>
<button class="text-sm" style="color: #4a7c59">撤回发布</button> <button class="text-sm" style="color: #4a7c59">撤回发布</button>
<router-link to="/firmware-upgrade" class="text-sm" style="color: #4a7c59">查看升级任务</router-link>
</div> </div>
</template> </template>
@ -137,5 +248,70 @@ const firmwares = ref<Firmware[]>([
</div> </div>
</div> </div>
</div> </div>
<!-- ============== 上传固件弹窗 ============== -->
<ElDialog v-model="uploadDialogVisible" title="上传固件" width="550px" append-to-body>
<ElForm :model="uploadForm" :rules="uploadFormRules" ref="uploadFormRef" label-width="110px" class="mt-4">
<!-- 固件版本 -->
<ElFormItem label="固件版本" prop="version">
<ElInput v-model="uploadForm.version" placeholder="例如v2.4.0" />
</ElFormItem>
<!-- 新增固件类型选择 -->
<ElFormItem label="固件类型" prop="firmwareType">
<ElSelect v-model="uploadForm.firmwareType" placeholder="请选择固件类型">
<ElOption v-for="type in firmwareTypes" :key="type" :label="type" :value="type" />
</ElSelect>
</ElFormItem>
<!-- 硬件版本范围 -->
<ElFormItem label="硬件版本范围" prop="hardwareRange">
<ElInput v-model="uploadForm.hardwareRange" placeholder="例如A1-A3" />
</ElFormItem>
<!-- 升级类型 -->
<ElFormItem label="升级类型" prop="upgradeType">
<ElSelect v-model="uploadForm.upgradeType" placeholder="请选择升级类型">
<ElOption v-for="type in upgradeTypes" :key="type" :label="type" :value="type" />
</ElSelect>
</ElFormItem>
<!-- 数字签名 -->
<ElFormItem label="数字签名">
<ElCheckbox v-model="uploadForm.signed">已签名</ElCheckbox>
</ElFormItem>
<!-- 固件上传 -->
<ElFormItem label="固件包" prop="file">
<ElUpload
:before-upload="beforeUpload"
:file-list="[]"
accept=".zip"
drag
>
<div class="el-upload__text">点击或拖拽 ZIP 文件到此处</div>
</ElUpload>
</ElFormItem>
<!-- 发布说明 -->
<ElFormItem label="发布说明">
<el-input
v-model="uploadForm.notes"
style="width: 100%"
:rows="4"
type="textarea"
placeholder="每行一条发布说明,自动换行分割"
/>
</ElFormItem>
</ElForm>
<!-- 弹窗底部按钮 -->
<template #footer>
<div class="flex justify-end gap-2">
<ElButton @click="uploadDialogVisible = false; resetUploadForm()">取消</ElButton>
<ElButton type="primary" style="background-color: #4a7c59; border-color: #4a7c59" @click="handleUploadSubmit">确认上传</ElButton>
</div>
</template>
</ElDialog>
</div> </div>
</template> </template>

View File

@ -1,164 +1,143 @@
<script setup lang="ts"> <script setup lang="ts">
import { Info, Download, Upload, Link as LinkIcon } from 'lucide-vue-next' import { ref } from 'vue'
import { Info, Download, Link as LinkIcon, X, ArrowLeft, Check, AlertTriangle } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
interface ScrapDevice { interface ScrapDevice {
sn: string sn: string; model: string; scrapDate: string; reason: string; sourceOrder: string
model: string status: '待审批' | '审批中' | '已审批' | '已驳回' | '回收中' | '已回收'
scrapDate: string applicant: string; approver: string; residualValue: number
status: '待审批' | '已审批' | '已回收' recyclableParts: string[]
sourceOrder: string
reason: string
} }
const scrapDevices: ScrapDevice[] = [ const scrapDevices = ref<ScrapDevice[]>([
{ { sn: 'GD30-2023-001234', model: 'GD-30 Supreme', scrapDate: '2024-03-01', status: '已回收', sourceOrder: 'WO-2024-0001', reason: '主板损坏无法修复', applicant: '李工', approver: '张主管', residualValue: 500, recyclableParts: ['采集板 AC20240308002', '测控板 CT20240308003'] },
sn: 'GD30-2023-001234', { sn: 'GT20-2023-000567', model: 'GT-20', scrapDate: '2024-03-05', status: '已审批', sourceOrder: 'WO-2024-0025', reason: '多个核心部件损坏', applicant: '王工', approver: '张主管', residualValue: 300, recyclableParts: ['电源模块 PS20240308004'] },
model: 'GD-30 Supreme', { sn: 'GD30-2023-000890', model: 'GD-30 Supreme', scrapDate: '2024-03-08', status: '待审批', sourceOrder: 'WO-2024-0048', reason: '维修成本超过设备价值', applicant: '李工', approver: '', residualValue: 800, recyclableParts: ['采集板 AC20240309001', '发射板 TX20240309002', '升压板 BO20240309003'] },
scrapDate: '2024-03-01', { sn: 'GD30-2023-001100', model: 'GD-30 Supreme', scrapDate: '2024-03-10', status: '审批中', sourceOrder: 'WO-2024-0052', reason: '设备老化严重', applicant: '张工', approver: '', residualValue: 200, recyclableParts: [] },
status: '已回收', { sn: 'GT20-2023-001200', model: 'GT-20', scrapDate: '2024-03-12', status: '已驳回', sourceOrder: 'WO-2024-0055', reason: '通信模块故障', applicant: '王工', approver: '张主管', residualValue: 0, recyclableParts: [] },
sourceOrder: 'WO-2024-0001', ])
reason: '主板损坏无法修复',
},
{
sn: 'GT20-2023-000567',
model: 'GT-20 瞬变电磁仪',
scrapDate: '2024-03-05',
status: '已审批',
sourceOrder: 'WO-2024-0025',
reason: '多个核心部件损坏',
},
{
sn: 'GM10-2023-000890',
model: 'GM10 大地电磁仪',
scrapDate: '2024-03-08',
status: '待审批',
sourceOrder: 'WO-2024-0048',
reason: '维修成本超过设备价值',
},
]
const getStatusStyle = (status: ScrapDevice['status']) => { // Drawers
switch (status) { const showDetailDrawer = ref(false)
case '已回收': const showApprovalDrawer = ref(false)
return { const showRecycleDrawer = ref(false)
backgroundColor: '#F6FFED', const currentDevice = ref<ScrapDevice | null>(null)
color: '#52C41A', const approvalComment = ref('')
border: '1px solid #B7EB8F', const recycleNote = ref('')
} const recycleChecked = ref<Set<string>>(new Set())
case '已审批':
return { const openDetail = (d: ScrapDevice) => { currentDevice.value = d; showDetailDrawer.value = true }
backgroundColor: '#eef5f0', const openApproval = (d: ScrapDevice) => { currentDevice.value = d; approvalComment.value = ''; showApprovalDrawer.value = true }
color: '#4a7c59', const openRecycle = (d: ScrapDevice) => {
border: '1px solid #a3c4ad', currentDevice.value = d; recycleNote.value = ''
} recycleChecked.value = new Set()
case '待审批': showRecycleDrawer.value = true
return { }
backgroundColor: '#FFFBE6',
color: '#FAAD14', const getStatusStyle = (s: ScrapDevice['status']) => {
border: '1px solid #FFE58F', const map: Record<string, any> = {
} '待审批': { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' },
'审批中': { backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' },
'已审批': { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' },
'已驳回': { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' },
'回收中': { backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' },
'已回收': { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' },
} }
return map[s]
} }
const statCounts = {
total: scrapDevices.value.length,
pending: scrapDevices.value.filter(d => d.status === '待审批' || d.status === '审批中').length,
approved: scrapDevices.value.filter(d => d.status === '已审批').length,
recycled: scrapDevices.value.filter(d => d.status === '已回收').length,
}
const flowSteps = [
{ label: '申请报废', desc: '维修工单发起' },
{ label: '主管审批', desc: '审核报废原因' },
{ label: '物料检测', desc: '检测可回收物料' },
{ label: '回收入库', desc: '物料回收登记' },
{ label: '报废完成', desc: '设备注销' },
]
</script> </script>
<template> <template>
<div class="p-6"> <div class="p-6">
<!-- Page Header --> <!-- Header -->
<div class="mb-6"> <div class="mb-6">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h2 class="text-2xl font-semibold">报废管理</h2> <h2 class="text-2xl font-semibold">报废管理</h2>
<div class="flex items-center gap-3"> <button class="px-4 py-2 rounded flex items-center gap-2" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)">
<button <Download :size="16" /> 导出
class="px-4 py-2 rounded flex items-center gap-2" </button>
style="border: 1px solid #D9D9D9; color: rgba(0, 0, 0, 0.85)"
>
<Download :size="16" />
导出
</button>
</div>
</div> </div>
<p class="text-sm" style="color: rgba(0, 0, 0, 0.45)">管理报废设备与物料回收</p> <p class="text-sm" style="color: rgba(0,0,0,0.45)">管理报废设备审批与物料回收</p>
</div> </div>
<!-- Info Banner --> <!-- Approval Flow -->
<div <div class="bg-white p-6 rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
class="mb-6 p-4 rounded-lg flex items-start gap-3" <h3 class="text-base font-semibold mb-4">报废审批流程</h3>
style="background-color: #eef5f0; border: 1px solid #a3c4ad" <div class="flex items-center justify-between">
> <div v-for="(step, i) in flowSteps" :key="i" class="flex items-center gap-3 flex-1">
<Info :size="20" style="color: #4a7c59; flex-shrink: 0; margin-top: 2px" /> <div class="text-center flex-1">
<div style="color: #2d5a3d"> <div class="w-10 h-10 rounded-full flex items-center justify-center mx-auto mb-2" :style="{ backgroundColor: '#eef5f0', border: '2px solid #4a7c59', color: '#4a7c59', fontWeight: 600, fontSize: '14px' }">{{ i + 1 }}</div>
<div class="font-medium mb-1">报废流程说明</div> <div class="text-sm font-medium">{{ step.label }}</div>
<div class="text-sm"> <div class="text-xs" style="color: rgba(0,0,0,0.45)">{{ step.desc }}</div>
报废单由维修工单中申请报废创建关联来源维修工单报废设备需经过审批流程审批通过后进行物料回收和入库 </div>
<div v-if="i < flowSteps.length - 1" style="color: #D9D9D9; font-size: 18px; flex-shrink: 0">&#8594;</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Stat Cards --> <!-- Stats -->
<div class="grid grid-cols-4 gap-6 mb-6"> <div class="grid grid-cols-4 gap-6 mb-6">
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">报废设备总数</div> <div class="text-sm mb-2" style="color: rgba(0,0,0,0.45)">报废总数</div>
<div class="text-3xl font-semibold" style="color: rgba(0, 0, 0, 0.85)">156</div> <div class="text-3xl font-semibold">{{ statCounts.total }}</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">待审批</div> <div class="text-sm mb-2" style="color: rgba(0,0,0,0.45)">待审批</div>
<div class="text-3xl font-semibold" style="color: #FAAD14">12</div> <div class="text-3xl font-semibold" style="color: #FAAD14">{{ statCounts.pending }}</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">已审批待回收</div> <div class="text-sm mb-2" style="color: rgba(0,0,0,0.45)">已审批待回收</div>
<div class="text-3xl font-semibold" style="color: #4a7c59">8</div> <div class="text-3xl font-semibold" style="color: #4a7c59">{{ statCounts.approved }}</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">已回收入库</div> <div class="text-sm mb-2" style="color: rgba(0,0,0,0.45)">已回收</div>
<div class="text-3xl font-semibold" style="color: #52C41A">136</div> <div class="text-3xl font-semibold" style="color: #52C41A">{{ statCounts.recycled }}</div>
</div> </div>
</div> </div>
<!-- Filter Card --> <!-- Filter -->
<div class="bg-white p-6 rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="grid grid-cols-4 gap-4"> <div class="grid grid-cols-4 gap-4">
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.65)">设备SN号</label> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.65)">设备SN号</label>
<input <input type="text" class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9" placeholder="搜索SN" />
type="text"
class="w-full px-3 py-2 border rounded"
style="border-color: #D9D9D9"
placeholder="输入设备SN号搜索"
/>
</div> </div>
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.65)">报废状态</label> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.65)">状态</label>
<select <select class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9; background-color: #fff">
class="w-full px-3 py-2 border rounded" <option>全部</option><option>待审批</option><option>审批中</option><option>已审批</option><option>已驳回</option><option>回收中</option><option>已回收</option>
style="border-color: #D9D9D9; background-color: #fff"
>
<option>全部</option>
<option>待审批</option>
<option>已审批</option>
<option>已回收</option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.65)">报废日期</label> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.65)">报废日期</label>
<input <input type="date" class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9" />
type="date"
class="w-full px-3 py-2 border rounded"
style="border-color: #D9D9D9"
/>
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button class="w-full px-4 py-2 rounded text-white" style="background-color: #4a7c59"> <button class="w-full px-4 py-2 rounded text-white" style="background-color: #4a7c59">查询</button>
查询
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Scrap Device List --> <!-- Table -->
<div class="bg-white rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="p-6 border-b" style="border-color: #F0F0F0"> <div class="p-6 border-b" style="border-color: #F0F0F0">
<h3 class="text-lg font-semibold">报废设备列表</h3> <h3 class="text-lg font-semibold">报废设备列表</h3>
</div> </div>
@ -166,42 +145,33 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
<table class="w-full"> <table class="w-full">
<thead style="background-color: #FAFAFA"> <thead style="background-color: #FAFAFA">
<tr> <tr>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">设备SN</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">设备SN</th>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">型号</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">型号</th>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">报废日期</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">报废原因</th>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">报废原因</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">申请人</th>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">状态</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">状态</th>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">来源工单</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">来源工单</th>
<th class="px-6 py-3 text-left text-sm font-medium" style="color: rgba(0, 0, 0, 0.85)">操作</th> <th class="px-5 py-3 text-left text-sm font-medium" style="color: rgba(0,0,0,0.85)">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-for="(d, i) in scrapDevices" :key="i" class="border-b" style="border-color: #F0F0F0">
v-for="(device, index) in scrapDevices" <td class="px-5 py-4 text-sm">{{ d.sn }}</td>
:key="index" <td class="px-5 py-4 text-sm" style="color: rgba(0,0,0,0.65)">{{ d.model }}</td>
class="border-b" <td class="px-5 py-4 text-sm" style="color: rgba(0,0,0,0.65)">{{ d.reason }}</td>
style="border-color: #F0F0F0" <td class="px-5 py-4 text-sm" style="color: rgba(0,0,0,0.65)">{{ d.applicant }}</td>
> <td class="px-5 py-4"><span class="px-2 py-0.5 rounded text-xs" :style="getStatusStyle(d.status)">{{ d.status }}</span></td>
<td class="px-6 py-4">{{ device.sn }}</td> <td class="px-5 py-4">
<td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ device.model }}</td> <router-link :to="'/repair/' + d.sourceOrder" class="text-sm flex items-center gap-1" style="color: #4a7c59">
<td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ device.scrapDate }}</td> <LinkIcon :size="14" /> {{ d.sourceOrder }}
<td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ device.reason }}</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded text-xs" :style="getStatusStyle(device.status)">
{{ device.status }}
</span>
</td>
<td class="px-6 py-4">
<router-link :to="'/repair/' + device.sourceOrder" class="text-sm flex items-center gap-1" style="color: #4a7c59">
<LinkIcon :size="14" />
{{ device.sourceOrder }}
</router-link> </router-link>
</td> </td>
<td class="px-6 py-4"> <td class="px-5 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3 text-sm">
<button class="text-sm" style="color: #4a7c59">查看详情</button> <button style="color: #4a7c59" @click="openDetail(d)">详情</button>
<button class="text-sm" style="color: #4a7c59">物料检测</button> <button v-if="d.status === '待审批' || d.status === '审批中'" style="color: #4a7c59" @click="openApproval(d)">审批</button>
<button v-if="device.status === '已审批'" class="text-sm" style="color: #52C41A" @click="router.push('/registration')">回收入库</button> <button v-if="d.status === '已审批'" style="color: #52C41A" @click="openRecycle(d)">回收入库</button>
<button v-if="d.status === '已驳回'" style="color: #FA8C16">重新申请</button>
</div> </div>
</td> </td>
</tr> </tr>
@ -210,24 +180,151 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
</div> </div>
</div> </div>
<!-- Pagination --> <!-- Detail Drawer -->
<div <div v-if="showDetailDrawer && currentDevice" class="fixed inset-0 z-50 flex justify-end" style="background-color: rgba(0,0,0,0.45)" @click.self="showDetailDrawer = false">
class="bg-white p-4 rounded-lg flex items-center justify-between" <div class="bg-white w-[520px] h-full flex flex-col" style="box-shadow: -4px 0 12px rgba(0,0,0,0.1)">
style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)" <div class="flex items-center gap-3 p-5 border-b" style="border-color: #F0F0F0">
> <button @click="showDetailDrawer = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.65)"><ArrowLeft :size="18" /></button>
<div class="text-sm" style="color: rgba(0, 0, 0, 0.65)">显示 1-10 / 156 </div> <h3 class="text-lg font-semibold">报废详情</h3>
<div class="flex items-center gap-2"> <span class="px-2 py-0.5 rounded text-xs" :style="getStatusStyle(currentDevice.status)">{{ currentDevice.status }}</span>
<button </div>
class="px-3 py-1 rounded border" <div class="flex-1 overflow-y-auto p-6 space-y-5">
style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.45)" <div class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
disabled <h4 class="text-base font-semibold mb-3">设备信息</h4>
> <div class="space-y-2 text-sm" style="color: rgba(0,0,0,0.65)">
上一页 <div>设备SN{{ currentDevice.sn }}</div>
</button> <div>设备型号{{ currentDevice.model }}</div>
<button class="px-3 py-1 rounded" style="background-color: #4a7c59; color: #fff">1</button> <div>报废日期{{ currentDevice.scrapDate }}</div>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button> <div>报废原因{{ currentDevice.reason }}</div>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button> <div>残值评估{{ currentDevice.residualValue }} </div>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button> </div>
</div>
<div class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<h4 class="text-base font-semibold mb-3">审批信息</h4>
<div class="space-y-2 text-sm" style="color: rgba(0,0,0,0.65)">
<div>申请人{{ currentDevice.applicant }}</div>
<div>审批人{{ currentDevice.approver || '待分配' }}</div>
<div>来源工单{{ currentDevice.sourceOrder }}</div>
</div>
</div>
<div v-if="currentDevice.recyclableParts.length" class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<h4 class="text-base font-semibold mb-3">可回收物料</h4>
<div class="flex flex-wrap gap-2">
<span v-for="p in currentDevice.recyclableParts" :key="p" class="px-3 py-1 rounded text-xs" style="background-color: #eef5f0; color: #4a7c59; border: 1px solid #a3c4ad">{{ p }}</span>
</div>
</div>
<!-- Approval Timeline -->
<div class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<h4 class="text-base font-semibold mb-3">审批记录</h4>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0" style="background-color: #F6FFED; border: 1px solid #B7EB8F"><Check :size="12" style="color: #52C41A" /></div>
<div class="text-sm" style="color: rgba(0,0,0,0.65)">
<div>{{ currentDevice.applicant }} 提交报废申请</div>
<div class="text-xs" style="color: rgba(0,0,0,0.35)">{{ currentDevice.scrapDate }}</div>
</div>
</div>
<div v-if="currentDevice.status !== '待审批'" class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0" :style="currentDevice.status === '已驳回' ? { backgroundColor: '#FFF1F0', border: '1px solid #FFCCC7' } : { backgroundColor: '#F6FFED', border: '1px solid #B7EB8F' }">
<X v-if="currentDevice.status === '已驳回'" :size="12" style="color: #FF4D4F" />
<Check v-else :size="12" style="color: #52C41A" />
</div>
<div class="text-sm" style="color: rgba(0,0,0,0.65)">
<div>{{ currentDevice.approver || '审批中' }} {{ currentDevice.status === '已驳回' ? '驳回申请' : '审批通过' }}</div>
</div>
</div>
<div v-if="currentDevice.status === '已回收'" class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0" style="background-color: #F6FFED; border: 1px solid #B7EB8F"><Check :size="12" style="color: #52C41A" /></div>
<div class="text-sm" style="color: rgba(0,0,0,0.65)">物料回收完成设备已注销</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-center gap-3 p-5 border-t" style="border-color: #F0F0F0">
<button class="px-6 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showDetailDrawer = false">关闭</button>
<button v-if="currentDevice.status === '待审批' || currentDevice.status === '审批中'" class="px-6 py-2 rounded text-white text-sm" style="background-color: #4a7c59" @click="showDetailDrawer = false; openApproval(currentDevice)">去审批</button>
<button v-if="currentDevice.status === '已审批'" class="px-6 py-2 rounded text-white text-sm" style="background-color: #52C41A" @click="showDetailDrawer = false; openRecycle(currentDevice)">回收入库</button>
</div>
</div>
</div>
<!-- Approval Drawer -->
<div v-if="showApprovalDrawer && currentDevice" class="fixed inset-0 z-50 flex justify-end" style="background-color: rgba(0,0,0,0.45)" @click.self="showApprovalDrawer = false">
<div class="bg-white w-[480px] h-full flex flex-col" style="box-shadow: -4px 0 12px rgba(0,0,0,0.1)">
<div class="flex items-center gap-3 p-5 border-b" style="border-color: #F0F0F0">
<button @click="showApprovalDrawer = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.65)"><ArrowLeft :size="18" /></button>
<h3 class="text-lg font-semibold">报废审批</h3>
</div>
<div class="flex-1 overflow-y-auto p-6 space-y-5">
<!-- Warning -->
<div class="p-4 rounded-lg flex items-start gap-3" style="background-color: #FFF1F0; border: 1px solid #FFCCC7">
<AlertTriangle :size="18" style="color: #FF4D4F; flex-shrink: 0; margin-top: 2px" />
<div class="text-sm" style="color: #CF1322">报废操作不可逆审批通过后设备将进入回收流程请仔细核实信息</div>
</div>
<!-- Device summary -->
<div class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<h4 class="text-sm font-semibold mb-3">报废设备</h4>
<div class="space-y-1 text-sm" style="color: rgba(0,0,0,0.65)">
<div>{{ currentDevice.sn }} · {{ currentDevice.model }}</div>
<div>报废原因{{ currentDevice.reason }}</div>
<div>残值评估{{ currentDevice.residualValue }} </div>
<div>申请人{{ currentDevice.applicant }}</div>
</div>
</div>
<!-- Recyclable parts -->
<div v-if="currentDevice.recyclableParts.length" class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<h4 class="text-sm font-semibold mb-3">可回收物料{{ currentDevice.recyclableParts.length }}</h4>
<div class="space-y-1 text-sm" style="color: rgba(0,0,0,0.65)">
<div v-for="p in currentDevice.recyclableParts" :key="p">{{ p }}</div>
</div>
</div>
<!-- Comment -->
<div>
<label class="block text-sm font-medium mb-2">审批意见</label>
<textarea v-model="approvalComment" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; min-height: 80px; resize: vertical" placeholder="请输入审批意见..."></textarea>
</div>
</div>
<div class="flex items-center justify-center gap-3 p-5 border-t" style="border-color: #F0F0F0">
<button class="px-6 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showApprovalDrawer = false">取消</button>
<button class="px-6 py-2 rounded text-sm" style="border: 1px solid #FF4D4F; color: #FF4D4F" @click="showApprovalDrawer = false">驳回</button>
<button class="px-6 py-2 rounded text-white text-sm" style="background-color: #4a7c59" @click="showApprovalDrawer = false">审批通过</button>
</div>
</div>
</div>
<!-- Recycle Drawer -->
<div v-if="showRecycleDrawer && currentDevice" class="fixed inset-0 z-50 flex justify-end" style="background-color: rgba(0,0,0,0.45)" @click.self="showRecycleDrawer = false">
<div class="bg-white w-[480px] h-full flex flex-col" style="box-shadow: -4px 0 12px rgba(0,0,0,0.1)">
<div class="flex items-center gap-3 p-5 border-b" style="border-color: #F0F0F0">
<button @click="showRecycleDrawer = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.65)"><ArrowLeft :size="18" /></button>
<h3 class="text-lg font-semibold">物料回收入库</h3>
</div>
<div class="flex-1 overflow-y-auto p-6 space-y-5">
<div class="p-5 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<h4 class="text-sm font-semibold mb-2">报废设备</h4>
<div class="text-sm" style="color: rgba(0,0,0,0.65)">{{ currentDevice.sn }} · {{ currentDevice.model }}</div>
</div>
<!-- Recyclable parts checklist -->
<div>
<h4 class="text-sm font-semibold mb-3">物料检测与回收</h4>
<div v-if="currentDevice.recyclableParts.length === 0" class="text-sm" style="color: rgba(0,0,0,0.45)">无可回收物料</div>
<div v-else class="space-y-2">
<label v-for="p in currentDevice.recyclableParts" :key="p" class="flex items-center gap-3 p-3 rounded cursor-pointer text-sm" :style="{ backgroundColor: recycleChecked.has(p) ? '#F6FFED' : '#FAFAFA', border: recycleChecked.has(p) ? '1px solid #B7EB8F' : '1px solid #F0F0F0' }">
<input type="checkbox" :checked="recycleChecked.has(p)" @change="recycleChecked.has(p) ? recycleChecked.delete(p) : recycleChecked.add(p)" class="w-4 h-4" style="accent-color: #4a7c59" />
<span>{{ p }}</span>
<span v-if="recycleChecked.has(p)" class="ml-auto text-xs" style="color: #52C41A">已检测</span>
</label>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-2">回收备注</label>
<textarea v-model="recycleNote" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; min-height: 70px; resize: vertical" placeholder="请输入回收备注..."></textarea>
</div>
</div>
<div class="flex items-center justify-center gap-3 p-5 border-t" style="border-color: #F0F0F0">
<button class="px-6 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showRecycleDrawer = false">取消</button>
<button class="px-6 py-2 rounded text-white text-sm" style="background-color: #52C41A" @click="showRecycleDrawer = false; router.push('/registration')">确认回收入库</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,8 +21,6 @@ export const router = createRouter({
{ path: 'repair/:orderId', component: () => import('./pages/RepairOrderDetail.vue') }, { path: 'repair/:orderId', component: () => import('./pages/RepairOrderDetail.vue') },
{ path: 'scrap', component: () => import('./pages/ScrapManagement.vue') }, { path: 'scrap', component: () => import('./pages/ScrapManagement.vue') },
{ path: 'firmware', component: () => import('./pages/FirmwareLibrary.vue') }, { path: 'firmware', component: () => import('./pages/FirmwareLibrary.vue') },
{ path: 'firmware-upgrade', component: () => import('./pages/PlaceholderPage.vue'), props: { title: '升级任务' } },
{ path: 'calibration-reports', component: () => import('./pages/PlaceholderPage.vue'), props: { title: '校准报告' } },
{ path: 'config-files', component: () => import('./pages/ConfigFileManagement.vue') }, { path: 'config-files', component: () => import('./pages/ConfigFileManagement.vue') },
{ path: 'config-files/:configId', component: () => import('./pages/ParameterConfiguration.vue') }, { path: 'config-files/:configId', component: () => import('./pages/ParameterConfiguration.vue') },
{ path: 'reports', component: () => import('./pages/PlaceholderPage.vue'), props: { title: '数据报表' } }, { path: 'reports', component: () => import('./pages/PlaceholderPage.vue'), props: { title: '数据报表' } },