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:
parent
9365231046
commit
db871f2ebd
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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="文件大小")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue