1、更新了APP版本管理页面

2、删除mock数据,用api接口导入
This commit is contained in:
徐星 2026-04-27 18:07:00 +08:00
parent fe5a5472bb
commit df2355e007
29 changed files with 2034 additions and 667 deletions

View File

@ -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"}

View File

@ -4,8 +4,8 @@
"dynamicRoutes": {},
"notFoundRoutes": [],
"preview": {
"previewModeId": "c55ffe53b7d1e0d2210bb27bdcda1a52",
"previewModeSigningKey": "29086773d8e35b0885c5db516b53db2363bab0a6a4585596e30d4b3fc76aec56",
"previewModeEncryptionKey": "0bac8c20a0a44a84b1dbe2603c546b8176144734ca42089bab1ebc967f277a1a"
"previewModeId": "2947f9339b9bc375ef2ec0325fd70e0c",
"previewModeSigningKey": "5fbfa8d7aaae4f6432fbd46cb9d1d06b81f02018d11093ad9f7c9c31305961d2",
"previewModeEncryptionKey": "ef8ca519881bb07275db39a1c0b131c870bd7a64c5097b76aef811ad5130b44e"
}
}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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": {}

View File

@ -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

View File

@ -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 | 启用 | 应用/版本已启用 |

View File

@ -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 })
}

View File

@ -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 })
}

View File

@ -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 })
}

View File

@ -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 })
}

View File

@ -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 })
}

View File

@ -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 })
}

View File

@ -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功能&#10;修复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功能&#10;修复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>
)
}

View File

@ -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 },

View File

@ -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>

View File

@ -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问题&#10;新增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>

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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 }}>

View File

@ -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 }}>

View File

@ -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 }}
>

View File

@ -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 }}>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
);
`)
}

View File

@ -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()
}