根据0417会议评审提出的问题修改:1、授权模块类型;2、单台设备的授权项和配置文件支持修改;3、登记设备的生产批次手动点击选择或者输入录入;4、每台设备的采集板和发射板、主板的SN扫码录入,保留手动输入的方式;5、设备型号BOM表里的板卡可以支持多个版本兼容,如果两块采集板,则需要两块的版本一致。
This commit is contained in:
parent
7d8941b76d
commit
bf21432ebd
|
|
@ -45,3 +45,88 @@
|
|||
{"timestamp":"01:03:11.671","source":"Server","level":"LOG","message":"✓ Compiled in 35ms"}
|
||||
{"timestamp":"01:03:41.505","source":"Server","level":"LOG","message":"✓ Compiled in 50ms"}
|
||||
{"timestamp":"01:03:51.542","source":"Server","level":"LOG","message":"✓ Compiled in 66ms"}
|
||||
{"timestamp":"19:20:28.294","source":"Server","level":"LOG","message":"✓ Compiled in 44ms"}
|
||||
{"timestamp":"19:20:47.357","source":"Server","level":"LOG","message":"✓ Compiled in 39ms"}
|
||||
{"timestamp":"19:20:57.178","source":"Server","level":"LOG","message":"✓ Compiled in 33ms"}
|
||||
{"timestamp":"20:05:25.241","source":"Server","level":"LOG","message":"✓ Compiled in 29ms"}
|
||||
{"timestamp":"20:06:03.061","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"20:06:40.080","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"23:00:54.762","source":"Server","level":"LOG","message":"✓ Compiled in 92ms"}
|
||||
{"timestamp":"23:01:33.547","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: BomContent is not defined"}
|
||||
{"timestamp":"23:01:33.551","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: BomContent is not defined\\u001b[39m\\n\\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\\u001b[39m\\n \\u001b[90m40 |\\u001b[0m \\u001b[36mreturn\\u001b[0m (\\n \\u001b[90m41 |\\u001b[0m <\\u001b[33mSuspense\\u001b[0m fallback={<div style={{ padding: \\u001b[35m24\\u001b[0m }}>加载中...</div>}>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m42 |\\u001b[0m <\\u001b[33mBomContent\\u001b[0m />\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m43 |\\u001b[0m </\\u001b[33mSuspense\\u001b[0m>\\n \\u001b[90m44 |\\u001b[0m )\\n \\u001b[90m45 |\\u001b[0m }\""}
|
||||
{"timestamp":"23:01:33.552","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: BomContent is not defined\u001b[39m\n\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\u001b[39m\n \u001b[90m40 |\u001b[0m \u001b[36mreturn\u001b[0m (\n \u001b[90m41 |\u001b[0m <\u001b[33mSuspense\u001b[0m fallback={<div style={{ padding: \u001b[35m24\u001b[0m }}>加载中...</div>}>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m42 |\u001b[0m <\u001b[33mBomContent\u001b[0m />\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m43 |\u001b[0m </\u001b[33mSuspense\u001b[0m>\n \u001b[90m44 |\u001b[0m )\n \u001b[90m45 |\u001b[0m }"}
|
||||
{"timestamp":"23:02:05.108","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"23:02:07.692","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: BomContent is not defined\\u001b[39m\\n\\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\\u001b[39m\\n \\u001b[90m40 |\\u001b[0m \\u001b[36mreturn\\u001b[0m (\\n \\u001b[90m41 |\\u001b[0m <\\u001b[33mSuspense\\u001b[0m fallback={<div style={{ padding: \\u001b[35m24\\u001b[0m }}>加载中...</div>}>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m42 |\\u001b[0m <\\u001b[33mBomContent\\u001b[0m />\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m43 |\\u001b[0m </\\u001b[33mSuspense\\u001b[0m>\\n \\u001b[90m44 |\\u001b[0m )\\n \\u001b[90m45 |\\u001b[0m }\""}
|
||||
{"timestamp":"23:02:07.693","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: BomContent is not defined\u001b[39m\n\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\u001b[39m\n \u001b[90m40 |\u001b[0m \u001b[36mreturn\u001b[0m (\n \u001b[90m41 |\u001b[0m <\u001b[33mSuspense\u001b[0m fallback={<div style={{ padding: \u001b[35m24\u001b[0m }}>加载中...</div>}>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m42 |\u001b[0m <\u001b[33mBomContent\u001b[0m />\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m43 |\u001b[0m </\u001b[33mSuspense\u001b[0m>\n \u001b[90m44 |\u001b[0m )\n \u001b[90m45 |\u001b[0m }"}
|
||||
{"timestamp":"23:02:07.694","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: BomContent is not defined"}
|
||||
{"timestamp":"23:02:09.616","source":"Server","level":"ERROR","message":"⨯ ReferenceError: BomContent is not defined"}
|
||||
{"timestamp":"23:02:09.804","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"23:02:09.884","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: BomContent is not defined"}
|
||||
{"timestamp":"23:02:09.886","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: BomContent is not defined\\u001b[39m\\n\\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\\u001b[39m\\n \\u001b[90m40 |\\u001b[0m \\u001b[36mreturn\\u001b[0m (\\n \\u001b[90m41 |\\u001b[0m <\\u001b[33mSuspense\\u001b[0m fallback={<div style={{ padding: \\u001b[35m24\\u001b[0m }}>加载中...</div>}>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m42 |\\u001b[0m <\\u001b[33mBomContent\\u001b[0m />\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m43 |\\u001b[0m </\\u001b[33mSuspense\\u001b[0m>\\n \\u001b[90m44 |\\u001b[0m )\\n \\u001b[90m45 |\\u001b[0m }\""}
|
||||
{"timestamp":"23:02:09.886","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: BomContent is not defined\u001b[39m\n\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\u001b[39m\n \u001b[90m40 |\u001b[0m \u001b[36mreturn\u001b[0m (\n \u001b[90m41 |\u001b[0m <\u001b[33mSuspense\u001b[0m fallback={<div style={{ padding: \u001b[35m24\u001b[0m }}>加载中...</div>}>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m42 |\u001b[0m <\u001b[33mBomContent\u001b[0m />\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m43 |\u001b[0m </\u001b[33mSuspense\u001b[0m>\n \u001b[90m44 |\u001b[0m )\n \u001b[90m45 |\u001b[0m }"}
|
||||
{"timestamp":"23:02:12.140","source":"Server","level":"ERROR","message":"⨯ ReferenceError: BomContent is not defined"}
|
||||
{"timestamp":"23:02:12.297","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"23:02:12.478","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: BomContent is not defined\\u001b[39m\\n\\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\\u001b[39m\\n \\u001b[90m40 |\\u001b[0m \\u001b[36mreturn\\u001b[0m (\\n \\u001b[90m41 |\\u001b[0m <\\u001b[33mSuspense\\u001b[0m fallback={<div style={{ padding: \\u001b[35m24\\u001b[0m }}>加载中...</div>}>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m42 |\\u001b[0m <\\u001b[33mBomContent\\u001b[0m />\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m43 |\\u001b[0m </\\u001b[33mSuspense\\u001b[0m>\\n \\u001b[90m44 |\\u001b[0m )\\n \\u001b[90m45 |\\u001b[0m }\""}
|
||||
{"timestamp":"23:02:12.478","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: BomContent is not defined\u001b[39m\n\u001b[31m at BomPage (src/app/models/bom/page.tsx:42:8)\u001b[39m\n \u001b[90m40 |\u001b[0m \u001b[36mreturn\u001b[0m (\n \u001b[90m41 |\u001b[0m <\u001b[33mSuspense\u001b[0m fallback={<div style={{ padding: \u001b[35m24\u001b[0m }}>加载中...</div>}>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m42 |\u001b[0m <\u001b[33mBomContent\u001b[0m />\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m43 |\u001b[0m </\u001b[33mSuspense\u001b[0m>\n \u001b[90m44 |\u001b[0m )\n \u001b[90m45 |\u001b[0m }"}
|
||||
{"timestamp":"23:02:12.479","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: BomContent is not defined"}
|
||||
{"timestamp":"23:02:16.733","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"23:06:18.411","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"88:23:26.407","source":"Server","level":"LOG","message":"✓ Compiled in 50ms"}
|
||||
{"timestamp":"88:23:40.714","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"88:23:40.900","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: acqVersionConsistent is not defined"}
|
||||
{"timestamp":"88:23:40.939","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: acqVersionConsistent is not defined\\u001b[39m\\n\\u001b[31m at BomContent (src/app/models/bom/page.tsx:107:32)\\n at BomPage (src/app/models/bom/page.tsx:40:25)\\u001b[39m\\n \\u001b[90m105 |\\u001b[0m 要求同一台设备的采集板版本必须一致\\n \\u001b[90m106 |\\u001b[0m </label>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m107 |\\u001b[0m {enforceAcqVersion && !acqVersionConsistent && (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m108 |\\u001b[0m <span style={{ display: \\u001b[32m'flex'\\u001b[0m, alignItems: \\u001b[32m'center'\\u001b[0m, gap: \\u001b[35m4\\u001b[0m, fontSize: \\u001b[35m12\\u001b[0m, color: \\u001b[32m'#FF4D4F'\\u001b[0m }}>\\n \\u001b[90m109 |\\u001b[0m <\\u001b[33mAlertTriangle\\u001b[0m size={\\u001b[35m13\\u001b[0m} />当前采集板配置的版本列表不一致\\n \\u001b[90m110 |\\u001b[0m </span>\""}
|
||||
{"timestamp":"88:23:40.941","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: acqVersionConsistent is not defined\u001b[39m\n\u001b[31m at BomContent (src/app/models/bom/page.tsx:107:32)\n at BomPage (src/app/models/bom/page.tsx:40:25)\u001b[39m\n \u001b[90m105 |\u001b[0m 要求同一台设备的采集板版本必须一致\n \u001b[90m106 |\u001b[0m </label>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m107 |\u001b[0m {enforceAcqVersion && !acqVersionConsistent && (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m108 |\u001b[0m <span style={{ display: \u001b[32m'flex'\u001b[0m, alignItems: \u001b[32m'center'\u001b[0m, gap: \u001b[35m4\u001b[0m, fontSize: \u001b[35m12\u001b[0m, color: \u001b[32m'#FF4D4F'\u001b[0m }}>\n \u001b[90m109 |\u001b[0m <\u001b[33mAlertTriangle\u001b[0m size={\u001b[35m13\u001b[0m} />当前采集板配置的版本列表不一致\n \u001b[90m110 |\u001b[0m </span>"}
|
||||
{"timestamp":"88:23:56.831","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"88:23:56.833","source":"Server","level":"LOG","message":"✓ Compiled in 116ms"}
|
||||
{"timestamp":"88:23:57.529","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"88:24:06.404","source":"Server","level":"LOG","message":"✓ Compiled in 55ms"}
|
||||
{"timestamp":"88:24:17.728","source":"Server","level":"LOG","message":"✓ Compiled in 58ms"}
|
||||
{"timestamp":"88:24:27.923","source":"Server","level":"LOG","message":"✓ Compiled in 48ms"}
|
||||
{"timestamp":"88:24:43.110","source":"Server","level":"LOG","message":"✓ Compiled in 43ms"}
|
||||
{"timestamp":"88:35:01.386","source":"Server","level":"LOG","message":"✓ Compiled in 105ms"}
|
||||
{"timestamp":"88:35:29.636","source":"Server","level":"LOG","message":"✓ Compiled in 142ms"}
|
||||
{"timestamp":"88:35:40.143","source":"Server","level":"LOG","message":"✓ Compiled in 64ms"}
|
||||
{"timestamp":"88:35:49.785","source":"Server","level":"LOG","message":"✓ Compiled in 56ms"}
|
||||
{"timestamp":"88:36:08.901","source":"Server","level":"LOG","message":"✓ Compiled in 88ms"}
|
||||
{"timestamp":"88:58:06.224","source":"Server","level":"LOG","message":"✓ Compiled in 36ms"}
|
||||
{"timestamp":"88:58:17.207","source":"Server","level":"LOG","message":"✓ Compiled in 74ms"}
|
||||
{"timestamp":"88:58:34.824","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"88:58:48.965","source":"Server","level":"LOG","message":"✓ Compiled in 38ms"}
|
||||
{"timestamp":"88:58:59.198","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"89:00:56.122","source":"Server","level":"LOG","message":"✓ Compiled in 56ms"}
|
||||
{"timestamp":"89:01:12.179","source":"Server","level":"LOG","message":"✓ Compiled in 43ms"}
|
||||
{"timestamp":"89:01:26.485","source":"Server","level":"LOG","message":"✓ Compiled in 64ms"}
|
||||
{"timestamp":"89:01:33.256","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"89:02:11.783","source":"Server","level":"LOG","message":"✓ Compiled in 41ms"}
|
||||
{"timestamp":"89:02:29.234","source":"Server","level":"LOG","message":"✓ Compiled in 49ms"}
|
||||
{"timestamp":"89:02:38.925","source":"Server","level":"LOG","message":"✓ Compiled in 41ms"}
|
||||
{"timestamp":"89:26:07.880","source":"Server","level":"LOG","message":"✓ Compiled in 64ms"}
|
||||
{"timestamp":"89:26:16.444","source":"Server","level":"LOG","message":"✓ Compiled in 33ms"}
|
||||
{"timestamp":"89:26:28.187","source":"Server","level":"LOG","message":"✓ Compiled in 35ms"}
|
||||
{"timestamp":"89:26:41.392","source":"Server","level":"LOG","message":"✓ Compiled in 41ms"}
|
||||
{"timestamp":"89:26:51.328","source":"Server","level":"LOG","message":"✓ Compiled in 51ms"}
|
||||
{"timestamp":"89:50:00.224","source":"Server","level":"LOG","message":"✓ Compiled in 30ms"}
|
||||
{"timestamp":"89:50:02.783","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"89:51:22.343","source":"Server","level":"LOG","message":"✓ Compiled in 57ms"}
|
||||
{"timestamp":"91:29:09.929","source":"Server","level":"LOG","message":"✓ Compiled in 37ms"}
|
||||
{"timestamp":"91:29:22.593","source":"Server","level":"LOG","message":"✓ Compiled in 36ms"}
|
||||
{"timestamp":"91:30:09.323","source":"Server","level":"LOG","message":"✓ Compiled in 38ms"}
|
||||
{"timestamp":"91:32:54.635","source":"Server","level":"LOG","message":"✓ Compiled in 54ms"}
|
||||
{"timestamp":"91:33:04.580","source":"Server","level":"LOG","message":"✓ Compiled in 115ms"}
|
||||
{"timestamp":"91:33:16.061","source":"Server","level":"LOG","message":"✓ Compiled in 50ms"}
|
||||
{"timestamp":"91:33:16.230","source":"Browser","level":"ERROR","message":"A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components"}
|
||||
{"timestamp":"91:33:16.255","source":"Server","level":"ERROR","message":"[browser] \"A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components\" \"\""}
|
||||
{"timestamp":"91:33:16.256","source":"Browser","level":"ERROR","message":"A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components \"\""}
|
||||
{"timestamp":"91:33:25.029","source":"Server","level":"LOG","message":"✓ Compiled in 44ms"}
|
||||
{"timestamp":"91:33:35.115","source":"Server","level":"LOG","message":"✓ Compiled in 51ms"}
|
||||
{"timestamp":"91:35:27.639","source":"Server","level":"LOG","message":"✓ Compiled in 57ms"}
|
||||
{"timestamp":"91:35:38.098","source":"Server","level":"LOG","message":"✓ Compiled in 41ms"}
|
||||
{"timestamp":"91:36:21.846","source":"Server","level":"LOG","message":"✓ Compiled in 134ms"}
|
||||
{"timestamp":"91:46:26.786","source":"Server","level":"LOG","message":"✓ Compiled in 54ms"}
|
||||
{"timestamp":"91:46:46.111","source":"Server","level":"LOG","message":"✓ Compiled in 116ms"}
|
||||
{"timestamp":"91:47:02.457","source":"Server","level":"LOG","message":"✓ Compiled in 58ms"}
|
||||
{"timestamp":"91:51:26.728","source":"Server","level":"LOG","message":"✓ Compiled in 128ms"}
|
||||
{"timestamp":"91:51:39.901","source":"Server","level":"LOG","message":"✓ Compiled in 40ms"}
|
||||
{"timestamp":"91:52:32.250","source":"Server","level":"LOG","message":"✓ Compiled in 43ms"}
|
||||
{"timestamp":"91:52:43.610","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"91:53:08.498","source":"Server","level":"LOG","message":"✓ Compiled in 38ms"}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"/devices/page": "app/devices/page.js",
|
||||
"/firmware/page": "app/firmware/page.js",
|
||||
"/licenses/page": "app/licenses/page.js",
|
||||
"/models/bom/page": "app/models/bom/page.js",
|
||||
"/models/page": "app/models/page.js",
|
||||
"/page": "app/page.js",
|
||||
"/registration/page": "app/registration/page.js",
|
||||
|
|
|
|||
129
.next/dev/trace
129
.next/dev/trace
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
// This file is generated automatically by Next.js
|
||||
// Do not edit this file manually
|
||||
|
||||
type AppRoutes = "/" | "/board-cards" | "/board-cards/register" | "/boards" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/models" | "/registration" | "/repair" | "/scrap"
|
||||
type AppRoutes = "/" | "/board-cards" | "/board-cards/register" | "/boards" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/models" | "/models/bom" | "/registration" | "/repair" | "/scrap"
|
||||
type PageRoutes = never
|
||||
type LayoutRoutes = "/"
|
||||
type RedirectRoutes = never
|
||||
|
|
@ -20,6 +20,7 @@ interface ParamMap {
|
|||
"/firmware": {}
|
||||
"/licenses": {}
|
||||
"/models": {}
|
||||
"/models/bom": {}
|
||||
"/registration": {}
|
||||
"/repair": {}
|
||||
"/scrap": {}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,15 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/models/bom/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/models/bom">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/models/bom/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/models/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/models">> = Specific
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
### 1、授权模块
|
||||
根据TM类型来
|
||||
一维
|
||||
二维
|
||||
三维
|
||||
跨孔
|
||||
水上
|
||||
电流场
|
||||
|
||||
### 2、授权文件:由授权项+配置文件生成
|
||||
|
||||
### 3、单台设备的授权项和配置文件支持修改
|
||||
|
||||
### 4、新增登记设备的生产批次手动点击选择或者输入录入
|
||||
|
||||
### 5、每台设备的采集板和发射板、主板的SN是扫码录入,保留手动输入的形式
|
||||
|
||||
### 6、平台的checklist中拍照上传功能
|
||||
- 平台支持手机web端、手机拍照上传
|
||||
- 平台支持上传照片时页面生成一个URL二维码,手机扫二维码进入上传拍照上传页面
|
||||
- 使用平板登录平台,直接拍照上传(最终考虑这个方案)**
|
||||
|
||||
### 7、设备型号BOM表里板卡需要加上版本(可以多个版本),但是需要采集板两块的版本一致(勾选框)
|
||||
265
docs/产品原型文档.md
265
docs/产品原型文档.md
|
|
@ -1,8 +1,8 @@
|
|||
# 地空业务支撑平台 —— 生产管理子系统 产品原型文档
|
||||
|
||||
> 版本:v1.0
|
||||
> 更新日期:2026-04-08
|
||||
> 技术栈:Vue 3 + Vue Router + Element Plus + TailwindCSS + Lucide Icons
|
||||
> 版本:v2.0
|
||||
> 更新日期:2026-04-20
|
||||
> 技术栈:Next.js 16 + React 19 + TailwindCSS + Lucide Icons
|
||||
> 主色调:`#4a7c59`(绿色系)
|
||||
|
||||
---
|
||||
|
|
@ -36,10 +36,8 @@
|
|||
| — | 首页 | `/` | — |
|
||||
| 设备 | 设备列表 | `/devices` | Monitor |
|
||||
| 设备 | 设备型号管理 | `/models` | Settings2 |
|
||||
| 设备 | 板卡型号管理 | `/boards` | Cpu |
|
||||
| 授权 | 授权管理 | `/licenses` | Key |
|
||||
| 配置 | 配置管理 | `/config-files` | FileCode |
|
||||
| 校准 | 校准记录 | `/calibration` | Gauge |
|
||||
| 板卡 | 板卡列表 | `/board-cards` | Gauge |
|
||||
| 板卡 | 板卡版本管理 | `/boards` | Cpu |
|
||||
| 维修 | 维修工单 | `/repair` | Wrench |
|
||||
| 维修 | 报废回收 | `/scrap` | Recycle |
|
||||
|
||||
|
|
@ -82,7 +80,7 @@
|
|||
|
||||
| 任务组 | 跳转 |
|
||||
|--------|------|
|
||||
| 校准即将到期 | `/calibration` |
|
||||
| 校准即将到期 | `/board-cards` |
|
||||
| 维修工单 | `/repair` |
|
||||
| 固件升级通知 | `/firmware` |
|
||||
| 授权即将到期 | `/licenses` |
|
||||
|
|
@ -106,10 +104,13 @@
|
|||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| 型号名称 | 如 GD-30 Supreme |
|
||||
| 编码 | 如 GD30-2024 |
|
||||
| 编码 | 如 GD30 |
|
||||
| 描述 | 如 高端高密度电法仪 |
|
||||
| 状态 | 在产(绿色标签)/ 停产(黄色标签) |
|
||||
| 操作 | 编辑 / 授权(跳转`/licenses`)/ 配置(跳转`/config-files`) |
|
||||
| 创建日期 | 日期 |
|
||||
| 操作 | 编辑 / 授权项(跳转`/licenses?model=型号名`)/ 配置(跳转`/config-files?model=型号名`)/ 固件(跳转`/firmware?model=型号代码`)/ BOM表(跳转`/models/bom?model=型号代码`) |
|
||||
|
||||
- 操作列为平铺按钮,不使用下拉菜单
|
||||
- 右上角按钮:「新增设备型号」
|
||||
|
||||
**3. 装配 Checklist 模板**
|
||||
|
|
@ -235,15 +236,11 @@
|
|||
|
||||
| 字段 | 示例 |
|
||||
|------|------|
|
||||
| 授权ID | LIC-2025-0001 |
|
||||
| 授权状态 | 已激活 |
|
||||
| 授权类型 | 正式授权 |
|
||||
| 生效日期 | 2025-02-10 |
|
||||
| 到期日期 | 2026-02-10 |
|
||||
| 剩余天数 | 317天 |
|
||||
|
||||
- 授权功能模块标签列表:1D SP, 2D SP, 3D SP, 1D VES, 2D ERT, 3D ERT, 1D IP, 2D IP, 3D IP
|
||||
- 授权文件下载链接
|
||||
- 授权功能模块标签列表:一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块, 水上, 跨孔, 电流法
|
||||
- 右上角「修改授权项」按钮,打开编辑抽屉可修改到期时间和勾选/取消授权模块
|
||||
- 未配置时显示「配置授权项」按钮
|
||||
|
||||
**4. 装配记录卡片**
|
||||
|
||||
|
|
@ -279,6 +276,9 @@
|
|||
| 配置文件版本 | v1.2.0 |
|
||||
| 配置同步时间 | 2024-03-01 10:35 |
|
||||
|
||||
- 配置文件 Tab 右上角「更换配置」按钮,打开抽屉可选择其他版本的配置文件替换
|
||||
- 未配置时显示「选择配置文件」按钮
|
||||
|
||||
**7. 维修历史(时间线)**
|
||||
- 每条记录:日期、类型(固件升级/主板更换/常规保养)、操作人、描述
|
||||
- 右上角「查看全部」链接跳转 `/repair`
|
||||
|
|
@ -300,11 +300,14 @@
|
|||
| 字段 | 类型 | 必填 | 示例 |
|
||||
|------|------|------|------|
|
||||
| 设备型号 | 下拉选择(GD-30/GD-20/GD-10 Supreme) | 是 | GD-30 Supreme |
|
||||
| 主机SN号 | 文本输入 | 是 | GD30-20240308-001 |
|
||||
| 主板SN号 | 文本输入 | 是 | MB20240308001 |
|
||||
| 主机SN号 | 文本输入 + 扫码按钮 | 是 | GD30-20240308-001 |
|
||||
| 主板SN号 | 文本输入 + 扫码按钮 | 是 | MB20240308001 |
|
||||
| 生产批次 | 下拉选择 + 手动输入 | 是 | BATCH-2025-Q1-001 |
|
||||
| 装机测试状态 | 下拉(测试通过/测试不通过) | — | 测试通过 |
|
||||
| 生产日期 | 日期选择器 | — | 2024-03-08 |
|
||||
| 登记人 | 文本输入(只读) | — | 张工 |
|
||||
| 登记人 | 文本输入 | — | 张工 |
|
||||
|
||||
> 主机SN号和主板SN号支持扫码录入(扫码图标按钮),同时保留手动输入。
|
||||
|
||||
**2. 型号匹配提示横幅**
|
||||
- 成功(绿色):显示匹配到的授权文件、配置文件、固件版本
|
||||
|
|
@ -315,13 +318,15 @@
|
|||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| 物料编码 | 如 MB-2024-001 |
|
||||
| 物料名称 | 主协板/采集板/测控板/发射板/升压板/外壳机箱 |
|
||||
| SN号 | 板卡SN号 |
|
||||
| 物料名称 | 主协板/采集板/发射板/升压板/外壳机箱 |
|
||||
| SN号 | 板卡SN号(带扫码图标,支持扫码录入) |
|
||||
| 型号 | 如 MB-V2.3 |
|
||||
| 版本 | 板卡版本号 |
|
||||
| 校准状态 | 已校准/无需校准 |
|
||||
| 数量 | 数字 |
|
||||
| 操作 | 编辑 / 删除 |
|
||||
|
||||
- 采集板版本一致性检查:当有多块采集板时(如GD-30有2块),自动检测版本是否一致,不一致时显示红色警告,一致时显示绿色确认
|
||||
- 右上角按钮:「导入」(打开Excel导入弹窗)
|
||||
|
||||
**4. 装配 Checklist(列表)**
|
||||
|
|
@ -345,7 +350,8 @@
|
|||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| 照片网格 | 已上传照片缩略图(可删除)+ 添加照片按钮 |
|
||||
| 上传方式 | 两个入口:「直接拍照上传」(平板端直接拍照)和「手机扫码上传」(生成二维码,手机扫码拍照上传) |
|
||||
| 照片网格 | 已上传照片缩略图(可删除)+ 添加更多按钮 |
|
||||
| 装配记录信息 | 文本域,输入装配记录 |
|
||||
| 操作 | 取消 / 确认上传(显示照片数量) |
|
||||
|
||||
|
|
@ -353,37 +359,46 @@
|
|||
|
||||
### 2.7 授权管理
|
||||
|
||||
**路由**:`/licenses`
|
||||
**路由**:`/licenses`(也支持 `?model=型号名` 参数筛选)
|
||||
|
||||
#### 功能描述
|
||||
管理设备授权文件,按设备型号管理授权模块配置。
|
||||
管理设备授权模块配置。每个设备型号对应一套授权模块,所有该型号设备共享同一套授权项。授权文件由授权项 + 配置文件共同生成。
|
||||
|
||||
#### 页面区块
|
||||
|
||||
**1. 信息提示横幅(青色)**
|
||||
- 说明授权文件按设备型号管理,设备在APP激活时自动下载对应型号的授权文件
|
||||
- 说明每个设备型号对应一套授权模块配置
|
||||
|
||||
**2. 筛选条件**
|
||||
|
||||
| 筛选项 | 类型 |
|
||||
|--------|------|
|
||||
| 设备型号 | 下拉(全部/GD-10/GD-20/GD-30) |
|
||||
| 状态 | 下拉(全部/已发布/草稿/已停用) |
|
||||
| 查询 | 按钮 |
|
||||
|
||||
**3. 授权列表(表格)**
|
||||
**3. 授权列表(表格,每型号一条记录)**
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| 适配型号 | 如 GD-10 Supreme |
|
||||
| 授权模块 | 如 1D SP, 2D SP, 1D VES... |
|
||||
| 有效期 | 永久 / 1年 / 2年 |
|
||||
| 创建日期 | 日期 |
|
||||
| 状态 | 已发布(绿)/ 草稿(黄)/ 已停用(灰) |
|
||||
| 操作 | 详情 / 下载 / 发布(草稿时)/ 停用(已发布时) |
|
||||
| 设备型号 | 如 GD-30 |
|
||||
| 授权模块 | 标签形式展示已授权模块 |
|
||||
| 更新日期 | 日期 |
|
||||
| 操作 | 编辑 |
|
||||
|
||||
- 右上角按钮:「导出」「选择授权项」
|
||||
- 分页
|
||||
- 从型号管理页跳转时自动按型号筛选,显示返回按钮
|
||||
|
||||
#### 授权模块定义(6个模块)
|
||||
|
||||
| 模块名称 | 说明 | GD-10 | GD-20 | GD-30 |
|
||||
|----------|------|-------|-------|-------|
|
||||
| 一维自电/电阻率/激电测试模块 | 包含一维自然电位法、电阻率测深、激发极化测深 | ✓ | ✓ | ✓ |
|
||||
| 二维自电/电阻率/激电测试模块 | 包含二维自然电位法、电阻率成像、激发极化成像 | ✓ | ✓ | ✓ |
|
||||
| 三维自电/电阻率/激电测试模块 | 包含三维自然电位法、电阻率成像、激发极化成像 | ✗ | ✓ | ✓ |
|
||||
| 水上 | 水上电法探测 | ✗ | ✗ | ✓ |
|
||||
| 跨孔 | 跨孔电阻率成像 | ✗ | ✗ | ✓ |
|
||||
| 电流法 | 电流场法 | ✗ | ✗ | ✓ |
|
||||
|
||||
> 注:一维/二维/三维各为一个整体模块,不再拆分自电、电阻率、激电。
|
||||
|
||||
#### 抽屉:选择授权项(640px宽)
|
||||
|
||||
|
|
@ -393,31 +408,15 @@
|
|||
| 授权有效期 | 下拉(永久/1年/2年/自定义) | — |
|
||||
| 到期日期 | 日期选择器 | 仅"自定义"时显示 |
|
||||
|
||||
**功能授权项列表(表格+勾选)**
|
||||
|
||||
| 授权项名称 | GD-10 | GD-20 | GD-30 |
|
||||
|------------|-------|-------|-------|
|
||||
| 1D SP | ✓ | ✓ | ✓ |
|
||||
| 2D SP | ✓ | ✓ | ✓ |
|
||||
| 3D SP | ✗ | ✓ | ✓ |
|
||||
| 1D VES | ✓ | ✓ | ✓ |
|
||||
| 2D ERT | ✓ | ✓ | ✓ |
|
||||
| 3D ERT | ✗ | ✓ | ✓ |
|
||||
| 1D IP | ✓ | ✓ | ✓ |
|
||||
| 2D IP | ✓ | ✓ | ✓ |
|
||||
| 3D IP | ✗ | ✓ | ✓ |
|
||||
| 跨孔(Cross-Hole) | ✗ | ✓ | ✓ |
|
||||
| 水上(Marine) | ✗ | ✓ | ✓ |
|
||||
|
||||
- 顶部显示:已选 X / Y 项
|
||||
- 操作:全选 / 清空
|
||||
- 底部:取消 / 保存
|
||||
- 授权项按分类(一维/二维/三维/水上/跨孔/电流法)分组展示
|
||||
- 底部提示:授权文件由选定的授权项与对应型号的配置文件共同生成
|
||||
- 操作:全选 / 清空 / 取消 / 保存
|
||||
|
||||
---
|
||||
|
||||
### 2.8 配置文件管理
|
||||
|
||||
**路由**:`/config-files`
|
||||
**路由**:`/config-files`(也支持 `?model=型号名` 参数筛选)
|
||||
|
||||
#### 功能描述
|
||||
管理设备型号配置文件,包含发射参数、采集参数、网络参数等。
|
||||
|
|
@ -540,10 +539,22 @@
|
|||
|
||||
### 2.10 固件库
|
||||
|
||||
**路由**:`/firmware`
|
||||
**路由**:`/firmware`(支持 `?board=板卡版本` 和 `?model=型号代码` 参数)
|
||||
|
||||
#### 功能描述
|
||||
管理固件版本,支持上传、查看详情、下载。
|
||||
管理固件版本,支持上传、查看详情、下载。支持板卡固件和设备型号固件两种模式。
|
||||
|
||||
#### 模式说明
|
||||
|
||||
**1. 通用模式**(直接访问 `/firmware`)
|
||||
- Tab 筛选:全部 / 主协板 / 采集板 / 发射板 / 升压板 / 主机固件 / 计算单元固件
|
||||
|
||||
**2. 板卡固件模式**(从板卡版本管理跳转,`?board=MB-V1.8`)
|
||||
- 仅显示该板卡版本的固件,带返回按钮
|
||||
|
||||
**3. 设备型号固件模式**(从型号管理跳转,`?model=GD30`)
|
||||
- 两个 Tab:主机固件 / 计算单元固件(带数量角标)
|
||||
- 带返回按钮,标题显示型号代码
|
||||
|
||||
#### 页面区块
|
||||
|
||||
|
|
@ -584,9 +595,93 @@
|
|||
|
||||
---
|
||||
|
||||
### 2.11 校准管理
|
||||
### 2.11 板卡列表
|
||||
|
||||
**路由**:`/calibration`
|
||||
**路由**:`/board-cards`
|
||||
|
||||
#### 功能描述
|
||||
管理所有板卡实例,跟踪板卡状态与校准信息。采集板支持查看校准文件。
|
||||
|
||||
#### 页面区块
|
||||
|
||||
**1. 统计卡片(5个)**
|
||||
- 板卡总数、在库、已装配、故障、待校准
|
||||
|
||||
**2. 筛选条件**
|
||||
|
||||
| 筛选项 | 类型 |
|
||||
|--------|------|
|
||||
| 板卡类型 | 下拉(全部/主协板/采集板/发射板/升压板) |
|
||||
| 板卡状态 | 下拉(全部/在库/已装配/故障/报废) |
|
||||
| 校准状态 | 下拉(全部/合格/不合格/待校准) |
|
||||
| SN / 设备SN | 文本搜索 |
|
||||
|
||||
**3. 板卡列表(表格)**
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| 板卡SN | 唯一标识 |
|
||||
| 类型 | 主协板/采集板/发射板/升压板 |
|
||||
| 版本 | 板卡版本号 |
|
||||
| 固件 | 固件版本 |
|
||||
| 状态 | 在库/已装配/故障/报废 |
|
||||
| 所属设备 | 关联设备SN |
|
||||
| 校准状态 | 合格/不合格/待校准(仅采集板) |
|
||||
| 操作 | 详情 / 校准文件(仅采集板) |
|
||||
|
||||
- 右上角按钮:「导出」「登记板卡」(跳转`/board-cards/register`)
|
||||
- 分页
|
||||
|
||||
#### 抽屉:板卡详情(520px宽)
|
||||
- 基本信息:SN、类型、版本、固件、生产日期、状态
|
||||
- 装配信息:所属设备
|
||||
- 校准信息(仅采集板):校准状态、校准日期
|
||||
|
||||
#### 抽屉:校准文件(640px宽,仅采集板)
|
||||
- 文件信息卡片:文件名、文件大小、MD5、上传时间、下载按钮
|
||||
- 校准数据表格:通道、参考值、测量值、偏差(%)、结果
|
||||
- 偏差超过1%标红
|
||||
|
||||
---
|
||||
|
||||
### 2.12 板卡登记
|
||||
|
||||
**路由**:`/board-cards/register`
|
||||
|
||||
#### 功能描述
|
||||
登记新板卡信息,支持单个或批量登记。采集板支持导入校准文件。
|
||||
|
||||
#### 页面区块
|
||||
|
||||
**1. 登记模式切换**:单个登记 / 批量登记
|
||||
|
||||
**2. 板卡信息表单(每条)**
|
||||
|
||||
| 字段 | 类型 | 必填 |
|
||||
|------|------|------|
|
||||
| 板卡类型 | 下拉(主协板/采集板/发射板/升压板) | 是 |
|
||||
| 板卡版本 | 下拉(按类型联动) | 是 |
|
||||
| 固件版本 | 自动填充(只读) | — |
|
||||
| 板卡SN号 | 文本输入 | 是 |
|
||||
| 生产日期 | 日期选择器 | 是 |
|
||||
| 备注 | 文本输入 | — |
|
||||
|
||||
**3. 采集板校准文件导入(仅采集板显示)**
|
||||
- 虚线框点击上传区域,支持 .csv / .xlsx / .dat 格式
|
||||
- 已选文件显示文件名和大小,可移除
|
||||
|
||||
**4. 登记预览表格**
|
||||
- 列:序号、类型、版本、固件、SN号、生产日期、校准文件、状态
|
||||
|
||||
**5. 底部操作栏**
|
||||
- 显示待登记数量和校准文件附件数
|
||||
- 取消 / 确认登记
|
||||
|
||||
---
|
||||
|
||||
### 2.13 校准管理
|
||||
|
||||
**路由**:`/calibration`(已迁移至板卡列表的校准文件功能)
|
||||
|
||||
#### 功能描述
|
||||
管理采集板校准数据。仅针对采集板,其他板卡无需校准。
|
||||
|
|
@ -917,43 +1012,49 @@
|
|||
|
||||
## 五、页面路由总览
|
||||
|
||||
| 路由 | 页面 | 交互方式 | 文件 |
|
||||
| 路由 | 页面 | 交互方式 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| `/` | 首页 Dashboard | 卡片点击跳转 | `Dashboard.vue` |
|
||||
| `/models` | 设备型号管理 | 抽屉(新增型号/Checklist模板) | `DeviceModelManagement.vue` |
|
||||
| `/boards` | 板卡型号管理 | Tab筛选 + 抽屉(详情) | `BoardManagement.vue` |
|
||||
| `/devices` | 设备列表 | 筛选 + 卡片列表 | `DeviceList.vue` |
|
||||
| `/devices/:id` | 设备详情 | 独立页面 | `DeviceDetail.vue` |
|
||||
| `/registration` | 设备登记 | 弹窗(导入/拍照) | `DeviceRegistration.vue` |
|
||||
| `/licenses` | 授权管理 | 抽屉(选择授权项) | `LicenseManagement.vue` |
|
||||
| `/firmware` | 固件库 | 弹窗(上传固件) | `FirmwareLibrary.vue` |
|
||||
| `/config-files` | 配置文件管理 | 抽屉(新建配置) | `ConfigFileManagement.vue` |
|
||||
| `/config-files/:configId` | 参数配置 | 独立页面 | `ParameterConfiguration.vue` |
|
||||
| `/calibration` | 校准管理 | 表格列表 | `CalibrationRecords.vue` |
|
||||
| `/repair` | 维修工单 | 抽屉(新建/处理/详情) | `RepairOrders.vue` |
|
||||
| `/repair/stats` | 维修统计 | 独立页面 | `RepairStats.vue` |
|
||||
| `/repair/:orderId` | 工单详情 | 独立页面 | `RepairOrderDetail.vue` |
|
||||
| `/scrap` | 报废管理 | 抽屉(详情/审批/回收) | `ScrapManagement.vue` |
|
||||
| `/` | 首页 Dashboard | 卡片点击跳转 | 全局数据概览 |
|
||||
| `/models` | 设备型号管理 | 抽屉(新增型号/Checklist模板) | 核心枢纽 |
|
||||
| `/models/bom?model=` | 型号BOM表 | 表格 + 抽屉(添加物料) | 按型号管理物料清单 |
|
||||
| `/boards` | 板卡版本管理 | Tab筛选 + 抽屉(详情) | 板卡型号版本管理 |
|
||||
| `/board-cards` | 板卡列表 | 筛选 + 表格 + 抽屉(详情/校准文件) | 板卡实例管理 |
|
||||
| `/board-cards/register` | 板卡登记 | 表单 + 校准文件导入 | 单个/批量登记 |
|
||||
| `/devices` | 设备列表 | 筛选 + 卡片列表 | 设备总览 |
|
||||
| `/devices/:sn` | 设备详情 | 独立页面(Tab切换) | 含可编辑授权项和配置文件 |
|
||||
| `/registration` | 设备登记 | 弹窗(导入/拍照/扫码) | 含生产批次、扫码录入 |
|
||||
| `/licenses` | 授权管理 | 抽屉(选择授权项) | 每型号一套授权模块 |
|
||||
| `/firmware` | 固件库 | 弹窗(上传固件) | 支持板卡/设备型号固件 |
|
||||
| `/config-files` | 配置文件管理 | 抽屉(新建配置) | 按型号绑定 |
|
||||
| `/config-files/:configId` | 参数配置 | 独立页面 | 详细参数配置 |
|
||||
| `/repair` | 维修工单 | 抽屉(新建/处理/详情) | 维修全生命周期 |
|
||||
| `/repair/stats` | 维修统计 | 独立页面 | 数据统计 |
|
||||
| `/repair/:orderId` | 工单详情 | 独立页面 | 维修工单详情 |
|
||||
| `/scrap` | 报废管理 | 抽屉(详情/审批/回收) | 报废审批流程 |
|
||||
|
||||
---
|
||||
|
||||
## 六、跨模块数据流关系
|
||||
|
||||
```
|
||||
设备型号管理 ──→ 授权管理(按设备型号绑定授权项)
|
||||
设备型号管理 ──→ 授权管理(每型号一套授权模块)
|
||||
设备型号管理 ──→ 配置管理(按设备型号绑定配置文件)
|
||||
板卡型号管理 ──→ 固件库(按板卡型号关联固件)
|
||||
设备型号管理 ──→ 固件库(按型号关联主机固件/计算单元固件)
|
||||
设备型号管理 ──→ BOM表(按型号定义物料清单和板卡版本)
|
||||
设备型号管理 ──→ 设备登记(Checklist模板按型号配置)
|
||||
板卡版本管理 ──→ 固件库(按板卡型号关联固件)
|
||||
|
||||
设备登记 ──→ 设备列表(登记完成后出现在列表中)
|
||||
设备列表 ──→ 设备详情(点击查看)
|
||||
设备列表 ──→ 设备详情(点击查看,支持修改授权项和配置文件)
|
||||
|
||||
板卡列表 ──→ 校准文件(采集板查看校准数据)
|
||||
板卡登记 ──→ 板卡列表(登记完成后出现在列表中)
|
||||
|
||||
设备详情 ──→ 校准记录(查看校准信息)
|
||||
设备详情 ──→ 维修工单(查看维修历史)
|
||||
|
||||
维修工单 ──→ 报废管理(申请报废)
|
||||
维修工单 ──→ 校准记录(更换采集板需重新校准)
|
||||
|
||||
报废管理 ──→ 维修工单(关联来源工单)
|
||||
报废管理 ──→ 设备登记(回收入库后物料重新登记)
|
||||
|
||||
授权项 + 配置文件 ──→ 授权文件(系统自动生成)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { use } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Cpu, Wifi, Monitor, Key, FileCode, Camera, Clock, X, CheckCircle, AlertTriangle, Package, ChevronLeft, ChevronRight, ZoomIn, Download } from 'lucide-react'
|
||||
import { ArrowLeft, Cpu, Wifi, Monitor, Key, FileCode, Camera, Clock, X, CheckCircle, AlertTriangle, Package, ChevronLeft, ChevronRight, ZoomIn, Download, Edit, Check } from 'lucide-react'
|
||||
|
||||
/** Mock: 所有设备数据 */
|
||||
const allDevices = [
|
||||
|
|
@ -33,9 +33,9 @@ const bomData: Record<string, { name: string; sn: string; model: string; calibra
|
|||
|
||||
/** Mock: 授权信息 */
|
||||
const licenseData: Record<string, { modules: string; expiry: string; status: string }> = {
|
||||
'GD30-2025-000001': { modules: '1D SP, 2D SP, 3D SP, 1D VES, 2D ERT, 3D ERT, 1D IP, 2D IP, 3D IP, 跨孔, 水上', expiry: '2026-01-15', status: '生效' },
|
||||
'GD30-2025-000002': { modules: '2D ERT, 3D ERT, 1D IP, 2D IP, 3D IP, 跨孔, 水上', expiry: '2025-12-31', status: '生效' },
|
||||
'GT20-2025-000045': { modules: '2D ERT, 3D ERT, 1D IP, 2D IP', expiry: '2025-06-30', status: '生效' },
|
||||
'GD30-2025-000001': { modules: '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块, 水上, 跨孔, 电流法', expiry: '2026-01-15', status: '生效' },
|
||||
'GD30-2025-000002': { modules: '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块, 水上, 跨孔, 电流法', expiry: '2025-12-31', status: '生效' },
|
||||
'GT20-2025-000045': { modules: '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块', expiry: '2025-06-30', status: '生效' },
|
||||
}
|
||||
|
||||
/** Mock: 配置文件(含详细参数) */
|
||||
|
|
@ -140,6 +140,8 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const [expandedItem, setExpandedItem] = useState<number | null>(null)
|
||||
const [lightbox, setLightbox] = useState<{ photos: string[]; index: number } | null>(null)
|
||||
const [licenseEditOpen, setLicenseEditOpen] = useState(false)
|
||||
const [configEditOpen, setConfigEditOpen] = useState(false)
|
||||
|
||||
const device = allDevices.find(d => d.sn === sn)
|
||||
const bom = bomData[sn] || []
|
||||
|
|
@ -432,7 +434,12 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
|
||||
{activeTab === 'license' && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>授权项信息</h3>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>授权项信息</h3>
|
||||
<button onClick={() => setLicenseEditOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 14px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: '#4a7c59' }}>
|
||||
<Edit size={14} />修改授权项
|
||||
</button>
|
||||
</div>
|
||||
{license ? (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24, marginBottom: 24 }}>
|
||||
|
|
@ -454,19 +461,64 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
) : (
|
||||
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||
<Key size={32} style={{ color: 'rgba(0,0,0,0.15)', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>该设备暂未配置授权项</div>
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)', marginBottom: 16 }}>该设备暂未配置授权项</div>
|
||||
<button onClick={() => setLicenseEditOpen(true)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>配置授权项</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* License Edit Drawer */}
|
||||
{licenseEditOpen && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setLicenseEditOpen(false)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 560, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>修改授权项 — {sn}</h3>
|
||||
<button onClick={() => setLicenseEditOpen(false)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>到期时间</label>
|
||||
<input type="date" defaultValue={license?.expiry || ''} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>授权模块</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{['一维自电/电阻率/激电测试模块', '二维自电/电阻率/激电测试模块', '三维自电/电阻率/激电测试模块', '水上', '跨孔', '电流法'].map(m => {
|
||||
const isSelected = license?.modules.includes(m) ?? false
|
||||
return (
|
||||
<label key={m} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', borderRadius: 6, backgroundColor: isSelected ? '#eef5f0' : '#FAFAFA', border: isSelected ? '1px solid #a3c4ad' : '1px solid #F0F0F0', cursor: 'pointer' }}>
|
||||
<div style={{ width: 16, height: 16, borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: isSelected ? '#4a7c59' : '#fff', border: isSelected ? 'none' : '1px solid #D9D9D9' }}>
|
||||
{isSelected && <Check size={11} color="#fff" />}
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: isSelected ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{m}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setLicenseEditOpen(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={() => setLicenseEditOpen(false)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'config' && (
|
||||
<div>
|
||||
{config ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>配置文件</h3>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>配置文件</h3>
|
||||
<button onClick={() => setConfigEditOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 14px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: '#4a7c59' }}>
|
||||
<Edit size={14} />修改参数
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>配置名称</div>
|
||||
|
|
@ -608,12 +660,112 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||
<FileCode size={32} style={{ color: 'rgba(0,0,0,0.15)', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>该设备暂未写入配置文件</div>
|
||||
<button onClick={() => setConfigEditOpen(true)} style={{ marginTop: 16, padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>配置参数</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Config Edit Drawer */}
|
||||
{configEditOpen && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setConfigEditOpen(false)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 560, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>修改配置参数 — {sn}</h3>
|
||||
<button onClick={() => setConfigEditOpen(false)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
{config && (
|
||||
<>
|
||||
<div style={{ marginBottom: 8, fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>当前配置:{config.name} ({config.version})</div>
|
||||
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 16, paddingBottom: 8, borderBottom: '1px solid #F0F0F0' }}>发射参数</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>最大发射电压</label>
|
||||
<select defaultValue={config.params.transmission.maxVoltage} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{['500V', '800V', '1000V', '1200V', '1500V'].map(v => <option key={v} value={v}>{v}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>最大发射电流</label>
|
||||
<select defaultValue={config.params.transmission.maxCurrent} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{['2A', '5A', '8A', '10A', '15A'].map(v => <option key={v} value={v}>{v}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>占空比</label>
|
||||
<select defaultValue={config.params.transmission.dutyCycle} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="50%">50%</option>
|
||||
<option value="50%、100%">50%、100%</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>
|
||||
<input type="checkbox" defaultChecked={config.params.transmission.fullWaveform} />全波形测量
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #F0F0F0' }}>采集参数</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>支持通道数</label>
|
||||
<select defaultValue={String(config.params.acquisition.channels)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{['1', '6', '12'].map(v => <option key={v} value={v}>{v}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>迭代次数范围</label>
|
||||
<input defaultValue={config.params.acquisition.iterationRange} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #F0F0F0' }}>保护参数</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>
|
||||
<input type="checkbox" defaultChecked={config.params.protection.overVoltage.enabled} />过压保护
|
||||
</label>
|
||||
<input defaultValue={config.params.protection.overVoltage.threshold} placeholder="阈值" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>
|
||||
<input type="checkbox" defaultChecked={config.params.protection.overCurrent.enabled} />过流保护
|
||||
</label>
|
||||
<input defaultValue={config.params.protection.overCurrent.threshold} placeholder="阈值" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
<input type="checkbox" defaultChecked={config.params.protection.shortCircuit} />短路保护
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>
|
||||
<input type="checkbox" defaultChecked={config.params.protection.overTemp.enabled} />高温保护
|
||||
</label>
|
||||
<input defaultValue={config.params.protection.overTemp.threshold} placeholder="阈值" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #F0F0F0' }}>网络参数</h4>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>WiFi SSID 前缀</label>
|
||||
<input defaultValue={config.params.network.wifiSsidPrefix} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setConfigEditOpen(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={() => setConfigEditOpen(false)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'logs' && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>操作日志</h3>
|
||||
|
|
|
|||
|
|
@ -36,25 +36,22 @@ function getWeekRange(yearWeek: string): string {
|
|||
}
|
||||
|
||||
const rawDevices = [
|
||||
{ id: 1, sn: 'GD30-2025-000001', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-15 14:30', customer: '北京地质研究院' },
|
||||
{ id: 2, sn: 'GD30-2025-000002', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-18 09:15', customer: '中国地质大学' },
|
||||
{ id: 3, sn: 'GD30-2024-000056', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-12-20 16:00', customer: '成都理工大学' },
|
||||
{ id: 4, sn: 'GT20-2025-000045', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-02-10 11:20', customer: '武汉地质调查中心' },
|
||||
{ id: 5, sn: 'GT20-2025-000046', model: 'GD-20', type: '二维电法仪', status: '装配中', firmware: 'v1.8.5', productionDate: '2025-03-01 08:45', customer: '-' },
|
||||
{ id: 6, sn: 'GD30-2024-000078', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-11-05 13:30', customer: '长安大学' },
|
||||
{ id: 7, sn: 'GD10-2024-000033', model: 'GD-10 Supreme', type: '入门级电法仪', status: '已激活', firmware: 'v1.5.2', productionDate: '2024-09-12 10:00', customer: '河海大学' },
|
||||
{ id: 8, sn: 'GD30-2024-000089', model: 'GD-30 Supreme', type: '高密度电法仪', status: '装配中', firmware: 'v2.3.5', productionDate: '2025-03-05 15:10', customer: '-' },
|
||||
{ id: 9, sn: 'GT20-2025-000012', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-01-22 09:30', customer: '中南大学' },
|
||||
{ id: 10, sn: 'GD30-2024-000102', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-10-18 14:00', customer: '吉林大学' },
|
||||
{ id: 11, sn: 'GD10-2024-000034', model: 'GD-10 Supreme', type: '入门级电法仪', status: '装配中', firmware: 'v1.5.2', productionDate: '2025-03-08 11:45', customer: '-' },
|
||||
{ id: 12, sn: 'GD30-2024-000145', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2024-08-25 16:20', customer: '同济大学' },
|
||||
{ id: 1, sn: 'GD30-2025-000001', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-15 14:30', customer: '北京地质研究院', batch: 'BATCH-2025-Q1-001' },
|
||||
{ id: 2, sn: 'GD30-2025-000002', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-18 09:15', customer: '中国地质大学', batch: 'BATCH-2025-Q1-001' },
|
||||
{ id: 3, sn: 'GD30-2024-000056', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-12-20 16:00', customer: '成都理工大学', batch: 'BATCH-2024-Q4-003' },
|
||||
{ id: 4, sn: 'GT20-2025-000045', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-02-10 11:20', customer: '武汉地质调查中心', batch: 'BATCH-2025-Q1-002' },
|
||||
{ id: 5, sn: 'GT20-2025-000046', model: 'GD-20', type: '二维电法仪', status: '装配中', firmware: 'v1.8.5', productionDate: '2025-03-01 08:45', customer: '-', batch: 'BATCH-2025-Q1-002' },
|
||||
{ id: 6, sn: 'GD30-2024-000078', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-11-05 13:30', customer: '长安大学', batch: 'BATCH-2024-Q4-002' },
|
||||
{ id: 7, sn: 'GD10-2024-000033', model: 'GD-10 Supreme', type: '入门级电法仪', status: '已激活', firmware: 'v1.5.2', productionDate: '2024-09-12 10:00', customer: '河海大学', batch: 'BATCH-2024-Q3-001' },
|
||||
{ id: 8, sn: 'GD30-2024-000089', model: 'GD-30 Supreme', type: '高密度电法仪', status: '装配中', firmware: 'v2.3.5', productionDate: '2025-03-05 15:10', customer: '-', batch: 'BATCH-2025-Q1-001' },
|
||||
{ id: 9, sn: 'GT20-2025-000012', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-01-22 09:30', customer: '中南大学', batch: 'BATCH-2025-Q1-002' },
|
||||
{ id: 10, sn: 'GD30-2024-000102', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-10-18 14:00', customer: '吉林大学', batch: 'BATCH-2024-Q4-001' },
|
||||
{ id: 11, sn: 'GD10-2024-000034', model: 'GD-10 Supreme', type: '入门级电法仪', status: '装配中', firmware: 'v1.5.2', productionDate: '2025-03-08 11:45', customer: '-', batch: 'BATCH-2025-Q1-003' },
|
||||
{ id: 12, sn: 'GD30-2024-000145', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2024-08-25 16:20', customer: '同济大学', batch: 'BATCH-2024-Q3-002' },
|
||||
]
|
||||
|
||||
/** 根据生产日期动态计算年周批次 */
|
||||
const devicesData = rawDevices.map(d => ({
|
||||
...d,
|
||||
batch: getYearWeek(d.productionDate),
|
||||
}))
|
||||
const devicesData = rawDevices
|
||||
|
||||
const modelOptions = ['全部', 'GD-30 Supreme', 'GD-20', 'GD-10 Supreme']
|
||||
const statusOptions = ['全部', '已激活', '已出厂', '装配中']
|
||||
|
|
@ -86,7 +83,7 @@ export default function DevicesPage() {
|
|||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const pageSize = 8
|
||||
|
||||
// 从数据中提取所有年周批次,按时间倒序排列,统计每个批次的设备数量,按年分组
|
||||
// 从数据中提取所有生产批次,按名称倒序排列,统计每个批次的设备数量,按年分组
|
||||
const batchGroups = useMemo(() => {
|
||||
const batchMap = new Map<string, number>()
|
||||
devicesData.forEach(d => {
|
||||
|
|
@ -94,7 +91,10 @@ export default function DevicesPage() {
|
|||
})
|
||||
const sorted = Array.from(batchMap.entries())
|
||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||
.map(([batch, count]) => ({ batch, count, year: batch.split('-W')[0] }))
|
||||
.map(([batch, count]) => {
|
||||
const match = batch.match(/BATCH-(\d{4})/)
|
||||
return { batch, count, year: match ? match[1] : '未知' }
|
||||
})
|
||||
|
||||
// 按年分组
|
||||
const groups = new Map<string, { batch: string; count: number }[]>()
|
||||
|
|
@ -209,7 +209,6 @@ export default function DevicesPage() {
|
|||
>
|
||||
<div>
|
||||
<div>{batch}</div>
|
||||
<div style={{ fontSize: 11, color: selectedBatch === batch ? '#6a9c79' : 'rgba(0,0,0,0.35)', marginTop: 1 }}>{getWeekRange(batch)}</div>
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: 12, padding: '1px 8px', borderRadius: 10, flexShrink: 0,
|
||||
|
|
@ -241,6 +240,7 @@ export default function DevicesPage() {
|
|||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', lineHeight: 2 }}>
|
||||
<div>型号:{device.model} {device.type}</div>
|
||||
<div>主机版本:{device.firmware}</div>
|
||||
<div>生产批次:{device.batch}</div>
|
||||
<div>生产日期:{device.productionDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,31 @@
|
|||
'use client'
|
||||
import { useState, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Download, Plus, Search, Info, ChevronLeft, ChevronRight, X, Check, ArrowLeft } from 'lucide-react'
|
||||
import { Download, Plus, Info, ChevronLeft, ChevronRight, X, Check, ArrowLeft } from 'lucide-react'
|
||||
|
||||
const allAuthItems = [
|
||||
{ id: '1D_SP', name: '1D SP', description: '一维自然电位法' },
|
||||
{ id: '2D_SP', name: '2D SP', description: '二维自然电位法' },
|
||||
{ id: '3D_SP', name: '3D SP', description: '三维自然电位法' },
|
||||
{ id: '1D_VES', name: '1D VES', description: '一维垂向电测深' },
|
||||
{ id: '2D_ERT', name: '2D ERT', description: '二维电阻率成像' },
|
||||
{ id: '3D_ERT', name: '3D ERT', description: '三维电阻率成像' },
|
||||
{ id: '1D_IP', name: '1D IP', description: '一维激发极化法' },
|
||||
{ id: '2D_IP', name: '2D IP', description: '二维激发极化法' },
|
||||
{ id: '3D_IP', name: '3D IP', description: '三维激发极化法' },
|
||||
{ id: 'CROSS', name: '跨孔', description: '跨孔电阻率成像' },
|
||||
{ id: 'WATER', name: '水上', description: '水上电法探测' },
|
||||
{ id: '1D', name: '一维自电/电阻率/激电测试模块', description: '包含一维自然电位法、电阻率测深、激发极化测深', category: '一维' },
|
||||
{ id: '2D', name: '二维自电/电阻率/激电测试模块', description: '包含二维自然电位法、电阻率成像、激发极化成像', category: '二维' },
|
||||
{ id: '3D', name: '三维自电/电阻率/激电测试模块', description: '包含三维自然电位法、电阻率成像、激发极化成像', category: '三维' },
|
||||
{ id: 'WATER', name: '水上', description: '水上电法探测', category: '水上' },
|
||||
{ id: 'CROSS', name: '跨孔', description: '跨孔电阻率成像', category: '跨孔' },
|
||||
{ id: 'CF', name: '电流场法', description: '电流场法', category: '电流场法' },
|
||||
]
|
||||
|
||||
const authCategories = ['一维', '二维', '三维', '水上', '跨孔', '电流场法']
|
||||
|
||||
const modelPresets: Record<string, string[]> = {
|
||||
'GD-10': ['1D_SP', '2D_SP', '1D_VES', '2D_ERT', '1D_IP', '2D_IP'],
|
||||
'GD-20': ['1D_SP', '2D_SP', '3D_SP', '1D_VES', '2D_ERT', '3D_ERT', '1D_IP', '2D_IP', '3D_IP'],
|
||||
'GD-30': ['1D_SP', '2D_SP', '3D_SP', '1D_VES', '2D_ERT', '3D_ERT', '1D_IP', '2D_IP', '3D_IP', 'CROSS', 'WATER'],
|
||||
'GD-10': ['1D', '2D'],
|
||||
'GD-20': ['1D', '2D', '3D'],
|
||||
'GD-30': ['1D', '2D', '3D', 'WATER', 'CROSS', 'CF'],
|
||||
}
|
||||
|
||||
const mockLicenses = [
|
||||
{ id: 1, model: 'GD-30', modules: '2D ERT, 3D ERT, 1D IP, 2D IP, 3D IP, 跨孔, 水上', expiry: '2025-12-31', date: '2025-01-15', status: '生效' },
|
||||
{ id: 2, model: 'GD-20', modules: '2D ERT, 3D ERT, 1D IP, 2D IP', expiry: '2025-06-30', date: '2024-07-01', status: '生效' },
|
||||
{ id: 3, model: 'GD-10', modules: '1D SP, 2D SP, 1D VES, 2D ERT', expiry: '2024-12-31', date: '2024-01-10', status: '已停用' },
|
||||
{ id: 4, model: 'GD-30', modules: '全部模块', expiry: '2026-06-30', date: '2025-03-20', status: '生效' },
|
||||
{ id: 5, model: 'GD-20', modules: '2D ERT, 1D IP, 2D IP', expiry: '2025-09-15', date: '2025-02-10', status: '生效' },
|
||||
{ id: 6, model: 'GD-10', modules: '1D SP, 2D SP', expiry: '2024-06-30', date: '2023-07-01', status: '已停用' },
|
||||
{ id: 7, model: 'GD-30', modules: '3D ERT, 3D IP, 跨孔', expiry: '2025-08-20', date: '2025-01-05', status: '生效' },
|
||||
{ id: 8, model: 'GD-20', modules: '1D VES, 2D ERT, 1D IP', expiry: '2025-11-30', date: '2025-04-01', status: '草稿' },
|
||||
{ id: 1, model: 'GD-30', modules: '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块, 水上, 跨孔, 电流场法', expiry: '2025-12-31', status: '生效' },
|
||||
{ id: 2, model: 'GD-20', modules: '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块', expiry: '2025-06-30', status: '生效' },
|
||||
{ id: 3, model: 'GD-10', modules: '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块', expiry: '2024-12-31', status: '生效' },
|
||||
]
|
||||
|
||||
const statusStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case '生效': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
case '草稿': return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }
|
||||
case '已停用': return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default function LicensesPage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}>
|
||||
|
|
@ -66,7 +49,6 @@ function LicensesContent() {
|
|||
const isFromModels = !!modelParam
|
||||
|
||||
const [filterModel, setFilterModel] = useState(initialModelFilter)
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [drawerModel, setDrawerModel] = useState('')
|
||||
|
|
@ -77,7 +59,6 @@ function LicensesContent() {
|
|||
|
||||
const filtered = mockLicenses.filter(l => {
|
||||
if (filterModel && l.model !== filterModel) return false
|
||||
if (filterStatus && l.status !== filterStatus) return false
|
||||
return true
|
||||
})
|
||||
const totalPages = Math.ceil(filtered.length / pageSize)
|
||||
|
|
@ -126,7 +107,7 @@ function LicensesContent() {
|
|||
<div className="flex items-start gap-3 p-4 rounded-lg mb-6" style={{ backgroundColor: '#E6FFFB', border: '1px solid #87E8DE' }}>
|
||||
<Info size={16} style={{ color: '#13C2C2', marginTop: 2, flexShrink: 0 }} />
|
||||
<div className="text-sm" style={{ color: 'rgba(0,0,0,0.65)' }}>
|
||||
授权管理用于控制设备可使用的功能模块。每台设备需要有效的授权才能使用对应的测量方法。授权到期前30天系统会自动提醒。
|
||||
每个设备型号对应一套授权模块配置。该授权模块定义了该型号设备可使用的全部测量方法,所有该型号设备共享同一套授权项。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -134,25 +115,13 @@ function LicensesContent() {
|
|||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm" style={{ color: 'rgba(0,0,0,0.65)' }}>设备型号</span>
|
||||
<select value={filterModel} onChange={e => setFilterModel(e.target.value)} className="px-3 py-1.5 rounded text-sm" style={{ border: '1px solid #D9D9D9', minWidth: 140 }}>
|
||||
<select value={filterModel} onChange={e => { setFilterModel(e.target.value); setCurrentPage(1) }} className="px-3 py-1.5 rounded text-sm" style={{ border: '1px solid #D9D9D9', minWidth: 140 }}>
|
||||
<option value="">全部型号</option>
|
||||
<option value="GD-10">GD-10</option>
|
||||
<option value="GD-20">GD-20</option>
|
||||
<option value="GD-30">GD-30</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm" style={{ color: 'rgba(0,0,0,0.65)' }}>状态</span>
|
||||
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-3 py-1.5 rounded text-sm" style={{ border: '1px solid #D9D9D9', minWidth: 120 }}>
|
||||
<option value="">全部状态</option>
|
||||
<option value="生效">生效</option>
|
||||
<option value="草稿">草稿</option>
|
||||
<option value="已停用">已停用</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={() => setCurrentPage(1)} className="flex items-center gap-1 px-4 py-1.5 rounded text-sm text-white" style={{ backgroundColor: '#4a7c59' }}>
|
||||
<Search size={14} />查询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -160,7 +129,7 @@ function LicensesContent() {
|
|||
<table className="w-full">
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['设备型号', '授权模块', '到期时间', '创建日期', '状态', '操作'].map(h => (
|
||||
{['设备型号', '授权模块','操作'].map(h => (
|
||||
<th key={h} className="text-left px-4 py-3 text-sm font-medium" style={{ color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -168,21 +137,17 @@ function LicensesContent() {
|
|||
<tbody>
|
||||
{paged.map(row => (
|
||||
<tr key={row.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>{row.model}</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0', maxWidth: 300 }}>
|
||||
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{row.modules}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>{row.expiry}</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>{row.date}</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<span className="px-2 py-0.5 rounded text-xs" style={statusStyle(row.status)}>{row.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="text-sm" style={{ color: '#4a7c59' }}>编辑</button>
|
||||
<button className="text-sm" style={{ color: row.status === '已停用' ? 'rgba(0,0,0,0.25)' : '#FF4D4F' }} disabled={row.status === '已停用'}>停用</button>
|
||||
<td className="px-4 py-3 text-sm font-medium" style={{ borderBottom: '1px solid #F0F0F0' }}>{row.model}</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0', maxWidth: 400 }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{row.modules.split(', ').map(m => (
|
||||
<span key={m} style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' }}>{m}</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<button className="text-sm" style={{ color: '#4a7c59', border: 'none', background: 'none', cursor: 'pointer' }}>编辑</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -217,16 +182,6 @@ function LicensesContent() {
|
|||
<option value="GD-30">GD-30</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm mb-2" style={{ color: 'rgba(0,0,0,0.85)' }}>授权期限</label>
|
||||
<select value={drawerExpiry} onChange={e => setDrawerExpiry(e.target.value)} className="w-full px-3 py-2 rounded text-sm" style={{ border: '1px solid #D9D9D9' }}>
|
||||
<option value="1year">1年</option>
|
||||
<option value="2year">2年</option>
|
||||
<option value="3year">3年</option>
|
||||
<option value="permanent">永久</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
{drawerExpiry === 'custom' && (
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm mb-2" style={{ color: 'rgba(0,0,0,0.85)' }}>自定义到期日期</label>
|
||||
|
|
@ -245,26 +200,35 @@ function LicensesContent() {
|
|||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
<th className="text-left px-3 py-2 text-sm font-medium" style={{ color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', width: 50 }}>选择</th>
|
||||
<th className="text-left px-3 py-2 text-sm font-medium" style={{ color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>分类</th>
|
||||
<th className="text-left px-3 py-2 text-sm font-medium" style={{ color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>名称</th>
|
||||
<th className="text-left px-3 py-2 text-sm font-medium" style={{ color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allAuthItems.map(item => (
|
||||
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => toggleItem(item.id)}>
|
||||
<td className="px-3 py-2" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div className="w-4 h-4 rounded flex items-center justify-center" style={{ border: selectedItems.includes(item.id) ? 'none' : '1px solid #D9D9D9', backgroundColor: selectedItems.includes(item.id) ? '#4a7c59' : '#fff' }}>
|
||||
{selectedItems.includes(item.id) && <Check size={12} color="#fff" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>{item.name}</td>
|
||||
<td className="px-3 py-2 text-sm" style={{ borderBottom: '1px solid #F0F0F0', color: 'rgba(0,0,0,0.45)' }}>{item.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
{authCategories.map(cat => {
|
||||
const items = allAuthItems.filter(i => i.category === cat)
|
||||
return items.map((item, idx) => (
|
||||
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => toggleItem(item.id)}>
|
||||
<td className="px-3 py-2" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div className="w-4 h-4 rounded flex items-center justify-center" style={{ border: selectedItems.includes(item.id) ? 'none' : '1px solid #D9D9D9', backgroundColor: selectedItems.includes(item.id) ? '#4a7c59' : '#fff' }}>
|
||||
{selectedItems.includes(item.id) && <Check size={12} color="#fff" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm" style={{ borderBottom: '1px solid #F0F0F0', color: '#4a7c59', fontWeight: idx === 0 ? 500 : 400 }}>{idx === 0 ? cat : ''}</td>
|
||||
<td className="px-3 py-2 text-sm" style={{ borderBottom: '1px solid #F0F0F0' }}>{item.name}</td>
|
||||
<td className="px-3 py-2 text-sm" style={{ borderBottom: '1px solid #F0F0F0', color: 'rgba(0,0,0,0.45)' }}>{item.description}</td>
|
||||
</tr>
|
||||
))
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-sm mb-6" style={{ color: 'rgba(0,0,0,0.45)' }}>已选择 {selectedItems.length} 项</div>
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg mb-6" style={{ backgroundColor: '#eef5f0', border: '1px solid #a3c4ad' }}>
|
||||
<Info size={14} style={{ color: '#4a7c59', flexShrink: 0, marginTop: 2 }} />
|
||||
<span className="text-xs" style={{ color: '#4a7c59' }}>授权文件由选定的授权项与对应型号的配置文件共同生成,保存后系统将自动生成授权文件。</span>
|
||||
</div>
|
||||
<button onClick={() => setDrawerOpen(false)} className="w-full py-2 rounded-lg text-sm text-white" style={{ backgroundColor: '#4a7c59' }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
'use client'
|
||||
import { useState, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { ArrowLeft, Plus, Trash2, X, Info, CheckCircle } from 'lucide-react'
|
||||
|
||||
interface BomItem {
|
||||
id: number
|
||||
name: string
|
||||
model: string
|
||||
versions: string[]
|
||||
qty: number
|
||||
required: boolean
|
||||
needCalibration: boolean
|
||||
enforceVersionMatch: boolean
|
||||
}
|
||||
|
||||
const modelBomData: Record<string, BomItem[]> = {
|
||||
GD30: [
|
||||
{ id: 1, name: '主协板', model: 'MCB-3000', versions: ['MB-V2.1', 'MB-V1.8'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
{ id: 2, name: '采集板', model: 'ACB-6000', versions: ['RX-V2.3', 'RX-V1.3'], qty: 2, required: true, needCalibration: true, enforceVersionMatch: true },
|
||||
{ id: 3, name: '发射板', model: 'TXB-1000', versions: ['TX-V2.1', 'TX-V1.5'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
{ id: 4, name: '升压板', model: 'BST-500', versions: ['BP600-V1.2'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
{ id: 5, name: '外壳机箱', model: 'GD30-CASE-A', versions: ['-'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
],
|
||||
GD20: [
|
||||
{ id: 1, name: '主协板', model: 'MCB-2000', versions: ['MB-V1.8', 'MB-V1.2'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
{ id: 2, name: '采集板', model: 'ACB-5000', versions: ['RX-V2.1', 'RX-V1.3'], qty: 1, required: true, needCalibration: true, enforceVersionMatch: false },
|
||||
{ id: 3, name: '发射板', model: 'TXB-800', versions: ['TX-V1.5'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
{ id: 4, name: '外壳机箱', model: 'GD20-CASE-A', versions: ['-'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
],
|
||||
GD10: [
|
||||
{ id: 1, name: '主协板', model: 'MCB-1000', versions: ['MB-V1.2'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
{ id: 2, name: '采集板', model: 'ACB-3000', versions: ['RX-V1.3'], qty: 1, required: true, needCalibration: true, enforceVersionMatch: false },
|
||||
{ id: 3, name: '外壳机箱', model: 'GD10-CASE-A', versions: ['-'], qty: 1, required: true, needCalibration: false, enforceVersionMatch: false },
|
||||
],
|
||||
}
|
||||
|
||||
const modelNames: Record<string, string> = { GD30: 'GD-30 Supreme', GD20: 'GD-20 Supreme', GD10: 'GD-10 Supreme' }
|
||||
|
||||
export default function BomPage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}>
|
||||
<BomContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function BomContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const modelCode = searchParams.get('model') || 'GD30'
|
||||
const modelName = modelNames[modelCode] || modelCode
|
||||
|
||||
const [bomList, setBomList] = useState<BomItem[]>(modelBomData[modelCode] || [])
|
||||
const [addDrawer, setAddDrawer] = useState(false)
|
||||
const [addForm, setAddForm] = useState({ name: '', model: '', versions: '', qty: 1, required: true, needCalibration: false, enforceVersionMatch: false })
|
||||
|
||||
const removeBomItem = (id: number) => setBomList(prev => prev.filter(b => b.id !== id))
|
||||
|
||||
const handleAdd = () => {
|
||||
const newItem: BomItem = {
|
||||
id: Date.now(),
|
||||
name: addForm.name,
|
||||
model: addForm.model,
|
||||
versions: addForm.versions.split(',').map(v => v.trim()).filter(Boolean),
|
||||
qty: addForm.qty,
|
||||
required: addForm.required,
|
||||
needCalibration: addForm.needCalibration,
|
||||
enforceVersionMatch: addForm.enforceVersionMatch,
|
||||
}
|
||||
setBomList(prev => [...prev, newItem])
|
||||
setAddDrawer(false)
|
||||
setAddForm({ name: '', model: '', versions: '', qty: 1, required: true, needCalibration: false, enforceVersionMatch: false })
|
||||
}
|
||||
|
||||
// 是否有多块采集板需要版本一致
|
||||
const hasMultiAcqBoards = bomList.some(b => b.name === '采集板' && b.qty >= 2 && b.enforceVersionMatch)
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||||
<button onClick={() => router.push('/models')} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>BOM表 — {modelName}</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理 {modelName} 型号的物料清单,定义装配所需板卡及版本</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, backgroundColor: '#eef5f0', borderRadius: 8, marginBottom: 24, border: '1px solid #a3c4ad' }}>
|
||||
<Info size={18} style={{ color: '#4a7c59', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ fontSize: 14, color: '#4a7c59', lineHeight: 1.6 }}>
|
||||
BOM表定义了该型号设备装配所需的全部物料。每种板卡可配置多个兼容版本,设备登记时按此清单进行装配。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 采集板版本一致性约束提示(仅当有多块采集板时显示) */}
|
||||
{hasMultiAcqBoards && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 16px', backgroundColor: '#eef5f0', borderRadius: 8, marginBottom: 24, border: '1px solid #a3c4ad' }}>
|
||||
<CheckCircle size={16} style={{ color: '#4a7c59', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 13, color: '#4a7c59' }}>
|
||||
该型号包含 {bomList.find(b => b.name === '采集板' && b.qty >= 2)?.qty} 块采集板,已启用版本一致性约束:装配时所有采集板必须使用相同版本。
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* BOM Table */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>物料清单(共 {bomList.reduce((s, b) => s + b.qty, 0)} 件)</h3>
|
||||
<button onClick={() => setAddDrawer(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
<Plus size={16} />添加物料
|
||||
</button>
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['物料名称', '型号', '兼容版本', '数量', '必需', '版本约束', '需校准', '操作'].map(h => (
|
||||
<th key={h} style={{ padding: '12px 16px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bomList.map(item => (
|
||||
<tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{item.name}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{item.model}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{item.versions.map(v => (
|
||||
<span key={v} style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, backgroundColor: v === '-' ? '#FAFAFA' : '#F0F5FF', color: v === '-' ? 'rgba(0,0,0,0.25)' : '#597EF7', border: v === '-' ? '1px solid #D9D9D9' : '1px solid #ADC6FF' }}>{v}</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14 }}>{item.qty}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
{item.required ? <span style={{ fontSize: 12, color: '#FF4D4F' }}>必需</span> : <span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>可选</span>}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
{item.qty >= 2 && item.enforceVersionMatch ? (
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>版本须一致</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
{item.needCalibration ? <span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }}>需校准</span> : <span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<button onClick={() => removeBomItem(item.id)} style={{ color: '#FF4D4F', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Trash2 size={13} />移除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Add Drawer */}
|
||||
{addDrawer && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setAddDrawer(false)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 480, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>添加物料</h3>
|
||||
<button onClick={() => setAddDrawer(false)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料名称</label>
|
||||
<select value={addForm.name} onChange={e => setAddForm({ ...addForm, name: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">请选择</option>
|
||||
{['主协板', '采集板', '发射板', '升压板', '外壳机箱', '电池组', '线缆组件'].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 型号</label>
|
||||
<input value={addForm.model} onChange={e => setAddForm({ ...addForm, model: e.target.value })} placeholder="如 MCB-3000" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>兼容版本(逗号分隔)</label>
|
||||
<input value={addForm.versions} onChange={e => setAddForm({ ...addForm, versions: e.target.value })} placeholder="如 MB-V2.1, MB-V1.8" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginTop: 4 }}>支持多个版本,用逗号分隔</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>数量</label>
|
||||
<input type="number" min={1} value={addForm.qty} onChange={e => setAddForm({ ...addForm, qty: parseInt(e.target.value) || 1 })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 24, marginBottom: 20 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={addForm.required} onChange={e => setAddForm({ ...addForm, required: e.target.checked })} />必需物料
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={addForm.needCalibration} onChange={e => setAddForm({ ...addForm, needCalibration: e.target.checked })} />需要校准
|
||||
</label>
|
||||
{addForm.qty >= 2 && (
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={addForm.enforceVersionMatch} onChange={e => setAddForm({ ...addForm, enforceVersionMatch: e.target.checked })} />版本须一致
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setAddDrawer(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={handleAdd} disabled={!addForm.name || !addForm.model} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: addForm.name && addForm.model ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: addForm.name && addForm.model ? 'pointer' : 'not-allowed', fontSize: 14 }}>添加</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, Cpu } from 'lucide-react'
|
||||
import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, Cpu, ClipboardList } from 'lucide-react'
|
||||
|
||||
const initialModelsData = [
|
||||
{ id: 1, name: 'GD-30 Supreme', code: 'GD30', status: '在产', description: '高端高密度电法仪', createDate: '2023-06-01' },
|
||||
|
|
@ -157,6 +157,12 @@ export default function ModelsPage() {
|
|||
>
|
||||
<Cpu size={14} />固件
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/models/bom?model=${model.code}`)}
|
||||
style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
<ClipboardList size={14} />BOM表
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Upload, Download, Trash2, Edit, CheckCircle, Camera, X, AlertTriangle, Info } from 'lucide-react'
|
||||
import { ArrowLeft, Upload, Download, Trash2, Edit, CheckCircle, Camera, X, AlertTriangle, Info, ScanLine, QrCode } from 'lucide-react'
|
||||
|
||||
const modelMatchInfo: Record<string, { license: string; config: string; firmware: string }> = {
|
||||
'GD-30 Supreme': { license: 'LIC-GD30-全模块授权', config: 'CFG-GD30-v1.3.0', firmware: 'v2.3.5' },
|
||||
|
|
@ -9,15 +9,40 @@ const modelMatchInfo: Record<string, { license: string; config: string; firmware
|
|||
'GD-10 Supreme': { license: 'LIC-GD10-基础授权', config: 'CFG-GD10-v1.0.0', firmware: 'v1.5.2' },
|
||||
}
|
||||
|
||||
/** 每个型号可选的配置文件列表 */
|
||||
const modelConfigOptions: Record<string, { name: string; version: string; status: string }[]> = {
|
||||
'GD-30 Supreme': [
|
||||
{ name: 'CFG-GD30-v1.3.0', version: 'v1.3.0', status: '生效' },
|
||||
{ name: 'CFG-GD30-v1.2.0', version: 'v1.2.0', status: '生效' },
|
||||
{ name: 'CFG-GD30-v1.1.0', version: 'v1.1.0', status: '已停用' },
|
||||
],
|
||||
'GD-20 Supreme': [
|
||||
{ name: 'CFG-GD20-v1.1.0', version: 'v1.1.0', status: '生效' },
|
||||
{ name: 'CFG-GD20-v1.0.0', version: 'v1.0.0', status: '生效' },
|
||||
],
|
||||
'GD-10 Supreme': [
|
||||
{ name: 'CFG-GD10-v1.0.0', version: 'v1.0.0', status: '生效' },
|
||||
],
|
||||
}
|
||||
|
||||
const defaultBOM = [
|
||||
{ id: 1, code: 'MB-2024-001', name: '主协板', sn: 'MCB-2024-0089', model: 'MCB-3000', calibration: '无需校准', qty: 1 },
|
||||
{ id: 2, code: 'AC-2024-001', name: '采集板', sn: 'ACB-2024-0156', model: 'ACB-6000', calibration: '已校准', qty: 1 },
|
||||
{ id: 3, code: 'AC-2024-002', name: '采集板', sn: 'ACB-2024-0157', model: 'ACB-6000', calibration: '已校准', qty: 1 },
|
||||
{ id: 5, code: 'TX-2024-001', name: '发射板', sn: 'TXB-2024-0034', model: 'TXB-1000', calibration: '无需校准', qty: 1 },
|
||||
{ id: 6, code: 'BS-2024-001', name: '升压板', sn: 'BST-2024-0021', model: 'BST-500', calibration: '无需校准', qty: 1 },
|
||||
{ id: 7, code: 'CS-2024-001', name: '外壳机箱', sn: '-', model: 'GD30-CASE-A', calibration: '无需校准', qty: 1 },
|
||||
{ id: 1, code: 'MB-2024-001', name: '主协板', sn: 'MCB-2024-0089', model: 'MCB-3000', version: 'MB-V2.1', calibration: '无需校准', qty: 1 },
|
||||
{ id: 2, code: 'AC-2024-001', name: '采集板', sn: 'ACB-2024-0156', model: 'ACB-6000', version: 'RX-V2.3', calibration: '已校准', qty: 1 },
|
||||
{ id: 3, code: 'AC-2024-002', name: '采集板', sn: 'ACB-2024-0157', model: 'ACB-6000', version: 'RX-V2.3', calibration: '已校准', qty: 1 },
|
||||
{ id: 5, code: 'TX-2024-001', name: '发射板', sn: 'TXB-2024-0034', model: 'TXB-1000', version: 'TX-V2.1', calibration: '无需校准', qty: 1 },
|
||||
{ id: 6, code: 'BS-2024-001', name: '升压板', sn: 'BST-2024-0021', model: 'BST-500', version: 'BP600-V1.2', calibration: '无需校准', qty: 1 },
|
||||
{ id: 7, code: 'CS-2024-001', name: '外壳机箱', sn: '-', model: 'GD30-CASE-A', version: '-', calibration: '无需校准', qty: 1 },
|
||||
]
|
||||
|
||||
/** 板卡型号对应的可选版本 */
|
||||
const boardVersionOptions: Record<string, string[]> = {
|
||||
'MCB-3000': ['MB-V2.1', 'MB-V1.8'],
|
||||
'ACB-6000': ['RX-V2.3', 'RX-V1.3'],
|
||||
'TXB-1000': ['TX-V2.1', 'TX-V1.5'],
|
||||
'BST-500': ['BP600-V1.2'],
|
||||
'GD30-CASE-A': ['-'],
|
||||
}
|
||||
|
||||
const defaultChecklist = [
|
||||
{ id: 1, name: '主板SN扫码绑定', required: true },
|
||||
{ id: 2, name: '采集板SN录入(×6)', required: true },
|
||||
|
|
@ -48,6 +73,8 @@ export default function RegistrationPage() {
|
|||
const [deviceModel, setDeviceModel] = useState('GD-30 Supreme')
|
||||
const [hostSN, setHostSN] = useState('')
|
||||
const [boardSN, setBoardSN] = useState('')
|
||||
const [batchNo, setBatchNo] = useState('')
|
||||
const [selectedConfig, setSelectedConfig] = useState('CFG-GD30-v1.3.0')
|
||||
const [testStatus, setTestStatus] = useState('测试通过')
|
||||
const [productionDate, setProductionDate] = useState('')
|
||||
const [bomList, setBomList] = useState(defaultBOM)
|
||||
|
|
@ -69,6 +96,10 @@ export default function RegistrationPage() {
|
|||
setBomList(prev => prev.filter(b => b.id !== id))
|
||||
}
|
||||
|
||||
const updateBomItem = (id: number, field: string, value: string) => {
|
||||
setBomList(prev => prev.map(b => b.id === id ? { ...b, [field]: value } : b))
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, padding: 24, paddingBottom: 80 }}>
|
||||
|
|
@ -89,7 +120,7 @@ export default function RegistrationPage() {
|
|||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 设备型号</label>
|
||||
<select value={deviceModel} onChange={e => setDeviceModel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<select value={deviceModel} onChange={e => { setDeviceModel(e.target.value); const cfgs = modelConfigOptions[e.target.value]; if (cfgs?.length) setSelectedConfig(cfgs[0].name) }} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="GD-30 Supreme">GD-30 Supreme</option>
|
||||
<option value="GD-20 Supreme">GD-20 Supreme</option>
|
||||
<option value="GD-10 Supreme">GD-10 Supreme</option>
|
||||
|
|
@ -97,11 +128,43 @@ export default function RegistrationPage() {
|
|||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 主机SN号</label>
|
||||
<input value={hostSN} onChange={e => setHostSN(e.target.value)} placeholder="如 GD30-20240308-001" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<input value={hostSN} onChange={e => setHostSN(e.target.value)} placeholder="扫码或手动输入" style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<button title="扫码录入" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', flexShrink: 0 }}>
|
||||
<ScanLine size={16} style={{ color: '#4a7c59' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 主板SN号</label>
|
||||
<input value={boardSN} onChange={e => setBoardSN(e.target.value)} placeholder="如 MB20240308001" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<input value={boardSN} onChange={e => setBoardSN(e.target.value)} placeholder="扫码或手动输入" style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<button title="扫码录入" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', flexShrink: 0 }}>
|
||||
<ScanLine size={16} style={{ color: '#4a7c59' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 生产批次</label>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<select value={batchNo} onChange={e => setBatchNo(e.target.value)} style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">选择或输入批次号</option>
|
||||
<option value="BATCH-2025-Q1-001">BATCH-2025-Q1-001</option>
|
||||
<option value="BATCH-2025-Q1-002">BATCH-2025-Q1-002</option>
|
||||
<option value="BATCH-2025-Q2-001">BATCH-2025-Q2-001</option>
|
||||
</select>
|
||||
<input value={batchNo} onChange={e => setBatchNo(e.target.value)} placeholder="手动输入" style={{ width: 140, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 配置文件</label>
|
||||
<select value={selectedConfig} onChange={e => setSelectedConfig(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{(modelConfigOptions[deviceModel] || []).map(cfg => (
|
||||
<option key={cfg.name} value={cfg.name} disabled={cfg.status === '已停用'}>
|
||||
{cfg.name}{cfg.status === '已停用' ? '(已停用)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>装机测试状态</label>
|
||||
|
|
@ -128,7 +191,7 @@ export default function RegistrationPage() {
|
|||
<CheckCircle size={18} style={{ color: '#52C41A', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', lineHeight: 1.8 }}>
|
||||
<div>已匹配型号 <span style={{ fontWeight: 600 }}>{deviceModel}</span> 的关联信息:</div>
|
||||
<div>授权项:<span style={{ color: '#4a7c59' }}>{matchInfo.license}</span> · 配置文件:<span style={{ color: '#4a7c59' }}>{matchInfo.config}</span></div>
|
||||
<div>授权项:<span style={{ color: '#4a7c59' }}>{matchInfo.license}</span> · 配置文件:<span style={{ color: '#4a7c59' }}>{selectedConfig}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -144,14 +207,11 @@ export default function RegistrationPage() {
|
|||
<div style={{ backgroundColor: '#fff', borderRadius: 8, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>装机清单 BOM</h3>
|
||||
<button onClick={() => setImportOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 14px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||
<Upload size={14} />导入
|
||||
</button>
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['物料编码', '物料名称', 'SN号', '型号', '校准状态', '数量', '操作'].map(h => (
|
||||
{['物料编码', '物料名称', 'SN号', '型号', '版本', '校准状态', '数量', '操作'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 16px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -161,8 +221,28 @@ export default function RegistrationPage() {
|
|||
<tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '10px 16px', fontSize: 13 }}>{item.code}</td>
|
||||
<td style={{ padding: '10px 16px', fontSize: 13 }}>{item.name}</td>
|
||||
<td style={{ padding: '10px 16px', fontSize: 13, fontWeight: 500 }}>{item.sn}</td>
|
||||
<td style={{ padding: '10px 16px', fontSize: 13, fontWeight: 500 }}>
|
||||
{item.sn === '-' ? (
|
||||
<span style={{ color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<input value={item.sn} onChange={e => updateBomItem(item.id, 'sn', e.target.value)} placeholder="扫码或手动输入SN" style={{ width: 160, padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, fontSize: 13, boxSizing: 'border-box' }} />
|
||||
<button title="扫码录入SN" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 28, height: 28, border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', flexShrink: 0 }}>
|
||||
<ScanLine size={13} style={{ color: '#4a7c59' }} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{item.model}</td>
|
||||
<td style={{ padding: '10px 16px' }}>
|
||||
{(boardVersionOptions[item.model] || []).length > 1 ? (
|
||||
<select value={item.version} onChange={e => updateBomItem(item.id, 'version', e.target.value)} style={{ padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, fontSize: 13 }}>
|
||||
{(boardVersionOptions[item.model] || []).map(v => <option key={v} value={v}>{v}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, color: item.version === '-' ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.65)' }}>{item.version}</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px' }}>
|
||||
<span style={{ padding: '1px 6px', borderRadius: 4, fontSize: 11, ...(item.calibration === '已校准' ? { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' } : { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }) }}>{item.calibration}</span>
|
||||
</td>
|
||||
|
|
@ -177,6 +257,28 @@ export default function RegistrationPage() {
|
|||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 采集板版本一致性检查 */}
|
||||
{(() => {
|
||||
const acqBoards = bomList.filter(b => b.name === '采集板')
|
||||
const versions = [...new Set(acqBoards.map(b => b.version))]
|
||||
if (acqBoards.length > 1 && versions.length > 1) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 16px', backgroundColor: '#FFF1F0', borderTop: '1px solid #FFCCC7' }}>
|
||||
<AlertTriangle size={14} style={{ color: '#FF4D4F', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 12, color: '#FF4D4F' }}>采集板版本不一致({versions.join('、')}),要求同一台设备的采集板版本必须相同。</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (acqBoards.length > 1 && versions.length === 1) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 16px', backgroundColor: '#F6FFED', borderTop: '1px solid #B7EB8F' }}>
|
||||
<CheckCircle size={14} style={{ color: '#52C41A', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 12, color: '#52C41A' }}>采集板版本一致:{versions[0]}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 装配 Checklist */}
|
||||
|
|
@ -270,6 +372,18 @@ export default function RegistrationPage() {
|
|||
<button onClick={() => setPhotoOpen(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
|
||||
<button onClick={() => setPhotoCount(prev => ({ ...prev, [photoOpen]: (prev[photoOpen] || 0) + 1 }))} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, padding: '16px 12px', border: '2px dashed #D9D9D9', borderRadius: 8, backgroundColor: '#FAFAFA', cursor: 'pointer' }}>
|
||||
<Camera size={24} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>直接拍照上传</span>
|
||||
<span style={{ fontSize: 11, color: 'rgba(0,0,0,0.35)' }}>平板端直接拍照</span>
|
||||
</button>
|
||||
<button style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, padding: '16px 12px', border: '2px dashed #a3c4ad', borderRadius: 8, backgroundColor: '#eef5f0', cursor: 'pointer' }}>
|
||||
<QrCode size={24} style={{ color: '#4a7c59' }} />
|
||||
<span style={{ fontSize: 13, color: '#4a7c59' }}>手机扫码上传</span>
|
||||
<span style={{ fontSize: 11, color: 'rgba(0,0,0,0.35)' }}>生成二维码,手机扫码拍照</span>
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
|
||||
{Array.from({ length: photoCount[photoOpen] || 0 }).map((_, i) => (
|
||||
<div key={i} style={{ position: 'relative', aspectRatio: '1', backgroundColor: '#F0F0F0', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
|
|
@ -279,7 +393,7 @@ export default function RegistrationPage() {
|
|||
))}
|
||||
<div onClick={() => setPhotoCount(prev => ({ ...prev, [photoOpen]: (prev[photoOpen] || 0) + 1 }))} style={{ aspectRatio: '1', border: '2px dashed #D9D9D9', borderRadius: 6, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', gap: 4 }}>
|
||||
<Camera size={20} style={{ color: 'rgba(0,0,0,0.25)' }} />
|
||||
<span style={{ fontSize: 11, color: 'rgba(0,0,0,0.35)' }}>添加</span>
|
||||
<span style={{ fontSize: 11, color: 'rgba(0,0,0,0.35)' }}>添加更多</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue