diff --git a/config/prod.ts b/config/prod.ts index b1c6ea4..c64e0a9 100644 --- a/config/prod.ts +++ b/config/prod.ts @@ -1 +1,8 @@ -export default {} +export default { + mini: { + // 生产关 sourcemap:主包体积硬指标 1.5M,.map 文件约 390K 必须剔除 + webpackChain(chain: { devtool: (v: false) => void }) { + chain.devtool(false) + } + } +} diff --git a/src/api/homeDevice.ts b/src/api/homeDevice.ts index c0ecfc3..89f96c0 100644 --- a/src/api/homeDevice.ts +++ b/src/api/homeDevice.ts @@ -18,16 +18,22 @@ export interface HomeDeviceUnbindReq { installId: string // 设备安装 ID } -// 骨架:本期仅锁定接口路径,字段类型联调时修正(spec D7) +// 响应字段按 2026-09-17 联调实测 export interface HomeDevice { - installId: string // 设备安装 ID(unbind 入参,按文档推断) - deviceId: string + installId: string + deviceName: string // 设备序列号(如 b4bfe9df0d0e) + name: string | null // 设备显示名称,展示用(未命名时回落序列号) homeId: string roomId: string - roomName: string - deviceName: string + roomName: string | null + productId: string + productCategory: string deviceType: string - online: boolean + deviceModel: string | null + subDeviceType: string | null + status: number + onlineStatus: number // 1-在线 0-离线 + lastOnlineTime: string | null } export function getHomeDeviceList(params: HomeDeviceListReq): Promise { diff --git a/src/api/homeRoom.ts b/src/api/homeRoom.ts index f91ae22..f5383a0 100644 --- a/src/api/homeRoom.ts +++ b/src/api/homeRoom.ts @@ -34,12 +34,13 @@ export interface HomeRoomDeleteReq { roomId: string } -// 骨架:本期仅锁定接口路径,字段类型联调时修正(spec D7) +// 响应字段按 2026-09-17 联调实测:name(非 roomName) export interface HomeRoom { roomId: string homeId: string - roomName: string + name: string sence?: string | null + icon?: string | null } // 文档路径为 POST + body { homeId }(YlHomeRoomListReq) diff --git a/src/app.config.ts b/src/app.config.ts index cc68536..13425d5 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,20 +1,25 @@ export default { + // 组件按需注入:未在 WXML 中声明的自定义组件不注入对应代码,削减启动注入开销 + lazyCodeLoading: 'requiredComponents', pages: [ 'pages/index/index', 'pages/login/index', 'pages/mine/index', 'pages/scene/select', 'pages/device/index', - 'pages/device/detail', - 'pages/device/add', - 'pages/device/config', - 'pages/device/ble-config', - 'pages/family/index', - 'pages/family/detail', - 'pages/family/contact/add', - 'pages/family/contact/detail', 'pages/event/index' ], + // 低频页面分包:设备详情/配置与家庭圈系列从主包剥离,压主包体积 + subPackages: [ + { + root: 'pages/device/feature', + pages: ['detail', 'add', 'config', 'ble-config'] + }, + { + root: 'pages/family', + pages: ['index', 'detail', 'contact/add', 'contact/detail'] + } + ], window: { navigationStyle: 'custom', // 与现状一致:所有页自绘 Navbar,避免原生导航栏与自研 Navbar 双栏 navigationBarTextStyle: 'black', diff --git a/src/constants/routes.ts b/src/constants/routes.ts index a31d18e..b720ca0 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -4,10 +4,10 @@ export const ROUTES = Object.freeze({ EVENT: '/pages/event/index', MINE: '/pages/mine/index', DEVICE_LIST: '/pages/device/index', - DEVICE_ADD: '/pages/device/add', - DEVICE_BLE_CONFIG: '/pages/device/ble-config', - DEVICE_CONFIG: '/pages/device/config', - DEVICE_DETAIL: '/pages/device/detail', + DEVICE_ADD: '/pages/device/feature/add', + DEVICE_BLE_CONFIG: '/pages/device/feature/ble-config', + DEVICE_CONFIG: '/pages/device/feature/config', + DEVICE_DETAIL: '/pages/device/feature/detail', FAMILY: '/pages/family/index', FAMILY_DETAIL: '/pages/family/detail', FAMILY_CONTACT_ADD: '/pages/family/contact/add', diff --git a/src/http/client.ts b/src/http/client.ts index 159e00e..50f0111 100644 --- a/src/http/client.ts +++ b/src/http/client.ts @@ -12,8 +12,8 @@ const GATEWAY = process.env.NODE_ENV === 'development' const USER_BASE = `${GATEWAY}/api/user` const BIZ_BASE = `${GATEWAY}/api/biz` -// 后端约定的未登录业务码(占位 '401',联调确认后更新——spec D5/Open Questions 1) -const UNAUTHORIZED_CODES = new Set(['401']) +// 后端约定的未登录业务码(202 为 2026-09-17 内网网关实测;401 为 HTTP 状态码分支) +const UNAUTHORIZED_CODES = new Set(['202', '401']) // 防重入:跳转登录页进行中不重复触发(并发 401 只踢一次) let redirectingToLogin = false diff --git a/src/pages/device/add.module.scss b/src/pages/device/feature/add.module.scss similarity index 100% rename from src/pages/device/add.module.scss rename to src/pages/device/feature/add.module.scss diff --git a/src/pages/device/add.tsx b/src/pages/device/feature/add.tsx similarity index 99% rename from src/pages/device/add.tsx rename to src/pages/device/feature/add.tsx index 7ed5ae1..54dda95 100644 --- a/src/pages/device/add.tsx +++ b/src/pages/device/feature/add.tsx @@ -148,7 +148,7 @@ export default function DeviceAdd() { className={`${styles.roomChip} ${roomId === room.roomId ? styles.roomChipActive : ''}`} onClick={() => setRoomId(room.roomId)} > - {room.roomName} + {room.name} ))} diff --git a/src/pages/device/ble-config.module.scss b/src/pages/device/feature/ble-config.module.scss similarity index 100% rename from src/pages/device/ble-config.module.scss rename to src/pages/device/feature/ble-config.module.scss diff --git a/src/pages/device/ble-config.tsx b/src/pages/device/feature/ble-config.tsx similarity index 100% rename from src/pages/device/ble-config.tsx rename to src/pages/device/feature/ble-config.tsx diff --git a/src/pages/device/config.module.scss b/src/pages/device/feature/config.module.scss similarity index 100% rename from src/pages/device/config.module.scss rename to src/pages/device/feature/config.module.scss diff --git a/src/pages/device/config.tsx b/src/pages/device/feature/config.tsx similarity index 100% rename from src/pages/device/config.tsx rename to src/pages/device/feature/config.tsx diff --git a/src/pages/device/detail.module.scss b/src/pages/device/feature/detail.module.scss similarity index 100% rename from src/pages/device/detail.module.scss rename to src/pages/device/feature/detail.module.scss diff --git a/src/pages/device/detail.tsx b/src/pages/device/feature/detail.tsx similarity index 100% rename from src/pages/device/detail.tsx rename to src/pages/device/feature/detail.tsx diff --git a/src/pages/device/index.module.scss b/src/pages/device/index.module.scss index 5c4286f..ad95322 100644 --- a/src/pages/device/index.module.scss +++ b/src/pages/device/index.module.scss @@ -108,6 +108,31 @@ font-weight: 600; } +// 房间筛选条:横向滚动 pill,默认「全部」 +.roomBar { + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + padding: 0 24px 24px; + gap: 16px; +} + +.roomChip { + flex-shrink: 0; + padding: 12px 32px; + background: $color-bg-white; + border-radius: $radius-pill; + box-shadow: $shadow-card; +} + +.roomChipActive { + background: $color-primary-grad; +} + +.roomChipTextActive { + color: #ffffff; +} + // 设备网格 .grid { display: flex; diff --git a/src/pages/device/index.tsx b/src/pages/device/index.tsx index 2f9c833..72d5f16 100644 --- a/src/pages/device/index.tsx +++ b/src/pages/device/index.tsx @@ -7,6 +7,7 @@ import { Loading } from '@/components/loading' import { StateView } from '@/components/state-view' import { BottomSheet } from '@/components/bottom-sheet' import { getHomeDeviceList, unbindHomeDevice, type HomeDevice } from '@/api/homeDevice' +import { getRoomList, type HomeRoom } from '@/api/homeRoom' import { getHomeList, type HomeInfo } from '@/api/home' import { ROUTES } from '@/constants/routes' import { navigateTo } from '@/utils/navigation' @@ -15,6 +16,9 @@ import styles from './index.module.scss' const FALLBACK_ICON = '⌂' +// 设备显示名:优先 name,未命名回落序列号 +const deviceLabel = (device: HomeDevice): string => device.name || device.deviceName + export default function DeviceList() { const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(false) @@ -26,8 +30,12 @@ export default function DeviceList() { const [familySheetOpen, setFamilySheetOpen] = useState(false) const [pendingFamily, setPendingFamily] = useState(null) - // 设备列表随家庭联动:useDidShow 覆盖首次进入/返回/tab 切回三种场景 - const loadDevices = useCallback(async (homeId?: string) => { + // 房间筛选:'' 为全部房间 + const [rooms, setRooms] = useState([]) + const [filterRoomId, setFilterRoomId] = useState('') + + // 设备列表随家庭 + 房间筛选联动 + const loadDevices = useCallback(async (homeId?: string, roomId?: string) => { const targetHomeId = homeId ?? currentFamily?.homeId if (!targetHomeId) { setDeviceList([]) @@ -37,7 +45,10 @@ export default function DeviceList() { setLoading(true) setLoadError(false) try { - const data = await getHomeDeviceList({ homeId: targetHomeId }) + const data = await getHomeDeviceList({ + homeId: targetHomeId, + roomId: roomId || undefined + }) setDeviceList(data ?? []) } catch { setLoadError(true) @@ -53,7 +64,11 @@ export default function DeviceList() { // 若当前未选中,默认选第一条 setCurrentFamily((prev) => { const next = prev ?? list[0] ?? null - if (next && next.homeId !== prev?.homeId) loadDevices(next.homeId) + if (next && next.homeId !== prev?.homeId) { + setFilterRoomId('') + loadDevices(next.homeId) + loadRooms(next.homeId) + } return next }) } catch { @@ -61,15 +76,34 @@ export default function DeviceList() { } }, [loadDevices]) + // 房间列表随家庭加载,供筛选条使用;失败静默(仅少了筛选能力) + const loadRooms = useCallback(async (homeId?: string) => { + const targetHomeId = homeId ?? currentFamily?.homeId + if (!targetHomeId) return + try { + const list = await getRoomList(targetHomeId) + setRooms(list ?? []) + } catch { + setRooms([]) + } + }, [currentFamily?.homeId]) + + const selectRoom = (roomId: string) => { + if (roomId === filterRoomId) return + setFilterRoomId(roomId) + loadDevices(undefined, roomId) + } + useDidShow(() => { - loadDevices() + loadDevices(undefined, filterRoomId) loadFamilies() + loadRooms() }) const goDetail = (device: HomeDevice) => { - const name = encodeURIComponent(device.deviceName) + const name = encodeURIComponent(deviceLabel(device)) navigateTo( - `${ROUTES.DEVICE_DETAIL}?id=${device.deviceId}&name=${name}&model=${device.deviceType}&online=${device.online ? 1 : 0}` + `${ROUTES.DEVICE_DETAIL}?id=${device.installId}&name=${name}&model=${device.subDeviceType ?? ''}&online=${device.onlineStatus ? 1 : 0}` ) } const goAdd = () => navigateTo(ROUTES.DEVICE_ADD) @@ -80,7 +114,7 @@ export default function DeviceList() { if (unbinding) return Taro.showModal({ title: '解绑设备', - content: `确定解绑「${device.deviceName}」吗?解绑后需重新添加。`, + content: `确定解绑「${deviceLabel(device)}」吗?解绑后需重新添加。`, confirmText: '解绑', confirmColor: '#EE4B4B', success: (res) => { @@ -118,7 +152,9 @@ export default function DeviceList() { const confirmFamilySheet = () => { if (pendingFamily && pendingFamily.homeId !== currentFamily?.homeId) { setCurrentFamily(pendingFamily) + setFilterRoomId('') loadDevices(pendingFamily.homeId) + loadRooms(pendingFamily.homeId) } closeFamilySheet() } @@ -163,6 +199,30 @@ export default function DeviceList() { + {/* 房间筛选条:横向滚动 chip,随当前家庭房间列表渲染 */} + {rooms.length > 0 && ( + + selectRoom('')} + > + 全部 + + {rooms.map((room) => { + const active = filterRoomId === room.roomId + return ( + selectRoom(room.roomId)} + > + {room.name} + + ) + })} + + )} + {/* 设备网格 */} {loading ? ( @@ -173,16 +233,16 @@ export default function DeviceList() { ) : ( {deviceList.map((device) => ( - + goDetail(device)}> - + {FALLBACK_ICON} - {device.deviceName} + {deviceLabel(device)} - - - {device.online ? '在线' : '离线'} + + + {device.onlineStatus ? '在线' : '离线'} diff --git a/src/pages/event/index.tsx b/src/pages/event/index.tsx index 3923d16..eddcb5c 100644 --- a/src/pages/event/index.tsx +++ b/src/pages/event/index.tsx @@ -60,7 +60,7 @@ export default function Event() { if (home) loadAlarms(filters) }) - const toRoomOption = (r: HomeRoom): FilterOption => ({ id: r.roomId, name: r.roomName }) + const toRoomOption = (r: HomeRoom): FilterOption => ({ id: r.roomId, name: r.name }) const closeDrawer = useCallback(() => setDrawer(''), []) diff --git a/src/pages/scene/select.tsx b/src/pages/scene/select.tsx index d3735db..f47e207 100644 --- a/src/pages/scene/select.tsx +++ b/src/pages/scene/select.tsx @@ -72,7 +72,7 @@ export default function SceneSelect() { const openEditForm = (room: HomeRoom) => { setEditingRoom(room) - setForm({ name: room.roomName, sence: room.sence ?? '' }) + setForm({ name: room.name, sence: room.sence ?? '' }) setFormOpen(true) } @@ -138,7 +138,7 @@ export default function SceneSelect() { {list.map((room) => ( openEditForm(room)}> - {room.roomName} + {room.name} {room.sence && ROOM_SENCE_MAP[room.sence] && ( {ROOM_SENCE_MAP[room.sence]} )} @@ -200,7 +200,7 @@ export default function SceneSelect() { e.stopPropagation()}> 删除房间 - 确定删除「{deletingRoom.roomName}」吗?该房间下的设备需重新分配。 + 确定删除「{deletingRoom.name}」吗?该房间下的设备需重新分配。 setDeletingRoom(null)}> diff --git a/tests/unit/http/client.spec.ts b/tests/unit/http/client.spec.ts index ace49e7..6d91ee5 100644 --- a/tests/unit/http/client.spec.ts +++ b/tests/unit/http/client.spec.ts @@ -142,6 +142,16 @@ describe('登录态失效踢回登录页', () => { expect(mockReLaunch).toHaveBeenCalledWith('/pages/login/index') }) + it("业务码 '202'(网关未登录实测码):同样踢登录并抛 AuthExpiredError", async () => { + mockRequest.mockResolvedValue({ + statusCode: 200, + data: { Code: '202', Message: '您不在登录状态,或者登录已经过期了。', Content: null } + }) + await expect(request.get('/yanglao/app/ylHome/getList')).rejects.toBeInstanceOf(AuthExpiredError) + expect(mockDispatch).toHaveBeenCalledWith({ type: 'auth/clearAuth' }) + expect(mockReLaunch).toHaveBeenCalledWith('/pages/login/index') + }) + it('HTTP 401 同样触发踢登录并抛 AuthExpiredError', async () => { mockRequest.mockResolvedValue({ statusCode: 401, data: { Code: '401', Message: 'token 无效' } }) await expect(request.get('/demo')).rejects.toMatchObject({ name: 'AuthExpiredError' })