2、调整了板卡管理的页面,以及校准文件
This commit is contained in:
parent
168772d4a5
commit
95259a701f
|
|
@ -0,0 +1,885 @@
|
|||
# 设计文档:前后端分离 — Java API 后端与前端集成
|
||||
|
||||
## 概述
|
||||
|
||||
本设计文档描述将现有 Next.js 16 前端生产管理子系统从硬编码模拟数据迁移到真实 RESTful API 的完整技术方案。涵盖 Java Spring Boot 3.3.6 后端多模块项目架构、PostgreSQL 数据库设计、DDD 分层实现,以及前端 API 集成层的构建。
|
||||
|
||||
系统为地球物理仪器(高密度电法仪等)的全生命周期管理平台,包含设备管理、板卡管理、固件库、校准管理、校准文件管理、授权管理、配置文件管理、维修工单、报废回收等核心业务模块。
|
||||
|
||||
### 设计目标
|
||||
|
||||
- 搭建符合 DDD 分层架构的 Spring Boot 多模块 Maven 项目
|
||||
- 设计 PostgreSQL `dev` schema 下的完整数据库表结构
|
||||
- 为所有前端页面提供对应的 RESTful API 端点
|
||||
- 在前端建立统一的 API 调用层,替换所有硬编码模拟数据
|
||||
- 前后端采用统一的分页、筛选、错误响应规范
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Frontend["前端 Next.js 16"]
|
||||
Pages["页面组件<br/>devices / boards / firmware / ..."]
|
||||
ApiLayer["API 集成层<br/>src/lib/api/"]
|
||||
Pages --> ApiLayer
|
||||
end
|
||||
|
||||
subgraph Backend["后端 Spring Boot 3.3.6"]
|
||||
subgraph ApiAdmin["api-admin 模块"]
|
||||
Controllers["REST Controllers<br/>/api/admin/*"]
|
||||
end
|
||||
subgraph ApiPortal["api-portal 模块"]
|
||||
PortalControllers["REST Controllers<br/>/api/portal/*<br/>(无需鉴权)"]
|
||||
end
|
||||
subgraph Business["business 模块"]
|
||||
subgraph Device["device 子模块"]
|
||||
Interfaces["interfaces 层<br/>Controller / VO / Assembler"]
|
||||
Application["application 层<br/>AppService / Command / Query"]
|
||||
Domain["domain 层<br/>Entity / Repository接口"]
|
||||
Infrastructure["infrastructure 层<br/>Mapper / DO / Repository实现"]
|
||||
end
|
||||
end
|
||||
subgraph Common["common 模块"]
|
||||
Response["统一响应体 R<T>"]
|
||||
Exception["全局异常处理"]
|
||||
Audit["审计字段自动填充"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph DB["PostgreSQL 12.14"]
|
||||
DevSchema["dev schema<br/>devices / device_models / board_types / board_cards / calibration_files / ..."]
|
||||
end
|
||||
|
||||
ApiLayer -->|HTTP REST| Controllers
|
||||
ApiLayer -->|HTTP REST| PortalControllers
|
||||
Controllers --> Interfaces
|
||||
PortalControllers --> Interfaces
|
||||
Interfaces --> Application
|
||||
Application --> Domain
|
||||
Domain --> Infrastructure
|
||||
Infrastructure -->|MyBatis-Plus| DevSchema
|
||||
```
|
||||
|
||||
### 后端项目结构
|
||||
|
||||
```
|
||||
apps/geo-bps-api/
|
||||
├── pom.xml # 父 POM
|
||||
├── api-admin/
|
||||
│ ├── pom.xml
|
||||
│ └── src/main/java/com/geomative/bps/admin/
|
||||
│ ├── ApiAdminApplication.java
|
||||
│ └── config/
|
||||
│ ├── CorsConfig.java # CORS 配置
|
||||
│ └── WebMvcConfig.java
|
||||
├── api-portal/
|
||||
│ ├── pom.xml
|
||||
│ └── src/main/java/com/geomative/bps/portal/
|
||||
│ ├── ApiPortalApplication.java
|
||||
│ └── config/
|
||||
│ ├── CorsConfig.java # CORS 配置
|
||||
│ └── WebMvcConfig.java # 文件上传大小限制等
|
||||
├── business/
|
||||
│ ├── pom.xml # pom packaging
|
||||
│ └── device/
|
||||
│ ├── pom.xml
|
||||
│ └── src/main/java/com/geomative/bps/device/
|
||||
│ ├── interfaces/ # Controller + VO
|
||||
│ ├── application/ # AppService + Command + Query
|
||||
│ ├── domain/ # Entity + Repository接口
|
||||
│ └── infrastructure/ # Mapper + DO + Repository实现
|
||||
└── common/
|
||||
├── pom.xml
|
||||
└── src/main/java/com/geomative/bps/common/
|
||||
├── response/
|
||||
│ └── R.java # 统一响应体
|
||||
├── exception/
|
||||
│ ├── BizException.java
|
||||
│ └── GlobalExceptionHandler.java
|
||||
├── mybatis/
|
||||
│ └── AuditMetaObjectHandler.java
|
||||
└── page/
|
||||
└── PageResult.java # 分页响应
|
||||
```
|
||||
|
||||
### 前端 API 集成层结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ └── api/
|
||||
│ ├── client.ts # Axios 实例,统一配置
|
||||
│ ├── types.ts # 通用响应/分页类型
|
||||
│ ├── deviceApi.ts # 设备管理 API
|
||||
│ ├── modelApi.ts # 设备型号 API
|
||||
│ ├── boardApi.ts # 板卡管理 API
|
||||
│ ├── firmwareApi.ts # 固件库 API
|
||||
│ ├── calibrationApi.ts # 校准管理 API
|
||||
│ ├── calibrationFileApi.ts # 校准文件 API
|
||||
│ ├── configFileApi.ts # 配置文件 API
|
||||
│ ├── licenseApi.ts # 授权管理 API
|
||||
│ ├── repairApi.ts # 维修工单 API
|
||||
│ ├── scrapApi.ts # 报废管理 API
|
||||
│ └── dashboardApi.ts # 首页统计 API
|
||||
└── hooks/
|
||||
└── useApi.ts # 通用数据获取 Hook
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 统一响应体
|
||||
|
||||
```java
|
||||
public class R<T> {
|
||||
private int code; // 业务状态码,0=成功
|
||||
private String message; // 提示信息
|
||||
private T data; // 响应数据
|
||||
|
||||
public static <T> R<T> ok(T data) { ... }
|
||||
public static <T> R<T> fail(int code, String message) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应体
|
||||
|
||||
```java
|
||||
public class PageResult<T> {
|
||||
private long total; // 总记录数
|
||||
private int page; // 当前页码
|
||||
private int pageSize; // 每页条数
|
||||
private List<T> records; // 数据列表
|
||||
}
|
||||
```
|
||||
|
||||
### 统一分页请求参数
|
||||
|
||||
所有列表接口统一使用:
|
||||
- `page`:页码,从 1 开始,默认 1
|
||||
- `pageSize`:每页条数,默认 10
|
||||
|
||||
### API 端点清单
|
||||
|
||||
| 模块 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 设备 | GET | `/api/admin/devices` | 分页设备列表(筛选:型号/状态/日期/SN/批次) |
|
||||
| 设备 | GET | `/api/admin/devices/{sn}` | 设备详情(含授权、子设备、固件) |
|
||||
| 设备 | POST | `/api/admin/devices` | 创建设备(设备登记) |
|
||||
| 设备 | GET | `/api/admin/devices/batches` | 生产批次列表 |
|
||||
| 型号 | GET | `/api/admin/device-models` | 设备型号列表 |
|
||||
| 型号 | POST | `/api/admin/device-models` | 创建设备型号 |
|
||||
| 型号 | GET | `/api/admin/checklist-templates` | Checklist 模板列表 |
|
||||
| 型号 | POST | `/api/admin/checklist-templates` | 创建 Checklist 模板 |
|
||||
| 板卡 | GET | `/api/admin/board-types` | 板卡型号列表(筛选:类型) |
|
||||
| 板卡 | GET | `/api/admin/board-types/{id}` | 板卡详情 |
|
||||
| 板卡 | POST | `/api/admin/board-types` | 创建板卡型号 |
|
||||
| 固件 | GET | `/api/admin/firmware` | 固件版本列表(筛选:类型/板卡型号) |
|
||||
| 固件 | POST | `/api/admin/firmware` | 创建固件版本 |
|
||||
| 固件 | GET | `/api/admin/firmware/{id}/download` | 下载固件文件 |
|
||||
| 校准 | GET | `/api/admin/calibrations` | 校准记录列表(筛选:SN/状态/人员) |
|
||||
| 校准 | GET | `/api/admin/calibrations/{id}` | 校准详情 |
|
||||
| 校准 | POST | `/api/admin/calibrations/import` | 批量导入校准记录 |
|
||||
| 校准文件 | POST | `/api/portal/calibration-files/upload` | 上传校准文件(无需鉴权,上位机调用) |
|
||||
| 校准文件 | GET | `/api/admin/board-cards/{id}/calibration-files` | 指定采集板的校准文件列表(按上传时间倒序) |
|
||||
| 校准文件 | GET | `/api/admin/calibration-files/{id}/download` | 下载校准文件 |
|
||||
| 配置 | GET | `/api/admin/config-files` | 配置文件列表(筛选:型号/版本/关键字) |
|
||||
| 配置 | GET | `/api/admin/config-files/{id}` | 配置文件详情 |
|
||||
| 配置 | POST | `/api/admin/config-files` | 创建配置文件 |
|
||||
| 配置 | PUT | `/api/admin/config-files/{id}` | 更新配置文件 |
|
||||
| 配置 | DELETE | `/api/admin/config-files/{id}` | 逻辑删除配置文件 |
|
||||
| 授权 | GET | `/api/admin/licenses` | 授权列表(筛选:型号/状态) |
|
||||
| 授权 | POST | `/api/admin/licenses` | 创建授权记录 |
|
||||
| 授权 | PUT | `/api/admin/licenses/{id}` | 更新授权记录 |
|
||||
| 授权 | PUT | `/api/admin/licenses/{id}/disable` | 停用授权 |
|
||||
| 维修 | GET | `/api/admin/repair-orders` | 工单列表(筛选:状态/优先级/人员/日期/SN) |
|
||||
| 维修 | GET | `/api/admin/repair-orders/{id}` | 工单详情 |
|
||||
| 维修 | POST | `/api/admin/repair-orders` | 创建工单 |
|
||||
| 维修 | PUT | `/api/admin/repair-orders/{id}/process` | 处理工单 |
|
||||
| 维修 | PUT | `/api/admin/repair-orders/{id}/close` | 关闭工单 |
|
||||
| 报废 | GET | `/api/admin/scrap-records` | 报废记录列表(筛选:SN/状态/日期) |
|
||||
| 报废 | GET | `/api/admin/scrap-records/{id}` | 报废详情 |
|
||||
| 报废 | PUT | `/api/admin/scrap-records/{id}/approve` | 审批通过 |
|
||||
| 报废 | PUT | `/api/admin/scrap-records/{id}/reject` | 驳回 |
|
||||
| 报废 | PUT | `/api/admin/scrap-records/{id}/recover` | 物料回收入库 |
|
||||
| 报废 | GET | `/api/admin/scrap-records/stats` | 报废统计 |
|
||||
| 首页 | GET | `/api/admin/dashboard/metrics` | 统计指标 |
|
||||
| 首页 | GET | `/api/admin/dashboard/device-status` | 设备状态分布 |
|
||||
| 首页 | GET | `/api/admin/dashboard/tasks` | 待处理任务 |
|
||||
|
||||
### 前端 API 客户端
|
||||
|
||||
```typescript
|
||||
// src/lib/api/client.ts
|
||||
import axios from 'axios';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
|
||||
timeout: 5000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// 响应拦截器:统一错误处理
|
||||
apiClient.interceptors.response.use(
|
||||
(res) => res.data,
|
||||
(error) => {
|
||||
const message = error.response?.data?.message || '请求失败,请稍后重试';
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
```
|
||||
|
||||
### 前端通用类型
|
||||
|
||||
```typescript
|
||||
// src/lib/api/types.ts
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
records: T[];
|
||||
}
|
||||
|
||||
export interface PageParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### ER 关系图
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
device_models ||--o{ devices : "型号关联"
|
||||
device_models ||--o{ checklist_templates : "模板关联"
|
||||
device_models ||--o{ config_files : "配置关联"
|
||||
device_models ||--o{ licenses : "授权关联"
|
||||
checklist_templates ||--o{ checklist_items : "包含检查项"
|
||||
devices ||--o{ device_boards : "装配板卡"
|
||||
devices ||--o{ repair_orders : "维修工单"
|
||||
devices ||--o{ scrap_records : "报废记录"
|
||||
board_types ||--o{ device_boards : "板卡实例"
|
||||
board_types ||--o{ firmware_versions : "固件版本"
|
||||
board_types ||--o{ calibration_records : "校准记录"
|
||||
board_cards ||--o{ calibration_files : "校准文件"
|
||||
|
||||
device_models {
|
||||
varchar(64) id PK
|
||||
varchar(100) name
|
||||
varchar(50) code UK
|
||||
varchar(20) status
|
||||
varchar(500) description
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
devices {
|
||||
varchar(64) id PK
|
||||
varchar(100) sn UK
|
||||
varchar(64) model_id FK
|
||||
varchar(100) model_name
|
||||
varchar(20) status
|
||||
varchar(20) firmware_version
|
||||
timestamp production_date
|
||||
varchar(200) customer
|
||||
varchar(20) batch
|
||||
varchar(64) operator
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
board_types {
|
||||
varchar(64) id PK
|
||||
varchar(50) type
|
||||
varchar(50) version UK
|
||||
varchar(20) latest_firmware
|
||||
date production_date
|
||||
varchar(20) status
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
device_boards {
|
||||
varchar(64) id PK
|
||||
varchar(64) device_id FK
|
||||
varchar(100) device_sn
|
||||
varchar(64) board_type_id FK
|
||||
varchar(100) board_sn
|
||||
varchar(50) board_name
|
||||
varchar(50) board_model
|
||||
varchar(20) calibration_status
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
firmware_versions {
|
||||
varchar(64) id PK
|
||||
varchar(20) version
|
||||
varchar(64) board_type_id FK
|
||||
varchar(50) board_version
|
||||
varchar(50) firmware_type
|
||||
date release_date
|
||||
varchar(20) status
|
||||
varchar(20) file_size
|
||||
int download_count
|
||||
varchar(200) hw_range
|
||||
varchar(20) upgrade_type
|
||||
boolean signed
|
||||
varchar(64) md5
|
||||
varchar(128) sha256
|
||||
text release_notes
|
||||
varchar(500) file_path
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
calibration_records {
|
||||
varchar(64) id PK
|
||||
varchar(100) board_sn
|
||||
varchar(64) board_type_id FK
|
||||
varchar(50) board_version
|
||||
date calibration_date
|
||||
date expiry_date
|
||||
varchar(100) calibrator
|
||||
varchar(20) status
|
||||
int channel_count
|
||||
decimal overall_deviation
|
||||
jsonb channel_results
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
config_files {
|
||||
varchar(64) id PK
|
||||
varchar(100) name
|
||||
varchar(64) model_id FK
|
||||
varchar(100) model_name
|
||||
varchar(20) version
|
||||
varchar(20) status
|
||||
jsonb transmission_params
|
||||
jsonb acquisition_params
|
||||
jsonb protection_params
|
||||
jsonb network_params
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
licenses {
|
||||
varchar(64) id PK
|
||||
varchar(64) model_id FK
|
||||
varchar(50) model_code
|
||||
jsonb modules
|
||||
varchar(20) validity_type
|
||||
date expiry_date
|
||||
varchar(20) status
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
repair_orders {
|
||||
varchar(64) id PK
|
||||
varchar(50) order_no UK
|
||||
varchar(100) device_sn
|
||||
varchar(50) fault_type
|
||||
varchar(20) status
|
||||
varchar(10) priority
|
||||
varchar(100) assignee
|
||||
varchar(500) description
|
||||
varchar(500) phenomenon
|
||||
date expected_fix_date
|
||||
text note
|
||||
jsonb process_records
|
||||
jsonb board_replacements
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
scrap_records {
|
||||
varchar(64) id PK
|
||||
varchar(100) device_sn
|
||||
varchar(100) model_name
|
||||
varchar(500) reason
|
||||
varchar(100) applicant
|
||||
varchar(20) status
|
||||
varchar(50) order_id
|
||||
decimal residual_value
|
||||
jsonb materials
|
||||
jsonb approval_timeline
|
||||
text approval_note
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
checklist_templates {
|
||||
varchar(64) id PK
|
||||
varchar(64) model_id FK
|
||||
varchar(50) model_code
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
checklist_items {
|
||||
varchar(64) id PK
|
||||
varchar(64) template_id FK
|
||||
varchar(200) name
|
||||
boolean required
|
||||
int sort_order
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
board_cards {
|
||||
varchar(64) id PK
|
||||
varchar(100) sn UK
|
||||
varchar(50) type
|
||||
varchar(50) version
|
||||
varchar(20) firmware_version
|
||||
varchar(20) status
|
||||
varchar(100) device_sn
|
||||
date production_date
|
||||
varchar(20) calib_status
|
||||
date calib_date
|
||||
varchar(500) remark
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
|
||||
calibration_files {
|
||||
varchar(64) id PK
|
||||
varchar(100) board_sn
|
||||
varchar(200) original_name
|
||||
varchar(500) file_path
|
||||
bigint file_size
|
||||
varchar(64) md5
|
||||
timestamp upload_time
|
||||
timestamp created_at
|
||||
varchar(64) created_by
|
||||
timestamp updated_at
|
||||
varchar(64) updated_by
|
||||
smallint deleted
|
||||
}
|
||||
```
|
||||
|
||||
### 表结构详细说明
|
||||
|
||||
#### 公共审计字段(所有表必须包含)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | VARCHAR(64) | PK | 应用层生成(雪花算法/UUID) |
|
||||
| created_at | TIMESTAMP | NOT NULL | 创建时间,INSERT 自动填充 |
|
||||
| created_by | VARCHAR(64) | NULL | 创建人 |
|
||||
| updated_at | TIMESTAMP | NOT NULL | 修改时间,INSERT/UPDATE 自动填充 |
|
||||
| updated_by | VARCHAR(64) | NULL | 修改人 |
|
||||
| deleted | SMALLINT | NOT NULL DEFAULT 0 | 逻辑删除标记 |
|
||||
|
||||
#### dev.devices — 设备表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| sn | VARCHAR(100) | 设备SN号,唯一索引 |
|
||||
| model_id | VARCHAR(64) | 关联设备型号 |
|
||||
| model_name | VARCHAR(100) | 型号名称(冗余) |
|
||||
| status | VARCHAR(20) | 装配中/已出厂/已激活/报废 |
|
||||
| firmware_version | VARCHAR(20) | 当前固件版本 |
|
||||
| production_date | TIMESTAMP | 生产日期 |
|
||||
| customer | VARCHAR(200) | 客户名称 |
|
||||
| batch | VARCHAR(20) | 生产批次(YYYY-WXX) |
|
||||
| operator | VARCHAR(64) | 登记人 |
|
||||
|
||||
#### dev.device_models — 设备型号表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| name | VARCHAR(100) | 型号名称(如 GD-30 Supreme) |
|
||||
| code | VARCHAR(50) | 型号代码(如 GD30),唯一索引 |
|
||||
| status | VARCHAR(20) | 在产/停产 |
|
||||
| description | VARCHAR(500) | 描述 |
|
||||
|
||||
#### dev.board_types — 板卡型号表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| type | VARCHAR(50) | 板卡类型(主协板/采集板/发射板/升压板) |
|
||||
| version | VARCHAR(50) | 版本号(如 MB-V1.8),唯一索引 |
|
||||
| latest_firmware | VARCHAR(20) | 最新固件版本 |
|
||||
| production_date | DATE | 生产日期 |
|
||||
| status | VARCHAR(20) | 在产/停产 |
|
||||
|
||||
#### dev.device_boards — 设备板卡关联表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| device_id | VARCHAR(64) | 关联设备 |
|
||||
| device_sn | VARCHAR(100) | 设备SN(冗余) |
|
||||
| board_type_id | VARCHAR(64) | 关联板卡型号 |
|
||||
| board_sn | VARCHAR(100) | 板卡SN号 |
|
||||
| board_name | VARCHAR(50) | 板卡名称(主协板/采集板等) |
|
||||
| board_model | VARCHAR(50) | 板卡型号 |
|
||||
| calibration_status | VARCHAR(20) | 校准状态 |
|
||||
|
||||
#### dev.firmware_versions — 固件版本表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| version | VARCHAR(20) | 版本号 |
|
||||
| board_type_id | VARCHAR(64) | 关联板卡型号 |
|
||||
| board_version | VARCHAR(50) | 板卡版本(冗余) |
|
||||
| firmware_type | VARCHAR(50) | 固件类型(主协板/采集板/发射板/升压板/主机固件/计算单元固件) |
|
||||
| release_date | DATE | 发布日期 |
|
||||
| status | VARCHAR(20) | 已发布/草稿 |
|
||||
| file_size | VARCHAR(20) | 文件大小 |
|
||||
| download_count | INT | 下载次数,默认 0 |
|
||||
| hw_range | VARCHAR(200) | 硬件版本范围 |
|
||||
| upgrade_type | VARCHAR(20) | 可选/强制 |
|
||||
| signed | BOOLEAN | 是否数字签名 |
|
||||
| md5 | VARCHAR(64) | MD5 校验值 |
|
||||
| sha256 | VARCHAR(128) | SHA256 校验值 |
|
||||
| release_notes | TEXT | 发布说明(JSON 数组) |
|
||||
| file_path | VARCHAR(500) | 文件存储路径 |
|
||||
|
||||
#### dev.calibration_records — 校准记录表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| board_sn | VARCHAR(100) | 采集板SN号 |
|
||||
| board_type_id | VARCHAR(64) | 关联板卡型号 |
|
||||
| board_version | VARCHAR(50) | 板卡版本(冗余) |
|
||||
| calibration_date | DATE | 校准日期 |
|
||||
| expiry_date | DATE | 到期日期 |
|
||||
| calibrator | VARCHAR(100) | 校准人员 |
|
||||
| status | VARCHAR(20) | 合格/不合格/待校准 |
|
||||
| channel_count | INT | 通道数 |
|
||||
| overall_deviation | DECIMAL(10,4) | 综合偏差 |
|
||||
| channel_results | JSONB | 各通道校准结果 |
|
||||
|
||||
#### dev.config_files — 配置文件表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| name | VARCHAR(100) | 配置文件名(如 CFG-GD30-v1.3.0) |
|
||||
| model_id | VARCHAR(64) | 关联设备型号 |
|
||||
| model_name | VARCHAR(100) | 型号名称(冗余) |
|
||||
| version | VARCHAR(20) | 配置版本 |
|
||||
| status | VARCHAR(20) | 生效/已停用 |
|
||||
| transmission_params | JSONB | 发射参数 |
|
||||
| acquisition_params | JSONB | 采集参数 |
|
||||
| protection_params | JSONB | 保护参数 |
|
||||
| network_params | JSONB | 网络参数 |
|
||||
|
||||
#### dev.licenses — 授权表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| model_id | VARCHAR(64) | 关联设备型号 |
|
||||
| model_code | VARCHAR(50) | 型号代码(冗余) |
|
||||
| modules | JSONB | 授权模块列表 |
|
||||
| validity_type | VARCHAR(20) | 永久/1年/2年/自定义 |
|
||||
| expiry_date | DATE | 到期日期 |
|
||||
| status | VARCHAR(20) | 生效/草稿/已停用 |
|
||||
|
||||
#### dev.repair_orders — 维修工单表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| order_no | VARCHAR(50) | 工单号(如 WO-2024-0001),唯一索引 |
|
||||
| device_sn | VARCHAR(100) | 设备SN号 |
|
||||
| fault_type | VARCHAR(50) | 故障类型 |
|
||||
| status | VARCHAR(20) | 待处理/处理中/已处理 |
|
||||
| priority | VARCHAR(10) | 高/中/低 |
|
||||
| assignee | VARCHAR(100) | 负责人 |
|
||||
| description | VARCHAR(500) | 故障描述 |
|
||||
| phenomenon | VARCHAR(500) | 故障现象 |
|
||||
| expected_fix_date | DATE | 预计修复日期 |
|
||||
| note | TEXT | 备注 |
|
||||
| process_records | JSONB | 处理记录时间线 |
|
||||
| board_replacements | JSONB | 板卡更换记录 |
|
||||
|
||||
#### dev.scrap_records — 报废记录表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| device_sn | VARCHAR(100) | 设备SN号 |
|
||||
| model_name | VARCHAR(100) | 设备型号 |
|
||||
| reason | VARCHAR(500) | 报废原因 |
|
||||
| applicant | VARCHAR(100) | 申请人 |
|
||||
| status | VARCHAR(20) | 待审批/审批中/已审批/已驳回/回收中/已回收 |
|
||||
| order_id | VARCHAR(50) | 来源工单号 |
|
||||
| residual_value | DECIMAL(12,2) | 残值评估 |
|
||||
| materials | JSONB | 可回收物料列表 |
|
||||
| approval_timeline | JSONB | 审批记录时间线 |
|
||||
| approval_note | TEXT | 审批意见 |
|
||||
|
||||
#### dev.checklist_templates — Checklist 模板表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| model_id | VARCHAR(64) | 关联设备型号 |
|
||||
| model_code | VARCHAR(50) | 型号代码(冗余) |
|
||||
|
||||
#### dev.checklist_items — Checklist 检查项表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| template_id | VARCHAR(64) | 关联模板 |
|
||||
| name | VARCHAR(200) | 检查项名称 |
|
||||
| required | BOOLEAN | 是否必填 |
|
||||
| sort_order | INT | 排序序号 |
|
||||
|
||||
#### dev.calibration_files — 校准文件表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| board_sn | VARCHAR(100) | 采集板SN号,关联 board_cards.sn |
|
||||
| original_name | VARCHAR(200) | 原始文件名 |
|
||||
| file_path | VARCHAR(500) | 服务器存储路径 |
|
||||
| file_size | BIGINT | 文件大小(字节) |
|
||||
| md5 | VARCHAR(64) | 文件MD5校验值 |
|
||||
| upload_time | TIMESTAMP | 上传时间 |
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*正确性属性是在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 统一响应格式一致性
|
||||
|
||||
*For any* API 请求(无论成功或失败),响应 JSON 都应包含且仅包含 `code`(整数)、`message`(字符串)、`data`(泛型)三个顶层字段。成功时 code=0,业务异常和参数校验异常均应返回 code≠0 的统一格式响应。
|
||||
|
||||
**Validates: Requirements 1.3, 1.4**
|
||||
|
||||
### Property 2: 审计字段自动填充
|
||||
|
||||
*For any* 通过 MyBatis-Plus 插入的实体记录,`created_at` 和 `updated_at` 字段应非空且为合理的时间戳;*For any* 更新操作,`updated_at` 应大于等于更新前的值。*For any* `dev` schema 下的业务表,都应包含 `id`、`created_at`、`created_by`、`updated_at`、`updated_by`、`deleted` 六个审计字段。
|
||||
|
||||
**Validates: Requirements 1.5, 2.12**
|
||||
|
||||
### Property 3: CRUD 数据往返一致性
|
||||
|
||||
*For any* 有效的创建命令(设备、型号、板卡、固件、配置文件、授权、工单、Checklist 模板),通过 POST 创建后再通过 GET 查询,返回的数据应与创建时提交的数据一致(在服务端生成的字段如 id、审计字段除外)。
|
||||
|
||||
**Validates: Requirements 3.2, 3.3, 4.2, 4.4, 5.3, 6.2, 8.3, 8.4, 9.2, 9.3, 10.3**
|
||||
|
||||
### Property 4: 唯一性约束与冲突检测
|
||||
|
||||
*For any* 已存在的唯一标识(设备SN号、型号代码、同一板卡型号下的固件版本号),尝试创建具有相同唯一标识的记录时,API 应返回 HTTP 409 冲突错误码和描述性错误消息,且数据库中不应产生重复记录。
|
||||
|
||||
**Validates: Requirements 3.5, 4.5, 6.4**
|
||||
|
||||
### Property 5: 筛选条件正确性
|
||||
|
||||
*For any* 列表查询接口和任意有效的筛选参数组合,返回的所有记录都应满足指定的筛选条件。例如:按型号筛选时,所有返回的设备型号应与筛选值匹配;按状态筛选时,所有返回记录的状态应与筛选值匹配。
|
||||
|
||||
**Validates: Requirements 3.1, 5.1, 6.1, 7.1, 8.1, 9.1, 10.1, 11.1**
|
||||
|
||||
### Property 6: 状态转换正确性
|
||||
|
||||
*For any* 支持状态转换的实体(授权停用、工单关闭、报废审批/驳回/回收),执行状态转换操作后,实体的状态应更新为目标状态,且相关附加信息(如驳回意见、回收物料清单)应被正确记录。
|
||||
|
||||
**Validates: Requirements 9.4, 10.5, 11.3, 11.4, 11.5**
|
||||
|
||||
### Property 7: 逻辑删除排除性
|
||||
|
||||
*For any* 被逻辑删除的记录(deleted=1),该记录不应出现在任何列表查询的结果中。
|
||||
|
||||
**Validates: Requirements 8.5**
|
||||
|
||||
### Property 8: 分页一致性
|
||||
|
||||
*For any* 列表查询接口,响应应包含 `total`、`page`、`pageSize`、`records` 四个字段;`records` 的长度应不超过 `pageSize`;当不传筛选条件或筛选条件为空时,应返回不带过滤的完整数据集(受分页限制)。
|
||||
|
||||
**Validates: Requirements 14.1, 14.2, 14.4**
|
||||
|
||||
### Property 9: 聚合统计一致性
|
||||
|
||||
*For any* 统计接口返回的数据,各分类计数之和应等于总数。例如:设备状态分布中各状态数量之和应等于设备总数;报废统计中各状态数量之和应等于报废总数。
|
||||
|
||||
**Validates: Requirements 11.6, 12.2**
|
||||
|
||||
### Property 10: 参数校验防御性
|
||||
|
||||
*For any* 无效的筛选参数(如非法的状态值、格式错误的日期),API 应返回 HTTP 400 错误码和描述性错误消息,而非返回错误数据或抛出未处理异常。
|
||||
|
||||
**Validates: Requirements 14.3**
|
||||
|
||||
### Property 11: 校准文件上传-下载往返一致性
|
||||
|
||||
*For any* 有效的校准文件和已存在的采集板SN号,通过 POST `/api/portal/calibration-files/upload` 上传后,再通过 GET `/api/admin/calibration-files/{id}/download` 下载,下载的文件内容应与上传的原始文件完全一致,且 `dev.calibration_files` 表中的记录应正确关联该采集板SN号,文件大小和MD5校验值应与原始文件匹配。
|
||||
|
||||
**Validates: Requirements 17.2, 17.3, 17.7**
|
||||
|
||||
### Property 12: 校准文件列表完整性与排序
|
||||
|
||||
*For any* 采集板,上传 N 个校准文件后,通过 GET `/api/admin/board-cards/{id}/calibration-files` 查询应返回恰好 N 条记录,每条记录包含 id、fileName、fileSize、md5、uploadTime 字段,且结果按 uploadTime 倒序排列。
|
||||
|
||||
**Validates: Requirements 17.6, 17.10**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 后端错误处理策略
|
||||
|
||||
| 错误类型 | HTTP 状态码 | 业务码 | 处理方式 |
|
||||
|----------|------------|--------|----------|
|
||||
| 参数校验失败 | 400 | 40001 | `GlobalExceptionHandler` 捕获 `MethodArgumentNotValidException`,返回字段级错误信息 |
|
||||
| 资源不存在 | 404 | 40401 | Service 层抛出 `BizException`,Handler 统一处理 |
|
||||
| 文件不存在 | 404 | 40402 | 校准文件在存储目录中不存在时,返回描述性错误消息 |
|
||||
| 唯一性冲突 | 409 | 40901 | Service 层检测重复后抛出 `BizException` |
|
||||
| 文件上传失败 | 500 | 50002 | 文件写入磁盘失败时,记录日志并返回错误消息 |
|
||||
| 服务器内部错误 | 500 | 50001 | `GlobalExceptionHandler` 兜底捕获,记录完整堆栈日志 |
|
||||
|
||||
### 自定义异常体系
|
||||
|
||||
```java
|
||||
public class BizException extends RuntimeException {
|
||||
private final int code;
|
||||
private final String message;
|
||||
|
||||
public BizException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局异常处理器
|
||||
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizException.class)
|
||||
public R<Void> handleBizException(BizException e) {
|
||||
return R.fail(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R<Void> handleValidation(MethodArgumentNotValidException e) {
|
||||
String msg = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(f -> f.getField() + ": " + f.getDefaultMessage())
|
||||
.collect(Collectors.joining("; "));
|
||||
return R.fail(40001, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R<Void> handleException(Exception e) {
|
||||
log.error("Unhandled exception", e);
|
||||
return R.fail(50001, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 前端错误处理策略
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| 网络超时(5秒) | 显示"网络请求超时,请检查网络连接" |
|
||||
| HTTP 400 | 显示后端返回的具体校验错误信息 |
|
||||
| HTTP 404 | 显示"请求的资源不存在" |
|
||||
| HTTP 409 | 显示后端返回的冲突描述(如"SN号已存在") |
|
||||
| HTTP 500 | 显示"服务器异常,请稍后重试" |
|
||||
| 请求进行中 | 页面显示 loading 骨架屏或 spinner |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 后端测试
|
||||
|
||||
#### 单元测试
|
||||
- 框架:JUnit 5 + Mockito
|
||||
- 覆盖范围:
|
||||
- Application Service 层的业务逻辑
|
||||
- Domain Entity 的状态转换逻辑
|
||||
- 参数校验逻辑
|
||||
- 异常处理逻辑
|
||||
|
||||
#### 集成测试
|
||||
- 框架:Spring Boot Test + TestContainers(PostgreSQL)
|
||||
- 覆盖范围:
|
||||
- Controller 层 API 端点的请求/响应格式
|
||||
- MyBatis-Plus Mapper 的 CRUD 操作
|
||||
- 审计字段自动填充
|
||||
- 逻辑删除行为
|
||||
- 分页查询
|
||||
|
||||
#### 属性测试
|
||||
- 框架:jqwik(Java 属性测试库)
|
||||
- 最少 100 次迭代
|
||||
- 每个属性测试需引用设计文档中的属性编号
|
||||
- 标签格式:`Feature: frontend-backend-separation, Property {number}: {property_text}`
|
||||
- 覆盖范围:
|
||||
- Property 1: 统一响应格式(生成随机数据类型,验证序列化格式)
|
||||
- Property 3: CRUD 往返一致性(生成随机实体数据,验证创建-查询一致性)
|
||||
- Property 4: 唯一性约束(生成随机唯一标识,验证重复创建返回 409)
|
||||
- Property 5: 筛选正确性(生成随机筛选参数,验证返回结果匹配)
|
||||
- Property 8: 分页一致性(生成随机 page/pageSize,验证响应格式)
|
||||
- Property 10: 参数校验(生成随机无效参数,验证返回 400)
|
||||
- Property 11: 校准文件上传-下载往返(生成随机文件内容和SN号,验证上传后下载内容一致、元数据正确)
|
||||
- Property 12: 校准文件列表完整性(生成随机数量的校准文件上传,验证列表返回数量和排序正确)
|
||||
|
||||
### 前端测试
|
||||
|
||||
#### 单元测试
|
||||
- 框架:Vitest + React Testing Library
|
||||
- 覆盖范围:
|
||||
- API 客户端的请求拦截和错误处理
|
||||
- 各 API 服务文件的请求参数构造
|
||||
- 页面组件的加载状态和错误状态渲染
|
||||
- 板卡详情抽屉中校准文件列表的渲染和下载按钮交互
|
||||
|
||||
#### 集成测试
|
||||
- 框架:Vitest + MSW(Mock Service Worker)
|
||||
- 覆盖范围:
|
||||
- 页面组件与 API 的完整交互流程
|
||||
- 筛选、分页操作的数据刷新
|
||||
- 表单提交与 API 调用
|
||||
|
||||
|
|
@ -2,21 +2,41 @@
|
|||
|
||||
## 简介
|
||||
|
||||
本项目旨在将现有的 Next.js 前端生产管理子系统进行前后端分离改造。当前前端所有页面(设备列表、板卡管理、校准管理、固件库、授权管理、配置文件管理、维修工单、报废管理、设备登记、设备型号管理)均使用硬编码的模拟数据。需要搭建 Java Spring Boot 3.3.6 后端 API 服务和 PostgreSQL 数据库,用真实的 RESTful API 接口和持久化数据替换前端模拟数据。
|
||||
本项目旨在将现有的 Next.js 16 前端生产管理子系统进行前后端分离改造。当前前端所有页面均使用硬编码的模拟数据,需要搭建 Java Spring Boot 3.3.6 后端 API 服务和 PostgreSQL 数据库,用真实的 RESTful API 接口和持久化数据替换前端模拟数据。
|
||||
|
||||
系统为地球物理仪器(高密度电法仪等)的全生命周期管理平台,包含以下核心页面模块:
|
||||
- 首页 Dashboard(`/`)— 8项统计指标、设备状态分布、4组待处理任务
|
||||
- 设备列表(`/devices`)— 卡片式设备列表,按批次侧边栏分组
|
||||
- 设备详情(`/devices/[sn]`)— 6个Tab页(概览、装机清单、装配记录、授权项信息、配置文件、操作日志)
|
||||
- 设备登记(`/registration`)— 装机信息表单、BOM清单、装配Checklist
|
||||
- 设备型号管理(`/models`)— 型号列表、装配Checklist模板
|
||||
- 板卡列表(`/calibration`)— 板卡实例管理,含统计卡片和筛选
|
||||
- 板卡登记(`/calibration/register`)— 单个/批量板卡登记
|
||||
- 板卡版本管理(`/boards`)— 板卡型号版本管理,Tab筛选
|
||||
- 固件库(`/firmware`)— 固件版本卡片列表,支持从板卡版本页跳转筛选
|
||||
- 授权管理(`/licenses`)— 授权列表,11项授权模块勾选
|
||||
- 配置文件管理(`/config-files`)— 配置文件列表,含详细参数
|
||||
- 维修工单(`/repair`)— 卡片式工单列表,新建/处理/详情抽屉
|
||||
- 报废管理(`/scrap`)— 审批流程、物料回收
|
||||
|
||||
## 术语表
|
||||
|
||||
- **API_Server**:基于 Spring Boot 3.3.6 的 Java 后端 API 服务,项目路径 `apps/geo-bps-api/`
|
||||
- **Database**:PostgreSQL 12.14 数据库实例,按模块使用不同 schema 隔离
|
||||
- **Frontend**:现有 Next.js 前端应用,位于 `src/app/` 目录
|
||||
- **Database**:PostgreSQL 12.14 数据库实例,设备管理模块使用 `dev` schema
|
||||
- **Frontend**:现有 Next.js 16 前端应用,位于 `src/app/` 目录
|
||||
- **Device_Module**:设备管理业务模块,包名 `com.geomative.bps.device`,数据库 schema `dev`
|
||||
- **Common_Module**:公共模块,包名 `com.geomative.bps.common`,提供统一响应体、全局异常处理、工具类
|
||||
- **API_Admin**:后台管理入口模块,提供需登录鉴权的 API 端点
|
||||
- **DDD**:领域驱动设计分层架构(interfaces / application / domain / infrastructure)
|
||||
- **Board_Card**:板卡实例,指具体的单个板卡物理实体,拥有唯一SN号、状态(在库/已装配/故障/报废)、校准状态
|
||||
- **Board_Type**:板卡版本(型号),指板卡的设计版本(如 MB-V1.8、RX-V2.3),管理版本的在产/停产状态
|
||||
- **DO**:数据库映射对象(Data Object)
|
||||
- **VO**:视图对象(View Object),用于 API 响应
|
||||
- **Query**:查询参数对象
|
||||
- **Command**:命令对象,用于创建/更新操作
|
||||
- **Calibration_File**:校准文件,采集板校准时由上位机生成并上传的文件,通过采集板SN号关联
|
||||
- **Upper_Computer**:上位机,校准设备的PC端软件,负责执行采集板校准并上传校准文件
|
||||
- **API_Portal**:前端门户入口模块,提供无需登录鉴权的公开 API 端点
|
||||
|
||||
## 需求
|
||||
|
||||
|
|
@ -41,144 +61,186 @@
|
|||
#### 验收标准
|
||||
|
||||
1. THE Database SHALL 在 `dev` schema 下创建设备管理相关的数据表
|
||||
2. THE Database SHALL 包含 `dev.devices` 表,存储设备信息(SN号、型号、状态、固件版本、生产日期、客户名称、批次号)
|
||||
3. THE Database SHALL 包含 `dev.device_models` 表,存储设备型号信息(型号名称、型号代码、状态、描述)
|
||||
4. THE Database SHALL 包含 `dev.board_types` 表,存储板卡型号信息(板卡类型、型号、固件版本、生产日期、状态)
|
||||
5. THE Database SHALL 包含 `dev.firmware_versions` 表,存储固件版本信息(版本号、板卡型号、固件类型、发布日期、状态、文件大小、下载次数、硬件版本范围、升级类型、是否签名、MD5、SHA256、发布说明)
|
||||
6. THE Database SHALL 包含 `dev.calibration_records` 表,存储校准记录(采集板SN号、板卡型号、校准日期、到期日期、校准人员、状态、通道数、综合偏差)
|
||||
7. THE Database SHALL 包含 `dev.config_files` 表,存储配置文件信息(配置名称、适配型号、版本、状态、发射参数、采集参数、网络参数)
|
||||
8. THE Database SHALL 包含 `dev.licenses` 表,存储授权信息(设备型号、授权模块列表、到期时间、状态)
|
||||
9. THE Database SHALL 包含 `dev.repair_orders` 表,存储维修工单信息(工单号、设备SN、故障类型、状态、优先级、负责人、描述)
|
||||
10. THE Database SHALL 包含 `dev.scrap_records` 表,存储报废记录(设备SN、型号、报废原因、申请人、状态、来源工单号、残值评估、可回收物料)
|
||||
11. THE Database SHALL 包含 `dev.checklist_templates` 表和 `dev.checklist_items` 表,存储装配 Checklist 模板及其检查项
|
||||
12. WHEN 创建任何业务表时,THE Database SHALL 包含 id(VARCHAR(64) 主键)、created_at、created_by、updated_at、updated_by、deleted 审计字段
|
||||
2. THE Database SHALL 包含 `dev.devices` 表,存储设备信息(SN号唯一索引、型号ID、型号名称、状态[装配中/已出厂/已激活/报废]、固件版本、生产日期、客户名称、批次号[YYYY-WXX格式]、登记人)
|
||||
3. THE Database SHALL 包含 `dev.device_models` 表,存储设备型号信息(型号名称如GD-30 Supreme、型号代码如GD30唯一索引、状态[在产/停产]、描述、创建日期)
|
||||
4. THE Database SHALL 包含 `dev.board_types` 表,存储板卡版本信息(板卡类型[主协板/采集板/发射板/升压板]、版本号如MB-V1.8唯一索引、生产日期、状态[在产/停产])
|
||||
5. THE Database SHALL 包含 `dev.board_cards` 表,存储板卡实例信息(板卡SN号唯一索引、板卡类型[主协板/采集板/发射板/升压板]、版本号、固件版本、状态[在库/已装配/故障/报废]、所属设备SN、生产日期、校准状态[合格/不合格/待校准/无需校准]、校准日期、备注)
|
||||
6. THE Database SHALL 包含 `dev.device_boards` 表,存储设备与板卡的装配关联关系(设备ID、设备SN、板卡实例ID、板卡SN、板卡名称、板卡型号、校准状态)
|
||||
7. THE Database SHALL 包含 `dev.firmware_versions` 表,存储固件版本信息(版本号、关联板卡版本ID、板卡版本号、固件类型[主协板/采集板/发射板/升压板/主机固件/计算单元固件]、发布日期、状态[已发布/草稿]、文件大小、下载次数、硬件版本范围、升级类型[可选/强制]、是否签名、MD5、SHA256、发布说明JSON数组、文件存储路径)
|
||||
8. THE Database SHALL 包含 `dev.calibration_records` 表,存储校准记录(采集板SN号、板卡版本ID、板卡版本号、校准日期、到期日期、校准人员、状态[合格/不合格/待校准]、通道数、综合偏差、各通道校准结果JSONB)
|
||||
9. THE Database SHALL 包含 `dev.config_files` 表,存储配置文件信息(配置名称、适配型号ID、型号名称、版本、状态[生效/已停用]、发射参数JSONB含电压/电流/占空比/脉宽/迭代次数/波形、采集参数JSONB含通道数/采样率/电压量程/全波形、保护参数JSONB含过压/过流/短路/高温、网络参数JSONB含WiFi SSID前缀)
|
||||
10. THE Database SHALL 包含 `dev.licenses` 表,存储授权信息(设备型号ID、型号代码、授权模块列表JSONB含11项模块[1D SP/2D SP/3D SP/1D VES/2D ERT/3D ERT/1D IP/2D IP/3D IP/跨孔/水上]、有效期类型[永久/1年/2年/3年/自定义]、到期日期、状态[生效/草稿/已停用])
|
||||
11. THE Database SHALL 包含 `dev.repair_orders` 表,存储维修工单信息(工单号唯一索引、设备SN、故障类型[板卡故障/固件异常/通信故障/电源故障/传感器故障/其他]、状态[待处理/处理中/已处理]、优先级[高/中/低]、负责人、故障描述、故障现象、预计修复日期、备注、处理记录JSONB时间线、板卡更换记录JSONB)
|
||||
12. THE Database SHALL 包含 `dev.scrap_records` 表,存储报废记录(设备SN、设备型号名称、报废原因、申请人、状态[待审批/审批中/已审批/已驳回/回收中/已回收]、来源工单号、残值评估金额、可回收物料列表JSONB、审批记录时间线JSONB、审批意见)
|
||||
13. THE Database SHALL 包含 `dev.checklist_templates` 表和 `dev.checklist_items` 表,存储装配 Checklist 模板及其检查项(检查项含名称、是否必填、排序序号)
|
||||
14. WHEN 创建任何业务表时,THE Database SHALL 包含 id(VARCHAR(64) 主键)、created_at、created_by、updated_at、updated_by、deleted 审计字段
|
||||
|
||||
### 需求 3:设备管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 获取和管理设备数据,以便替换设备列表页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 获取和管理设备数据,以便替换设备列表页面(`/devices`)和设备详情页面(`/devices/[sn]`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/devices` 时,THE API_Server SHALL 返回分页的设备列表,支持按型号、状态、生产日期、SN号、批次号筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/devices/{sn}` 时,THE API_Server SHALL 返回指定设备的详细信息,包含关联的授权信息、子设备列表、固件信息
|
||||
3. WHEN 前端请求 POST `/api/admin/devices` 时,THE API_Server SHALL 创建新设备记录(设备登记),包含装机信息和 BOM 清单
|
||||
4. WHEN 前端请求 GET `/api/admin/devices/batches` 时,THE API_Server SHALL 返回所有生产批次列表及每个批次的设备数量
|
||||
1. WHEN 前端请求 GET `/api/admin/devices` 时,THE API_Server SHALL 返回分页的设备列表,支持按型号、状态、生产日期、SN号、批次号筛选,每条记录包含 sn、model、type、status、firmware、productionDate、customer、batch 字段
|
||||
2. WHEN 前端请求 GET `/api/admin/devices/{sn}` 时,THE API_Server SHALL 返回指定设备的完整详情,包含6个Tab页所需的全部数据:
|
||||
- 概览Tab:基本信息(SN、型号、类型、固件版本、生产日期、登记人、状态)
|
||||
- 装机清单Tab:BOM列表(物料名称、板卡SN、型号、校准状态)
|
||||
- 装配记录Tab:Checklist检查项列表(项目名称、通过状态、照片URL列表、装配记录备注),含通过数/总数统计
|
||||
- 授权项信息Tab:授权模块列表、到期时间、授权状态
|
||||
- 配置文件Tab:配置文件名称、版本、发射参数(电压/电流/占空比/脉宽/波形/全波形)、采集参数(通道数/采样率/电压量程/迭代次数)、保护参数(过压/过流/短路/高温阈值)、网络参数(WiFi SSID前缀)
|
||||
- 操作日志Tab:操作记录时间线(日期、操作类型、操作人、详情描述)
|
||||
3. WHEN 前端请求 POST `/api/admin/devices` 时,THE API_Server SHALL 创建新设备记录(设备登记),包含装机信息(型号、主机SN、主板SN、测试状态、生产日期、登记人)、BOM清单列表和装配Checklist状态
|
||||
4. WHEN 前端请求 GET `/api/admin/devices/batches` 时,THE API_Server SHALL 返回所有生产批次列表,每个批次包含批次号(YYYY-WXX格式)、设备数量,按年份分组并按时间倒序排列
|
||||
5. IF 请求参数中的 SN 号已存在,THEN THE API_Server SHALL 返回 409 冲突错误码和描述性错误消息
|
||||
|
||||
### 需求 4:设备型号管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备型号和装配 Checklist 模板,以便替换型号管理页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备型号和装配 Checklist 模板,以便替换型号管理页面(`/models`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/device-models` 时,THE API_Server SHALL 返回所有设备型号列表
|
||||
2. WHEN 前端请求 POST `/api/admin/device-models` 时,THE API_Server SHALL 创建新设备型号记录
|
||||
3. WHEN 前端请求 GET `/api/admin/checklist-templates?modelCode={code}` 时,THE API_Server SHALL 返回指定型号的装配 Checklist 模板及其检查项列表
|
||||
4. WHEN 前端请求 POST `/api/admin/checklist-templates` 时,THE API_Server SHALL 创建新的 Checklist 模板,包含检查项列表
|
||||
5. IF 请求的型号代码已存在,THEN THE API_Server SHALL 返回 409 冲突错误码
|
||||
1. WHEN 前端请求 GET `/api/admin/device-models` 时,THE API_Server SHALL 返回所有设备型号列表,每条记录包含 name、code、status、description、createDate 字段
|
||||
2. WHEN 前端请求 POST `/api/admin/device-models` 时,THE API_Server SHALL 创建新设备型号记录,包含型号名称、型号编码、描述、状态[在产/停产]
|
||||
3. WHEN 前端请求 PUT `/api/admin/device-models/{id}` 时,THE API_Server SHALL 更新指定型号的状态(在产/停产切换)
|
||||
4. WHEN 前端请求 GET `/api/admin/checklist-templates?modelCode={code}` 时,THE API_Server SHALL 返回指定型号的装配 Checklist 模板及其检查项列表(含名称、是否必填、排序序号)
|
||||
5. WHEN 前端请求 POST `/api/admin/checklist-templates` 时,THE API_Server SHALL 创建新的 Checklist 模板,包含关联型号和检查项列表
|
||||
6. IF 请求的型号代码已存在,THEN THE API_Server SHALL 返回 409 冲突错误码
|
||||
|
||||
### 需求 5:板卡型号管理 API
|
||||
### 需求 5:板卡版本管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理板卡型号数据,以便替换板卡管理页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理板卡版本(型号)数据,以便替换板卡版本管理页面(`/boards`)的模拟数据。该页面管理的是板卡的设计版本(如 MB-V1.8),而非单个板卡实例。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/board-types` 时,THE API_Server SHALL 返回板卡型号列表,支持按板卡类型(主协板、采集板、发射板、升压板)筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/board-types/{id}` 时,THE API_Server SHALL 返回板卡详情,包含升级历史、校准历史、保养历史、维修历史
|
||||
3. WHEN 前端请求 POST `/api/admin/board-types` 时,THE API_Server SHALL 创建新板卡型号记录
|
||||
1. WHEN 前端请求 GET `/api/admin/board-types` 时,THE API_Server SHALL 返回板卡版本列表,支持按板卡类型(全部/主协板/采集板/发射板/升压板)Tab筛选,每条记录包含 type、version、productionDate、status 字段
|
||||
2. WHEN 前端请求 GET `/api/admin/board-types/{id}` 时,THE API_Server SHALL 返回板卡版本详情,包含基本信息(类型、版本、生产日期、状态)
|
||||
3. WHEN 前端请求 POST `/api/admin/board-types` 时,THE API_Server SHALL 创建新板卡版本记录,包含板卡类型、版本号、生产日期、状态
|
||||
4. WHEN 前端请求 PUT `/api/admin/board-types/{id}/toggle-status` 时,THE API_Server SHALL 切换板卡版本的状态(在产↔停产)
|
||||
|
||||
### 需求 6:固件库管理 API
|
||||
### 需求 6:板卡实例管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理固件版本数据,以便替换固件库页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理板卡实例数据,以便替换板卡列表页面(`/calibration`)的模拟数据。该页面管理的是具体的单个板卡物理实体,每个板卡有唯一SN号、库存状态和校准状态。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/firmware` 时,THE API_Server SHALL 返回固件版本列表,支持按固件类型和板卡型号筛选
|
||||
2. WHEN 前端请求 POST `/api/admin/firmware` 时,THE API_Server SHALL 创建新固件版本记录,包含版本号、硬件版本范围、升级类型、固件类型、签名状态、发布说明
|
||||
3. WHEN 前端请求 GET `/api/admin/firmware/{id}/download` 时,THE API_Server SHALL 返回固件文件的下载流,并将该固件的下载次数加 1
|
||||
4. IF 上传的固件版本号与同一板卡型号的已有版本重复,THEN THE API_Server SHALL 返回 409 冲突错误码
|
||||
1. WHEN 前端请求 GET `/api/admin/board-cards` 时,THE API_Server SHALL 返回分页的板卡实例列表,支持按板卡类型(全部/主协板/采集板/发射板/升压板)、板卡状态(全部/在库/已装配/故障/报废)、校准状态(全部/合格/不合格/待校准)筛选,支持按板卡SN或设备SN搜索,每条记录包含 sn、type、version、firmware、status、deviceSn、productionDate、calibStatus、calibDate 字段
|
||||
2. WHEN 前端请求 GET `/api/admin/board-cards/{id}` 时,THE API_Server SHALL 返回板卡实例详情,包含基本信息(SN、类型、版本、固件版本、生产日期、状态)、装配信息(所属设备SN)、校准信息(校准状态、校准日期,仅采集板显示)
|
||||
3. WHEN 前端请求 GET `/api/admin/board-cards/stats` 时,THE API_Server SHALL 返回板卡统计数据,包含板卡总数、在库数量、已装配数量、故障数量、待校准数量
|
||||
4. IF 请求参数中的板卡SN号已存在,THEN THE API_Server SHALL 返回 409 冲突错误码和描述性错误消息
|
||||
|
||||
### 需求 7:校准管理 API
|
||||
### 需求 7:板卡登记 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理采集板校准数据,以便替换校准管理页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 登记新板卡,以便替换板卡登记页面(`/calibration/register`)的模拟数据。该页面支持单个和批量登记板卡。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/calibrations` 时,THE API_Server SHALL 返回分页的校准记录列表,支持按采集板SN号、校准状态、校准人员筛选
|
||||
1. WHEN 前端请求 POST `/api/admin/board-cards` 时,THE API_Server SHALL 创建单个板卡实例记录,包含板卡类型、版本号、固件版本(根据版本自动填充)、板卡SN号、生产日期、备注
|
||||
2. WHEN 前端请求 POST `/api/admin/board-cards/batch` 时,THE API_Server SHALL 支持批量创建多个板卡实例记录,请求体为板卡信息数组
|
||||
3. WHEN 创建采集板类型的板卡时,THE API_Server SHALL 自动将校准状态设置为"待校准"
|
||||
4. WHEN 创建非采集板类型的板卡时,THE API_Server SHALL 自动将板卡状态设置为"在库",校准状态设置为无需校准
|
||||
5. THE API_Server SHALL 提供 GET `/api/admin/board-types/{type}/versions` 接口,返回指定板卡类型的可选版本列表及对应固件版本,用于板卡登记表单的版本下拉选择
|
||||
|
||||
### 需求 8:固件库管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理固件版本数据,以便替换固件库页面(`/firmware`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/firmware` 时,THE API_Server SHALL 返回固件版本列表,支持按固件类型(全部/主协板/采集板/发射板/升压板/主机固件/计算单元固件)Tab筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/firmware?boardVersion={version}` 时,THE API_Server SHALL 返回指定板卡版本的固件列表,用于从板卡版本管理页面(`/boards`)通过 `?board=` 参数跳转后的筛选展示
|
||||
3. WHEN 前端请求 POST `/api/admin/firmware` 时,THE API_Server SHALL 创建新固件版本记录,包含版本号、硬件版本范围、升级类型[可选/强制]、签名状态、发布说明(多行文本,每行一条)
|
||||
4. WHEN 前端请求 GET `/api/admin/firmware/{id}/download` 时,THE API_Server SHALL 返回固件文件的下载流,并将该固件的下载次数加 1
|
||||
5. IF 上传的固件版本号与同一板卡版本的已有版本重复,THEN THE API_Server SHALL 返回 409 冲突错误码
|
||||
|
||||
### 需求 9:校准记录管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理采集板校准数据。校准功能是板卡实例管理的一部分,校准状态直接关联到板卡实例(`dev.board_cards`)的 calibStatus 和 calibDate 字段。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/calibrations` 时,THE API_Server SHALL 返回分页的校准记录列表,支持按采集板SN号、校准状态[合格/不合格/待校准]、校准人员筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/calibrations/{id}` 时,THE API_Server SHALL 返回校准详情,包含各通道的校准结果(参考值、测量值、偏差、结果)
|
||||
3. WHEN 前端请求 POST `/api/admin/calibrations/import` 时,THE API_Server SHALL 支持批量导入校准记录数据
|
||||
3. WHEN 前端请求 POST `/api/admin/calibrations/import` 时,THE API_Server SHALL 支持批量导入校准记录数据,并同步更新对应板卡实例(`dev.board_cards`)的校准状态和校准日期
|
||||
|
||||
### 需求 8:配置文件管理 API
|
||||
### 需求 10:配置文件管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备配置文件,以便替换配置文件管理页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备配置文件,以便替换配置文件管理页面(`/config-files`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/config-files` 时,THE API_Server SHALL 返回分页的配置文件列表,支持按适配型号、版本、关键字筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/config-files/{id}` 时,THE API_Server SHALL 返回配置文件详情,包含发射参数、采集参数、网络参数
|
||||
3. WHEN 前端请求 POST `/api/admin/config-files` 时,THE API_Server SHALL 创建新配置文件记录
|
||||
1. WHEN 前端请求 GET `/api/admin/config-files` 时,THE API_Server SHALL 返回分页的配置文件列表,支持按适配型号、版本、关键字筛选,每条记录包含 name、model、version、createTime、status 字段
|
||||
2. WHEN 前端请求 GET `/api/admin/config-files/{id}` 时,THE API_Server SHALL 返回配置文件详情,包含基本信息(名称、型号、版本、状态、创建时间)、发射参数(电压、电流、占空比、脉宽范围、迭代次数)、采集参数(通道数、采样率、电压量程、全波形采集)、网络参数(WiFi SSID前缀)
|
||||
3. WHEN 前端请求 POST `/api/admin/config-files` 时,THE API_Server SHALL 创建新配置文件记录,包含适配型号、版本、发射参数、采集参数、网络参数
|
||||
4. WHEN 前端请求 PUT `/api/admin/config-files/{id}` 时,THE API_Server SHALL 更新指定配置文件记录
|
||||
5. WHEN 前端请求 DELETE `/api/admin/config-files/{id}` 时,THE API_Server SHALL 逻辑删除指定配置文件(设置 deleted=1)
|
||||
|
||||
### 需求 9:授权管理 API
|
||||
### 需求 11:授权管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备授权数据,以便替换授权管理页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备授权数据,以便替换授权管理页面(`/licenses`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/licenses` 时,THE API_Server SHALL 返回分页的授权列表,支持按设备型号和状态筛选
|
||||
2. WHEN 前端请求 POST `/api/admin/licenses` 时,THE API_Server SHALL 创建新授权记录,包含设备型号、授权模块列表、授权期限
|
||||
1. WHEN 前端请求 GET `/api/admin/licenses` 时,THE API_Server SHALL 返回分页的授权列表,支持按设备型号(GD-10/GD-20/GD-30)和状态(生效/草稿/已停用)筛选,每条记录包含 model、modules、expiry、date、status 字段
|
||||
2. WHEN 前端请求 POST `/api/admin/licenses` 时,THE API_Server SHALL 创建新授权记录,包含设备型号、授权模块列表(从11项模块中勾选)、授权期限[1年/2年/3年/永久/自定义]、自定义到期日期
|
||||
3. WHEN 前端请求 PUT `/api/admin/licenses/{id}` 时,THE API_Server SHALL 更新指定授权记录
|
||||
4. WHEN 前端请求 PUT `/api/admin/licenses/{id}/disable` 时,THE API_Server SHALL 将指定授权记录状态设置为已停用
|
||||
5. THE API_Server SHALL 提供 GET `/api/admin/licenses/auth-items` 接口,返回全部11项授权模块定义(ID、名称、描述)
|
||||
6. THE API_Server SHALL 提供 GET `/api/admin/licenses/model-presets` 接口,返回各型号的默认授权模块预选配置(GD-10: 6项, GD-20: 9项, GD-30: 全部11项)
|
||||
|
||||
### 需求 10:维修工单管理 API
|
||||
### 需求 12:维修工单管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理维修工单数据,以便替换维修工单页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理维修工单数据,以便替换维修工单页面(`/repair`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/repair-orders` 时,THE API_Server SHALL 返回分页的维修工单列表,支持按状态、优先级、负责人、日期范围、设备SN筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/repair-orders/{id}` 时,THE API_Server SHALL 返回工单详情,包含设备信息、故障信息、处理记录时间线、板卡更换记录
|
||||
3. WHEN 前端请求 POST `/api/admin/repair-orders` 时,THE API_Server SHALL 创建新维修工单
|
||||
4. WHEN 前端请求 PUT `/api/admin/repair-orders/{id}/process` 时,THE API_Server SHALL 更新工单处理信息(处理操作、板卡更换、授权处理、处理备注)
|
||||
1. WHEN 前端请求 GET `/api/admin/repair-orders` 时,THE API_Server SHALL 返回分页的维修工单列表,支持按状态[全部/待处理/处理中/已处理]、优先级[全部/高/中/低]、负责人、日期范围(开始~结束)、设备SN筛选,每条记录包含 id、sn、faultType、status、priority、assignee、createDate、description 字段
|
||||
2. WHEN 前端请求 GET `/api/admin/repair-orders/{id}` 时,THE API_Server SHALL 返回工单详情,包含:
|
||||
- 工单信息(工单号、状态、优先级、创建时间、负责人、预计修复时间)
|
||||
- 设备信息(设备SN、型号、类型、固件版本、位置,通过 deviceInfoMap 查询)
|
||||
- 故障信息(故障类型、描述、现象)
|
||||
- 处理记录时间线(日期、操作人、操作类型、备注)
|
||||
- 板卡更换记录(原板卡SN、新板卡SN、板卡类型、板卡型号、更换日期、操作人)
|
||||
3. WHEN 前端请求 POST `/api/admin/repair-orders` 时,THE API_Server SHALL 创建新维修工单,包含设备SN(从设备列表选择)、故障类型(单选6种)、故障描述、故障现象、优先级(低/中/高)、维修人员、预计修复时间、备注
|
||||
4. WHEN 前端请求 PUT `/api/admin/repair-orders/{id}/process` 时,THE API_Server SHALL 更新工单处理信息,包含处理操作类型[更换板卡/固件修复/参数重置/其他处理]、板卡更换信息(板卡类型、板卡型号、原SN、新SN)、授权处理(重新生成授权文件/推送适配固件)、处理备注
|
||||
5. WHEN 前端请求 PUT `/api/admin/repair-orders/{id}/close` 时,THE API_Server SHALL 关闭工单并将状态设置为已处理
|
||||
6. THE API_Server SHALL 提供 GET `/api/admin/repair-orders/device-info/{sn}` 接口,返回指定设备SN的设备信息(型号、类型、固件版本、位置),用于新建工单时选择设备后展示设备信息
|
||||
|
||||
### 需求 11:报废管理 API
|
||||
### 需求 13:报废管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理报废审批和物料回收数据,以便替换报废管理页面的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理报废审批和物料回收数据,以便替换报废管理页面(`/scrap`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/scrap-records` 时,THE API_Server SHALL 返回分页的报废记录列表,支持按设备SN、状态、日期筛选
|
||||
2. WHEN 前端请求 GET `/api/admin/scrap-records/{id}` 时,THE API_Server SHALL 返回报废详情,包含设备信息、审批信息、可回收物料列表、审批记录时间线
|
||||
3. WHEN 前端请求 PUT `/api/admin/scrap-records/{id}/approve` 时,THE API_Server SHALL 审批通过报废申请,更新状态为已审批
|
||||
1. WHEN 前端请求 GET `/api/admin/scrap-records` 时,THE API_Server SHALL 返回分页的报废记录列表,支持按设备SN、状态[全部/待审批/审批中/已审批/已驳回/回收中/已回收]、日期筛选,每条记录包含 sn、model、reason、applicant、status、orderId、date、value、materials 字段
|
||||
2. WHEN 前端请求 GET `/api/admin/scrap-records/{id}` 时,THE API_Server SHALL 返回报废详情,包含设备信息(SN、型号、报废日期、报废原因、残值评估)、审批信息(申请人、状态、来源工单号)、可回收物料标签列表、审批记录时间线(日期、操作类型、操作人、备注)
|
||||
3. WHEN 前端请求 PUT `/api/admin/scrap-records/{id}/approve` 时,THE API_Server SHALL 审批通过报废申请,更新状态为已审批,记录审批意见
|
||||
4. WHEN 前端请求 PUT `/api/admin/scrap-records/{id}/reject` 时,THE API_Server SHALL 驳回报废申请,更新状态为已驳回,记录驳回意见
|
||||
5. WHEN 前端请求 PUT `/api/admin/scrap-records/{id}/recover` 时,THE API_Server SHALL 完成物料回收入库,更新状态为已回收,记录回收的物料清单
|
||||
5. WHEN 前端请求 PUT `/api/admin/scrap-records/{id}/recover` 时,THE API_Server SHALL 完成物料回收入库,更新状态为已回收,记录已检测的回收物料清单和回收备注
|
||||
6. THE API_Server SHALL 提供 GET `/api/admin/scrap-records/stats` 接口,返回报废统计数据(报废总数、待审批数、已审批待回收数、已回收数)
|
||||
|
||||
### 需求 12:首页 Dashboard 统计 API
|
||||
### 需求 14:首页 Dashboard 统计 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 获取首页仪表盘的统计数据,以便替换首页的模拟数据。
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 获取首页仪表盘的统计数据,以便替换首页(`/`)的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/dashboard/metrics` 时,THE API_Server SHALL 返回设备总数、装配中数量、已激活数量、维修中数量、报废数量、授权即将到期数量等统计指标
|
||||
2. WHEN 前端请求 GET `/api/admin/dashboard/device-status` 时,THE API_Server SHALL 返回设备状态分布数据(已装配、已出厂、已激活、报废各状态的数量)
|
||||
3. WHEN 前端请求 GET `/api/admin/dashboard/tasks` 时,THE API_Server SHALL 返回待处理任务列表,包含校准即将到期、维修工单、固件升级通知、授权即将到期四个分组
|
||||
1. WHEN 前端请求 GET `/api/admin/dashboard/metrics` 时,THE API_Server SHALL 返回8项统计指标:设备总数、装配中数量、已激活数量、有新版本数量、维修中数量、报废数量、授权即将到期数量、可升级数量,每项指标包含当前值和趋势变化百分比
|
||||
2. WHEN 前端请求 GET `/api/admin/dashboard/device-status` 时,THE API_Server SHALL 返回设备状态分布数据,包含已装配、已出厂、已激活、报废四种状态的数量,用于横向条形图展示
|
||||
3. WHEN 前端请求 GET `/api/admin/dashboard/tasks` 时,THE API_Server SHALL 返回待处理任务列表,包含4个分组(校准即将到期、维修工单、固件升级通知、授权即将到期),每组包含总数和最近2条任务详情(设备SN/名称、描述、时间、跳转链接)
|
||||
|
||||
### 需求 13:前端 API 集成层
|
||||
### 需求 15:前端 API 集成层
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望在前端建立统一的 API 调用层,以便各页面组件可以方便地调用后端接口替换模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Frontend SHALL 创建统一的 API 客户端模块,配置后端 API 基础 URL、请求超时时间(5秒)、统一错误处理
|
||||
2. THE Frontend SHALL 为每个业务模块创建独立的 API 服务文件(如 deviceApi.ts、boardApi.ts、firmwareApi.ts 等)
|
||||
1. THE Frontend SHALL 创建统一的 API 客户端模块(`src/lib/api/client.ts`),配置后端 API 基础 URL(通过 NEXT_PUBLIC_API_URL 环境变量)、请求超时时间(5秒)、统一错误处理
|
||||
2. THE Frontend SHALL 为每个业务模块创建独立的 API 服务文件:deviceApi.ts、modelApi.ts、boardTypeApi.ts、boardCardApi.ts、firmwareApi.ts、calibrationApi.ts、configFileApi.ts、licenseApi.ts、repairApi.ts、scrapApi.ts、dashboardApi.ts
|
||||
3. WHEN API 请求失败时,THE Frontend SHALL 在页面上展示友好的错误提示信息
|
||||
4. WHEN API 请求正在进行时,THE Frontend SHALL 展示加载状态指示器
|
||||
5. THE Frontend SHALL 将各页面组件中的硬编码模拟数据替换为 API 调用,使用 React 的 useState 和 useEffect 管理数据获取状态
|
||||
|
||||
### 需求 14:分页与筛选标准化
|
||||
### 需求 16:分页与筛选标准化
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望前后端采用统一的分页和筛选参数规范,以便各列表页面的数据交互保持一致。
|
||||
|
||||
|
|
@ -188,3 +250,20 @@
|
|||
2. THE API_Server SHALL 对所有列表接口返回统一的分页响应格式,包含 total(总记录数)、page(当前页码)、pageSize(每页条数)、records(数据列表)
|
||||
3. THE API_Server SHALL 对所有筛选参数进行服务端校验,无效参数返回 400 错误码和描述性错误消息
|
||||
4. WHEN 筛选条件为空或为"全部"时,THE API_Server SHALL 返回不带该条件过滤的完整数据集
|
||||
|
||||
### 需求 17:采集板校准文件管理
|
||||
|
||||
**用户故事:** 作为上位机(校准设备PC端软件)操作员,我希望在完成采集板校准后将校准文件上传到设备管理平台,并通过采集板SN号自动匹配关联,以便前端板卡详情中可以查看和下载校准文件。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Database SHALL 包含 `dev.calibration_files` 表,存储校准文件信息(采集板SN号、原始文件名、存储路径、文件大小(字节)、文件MD5校验值、上传时间),并包含标准审计字段
|
||||
2. WHEN 上位机请求 POST `/api/portal/calibration-files/upload` 并携带采集板SN号和校准文件时,THE API_Server SHALL 保存校准文件到服务器存储目录,并在 `dev.calibration_files` 表中创建关联记录
|
||||
3. WHEN 上传校准文件时,THE API_Server SHALL 通过采集板SN号匹配 `dev.board_cards` 表中的板卡实例记录,将校准文件与对应的采集板关联
|
||||
4. IF 上传请求中的采集板SN号在 `dev.board_cards` 表中不存在,THEN THE API_Server SHALL 返回 404 错误码和描述性错误消息
|
||||
5. THE API_Server SHALL 将校准文件上传接口注册在 `api-portal` 模块中,该接口无需登录鉴权
|
||||
6. WHEN 前端请求 GET `/api/admin/board-cards/{id}/calibration-files` 时,THE API_Server SHALL 返回指定采集板关联的校准文件列表,每条记录包含 id、fileName(原始文件名)、fileSize(文件大小)、md5(校验值)、uploadTime(上传时间)字段,按上传时间倒序排列
|
||||
7. WHEN 前端请求 GET `/api/admin/calibration-files/{id}/download` 时,THE API_Server SHALL 返回校准文件的下载流,响应头包含正确的文件名和 Content-Type
|
||||
8. IF 请求下载的校准文件在存储目录中不存在,THEN THE API_Server SHALL 返回 404 错误码和描述性错误消息
|
||||
9. THE Frontend SHALL 在板卡详情抽屉的校准信息区域中展示关联的校准文件列表,每条记录显示文件名、文件大小、上传时间,并提供下载按钮
|
||||
10. WHEN 同一采集板SN号多次上传校准文件时,THE API_Server SHALL 保留所有历史校准文件记录,支持查看完整的校准文件历史
|
||||
|
|
|
|||
|
|
@ -1,30 +1,6 @@
|
|||
{
|
||||
"pages": {
|
||||
"/_app": [
|
||||
"static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js",
|
||||
"static/chunks/node_modules_next_dist_shared_lib_0~pg0mt._.js",
|
||||
"static/chunks/node_modules_next_dist_client_0pe1dg-._.js",
|
||||
"static/chunks/node_modules_next_dist_0u_w_5s._.js",
|
||||
"static/chunks/node_modules_next_app_0jt-zj..js",
|
||||
"static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js",
|
||||
"static/chunks/node_modules_react-dom_0bruynb._.js",
|
||||
"static/chunks/node_modules_0lx093h._.js",
|
||||
"static/chunks/[root-of-the-server]__0c0okpg._.js",
|
||||
"static/chunks/pages__app_07xvfw~._.js",
|
||||
"static/chunks/turbopack-pages__app_0_wu8vy._.js"
|
||||
],
|
||||
"/_error": [
|
||||
"static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js",
|
||||
"static/chunks/node_modules_next_dist_shared_lib_12bi_n7._.js",
|
||||
"static/chunks/node_modules_next_dist_client_0pe1dg-._.js",
|
||||
"static/chunks/node_modules_next_dist_0rt-2cr._.js",
|
||||
"static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js",
|
||||
"static/chunks/node_modules_react-dom_0bruynb._.js",
|
||||
"static/chunks/node_modules_0lx093h._.js",
|
||||
"static/chunks/[root-of-the-server]__01mw43t._.js",
|
||||
"static/chunks/pages__error_07xvfw~._.js",
|
||||
"static/chunks/turbopack-pages__error_016chbq._.js"
|
||||
]
|
||||
"/_app": []
|
||||
},
|
||||
"devFiles": [],
|
||||
"polyfillFiles": [
|
||||
|
|
|
|||
|
|
@ -1,30 +1,6 @@
|
|||
{
|
||||
"pages": {
|
||||
"/_app": [
|
||||
"static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js",
|
||||
"static/chunks/node_modules_next_dist_shared_lib_0~pg0mt._.js",
|
||||
"static/chunks/node_modules_next_dist_client_0pe1dg-._.js",
|
||||
"static/chunks/node_modules_next_dist_0u_w_5s._.js",
|
||||
"static/chunks/node_modules_next_app_0jt-zj..js",
|
||||
"static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js",
|
||||
"static/chunks/node_modules_react-dom_0bruynb._.js",
|
||||
"static/chunks/node_modules_0lx093h._.js",
|
||||
"static/chunks/[root-of-the-server]__0c0okpg._.js",
|
||||
"static/chunks/pages__app_07xvfw~._.js",
|
||||
"static/chunks/turbopack-pages__app_0_wu8vy._.js"
|
||||
],
|
||||
"/_error": [
|
||||
"static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js",
|
||||
"static/chunks/node_modules_next_dist_shared_lib_12bi_n7._.js",
|
||||
"static/chunks/node_modules_next_dist_client_0pe1dg-._.js",
|
||||
"static/chunks/node_modules_next_dist_0rt-2cr._.js",
|
||||
"static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js",
|
||||
"static/chunks/node_modules_react-dom_0bruynb._.js",
|
||||
"static/chunks/node_modules_0lx093h._.js",
|
||||
"static/chunks/[root-of-the-server]__01mw43t._.js",
|
||||
"static/chunks/pages__error_07xvfw~._.js",
|
||||
"static/chunks/turbopack-pages__error_016chbq._.js"
|
||||
]
|
||||
"/_app": []
|
||||
},
|
||||
"devFiles": [],
|
||||
"polyfillFiles": [],
|
||||
|
|
|
|||
|
|
@ -1,233 +1,47 @@
|
|||
{"timestamp":"00:00:01.061","source":"Server","level":"LOG","message":""}
|
||||
{"timestamp":"00:00:03.305","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"19:08:22.325","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"19:08:25.916","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"117:32:37.131","source":"Server","level":"LOG","message":"✓ Compiled in 153ms"}
|
||||
{"timestamp":"117:32:38.097","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:38.544","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:38.547","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload when ./src/app/devices/page.tsx changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload"}
|
||||
{"timestamp":"117:32:38.940","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:39.220","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:40.255","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:40.535","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:40.837","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:41.104","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:41.590","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:41.994","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:42.214","source":"Browser","level":"ERROR","message":"uncaughtError: Error: The default export is not a React Component in \"/devices/page\""}
|
||||
{"timestamp":"117:32:42.258","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: The default export is not a React Component in \\\"/devices/page\\\"\\u001b[39m\""}
|
||||
{"timestamp":"117:32:42.259","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: The default export is not a React Component in \"/devices/page\"\u001b[39m"}
|
||||
{"timestamp":"117:33:29.348","source":"Server","level":"LOG","message":"✓ Compiled in 274ms"}
|
||||
{"timestamp":"117:33:30.697","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"118:56:04.251","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"118:56:06.098","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"118:56:21.517","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:13:03.183","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:13:04.798","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:14:07.937","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:18:22.932","source":"Server","level":"LOG","message":"✓ Compiled in 65ms"}
|
||||
{"timestamp":"119:25:27.462","source":"Server","level":"LOG","message":"✓ Compiled in 29ms"}
|
||||
{"timestamp":"119:28:26.107","source":"Server","level":"LOG","message":"✓ Compiled in 58ms"}
|
||||
{"timestamp":"119:28:32.906","source":"Server","level":"LOG","message":"✓ Compiled in 30ms"}
|
||||
{"timestamp":"119:29:36.058","source":"Server","level":"LOG","message":"✓ Compiled in 49ms"}
|
||||
{"timestamp":"119:30:08.040","source":"Server","level":"LOG","message":"✓ Compiled in 48ms"}
|
||||
{"timestamp":"119:36:42.082","source":"Server","level":"LOG","message":"✓ Compiled in 67ms"}
|
||||
{"timestamp":"119:36:42.377","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: modelsData is not defined"}
|
||||
{"timestamp":"119:36:42.484","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: modelsData is not defined\\u001b[39m\\n\\u001b[31m at ModelsPage (src/app/models/page.tsx:107:14)\\u001b[39m\\n \\u001b[90m105 |\\u001b[0m </thead>\\n \\u001b[90m106 |\\u001b[0m <tbody>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m107 |\\u001b[0m {modelsData.map(model => (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m108 |\\u001b[0m <tr key={model.id} style={{ borderBottom: \\u001b[32m'1px solid #F0F0F0'\\u001b[0m }}>\\n \\u001b[90m109 |\\u001b[0m <td style={{ padding: \\u001b[32m'12px 16px'\\u001b[0m, fontSize: \\u001b[35m14\\u001b[0m, fontWeight: \\u001b[35m500\\u001b[0m }}>{model.name}</td>\\n \\u001b[90m110 |\\u001b[0m <td style={{ padding: \\u001b[32m'12px 16px'\\u001b[0m, fontSize: \\u001b[35m14\\u001b[0m, color: \\u001b[32m'rgba(0,0,0,0.65)'\\u001b[0m }}>{model.code}</td>\""}
|
||||
{"timestamp":"119:36:42.484","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: modelsData is not defined\u001b[39m\n\u001b[31m at ModelsPage (src/app/models/page.tsx:107:14)\u001b[39m\n \u001b[90m105 |\u001b[0m </thead>\n \u001b[90m106 |\u001b[0m <tbody>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m107 |\u001b[0m {modelsData.map(model => (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m108 |\u001b[0m <tr key={model.id} style={{ borderBottom: \u001b[32m'1px solid #F0F0F0'\u001b[0m }}>\n \u001b[90m109 |\u001b[0m <td style={{ padding: \u001b[32m'12px 16px'\u001b[0m, fontSize: \u001b[35m14\u001b[0m, fontWeight: \u001b[35m500\u001b[0m }}>{model.name}</td>\n \u001b[90m110 |\u001b[0m <td style={{ padding: \u001b[32m'12px 16px'\u001b[0m, fontSize: \u001b[35m14\u001b[0m, color: \u001b[32m'rgba(0,0,0,0.65)'\u001b[0m }}>{model.code}</td>"}
|
||||
{"timestamp":"119:36:56.088","source":"Server","level":"LOG","message":"✓ Compiled in 100ms"}
|
||||
{"timestamp":"119:36:56.099","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"119:36:57.229","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:37:03.520","source":"Server","level":"LOG","message":"✓ Compiled in 36ms"}
|
||||
{"timestamp":"119:37:24.669","source":"Server","level":"LOG","message":"✓ Compiled in 86ms"}
|
||||
{"timestamp":"119:37:43.557","source":"Server","level":"LOG","message":"✓ Compiled in 47ms"}
|
||||
{"timestamp":"119:38:59.568","source":"Server","level":"LOG","message":"✓ Compiled in 194ms"}
|
||||
{"timestamp":"119:39:00.886","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:41:47.398","source":"Server","level":"LOG","message":"✓ Compiled in 30ms"}
|
||||
{"timestamp":"119:42:31.585","source":"Server","level":"LOG","message":"✓ Compiled in 32ms"}
|
||||
{"timestamp":"119:45:00.791","source":"Server","level":"LOG","message":"✓ Compiled in 52ms"}
|
||||
{"timestamp":"119:45:16.637","source":"Server","level":"LOG","message":"✓ Compiled in 86ms"}
|
||||
{"timestamp":"119:45:16.991","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: batchList is not defined"}
|
||||
{"timestamp":"119:45:17.051","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: batchList is not defined\\u001b[39m\\n\\u001b[31m at DevicesPage (src/app/devices/page.tsx:198:14)\\u001b[39m\\n \\u001b[90m196 |\\u001b[0m }}>{devicesData.length}</span>\\n \\u001b[90m197 |\\u001b[0m </button>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m198 |\\u001b[0m {batchList.map(({ batch, count }) => (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m199 |\\u001b[0m <button\\n \\u001b[90m200 |\\u001b[0m key={batch}\\n \\u001b[90m201 |\\u001b[0m onClick={() => handleBatchSelect(batch)}\""}
|
||||
{"timestamp":"119:45:17.053","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: batchList is not defined\u001b[39m\n\u001b[31m at DevicesPage (src/app/devices/page.tsx:198:14)\u001b[39m\n \u001b[90m196 |\u001b[0m }}>{devicesData.length}</span>\n \u001b[90m197 |\u001b[0m </button>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m198 |\u001b[0m {batchList.map(({ batch, count }) => (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m199 |\u001b[0m <button\n \u001b[90m200 |\u001b[0m key={batch}\n \u001b[90m201 |\u001b[0m onClick={() => handleBatchSelect(batch)}"}
|
||||
{"timestamp":"119:45:41.032","source":"Server","level":"LOG","message":"✓ Compiled in 52ms"}
|
||||
{"timestamp":"119:45:41.037","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"119:45:42.083","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"119:49:42.815","source":"Server","level":"LOG","message":"✓ Compiled in 89ms"}
|
||||
{"timestamp":"120:11:46.034","source":"Server","level":"LOG","message":"✓ Compiled in 46ms"}
|
||||
{"timestamp":"120:15:49.161","source":"Server","level":"LOG","message":"✓ Compiled in 38ms"}
|
||||
{"timestamp":"120:15:54.549","source":"Server","level":"ERROR","message":"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."}
|
||||
{"timestamp":"120:15:54.759","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:15:54.834","source":"Browser","level":"ERROR","message":"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."}
|
||||
{"timestamp":"120:15:54.853","source":"Server","level":"ERROR","message":"[browser] \"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.\" \"\""}
|
||||
{"timestamp":"120:15:54.854","source":"Browser","level":"ERROR","message":"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`. \"\""}
|
||||
{"timestamp":"120:16:05.201","source":"Server","level":"LOG","message":"✓ Compiled in 67ms"}
|
||||
{"timestamp":"120:21:20.618","source":"Server","level":"LOG","message":"✓ Compiled in 71ms"}
|
||||
{"timestamp":"120:21:52.821","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:53.298","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:53.333","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload when ./src/app/calibration/page.tsx changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload"}
|
||||
{"timestamp":"120:21:53.887","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:54.306","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:54.655","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:54.957","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:55.271","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:55.651","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:56.158","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:56.535","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:56.652","source":"Browser","level":"ERROR","message":"uncaughtError: Error: The default export is not a React Component in \"/calibration/page\""}
|
||||
{"timestamp":"120:21:56.662","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: The default export is not a React Component in \\\"/calibration/page\\\"\\u001b[39m\""}
|
||||
{"timestamp":"120:21:56.663","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: The default export is not a React Component in \"/calibration/page\"\u001b[39m"}
|
||||
{"timestamp":"120:22:31.629","source":"Server","level":"ERROR","message":"⨯ ./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:22:31.636","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:22:31.816","source":"Server","level":"ERROR","message":"⨯ ./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:22:31.818","source":"Server","level":"ERROR","message":"[browser] \"./src/app/calibration/page.tsx:138:1\\nExpected '</', got '<eof>'\\n 136 | </div>\\n 137 | </div>\\n> 138 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/calibration/page.tsx [Client Component Browser]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/calibration/page.tsx [Client Component SSR]\\n ./src/app/calibration/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:22:31.819","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:22:31.821","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:22:31.822","source":"Server","level":"ERROR","message":"[browser] \"./src/app/calibration/page.tsx:138:1\\nExpected '</', got '<eof>'\\n 136 | </div>\\n 137 | </div>\\n> 138 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/calibration/page.tsx [Client Component Browser]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/calibration/page.tsx [Client Component SSR]\\n ./src/app/calibration/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:22:31.822","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:22:31.902","source":"Browser","level":"ERROR","message":"uncaughtError: Error: ./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:22:31.904","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: ./src/app/calibration/page.tsx:138:1\\nExpected '</', got '<eof>'\\n 136 | </div>\\n 137 | </div>\\n> 138 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/calibration/page.tsx [Client Component Browser]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/calibration/page.tsx [Client Component SSR]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n\\u001b[39m\\n\\u001b[31m at <unknown> (Error: ./src/app/calibration/page.tsx:138:1)\\n at <unknown> (Error: (./src/app/calibration/page.tsx:138:1)\\u001b[39m\""}
|
||||
{"timestamp":"120:22:31.904","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: ./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]\n\n\u001b[39m\n\u001b[31m at <unknown> (Error: ./src/app/calibration/page.tsx:138:1)\n at <unknown> (Error: (./src/app/calibration/page.tsx:138:1)\u001b[39m"}
|
||||
{"timestamp":"120:22:31.923","source":"Server","level":"ERROR","message":"[browser] \"./src/app/calibration/page.tsx:138:1\\nExpected '</', got '<eof>'\\n 136 | </div>\\n 137 | </div>\\n> 138 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/calibration/page.tsx [Client Component Browser]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/calibration/page.tsx [Client Component SSR]\\n ./src/app/calibration/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:22:31.923","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:22:31.924","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:22:31.954","source":"Server","level":"ERROR","message":"[browser] \"./src/app/calibration/page.tsx:138:1\\nExpected '</', got '<eof>'\\n 136 | </div>\\n 137 | </div>\\n> 138 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/calibration/page.tsx [Client Component Browser]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/calibration/page.tsx [Client Component SSR]\\n ./src/app/calibration/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:22:31.955","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:22:31.955","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:138:1\nExpected '</', got '<eof>'\n 136 | </div>\n 137 | </div>\n> 138 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:22:55.601","source":"Server","level":"ERROR","message":"[browser] \"./src/app/calibration/page.tsx:192:1\\nExpected '</', got '<eof>'\\n 190 | </div>\\n 191 | </div>\\n> 192 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/calibration/page.tsx [Client Component Browser]\\n ./src/app/calibration/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/calibration/page.tsx [Client Component SSR]\\n ./src/app/calibration/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:22:55.601","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:192:1\nExpected '</', got '<eof>'\n 190 | </div>\n 191 | </div>\n> 192 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:22:55.602","source":"Browser","level":"ERROR","message":"./src/app/calibration/page.tsx:192:1\nExpected '</', got '<eof>'\n 190 | </div>\n 191 | </div>\n> 192 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/calibration/page.tsx [Client Component Browser]\n ./src/app/calibration/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/calibration/page.tsx [Client Component SSR]\n ./src/app/calibration/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:23:28.321","source":"Server","level":"LOG","message":"✓ Compiled in 65ms"}
|
||||
{"timestamp":"120:23:29.053","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:24:00.938","source":"Server","level":"LOG","message":"✓ Compiled in 76ms"}
|
||||
{"timestamp":"120:24:01.199","source":"Browser","level":"ERROR","message":"uncaughtError: TypeError: Cannot read properties of undefined (reading 'map')"}
|
||||
{"timestamp":"120:24:01.332","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught TypeError: Cannot read properties of undefined (reading 'map')\\u001b[39m\\n\\u001b[31m at eval (src/app/components/sidebar.tsx:36:24)\\n at Array.map (<anonymous>)\\n at Sidebar (src/app/components/sidebar.tsx:33:19)\\n at RootLayout (src\\\\app\\\\layout.tsx:14:11)\\u001b[39m\\n \\u001b[90m34 |\\u001b[0m <div key={group.title}>\\n \\u001b[90m35 |\\u001b[0m <div className=\\u001b[32m\\\"px-4 py-2.5 text-xs font-semibold uppercase tracking-wider\\\"\\u001b[0m style={{ color: \\u001b[32m'rgba(0,0,0,0.35..\\u001b[0m.\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m36 |\\u001b[0m {group.items.map(item => {\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m37 |\\u001b[0m \\u001b[36mconst\\u001b[0m \\u001b[33mIcon\\u001b[0m = item.icon\\n \\u001b[90m38 |\\u001b[0m \\u001b[36mreturn\\u001b[0m (\\n \\u001b[90m39 |\\u001b[0m <\\u001b[33mLink\\u001b[0m key={item.path} href={item.path} className=\\u001b[32m\\\"flex items-center gap-2 px-4 py-2 text-sm transition-c...\\u001b[0m\""}
|
||||
{"timestamp":"120:24:01.334","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught TypeError: Cannot read properties of undefined (reading 'map')\u001b[39m\n\u001b[31m at eval (src/app/components/sidebar.tsx:36:24)\n at Array.map (<anonymous>)\n at Sidebar (src/app/components/sidebar.tsx:33:19)\n at RootLayout (src\\app\\layout.tsx:14:11)\u001b[39m\n \u001b[90m34 |\u001b[0m <div key={group.title}>\n \u001b[90m35 |\u001b[0m <div className=\u001b[32m\"px-4 py-2.5 text-xs font-semibold uppercase tracking-wider\"\u001b[0m style={{ color: \u001b[32m'rgba(0,0,0,0.35..\u001b[0m.\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m36 |\u001b[0m {group.items.map(item => {\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m37 |\u001b[0m \u001b[36mconst\u001b[0m \u001b[33mIcon\u001b[0m = item.icon\n \u001b[90m38 |\u001b[0m \u001b[36mreturn\u001b[0m (\n \u001b[90m39 |\u001b[0m <\u001b[33mLink\u001b[0m key={item.path} href={item.path} className=\u001b[32m\"flex items-center gap-2 px-4 py-2 text-sm transition-c...\u001b[0m"}
|
||||
{"timestamp":"120:24:25.345","source":"Server","level":"LOG","message":"✓ Compiled in 49ms"}
|
||||
{"timestamp":"120:24:25.347","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"120:24:25.908","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:26:49.542","source":"Server","level":"LOG","message":"✓ Compiled in 70ms"}
|
||||
{"timestamp":"120:31:09.492","source":"Browser","level":"ERROR","message":"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."}
|
||||
{"timestamp":"120:31:09.494","source":"Server","level":"ERROR","message":"[browser] \"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.\" \"\""}
|
||||
{"timestamp":"120:31:09.494","source":"Browser","level":"ERROR","message":"You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`. \"\""}
|
||||
{"timestamp":"120:31:53.655","source":"Server","level":"LOG","message":"✓ Compiled in 26ms"}
|
||||
{"timestamp":"120:33:07.451","source":"Server","level":"LOG","message":"✓ Compiled in 31ms"}
|
||||
{"timestamp":"120:33:49.615","source":"Server","level":"LOG","message":"✓ Compiled in 35ms"}
|
||||
{"timestamp":"120:33:49.785","source":"Browser","level":"ERROR","message":"A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components"}
|
||||
{"timestamp":"120:33:49.807","source":"Server","level":"ERROR","message":"[browser] \"A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components\" \"\""}
|
||||
{"timestamp":"120:33:49.808","source":"Browser","level":"ERROR","message":"A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components \"\""}
|
||||
{"timestamp":"120:33:54.669","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:35:37.666","source":"Server","level":"LOG","message":"✓ Compiled in 37ms"}
|
||||
{"timestamp":"120:37:14.496","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"120:37:47.169","source":"Server","level":"LOG","message":"✓ Compiled in 30ms"}
|
||||
{"timestamp":"120:37:53.739","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:38:48.262","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:39:20.372","source":"Server","level":"LOG","message":"✓ Compiled in 44ms"}
|
||||
{"timestamp":"120:40:15.243","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:41:21.738","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:22.130","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:22.971","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:23.735","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:24.485","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:24.972","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:25.720","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:26.046","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:26.173","source":"Browser","level":"ERROR","message":"uncaughtError: Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:26.185","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: The default export is not a React Component in \\\"/devices/[sn]/page\\\"\\u001b[39m\""}
|
||||
{"timestamp":"120:41:26.186","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: The default export is not a React Component in \"/devices/[sn]/page\"\u001b[39m"}
|
||||
{"timestamp":"120:41:58.004","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:58.319","source":"Server","level":"ERROR","message":"⨯ Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:58.527","source":"Browser","level":"ERROR","message":"uncaughtError: Error: The default export is not a React Component in \"/devices/[sn]/page\""}
|
||||
{"timestamp":"120:41:58.528","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: The default export is not a React Component in \\\"/devices/[sn]/page\\\"\\u001b[39m\""}
|
||||
{"timestamp":"120:41:58.529","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: The default export is not a React Component in \"/devices/[sn]/page\"\u001b[39m"}
|
||||
{"timestamp":"120:42:20.220","source":"Server","level":"ERROR","message":"⨯ ./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:42:20.228","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:42:20.230","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:154:1\\nExpected '</', got '<eof>'\\n 152 | </div>\\n 153 | </div>\\n> 154 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:42:20.233","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:42:20.368","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:42:20.368","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:154:1\\nExpected '</', got '<eof>'\\n 152 | </div>\\n 153 | </div>\\n> 154 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:42:20.368","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:42:20.370","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:42:20.371","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:154:1\\nExpected '</', got '<eof>'\\n 152 | </div>\\n 153 | </div>\\n> 154 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:42:20.371","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:42:20.381","source":"Server","level":"ERROR","message":"⨯ ./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:42:20.499","source":"Browser","level":"ERROR","message":"uncaughtError: Error: ./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:42:20.504","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: ./src/app/devices/[sn]/page.tsx:154:1\\nExpected '</', got '<eof>'\\n 152 | </div>\\n 153 | </div>\\n> 154 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n\\u001b[39m\\n\\u001b[31m at <unknown> (Error: ./src/app/devices/[sn]/page.tsx:154:1)\\n at <unknown> (Error: (./src/app/devices/[sn]/page.tsx:154:1)\\u001b[39m\""}
|
||||
{"timestamp":"120:42:20.505","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: ./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n\u001b[39m\n\u001b[31m at <unknown> (Error: ./src/app/devices/[sn]/page.tsx:154:1)\n at <unknown> (Error: (./src/app/devices/[sn]/page.tsx:154:1)\u001b[39m"}
|
||||
{"timestamp":"120:42:20.519","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:154:1\\nExpected '</', got '<eof>'\\n 152 | </div>\\n 153 | </div>\\n> 154 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:42:20.519","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:42:20.520","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:42:20.587","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:154:1\\nExpected '</', got '<eof>'\\n 152 | </div>\\n 153 | </div>\\n> 154 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:42:20.588","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:42:20.590","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:154:1\nExpected '</', got '<eof>'\n 152 | </div>\n 153 | </div>\n> 154 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:42:40.514","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:199:1\\nExpected '</', got '<eof>'\\n 197 | ))}\\n 198 | </div>\\n> 199 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:42:40.515","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:199:1\nExpected '</', got '<eof>'\n 197 | ))}\n 198 | </div>\n> 199 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:42:40.515","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:199:1\nExpected '</', got '<eof>'\n 197 | ))}\n 198 | </div>\n> 199 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:43:14.065","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:269:1\\nExpected '</', got '<eof>'\\n 267 | </div>\\n 268 | )}\\n> 269 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:43:14.065","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:269:1\nExpected '</', got '<eof>'\n 267 | </div>\n 268 | )}\n> 269 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:43:14.066","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:269:1\nExpected '</', got '<eof>'\n 267 | </div>\n 268 | )}\n> 269 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:43:45.661","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:328:1\\nExpected '</', got '<eof>'\\n 326 | </div>\\n 327 | )}\\n> 328 |\\n | ^\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:43:45.663","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:328:1\nExpected '</', got '<eof>'\n 326 | </div>\n 327 | )}\n> 328 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:43:45.665","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:328:1\nExpected '</', got '<eof>'\n 326 | </div>\n 327 | )}\n> 328 |\n | ^\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:44:03.932","source":"Server","level":"LOG","message":"✓ Compiled in 145ms"}
|
||||
{"timestamp":"120:44:04.986","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:45:42.964","source":"Server","level":"LOG","message":"✓ Compiled in 38ms"}
|
||||
{"timestamp":"120:48:15.982","source":"Server","level":"LOG","message":"✓ Compiled in 40ms"}
|
||||
{"timestamp":"120:52:11.129","source":"Server","level":"LOG","message":"✓ Compiled in 36ms"}
|
||||
{"timestamp":"120:53:27.719","source":"Server","level":"LOG","message":"✓ Compiled in 43ms"}
|
||||
{"timestamp":"120:53:57.768","source":"Server","level":"LOG","message":"✓ Compiled in 57ms"}
|
||||
{"timestamp":"120:54:02.970","source":"Server","level":"LOG","message":"✓ Compiled in 36ms"}
|
||||
{"timestamp":"120:54:14.218","source":"Server","level":"LOG","message":"✓ Compiled in 30ms"}
|
||||
{"timestamp":"120:54:30.156","source":"Server","level":"LOG","message":"✓ Compiled in 33ms"}
|
||||
{"timestamp":"120:54:51.314","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"120:54:59.763","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"120:55:14.825","source":"Server","level":"LOG","message":"✓ Compiled in 44ms"}
|
||||
{"timestamp":"120:58:05.296","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"120:58:05.328","source":"Server","level":"ERROR","message":"⨯ ./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:58:05.369","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:58:05.369","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:58:05.381","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:58:05.393","source":"Server","level":"ERROR","message":"⨯ ./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:58:05.418","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:58:05.419","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:58:05.422","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:58:05.423","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:58:05.425","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:58:05.426","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:58:05.447","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:58:05.449","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:58:05.450","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:58:05.456","source":"Server","level":"ERROR","message":"⨯ ./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:58:05.470","source":"Server","level":"ERROR","message":"⨯ ./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:58:05.773","source":"Browser","level":"ERROR","message":"uncaughtError: Error: ./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n"}
|
||||
{"timestamp":"120:58:05.785","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught Error: ./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n\\u001b[39m\\n\\u001b[31m at <unknown> (Error: ./src/app/devices/[sn]/page.tsx:179:7)\\n at <unknown> (Error: (./src/app/devices/[sn]/page.tsx:179:7)\\u001b[39m\""}
|
||||
{"timestamp":"120:58:05.787","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught Error: ./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n\u001b[39m\n\u001b[31m at <unknown> (Error: ./src/app/devices/[sn]/page.tsx:179:7)\n at <unknown> (Error: (./src/app/devices/[sn]/page.tsx:179:7)\u001b[39m"}
|
||||
{"timestamp":"120:58:05.802","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:58:05.804","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:58:05.805","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:58:05.911","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component]"}
|
||||
{"timestamp":"120:58:05.912","source":"Server","level":"ERROR","message":"[browser] \"./src/app/devices/[sn]/page.tsx:179:7\\nExpected ',', got '{'\\n 177 | </div>\\n 178 |\\n> 179 | {/* Tabs */}\\n | ^\\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\\n 181 | {tabs.map(tab => (\\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\\n\\nParsing ecmascript source code failed\\n\\nImport traces:\\n Client Component Browser:\\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\\n\\n Client Component SSR:\\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\\n ./src/app/devices/[sn]/page.tsx [Server Component]\" \"\""}
|
||||
{"timestamp":"120:58:05.912","source":"Browser","level":"ERROR","message":"./src/app/devices/[sn]/page.tsx:179:7\nExpected ',', got '{'\n 177 | </div>\n 178 |\n> 179 | {/* Tabs */}\n | ^\n 180 | <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>\n 181 | {tabs.map(tab => (\n 182 | <button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{\n\nParsing ecmascript source code failed\n\nImport traces:\n Client Component Browser:\n ./src/app/devices/[sn]/page.tsx [Client Component Browser]\n ./src/app/devices/[sn]/page.tsx [Server Component]\n\n Client Component SSR:\n ./src/app/devices/[sn]/page.tsx [Client Component SSR]\n ./src/app/devices/[sn]/page.tsx [Server Component] \"\""}
|
||||
{"timestamp":"120:58:20.378","source":"Server","level":"LOG","message":"✓ Compiled in 112ms"}
|
||||
{"timestamp":"120:58:21.227","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"120:58:30.947","source":"Server","level":"LOG","message":"✓ Compiled in 33ms"}
|
||||
{"timestamp":"121:35:19.134","source":"Server","level":"LOG","message":"✓ Compiled in 54ms"}
|
||||
{"timestamp":"121:35:26.186","source":"Server","level":"LOG","message":"✓ Compiled in 92ms"}
|
||||
{"timestamp":"136:35:03.263","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"136:38:00.817","source":"Server","level":"LOG","message":"✓ Compiled in 52ms"}
|
||||
{"timestamp":"136:40:21.076","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"136:43:58.112","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"136:47:36.538","source":"Server","level":"LOG","message":"✓ Compiled in 39ms"}
|
||||
{"timestamp":"136:49:42.435","source":"Server","level":"LOG","message":"✓ Compiled in 58ms"}
|
||||
{"timestamp":"136:50:09.938","source":"Server","level":"LOG","message":"✓ Compiled in 45ms"}
|
||||
{"timestamp":"136:50:19.856","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"136:51:19.379","source":"Server","level":"LOG","message":"✓ Compiled in 32ms"}
|
||||
{"timestamp":"136:51:32.486","source":"Server","level":"LOG","message":"✓ Compiled in 37ms"}
|
||||
{"timestamp":"136:56:56.629","source":"Server","level":"LOG","message":"✓ Compiled in 29ms"}
|
||||
{"timestamp":"136:57:06.457","source":"Server","level":"LOG","message":"✓ Compiled in 56ms"}
|
||||
{"timestamp":"136:57:17.332","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"144:03:02.593","source":"Server","level":"LOG","message":"✓ Compiled in 33ms"}
|
||||
{"timestamp":"144:03:05.593","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"144:03:35.022","source":"Server","level":"LOG","message":"✓ Compiled in 29ms"}
|
||||
{"timestamp":"144:03:51.002","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"144:04:23.708","source":"Server","level":"LOG","message":"✓ Compiled in 28ms"}
|
||||
{"timestamp":"00:00:01.927","source":"Server","level":"LOG","message":""}
|
||||
{"timestamp":"00:00:05.166","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:02:55.137","source":"Server","level":"LOG","message":"✓ Compiled in 505ms"}
|
||||
{"timestamp":"00:02:55.338","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: taskGroups is not defined"}
|
||||
{"timestamp":"00:02:55.342","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: taskGroups is not defined\\u001b[39m\\n\\u001b[31m at DashboardPage (src/app/page.tsx:77:12)\\u001b[39m\\n \\u001b[90m75 |\\u001b[0m <h3 className=\\u001b[32m\\\"text-lg font-semibold mb-6\\\"\\u001b[0m>待处理任务</h3>\\n \\u001b[90m76 |\\u001b[0m <div className=\\u001b[32m\\\"grid grid-cols-2 gap-6\\\"\\u001b[0m>\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m77 |\\u001b[0m {taskGroups.map((group, gi) => (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m78 |\\u001b[0m <div key={gi}>\\n \\u001b[90m79 |\\u001b[0m <div className=\\u001b[32m\\\"flex items-center justify-between mb-4\\\"\\u001b[0m>\\n \\u001b[90m80 |\\u001b[0m <h4 className=\\u001b[32m\\\"text-base font-medium\\\"\\u001b[0m>{group.title}</h4>\""}
|
||||
{"timestamp":"00:02:55.343","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: taskGroups is not defined\u001b[39m\n\u001b[31m at DashboardPage (src/app/page.tsx:77:12)\u001b[39m\n \u001b[90m75 |\u001b[0m <h3 className=\u001b[32m\"text-lg font-semibold mb-6\"\u001b[0m>待处理任务</h3>\n \u001b[90m76 |\u001b[0m <div className=\u001b[32m\"grid grid-cols-2 gap-6\"\u001b[0m>\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m77 |\u001b[0m {taskGroups.map((group, gi) => (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m78 |\u001b[0m <div key={gi}>\n \u001b[90m79 |\u001b[0m <div className=\u001b[32m\"flex items-center justify-between mb-4\"\u001b[0m>\n \u001b[90m80 |\u001b[0m <h4 className=\u001b[32m\"text-base font-medium\"\u001b[0m>{group.title}</h4>"}
|
||||
{"timestamp":"00:03:27.339","source":"Server","level":"LOG","message":"✓ Compiled in 43ms"}
|
||||
{"timestamp":"00:03:27.342","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"00:03:27.675","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:04:00.436","source":"Server","level":"LOG","message":"✓ Compiled in 31ms"}
|
||||
{"timestamp":"00:15:02.952","source":"Server","level":"LOG","message":"✓ Compiled in 50ms"}
|
||||
{"timestamp":"00:15:10.621","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"00:15:19.794","source":"Server","level":"LOG","message":"✓ Compiled in 36ms"}
|
||||
{"timestamp":"00:15:34.242","source":"Server","level":"LOG","message":"✓ Compiled in 59ms"}
|
||||
{"timestamp":"00:15:52.132","source":"Server","level":"LOG","message":"✓ Compiled in 71ms"}
|
||||
{"timestamp":"00:16:01.417","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"00:16:09.653","source":"Server","level":"LOG","message":"✓ Compiled in 34ms"}
|
||||
{"timestamp":"00:16:18.455","source":"Server","level":"LOG","message":"✓ Compiled in 53ms"}
|
||||
{"timestamp":"00:23:51.418","source":"Server","level":"LOG","message":"✓ Compiled in 54ms"}
|
||||
{"timestamp":"00:24:00.421","source":"Server","level":"LOG","message":"✓ Compiled in 32ms"}
|
||||
{"timestamp":"00:24:08.371","source":"Server","level":"LOG","message":"✓ Compiled in 39ms"}
|
||||
{"timestamp":"00:24:12.614","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:24:36.581","source":"Server","level":"LOG","message":"✓ Compiled in 32ms"}
|
||||
{"timestamp":"00:33:32.945","source":"Server","level":"LOG","message":"✓ Compiled in 104ms"}
|
||||
{"timestamp":"00:43:37.634","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:44:30.678","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:46:40.902","source":"Server","level":"LOG","message":"✓ Compiled in 106ms"}
|
||||
{"timestamp":"00:46:57.334","source":"Server","level":"LOG","message":"✓ Compiled in 46ms"}
|
||||
{"timestamp":"00:46:57.600","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: actionMenuId is not defined"}
|
||||
{"timestamp":"00:46:57.634","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: actionMenuId is not defined\\u001b[39m\\n\\u001b[31m at ModelsPage (src/app/models/page.tsx:249:8)\\u001b[39m\\n \\u001b[90m247 |\\u001b[0m\\n \\u001b[90m248 |\\u001b[0m {\\u001b[90m/* Click outside to close action menu */\\u001b[0m}\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m249 |\\u001b[0m {actionMenuId !== \\u001b[36mnull\\u001b[0m && (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m250 |\\u001b[0m <div onClick={() => setActionMenuId(\\u001b[36mnull\\u001b[0m)} style={{ position: \\u001b[32m'fixed'\\u001b[0m, inset: \\u001b[35m0\\u001b[0m, zIndex: \\u001b[35m5\\u001b[0m }} />\\n \\u001b[90m251 |\\u001b[0m )}\\n \\u001b[90m252 |\\u001b[0m\""}
|
||||
{"timestamp":"00:46:57.635","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: actionMenuId is not defined\u001b[39m\n\u001b[31m at ModelsPage (src/app/models/page.tsx:249:8)\u001b[39m\n \u001b[90m247 |\u001b[0m\n \u001b[90m248 |\u001b[0m {\u001b[90m/* Click outside to close action menu */\u001b[0m}\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m249 |\u001b[0m {actionMenuId !== \u001b[36mnull\u001b[0m && (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m250 |\u001b[0m <div onClick={() => setActionMenuId(\u001b[36mnull\u001b[0m)} style={{ position: \u001b[32m'fixed'\u001b[0m, inset: \u001b[35m0\u001b[0m, zIndex: \u001b[35m5\u001b[0m }} />\n \u001b[90m251 |\u001b[0m )}\n \u001b[90m252 |\u001b[0m"}
|
||||
{"timestamp":"00:47:05.547","source":"Server","level":"LOG","message":"✓ Compiled in 35ms"}
|
||||
{"timestamp":"00:47:05.554","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"00:47:05.961","source":"Server","level":"ERROR","message":"⨯ ReferenceError: actionMenuId is not defined"}
|
||||
{"timestamp":"00:47:05.989","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: actionMenuId is not defined"}
|
||||
{"timestamp":"00:47:06.088","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: actionMenuId is not defined\\u001b[39m\\n\\u001b[31m at ModelsPage (src/app/models/page.tsx:248:8)\\n at Set.forEach (<anonymous>)\\u001b[39m\\n \\u001b[90m246 |\\u001b[0m\\n \\u001b[90m247 |\\u001b[0m {\\u001b[90m/* Click outside to close action menu */\\u001b[0m}\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m248 |\\u001b[0m {actionMenuId !== \\u001b[36mnull\\u001b[0m && (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m249 |\\u001b[0m <div onClick={() => setActionMenuId(\\u001b[36mnull\\u001b[0m)} style={{ position: \\u001b[32m'fixed'\\u001b[0m, inset: \\u001b[35m0\\u001b[0m, zIndex: \\u001b[35m5\\u001b[0m }} />\\n \\u001b[90m250 |\\u001b[0m )}\\n \\u001b[90m251 |\\u001b[0m\""}
|
||||
{"timestamp":"00:47:06.091","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: actionMenuId is not defined\u001b[39m\n\u001b[31m at ModelsPage (src/app/models/page.tsx:248:8)\n at Set.forEach (<anonymous>)\u001b[39m\n \u001b[90m246 |\u001b[0m\n \u001b[90m247 |\u001b[0m {\u001b[90m/* Click outside to close action menu */\u001b[0m}\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m248 |\u001b[0m {actionMenuId !== \u001b[36mnull\u001b[0m && (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m249 |\u001b[0m <div onClick={() => setActionMenuId(\u001b[36mnull\u001b[0m)} style={{ position: \u001b[32m'fixed'\u001b[0m, inset: \u001b[35m0\u001b[0m, zIndex: \u001b[35m5\u001b[0m }} />\n \u001b[90m250 |\u001b[0m )}\n \u001b[90m251 |\u001b[0m"}
|
||||
{"timestamp":"00:47:06.199","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"00:47:06.347","source":"Browser","level":"ERROR","message":"uncaughtError: ReferenceError: actionMenuId is not defined"}
|
||||
{"timestamp":"00:47:06.349","source":"Server","level":"ERROR","message":"[browser] \"\\u001b[31mUncaught ReferenceError: actionMenuId is not defined\\u001b[39m\\n\\u001b[31m at ModelsPage (src/app/models/page.tsx:248:8)\\u001b[39m\\n \\u001b[90m246 |\\u001b[0m\\n \\u001b[90m247 |\\u001b[0m {\\u001b[90m/* Click outside to close action menu */\\u001b[0m}\\n\\u001b[31m\\u001b[1m>\\u001b[0m \\u001b[90m248 |\\u001b[0m {actionMenuId !== \\u001b[36mnull\\u001b[0m && (\\n \\u001b[90m |\\u001b[0m \\u001b[31m\\u001b[1m^\\u001b[0m\\n \\u001b[90m249 |\\u001b[0m <div onClick={() => setActionMenuId(\\u001b[36mnull\\u001b[0m)} style={{ position: \\u001b[32m'fixed'\\u001b[0m, inset: \\u001b[35m0\\u001b[0m, zIndex: \\u001b[35m5\\u001b[0m }} />\\n \\u001b[90m250 |\\u001b[0m )}\\n \\u001b[90m251 |\\u001b[0m\""}
|
||||
{"timestamp":"00:47:06.349","source":"Browser","level":"ERROR","message":"\u001b[31mUncaught ReferenceError: actionMenuId is not defined\u001b[39m\n\u001b[31m at ModelsPage (src/app/models/page.tsx:248:8)\u001b[39m\n \u001b[90m246 |\u001b[0m\n \u001b[90m247 |\u001b[0m {\u001b[90m/* Click outside to close action menu */\u001b[0m}\n\u001b[31m\u001b[1m>\u001b[0m \u001b[90m248 |\u001b[0m {actionMenuId !== \u001b[36mnull\u001b[0m && (\n \u001b[90m |\u001b[0m \u001b[31m\u001b[1m^\u001b[0m\n \u001b[90m249 |\u001b[0m <div onClick={() => setActionMenuId(\u001b[36mnull\u001b[0m)} style={{ position: \u001b[32m'fixed'\u001b[0m, inset: \u001b[35m0\u001b[0m, zIndex: \u001b[35m5\u001b[0m }} />\n \u001b[90m250 |\u001b[0m )}\n \u001b[90m251 |\u001b[0m"}
|
||||
{"timestamp":"00:47:33.817","source":"Server","level":"LOG","message":"✓ Compiled in 44ms"}
|
||||
{"timestamp":"00:47:33.825","source":"Server","level":"WARN","message":"⚠ Fast Refresh had to perform a full reload due to a runtime error."}
|
||||
{"timestamp":"00:47:34.427","source":"Browser","level":"INFO","message":"%cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold"}
|
||||
{"timestamp":"01:03:11.671","source":"Server","level":"LOG","message":"✓ Compiled in 35ms"}
|
||||
{"timestamp":"01:03:41.505","source":"Server","level":"LOG","message":"✓ Compiled in 50ms"}
|
||||
{"timestamp":"01:03:51.542","source":"Server","level":"LOG","message":"✓ Compiled in 66ms"}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
"dynamicRoutes": {},
|
||||
"notFoundRoutes": [],
|
||||
"preview": {
|
||||
"previewModeId": "2925825860b67ed4c439317f2cac20b3",
|
||||
"previewModeSigningKey": "7937a0b3e643f2be8042280f66ac7478a5b4aef1395c235ab35c1995bd204a8c",
|
||||
"previewModeEncryptionKey": "22c27792d58e7d752d9b9deffc5d8cdab0fcf88e7274087d3703af1d2f4c5eb3"
|
||||
"previewModeId": "1947d233f6bd907465d44e9106e1529f",
|
||||
"previewModeSigningKey": "19d915bf45327c04933031dcd1bf3dbb51437d192bd72e75782313759599b542",
|
||||
"previewModeEncryptionKey": "7b1b69cc42bec06b4a05f44a9eaed9da30d6309359a920dd5e4d809ba31844c1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"/_not-found/page": "app/_not-found/page.js",
|
||||
"/board-cards/page": "app/board-cards/page.js",
|
||||
"/board-cards/register/page": "app/board-cards/register/page.js",
|
||||
"/boards/page": "app/boards/page.js",
|
||||
"/calibration/page": "app/calibration/page.js",
|
||||
"/calibration/register/page": "app/calibration/register/page.js",
|
||||
"/config-files/page": "app/config-files/page.js",
|
||||
"/devices/[sn]/page": "app/devices/[sn]/page.js",
|
||||
"/devices/page": "app/devices/page.js",
|
||||
|
|
@ -11,6 +10,5 @@
|
|||
"/models/page": "app/models/page.js",
|
||||
"/page": "app/page.js",
|
||||
"/registration/page": "app/registration/page.js",
|
||||
"/repair/page": "app/repair/page.js",
|
||||
"/scrap/page": "app/scrap/page.js"
|
||||
"/repair/page": "app/repair/page.js"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,30 +1,6 @@
|
|||
globalThis.__BUILD_MANIFEST = {
|
||||
"pages": {
|
||||
"/_app": [
|
||||
"static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js",
|
||||
"static/chunks/node_modules_next_dist_shared_lib_0~pg0mt._.js",
|
||||
"static/chunks/node_modules_next_dist_client_0pe1dg-._.js",
|
||||
"static/chunks/node_modules_next_dist_0u_w_5s._.js",
|
||||
"static/chunks/node_modules_next_app_0jt-zj..js",
|
||||
"static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js",
|
||||
"static/chunks/node_modules_react-dom_0bruynb._.js",
|
||||
"static/chunks/node_modules_0lx093h._.js",
|
||||
"static/chunks/[root-of-the-server]__0c0okpg._.js",
|
||||
"static/chunks/pages__app_07xvfw~._.js",
|
||||
"static/chunks/turbopack-pages__app_0_wu8vy._.js"
|
||||
],
|
||||
"/_error": [
|
||||
"static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js",
|
||||
"static/chunks/node_modules_next_dist_shared_lib_12bi_n7._.js",
|
||||
"static/chunks/node_modules_next_dist_client_0pe1dg-._.js",
|
||||
"static/chunks/node_modules_next_dist_0rt-2cr._.js",
|
||||
"static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js",
|
||||
"static/chunks/node_modules_react-dom_0bruynb._.js",
|
||||
"static/chunks/node_modules_0lx093h._.js",
|
||||
"static/chunks/[root-of-the-server]__01mw43t._.js",
|
||||
"static/chunks/pages__error_07xvfw~._.js",
|
||||
"static/chunks/turbopack-pages__error_016chbq._.js"
|
||||
]
|
||||
"/_app": []
|
||||
},
|
||||
"devFiles": [],
|
||||
"polyfillFiles": [
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
{
|
||||
"/_app": "pages/_app.js",
|
||||
"/_document": "pages/_document.js",
|
||||
"/_error": "pages/_error.js"
|
||||
}
|
||||
{}
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
self.__BUILD_MANIFEST = {
|
||||
"/_error": [
|
||||
"static/chunks/pages/_error.js"
|
||||
],
|
||||
"__rewrites": {
|
||||
"afterFiles": [],
|
||||
"beforeFiles": [],
|
||||
|
|
|
|||
186
.next/dev/trace
186
.next/dev/trace
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
// This file is generated automatically by Next.js
|
||||
// Do not edit this file manually
|
||||
|
||||
type AppRoutes = "/" | "/boards" | "/calibration" | "/calibration/register" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/models" | "/registration" | "/repair" | "/scrap"
|
||||
type AppRoutes = "/" | "/board-cards" | "/board-cards/register" | "/boards" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/models" | "/registration" | "/repair" | "/scrap"
|
||||
type PageRoutes = never
|
||||
type LayoutRoutes = "/"
|
||||
type RedirectRoutes = never
|
||||
|
|
@ -11,9 +11,9 @@ type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRo
|
|||
|
||||
interface ParamMap {
|
||||
"/": {}
|
||||
"/board-cards": {}
|
||||
"/board-cards/register": {}
|
||||
"/boards": {}
|
||||
"/calibration": {}
|
||||
"/calibration/register": {}
|
||||
"/config-files": {}
|
||||
"/devices": {}
|
||||
"/devices/[sn]": { "sn": string; }
|
||||
|
|
|
|||
|
|
@ -36,6 +36,24 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
|
|||
}
|
||||
|
||||
|
||||
// Validate ../../../src/app/board-cards/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/board-cards">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/board-cards/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/board-cards/register/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/board-cards/register">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/board-cards/register/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/boards/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/boards">> = Specific
|
||||
|
|
@ -45,24 +63,6 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/calibration/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/calibration">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/calibration/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/calibration/register/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/calibration/register">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/calibration/register/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/config-files/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/config-files">> = Specific
|
||||
|
|
|
|||
|
|
@ -1,29 +1,66 @@
|
|||
'use client'
|
||||
import { useState, useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Search, ChevronLeft, ChevronRight, Plus, Download, Info, Eye, X, Tag } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Plus, Download, Eye, X, FileText, Download as DownloadIcon } from 'lucide-react'
|
||||
|
||||
const boardCardsData = [
|
||||
{ id: 1, sn: 'MB25011500', type: '主协板', model: 'MB-V2.1', firmware: 'v2.1', status: '在库', deviceSn: '-', productionDate: '2025-01-15', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 2, sn: 'MCB-3000-20250118002', type: '主协板', model: 'MCB-3000', firmware: 'v2.1.0', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-18', calibStatus: '-', calibDate: '-', },
|
||||
{ id: 3, sn: 'ACB-6000-20250110001', type: '采集板', model: 'ACB-6000', firmware: 'v3.0.2', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-10', calibStatus: '合格', calibDate: '2025-01-12'},
|
||||
{ id: 4, sn: 'ACB-6000-20250110002', type: '采集板', model: 'ACB-6000', firmware: 'v3.0.2', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-10', calibStatus: '合格', calibDate: '2025-01-12'},
|
||||
{ id: 5, sn: 'ACB-6000-20250112003', type: '采集板', model: 'ACB-6000', firmware: 'v3.0.2', status: '在库', deviceSn: '-', productionDate: '2025-01-12', calibStatus: '待校准', calibDate: '-', },
|
||||
{ id: 6, sn: 'ACB-5000-20241205001', type: '采集板', model: 'ACB-5000', firmware: 'v2.5.1', status: '已装配', deviceSn: 'GD20-2024-000045', productionDate: '2024-12-05', calibStatus: '合格', calibDate: '2024-12-08'},
|
||||
{ id: 7, sn: 'TXB-1000-20250120001', type: '发射板', model: 'TXB-1000', firmware: 'v1.2.0', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-20', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 8, sn: 'TXB-1000-20250122002', type: '发射板', model: 'TXB-1000', firmware: 'v1.2.0', status: '在库', deviceSn: '-', productionDate: '2025-01-22', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 9, sn: 'BST-500-20250201001', type: '升压板', model: 'BST-500', firmware: 'v1.1.0', status: '已装配', deviceSn: 'GD30-2025-000002', productionDate: '2025-02-01', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 10, sn: 'BST-500-20250203002', type: '升压板', model: 'BST-500', firmware: 'v1.1.0', status: '在库', deviceSn: '-', productionDate: '2025-02-03', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 11, sn: 'ACB-6000-20241120001', type: '采集板', model: 'ACB-6000', firmware: 'v3.0.2', status: '故障', deviceSn: '-', productionDate: '2024-11-20', calibStatus: '不合格', calibDate: '2025-02-10'},
|
||||
{ id: 12, sn: 'MCB-2000-20240915001', type: '主协板', model: 'MCB-2000', firmware: 'v1.8.5', status: '已装配', deviceSn: 'GD20-2024-000046', productionDate: '2024-09-15', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 13, sn: 'ACB-6000-20250305001', type: '采集板', model: 'ACB-6000', firmware: 'v3.0.2', status: '在库', deviceSn: '-', productionDate: '2025-03-05', calibStatus: '待校准', calibDate: '-'},
|
||||
{ id: 14, sn: 'TXB-800-20240610001', type: '发射板', model: 'TXB-800', firmware: 'v1.0.3', status: '报废', deviceSn: '-', productionDate: '2024-06-10', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 1, sn: 'MB25011500', type: '主协板', version: 'MB-V2.1', firmware: 'v2.1', status: '在库', deviceSn: '-', productionDate: '2025-01-15', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 2, sn: 'MCB-3000-20250118002', type: '主协板', version: 'MCB-3000', firmware: 'v2.1.0', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-18', calibStatus: '-', calibDate: '-', },
|
||||
{ id: 3, sn: 'ACB-6000-20250110001', type: '采集板', version: 'ACB-6000', firmware: 'v3.0.2', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-10', calibStatus: '合格', calibDate: '2025-01-12'},
|
||||
{ id: 4, sn: 'ACB-6000-20250110002', type: '采集板', version: 'ACB-6000', firmware: 'v3.0.2', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-10', calibStatus: '合格', calibDate: '2025-01-12'},
|
||||
{ id: 5, sn: 'ACB-6000-20250112003', type: '采集板', version: 'ACB-6000', firmware: 'v3.0.2', status: '在库', deviceSn: '-', productionDate: '2025-01-12', calibStatus: '待校准', calibDate: '-', },
|
||||
{ id: 6, sn: 'ACB-5000-20241205001', type: '采集板', version: 'ACB-5000', firmware: 'v2.5.1', status: '已装配', deviceSn: 'GD20-2024-000045', productionDate: '2024-12-05', calibStatus: '合格', calibDate: '2024-12-08'},
|
||||
{ id: 7, sn: 'TXB-1000-20250120001', type: '发射板', version: 'TXB-1000', firmware: 'v1.2.0', status: '已装配', deviceSn: 'GD30-2025-000001', productionDate: '2025-01-20', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 8, sn: 'TXB-1000-20250122002', type: '发射板', version: 'TXB-1000', firmware: 'v1.2.0', status: '在库', deviceSn: '-', productionDate: '2025-01-22', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 9, sn: 'BST-500-20250201001', type: '升压板', version: 'BST-500', firmware: 'v1.1.0', status: '已装配', deviceSn: 'GD30-2025-000002', productionDate: '2025-02-01', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 10, sn: 'BST-500-20250203002', type: '升压板', version: 'BST-500', firmware: 'v1.1.0', status: '在库', deviceSn: '-', productionDate: '2025-02-03', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 11, sn: 'ACB-6000-20241120001', type: '采集板', version: 'ACB-6000', firmware: 'v3.0.2', status: '故障', deviceSn: '-', productionDate: '2024-11-20', calibStatus: '不合格', calibDate: '2025-02-10'},
|
||||
{ id: 12, sn: 'MCB-2000-20240915001', type: '主协板', version: 'MCB-2000', firmware: 'v1.8.5', status: '已装配', deviceSn: 'GD20-2024-000046', productionDate: '2024-09-15', calibStatus: '-', calibDate: '-'},
|
||||
{ id: 13, sn: 'ACB-6000-20250305001', type: '采集板', version: 'ACB-6000', firmware: 'v3.0.2', status: '在库', deviceSn: '-', productionDate: '2025-03-05', calibStatus: '待校准', calibDate: '-'},
|
||||
{ id: 14, sn: 'TXB-800-20240610001', type: '发射板', version: 'TXB-800', firmware: 'v1.0.3', status: '报废', deviceSn: '-', productionDate: '2024-06-10', calibStatus: '-', calibDate: '-'},
|
||||
]
|
||||
|
||||
const typeOptions = ['全部', '主协板', '采集板', '发射板', '升压板']
|
||||
const statusOptions = ['全部', '在库', '已装配', '故障', '报废']
|
||||
const calibStatusOptions = ['全部', '合格', '不合格', '待校准']
|
||||
|
||||
/** 模拟校准文件数据:key 为板卡 SN */
|
||||
const calibrationFilesMap: Record<string, { id: string; fileName: string; fileSize: number; md5: string; uploadTime: string; channels: { channel: string; refValue: number; measuredValue: number; deviation: number; result: string }[] }[]> = {
|
||||
'ACB-6000-20250110001': [
|
||||
{ id: 'cf-1', fileName: 'ACB-6000-20250110001_calib_20250112.csv', fileSize: 24576, md5: 'a1b2c3d4e5f6', uploadTime: '2025-01-12 14:30:00', channels: [
|
||||
{ channel: 'CH1', refValue: 100.0, measuredValue: 99.8, deviation: -0.2, result: '合格' },
|
||||
{ channel: 'CH2', refValue: 200.0, measuredValue: 200.3, deviation: 0.15, result: '合格' },
|
||||
{ channel: 'CH3', refValue: 500.0, measuredValue: 499.5, deviation: -0.1, result: '合格' },
|
||||
{ channel: 'CH4', refValue: 1000.0, measuredValue: 999.2, deviation: -0.08, result: '合格' },
|
||||
]},
|
||||
],
|
||||
'ACB-6000-20250110002': [
|
||||
{ id: 'cf-2', fileName: 'ACB-6000-20250110002_calib_20250112.csv', fileSize: 23040, md5: 'b2c3d4e5f6a1', uploadTime: '2025-01-12 15:10:00', channels: [
|
||||
{ channel: 'CH1', refValue: 100.0, measuredValue: 100.1, deviation: 0.1, result: '合格' },
|
||||
{ channel: 'CH2', refValue: 200.0, measuredValue: 199.7, deviation: -0.15, result: '合格' },
|
||||
{ channel: 'CH3', refValue: 500.0, measuredValue: 500.8, deviation: 0.16, result: '合格' },
|
||||
]},
|
||||
],
|
||||
'ACB-5000-20241205001': [
|
||||
{ id: 'cf-3', fileName: 'ACB-5000-20241205001_calib_20241208.csv', fileSize: 18432, md5: 'c3d4e5f6a1b2', uploadTime: '2024-12-08 09:45:00', channels: [
|
||||
{ channel: 'CH1', refValue: 100.0, measuredValue: 99.9, deviation: -0.1, result: '合格' },
|
||||
{ channel: 'CH2', refValue: 200.0, measuredValue: 200.5, deviation: 0.25, result: '合格' },
|
||||
]},
|
||||
],
|
||||
'ACB-6000-20241120001': [
|
||||
{ id: 'cf-4', fileName: 'ACB-6000-20241120001_calib_20250210.csv', fileSize: 25600, md5: 'd4e5f6a1b2c3', uploadTime: '2025-02-10 11:20:00', channels: [
|
||||
{ channel: 'CH1', refValue: 100.0, measuredValue: 98.2, deviation: -1.8, result: '不合格' },
|
||||
{ channel: 'CH2', refValue: 200.0, measuredValue: 203.6, deviation: 1.8, result: '不合格' },
|
||||
{ channel: 'CH3', refValue: 500.0, measuredValue: 500.1, deviation: 0.02, result: '合格' },
|
||||
]},
|
||||
],
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
return (bytes / 1024).toFixed(1) + ' KB'
|
||||
}
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
case '在库': return { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }
|
||||
|
|
@ -50,6 +87,7 @@ export default function BoardCardsPage() {
|
|||
const [searchText, setSearchText] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [detailDrawer, setDetailDrawer] = useState<typeof boardCardsData[0] | null>(null)
|
||||
const [calibFileDrawer, setCalibFileDrawer] = useState<typeof boardCardsData[0] | null>(null)
|
||||
const pageSize = 8
|
||||
|
||||
const filtered = useMemo(() => boardCardsData.filter(b => {
|
||||
|
|
@ -84,7 +122,7 @@ export default function BoardCardsPage() {
|
|||
<button style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
<Download size={16} />导出
|
||||
</button>
|
||||
<Link href="/calibration/register" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14, textDecoration: 'none' }}>
|
||||
<Link href="/board-cards/register" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14, textDecoration: 'none' }}>
|
||||
<Plus size={16} />登记板卡
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -139,7 +177,7 @@ export default function BoardCardsPage() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['板卡SN', '类型', '型号', '固件', '状态', '所属设备', '校准状态', '操作'].map(h => (
|
||||
{['板卡SN', '类型', '版本', '固件', '状态', '所属设备', '校准状态', '操作'].map(h => (
|
||||
<th key={h} style={{ padding: '12px 14px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', whiteSpace: 'nowrap' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -149,7 +187,7 @@ export default function BoardCardsPage() {
|
|||
<tr key={row.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, fontWeight: 500 }}>{row.sn}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.type}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.model}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.version}</td>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.firmware}</td>
|
||||
<td style={{ padding: '12px 14px' }}>
|
||||
<span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span>
|
||||
|
|
@ -163,9 +201,16 @@ export default function BoardCardsPage() {
|
|||
)}
|
||||
</td>
|
||||
<td style={{ padding: '12px 14px' }}>
|
||||
<button onClick={() => setDetailDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Eye size={14} />详情
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => setDetailDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Eye size={14} />详情
|
||||
</button>
|
||||
{row.type === '采集板' && (
|
||||
<button onClick={() => setCalibFileDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<FileText size={14} />校准文件
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -203,7 +248,7 @@ export default function BoardCardsPage() {
|
|||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>板卡SN:</span>{detailDrawer.sn}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>板卡类型:</span>{detailDrawer.type}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>型号:</span>{detailDrawer.model}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>版本:</span>{detailDrawer.version}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>固件版本:</span>{detailDrawer.firmware}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>生产日期:</span>{detailDrawer.productionDate}</div>
|
||||
<div>
|
||||
|
|
@ -250,6 +295,82 @@ export default function BoardCardsPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Calibration File Drawer */}
|
||||
{calibFileDrawer && (() => {
|
||||
const files = calibrationFilesMap[calibFileDrawer.sn] || []
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => setCalibFileDrawer(null)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 640, 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 }}>校准文件 - {calibFileDrawer.sn}</h3>
|
||||
<button onClick={() => setCalibFileDrawer(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
{files.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '48px 0', color: 'rgba(0,0,0,0.25)' }}>
|
||||
<FileText size={40} style={{ marginBottom: 12, opacity: 0.3 }} />
|
||||
<div style={{ fontSize: 14 }}>暂无校准文件</div>
|
||||
</div>
|
||||
) : (
|
||||
files.map(file => (
|
||||
<div key={file.id} style={{ marginBottom: 20 }}>
|
||||
{/* 文件信息卡片 */}
|
||||
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, border: '1px solid #F0F0F0', marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FileText size={16} style={{ color: '#4a7c59' }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500 }}>{file.fileName}</span>
|
||||
</div>
|
||||
<button style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', fontSize: 12, color: '#4a7c59' }}>
|
||||
<Download size={12} />下载
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>文件大小:</span>{formatFileSize(file.fileSize)}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>MD5:</span><span style={{ fontFamily: 'monospace' }}>{file.md5}</span></div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>上传时间:</span>{file.uploadTime}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 校准数据内容 */}
|
||||
<div style={{ borderRadius: 8, border: '1px solid #F0F0F0', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '10px 16px', backgroundColor: '#eef5f0', fontSize: 13, fontWeight: 600, color: '#4a7c59' }}>校准数据</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['通道', '参考值', '测量值', '偏差(%)', '结果'].map(h => (
|
||||
<th key={h} style={{ padding: '8px 12px', textAlign: 'left', fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{file.channels.map(ch => (
|
||||
<tr key={ch.channel} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '8px 12px', fontSize: 12, fontWeight: 500 }}>{ch.channel}</td>
|
||||
<td style={{ padding: '8px 12px', fontSize: 12, fontFamily: 'monospace' }}>{ch.refValue.toFixed(1)}</td>
|
||||
<td style={{ padding: '8px 12px', fontSize: 12, fontFamily: 'monospace' }}>{ch.measuredValue.toFixed(1)}</td>
|
||||
<td style={{ padding: '8px 12px', fontSize: 12, fontFamily: 'monospace', color: Math.abs(ch.deviation) > 1 ? '#FF4D4F' : 'rgba(0,0,0,0.65)' }}>{ch.deviation > 0 ? '+' : ''}{ch.deviation.toFixed(2)}</td>
|
||||
<td style={{ padding: '8px 12px' }}>
|
||||
<span style={{ padding: '1px 6px', borderRadius: 4, fontSize: 11, ...(ch.result === '合格' ? { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' } : { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }) }}>{ch.result}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button onClick={() => setCalibFileDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,38 +1,40 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Plus, Trash2, Upload, Info, CheckCircle } from 'lucide-react'
|
||||
import { useRef } from 'react'
|
||||
import { ArrowLeft, Plus, Trash2, Upload, Info, CheckCircle, FileText, X } from 'lucide-react'
|
||||
|
||||
/** 板卡类型 -> 可选型号 */
|
||||
const modelsByType: Record<string, { model: string; firmware: string }[]> = {
|
||||
/** 板卡类型 -> 可选版本 */
|
||||
const versionsByType: Record<string, { version: string; firmware: string }[]> = {
|
||||
'主协板': [
|
||||
{ model: 'MB-V1.2', firmware: 'v2.1' },
|
||||
{ model: 'MB-V2.1', firmware: 'v1.8' },
|
||||
{ version: 'MB-V1.2', firmware: 'v2.1' },
|
||||
{ version: 'MB-V2.1', firmware: 'v1.8' },
|
||||
],
|
||||
'采集板': [
|
||||
{ model: 'RX-V1.3', firmware: 'v3.0' },
|
||||
{ model: 'RX-V2.1', firmware: 'v2.5' },
|
||||
{ version: 'RX-V1.3', firmware: 'v3.0' },
|
||||
{ version: 'RX-V2.1', firmware: 'v2.5' },
|
||||
],
|
||||
'发射板': [
|
||||
{ model: 'TX-V1.5', firmware: 'v1.2' },
|
||||
{ model: 'TX-V2.1', firmware: 'v1.0' },
|
||||
{ version: 'TX-V1.5', firmware: 'v1.2' },
|
||||
{ version: 'TX-V2.1', firmware: 'v1.0' },
|
||||
],
|
||||
'升压板': [
|
||||
{ model: 'BO-V2.1', firmware: 'v1.1' },
|
||||
{ model: 'BO-V2.2', firmware: 'v0.9' },
|
||||
{ version: 'BO-V2.1', firmware: 'v1.1' },
|
||||
{ version: 'BO-V2.2', firmware: 'v0.9' },
|
||||
],
|
||||
}
|
||||
|
||||
const typeOptions = Object.keys(modelsByType)
|
||||
const typeOptions = Object.keys(versionsByType)
|
||||
|
||||
interface BoardEntry {
|
||||
id: number
|
||||
type: string
|
||||
model: string
|
||||
version: string
|
||||
firmware: string
|
||||
sn: string
|
||||
productionDate: string
|
||||
remark: string
|
||||
calibFile: File | null
|
||||
}
|
||||
|
||||
let nextId = 1
|
||||
|
|
@ -43,7 +45,7 @@ export default function BoardRegisterPage() {
|
|||
const [batchMode, setBatchMode] = useState(false)
|
||||
|
||||
function createEntry(): BoardEntry {
|
||||
return { id: nextId++, type: '采集板', model: 'RX-V2.1', firmware: 'v2.1', sn: '', productionDate: '', remark: '' }
|
||||
return { id: nextId++, type: '采集板', version: 'RX-V2.1', firmware: 'v2.1', sn: '', productionDate: '', remark: '', calibFile: null }
|
||||
}
|
||||
|
||||
const addEntry = () => {
|
||||
|
|
@ -59,24 +61,38 @@ export default function BoardRegisterPage() {
|
|||
setEntries(prev => prev.map(e => {
|
||||
if (e.id !== id) return e
|
||||
const updated = { ...e, [field]: value }
|
||||
// 切换类型时自动选第一个型号和固件
|
||||
// 切换类型时自动选第一个版本和固件
|
||||
if (field === 'type') {
|
||||
const models = modelsByType[value]
|
||||
if (models && models.length > 0) {
|
||||
updated.model = models[0].model
|
||||
updated.firmware = models[0].firmware
|
||||
const versions = versionsByType[value]
|
||||
if (versions && versions.length > 0) {
|
||||
updated.version = versions[0].version
|
||||
updated.firmware = versions[0].firmware
|
||||
}
|
||||
// 切换到非采集板时清除校准文件
|
||||
if (value !== '采集板') updated.calibFile = null
|
||||
}
|
||||
// 切换型号时自动填充固件
|
||||
if (field === 'model') {
|
||||
const models = modelsByType[updated.type]
|
||||
const match = models?.find(m => m.model === value)
|
||||
// 切换版本时自动填充固件
|
||||
if (field === 'version') {
|
||||
const versions = versionsByType[updated.type]
|
||||
const match = versions?.find(m => m.version === value)
|
||||
if (match) updated.firmware = match.firmware
|
||||
}
|
||||
return updated
|
||||
}))
|
||||
}
|
||||
|
||||
const fileInputRefs = useRef<Record<number, HTMLInputElement | null>>({})
|
||||
|
||||
const handleCalibFileChange = (entryId: number, file: File | null) => {
|
||||
setEntries(prev => prev.map(e => e.id === entryId ? { ...e, calibFile: file } : e))
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
const isValid = entries.every(e => e.sn.trim() && e.productionDate)
|
||||
|
||||
return (
|
||||
|
|
@ -84,7 +100,7 @@ export default function BoardRegisterPage() {
|
|||
<div style={{ flex: 1, padding: 24, paddingBottom: 80 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||||
<button onClick={() => router.push('/calibration')} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||||
<button onClick={() => router.push('/board-cards')} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<div>
|
||||
|
|
@ -97,7 +113,7 @@ export default function BoardRegisterPage() {
|
|||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, backgroundColor: '#eef5f0', borderRadius: 8, marginBottom: 24, border: '1px solid #a3c4ad' }}>
|
||||
<Info size={18} style={{ color: '#4a7c59', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ fontSize: 14, color: '#4a7c59', lineHeight: 1.6 }}>
|
||||
板卡SN号为唯一标识,请确保录入正确。采集板登记后需要进行校准才能用于装配。选择型号后固件版本会自动填充。
|
||||
板卡SN号为唯一标识,请确保录入正确。采集板登记后需要进行校准才能用于装配。选择版本后固件版本会自动填充。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -136,11 +152,11 @@ export default function BoardRegisterPage() {
|
|||
</select>
|
||||
</div>
|
||||
|
||||
{/* 板卡型号 */}
|
||||
{/* 板卡版本 */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 板卡型号</label>
|
||||
<select value={entry.model} onChange={e => updateEntry(entry.id, 'model', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{(modelsByType[entry.type] || []).map(m => <option key={m.model} value={m.model}>{m.model}</option>)}
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 板卡版本</label>
|
||||
<select value={entry.version} onChange={e => updateEntry(entry.id, 'version', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{(versionsByType[entry.type] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -153,7 +169,7 @@ export default function BoardRegisterPage() {
|
|||
{/* 板卡SN */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 板卡SN号</label>
|
||||
<input value={entry.sn} onChange={e => updateEntry(entry.id, 'sn', e.target.value)} placeholder={`如 ${entry.model}-20250401001`} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<input value={entry.sn} onChange={e => updateEntry(entry.id, 'sn', e.target.value)} placeholder={`如 ${entry.version}-20250401001`} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
{/* 生产日期 */}
|
||||
|
|
@ -176,6 +192,38 @@ export default function BoardRegisterPage() {
|
|||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>采集板登记后状态为"待校准",需完成校准后才能用于设备装配。</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 采集板校准文件导入 */}
|
||||
{entry.type === '采集板' && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 8 }}>校准文件(可选)</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls,.dat"
|
||||
ref={el => { fileInputRefs.current[entry.id] = el }}
|
||||
onChange={e => handleCalibFileChange(entry.id, e.target.files?.[0] || null)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{entry.calibFile ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', backgroundColor: '#eef5f0', borderRadius: 6, border: '1px solid #a3c4ad' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FileText size={16} style={{ color: '#4a7c59', flexShrink: 0 }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: 'rgba(0,0,0,0.85)' }}>{entry.calibFile.name}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>{formatFileSize(entry.calibFile.size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => { handleCalibFileChange(entry.id, null); if (fileInputRefs.current[entry.id]) fileInputRefs.current[entry.id]!.value = '' }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24, border: 'none', background: 'none', cursor: 'pointer', color: 'rgba(0,0,0,0.45)', borderRadius: 4 }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => fileInputRefs.current[entry.id]?.click()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: '100%', padding: '14px 0', border: '2px dashed #D9D9D9', borderRadius: 6, backgroundColor: '#FAFAFA', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
<Upload size={16} />导入校准文件(支持 .csv / .xlsx / .dat)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
|
@ -193,7 +241,7 @@ export default function BoardRegisterPage() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['序号', '类型', '型号', '固件', 'SN号', '生产日期', '状态'].map(h => (
|
||||
{['序号', '类型', '版本', '固件', 'SN号', '生产日期', '校准文件', '状态'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 14px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -203,10 +251,23 @@ export default function BoardRegisterPage() {
|
|||
<tr key={entry.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{i + 1}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.type}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.model}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.version}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{entry.firmware}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, fontWeight: 500, color: entry.sn ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.25)' }}>{entry.sn || '未填写'}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13, color: entry.productionDate ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0.25)' }}>{entry.productionDate || '未填写'}</td>
|
||||
<td style={{ padding: '10px 14px', fontSize: 13 }}>
|
||||
{entry.type === '采集板' ? (
|
||||
entry.calibFile ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: '#4a7c59', fontSize: 12 }}>
|
||||
<FileText size={12} />{entry.calibFile.name}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>未导入</span>
|
||||
)
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px' }}>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, ...(entry.type === '采集板' ? { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' } : { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }) }}>
|
||||
{entry.type === '采集板' ? '待校准' : '在库'}
|
||||
|
|
@ -225,11 +286,12 @@ export default function BoardRegisterPage() {
|
|||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
共 {entries.length} 条板卡待登记
|
||||
{entries.some(e => e.type === '采集板') && <span style={{ color: '#FAAD14' }}> · 含采集板需后续校准</span>}
|
||||
{entries.some(e => e.type === '采集板' && e.calibFile) && <span style={{ color: '#4a7c59' }}> · {entries.filter(e => e.calibFile).length} 个已附校准文件</span>}
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => router.push('/calibration')} style={{ padding: '8px 24px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={() => router.push('/board-cards')} style={{ padding: '8px 24px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button
|
||||
onClick={() => router.push('/calibration')}
|
||||
onClick={() => router.push('/board-cards')}
|
||||
disabled={!isValid}
|
||||
style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: isValid ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: isValid ? 'pointer' : 'not-allowed', fontSize: 14 }}
|
||||
>
|
||||
|
|
@ -1,41 +1,17 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Download, Plus, X, Eye, Upload, Clock, Wrench, CheckCircle, ChevronRight, GripVertical } from 'lucide-react'
|
||||
import { Download, Plus, X, Eye, Upload, Wrench, CheckCircle, RefreshCw } from 'lucide-react'
|
||||
|
||||
const tabs = ['全部', '主协板', '采集板', '发射板', '升压板']
|
||||
|
||||
const boardsData = [
|
||||
{ id: 1, type: '主协板', model: 'MB25130025', firmwareVersion: 'v2.1.0', productionDate: '2024-03-15', status: '在产' },
|
||||
{ id: 2, type: '主协板', model: 'MB25130024', firmwareVersion: 'v1.8.5', productionDate: '2023-11-20', status: '在产' },
|
||||
{ id: 3, type: '采集板', model: 'RX25130024', firmwareVersion: 'v3.0.2', productionDate: '2024-01-10', status: '在产' },
|
||||
{ id: 4, type: '采集板', model: 'RX25130012', firmwareVersion: 'v2.5.1', productionDate: '2023-09-05', status: '停产' },
|
||||
{ id: 5, type: '发射板', model: 'TXB-1000', firmwareVersion: 'v1.2.0', productionDate: '2024-02-28', status: '在产' },
|
||||
{ id: 6, type: '发射板', model: 'TXB-800', firmwareVersion: 'v1.0.3', productionDate: '2023-06-15', status: '停产' },
|
||||
{ id: 7, type: '升压板', model: 'BST-500', firmwareVersion: 'v1.1.0', productionDate: '2024-04-01', status: '在产' },
|
||||
{ id: 8, type: '升压板', model: 'BST-300', firmwareVersion: 'v0.9.8', productionDate: '2023-08-22', status: '停产' },
|
||||
const initialBoardsData = [
|
||||
{ id: 1, type: '主协板', version: 'MB-V1.8', status: '在产' },
|
||||
{ id: 3, type: '采集板', version: 'RX-V2.3', status: '在产' },
|
||||
{ id: 6, type: '发射板', version: 'TX-V2.1', status: '停产' },
|
||||
{ id: 8, type: '升压板', version: 'BP600-V1.2', status: '停产' },
|
||||
]
|
||||
|
||||
const upgradeHistory = [
|
||||
{ version: 'v2.1.0', date: '2024-03-10', operator: '张工', note: '修复通信协议兼容性问题' },
|
||||
{ version: 'v2.0.0', date: '2024-01-15', operator: '李工', note: '新增多通道采集支持' },
|
||||
{ version: 'v1.8.5', date: '2023-11-01', operator: '张工', note: '优化功耗管理' },
|
||||
]
|
||||
|
||||
const calibrationHistory = [
|
||||
{ date: '2024-03-01', result: '合格', operator: '王工', nextDate: '2025-03-01' },
|
||||
{ date: '2023-03-05', result: '合格', operator: '王工', nextDate: '2024-03-05' },
|
||||
]
|
||||
|
||||
const serviceHistory = [
|
||||
{ date: '2024-02-15', type: '例行保养', operator: '赵工', description: '清洁接口,检查焊点' },
|
||||
{ date: '2023-08-20', type: '例行保养', operator: '赵工', description: '更换散热硅脂' },
|
||||
]
|
||||
|
||||
const repairHistory = [
|
||||
{ date: '2024-01-20', issue: '通信异常', operator: '李工', result: '更换通信芯片,已修复' },
|
||||
]
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
|
|
@ -48,20 +24,27 @@ function getStatusStyle(status: string) {
|
|||
export default function BoardsPage() {
|
||||
const router = useRouter()
|
||||
const [activeTab, setActiveTab] = useState('全部')
|
||||
const [detailDrawer, setDetailDrawer] = useState<typeof boardsData[0] | null>(null)
|
||||
const [boardsData, setBoardsData] = useState(initialBoardsData)
|
||||
const [detailDrawer, setDetailDrawer] = useState<typeof initialBoardsData[0] | null>(null)
|
||||
const [addDrawer, setAddDrawer] = useState(false)
|
||||
const [detailTab, setDetailTab] = useState('basic')
|
||||
const [formData, setFormData] = useState({ type: '主协板', model: '', firmwareVersion: '', productionDate: '' })
|
||||
const [formData, setFormData] = useState({ type: '主协板', version: '', firmwareVersion: '', productionDate: '', status: '在产' })
|
||||
|
||||
const filteredBoards = activeTab === '全部' ? boardsData : boardsData.filter(b => b.type === activeTab)
|
||||
|
||||
const handleToggleStatus = (id: number) => {
|
||||
setBoardsData(prev => prev.map(b =>
|
||||
b.id === id ? { ...b, status: b.status === '在产' ? '停产' : '在产' } : b
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>板卡型号管理</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理所有板卡型号及固件版本</p>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>板卡版本管理</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理所有板卡版本及固件版本</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => {}} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
|
|
@ -90,7 +73,7 @@ export default function BoardsPage() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['板卡类型', '型号', '固件版本', '生产日期', '状态', '操作'].map(h => (
|
||||
{['板卡类型', '版本', '状态', '操作'].map(h => (
|
||||
<th key={h} style={{ padding: '12px 16px', textAlign: 'left', fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -99,9 +82,7 @@ export default function BoardsPage() {
|
|||
{filteredBoards.map(board => (
|
||||
<tr key={board.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14 }}>{board.type}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{board.model}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14 }}>{board.firmwareVersion}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{board.productionDate}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{board.version}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<span style={{ ...getStatusStyle(board.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{board.status}</span>
|
||||
</td>
|
||||
|
|
@ -110,9 +91,12 @@ export default function BoardsPage() {
|
|||
<button onClick={() => { setDetailDrawer(board); setDetailTab('basic') }} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Eye size={14} />详情
|
||||
</button>
|
||||
<button onClick={() => router.push(`/firmware?board=${board.model}`)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<button onClick={() => router.push(`/firmware?board=${board.version}`)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Upload size={14} />固件
|
||||
</button>
|
||||
<button onClick={() => handleToggleStatus(board.id)} style={{ color: board.status === '在产' ? '#FAAD14' : '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<RefreshCw size={14} />{board.status === '在产' ? '停产' : '恢复在产'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -127,17 +111,13 @@ export default function BoardsPage() {
|
|||
<div onClick={() => setDetailDrawer(null)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 560, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>板卡详情 - {detailDrawer.model}</h3>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>板卡详情 - {detailDrawer.version}</h3>
|
||||
<button onClick={() => setDetailDrawer(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
{/* Detail Tabs */}
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #F0F0F0' }}>
|
||||
{[
|
||||
{ key: 'basic', label: '基本信息' },
|
||||
{ key: 'upgrade', label: '升级历史' },
|
||||
{ key: 'calibration', label: '校准历史' },
|
||||
{ key: 'service', label: '保养历史' },
|
||||
{ key: 'repair', label: '维修历史' },
|
||||
].map(t => (
|
||||
<button key={t.key} onClick={() => setDetailTab(t.key)} style={{
|
||||
padding: '10px 16px', fontSize: 13, cursor: 'pointer', border: 'none', backgroundColor: 'transparent',
|
||||
|
|
@ -151,9 +131,7 @@ export default function BoardsPage() {
|
|||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
||||
{[
|
||||
{ label: '板卡类型', value: detailDrawer.type },
|
||||
{ label: '型号', value: detailDrawer.model },
|
||||
{ label: '固件版本', value: detailDrawer.firmwareVersion },
|
||||
{ label: '生产日期', value: detailDrawer.productionDate },
|
||||
{ label: '版本', value: detailDrawer.version },
|
||||
{ label: '状态', value: detailDrawer.status },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
|
|
@ -167,70 +145,6 @@ export default function BoardsPage() {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{detailTab === 'upgrade' && (
|
||||
<div>
|
||||
{upgradeHistory.map((h, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 12, padding: '12px 0', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', backgroundColor: '#eef5f0', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Upload size={14} style={{ color: '#4a7c59' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4 }}>{h.version}</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{h.note}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', marginTop: 4 }}>{h.date} · {h.operator}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{detailTab === 'calibration' && (
|
||||
<div>
|
||||
{calibrationHistory.map((h, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 12, padding: '12px 0', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', backgroundColor: '#F6FFED', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<CheckCircle size={14} style={{ color: '#52C41A' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4 }}>校准结果: <span style={{ color: '#52C41A' }}>{h.result}</span></div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>下次校准: {h.nextDate}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', marginTop: 4 }}>{h.date} · {h.operator}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{detailTab === 'service' && (
|
||||
<div>
|
||||
{serviceHistory.map((h, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 12, padding: '12px 0', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', backgroundColor: '#eef5f0', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Wrench size={14} style={{ color: '#4a7c59' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4 }}>{h.type}</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{h.description}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', marginTop: 4 }}>{h.date} · {h.operator}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{detailTab === 'repair' && (
|
||||
<div>
|
||||
{repairHistory.map((h, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 12, padding: '12px 0', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', backgroundColor: '#FFF2F0', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Wrench size={14} style={{ color: '#FF4D4F' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4 }}>故障: {h.issue}</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{h.result}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', marginTop: 4 }}>{h.date} · {h.operator}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -253,16 +167,14 @@ export default function BoardsPage() {
|
|||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>型号</label>
|
||||
<input value={formData.model} onChange={e => setFormData({ ...formData, model: e.target.value })} placeholder="请输入型号" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, outline: 'none', boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>版本</label>
|
||||
<input value={formData.version} onChange={e => setFormData({ ...formData, version: e.target.value })} placeholder="请输入版本" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, outline: 'none', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>固件版本</label>
|
||||
<input value={formData.firmwareVersion} onChange={e => setFormData({ ...formData, firmwareVersion: e.target.value })} placeholder="请输入固件版本" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, outline: 'none', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>生产日期</label>
|
||||
<input type="date" value={formData.productionDate} onChange={e => setFormData({ ...formData, productionDate: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, outline: 'none', boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>状态</label>
|
||||
<select value={formData.status} onChange={e => setFormData({ ...formData, status: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, outline: 'none' }}>
|
||||
{['在产', '停产'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ const menuGroups = [
|
|||
{ path: '/devices', label: '设备列表', icon: Monitor },
|
||||
{ path: '/models', label: '设备型号管理', icon: Settings2 },
|
||||
]},
|
||||
{ title: '板卡', items: [{ path: '/calibration', label: '板卡列表', icon: Gauge },
|
||||
{ path: '/boards', label: '板卡型号管理', icon: Cpu },] },
|
||||
{ title: '板卡', items: [{ path: '/board-cards', label: '板卡列表', icon: Gauge },
|
||||
{ path: '/boards', label: '板卡版本管理', icon: Cpu },] },
|
||||
{ title: '维修', items: [
|
||||
{ path: '/repair', label: '维修工单', icon: Wrench },
|
||||
{ path: '/scrap', label: '报废回收', icon: Recycle },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Plus, Search, Info, ChevronLeft, ChevronRight, X, Download, Trash2, Eye, Edit } from 'lucide-react'
|
||||
import { useState, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Plus, Search, Info, ChevronLeft, ChevronRight, X, Download, Trash2, Eye, Edit, ArrowLeft } from 'lucide-react'
|
||||
|
||||
const configData = [
|
||||
{ id: 1, name: 'CFG-GD30-v1.2.0', model: 'GD-30 Supreme', version: 'v1.2.0', createTime: '2025-01-15 10:30', status: '生效', voltage: '1500V', current: '10A', dutyCycle: '50%、100%', pulseWidth: '0.25s~64s', iterations: '1~256', channels: 12, sampleRate: '50Hz/60Hz/100Hz/1000Hz', voltageRange: '±2.5V/±80V', waveform: '支持', ssid: 'GD30' },
|
||||
|
|
@ -23,7 +24,28 @@ function getStatusStyle(status: string) {
|
|||
}
|
||||
|
||||
export default function ConfigFilesPage() {
|
||||
const [filterModel, setFilterModel] = useState('全部')
|
||||
return (
|
||||
<Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}>
|
||||
<ConfigFilesContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function ConfigFilesContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const modelParam = searchParams.get('model') || ''
|
||||
|
||||
// Map model name to filter value
|
||||
const modelNameMap: Record<string, string> = {
|
||||
'GD-30 Supreme': 'GD-30 Supreme',
|
||||
'GD-20 Supreme': 'GD-20 Supreme',
|
||||
'GD-10 Supreme': 'GD-10 Supreme',
|
||||
}
|
||||
const initialModelFilter = modelParam ? (modelNameMap[modelParam] || modelParam) : '全部'
|
||||
const isFromModels = !!modelParam
|
||||
|
||||
const [filterModel, setFilterModel] = useState(initialModelFilter)
|
||||
const [filterVersion, setFilterVersion] = useState('全部')
|
||||
const [filterKeyword, setFilterKeyword] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
|
@ -50,9 +72,16 @@ export default function ConfigFilesPage() {
|
|||
<div style={{ padding: 24 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>配置文件管理</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理设备型号配置文件</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{isFromModels && (
|
||||
<button onClick={() => router.back()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
)}
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>{isFromModels ? `配置文件管理 — ${modelParam}` : '配置文件管理'}</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理设备型号配置文件</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setDrawerOpen(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} />新建配置
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@
|
|||
import { use } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Cpu, Wifi, Monitor, Key, FileCode, Upload, Clock, Wrench, CheckCircle, AlertTriangle, Package, FileDown } from 'lucide-react'
|
||||
import { ArrowLeft, Cpu, Wifi, Monitor, Key, FileCode, Camera, Clock, X, CheckCircle, AlertTriangle, Package, ChevronLeft, ChevronRight, ZoomIn, Download } from 'lucide-react'
|
||||
|
||||
/** Mock: 所有设备数据 */
|
||||
const allDevices = [
|
||||
{ id: 1, sn: 'GD30-2025-000001', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-15 14:30', customer: '北京地质研究院', operator: '张工程师' },
|
||||
{ id: 2, sn: 'GD30-2025-000002', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-18 09:15', customer: '中国地质大学', operator: '张工程师' },
|
||||
{ id: 3, sn: 'GD30-2024-000056', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-12-20 16:00', customer: '成都理工大学', operator: '李工程师' },
|
||||
{ id: 4, sn: 'GT20-2025-000045', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-02-10 11:20', customer: '武汉地质调查中心', operator: '王工程师' },
|
||||
{ id: 5, sn: 'GT20-2025-000046', model: 'GD-20', type: '二维电法仪', status: '装配中', firmware: 'v1.8.5', productionDate: '2025-03-01 08:45', customer: '-', operator: '王工程师' },
|
||||
{ id: 6, sn: 'GD30-2024-000078', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-11-05 13:30', customer: '长安大学', operator: '张工程师' },
|
||||
{ id: 7, sn: 'GD10-2024-000033', model: 'GD-10 Supreme', type: '入门级电法仪', status: '已激活', firmware: 'v1.5.2', productionDate: '2024-09-12 10:00', customer: '河海大学', operator: '李工程师' },
|
||||
{ id: 8, sn: 'GD30-2024-000089', model: 'GD-30 Supreme', type: '高密度电法仪', status: '装配中', firmware: 'v2.3.5', productionDate: '2025-03-05 15:10', customer: '-', operator: '张工程师' },
|
||||
{ id: 9, sn: 'GT20-2025-000012', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-01-22 09:30', customer: '中南大学', operator: '王工程师' },
|
||||
{ id: 10, sn: 'GD30-2024-000102', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-10-18 14:00', customer: '吉林大学', operator: '李工程师' },
|
||||
{ id: 11, sn: 'GD10-2024-000034', model: 'GD-10 Supreme', type: '入门级电法仪', status: '装配中', firmware: 'v1.5.2', productionDate: '2025-03-08 11:45', customer: '-', operator: '王工程师' },
|
||||
{ id: 12, sn: 'GD30-2024-000145', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2024-08-25 16:20', customer: '同济大学', operator: '张工程师' },
|
||||
{ id: 1, sn: 'GD30-2025-000001', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-15 14:30', operator: '张工程师' },
|
||||
{ id: 2, sn: 'GD30-2025-000002', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-18 09:15', operator: '张工程师' },
|
||||
{ id: 3, sn: 'GD30-2024-000056', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-12-20 16:00', operator: '李工程师' },
|
||||
{ id: 4, sn: 'GT20-2025-000045', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-02-10 11:20', operator: '王工程师' },
|
||||
{ id: 5, sn: 'GT20-2025-000046', model: 'GD-20', type: '二维电法仪', status: '装配中', firmware: 'v1.8.5', productionDate: '2025-03-01 08:45', operator: '王工程师' },
|
||||
{ id: 6, sn: 'GD30-2024-000078', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-11-05 13:30', operator: '张工程师' },
|
||||
{ id: 7, sn: 'GD10-2024-000033', model: 'GD-10 Supreme', type: '入门级电法仪', status: '已激活', firmware: 'v1.5.2', productionDate: '2024-09-12 10:00', operator: '李工程师' },
|
||||
{ id: 8, sn: 'GD30-2024-000089', model: 'GD-30 Supreme', type: '高密度电法仪', status: '装配中', firmware: 'v2.3.5', productionDate: '2025-03-05 15:10',operator: '张工程师' },
|
||||
{ id: 9, sn: 'GT20-2025-000012', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-01-22 09:30', operator: '王工程师' },
|
||||
{ id: 10, sn: 'GD30-2024-000102', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-10-18 14:00', operator: '李工程师' },
|
||||
{ id: 11, sn: 'GD10-2024-000034', model: 'GD-10 Supreme', type: '入门级电法仪', status: '装配中', firmware: 'v1.5.2', productionDate: '2025-03-08 11:45', operator: '王工程师' },
|
||||
{ id: 12, sn: 'GD30-2024-000145', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2024-08-25 16:20', operator: '张工程师' },
|
||||
]
|
||||
|
||||
/** Mock: 装机BOM清单 */
|
||||
|
|
@ -38,11 +38,46 @@ const licenseData: Record<string, { modules: string; expiry: string; status: str
|
|||
'GT20-2025-000045': { modules: '2D ERT, 3D ERT, 1D IP, 2D IP', expiry: '2025-06-30', status: '生效' },
|
||||
}
|
||||
|
||||
/** Mock: 配置文件 */
|
||||
const configData: Record<string, { name: string; version: string; uploadDate: string }> = {
|
||||
'GD30-2025-000001': { name: 'CFG-GD30-v1.3.0', version: 'v1.3.0', uploadDate: '2025-01-15' },
|
||||
'GD30-2025-000002': { name: 'CFG-GD30-v1.3.0', version: 'v1.3.0', uploadDate: '2025-01-18' },
|
||||
'GT20-2025-000045': { name: 'CFG-GD20-v1.1.0', version: 'v1.1.0', uploadDate: '2025-02-10' },
|
||||
/** Mock: 配置文件(含详细参数) */
|
||||
interface ConfigDetail {
|
||||
name: string
|
||||
version: string
|
||||
uploadDate: string
|
||||
params: {
|
||||
transmission: { maxVoltage: string; maxCurrent: string; dutyCycle: string; pulseWidths: string[]; waveforms: string[]; fullWaveform: boolean }
|
||||
acquisition: { channels: number; sampleRates: string[]; voltageRanges: string[]; iterationRange: string }
|
||||
protection: { overVoltage: { enabled: boolean; threshold: string }; overCurrent: { enabled: boolean; threshold: string }; shortCircuit: boolean; overTemp: { enabled: boolean; threshold: string } }
|
||||
network: { wifiSsidPrefix: string }
|
||||
}
|
||||
}
|
||||
const configData: Record<string, ConfigDetail> = {
|
||||
'GD30-2025-000001': {
|
||||
name: 'CFG-GD30-v1.3.0', version: 'v1.3.0', uploadDate: '2025-01-15',
|
||||
params: {
|
||||
transmission: { maxVoltage: '1500V', maxCurrent: '10A', dutyCycle: '50%、100%', pulseWidths: ['0.25s', '0.5s', '1s', '2s', '4s', '8s', '16s', '32s', '64s'], waveforms: ['0+0-', '+0-0', '+-'], fullWaveform: true },
|
||||
acquisition: { channels: 12, sampleRates: ['50Hz', '60Hz', '100Hz', '1000Hz'], voltageRanges: ['±2.5V', '±80V', '±600V'], iterationRange: '1~256' },
|
||||
protection: { overVoltage: { enabled: true, threshold: '1600V' }, overCurrent: { enabled: true, threshold: '12A' }, shortCircuit: true, overTemp: { enabled: true, threshold: '75°C' } },
|
||||
network: { wifiSsidPrefix: 'GD30-Supreme' },
|
||||
},
|
||||
},
|
||||
'GD30-2025-000002': {
|
||||
name: 'CFG-GD30-v1.3.0', version: 'v1.3.0', uploadDate: '2025-01-18',
|
||||
params: {
|
||||
transmission: { maxVoltage: '1500V', maxCurrent: '10A', dutyCycle: '50%、100%', pulseWidths: ['0.25s', '0.5s', '1s', '2s', '4s', '8s', '16s', '32s', '64s'], waveforms: ['0+0-', '+0-0', '+-'], fullWaveform: true },
|
||||
acquisition: { channels: 12, sampleRates: ['50Hz', '60Hz', '100Hz', '1000Hz'], voltageRanges: ['±2.5V', '±80V', '±600V'], iterationRange: '1~256' },
|
||||
protection: { overVoltage: { enabled: true, threshold: '1600V' }, overCurrent: { enabled: true, threshold: '12A' }, shortCircuit: true, overTemp: { enabled: true, threshold: '75°C' } },
|
||||
network: { wifiSsidPrefix: 'GD30-Supreme' },
|
||||
},
|
||||
},
|
||||
'GT20-2025-000045': {
|
||||
name: 'CFG-GD20-v1.1.0', version: 'v1.1.0', uploadDate: '2025-02-10',
|
||||
params: {
|
||||
transmission: { maxVoltage: '800V', maxCurrent: '5A', dutyCycle: '50%', pulseWidths: ['0.25s', '0.5s', '1s', '2s', '4s', '8s'], waveforms: ['0+0-', '+0-0'], fullWaveform: false },
|
||||
acquisition: { channels: 6, sampleRates: ['50Hz', '60Hz'], voltageRanges: ['±2.5V', '±80V'], iterationRange: '1~128' },
|
||||
protection: { overVoltage: { enabled: true, threshold: '900V' }, overCurrent: { enabled: true, threshold: '6A' }, shortCircuit: true, overTemp: { enabled: true, threshold: '70°C' } },
|
||||
network: { wifiSsidPrefix: 'GD20' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/** Mock: 操作日志 */
|
||||
|
|
@ -55,30 +90,30 @@ const operationLogs = [
|
|||
{ date: '2025-01-16 09:00', action: '出厂检测', operator: '李工程师', detail: '出厂检测通过,设备状态变更为已出厂' },
|
||||
]
|
||||
|
||||
/** Mock: 装配 Checklist */
|
||||
/** Mock: 装配 Checklist(含照片和记录) */
|
||||
const checklistData = [
|
||||
{ name: '主板SN扫码绑定', passed: true },
|
||||
{ name: '采集板SN录入(×6)', passed: true },
|
||||
{ name: '发射板安装检查', passed: true },
|
||||
{ name: '升压板安装检查', passed: true },
|
||||
{ name: '线缆连接检查', passed: true },
|
||||
{ name: '整机通电测试', passed: true },
|
||||
{ name: 'GPS/北斗模块检测', passed: true },
|
||||
{ name: 'WiFi通信测试', passed: true },
|
||||
{ name: '蓝牙通信测试', passed: true },
|
||||
{ name: '采集通道校准验证', passed: true },
|
||||
{ name: '发射电压测试', passed: true },
|
||||
{ name: '电池安装与充电测试', passed: true },
|
||||
{ name: 'IP66防护检测', passed: true },
|
||||
{ name: '固件版本校验', passed: true },
|
||||
{ name: '配置文件写入', passed: true },
|
||||
{ name: '授权文件写入', passed: true },
|
||||
{ name: '整机功能测试', passed: true },
|
||||
{ name: '数据采集验证', passed: true },
|
||||
{ name: '外观检查', passed: true },
|
||||
{ name: '标签粘贴', passed: true },
|
||||
{ name: '配件清点', passed: true },
|
||||
{ name: '包装检查', passed: true },
|
||||
{ name: '主板SN扫码绑定', passed: true, photos: ['https://picsum.photos/seed/mb1/800/600', 'https://picsum.photos/seed/mb2/800/600'], note: '主板SN MB25011501 扫码绑定成功,条码清晰可读' },
|
||||
{ name: '采集板SN录入(×6)', passed: true, photos: ['https://picsum.photos/seed/rx1/800/600', 'https://picsum.photos/seed/rx2/800/600', 'https://picsum.photos/seed/rx3/800/600'], note: '6块采集板全部录入完成,SN号已核对一致' },
|
||||
{ name: '发射板安装检查', passed: true, photos: ['https://picsum.photos/seed/tx1/800/600'], note: '发射板安装到位,螺丝紧固力矩达标' },
|
||||
{ name: '升压板安装检查', passed: true, photos: ['https://picsum.photos/seed/bp1/800/600'], note: '升压板安装完成,接线端子牢固' },
|
||||
{ name: '线缆连接检查', passed: true, photos: ['https://picsum.photos/seed/cable1/800/600', 'https://picsum.photos/seed/cable2/800/600'], note: '所有线缆连接正确,无松动现象' },
|
||||
{ name: '整机通电测试', passed: true, photos: ['https://picsum.photos/seed/power1/800/600'], note: '通电正常,各指示灯状态正确' },
|
||||
{ name: 'GPS/北斗模块检测', passed: true, photos: ['https://picsum.photos/seed/gps1/800/600'], note: 'GPS/北斗信号接收正常,定位精度达标' },
|
||||
{ name: 'WiFi通信测试', passed: true, photos: [], note: 'WiFi连接稳定,信号强度 -45dBm' },
|
||||
{ name: '蓝牙通信测试', passed: true, photos: [], note: '蓝牙配对成功,数据传输正常' },
|
||||
{ name: '采集通道校准验证', passed: true, photos: ['https://picsum.photos/seed/cal1/800/600', 'https://picsum.photos/seed/cal2/800/600'], note: '所有采集通道校准偏差 < 0.1%,符合标准' },
|
||||
{ name: '发射电压测试', passed: true, photos: ['https://picsum.photos/seed/volt1/800/600'], note: '发射电压 800V 测试通过,波形正常' },
|
||||
{ name: '电池安装与充电测试', passed: true, photos: ['https://picsum.photos/seed/bat1/800/600'], note: '电池安装到位,充电电流正常' },
|
||||
{ name: 'IP66防护检测', passed: true, photos: ['https://picsum.photos/seed/ip1/800/600', 'https://picsum.photos/seed/ip2/800/600'], note: 'IP66防护等级测试通过,密封良好' },
|
||||
{ name: '固件版本校验', passed: true, photos: [], note: '固件版本 v2.3.5 校验通过' },
|
||||
{ name: '配置文件写入', passed: true, photos: [], note: '配置文件 CFG-GD30-v1.3.0 写入成功' },
|
||||
{ name: '授权文件写入', passed: true, photos: [], note: '全模块授权项写入完成' },
|
||||
{ name: '整机功能测试', passed: true, photos: ['https://picsum.photos/seed/func1/800/600'], note: '整机功能测试全部通过' },
|
||||
{ name: '数据采集验证', passed: true, photos: ['https://picsum.photos/seed/data1/800/600', 'https://picsum.photos/seed/data2/800/600'], note: '数据采集验证通过,采集数据与标准值一致' },
|
||||
{ name: '外观检查', passed: true, photos: ['https://picsum.photos/seed/look1/800/600'], note: '外观无划痕、无变形,表面处理合格' },
|
||||
{ name: '标签粘贴', passed: true, photos: ['https://picsum.photos/seed/label1/800/600'], note: 'SN标签、型号标签、安全标签粘贴完成' },
|
||||
{ name: '配件清点', passed: true, photos: ['https://picsum.photos/seed/acc1/800/600'], note: '配件清单核对完成,数量一致' },
|
||||
{ name: '包装检查', passed: true, photos: ['https://picsum.photos/seed/pack1/800/600', 'https://picsum.photos/seed/pack2/800/600'], note: '包装完好,防震材料到位' },
|
||||
]
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
|
|
@ -103,6 +138,8 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
const { sn } = use(params)
|
||||
const router = useRouter()
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const [expandedItem, setExpandedItem] = useState<number | null>(null)
|
||||
const [lightbox, setLightbox] = useState<{ photos: string[]; index: number } | null>(null)
|
||||
|
||||
const device = allDevices.find(d => d.sn === sn)
|
||||
const bom = bomData[sn] || []
|
||||
|
|
@ -157,22 +194,6 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 600, color: '#4a7c59' }}>{bom.length}</div>
|
||||
</div>
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<Key size={16} style={{ color: license ? '#52C41A' : '#FAAD14' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>授权项状态</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: license ? '#52C41A' : '#FAAD14' }}>{license ? license.status : '未授权'}</div>
|
||||
{license && <div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', marginTop: 2 }}>到期:{license.expiry}</div>}
|
||||
</div>
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<FileCode size={16} style={{ color: config ? '#52C41A' : 'rgba(0,0,0,0.25)' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>配置文件</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: config ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.25)' }}>{config ? config.version : '未配置'}</div>
|
||||
{config && <div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', marginTop: 2 }}>{config.name}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
|
|
@ -198,7 +219,6 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
{ label: '固件版本', value: device.firmware },
|
||||
{ label: '生产日期', value: device.productionDate },
|
||||
{ label: '登记人', value: device.operator },
|
||||
{ label: '客户', value: device.customer },
|
||||
{ label: '设备状态', value: device.status, isStatus: true },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
|
|
@ -259,39 +279,164 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
{activeTab === 'checklist' && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>装配 Checklist</h3>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
通过 <span style={{ color: '#52C41A', fontWeight: 600 }}>{checklistData.filter(c => c.passed).length}</span>/{checklistData.length}
|
||||
</span>
|
||||
<div>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>装配记录</h3>
|
||||
<p style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>点击项目查看照片和装配记录详情</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
通过 <span style={{ color: '#52C41A', fontWeight: 600 }}>{checklistData.filter(c => c.passed).length}</span>/{checklistData.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const rows = checklistData.map((item, i) => [
|
||||
i + 1,
|
||||
item.name,
|
||||
item.passed ? '通过' : '未通过',
|
||||
item.note || '-',
|
||||
item.photos.length > 0 ? `${item.photos.length}张` : '无',
|
||||
])
|
||||
const header = ['序号', '检查项', '结果', '装配记录', '照片数量']
|
||||
const csvContent = '\uFEFF' + [header, ...rows].map(r => r.map(c => `"${c}"`).join(',')).join('\n')
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `装配记录_${sn}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 14px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13 }}
|
||||
>
|
||||
<Download size={14} />导出记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 6, backgroundColor: '#F0F0F0', borderRadius: 3, marginBottom: 20, overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${(checklistData.filter(c => c.passed).length / checklistData.length) * 100}%`, backgroundColor: '#52C41A', borderRadius: 3 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{checklistData.map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', borderRadius: 8, backgroundColor: item.passed ? '#F6FFED' : '#FFF1F0', border: item.passed ? '1px solid #B7EB8F' : '1px solid #FFCCC7' }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: item.passed ? '#52C41A' : '#FF4D4F', flexShrink: 0 }}>
|
||||
<CheckCircle size={14} color="#fff" />
|
||||
{checklistData.map((item, i) => {
|
||||
const isExpanded = expandedItem === i
|
||||
return (
|
||||
<div key={i}>
|
||||
<div
|
||||
onClick={() => setExpandedItem(isExpanded ? null : i)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', borderRadius: isExpanded ? '8px 8px 0 0' : 8,
|
||||
backgroundColor: item.passed ? '#F6FFED' : '#FFF1F0', border: item.passed ? '1px solid #B7EB8F' : '1px solid #FFCCC7',
|
||||
borderBottom: isExpanded ? 'none' : undefined, cursor: 'pointer', transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: item.passed ? '#52C41A' : '#FF4D4F', flexShrink: 0 }}>
|
||||
<CheckCircle size={14} color="#fff" />
|
||||
</div>
|
||||
<div style={{ width: 28, height: 28, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 600, backgroundColor: item.passed ? '#eef5f0' : '#FFF2F0', color: item.passed ? '#4a7c59' : '#FF4D4F', flexShrink: 0 }}>{i + 1}</div>
|
||||
<span style={{ fontSize: 14, color: item.passed ? '#4a7c59' : '#FF4D4F', flex: 1 }}>{item.name}</span>
|
||||
{item.photos.length > 0 && (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'rgba(0,0,0,0.45)', padding: '2px 8px', borderRadius: 4, backgroundColor: 'rgba(0,0,0,0.04)' }}>
|
||||
<Camera size={12} />{item.photos.length}张照片
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: 12, color: item.passed ? '#52C41A' : '#FF4D4F' }}>{item.passed ? '通过' : '未通过'}</span>
|
||||
<ChevronRight size={14} style={{ color: 'rgba(0,0,0,0.25)', transform: isExpanded ? 'rotate(90deg)' : 'none', transition: 'transform 0.15s' }} />
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div style={{
|
||||
padding: 16, backgroundColor: item.passed ? '#FCFFF8' : '#FFFAFA',
|
||||
border: item.passed ? '1px solid #B7EB8F' : '1px solid #FFCCC7', borderTop: 'none',
|
||||
borderRadius: '0 0 8px 8px',
|
||||
}}>
|
||||
{item.note && (
|
||||
<div style={{ marginBottom: item.photos.length > 0 ? 12 : 0 }}>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 4 }}>装配记录</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.85)', lineHeight: 1.6 }}>{item.note}</div>
|
||||
</div>
|
||||
)}
|
||||
{item.photos.length > 0 && (
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>装配照片</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{item.photos.map((photo, pi) => (
|
||||
<div
|
||||
key={pi}
|
||||
onClick={(e) => { e.stopPropagation(); setLightbox({ photos: item.photos, index: pi }) }}
|
||||
style={{
|
||||
width: 100, height: 75, borderRadius: 6, overflow: 'hidden', cursor: 'pointer',
|
||||
border: '1px solid #e8e8e8', position: 'relative',
|
||||
}}
|
||||
>
|
||||
<img src={photo} alt={`${item.name} 照片${pi + 1}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0,0,0,0.3)'; (e.currentTarget.querySelector('svg') as SVGElement).style.opacity = '1' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0,0,0,0)'; (e.currentTarget.querySelector('svg') as SVGElement).style.opacity = '0' }}
|
||||
>
|
||||
<ZoomIn size={20} color="#fff" style={{ opacity: 0, transition: 'opacity 0.15s' }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!item.note && item.photos.length === 0 && (
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.25)' }}>暂无装配记录和照片</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ width: 28, height: 28, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 600, backgroundColor: item.passed ? '#eef5f0' : '#FFF2F0', color: item.passed ? '#4a7c59' : '#FF4D4F', flexShrink: 0 }}>{i + 1}</div>
|
||||
<span style={{ fontSize: 14, color: item.passed ? '#4a7c59' : '#FF4D4F' }}>{item.name}</span>
|
||||
<span style={{ marginLeft: 'auto', fontSize: 12, color: item.passed ? '#52C41A' : '#FF4D4F' }}>{item.passed ? '通过' : '未通过'}</span>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Photo Lightbox */}
|
||||
{lightbox && (
|
||||
<div
|
||||
onClick={() => setLightbox(null)}
|
||||
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.85)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<button onClick={() => setLightbox(null)} style={{ position: 'absolute', top: 16, right: 16, background: 'none', border: 'none', cursor: 'pointer', padding: 8 }}>
|
||||
<X size={24} color="#fff" />
|
||||
</button>
|
||||
<div style={{ position: 'absolute', bottom: 20, color: '#fff', fontSize: 14 }}>
|
||||
{lightbox.index + 1} / {lightbox.photos.length}
|
||||
</div>
|
||||
{lightbox.photos.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setLightbox({ ...lightbox, index: (lightbox.index - 1 + lightbox.photos.length) % lightbox.photos.length }) }}
|
||||
style={{ position: 'absolute', left: 16, background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: '50%', width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
|
||||
>
|
||||
<ChevronLeft size={24} color="#fff" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setLightbox({ ...lightbox, index: (lightbox.index + 1) % lightbox.photos.length }) }}
|
||||
style={{ position: 'absolute', right: 16, background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: '50%', width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
|
||||
>
|
||||
<ChevronRight size={24} color="#fff" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<img
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
src={lightbox.photos[lightbox.index]}
|
||||
alt="装配照片"
|
||||
style={{ maxWidth: '85vw', maxHeight: '85vh', borderRadius: 8, objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'license' && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>授权项信息</h3>
|
||||
{license ? (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24, marginBottom: 24 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>授权项状态</div>
|
||||
<span style={{ padding: '2px 10px', borderRadius: 4, fontSize: 12, backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }}>{license.status}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>到期时间</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{license.expiry}</div>
|
||||
|
|
@ -316,27 +461,154 @@ export default function DeviceDetailPage({ params }: { params: Promise<{ sn: str
|
|||
)}
|
||||
|
||||
{activeTab === 'config' && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>配置文件</h3>
|
||||
<div>
|
||||
{config ? (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>配置名称</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.name}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>配置文件</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>配置名称</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>版本</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.version}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>写入日期</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.uploadDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>版本</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.version}</div>
|
||||
|
||||
{/* 发射参数 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 4, height: 16, borderRadius: 2, backgroundColor: '#4a7c59', display: 'inline-block' }} />
|
||||
发射参数
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>最大发射电压</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.params.transmission.maxVoltage}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>最大发射电流</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.params.transmission.maxCurrent}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>占空比</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.params.transmission.dutyCycle}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>支持全波形测量</div>
|
||||
<span style={{
|
||||
padding: '2px 10px', borderRadius: 4, fontSize: 12,
|
||||
backgroundColor: config.params.transmission.fullWaveform ? '#F6FFED' : '#FAFAFA',
|
||||
color: config.params.transmission.fullWaveform ? '#52C41A' : 'rgba(0,0,0,0.45)',
|
||||
border: config.params.transmission.fullWaveform ? '1px solid #B7EB8F' : '1px solid #D9D9D9',
|
||||
}}>{config.params.transmission.fullWaveform ? '支持' : '不支持'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>发射脉宽</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{config.params.transmission.pulseWidths.map(pw => (
|
||||
<span key={pw} style={{ padding: '3px 10px', borderRadius: 4, fontSize: 12, backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' }}>{pw}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>发射波形</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{config.params.transmission.waveforms.map(wf => (
|
||||
<span key={wf} style={{ padding: '3px 10px', borderRadius: 4, fontSize: 12, backgroundColor: '#f5f5f5', color: 'rgba(0,0,0,0.65)', border: '1px solid #e8e8e8', fontFamily: 'monospace' }}>{wf}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>写入日期</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.uploadDate}</div>
|
||||
|
||||
{/* 采集参数 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 4, height: 16, borderRadius: 2, backgroundColor: '#597EF7', display: 'inline-block' }} />
|
||||
采集参数
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>支持通道数</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.params.acquisition.channels} 通道</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>迭代次数范围</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{config.params.acquisition.iterationRange}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>采样率</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{config.params.acquisition.sampleRates.map(sr => (
|
||||
<span key={sr} style={{ padding: '3px 10px', borderRadius: 4, fontSize: 12, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{sr}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>电压测量范围</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{config.params.acquisition.voltageRanges.map(vr => (
|
||||
<span key={vr} style={{ padding: '3px 10px', borderRadius: 4, fontSize: 12, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{vr}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保护参数 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 4, height: 16, borderRadius: 2, backgroundColor: '#FA8C16', display: 'inline-block' }} />
|
||||
保护参数
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
|
||||
{[
|
||||
{ label: '过压保护', enabled: config.params.protection.overVoltage.enabled, value: config.params.protection.overVoltage.threshold },
|
||||
{ label: '过流保护', enabled: config.params.protection.overCurrent.enabled, value: config.params.protection.overCurrent.threshold },
|
||||
{ label: '短路保护', enabled: config.params.protection.shortCircuit, value: null },
|
||||
{ label: '高温保护', enabled: config.params.protection.overTemp.enabled, value: config.params.protection.overTemp.threshold },
|
||||
].map(item => (
|
||||
<div key={item.label} style={{ padding: 14, borderRadius: 8, backgroundColor: item.enabled ? '#FFFBE6' : '#FAFAFA', border: item.enabled ? '1px solid #FFE58F' : '1px solid #F0F0F0' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: item.enabled ? '#52C41A' : '#D9D9D9' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{item.label}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: item.enabled ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.25)' }}>
|
||||
{item.enabled ? (item.value ? `阈值 ${item.value}` : '已启用') : '未启用'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 网络参数 */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 4, height: 16, borderRadius: 2, backgroundColor: '#52C41A', display: 'inline-block' }} />
|
||||
网络参数
|
||||
</h3>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>WiFi SSID 前缀</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, fontFamily: 'monospace', padding: '4px 10px', backgroundColor: '#f5f5f5', borderRadius: 4, display: 'inline-block' }}>{config.params.network.wifiSsidPrefix}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||
<FileCode size={32} style={{ color: 'rgba(0,0,0,0.15)', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>该设备暂未写入配置文件</div>
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: '0 0 20px' }}>配置文件</h3>
|
||||
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||
<FileCode size={32} style={{ color: 'rgba(0,0,0,0.15)', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 14, color: 'rgba(0,0,0,0.25)' }}>该设备暂未写入配置文件</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,17 +6,48 @@ import { ArrowLeft, Upload, Download, ChevronDown, ChevronUp, X, Package, Shield
|
|||
const firmwareTypes = ['全部', '主协板', '采集板', '发射板', '升压板', '主机固件', '计算单元固件']
|
||||
|
||||
const firmwareData = [
|
||||
{ id: 1, version: 'v2.1.0', boardModel: 'MB25130025', type: '主协板', date: '2024-03-10', status: '已发布', size: '12.5MB', downloads: 1234, hwRange: 'MB25130025 Rev.A~C', upgradeType: '可选', signed: true, md5: 'a1b2c3d4e5f6...', sha256: '9f8e7d6c5b4a...', notes: ['修复通信协议兼容性问题', '优化低功耗模式切换', '新增看门狗超时配置'] },
|
||||
{ id: 2, version: 'v2.0.0', boardModel: 'MB25130025', type: '主协板', date: '2024-01-15', status: '已发布', size: '11.8MB', downloads: 2456, hwRange: 'MB25130025 Rev.A~C', upgradeType: '强制', signed: true, md5: 'b2c3d4e5f6a1...', sha256: '8e7d6c5b4a9f...', notes: ['新增多通道采集支持', '重构通信协议栈'] },
|
||||
{ id: 3, version: 'v1.8.5', boardModel: 'MCB-2000', type: '主协板', date: '2023-11-01', status: '已发布', size: '9.2MB', downloads: 3120, hwRange: 'MCB-2000 Rev.B~D', upgradeType: '可选', signed: true, md5: 'c3d4e5f6a1b2...', sha256: '7d6c5b4a9f8e...', notes: ['优化功耗管理', '修复偶发重启问题'] },
|
||||
{ id: 4, version: 'v3.0.2', boardModel: 'ACB-6000', type: '采集板', date: '2024-02-20', status: '已发布', size: '8.7MB', downloads: 890, hwRange: 'ACB-6000 Rev.A~B', upgradeType: '可选', signed: true, md5: 'd4e5f6a1b2c3...', sha256: '6c5b4a9f8e7d...', notes: ['提升采样精度', '修复通道串扰问题', '新增自校准功能'] },
|
||||
{ id: 5, version: 'v2.5.1', boardModel: 'ACB-5000', type: '采集板', date: '2023-09-15', status: '已发布', size: '7.1MB', downloads: 1567, hwRange: 'ACB-5000 Rev.A~C', upgradeType: '可选', signed: true, md5: 'e5f6a1b2c3d4...', sha256: '5b4a9f8e7d6c...', notes: ['优化ADC驱动', '修复温漂补偿算法'] },
|
||||
{ id: 6, version: 'v1.2.0', boardModel: 'TXB-1000', type: '发射板', date: '2024-02-28', status: '已发布', size: '6.3MB', downloads: 456, hwRange: 'TXB-1000 Rev.A', upgradeType: '强制', signed: true, md5: 'f6a1b2c3d4e5...', sha256: '4a9f8e7d6c5b...', notes: ['新增过流保护', '优化PWM控制算法'] },
|
||||
{ id: 7, version: 'v1.0.3', boardModel: 'TXB-800', type: '发射板', date: '2023-06-20', status: '已发布', size: '5.8MB', downloads: 789, hwRange: 'TXB-800 Rev.A~B', upgradeType: '可选', signed: false, md5: 'a1c3e5b2d4f6...', sha256: '3f8e6d4b2a9c...', notes: ['修复高压输出不稳定'] },
|
||||
{ id: 8, version: 'v1.1.0', boardModel: 'BST-500', type: '升压板', date: '2024-04-01', status: '草稿', size: '4.2MB', downloads: 0, hwRange: 'BST-500 Rev.A', upgradeType: '可选', signed: false, md5: '-', sha256: '-', notes: ['初始版本', '支持500V~1500V输出'] },
|
||||
{ id: 9, version: 'v0.9.8', boardModel: 'BST-300', type: '升压板', date: '2023-08-25', status: '已发布', size: '3.9MB', downloads: 345, hwRange: 'BST-300 Rev.A', upgradeType: '可选', signed: true, md5: 'b2d4f6a1c3e5...', sha256: '2a9c8e6d4b3f...', notes: ['修复过温保护阈值'] },
|
||||
{ id: 1, version: 'v2.1.0', boardVersion: 'MB-V1.8', type: '主协板', date: '2024-03-10', status: '已发布', size: '12.5MB', downloads: 1234, hwRange: 'MB25130025 Rev.A~C', upgradeType: '可选', signed: true, md5: 'a1b2c3d4e5f6...', sha256: '9f8e7d6c5b4a...', notes: ['修复通信协议兼容性问题', '优化低功耗模式切换', '新增看门狗超时配置'] },
|
||||
{ id: 2, version: 'v2.0.0', boardVersion: 'MB-V1.8', type: '主协板', date: '2024-01-15', status: '已发布', size: '11.8MB', downloads: 2456, hwRange: 'MB25130025 Rev.A~C', upgradeType: '强制', signed: true, md5: 'b2c3d4e5f6a1...', sha256: '8e7d6c5b4a9f...', notes: ['新增多通道采集支持', '重构通信协议栈'] },
|
||||
{ id: 3, version: 'v1.8.5', boardVersion: 'MB-V1.8', type: '主协板', date: '2023-11-01', status: '已发布', size: '9.2MB', downloads: 3120, hwRange: 'MCB-2000 Rev.B~D', upgradeType: '可选', signed: true, md5: 'c3d4e5f6a1b2...', sha256: '7d6c5b4a9f8e...', notes: ['优化功耗管理', '修复偶发重启问题'] },
|
||||
{ id: 4, version: 'v3.0.2', boardVersion: 'RX-V2.3', type: '采集板', date: '2024-02-20', status: '已发布', size: '8.7MB', downloads: 890, hwRange: 'ACB-6000 Rev.A~B', upgradeType: '可选', signed: true, md5: 'd4e5f6a1b2c3...', sha256: '6c5b4a9f8e7d...', notes: ['提升采样精度', '修复通道串扰问题', '新增自校准功能'] },
|
||||
{ id: 5, version: 'v2.5.1', boardVersion: 'RX-V2.3', type: '采集板', date: '2023-09-15', status: '已发布', size: '7.1MB', downloads: 1567, hwRange: 'ACB-5000 Rev.A~C', upgradeType: '可选', signed: true, md5: 'e5f6a1b2c3d4...', sha256: '5b4a9f8e7d6c...', notes: ['优化ADC驱动', '修复温漂补偿算法'] },
|
||||
{ id: 6, version: 'v1.2.0', boardVersion: 'TX-V2.1', type: '发射板', date: '2024-02-28', status: '已发布', size: '6.3MB', downloads: 456, hwRange: 'TXB-1000 Rev.A', upgradeType: '强制', signed: true, md5: 'f6a1b2c3d4e5...', sha256: '4a9f8e7d6c5b...', notes: ['新增过流保护', '优化PWM控制算法'] },
|
||||
{ id: 7, version: 'v1.0.3', boardVersion: 'TX-V2.1', type: '发射板', date: '2023-06-20', status: '已发布', size: '5.8MB', downloads: 789, hwRange: 'TXB-800 Rev.A~B', upgradeType: '可选', signed: false, md5: 'a1c3e5b2d4f6...', sha256: '3f8e6d4b2a9c...', notes: ['修复高压输出不稳定'] },
|
||||
{ id: 8, version: 'v1.1.0', boardVersion: 'BP600-V1.2', type: '升压板', date: '2024-04-01', status: '草稿', size: '4.2MB', downloads: 0, hwRange: 'BST-500 Rev.A', upgradeType: '可选', signed: false, md5: '-', sha256: '-', notes: ['初始版本', '支持100V~600V输出'] },
|
||||
]
|
||||
|
||||
/** 设备型号固件数据:按型号 + 固件类别 */
|
||||
const deviceFirmwareData: Record<string, Record<string, typeof firmwareData>> = {
|
||||
GD30: {
|
||||
'主机固件': [
|
||||
{ id: 101, version: 'v4.2.0', boardVersion: '-', type: '主机固件', date: '2025-03-15', status: '已发布', size: '45.6MB', downloads: 320, hwRange: 'GD30 Rev.A~C', upgradeType: '可选', signed: true, md5: 'gd30h42a1b2c3...', sha256: 'gd30h42x9f8e7d...', notes: ['新增远程诊断功能', '优化数据采集流程', '修复蓝牙连接稳定性'] },
|
||||
{ id: 102, version: 'v4.1.0', boardVersion: '-', type: '主机固件', date: '2025-01-20', status: '已发布', size: '44.2MB', downloads: 580, hwRange: 'GD30 Rev.A~C', upgradeType: '强制', signed: true, md5: 'gd30h41b2c3d4...', sha256: 'gd30h41y8e7d6c...', notes: ['重构通信协议栈', '新增OTA增量升级支持'] },
|
||||
{ id: 103, version: 'v4.0.0', boardVersion: '-', type: '主机固件', date: '2024-10-01', status: '已发布', size: '42.8MB', downloads: 1200, hwRange: 'GD30 Rev.A~B', upgradeType: '强制', signed: true, md5: 'gd30h40c3d4e5...', sha256: 'gd30h40z7d6c5b...', notes: ['GD30 首个正式版本', '支持最大60通道采集'] },
|
||||
],
|
||||
'计算单元固件': [
|
||||
{ id: 201, version: 'v2.3.1', boardVersion: '-', type: '计算单元固件', date: '2025-02-28', status: '已发布', size: '18.3MB', downloads: 290, hwRange: 'GD30-CU Rev.A', upgradeType: '可选', signed: true, md5: 'gd30c23a1b2c3...', sha256: 'gd30c23x9f8e7d...', notes: ['优化实时数据处理算法', '修复大数据量下内存溢出'] },
|
||||
{ id: 202, version: 'v2.2.0', boardVersion: '-', type: '计算单元固件', date: '2024-12-10', status: '已发布', size: '17.8MB', downloads: 450, hwRange: 'GD30-CU Rev.A', upgradeType: '可选', signed: true, md5: 'gd30c22b2c3d4...', sha256: 'gd30c22y8e7d6c...', notes: ['新增Wenner排列自动识别', '优化反演计算性能'] },
|
||||
],
|
||||
},
|
||||
GD20: {
|
||||
'主机固件': [
|
||||
{ id: 301, version: 'v3.5.2', boardVersion: '-', type: '主机固件', date: '2025-02-10', status: '已发布', size: '38.1MB', downloads: 670, hwRange: 'GD20 Rev.A~D', upgradeType: '可选', signed: true, md5: 'gd20h35a1b2c3...', sha256: 'gd20h35x9f8e7d...', notes: ['修复低温环境下启动异常', '优化电池管理策略'] },
|
||||
{ id: 302, version: 'v3.5.0', boardVersion: '-', type: '主机固件', date: '2024-11-05', status: '已发布', size: '37.5MB', downloads: 890, hwRange: 'GD20 Rev.A~D', upgradeType: '强制', signed: true, md5: 'gd20h35b2c3d4...', sha256: 'gd20h35y8e7d6c...', notes: ['新增GPS定位集成', '支持WiFi数据传输'] },
|
||||
],
|
||||
'计算单元固件': [
|
||||
{ id: 401, version: 'v1.8.0', boardVersion: '-', type: '计算单元固件', date: '2025-01-15', status: '已发布', size: '14.2MB', downloads: 340, hwRange: 'GD20-CU Rev.A~B', upgradeType: '可选', signed: true, md5: 'gd20c18a1b2c3...', sha256: 'gd20c18x9f8e7d...', notes: ['优化数据预处理流水线', '新增异常数据自动剔除'] },
|
||||
],
|
||||
},
|
||||
GD10: {
|
||||
'主机固件': [
|
||||
{ id: 501, version: 'v2.1.3', boardVersion: '-', type: '主机固件', date: '2024-06-20', status: '已发布', size: '28.5MB', downloads: 1450, hwRange: 'GD10 Rev.A~C', upgradeType: '可选', signed: true, md5: 'gd10h21a1b2c3...', sha256: 'gd10h21x9f8e7d...', notes: ['最终维护版本', '修复已知稳定性问题'] },
|
||||
],
|
||||
'计算单元固件': [
|
||||
{ id: 601, version: 'v1.2.0', boardVersion: '-', type: '计算单元固件', date: '2024-04-10', status: '已发布', size: '10.8MB', downloads: 980, hwRange: 'GD10-CU Rev.A', upgradeType: '可选', signed: false, md5: 'gd10c12a1b2c3...', sha256: 'gd10c12x9f8e7d...', notes: ['最终维护版本'] },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
case '已发布': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
|
|
@ -37,52 +68,56 @@ function FirmwareContent() {
|
|||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const boardParam = searchParams.get('board') || ''
|
||||
const modelParam = searchParams.get('model') || ''
|
||||
|
||||
// Derive initial type filter from board param
|
||||
const matchedFw = boardParam ? firmwareData.find(f => f.boardModel === boardParam) : null
|
||||
const isFromBoards = !!boardParam
|
||||
const isFromModels = !!modelParam
|
||||
const matchedFw = boardParam ? firmwareData.find(f => f.boardVersion === boardParam) : null
|
||||
const initialType = matchedFw ? matchedFw.type : '全部'
|
||||
|
||||
const [filterType, setFilterType] = useState(initialType)
|
||||
const [filterBoard, setFilterBoard] = useState(boardParam)
|
||||
const [filterBoard, setFilterBoard] = useState('')
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null)
|
||||
const [uploadOpen, setUploadOpen] = useState(false)
|
||||
const [uploadForm, setUploadForm] = useState({ version: '', hwRange: '', upgradeType: '可选', firmwareType: '主协板', signed: false, notes: '' })
|
||||
const [deviceFwTab, setDeviceFwTab] = useState<'主机固件' | '计算单元固件'>('主机固件')
|
||||
|
||||
const filtered = firmwareData.filter(f => {
|
||||
// Device model firmware mode
|
||||
const modelFirmware = modelParam ? deviceFirmwareData[modelParam] : null
|
||||
const deviceFiltered = modelFirmware ? (modelFirmware[deviceFwTab] || []) : []
|
||||
|
||||
// Board firmware mode
|
||||
const filtered = isFromModels ? deviceFiltered : firmwareData.filter(f => {
|
||||
if (isFromBoards) return f.boardVersion === boardParam
|
||||
if (filterType !== '全部' && f.type !== filterType) return false
|
||||
if (filterBoard && f.boardModel !== filterBoard) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// Get unique board models for the current type filter
|
||||
const boardModels = [...new Set(
|
||||
firmwareData
|
||||
.filter(f => filterType === '全部' || f.type === filterType)
|
||||
.map(f => f.boardModel)
|
||||
)]
|
||||
|
||||
const handleTypeChange = (type: string) => {
|
||||
setFilterType(type)
|
||||
setFilterBoard('')
|
||||
}
|
||||
|
||||
const pageTitle = filterBoard
|
||||
? `固件库 — ${filterBoard}`
|
||||
: '固件库'
|
||||
const pageTitle = isFromModels
|
||||
? `固件管理 — ${modelParam}`
|
||||
: isFromBoards
|
||||
? `固件库 — ${boardParam}`
|
||||
: '固件库'
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{boardParam && (
|
||||
{(isFromBoards || isFromModels) && (
|
||||
<button onClick={() => router.back()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
)}
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>{pageTitle}</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>管理固件版本,支持上传与下载</p>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>
|
||||
{isFromModels ? '管理设备型号的主机固件与计算单元固件' : '管理固件版本,支持上传与下载'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setUploadOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
|
|
@ -90,7 +125,27 @@ function FirmwareContent() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Device Model Firmware Tabs */}
|
||||
{isFromModels && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #F0F0F0' }}>
|
||||
{(['主机固件', '计算单元固件'] as const).map(tab => (
|
||||
<button key={tab} onClick={() => { setDeviceFwTab(tab); setExpandedId(null) }} style={{
|
||||
padding: '10px 20px', fontSize: 14, cursor: 'pointer', border: 'none', backgroundColor: 'transparent',
|
||||
borderBottom: deviceFwTab === tab ? '2px solid #4a7c59' : '2px solid transparent',
|
||||
color: deviceFwTab === tab ? '#4a7c59' : 'rgba(0,0,0,0.65)', fontWeight: deviceFwTab === tab ? 600 : 400,
|
||||
}}>{tab}
|
||||
<span style={{ marginLeft: 6, padding: '1px 6px', borderRadius: 10, fontSize: 11, backgroundColor: deviceFwTab === tab ? '#eef5f0' : '#F5F5F5', color: deviceFwTab === tab ? '#4a7c59' : 'rgba(0,0,0,0.45)' }}>
|
||||
{(modelFirmware?.[tab] || []).length}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Type Tabs + Board Filter */}
|
||||
{!isFromBoards && !isFromModels && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #F0F0F0' }}>
|
||||
{firmwareTypes.map(type => (
|
||||
|
|
@ -101,16 +156,8 @@ function FirmwareContent() {
|
|||
}}>{type}</button>
|
||||
))}
|
||||
</div>
|
||||
{boardModels.length > 1 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 20px' }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', flexShrink: 0 }}>板卡型号:</span>
|
||||
<button onClick={() => setFilterBoard('')} style={{ padding: '4px 12px', borderRadius: 4, fontSize: 13, cursor: 'pointer', border: !filterBoard ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: !filterBoard ? '#eef5f0' : '#fff', color: !filterBoard ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>全部</button>
|
||||
{boardModels.map(bm => (
|
||||
<button key={bm} onClick={() => setFilterBoard(bm)} style={{ padding: '4px 12px', borderRadius: 4, fontSize: 13, cursor: 'pointer', border: filterBoard === bm ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: filterBoard === bm ? '#eef5f0' : '#fff', color: filterBoard === bm ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{bm}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Firmware Cards */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
|
|
@ -125,7 +172,6 @@ function FirmwareContent() {
|
|||
<span style={{ fontSize: 18, fontWeight: 600 }}>{fw.version}</span>
|
||||
<span style={{ ...getStatusStyle(fw.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{fw.status}</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, backgroundColor: '#F0F5FF', color: '#597EF7', border: '1px solid #ADC6FF' }}>{fw.type}</span>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{fw.boardModel}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 24, fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
<span>发布日期:{fw.date}</span>
|
||||
|
|
@ -228,12 +274,6 @@ function FirmwareContent() {
|
|||
<option value="强制">强制</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 固件类型</label>
|
||||
<select value={uploadForm.firmwareType} onChange={e => setUploadForm({ ...uploadForm, firmwareType: e.target.value })} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{['主协板', '采集板', '发射板', '升压板', '主机固件', '计算单元固件'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, cursor: 'pointer' }}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Download, Plus, Search, Info, ChevronLeft, ChevronRight, X, Check } from 'lucide-react'
|
||||
import { useState, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Download, Plus, Search, Info, ChevronLeft, ChevronRight, X, Check, ArrowLeft } from 'lucide-react'
|
||||
|
||||
const allAuthItems = [
|
||||
{ id: '1D_SP', name: '1D SP', description: '一维自然电位法' },
|
||||
|
|
@ -44,8 +44,28 @@ const statusStyle = (status: string) => {
|
|||
}
|
||||
|
||||
export default function LicensesPage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ padding: 24 }}>加载中...</div>}>
|
||||
<LicensesContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function LicensesContent() {
|
||||
const router = useRouter()
|
||||
const [filterModel, setFilterModel] = useState('')
|
||||
const searchParams = useSearchParams()
|
||||
const modelParam = searchParams.get('model') || ''
|
||||
|
||||
// Map model name (e.g. "GD-30 Supreme") to license model format (e.g. "GD-30")
|
||||
const modelNameToLicense: Record<string, string> = {
|
||||
'GD-30 Supreme': 'GD-30',
|
||||
'GD-20 Supreme': 'GD-20',
|
||||
'GD-10 Supreme': 'GD-10',
|
||||
}
|
||||
const initialModelFilter = modelParam ? (modelNameToLicense[modelParam] || modelParam) : ''
|
||||
const isFromModels = !!modelParam
|
||||
|
||||
const [filterModel, setFilterModel] = useState(initialModelFilter)
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
|
@ -82,9 +102,16 @@ export default function LicensesPage() {
|
|||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-1">授权管理</h2>
|
||||
<p className="text-sm" style={{ color: 'rgba(0,0,0,0.45)' }}>管理设备授权许可</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{isFromModels && (
|
||||
<button onClick={() => router.back()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-1">{isFromModels ? `授权管理 — ${modelParam}` : '授权管理'}</h2>
|
||||
<p className="text-sm" style={{ color: 'rgba(0,0,0,0.45)' }}>管理设备授权许可</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm" style={{ border: '1px solid #D9D9D9', backgroundColor: '#fff', color: 'rgba(0,0,0,0.65)' }}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, ChevronDown } from 'lucide-react'
|
||||
import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, Cpu } from 'lucide-react'
|
||||
|
||||
const initialModelsData = [
|
||||
{ id: 1, name: 'GD-30 Supreme', code: 'GD30', status: '在产', description: '高端高密度电法仪', createDate: '2023-06-01' },
|
||||
|
|
@ -61,13 +61,11 @@ export default function ModelsPage() {
|
|||
const [editDrawer, setEditDrawer] = useState(false)
|
||||
const [editingModel, setEditingModel] = useState<typeof initialModelsData[0] | null>(null)
|
||||
const [editStatus, setEditStatus] = useState('')
|
||||
const [actionMenuId, setActionMenuId] = useState<number | null>(null)
|
||||
|
||||
const handleEdit = (model: typeof initialModelsData[0]) => {
|
||||
setEditingModel(model)
|
||||
setEditStatus(model.status)
|
||||
setEditDrawer(true)
|
||||
setActionMenuId(null)
|
||||
}
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
|
|
@ -134,41 +132,31 @@ export default function ModelsPage() {
|
|||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{model.createDate}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button
|
||||
onClick={() => setActionMenuId(actionMenuId === model.id ? null : model.id)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}
|
||||
onClick={() => handleEdit(model)}
|
||||
style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
操作 <ChevronDown size={14} />
|
||||
<Edit size={14} />编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/licenses?model=${model.name}`)}
|
||||
style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
<Key size={14} />授权项
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/config-files?model=${model.name}`)}
|
||||
style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
<FileCode size={14} />配置
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/firmware?model=${model.code}`)}
|
||||
style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
<Cpu size={14} />固件
|
||||
</button>
|
||||
{actionMenuId === model.id && (
|
||||
<div style={{ position: 'absolute', top: '100%', left: 0, marginTop: 4, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', border: '1px solid #F0F0F0', zIndex: 10, minWidth: 160, overflow: 'hidden' }}>
|
||||
<button
|
||||
onClick={() => handleEdit(model)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '10px 16px', border: 'none', backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.85)', textAlign: 'left' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#F5F5F5')}
|
||||
onMouseLeave={e => (e.currentTarget.style.backgroundColor = '#fff')}
|
||||
>
|
||||
<Edit size={14} style={{ color: '#4a7c59' }} />编辑状态
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { router.push('/licenses'); setActionMenuId(null) }}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '10px 16px', border: 'none', backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.85)', textAlign: 'left' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#F5F5F5')}
|
||||
onMouseLeave={e => (e.currentTarget.style.backgroundColor = '#fff')}
|
||||
>
|
||||
<Key size={14} style={{ color: '#4a7c59' }} />授权项管理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { router.push('/config-files'); setActionMenuId(null) }}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '10px 16px', border: 'none', backgroundColor: '#fff', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.85)', textAlign: 'left' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#F5F5F5')}
|
||||
onMouseLeave={e => (e.currentTarget.style.backgroundColor = '#fff')}
|
||||
>
|
||||
<FileCode size={14} style={{ color: '#4a7c59' }} />配置管理
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -256,10 +244,6 @@ export default function ModelsPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Click outside to close action menu */}
|
||||
{actionMenuId !== null && (
|
||||
<div onClick={() => setActionMenuId(null)} style={{ position: 'fixed', inset: 0, zIndex: 5 }} />
|
||||
)}
|
||||
|
||||
{/* New Model Drawer */}
|
||||
{modelDrawer && (
|
||||
|
|
|
|||
|
|
@ -22,18 +22,10 @@ const deviceStatusData = [
|
|||
]
|
||||
|
||||
const taskGroups = [
|
||||
{ title: '校准即将到期', count: 23, link: '/calibration', tasks: [
|
||||
{ deviceSN: 'AC20240308005', description: '采集板校准即将到期', time: '3天后到期', link: '/calibration' },
|
||||
{ deviceSN: 'AC20240308006', description: '采集板校准即将到期', time: '今天到期', link: '/calibration' },
|
||||
]},
|
||||
{ title: '维修工单', count: 5, link: '/repair', tasks: [
|
||||
{ deviceSN: 'GD30-2024-000056', description: '板卡故障,待处理', time: '4小时前', link: '/repair/WO-2024-0001' },
|
||||
{ deviceSN: 'GD30-2024-000078', description: '固件异常', time: '6小时前', link: '/repair/WO-2024-0002' },
|
||||
]},
|
||||
{ title: '固件升级通知', count: 8, link: '/firmware', tasks: [
|
||||
{ deviceSN: 'GD-30 Supreme', description: '固件版本v2.3.5可用', time: '1天前', link: '/firmware' },
|
||||
{ deviceSN: 'GD-10 Supreme', description: '固件版本v2.3.5可用', time: '1天前', link: '/firmware' },
|
||||
]},
|
||||
{ title: '授权即将到期', count: 45, link: '/licenses', tasks: [
|
||||
{ deviceSN: 'GD30-2025-000001', description: '授权将于30天后到期', time: '30天', link: '/licenses' },
|
||||
{ deviceSN: 'GT20-2025-000045', description: '授权将于15天后到期', time: '15天', link: '/licenses' },
|
||||
|
|
|
|||
Loading…
Reference in New Issue