feat(device-tab): 设备列表页从首页分离为独立页面

This commit is contained in:
ozh 2026-09-08 17:20:49 +08:00
parent 0fc0f2b91d
commit e3af7c26fb
2 changed files with 229 additions and 0 deletions

View File

@ -0,0 +1,131 @@
.page {
position: relative;
min-height: 100vh;
background: $color-bg;
padding-bottom: 40px;
}
.siteRow {
height: 108px;
padding: 0 32px;
background: $color-bg-white;
display: flex;
align-items: center;
justify-content: space-between;
}
.siteText {
font-size: 34px;
color: $color-text;
font-weight: 600;
}
.siteArrow {
font-size: 36px;
color: $color-primary;
}
.stateWrap {
margin-top: 80px;
display: flex;
flex-direction: column;
align-items: center;
}
.retryBtn {
margin-top: 40px;
width: 240px;
height: 80px;
background: $color-primary;
border-radius: 40px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 28px;
}
.grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 24px;
}
.card {
width: 48.5%;
margin-bottom: 24px;
padding: 36px 20px 28px;
box-sizing: border-box;
background: $color-bg-white;
border-radius: $radius-lg;
box-shadow: $shadow-card;
display: flex;
flex-direction: column;
align-items: center;
}
.icon {
width: 108px;
height: 108px;
border-radius: $radius-circle;
background: $color-primary-grad;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.iconOffline { background: $color-bg-disabled; }
.iconText {
font-size: 36px;
color: #ffffff;
}
.iconOffline .iconText { color: $color-text-tertiary; }
.name {
max-width: 100%;
font-size: 28px;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusRow {
margin-top: 12px;
display: flex;
align-items: center;
}
.dot {
width: 14px;
height: 14px;
border-radius: $radius-circle;
margin-right: 10px;
}
.dotOn { background: $color-primary; }
.dotOff { background: $color-text-placeholder; }
.statusText { font-size: 24px; color: $color-primary-deep; }
.statusTextOff { color: $color-text-secondary; }
.fab {
position: fixed;
right: 40px;
bottom: 60px;
width: 112px;
height: 112px;
border-radius: $radius-circle;
background: $color-primary-grad;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-fab;
}
.fabIcon { font-size: 48px; color: #ffffff; font-weight: 300; }

View File

@ -0,0 +1,98 @@
import { useState, useCallback } from 'react'
import { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { setDeviceList, type Device } from '@/store/deviceSlice'
import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading'
import { Empty } from '@/components/empty'
import { getDeviceList } from '@/api/device'
import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums'
import { ROUTES } from '@/constants/routes'
import { navigateTo } from '@/utils/navigation'
import styles from './index.module.scss'
const FALLBACK_ICON = '⌂'
export default function DeviceList() {
const dispatch = useAppDispatch()
const scene = useAppSelector((s) => s.scene.current)
const deviceList = useAppSelector((s) => s.device.list)
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false)
// 在线状态需每次进入页面都新鲜:useDidShow 覆盖
// 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景
const loadDevices = useCallback(async () => {
setLoading(true)
setLoadError(false)
try {
const data = await getDeviceList(scene?.id ?? null)
dispatch(setDeviceList(data))
} catch {
setLoadError(true)
} finally {
setLoading(false)
}
}, [dispatch, scene?.id])
useDidShow(() => {
loadDevices()
})
const goDetail = (device: Device) => {
const name = encodeURIComponent(device.name)
navigateTo(`${ROUTES.DEVICE_DETAIL}?id=${device.id}&name=${name}&model=${device.model}&online=${device.online ? 1 : 0}`)
}
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
const goSceneSelect = () => navigateTo(ROUTES.SCENE_SELECT)
const iconOf = (model: DeviceModel) => DEVICE_ICON_MAP[model] ?? FALLBACK_ICON
return (
<View className={styles.page}>
<Navbar title="智能设备" autoBack={false} />
<View className={styles.siteRow} onClick={goSceneSelect}>
<Text className={styles.siteText}>{scene?.name || '请选择场所'}</Text>
<Text className={styles.siteArrow}>›</Text>
</View>
{loading ? (
<View className={styles.stateWrap}><Loading /></View>
) : loadError ? (
<View className={styles.stateWrap}>
<Empty text="加载失败,请检查网络" mode="wifi" />
<View className={styles.retryBtn} onClick={loadDevices}><Text>重试</Text></View>
</View>
) : deviceList.length === 0 ? (
<View className={styles.stateWrap}>
<Empty text="暂无设备" />
<View className={styles.retryBtn} onClick={goAdd}><Text>添加设备</Text></View>
</View>
) : (
<View className={styles.grid}>
{deviceList.map((device) => (
<View key={device.id} className={styles.card} onClick={() => goDetail(device)}>
<View className={`${styles.icon} ${!device.online ? styles.iconOffline : ''}`}>
<Text className={styles.iconText}>{iconOf(device.model)}</Text>
</View>
<Text className={styles.name}>{device.name}</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 ? '在线' : '离线'}
</Text>
</View>
</View>
))}
</View>
)}
<View className={styles.fab} onClick={goAdd}>
<Text className={styles.fabIcon}>+</Text>
</View>
</View>
)
}