Skip to content

feat(web): group room devices by category - #442

Open
wei-dou wants to merge 2 commits into
XiaoMi:mainfrom
wei-dou:feat/device-category-grouping
Open

feat(web): group room devices by category#442
wei-dou wants to merge 2 commits into
XiaoMi:mainfrom
wei-dou:feat/device-category-grouping

Conversation

@wei-dou

@wei-dou wei-dou commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • surface raw MIoT category to the web device model for finer display grouping
  • group room devices by online state and category with stable localized ordering
  • add localized group labels and focused grouping contract tests

Split from #360 so room device grouping can be reviewed independently from status semantics. This PR intentionally does not depend on the statusKind changes.

Verification

  • cd web && corepack pnpm build
  • cd web && NODE_OPTIONS='--no-experimental-webstorage' ./node_modules/.bin/vitest run tests/DevicesByRoom.test.ts

@github-actions

Copy link
Copy Markdown

👋 感谢提交 PR @wei-dou!维护者会尽快 review。

提交前请确认:

  • CI 全绿(test / lint / build)
  • 改动聚焦单一主题,便于审阅
  • 若改动了依赖(lockfile / pyproject.toml / package.json),需维护者评论 /allow-dependencies-change <当前 head SHA> 放行(之后再 push 需重新放行)

@github-actions

Copy link
Copy Markdown

[PR #442]: feat(web): group room devices by category

作者: wei-dou
范围: feat/device-category-grouping → main

修改方案

要解决的问题:设备总览页此前只按房间平铺展示设备,一个房间几十个设备时列表又长又乱,用户很难在同一房间内快速定位某一类设备(灯、传感器、安防…)。本 PR 从 #360 拆出,只做「房间内按类别聚类 + 排序」,不触碰 statusKind 语义。

整体方案

  • 把原始 MIoT 品类透传到前端设备模型:后端返回的 d.category 是米家原始品类(中文名如「扫地机器人」或英文 slug 如 wall-switch),此前只保留了归一化后的 9 类 DeviceCategory(灯/空调/传感器…,用于选图标)。现在在设备映射时额外挂上原始值(real.ts#L682),并在 Device 类型上加可选字段(types.ts#L79),供更细粒度的分组用。
  • 两级品类映射 + 优先级:新增 15 个展示分组(清洁电器/安防/传感器/…/其他)。判定一个设备归哪个组时(deviceGroup),先查原始品类表(RAW_CATEGORY_TO_GROUP,覆盖 ~100 个中英文品类),命中就用;没命中再退回按归一化的 9 类粗分(DEVICE_TO_GROUP);再兜底到「其他」。原始表优先是为了拿到比 9 类更细的划分(例如 wall-switch 归一化成 light,但原始表能把它正确分到「插座开关」而非「照明」)。
  • 在线优先 + 稳定本地化排序:设备排序(sortDevicesForDisplay)先按在线状态(在线排前),再按分组在 GROUP_ORDER 里的固定次序,最后按中文本地化名字(localeCompare("zh-Hans-CN"))、名字相同再按 did 兜底,保证顺序稳定不抖动。分组排序(groupDevicesByCategory)把设备装桶后,再让「含在线设备的组」整体排在「全离线的组」前面,同级按 GROUP_ORDER
  • 房间内渲染改为分组小标题 + 设备行:房间展开后不再直接平铺设备行,而是先按 categoryGroups 渲染每个类别的小标题(本地化组名 · 数量),再渲染该组设备(DevicesByRoom.tsx#L350)。useMemo 里对每个房间预算出 categoryGroups
  • 本地化组名 + 契约测试:中英各补 15 个 devices.group.* 文案(zh/en);测试覆盖在线优先、rawCategory 优先于归一化、含在线组排前、同组内名字/did 稳定排序四条契约(tests)。

测试覆盖

契约 用例
在线优先 sortDevicesForDisplay 在线排离线前
原始品类优先 wall-switch 归一化成 light 但仍分到 plug_switch
含在线组优先 在线灯组排在全离线摄像头组前
组内稳定排序 同名按 did 兜底、按中文名排序

整体实现干净:类型用 satisfies/映射类型保证分组标签与 DeviceCategory 穷举,无重复键,PR 描述三条声明均可在 diff 中一一对上。

问题

🔵 建议(可选优化)

  • web/src/components/DevicesByRoom.tsx:271-275useMemo 里的 sorted 是无用的重排计算

    • 背景: 每个房间的 memo 结果是三元组 [room, sorted, categoryGroups],解构时第二个元素被命名为 listL308)。sortedcategoryGroups.flatMap(([, categoryDevices]) => categoryDevices) 得到,是把已分好组的设备再拍平成一个数组。
    • 问题: list 的全部消费点都与顺序无关——只有 list.filter((d) => d.online)(onlineCount)、onCount 的 filter、以及 list.lengthL309-343)。而 groupDevicesByCategory 已经遍历过所有设备,sorted 与原始 list 是同一批设备的不同排列。因此这次 flatMap 重排是纯浪费——渲染实际用的是 categoryGroups,计数用的是与顺序无关的聚合。设备多的大户型每次 devices 变化都白跑一遍。
    • 改进: 直接把原始 list 透传,省掉 flatMap:
      return [...m.entries()].map(([room, list]) => {
        const categoryGroups = groupDevicesByCategory(list);
        return [room, list, categoryGroups] as const;
      });
  • web/src/components/DevicesByRoom.tsx:352-356 — 英文下类别小标题的量词略显冗余

    • 背景: 类别小标题复用了 devices.countUnit 文案渲染数量(L355)。该 key 在英文里是 "{{n}} devices"en/devices.json#L6)。
    • 问题: 中文渲染成「照明 · 3 个」自然;英文渲染成「Lighting · 3 devices」,在已经明确是设备分组的上下文里重复 "devices" 一词,读起来有点啰嗦(房间行也用同一 key,那里是「N devices · M rooms」语义合适,小标题下则偏冗余)。纯文案观感,不影响功能。
    • 改进: 如在意可为小标题单独引一个只出数字的 key(如 devices.group.count = "{{n}}"),或接受现状。
  • web/src/components/DevicesByRoom.tsx:220-225 / real.ts:409 — 部分设备的图标与所属分组会「打架」

    • 背景: 图标由归一化的 device.category 决定(DeviceRowCATEGORY_ICON),而分组由 rawCategory 优先决定。两套映射对同一设备可能给出不同大类。
    • 问题: 典型例:wall-switchCATEGORY_MAP 归一化成 light → 图标是灯泡(real.ts#L451),但 RAW_CATEGORY_TO_GROUP 把它分到「插座开关」。结果是一个灯泡图标的行出现在「插座开关」小标题下,用户观感略微割裂(「杀菌灯」→ 分到照明但归一化落 other → 插头图标,同理)。这是「更细分组用 rawCategory、图标仍用粗分类」两套映射并存的固有副作用,happy path 不坏,仅视觉一致性。
    • 改进: 若要图标与分组对齐,可让 DeviceRow 的图标也在 rawCategory 命中时走一张更细的图标表;或接受现状(分组更准比图标更重要)。

结论

LGTM — 实现干净、类型穷举有保障、无重复键、PR 描述与代码逐条对得上,测试覆盖了核心排序/分组契约。仅 3 条 🔵 可选优化(一处无用重排、一处英文文案冗余、一处图标/分组视觉不一致),均不阻塞合并。


由 review-pr skill v1.6 生成

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant