# Device Planning 快速说明

`devicePlanning` 是 `SaasDevicePlanning` 低代码物料源码，用来管理租户设备规划。当前页面覆盖工作区管理、三类设备列表、设备增删改、设备详情、智能设备与外设连接。

## 先看哪里

- `index.tsx`：页面入口，只组合 hooks、渲染页面骨架和串联弹窗动作。
- `serve.ts`：后端接口封装，所有请求都从这里进入。
- `types.ts`：工作区、设备、型号、profile、连接关系等领域类型。
- `utils.ts`：展示模型转换、设备状态、型号扁平化、工作区删除计数等工具。
- `hooks/`：页面级状态和业务请求。
- `components/`：列表、弹窗、详情抽屉和内部共享 UI。
- `locales.ts`：静态文案，多语言字段展示不要从这里取。
- `*.less`：按页面和组件职责拆分样式，入口只保留页面壳层样式。

低代码引擎通过 `useEngineContext()` 注入 `appHelper.utils.request`。入口组件会在初始化时调用 `request.setRequest(utils.request)`，所以子模块不要自己创建 request client。

## 当前功能

- 左侧工作区支持列表、选中、创建、编辑、删除和拖拽排序。
- 删除工作区前会拉取 `smartDevices`、`dumbDevices`、`integratedDevices` 数量；工作区下仍有设备时由删除弹窗阻断确认。
- 右侧设备按工作区和设备种类展示，种类为 `smart`、`dumb`、`integrated`。
- 设备列表支持搜索、型号筛选、本地分页、拖拽排序和空状态快捷新增。
- 新增设备先选型号，再填写名称和 profile；创建成功后刷新列表，并自动打开新设备详情。
- 编辑设备可更新工作区、名称和 smart profile；删除设备成功后关闭详情并刷新列表。
- 详情抽屉包含信息页和连接页；`integrated` 设备没有连接页。
- 连接页支持连接已有设备、新增外设并连接、编辑连接配置、删除连接。

## 设备类型

- `smart`：智能设备，例如 POS。可绑定 profile，可连接外设。
- `dumb`：外设，例如打印机、扫码枪。通常连接到智能设备。
- `integrated`：一体化设备。没有连接页，创建后详情默认停留在信息页。

列表和详情使用 `DeviceRow` 作为展示模型，原始后端数据保留在 `_raw`。不要手写 `DeviceRow`，统一使用 `utils.createDeviceRow()`，因为 `link/status/profileName` 等展示值在这里统一处理。未连接设备的在线状态会展示为 `unknown`，避免误用后端原始在线状态。

## 入口组件职责

`index.tsx` 保持薄入口，主要做四件事：

1. 初始化 request 和静态文案。
2. 维护当前设备 tab：`activeKind`。
3. 组合 `useWorkAreas`、`useDeviceModels`、`useDeviceList`、`useDevicePlanningDialogs`。
4. 串联创建、编辑、删除后的刷新、toast、弹窗关闭和详情打开。

页面结构：

```mermaid
flowchart LR
  WorkAreaList[WorkAreaList] --> Index[index.tsx]
  Index --> DeviceTable[DeviceTable]
  Index --> DeviceDetailDrawer[DeviceDetailDrawer]
  Index --> DeviceModals[Add/Edit/Delete Device Modals]
  Index --> WorkAreaModals[WorkArea/Delete WorkArea Modals]
```

## 主要数据流

设备列表由 `selectedWorkAreaId + activeKind` 驱动：

```mermaid
flowchart TD
  WorkArea[selectedWorkAreaId] --> DeviceList[useDeviceList]
  Kind[activeKind] --> DeviceList
  Models[useDeviceModels] --> TypeOptions[typeOptions]
  DeviceList --> Rows[DeviceRow tableData]
  TypeOptions --> DeviceTable[DeviceTable]
  Rows --> DeviceTable
  DeviceTable --> Detail[DeviceDetailDrawer]
```

切换工作区或设备种类时必须调用 `deviceList.resetDeviceView()`，清空搜索、型号筛选并回到第一页。

`useDeviceModels` 按 `DeviceKind` 懒加载型号：

- 每类设备型号只请求一次，请求失败会允许下次重试。
- `typeOptions` 给设备列表型号筛选使用。
- `profileNameMap` 合并型号 active profiles 和设备自身 profile，用于列表展示 profile 名称。
- 外设品牌 `getDeviceBrands()` 只在新增入口按需加载。

## 工作区

`useWorkAreas` 管理工作区列表、当前选中项和工作区 CRUD。

- 初次加载后默认选中排序后的第一个工作区。
- 创建或编辑成功后刷新工作区，并选中新建/编辑项。
- 拖拽排序先乐观更新，再调用 `/tenant/device/workarea/sort`；失败时重新拉取后端顺序。
- 删除前使用 `getWorkAreaDetail()` 拉取设备数量，弹窗展示计数并决定是否允许删除。

没有工作区时，右侧只展示空状态，不发设备列表请求。

## 设备列表

`useDeviceList` 管理设备列表请求、搜索、型号筛选、分页、排序和设备 CRUD 请求。

- 请求接口：`getDevices({ workarea_id, type_kind, with: ['type', 'model', 'workarea'] })`。
- 搜索、型号筛选、分页都在前端完成。
- 表格拖拽排序会提交当前过滤后列表的 id 顺序到 `/tenant/device/sort`；失败时重新拉取。
- `dumb` 列表只展示名称、设备类型、型号。
- `smart` 和 `integrated` 列表额外展示 profile、连接状态和在线状态。

新增设备成功后的特殊逻辑在 `handleCreateDevice()`：

- 关闭新增弹窗。
- 页码回到第一页。
- 重新拉取设备列表。
- 如果后端返回了新设备 id，则从刷新后的列表中找到新设备并打开详情。
- 新增 `integrated` 后详情打开 `info` tab；其他类型默认打开 `connections` tab。

## 详情抽屉

`DeviceDetailDrawer` 由入口组件条件挂载，并用设备 id 作为 key：

```tsx
dialogs.detailOpen && dialogs.selectedDevice ? (
  <DeviceDetailDrawer key={dialogs.selectedDevice.id} ... />
) : null
```

这样每次切换设备都会销毁旧抽屉状态，避免 A 设备连接列表闪现在 B 设备里。关闭详情时 `useDevicePlanningDialogs.closeDeviceDetail()` 会同时清空 `selectedDevice`。

详情内部 hooks：

- `useDeviceDetail`：管理当前 tab、详情补拉取、`integrated` 禁止进入连接页。
- `useDeviceConnections`：只在抽屉打开且 tab 为 `connections` 时加载连接列表。

连接变更后必须触发 `onConnectionsChanged`，父级会刷新设备列表，保证列表上的连接状态和在线状态同步。

## 连接功能

连接入口在 `DeviceConnectionsPanel`：

- smart 设备可以连接已有外设，也可以新增外设并连接。
- dumb 设备只能选择已有 smart 设备建立连接。
- integrated 设备不会显示连接页。

连接弹窗 `ConnectExistingDeviceModal` 的状态由 `useConnectDeviceModal` 管理，支持三种模式：

- `existing`：选择已有目标设备，再填写连接配置。
- `new`：选择外设型号，填写外设基础信息，再填写连接配置。
- `edit`：编辑已有连接的 `config` 和 `profile_ids`。

连接配置规则：

- `connection_type` 必填。
- `ethernet` 和 `wifi` 需要 `connection_method`。
- `connection_method` 为 `manual` 时才提交 `connection_ip`。
- profile 是否必填由型号的 `profile_structure.required` 决定。

`Add New Peripheral` 不调用 `createDevice()`，而是通过创建连接接口嵌套创建外设：

```ts
{
  type: 'dumb',
  source_device_id,
  target_device_action: 'new',
  target_device: {
    workarea_id,
    model_code,
    name,
  },
  config,
  profile_ids,
}
```

默认外设名称只填 `original`，不要自动补 `en`。

## API 对照

- `GET /tenant/device/workarea`：工作区列表，可带设备数量 relation count。
- `POST /tenant/device/workarea`：创建工作区。
- `PUT /tenant/device/workarea/:id`：编辑工作区。
- `POST /tenant/device/workarea/sort`：工作区排序。
- `DELETE /tenant/device/workarea/:id`：删除工作区。
- `GET /tenant/device`：设备列表。
- `GET /tenant/device/:id`：设备详情。
- `POST /tenant/device`：单独创建设备。
- `PUT /tenant/device/:id`：编辑设备。
- `POST /tenant/device/sort`：设备排序。
- `DELETE /tenant/device/:id`：删除设备。
- `GET /tenant/device/type/all`：设备类型、型号和 active profiles。
- `GET /api/device_brand:list`：外设品牌列表，走 NocoBase 数据源。
- `GET /tenant/device/connection`：连接列表，当前按 `type=dumb` 查询哑设备连接。
- `GET /tenant/device/connection/:id`：连接详情，返回包含 `type`。
- `POST /tenant/device/connection`：创建连接，当前必须提交 `type: 'dumb'`，支持已有外设或新增外设。
- `PUT /tenant/device/connection/:id`：编辑连接配置，不提交 `type`。
- `DELETE /tenant/device/connection/:id`：删除连接。

## 共享 UI

`components/shared/` 放 devicePlanning 内部共享组件：

- `DeviceIcon`：统一设备图标颜色。
- `LinkStatusTag`：连接状态标签。
- `OnlineStatus`：在线状态展示。
- `OnlineWarningAlert`：在线设备操作警告。
- `I18nNameFormItem`：多语言名称表单项。
- `ModelPickerGrid`：型号分组网格。
- `DeviceModelPicker`：型号选择完整 UI，供新增设备和连接新增外设复用。
- `DeviceBasicInfoForm`：基础信息表单，供新增设备和连接新增外设复用。

共享组件只负责 UI 和表单，不调用 API，不组装最终业务 payload。

## 样式组织

`devicePlanning` 的样式按组件职责维护，避免多个业务组件长期共用同一个大型 less 文件：

- `index.less`：只放页面级布局样式，例如 `.saas-device-planning` 容器、右侧内容区、页面标题、空工作区状态和 Tabs 间距覆盖。
- `components/WorkAreaList/index.less`、`components/DeviceTable/index.less`、`components/AddDeviceModal/index.less`、`components/EditDeviceModal/index.less`、`components/DeleteWorkAreaModal/index.less`：对应一级组件自己的样式。
- `components/DeviceDetailDrawer/index.less`：只保留抽屉壳层、Drawer body 覆盖、左侧 sidebar、header、content 和 footer。
- `components/DeviceDetailDrawer/components/DeviceInfoPanel.less`：维护详情信息页的 card、基础信息、上报信息、在线状态和电池展示样式。
- `components/DeviceDetailDrawer/components/DeviceConnectionsPanel.less`：维护连接页列表、空状态和连接项样式。
- `components/DeviceDetailDrawer/components/ConnectExistingDeviceModal.less`：维护连接弹窗、选择已有设备、新增外设、连接配置和连接摘要样式。

拆分时保持现有 className 稳定，不为了移动样式同时改 JSX 结构。`DeviceIcon`、`OnlineStatus`、`LinkStatusTag` 这类小型共享组件暂不单独拆样式；只有样式复杂度上升或被跨页面复用时再考虑独立样式文件。

## 修改注意事项

- 静态文案使用 `getText(key)`；后端多语言字段使用 `translation(value)`。
- 不要绕过 `createDeviceRow()` 自己组装列表或详情展示模型。
- 修改设备连接 payload 时，同步检查 `useConnectDeviceModal.handleSubmit()`。
- 新增外设并连接必须走 `createDeviceConnection()`，不要拆成 `createDevice()` 加 `createDeviceConnection()` 两次请求。
- 连接增删改后要保持 `onConnectionsChanged`，否则列表状态会滞后。
- 强依赖当前设备或当前连接的弹窗优先条件挂载，关闭即销毁，避免旧数据残留。
- 工作区或设备 tab 切换后要重置列表视图，避免搜索、筛选、页码串到下一类数据。
- 修改样式时优先改对应组件相邻 less；不要把子组件样式重新放回 `DeviceDetailDrawer/index.less` 或页面入口 `index.less`。

## 最小回归路径

1. 创建、编辑、拖拽排序、删除空工作区；尝试删除有设备的工作区。
2. 切换工作区和 `smart/dumb/integrated` 三类设备 tab，确认列表筛选会重置。
3. 分别新增 smart、dumb、integrated 设备，确认新增成功后自动打开正确详情 tab。
4. 搜索、型号筛选、分页和设备拖拽排序。
5. 打开 smart 详情，连接已有外设、新增外设并连接、编辑连接、删除连接。
6. 打开 dumb 详情，连接已有 smart 设备。
7. 打开 integrated 详情，确认没有连接页。
8. 关闭 A 设备详情后打开 B 设备详情，确认不会闪现 A 的连接数据。
