修改和调整页面

This commit is contained in:
徐星 2026-04-16 08:51:13 +08:00
parent 184c0d092b
commit d66ba233ad
18 changed files with 1700 additions and 187 deletions

View File

@ -0,0 +1 @@
{"specId": "6761b341-a126-48c6-b6af-57b888694c48", "workflowType": "requirements-first", "specType": "feature"}

View File

@ -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 包含 idVARCHAR(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 返回不带该条件过滤的完整数据集

127
.kiro/steering/java-api.md Normal file
View File

@ -0,0 +1,127 @@
# Java API 技术规范
## 运行环境
- JDK 17
## 框架选型
- Spring Boot 3.3.6 (spring-boot-starter-parent)
- ORMMyBatis-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模块支持多实例横向扩展

View File

@ -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: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: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":"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"}

View File

@ -2,7 +2,9 @@
"/_not-found/page": "app/_not-found/page.js", "/_not-found/page": "app/_not-found/page.js",
"/boards/page": "app/boards/page.js", "/boards/page": "app/boards/page.js",
"/calibration/page": "app/calibration/page.js", "/calibration/page": "app/calibration/page.js",
"/calibration/register/page": "app/calibration/register/page.js",
"/config-files/page": "app/config-files/page.js", "/config-files/page": "app/config-files/page.js",
"/devices/[sn]/page": "app/devices/[sn]/page.js",
"/devices/page": "app/devices/page.js", "/devices/page": "app/devices/page.js",
"/firmware/page": "app/firmware/page.js", "/firmware/page": "app/firmware/page.js",
"/licenses/page": "app/licenses/page.js", "/licenses/page": "app/licenses/page.js",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
// This file is generated automatically by Next.js // This file is generated automatically by Next.js
// Do not edit this file manually // 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 PageRoutes = never
type LayoutRoutes = "/" type LayoutRoutes = "/"
type RedirectRoutes = never type RedirectRoutes = never
@ -13,8 +13,10 @@ interface ParamMap {
"/": {} "/": {}
"/boards": {} "/boards": {}
"/calibration": {} "/calibration": {}
"/calibration/register": {}
"/config-files": {} "/config-files": {}
"/devices": {} "/devices": {}
"/devices/[sn]": { "sn": string; }
"/firmware": {} "/firmware": {}
"/licenses": {} "/licenses": {}
"/models": {} "/models": {}

View File

@ -54,6 +54,15 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
type __Unused = __Check 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 // Validate ../../../src/app/config-files/page.tsx
{ {
type __IsExpected<Specific extends AppPageConfig<"/config-files">> = Specific type __IsExpected<Specific extends AppPageConfig<"/config-files">> = Specific
@ -63,6 +72,15 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
type __Unused = __Check 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 // Validate ../../../src/app/devices/page.tsx
{ {
type __IsExpected<Specific extends AppPageConfig<"/devices">> = Specific type __IsExpected<Specific extends AppPageConfig<"/devices">> = Specific

View File

@ -7,10 +7,10 @@ import { Download, Plus, X, Eye, Upload, Clock, Wrench, CheckCircle, ChevronRigh
const tabs = ['全部', '主协板', '采集板', '发射板', '升压板'] const tabs = ['全部', '主协板', '采集板', '发射板', '升压板']
const boardsData = [ const boardsData = [
{ id: 1, type: '主协板', model: 'MCB-3000', firmwareVersion: 'v2.1.0', productionDate: '2024-03-15', status: '在产' }, { id: 1, type: '主协板', model: 'MB25130025', 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: 2, type: '主协板', model: 'MB25130024', 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: 3, type: '采集板', model: 'RX25130024', 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: 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: 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: 6, type: '发射板', model: 'TXB-800', firmwareVersion: 'v1.0.3', productionDate: '2023-06-15', status: '停产' },
{ id: 7, type: '升压板', model: 'BST-500', firmwareVersion: 'v1.1.0', productionDate: '2024-04-01', status: '在产' }, { id: 7, type: '升压板', model: 'BST-500', firmwareVersion: 'v1.1.0', productionDate: '2024-04-01', status: '在产' },

View File

@ -1,104 +1,136 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useMemo } from 'react'
import { Info, Search, Download, ChevronLeft, ChevronRight, X, Eye, FileDown } from 'lucide-react' import Link from 'next/link'
import { Search, ChevronLeft, ChevronRight, Plus, Download, Info, Eye, X, Tag } from 'lucide-react'
const calibrationData = [ const boardCardsData = [
{ id: 1, sn: 'RX20240308001', boardModel: 'ACB-6000', calibDate: '2024-03-01', expiryDate: '2025-03-01', operator: '王工程师', status: '合格', channel: 6, deviation: '±0.05%' }, { id: 1, sn: 'MB25011500', type: '主协板', model: 'MB-V2.1', firmware: 'v2.1', status: '在库', deviceSn: '-', productionDate: '2025-01-15', calibStatus: '-', calibDate: '-'},
{ id: 2, sn: 'RX20240308002', boardModel: 'ACB-6000', calibDate: '2024-02-15', expiryDate: '2025-02-15', operator: '张工程师', status: '合格', channel: 6, deviation: '±0.03%' }, { 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: 'RX20240115003', boardModel: 'ACB-5000', calibDate: '2024-01-15', expiryDate: '2025-01-15', operator: '王工程师', status: '合格', channel: 4, deviation: '±0.08%' }, { 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: 'RX20240420004', boardModel: 'ACB-6000', calibDate: '2024-04-20', expiryDate: '2025-04-20', operator: '李工程师', status: '待校准', channel: 6, deviation: '-' }, { 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: 'RX20231205005', boardModel: 'ACB-5000', calibDate: '2023-12-05', expiryDate: '2024-12-05', operator: '王工程师', status: '不合格', channel: 4, deviation: '±0.25%' }, { 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: 'RX20240610006', boardModel: 'ACB-6000', calibDate: '2024-06-10', expiryDate: '2025-06-10', operator: '张工程师', status: '合格', channel: 6, deviation: '±0.04%' }, { 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: 'RX20240801007', boardModel: 'ACB-6000', calibDate: '2024-08-01', expiryDate: '2025-08-01', operator: '李工程师', status: '待校准', channel: 6, deviation: '-' }, { 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: 'RX20240305008', boardModel: 'ACB-5000', calibDate: '2024-03-05', expiryDate: '2025-03-05', operator: '王工程师', status: '合格', channel: 4, deviation: '±0.06%' }, { 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: 'RX20240922009', boardModel: 'ACB-6000', calibDate: '2024-09-22', expiryDate: '2025-09-22', operator: '张工程师', status: '合格', channel: 6, deviation: '±0.02%' }, { 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: 'RX20231110010', boardModel: 'ACB-5000', calibDate: '2023-11-10', expiryDate: '2024-11-10', operator: '李工程师', status: '不合格', channel: 4, deviation: '±0.30%' }, { 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 typeOptions = ['全部', '主协板', '采集板', '发射板', '升压板']
const statusOptions = ['全部', '合格', '不合格', '待校准'] const statusOptions = ['全部', '在库', '已装配', '故障', '报废']
const calibStatusOptions = ['全部', '合格', '不合格', '待校准']
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: '合格' },
],
}
function getStatusStyle(status: string) { function getStatusStyle(status: string) {
switch (status) { switch (status) {
case '合格': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' } case '在库': return { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }
case '不合格': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' } case '已装配': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
case '待校准': return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' } 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' } default: return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
} }
} }
export default function CalibrationPage() { function getCalibStyle(status: string) {
const [filterSN, setFilterSN] = useState('') switch (status) {
const [filterStatus, setFilterStatus] = useState('全部') case '合格': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
const [filterOperator, setFilterOperator] = useState('全部') case '不合格': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }
const [currentPage, setCurrentPage] = useState(1) case '待校准': return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }
const [detailDrawer, setDetailDrawer] = useState<typeof calibrationData[0] | null>(null) default: return {}
const pageSize = 6 }
}
const filtered = calibrationData.filter(r => { export default function BoardCardsPage() {
if (filterSN && !r.sn.toLowerCase().includes(filterSN.toLowerCase())) return false const [filterType, setFilterType] = useState('全部')
if (filterStatus !== '全部' && r.status !== filterStatus) return false const [filterStatus, setFilterStatus] = useState('全部')
if (filterOperator !== '全部' && r.operator !== filterOperator) return false 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 return true
}) }), [filterType, filterStatus, filterCalib, searchText])
const totalPages = Math.ceil(filtered.length / pageSize) const totalPages = Math.ceil(filtered.length / pageSize)
const paged = filtered.slice((currentPage - 1) * pageSize, currentPage * 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 ( return (
<div style={{ padding: 24 }}> <div style={{ padding: 24 }}>
{/* Header */} {/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<div> <div>
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}></h2> <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> <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> </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> </div>
{/* Info Banner */} {/* Stats Cards */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, backgroundColor: '#F9F0FF', borderRadius: 8, marginBottom: 24, border: '1px solid #D3ADF7' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16, marginBottom: 24 }}>
<Info size={18} style={{ color: '#722ED1', flexShrink: 0, marginTop: 2 }} /> {[
<div style={{ fontSize: 14, color: '#722ED1', lineHeight: 1.6 }}> { label: '板卡总数', value: stats.total, color: '#4a7c59', bg: '#eef5f0' },
ACB系列30 { label: '在库', value: stats.inStock, color: '#1890FF', bg: '#E6F7FF' },
</div> { 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> </div>
{/* Filter */} {/* Filter */}
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}> <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={{ display: 'flex', alignItems: 'flex-end', gap: 16 }}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>SN号</label> <label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}></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' }} /> <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>
<div style={{ flex: 1 }}> <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 }}> <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>)} {statusOptions.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div style={{ flex: 1 }}> <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={filterOperator} onChange={e => { setFilterOperator(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}> <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 }}>
{operatorOptions.map(o => <option key={o} value={o}>{o}</option>)} {calibStatusOptions.map(c => <option key={c} value={c}>{c}</option>)}
</select> </select>
</div> </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 }}> <div style={{ flex: 1 }}>
<Search size={14} /> <label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}>SN / SN</label>
</button> <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>
</div> </div>
@ -107,31 +139,33 @@ export default function CalibrationPage() {
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead> <thead>
<tr style={{ backgroundColor: '#FAFAFA' }}> <tr style={{ backgroundColor: '#FAFAFA' }}>
{['采集板SN号', '板卡型号', '校准日期', '到期日期', '校准人员', '状态', '操作'].map(h => ( {['板卡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> <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> </tr>
</thead> </thead>
<tbody> <tbody>
{paged.map(row => ( {paged.map(row => (
<tr key={row.id} style={{ borderBottom: '1px solid #F0F0F0' }}> <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 14px', fontSize: 13, 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 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.type}</td>
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.calibDate}</td> <td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.model}</td>
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.expiryDate}</td> <td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.firmware}</td>
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{row.operator}</td> <td style={{ padding: '12px 14px' }}>
<td style={{ padding: '12px 16px' }}>
<span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span> <span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span>
</td> </td>
<td style={{ padding: '12px 16px' }}> <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>
<div style={{ display: 'flex', gap: 12 }}> <td style={{ padding: '12px 14px' }}>
<button onClick={() => setDetailDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}> {row.calibStatus !== '-' ? (
<Eye size={14} /> <span style={{ ...getCalibStyle(row.calibStatus), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.calibStatus}</span>
</button> ) : (
<button style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 14, display: 'flex', alignItems: 'center', gap: 4 }}> <span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>
<FileDown size={14} /> )}
</button> </td>
</div> <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> </td>
</tr> </tr>
))} ))}
@ -140,13 +174,15 @@ export default function CalibrationPage() {
{/* Pagination */} {/* Pagination */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderTop: '1px solid #F0F0F0' }}> <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 }}> <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> <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) => ( {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 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> </div>
</div> </div>
@ -155,58 +191,60 @@ export default function CalibrationPage() {
{detailDrawer && ( {detailDrawer && (
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}> <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 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' }}> <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> <button onClick={() => setDetailDrawer(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
</div> </div>
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}> <div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
{/* 基本信息 */} {/* 基本信息 */}
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}> <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 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)' }}>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.type}</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.model}</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.firmware}</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.productionDate}</div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.expiryDate}</div> <div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.operator}</div> <span style={{ color: 'rgba(0,0,0,0.45)' }}></span>
<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> <span style={{ ...getStatusStyle(detailDrawer.status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.status}</span>
</div>
</div> </div>
</div> </div>
{/* 通道校准结果 */} {/* 装配信息 */}
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}> <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>
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <div style={{ fontSize: 13 }}>
<thead> <span style={{ color: 'rgba(0,0,0,0.45)' }}></span>
<tr> {detailDrawer.deviceSn === '-' ? (
{['通道', '参考值', '测量值', '偏差', '结果'].map(h => ( <span style={{ color: 'rgba(0,0,0,0.25)' }}></span>
<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> ) : (
))} <span style={{ color: '#4a7c59', fontWeight: 500 }}>{detailDrawer.deviceSn}</span>
</tr> )}
</thead> </div>
<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>
</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>
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end', gap: 12 }}> <div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end' }}>
<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>
<button onClick={() => setDetailDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}></button> <button onClick={() => setDetailDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}></button>
</div> </div>
</div> </div>

View File

@ -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>
)
}

View File

@ -1,17 +1,15 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' 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 = [ const menuGroups = [
{ title: '设备', items: [ { title: '设备', items: [
{ path: '/devices', label: '设备列表', icon: Monitor }, { path: '/devices', label: '设备列表', icon: Monitor },
{ path: '/models', label: '设备型号管理', icon: Settings2 }, { path: '/models', label: '设备型号管理', icon: Settings2 },
{ path: '/boards', label: '板卡型号管理', icon: Cpu },
]}, ]},
{ title: '授权', items: [{ path: '/licenses', label: '授权管理', icon: Key }] }, { title: '板卡', items: [{ path: '/calibration', label: '板卡列表', icon: Gauge },
{ title: '配置', items: [{ path: '/config-files', label: '配置管理', icon: FileCode }] }, { path: '/boards', label: '板卡型号管理', icon: Cpu },] },
{ title: '校准', items: [{ path: '/calibration', label: '校准记录', icon: Gauge }] },
{ title: '维修', items: [ { title: '维修', items: [
{ path: '/repair', label: '维修工单', icon: Wrench }, { path: '/repair', label: '维修工单', icon: Wrench },
{ path: '/scrap', label: '报废回收', icon: Recycle }, { path: '/scrap', label: '报废回收', icon: Recycle },

View File

@ -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>
)
}

View File

@ -3,21 +3,59 @@ import { useState, useMemo } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Download, Plus, Search, ChevronLeft, ChevronRight, Monitor, Cpu, Wifi, Power, Tag } from 'lucide-react' 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' }, * ISO "YYYY-WXX"
{ 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' }, * ISO 86011
{ 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' }, function getYearWeek(dateStr: string): string {
{ 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' }, const date = new Date(dateStr)
{ 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' }, const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
{ 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' }, // 调整到最近的周四ISO 周定义)
{ 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' }, d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7))
{ 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' }, const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
{ 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' }, const weekNo = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
{ 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' }, return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`
{ 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' }, }
/**
* ~
*/
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 modelOptions = ['全部', 'GD-30 Supreme', 'GD-20', 'GD-10 Supreme']
const statusOptions = ['全部', '已激活', '已出厂', '装配中'] const statusOptions = ['全部', '已激活', '已出厂', '装配中']
@ -48,15 +86,24 @@ export default function DevicesPage() {
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const pageSize = 8 const pageSize = 8
// 从数据中提取所有批次并按时间倒序排列,统计每个批次的设备数量 // 从数据中提取所有年周批次,按时间倒序排列,统计每个批次的设备数量,按年分组
const batchList = useMemo(() => { const batchGroups = useMemo(() => {
const batchMap = new Map<string, number>() const batchMap = new Map<string, number>()
devicesData.forEach(d => { devicesData.forEach(d => {
batchMap.set(d.batch, (batchMap.get(d.batch) || 0) + 1) 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])) .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 => { 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> <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' }} /> <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> </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>
</div> </div>
@ -148,25 +192,33 @@ export default function DevicesPage() {
color: selectedBatch === '全部' ? '#fff' : 'rgba(0,0,0,0.45)', color: selectedBatch === '全部' ? '#fff' : 'rgba(0,0,0,0.45)',
}}>{devicesData.length}</span> }}>{devicesData.length}</span>
</button> </button>
{batchList.map(({ batch, count }) => ( {Array.from(batchGroups.entries()).map(([year, batches]) => (
<button <div key={year}>
key={batch} <div style={{ padding: '8px 12px 4px', fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.35)', letterSpacing: '0.5px' }}>{year} </div>
onClick={() => handleBatchSelect(batch)} {batches.map(({ batch, count }) => (
style={{ <button
display: 'flex', justifyContent: 'space-between', alignItems: 'center', key={batch}
padding: '8px 12px', borderRadius: 6, border: 'none', cursor: 'pointer', fontSize: 13, textAlign: 'left', onClick={() => handleBatchSelect(batch)}
backgroundColor: selectedBatch === batch ? '#eef5f0' : 'transparent', style={{
color: selectedBatch === batch ? '#4a7c59' : 'rgba(0,0,0,0.65)', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
fontWeight: selectedBatch === batch ? 600 : 400, 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)',
<span>{batch}</span> fontWeight: selectedBatch === batch ? 600 : 400,
<span style={{ }}
fontSize: 12, padding: '1px 8px', borderRadius: 10, >
backgroundColor: selectedBatch === batch ? '#4a7c59' : '#f0f0f0', <div>
color: selectedBatch === batch ? '#fff' : 'rgba(0,0,0,0.45)', <div>{batch}</div>
}}>{count}</span> <div style={{ fontSize: 11, color: selectedBatch === batch ? '#6a9c79' : 'rgba(0,0,0,0.35)', marginTop: 1 }}>{getWeekRange(batch)}</div>
</button> </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>
</div> </div>

View File

@ -6,8 +6,8 @@ import { ArrowLeft, Upload, Download, ChevronDown, ChevronUp, X, Package, Shield
const firmwareTypes = ['全部', '主协板', '采集板', '发射板', '升压板', '主机固件', '计算单元固件'] const firmwareTypes = ['全部', '主协板', '采集板', '发射板', '升压板', '主机固件', '计算单元固件']
const firmwareData = [ 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: 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: '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: 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: 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: 4, version: 'v3.0.2', boardModel: 'ACB-6000', type: '采集板', date: '2024-02-20', status: '已发布', size: '8.7MB', downloads: 890, hwRange: 'ACB-6000 Rev.A~B', upgradeType: '可选', signed: true, md5: 'd4e5f6a1b2c3...', sha256: '6c5b4a9f8e7d...', notes: ['提升采样精度', '修复通道串扰问题', '新增自校准功能'] },
{ id: 5, version: 'v2.5.1', boardModel: 'ACB-5000', type: '采集板', date: '2023-09-15', status: '已发布', size: '7.1MB', downloads: 1567, hwRange: 'ACB-5000 Rev.A~C', upgradeType: '可选', signed: true, md5: 'e5f6a1b2c3d4...', sha256: '5b4a9f8e7d6c...', notes: ['优化ADC驱动', '修复温漂补偿算法'] }, { id: 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>
<div> <div>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> </label> <label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> </label>
<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> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>

View File

@ -1,19 +1,18 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation' 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 = [ const initialModelsData = [
{ id: 1, name: 'GD-30 Supreme', code: 'GD30', status: '在产', description: '高端三维电法仪', createDate: '2023-06-01' }, { 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: 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' }, { id: 3, name: 'GD-10 Supreme', code: 'GD10', status: '停产', description: '入门级高密度电法仪', createDate: '2022-03-10' },
] ]
const checklistTemplates: Record<string, { id: number; name: string; required: boolean }[]> = { const checklistTemplates: Record<string, { id: number; name: string; required: boolean }[]> = {
GD30: [ GD30: [
{ id: 1, name: '主协板安装检查', required: true }, { id: 1, name: '主协板安装检查', required: true },
{ id: 2, name: '采集板安装检查×6', required: true }, { id: 2, name: '采集板安装检查', required: true },
{ id: 3, name: '发射板安装检查', required: true }, { id: 3, name: '发射板安装检查', required: true },
{ id: 4, name: '升压板安装检查', required: true }, { id: 4, name: '升压板安装检查', required: true },
{ id: 5, name: '线缆连接检查', required: true }, { id: 5, name: '线缆连接检查', required: true },
@ -25,7 +24,7 @@ const checklistTemplates: Record<string, { id: number; name: string; required: b
], ],
GD20: [ GD20: [
{ id: 1, name: '主协板安装检查', required: true }, { id: 1, name: '主协板安装检查', required: true },
{ id: 2, name: '采集板安装检查×4', required: true }, { id: 2, name: '采集板安装检查', required: true },
{ id: 3, name: '发射板安装检查', required: true }, { id: 3, name: '发射板安装检查', required: true },
{ id: 4, name: '线缆连接检查', required: true }, { id: 4, name: '线缆连接检查', required: true },
{ id: 5, name: '整机通电测试', required: true }, { id: 5, name: '整机通电测试', required: true },
@ -35,7 +34,7 @@ const checklistTemplates: Record<string, { id: number; name: string; required: b
], ],
GD10: [ GD10: [
{ id: 1, name: '主协板安装检查', required: true }, { id: 1, name: '主协板安装检查', required: true },
{ id: 2, name: '采集板安装检查×2', required: true }, { id: 2, name: '采集板安装检查', required: true },
{ id: 3, name: '线缆连接检查', required: true }, { id: 3, name: '线缆连接检查', required: true },
{ id: 4, name: '整机通电测试', required: true }, { id: 4, name: '整机通电测试', required: true },
{ id: 5, name: '通信功能测试', required: true }, { id: 5, name: '通信功能测试', required: true },
@ -53,11 +52,31 @@ function getStatusStyle(status: string) {
export default function ModelsPage() { export default function ModelsPage() {
const router = useRouter() const router = useRouter()
const [modelsData, setModelsData] = useState(initialModelsData)
const [modelDrawer, setModelDrawer] = useState(false) const [modelDrawer, setModelDrawer] = useState(false)
const [checklistDrawer, setChecklistDrawer] = useState(false) const [checklistDrawer, setChecklistDrawer] = useState(false)
const [checklistTab, setChecklistTab] = useState('GD30') const [checklistTab, setChecklistTab] = useState('GD30')
const [modelForm, setModelForm] = useState({ name: '', code: '', status: '在产' }) const [modelForm, setModelForm] = useState({ name: '', code: '', description:'',status: '在产' })
const [checklistForm, setChecklistForm] = useState({ model: 'GD30', items: [{ name: '', required: true }] }) 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 = () => { const addChecklistItem = () => {
setChecklistForm({ ...checklistForm, items: [...checklistForm.items, { name: '', required: true }] }) 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' }}> <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 }} /> <Info size={18} style={{ color: '#4a7c59', flexShrink: 0, marginTop: 2 }} />
<div style={{ fontSize: 14, color: '#4a7c59', lineHeight: 1.6 }}> <div style={{ fontSize: 14, color: '#4a7c59', lineHeight: 1.6 }}>
</div> </div>
</div> </div>
@ -99,7 +118,7 @@ export default function ModelsPage() {
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead> <thead>
<tr style={{ backgroundColor: '#FAFAFA' }}> <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> <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> </tr>
@ -114,6 +133,44 @@ export default function ModelsPage() {
<span style={{ ...getStatusStyle(model.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{model.status}</span> <span style={{ ...getStatusStyle(model.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{model.status}</span>
</td> </td>
<td style={{ padding: '12px 16px', fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>{model.createDate}</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> </tr>
))} ))}
</tbody> </tbody>
@ -164,6 +221,46 @@ export default function ModelsPage() {
</table> </table>
</div> </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 */} {/* New Model Drawer */}
{modelDrawer && ( {modelDrawer && (
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}> <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> <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' }} /> <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>
<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 }}> <div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label> <label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8 }}></label>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>

View File

@ -106,6 +106,7 @@ export default function RegistrationPage() {
<div> <div>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}></label> <label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}></label>
<select value={testStatus} onChange={e => setTestStatus(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}> <select value={testStatus} onChange={e => setTestStatus(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
<option value="装配中"></option>
<option value="测试通过"></option> <option value="测试通过"></option>
<option value="测试不通过"></option> <option value="测试不通过"></option>
</select> </select>
@ -116,7 +117,7 @@ export default function RegistrationPage() {
</div> </div>
<div> <div>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}></label> <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> </div>
</div> </div>
@ -127,7 +128,7 @@ export default function RegistrationPage() {
<CheckCircle size={18} style={{ color: '#52C41A', flexShrink: 0, marginTop: 2 }} /> <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 style={{ fontSize: 13, color: 'rgba(0,0,0,0.65)', lineHeight: 1.8 }}>
<div> <span style={{ fontWeight: 600 }}>{deviceModel}</span> </div> <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>
</div> </div>
) : ( ) : (