parent
fe5a5472bb
commit
df2355e007
|
|
@ -1,3 +1,62 @@
|
|||
{"timestamp":"00:00:02.550","source":"Server","level":"LOG","message":""}
|
||||
{"timestamp":"00:00:09.331","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:01:35.847","source":"Server","level":"LOG","message":"✓ Compiled in 898ms"}
|
||||
{"timestamp":"00:00:02.205","source":"Server","level":"LOG","message":""}
|
||||
{"timestamp":"00:00:04.101","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:00:18.274","source":"Server","level":"ERROR","message":"⨯ SqliteError: no such column: \"\" - should this be a string literal in single-quotes?"}
|
||||
{"timestamp":"00:08:58.067","source":"Server","level":"LOG","message":"✓ Compiled in 107ms"}
|
||||
{"timestamp":"00:08:58.497","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: versionsByType is not defined"}
|
||||
{"timestamp":"00:08:58.523","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: versionsByType is not defined\\u001b[39m\\n\\u001b[31m at eval (src/app/materials/register/page.tsx:149:21)\\n at Array.map (<anonymous>)\\n at BoardRegisterPage (src/app/materials/register/page.tsx:123:18)\\u001b[39m\\n \\u001b[90m147 |\\u001b[0m <label style={{ display: \\u001b[32m'block'\\u001b[0m, fontSize: \\u001b[35m13\\u001b[0m, fontWeight: \\u001b[35m500\\u001b[0m, marginBottom: \\u001b[35m6\\u001b[0m }}><span style={{ color: \\u001b[32m'#FF4D4F'\\u001b[0m }}>*<\\u001b[35m/span...\\u001b[0m\\n \\u001b[90m148 |\\u001b[0m <select value={entry.version} onChange={e => updateEntry(entry.id, \\u001b[32m'version'\\u001b[0m, e.target.value)} style={{ width: \\u001b[32m'100%'\\u001b[0m, padding..\\u001b[32m.\\u001b[0m\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m149 |\\u001b[0m {(versionsByType[entry.\\u001b[36mtype\\u001b[0m] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m150 |\\u001b[0m </select>\\n \\u001b[90m151 |\\u001b[0m </div>\\n \\u001b[90m152 |\\u001b[0m\""}
|
||||
{"timestamp":"00:08:58.525","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: versionsByType is not defined\u001b[39m\n\u001b[31m at eval (src/app/materials/register/page.tsx:149:21)\n at Array.map (<anonymous>)\n at BoardRegisterPage (src/app/materials/register/page.tsx:123:18)\u001b[39m\n \u001b[90m147 |\u001b[0m <label style={{ display: \u001b[32m'block'\u001b[0m, fontSize: \u001b[35m13\u001b[0m, fontWeight: \u001b[35m500\u001b[0m, marginBottom: \u001b[35m6\u001b[0m }}><span style={{ color: \u001b[32m'#FF4D4F'\u001b[0m }}>*<\u001b[35m/span...\u001b[0m\n \u001b[90m148 |\u001b[0m <select value={entry.version} onChange={e => updateEntry(entry.id, \u001b[32m'version'\u001b[0m, e.target.value)} style={{ width: \u001b[32m'100%'\u001b[0m, padding..\u001b[32m.\u001b[0m\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m149 |\u001b[0m {(versionsByType[entry.\u001b[36mtype\u001b[0m] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m150 |\u001b[0m </select>\n \u001b[90m151 |\u001b[0m </div>\n \u001b[90m152 |\u001b[0m"}
|
||||
{"timestamp":"00:09:16.060","source":"Server","level":"LOG","message":"✓ Compiled in 69ms"}
|
||||
{"timestamp":"00:09:16.065","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"00:09:16.485","source":"Server","level":"ERROR","message":"⨯ ReferenceError: versionsByType is not defined"}
|
||||
{"timestamp":"00:09:16.517","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: versionsByType is not defined"}
|
||||
{"timestamp":"00:09:16.538","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: versionsByType is not defined\\u001b[39m\\n\\u001b[31m at eval (src/app/materials/register/page.tsx:140:21)\\n at Array.map (<anonymous>)\\n at BoardRegisterPage (src/app/materials/register/page.tsx:114:18)\\n at Set.forEach (<anonymous>)\\u001b[39m\\n \\u001b[90m138 |\\u001b[0m <label style={{ display: \\u001b[32m'block'\\u001b[0m, fontSize: \\u001b[35m13\\u001b[0m, fontWeight: \\u001b[35m500\\u001b[0m, marginBottom: \\u001b[35m6\\u001b[0m }}><span style={{ color: \\u001b[32m'#FF4D4F'\\u001b[0m }}>*<\\u001b[35m/span...\\u001b[0m\\n \\u001b[90m139 |\\u001b[0m <select value={entry.version} onChange={e => updateEntry(entry.id, \\u001b[32m'version'\\u001b[0m, e.target.value)} style={{ width: \\u001b[32m'100%'\\u001b[0m, padding..\\u001b[32m.\\u001b[0m\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m140 |\\u001b[0m {(versionsByType[entry.\\u001b[36mtype\\u001b[0m] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m141 |\\u001b[0m </select>\\n \\u001b[90m142 |\\u001b[0m </div>\\n \\u001b[90m143 |\\u001b[0m\""}
|
||||
{"timestamp":"00:09:16.539","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: versionsByType is not defined\u001b[39m\n\u001b[31m at eval (src/app/materials/register/page.tsx:140:21)\n at Array.map (<anonymous>)\n at BoardRegisterPage (src/app/materials/register/page.tsx:114:18)\n at Set.forEach (<anonymous>)\u001b[39m\n \u001b[90m138 |\u001b[0m <label style={{ display: \u001b[32m'block'\u001b[0m, fontSize: \u001b[35m13\u001b[0m, fontWeight: \u001b[35m500\u001b[0m, marginBottom: \u001b[35m6\u001b[0m }}><span style={{ color: \u001b[32m'#FF4D4F'\u001b[0m }}>*<\u001b[35m/span...\u001b[0m\n \u001b[90m139 |\u001b[0m <select value={entry.version} onChange={e => updateEntry(entry.id, \u001b[32m'version'\u001b[0m, e.target.value)} style={{ width: \u001b[32m'100%'\u001b[0m, padding..\u001b[32m.\u001b[0m\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m140 |\u001b[0m {(versionsByType[entry.\u001b[36mtype\u001b[0m] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m141 |\u001b[0m </select>\n \u001b[90m142 |\u001b[0m </div>\n \u001b[90m143 |\u001b[0m"}
|
||||
{"timestamp":"00:09:16.654","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:09:16.820","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: versionsByType is not defined"}
|
||||
{"timestamp":"00:09:16.825","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: versionsByType is not defined\\u001b[39m\\n\\u001b[31m at <unknown> (src/app/materials/register/page.tsx:140:21)\\n at Array.map (<anonymous>)\\n at BoardRegisterPage (src/app/materials/register/page.tsx:114:18)\\u001b[39m\\n \\u001b[90m138 |\\u001b[0m <label style={{ display: \\u001b[32m'block'\\u001b[0m, fontSize: \\u001b[35m13\\u001b[0m, fontWeight: \\u001b[35m500\\u001b[0m, marginBottom: \\u001b[35m6\\u001b[0m }}><span style={{ color: \\u001b[32m'#FF4D4F'\\u001b[0m }}>*<\\u001b[35m/span...\\u001b[0m\\n \\u001b[90m139 |\\u001b[0m <select value={entry.version} onChange={e => updateEntry(entry.id, \\u001b[32m'version'\\u001b[0m, e.target.value)} style={{ width: \\u001b[32m'100%'\\u001b[0m, padding..\\u001b[32m.\\u001b[0m\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m140 |\\u001b[0m {(versionsByType[entry.\\u001b[36mtype\\u001b[0m] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m141 |\\u001b[0m </select>\\n \\u001b[90m142 |\\u001b[0m </div>\\n \\u001b[90m143 |\\u001b[0m\""}
|
||||
{"timestamp":"00:09:16.826","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: versionsByType is not defined\u001b[39m\n\u001b[31m at <unknown> (src/app/materials/register/page.tsx:140:21)\n at Array.map (<anonymous>)\n at BoardRegisterPage (src/app/materials/register/page.tsx:114:18)\u001b[39m\n \u001b[90m138 |\u001b[0m <label style={{ display: \u001b[32m'block'\u001b[0m, fontSize: \u001b[35m13\u001b[0m, fontWeight: \u001b[35m500\u001b[0m, marginBottom: \u001b[35m6\u001b[0m }}><span style={{ color: \u001b[32m'#FF4D4F'\u001b[0m }}>*<\u001b[35m/span...\u001b[0m\n \u001b[90m139 |\u001b[0m <select value={entry.version} onChange={e => updateEntry(entry.id, \u001b[32m'version'\u001b[0m, e.target.value)} style={{ width: \u001b[32m'100%'\u001b[0m, padding..\u001b[32m.\u001b[0m\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m140 |\u001b[0m {(versionsByType[entry.\u001b[36mtype\u001b[0m] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m141 |\u001b[0m </select>\n \u001b[90m142 |\u001b[0m </div>\n \u001b[90m143 |\u001b[0m"}
|
||||
{"timestamp":"00:09:30.738","source":"Server","level":"LOG","message":"✓ Compiled in 99ms"}
|
||||
{"timestamp":"00:09:30.748","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"00:09:31.514","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:09:43.714","source":"Server","level":"LOG","message":"✓ Compiled in 69ms"}
|
||||
{"timestamp":"00:09:55.643","source":"Server","level":"LOG","message":"✓ Compiled in 51ms"}
|
||||
{"timestamp":"00:10:12.495","source":"Server","level":"LOG","message":"✓ Compiled in 68ms"}
|
||||
{"timestamp":"00:16:23.501","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"00:16:38.076","source":"Server","level":"LOG","message":"✓ Compiled in 42ms"}
|
||||
{"timestamp":"00:16:54.064","source":"Server","level":"LOG","message":"✓ Compiled in 65ms"}
|
||||
{"timestamp":"00:17:02.570","source":"Server","level":"LOG","message":"✓ Compiled in 56ms"}
|
||||
{"timestamp":"00:17:13.333","source":"Server","level":"LOG","message":"✓ Compiled in 81ms"}
|
||||
{"timestamp":"00:17:21.099","source":"Server","level":"LOG","message":"✓ Compiled in 70ms"}
|
||||
{"timestamp":"00:17:31.082","source":"Server","level":"LOG","message":"✓ Compiled in 110ms"}
|
||||
{"timestamp":"00:17:45.317","source":"Server","level":"LOG","message":"✓ Compiled in 61ms"}
|
||||
{"timestamp":"00:19:03.427","source":"Server","level":"ERROR","message":"⨯ SqliteError: UNIQUE constraint failed: materials.sn"}
|
||||
{"timestamp":"00:21:25.131","source":"Server","level":"LOG","message":"✓ Compiled in 92ms"}
|
||||
{"timestamp":"00:21:42.436","source":"Server","level":"LOG","message":"✓ Compiled in 77ms"}
|
||||
{"timestamp":"00:21:53.516","source":"Server","level":"LOG","message":"✓ Compiled in 89ms"}
|
||||
{"timestamp":"00:22:23.547","source":"Server","level":"LOG","message":"✓ Compiled in 50ms"}
|
||||
{"timestamp":"00:22:45.750","source":"Server","level":"LOG","message":"✓ Compiled in 71ms"}
|
||||
{"timestamp":"00:36:02.534","source":"Server","level":"LOG","message":"✓ Compiled in 78ms"}
|
||||
{"timestamp":"00:36:14.558","source":"Server","level":"LOG","message":"✓ Compiled in 65ms"}
|
||||
{"timestamp":"00:36:29.877","source":"Server","level":"LOG","message":"✓ Compiled in 55ms"}
|
||||
{"timestamp":"00:37:04.371","source":"Server","level":"LOG","message":"✓ Compiled in 68ms"}
|
||||
{"timestamp":"00:37:17.354","source":"Server","level":"LOG","message":"✓ Compiled in 47ms"}
|
||||
{"timestamp":"00:37:26.083","source":"Server","level":"LOG","message":"✓ Compiled in 51ms"}
|
||||
{"timestamp":"00:38:29.212","source":"Server","level":"LOG","message":"✓ Compiled in 68ms"}
|
||||
{"timestamp":"00:38:40.701","source":"Server","level":"LOG","message":"✓ Compiled in 70ms"}
|
||||
{"timestamp":"00:39:19.999","source":"Server","level":"LOG","message":"✓ Compiled in 46ms"}
|
||||
{"timestamp":"00:39:32.065","source":"Server","level":"LOG","message":"✓ Compiled in 72ms"}
|
||||
{"timestamp":"00:39:42.768","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"00:41:04.081","source":"Server","level":"LOG","message":"✓ Compiled in 65ms"}
|
||||
{"timestamp":"00:41:12.582","source":"Server","level":"LOG","message":"✓ Compiled in 72ms"}
|
||||
{"timestamp":"00:45:52.932","source":"Server","level":"LOG","message":"✓ Compiled in 92ms"}
|
||||
{"timestamp":"00:46:08.338","source":"Server","level":"LOG","message":"✓ Compiled in 146ms"}
|
||||
{"timestamp":"00:49:04.049","source":"Server","level":"LOG","message":"✓ Compiled in 110ms"}
|
||||
{"timestamp":"00:49:23.180","source":"Server","level":"LOG","message":"✓ Compiled in 75ms"}
|
||||
{"timestamp":"00:51:39.013","source":"Server","level":"LOG","message":"✓ Compiled in 58ms"}
|
||||
{"timestamp":"00:51:47.469","source":"Server","level":"LOG","message":"✓ Compiled in 63ms"}
|
||||
{"timestamp":"00:52:00.952","source":"Server","level":"LOG","message":"✓ Compiled in 46ms"}
|
||||
{"timestamp":"00:52:11.991","source":"Server","level":"LOG","message":"✓ Compiled in 87ms"}
|
||||
{"timestamp":"00:52:30.998","source":"Server","level":"LOG","message":"✓ Compiled in 143ms"}
|
||||
{"timestamp":"00:52:44.891","source":"Server","level":"LOG","message":"✓ Compiled in 89ms"}
|
||||
{"timestamp":"00:53:28.683","source":"Server","level":"LOG","message":"✓ Compiled in 47ms"}
|
||||
{"timestamp":"00:53:39.177","source":"Server","level":"LOG","message":"✓ Compiled in 42ms"}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
"dynamicRoutes": {},
|
||||
"notFoundRoutes": [],
|
||||
"preview": {
|
||||
"previewModeId": "c55ffe53b7d1e0d2210bb27bdcda1a52",
|
||||
"previewModeSigningKey": "29086773d8e35b0885c5db516b53db2363bab0a6a4585596e30d4b3fc76aec56",
|
||||
"previewModeEncryptionKey": "0bac8c20a0a44a84b1dbe2603c546b8176144734ca42089bab1ebc967f277a1a"
|
||||
"previewModeId": "2947f9339b9bc375ef2ec0325fd70e0c",
|
||||
"previewModeSigningKey": "5fbfa8d7aaae4f6432fbd46cb9d1d06b81f02018d11093ad9f7c9c31305961d2",
|
||||
"previewModeEncryptionKey": "ef8ca519881bb07275db39a1c0b131c870bd7a64c5097b76aef811ad5130b44e"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,19 @@
|
|||
{
|
||||
"/api/dashboard/route": "app/api/dashboard/route.js",
|
||||
"/api/devices/route": "app/api/devices/route.js",
|
||||
"/api/firmware/route": "app/api/firmware/route.js",
|
||||
"/api/material-categories/route": "app/api/material-categories/route.js",
|
||||
"/api/material-types/route": "app/api/material-types/route.js",
|
||||
"/api/material-versions/route": "app/api/material-versions/route.js",
|
||||
"/api/materials/route": "app/api/materials/route.js",
|
||||
"/api/models/bom/route": "app/api/models/bom/route.js",
|
||||
"/api/models/checklist/route": "app/api/models/checklist/route.js",
|
||||
"/api/models/route": "app/api/models/route.js",
|
||||
"/api/repair/route": "app/api/repair/route.js",
|
||||
"/api/scrap/route": "app/api/scrap/route.js",
|
||||
"/config-files/page": "app/config-files/page.js",
|
||||
"/devices/page": "app/devices/page.js",
|
||||
"/firmware/page": "app/firmware/page.js",
|
||||
"/licenses/page": "app/licenses/page.js",
|
||||
"/materials/manage/page": "app/materials/manage/page.js",
|
||||
"/materials/page": "app/materials/page.js",
|
||||
"/materials/register/page": "app/materials/register/page.js",
|
||||
"/models/bom/page": "app/models/bom/page.js",
|
||||
"/models/page": "app/models/page.js",
|
||||
"/page": "app/page.js",
|
||||
"/repair/page": "app/repair/page.js",
|
||||
"/scrap/page": "app/scrap/page.js"
|
||||
"/registration/page": "app/registration/page.js"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
336
.next/dev/trace
336
.next/dev/trace
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +1,8 @@
|
|||
// This file is generated automatically by Next.js
|
||||
// Do not edit this file manually
|
||||
|
||||
type AppRoutes = "/" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/materials" | "/materials/manage" | "/materials/register" | "/models" | "/models/bom" | "/registration" | "/repair" | "/scrap"
|
||||
type AppRouteHandlerRoutes = "/api/board-types" | "/api/boards" | "/api/config-files" | "/api/devices" | "/api/firmware" | "/api/licenses" | "/api/material-types" | "/api/material-versions" | "/api/materials" | "/api/models" | "/api/models/bom" | "/api/models/checklist" | "/api/repair" | "/api/scrap"
|
||||
type AppRoutes = "/" | "/app-versions" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/materials" | "/materials/categories" | "/materials/manage" | "/materials/register" | "/models" | "/models/bom" | "/registration" | "/repair" | "/scrap"
|
||||
type AppRouteHandlerRoutes = "/api/app-versions" | "/api/board-types" | "/api/boards" | "/api/config-files" | "/api/dashboard" | "/api/devices" | "/api/firmware" | "/api/licenses" | "/api/material-categories" | "/api/material-types" | "/api/material-versions" | "/api/materials" | "/api/models" | "/api/models/bom" | "/api/models/checklist" | "/api/repair" | "/api/scrap"
|
||||
type PageRoutes = never
|
||||
type LayoutRoutes = "/"
|
||||
type RedirectRoutes = never
|
||||
|
|
@ -12,12 +12,15 @@ type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRo
|
|||
|
||||
interface ParamMap {
|
||||
"/": {}
|
||||
"/api/app-versions": {}
|
||||
"/api/board-types": {}
|
||||
"/api/boards": {}
|
||||
"/api/config-files": {}
|
||||
"/api/dashboard": {}
|
||||
"/api/devices": {}
|
||||
"/api/firmware": {}
|
||||
"/api/licenses": {}
|
||||
"/api/material-categories": {}
|
||||
"/api/material-types": {}
|
||||
"/api/material-versions": {}
|
||||
"/api/materials": {}
|
||||
|
|
@ -26,12 +29,14 @@ interface ParamMap {
|
|||
"/api/models/checklist": {}
|
||||
"/api/repair": {}
|
||||
"/api/scrap": {}
|
||||
"/app-versions": {}
|
||||
"/config-files": {}
|
||||
"/devices": {}
|
||||
"/devices/[sn]": { "sn": string; }
|
||||
"/firmware": {}
|
||||
"/licenses": {}
|
||||
"/materials": {}
|
||||
"/materials/categories": {}
|
||||
"/materials/manage": {}
|
||||
"/materials/register": {}
|
||||
"/models": {}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRou
|
|||
}
|
||||
|
||||
|
||||
// Validate ../../../src/app/app-versions/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/app-versions">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/app-versions/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/config-files/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/config-files">> = Specific
|
||||
|
|
@ -92,6 +101,15 @@ type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRou
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/materials/categories/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/materials/categories">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/materials/categories/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/materials/manage/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/materials/manage">> = Specific
|
||||
|
|
@ -173,6 +191,15 @@ type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRou
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/api/app-versions/route.ts
|
||||
{
|
||||
type __IsExpected<Specific extends RouteHandlerConfig<"/api/app-versions">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/api/app-versions/route.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/api/board-types/route.ts
|
||||
{
|
||||
type __IsExpected<Specific extends RouteHandlerConfig<"/api/board-types">> = Specific
|
||||
|
|
@ -200,6 +227,15 @@ type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRou
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/api/dashboard/route.ts
|
||||
{
|
||||
type __IsExpected<Specific extends RouteHandlerConfig<"/api/dashboard">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/api/dashboard/route.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/api/devices/route.ts
|
||||
{
|
||||
type __IsExpected<Specific extends RouteHandlerConfig<"/api/devices">> = Specific
|
||||
|
|
@ -227,6 +263,15 @@ type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRou
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/api/material-categories/route.ts
|
||||
{
|
||||
type __IsExpected<Specific extends RouteHandlerConfig<"/api/material-categories">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/api/material-categories/route.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/api/material-types/route.ts
|
||||
{
|
||||
type __IsExpected<Specific extends RouteHandlerConfig<"/api/material-types">> = Specific
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
# 应用管理实体关系图
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
Application["应用列表 Application"] {
|
||||
id BIGINT "主键"
|
||||
name VARCHAR "应用名称"
|
||||
package_name VARCHAR "应用包名/标识符"
|
||||
description TEXT "应用描述"
|
||||
logo_url VARCHAR "应用logo地址"
|
||||
status TINYINT "状态(启用/禁用)"
|
||||
create_time DATETIME "创建时间"
|
||||
update_time DATETIME "更新时间"
|
||||
}
|
||||
|
||||
Platform["应用平台列表 Platform"] {
|
||||
id BIGINT "主键"
|
||||
app_id BIGINT "应用ID"
|
||||
platform_type INT "平台类型"
|
||||
description VARCHAR "描述"
|
||||
extend_info VARCHAR "扩展信息"
|
||||
create_time DATETIME "创建时间"
|
||||
}
|
||||
|
||||
PlatformVersion["版本信息 PlatformVersion"] {
|
||||
id BIGINT "主键"
|
||||
app_id BIGINT "应用ID"
|
||||
platform_id BIGINT "平台ID"
|
||||
major_version INT "主版本号"
|
||||
minor_version INT "次版本号"
|
||||
patch_version INT "修订版本号"
|
||||
version_name VARCHAR "版本显示名称(如1.2.3)"
|
||||
description TEXT "版本描述"
|
||||
file_type VARCHAR "文件类型"
|
||||
file_url VARCHAR "安装包地址"
|
||||
file_size BIGINT "文件大小"
|
||||
distribution_type VARCHAR "分发类型"
|
||||
primary_url VARCHAR "主要分发链接"
|
||||
fallback_url VARCHAR "备用链接"
|
||||
url_expire_time DATETIME "链接有效期"
|
||||
signature_info TEXT "签名信息"
|
||||
min_support_version VARCHAR "最低支持版本"
|
||||
os_min_version VARCHAR "最低系统版本"
|
||||
status TINYINT "版本状态"
|
||||
is_force_update BOOLEAN "是否强制更新"
|
||||
}
|
||||
|
||||
Statistics["app统计信息 Statistics"] {
|
||||
id BIGINT "主键"
|
||||
app_id BIGINT "应用ID"
|
||||
version_id BIGINT "版本ID"
|
||||
date DATE "日期"
|
||||
download_count INT "下载量"
|
||||
install_count INT "安装量"
|
||||
active_count INT "活跃量"
|
||||
crash_count INT "崩溃次数"
|
||||
create_time DATETIME "创建时间"
|
||||
}
|
||||
|
||||
Application ||--o{ Platform : "拥有平台"
|
||||
Platform ||--o{ PlatformVersion : "拥有版本"
|
||||
Application ||--o{ PlatformVersion : "拥有版本"
|
||||
Application ||--o{ Statistics : "拥有统计"
|
||||
PlatformVersion ||--o{ Statistics : "拥有统计"
|
||||
```
|
||||
|
||||
## 枚举定义
|
||||
|
||||
### platform_type 平台类型
|
||||
|
||||
| 值 | 名称 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | iOS | Apple 移动端 |
|
||||
| 2 | Android | Android 移动端 |
|
||||
| 3 | HarmonyOS | 鸿蒙移动端 |
|
||||
| 4 | Windows | Windows 桌面端 |
|
||||
| 5 | macOS | macOS 桌面端 |
|
||||
| 6 | Linux | Linux 桌面端 |
|
||||
| 7 | Web | 网页端 |
|
||||
|
||||
### file_type 文件类型
|
||||
|
||||
| 可选值 | 说明 |
|
||||
|--------|------|
|
||||
| ipa | iOS 安装包 |
|
||||
| apk | Android 安装包 |
|
||||
| aab | Android App Bundle |
|
||||
| hap | HarmonyOS 安装包 |
|
||||
| exe | Windows 可执行文件 |
|
||||
| msi | Windows 安装程序 |
|
||||
| appx | Windows 应用包 |
|
||||
| dmg | macOS 磁盘映像 |
|
||||
| pkg | macOS 安装包 |
|
||||
| deb | Linux Debian 包 |
|
||||
| rpm | Linux RPM 包 |
|
||||
| AppImage | Linux 通用包 |
|
||||
|
||||
### distribution_type 分发类型
|
||||
|
||||
| 平台 | 可选值 | 说明 |
|
||||
|------|--------|------|
|
||||
| iOS | app_store | App Store 分发 |
|
||||
| iOS | testflight | TestFlight 测试分发 |
|
||||
| iOS | enterprise | 企业签名分发 |
|
||||
| Android | google_play | Google Play 分发 |
|
||||
| Android | direct | 直接下载分发 |
|
||||
| Android | huawei | 华为应用市场分发 |
|
||||
| Windows | microsoft_store | Microsoft Store 分发 |
|
||||
| Windows | direct | 直接下载分发 |
|
||||
|
||||
### status 状态
|
||||
|
||||
| 值 | 名称 | 说明 |
|
||||
|---|------|------|
|
||||
| 0 | 禁用 | 应用/版本已禁用 |
|
||||
| 1 | 启用 | 应用/版本已启用 |
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getDb } from '@/lib/db'
|
||||
import { seedIfEmpty } from '@/lib/seed'
|
||||
|
||||
const PLATFORM_MAP: Record<number, string> = { 1: 'iOS', 2: 'Android', 3: 'HarmonyOS', 4: 'Windows', 5: 'macOS', 6: 'Linux', 7: 'Web' }
|
||||
const PLATFORM_REVERSE: Record<string, number> = Object.fromEntries(Object.entries(PLATFORM_MAP).map(([k, v]) => [v, Number(k)]))
|
||||
|
||||
const FILE_TYPES_BY_PLATFORM: Record<number, string[]> = {
|
||||
1: ['ipa'], 2: ['apk', 'aab'], 3: ['hap'], 4: ['exe', 'msi', 'appx'], 5: ['dmg', 'pkg'], 6: ['deb', 'rpm', 'AppImage'], 7: [],
|
||||
}
|
||||
|
||||
const DIST_TYPES_BY_PLATFORM: Record<number, { value: string; label: string }[]> = {
|
||||
1: [{ value: 'app_store', label: 'App Store' }, { value: 'testflight', label: 'TestFlight' }, { value: 'enterprise', label: '企业签名' }],
|
||||
2: [{ value: 'google_play', label: 'Google Play' }, { value: 'direct', label: '直接下载' }, { value: 'huawei', label: '华为应用市场' }],
|
||||
3: [{ value: 'huawei', label: '华为应用市场' }, { value: 'direct', label: '直接下载' }],
|
||||
4: [{ value: 'microsoft_store', label: 'Microsoft Store' }, { value: 'direct', label: '直接下载' }],
|
||||
5: [{ value: 'app_store', label: 'Mac App Store' }, { value: 'direct', label: '直接下载' }],
|
||||
6: [{ value: 'direct', label: '直接下载' }],
|
||||
7: [{ value: 'direct', label: '直接访问' }],
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const appId = req.nextUrl.searchParams.get('app_id')
|
||||
const meta = req.nextUrl.searchParams.get('meta')
|
||||
|
||||
// 返回枚举元数据
|
||||
if (meta === 'enums') {
|
||||
return NextResponse.json({ platforms: PLATFORM_MAP, fileTypes: FILE_TYPES_BY_PLATFORM, distTypes: DIST_TYPES_BY_PLATFORM })
|
||||
}
|
||||
|
||||
if (appId) {
|
||||
const app = db.prepare('SELECT * FROM applications WHERE id = ?').get(appId) as Record<string, unknown> | undefined
|
||||
if (!app) return NextResponse.json({ error: 'not found' }, { status: 404 })
|
||||
const platforms = db.prepare('SELECT * FROM app_platforms WHERE app_id = ? ORDER BY id').all(appId) as Record<string, unknown>[]
|
||||
const versions = db.prepare('SELECT * FROM app_platform_versions WHERE app_id = ? ORDER BY major_version DESC, minor_version DESC, patch_version DESC').all(appId) as Record<string, unknown>[]
|
||||
const parsedVersions = versions.map(v => ({
|
||||
...v,
|
||||
changelog: JSON.parse(v.changelog as string),
|
||||
platform_name: PLATFORM_MAP[(platforms.find(p => p.id === v.platform_id) as Record<string, unknown>)?.platform_type as number] || '未知',
|
||||
}))
|
||||
const parsedPlatforms = platforms.map(p => ({
|
||||
...p,
|
||||
platform_name: PLATFORM_MAP[p.platform_type as number] || '未知',
|
||||
file_types: FILE_TYPES_BY_PLATFORM[p.platform_type as number] || [],
|
||||
dist_types: DIST_TYPES_BY_PLATFORM[p.platform_type as number] || [],
|
||||
}))
|
||||
return NextResponse.json({ ...app, platforms: parsedPlatforms, versions: parsedVersions })
|
||||
}
|
||||
|
||||
const apps = db.prepare('SELECT * FROM applications ORDER BY id DESC').all() as Record<string, unknown>[]
|
||||
const result = apps.map(app => {
|
||||
const platforms = db.prepare('SELECT * FROM app_platforms WHERE app_id = ?').all(app.id) as Record<string, unknown>[]
|
||||
const versionCount = (db.prepare('SELECT COUNT(*) as c FROM app_platform_versions WHERE app_id = ?').get(app.id) as { c: number }).c
|
||||
return { ...app, platforms: platforms.map(p => ({ ...p, platform_name: PLATFORM_MAP[p.platform_type as number] || '未知' })), versionCount }
|
||||
})
|
||||
return NextResponse.json(result)
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const body = await req.json()
|
||||
const { action } = body
|
||||
const now = new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||||
|
||||
if (action === 'create_app') {
|
||||
const { name, package_name, description, logo_url } = body
|
||||
const result = db.prepare('INSERT INTO applications (name, package_name, description, logo_url, status, create_time, update_time) VALUES (?, ?, ?, ?, 1, ?, ?)').run(name, package_name || '', description || '', logo_url || '', now, now)
|
||||
return NextResponse.json({ id: result.lastInsertRowid })
|
||||
}
|
||||
|
||||
if (action === 'add_platform') {
|
||||
const { app_id, platform_type, description } = body
|
||||
const pt = typeof platform_type === 'string' ? (PLATFORM_REVERSE[platform_type] || 2) : platform_type
|
||||
const result = db.prepare('INSERT INTO app_platforms (app_id, platform_type, description, extend_info, create_time) VALUES (?, ?, ?, ?, ?)').run(app_id, pt, description || '', '', now)
|
||||
return NextResponse.json({ id: result.lastInsertRowid })
|
||||
}
|
||||
|
||||
if (action === 'add_version') {
|
||||
const { app_id, platform_id, major_version, minor_version, patch_version, version_name, description, file_type, file_url, file_size, distribution_type, primary_url, fallback_url, signature_info, min_support_version, os_min_version, is_force_update, changelog } = body
|
||||
const result = db.prepare(`INSERT INTO app_platform_versions (app_id, platform_id, major_version, minor_version, patch_version, version_name, description, file_type, file_url, file_size, distribution_type, primary_url, fallback_url, signature_info, min_support_version, os_min_version, status, is_force_update, release_date, changelog) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`).run(
|
||||
app_id, platform_id, major_version || 0, minor_version || 0, patch_version || 0,
|
||||
version_name || `${major_version || 0}.${minor_version || 0}.${patch_version || 0}`,
|
||||
description || '', file_type || '', file_url || '', file_size || 0,
|
||||
distribution_type || '', primary_url || '', fallback_url || '', signature_info || '',
|
||||
min_support_version || '', os_min_version || '', is_force_update ? 1 : 0,
|
||||
now.substring(0, 10), JSON.stringify(changelog || [])
|
||||
)
|
||||
return NextResponse.json({ id: result.lastInsertRowid })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'unknown action' }, { status: 400 })
|
||||
}
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const body = await req.json()
|
||||
const { action } = body
|
||||
const now = new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||||
|
||||
if (action === 'update_version_status') {
|
||||
const { id, status } = body
|
||||
db.prepare('UPDATE app_platform_versions SET status = ? WHERE id = ?').run(status, id)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
||||
if (action === 'update_app') {
|
||||
const { id, name, package_name, description, status } = body
|
||||
db.prepare('UPDATE applications SET name=?, package_name=?, description=?, status=?, update_time=? WHERE id=?').run(name, package_name || '', description || '', status ?? 1, now, id)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'unknown action' }, { status: 400 })
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const id = req.nextUrl.searchParams.get('id')
|
||||
const type = req.nextUrl.searchParams.get('type')
|
||||
if (type === 'app' && id) {
|
||||
db.prepare('DELETE FROM app_statistics WHERE app_id = ?').run(id)
|
||||
db.prepare('DELETE FROM app_platform_versions WHERE app_id = ?').run(id)
|
||||
db.prepare('DELETE FROM app_platforms WHERE app_id = ?').run(id)
|
||||
db.prepare('DELETE FROM applications WHERE id = ?').run(id)
|
||||
}
|
||||
if (type === 'version' && id) {
|
||||
db.prepare('DELETE FROM app_platform_versions WHERE id = ?').run(id)
|
||||
}
|
||||
if (type === 'platform' && id) {
|
||||
db.prepare('DELETE FROM app_platform_versions WHERE platform_id = ?').run(id)
|
||||
db.prepare('DELETE FROM app_platforms WHERE id = ?').run(id)
|
||||
}
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { getDb } from '@/lib/db'
|
||||
import { seedIfEmpty } from '@/lib/seed'
|
||||
|
||||
export async function GET() {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
|
||||
const q = (sql: string) => (db.prepare(sql).get() as { c: number }).c
|
||||
|
||||
const devices = {
|
||||
total: q('SELECT COUNT(*) as c FROM devices'),
|
||||
assembling: q("SELECT COUNT(*) as c FROM devices WHERE status = '装配中'"),
|
||||
activated: q("SELECT COUNT(*) as c FROM devices WHERE status = '已激活'"),
|
||||
shipped: q("SELECT COUNT(*) as c FROM devices WHERE status = '已出厂'"),
|
||||
}
|
||||
|
||||
const materials = {
|
||||
total: q('SELECT COUNT(*) as c FROM materials'),
|
||||
inStock: q("SELECT COUNT(*) as c FROM materials WHERE status = '在库'"),
|
||||
assembled: q("SELECT COUNT(*) as c FROM materials WHERE status = '已装配'"),
|
||||
faulty: q("SELECT COUNT(*) as c FROM materials WHERE status = '故障'"),
|
||||
}
|
||||
|
||||
const repair = {
|
||||
total: q('SELECT COUNT(*) as c FROM repair_orders'),
|
||||
pending: q("SELECT COUNT(*) as c FROM repair_orders WHERE status = '待处理'"),
|
||||
processing: q("SELECT COUNT(*) as c FROM repair_orders WHERE status = '处理中'"),
|
||||
done: q("SELECT COUNT(*) as c FROM repair_orders WHERE status = '已处理'"),
|
||||
}
|
||||
|
||||
const scrap = {
|
||||
total: q('SELECT COUNT(*) as c FROM scrap_records'),
|
||||
pendingApproval: q("SELECT COUNT(*) as c FROM scrap_records WHERE status = '待审批'"),
|
||||
}
|
||||
|
||||
const firmware = { total: q('SELECT COUNT(*) as c FROM firmware') }
|
||||
const licenses = { total: q('SELECT COUNT(*) as c FROM licenses') }
|
||||
|
||||
// Recent pending repair orders
|
||||
const recentRepairs = db.prepare("SELECT id, sn, fault_type, status, priority, create_date FROM repair_orders WHERE status != '已处理' ORDER BY create_date DESC LIMIT 4").all()
|
||||
|
||||
// Recent scrap pending approval
|
||||
const recentScraps = db.prepare("SELECT id, sn, model, status, date FROM scrap_records WHERE status = '待审批' OR status = '审批中' ORDER BY date DESC LIMIT 4").all()
|
||||
|
||||
return NextResponse.json({ devices, materials, repair, scrap, firmware, licenses, recentRepairs, recentScraps })
|
||||
}
|
||||
|
|
@ -13,7 +13,12 @@ export async function GET(req: NextRequest) {
|
|||
} else if (model) {
|
||||
items = db.prepare('SELECT * FROM firmware WHERE model_code = ? ORDER BY date DESC').all(model)
|
||||
} else {
|
||||
items = db.prepare('SELECT * FROM firmware ORDER BY date DESC').all()
|
||||
const type = req.nextUrl.searchParams.get('type') || ''
|
||||
if (type) {
|
||||
items = db.prepare('SELECT * FROM firmware WHERE type = ? ORDER BY date DESC').all(type)
|
||||
} else {
|
||||
items = db.prepare('SELECT * FROM firmware ORDER BY date DESC').all()
|
||||
}
|
||||
}
|
||||
const parsed = (items as { notes: string; [k: string]: unknown }[]).map(i => ({ ...i, notes: JSON.parse(i.notes as string) }))
|
||||
return NextResponse.json(parsed)
|
||||
|
|
@ -24,6 +29,6 @@ export async function POST(req: Request) {
|
|||
const db = getDb()
|
||||
const body = await req.json()
|
||||
const { version, board_version, type, date, status, size, hw_range, upgrade_type, signed, notes, model_code } = body
|
||||
const result = db.prepare('INSERT INTO firmware (version, board_version, type, date, status, size, downloads, hw_range, upgrade_type, signed, md5, sha256, notes, model_code) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, "", "", ?, ?)').run(version, board_version || '-', type, date || new Date().toISOString().split('T')[0], status || '草稿', size || '', hw_range || '', upgrade_type || '可选', signed ? 1 : 0, JSON.stringify(notes || []), model_code || '')
|
||||
const result = db.prepare("INSERT INTO firmware (version, board_version, type, date, status, size, downloads, hw_range, upgrade_type, signed, md5, sha256, notes, model_code) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, '', '', ?, ?)").run(version, board_version || '-', type, date || new Date().toISOString().split('T')[0], status || '草稿', size || '', hw_range || '', upgrade_type || '可选', signed ? 1 : 0, JSON.stringify(notes || []), model_code || '')
|
||||
return NextResponse.json({ id: result.lastInsertRowid })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { getDb } from '@/lib/db'
|
||||
import { seedIfEmpty } from '@/lib/seed'
|
||||
|
||||
export async function GET() {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const items = db.prepare('SELECT * FROM material_categories ORDER BY sort_order').all()
|
||||
return NextResponse.json(items)
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const body = await req.json()
|
||||
const { name, description, has_firmware, has_calibration, sort_order, status } = body
|
||||
const result = db.prepare('INSERT INTO material_categories (name, description, has_firmware, has_calibration, sort_order, status) VALUES (?, ?, ?, ?, ?, ?)').run(name, description || '', has_firmware ? 1 : 0, has_calibration ? 1 : 0, sort_order || 0, status || '启用')
|
||||
return NextResponse.json({ id: result.lastInsertRowid })
|
||||
}
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const body = await req.json()
|
||||
const { id, name, description, has_firmware, has_calibration, sort_order, status } = body
|
||||
db.prepare('UPDATE material_categories SET name=?, description=?, has_firmware=?, has_calibration=?, sort_order=?, status=? WHERE id=?').run(name, description || '', has_firmware ? 1 : 0, has_calibration ? 1 : 0, sort_order || 0, status || '启用', id)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const { id } = await req.json()
|
||||
db.prepare('DELETE FROM material_categories WHERE id = ?').run(id)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
|
@ -17,3 +17,11 @@ export async function POST(req: Request) {
|
|||
const result = db.prepare('INSERT INTO materials (sn, name, category, type, device_model, version, description, firmware, status, device_sn, production_date, calib_status, calib_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(sn, name || '', category || '', type, device_model || '', version, description || '', firmware || '-', status || '在库', device_sn || '-', production_date, calib_status || '-', calib_date || '-')
|
||||
return NextResponse.json({ id: result.lastInsertRowid })
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const { id } = await req.json()
|
||||
db.prepare('DELETE FROM materials WHERE id = ?').run(id)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,3 +19,17 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
return NextResponse.json(grouped)
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
seedIfEmpty()
|
||||
const db = getDb()
|
||||
const body = await req.json()
|
||||
const { model_code, items } = body as { model_code: string; items: { name: string; required: boolean }[] }
|
||||
const insert = db.prepare('INSERT INTO checklist_templates (model_code, name, required, sort_order) VALUES (?, ?, ?, ?)')
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].name.trim()) {
|
||||
insert.run(model_code, items[i].name.trim(), items[i].required ? 1 : 0, i)
|
||||
}
|
||||
}
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,472 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Plus, Upload, Download, X, ChevronDown, ChevronUp, Smartphone, Package, Trash2, Layers, Monitor, Globe } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
interface AppPlatform { id: number; app_id: number; platform_type: number; platform_name: string; description: string; file_types: string[]; dist_types: { value: string; label: string }[] }
|
||||
interface AppVersion { id: number; app_id: number; platform_id: number; major_version: number; minor_version: number; patch_version: number; version_name: string; description: string; file_type: string; file_url: string; file_size: number; distribution_type: string; primary_url: string; min_support_version: string; os_min_version: string; status: number; is_force_update: number; release_date: string; changelog: string[]; platform_name: string }
|
||||
interface AppItem { id: number; name: string; package_name: string; description: string; status: number; create_time: string; platforms: { id: number; platform_type: number; platform_name: string }[]; versionCount: number }
|
||||
interface AppDetail extends Omit<AppItem, 'versionCount'> { platforms: AppPlatform[]; versions: AppVersion[] }
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ value: 1, label: 'iOS', icon: Smartphone },
|
||||
{ value: 2, label: 'Android', icon: Smartphone },
|
||||
{ value: 3, label: 'HarmonyOS', icon: Smartphone },
|
||||
{ value: 4, label: 'Windows', icon: Monitor },
|
||||
{ value: 5, label: 'macOS', icon: Monitor },
|
||||
{ value: 6, label: 'Linux', icon: Monitor },
|
||||
{ value: 7, label: 'Web', icon: Globe },
|
||||
]
|
||||
|
||||
const FILE_TYPES: Record<number, string[]> = { 1: ['ipa'], 2: ['apk', 'aab'], 3: ['hap'], 4: ['exe', 'msi', 'appx'], 5: ['dmg', 'pkg'], 6: ['deb', 'rpm', 'AppImage'], 7: [] }
|
||||
const DIST_TYPES: Record<number, { value: string; label: string }[]> = {
|
||||
1: [{ value: 'app_store', label: 'App Store' }, { value: 'testflight', label: 'TestFlight' }, { value: 'enterprise', label: '企业签名' }],
|
||||
2: [{ value: 'google_play', label: 'Google Play' }, { value: 'direct', label: '直接下载' }, { value: 'huawei', label: '华为应用市场' }],
|
||||
3: [{ value: 'huawei', label: '华为应用市场' }, { value: 'direct', label: '直接下载' }],
|
||||
4: [{ value: 'microsoft_store', label: 'Microsoft Store' }, { value: 'direct', label: '直接下载' }],
|
||||
5: [{ value: 'app_store', label: 'Mac App Store' }, { value: 'direct', label: '直接下载' }],
|
||||
6: [{ value: 'direct', label: '直接下载' }], 7: [{ value: 'direct', label: '直接访问' }],
|
||||
}
|
||||
|
||||
function vStatusStyle(s: number) {
|
||||
return s === 1 ? { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
: s === 0 ? { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
: { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
}
|
||||
const vStatusText = (s: number) => s === 1 ? '启用' : '禁用'
|
||||
|
||||
export default function AppManagePage() {
|
||||
const { data: apps, loading, refetch } = useApi<AppItem[]>('/api/app-versions', [])
|
||||
const [selectedAppId, setSelectedAppId] = useState<number | null>(null)
|
||||
const [appDetail, setAppDetail] = useState<AppDetail | null>(null)
|
||||
const [appDrawer, setAppDrawer] = useState(false)
|
||||
const [platformDrawer, setPlatformDrawer] = useState(false)
|
||||
const [versionDrawer, setVersionDrawer] = useState(false)
|
||||
const [appForm, setAppForm] = useState({ name: '', package_name: '', description: '', platform_type: 2, major: 1, minor: 0, patch: 0, file_type: '', file_size: '', distribution_type: '', os_min_version: '', is_force_update: false, changelog: '' })
|
||||
const [platForm, setPlatForm] = useState({ platform_type: 2, description: '' })
|
||||
const [verForm, setVerForm] = useState({ platform_id: 0, major: 0, minor: 0, patch: 0, description: '', file_type: '', file_size: '', distribution_type: '', os_min_version: '', is_force_update: false, changelog: '' })
|
||||
const [expandedVerId, setExpandedVerId] = useState<number | null>(null)
|
||||
|
||||
const loadDetail = async (id: number) => {
|
||||
setSelectedAppId(id)
|
||||
const r = await fetch(`/api/app-versions?app_id=${id}`)
|
||||
setAppDetail(await r.json())
|
||||
}
|
||||
const handleCreateApp = async () => {
|
||||
// 1. 创建应用
|
||||
const appRes = await fetch('/api/app-versions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create_app', name: appForm.name, package_name: appForm.package_name, description: appForm.description }) })
|
||||
const { id: appId } = await appRes.json()
|
||||
// 2. 创建平台
|
||||
const platRes = await fetch('/api/app-versions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add_platform', app_id: appId, platform_type: appForm.platform_type }) })
|
||||
const { id: platformId } = await platRes.json()
|
||||
// 3. 创建首个版本
|
||||
const vn = `${appForm.major}.${appForm.minor}.${appForm.patch}`
|
||||
await fetch('/api/app-versions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add_version', app_id: appId, platform_id: platformId, major_version: appForm.major, minor_version: appForm.minor, patch_version: appForm.patch, version_name: vn, file_type: appForm.file_type, file_size: parseInt(appForm.file_size) || 0, distribution_type: appForm.distribution_type, os_min_version: appForm.os_min_version, is_force_update: appForm.is_force_update, changelog: appForm.changelog.split('\n').filter(Boolean) }) })
|
||||
refetch(); setAppDrawer(false); loadDetail(appId)
|
||||
}
|
||||
const handleAddPlatform = async () => {
|
||||
if (!selectedAppId) return
|
||||
await fetch('/api/app-versions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add_platform', app_id: selectedAppId, platform_type: platForm.platform_type, description: platForm.description }) })
|
||||
loadDetail(selectedAppId); setPlatformDrawer(false)
|
||||
}
|
||||
const handleAddVersion = async () => {
|
||||
if (!selectedAppId || !verForm.platform_id) return
|
||||
const vn = `${verForm.major}.${verForm.minor}.${verForm.patch}`
|
||||
await fetch('/api/app-versions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add_version', app_id: selectedAppId, platform_id: verForm.platform_id, major_version: verForm.major, minor_version: verForm.minor, patch_version: verForm.patch, version_name: vn, description: verForm.description, file_type: verForm.file_type, file_size: parseInt(verForm.file_size) || 0, distribution_type: verForm.distribution_type, os_min_version: verForm.os_min_version, is_force_update: verForm.is_force_update, changelog: verForm.changelog.split('\n').filter(Boolean) }) })
|
||||
loadDetail(selectedAppId); setVersionDrawer(false)
|
||||
}
|
||||
const handleVerStatus = async (id: number, status: number) => {
|
||||
await fetch('/api/app-versions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_version_status', id, status }) })
|
||||
if (selectedAppId) loadDetail(selectedAppId)
|
||||
}
|
||||
const handleDeleteApp = async (id: number) => {
|
||||
await fetch(`/api/app-versions?id=${id}&type=app`, { method: 'DELETE' })
|
||||
refetch(); if (selectedAppId === id) { setSelectedAppId(null); setAppDetail(null) }
|
||||
}
|
||||
const handleDeletePlatform = async (pid: number) => {
|
||||
await fetch(`/api/app-versions?id=${pid}&type=platform`, { method: 'DELETE' })
|
||||
if (selectedAppId) loadDetail(selectedAppId)
|
||||
}
|
||||
|
||||
// 当前选中平台的文件类型和分发类型
|
||||
const selectedPlatform = appDetail?.platforms.find(p => p.id === verForm.platform_id)
|
||||
|
||||
if (loading) return <div style={{ padding: 24 }}>加载中...</div>
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>应用管理</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理应用、平台与版本发布</p>
|
||||
</div>
|
||||
<button onClick={() => { setAppDrawer(true); setAppForm({ name: '', package_name: '', description: '', platform_type: 2, major: 1, minor: 0, patch: 0, file_type: '', file_size: '', distribution_type: '', os_min_version: '', is_force_update: false, changelog: '' }) }} 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>
|
||||
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
{/* 左侧应用列表 */}
|
||||
<div style={{ width: 300, flexShrink: 0 }}>
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ padding: '14px 16px', borderBottom: '1px solid #F0F0F0', fontSize: 14, fontWeight: 600 }}>应用列表({apps.length})</div>
|
||||
{apps.length === 0 ? (
|
||||
<div style={{ padding: 32, textAlign: 'center', color: 'rgba(0,0,0,0.25)', fontSize: 13 }}>暂无应用</div>
|
||||
) : apps.map(app => (
|
||||
<div key={app.id} onClick={() => loadDetail(app.id)} style={{ padding: '14px 16px', borderBottom: '1px solid #F0F0F0', cursor: 'pointer', backgroundColor: selectedAppId === app.id ? '#eef5f0' : '#fff', borderLeft: selectedAppId === app.id ? '3px solid #4a7c59' : '3px solid transparent' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500 }}>{app.name}</span>
|
||||
<span style={{ fontSize: 11, color: app.status === 1 ? '#52C41A' : 'rgba(0,0,0,0.45)' }}>{app.status === 1 ? '启用' : '禁用'}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 4 }}>{app.package_name || '-'}</div>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{app.platforms.map(p => <span key={p.id} style={{ padding: '1px 6px', borderRadius: 3, fontSize: 10, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{p.platform_name}</span>)}
|
||||
<span style={{ fontSize: 10, color: 'rgba(0,0,0,0.35)' }}>{app.versionCount}个版本</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧详情 */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{!appDetail ? (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 48, textAlign: 'center', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<Layers size={40} style={{ color: 'rgba(0,0,0,0.15)', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>选择左侧应用查看详情</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 应用信息 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600, marginBottom: 4 }}>{appDetail.name}</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{appDetail.package_name || '未设置包名'}</div>
|
||||
{appDetail.description && <div style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', marginTop: 4 }}>{appDetail.description}</div>}
|
||||
</div>
|
||||
<button onClick={() => handleDeleteApp(appDetail.id)} style={{ color: '#FF4D4F', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}><Trash2 size={14} />删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 平台管理 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: 0 }}>支持平台</h3>
|
||||
<button onClick={() => { setPlatformDrawer(true); setPlatForm({ platform_type: 2, description: '' }) }} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px', border: '1px solid #4a7c59', borderRadius: 6, backgroundColor: '#fff', color: '#4a7c59', cursor: 'pointer', fontSize: 13 }}><Plus size={14} />添加平台</button>
|
||||
</div>
|
||||
{appDetail.platforms.length === 0 ? (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'rgba(0,0,0,0.25)', fontSize: 13 }}>暂无平台</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
{appDetail.platforms.map(p => {
|
||||
const count = appDetail.versions.filter(v => v.platform_id === p.id).length
|
||||
return (
|
||||
<div key={p.id} style={{ padding: '12px 16px', borderRadius: 8, border: '1px solid #F0F0F0', backgroundColor: '#FAFAFA', display: 'flex', alignItems: 'center', gap: 10, minWidth: 150 }}>
|
||||
<Smartphone size={18} style={{ color: '#4a7c59' }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{p.platform_name}</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(0,0,0,0.45)' }}>{count}个版本</div>
|
||||
</div>
|
||||
<button onClick={(e) => { e.stopPropagation(); handleDeletePlatform(p.id) }} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'rgba(0,0,0,0.25)', padding: 2 }}><Trash2 size={12} /></button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 版本列表 */}
|
||||
<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: '14px 20px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: 0 }}>版本列表({appDetail.versions.length})</h3>
|
||||
<button onClick={() => { setVersionDrawer(true); setVerForm({ platform_id: appDetail.platforms[0]?.id || 0, major: 0, minor: 0, patch: 0, description: '', file_type: '', file_size: '', distribution_type: '', os_min_version: '', is_force_update: false, changelog: '' }) }} disabled={appDetail.platforms.length === 0} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 14px', border: 'none', borderRadius: 6, backgroundColor: appDetail.platforms.length > 0 ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: appDetail.platforms.length > 0 ? 'pointer' : 'not-allowed', fontSize: 13 }}><Upload size={14} />上传新版本</button>
|
||||
</div>
|
||||
{appDetail.versions.length === 0 ? (
|
||||
<div style={{ padding: 32, textAlign: 'center', color: 'rgba(0,0,0,0.25)', fontSize: 13 }}>暂无版本</div>
|
||||
) : appDetail.versions.map(ver => {
|
||||
const isExp = expandedVerId === ver.id
|
||||
return (
|
||||
<div key={ver.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div style={{ padding: '14px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 600 }}>{ver.version_name}</span>
|
||||
<span style={{ ...vStatusStyle(ver.status), padding: '2px 8px', borderRadius: 4, fontSize: 11 }}>{vStatusText(ver.status)}</span>
|
||||
{ver.is_force_update ? <span style={{ padding: '2px 6px', borderRadius: 4, fontSize: 10, backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }}>强制更新</span> : null}
|
||||
<span style={{ padding: '2px 6px', borderRadius: 4, fontSize: 10, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{ver.platform_name}</span>
|
||||
{ver.distribution_type && <span style={{ padding: '2px 6px', borderRadius: 4, fontSize: 10, backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }}>{ver.distribution_type}</span>}
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>{ver.release_date}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
||||
{ver.status === 1 && <button onClick={() => handleVerStatus(ver.id, 0)} style={{ padding: '4px 10px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', fontSize: 12 }}>禁用</button>}
|
||||
{ver.status === 0 && <button onClick={() => handleVerStatus(ver.id, 1)} style={{ padding: '4px 10px', border: 'none', borderRadius: 4, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 12 }}>启用</button>}
|
||||
<button onClick={() => setExpandedVerId(isExp ? null : ver.id)} style={{ padding: '4px 10px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', fontSize: 12, color: '#4a7c59' }}>{isExp ? '收起' : '详情'}</button>
|
||||
</div>
|
||||
</div>
|
||||
{isExp && (
|
||||
<div style={{ padding: '0 20px 16px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div style={{ padding: 14, backgroundColor: '#FAFAFA', borderRadius: 6, border: '1px solid #F0F0F0', fontSize: 13, lineHeight: 2 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>版本号:</span>{ver.major_version}.{ver.minor_version}.{ver.patch_version}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>显示名称:</span>{ver.version_name}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>平台:</span>{ver.platform_name}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>最低系统版本:</span>{ver.os_min_version || '-'}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>最低支持版本:</span>{ver.min_support_version || '-'}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>强制更新:</span>{ver.is_force_update ? '是' : '否'}</div>
|
||||
</div>
|
||||
<div style={{ padding: 14, backgroundColor: '#FAFAFA', borderRadius: 6, border: '1px solid #F0F0F0', fontSize: 13, lineHeight: 2 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>文件类型:</span>{ver.file_type || '-'}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>文件大小:</span>{ver.file_size ? `${(ver.file_size / 1024 / 1024).toFixed(1)}MB` : '-'}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>分发类型:</span>{ver.distribution_type || '-'}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>下载链接:</span>{ver.primary_url || ver.file_url || '-'}</div>
|
||||
{(ver.file_url || ver.primary_url) && <button style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 4, padding: '3px 10px', border: '1px solid #4a7c59', borderRadius: 4, backgroundColor: '#fff', color: '#4a7c59', cursor: 'pointer', fontSize: 12 }}><Download size={12} />下载</button>}
|
||||
</div>
|
||||
</div>
|
||||
{ver.changelog.length > 0 && (
|
||||
<div style={{ marginTop: 12, padding: 14, backgroundColor: '#FAFAFA', borderRadius: 6, border: '1px solid #F0F0F0' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>更新日志</div>
|
||||
<ul style={{ margin: 0, paddingLeft: 18, fontSize: 13, color: 'rgba(0,0,0,0.65)', lineHeight: 1.8 }}>
|
||||
{ver.changelog.map((log, i) => <li key={i}>{log}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{ver.description && <div style={{ marginTop: 12, padding: 14, backgroundColor: '#FAFAFA', borderRadius: 6, border: '1px solid #F0F0F0', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{ver.description}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 新建应用抽屉 */}
|
||||
{appDrawer && (() => {
|
||||
const ft = FILE_TYPES[appForm.platform_type] || []
|
||||
const dt = DIST_TYPES[appForm.platform_type] || []
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setAppDrawer(false)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 580, 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={() => setAppDrawer(false)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
{/* 应用基本信息 */}
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 16, paddingBottom: 8, borderBottom: '1px solid #F0F0F0', marginTop: 0 }}>应用信息</h4>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 应用名称</label>
|
||||
<input value={appForm.name} onChange={e => setAppForm({ ...appForm, name: e.target.value })} placeholder="如 GeoApp" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>应用包名</label>
|
||||
<input value={appForm.package_name} onChange={e => setAppForm({ ...appForm, package_name: e.target.value })} placeholder="如 com.geomative.app" 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: 13, fontWeight: 500, marginBottom: 6 }}>应用描述</label>
|
||||
<textarea value={appForm.description} onChange={e => setAppForm({ ...appForm, description: e.target.value })} rows={2} placeholder="应用描述" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
</div>
|
||||
|
||||
{/* 平台选择 */}
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 16, paddingBottom: 8, borderBottom: '1px solid #F0F0F0', marginTop: 0 }}>平台与版本</h4>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 目标平台</label>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{PLATFORM_OPTIONS.map(p => (
|
||||
<button key={p.value} onClick={() => setAppForm({ ...appForm, platform_type: p.value, file_type: '', distribution_type: '' })} style={{ padding: '6px 14px', borderRadius: 6, fontSize: 13, cursor: 'pointer', border: appForm.platform_type === p.value ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: appForm.platform_type === p.value ? '#eef5f0' : '#fff', color: appForm.platform_type === p.value ? '#4a7c59' : 'rgba(0,0,0,0.65)', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<p.icon size={13} />{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本号 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 版本号</label>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input type="number" min={0} value={appForm.major} onChange={e => setAppForm({ ...appForm, major: parseInt(e.target.value) || 0 })} style={{ width: 70, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, textAlign: 'center', boxSizing: 'border-box' }} />
|
||||
<span style={{ fontSize: 18, color: 'rgba(0,0,0,0.45)' }}>.</span>
|
||||
<input type="number" min={0} value={appForm.minor} onChange={e => setAppForm({ ...appForm, minor: parseInt(e.target.value) || 0 })} style={{ width: 70, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, textAlign: 'center', boxSizing: 'border-box' }} />
|
||||
<span style={{ fontSize: 18, color: 'rgba(0,0,0,0.45)' }}>.</span>
|
||||
<input type="number" min={0} value={appForm.patch} onChange={e => setAppForm({ ...appForm, patch: parseInt(e.target.value) || 0 })} style={{ width: 70, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, textAlign: 'center', boxSizing: 'border-box' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginLeft: 8 }}>→ {appForm.major}.{appForm.minor}.{appForm.patch}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 安装包上传 */}
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 16, paddingBottom: 8, borderBottom: '1px solid #F0F0F0', marginTop: 0 }}>安装包</h4>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ border: '2px dashed #D9D9D9', borderRadius: 8, padding: 24, textAlign: 'center', cursor: 'pointer', backgroundColor: '#FAFAFA' }}>
|
||||
<Upload size={28} style={{ color: 'rgba(0,0,0,0.25)', marginBottom: 6 }} />
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>点击或拖拽上传安装包</div>
|
||||
{ft.length > 0 && <div style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)', marginTop: 4 }}>支持格式:{ft.join(' / ')}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
||||
{ft.length > 0 && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>文件类型</label>
|
||||
<select value={appForm.file_type} onChange={e => setAppForm({ ...appForm, file_type: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">请选择</option>
|
||||
{ft.map(f => <option key={f} value={f}>{f}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{dt.length > 0 && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>分发类型</label>
|
||||
<select value={appForm.distribution_type} onChange={e => setAppForm({ ...appForm, distribution_type: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">请选择</option>
|
||||
{dt.map(d => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>文件大小(字节)</label>
|
||||
<input value={appForm.file_size} onChange={e => setAppForm({ ...appForm, file_size: e.target.value })} placeholder="如 47185920" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>最低系统版本</label>
|
||||
<input value={appForm.os_min_version} onChange={e => setAppForm({ ...appForm, os_min_version: e.target.value })} placeholder="如 Android 10" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={appForm.is_force_update} onChange={e => setAppForm({ ...appForm, is_force_update: e.target.checked })} />强制更新
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>更新日志(每行一条)</label>
|
||||
<textarea value={appForm.changelog} onChange={e => setAppForm({ ...appForm, changelog: e.target.value })} rows={3} placeholder="新增XX功能 修复XX问题" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setAppDrawer(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={handleCreateApp} disabled={!appForm.name} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: appForm.name ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: appForm.name ? 'pointer' : 'not-allowed', fontSize: 14 }}>创建应用</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* 添加平台抽屉 */}
|
||||
{platformDrawer && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setPlatformDrawer(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={() => setPlatformDrawer(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>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{PLATFORM_OPTIONS.map(p => (
|
||||
<button key={p.value} onClick={() => setPlatForm({ ...platForm, platform_type: p.value })} style={{ padding: '8px 16px', borderRadius: 6, fontSize: 13, cursor: 'pointer', border: platForm.platform_type === p.value ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: platForm.platform_type === p.value ? '#eef5f0' : '#fff', color: platForm.platform_type === p.value ? '#4a7c59' : 'rgba(0,0,0,0.65)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<p.icon size={14} />{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>描述</label>
|
||||
<input value={platForm.description} onChange={e => setPlatForm({ ...platForm, description: e.target.value })} placeholder="平台描述" 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={() => setPlatformDrawer(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={handleAddPlatform} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>添加</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传版本抽屉 */}
|
||||
{versionDrawer && appDetail && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setVersionDrawer(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 }}>上传新版本</h3>
|
||||
<button onClick={() => setVersionDrawer(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={verForm.platform_id} onChange={e => setVerForm({ ...verForm, platform_id: Number(e.target.value), file_type: '', distribution_type: '' })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{appDetail.platforms.map(p => <option key={p.id} value={p.id}>{p.platform_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 版本号</label>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input type="number" min={0} value={verForm.major} onChange={e => setVerForm({ ...verForm, major: parseInt(e.target.value) || 0 })} placeholder="主版本" style={{ width: 80, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, textAlign: 'center', boxSizing: 'border-box' }} />
|
||||
<span style={{ fontSize: 18, color: 'rgba(0,0,0,0.45)' }}>.</span>
|
||||
<input type="number" min={0} value={verForm.minor} onChange={e => setVerForm({ ...verForm, minor: parseInt(e.target.value) || 0 })} placeholder="次版本" style={{ width: 80, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, textAlign: 'center', boxSizing: 'border-box' }} />
|
||||
<span style={{ fontSize: 18, color: 'rgba(0,0,0,0.45)' }}>.</span>
|
||||
<input type="number" min={0} value={verForm.patch} onChange={e => setVerForm({ ...verForm, patch: parseInt(e.target.value) || 0 })} placeholder="修订" style={{ width: 80, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, textAlign: 'center', boxSizing: 'border-box' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginLeft: 8 }}>→ {verForm.major}.{verForm.minor}.{verForm.patch}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>安装包</label>
|
||||
<div style={{ border: '2px dashed #D9D9D9', borderRadius: 8, padding: 24, textAlign: 'center', cursor: 'pointer', backgroundColor: '#FAFAFA' }}>
|
||||
<Upload size={28} style={{ color: 'rgba(0,0,0,0.25)', marginBottom: 6 }} />
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>点击或拖拽上传安装包</div>
|
||||
{selectedPlatform && <div style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)', marginTop: 4 }}>支持格式:{selectedPlatform.file_types.join(' / ') || '任意'}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
|
||||
{selectedPlatform && selectedPlatform.file_types.length > 0 && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>文件类型</label>
|
||||
<select value={verForm.file_type} onChange={e => setVerForm({ ...verForm, file_type: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">请选择</option>
|
||||
{selectedPlatform.file_types.map(ft => <option key={ft} value={ft}>{ft}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{selectedPlatform && selectedPlatform.dist_types.length > 0 && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>分发类型</label>
|
||||
<select value={verForm.distribution_type} onChange={e => setVerForm({ ...verForm, distribution_type: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">请选择</option>
|
||||
{selectedPlatform.dist_types.map(dt => <option key={dt.value} value={dt.value}>{dt.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>文件大小(字节)</label>
|
||||
<input value={verForm.file_size} onChange={e => setVerForm({ ...verForm, file_size: e.target.value })} placeholder="如 47185920" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>最低系统版本</label>
|
||||
<input value={verForm.os_min_version} onChange={e => setVerForm({ ...verForm, os_min_version: e.target.value })} placeholder="如 Android 10" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={verForm.is_force_update} onChange={e => setVerForm({ ...verForm, is_force_update: e.target.checked })} />强制更新
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>版本描述</label>
|
||||
<textarea value={verForm.description} onChange={e => setVerForm({ ...verForm, description: e.target.value })} rows={2} placeholder="版本描述" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>更新日志(每行一条)</label>
|
||||
<textarea value={verForm.changelog} onChange={e => setVerForm({ ...verForm, changelog: e.target.value })} rows={4} placeholder="新增XX功能 修复XX问题" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setVersionDrawer(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={handleAddVersion} disabled={!verForm.platform_id} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: verForm.platform_id ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: verForm.platform_id ? 'pointer' : 'not-allowed', fontSize: 14 }}>上传版本</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Monitor, Settings2, Gauge, Wrench, Recycle, Layers } from 'lucide-react'
|
||||
import { Monitor, Settings2, Gauge, Wrench, Recycle, Layers, FolderTree, Smartphone } from 'lucide-react'
|
||||
|
||||
const menuGroups = [
|
||||
{ title: '设备', items: [
|
||||
|
|
@ -11,7 +11,11 @@ const menuGroups = [
|
|||
{ title: '物料', items: [
|
||||
{ path: '/materials', label: '物料列表', icon: Gauge },
|
||||
{ path: '/materials/manage', label: '物料管理', icon: Layers },
|
||||
{ path: '/materials/categories', label: '分类管理', icon: FolderTree },
|
||||
] },
|
||||
{ title: '软件', items: [
|
||||
{ path: '/app-versions', label: 'APP版本管理', icon: Smartphone },
|
||||
]},
|
||||
{ title: '维修', items: [
|
||||
{ path: '/repair', label: '维修工单', icon: Wrench },
|
||||
{ path: '/scrap', label: '报废回收', icon: Recycle },
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@
|
|||
import { useState, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Plus, Search, Info, ChevronLeft, ChevronRight, X, Download, Trash2, Eye, Edit, ArrowLeft } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
const configData = [
|
||||
{ id: 1, name: 'CFG-GD30-v1.2.0', model: 'GD-30 Supreme', version: 'v1.2.0', createTime: '2025-01-15 10:30', status: '生效', voltage: '1500V', current: '10A', dutyCycle: '0+0-、+0-0、+-', pulseWidth: '0.25s~64s', channels: 12, sampleRate: '50Hz/60Hz/100Hz/1000Hz', voltageRange: '±2.5V/±80V', waveform: '支持', ssid: 'GD30' },
|
||||
{ id: 2, name: 'CFG-GD30-v1.1.0', model: 'GD-30 Supreme', version: 'v1.1.0', createTime: '2024-09-20 14:00', status: '已停用', voltage: '1200V', current: '10A', dutyCycle: '0+0-、+0-0', pulseWidth: '0.25s~32s', channels: 12, sampleRate: '50Hz/60Hz/100Hz', voltageRange: '±2.5V/±80V', waveform: '支持', ssid: 'GD30' },
|
||||
{ id: 3, name: 'CFG-GD20-v1.0.0', model: 'GD-20 Supreme', version: 'v1.0.0', createTime: '2024-08-10 09:15', status: '生效', voltage: '1000V', current: '8A', dutyCycle: '0+0-、+0-0', pulseWidth: '0.25s~8s', channels: 6, sampleRate: '50Hz/60Hz', voltageRange: '±2.5V/±80V', waveform: '不支持', ssid: 'GD20' },
|
||||
{ id: 4, name: 'CFG-GD10-v1.0.0', model: 'GD-10 Supreme', version: 'v1.0.0', createTime: '2024-06-05 16:45', status: '生效', voltage: '800V', current: '5A', dutyCycle: '0+0-', pulseWidth: '0.5s~8s', channels: 1, sampleRate: '50Hz/60Hz', voltageRange: '±2.5V', waveform: '不支持', ssid: 'GD10' },
|
||||
{ id: 5, name: 'CFG-GD20-v1.1.0', model: 'GD-20 Supreme', version: 'v1.1.0', createTime: '2025-02-28 11:20', status: '生效', voltage: '1000V', current: '8A', dutyCycle: '0+0-、+0-0、+-', pulseWidth: '0.25s~16s', channels: 6, sampleRate: '50Hz/60Hz/100Hz', voltageRange: '±2.5V/±80V', waveform: '支持', ssid: 'GD20' },
|
||||
{ id: 6, name: 'CFG-GD30-v1.3.0', model: 'GD-30 Supreme', version: 'v1.3.0', createTime: '2025-03-15 08:00', status: '生效', voltage: '1500V', current: '10A', dutyCycle: '0+0-、+0-0、+-', pulseWidth: '0.25s~64s', channels: 12, sampleRate: '50Hz/60Hz/100Hz/1000Hz', voltageRange: '±2.5V/±80V', waveform: '支持', ssid: 'GD30' },
|
||||
]
|
||||
interface ConfigItem {
|
||||
id: number; name: string; model: string; version: string; create_time: string; status: string
|
||||
voltage: string; current: string; duty_cycle: string; pulse_width: string
|
||||
channels: number; sample_rate: string; voltage_range: string; waveform: string; ssid: string
|
||||
}
|
||||
|
||||
const modelOptions = ['全部', 'GD-30 Supreme', 'GD-20 Supreme', 'GD-10 Supreme']
|
||||
const versionOptions = ['全部', 'v1.0.0', 'v1.1.0', 'v1.2.0', 'v1.3.0']
|
||||
|
|
@ -35,8 +33,8 @@ function ConfigFilesContent() {
|
|||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const modelParam = searchParams.get('model') || ''
|
||||
const { data: configData, loading, refetch } = useApi<ConfigItem[]>('/api/config-files', [])
|
||||
|
||||
// Map model name to filter value
|
||||
const modelNameMap: Record<string, string> = {
|
||||
'GD-30 Supreme': 'GD-30 Supreme',
|
||||
'GD-20 Supreme': 'GD-20 Supreme',
|
||||
|
|
@ -44,13 +42,14 @@ function ConfigFilesContent() {
|
|||
}
|
||||
const initialModelFilter = modelParam ? (modelNameMap[modelParam] || modelParam) : '全部'
|
||||
const isFromModels = !!modelParam
|
||||
const versionOptions = ['全部', ...Array.from(new Set(configData.map(c => c.version)))]
|
||||
|
||||
const [filterModel, setFilterModel] = useState(initialModelFilter)
|
||||
const [filterVersion, setFilterVersion] = useState('全部')
|
||||
const [filterKeyword, setFilterKeyword] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [detailDrawer, setDetailDrawer] = useState<typeof configData[0] | null>(null)
|
||||
const [detailDrawer, setDetailDrawer] = useState<ConfigItem | null>(null)
|
||||
const [form, setForm] = useState({
|
||||
model: 'GD-30 Supreme', version: '', voltage: '1500V', current: '10A',
|
||||
dutyCycle: ['0+0-'] as string[], pulseWidth: '0.25s/0.5s/1s/2s/4s/8s/16s/32s/64s',
|
||||
|
|
@ -59,6 +58,8 @@ function ConfigFilesContent() {
|
|||
})
|
||||
const pageSize = 5
|
||||
|
||||
if (loading) return <div style={{ padding: 24 }}>加载中...</div>
|
||||
|
||||
const filtered = configData.filter(c => {
|
||||
if (filterModel !== '全部' && c.model !== filterModel) return false
|
||||
if (filterVersion !== '全部' && c.version !== filterVersion) return false
|
||||
|
|
@ -137,7 +138,7 @@ function ConfigFilesContent() {
|
|||
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500, color: '#4a7c59' }}>{row.name}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.model}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14 }}>{row.version}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.createTime}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.create_time}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span>
|
||||
</td>
|
||||
|
|
@ -183,7 +184,7 @@ function ConfigFilesContent() {
|
|||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>适配型号:</span>{detailDrawer.model}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>版本:</span>{detailDrawer.version}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>状态:</span><span style={{ ...getStatusStyle(detailDrawer.status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.status}</span></div>
|
||||
<div style={{ gridColumn: '1 / -1' }}><span style={{ color: 'rgba(0,0,0,0.45)' }}>创建时间:</span>{detailDrawer.createTime}</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}><span style={{ color: 'rgba(0,0,0,0.45)' }}>创建时间:</span>{detailDrawer.create_time}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 发射参数 */}
|
||||
|
|
@ -192,8 +193,8 @@ function ConfigFilesContent() {
|
|||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>最大发射电压:</span>{detailDrawer.voltage}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>最大发射电流:</span>{detailDrawer.current}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>发射波形:</span>{detailDrawer.dutyCycle}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>脉宽范围:</span>{detailDrawer.pulseWidth}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>发射波形:</span>{detailDrawer.duty_cycle}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>脉宽范围:</span>{detailDrawer.pulse_width}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 采集参数 */}
|
||||
|
|
@ -201,8 +202,8 @@ function ConfigFilesContent() {
|
|||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12 }}>采集参数</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>支持通道数:</span>{detailDrawer.channels}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>采样率:</span>{detailDrawer.sampleRate}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>电压测量量程:</span>{detailDrawer.voltageRange}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>采样率:</span>{detailDrawer.sample_rate}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>电压测量量程:</span>{detailDrawer.voltage_range}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>全波形采集:</span>{detailDrawer.waveform}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -322,7 +323,12 @@ function ConfigFilesContent() {
|
|||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setDrawerOpen(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={() => setDrawerOpen(false)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
<button onClick={async () => {
|
||||
if (!form.version) return
|
||||
const name = `CFG-${form.model.split(' ')[0].replace('GD-', 'GD')}-${form.version}`
|
||||
await fetch('/api/config-files', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, model: form.model, version: form.version, create_time: new Date().toISOString().slice(0, 16).replace('T', ' '), status: '生效', voltage: form.voltage, current: form.current, duty_cycle: form.dutyCycle.join('、'), pulse_width: form.pulseWidth, channels: parseInt(form.channels), sample_rate: form.sampleRate, voltage_range: form.voltageRange, waveform: form.waveform, ssid: form.ssid }) })
|
||||
refetch(); setDrawerOpen(false)
|
||||
}} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,15 @@
|
|||
'use client'
|
||||
import { useState, Suspense } from 'react'
|
||||
import { useState, Suspense, useEffect, useCallback } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { ArrowLeft, Upload, Download, ChevronDown, ChevronUp, X, Package, Shield, FileText } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
const firmwareTypes = ['全部', '主协板', '采集板', '发射板', '主机固件', '计算单元固件']
|
||||
|
||||
const firmwareData = [
|
||||
{ id: 1, version: 'v2.1.0', boardVersion: 'MB-V1.8', type: '主协板', date: '2024-03-10', status: '已发布', size: '12.5MB', downloads: 1234, hwRange: 'MB25130025 Rev.A~C', upgradeType: '可选', signed: true, md5: 'a1b2c3d4e5f6...', sha256: '9f8e7d6c5b4a...', notes: ['修复通信协议兼容性问题', '优化低功耗模式切换', '新增看门狗超时配置'] },
|
||||
{ id: 2, version: 'v2.0.0', boardVersion: 'MB-V1.8', type: '主协板', date: '2024-01-15', status: '已发布', size: '11.8MB', downloads: 2456, hwRange: 'MB25130025 Rev.A~C', upgradeType: '强制', signed: true, md5: 'b2c3d4e5f6a1...', sha256: '8e7d6c5b4a9f...', notes: ['新增多通道采集支持', '重构通信协议栈'] },
|
||||
{ id: 3, version: 'v1.8.5', boardVersion: 'MB-V1.8', type: '主协板', date: '2023-11-01', status: '已发布', size: '9.2MB', downloads: 3120, hwRange: 'MCB-2000 Rev.B~D', upgradeType: '可选', signed: true, md5: 'c3d4e5f6a1b2...', sha256: '7d6c5b4a9f8e...', notes: ['优化功耗管理', '修复偶发重启问题'] },
|
||||
{ id: 4, version: 'v3.0.2', boardVersion: 'RX-V2.3', type: '采集板', date: '2024-02-20', status: '已发布', size: '8.7MB', downloads: 890, hwRange: 'ACB-6000 Rev.A~B', upgradeType: '可选', signed: true, md5: 'd4e5f6a1b2c3...', sha256: '6c5b4a9f8e7d...', notes: ['提升采样精度', '修复通道串扰问题', '新增自校准功能'] },
|
||||
{ id: 5, version: 'v2.5.1', boardVersion: 'RX-V2.3', type: '采集板', date: '2023-09-15', status: '已发布', size: '7.1MB', downloads: 1567, hwRange: 'ACB-5000 Rev.A~C', upgradeType: '可选', signed: true, md5: 'e5f6a1b2c3d4...', sha256: '5b4a9f8e7d6c...', notes: ['优化ADC驱动', '修复温漂补偿算法'] },
|
||||
{ id: 6, version: 'v1.2.0', boardVersion: 'TX-V2.1', type: '发射板', date: '2024-02-28', status: '已发布', size: '6.3MB', downloads: 456, hwRange: 'TXB-1000 Rev.A', upgradeType: '强制', signed: true, md5: 'f6a1b2c3d4e5...', sha256: '4a9f8e7d6c5b...', notes: ['新增过流保护', '优化PWM控制算法'] },
|
||||
{ id: 7, version: 'v1.0.3', boardVersion: 'TX-V2.1', type: '发射板', date: '2023-06-20', status: '已发布', size: '5.8MB', downloads: 789, hwRange: 'TXB-800 Rev.A~B', upgradeType: '可选', signed: false, md5: 'a1c3e5b2d4f6...', sha256: '3f8e6d4b2a9c...', notes: ['修复高压输出不稳定'] },
|
||||
]
|
||||
|
||||
/** 设备型号固件数据:按型号 + 固件类别 */
|
||||
const deviceFirmwareData: Record<string, Record<string, typeof firmwareData>> = {
|
||||
GD30: {
|
||||
'主机固件': [
|
||||
{ id: 101, version: 'v4.2.0', boardVersion: '-', type: '主机固件', date: '2025-03-15', status: '已发布', size: '45.6MB', downloads: 320, hwRange: 'GD30 Rev.A~C', upgradeType: '可选', signed: true, md5: 'gd30h42a1b2c3...', sha256: 'gd30h42x9f8e7d...', notes: ['新增远程诊断功能', '优化数据采集流程', '修复蓝牙连接稳定性'] },
|
||||
{ id: 102, version: 'v4.1.0', boardVersion: '-', type: '主机固件', date: '2025-01-20', status: '已发布', size: '44.2MB', downloads: 580, hwRange: 'GD30 Rev.A~C', upgradeType: '强制', signed: true, md5: 'gd30h41b2c3d4...', sha256: 'gd30h41y8e7d6c...', notes: ['重构通信协议栈', '新增OTA增量升级支持'] },
|
||||
{ id: 103, version: 'v4.0.0', boardVersion: '-', type: '主机固件', date: '2024-10-01', status: '已发布', size: '42.8MB', downloads: 1200, hwRange: 'GD30 Rev.A~B', upgradeType: '强制', signed: true, md5: 'gd30h40c3d4e5...', sha256: 'gd30h40z7d6c5b...', notes: ['GD30 首个正式版本', '支持最大60通道采集'] },
|
||||
],
|
||||
'计算单元固件': [
|
||||
{ id: 201, version: 'v2.3.1', boardVersion: '-', type: '计算单元固件', date: '2025-02-28', status: '已发布', size: '18.3MB', downloads: 290, hwRange: 'GD30-CU Rev.A', upgradeType: '可选', signed: true, md5: 'gd30c23a1b2c3...', sha256: 'gd30c23x9f8e7d...', notes: ['优化实时数据处理算法', '修复大数据量下内存溢出'] },
|
||||
{ id: 202, version: 'v2.2.0', boardVersion: '-', type: '计算单元固件', date: '2024-12-10', status: '已发布', size: '17.8MB', downloads: 450, hwRange: 'GD30-CU Rev.A', upgradeType: '可选', signed: true, md5: 'gd30c22b2c3d4...', sha256: 'gd30c22y8e7d6c...', notes: ['新增Wenner排列自动识别', '优化反演计算性能'] },
|
||||
],
|
||||
},
|
||||
GD20: {
|
||||
'主机固件': [
|
||||
{ id: 301, version: 'v3.5.2', boardVersion: '-', type: '主机固件', date: '2025-02-10', status: '已发布', size: '38.1MB', downloads: 670, hwRange: 'GD20 Rev.A~D', upgradeType: '可选', signed: true, md5: 'gd20h35a1b2c3...', sha256: 'gd20h35x9f8e7d...', notes: ['修复低温环境下启动异常', '优化电池管理策略'] },
|
||||
{ id: 302, version: 'v3.5.0', boardVersion: '-', type: '主机固件', date: '2024-11-05', status: '已发布', size: '37.5MB', downloads: 890, hwRange: 'GD20 Rev.A~D', upgradeType: '强制', signed: true, md5: 'gd20h35b2c3d4...', sha256: 'gd20h35y8e7d6c...', notes: ['新增GPS定位集成', '支持WiFi数据传输'] },
|
||||
],
|
||||
'计算单元固件': [
|
||||
{ id: 401, version: 'v1.8.0', boardVersion: '-', type: '计算单元固件', date: '2025-01-15', status: '已发布', size: '14.2MB', downloads: 340, hwRange: 'GD20-CU Rev.A~B', upgradeType: '可选', signed: true, md5: 'gd20c18a1b2c3...', sha256: 'gd20c18x9f8e7d...', notes: ['优化数据预处理流水线', '新增异常数据自动剔除'] },
|
||||
],
|
||||
},
|
||||
GD10: {
|
||||
'主机固件': [
|
||||
{ id: 501, version: 'v2.1.3', boardVersion: '-', type: '主机固件', date: '2024-06-20', status: '已发布', size: '28.5MB', downloads: 1450, hwRange: 'GD10 Rev.A~C', upgradeType: '可选', signed: true, md5: 'gd10h21a1b2c3...', sha256: 'gd10h21x9f8e7d...', notes: ['最终维护版本', '修复已知稳定性问题'] },
|
||||
],
|
||||
'计算单元固件': [
|
||||
{ id: 601, version: 'v1.2.0', boardVersion: '-', type: '计算单元固件', date: '2024-04-10', status: '已发布', size: '10.8MB', downloads: 980, hwRange: 'GD10-CU Rev.A', upgradeType: '可选', signed: false, md5: 'gd10c12a1b2c3...', sha256: 'gd10c12x9f8e7d...', notes: ['最终维护版本'] },
|
||||
],
|
||||
},
|
||||
interface FirmwareItem {
|
||||
id: number; version: string; board_version: string; type: string; date: string; status: string
|
||||
size: string; downloads: number; hw_range: string; upgrade_type: string; signed: number
|
||||
md5: string; sha256: string; notes: string[]; model_code: string
|
||||
}
|
||||
interface CategoryItem { id: number; name: string; has_firmware: number; status: string }
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
|
|
@ -56,67 +20,70 @@ function getStatusStyle(status: string) {
|
|||
}
|
||||
|
||||
export default function FirmwarePage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}>
|
||||
<FirmwareContent />
|
||||
</Suspense>
|
||||
)
|
||||
return <Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}><FirmwareContent /></Suspense>
|
||||
}
|
||||
|
||||
function FirmwareContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const typeParam = searchParams.get('type') || ''
|
||||
const boardParam = searchParams.get('board') || ''
|
||||
const modelParam = searchParams.get('model') || ''
|
||||
const isFromType = !!typeParam
|
||||
const isFromBoard = !!boardParam
|
||||
|
||||
const isFromBoards = !!boardParam
|
||||
const isFromModels = !!modelParam
|
||||
const matchedFw = boardParam ? firmwareData.find(f => f.boardVersion === boardParam) : null
|
||||
const initialType = matchedFw ? matchedFw.type : '全部'
|
||||
const { data: categories } = useApi<CategoryItem[]>('/api/material-categories', [])
|
||||
const firmwareTabs = categories.filter(c => c.has_firmware && c.status === '启用').map(c => c.name)
|
||||
|
||||
const [filterType, setFilterType] = useState(initialType)
|
||||
const [filterBoard, setFilterBoard] = useState('')
|
||||
const [filterType, setFilterType] = useState(typeParam || '全部')
|
||||
const [firmwareList, setFirmwareList] = useState<FirmwareItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null)
|
||||
const [uploadOpen, setUploadOpen] = useState(false)
|
||||
const [uploadForm, setUploadForm] = useState({ version: '', hwRange: '', upgradeType: '可选', firmwareType: '主协板', signed: false, notes: '' })
|
||||
const [deviceFwTab, setDeviceFwTab] = useState<'主机固件' | '计算单元固件'>('主机固件')
|
||||
const [uploadForm, setUploadForm] = useState({ version: '', hwRange: '', upgradeType: '可选', firmwareType: typeParam || '', signed: false, size: '', notes: '' })
|
||||
|
||||
// Device model firmware mode
|
||||
const modelFirmware = modelParam ? deviceFirmwareData[modelParam] : null
|
||||
const deviceFiltered = modelFirmware ? (modelFirmware[deviceFwTab] || []) : []
|
||||
const fetchFirmware = useCallback(async () => {
|
||||
setLoading(true)
|
||||
let url = '/api/firmware'
|
||||
if (isFromBoard) url += `?board=${boardParam}`
|
||||
else if (isFromType) url += `?type=${typeParam}`
|
||||
else if (filterType !== '全部') url += `?type=${filterType}`
|
||||
const res = await fetch(url)
|
||||
setFirmwareList(await res.json())
|
||||
setLoading(false)
|
||||
}, [filterType, typeParam, boardParam, isFromType, isFromBoard])
|
||||
|
||||
// Board firmware mode
|
||||
const filtered = isFromModels ? deviceFiltered : firmwareData.filter(f => {
|
||||
if (isFromBoards) return f.boardVersion === boardParam
|
||||
if (filterType !== '全部' && f.type !== filterType) return false
|
||||
return true
|
||||
})
|
||||
const handleTypeChange = (type: string) => {
|
||||
setFilterType(type)
|
||||
setFilterBoard('')
|
||||
useEffect(() => { fetchFirmware() }, [fetchFirmware])
|
||||
|
||||
const handleUpload = async () => {
|
||||
const fwType = isFromType ? typeParam : (uploadForm.firmwareType || '')
|
||||
if (!fwType || fwType === '全部') return
|
||||
await fetch('/api/firmware', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
version: uploadForm.version, type: fwType,
|
||||
hw_range: uploadForm.hwRange, upgrade_type: uploadForm.upgradeType,
|
||||
signed: uploadForm.signed, size: uploadForm.size, status: '草稿',
|
||||
notes: uploadForm.notes.split('\n').filter(Boolean),
|
||||
})
|
||||
})
|
||||
fetchFirmware(); setUploadOpen(false)
|
||||
setUploadForm({ version: '', hwRange: '', upgradeType: '可选', firmwareType: typeParam || '', signed: false, size: '', notes: '' })
|
||||
}
|
||||
|
||||
const pageTitle = isFromModels
|
||||
? `固件管理 — ${modelParam}`
|
||||
: isFromBoards
|
||||
? `固件库 — ${boardParam}`
|
||||
: '固件库'
|
||||
const pageTitle = isFromType ? `固件库 — ${typeParam}` : isFromBoard ? `固件库 — ${boardParam}` : '固件库'
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{(isFromBoards || isFromModels) && (
|
||||
{(isFromType || isFromBoard) && (
|
||||
<button onClick={() => router.back()} 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 }}>{pageTitle}</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>
|
||||
{isFromModels ? '管理设备型号的主机固件与计算单元固件' : '管理固件版本,支持上传与下载'}
|
||||
</p>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理固件版本,支持上传与下载</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setUploadOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
|
|
@ -124,126 +91,89 @@ function FirmwareContent() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Device Model Firmware Tabs */}
|
||||
{isFromModels && (
|
||||
{/* Tabs — 仅通用模式显示 */}
|
||||
{!isFromType && !isFromBoard && firmwareTabs.length > 0 && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #F0F0F0' }}>
|
||||
{(['主机固件', '计算单元固件'] as const).map(tab => (
|
||||
<button key={tab} onClick={() => { setDeviceFwTab(tab); setExpandedId(null) }} style={{
|
||||
{['全部', ...firmwareTabs].map(tab => (
|
||||
<button key={tab} onClick={() => { setFilterType(tab); setExpandedId(null) }} style={{
|
||||
padding: '10px 20px', fontSize: 14, cursor: 'pointer', border: 'none', backgroundColor: 'transparent',
|
||||
borderBottom: deviceFwTab === tab ? '2px solid #4a7c59' : '2px solid transparent',
|
||||
color: deviceFwTab === tab ? '#4a7c59' : 'rgba(0,0,0,0.65)', fontWeight: deviceFwTab === tab ? 600 : 400,
|
||||
}}>{tab}
|
||||
<span style={{ marginLeft: 6, padding: '1px 6px', borderRadius: 10, fontSize: 11, backgroundColor: deviceFwTab === tab ? '#eef5f0' : '#F5F5F5', color: deviceFwTab === tab ? '#4a7c59' : 'rgba(0,0,0,0.45)' }}>
|
||||
{(modelFirmware?.[tab] || []).length}
|
||||
</span>
|
||||
</button>
|
||||
borderBottom: filterType === tab ? '2px solid #4a7c59' : '2px solid transparent',
|
||||
color: filterType === tab ? '#4a7c59' : 'rgba(0,0,0,0.65)', fontWeight: filterType === tab ? 600 : 400,
|
||||
}}>{tab}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Type Tabs + Board Filter */}
|
||||
{!isFromBoards && !isFromModels && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #F0F0F0' }}>
|
||||
{firmwareTypes.map(type => (
|
||||
<button key={type} onClick={() => handleTypeChange(type)} style={{
|
||||
padding: '10px 20px', fontSize: 14, cursor: 'pointer', border: 'none', backgroundColor: 'transparent',
|
||||
borderBottom: filterType === type ? '2px solid #4a7c59' : '2px solid transparent',
|
||||
color: filterType === type ? '#4a7c59' : 'rgba(0,0,0,0.65)', fontWeight: filterType === type ? 600 : 400,
|
||||
}}>{type}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Firmware Cards */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{filtered.map(fw => {
|
||||
const isExpanded = expandedId === fw.id
|
||||
return (
|
||||
<div key={fw.id} style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
|
||||
<div style={{ padding: 20 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 18, fontWeight: 600 }}>{fw.version}</span>
|
||||
<span style={{ ...getStatusStyle(fw.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{fw.status}</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{fw.type}</span>
|
||||
{loading ? <div style={{ padding: 24 }}>加载中...</div> : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{firmwareList.map(fw => {
|
||||
const isExpanded = expandedId === fw.id
|
||||
return (
|
||||
<div key={fw.id} style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
|
||||
<div style={{ padding: 20 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 18, fontWeight: 600 }}>{fw.version}</span>
|
||||
<span style={{ ...getStatusStyle(fw.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{fw.status}</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{fw.type}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 24, fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
<span>发布日期:{fw.date}</span>
|
||||
{fw.size && <span>文件大小:{fw.size}</span>}
|
||||
<span>下载次数:{fw.downloads.toLocaleString()}次</span>
|
||||
<span>升级类型:<span style={{ color: fw.upgrade_type === '强制' ? '#FF4D4F' : '#4a7c59' }}>{fw.upgrade_type}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 24, fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
<span>发布日期:{fw.date}</span>
|
||||
<span>文件大小:{fw.size}</span>
|
||||
<span>下载次数:{fw.downloads.toLocaleString()}次</span>
|
||||
<span>升级类型:<span style={{ color: fw.upgradeType === '强制' ? '#FF4D4F' : '#4a7c59' }}>{fw.upgradeType}</span></span>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0, marginLeft: 16 }}>
|
||||
<button style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: '#4a7c59' }}>
|
||||
<Download size={14} />下载
|
||||
</button>
|
||||
<button onClick={() => setExpandedId(isExpanded ? null : fw.id)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
{isExpanded ? '收起' : '详情'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0, marginLeft: 16 }}>
|
||||
<button style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: '#4a7c59' }}>
|
||||
<Download size={14} />下载
|
||||
</button>
|
||||
<button onClick={() => setExpandedId(isExpanded ? null : fw.id)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
{isExpanded ? '收起' : '详情'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div style={{ borderTop: '1px solid #F0F0F0', padding: 20, backgroundColor: '#FAFAFA' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Shield size={14} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<div><div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>数字签名</div><div style={{ fontSize: 13 }}>{fw.signed ? '已签名' : '未签名'}</div></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Package size={14} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<div><div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>硬件版本范围</div><div style={{ fontSize: 13 }}>{fw.hw_range || '-'}</div></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FileText size={14} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<div><div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>板卡版本</div><div style={{ fontSize: 13 }}>{fw.board_version || '-'}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{fw.md5 && <div style={{ marginBottom: 12 }}><div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 4 }}>MD5</div><div style={{ fontSize: 13, fontFamily: 'monospace', color: 'rgba(0,0,0,0.65)' }}>{fw.md5}</div></div>}
|
||||
{fw.sha256 && <div style={{ marginBottom: 12 }}><div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 4 }}>SHA256</div><div style={{ fontSize: 13, fontFamily: 'monospace', color: 'rgba(0,0,0,0.65)' }}>{fw.sha256}</div></div>}
|
||||
{fw.notes.length > 0 && (
|
||||
<div><div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>发布说明</div>
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>{fw.notes.map((n, i) => <li key={i} style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 4 }}>{n}</li>)}</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded Detail */}
|
||||
{isExpanded && (
|
||||
<div style={{ borderTop: '1px solid #F0F0F0', padding: 20, backgroundColor: '#FAFAFA' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Shield size={14} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>数字签名</div>
|
||||
<div style={{ fontSize: 13 }}>{fw.signed ? '已签名' : '未签名'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Package size={14} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>硬件版本范围</div>
|
||||
<div style={{ fontSize: 13 }}>{fw.hwRange}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FileText size={14} style={{ color: 'rgba(0,0,0,0.35)' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>升级类型</div>
|
||||
<div style={{ fontSize: 13, color: fw.upgradeType === '强制' ? '#FF4D4F' : '#4a7c59' }}>{fw.upgradeType}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 4 }}>MD5</div>
|
||||
<div style={{ fontSize: 13, fontFamily: 'monospace', color: 'rgba(0,0,0,0.65)' }}>{fw.md5}</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 4 }}>SHA256</div>
|
||||
<div style={{ fontSize: 13, fontFamily: 'monospace', color: 'rgba(0,0,0,0.65)' }}>{fw.sha256}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>发布说明</div>
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||
{fw.notes.map((note, i) => (
|
||||
<li key={i} style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 4 }}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
{firmwareList.length === 0 && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 60, textAlign: 'center', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>暂无固件版本</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{filtered.length === 0 && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 60, textAlign: 'center', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>暂无固件版本</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Dialog */}
|
||||
{uploadOpen && (
|
||||
|
|
@ -261,41 +191,54 @@ function FirmwareContent() {
|
|||
<input value={uploadForm.version} onChange={e => setUploadForm({ ...uploadForm, version: e.target.value })} placeholder="如 v2.4.0" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 硬件版本范围</label>
|
||||
<input value={uploadForm.hwRange} onChange={e => setUploadForm({ ...uploadForm, hwRange: e.target.value })} placeholder="如 MB25130025 Rev.A~C" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 固件类型</label>
|
||||
{isFromType ? (
|
||||
<div style={{ padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, backgroundColor: '#FAFAFA' }}>{typeParam}</div>
|
||||
) : (
|
||||
<select value={uploadForm.firmwareType} onChange={e => setUploadForm({ ...uploadForm, firmwareType: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="">请选择</option>
|
||||
{firmwareTabs.map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 升级类型</label>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>硬件版本范围</label>
|
||||
<input value={uploadForm.hwRange} onChange={e => setUploadForm({ ...uploadForm, hwRange: e.target.value })} placeholder="如 MB25130025 Rev.A~C" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>升级类型</label>
|
||||
<select value={uploadForm.upgradeType} onChange={e => setUploadForm({ ...uploadForm, upgradeType: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="可选">可选</option>
|
||||
<option value="强制">强制</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>文件大小</label>
|
||||
<input value={uploadForm.size} onChange={e => setUploadForm({ ...uploadForm, size: e.target.value })} placeholder="如 12.5MB" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={uploadForm.signed} onChange={e => setUploadForm({ ...uploadForm, signed: e.target.checked })} />
|
||||
数字签名
|
||||
<input type="checkbox" checked={uploadForm.signed} onChange={e => setUploadForm({ ...uploadForm, signed: e.target.checked })} />数字签名
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 固件包</label>
|
||||
<div style={{ border: '2px dashed #D9D9D9', borderRadius: 8, padding: '32px 24px', textAlign: 'center', cursor: 'pointer' }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>固件包</label>
|
||||
<div style={{ border: '2px dashed #D9D9D9', borderRadius: 8, padding: '24px', textAlign: 'center', cursor: 'pointer' }}>
|
||||
<Upload size={24} style={{ color: 'rgba(0,0,0,0.25)', marginBottom: 8 }} />
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>点击或拖拽上传 ZIP 文件</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)', marginTop: 4 }}>支持 .zip 格式</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>发布说明</label>
|
||||
<textarea value={uploadForm.notes} onChange={e => setUploadForm({ ...uploadForm, notes: e.target.value })} rows={3} placeholder="每行一条发布说明" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, resize: 'vertical', boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>发布说明(每行一条)</label>
|
||||
<textarea value={uploadForm.notes} onChange={e => setUploadForm({ ...uploadForm, notes: e.target.value })} rows={3} placeholder="修复XX问题 新增XX功能" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, resize: 'vertical', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setUploadOpen(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={() => setUploadOpen(false)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>上传</button>
|
||||
<button onClick={handleUpload} disabled={!uploadForm.version || (!isFromType && !uploadForm.firmwareType)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: (uploadForm.version && (isFromType || uploadForm.firmwareType)) ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: (uploadForm.version && (isFromType || uploadForm.firmwareType)) ? 'pointer' : 'not-allowed', fontSize: 14 }}>上传</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
import { useState, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Download, Plus, Info, ChevronLeft, ChevronRight, X, Check, ArrowLeft } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
interface LicenseItem { id: number; model: string; modules: string; expiry: string; status: string }
|
||||
interface DeviceModel { id: number; name: string; code: string; status: string }
|
||||
|
||||
const allAuthItems = [
|
||||
{ id: '1D', name: '一维自电/电阻率/激电测试模块', description: '包含一维自然电位法、电阻率测深、激发极化测深', category: '一维' },
|
||||
|
|
@ -20,12 +24,6 @@ const modelPresets: Record<string, string[]> = {
|
|||
'GD-30': ['1D', '2D', '3D', 'WATER', 'CROSS', 'CF'],
|
||||
}
|
||||
|
||||
const mockLicenses = [
|
||||
{ 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: '生效' },
|
||||
]
|
||||
|
||||
export default function LicensesPage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}>
|
||||
|
|
@ -38,17 +36,15 @@ function LicensesContent() {
|
|||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const modelParam = searchParams.get('model') || ''
|
||||
const { data: licensesData, loading, refetch } = useApi<LicenseItem[]>('/api/licenses', [])
|
||||
const { data: deviceModels } = useApi<DeviceModel[]>('/api/models', [])
|
||||
|
||||
// Map model name (e.g. "GD-30 Supreme") to license model format (e.g. "GD-30")
|
||||
const modelNameToLicense: Record<string, string> = {
|
||||
'GD-30 Supreme': 'GD-30',
|
||||
'GD-20 Supreme': 'GD-20',
|
||||
'GD-10 Supreme': 'GD-10',
|
||||
}
|
||||
const initialModelFilter = modelParam ? (modelNameToLicense[modelParam] || modelParam) : ''
|
||||
// 从型号管理跳转时,锁定到该型号
|
||||
const isFromModels = !!modelParam
|
||||
// 从型号名(如 "GD-30 Supreme")提取短名(如 "GD-30")
|
||||
const modelShortName = modelParam ? modelParam.replace(' Supreme', '') : ''
|
||||
|
||||
const [filterModel, setFilterModel] = useState(initialModelFilter)
|
||||
const [filterModel, setFilterModel] = useState(modelShortName)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [drawerModel, setDrawerModel] = useState('')
|
||||
|
|
@ -57,10 +53,14 @@ function LicensesContent() {
|
|||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
const pageSize = 5
|
||||
|
||||
const filtered = mockLicenses.filter(l => {
|
||||
if (loading) return <div style={{ padding: 24 }}>加载中...</div>
|
||||
|
||||
const filtered = licensesData.filter(l => {
|
||||
if (filterModel && l.model !== filterModel) return false
|
||||
return true
|
||||
})
|
||||
// 从型号管理进来时,查找该型号已有的授权记录
|
||||
const existingLicense = isFromModels ? licensesData.find(l => l.model === modelShortName) : null
|
||||
const totalPages = Math.ceil(filtered.length / pageSize)
|
||||
const paged = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
||||
|
||||
|
|
@ -98,7 +98,16 @@ function LicensesContent() {
|
|||
<button className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm" style={{ border: '1px solid #D9D9D9', backgroundColor: '#fff', color: 'rgba(0,0,0,0.65)' }}>
|
||||
<Download size={16} />导出
|
||||
</button>
|
||||
<button onClick={() => { setDrawerOpen(true); setDrawerModel(''); setSelectedItems([]); setDrawerExpiry('1year'); setDrawerCustomDate('') }} className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-white" style={{ backgroundColor: '#4a7c59' }}>
|
||||
<button onClick={() => {
|
||||
const preModel = isFromModels ? modelShortName : ''
|
||||
setDrawerOpen(true); setDrawerModel(preModel); setDrawerExpiry('1year'); setDrawerCustomDate('')
|
||||
if (preModel && modelPresets[preModel]) { setSelectedItems([...modelPresets[preModel]]) }
|
||||
else if (preModel && existingLicense) {
|
||||
// 已有授权记录时,回填已选项
|
||||
const existingModules = existingLicense.modules.split(', ')
|
||||
setSelectedItems(allAuthItems.filter(a => existingModules.includes(a.name)).map(a => a.id))
|
||||
} else { setSelectedItems([]) }
|
||||
}} className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-white" style={{ backgroundColor: '#4a7c59' }}>
|
||||
<Plus size={16} />选择授权项
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -111,19 +120,21 @@ function LicensesContent() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-4 mb-6" style={{ boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<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); 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>
|
||||
{!isFromModels && (
|
||||
<div className="bg-white rounded-lg p-4 mb-6" style={{ boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<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); setCurrentPage(1) }} className="px-3 py-1.5 rounded text-sm" style={{ border: '1px solid #D9D9D9', minWidth: 140 }}>
|
||||
<option value="">全部型号</option>
|
||||
{deviceModels.map(dm => (
|
||||
<option key={dm.id} value={dm.name.replace(' Supreme', '')}>{dm.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-lg overflow-hidden" style={{ boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<table className="w-full">
|
||||
|
|
@ -175,12 +186,16 @@ function LicensesContent() {
|
|||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm mb-2" style={{ color: 'rgba(0,0,0,0.85)' }}>设备型号</label>
|
||||
<select value={drawerModel} onChange={e => handleModelChange(e.target.value)} className="w-full px-3 py-2 rounded text-sm" style={{ border: '1px solid #D9D9D9' }}>
|
||||
<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>
|
||||
{isFromModels ? (
|
||||
<div className="px-3 py-2 rounded text-sm" style={{ border: '1px solid #D9D9D9', backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.85)' }}>{modelParam}</div>
|
||||
) : (
|
||||
<select value={drawerModel} onChange={e => handleModelChange(e.target.value)} className="w-full px-3 py-2 rounded text-sm" style={{ border: '1px solid #D9D9D9' }}>
|
||||
<option value="">请选择型号</option>
|
||||
{deviceModels.map(dm => (
|
||||
<option key={dm.id} value={dm.name.replace(' Supreme', '')}>{dm.name}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
{drawerExpiry === 'custom' && (
|
||||
<div className="mb-6">
|
||||
|
|
@ -229,7 +244,12 @@ function LicensesContent() {
|
|||
<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>
|
||||
<button onClick={async () => {
|
||||
if (!drawerModel) return
|
||||
const modules = selectedItems.map(id => allAuthItems.find(a => a.id === id)?.name).filter(Boolean).join(', ')
|
||||
await fetch('/api/licenses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: drawerModel, modules, expiry: drawerExpiry === 'custom' ? drawerCustomDate : '', status: '生效' }) })
|
||||
refetch(); setDrawerOpen(false)
|
||||
}} className="w-full py-2 rounded-lg text-sm text-white" style={{ backgroundColor: '#4a7c59' }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Plus, Edit, Trash2, X, Info, CheckCircle, XCircle } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
interface MaterialCategory {
|
||||
id: number; name: string; description: string; has_firmware: number; has_calibration: number; sort_order: number; status: string
|
||||
}
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
return status === '启用'
|
||||
? { backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' }
|
||||
: { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
}
|
||||
|
||||
export default function MaterialCategoriesPage() {
|
||||
const { data: categories, loading, refetch } = useApi<MaterialCategory[]>('/api/material-categories', [])
|
||||
const [addDrawer, setAddDrawer] = useState(false)
|
||||
const [editDrawer, setEditDrawer] = useState<MaterialCategory | null>(null)
|
||||
const [form, setForm] = useState({ name: '', description: '', has_firmware: false, has_calibration: false, sort_order: 0, status: '启用' })
|
||||
|
||||
const resetForm = () => setForm({ name: '', description: '', has_firmware: false, has_calibration: false, sort_order: 0, status: '启用' })
|
||||
|
||||
const handleAdd = async () => {
|
||||
await fetch('/api/material-categories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) })
|
||||
refetch(); setAddDrawer(false); resetForm()
|
||||
}
|
||||
const handleEdit = async () => {
|
||||
if (!editDrawer) return
|
||||
await fetch('/api/material-categories', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editDrawer) })
|
||||
refetch(); setEditDrawer(null)
|
||||
}
|
||||
const handleDelete = async (id: number) => {
|
||||
await fetch('/api/material-categories', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) })
|
||||
refetch()
|
||||
}
|
||||
|
||||
if (loading) return <div style={{ padding: 24 }}>加载中...</div>
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>物料分类管理</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理物料分类类型,定义分类属性(是否有固件、是否需要校准)</p>
|
||||
</div>
|
||||
|
||||
<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 }}>物料分类定义了物料的大类属性。新增物料类型时需选择所属分类。标记"有固件"的分类在版本管理中可进行固件管理。</div>
|
||||
</div>
|
||||
|
||||
<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 }}>分类列表(共 {categories.length} 项)</h3>
|
||||
<button onClick={() => { resetForm(); 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>
|
||||
{categories.map(cat => (
|
||||
<tr key={cat.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>{cat.sort_order}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{cat.name}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{cat.description || '-'}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
{cat.has_firmware ? <CheckCircle size={16} style={{ color: '#52C41A' }} /> : <XCircle size={16} style={{ color: 'rgba(0,0,0,0.15)' }} />}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
{cat.has_calibration ? <CheckCircle size={16} style={{ color: '#FAAD14' }} /> : <XCircle size={16} style={{ color: 'rgba(0,0,0,0.15)' }} />}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}><span style={{ ...getStatusStyle(cat.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{cat.status}</span></td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => setEditDrawer({ ...cat })} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}><Edit size={14} />编辑</button>
|
||||
<button onClick={() => handleDelete(cat.id)} style={{ color: '#FF4D4F', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}><Trash2 size={14} />删除</button>
|
||||
</div>
|
||||
</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>
|
||||
<input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="如 传感器" 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>
|
||||
<textarea value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} placeholder="分类描述" rows={3} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>排序</label>
|
||||
<input type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} 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={form.has_firmware} onChange={e => setForm({ ...form, has_firmware: e.target.checked })} />有固件
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={form.has_calibration} onChange={e => setForm({ ...form, has_calibration: 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={!form.name} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: form.name ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: form.name ? 'pointer' : 'not-allowed', fontSize: 14 }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Drawer */}
|
||||
{editDrawer && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setEditDrawer(null)} 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={() => setEditDrawer(null)} 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>
|
||||
<input value={editDrawer.name} onChange={e => setEditDrawer({ ...editDrawer, name: e.target.value })} 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>
|
||||
<textarea value={editDrawer.description} onChange={e => setEditDrawer({ ...editDrawer, description: e.target.value })} rows={3} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>排序</label>
|
||||
<input type="number" value={editDrawer.sort_order} onChange={e => setEditDrawer({ ...editDrawer, sort_order: parseInt(e.target.value) || 0 })} 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={!!editDrawer.has_firmware} onChange={e => setEditDrawer({ ...editDrawer, has_firmware: e.target.checked ? 1 : 0 })} />有固件
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={!!editDrawer.has_calibration} onChange={e => setEditDrawer({ ...editDrawer, has_calibration: e.target.checked ? 1 : 0 })} />需校准
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>状态</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{['启用', '停用'].map(s => (
|
||||
<button key={s} onClick={() => setEditDrawer({ ...editDrawer, status: s })} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: editDrawer.status === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: editDrawer.status === s ? '#eef5f0' : '#fff', color: editDrawer.status === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setEditDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={handleEdit} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,8 +11,8 @@ interface MaterialVersion {
|
|||
id: number; type: string; version: string; status: string
|
||||
}
|
||||
|
||||
const categoryOptions = ['全部', '主协板', '采集板', '发射板', '升压板', '电缆头', '电缆', '机箱', '电源']
|
||||
const deviceModelOptions = ['GD-30 Supreme', 'GD-20 Supreme', 'GD-10 Supreme', 'GM-10', 'GT-10', 'GP-10']
|
||||
interface CategoryItem { id: number; name: string; status: string; has_firmware: number }
|
||||
interface DeviceModel { id: number; name: string; code: string; status: string }
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
|
|
@ -26,6 +26,10 @@ export default function MaterialsManagePage() {
|
|||
const router = useRouter()
|
||||
const { data: materialTypes, loading: lt, refetch: refetchTypes } = useApi<MaterialType[]>('/api/material-types', [])
|
||||
const { data: versionsData, loading: lv, refetch: refetchVersions } = useApi<MaterialVersion[]>('/api/material-versions', [])
|
||||
const { data: categoriesData } = useApi<CategoryItem[]>('/api/material-categories', [])
|
||||
const { data: deviceModelsData } = useApi<DeviceModel[]>('/api/models', [])
|
||||
const categoryOptions = ['全部', ...categoriesData.filter(c => c.status === '启用').map(c => c.name)]
|
||||
const deviceModelOptions = deviceModelsData.map(dm => dm.name)
|
||||
|
||||
const [filterCategory, setFilterCategory] = useState('全部')
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
|
@ -52,7 +56,7 @@ export default function MaterialsManagePage() {
|
|||
const handleAddType = async () => {
|
||||
await fetch('/api/material-types', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(typeForm) })
|
||||
refetchTypes(); setAddDrawer(false)
|
||||
setTypeForm({ name: '', category: '主协板', deviceModels: [], description: '', status: '启用' })
|
||||
setTypeForm({ name: '', category: categoryOptions.find(c => c !== '全部') || '', deviceModels: [], description: '', status: '启用' })
|
||||
}
|
||||
const handleEditType = async () => {
|
||||
if (!editDrawer) return
|
||||
|
|
@ -114,7 +118,7 @@ export default function MaterialsManagePage() {
|
|||
<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 }}>物料列表(共 {filtered.length} 项)</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>
|
||||
<button onClick={() => { setTypeForm({ name: '', category: categoryOptions.find(c => c !== '全部') || '', deviceModels: [], description: '', status: '启用' }); 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' }}>
|
||||
|
|
@ -148,6 +152,9 @@ export default function MaterialsManagePage() {
|
|||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => setEditDrawer({ ...bt })} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}><Edit size={14} />编辑</button>
|
||||
{categoriesData.find(c => c.name === bt.category)?.has_firmware ? (
|
||||
<button onClick={() => router.push(`/firmware?type=${bt.category}`)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}><Upload size={14} />固件</button>
|
||||
) : null}
|
||||
<button onClick={() => handleDeleteType(bt.id)} style={{ color: '#FF4D4F', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}><Trash2 size={14} />删除</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -252,8 +259,8 @@ export default function MaterialsManagePage() {
|
|||
</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>
|
||||
<input value={typeForm.name} onChange={e => setTypeForm({ ...typeForm, name: e.target.value })} placeholder="如 GM10 采集板" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料名称</label>
|
||||
<input value={typeForm.name} onChange={e => setTypeForm({ ...typeForm, name: e.target.value })} placeholder="如 GD30 采集板" 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 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料分类</label>
|
||||
|
|
@ -269,6 +276,14 @@ export default function MaterialsManagePage() {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>状态</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{['启用', '停用'].map(s => (
|
||||
<button key={s} onClick={() => setTypeForm({ ...typeForm, status: s })} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: typeForm.status === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: typeForm.status === s ? '#eef5f0' : '#fff', color: typeForm.status === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>描述</label>
|
||||
<textarea value={typeForm.description} onChange={e => setTypeForm({ ...typeForm, description: e.target.value })} placeholder="物料类型描述" rows={3} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', resize: 'vertical' }} />
|
||||
|
|
@ -293,7 +308,7 @@ export default function MaterialsManagePage() {
|
|||
</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>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料名称</label>
|
||||
<input value={editDrawer.name} onChange={e => setEditDrawer({ ...editDrawer, name: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import { useState, useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ChevronLeft, ChevronRight, Plus, Download, Eye, X, FileText, Download as DownloadIcon } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Plus, Download, Eye, X, FileText, Download as DownloadIcon, Trash2 } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
interface BoardCard {
|
||||
|
|
@ -21,7 +21,8 @@ interface BoardCard {
|
|||
calib_date: string
|
||||
}
|
||||
|
||||
const typeOptions = ['全部', '主协板', '采集板', '发射板', '升压板', '电缆头', '电缆', '机箱', '电源']
|
||||
interface CategoryItem { id: number; name: string; status: string }
|
||||
|
||||
const statusOptions = ['全部', '在库', '已装配', '故障', '报废']
|
||||
const calibStatusOptions = ['全部', '合格', '不合格', '待校准']
|
||||
|
||||
|
|
@ -82,7 +83,9 @@ function getCalibStyle(status: string) {
|
|||
}
|
||||
|
||||
export default function BoardCardsPage() {
|
||||
const { data: boardCardsData, loading } = useApi<BoardCard[]>('/api/materials', [])
|
||||
const { data: boardCardsData, loading, refetch } = useApi<BoardCard[]>('/api/materials', [])
|
||||
const { data: categoriesData } = useApi<CategoryItem[]>('/api/material-categories', [])
|
||||
const typeOptions = ['全部', ...categoriesData.filter(c => c.status === '启用').map(c => c.name)]
|
||||
const [filterType, setFilterType] = useState('全部')
|
||||
const [filterStatus, setFilterStatus] = useState('全部')
|
||||
const [filterCalib, setFilterCalib] = useState('全部')
|
||||
|
|
@ -98,7 +101,7 @@ export default function BoardCardsPage() {
|
|||
if (filterType !== '全部' && b.category !== filterType) return false
|
||||
if (filterStatus !== '全部' && b.status !== filterStatus) return false
|
||||
if (filterCalib !== '全部' && b.calib_status !== filterCalib) return false
|
||||
if (searchText && !b.sn.toLowerCase().includes(searchText.toLowerCase()) && !b.name.toLowerCase().includes(searchText.toLowerCase())) return false
|
||||
if (searchText && !b.sn.toLowerCase().includes(searchText.toLowerCase()) && !(b.category || b.name).toLowerCase().includes(searchText.toLowerCase())) return false
|
||||
return true
|
||||
})
|
||||
|
||||
|
|
@ -170,8 +173,8 @@ export default function BoardCardsPage() {
|
|||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>SN / 物料名称</label>
|
||||
<input type="text" value={searchText} onChange={e => { setSearchText(e.target.value); setCurrentPage(1) }} placeholder="搜索SN或物料名称" style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>搜索</label>
|
||||
<input type="text" value={searchText} onChange={e => { setSearchText(e.target.value); setCurrentPage(1) }} placeholder="搜索SN或物料分类" style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -181,7 +184,7 @@ export default function BoardCardsPage() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['物料名称', '物料类型', '适配设备型号', '物料版本', '描述', '状态', '操作'].map(h => (
|
||||
{['物料SN', '物料分类', '物料版本', '生产日期', '状态', '所属设备', '操作'].map(h => (
|
||||
<th key={h} style={{ padding: '12px 14px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', whiteSpace: 'nowrap' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -189,14 +192,14 @@ export default function BoardCardsPage() {
|
|||
<tbody>
|
||||
{paged.map(row => (
|
||||
<tr key={row.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, fontWeight: 500 }}>{row.name}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.type}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.device_model}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, fontWeight: 500 }}>{row.sn}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.category || row.name}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.version}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{row.description || '-'}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.production_date}</td>
|
||||
<td style={{ padding: '12px 14px' }}>
|
||||
<span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: row.device_sn === '-' ? 'rgba(0,0,0,0.25)' : '#4a7c59', fontWeight: row.device_sn === '-' ? 400 : 500 }}>{row.device_sn}</td>
|
||||
<td style={{ padding: '12px 14px' }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => setDetailDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
|
|
@ -207,6 +210,9 @@ export default function BoardCardsPage() {
|
|||
<FileText size={14} />校准文件
|
||||
</button>
|
||||
)}
|
||||
<button onClick={async () => { await fetch('/api/materials', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: row.id }) }); refetch() }} style={{ color: '#FF4D4F', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Trash2 size={14} />删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -244,14 +250,14 @@ export default function BoardCardsPage() {
|
|||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}>基本信息</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>物料SN:</span>{detailDrawer.sn}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>物料类型:</span>{detailDrawer.type}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>版本:</span>{detailDrawer.version}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>固件版本:</span>{detailDrawer.firmware}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>物料分类:</span>{detailDrawer.category || detailDrawer.name}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>物料版本:</span>{detailDrawer.version}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>生产日期:</span>{detailDrawer.production_date}</div>
|
||||
<div>
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)' }}>状态:</span>
|
||||
<span style={{ ...getStatusStyle(detailDrawer.status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.status}</span>
|
||||
</div>
|
||||
{detailDrawer.description && <div><span style={{ color: 'rgba(0,0,0,0.45)' }}>备注:</span>{detailDrawer.description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -269,7 +275,7 @@ export default function BoardCardsPage() {
|
|||
</div>
|
||||
|
||||
{/* 校准信息 */}
|
||||
{(detailDrawer.type === '采集板') && (
|
||||
{detailDrawer.calib_status && detailDrawer.calib_status !== '-' && (
|
||||
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}>校准信息</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,20 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useRef } from 'react'
|
||||
import { ArrowLeft, Plus, Trash2, Upload, Info, CheckCircle, FileText, X, ScanLine } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
/** 板卡类型 -> 可选版本 */
|
||||
const versionsByType: Record<string, { version: string; firmware: string }[]> = {
|
||||
'主协板': [
|
||||
{ version: 'MB-V1.2', firmware: 'v2.1' },
|
||||
{ version: 'MB-V2.1', firmware: 'v1.8' },
|
||||
],
|
||||
'采集板': [
|
||||
{ version: 'RX-V1.3', firmware: 'v3.0' },
|
||||
{ version: 'RX-V2.1', firmware: 'v2.5' },
|
||||
],
|
||||
'发射板': [
|
||||
{ version: 'TX-V1.5', firmware: 'v1.2' },
|
||||
{ version: 'TX-V2.1', firmware: 'v1.0' },
|
||||
],
|
||||
'升压板': [
|
||||
{ version: 'BO-V2.1', firmware: 'v1.1' },
|
||||
{ version: 'BO-V2.2', firmware: 'v0.9' },
|
||||
],
|
||||
'电缆头': [
|
||||
{ version: 'CBH-V1.0', firmware: '-' },
|
||||
],
|
||||
'电缆': [
|
||||
{ version: 'CBL-30M', firmware: '-' },
|
||||
{ version: 'CBL-60M', firmware: '-' },
|
||||
{ version: 'CBL-100M', firmware: '-' },
|
||||
],
|
||||
'机箱': [
|
||||
{ version: 'GD30-CASE-A', firmware: '-' },
|
||||
{ version: 'GD30-CASE-B', firmware: '-' },
|
||||
{ version: 'GM10-CASE-A', firmware: '-' },
|
||||
],
|
||||
'电源': [
|
||||
{ version: 'BP150-V1.0', firmware: '-' },
|
||||
{ version: 'BP300-V1.0', firmware: '-' },
|
||||
{ version: 'BP600-V1.0', firmware: '-' },
|
||||
{ version: 'BP600-V2.0', firmware: '-' },
|
||||
],
|
||||
}
|
||||
|
||||
const typeOptions = Object.keys(versionsByType)
|
||||
interface CategoryItem { id: number; name: string; status: string; has_calibration: number }
|
||||
interface VersionItem { id: number; type: string; version: string; status: string }
|
||||
|
||||
interface BoardEntry {
|
||||
id: number
|
||||
type: string
|
||||
version: string
|
||||
firmware: string
|
||||
sn: string
|
||||
productionDate: string
|
||||
remark: string
|
||||
status: string
|
||||
calibStatus: string
|
||||
calibFile: File | null
|
||||
}
|
||||
|
||||
|
|
@ -60,15 +22,28 @@ let nextId = 1
|
|||
|
||||
export default function BoardRegisterPage() {
|
||||
const router = useRouter()
|
||||
const { data: categoriesData } = useApi<CategoryItem[]>('/api/material-categories', [])
|
||||
const { data: versionsData } = useApi<VersionItem[]>('/api/material-versions', [])
|
||||
const typeOptions = categoriesData.filter(c => c.status === '启用').map(c => c.name)
|
||||
|
||||
// 根据分类名获取该分类下的版本列表
|
||||
const getVersionsForType = (type: string) => versionsData.filter(v => v.type === type && v.status === '在产')
|
||||
// 判断分类是否需要校准
|
||||
const needsCalibration = (type: string) => !!categoriesData.find(c => c.name === type)?.has_calibration
|
||||
|
||||
function createEntry(): BoardEntry {
|
||||
const defaultType = typeOptions[0] || ''
|
||||
const versions = getVersionsForType(defaultType)
|
||||
return { id: nextId++, type: defaultType, version: versions[0]?.version || '', sn: '', productionDate: '', status: '在库', calibStatus: '-', calibFile: null }
|
||||
}
|
||||
|
||||
const [entries, setEntries] = useState<BoardEntry[]>([createEntry()])
|
||||
const [batchMode, setBatchMode] = useState(false)
|
||||
|
||||
function createEntry(): BoardEntry {
|
||||
return { id: nextId++, type: '采集板', version: 'RX-V2.1', firmware: 'v2.1', sn: '', productionDate: '', remark: '', calibFile: null }
|
||||
}
|
||||
|
||||
const addEntry = () => {
|
||||
setEntries(prev => [...prev, createEntry()])
|
||||
const defaultType = typeOptions[0] || ''
|
||||
const versions = getVersionsForType(defaultType)
|
||||
setEntries(prev => [...prev, { id: nextId++, type: defaultType, version: versions[0]?.version || '', sn: '', productionDate: '', status: '在库', calibStatus: '-', calibFile: null }])
|
||||
}
|
||||
|
||||
const removeEntry = (id: number) => {
|
||||
|
|
@ -80,21 +55,11 @@ export default function BoardRegisterPage() {
|
|||
setEntries(prev => prev.map(e => {
|
||||
if (e.id !== id) return e
|
||||
const updated = { ...e, [field]: value }
|
||||
// 切换类型时自动选第一个版本和固件
|
||||
// 切换类型时自动选第一个版本
|
||||
if (field === 'type') {
|
||||
const versions = versionsByType[value]
|
||||
if (versions && versions.length > 0) {
|
||||
updated.version = versions[0].version
|
||||
updated.firmware = versions[0].firmware
|
||||
}
|
||||
// 切换到非采集板时清除校准文件
|
||||
if (value !== '采集板') updated.calibFile = null
|
||||
}
|
||||
// 切换版本时自动填充固件
|
||||
if (field === 'version') {
|
||||
const versions = versionsByType[updated.type]
|
||||
const match = versions?.find(m => m.version === value)
|
||||
if (match) updated.firmware = match.firmware
|
||||
const versions = getVersionsForType(value)
|
||||
updated.version = versions[0]?.version || ''
|
||||
if (needsCalibration(value)) { updated.calibStatus = '待校准' } else { updated.calibStatus = '-'; updated.calibFile = null }
|
||||
}
|
||||
return updated
|
||||
}))
|
||||
|
|
@ -175,16 +140,11 @@ export default function BoardRegisterPage() {
|
|||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料版本</label>
|
||||
<select value={entry.version} onChange={e => updateEntry(entry.id, 'version', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{(versionsByType[entry.type] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}
|
||||
{getVersionsForType(entry.type).map(v => <option key={v.id} value={v.version}>{v.version}</option>)}
|
||||
{getVersionsForType(entry.type).length === 0 && <option value="">暂无版本</option>}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 固件版本(自动填充,只读) */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>固件版本</label>
|
||||
<input value={entry.firmware} readOnly style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.65)', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
{/* 物料SN */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料SN号</label>
|
||||
|
|
@ -202,28 +162,41 @@ export default function BoardRegisterPage() {
|
|||
<input type="date" value={entry.productionDate} onChange={e => updateEntry(entry.id, 'productionDate', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
{/* 物料状态 */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>备注</label>
|
||||
<input value={entry.remark} onChange={e => updateEntry(entry.id, 'remark', e.target.value)} placeholder="可选" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>物料状态</label>
|
||||
<select value={entry.status} onChange={e => updateEntry(entry.id, 'status', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{['在库', '已装配', '故障', '报废'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 校准状态(仅需校准的分类显示) */}
|
||||
{needsCalibration(entry.type) && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>校准状态</label>
|
||||
<select value={entry.calibStatus} onChange={e => updateEntry(entry.id, 'calibStatus', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{['待校准', '合格', '不合格'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* 采集板校准提示 */}
|
||||
{entry.type === '采集板' && (
|
||||
{/* 需校准物料提示 */}
|
||||
{needsCalibration(entry.type) && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 16, padding: '8px 12px', backgroundColor: '#FFFBE6', borderRadius: 6, border: '1px solid #FFE58F' }}>
|
||||
<Info size={14} style={{ color: '#FAAD14', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>采集板登记后状态为"待校准",需完成校准后才能用于设备装配。</span>
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>该物料登记后状态为"待校准",需完成校准后才能用于设备装配。</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 采集板校准文件导入 */}
|
||||
{entry.type === '采集板' && (
|
||||
{/* 校准文件导入(需校准的分类显示) */}
|
||||
{needsCalibration(entry.type) && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 8 }}>校准文件(可选)</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls,.dat"
|
||||
accept=".csv,.xlsx,.xls,.dat,.json"
|
||||
ref={el => { fileInputRefs.current[entry.id] = el }}
|
||||
onChange={e => handleCalibFileChange(entry.id, e.target.files?.[0] || null)}
|
||||
style={{ display: 'none' }}
|
||||
|
|
@ -243,7 +216,7 @@ export default function BoardRegisterPage() {
|
|||
</div>
|
||||
) : (
|
||||
<button onClick={() => fileInputRefs.current[entry.id]?.click()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: '100%', padding: '14px 0', border: '2px dashed #D9D9D9', borderRadius: 6, backgroundColor: '#FAFAFA', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
<Upload size={16} />导入校准文件(支持 .csv / .xlsx / .dat)
|
||||
<Upload size={16} />导入校准文件(支持 .csv / .xlsx / .dat / .json)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -265,7 +238,7 @@ export default function BoardRegisterPage() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['序号', '类型', '版本', '固件', 'SN号', '生产日期', '校准文件', '状态'].map(h => (
|
||||
{['序号', '物料分类', '物料版本', '物料SN号', '生产日期', '物料状态', '校准状态', '校准文件'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 14px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -276,27 +249,25 @@ export default function BoardRegisterPage() {
|
|||
<td style={{ padding: '10px 14px', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{i + 1}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.type}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.version}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{entry.firmware}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, fontWeight: 500, color: entry.sn ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.25)' }}>{entry.sn || '未填写'}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, color: entry.productionDate ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0.25)' }}>{entry.productionDate || '未填写'}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.status}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>
|
||||
{entry.type === '采集板' ? (
|
||||
{needsCalibration(entry.type) ? entry.calibStatus : <span style={{ color: 'rgba(0,0,0,0.25)' }}>-</span>}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>
|
||||
{needsCalibration(entry.type) ? (
|
||||
entry.calibFile ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: '#4a7c59', fontSize: 12 }}>
|
||||
<FileText size={12} />{entry.calibFile.name}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>未导入</span>
|
||||
<span style={{ fontSize: 12, color: '#FAAD14' }}>待上传</span>
|
||||
)
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px' }}>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, ...(entry.type === '采集板' ? { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' } : { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }) }}>
|
||||
{entry.type === '采集板' ? '待校准' : '在库'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -309,13 +280,28 @@ export default function BoardRegisterPage() {
|
|||
<div style={{ position: 'sticky', bottom: 0, backgroundColor: '#fff', borderTop: '1px solid #F0F0F0', padding: '12px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', zIndex: 10 }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
共 {entries.length} 条物料待登记
|
||||
{entries.some(e => e.type === '采集板') && <span style={{ color: '#FAAD14' }}> · 含采集板需后续校准</span>}
|
||||
{entries.some(e => e.type === '采集板' && e.calibFile) && <span style={{ color: '#4a7c59' }}> · {entries.filter(e => e.calibFile).length} 个已附校准文件</span>}
|
||||
{entries.some(e => needsCalibration(e.type)) && <span style={{ color: '#FAAD14' }}> · 含需校准物料</span>}
|
||||
{entries.some(e => needsCalibration(e.type) && e.calibFile) && <span style={{ color: '#4a7c59' }}> · {entries.filter(e => e.calibFile).length} 个已附校准文件</span>}
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => router.push('/materials')} style={{ padding: '8px 24px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button
|
||||
onClick={() => router.push('/materials')}
|
||||
onClick={async () => {
|
||||
for (const entry of entries) {
|
||||
await fetch('/api/materials', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sn: entry.sn, name: entry.type, category: entry.type, type: entry.version,
|
||||
device_model: '', version: entry.version, description: '',
|
||||
firmware: '-', status: entry.status,
|
||||
production_date: entry.productionDate,
|
||||
calib_status: needsCalibration(entry.type) ? entry.calibStatus : '-',
|
||||
calib_date: '-',
|
||||
})
|
||||
})
|
||||
}
|
||||
router.push('/materials')
|
||||
}}
|
||||
disabled={!isValid}
|
||||
style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: isValid ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: isValid ? 'pointer' : 'not-allowed', fontSize: 14 }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
import { useState, Suspense, useEffect, useCallback } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { ArrowLeft, Plus, Trash2, X, Info, CheckCircle } from 'lucide-react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
interface CategoryItem { id: number; name: string; status: string }
|
||||
|
||||
interface BomItem {
|
||||
id: number
|
||||
|
|
@ -31,6 +34,7 @@ function BomContent() {
|
|||
const searchParams = useSearchParams()
|
||||
const modelCode = searchParams.get('model') || 'GD30'
|
||||
const modelName = modelNames[modelCode] || modelCode
|
||||
const { data: categoriesData } = useApi<CategoryItem[]>('/api/material-categories', [])
|
||||
|
||||
const [bomList, setBomList] = useState<BomItem[]>([])
|
||||
const [addDrawer, setAddDrawer] = useState(false)
|
||||
|
|
@ -163,7 +167,7 @@ function BomContent() {
|
|||
<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>)}
|
||||
{categoriesData.filter(c => c.status === '启用').map(c => <option key={c.name} value={c.name}>{c.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function getStatusStyle(status: string) {
|
|||
export default function ModelsPage() {
|
||||
const router = useRouter()
|
||||
const { data: modelsData, refetch: refetchModels } = useApi<ModelRow[]>('/api/models', [])
|
||||
const { data: checklistTemplates } = useApi<Record<string, CLItem[]>>('/api/models/checklist', {})
|
||||
const { data: checklistTemplates, refetch: refetchChecklist } = useApi<Record<string, CLItem[]>>('/api/models/checklist', {})
|
||||
const [modelDrawer, setModelDrawer] = useState(false)
|
||||
const [checklistDrawer, setChecklistDrawer] = useState(false)
|
||||
const [checklistTab, setChecklistTab] = useState('GD30')
|
||||
|
|
@ -142,7 +142,7 @@ export default function ModelsPage() {
|
|||
<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 }}>装配 Checklist 模板</h3>
|
||||
<button onClick={() => setChecklistDrawer(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
<button onClick={() => { setChecklistForm({ model: modelsData[0]?.code || '', items: [{ name: '', required: true }] }); setChecklistDrawer(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>
|
||||
|
|
@ -271,7 +271,7 @@ export default function ModelsPage() {
|
|||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 设备型号</label>
|
||||
<select value={checklistForm.model} onChange={e => setChecklistForm({ ...checklistForm, model: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{Object.keys(checklistTemplates).map(m => <option key={m} value={m}>{m}</option>)}
|
||||
{modelsData.map(m => <option key={m.code} value={m.code}>{m.name}({m.code})</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
|
|
@ -294,7 +294,10 @@ export default function ModelsPage() {
|
|||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => setChecklistDrawer(false)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={() => setChecklistDrawer(false)} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
<button onClick={async () => {
|
||||
await fetch('/api/models/checklist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_code: checklistForm.model, items: checklistForm.items }) })
|
||||
refetchChecklist(); setChecklistDrawer(false); setChecklistTab(checklistForm.model)
|
||||
}} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
177
src/app/page.tsx
177
src/app/page.tsx
|
|
@ -1,39 +1,53 @@
|
|||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { TrendingUp, TrendingDown, Server, Wifi, CheckCircle, PackageCheck, Wrench, Target, Clock, Upload } from 'lucide-react'
|
||||
import { Server, Wifi, CheckCircle, PackageCheck, Wrench, Target, Clock, Upload } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useApi } from '@/lib/hooks'
|
||||
|
||||
const metrics = [
|
||||
{ label: '设备总数', value: '5,234', trend: 'up' as const, trendValue: '+5.2%', color: '#4a7c59', icon: Server, link: '/devices' },
|
||||
{ label: '装配中', value: '4,856', trend: 'up' as const, trendValue: '+2.8%', color: '#52C41A', icon: Wifi, link: '/devices' },
|
||||
{ label: '已激活', value: '4,912', trend: 'up' as const, trendValue: '+1.5%', color: '#4a7c59', icon: CheckCircle, link: '/devices' },
|
||||
{ label: '有新版本', value: '156', color: '#722ED1', icon: PackageCheck, link: '/firmware' },
|
||||
{ label: '维修中', value: '23', trend: 'down' as const, trendValue: '-12.3%', color: '#FF4D4F', icon: Wrench, link: '/repair' },
|
||||
{ label: '报废', value: '56', color: '#FA8C16', icon: Target, link: '/scrap' },
|
||||
{ label: '授权即将到期', value: '45', color: '#FAAD14', icon: Clock, link: '/licenses' },
|
||||
{ label: '可升级', value: '8', color: '#13C2C2', icon: Upload, link: '/firmware' },
|
||||
]
|
||||
interface DashboardData {
|
||||
devices: { total: number; assembling: number; activated: number; shipped: number }
|
||||
materials: { total: number; inStock: number; assembled: number; faulty: number }
|
||||
repair: { total: number; pending: number; processing: number; done: number }
|
||||
scrap: { total: number; pendingApproval: number }
|
||||
firmware: { total: number }
|
||||
licenses: { total: number }
|
||||
recentRepairs: { id: string; sn: string; fault_type: string; status: string; priority: string; create_date: string }[]
|
||||
recentScraps: { id: number; sn: string; model: string; status: string; date: string }[]
|
||||
}
|
||||
|
||||
const deviceStatusData = [
|
||||
{ name: '已装配', value: 45, color: '#52C41A' },
|
||||
{ name: '已出厂', value: 378, color: '#FF4D4F' },
|
||||
{ name: '已激活', value: 286, color: '#FAAD14' },
|
||||
{ name: '报废', value: 7, color: '#8C8C8C' },
|
||||
]
|
||||
|
||||
const taskGroups = [
|
||||
{ title: '维修工单', count: 5, link: '/repair', tasks: [
|
||||
{ deviceSN: 'GD30-2024-000056', description: '板卡故障,待处理', time: '4小时前', link: '/repair/WO-2024-0001' },
|
||||
{ deviceSN: 'GD30-2024-000078', description: '固件异常', time: '6小时前', link: '/repair/WO-2024-0002' },
|
||||
]},
|
||||
{ title: '授权即将到期', count: 45, link: '/licenses', tasks: [
|
||||
{ deviceSN: 'GD30-2025-000001', description: '授权将于30天后到期', time: '30天', link: '/licenses' },
|
||||
{ deviceSN: 'GT20-2025-000045', description: '授权将于15天后到期', time: '15天', link: '/licenses' },
|
||||
]},
|
||||
]
|
||||
const defaultData: DashboardData = {
|
||||
devices: { total: 0, assembling: 0, activated: 0, shipped: 0 },
|
||||
materials: { total: 0, inStock: 0, assembled: 0, faulty: 0 },
|
||||
repair: { total: 0, pending: 0, processing: 0, done: 0 },
|
||||
scrap: { total: 0, pendingApproval: 0 },
|
||||
firmware: { total: 0 },
|
||||
licenses: { total: 0 },
|
||||
recentRepairs: [],
|
||||
recentScraps: [],
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const maxStatusValue = useMemo(() => Math.max(...deviceStatusData.map(d => d.value)), [])
|
||||
const { data, loading } = useApi<DashboardData>('/api/dashboard', defaultData)
|
||||
|
||||
const metrics = [
|
||||
{ label: '设备总数', value: data.devices.total, color: '#4a7c59', icon: Server, link: '/devices' },
|
||||
{ label: '装配中', value: data.devices.assembling, color: '#52C41A', icon: Wifi, link: '/devices' },
|
||||
{ label: '已激活', value: data.devices.activated, color: '#4a7c59', icon: CheckCircle, link: '/devices' },
|
||||
{ label: '固件版本', value: data.firmware.total, color: '#722ED1', icon: PackageCheck, link: '/firmware' },
|
||||
{ label: '维修中', value: data.repair.processing, color: '#FF4D4F', icon: Wrench, link: '/repair' },
|
||||
{ label: '报废', value: data.scrap.total, color: '#FA8C16', icon: Target, link: '/scrap' },
|
||||
{ label: '授权配置', value: data.licenses.total, color: '#FAAD14', icon: Clock, link: '/licenses' },
|
||||
{ label: '物料总数', value: data.materials.total, color: '#13C2C2', icon: Upload, link: '/materials' },
|
||||
]
|
||||
|
||||
const deviceStatusData = [
|
||||
{ name: '装配中', value: data.devices.assembling, color: '#FAAD14' },
|
||||
{ name: '已出厂', value: data.devices.shipped, color: '#597EF7' },
|
||||
{ name: '已激活', value: data.devices.activated, color: '#52C41A' },
|
||||
]
|
||||
const maxStatusValue = useMemo(() => Math.max(...deviceStatusData.map(d => d.value), 1), [data])
|
||||
|
||||
if (loading) return <div className="p-6">加载中...</div>
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
|
|
@ -50,12 +64,6 @@ export default function DashboardPage() {
|
|||
<div className="flex-1">
|
||||
<div className="text-sm mb-2" style={{ color: 'rgba(0,0,0,0.65)' }}>{m.label}</div>
|
||||
<div className="text-3xl font-semibold mb-2">{m.value}</div>
|
||||
{m.trend && m.trendValue && (
|
||||
<div className="flex items-center gap-1" style={{ color: m.trend === 'up' ? '#52C41A' : '#FF4D4F' }}>
|
||||
{m.trend === 'up' ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
|
||||
<span className="text-sm">{m.trendValue}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-lg flex items-center justify-center" style={{ backgroundColor: m.color + '15' }}>
|
||||
<Icon size={24} style={{ color: m.color }} />
|
||||
|
|
@ -66,47 +74,82 @@ export default function DashboardPage() {
|
|||
})}
|
||||
</div>
|
||||
|
||||
{/* 设备状态分布 */}
|
||||
<div className="bg-white p-6 rounded-lg mb-6" style={{ boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 className="text-lg font-semibold mb-6">设备状态分布</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{deviceStatusData.map(item => (
|
||||
<div key={item.name} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 60, textAlign: 'right', fontSize: 14, color: 'rgba(0,0,0,0.65)', flexShrink: 0 }}>{item.name}</div>
|
||||
<div style={{ flex: 1, backgroundColor: '#F5F5F5', borderRadius: 4, height: 24, overflow: 'hidden' }}>
|
||||
<div style={{ width: `${(item.value / maxStatusValue) * 100}%`, height: '100%', backgroundColor: item.color, borderRadius: '0 4px 4px 0', transition: 'width 0.3s ease', minWidth: item.value > 0 ? 2 : 0 }} />
|
||||
{data.devices.total === 0 ? (
|
||||
<div className="text-center py-8" style={{ color: 'rgba(0,0,0,0.25)', fontSize: 14 }}>暂无设备数据</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{deviceStatusData.map(item => (
|
||||
<div key={item.name} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 60, textAlign: 'right', fontSize: 14, color: 'rgba(0,0,0,0.65)', flexShrink: 0 }}>{item.name}</div>
|
||||
<div style={{ flex: 1, backgroundColor: '#F5F5F5', borderRadius: 4, height: 24, overflow: 'hidden' }}>
|
||||
<div style={{ width: `${(item.value / maxStatusValue) * 100}%`, height: '100%', backgroundColor: item.color, borderRadius: '0 4px 4px 0', transition: 'width 0.3s ease', minWidth: item.value > 0 ? 2 : 0 }} />
|
||||
</div>
|
||||
<div style={{ width: 40, fontSize: 14, color: 'rgba(0,0,0,0.85)' }}>{item.value}</div>
|
||||
</div>
|
||||
<div style={{ width: 40, fontSize: 14, color: 'rgba(0,0,0,0.85)' }}>{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 待处理任务 */}
|
||||
<div className="bg-white p-6 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 className="text-lg font-semibold mb-6">待处理任务</h3>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{taskGroups.map((group, gi) => (
|
||||
<div key={gi}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-base font-medium">{group.title}</h4>
|
||||
<span className="px-2 py-1 rounded text-xs" style={{ backgroundColor: '#F0F2F5', color: 'rgba(0,0,0,0.65)' }}>{group.count}</span>
|
||||
</div>
|
||||
{group.tasks.map((task, ti) => (
|
||||
<div key={ti} className="flex items-start justify-between py-3" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm mb-1">{task.deviceSN}</div>
|
||||
<div className="text-sm" style={{ color: 'rgba(0,0,0,0.45)' }}>{task.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{task.time && <span className="text-xs" style={{ color: 'rgba(0,0,0,0.45)' }}>{task.time}</span>}
|
||||
<Link href={task.link} className="text-sm" style={{ color: '#4a7c59' }}>处理</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{group.tasks.length < group.count && (
|
||||
<Link href={group.link} className="block w-full mt-3 text-center text-sm" style={{ color: '#4a7c59' }}>查看全部 {group.count} 项</Link>
|
||||
)}
|
||||
{/* 维修工单 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-base font-medium">维修工单</h4>
|
||||
<span className="px-2 py-1 rounded text-xs" style={{ backgroundColor: '#F0F2F5', color: 'rgba(0,0,0,0.65)' }}>{data.repair.pending + data.repair.processing}</span>
|
||||
</div>
|
||||
))}
|
||||
{data.recentRepairs.length === 0 ? (
|
||||
<div className="text-center py-6" style={{ color: 'rgba(0,0,0,0.25)', fontSize: 13 }}>暂无待处理工单</div>
|
||||
) : (
|
||||
<>
|
||||
{data.recentRepairs.map(r => (
|
||||
<div key={r.id} className="flex items-start justify-between py-3" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm mb-1">{r.sn}</div>
|
||||
<div className="text-sm" style={{ color: 'rgba(0,0,0,0.45)' }}>{r.fault_type} · {r.status}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs" style={{ color: 'rgba(0,0,0,0.45)' }}>{r.create_date}</span>
|
||||
<Link href="/repair" className="text-sm" style={{ color: '#4a7c59' }}>处理</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Link href="/repair" className="block w-full mt-3 text-center text-sm" style={{ color: '#4a7c59' }}>查看全部</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 报废审批 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-base font-medium">报废审批</h4>
|
||||
<span className="px-2 py-1 rounded text-xs" style={{ backgroundColor: '#F0F2F5', color: 'rgba(0,0,0,0.65)' }}>{data.scrap.pendingApproval}</span>
|
||||
</div>
|
||||
{data.recentScraps.length === 0 ? (
|
||||
<div className="text-center py-6" style={{ color: 'rgba(0,0,0,0.25)', fontSize: 13 }}>暂无待审批记录</div>
|
||||
) : (
|
||||
<>
|
||||
{data.recentScraps.map(s => (
|
||||
<div key={s.id} className="flex items-start justify-between py-3" style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm mb-1">{s.sn}</div>
|
||||
<div className="text-sm" style={{ color: 'rgba(0,0,0,0.45)' }}>{s.model} · {s.status}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs" style={{ color: 'rgba(0,0,0,0.45)' }}>{s.date}</span>
|
||||
<Link href="/scrap" className="text-sm" style={{ color: '#4a7c59' }}>查看</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Link href="/scrap" className="block w-full mt-3 text-center text-sm" style={{ color: '#4a7c59' }}>查看全部</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Upload, Download, Trash2, Edit, CheckCircle, Camera, X, AlertTriangle, Info, ScanLine, QrCode } from 'lucide-react'
|
||||
|
||||
|
|
@ -34,15 +34,6 @@ const defaultBOM = [
|
|||
{ 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 },
|
||||
|
|
@ -100,6 +91,24 @@ export default function RegistrationPage() {
|
|||
setBomList(prev => prev.map(b => b.id === id ? { ...b, [field]: value } : b))
|
||||
}
|
||||
|
||||
/** 根据SN查询物料信息,自动填充版本和校准状态 */
|
||||
const handleSnChange = useCallback(async (id: number, sn: string) => {
|
||||
setBomList(prev => prev.map(b => b.id === id ? { ...b, sn } : b))
|
||||
if (!sn || sn === '-') return
|
||||
try {
|
||||
const res = await fetch('/api/materials')
|
||||
const materials = await res.json()
|
||||
const matched = materials.find((m: { sn: string }) => m.sn === sn)
|
||||
if (matched) {
|
||||
setBomList(prev => prev.map(b => b.id === id ? {
|
||||
...b, sn,
|
||||
version: matched.version || b.version,
|
||||
calibration: matched.calib_status === '合格' ? '已校准' : matched.calib_status === '待校准' ? '待校准' : '无需校准',
|
||||
} : b))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, padding: 24, paddingBottom: 80 }}>
|
||||
|
|
@ -226,7 +235,7 @@ export default function RegistrationPage() {
|
|||
<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' }} />
|
||||
<input value={item.sn} onChange={e => handleSnChange(item.id, e.target.value)} onBlur={() => handleSnChange(item.id, item.sn)} 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>
|
||||
|
|
@ -235,13 +244,7 @@ export default function RegistrationPage() {
|
|||
</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>
|
||||
)}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -175,5 +175,81 @@ function initTables(db: Database.Database) {
|
|||
value INTEGER DEFAULT 0,
|
||||
materials TEXT DEFAULT '[]'
|
||||
);
|
||||
|
||||
-- 物料分类类型
|
||||
CREATE TABLE IF NOT EXISTS material_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
has_firmware INTEGER NOT NULL DEFAULT 0,
|
||||
has_calibration INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT '启用'
|
||||
);
|
||||
|
||||
-- 应用列表
|
||||
CREATE TABLE IF NOT EXISTS applications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
package_name TEXT DEFAULT '',
|
||||
description TEXT DEFAULT '',
|
||||
logo_url TEXT DEFAULT '',
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- 应用平台
|
||||
CREATE TABLE IF NOT EXISTS app_platforms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
app_id INTEGER NOT NULL,
|
||||
platform_type INTEGER NOT NULL DEFAULT 2,
|
||||
description TEXT DEFAULT '',
|
||||
extend_info TEXT DEFAULT '',
|
||||
create_time TEXT NOT NULL,
|
||||
FOREIGN KEY (app_id) REFERENCES applications(id)
|
||||
);
|
||||
|
||||
-- 平台版本
|
||||
CREATE TABLE IF NOT EXISTS app_platform_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
app_id INTEGER NOT NULL,
|
||||
platform_id INTEGER NOT NULL,
|
||||
major_version INTEGER NOT NULL DEFAULT 0,
|
||||
minor_version INTEGER NOT NULL DEFAULT 0,
|
||||
patch_version INTEGER NOT NULL DEFAULT 0,
|
||||
version_name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
file_type TEXT DEFAULT '',
|
||||
file_url TEXT DEFAULT '',
|
||||
file_size INTEGER DEFAULT 0,
|
||||
distribution_type TEXT DEFAULT '',
|
||||
primary_url TEXT DEFAULT '',
|
||||
fallback_url TEXT DEFAULT '',
|
||||
url_expire_time TEXT DEFAULT '',
|
||||
signature_info TEXT DEFAULT '',
|
||||
min_support_version TEXT DEFAULT '',
|
||||
os_min_version TEXT DEFAULT '',
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
is_force_update INTEGER NOT NULL DEFAULT 0,
|
||||
release_date TEXT NOT NULL,
|
||||
changelog TEXT DEFAULT '[]',
|
||||
FOREIGN KEY (app_id) REFERENCES applications(id),
|
||||
FOREIGN KEY (platform_id) REFERENCES app_platforms(id)
|
||||
);
|
||||
|
||||
-- 应用统计
|
||||
CREATE TABLE IF NOT EXISTS app_statistics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
app_id INTEGER NOT NULL,
|
||||
version_id INTEGER DEFAULT NULL,
|
||||
date TEXT NOT NULL,
|
||||
download_count INTEGER DEFAULT 0,
|
||||
install_count INTEGER DEFAULT 0,
|
||||
active_count INTEGER DEFAULT 0,
|
||||
crash_count INTEGER DEFAULT 0,
|
||||
create_time TEXT NOT NULL,
|
||||
FOREIGN KEY (app_id) REFERENCES applications(id)
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
|
|
|||
199
src/lib/seed.ts
199
src/lib/seed.ts
|
|
@ -1,201 +1,6 @@
|
|||
import { getDb } from './db'
|
||||
|
||||
export function seedIfEmpty() {
|
||||
const db = getDb()
|
||||
const count = db.prepare('SELECT COUNT(*) as c FROM device_models').get() as { c: number }
|
||||
if (count.c > 0) return // already seeded
|
||||
|
||||
// Device Models
|
||||
const insertModel = db.prepare('INSERT INTO device_models (name, code, status, description, create_date) VALUES (?, ?, ?, ?, ?)')
|
||||
const models = [
|
||||
['GD-30 Supreme', 'GD30', '在产', '高端高密度电法仪', '2023-06-01'],
|
||||
['GD-20 Supreme', 'GD20', '在产', '中端高密度电法仪', '2023-08-15'],
|
||||
['GD-10 Supreme', 'GD10', '停产', '入门级高密度电法仪', '2022-03-10'],
|
||||
['GM-10', 'GM10', '在产', '大地电磁仪', '2025-01-10'],
|
||||
['GT-10', 'GT10', '在产', '瞬变电磁仪', '2025-02-15'],
|
||||
['GP-10', 'GP10', '在产', '磁力仪', '2025-03-01'],
|
||||
]
|
||||
for (const m of models) insertModel.run(...m)
|
||||
|
||||
// Checklist Templates
|
||||
const insertCL = db.prepare('INSERT INTO checklist_templates (model_code, name, required, sort_order) VALUES (?, ?, ?, ?)')
|
||||
const clGD30 = ['主协板安装检查','采集板安装检查','发射板安装检查','升压板安装检查','线缆连接检查','整机通电测试','通信功能测试','采集通道校准','外观检查','包装检查']
|
||||
clGD30.forEach((n, i) => insertCL.run('GD30', n, i < 8 ? 1 : 0, i))
|
||||
const clGD20 = ['主协板安装检查','采集板安装检查','发射板安装检查','线缆连接检查','整机通电测试','通信功能测试','采集通道校准','外观检查']
|
||||
clGD20.forEach((n, i) => insertCL.run('GD20', n, i < 7 ? 1 : 0, i))
|
||||
const clGD10 = ['主协板安装检查','采集板安装检查','线缆连接检查','整机通电测试','通信功能测试','外观检查']
|
||||
clGD10.forEach((n, i) => insertCL.run('GD10', n, i < 5 ? 1 : 0, i))
|
||||
const clGM10 = ['主协板安装检查','采集板安装检查','电磁传感器安装检查','线缆连接检查','整机通电测试','通信功能测试','采集通道校准','外观检查']
|
||||
clGM10.forEach((n, i) => insertCL.run('GM10', n, i < 7 ? 1 : 0, i))
|
||||
const clGT10 = ['主协板安装检查','采集板安装检查','发射板安装检查','瞬变电磁传感器检查','线缆连接检查','整机通电测试','通信功能测试','外观检查']
|
||||
clGT10.forEach((n, i) => insertCL.run('GT10', n, i < 7 ? 1 : 0, i))
|
||||
const clGP10 = ['主协板安装检查','磁力传感器安装检查','线缆连接检查','整机通电测试','通信功能测试','磁力校准','外观检查']
|
||||
clGP10.forEach((n, i) => insertCL.run('GP10', n, i < 6 ? 1 : 0, i))
|
||||
|
||||
// Board Types
|
||||
const insertBT = db.prepare('INSERT INTO board_types (name, category, device_models, description, status) VALUES (?, ?, ?, ?, ?)')
|
||||
const boardTypes = [
|
||||
['GD30 主协板', '主协板', '["GD-30 Supreme"]', 'GD-30 Supreme 专用主协板', '启用'],
|
||||
['GD30 采集板', '采集板', '["GD-30 Supreme"]', 'GD-30 Supreme 专用采集板,6通道', '启用'],
|
||||
['GD20 采集板', '采集板', '["GD-20 Supreme"]', 'GD-20 Supreme 专用采集板,5通道', '启用'],
|
||||
['GD30 发射板', '发射板', '["GD-30 Supreme"]', 'GD-30 Supreme 专用发射板', '启用'],
|
||||
['GD30 升压板', '升压板', '["GD-30 Supreme"]', 'GD-30 Supreme 专用升压板', '启用'],
|
||||
['GM10 采集板', '采集板', '["GM-10"]', '大地电磁仪 GM-10 专用采集板', '启用'],
|
||||
['GM10 主协板', '主协板', '["GM-10"]', '大地电磁仪 GM-10 专用主协板', '启用'],
|
||||
['GT10 采集板', '采集板', '["GT-10"]', '瞬变电磁仪 GT-10 专用采集板', '启用'],
|
||||
['GT10 发射板', '发射板', '["GT-10"]', '瞬变电磁仪 GT-10 专用发射板', '启用'],
|
||||
['GP10 主协板', '主协板', '["GP-10"]', '磁力仪 GP-10 专用主协板', '启用'],
|
||||
['通用电缆头 SR10/SR20', '电缆头', '["GD-30 Supreme","GD-20 Supreme","GD-10 Supreme"]', 'SR10/SR20 电缆头,适配 GD 系列', '启用'],
|
||||
['通用电缆头 CS60', '电缆头', '["GD-30 Supreme","GD-20 Supreme","GD-10 Supreme"]', 'CS60 电缆头,适配 GD 系列', '启用'],
|
||||
['通用电缆头 SR60/SR60PLUS', '电缆头', '["GD-30 Supreme","GD-20 Supreme","GD-10 Supreme"]', 'SR60/SR60PLUS 电缆头,适配 GD 系列', '启用'],
|
||||
['通用电缆', '电缆', '["GD-30 Supreme","GD-20 Supreme","GM-10","GT-10"]', '通用电缆', '启用'],
|
||||
['通用机箱', '机箱', '["GD-30 Supreme","GD-20 Supreme","GD-10 Supreme"]', '通用机箱外壳', '启用'],
|
||||
['BP150 电源', '电源', '["GD-10 Supreme"]', 'BP150 便携电源模块,适配入门级设备', '启用'],
|
||||
['BP300 电源', '电源', '["GD-20 Supreme","GT-10"]', 'BP300 中功率电源模块', '启用'],
|
||||
['BP600 电源', '电源', '["GD-30 Supreme","GM-10","GT-10","GP-10"]', 'BP600 大功率电源模块', '启用'],
|
||||
]
|
||||
for (const bt of boardTypes) insertBT.run(...bt)
|
||||
|
||||
// Board Versions
|
||||
const insertBV = db.prepare('INSERT INTO board_versions (type, version, status) VALUES (?, ?, ?)')
|
||||
const bvs = [
|
||||
['主协板', 'MB-V1.8', '在产'], ['采集板', 'RX-V2.3', '在产'], ['发射板', 'TX-V2.1', '停产'],
|
||||
['升压板', 'BP600-V1.2', '停产'], ['电缆头', 'SR10', '在产'], ['电缆头', 'SR20', '在产'], ['电缆头', 'CS60', '在产'], ['电缆头', 'SR60', '在产'], ['电缆头', 'SR60PLUS', '在产'], ['电缆', 'CBL-60M', '在产'],
|
||||
['机箱', 'GD30-CASE-B', '在产'], ['电源', 'BP150-V1.0', '在产'], ['电源', 'BP300-V1.0', '在产'], ['电源', 'BP600-V2.0', '在产'],
|
||||
]
|
||||
for (const bv of bvs) insertBV.run(...bv)
|
||||
|
||||
// Materials
|
||||
const insertMat = db.prepare('INSERT INTO materials (sn, name, category, type, device_model, version, description, firmware, status, device_sn, production_date, calib_status, calib_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const mats = [
|
||||
['MB25011500', 'GD30 主协板', '主协板', 'MCB-3000', 'GD-30 Supreme', 'MB-V2.1', 'GD-30 Supreme 专用主协板', 'v2.1', '在库', '-', '2025-01-15', '-', '-'],
|
||||
['MCB-3000-20250118002', 'GD30 主协板', '主协板', 'MCB-3000', 'GD-30 Supreme', 'MCB-3000', 'GD-30 Supreme 专用主协板', 'v2.1.0', '已装配', 'GD30-2025-000001', '2025-01-18', '-', '-'],
|
||||
['ACB-6000-20250110001', 'GD30 采集板', '采集板', 'ACB-6000', 'GD-30 Supreme', 'ACB-6000', 'GD-30 Supreme 专用采集板,6通道', 'v3.0.2', '已装配', 'GD30-2025-000001', '2025-01-10', '合格', '2025-01-12'],
|
||||
['ACB-6000-20250110002', 'GD30 采集板', '采集板', 'ACB-6000', 'GD-30 Supreme', 'ACB-6000', 'GD-30 Supreme 专用采集板,6通道', 'v3.0.2', '已装配', 'GD30-2025-000001', '2025-01-10', '合格', '2025-01-12'],
|
||||
['ACB-6000-20250112003', 'GD30 采集板', '采集板', 'ACB-6000', 'GD-30 Supreme', 'ACB-6000', 'GD-30 Supreme 专用采集板,6通道', 'v3.0.2', '在库', '-', '2025-01-12', '待校准', '-'],
|
||||
['ACB-5000-20241205001', 'GD20 采集板', '采集板', 'ACB-5000', 'GD-20 Supreme', 'ACB-5000', 'GD-20 Supreme 专用采集板,5通道', 'v2.5.1', '已装配', 'GD20-2024-000045', '2024-12-05', '合格', '2024-12-08'],
|
||||
['TXB-1000-20250120001', 'GD30 发射板', '发射板', 'TXB-1000', 'GD-30 Supreme', 'TXB-1000', 'GD-30 Supreme 专用发射板', 'v1.2.0', '已装配', 'GD30-2025-000001', '2025-01-20', '-', '-'],
|
||||
['TXB-1000-20250122002', 'GD30 发射板', '发射板', 'TXB-1000', 'GD-30 Supreme', 'TXB-1000', 'GD-30 Supreme 专用发射板', 'v1.2.0', '在库', '-', '2025-01-22', '-', '-'],
|
||||
['BST-500-20250201001', 'GD30 升压板', '升压板', 'BST-500', 'GD-30 Supreme', 'BST-500', 'GD-30 Supreme 专用升压板', '-', '已装配', 'GD30-2025-000002', '2025-02-01', '-', '-'],
|
||||
['BST-500-20250203002', 'GD30 升压板', '升压板', 'BST-500', 'GD-30 Supreme', 'BST-500', 'GD-30 Supreme 专用升压板', '-', '在库', '-', '2025-02-03', '-', '-'],
|
||||
['ACB-6000-20241120001', 'GD30 采集板', '采集板', 'ACB-6000', 'GD-30 Supreme', 'ACB-6000', 'GD-30 Supreme 专用采集板,6通道', 'v3.0.2', '故障', '-', '2024-11-20', '不合格', '2025-02-10'],
|
||||
['MCB-2000-20240915001', 'GD20 主协板', '主协板', 'MCB-2000', 'GD-20 Supreme', 'MCB-2000', 'GD-20 Supreme 专用主协板', 'v1.8.5', '已装配', 'GD20-2024-000046', '2024-09-15', '-', '-'],
|
||||
['ACB-6000-20250305001', 'GD30 采集板', '采集板', 'ACB-6000', 'GD-30 Supreme', 'ACB-6000', 'GD-30 Supreme 专用采集板,6通道', 'v3.0.2', '在库', '-', '2025-03-05', '待校准', '-'],
|
||||
['TXB-800-20240610001', 'GD20 发射板', '发射板', 'TXB-800', 'GD-20 Supreme', 'TXB-800', 'GD-20 Supreme 专用发射板', 'v1.0.3', '报废', '-', '2024-06-10', '-', '-'],
|
||||
['CBH-SR10-20250301001', '通用电缆头 SR10/SR20', '电缆头', 'SR10', 'GD-30 Supreme', 'SR10', 'SR10/SR20 电缆头,适配 GD 系列', '-', '在库', '-', '2025-03-01', '-', '-'],
|
||||
['CBL-010-20250305001', '通用电缆', '电缆', 'CBL-60M', 'GD-30 Supreme', 'CBL-60M', '通用电缆', '-', '已装配', 'GD30-2025-000001', '2025-03-05', '-', '-'],
|
||||
['CHS-GD30-20250310001', '通用机箱', '机箱', 'GD30-CASE-B', 'GD-30 Supreme', 'GD30-CASE-B', '通用机箱外壳', '-', '在库', '-', '2025-03-10', '-', '-'],
|
||||
['BP600-20250312001', 'BP600 电源', '电源', 'BP600-V2.0', 'GD-30 Supreme', 'BP600-V2.0', 'BP600 大功率电源模块', '-', '已装配', 'GD30-2025-000002', '2025-03-12', '-', '-'],
|
||||
['BP300-20250315001', 'BP300 电源', '电源', 'BP300-V1.0', 'GD-20 Supreme', 'BP300-V1.0', 'BP300 中功率电源模块', '-', '在库', '-', '2025-03-15', '-', '-'],
|
||||
['BP150-20250318001', 'BP150 电源', '电源', 'BP150-V1.0', 'GD-10 Supreme', 'BP150-V1.0', 'BP150 便携电源模块', '-', '在库', '-', '2025-03-18', '-', '-'],
|
||||
]
|
||||
for (const m of mats) insertMat.run(...m)
|
||||
|
||||
// Devices
|
||||
const insertDev = db.prepare('INSERT INTO devices (sn, model, type, status, firmware, production_date, customer, batch) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const devs = [
|
||||
['GD30-2025-000001', 'GD-30 Supreme', '高密度电法仪', '已激活', 'v2.3.5', '2025-01-15 14:30', '北京地质研究院', 'BATCH-2025-Q1-001'],
|
||||
['GD30-2025-000002', 'GD-30 Supreme', '高密度电法仪', '已激活', 'v2.3.5', '2025-01-18 09:15', '中国地质大学', 'BATCH-2025-Q1-001'],
|
||||
['GD30-2024-000056', 'GD-30 Supreme', '高密度电法仪', '已出厂', 'v2.3.4', '2024-12-20 16:00', '成都理工大学', 'BATCH-2024-Q4-003'],
|
||||
['GT20-2025-000045', 'GD-20', '二维电法仪', '已激活', 'v1.8.5', '2025-02-10 11:20', '武汉地质调查中心', 'BATCH-2025-Q1-002'],
|
||||
['GT20-2025-000046', 'GD-20', '二维电法仪', '装配中', 'v1.8.5', '2025-03-01 08:45', '-', 'BATCH-2025-Q1-002'],
|
||||
['GD30-2024-000078', 'GD-30 Supreme', '高密度电法仪', '已出厂', 'v2.3.4', '2024-11-05 13:30', '长安大学', 'BATCH-2024-Q4-002'],
|
||||
['GD10-2024-000033', 'GD-10 Supreme', '入门级电法仪', '已激活', 'v1.5.2', '2024-09-12 10:00', '河海大学', 'BATCH-2024-Q3-001'],
|
||||
['GD30-2024-000089', 'GD-30 Supreme', '高密度电法仪', '装配中', 'v2.3.5', '2025-03-05 15:10', '-', 'BATCH-2025-Q1-001'],
|
||||
['GT20-2025-000012', 'GD-20', '二维电法仪', '已激活', 'v1.8.5', '2025-01-22 09:30', '中南大学', 'BATCH-2025-Q1-002'],
|
||||
['GD30-2024-000102', 'GD-30 Supreme', '高密度电法仪', '已出厂', 'v2.3.4', '2024-10-18 14:00', '吉林大学', 'BATCH-2024-Q4-001'],
|
||||
['GD10-2024-000034', 'GD-10 Supreme', '入门级电法仪', '装配中', 'v1.5.2', '2025-03-08 11:45', '-', 'BATCH-2025-Q1-003'],
|
||||
['GD30-2024-000145', 'GD-30 Supreme', '高密度电法仪', '已激活', 'v2.3.5', '2024-08-25 16:20', '同济大学', 'BATCH-2024-Q3-002'],
|
||||
]
|
||||
for (const d of devs) insertDev.run(...d)
|
||||
|
||||
// Licenses
|
||||
const insertLic = db.prepare('INSERT INTO licenses (model, modules, expiry, status) VALUES (?, ?, ?, ?)')
|
||||
insertLic.run('GD-30', '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块, 水上, 跨孔, 电流场法', '2025-12-31', '生效')
|
||||
insertLic.run('GD-20', '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块, 三维自电/电阻率/激电测试模块', '2025-06-30', '生效')
|
||||
insertLic.run('GD-10', '一维自电/电阻率/激电测试模块, 二维自电/电阻率/激电测试模块', '2024-12-31', '生效')
|
||||
|
||||
// Repair Orders
|
||||
const insertRO = db.prepare('INSERT INTO repair_orders (id, sn, fault_type, status, priority, assignee, create_date, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const ros = [
|
||||
['WO-2024-0001', 'GD30-2024-000056', '板卡故障', '处理中', '高', '张工', '2024-03-15', '主协板通信异常,无法正常采集数据'],
|
||||
['WO-2024-0002', 'GD30-2024-000078', '固件异常', '待处理', '中', '李工', '2024-03-14', '固件升级后设备无法启动'],
|
||||
['WO-2024-0003', 'GT20-2025-000045', '通信故障', '已处理', '低', '王工', '2024-03-13', 'WiFi模块连接不稳定'],
|
||||
['WO-2024-0004', 'GD30-2024-000102', '电源故障', '处理中', '高', '赵工', '2024-03-12', '升压板输出电压不稳定'],
|
||||
['WO-2024-0005', 'GD10-2024-000033', '传感器故障', '待处理', '中', '张工', '2024-03-11', '温度传感器读数异常'],
|
||||
['WO-2024-0006', 'GD30-2024-000089', '板卡故障', '已处理', '低', '李工', '2024-03-10', '采集板通道3数据丢失'],
|
||||
['WO-2024-0007', 'GT20-2025-000012', '其他', '待处理', '高', '王工', '2024-03-09', '设备外壳损坏,需更换'],
|
||||
['WO-2024-0008', 'GD30-2024-000145', '固件异常', '处理中', '中', '赵工', '2024-03-08', '配置文件丢失,参数重置'],
|
||||
]
|
||||
for (const r of ros) insertRO.run(...r)
|
||||
|
||||
// Scrap Records
|
||||
const insertSR = db.prepare('INSERT INTO scrap_records (sn, model, reason, applicant, status, order_id, date, value, materials) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const srs = [
|
||||
['GD30-2023-001234', 'GD-30 Supreme', '主板损坏无法修复', '李工', '待审批', 'WO-2024-0001', '2024-03-10', 12000, '["采集板 AC20240308002","测控板 CT20240308003","升压板 BS20240308001"]'],
|
||||
['GD30-2023-001567', 'GD-30 Supreme', '多个核心部件损坏', '张工', '审批中', 'WO-2024-0003', '2024-03-08', 8500, '["采集板 AC20240215006","发射板 TX20240215003"]'],
|
||||
['GT20-2023-000890', 'GD-20 Supreme', '维修成本超过设备价值', '王工', '已审批', 'WO-2024-0005', '2024-02-28', 5200, '["主协板 MC20231205004"]'],
|
||||
['GD10-2023-000456', 'GD-10 Supreme', '设备老化严重', '赵工', '已驳回', 'WO-2024-0006', '2024-02-20', 3000, '[]'],
|
||||
['GD30-2023-002345', 'GD-30 Supreme', '主板损坏无法修复', '李工', '回收中', 'WO-2024-0008', '2024-02-15', 15000, '["采集板 AC20231110010","采集板 AC20231110011","升压板 BS20231110002"]'],
|
||||
['GT20-2023-000123', 'GD-20 Supreme', '多个核心部件损坏', '张工', '已回收', 'WO-2024-0010', '2024-01-25', 6800, '["采集板 AC20230801007","发射板 TX20230801002"]'],
|
||||
['GD30-2023-003456', 'GD-30 Supreme', '维修成本超过设备价值', '王工', '已回收', 'WO-2024-0012', '2024-01-10', 9200, '["主协板 MC20230610003","采集板 AC20230610008"]'],
|
||||
]
|
||||
for (const s of srs) insertSR.run(...s)
|
||||
|
||||
// Config Files
|
||||
const insertCF = db.prepare('INSERT INTO config_files (name, model, version, create_time, status, voltage, current, duty_cycle, pulse_width, channels, sample_rate, voltage_range, waveform, ssid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const cfs = [
|
||||
['CFG-GD30-v1.2.0', 'GD-30 Supreme', 'v1.2.0', '2025-01-15 10:30', '生效', '1500V', '10A', '0+0-、+0-0、+-', '0.25s~64s', 12, '50Hz/60Hz/100Hz/1000Hz', '±2.5V/±80V', '支持', 'GD30'],
|
||||
['CFG-GD30-v1.1.0', 'GD-30 Supreme', 'v1.1.0', '2024-09-20 14:00', '已停用', '1200V', '10A', '0+0-、+0-0', '0.25s~32s', 12, '50Hz/60Hz/100Hz', '±2.5V/±80V', '支持', 'GD30'],
|
||||
['CFG-GD20-v1.0.0', 'GD-20 Supreme', 'v1.0.0', '2024-08-10 09:15', '生效', '1000V', '8A', '0+0-、+0-0', '0.25s~8s', 6, '50Hz/60Hz', '±2.5V/±80V', '不支持', 'GD20'],
|
||||
['CFG-GD10-v1.0.0', 'GD-10 Supreme', 'v1.0.0', '2024-06-05 16:45', '生效', '800V', '5A', '0+0-', '0.5s~8s', 1, '50Hz/60Hz', '±2.5V', '不支持', 'GD10'],
|
||||
['CFG-GD20-v1.1.0', 'GD-20 Supreme', 'v1.1.0', '2025-02-28 11:20', '生效', '1000V', '8A', '0+0-、+0-0、+-', '0.25s~16s', 6, '50Hz/60Hz/100Hz', '±2.5V/±80V', '支持', 'GD20'],
|
||||
['CFG-GD30-v1.3.0', 'GD-30 Supreme', 'v1.3.0', '2025-03-15 08:00', '生效', '1500V', '10A', '0+0-、+0-0、+-', '0.25s~64s', 12, '50Hz/60Hz/100Hz/1000Hz', '±2.5V/±80V', '支持', 'GD30'],
|
||||
]
|
||||
for (const c of cfs) insertCF.run(...c)
|
||||
|
||||
// BOM Templates
|
||||
const insertBOM = db.prepare('INSERT INTO bom_templates (model_code, name, material_name, model, versions, qty, required, need_calibration, enforce_version_match) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const boms: [string, string, string, string, string, number, number, number, number][] = [
|
||||
['GD30', '主协板', 'GD30 主协板', 'MCB-3000', '["MB-V2.1","MB-V1.8"]', 1, 1, 0, 0],
|
||||
['GD30', '采集板', 'GD30 采集板', 'ACB-6000', '["RX-V2.3","RX-V1.3"]', 2, 1, 1, 1],
|
||||
['GD30', '发射板', 'GD30 发射板', 'TXB-1000', '["TX-V2.1","TX-V1.5"]', 1, 1, 0, 0],
|
||||
['GD30', '升压板', 'GD30 升压板', 'BST-500', '["BP600-V1.2"]', 1, 1, 0, 0],
|
||||
['GD30', '机箱', '通用机箱', 'GD30-CASE-A', '["GD30-CASE-A","GD30-CASE-B"]', 1, 1, 0, 0],
|
||||
['GD30', '电缆', '通用电缆', 'CBL-GD30', '["CBL-60M","CBL-100M"]', 1, 1, 0, 0],
|
||||
['GD30', '电缆头', '通用电缆头 SR10/SR20', 'CBH-GD30', '["SR10","SR20","CS60","SR60","SR60PLUS"]', 2, 1, 0, 0],
|
||||
['GD30', '电源', 'BP600 电源', 'BP600', '["BP600-V2.0","BP600-V1.0"]', 1, 1, 0, 0],
|
||||
['GD20', '主协板', 'GD20 主协板', 'MCB-2000', '["MB-V1.8","MB-V1.2"]', 1, 1, 0, 0],
|
||||
['GD20', '采集板', 'GD20 采集板', 'ACB-5000', '["RX-V2.1","RX-V1.3"]', 1, 1, 1, 0],
|
||||
['GD20', '发射板', 'GD20 发射板', 'TXB-800', '["TX-V1.5"]', 1, 1, 0, 0],
|
||||
['GD20', '机箱', '通用机箱', 'GD20-CASE-A', '["-"]', 1, 1, 0, 0],
|
||||
['GD20', '电源', 'BP300 电源', 'BP300', '["BP300-V1.0"]', 1, 1, 0, 0],
|
||||
['GD10', '主协板', 'GD10 主协板', 'MCB-1000', '["MB-V1.2"]', 1, 1, 0, 0],
|
||||
['GD10', '采集板', 'GD10 采集板', 'ACB-3000', '["RX-V1.3"]', 1, 1, 1, 0],
|
||||
['GD10', '机箱', '通用机箱', 'GD10-CASE-A', '["-"]', 1, 1, 0, 0],
|
||||
['GD10', '电源', 'BP150 电源', 'BP150', '["BP150-V1.0"]', 1, 0, 0, 0],
|
||||
['GM10', '主协板', 'GM10 主协板', 'MCB-GM10', '["MB-V2.1"]', 1, 1, 0, 0],
|
||||
['GM10', '采集板', 'GM10 采集板', 'ACB-GM10', '["RX-V2.3"]', 2, 1, 1, 1],
|
||||
['GM10', '机箱', '通用机箱', 'GM10-CASE-A', '["GM10-CASE-A"]', 1, 1, 0, 0],
|
||||
['GM10', '电缆', '通用电缆', 'CBL-GM10', '["CBL-30M"]', 1, 1, 0, 0],
|
||||
['GM10', '电源', 'BP600 电源', 'BP600', '["BP600-V2.0"]', 1, 1, 0, 0],
|
||||
['GT10', '主协板', 'GT10 主协板', 'MCB-GT10', '["MB-V2.1"]', 1, 1, 0, 0],
|
||||
['GT10', '采集板', 'GT10 采集板', 'ACB-GT10', '["RX-V2.3"]', 1, 1, 1, 0],
|
||||
['GT10', '发射板', 'GT10 发射板', 'TXB-GT10', '["TX-V2.1"]', 1, 1, 0, 0],
|
||||
['GT10', '机箱', '通用机箱', 'GT10-CASE-A', '["GT10-CASE-A"]', 1, 1, 0, 0],
|
||||
['GT10', '电源', 'BP600 电源', 'BP600', '["BP600-V2.0"]', 1, 1, 0, 0],
|
||||
['GP10', '主协板', 'GP10 主协板', 'MCB-GP10', '["MB-V2.1"]', 1, 1, 0, 0],
|
||||
['GP10', '机箱', '通用机箱', 'GP10-CASE-A', '["GP10-CASE-A"]', 1, 1, 0, 0],
|
||||
['GP10', '电源', 'BP600 电源', 'BP600', '["BP600-V2.0"]', 1, 0, 0, 0],
|
||||
]
|
||||
for (const b of boms) insertBOM.run(...b)
|
||||
|
||||
// Firmware
|
||||
const insertFW = db.prepare('INSERT INTO firmware (version, board_version, type, date, status, size, downloads, hw_range, upgrade_type, signed, md5, sha256, notes, model_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
const fws = [
|
||||
['v2.1.0', 'MB-V1.8', '主协板', '2024-03-10', '已发布', '12.5MB', 1234, 'MB25130025 Rev.A~C', '可选', 1, 'a1b2c3d4e5f6...', '9f8e7d6c5b4a...', '["修复通信协议兼容性问题","优化低功耗模式切换","新增看门狗超时配置"]', ''],
|
||||
['v3.0.2', 'RX-V2.3', '采集板', '2024-02-20', '已发布', '8.7MB', 890, 'ACB-6000 Rev.A~B', '可选', 1, 'd4e5f6a1b2c3...', '6c5b4a9f8e7d...', '["提升采样精度","修复通道串扰问题","新增自校准功能"]', ''],
|
||||
['v1.2.0', 'TX-V2.1', '发射板', '2024-02-28', '已发布', '6.3MB', 456, 'TXB-1000 Rev.A', '强制', 1, 'f6a1b2c3d4e5...', '4a9f8e7d6c5b...', '["新增过流保护","优化PWM控制算法"]', ''],
|
||||
]
|
||||
for (const f of fws) insertFW.run(...f)
|
||||
// 所有数据通过页面手动录入,不再自动填充种子数据
|
||||
getDb()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue