# DevicePasscodes 组件说明

`SaasDevicePasscodes` 是租户端设备通行码管理 LowCode 组件，负责设备通行码的查询、生成、重新生成、延长、启用、禁用和删除。

## 数据加载原则

- 每次组件挂载只请求一次 `GET /tenant/device`。
- 查询固定带上 `with[]=model`、`with[]=workarea`、`with[]=passcode`。
- 列表接口不分页且调用成本较高，任何通行码写操作完成后都不能重新请求设备列表。
- 首次加载失败时，只有用户主动点击 Retry 才允许再次调用列表接口。
- 组件不包含运行时 Mock、设计态 Mock 或接口失败后的 Mock 降级；LowCode 设计态也依赖真实登录态、租户上下文和公共 request。

## 写接口及 ID 约定

| 操作             | 方法与路径                                | Path ID   |
| ---------------- | ----------------------------------------- | --------- |
| 创建             | `POST /tenant/device/{id}/passcode`       | 设备 ID   |
| 重新生成         | `POST /tenant/device/{id}/passcode/reset` | 设备 ID   |
| 延长、启用、禁用 | `PUT /tenant/device/passcode/{id}`        | 通行码 ID |
| 删除             | `DELETE /tenant/device/passcode/{id}`     | 通行码 ID |

创建、重新生成和 PUT 接口的实际部署响应为：

```ts
{
  status: true;
  code: 200;
  message: string;
  data: DevicePasscode;
}
```

早期接口截图中写接口的 `data` 为 `null`，该内容已经不是当前部署合同。实现必须使用最新的 `data` 局部更新设备，不能在写操作后调用列表接口或单设备通行码查询接口。

如果响应明确返回 `device_id`，组件会校验其与请求目标设备 ID 一致，避免并发批处理将通行码写入错误设备。删除成功后直接把目标设备的 `passcode` 置为 `null`。

## 本地状态与并发安全

设备数据由有序 `deviceIds` 和 `devicesById` 组成。`UPSERT_PASSCODE`、`REMOVE_PASSCODE` reducer 动作每次只替换一个设备，因此并行请求以任意顺序完成时不会覆盖其他设备已经写入的结果。

- 批量生成、重新生成：严格串行，同一时间只有一个在途请求。
- 批量禁用、延长时间：使用并发池，最大并发固定为 5。
- 单个任务失败不会中断剩余任务。
- 批量结束后，成功设备立即保留新数据；失败设备保持旧数据并继续保持选中。批量创建全部成功时切换到 Active，存在失败时留在 No passcodes 以便直接重试。
- 任务队列在开始操作时按当前表格顺序生成快照，后续标签变化不会修改该批次。

## 状态和时间计算

标签分类优先级如下：

1. `passcode === null`：No passcodes。
2. 存在 `expires_at` 且不晚于当前时间：Expired。
3. 其他：Active。

因此 Disabled、使用次数耗尽、`expires_at === null` 的通行码仍属于 Active。页面只有一个一秒定时器；倒计时到零时，设备会在本地自动从 Active 移入 Expired，不发起网络请求。

接口返回的 `expires_at` 已经是店铺时区对应的本地时间，前端不再对该值做二次时区转换。倒计时和过期分类会把当前时刻投影到店铺时区，再与原始 `expires_at` 按墙上时间比较。店铺时区读取 `global.globalConfig.timezone`，缺失时使用 `Australia/Sydney`。

`usage_limit`、`usage_count` 在领域模型中统一为数字，兼容接口返回数字或数字字符串。写接口按当前合同把 `usage_limit` 发送为字符串，其中 `"0"` 表示不限次数。

Generate、Regenerate 和 Extend 选择 Duration 时，当前时间均先换算为店铺墙上时间。Extend 对未过期通行码从原到期时间继续累加，对已过期或没有明确到期时间的通行码从店铺当前时间起算。选择 Specific date 时直接使用指定的新到期时间，不做时区转换。

## 交互说明

- 搜索匹配设备名称、工作区和设备型号，不匹配通行码明文。
- 选择以设备 ID 保存，搜索或筛选隐藏行后仍保留选择；切换标签会清空选择，避免对另一类设备误操作。
- 点击设备行后打开抽屉并请求 `GET /tenant/device/{id}/passcode`；详情成功后同步回列表，返回 `null` 时清除该设备的列表通行码。
- 单设备操作成功后抽屉保持打开并展示写接口返回的最新数据。
- Device Planning 详情抽屉每次从其他 tab 重新进入 Passcode 时都会查询一次单设备通行码；停留在 Passcode tab 时的普通重渲染不会重复查询。
- 删除后抽屉保持打开，并切换为未生成通行码状态。
- 显示时将 16 位通行码按四位分组；复制和二维码使用不带空格的原始值。
- 禁止在日志、错误上报或埋点中记录通行码明文。

## 暂不实现

以下功能的入口、菜单、参数和提示语均不渲染：

- Export as PDF
- Force Sign Out
- View Passcode Records

禁用仅表示通行码不能用于新的登录，不承诺已有设备会被强制退出。

## 国际化

组件提供 `en`、`zh-CN`、`zh-HK`、`ja`、`pt` 五种语言。新增用户可见文案必须先添加英文语义，再同步补齐其余四种语言，禁止在 JSX 中写死展示文案。
