feat(device-tab): 设备列表页从首页分离为独立页面
This commit is contained in:
parent
0fc0f2b91d
commit
e3af7c26fb
|
|
@ -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; }
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue