修改和调整页面
This commit is contained in:
parent
184c0d092b
commit
d66ba233ad
|
|
@ -0,0 +1 @@
|
|||
{"specId": "6761b341-a126-48c6-b6af-57b888694c48", "workflowType": "requirements-first", "specType": "feature"}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
# 需求文档:前后端分离 — Java API 后端与 PostgreSQL 数据库
|
||||
|
||||
## 简介
|
||||
|
||||
本项目旨在将现有的 Next.js 前端生产管理子系统进行前后端分离改造。当前前端所有页面(设备列表、板卡管理、校准管理、固件库、授权管理、配置文件管理、维修工单、报废管理、设备登记、设备型号管理)均使用硬编码的模拟数据。需要搭建 Java Spring Boot 3.3.6 后端 API 服务和 PostgreSQL 数据库,用真实的 RESTful API 接口和持久化数据替换前端模拟数据。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **API_Server**:基于 Spring Boot 3.3.6 的 Java 后端 API 服务,项目路径 `apps/geo-bps-api/`
|
||||
- **Database**:PostgreSQL 12.14 数据库实例,按模块使用不同 schema 隔离
|
||||
- **Frontend**:现有 Next.js 前端应用,位于 `src/app/` 目录
|
||||
- **Device_Module**:设备管理业务模块,包名 `com.geomative.bps.device`,数据库 schema `dev`
|
||||
- **Common_Module**:公共模块,包名 `com.geomative.bps.common`,提供统一响应体、全局异常处理、工具类
|
||||
- **API_Admin**:后台管理入口模块,提供需登录鉴权的 API 端点
|
||||
- **DDD**:领域驱动设计分层架构(interfaces / application / domain / infrastructure)
|
||||
- **DO**:数据库映射对象(Data Object)
|
||||
- **VO**:视图对象(View Object),用于 API 响应
|
||||
- **Query**:查询参数对象
|
||||
- **Command**:命令对象,用于创建/更新操作
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:后端项目骨架搭建
|
||||
|
||||
**用户故事:** 作为开发者,我希望搭建一个符合技术规范的 Java Spring Boot 多模块 Maven 项目骨架,以便后续各业务模块可以在此基础上开发。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE API_Server SHALL 采用多模块 Maven 项目结构,父 POM 统一管理 Spring Boot 3.3.6、MyBatis-Plus、Lombok 等依赖版本
|
||||
2. THE API_Server SHALL 包含以下子模块:api-admin、api-portal、business(含 device 子模块)、common
|
||||
3. THE Common_Module SHALL 提供统一 JSON 响应格式,包含 code(整数)、message(字符串)、data(泛型)三个字段
|
||||
4. THE Common_Module SHALL 提供全局异常处理器,捕获业务异常和参数校验异常并返回统一格式的错误响应
|
||||
5. THE Common_Module SHALL 提供 MyBatis-Plus 的 MetaObjectHandler 实现,自动填充 created_at 和 updated_at 审计字段
|
||||
6. THE API_Admin SHALL 配置 CORS 策略,允许前端开发服务器(localhost:3000)的跨域请求
|
||||
7. THE API_Server SHALL 在 application.yml 中配置 PostgreSQL 数据源连接和 MyBatis-Plus 逻辑删除策略
|
||||
|
||||
### 需求 2:数据库 Schema 与表结构设计
|
||||
|
||||
**用户故事:** 作为开发者,我希望设计并创建 PostgreSQL 数据库表结构,以便持久化存储各业务模块的数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 审计字段
|
||||
|
||||
### 需求 3:设备管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 获取和管理设备数据,以便替换设备列表页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 返回所有生产批次列表及每个批次的设备数量
|
||||
5. IF 请求参数中的 SN 号已存在,THEN THE API_Server SHALL 返回 409 冲突错误码和描述性错误消息
|
||||
|
||||
### 需求 4:设备型号管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备型号和装配 Checklist 模板,以便替换型号管理页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 冲突错误码
|
||||
|
||||
### 需求 5:板卡型号管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理板卡型号数据,以便替换板卡管理页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 创建新板卡型号记录
|
||||
|
||||
### 需求 6:固件库管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理固件版本数据,以便替换固件库页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 冲突错误码
|
||||
|
||||
### 需求 7:校准管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理采集板校准数据,以便替换校准管理页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 支持批量导入校准记录数据
|
||||
|
||||
### 需求 8:配置文件管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备配置文件,以便替换配置文件管理页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 创建新配置文件记录
|
||||
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
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理设备授权数据,以便替换授权管理页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 前端请求 GET `/api/admin/licenses` 时,THE API_Server SHALL 返回分页的授权列表,支持按设备型号和状态筛选
|
||||
2. WHEN 前端请求 POST `/api/admin/licenses` 时,THE API_Server SHALL 创建新授权记录,包含设备型号、授权模块列表、授权期限
|
||||
3. WHEN 前端请求 PUT `/api/admin/licenses/{id}` 时,THE API_Server SHALL 更新指定授权记录
|
||||
4. WHEN 前端请求 PUT `/api/admin/licenses/{id}/disable` 时,THE API_Server SHALL 将指定授权记录状态设置为已停用
|
||||
|
||||
### 需求 10:维修工单管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理维修工单数据,以便替换维修工单页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 更新工单处理信息(处理操作、板卡更换、授权处理、处理备注)
|
||||
5. WHEN 前端请求 PUT `/api/admin/repair-orders/{id}/close` 时,THE API_Server SHALL 关闭工单并将状态设置为已处理
|
||||
|
||||
### 需求 11:报废管理 API
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望通过 API 管理报废审批和物料回收数据,以便替换报废管理页面的模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
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 审批通过报废申请,更新状态为已审批
|
||||
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 完成物料回收入库,更新状态为已回收,记录回收的物料清单
|
||||
6. THE API_Server SHALL 提供 GET `/api/admin/scrap-records/stats` 接口,返回报废统计数据(报废总数、待审批数、已审批待回收数、已回收数)
|
||||
|
||||
### 需求 12:首页 Dashboard 统计 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 返回待处理任务列表,包含校准即将到期、维修工单、固件升级通知、授权即将到期四个分组
|
||||
|
||||
### 需求 13:前端 API 集成层
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望在前端建立统一的 API 调用层,以便各页面组件可以方便地调用后端接口替换模拟数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Frontend SHALL 创建统一的 API 客户端模块,配置后端 API 基础 URL、请求超时时间(5秒)、统一错误处理
|
||||
2. THE Frontend SHALL 为每个业务模块创建独立的 API 服务文件(如 deviceApi.ts、boardApi.ts、firmwareApi.ts 等)
|
||||
3. WHEN API 请求失败时,THE Frontend SHALL 在页面上展示友好的错误提示信息
|
||||
4. WHEN API 请求正在进行时,THE Frontend SHALL 展示加载状态指示器
|
||||
5. THE Frontend SHALL 将各页面组件中的硬编码模拟数据替换为 API 调用,使用 React 的 useState 和 useEffect 管理数据获取状态
|
||||
|
||||
### 需求 14:分页与筛选标准化
|
||||
|
||||
**用户故事:** 作为前端开发者,我希望前后端采用统一的分页和筛选参数规范,以便各列表页面的数据交互保持一致。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE API_Server SHALL 对所有列表接口采用统一的分页参数格式:page(页码,从1开始)、pageSize(每页条数,默认10)
|
||||
2. THE API_Server SHALL 对所有列表接口返回统一的分页响应格式,包含 total(总记录数)、page(当前页码)、pageSize(每页条数)、records(数据列表)
|
||||
3. THE API_Server SHALL 对所有筛选参数进行服务端校验,无效参数返回 400 错误码和描述性错误消息
|
||||
4. WHEN 筛选条件为空或为"全部"时,THE API_Server SHALL 返回不带该条件过滤的完整数据集
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# Java API 技术规范
|
||||
|
||||
## 运行环境
|
||||
- JDK 17
|
||||
|
||||
## 框架选型
|
||||
- Spring Boot 3.3.6 (spring-boot-starter-parent)
|
||||
- ORM:MyBatis-Plus
|
||||
- Lombok
|
||||
|
||||
## 架构约束
|
||||
- API 采用多模块 Maven 项目结构,父项目统一管理依赖版本
|
||||
- 项目名: geo-bps
|
||||
- 包名前缀:com.geomative.bps.*
|
||||
- 子模块划分:
|
||||
- `api-admin` 后台管理入口模块:需登录鉴权,引入 `website`、`device`、`library`、`permission` 等模块
|
||||
- `api-portal` 前端门户入口模块:无需登录,仅引入对外开放的业务模块
|
||||
- `device` 设备管理模块
|
||||
- `website` 网站管理后台
|
||||
- `library` 资料库模块
|
||||
- `permission` 权限管理模块
|
||||
- `common` 公共模块:工具类、基础配置、统一响应格式、全局异常处理等共享代码
|
||||
|
||||
- 项目路径:`apps/geo-bps-api/`
|
||||
- groupId:`com.geomative.bsp`
|
||||
- artifactId:`geo-bps-api`
|
||||
- 项目整体结构:
|
||||
```
|
||||
apps/geo-bps-api/
|
||||
├── pom.xml # 父 POM,统一依赖版本管理
|
||||
├── api-admin/ # 后台管理入口,Spring Security 鉴权配置
|
||||
├── api-portal/ # 前端门户入口,公开接口无需授权
|
||||
├── business/ # 业务模块聚合器(pom packaging)
|
||||
│ ├── pom.xml
|
||||
│ ├── permission/ # 权限管理模块
|
||||
│ ├── website/ # 网站业务模块(被两个入口共享)
|
||||
│ ├── device/ # 设备管理模块
|
||||
│ └── library/ # 资料库模块
|
||||
└── common/ # 公共工具、基础配置、统一响应体
|
||||
```
|
||||
|
||||
## 编码规范
|
||||
|
||||
### 命名规范
|
||||
- 类名使用 UpperCamelCase,如 `DeviceService`、`UserDO`
|
||||
- 方法名、变量名使用 lowerCamelCase,如 `getUserById`、`deviceList`
|
||||
- 常量全部大写,单词间用下划线,如 `MAX_RETRY_COUNT`
|
||||
- 包名全部小写,如 `com.example.device.domain`
|
||||
- 抽象类命名以 `Abstract` 开头;异常类命名以 `Exception` 结尾;测试类命名以被测类名开头、`Test` 结尾
|
||||
- POJO 类中布尔类型变量不加 `is` 前缀,避免序列化问题
|
||||
- 领域对象命名后缀约定:
|
||||
- 数据库映射对象:`XxxDO`
|
||||
- 数据传输对象:`XxxDTO`
|
||||
- 视图对象:`XxxVO`
|
||||
- 查询对象:`XxxQuery`
|
||||
- 命令对象:`XxxCommand`
|
||||
|
||||
### 注释规范
|
||||
- 所有 public 类、方法必须有 Javadoc 注释
|
||||
- 方法注释需说明功能、参数含义、返回值、可能抛出的异常
|
||||
- 代码逻辑复杂处需有行内注释说明意图,而非描述代码本身
|
||||
- 禁止保留无意义的注释和注释掉的废弃代码
|
||||
|
||||
### 异常处理
|
||||
- 不允许捕获 `Exception` 等大类异常后不做任何处理(空 catch)
|
||||
- 业务异常统一使用自定义异常类,继承 `RuntimeException`
|
||||
- 不允许用异常控制正常业务流程
|
||||
- finally 块中不允许使用 return
|
||||
|
||||
### 日志规范
|
||||
- 使用 SLF4J + Logback,禁止直接使用 `System.out.println`
|
||||
- 日志变量声明:`private static final Logger log = LoggerFactory.getLogger(XxxClass.class);`
|
||||
- 日志输出使用占位符,禁止字符串拼接:`log.info("userId: {}", userId)`
|
||||
- 异常日志必须输出完整堆栈:`log.error("message", e)`
|
||||
|
||||
### 集合与并发
|
||||
- 初始化集合时指定初始容量,如 `new ArrayList<>(16)`
|
||||
- 禁止在 foreach 循环中对集合进行 add/remove 操作
|
||||
- 线程池不允许使用 `Executors` 创建,需通过 `ThreadPoolExecutor` 显式配置参数
|
||||
|
||||
### 其他
|
||||
- 所有方法入参需做非空校验,使用 `Objects.requireNonNull` 或断言
|
||||
- 魔法值(Magic Number)禁止直接出现在代码中,需定义为常量
|
||||
- 返回值为集合类型时,不允许返回 null,应返回空集合
|
||||
|
||||
### DDD 分层架构
|
||||
- 采用领域驱动设计(DDD)分层架构,每个子模块内部结构如下:
|
||||
|
||||
```
|
||||
{module}/
|
||||
├── interfaces/ # 接口层:Controller、DTO、Assembler
|
||||
├── application/ # 应用层:ApplicationService、Command、Query
|
||||
├── domain/ # 领域层:Entity、ValueObject、DomainService、Repository接口、DomainEvent
|
||||
└── infrastructure/ # 基础设施层:Repository实现、Mapper、持久化对象(PO)、外部服务适配
|
||||
```
|
||||
|
||||
## 数据库
|
||||
- PostgreSQL 12.14 (Debian 12.14-1.pgdg110+1)
|
||||
|
||||
### 审计字段规范
|
||||
- 所有业务表必须包含以下审计字段:
|
||||
- `created_at` TIMESTAMP NOT NULL — 创建时间,INSERT 时自动填充
|
||||
- `created_by` VARCHAR(64) NULL — 创建人(用户ID或用户名),字符类型
|
||||
- `updated_at` TIMESTAMP NOT NULL — 最后修改时间,INSERT 和 UPDATE 时自动填充
|
||||
- `updated_by` VARCHAR(64) NULL — 最后修改人(用户ID或用户名),字符类型
|
||||
- `deleted` TINYINT NOT NULL DEFAULT 0 — 删除状态,0=未删除,1=已删除
|
||||
|
||||
### 主键规范
|
||||
- 所有业务表主键类型为 VARCHAR(64),由应用层生成(如雪花算法、UUID 字符串等),不使用数据库自增或 UUID 类型
|
||||
|
||||
### PostgreSQL Schema 分模块
|
||||
- 不同子模块的表使用不同的 PostgreSQL schema 进行隔离:
|
||||
- `perm` — permission 模块(admin_users、revoked_tokens、admin_menus、admin_roles 等)
|
||||
- `web` — website 模块(nav_menus、branches、sub_branches、pages、page_contents、page_seo、fixed_page_contents、enabled_locales 等)
|
||||
- `dev` — device 模块(quotations、access_logs、configurator_steps 等)
|
||||
- `lib` — library 模块(knowledge_articles 等)
|
||||
- `common` — 公共表(contact_submissions、system_config 等)
|
||||
- MyBatis-Plus 通过 `@TableName(schema = "xxx")` 或全局配置指定 schema
|
||||
- MyBatis-Plus 配置:
|
||||
- 通过 `MetaObjectHandler` 自动填充 `created_at`、`updated_at`
|
||||
- 通过 `@TableLogic` 注解标记 `deleted` 字段实现逻辑删除
|
||||
- 查询默认过滤 `deleted=1` 的记录
|
||||
|
||||
## 部署
|
||||
- 集成 Nginx 作为前置代理
|
||||
- `api-admin`:Nginx 直接流量穿透,转发至单实例后台管理服务
|
||||
- `api-portal`:Nginx 4层负载均衡(TCP/stream模块),支持多实例横向扩展
|
||||
|
|
@ -19,3 +19,215 @@
|
|||
{"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"}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
"/_not-found/page": "app/_not-found/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",
|
||||
"/firmware/page": "app/firmware/page.js",
|
||||
"/licenses/page": "app/licenses/page.js",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
154
.next/dev/trace
154
.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" | "/config-files" | "/devices" | "/firmware" | "/licenses" | "/models" | "/registration" | "/repair" | "/scrap"
|
||||
type AppRoutes = "/" | "/boards" | "/calibration" | "/calibration/register" | "/config-files" | "/devices" | "/devices/[sn]" | "/firmware" | "/licenses" | "/models" | "/registration" | "/repair" | "/scrap"
|
||||
type PageRoutes = never
|
||||
type LayoutRoutes = "/"
|
||||
type RedirectRoutes = never
|
||||
|
|
@ -13,8 +13,10 @@ interface ParamMap {
|
|||
"/": {}
|
||||
"/boards": {}
|
||||
"/calibration": {}
|
||||
"/calibration/register": {}
|
||||
"/config-files": {}
|
||||
"/devices": {}
|
||||
"/devices/[sn]": { "sn": string; }
|
||||
"/firmware": {}
|
||||
"/licenses": {}
|
||||
"/models": {}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
|
|||
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
|
||||
|
|
@ -63,6 +72,15 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
|
|||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/devices/[sn]/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/devices/[sn]">> = Specific
|
||||
const handler = {} as typeof import("../../../src/app/devices/[sn]/page.js")
|
||||
type __Check = __IsExpected<typeof handler>
|
||||
// @ts-ignore
|
||||
type __Unused = __Check
|
||||
}
|
||||
|
||||
// Validate ../../../src/app/devices/page.tsx
|
||||
{
|
||||
type __IsExpected<Specific extends AppPageConfig<"/devices">> = Specific
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { Download, Plus, X, Eye, Upload, Clock, Wrench, CheckCircle, ChevronRigh
|
|||
const tabs = ['全部', '主协板', '采集板', '发射板', '升压板']
|
||||
|
||||
const boardsData = [
|
||||
{ id: 1, type: '主协板', model: 'MCB-3000', firmwareVersion: 'v2.1.0', productionDate: '2024-03-15', status: '在产' },
|
||||
{ id: 2, type: '主协板', model: 'MCB-2000', firmwareVersion: 'v1.8.5', productionDate: '2023-11-20', status: '在产' },
|
||||
{ id: 3, type: '采集板', model: 'ACB-6000', firmwareVersion: 'v3.0.2', productionDate: '2024-01-10', status: '在产' },
|
||||
{ id: 4, type: '采集板', model: 'ACB-5000', firmwareVersion: 'v2.5.1', productionDate: '2023-09-05', status: '停产' },
|
||||
{ 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: '在产' },
|
||||
|
|
|
|||
|
|
@ -1,104 +1,136 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Info, Search, Download, ChevronLeft, ChevronRight, X, Eye, FileDown } from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Search, ChevronLeft, ChevronRight, Plus, Download, Info, Eye, X, Tag } from 'lucide-react'
|
||||
|
||||
const calibrationData = [
|
||||
{ id: 1, sn: 'RX20240308001', boardModel: 'ACB-6000', calibDate: '2024-03-01', expiryDate: '2025-03-01', operator: '王工程师', status: '合格', channel: 6, deviation: '±0.05%' },
|
||||
{ id: 2, sn: 'RX20240308002', boardModel: 'ACB-6000', calibDate: '2024-02-15', expiryDate: '2025-02-15', operator: '张工程师', status: '合格', channel: 6, deviation: '±0.03%' },
|
||||
{ id: 3, sn: 'RX20240115003', boardModel: 'ACB-5000', calibDate: '2024-01-15', expiryDate: '2025-01-15', operator: '王工程师', status: '合格', channel: 4, deviation: '±0.08%' },
|
||||
{ id: 4, sn: 'RX20240420004', boardModel: 'ACB-6000', calibDate: '2024-04-20', expiryDate: '2025-04-20', operator: '李工程师', status: '待校准', channel: 6, deviation: '-' },
|
||||
{ id: 5, sn: 'RX20231205005', boardModel: 'ACB-5000', calibDate: '2023-12-05', expiryDate: '2024-12-05', operator: '王工程师', status: '不合格', channel: 4, deviation: '±0.25%' },
|
||||
{ id: 6, sn: 'RX20240610006', boardModel: 'ACB-6000', calibDate: '2024-06-10', expiryDate: '2025-06-10', operator: '张工程师', status: '合格', channel: 6, deviation: '±0.04%' },
|
||||
{ id: 7, sn: 'RX20240801007', boardModel: 'ACB-6000', calibDate: '2024-08-01', expiryDate: '2025-08-01', operator: '李工程师', status: '待校准', channel: 6, deviation: '-' },
|
||||
{ id: 8, sn: 'RX20240305008', boardModel: 'ACB-5000', calibDate: '2024-03-05', expiryDate: '2025-03-05', operator: '王工程师', status: '合格', channel: 4, deviation: '±0.06%' },
|
||||
{ id: 9, sn: 'RX20240922009', boardModel: 'ACB-6000', calibDate: '2024-09-22', expiryDate: '2025-09-22', operator: '张工程师', status: '合格', channel: 6, deviation: '±0.02%' },
|
||||
{ id: 10, sn: 'RX20231110010', boardModel: 'ACB-5000', calibDate: '2023-11-10', expiryDate: '2024-11-10', operator: '李工程师', status: '不合格', channel: 4, deviation: '±0.30%' },
|
||||
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: '-'},
|
||||
]
|
||||
|
||||
const operatorOptions = ['全部', '王工程师', '张工程师', '李工程师']
|
||||
const statusOptions = ['全部', '合格', '不合格', '待校准']
|
||||
|
||||
const calibDetailData = {
|
||||
basicInfo: { sn: 'RX20240308001', model: 'ACB-6000', channels: 6, productionDate: '2024-01-10' },
|
||||
calibResults: [
|
||||
{ channel: 'CH1', reference: '1000.00Ω', measured: '999.95Ω', deviation: '+0.005%', result: '合格' },
|
||||
{ channel: 'CH2', reference: '1000.00Ω', measured: '1000.03Ω', deviation: '-0.003%', result: '合格' },
|
||||
{ channel: 'CH3', reference: '1000.00Ω', measured: '999.98Ω', deviation: '+0.002%', result: '合格' },
|
||||
{ channel: 'CH4', reference: '1000.00Ω', measured: '1000.08Ω', deviation: '-0.008%', result: '合格' },
|
||||
{ channel: 'CH5', reference: '1000.00Ω', measured: '999.92Ω', deviation: '+0.008%', result: '合格' },
|
||||
{ channel: 'CH6', reference: '1000.00Ω', measured: '1000.05Ω', deviation: '-0.005%', result: '合格' },
|
||||
],
|
||||
}
|
||||
const typeOptions = ['全部', '主协板', '采集板', '发射板', '升压板']
|
||||
const statusOptions = ['全部', '在库', '已装配', '故障', '报废']
|
||||
const calibStatusOptions = ['全部', '合格', '不合格', '待校准']
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
case '合格': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
case '不合格': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }
|
||||
case '待校准': return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }
|
||||
case '在库': return { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }
|
||||
case '已装配': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
case '故障': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }
|
||||
case '报废': return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
default: return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
}
|
||||
}
|
||||
|
||||
export default function CalibrationPage() {
|
||||
const [filterSN, setFilterSN] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState('全部')
|
||||
const [filterOperator, setFilterOperator] = useState('全部')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [detailDrawer, setDetailDrawer] = useState<typeof calibrationData[0] | null>(null)
|
||||
const pageSize = 6
|
||||
function getCalibStyle(status: string) {
|
||||
switch (status) {
|
||||
case '合格': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
case '不合格': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }
|
||||
case '待校准': return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = calibrationData.filter(r => {
|
||||
if (filterSN && !r.sn.toLowerCase().includes(filterSN.toLowerCase())) return false
|
||||
if (filterStatus !== '全部' && r.status !== filterStatus) return false
|
||||
if (filterOperator !== '全部' && r.operator !== filterOperator) return false
|
||||
export default function BoardCardsPage() {
|
||||
const [filterType, setFilterType] = useState('全部')
|
||||
const [filterStatus, setFilterStatus] = useState('全部')
|
||||
const [filterCalib, setFilterCalib] = useState('全部')
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [detailDrawer, setDetailDrawer] = useState<typeof boardCardsData[0] | null>(null)
|
||||
const pageSize = 8
|
||||
|
||||
const filtered = useMemo(() => boardCardsData.filter(b => {
|
||||
if (filterType !== '全部' && b.type !== filterType) return false
|
||||
if (filterStatus !== '全部' && b.status !== filterStatus) return false
|
||||
if (filterCalib !== '全部' && b.calibStatus !== filterCalib) return false
|
||||
if (searchText && !b.sn.toLowerCase().includes(searchText.toLowerCase()) && !b.deviceSn.toLowerCase().includes(searchText.toLowerCase())) return false
|
||||
return true
|
||||
})
|
||||
}), [filterType, filterStatus, filterCalib, searchText])
|
||||
|
||||
const totalPages = Math.ceil(filtered.length / pageSize)
|
||||
const paged = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
||||
|
||||
// 统计
|
||||
const stats = useMemo(() => ({
|
||||
total: boardCardsData.length,
|
||||
inStock: boardCardsData.filter(b => b.status === '在库').length,
|
||||
assembled: boardCardsData.filter(b => b.status === '已装配').length,
|
||||
faulty: boardCardsData.filter(b => b.status === '故障').length,
|
||||
pendingCalib: boardCardsData.filter(b => b.calibStatus === '待校准').length,
|
||||
}), [])
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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 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' }}>
|
||||
<Plus size={16} />登记板卡
|
||||
</Link>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, backgroundColor: '#F9F0FF', borderRadius: 8, marginBottom: 24, border: '1px solid #D3ADF7' }}>
|
||||
<Info size={18} style={{ color: '#722ED1', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ fontSize: 14, color: '#722ED1', lineHeight: 1.6 }}>
|
||||
校准管理仅针对采集板(ACB系列),其他类型板卡无需校准。校准到期前30天系统会自动提醒。
|
||||
</div>
|
||||
{/* Stats Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
{[
|
||||
{ label: '板卡总数', value: stats.total, color: '#4a7c59', bg: '#eef5f0' },
|
||||
{ label: '在库', value: stats.inStock, color: '#1890FF', bg: '#E6F7FF' },
|
||||
{ label: '已装配', value: stats.assembled, color: '#52C41A', bg: '#F6FFED' },
|
||||
{ label: '故障', value: stats.faulty, color: '#FF4D4F', bg: '#FFF1F0' },
|
||||
{ label: '待校准', value: stats.pendingCalib, color: '#FAAD14', bg: '#FFFBE6' },
|
||||
].map(s => (
|
||||
<div key={s.label} style={{ backgroundColor: '#fff', borderRadius: 8, padding: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>{s.label}</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 600, color: s.color }}>{s.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>采集板SN号</label>
|
||||
<input type="text" value={filterSN} onChange={e => { setFilterSN(e.target.value); setCurrentPage(1) }} placeholder="输入SN号搜索" style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>板卡类型</label>
|
||||
<select value={filterType} onChange={e => { setFilterType(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{typeOptions.map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>校准状态</label>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>板卡状态</label>
|
||||
<select value={filterStatus} onChange={e => { setFilterStatus(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{statusOptions.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>校准人员</label>
|
||||
<select value={filterOperator} onChange={e => { setFilterOperator(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{operatorOptions.map(o => <option key={o} value={o}>{o}</option>)}
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>校准状态</label>
|
||||
<select value={filterCalib} onChange={e => { setFilterCalib(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{calibStatusOptions.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={() => setCurrentPage(1)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14, flexShrink: 0 }}>
|
||||
<Search size={14} />查询
|
||||
</button>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>SN / 设备SN</label>
|
||||
<input type="text" value={searchText} onChange={e => { setSearchText(e.target.value); setCurrentPage(1) }} placeholder="搜索板卡SN或设备SN" style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -107,31 +139,33 @@ export default function CalibrationPage() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['采集板SN号', '板卡型号', '校准日期', '到期日期', '校准人员', '状态', '操作'].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>
|
||||
{['板卡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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paged.map(row => (
|
||||
<tr key={row.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, fontWeight: 500 }}>{row.sn}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.boardModel}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.calibDate}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.expiryDate}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.operator}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<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.firmware}</td>
|
||||
<td style={{ padding: '12px 14px' }}>
|
||||
<span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={() => setDetailDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Eye size={14} />详情
|
||||
</button>
|
||||
<button style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<FileDown size={14} />校准文件
|
||||
</button>
|
||||
</div>
|
||||
<td style={{ padding: '12px 14px', fontSize: 13, color: row.deviceSn === '-' ? 'rgba(0,0,0,0.25)' : '#4a7c59', fontWeight: row.deviceSn === '-' ? 400 : 500 }}>{row.deviceSn}</td>
|
||||
<td style={{ padding: '12px 14px' }}>
|
||||
{row.calibStatus !== '-' ? (
|
||||
<span style={{ ...getCalibStyle(row.calibStatus), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.calibStatus}</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||||
)}
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -140,13 +174,15 @@ export default function CalibrationPage() {
|
|||
|
||||
{/* Pagination */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderTop: '1px solid #F0F0F0' }}>
|
||||
<span style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>显示 {(currentPage - 1) * pageSize + 1}-{Math.min(currentPage * pageSize, filtered.length)} / 共 {filtered.length} 条</span>
|
||||
<span style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>
|
||||
显示 {filtered.length > 0 ? (currentPage - 1) * pageSize + 1 : 0}-{Math.min(currentPage * pageSize, filtered.length)} / 共 {filtered.length} 条
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} style={{ padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', opacity: currentPage === 1 ? 0.4 : 1 }}><ChevronLeft size={16} /></button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<button key={i} onClick={() => setCurrentPage(i + 1)} style={{ width: 32, height: 32, borderRadius: 4, fontSize: 14, border: currentPage === i + 1 ? '1px solid #4a7c59' : '1px solid #D9D9D9', color: currentPage === i + 1 ? '#4a7c59' : 'rgba(0,0,0,0.65)', backgroundColor: currentPage === i + 1 ? '#eef5f0' : '#fff', cursor: 'pointer' }}>{i + 1}</button>
|
||||
))}
|
||||
<button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} style={{ padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: currentPage === totalPages ? 'not-allowed' : 'pointer', opacity: currentPage === totalPages ? 0.4 : 1 }}><ChevronRight size={16} /></button>
|
||||
<button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages || totalPages === 0} style={{ padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: currentPage === totalPages || totalPages === 0 ? 'not-allowed' : 'pointer', opacity: currentPage === totalPages || totalPages === 0 ? 0.4 : 1 }}><ChevronRight size={16} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,58 +191,60 @@ export default function CalibrationPage() {
|
|||
{detailDrawer && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<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={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 520, 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.sn}</h3>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>板卡详情</h3>
|
||||
<button onClick={() => setDetailDrawer(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12 }}>基本信息</h4>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}>基本信息</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>采集板SN:</span>{detailDrawer.sn}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>板卡型号:</span>{detailDrawer.boardModel}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>通道数:</span>{detailDrawer.channel}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>综合偏差:</span>{detailDrawer.deviation}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>校准日期:</span>{detailDrawer.calibDate}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>到期日期:</span>{detailDrawer.expiryDate}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>校准人员:</span>{detailDrawer.operator}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>状态:</span><span style={{ ...getStatusStyle(detailDrawer.status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.status}</span></div>
|
||||
<div><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.firmware}</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>生产日期:</span>{detailDrawer.productionDate}</div>
|
||||
<div>
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)' }}>状态:</span>
|
||||
<span style={{ ...getStatusStyle(detailDrawer.status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 通道校准结果 */}
|
||||
{/* 装配信息 */}
|
||||
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12 }}>通道校准结果</h4>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{['通道', '参考值', '测量值', '偏差', '结果'].map(h => (
|
||||
<th key={h} style={{ padding: '8px 10px', textAlign: 'left', fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.65)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{calibDetailData.calibResults.map((r, i) => (
|
||||
<tr key={i}>
|
||||
<td style={{ padding: '8px 10px', fontSize: 13 }}>{r.channel}</td>
|
||||
<td style={{ padding: '8px 10px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{r.reference}</td>
|
||||
<td style={{ padding: '8px 10px', fontSize: 13 }}>{r.measured}</td>
|
||||
<td style={{ padding: '8px 10px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{r.deviation}</td>
|
||||
<td style={{ padding: '8px 10px' }}>
|
||||
<span style={{ ...getStatusStyle(r.result), padding: '1px 6px', borderRadius: 4, fontSize: 11 }}>{r.result}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}>装配信息</h4>
|
||||
<div style={{ fontSize: 13 }}>
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)' }}>所属设备:</span>
|
||||
{detailDrawer.deviceSn === '-' ? (
|
||||
<span style={{ color: 'rgba(0,0,0,0.25)' }}>未装配</span>
|
||||
) : (
|
||||
<span style={{ color: '#4a7c59', fontWeight: 500 }}>{detailDrawer.deviceSn}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 校准信息 */}
|
||||
{(detailDrawer.type === '采集板') && (
|
||||
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}>校准信息</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
|
||||
<div>
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)' }}>校准状态:</span>
|
||||
{detailDrawer.calibStatus !== '-' ? (
|
||||
<span style={{ ...getCalibStyle(detailDrawer.calibStatus), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.calibStatus}</span>
|
||||
) : (
|
||||
<span style={{ color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||||
)}
|
||||
</div>
|
||||
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>校准日期:</span>{detailDrawer.calibDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>
|
||||
<FileDown size={14} />下载校准文件
|
||||
</button>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button onClick={() => setDetailDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Plus, Trash2, Upload, Info, CheckCircle } from 'lucide-react'
|
||||
|
||||
/** 板卡类型 -> 可选型号 */
|
||||
const modelsByType: Record<string, { model: string; firmware: string }[]> = {
|
||||
'主协板': [
|
||||
{ model: 'MB-V1.2', firmware: 'v2.1' },
|
||||
{ model: 'MB-V2.1', firmware: 'v1.8' },
|
||||
],
|
||||
'采集板': [
|
||||
{ model: 'RX-V1.3', firmware: 'v3.0' },
|
||||
{ model: 'RX-V2.1', firmware: 'v2.5' },
|
||||
],
|
||||
'发射板': [
|
||||
{ model: 'TX-V1.5', firmware: 'v1.2' },
|
||||
{ model: 'TX-V2.1', firmware: 'v1.0' },
|
||||
],
|
||||
'升压板': [
|
||||
{ model: 'BO-V2.1', firmware: 'v1.1' },
|
||||
{ model: 'BO-V2.2', firmware: 'v0.9' },
|
||||
],
|
||||
}
|
||||
|
||||
const typeOptions = Object.keys(modelsByType)
|
||||
|
||||
interface BoardEntry {
|
||||
id: number
|
||||
type: string
|
||||
model: string
|
||||
firmware: string
|
||||
sn: string
|
||||
productionDate: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
let nextId = 1
|
||||
|
||||
export default function BoardRegisterPage() {
|
||||
const router = useRouter()
|
||||
const [entries, setEntries] = useState<BoardEntry[]>([createEntry()])
|
||||
const [batchMode, setBatchMode] = useState(false)
|
||||
|
||||
function createEntry(): BoardEntry {
|
||||
return { id: nextId++, type: '采集板', model: 'RX-V2.1', firmware: 'v2.1', sn: '', productionDate: '', remark: '' }
|
||||
}
|
||||
|
||||
const addEntry = () => {
|
||||
setEntries(prev => [...prev, createEntry()])
|
||||
}
|
||||
|
||||
const removeEntry = (id: number) => {
|
||||
if (entries.length <= 1) return
|
||||
setEntries(prev => prev.filter(e => e.id !== id))
|
||||
}
|
||||
|
||||
const updateEntry = (id: number, field: keyof BoardEntry, value: string) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
// 切换型号时自动填充固件
|
||||
if (field === 'model') {
|
||||
const models = modelsByType[updated.type]
|
||||
const match = models?.find(m => m.model === value)
|
||||
if (match) updated.firmware = match.firmware
|
||||
}
|
||||
return updated
|
||||
}))
|
||||
}
|
||||
|
||||
const isValid = entries.every(e => e.sn.trim() && e.productionDate)
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<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' }}>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<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号为唯一标识,请确保录入正确。采集板登记后需要进行校准才能用于装配。选择型号后固件版本会自动填充。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode Toggle */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||||
<span style={{ fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>登记模式:</span>
|
||||
<div style={{ display: 'flex', gap: 0, borderRadius: 6, overflow: 'hidden', border: '1px solid #D9D9D9' }}>
|
||||
<button onClick={() => setBatchMode(false)} style={{ padding: '6px 16px', fontSize: 13, border: 'none', cursor: 'pointer', backgroundColor: !batchMode ? '#4a7c59' : '#fff', color: !batchMode ? '#fff' : 'rgba(0,0,0,0.65)' }}>单个登记</button>
|
||||
<button onClick={() => setBatchMode(true)} style={{ padding: '6px 16px', fontSize: 13, border: 'none', cursor: 'pointer', backgroundColor: batchMode ? '#4a7c59' : '#fff', color: batchMode ? '#fff' : 'rgba(0,0,0,0.65)', borderLeft: '1px solid #D9D9D9' }}>批量登记</button>
|
||||
</div>
|
||||
{batchMode && (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>当前 {entries.length} 条</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Entry Cards */}
|
||||
{entries.map((entry, idx) => (
|
||||
<div key={entry.id} style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', border: '1px solid #F0F0F0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: 0, color: 'rgba(0,0,0,0.85)' }}>
|
||||
{batchMode ? `板卡 #${idx + 1}` : '板卡信息'}
|
||||
</h3>
|
||||
{batchMode && entries.length > 1 && (
|
||||
<button onClick={() => removeEntry(entry.id)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px', border: '1px solid #FFCCC7', borderRadius: 6, backgroundColor: '#FFF1F0', color: '#FF4D4F', cursor: 'pointer', fontSize: 13 }}>
|
||||
<Trash2 size={13} />移除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
{/* 板卡类型 */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 板卡类型</label>
|
||||
<select value={entry.type} onChange={e => updateEntry(entry.id, 'type', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
{typeOptions.map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</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>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 固件版本(自动填充,只读) */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>固件版本</label>
|
||||
<input value={entry.firmware} readOnly style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.65)', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
{/* 板卡SN */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 板卡SN号</label>
|
||||
<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' }} />
|
||||
</div>
|
||||
|
||||
{/* 生产日期 */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 生产日期</label>
|
||||
<input type="date" value={entry.productionDate} onChange={e => updateEntry(entry.id, 'productionDate', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>备注</label>
|
||||
<input value={entry.remark} onChange={e => updateEntry(entry.id, 'remark', e.target.value)} placeholder="可选" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 采集板校准提示 */}
|
||||
{entry.type === '采集板' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 16, padding: '8px 12px', backgroundColor: '#FFFBE6', borderRadius: 6, border: '1px solid #FFE58F' }}>
|
||||
<Info size={14} style={{ color: '#FAAD14', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>采集板登记后状态为"待校准",需完成校准后才能用于设备装配。</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add More Button (batch mode) */}
|
||||
{batchMode && (
|
||||
<button onClick={addEntry} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, width: '100%', padding: '12px 0', border: '2px dashed #D9D9D9', borderRadius: 8, backgroundColor: '#FAFAFA', cursor: 'pointer', fontSize: 14, color: 'rgba(0,0,0,0.45)', marginBottom: 16 }}>
|
||||
<Plus size={16} />添加一条板卡
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Preview Summary */}
|
||||
{entries.length > 0 && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', border: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 16px', color: 'rgba(0,0,0,0.85)' }}>登记预览</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['序号', '类型', '型号', '固件', '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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry, i) => (
|
||||
<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, 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' }}>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, ...(entry.type === '采集板' ? { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' } : { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }) }}>
|
||||
{entry.type === '采集板' ? '待校准' : '在库'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sticky Bottom Bar */}
|
||||
<div style={{ position: 'sticky', bottom: 0, backgroundColor: '#fff', borderTop: '1px solid #F0F0F0', padding: '12px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', zIndex: 10 }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||||
共 {entries.length} 条板卡待登记
|
||||
{entries.some(e => e.type === '采集板') && <span style={{ color: '#FAAD14' }}> · 含采集板需后续校准</span>}
|
||||
</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('/calibration')}
|
||||
disabled={!isValid}
|
||||
style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: isValid ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: isValid ? 'pointer' : 'not-allowed', fontSize: 14 }}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<CheckCircle size={14} />确认登记
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Monitor, Settings2, Key, Cpu, FileCode, Gauge, Wrench, Recycle } from 'lucide-react'
|
||||
import { Monitor, Settings2, Cpu, Gauge, Wrench, Recycle } from 'lucide-react'
|
||||
|
||||
const menuGroups = [
|
||||
{ title: '设备', items: [
|
||||
{ path: '/devices', label: '设备列表', icon: Monitor },
|
||||
{ path: '/models', label: '设备型号管理', icon: Settings2 },
|
||||
{ path: '/boards', label: '板卡型号管理', icon: Cpu },
|
||||
]},
|
||||
{ title: '授权', items: [{ path: '/licenses', label: '授权管理', icon: Key }] },
|
||||
{ title: '配置', items: [{ path: '/config-files', label: '配置管理', icon: FileCode }] },
|
||||
{ title: '校准', items: [{ path: '/calibration', label: '校准记录', icon: Gauge }] },
|
||||
{ title: '板卡', items: [{ path: '/calibration', label: '板卡列表', icon: Gauge },
|
||||
{ path: '/boards', label: '板卡型号管理', icon: Cpu },] },
|
||||
{ title: '维修', items: [
|
||||
{ path: '/repair', label: '维修工单', icon: Wrench },
|
||||
{ path: '/scrap', label: '报废回收', icon: Recycle },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,372 @@
|
|||
'use client'
|
||||
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'
|
||||
|
||||
/** 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: '张工程师' },
|
||||
]
|
||||
|
||||
/** Mock: 装机BOM清单 */
|
||||
const bomData: Record<string, { name: string; sn: string; model: string; calibration: string }[]> = {
|
||||
'GD30-2025-000001': [
|
||||
{ name: '主协板', sn: 'MB25011501', model: 'MB-V2.1', calibration: '-' },
|
||||
{ name: '采集板', sn: 'RX25011000', model: 'RX-V2.1', calibration: '合格' },
|
||||
{ name: '采集板', sn: 'RX25011000', model: 'RX-V2.1', calibration: '合格' },
|
||||
{ name: '发射板', sn: 'TX25012000', model: 'TX-V2.1', calibration: '-' },
|
||||
{ name: '升压板', sn: 'BO25020100', model: 'BP600-V2.1', calibration: '-' },
|
||||
],
|
||||
}
|
||||
|
||||
/** Mock: 授权信息 */
|
||||
const licenseData: Record<string, { modules: string; expiry: string; status: string }> = {
|
||||
'GD30-2025-000001': { modules: '1D SP, 2D SP, 3D SP, 1D VES, 2D ERT, 3D ERT, 1D IP, 2D IP, 3D IP, 跨孔, 水上', expiry: '2026-01-15', status: '生效' },
|
||||
'GD30-2025-000002': { modules: '2D ERT, 3D ERT, 1D IP, 2D IP, 3D IP, 跨孔, 水上', expiry: '2025-12-31', status: '生效' },
|
||||
'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: 操作日志 */
|
||||
const operationLogs = [
|
||||
{ date: '2025-01-15 14:30', action: '设备登记', operator: '张工程师', detail: '完成设备登记,型号 GD-30 Supreme' },
|
||||
{ date: '2025-01-15 15:00', action: '装配完成', operator: '张工程师', detail: '装配 Checklist 全部通过' },
|
||||
{ date: '2025-01-15 16:00', action: '授权项写入', operator: '系统', detail: '写入全模块授权项' },
|
||||
{ date: '2025-01-15 16:05', action: '配置写入', operator: '系统', detail: '写入配置文件 CFG-GD30-v1.3.0' },
|
||||
{ date: '2025-01-15 16:10', action: '固件校验', operator: '系统', detail: '固件版本 v2.3.5 校验通过' },
|
||||
{ date: '2025-01-16 09:00', action: '出厂检测', operator: '李工程师', detail: '出厂检测通过,设备状态变更为已出厂' },
|
||||
]
|
||||
|
||||
/** 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 },
|
||||
]
|
||||
|
||||
function getStatusStyle(status: string) {
|
||||
switch (status) {
|
||||
case '已激活': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
|
||||
case '已出厂': return { backgroundColor: '#FFF7E6', color: '#FA8C16', border: '1px solid #FFD591' }
|
||||
case '装配中': return { backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' }
|
||||
default: return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string) {
|
||||
switch (status) {
|
||||
case '已激活': return <Wifi size={16} style={{ color: '#52C41A' }} />
|
||||
case '已出厂': return <Monitor size={16} style={{ color: '#FA8C16' }} />
|
||||
case '装配中': return <Cpu size={16} style={{ color: '#4a7c59' }} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
export default function DeviceDetailPage({ params }: { params: Promise<{ sn: string }> }) {
|
||||
const { sn } = use(params)
|
||||
const router = useRouter()
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
|
||||
const device = allDevices.find(d => d.sn === sn)
|
||||
const bom = bomData[sn] || []
|
||||
const license = licenseData[sn]
|
||||
const config = configData[sn]
|
||||
|
||||
if (!device) {
|
||||
return (
|
||||
<div style={{ padding: 24, textAlign: 'center', paddingTop: 120 }}>
|
||||
<AlertTriangle size={48} style={{ color: '#FAAD14', marginBottom: 16 }} />
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>设备未找到</h2>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', marginBottom: 24 }}>SN号 {sn} 对应的设备不存在</p>
|
||||
<button onClick={() => router.push('/devices')} style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>返回设备列表</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ key: 'overview', label: '概览' },
|
||||
{ key: 'bom', label: '装机清单' },
|
||||
{ key: 'checklist', label: '装配记录' },
|
||||
{ key: 'license', label: '授权项信息' },
|
||||
{ key: 'config', label: '配置文件' },
|
||||
{ key: 'logs', label: '操作日志' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||||
<button onClick={() => router.push('/devices')} 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 style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>{device.sn}</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{getStatusIcon(device.status)}
|
||||
<span style={{ ...getStatusStyle(device.status), padding: '2px 10px', borderRadius: 4, fontSize: 12 }}>{device.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}>{device.model} · {device.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Cards Row */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
<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 }}>
|
||||
<Package size={16} style={{ color: '#4a7c59' }} />
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>板卡数量</span>
|
||||
</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 */}
|
||||
<div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #F0F0F0', marginBottom: 24 }}>
|
||||
{tabs.map(tab => (
|
||||
<button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{
|
||||
padding: '10px 20px', fontSize: 14, cursor: 'pointer', border: 'none', backgroundColor: 'transparent',
|
||||
borderBottom: activeTab === tab.key ? '2px solid #4a7c59' : '2px solid transparent',
|
||||
color: activeTab === tab.key ? '#4a7c59' : 'rgba(0,0,0,0.65)', fontWeight: activeTab === tab.key ? 600 : 400,
|
||||
}}>{tab.label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<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 }}>
|
||||
{[
|
||||
{ label: '设备SN', value: device.sn },
|
||||
{ label: '设备型号', value: device.model },
|
||||
{ label: '设备类型', value: device.type },
|
||||
{ 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}>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 6 }}>{item.label}</div>
|
||||
{item.isStatus ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{getStatusIcon(item.value)}
|
||||
<span style={{ ...getStatusStyle(item.value), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{item.value}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: 14, color: item.value === '-' ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.85)', fontWeight: 500 }}>{item.value}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'bom' && (
|
||||
<div style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '16px 20px', borderBottom: '1px solid #F0F0F0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>装机清单 BOM</h3>
|
||||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>共 {bom.length} 项</span>
|
||||
</div>
|
||||
{bom.length > 0 ? (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{['序号', '物料名称', '板卡SN', '型号', '校准状态'].map(h => (
|
||||
<th key={h} style={{ padding: '12px 16px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bom.map((item, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||||
<td style={{ padding: '12px 16px', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{i + 1}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 13 }}>{item.name}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 13, fontWeight: 500 }}>{item.sn}</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{item.model}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
{item.calibration === '-' ? (
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>无需校准</span>
|
||||
) : (
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }}>{item.calibration}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div style={{ padding: 40, textAlign: 'center', color: 'rgba(0,0,0,0.25)', fontSize: 14 }}>暂无装机清单数据</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<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" />
|
||||
</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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>授权项模块</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{license.modules.split(', ').map(m => (
|
||||
<span key={m} style={{ padding: '4px 12px', borderRadius: 6, fontSize: 13, backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' }}>{m}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||
<Key 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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
{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>
|
||||
<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 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>
|
||||
)}
|
||||
|
||||
{activeTab === 'logs' && (
|
||||
<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={{ position: 'relative', paddingLeft: 24 }}>
|
||||
{/* Timeline line */}
|
||||
<div style={{ position: 'absolute', left: 7, top: 8, bottom: 8, width: 2, backgroundColor: '#F0F0F0' }} />
|
||||
{operationLogs.map((log, i) => (
|
||||
<div key={i} style={{ position: 'relative', paddingBottom: i < operationLogs.length - 1 ? 24 : 0 }}>
|
||||
{/* Timeline dot */}
|
||||
<div style={{ position: 'absolute', left: -20, top: 6, width: 12, height: 12, borderRadius: '50%', backgroundColor: i === 0 ? '#4a7c59' : '#D9D9D9', border: '2px solid #fff' }} />
|
||||
<div style={{ marginLeft: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: 'rgba(0,0,0,0.85)' }}>{log.action}</span>
|
||||
<span style={{ fontSize: 12, padding: '1px 8px', borderRadius: 4, backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #F0F0F0' }}>{log.operator}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 2 }}>{log.detail}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Clock size={12} />{log.date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,21 +3,59 @@ import { useState, useMemo } from 'react'
|
|||
import Link from 'next/link'
|
||||
import { Download, Plus, Search, ChevronLeft, ChevronRight, Monitor, Cpu, Wifi, Power, Tag } from 'lucide-react'
|
||||
|
||||
const devicesData = [
|
||||
{ id: 1, sn: 'GD30-2025-000001', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-15 14:30', customer: '北京地质研究院', batch: 'B2025-01' },
|
||||
{ id: 2, sn: 'GD30-2025-000002', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-18 09:15', customer: '中国地质大学', batch: 'B2025-01' },
|
||||
{ id: 3, sn: 'GD30-2024-000056', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-12-20 16:00', customer: '成都理工大学', batch: 'B2024-12' },
|
||||
{ id: 4, sn: 'GT20-2025-000045', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-02-10 11:20', customer: '武汉地质调查中心', batch: 'B2025-02' },
|
||||
{ id: 5, sn: 'GT20-2025-000046', model: 'GD-20', type: '二维电法仪', status: '装配中', firmware: 'v1.8.5', productionDate: '2025-03-01 08:45', customer: '-', batch: 'B2025-03' },
|
||||
{ id: 6, sn: 'GD30-2024-000078', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-11-05 13:30', customer: '长安大学', batch: 'B2024-11' },
|
||||
{ id: 7, sn: 'GD10-2024-000033', model: 'GD-10 Supreme', type: '入门级电法仪', status: '已激活', firmware: 'v1.5.2', productionDate: '2024-09-12 10:00', customer: '河海大学', batch: 'B2024-09' },
|
||||
{ id: 8, sn: 'GD30-2024-000089', model: 'GD-30 Supreme', type: '高密度电法仪', status: '装配中', firmware: 'v2.3.5', productionDate: '2025-03-05 15:10', customer: '-', batch: 'B2025-03' },
|
||||
{ id: 9, sn: 'GT20-2025-000012', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-01-22 09:30', customer: '中南大学', batch: 'B2025-01' },
|
||||
{ id: 10, sn: 'GD30-2024-000102', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-10-18 14:00', customer: '吉林大学', batch: 'B2024-10' },
|
||||
{ id: 11, sn: 'GD10-2024-000034', model: 'GD-10 Supreme', type: '入门级电法仪', status: '装配中', firmware: 'v1.5.2', productionDate: '2025-03-08 11:45', customer: '-', batch: 'B2025-03' },
|
||||
{ id: 12, sn: 'GD30-2024-000145', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2024-08-25 16:20', customer: '同济大学', batch: 'B2024-08' },
|
||||
/**
|
||||
* 根据日期计算 ISO 周数,返回 "YYYY-WXX" 格式
|
||||
* ISO 8601:每周从周一开始,包含该年第一个周四的那周为第1周
|
||||
*/
|
||||
function getYearWeek(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
||||
// 调整到最近的周四(ISO 周定义)
|
||||
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7))
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
||||
const weekNo = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
|
||||
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据年周字符串计算该周的起止日期范围(周一~周日),用于侧边栏展示
|
||||
*/
|
||||
function getWeekRange(yearWeek: string): string {
|
||||
const [yearStr, weekStr] = yearWeek.split('-W')
|
||||
const year = parseInt(yearStr, 10)
|
||||
const week = parseInt(weekStr, 10)
|
||||
// ISO 周:找到该年1月4日所在周的周一,再偏移到目标周
|
||||
const jan4 = new Date(Date.UTC(year, 0, 4))
|
||||
const dayOfWeek = jan4.getUTCDay() || 7
|
||||
const monday = new Date(jan4.getTime())
|
||||
monday.setUTCDate(jan4.getUTCDate() - dayOfWeek + 1 + (week - 1) * 7)
|
||||
const sunday = new Date(monday.getTime())
|
||||
sunday.setUTCDate(monday.getUTCDate() + 6)
|
||||
const fmt = (d: Date) => `${String(d.getUTCMonth() + 1).padStart(2, '0')}.${String(d.getUTCDate()).padStart(2, '0')}`
|
||||
return `${fmt(monday)}-${fmt(sunday)}`
|
||||
}
|
||||
|
||||
const rawDevices = [
|
||||
{ id: 1, sn: 'GD30-2025-000001', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-15 14:30', customer: '北京地质研究院' },
|
||||
{ id: 2, sn: 'GD30-2025-000002', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2025-01-18 09:15', customer: '中国地质大学' },
|
||||
{ id: 3, sn: 'GD30-2024-000056', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-12-20 16:00', customer: '成都理工大学' },
|
||||
{ id: 4, sn: 'GT20-2025-000045', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-02-10 11:20', customer: '武汉地质调查中心' },
|
||||
{ id: 5, sn: 'GT20-2025-000046', model: 'GD-20', type: '二维电法仪', status: '装配中', firmware: 'v1.8.5', productionDate: '2025-03-01 08:45', customer: '-' },
|
||||
{ id: 6, sn: 'GD30-2024-000078', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-11-05 13:30', customer: '长安大学' },
|
||||
{ id: 7, sn: 'GD10-2024-000033', model: 'GD-10 Supreme', type: '入门级电法仪', status: '已激活', firmware: 'v1.5.2', productionDate: '2024-09-12 10:00', customer: '河海大学' },
|
||||
{ id: 8, sn: 'GD30-2024-000089', model: 'GD-30 Supreme', type: '高密度电法仪', status: '装配中', firmware: 'v2.3.5', productionDate: '2025-03-05 15:10', customer: '-' },
|
||||
{ id: 9, sn: 'GT20-2025-000012', model: 'GD-20', type: '二维电法仪', status: '已激活', firmware: 'v1.8.5', productionDate: '2025-01-22 09:30', customer: '中南大学' },
|
||||
{ id: 10, sn: 'GD30-2024-000102', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已出厂', firmware: 'v2.3.4', productionDate: '2024-10-18 14:00', customer: '吉林大学' },
|
||||
{ id: 11, sn: 'GD10-2024-000034', model: 'GD-10 Supreme', type: '入门级电法仪', status: '装配中', firmware: 'v1.5.2', productionDate: '2025-03-08 11:45', customer: '-' },
|
||||
{ id: 12, sn: 'GD30-2024-000145', model: 'GD-30 Supreme', type: '高密度电法仪', status: '已激活', firmware: 'v2.3.5', productionDate: '2024-08-25 16:20', customer: '同济大学' },
|
||||
]
|
||||
|
||||
/** 根据生产日期动态计算年周批次 */
|
||||
const devicesData = rawDevices.map(d => ({
|
||||
...d,
|
||||
batch: getYearWeek(d.productionDate),
|
||||
}))
|
||||
|
||||
const modelOptions = ['全部', 'GD-30 Supreme', 'GD-20', 'GD-10 Supreme']
|
||||
const statusOptions = ['全部', '已激活', '已出厂', '装配中']
|
||||
|
||||
|
|
@ -48,15 +86,24 @@ export default function DevicesPage() {
|
|||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const pageSize = 8
|
||||
|
||||
// 从数据中提取所有批次并按时间倒序排列,统计每个批次的设备数量
|
||||
const batchList = useMemo(() => {
|
||||
// 从数据中提取所有年周批次,按时间倒序排列,统计每个批次的设备数量,按年分组
|
||||
const batchGroups = useMemo(() => {
|
||||
const batchMap = new Map<string, number>()
|
||||
devicesData.forEach(d => {
|
||||
batchMap.set(d.batch, (batchMap.get(d.batch) || 0) + 1)
|
||||
})
|
||||
return Array.from(batchMap.entries())
|
||||
const sorted = Array.from(batchMap.entries())
|
||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||
.map(([batch, count]) => ({ batch, count }))
|
||||
.map(([batch, count]) => ({ batch, count, year: batch.split('-W')[0] }))
|
||||
|
||||
// 按年分组
|
||||
const groups = new Map<string, { batch: string; count: number }[]>()
|
||||
sorted.forEach(item => {
|
||||
const list = groups.get(item.year) || []
|
||||
list.push({ batch: item.batch, count: item.count })
|
||||
groups.set(item.year, list)
|
||||
})
|
||||
return groups
|
||||
}, [])
|
||||
|
||||
const filtered = devicesData.filter(d => {
|
||||
|
|
@ -116,9 +163,6 @@ export default function DevicesPage() {
|
|||
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>SN搜索</label>
|
||||
<input type="text" value={searchText} onChange={e => { setSearchText(e.target.value); setCurrentPage(1) }} placeholder="输入设备SN号" style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<button onClick={() => setCurrentPage(1)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14, flexShrink: 0 }}>
|
||||
<Search size={14} />搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -148,25 +192,33 @@ export default function DevicesPage() {
|
|||
color: selectedBatch === '全部' ? '#fff' : 'rgba(0,0,0,0.45)',
|
||||
}}>{devicesData.length}</span>
|
||||
</button>
|
||||
{batchList.map(({ batch, count }) => (
|
||||
<button
|
||||
key={batch}
|
||||
onClick={() => handleBatchSelect(batch)}
|
||||
style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '8px 12px', borderRadius: 6, border: 'none', cursor: 'pointer', fontSize: 13, textAlign: 'left',
|
||||
backgroundColor: selectedBatch === batch ? '#eef5f0' : 'transparent',
|
||||
color: selectedBatch === batch ? '#4a7c59' : 'rgba(0,0,0,0.65)',
|
||||
fontWeight: selectedBatch === batch ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
<span>{batch}</span>
|
||||
<span style={{
|
||||
fontSize: 12, padding: '1px 8px', borderRadius: 10,
|
||||
backgroundColor: selectedBatch === batch ? '#4a7c59' : '#f0f0f0',
|
||||
color: selectedBatch === batch ? '#fff' : 'rgba(0,0,0,0.45)',
|
||||
}}>{count}</span>
|
||||
</button>
|
||||
{Array.from(batchGroups.entries()).map(([year, batches]) => (
|
||||
<div key={year}>
|
||||
<div style={{ padding: '8px 12px 4px', fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.35)', letterSpacing: '0.5px' }}>{year} 年</div>
|
||||
{batches.map(({ batch, count }) => (
|
||||
<button
|
||||
key={batch}
|
||||
onClick={() => handleBatchSelect(batch)}
|
||||
style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '7px 12px', borderRadius: 6, border: 'none', cursor: 'pointer', fontSize: 13, textAlign: 'left', width: '100%',
|
||||
backgroundColor: selectedBatch === batch ? '#eef5f0' : 'transparent',
|
||||
color: selectedBatch === batch ? '#4a7c59' : 'rgba(0,0,0,0.65)',
|
||||
fontWeight: selectedBatch === batch ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>{batch}</div>
|
||||
<div style={{ fontSize: 11, color: selectedBatch === batch ? '#6a9c79' : 'rgba(0,0,0,0.35)', marginTop: 1 }}>{getWeekRange(batch)}</div>
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: 12, padding: '1px 8px', borderRadius: 10, flexShrink: 0,
|
||||
backgroundColor: selectedBatch === batch ? '#4a7c59' : '#f0f0f0',
|
||||
color: selectedBatch === batch ? '#fff' : 'rgba(0,0,0,0.45)',
|
||||
}}>{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { ArrowLeft, Upload, Download, ChevronDown, ChevronUp, X, Package, Shield
|
|||
const firmwareTypes = ['全部', '主协板', '采集板', '发射板', '升压板', '主机固件', '计算单元固件']
|
||||
|
||||
const firmwareData = [
|
||||
{ id: 1, version: 'v2.1.0', boardModel: 'MCB-3000', type: '主协板', date: '2024-03-10', status: '已发布', size: '12.5MB', downloads: 1234, hwRange: 'MCB-3000 Rev.A~C', upgradeType: '可选', signed: true, md5: 'a1b2c3d4e5f6...', sha256: '9f8e7d6c5b4a...', notes: ['修复通信协议兼容性问题', '优化低功耗模式切换', '新增看门狗超时配置'] },
|
||||
{ id: 2, version: 'v2.0.0', boardModel: 'MCB-3000', type: '主协板', date: '2024-01-15', status: '已发布', size: '11.8MB', downloads: 2456, hwRange: 'MCB-3000 Rev.A~C', upgradeType: '强制', signed: true, md5: 'b2c3d4e5f6a1...', sha256: '8e7d6c5b4a9f...', notes: ['新增多通道采集支持', '重构通信协议栈'] },
|
||||
{ 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驱动', '修复温漂补偿算法'] },
|
||||
|
|
@ -217,7 +217,7 @@ function FirmwareContent() {
|
|||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 硬件版本范围</label>
|
||||
<input value={uploadForm.hwRange} onChange={e => setUploadForm({ ...uploadForm, hwRange: e.target.value })} placeholder="如 MCB-3000 Rev.A~C" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
<input value={uploadForm.hwRange} onChange={e => setUploadForm({ ...uploadForm, hwRange: e.target.value })} placeholder="如 MB25130025 Rev.A~C" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Plus, X, Info, GripVertical, Trash2, Edit } from 'lucide-react'
|
||||
import { Plus, X, Info, GripVertical, Trash2, Edit, Key, FileCode, ChevronDown } from 'lucide-react'
|
||||
|
||||
const modelsData = [
|
||||
{ id: 1, name: 'GD-30 Supreme', code: 'GD30', status: '在产', description: '高端三维电法仪', createDate: '2023-06-01' },
|
||||
{ id: 2, name: 'GD-20 Supreme', code: 'GD20', status: '在产', description: '中端二维电法仪', createDate: '2023-08-15' },
|
||||
{ id: 3, name: 'GD-10 Supreme', code: 'GD10', status: '停产', description: '入门级电法仪', createDate: '2022-03-10' },
|
||||
const initialModelsData = [
|
||||
{ id: 1, name: 'GD-30 Supreme', code: 'GD30', status: '在产', description: '高端高密度电法仪', createDate: '2023-06-01' },
|
||||
{ id: 2, name: 'GD-20 Supreme', code: 'GD20', status: '在产', description: '中端高密度电法仪', createDate: '2023-08-15' },
|
||||
{ id: 3, name: 'GD-10 Supreme', code: 'GD10', status: '停产', description: '入门级高密度电法仪', createDate: '2022-03-10' },
|
||||
]
|
||||
|
||||
const checklistTemplates: Record<string, { id: number; name: string; required: boolean }[]> = {
|
||||
GD30: [
|
||||
{ id: 1, name: '主协板安装检查', required: true },
|
||||
{ id: 2, name: '采集板安装检查(×6)', required: true },
|
||||
{ id: 2, name: '采集板安装检查', required: true },
|
||||
{ id: 3, name: '发射板安装检查', required: true },
|
||||
{ id: 4, name: '升压板安装检查', required: true },
|
||||
{ id: 5, name: '线缆连接检查', required: true },
|
||||
|
|
@ -25,7 +24,7 @@ const checklistTemplates: Record<string, { id: number; name: string; required: b
|
|||
],
|
||||
GD20: [
|
||||
{ id: 1, name: '主协板安装检查', required: true },
|
||||
{ id: 2, name: '采集板安装检查(×4)', required: true },
|
||||
{ id: 2, name: '采集板安装检查', required: true },
|
||||
{ id: 3, name: '发射板安装检查', required: true },
|
||||
{ id: 4, name: '线缆连接检查', required: true },
|
||||
{ id: 5, name: '整机通电测试', required: true },
|
||||
|
|
@ -35,7 +34,7 @@ const checklistTemplates: Record<string, { id: number; name: string; required: b
|
|||
],
|
||||
GD10: [
|
||||
{ id: 1, name: '主协板安装检查', required: true },
|
||||
{ id: 2, name: '采集板安装检查(×2)', required: true },
|
||||
{ id: 2, name: '采集板安装检查', required: true },
|
||||
{ id: 3, name: '线缆连接检查', required: true },
|
||||
{ id: 4, name: '整机通电测试', required: true },
|
||||
{ id: 5, name: '通信功能测试', required: true },
|
||||
|
|
@ -53,11 +52,31 @@ function getStatusStyle(status: string) {
|
|||
|
||||
export default function ModelsPage() {
|
||||
const router = useRouter()
|
||||
const [modelsData, setModelsData] = useState(initialModelsData)
|
||||
const [modelDrawer, setModelDrawer] = useState(false)
|
||||
const [checklistDrawer, setChecklistDrawer] = useState(false)
|
||||
const [checklistTab, setChecklistTab] = useState('GD30')
|
||||
const [modelForm, setModelForm] = useState({ name: '', code: '', status: '在产' })
|
||||
const [modelForm, setModelForm] = useState({ name: '', code: '', description:'',status: '在产' })
|
||||
const [checklistForm, setChecklistForm] = useState({ model: 'GD30', items: [{ name: '', required: true }] })
|
||||
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 = () => {
|
||||
if (editingModel) {
|
||||
setModelsData(prev => prev.map(m => m.id === editingModel.id ? { ...m, status: editStatus } : m))
|
||||
setEditDrawer(false)
|
||||
setEditingModel(null)
|
||||
}
|
||||
}
|
||||
|
||||
const addChecklistItem = () => {
|
||||
setChecklistForm({ ...checklistForm, items: [...checklistForm.items, { name: '', required: true }] })
|
||||
|
|
@ -84,7 +103,7 @@ export default function ModelsPage() {
|
|||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, backgroundColor: '#eef5f0', borderRadius: 8, marginBottom: 24, border: '1px solid #a3c4ad' }}>
|
||||
<Info size={18} style={{ color: '#4a7c59', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ fontSize: 14, color: '#4a7c59', lineHeight: 1.6 }}>
|
||||
设备型号管理是生产管理的核心枢纽。每个型号定义了设备的板卡组成、装配流程和检测标准。新增型号后,请及时配置对应的装配清单模板。
|
||||
设备型号管理是生产管理的核心枢纽。每个型号定义了设备的装配流程和检测标准。新增型号后,请及时配置对应的装配清单模板。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -99,7 +118,7 @@ export default function ModelsPage() {
|
|||
<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>
|
||||
|
|
@ -114,6 +133,44 @@ export default function ModelsPage() {
|
|||
<span style={{ ...getStatusStyle(model.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{model.status}</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{model.createDate}</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<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)' }}
|
||||
>
|
||||
操作 <ChevronDown 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>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -164,6 +221,46 @@ export default function ModelsPage() {
|
|||
</table>
|
||||
</div>
|
||||
|
||||
{/* Edit Status Drawer */}
|
||||
{editDrawer && editingModel && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
<div onClick={() => { setEditDrawer(false); setEditingModel(null) }} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
|
||||
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 480, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>编辑设备型号</h3>
|
||||
<button onClick={() => { setEditDrawer(false); setEditingModel(null) }} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8, color: 'rgba(0,0,0,0.65)' }}>型号名称</label>
|
||||
<div style={{ padding: '8px 12px', backgroundColor: '#FAFAFA', borderRadius: 6, fontSize: 14, color: 'rgba(0,0,0,0.85)' }}>{editingModel.name}</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8, color: 'rgba(0,0,0,0.65)' }}>型号编码</label>
|
||||
<div style={{ padding: '8px 12px', backgroundColor: '#FAFAFA', borderRadius: 6, fontSize: 14, color: 'rgba(0,0,0,0.85)' }}>{editingModel.code}</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>状态</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{['在产', '停产'].map(s => (
|
||||
<button key={s} onClick={() => setEditStatus(s)} style={{ padding: '6px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', border: editStatus === s ? '1px solid #4a7c59' : '1px solid #D9D9D9', backgroundColor: editStatus === s ? '#eef5f0' : '#fff', color: editStatus === s ? '#4a7c59' : 'rgba(0,0,0,0.65)' }}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button onClick={() => { setEditDrawer(false); setEditingModel(null) }} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||||
<button onClick={handleSaveEdit} style={{ padding: '8px 20px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14 }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Click outside to close action menu */}
|
||||
{actionMenuId !== null && (
|
||||
<div onClick={() => setActionMenuId(null)} style={{ position: 'fixed', inset: 0, zIndex: 5 }} />
|
||||
)}
|
||||
|
||||
{/* New Model Drawer */}
|
||||
{modelDrawer && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
|
||||
|
|
@ -182,6 +279,11 @@ export default function ModelsPage() {
|
|||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}><span style={{ color: '#FF4D4F' }}>*</span> 型号编码</label>
|
||||
<input value={modelForm.code} onChange={e => setModelForm({ ...modelForm, code: e.target.value })} placeholder="如 GD30" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}> 描述</label>
|
||||
<input value={modelForm.description} onChange={e => setModelForm({ ...modelForm, description: e.target.value })} placeholder="如 高端高密度电法仪" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}>状态</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export default function RegistrationPage() {
|
|||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>装机测试状态</label>
|
||||
<select value={testStatus} onChange={e => setTestStatus(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||||
<option value="装配中">装配中</option>
|
||||
<option value="测试通过">测试通过</option>
|
||||
<option value="测试不通过">测试不通过</option>
|
||||
</select>
|
||||
|
|
@ -116,7 +117,7 @@ export default function RegistrationPage() {
|
|||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>登记人</label>
|
||||
<input value="张工" readOnly style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)' }} />
|
||||
<input type='text' style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -127,7 +128,7 @@ export default function RegistrationPage() {
|
|||
<CheckCircle size={18} style={{ color: '#52C41A', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', lineHeight: 1.8 }}>
|
||||
<div>已匹配型号 <span style={{ fontWeight: 600 }}>{deviceModel}</span> 的关联信息:</div>
|
||||
<div>授权文件:<span style={{ color: '#4a7c59' }}>{matchInfo.license}</span> · 配置文件:<span style={{ color: '#4a7c59' }}>{matchInfo.config}</span></div>
|
||||
<div>授权项:<span style={{ color: '#4a7c59' }}>{matchInfo.license}</span> · 配置文件:<span style={{ color: '#4a7c59' }}>{matchInfo.config}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Reference in New Issue