feat(build): 组件按需注入+分包+生产关 sourcemap,主包压至 0.42M

- app.config.ts 开启 lazyCodeLoading=requiredComponents
- 设备详情/添加/配置/BLE 迁移至 pages/device/feature 分包,家庭圈系列独立分包
- 路由常量同步分包路径;prod 构建关 devtool 剔除 .map
- 生产实测:主包 426K(目标 <1.5M 达成),family 分包 35K、device/feature 86K
- 附带:未登录业务码 202 接入踢登录(网关实测),房间/设备响应字段按联调实测修正
This commit is contained in:
ozh 2026-09-18 17:07:26 +08:00
parent 6377e53ecb
commit ccca973d62
19 changed files with 156 additions and 42 deletions

View File

@ -1 +1,8 @@
export default {}
export default {
mini: {
// 生产关 sourcemap:主包体积硬指标 1.5M,.map 文件约 390K 必须剔除
webpackChain(chain: { devtool: (v: false) => void }) {
chain.devtool(false)
}
}
}

View File

@ -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<HomeDevice[]> {

View File

@ -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)

View File

@ -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',

View File

@ -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',

View File

@ -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

View File

@ -148,7 +148,7 @@ export default function DeviceAdd() {
className={`${styles.roomChip} ${roomId === room.roomId ? styles.roomChipActive : ''}`}
onClick={() => setRoomId(room.roomId)}
>
<Text>{room.roomName}</Text>
<Text>{room.name}</Text>
</View>
))}
</View>

View File

@ -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;

View File

@ -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<HomeInfo | null>(null)
// 设备列表随家庭联动:useDidShow 覆盖首次进入/返回/tab 切回三种场景
const loadDevices = useCallback(async (homeId?: string) => {
// 房间筛选:'' 为全部房间
const [rooms, setRooms] = useState<HomeRoom[]>([])
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() {
</View>
</View>
{/* 房间筛选条:横向滚动 chip,随当前家庭房间列表渲染 */}
{rooms.length > 0 && (
<View className={styles.roomBar}>
<View
className={`${styles.roomChip} ${filterRoomId === '' ? styles.roomChipActive : ''}`}
onClick={() => selectRoom('')}
>
<Text className={filterRoomId === '' ? styles.roomChipTextActive : ''}>全部</Text>
</View>
{rooms.map((room) => {
const active = filterRoomId === room.roomId
return (
<View
key={room.roomId}
className={`${styles.roomChip} ${active ? styles.roomChipActive : ''}`}
onClick={() => selectRoom(room.roomId)}
>
<Text className={active ? styles.roomChipTextActive : ''}>{room.name}</Text>
</View>
)
})}
</View>
)}
{/* 设备网格 */}
{loading ? (
<View className={styles.stateWrap}><Loading /></View>
@ -173,16 +233,16 @@ export default function DeviceList() {
) : (
<View className={styles.grid}>
{deviceList.map((device) => (
<View key={device.installId || device.deviceId} className={styles.card}>
<View key={device.installId} className={styles.card}>
<View className={styles.cardTop} onClick={() => goDetail(device)}>
<View className={`${styles.icon} ${!device.online ? styles.iconOffline : ''}`}>
<View className={`${styles.icon} ${!device.onlineStatus ? styles.iconOffline : ''}`}>
<Text className={styles.iconText}>{FALLBACK_ICON}</Text>
</View>
<Text className={styles.name}>{device.deviceName}</Text>
<Text className={styles.name}>{deviceLabel(device)}</Text>
<View className={styles.statusRow}>
<View className={`${styles.dot} ${device.online ? styles.dotOn : styles.dotOff}`} />
<Text className={`${styles.statusText} ${!device.online ? styles.statusTextOff : ''}`}>
{device.online ? '在线' : '离线'}
<View className={`${styles.dot} ${device.onlineStatus ? styles.dotOn : styles.dotOff}`} />
<Text className={`${styles.statusText} ${!device.onlineStatus ? styles.statusTextOff : ''}`}>
{device.onlineStatus ? '在线' : '离线'}
</Text>
</View>
</View>

View File

@ -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(''), [])

View File

@ -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) => (
<View key={room.roomId} className={styles.item}>
<View className={styles.itemMain} onClick={() => openEditForm(room)}>
<Text className={styles.name}>{room.roomName}</Text>
<Text className={styles.name}>{room.name}</Text>
{room.sence && ROOM_SENCE_MAP[room.sence] && (
<Text className={styles.senceTag}>{ROOM_SENCE_MAP[room.sence]}</Text>
)}
@ -200,7 +200,7 @@ export default function SceneSelect() {
<View className={styles.delDialog} onClick={(e) => e.stopPropagation()}>
<Text className={styles.delTitle}>删除房间</Text>
<Text className={styles.delContent}>
确定删除「{deletingRoom.roomName}」吗?该房间下的设备需重新分配。
确定删除「{deletingRoom.name}」吗?该房间下的设备需重新分配。
</Text>
<View className={styles.delBtnRow}>
<View className={styles.delCancel} onClick={() => setDeletingRoom(null)}>

View File

@ -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' })