SEC_Warehouse/src/pages/device/index.tsx

305 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useCallback } from 'react'
import { useDidShow } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading'
import { StateView } from '@/components/state-view'
import { BottomSheet } from '@/components/bottom-sheet'
import { store } from '@/store'
import { storage } from '@/utils/storage'
import { getHomeDeviceList, type HomeDevice } from '@/api/homeDevice'
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
import { getHomeList, type HomeInfo } from '@/api/home'
import { resolveCurrentHome, setCurrentHomeId } from '@/store/currentHome'
import { ROUTES } from '@/constants/routes'
import { navigateTo } from '@/utils/navigation'
import ed713Icon from '@/assets/device/ed713.png'
import ed719Icon from '@/assets/device/ed719.png'
import eb15Icon from '@/assets/device/eb15.jpg'
import styles from './index.module.scss'
const FALLBACK_ICON = '⌂'
// 设备型号 → 产品图:sub-device 优先(EB15 是 EP832 网关下的子设备,
// 在 deviceModel 中没有对应值,需要走 subDeviceType 分支)。后端有时把型号
// 放在 deviceModel、有时放 subDeviceType,所以两个字段都查一遍
const pickDeviceIcon = (device: HomeDevice): string | null => {
const model = device.subDeviceType ?? device.deviceModel
if (model === 'EB15') return eb15Icon
if (model === 'ED713') return ed713Icon
if (model === 'ED719') return ed719Icon
return null
}
// 设备显示名:优先 name,未命名回落序列号
const deviceLabel = (device: HomeDevice): string => device.name || device.deviceName
export default function DeviceList() {
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false)
const [deviceList, setDeviceList] = useState<HomeDevice[]>([])
const [families, setFamilies] = useState<HomeInfo[]>([])
const [currentFamily, setCurrentFamily] = useState<HomeInfo | null>(null)
const [familySheetOpen, setFamilySheetOpen] = useState(false)
const [pendingFamily, setPendingFamily] = useState<HomeInfo | null>(null)
// 房间筛选:'' 为全部房间
const [rooms, setRooms] = useState<HomeRoom[]>([])
const [filterRoomId, setFilterRoomId] = useState('')
// 游客浏览:未登录不拉数据,展示引导登录(微信审核要求不强制授权)
const isGuest = () =>
!store.getState().auth.token && !storage.get<string>('auth_token')
const isGuestEmpty = isGuest()
// 设备列表随家庭 + 房间筛选联动
const loadDevices = useCallback(async (homeId?: string, roomId?: string) => {
const targetHomeId = homeId ?? currentFamily?.homeId
if (!targetHomeId) {
setDeviceList([])
setLoading(false)
return
}
setLoading(true)
setLoadError(false)
try {
const data = await getHomeDeviceList({
homeId: targetHomeId,
roomId: roomId || undefined
})
setDeviceList(data ?? [])
} catch {
setLoadError(true)
} finally {
setLoading(false)
}
}, [currentFamily?.homeId])
const loadFamilies = useCallback(async () => {
try {
const list = await getHomeList()
setFamilies(list)
// 未选中时按持久化的 homeId 恢复,失效则回落第一条
setCurrentFamily((prev) => {
const next = prev ?? resolveCurrentHome(list)
if (next && next.homeId !== prev?.homeId) {
setCurrentHomeId(next.homeId)
setFilterRoomId('')
loadDevices(next.homeId)
loadRooms(next.homeId)
}
return next
})
} catch {
// 静默:家庭圈加载失败不阻断设备列表
}
}, [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(() => {
if (isGuest()) {
setDeviceList([])
setLoading(false)
setLoadError(false)
return
}
loadDevices(undefined, filterRoomId)
loadFamilies()
loadRooms()
})
// EventChannel 把整个设备对象传给详情页:首屏零延迟渲染体征快照
const goDetail = (device: HomeDevice) => {
const name = encodeURIComponent(deviceLabel(device))
Taro.navigateTo({
url: `${ROUTES.DEVICE_DETAIL}?id=${device.installId}&name=${name}&model=${device.subDeviceType ?? ''}&online=${device.onlineStatus ? 1 : 0}`,
success: (res) => res.eventChannel.emit('device', device)
})
}
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
const goLogin = () => navigateTo(ROUTES.LOGIN)
const openFamilySheet = () => {
setPendingFamily(currentFamily)
setFamilySheetOpen(true)
}
const closeFamilySheet = () => {
setFamilySheetOpen(false)
setPendingFamily(null)
}
const confirmFamilySheet = () => {
if (pendingFamily && pendingFamily.homeId !== currentFamily?.homeId) {
setCurrentFamily(pendingFamily)
setCurrentHomeId(pendingFamily.homeId)
setFilterRoomId('')
loadDevices(pendingFamily.homeId)
loadRooms(pendingFamily.homeId)
}
closeFamilySheet()
}
return (
<View className={styles.page}>
<Navbar title="智能设备" autoBack={false} variant="hero" />
{/* 绿色衬底:承接浮卡的上浮区,避免 -40px 压到导航标题 */}
<View className={styles.heroSpace} />
{/* 设备应用家庭信息卡 */}
<View className={styles.familyCard}>
<View className={styles.familyRow}>
<Text className={styles.familyLabel}>设备应用家庭</Text>
<Text className={styles.familyValue}>{currentFamily?.name ?? '未选择'}</Text>
</View>
<View className={styles.familyRow}>
<Text className={styles.familyLabel}>关爱对象</Text>
<Text className={styles.familyValue}>
{currentFamily ? (currentFamily.careMemberCount > 0 ? `${currentFamily.careMemberCount} 人` : '--') : '--'}
</Text>
</View>
<View className={styles.familyRow}>
<Text className={styles.familyLabel}>关爱地址</Text>
<Text className={styles.familyValue}>{currentFamily?.address ?? '--'}</Text>
</View>
<View className={styles.switchBtn} hoverClass={styles.switchBtnPressed} onClick={openFamilySheet}>
<Text className={styles.switchBtnText}>切换家庭圈</Text>
</View>
</View>
{/* 房间管理 + 添加设备 两张并排 */}
<View className={styles.funcRow}>
<View className={styles.funcCard} hoverClass={styles.funcCardPressed} onClick={goRoom}>
<View className={styles.funcIconWrap}>
<Text className={styles.funcIcon}>⌂</Text>
</View>
<Text className={styles.funcText}>房间管理</Text>
</View>
<View className={styles.funcCard} hoverClass={styles.funcCardPressed} onClick={goAdd}>
<View className={styles.funcIconWrap}>
<Text className={styles.funcIcon}>+</Text>
</View>
<Text className={styles.funcText}>添加设备</Text>
</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 && !loadError && !isGuestEmpty && (
<View className={styles.sectionTitle}>
<Text>我的设备</Text>
<Text className={styles.sectionCount}>{deviceList.length}</Text>
</View>
)}
{/* 设备网格 */}
{loading ? (
<View className={styles.stateWrap}><Loading /></View>
) : isGuestEmpty ? (
<StateView status="error" mode="list" text="登录后查看你的设备" retryText="去登录" onRetry={goLogin} />
) : loadError ? (
<StateView status="error" onRetry={() => loadDevices()} />
) : deviceList.length === 0 ? (
<StateView status="error" mode="list" text="暂无设备" retryText="添加设备" onRetry={goAdd} />
) : (
<View className={styles.grid}>
{deviceList.map((device) => (
<View key={device.installId} className={styles.card}>
<View className={styles.cardTop} onClick={() => goDetail(device)}>
<View className={styles.icon}>
{(() => {
const src = pickDeviceIcon(device)
return src ? (
<Image className={styles.iconImg} src={src} mode="aspectFit" />
) : (
<Text className={styles.iconText}>{FALLBACK_ICON}</Text>
)
})()}
</View>
<Text className={styles.name}>{deviceLabel(device)}</Text>
<View className={styles.statusRow}>
<View className={`${styles.dot} ${device.onlineStatus ? styles.dotOn : styles.dotOff}`} />
<Text className={`${styles.statusText} ${!device.onlineStatus ? styles.statusTextOff : ''}`}>
{device.onlineStatus ? '在线' : '离线'}
</Text>
</View>
</View>
</View>
))}
</View>
)}
{/* 切换家庭圈弹层 */}
<BottomSheet visible={familySheetOpen} title="切换家庭圈" onClose={closeFamilySheet}>
<View className={styles.sheetList}>
{families.map((family) => {
const active = pendingFamily?.homeId === family.homeId
return (
<View
key={family.homeId}
className={`${styles.sheetItem} ${active ? styles.sheetItemActive : ''}`}
onClick={() => setPendingFamily(family)}
>
<View className={styles.sheetHead}>
<Text className={styles.sheetName}>{family.name}</Text>
</View>
<View className={styles.sheetMeta}>
<Text className={styles.sheetMetaItem}>
关爱对象 {family.careMemberCount > 0 ? `${family.careMemberCount} 人` : '--'}
</Text>
<Text className={styles.sheetMetaItem}>设备数量 {family.deviceCount}台</Text>
</View>
</View>
)
})}
</View>
<View className={styles.sheetConfirm} hoverClass={styles.sheetConfirmPressed} onClick={confirmFamilySheet}>
<Text className={styles.sheetConfirmText}>确定</Text>
</View>
</BottomSheet>
</View>
)
}