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
.next
# Dependencies
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/
/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
# 数据库文件路径(放在当前目录下,轻量本地运行)
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:
@ -20,6 +35,7 @@ def get_db_connection() -> sqlite3.Connection:
"""
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
@ -40,41 +56,843 @@ def get_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:
# 1. 设备表(统一前后端结构)
db.execute("""
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 '待激活',
activated_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
firmware TEXT DEFAULT '',
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()
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():
"""
插入演示数据方便新手直接体验接口
如果已存在相同 SN则忽略INSERT OR IGNORE
"""
# 将演示日期转换为时间戳
demo_date_ts = int(datetime.strptime("2026-05-07", "%Y-%m-%d").timestamp())
demo_devices = [
("GD30-20260507-001", "待激活"),
("GD30-20260507-002", "待激活"),
("MT-20260507-003", "已禁用"),
("GD30-20260507-001", "GD30-2024", "标准型", "待激活", "", demo_date_ts, "华东地质局", "B2026-05"),
("GD30-20260507-002", "GD30-2024", "标准型", "待激活", "", demo_date_ts, "华北勘探院", "B2026-05"),
("MT-20260507-003", "MT-2024", "标准型", "已禁用", "", demo_date_ts, "-", "B2026-05"),
]
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(
"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()
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):
@ -23,6 +23,7 @@ class LicenseGenerateRequest(BaseModel):
sn: str = Field(..., description="目标设备 SN")
modules: List[str] = Field(default=[], description="激活的模块列表,如 ['1D', '2D']")
valid_days: int = Field(default=365, ge=1, le=3650, description="授权有效期(天),默认一年")
config_id: Optional[int] = Field(None, description="关联的配置文件 ID授权文件中会嵌入该配置")
class ActivateReportRequest(BaseModel):
@ -31,10 +32,6 @@ class ActivateReportRequest(BaseModel):
status: str = Field(..., pattern="^(已激活|激活失败)$", description="上报状态:已激活 或 激活失败")
# ═══════════════════════════════════════════════════════════════
# 响应模型(服务端返回)
# ═══════════════════════════════════════════════════════════════
class DeviceInfo(BaseModel):
"""设备信息响应"""
sn: str
@ -75,3 +72,267 @@ class ActivateResponse(BaseModel):
new_status: str
activated_at: Optional[str] = None
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 { getDb } from '@/lib/db'
import { seedIfEmpty } from '@/lib/seed'
import { API_BASE } from '@/lib/api-proxy'
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)
const res = await fetch(`${API_BASE}/api/material-categories`)
const data = await res.json()
return NextResponse.json(data)
}
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 })
const res = await fetch(`${API_BASE}/api/material-categories`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data)
}
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 })
const res = await fetch(`${API_BASE}/api/material-categories`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data)
}
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 })
const url = new URL(req.url)
const id = url.searchParams.get('id')
if (!id) {
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() {
const { data: categories, loading, refetch } = useApi<MaterialCategory[]>('/api/material-categories', [])
// 确保数据是数组
const categoriesArray = Array.isArray(categories) ? 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: '启用' })
@ -31,7 +35,7 @@ export default function MaterialCategoriesPage() {
refetch(); setEditDrawer(null)
}
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()
}
@ -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={{ 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>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
@ -61,7 +65,7 @@ export default function MaterialCategoriesPage() {
))}
</tr></thead>
<tbody>
{categories.map(cat => (
{categoriesArray.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>
@ -88,37 +92,47 @@ export default function MaterialCategoriesPage() {
{/* 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 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={{ 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>
<button onClick={() => { setAddDrawer(false); resetForm() }} 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' }} />
<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' }} />
<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 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' }} />
<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 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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<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 htmlFor="has_firmware" style={{ fontSize: 14, fontWeight: 500 }}></label>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<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 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>
<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} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}></button>
</div>
</div>
</div>
@ -136,23 +150,25 @@ export default function MaterialCategoriesPage() {
<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' }} />
<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 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' }} />
<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 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' }} />
<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 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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<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 htmlFor="edit_has_firmware" style={{ fontSize: 14, fontWeight: 500 }}></label>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<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 style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>

View File

@ -2,10 +2,10 @@
import { useState } from 'react'
import { useRouter } from 'next/navigation'
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 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) {
switch (status) {
@ -18,12 +18,27 @@ function getStatusStyle(status: string) {
export default function ModelsPage() {
const router = useRouter()
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 [checklistDrawer, setChecklistDrawer] = useState(false)
const [checklistTab, setChecklistTab] = useState('GD30')
const [modelForm, setModelForm] = useState({ name: '', code: '', description:'',status: '在产' })
const [checklistForm, setChecklistForm] = useState({ model: 'GD30', items: [{ name: '', required: true }] })
const [modelForm, setModelForm] = useState({ name: '', code: '', description:'',status: defaultModelStatus })
const [checklistForm, setChecklistForm] = useState({ model: 'GD30', items: [{ name: '', required: true, standard: '' }] })
const [editDrawer, setEditDrawer] = useState(false)
const [editingModel, setEditingModel] = useState<ModelRow | null>(null)
const [editStatus, setEditStatus] = useState('')
@ -44,14 +59,14 @@ export default function ModelsPage() {
}
const addChecklistItem = () => {
setChecklistForm({ ...checklistForm, items: [...checklistForm.items, { name: '', required: true }] })
setChecklistForm({ ...checklistForm, items: [...checklistForm.items, { name: '', required: true, standard: '' }] })
}
const removeChecklistItem = (index: number) => {
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]
items[index] = { ...items[index], [field]: value }
setChecklistForm({ ...checklistForm, items })
@ -89,7 +104,7 @@ export default function ModelsPage() {
</tr>
</thead>
<tbody>
{modelsData.map(model => (
{Array.isArray(modelsData) && modelsData.map(model => (
<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, color: 'rgba(0,0,0,0.65)' }}>{model.code}</td>
@ -97,7 +112,7 @@ export default function ModelsPage() {
<td style={{ padding: '12px 16px' }}>
<span style={{ ...getStatusStyle(model.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{model.status}</span>
</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' }}>
<div style={{ display: 'flex', gap: 12 }}>
<button
@ -107,7 +122,7 @@ export default function ModelsPage() {
<Edit size={14} />
</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 }}
>
<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={{ 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={() => { 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} />
</button>
</div>
@ -160,11 +175,12 @@ export default function ModelsPage() {
<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' }}></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>
</tr>
</thead>
<tbody>
{(checklistTemplates[checklistTab] || []).map((item, i) => (
{Array.isArray(checklistTemplates[checklistTab]) && checklistTemplates[checklistTab].map((item, i) => (
<tr key={item.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
<td style={{ padding: '10px 16px', fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
@ -173,6 +189,7 @@ export default function ModelsPage() {
</div>
</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' }}>
{item.required ? <span style={{ fontSize: 12, color: '#FF4D4F' }}></span> : null}
</td>
@ -203,7 +220,7 @@ export default function ModelsPage() {
<div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>
<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>
))}
</div>
@ -244,7 +261,7 @@ export default function ModelsPage() {
<div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>
<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>
))}
</div>
@ -282,12 +299,19 @@ export default function ModelsPage() {
</button>
</div>
{checklistForm.items.map((item, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<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' }} />
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 13, flexShrink: 0 }}>
<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 key={i} style={{ marginBottom: 12, padding: 12, border: '1px solid #F0F0F0', borderRadius: 6, backgroundColor: '#FAFAFA' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 13, fontWeight: 500, color: 'rgba(0,0,0,0.65)', minWidth: 60 }}> {i + 1}</span>
<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 style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 13, flexShrink: 0 }}>
<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>

View File

@ -1,5 +1,5 @@
'use client'
import { useState, useCallback, useEffect } from 'react'
import { useState, useCallback, useEffect, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { ArrowLeft, Upload, Download, Trash2, Edit, CheckCircle, Camera, X, AlertTriangle, Info, ScanLine, QrCode } from 'lucide-react'
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 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 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 Device { id: number; sn: string; model: string; type: string; status: string; firmware: string; production_date: string; customer: string; batch: string }
export default function RegistrationPage() {
const router = useRouter()
@ -17,12 +18,30 @@ export default function RegistrationPage() {
const { data: allConfigs } = useApi<ConfigFile[]>('/api/config-files', [])
const { data: allLicenses } = useApi<LicenseItem[]>('/api/licenses', [])
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 [hostSN, setHostSN] = useState('')
const [batchNo, setBatchNo] = useState('')
const [selectedConfig, setSelectedConfig] = useState('')
const [testStatus, setTestStatus] = useState('测试通过')
const [testStatus, setTestStatus] = useState(defaultTestStatus)
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 [checklistItems, setChecklistItems] = useState<CLItem[]>([])
@ -34,13 +53,13 @@ export default function RegistrationPage() {
// 自动选中第一个型号
useEffect(() => {
if (modelsData.length > 0 && !deviceModel) setDeviceModel(modelsData[0].name)
}, [modelsData, deviceModel])
if (modelsArray.length > 0 && !deviceModel) setDeviceModel(modelsArray[0].name)
}, [modelsArray, deviceModel])
// 切换型号时加载BOM和Checklist
useEffect(() => {
if (!deviceModel) return
const model = modelsData.find(m => m.name === deviceModel)
const model = modelsArray.find(m => m.name === deviceModel)
if (!model) return
// 加载BOM
fetch(`/api/models/bom?model=${model.code}`).then(r => r.json()).then((items: BomItem[]) => {
@ -59,15 +78,15 @@ export default function RegistrationPage() {
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)
else setSelectedConfig('')
}, [deviceModel, modelsData, allConfigs])
}, [deviceModel, modelsArray, configsArray])
// 当前型号的配置文件和授权
const modelConfigs = allConfigs.filter(c => c.model === deviceModel)
const modelCode = modelsData.find(m => m.name === deviceModel)?.code || ''
const modelLicense = allLicenses.find(l => l.model === modelCode || l.model === deviceModel)
const modelConfigs = configsArray.filter(c => c.model === deviceModel)
const modelCode = modelsArray.find(m => m.name === deviceModel)?.code || ''
const modelLicense = licensesArray.find(l => l.model === modelCode || l.model === deviceModel)
const hasMatch = modelConfigs.length > 0 || !!modelLicense
const completedCount = checkedItems.length
@ -89,17 +108,17 @@ export default function RegistrationPage() {
const handleSnChange = useCallback((id: number, sn: string) => {
setBomList(prev => prev.map(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) {
return { ...b, sn, version: matched.version || b.version, calibration: matched.calib_status === '合格' ? '已校准' : matched.calib_status === '待校准' ? '待校准' : '无需校准' }
}
return { ...b, sn }
}))
}, [allMaterials])
}, [materialsArray])
/** 获取该BOM项分类下可用的物料在库状态 */
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 (
@ -123,8 +142,8 @@ export default function RegistrationPage() {
<div>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> </label>
<select value={deviceModel} onChange={e => setDeviceModel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
{modelsData.map(m => <option key={m.id} value={m.name}>{m.name}</option>)}
{modelsData.length === 0 && <option value=""></option>}
{modelsArray.map(m => <option key={m.id} value={m.name}>{m.name}</option>)}
{modelsArray.length === 0 && <option value=""></option>}
</select>
</div>
<div>
@ -141,9 +160,7 @@ export default function RegistrationPage() {
<div style={{ display: 'flex', gap: 6 }}>
<select value={batchNo} onChange={e => setBatchNo(e.target.value)} style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
<option value=""></option>
<option value="BATCH-2025-Q1-001">BATCH-2025-Q1-001</option>
<option value="BATCH-2025-Q1-002">BATCH-2025-Q1-002</option>
<option value="BATCH-2025-Q2-001">BATCH-2025-Q2-001</option>
{batchOptions.map(b => <option key={b} value={b}>{b}</option>)}
</select>
<input value={batchNo} onChange={e => setBatchNo(e.target.value)} placeholder="手动输入" style={{ width: 140, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
</div>
@ -162,9 +179,7 @@ export default function RegistrationPage() {
<div>
<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 }}>
<option value="装配中"></option>
<option value="测试通过"></option>
<option value="测试不通过"></option>
{testStatusOptions.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
@ -210,7 +225,7 @@ export default function RegistrationPage() {
</tr>
</thead>
<tbody>
{bomList.map(item => (
{Array.isArray(bomList) && bomList.map(item => (
<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, fontWeight: 500 }}>
@ -252,7 +267,8 @@ export default function RegistrationPage() {
</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))]
if (acqBoards.length > 1 && versions.length > 1) {
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={async () => {
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记录和操作日志
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}` } }) })
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>