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:
parent
6377e53ecb
commit
ccca973d62
|
|
@ -1 +1,8 @@
|
|||
export default {}
|
||||
export default {
|
||||
mini: {
|
||||
// 生产关 sourcemap:主包体积硬指标 1.5M,.map 文件约 390K 必须剔除
|
||||
webpackChain(chain: { devtool: (v: false) => void }) {
|
||||
chain.devtool(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(''), [])
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
Loading…
Reference in New Issue