# appDevicePlanning 功能说明

`appDevicePlanning` 是私有物料库中的 SaaS 设备设置页面，对外导出为 `SaasAppDevicePlanning`。它用于在 App/POS 端选择设备型号、管理工作区域、查看设备列表，并完成设备绑定、接管、增删改等操作。

## 入口文件

- 组件入口：`index.tsx`
- 类型定义：`types.ts`
- 接口封装：`serve.ts`
- 文案配置：`locales.ts`
- 样式文件：`index.less`
- 子组件目录：`components/`

## 使用场景

该页面主要服务设备初始化与设备切换流程：

- 首次进入时，如果本地没有设备类型和型号信息，会先进入设备型号选择页。
- 已有设备型号后，进入设备设置页，展示当前型号下的设备列表。
- 用户可以按工作区域筛选设备，搜索设备，添加、编辑、删除设备。
- 用户可以将当前终端绑定到未绑定设备，也可以接管已绑定设备。
- 页面会定时刷新设备列表，以同步设备在线、绑定等状态。

## Props

`SaasAppDevicePlanningProps` 定义在 `types.ts`：

- `kind?: 'smart' | 'dumb' | 'integrated'`：设备种类，默认 `smart`。
- `showBack?: boolean`：是否展示返回按钮。
- `onBack?: () => void`：自定义返回逻辑；未提供时使用引擎上下文里的 history 返回。
- `onLinked?: (params) => void | Promise<void>`：设备绑定成功后的回调，参数包含绑定后的 `device` 和 `channel`。
- `variant?: 'desktop' | 'phone' | ...`：来自响应式基础能力，用于控制桌面/手机端布局。

注意：`types.ts` 中虽然保留了 `typeCode`、`modelCode` 字段，但当前页面逻辑实际从本地存储读取设备类型和型号。

## 本地存储依赖

页面通过以下本地存储键维护设备上下文：

- `pisell_iot_type_code`：当前设备类型 code。
- `pisell_iot_model_code`：当前设备型号 code。
- `pisell_app_type`：应用类型；值为 `universal` 时展示切换型号按钮。
- `pisell_app_channel`：绑定设备后保存的渠道信息。
- App storage 的 `iotDevice`：当前使用设备，用于标记 `Current Device`。
- App storage 的 `iotDeviceLastUsed`：上次使用设备，用于标记 `Last Used`。

## 页面状态流

### 1. 请求注入

组件从 `useEngineContext()` 获取 `context.appHelper.utils.request`，并通过 `request.setRequest()` 注入到本模块接口封装中。所有接口都走 `serve.ts` 中的 `request.getRequest()`。

### 2. 型号选择

当 `typeCode` 或 `modelCode` 缺失时，页面进入型号选择状态：

- 调用 `getDeviceTypes({ kind, with: ['activeModels', 'activeModels.activeProfiles'] })`。
- 根据 `context.appHelper.constants.iotEndpoint` 过滤可用型号。
- 用户选择型号后，写入本地存储 `pisell_iot_type_code` 和 `pisell_iot_model_code`，然后进入设备列表页。
- 如果 `showBack` 为 `true`，选择型号不会持久化到本地存储。

### 3. 设备列表页

进入设备列表页后会加载：

- 工作区域列表：`getWorkAreas()`。
- 当前型号详情：`getDeviceModel(modelCode)`。
- 当前设备列表：`getDevices()`。

设备列表查询条件由 `getDeviceListParams()` 统一生成：

- `workarea_id`
- `type_kind`
- `type_code`
- `model_code`
- `with: ['type', 'model', 'profile', 'workarea']`

`reloadDevices()` 复用同一套查询条件，供首次加载、手动刷新、定时刷新以及增删改后刷新使用。

### 4. 自动和手动刷新

当前设备列表支持两种刷新方式：

- 页面进入设备列表状态后立即刷新一次。
- 每 60 秒通过 `setInterval` 自动刷新一次，组件卸载或查询条件变化时会清理旧定时器。
- 页面标题区域提供 `Refresh` 按钮，点击后立即调用 `reloadDevices()`。

刷新只更新当前条件下的设备列表，不刷新左侧工作区域列表。

## 工作区域功能

工作区域由 `WorkAreaList` 展示：

- 桌面端：左侧固定侧栏展示。
- 手机端：通过 `Work Areas` 触发器打开底部抽屉展示。

支持功能：

- 查看全部工作区域和指定工作区域。
- 创建工作区域：`createWorkArea()`。
- 编辑工作区域：`updateWorkArea()`。
- 删除工作区域：`deleteWorkArea()`。

删除工作区域前会调用 `getWorkAreaDetail()` 获取设备数量：

- `smartDevices`
- `dumbDevices`
- `integratedDevices`

如果工作区域内仍有设备，删除弹窗会展示阻止信息和数量。

## 设备列表功能

设备列表由 `DeviceTable` 展示，数据来自 `tableData`：

- 会先按当前 `modelCode` 过滤设备。
- 会把后端 `DeviceItem` 映射为展示用 `DeviceRow`。
- 支持按设备名称、类型、型号、配置方案、工作区域名称进行客户端搜索。
- 支持客户端分页。

设备卡片展示内容：

- 设备名称。
- 工作区域描述或工作区域名称。
- 绑定状态：`Linked` / `Unlinked`。
- 在线状态：绑定设备才展示 `Online` / `Offline`。
- 设备型号、配置方案、硬件。
- 当前设备标记：`Current Device`。
- 上次使用标记：`Last Used`。

## 设备增删改

### 添加设备

`AddDeviceModal` 负责创建设备：

- 使用当前 `modelCode`。
- 默认带入当前工作区域。
- 提交后调用 `createDevice()`。
- 成功后关闭弹窗、页码回到第一页，并刷新设备列表。

### 编辑设备

`EditDeviceModal` 负责编辑设备：

- 可修改工作区域、配置方案和名称。
- 提交后调用 `updateDevice()`。
- 成功后关闭弹窗并刷新设备列表。

### 删除设备

`DeleteDeviceModal` 负责删除设备：

- 在线设备删除时弹窗会展示风险提示。
- 提交后调用 `deleteDevice()`。
- 成功后关闭弹窗并刷新设备列表。

## 绑定与接管

设备操作按钮规则：

- 当前设备：展示禁用态 `Current Device`。
- 已绑定但不是当前设备：展示 `Takeover`，点击后先弹出确认接管弹窗。
- 未绑定设备：展示 `Link`，点击后直接执行绑定。

绑定逻辑在 `handleLinkDevice()`：

1. 通过 App 插件 `app.plugins.get('device').getDeviceDetail()` 获取当前终端设备详情。
2. 调用 `linkDevice(id, data)` 将当前终端绑定到选中设备。
3. 写入 App storage：`iotDevice` 和 `iotDeviceLastUsed`。
4. 持久化当前设备类型和型号。
5. 调用 `getDeviceSetting(id, ['general.channel'])` 获取渠道，默认值为 `system`。
6. 写入 `pisell_app_channel`。
7. 刷新设备列表。
8. 调用外部 `onLinked({ device, channel })` 回调。

接管只是绑定流程前多了一层确认弹窗；确认后仍复用 `handleLinkDevice()`。

## 响应式布局

组件通过 `withResponsive(SaasAppDevicePlanning)` 导出，内部根据 `variant` 判断当前布局：

- 桌面端：
  - 全局 header 在顶部。
  - 工作区域列表在左侧。
  - 内容区域顶部展示型号标题、`Refresh` 和 `Add Device`。
  - 设备卡片按网格展示。

- 手机端：
  - 顶部 header 可换行展示返回、标题、设备类型和切换按钮。
  - 工作区域列表改为底部抽屉。
  - `Refresh` 和 `Add Device` 在型号标题下方同一行等分展示。
  - 设备列表适配窄屏卡片布局。

## 国际化

页面使用 `@pisell/utils` 的 `locales.getText(key)` 获取文案，文案定义在 `locales.ts`。

当前语言包包含：

- `en`
- `zh-CN`
- `zh-HK`
- `ja`
- `pt`

注意：新增文案时需要同步补齐这些语言。

## 主要接口

接口封装在 `serve.ts`：

- `GET /tenant/device/workarea`：获取工作区域列表。
- `POST /tenant/device/workarea`：创建工作区域。
- `PUT /tenant/device/workarea/:id`：更新工作区域。
- `DELETE /tenant/device/workarea/:id`：删除工作区域。
- `GET /tenant/device`：获取设备列表。
- `GET /tenant/device/model/:code`：获取设备型号详情。
- `GET /tenant/device/type/all`：获取设备类型和可用型号。
- `POST /tenant/device`：创建设备。
- `PUT /tenant/device/:id`：更新设备。
- `DELETE /tenant/device/:id`：删除设备。
- `PUT /tenant/device/:id/link`：绑定或接管设备。
- `POST /tenant/device/:id/setting`：读取设备设置项。

所有接口都会传入 `{ isPisell2: true }` 配置。

## 关键文件职责

- `index.tsx`：页面主状态、数据请求、设备型号选择、刷新逻辑、弹窗编排。
- `components/WorkAreaList`：工作区域侧栏/抽屉内容。
- `components/DeviceTable`：设备搜索、卡片展示、分页和设备操作入口。
- `components/AddDeviceModal`：添加设备表单。
- `components/EditDeviceModal`：编辑设备表单。
- `components/DeleteDeviceModal`：删除设备确认弹窗。
- `components/WorkAreaModal`：创建/编辑工作区域表单。
- `components/DeleteWorkAreaModal`：删除工作区域确认和阻止提示。
- `types.ts`：页面共享类型、表单 payload、设备行展示模型。
- `serve.ts`：后端接口封装。
- `locales.ts`：页面多语言文案。

## 修改注意事项

- 设备列表刷新应优先复用 `reloadDevices()`，不要在新逻辑里重复拼装查询参数。
- 工作区域刷新和设备列表刷新是两条链路；只有工作区域变更后才需要调用 `reloadWorkAreas()`。
- 设备字段语义以 `types.ts` 和 `serve.ts` 为准，不要自行推断后端字段含义。
- 绑定逻辑涉及 App 插件、App storage、本地存储和外部回调，修改时需要同时考虑这些副作用。
- 手机端和桌面端样式共用主结构，新增布局时优先限定在 `saas-app-device-planning--phone` 或对应 variant 下，避免影响桌面端。
