diff --git a/src/pages/device/index.module.scss b/src/pages/device/index.module.scss new file mode 100644 index 0000000..eeaa140 --- /dev/null +++ b/src/pages/device/index.module.scss @@ -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; } diff --git a/src/pages/device/index.tsx b/src/pages/device/index.tsx new file mode 100644 index 0000000..fe5ddd2 --- /dev/null +++ b/src/pages/device/index.tsx @@ -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 ( + + + + + {scene?.name || '请选择场所'} + › + + + {loading ? ( + + ) : loadError ? ( + + + 重试 + + ) : deviceList.length === 0 ? ( + + + 添加设备 + + ) : ( + + {deviceList.map((device) => ( + goDetail(device)}> + + {iconOf(device.model)} + + {device.name} + + + + {device.online ? '在线' : '离线'} + + + + ))} + + )} + + + + + + + ) +}