feat: 为checklist模板添加检查标准字段并修复相关bug

- 数据库:为checklist_templates表添加standard字段,支持检查标准存储
- 后端API:更新POST /api/models/checklist接口,支持保存standard字段
- 前端页面:在型号管理页面添加检查标准列和编辑功能
- 类型定义:更新CLItem接口,添加可选的standard字段
- 数据转换:修复checklistTemplates数据结构,将扁平数组转换为分组对象
- 防御性编程:添加Array.isArray()类型检查,避免运行时错误
- 物料分类:修复DELETE接口参数传递方式,从body改为URL查询参数
This commit is contained in:
徐星 2026-05-11 09:21:41 +08:00
parent 9365231046
commit db871f2ebd
8 changed files with 3812 additions and 149 deletions

89
.gitignore vendored
View File

@ -1,6 +1,89 @@
node_modules # Dependencies
.next node_modules/
package-lock.json
# Next.js build output
.next/
out/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Database files
*.db
*.sqlite
*.sqlite3
data/ data/
/src/generated/prisma # Python virtual environment
venv/
python_backend/venv/
# Python cache files
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# TypeScript build info
tsconfig.tsbuildinfo
next-env.d.ts
# Prisma generated files
/src/generated/prisma/
prisma/migrations/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Testing
coverage/
.nyc_output/
# Temporary files
*.tmp
*.temp
.cache/
# Upload files (if contains sensitive data)
python_backend/uploads/
# Claude and Kiro AI assistant files
.claude/
.kiro/
# Null file (Windows artifact)
$null

View File

@ -10,7 +10,22 @@ from contextlib import contextmanager
from datetime import datetime from datetime import datetime
# 数据库文件路径(放在当前目录下,轻量本地运行) # 数据库文件路径(放在当前目录下,轻量本地运行)
DB_PATH = os.path.join(os.path.dirname(__file__), "device_platform.db") DB_PATH = os.path.join(os.path.dirname(__file__), "app.db")
# 上传文件根目录(与 uploads 子目录共用)
UPLOAD_DIR = os.path.join(os.path.dirname(__file__), "uploads")
# 固件公共目录Next.js public 下,已有实际固件文件)
FIRMWARE_PUBLIC_DIR = os.path.join(os.path.dirname(__file__), "..", "public", "uploads", "GD", "firmware")
FIRMWARE_PUBLIC_DIR = os.path.normpath(FIRMWARE_PUBLIC_DIR)
# 固件类型到子目录映射
FIRMWARE_TYPE_FOLDER = {
"采集板": "CJB",
"发射板": "FSB",
"主协板": "XCL",
"主机服务": "",
}
def get_db_connection() -> sqlite3.Connection: def get_db_connection() -> sqlite3.Connection:
@ -20,6 +35,7 @@ def get_db_connection() -> sqlite3.Connection:
""" """
conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn return conn
@ -40,41 +56,843 @@ def get_db():
def init_db(): def init_db():
""" """
初始化数据库创建设备表如果不存在 初始化数据库创建所有表如果不存在
设备表字段
- sn: 设备序列号主键唯一标识一台设备
- status: 设备状态待激活 / 已激活 / 已禁用
- activated_at: 激活时间为空表示尚未激活
- created_at: 记录创建时间
""" """
# 确保上传目录存在
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(os.path.join(UPLOAD_DIR, "calibration"), exist_ok=True)
os.makedirs(os.path.join(UPLOAD_DIR, "apps"), exist_ok=True)
os.makedirs(os.path.join(UPLOAD_DIR, "firmware"), exist_ok=True)
with get_db() as db: with get_db() as db:
# 1. 设备表(统一前后端结构)
db.execute(""" db.execute("""
CREATE TABLE IF NOT EXISTS devices ( CREATE TABLE IF NOT EXISTS devices (
sn TEXT PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT,
sn TEXT NOT NULL UNIQUE,
model TEXT,
type TEXT,
status TEXT NOT NULL DEFAULT '待激活', status TEXT NOT NULL DEFAULT '待激活',
activated_at TEXT, firmware TEXT DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) production_date INTEGER,
customer TEXT DEFAULT '-',
batch TEXT DEFAULT '',
activated_at INTEGER,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
) )
""") """)
# 2. 校准文件表
db.execute("""
CREATE TABLE IF NOT EXISTS calibration_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
material_sn TEXT NOT NULL,
sn TEXT,
uid TEXT,
file_name TEXT,
file_path TEXT,
file_size INTEGER,
md5 TEXT,
result TEXT,
operator TEXT,
remark TEXT,
upload_time INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
channels_count INTEGER DEFAULT 0
)
""")
# 3. 校准通道数据表
db.execute("""
CREATE TABLE IF NOT EXISTS calibration_channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
calibration_id INTEGER NOT NULL,
channel_id INTEGER,
factor_80v REAL,
offset_80v REAL,
factor_2_5v REAL,
offset_2_5v REAL,
FOREIGN KEY (calibration_id) REFERENCES calibration_files(id) ON DELETE CASCADE
)
""")
# 4. APP 版本表
db.execute("""
CREATE TABLE IF NOT EXISTS app_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app_name TEXT NOT NULL,
package_name TEXT,
platform_type INTEGER DEFAULT 2,
version_name TEXT NOT NULL,
major_version INTEGER DEFAULT 0,
minor_version INTEGER DEFAULT 0,
patch_version INTEGER DEFAULT 0,
file_name TEXT,
file_path TEXT,
file_size INTEGER DEFAULT 0,
file_type TEXT,
distribution_type TEXT DEFAULT 'direct',
primary_url TEXT,
os_min_version TEXT,
is_force_update INTEGER DEFAULT 0,
changelog TEXT,
status INTEGER DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 5. 配置文件表
db.execute("""
CREATE TABLE IF NOT EXISTS config_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
model TEXT NOT NULL,
version TEXT NOT NULL DEFAULT 'v1.0',
status TEXT NOT NULL DEFAULT '生效',
max_tx_voltage TEXT,
max_tx_current TEXT,
tx_waveform TEXT,
tx_pulse_width TEXT,
acq_channels TEXT,
acq_sample_rate TEXT,
acq_voltage_range TEXT,
full_waveform_capture TEXT,
ssid_prefix TEXT,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 6. 固件版本表
db.execute("""
CREATE TABLE IF NOT EXISTS firmware_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL,
firmware_type TEXT NOT NULL,
board_model TEXT,
device_model TEXT,
file_name TEXT,
file_path TEXT,
file_size INTEGER DEFAULT 0,
hw_range TEXT,
upgrade_type TEXT DEFAULT '可选',
signed INTEGER DEFAULT 0,
notes TEXT,
status TEXT DEFAULT '已发布',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 7. 设备型号表
db.execute("""
CREATE TABLE IF NOT EXISTS device_models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
code TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT '在产',
description TEXT DEFAULT '',
create_date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 8. 装配Checklist模板表
db.execute("""
CREATE TABLE IF NOT EXISTS checklist_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_code TEXT NOT NULL,
name TEXT NOT NULL,
required INTEGER NOT NULL DEFAULT 1,
standard TEXT,
sort_order INTEGER NOT NULL DEFAULT 0
)
""")
# 9. 板卡类型表
db.execute("""
CREATE TABLE IF NOT EXISTS board_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT NOT NULL,
device_models TEXT NOT NULL DEFAULT '[]',
description TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT '启用'
)
""")
# 10. 板卡版本表
db.execute("""
CREATE TABLE IF NOT EXISTS board_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
version TEXT NOT NULL,
status TEXT NOT NULL DEFAULT '在产'
)
""")
# 11. 物料实例表
db.execute("""
CREATE TABLE IF NOT EXISTS materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sn TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL,
device_model TEXT NOT NULL DEFAULT '',
version TEXT NOT NULL,
description TEXT DEFAULT '',
firmware TEXT DEFAULT '-',
status TEXT NOT NULL DEFAULT '在库',
device_sn TEXT DEFAULT '-',
production_date INTEGER,
calib_status TEXT DEFAULT '-',
calib_date INTEGER DEFAULT 0
)
""")
# 12. BOM模板表
db.execute("""
CREATE TABLE IF NOT EXISTS bom_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_code TEXT NOT NULL,
name TEXT NOT NULL,
material_name TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL,
versions TEXT NOT NULL DEFAULT '[]',
qty INTEGER NOT NULL DEFAULT 1,
required INTEGER NOT NULL DEFAULT 1,
need_calibration INTEGER NOT NULL DEFAULT 0,
enforce_version_match INTEGER NOT NULL DEFAULT 0
)
""")
# 13. 授权表
db.execute("""
CREATE TABLE IF NOT EXISTS licenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model TEXT NOT NULL,
modules TEXT NOT NULL,
expiry TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT '生效',
config_id INTEGER DEFAULT NULL,
license_file TEXT DEFAULT '',
device_sn TEXT DEFAULT '',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (config_id) REFERENCES config_files(id) ON DELETE SET NULL
)
""")
# 14. 维修工单表
db.execute("""
CREATE TABLE IF NOT EXISTS repair_orders (
id TEXT PRIMARY KEY,
sn TEXT NOT NULL,
fault_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT '待处理',
priority TEXT NOT NULL DEFAULT '',
assignee TEXT DEFAULT '',
create_date INTEGER NOT NULL,
description TEXT DEFAULT ''
)
""")
# 15. 报废记录表
db.execute("""
CREATE TABLE IF NOT EXISTS scrap_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sn TEXT NOT NULL,
model TEXT NOT NULL,
reason TEXT NOT NULL,
applicant TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT '待审批',
order_id TEXT DEFAULT '',
date INTEGER NOT NULL,
value INTEGER DEFAULT 0,
materials TEXT DEFAULT '[]'
)
""")
# 16. 物料分类表
db.execute("""
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 '启用'
)
""")
# 17. 设备装机BOM记录表
db.execute("""
CREATE TABLE IF NOT EXISTS device_bom_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_sn TEXT NOT NULL,
name TEXT NOT NULL,
material_sn TEXT DEFAULT '',
model TEXT DEFAULT '',
version TEXT DEFAULT '',
calibration TEXT DEFAULT '无需校准'
)
""")
# 18. 设备操作日志表
db.execute("""
CREATE TABLE IF NOT EXISTS device_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_sn TEXT NOT NULL,
action TEXT NOT NULL,
operator TEXT DEFAULT '',
detail TEXT DEFAULT '',
date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 19. 更新日志表
db.execute("""
CREATE TABLE IF NOT EXISTS update_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT DEFAULT '',
category TEXT NOT NULL DEFAULT 'feature',
version TEXT DEFAULT '',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 20. 授权下载日志表
db.execute("""
CREATE TABLE IF NOT EXISTS license_download_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
license_id INTEGER NOT NULL,
device_sn TEXT NOT NULL,
download_time INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
ip_address TEXT DEFAULT '',
app_version TEXT DEFAULT ''
)
""")
# 21. 授权模板表
db.execute("""
CREATE TABLE IF NOT EXISTS license_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
model_code TEXT NOT NULL,
auth_items TEXT NOT NULL DEFAULT '[]',
config_id INTEGER DEFAULT NULL,
status TEXT NOT NULL DEFAULT '启用',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 22. 授权项定义表(全局授权模块字典)
db.execute("""
CREATE TABLE IF NOT EXISTS auth_items (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
category TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT '启用'
)
""")
# 23. 设备装配检查记录表
db.execute("""
CREATE TABLE IF NOT EXISTS device_checklist_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_sn TEXT NOT NULL,
checklist_name TEXT NOT NULL,
passed INTEGER DEFAULT 0,
photos TEXT DEFAULT '[]',
note TEXT DEFAULT '',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 24. 维修处理记录表
db.execute("""
CREATE TABLE IF NOT EXISTS repair_process_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
action TEXT NOT NULL,
operator TEXT DEFAULT '',
date INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
note TEXT DEFAULT ''
)
""")
# 25. 维修板卡更换记录表
db.execute("""
CREATE TABLE IF NOT EXISTS repair_board_replacements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
type TEXT NOT NULL,
model TEXT DEFAULT '',
old_sn TEXT DEFAULT '',
new_sn TEXT DEFAULT '',
date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 27. 通用字典表(枚举值、选项配置)
db.execute("""
CREATE TABLE IF NOT EXISTS reference_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
code TEXT NOT NULL,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT '启用',
description TEXT DEFAULT '',
extra TEXT DEFAULT '{}',
UNIQUE(category, code)
)
""")
# 创建索引优化查询性能
indexes = [
("idx_devices_status", "devices", "status"),
("idx_devices_model", "devices", "model"),
("idx_calibration_material_sn", "calibration_files", "material_sn"),
("idx_calibration_sn", "calibration_files", "sn"),
("idx_calibration_uid", "calibration_files", "uid"),
("idx_app_name_platform", "app_versions", "app_name, platform_type"),
("idx_app_versions_status", "app_versions", "status"),
("idx_config_model", "config_files", "model"),
("idx_config_status", "config_files", "status"),
("idx_firmware_type_model", "firmware_versions", "firmware_type, device_model, board_model"),
("idx_firmware_status", "firmware_versions", "status"),
("idx_licenses_device_sn", "licenses", "device_sn"),
("idx_licenses_config_id", "licenses", "config_id"),
("idx_materials_device_sn", "materials", "device_sn"),
("idx_materials_category", "materials", "category"),
("idx_repair_status", "repair_orders", "status"),
("idx_scrap_status", "scrap_records", "status"),
("idx_device_bom_sn", "device_bom_records", "device_sn"),
("idx_bom_model", "bom_templates", "model_code"),
("idx_checklist_model", "checklist_templates", "model_code"),
("idx_reference_category", "reference_values", "category, sort_order"),
]
for idx_name, table, columns in indexes:
db.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}({columns})")
db.commit() db.commit()
print(f"[DB] 数据库已初始化: {DB_PATH}") print(f"[DB] 数据库已初始化: {DB_PATH}")
def _column_exists(db: sqlite3.Connection, table: str, column: str) -> bool:
"""检查表中是否已存在指定列"""
rows = db.execute(f"PRAGMA table_info({table})").fetchall()
return any(r["name"] == column for r in rows)
def migrate_db():
"""
增量迁移为已存在的表添加新列和索引不删除数据
将现有 TEXT 格式的时间数据迁移为 INTEGER Unix 时间戳
"""
with get_db() as db:
# 1. config_files 表列迁移
new_columns = [
("max_tx_voltage", "TEXT"),
("max_tx_current", "TEXT"),
("tx_waveform", "TEXT"),
("tx_pulse_width", "TEXT"),
("acq_channels", "TEXT"),
("acq_sample_rate", "TEXT"),
("acq_voltage_range", "TEXT"),
("full_waveform_capture", "TEXT"),
("ssid_prefix", "TEXT"),
]
for col, ctype in new_columns:
if not _column_exists(db, "config_files", col):
try:
db.execute(f"ALTER TABLE config_files ADD COLUMN {col} {ctype}")
print(f"[DB MIGRATE] config_files 新增列: {col}")
except Exception as e:
print(f"[DB MIGRATE] 添加列 {col} 失败: {e}")
# 2. 创建索引(对已存在的数据库补充索引)
indexes = [
("idx_devices_status", "devices", "status"),
("idx_devices_model", "devices", "model"),
("idx_calibration_material_sn", "calibration_files", "material_sn"),
("idx_calibration_sn", "calibration_files", "sn"),
("idx_calibration_uid", "calibration_files", "uid"),
("idx_app_name_platform", "app_versions", "app_name, platform_type"),
("idx_app_versions_status", "app_versions", "status"),
("idx_config_model", "config_files", "model"),
("idx_config_status", "config_files", "status"),
("idx_firmware_type_model", "firmware_versions", "firmware_type, device_model, board_model"),
("idx_firmware_status", "firmware_versions", "status"),
("idx_licenses_device_sn", "licenses", "device_sn"),
("idx_licenses_config_id", "licenses", "config_id"),
("idx_materials_device_sn", "materials", "device_sn"),
("idx_materials_category", "materials", "category"),
("idx_repair_status", "repair_orders", "status"),
("idx_scrap_status", "scrap_records", "status"),
("idx_device_bom_sn", "device_bom_records", "device_sn"),
("idx_bom_model", "bom_templates", "model_code"),
("idx_checklist_model", "checklist_templates", "model_code"),
]
for idx_name, table, columns in indexes:
try:
db.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}({columns})")
except Exception as e:
print(f"[DB MIGRATE] 创建索引 {idx_name} 失败: {e}")
# 3. 授权项定义表(兼容已有数据库)
db.execute("""
CREATE TABLE IF NOT EXISTS auth_items (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
category TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT '启用'
)
""")
# 4. 设备装配检查记录表(兼容已有数据库)
db.execute("""
CREATE TABLE IF NOT EXISTS device_checklist_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_sn TEXT NOT NULL,
checklist_name TEXT NOT NULL,
passed INTEGER DEFAULT 0,
photos TEXT DEFAULT '[]',
note TEXT DEFAULT '',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 5. 维修处理记录表(兼容已有数据库)
db.execute("""
CREATE TABLE IF NOT EXISTS repair_process_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
action TEXT NOT NULL,
operator TEXT DEFAULT '',
date INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
note TEXT DEFAULT ''
)
""")
# 6. 维修板卡更换记录表(兼容已有数据库)
db.execute("""
CREATE TABLE IF NOT EXISTS repair_board_replacements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
type TEXT NOT NULL,
model TEXT DEFAULT '',
old_sn TEXT DEFAULT '',
new_sn TEXT DEFAULT '',
date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
""")
# 7. 通用字典表(兼容已有数据库)
db.execute("""
CREATE TABLE IF NOT EXISTS reference_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
code TEXT NOT NULL,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT '启用',
description TEXT DEFAULT '',
extra TEXT DEFAULT '{}',
UNIQUE(category, code)
)
""")
# 8. 将现有 TEXT 时间数据迁移为 INTEGER 时间戳SQLite 动态类型,无需重建表)
timestamp_tables = {
"devices": ["created_at", "activated_at", "production_date"],
"calibration_files": ["upload_time"],
"app_versions": ["created_at", "updated_at"],
"config_files": ["created_at", "updated_at"],
"firmware_versions": ["created_at", "updated_at"],
"device_models": ["create_date"],
"licenses": ["created_at", "updated_at"],
"repair_orders": ["create_date"],
"scrap_records": ["date"],
"device_logs": ["date"],
"update_logs": ["created_at"],
"license_download_logs": ["download_time"],
"license_templates": ["created_at"],
"device_checklist_records": ["created_at", "updated_at"],
"repair_process_records": ["date"],
"repair_board_replacements": ["date"],
"materials": ["production_date", "calib_date"],
}
for tbl, cols in timestamp_tables.items():
for col in cols:
try:
db.execute(f"""
UPDATE {tbl}
SET {col} = CAST(strftime('%s', COALESCE({col}, '1970-01-01 00:00:00')) AS INTEGER)
WHERE {col} IS NOT NULL
AND typeof({col}) = 'text'
AND {col} != ''
""")
if db.total_changes > 0:
print(f"[DB MIGRATE] {tbl}.{col} 已迁移为时间戳")
except Exception as e:
print(f"[DB MIGRATE] {tbl}.{col} 迁移失败或无需迁移: {e}")
# 9. 数据库迁移:为 checklist_templates 表添加 standard 字段
if not _column_exists(db, "checklist_templates", "standard"):
try:
db.execute("ALTER TABLE checklist_templates ADD COLUMN standard TEXT")
print("[DB] 已为 checklist_templates 表添加 standard 字段")
except Exception as e:
print(f"[DB] 添加 standard 字段失败: {e}")
db.commit()
print("[DB MIGRATE] 数据库迁移完成")
def seed_demo_data(): def seed_demo_data():
""" """
插入演示数据方便新手直接体验接口 插入演示数据方便新手直接体验接口
如果已存在相同 SN则忽略INSERT OR IGNORE 如果已存在相同 SN则忽略INSERT OR IGNORE
""" """
# 将演示日期转换为时间戳
demo_date_ts = int(datetime.strptime("2026-05-07", "%Y-%m-%d").timestamp())
demo_devices = [ demo_devices = [
("GD30-20260507-001", "待激活"), ("GD30-20260507-001", "GD30-2024", "标准型", "待激活", "", demo_date_ts, "华东地质局", "B2026-05"),
("GD30-20260507-002", "待激活"), ("GD30-20260507-002", "GD30-2024", "标准型", "待激活", "", demo_date_ts, "华北勘探院", "B2026-05"),
("MT-20260507-003", "已禁用"), ("MT-20260507-003", "MT-2024", "标准型", "已禁用", "", demo_date_ts, "-", "B2026-05"),
] ]
with get_db() as db: with get_db() as db:
for sn, status in demo_devices: for sn, model, type_, status, firmware, production_date, customer, batch in demo_devices:
db.execute( db.execute(
"INSERT OR IGNORE INTO devices (sn, status) VALUES (?, ?)", """
(sn, status) INSERT OR IGNORE INTO devices
(sn, model, type, status, firmware, production_date, customer, batch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(sn, model, type_, status, firmware, production_date, customer, batch)
) )
# 授权项定义种子数据
demo_auth_items = [
('1D', '一维自电/电阻率/激电测试模块', '包含一维自然电位法、电阻率测深、激发极化测深', '一维', 1),
('2D', '二维自电/电阻率/激电测试模块', '包含二维自然电位法、电阻率成像、激发极化成像', '二维', 2),
('3D', '三维自电/电阻率/激电测试模块', '包含三维自然电位法、电阻率成像、激发极化成像', '三维', 3),
('WATER', '水上', '水上电法探测', '水上', 4),
('CROSS', '跨孔', '跨孔电阻率成像', '跨孔', 5),
('CF', '电流场法', '电流场法', '电流场法', 6),
]
for id_, name, desc, cat, sort in demo_auth_items:
db.execute(
"INSERT OR IGNORE INTO auth_items (id, name, description, category, sort_order) VALUES (?, ?, ?, ?, ?)",
(id_, name, desc, cat, sort)
)
# 固件分类种子数据确保4种固件类型分类存在
demo_categories = [
("采集板", "数据采集板固件", 1, 0, 1),
("发射板", "信号发射板固件", 1, 0, 2),
("主协板", "主控协处理板固件", 1, 0, 3),
("主机服务", "主机服务升级包", 1, 0, 4),
]
for name, desc, has_fw, has_cal, sort in demo_categories:
db.execute(
"""
INSERT OR IGNORE INTO material_categories
(name, description, has_firmware, has_calibration, sort_order, status)
VALUES (?, ?, ?, ?, ?, '启用')
""",
(name, desc, has_fw, has_cal, sort)
)
# 通用字典种子数据
demo_refs = [
# 设备状态
("device_status", "待激活", "待激活", 1),
("device_status", "装配中", "装配中", 2),
("device_status", "测试通过", "测试通过", 3),
("device_status", "测试不通过", "测试不通过", 4),
("device_status", "已出厂", "已出厂", 5),
("device_status", "已激活", "已激活", 6),
("device_status", "已禁用", "已禁用", 7),
# 型号状态
("model_status", "在产", "在产", 1),
("model_status", "停产", "停产", 2),
# 物料状态
("material_status", "在库", "在库", 1),
("material_status", "已装配", "已装配", 2),
("material_status", "故障", "故障", 3),
("material_status", "报废", "报废", 4),
# 校准状态
("calibration_status", "待校准", "待校准", 1),
("calibration_status", "合格", "合格", 2),
("calibration_status", "不合格", "不合格", 3),
("calibration_status", "无需校准", "无需校准", 4),
# 维修状态
("repair_status", "待处理", "待处理", 1),
("repair_status", "处理中", "处理中", 2),
("repair_status", "已处理", "已处理", 3),
# 维修优先级
("repair_priority", "", "", 1),
("repair_priority", "", "", 2),
("repair_priority", "", "", 3),
# 维修故障类型
("repair_fault_type", "板卡故障", "板卡故障", 1),
("repair_fault_type", "固件异常", "固件异常", 2),
("repair_fault_type", "通信故障", "通信故障", 3),
("repair_fault_type", "电源故障", "电源故障", 4),
("repair_fault_type", "传感器故障", "传感器故障", 5),
("repair_fault_type", "其他", "其他", 6),
# 维修处理动作
("repair_action", "更换板卡", "更换板卡", 1),
("repair_action", "固件修复", "固件修复", 2),
("repair_action", "参数重置", "参数重置", 3),
("repair_action", "其他处理", "其他处理", 4),
# 报废状态
("scrap_status", "待审批", "待审批", 1),
("scrap_status", "审批中", "审批中", 2),
("scrap_status", "已审批", "已审批", 3),
("scrap_status", "已驳回", "已驳回", 4),
("scrap_status", "回收中", "回收中", 5),
("scrap_status", "已回收", "已回收", 6),
# 报废流程步骤
("scrap_step", "申请报废", "申请报废", 1),
("scrap_step", "主管审批", "主管审批", 2),
("scrap_step", "物料检测", "物料检测", 3),
("scrap_step", "回收入库", "回收入库", 4),
("scrap_step", "报废完成", "报废完成", 5),
# 固件升级类型
("firmware_upgrade_type", "可选", "可选", 1),
("firmware_upgrade_type", "强制", "强制", 2),
# 注册测试状态
("registration_test_status", "装配中", "装配中", 1),
("registration_test_status", "测试通过", "测试通过", 2),
("registration_test_status", "测试不通过", "测试不通过", 3),
# 配置文件电压选项
("config_voltage", "500V", "500V", 1),
("config_voltage", "800V", "800V", 2),
("config_voltage", "1000V", "1000V", 3),
("config_voltage", "1200V", "1200V", 4),
("config_voltage", "1500V", "1500V", 5),
# 配置文件电流选项
("config_current", "2A", "2A", 1),
("config_current", "5A", "5A", 2),
("config_current", "8A", "8A", 3),
("config_current", "10A", "10A", 4),
("config_current", "15A", "15A", 5),
# 配置文件波形选项
("config_waveform", "0+0-", "0+0-", 1),
("config_waveform", "+0-0", "+0-0", 2),
("config_waveform", "+-", "+-", 3),
# 配置文件脉冲宽度选项
("config_pulse_width", "0.25s/0.5s/1s/2s/4s/8s", "0.25s/0.5s/1s/2s/4s/8s", 1),
("config_pulse_width", "0.25s/0.5s/1s/2s/4s/8s/16s/32s/64s", "0.25s/0.5s/1s/2s/4s/8s/16s/32s/64s", 2),
# 配置文件通道数选项
("config_channels", "1", "1", 1),
("config_channels", "6", "6", 2),
("config_channels", "12", "12", 3),
# 配置文件采样率选项
("config_sample_rate", "50Hz/60Hz", "50Hz/60Hz", 1),
("config_sample_rate", "50Hz/60Hz/100Hz/1000Hz", "50Hz/60Hz/100Hz/1000Hz", 2),
# 配置文件电压范围选项
("config_voltage_range", "±2.5V", "±2.5V", 1),
("config_voltage_range", "±2.5V/±80V", "±2.5V/±80V", 2),
("config_voltage_range", "±80V/±600V", "±80V/±600V", 3),
# APP平台类型
("app_platform", "1", "iOS", 1),
("app_platform", "2", "Android", 2),
("app_platform", "3", "HarmonyOS", 3),
("app_platform", "4", "Windows", 4),
("app_platform", "5", "macOS", 5),
("app_platform", "6", "Linux", 6),
("app_platform", "7", "Web", 7),
]
for cat, code, label, sort in demo_refs:
db.execute(
"INSERT OR IGNORE INTO reference_values (category, code, label, sort_order) VALUES (?, ?, ?, ?)",
(cat, code, label, sort)
)
db.commit() db.commit()
print("[DB] 演示数据已插入") print("[DB] 演示数据已插入")
def scan_firmware_directory():
"""
扫描 FIRMWARE_PUBLIC_DIR 目录自动发现固件文件
返回列表每项包含type, folder, file_name, file_path, file_size, version
"""
results = []
base_dir = FIRMWARE_PUBLIC_DIR
if not os.path.isdir(base_dir):
return results
type_map = {
"CJB": "采集板",
"FSB": "发射板",
"XCL": "主协板",
}
# 扫描子目录
for folder, fw_type in type_map.items():
folder_path = os.path.join(base_dir, folder)
if not os.path.isdir(folder_path):
continue
for fname in os.listdir(folder_path):
fpath = os.path.join(folder_path, fname)
if not os.path.isfile(fpath):
continue
size = os.path.getsize(fpath)
# 尝试从文件名解析版本,如 CJB_2_8(Checksum=0xc5b1).bin -> 2.8
version = "-"
if "_" in fname:
parts = fname.split("_")
if len(parts) >= 3:
try:
major = parts[1]
minor = parts[2].split("(")[0].split(".")[0]
version = f"{major}.{minor}"
except Exception:
pass
results.append({
"type": fw_type,
"folder": folder,
"file_name": fname,
"file_path": fpath,
"file_size": size,
"version": version,
})
# 扫描根目录(主机服务包)
for fname in os.listdir(base_dir):
fpath = os.path.join(base_dir, fname)
if not os.path.isfile(fpath):
continue
if fname.startswith("package_arm") and fname.endswith(".tar.gz"):
size = os.path.getsize(fpath)
# 尝试从文件名解析版本,如 package_arm_20260507.tar.gz -> 2026.05.07
version = "-"
if fname.startswith("package_arm_"):
ver_part = fname[len("package_arm_"):].replace(".tar.gz", "")
if len(ver_part) == 8 and ver_part.isdigit():
version = f"{ver_part[:4]}.{ver_part[4:6]}.{ver_part[6:8]}"
else:
version = ver_part
results.append({
"type": "主机服务",
"folder": "",
"file_name": fname,
"file_path": fpath,
"file_size": size,
"version": version,
})
return results

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ from datetime import datetime
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# 请求模型(客户端传入) # 设备相关模型
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class DeviceCheckRequest(BaseModel): class DeviceCheckRequest(BaseModel):
@ -23,6 +23,7 @@ class LicenseGenerateRequest(BaseModel):
sn: str = Field(..., description="目标设备 SN") sn: str = Field(..., description="目标设备 SN")
modules: List[str] = Field(default=[], description="激活的模块列表,如 ['1D', '2D']") modules: List[str] = Field(default=[], description="激活的模块列表,如 ['1D', '2D']")
valid_days: int = Field(default=365, ge=1, le=3650, description="授权有效期(天),默认一年") valid_days: int = Field(default=365, ge=1, le=3650, description="授权有效期(天),默认一年")
config_id: Optional[int] = Field(None, description="关联的配置文件 ID授权文件中会嵌入该配置")
class ActivateReportRequest(BaseModel): class ActivateReportRequest(BaseModel):
@ -31,10 +32,6 @@ class ActivateReportRequest(BaseModel):
status: str = Field(..., pattern="^(已激活|激活失败)$", description="上报状态:已激活 或 激活失败") status: str = Field(..., pattern="^(已激活|激活失败)$", description="上报状态:已激活 或 激活失败")
# ═══════════════════════════════════════════════════════════════
# 响应模型(服务端返回)
# ═══════════════════════════════════════════════════════════════
class DeviceInfo(BaseModel): class DeviceInfo(BaseModel):
"""设备信息响应""" """设备信息响应"""
sn: str sn: str
@ -75,3 +72,267 @@ class ActivateResponse(BaseModel):
new_status: str new_status: str
activated_at: Optional[str] = None activated_at: Optional[str] = None
message: str message: str
# ═══════════════════════════════════════════════════════════════
# 配置文件相关模型
# ═══════════════════════════════════════════════════════════════
class ConfigFileCreateRequest(BaseModel):
"""创建配置文件请求"""
name: str = Field(..., description="配置名称,如 GD30-2024-标准参数")
model: str = Field(..., description="适用设备型号,如 GD30-2024")
version: str = Field(default="v1.0", description="配置版本号")
status: str = Field(default="生效", description="状态:生效/失效")
# 发射参数
max_tx_voltage: Optional[str] = Field(None, description="最大发射电压,如 1200V / 1500V")
max_tx_current: Optional[str] = Field(None, description="最大发射电流,如 6A / 10A")
tx_waveform: Optional[str] = Field(None, description="发射波形,如 0+0- / +0-0 / +-")
tx_pulse_width: Optional[str] = Field(None, description="发射脉宽,如 0.25s/0.5s/1s/2s/4s/8s/16s")
# 采集参数
acq_channels: Optional[str] = Field(None, description="支持通道数,如 1 / 6 / 12")
acq_sample_rate: Optional[str] = Field(None, description="采样率,如 50Hz/60Hz/100Hz/1000Hz")
acq_voltage_range: Optional[str] = Field(None, description="电压测量量程,如 ±2.5V/±80V / ±80V/±600V")
full_waveform_capture: Optional[str] = Field(None, description="全波形采集,如 支持 / 不支持")
# 网络参数
ssid_prefix: Optional[str] = Field(None, description="WiFi SSID前缀如 GD30-AP")
class ConfigFileUpdateRequest(BaseModel):
"""更新配置文件请求"""
name: Optional[str] = Field(None, description="配置名称")
model: Optional[str] = Field(None, description="适用设备型号")
version: Optional[str] = Field(None, description="配置版本号")
status: Optional[str] = Field(None, description="状态:生效/失效")
# 发射参数
max_tx_voltage: Optional[str] = Field(None, description="最大发射电压")
max_tx_current: Optional[str] = Field(None, description="最大发射电流")
tx_waveform: Optional[str] = Field(None, description="发射波形")
tx_pulse_width: Optional[str] = Field(None, description="发射脉宽")
# 采集参数
acq_channels: Optional[str] = Field(None, description="支持通道数")
acq_sample_rate: Optional[str] = Field(None, description="采样率")
acq_voltage_range: Optional[str] = Field(None, description="电压测量量程")
full_waveform_capture: Optional[str] = Field(None, description="全波形采集")
# 网络参数
ssid_prefix: Optional[str] = Field(None, description="WiFi SSID前缀")
class ConfigFileInfo(BaseModel):
"""配置文件信息响应"""
id: int
name: str
model: str
version: str
status: str
# 发射参数
max_tx_voltage: Optional[str] = None
max_tx_current: Optional[str] = None
tx_waveform: Optional[str] = None
tx_pulse_width: Optional[str] = None
# 采集参数
acq_channels: Optional[str] = None
acq_sample_rate: Optional[str] = None
acq_voltage_range: Optional[str] = None
full_waveform_capture: Optional[str] = None
# 网络参数
ssid_prefix: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
# ═══════════════════════════════════════════════════════════════
# 校准文件相关模型
# ═══════════════════════════════════════════════════════════════
class CalibChannelData(BaseModel):
"""校准通道数据"""
ChannelId: int = Field(..., description="通道ID")
factor_80v: float = Field(..., description="80V量程校准系数")
offset_80v: float = Field(..., description="80V量程偏移量")
factor_2_5v: float = Field(..., description="2.5V量程校准系数")
offset_2_5v: float = Field(..., description="2.5V量程偏移量")
class CalibrationUploadRequest(BaseModel):
"""校准文件上传请求JSON格式由Windows校准软件调用"""
SN: str = Field(..., description="校准文件中的SN")
UID: str = Field(..., description="校准文件中的UID")
CalibrateFactor: List[dict] = Field(..., description="各通道校准系数数组")
class CalibrationFileInfo(BaseModel):
"""校准文件信息响应"""
id: int
material_sn: str = Field(..., description="关联物料SN")
sn: Optional[str] = Field(None, description="校准SN")
uid: Optional[str] = Field(None, description="校准UID")
file_name: Optional[str] = Field(None, description="文件名")
file_size: Optional[int] = Field(None, description="文件大小(字节)")
md5: Optional[str] = Field(None, description="文件MD5")
result: Optional[str] = Field(None, description="校准结果:合格/不合格")
operator: Optional[str] = Field(None, description="操作员")
remark: Optional[str] = Field(None, description="备注")
upload_time: Optional[str] = Field(None, description="上传时间")
channels_count: Optional[int] = Field(None, description="通道数量")
channels: Optional[List[dict]] = Field(None, description="通道详细数据")
class CalibrationUpdateRequest(BaseModel):
"""校准文件更新请求"""
operator: Optional[str] = Field(None, description="操作员")
remark: Optional[str] = Field(None, description="备注")
result: Optional[str] = Field(None, description="校准结果")
# ═══════════════════════════════════════════════════════════════
# APP 版本相关模型
# ═══════════════════════════════════════════════════════════════
class AppVersionCreateRequest(BaseModel):
"""创建 APP 版本请求"""
app_name: str = Field(..., description="应用名称,如 GeoData Mobile")
package_name: Optional[str] = Field(None, description="包名,如 com.geomative.geodata")
platform_type: int = Field(default=2, description="平台类型1=iOS, 2=Android")
version_name: str = Field(..., description="版本号名称,如 1.2.0")
major_version: int = Field(default=1, ge=0)
minor_version: int = Field(default=0, ge=0)
patch_version: int = Field(default=0, ge=0)
file_type: str = Field(default="apk", description="文件类型apk / ipa")
distribution_type: str = Field(default="direct", description="分发方式direct / appstore")
os_min_version: Optional[str] = Field(None, description="最低系统版本要求")
is_force_update: bool = Field(default=False, description="是否强制更新")
changelog: Optional[List[str]] = Field(None, description="更新日志列表")
status: int = Field(default=1, description="状态0=草稿, 1=已发布, 2=已下架")
class AppVersionUpdateRequest(BaseModel):
"""更新 APP 版本请求"""
app_name: Optional[str] = Field(None, description="应用名称")
package_name: Optional[str] = Field(None, description="包名")
version_name: Optional[str] = Field(None, description="版本号")
platform_type: Optional[int] = Field(None, description="平台类型")
os_min_version: Optional[str] = Field(None, description="最低系统版本")
is_force_update: Optional[bool] = Field(None, description="是否强制更新")
changelog: Optional[List[str]] = Field(None, description="更新日志")
status: Optional[int] = Field(None, description="状态0=草稿, 1=已发布, 2=已下架")
class AppVersionInfo(BaseModel):
"""APP 版本信息响应"""
id: int
app_name: str
package_name: Optional[str] = None
platform_type: int
version_name: str
major_version: int
minor_version: int
patch_version: int
file_name: Optional[str] = None
file_size: Optional[int] = None
file_type: Optional[str] = None
distribution_type: Optional[str] = None
primary_url: Optional[str] = None
os_min_version: Optional[str] = None
is_force_update: bool = False
changelog: Optional[str] = None
status: int
created_at: Optional[str] = None
updated_at: Optional[str] = None
class AppDownloadQuery(BaseModel):
"""APP 下载查询参数"""
app_name: Optional[str] = Field(None, description="应用名称")
platform_type: Optional[int] = Field(None, description="平台类型1=iOS, 2=Android")
version_name: Optional[str] = Field(None, description="指定版本号")
class AppCheckUpdateRequest(BaseModel):
"""APP 检查更新请求"""
app_name: str = Field(..., description="应用名称")
platform_type: int = Field(..., description="平台类型1=iOS, 2=Android")
current_version: str = Field(..., description="当前版本号,如 1.1.0")
class AppCheckUpdateResponse(BaseModel):
"""APP 检查更新响应"""
has_update: bool = Field(..., description="是否有更新")
force_update: bool = Field(..., description="是否强制更新")
version_name: Optional[str] = Field(None, description="最新版本号")
download_url: Optional[str] = Field(None, description="下载地址")
changelog: Optional[List[str]] = Field(None, description="更新日志")
file_size: Optional[int] = Field(None, description="文件大小")
# ═══════════════════════════════════════════════════════════════
# 固件版本相关模型
# ═══════════════════════════════════════════════════════════════
class FirmwareCreateRequest(BaseModel):
"""创建固件版本请求"""
version: str = Field(..., description="固件版本号,如 v3.2.1")
firmware_type: str = Field(..., description="固件类型:采集板/发射板/主协板/主机服务")
board_model: Optional[str] = Field(None, description="板卡型号,如 GD30-ACQ-01")
device_model: Optional[str] = Field(None, description="设备型号,如 GD30-2024")
hw_range: Optional[str] = Field(None, description="硬件适用范围")
upgrade_type: str = Field(default="可选", description="升级类型:强制/可选")
signed: bool = Field(default=False, description="是否已签名")
notes: Optional[List[str]] = Field(None, description="更新说明列表")
status: str = Field(default="已发布", description="状态:草稿/已发布/已下架/兼容")
class FirmwareUpdateRequest(BaseModel):
"""更新固件版本请求"""
version: Optional[str] = Field(None, description="版本号")
firmware_type: Optional[str] = Field(None, description="固件类型")
board_model: Optional[str] = Field(None, description="板卡型号")
device_model: Optional[str] = Field(None, description="设备型号")
hw_range: Optional[str] = Field(None, description="硬件适用范围")
upgrade_type: Optional[str] = Field(None, description="升级类型")
signed: Optional[bool] = Field(None, description="是否已签名")
notes: Optional[List[str]] = Field(None, description="更新说明")
status: Optional[str] = Field(None, description="状态")
class FirmwareInfo(BaseModel):
"""固件版本信息响应"""
id: int
version: str
firmware_type: str
board_model: Optional[str] = None
device_model: Optional[str] = None
file_name: Optional[str] = None
file_size: Optional[int] = None
hw_range: Optional[str] = None
upgrade_type: Optional[str] = None
signed: bool = False
notes: Optional[str] = None
status: str
created_at: Optional[str] = None
updated_at: Optional[str] = None
class FirmwareDownloadQuery(BaseModel):
"""固件下载查询参数"""
firmware_type: str = Field(..., description="固件类型")
device_model: Optional[str] = Field(None, description="设备型号")
board_model: Optional[str] = Field(None, description="板卡型号")
class FirmwareCheckUpdateRequest(BaseModel):
"""固件检查更新请求"""
firmware_type: str = Field(..., description="固件类型")
device_model: Optional[str] = Field(None, description="设备型号")
board_model: Optional[str] = Field(None, description="板卡型号")
current_version: str = Field(..., description="当前版本号")
class FirmwareCheckUpdateResponse(BaseModel):
"""固件检查更新响应"""
has_update: bool = Field(..., description="是否有更新")
force_update: bool = Field(..., description="是否强制更新")
version: Optional[str] = Field(None, description="最新版本号")
download_url: Optional[str] = Field(None, description="下载地址")
notes: Optional[List[str]] = Field(None, description="更新说明")
file_size: Optional[int] = Field(None, description="文件大小")

View File

@ -1,36 +1,51 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { getDb } from '@/lib/db' import { API_BASE } from '@/lib/api-proxy'
import { seedIfEmpty } from '@/lib/seed'
export async function GET() { export async function GET() {
seedIfEmpty() const res = await fetch(`${API_BASE}/api/material-categories`)
const db = getDb() const data = await res.json()
const items = db.prepare('SELECT * FROM material_categories ORDER BY sort_order').all() return NextResponse.json(data)
return NextResponse.json(items)
} }
export async function POST(req: Request) { export async function POST(req: Request) {
seedIfEmpty()
const db = getDb()
const body = await req.json() const body = await req.json()
const { name, description, has_firmware, has_calibration, sort_order, status } = body const res = await fetch(`${API_BASE}/api/material-categories`, {
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 || '启用') method: 'POST',
return NextResponse.json({ id: result.lastInsertRowid }) headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data)
} }
export async function PUT(req: Request) { export async function PUT(req: Request) {
seedIfEmpty()
const db = getDb()
const body = await req.json() const body = await req.json()
const { id, name, description, has_firmware, has_calibration, sort_order, status } = body const res = await fetch(`${API_BASE}/api/material-categories`, {
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) method: 'PUT',
return NextResponse.json({ ok: true }) headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data)
} }
export async function DELETE(req: Request) { export async function DELETE(req: Request) {
seedIfEmpty() const url = new URL(req.url)
const db = getDb() const id = url.searchParams.get('id')
const { id } = await req.json()
db.prepare('DELETE FROM material_categories WHERE id = ?').run(id) if (!id) {
return NextResponse.json({ ok: true }) return NextResponse.json({ error: '缺少分类ID' }, { status: 400 })
}
const res = await fetch(`${API_BASE}/api/material-categories?id=${encodeURIComponent(id)}`, {
method: 'DELETE',
})
if (!res.ok) {
console.error(`API request failed with status ${res.status}:`, await res.text())
return NextResponse.json({ error: `删除失败: ${res.status}` }, { status: res.status })
}
const data = await res.json()
return NextResponse.json(data)
} }

View File

@ -15,6 +15,10 @@ function getStatusStyle(status: string) {
export default function MaterialCategoriesPage() { export default function MaterialCategoriesPage() {
const { data: categories, loading, refetch } = useApi<MaterialCategory[]>('/api/material-categories', []) const { data: categories, loading, refetch } = useApi<MaterialCategory[]>('/api/material-categories', [])
// 确保数据是数组
const categoriesArray = Array.isArray(categories) ? categories : []
const [addDrawer, setAddDrawer] = useState(false) const [addDrawer, setAddDrawer] = useState(false)
const [editDrawer, setEditDrawer] = useState<MaterialCategory | null>(null) const [editDrawer, setEditDrawer] = useState<MaterialCategory | null>(null)
const [form, setForm] = useState({ name: '', description: '', has_firmware: false, has_calibration: false, sort_order: 0, status: '启用' }) const [form, setForm] = useState({ name: '', description: '', has_firmware: false, has_calibration: false, sort_order: 0, status: '启用' })
@ -31,7 +35,7 @@ export default function MaterialCategoriesPage() {
refetch(); setEditDrawer(null) refetch(); setEditDrawer(null)
} }
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
await fetch('/api/material-categories', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }) await fetch(`/api/material-categories?id=${id}`, { method: 'DELETE' })
refetch() refetch()
} }
@ -51,7 +55,7 @@ export default function MaterialCategoriesPage() {
<div style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}> <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' }}> <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> <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}> {categoriesArray.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> <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> </div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
@ -61,7 +65,7 @@ export default function MaterialCategoriesPage() {
))} ))}
</tr></thead> </tr></thead>
<tbody> <tbody>
{categories.map(cat => ( {categoriesArray.map(cat => (
<tr key={cat.id} style={{ borderBottom: '1px solid #F0F0F0' }}> <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, 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: 14, fontWeight: 500 }}>{cat.name}</td>
@ -88,37 +92,47 @@ export default function MaterialCategoriesPage() {
{/* Add Drawer */} {/* Add Drawer */}
{addDrawer && ( {addDrawer && (
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}> <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 onClick={() => { setAddDrawer(false); resetForm() }} 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={{ 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' }}> <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> <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> <button onClick={() => { setAddDrawer(false); resetForm() }} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
</div> </div>
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}> <div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
<div style={{ marginBottom: 20 }}> <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={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' }} /> <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>
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <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' }} /> <input value={form.description} onChange={e => setForm({ ...form, 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={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <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' }} /> <input type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: Number(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={{ display: 'flex', gap: 24, marginBottom: 20 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input type="checkbox" checked={form.has_firmware} onChange={e => setForm({ ...form, has_firmware: e.target.checked })} /> <input type="checkbox" id="has_firmware" checked={form.has_firmware} onChange={e => setForm({ ...form, has_firmware: e.target.checked })} style={{ width: 16, height: 16 }} />
</label> <label htmlFor="has_firmware" style={{ fontSize: 14, fontWeight: 500 }}></label>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}> </div>
<input type="checkbox" checked={form.has_calibration} onChange={e => setForm({ ...form, has_calibration: e.target.checked })} /> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
</label> <input type="checkbox" id="has_calib" checked={form.has_calibration} onChange={e => setForm({ ...form, has_calibration: e.target.checked })} style={{ width: 16, height: 16 }} />
<label htmlFor="has_calib" style={{ fontSize: 14, fontWeight: 500 }}></label>
</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={() => setForm({ ...form, status: s })} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: form.status === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: form.status === s ? '#eef5f0' : '#fff', color: form.status === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button>
))}
</div>
</div> </div>
</div> </div>
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}> <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={() => { setAddDrawer(false); resetForm() }} 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> <button onClick={handleAdd} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}></button>
</div> </div>
</div> </div>
</div> </div>
@ -136,23 +150,25 @@ export default function MaterialCategoriesPage() {
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}> <div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
<div style={{ marginBottom: 20 }}> <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' }} /> <input value={editDrawer.name} onChange={e => setEditDrawer({ ...editDrawer, name: 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={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <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' }} /> <input value={editDrawer.description} onChange={e => setEditDrawer({ ...editDrawer, 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={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <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' }} /> <input type="number" value={editDrawer.sort_order} onChange={e => setEditDrawer({ ...editDrawer, sort_order: Number(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={{ display: 'flex', gap: 24, marginBottom: 20 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input type="checkbox" checked={!!editDrawer.has_firmware} onChange={e => setEditDrawer({ ...editDrawer, has_firmware: e.target.checked ? 1 : 0 })} /> <input type="checkbox" id="edit_has_firmware" checked={!!editDrawer.has_firmware} onChange={e => setEditDrawer({ ...editDrawer, has_firmware: e.target.checked ? 1 : 0 })} style={{ width: 16, height: 16 }} />
</label> <label htmlFor="edit_has_firmware" style={{ fontSize: 14, fontWeight: 500 }}></label>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, cursor: 'pointer' }}> </div>
<input type="checkbox" checked={!!editDrawer.has_calibration} onChange={e => setEditDrawer({ ...editDrawer, has_calibration: e.target.checked ? 1 : 0 })} /> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
</label> <input type="checkbox" id="edit_has_calib" checked={!!editDrawer.has_calibration} onChange={e => setEditDrawer({ ...editDrawer, has_calibration: e.target.checked ? 1 : 0 })} style={{ width: 16, height: 16 }} />
<label htmlFor="edit_has_calib" style={{ fontSize: 14, fontWeight: 500 }}></label>
</div>
</div> </div>
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>

View File

@ -2,10 +2,10 @@
import { useState } from 'react' import { useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, Cpu, ClipboardList } from 'lucide-react' import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, Cpu, ClipboardList } from 'lucide-react'
import { useApi } from '@/lib/hooks' import { useApi, formatTime } from '@/lib/hooks'
interface ModelRow { id: number; name: string; code: string; status: string; description: string; create_date: string } interface ModelRow { id: number; name: string; code: string; status: string; description: string; create_date: string }
interface CLItem { id: number; name: string; required: number; model_code: string; sort_order: number } interface CLItem { id: number; name: string; required: number; standard?: string; model_code: string; sort_order: number }
function getStatusStyle(status: string) { function getStatusStyle(status: string) {
switch (status) { switch (status) {
@ -18,12 +18,27 @@ function getStatusStyle(status: string) {
export default function ModelsPage() { export default function ModelsPage() {
const router = useRouter() const router = useRouter()
const { data: modelsData, refetch: refetchModels } = useApi<ModelRow[]>('/api/models', []) const { data: modelsData, refetch: refetchModels } = useApi<ModelRow[]>('/api/models', [])
const { data: checklistTemplates, refetch: refetchChecklist } = useApi<Record<string, CLItem[]>>('/api/models/checklist', {}) const { data: rawChecklistData, refetch: refetchChecklist } = useApi<CLItem[]>('/api/models/checklist', [])
const { data: refModelStatuses } = useApi<{ code: string; label: string }[]>('/api/reference-values?category=model_status', [])
// 将扁平数组转换为按型号分组的对象结构
const checklistTemplates = Array.isArray(rawChecklistData)
? rawChecklistData.reduce((acc, item) => {
if (!acc[item.model_code]) {
acc[item.model_code] = []
}
acc[item.model_code].push(item)
return acc
}, {} as Record<string, CLItem[]>)
: {}
const modelStatusOptions = Array.isArray(refModelStatuses) && refModelStatuses.length > 0 ? refModelStatuses.map(r => r.label) : ['在产', '停产']
const defaultModelStatus = modelStatusOptions[0] || '在产'
const [modelDrawer, setModelDrawer] = useState(false) const [modelDrawer, setModelDrawer] = useState(false)
const [checklistDrawer, setChecklistDrawer] = useState(false) const [checklistDrawer, setChecklistDrawer] = useState(false)
const [checklistTab, setChecklistTab] = useState('GD30') const [checklistTab, setChecklistTab] = useState('GD30')
const [modelForm, setModelForm] = useState({ name: '', code: '', description:'',status: '在产' }) const [modelForm, setModelForm] = useState({ name: '', code: '', description:'',status: defaultModelStatus })
const [checklistForm, setChecklistForm] = useState({ model: 'GD30', items: [{ name: '', required: true }] }) const [checklistForm, setChecklistForm] = useState({ model: 'GD30', items: [{ name: '', required: true, standard: '' }] })
const [editDrawer, setEditDrawer] = useState(false) const [editDrawer, setEditDrawer] = useState(false)
const [editingModel, setEditingModel] = useState<ModelRow | null>(null) const [editingModel, setEditingModel] = useState<ModelRow | null>(null)
const [editStatus, setEditStatus] = useState('') const [editStatus, setEditStatus] = useState('')
@ -44,14 +59,14 @@ export default function ModelsPage() {
} }
const addChecklistItem = () => { const addChecklistItem = () => {
setChecklistForm({ ...checklistForm, items: [...checklistForm.items, { name: '', required: true }] }) setChecklistForm({ ...checklistForm, items: [...checklistForm.items, { name: '', required: true, standard: '' }] })
} }
const removeChecklistItem = (index: number) => { const removeChecklistItem = (index: number) => {
setChecklistForm({ ...checklistForm, items: checklistForm.items.filter((_, i) => i !== index) }) setChecklistForm({ ...checklistForm, items: checklistForm.items.filter((_, i) => i !== index) })
} }
const updateChecklistItem = (index: number, field: string, value: string | boolean) => { const updateChecklistItem = (index: number, field: string, value: any) => {
const items = [...checklistForm.items] const items = [...checklistForm.items]
items[index] = { ...items[index], [field]: value } items[index] = { ...items[index], [field]: value }
setChecklistForm({ ...checklistForm, items }) setChecklistForm({ ...checklistForm, items })
@ -89,7 +104,7 @@ export default function ModelsPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{modelsData.map(model => ( {Array.isArray(modelsData) && modelsData.map(model => (
<tr key={model.id} style={{ borderBottom: '1px solid #F0F0F0' }}> <tr key={model.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{model.name}</td> <td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{model.name}</td>
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{model.code}</td> <td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{model.code}</td>
@ -97,7 +112,7 @@ export default function ModelsPage() {
<td style={{ padding: '12px 16px' }}> <td style={{ padding: '12px 16px' }}>
<span style={{ ...getStatusStyle(model.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{model.status}</span> <span style={{ ...getStatusStyle(model.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{model.status}</span>
</td> </td>
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{model.create_date}</td> <td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{formatTime(model.create_date)}</td>
<td style={{ padding: '12px 16px' }}> <td style={{ padding: '12px 16px' }}>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
<button <button
@ -107,7 +122,7 @@ export default function ModelsPage() {
<Edit size={14} /> <Edit size={14} />
</button> </button>
<button <button
onClick={() => router.push(`/licenses?model=${model.name}`)} onClick={() => router.push(`/licenses?model=${model.code}`)}
style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
> >
<Key size={14} /> <Key size={14} />
@ -142,7 +157,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={{ 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' }}> <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> <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}> Checklist </h3>
<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 }}> <button onClick={() => { setChecklistForm({ model: modelsData[0]?.code || '', items: [{ name: '', required: true, standard: '' }] }); 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} /> <Plus size={16} />
</button> </button>
</div> </div>
@ -160,11 +175,12 @@ export default function ModelsPage() {
<tr style={{ backgroundColor: '#FAFAFA' }}> <tr style={{ backgroundColor: '#FAFAFA' }}>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', width: 60 }}></th> <th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', width: 60 }}></th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}></th> <th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}></th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}></th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', width: 80 }}></th> <th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', width: 80 }}></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{(checklistTemplates[checklistTab] || []).map((item, i) => ( {Array.isArray(checklistTemplates[checklistTab]) && checklistTemplates[checklistTab].map((item, i) => (
<tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}> <tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
<td style={{ padding: '10px 16px', fontSize: 14, color: 'rgba(0,0,0,0.45)' }}> <td style={{ padding: '10px 16px', fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
@ -173,6 +189,7 @@ export default function ModelsPage() {
</div> </div>
</td> </td>
<td style={{ padding: '10px 16px', fontSize: 14 }}>{item.name}</td> <td style={{ padding: '10px 16px', fontSize: 14 }}>{item.name}</td>
<td style={{ padding: '10px 16px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{item.standard || '-'}</td>
<td style={{ padding: '10px 16px' }}> <td style={{ padding: '10px 16px' }}>
{item.required ? <span style={{ fontSize: 12, color: '#FF4D4F' }}></span> : null} {item.required ? <span style={{ fontSize: 12, color: '#FF4D4F' }}></span> : null}
</td> </td>
@ -203,7 +220,7 @@ export default function ModelsPage() {
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
{['在产', '停产'].map(s => ( {modelStatusOptions.map(s => (
<button key={s} onClick={() => setEditStatus(s)} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: editStatus === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: editStatus === s ? '#eef5f0' : '#fff', color: editStatus === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button> <button key={s} onClick={() => setEditStatus(s)} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: editStatus === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: editStatus === s ? '#eef5f0' : '#fff', color: editStatus === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button>
))} ))}
</div> </div>
@ -244,7 +261,7 @@ export default function ModelsPage() {
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
{['在产', '停产'].map(s => ( {modelStatusOptions.map(s => (
<button key={s} onClick={() => setModelForm({ ...modelForm, status: s })} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: modelForm.status === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: modelForm.status === s ? '#eef5f0' : '#fff', color: modelForm.status === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button> <button key={s} onClick={() => setModelForm({ ...modelForm, status: s })} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: modelForm.status === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: modelForm.status === s ? '#eef5f0' : '#fff', color: modelForm.status === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button>
))} ))}
</div> </div>
@ -282,12 +299,19 @@ export default function ModelsPage() {
</button> </button>
</div> </div>
{checklistForm.items.map((item, i) => ( {checklistForm.items.map((item, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}> <div key={i} style={{ marginBottom: 12, padding: 12, border: '1px solid #F0F0F0', borderRadius: 6, backgroundColor: '#FAFAFA' }}>
<input value={item.name} onChange={e => updateChecklistItem(i, 'name', e.target.value)} placeholder={`检查项 ${i + 1}`} style={{ flex: 1, padding: '6px 10px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 13, flexShrink: 0 }}> <span style={{ fontSize: 13, fontWeight: 500, color: 'rgba(0,0,0,0.65)', minWidth: 60 }}> {i + 1}</span>
<input type="checkbox" checked={item.required} onChange={e => updateChecklistItem(i, 'required', e.target.checked)} /> <input value={item.name} onChange={e => updateChecklistItem(i, 'name', e.target.value)} placeholder="请输入检查项名称" style={{ flex: 1, padding: '6px 10px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} />
</label> <label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 13, flexShrink: 0 }}>
<button onClick={() => removeChecklistItem(i)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4, color: '#FF4D4F' }}><Trash2 size={14} /></button> <input type="checkbox" checked={item.required} onChange={e => updateChecklistItem(i, 'required', e.target.checked)} />
</label>
<button onClick={() => removeChecklistItem(i)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4, color: '#FF4D4F' }}><Trash2 size={14} /></button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, fontWeight: 500, color: 'rgba(0,0,0,0.65)', minWidth: 60 }}></span>
<input value={item.standard || ''} onChange={e => updateChecklistItem(i, 'standard', e.target.value)} placeholder="请输入检查标准(可选)" style={{ flex: 1, padding: '6px 10px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} />
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -1,5 +1,5 @@
'use client' 'use client'
import { useState, useCallback, useEffect } from 'react' import { useState, useCallback, useEffect, useMemo } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { ArrowLeft, Upload, Download, Trash2, Edit, CheckCircle, Camera, X, AlertTriangle, Info, ScanLine, QrCode } from 'lucide-react' import { ArrowLeft, Upload, Download, Trash2, Edit, CheckCircle, Camera, X, AlertTriangle, Info, ScanLine, QrCode } from 'lucide-react'
import { useApi } from '@/lib/hooks' import { useApi } from '@/lib/hooks'
@ -8,8 +8,9 @@ interface ModelRow { id: number; name: string; code: string; status: string }
interface ConfigFile { id: number; name: string; model: string; version: string; status: string } interface ConfigFile { id: number; name: string; model: string; version: string; status: string }
interface LicenseItem { id: number; model: string; modules: string } interface LicenseItem { id: number; model: string; modules: string }
interface BomItem { id: number; name: string; material_name: string; model: string; versions: string[]; qty: number; required: number; need_calibration: number; enforce_version_match: number; model_code: string } interface BomItem { id: number; name: string; material_name: string; model: string; versions: string[]; qty: number; required: number; need_calibration: number; enforce_version_match: number; model_code: string }
interface CLItem { id: number; name: string; required: number; model_code: string; sort_order: number } interface CLItem { id: number; name: string; required: number; standard?: string; model_code: string; sort_order: number }
interface MaterialItem { id: number; sn: string; name: string; category: string; version: string; status: string; calib_status: string; device_sn: string } interface MaterialItem { id: number; sn: string; name: string; category: string; version: string; status: string; calib_status: string; device_sn: string }
interface Device { id: number; sn: string; model: string; type: string; status: string; firmware: string; production_date: string; customer: string; batch: string }
export default function RegistrationPage() { export default function RegistrationPage() {
const router = useRouter() const router = useRouter()
@ -17,12 +18,30 @@ export default function RegistrationPage() {
const { data: allConfigs } = useApi<ConfigFile[]>('/api/config-files', []) const { data: allConfigs } = useApi<ConfigFile[]>('/api/config-files', [])
const { data: allLicenses } = useApi<LicenseItem[]>('/api/licenses', []) const { data: allLicenses } = useApi<LicenseItem[]>('/api/licenses', [])
const { data: allMaterials } = useApi<MaterialItem[]>('/api/materials', []) const { data: allMaterials } = useApi<MaterialItem[]>('/api/materials', [])
const { data: allDevices } = useApi<Device[]>('/api/devices', [])
// 确保数据是数组
const modelsArray = Array.isArray(modelsData) ? modelsData : []
const configsArray = Array.isArray(allConfigs) ? allConfigs : []
const licensesArray = Array.isArray(allLicenses) ? allLicenses : []
const materialsArray = Array.isArray(allMaterials) ? allMaterials : []
const devicesArray = Array.isArray(allDevices) ? allDevices : []
const batchOptions = useMemo(() => {
const batches = new Set<string>()
devicesArray.forEach(d => { if (d.batch) batches.add(d.batch) })
return Array.from(batches).sort((a, b) => b.localeCompare(a))
}, [devicesArray])
const { data: refTestStatuses } = useApi<{ code: string; label: string }[]>('/api/reference-values?category=registration_test_status', [])
const testStatusOptions = Array.isArray(refTestStatuses) ? refTestStatuses.map(r => r.label) : ['装配中', '测试通过', '测试不通过']
const defaultTestStatus = testStatusOptions[1] || '测试通过'
const [deviceModel, setDeviceModel] = useState('') const [deviceModel, setDeviceModel] = useState('')
const [hostSN, setHostSN] = useState('') const [hostSN, setHostSN] = useState('')
const [batchNo, setBatchNo] = useState('') const [batchNo, setBatchNo] = useState('')
const [selectedConfig, setSelectedConfig] = useState('') const [selectedConfig, setSelectedConfig] = useState('')
const [testStatus, setTestStatus] = useState('测试通过') const [testStatus, setTestStatus] = useState(defaultTestStatus)
const [productionDate, setProductionDate] = useState('') const [productionDate, setProductionDate] = useState('')
const [bomList, setBomList] = useState<{ id: number; code: string; name: string; sn: string; model: string; version: string; calibration: string; qty: number }[]>([]) const [bomList, setBomList] = useState<{ id: number; code: string; name: string; sn: string; model: string; version: string; calibration: string; qty: number }[]>([])
const [checklistItems, setChecklistItems] = useState<CLItem[]>([]) const [checklistItems, setChecklistItems] = useState<CLItem[]>([])
@ -34,13 +53,13 @@ export default function RegistrationPage() {
// 自动选中第一个型号 // 自动选中第一个型号
useEffect(() => { useEffect(() => {
if (modelsData.length > 0 && !deviceModel) setDeviceModel(modelsData[0].name) if (modelsArray.length > 0 && !deviceModel) setDeviceModel(modelsArray[0].name)
}, [modelsData, deviceModel]) }, [modelsArray, deviceModel])
// 切换型号时加载BOM和Checklist // 切换型号时加载BOM和Checklist
useEffect(() => { useEffect(() => {
if (!deviceModel) return if (!deviceModel) return
const model = modelsData.find(m => m.name === deviceModel) const model = modelsArray.find(m => m.name === deviceModel)
if (!model) return if (!model) return
// 加载BOM // 加载BOM
fetch(`/api/models/bom?model=${model.code}`).then(r => r.json()).then((items: BomItem[]) => { fetch(`/api/models/bom?model=${model.code}`).then(r => r.json()).then((items: BomItem[]) => {
@ -59,15 +78,15 @@ export default function RegistrationPage() {
setCheckedItems([]) setCheckedItems([])
}) })
// 自动选配置文件 // 自动选配置文件
const cfgs = allConfigs.filter(c => c.model === deviceModel && c.status === '生效') const cfgs = configsArray.filter(c => c.model === deviceModel && c.status === '生效')
if (cfgs.length > 0) setSelectedConfig(cfgs[0].name) if (cfgs.length > 0) setSelectedConfig(cfgs[0].name)
else setSelectedConfig('') else setSelectedConfig('')
}, [deviceModel, modelsData, allConfigs]) }, [deviceModel, modelsArray, configsArray])
// 当前型号的配置文件和授权 // 当前型号的配置文件和授权
const modelConfigs = allConfigs.filter(c => c.model === deviceModel) const modelConfigs = configsArray.filter(c => c.model === deviceModel)
const modelCode = modelsData.find(m => m.name === deviceModel)?.code || '' const modelCode = modelsArray.find(m => m.name === deviceModel)?.code || ''
const modelLicense = allLicenses.find(l => l.model === modelCode || l.model === deviceModel) const modelLicense = licensesArray.find(l => l.model === modelCode || l.model === deviceModel)
const hasMatch = modelConfigs.length > 0 || !!modelLicense const hasMatch = modelConfigs.length > 0 || !!modelLicense
const completedCount = checkedItems.length const completedCount = checkedItems.length
@ -89,17 +108,17 @@ export default function RegistrationPage() {
const handleSnChange = useCallback((id: number, sn: string) => { const handleSnChange = useCallback((id: number, sn: string) => {
setBomList(prev => prev.map(b => { setBomList(prev => prev.map(b => {
if (b.id !== id) return b if (b.id !== id) return b
const matched = allMaterials.find(m => m.sn === sn) const matched = materialsArray.find(m => m.sn === sn)
if (matched) { if (matched) {
return { ...b, sn, version: matched.version || b.version, calibration: matched.calib_status === '合格' ? '已校准' : matched.calib_status === '待校准' ? '待校准' : '无需校准' } return { ...b, sn, version: matched.version || b.version, calibration: matched.calib_status === '合格' ? '已校准' : matched.calib_status === '待校准' ? '待校准' : '无需校准' }
} }
return { ...b, sn } return { ...b, sn }
})) }))
}, [allMaterials]) }, [materialsArray])
/** 获取该BOM项分类下可用的物料在库状态 */ /** 获取该BOM项分类下可用的物料在库状态 */
const getAvailableMaterials = (bomName: string) => { const getAvailableMaterials = (bomName: string) => {
return allMaterials.filter(m => (m.category === bomName || m.name === bomName) && m.status === '在库' && m.device_sn === '-') return materialsArray.filter(m => (m.category === bomName || m.name === bomName) && m.status === '在库' && m.device_sn === '-')
} }
return ( return (
@ -123,8 +142,8 @@ export default function RegistrationPage() {
<div> <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 }}><span style={{ color: '#FF4D4F' }}>*</span> </label>
<select value={deviceModel} onChange={e => setDeviceModel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}> <select value={deviceModel} onChange={e => setDeviceModel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
{modelsData.map(m => <option key={m.id} value={m.name}>{m.name}</option>)} {modelsArray.map(m => <option key={m.id} value={m.name}>{m.name}</option>)}
{modelsData.length === 0 && <option value=""></option>} {modelsArray.length === 0 && <option value=""></option>}
</select> </select>
</div> </div>
<div> <div>
@ -141,9 +160,7 @@ export default function RegistrationPage() {
<div style={{ display: 'flex', gap: 6 }}> <div style={{ display: 'flex', gap: 6 }}>
<select value={batchNo} onChange={e => setBatchNo(e.target.value)} style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}> <select value={batchNo} onChange={e => setBatchNo(e.target.value)} style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
<option value=""></option> <option value=""></option>
<option value="BATCH-2025-Q1-001">BATCH-2025-Q1-001</option> {batchOptions.map(b => <option key={b} value={b}>{b}</option>)}
<option value="BATCH-2025-Q1-002">BATCH-2025-Q1-002</option>
<option value="BATCH-2025-Q2-001">BATCH-2025-Q2-001</option>
</select> </select>
<input value={batchNo} onChange={e => setBatchNo(e.target.value)} placeholder="手动输入" style={{ width: 140, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} /> <input value={batchNo} onChange={e => setBatchNo(e.target.value)} placeholder="手动输入" style={{ width: 140, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
</div> </div>
@ -162,9 +179,7 @@ export default function RegistrationPage() {
<div> <div>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}></label> <label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}></label>
<select value={testStatus} onChange={e => setTestStatus(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}> <select value={testStatus} onChange={e => setTestStatus(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
<option value="装配中"></option> {testStatusOptions.map(s => <option key={s} value={s}>{s}</option>)}
<option value="测试通过"></option>
<option value="测试不通过"></option>
</select> </select>
</div> </div>
<div> <div>
@ -210,7 +225,7 @@ export default function RegistrationPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{bomList.map(item => ( {Array.isArray(bomList) && bomList.map(item => (
<tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}> <tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
<td style={{ padding: '10px 16px', fontSize: 13 }}>{item.name}</td> <td style={{ padding: '10px 16px', fontSize: 13 }}>{item.name}</td>
<td style={{ padding: '10px 16px', fontSize: 13, fontWeight: 500 }}> <td style={{ padding: '10px 16px', fontSize: 13, fontWeight: 500 }}>
@ -252,7 +267,8 @@ export default function RegistrationPage() {
</table> </table>
{/* 采集板版本一致性检查 */} {/* 采集板版本一致性检查 */}
{(() => { {(() => {
const acqBoards = bomList.filter(b => b.name === '采集板') const bomListArray = Array.isArray(bomList) ? bomList : []
const acqBoards = bomListArray.filter(b => b.name === '采集板')
const versions = [...new Set(acqBoards.map(b => b.version))] const versions = [...new Set(acqBoards.map(b => b.version))]
if (acqBoards.length > 1 && versions.length > 1) { if (acqBoards.length > 1 && versions.length > 1) {
return ( return (
@ -316,9 +332,10 @@ export default function RegistrationPage() {
<button onClick={() => router.back()} style={{ padding: '8px 24px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}></button> <button onClick={() => router.back()} style={{ padding: '8px 24px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}></button>
<button onClick={async () => { <button onClick={async () => {
if (!hostSN || !deviceModel) return if (!hostSN || !deviceModel) return
await fetch('/api/devices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sn: hostSN, model: deviceModel, type: deviceModel, status: testStatus === '测试通过' ? '已激活' : '装配中', firmware: '', production_date: productionDate || new Date().toISOString(), customer: '-', batch: batchNo }) }) await fetch('/api/devices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sn: hostSN, model: deviceModel, type: deviceModel, status: testStatus === '测试通过' ? '已激活' : '装配中', firmware: '', production_date: productionDate ? Math.floor(new Date(productionDate).getTime() / 1000) : Math.floor(Date.now() / 1000), customer: '-', batch: batchNo }) })
// 保存BOM记录和操作日志 // 保存BOM记录和操作日志
const bomItems = bomList.filter(b => b.sn).map(b => ({ name: b.name, sn: b.sn, model: b.model, version: b.version, calibration: b.calibration })) const bomListArray = Array.isArray(bomList) ? bomList : []
const bomItems = bomListArray.filter(b => b.sn).map(b => ({ name: b.name, sn: b.sn, model: b.model, version: b.version, calibration: b.calibration }))
await fetch('/api/devices/detail', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_sn: hostSN, bom_items: bomItems, log: { action: '设备登记', operator: '', detail: `完成设备登记,型号 ${deviceModel},配置文件 ${selectedConfig}` } }) }) await fetch('/api/devices/detail', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_sn: hostSN, bom_items: bomItems, log: { action: '设备登记', operator: '', detail: `完成设备登记,型号 ${deviceModel},配置文件 ${selectedConfig}` } }) })
router.push('/devices') router.push('/devices')
}} disabled={!hostSN || !deviceModel} style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: hostSN && deviceModel ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: hostSN && deviceModel ? 'pointer' : 'not-allowed', fontSize: 14 }}></button> }} disabled={!hostSN || !deviceModel} style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: hostSN && deviceModel ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: hostSN && deviceModel ? 'pointer' : 'not-allowed', fontSize: 14 }}></button>