feat(home): 首页重构为告警中心,移除 banner/设备网格/FAB

This commit is contained in:
ozh 2026-09-08 17:49:36 +08:00
parent ed1b3244b8
commit 727b91fb07
2 changed files with 172 additions and 164 deletions

View File

@ -1,46 +1,7 @@
.page { position: relative; } .page {
min-height: 100vh;
.banner { background: $color-bg;
min-height: 180px; padding-bottom: 40px;
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
padding: 48px 44px;
display: flex;
align-items: center;
}
.bannerLeft { display: flex; flex-direction: column; }
.bannerTitle {
font-size: 52px;
color: #ffffff;
font-weight: 700;
margin-bottom: 20px;
}
.bannerSub {
font-size: 34px;
color: #e9fff8;
font-weight: 600;
}
.siteRow {
height: 108px;
padding: 0 32px;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
}
.siteText {
font-size: 34px;
color: #222;
font-weight: 600;
}
.siteArrow {
font-size: 36px;
color: #26c996;
} }
.stateWrap { .stateWrap {
@ -54,7 +15,7 @@
margin-top: 40px; margin-top: 40px;
width: 240px; width: 240px;
height: 80px; height: 80px;
background: #2ccb98; background: $color-primary;
border-radius: 40px; border-radius: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -63,86 +24,116 @@
font-size: 28px; font-size: 28px;
} }
.grid { // ===== 家庭圈入口 =====
display: flex; .familyEntry {
flex-wrap: wrap; margin: 24px;
justify-content: space-between; padding-left: 48px;
padding: 24px;
}
.card {
width: 48.5%;
margin-bottom: 24px;
padding: 36px 20px 28px;
box-sizing: border-box;
background: #ffffff;
border-radius: 24px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.icon { .familyIcon {
width: 108px; width: 108px;
height: 108px; height: 108px;
border-radius: 50%; border-radius: $radius-circle;
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%); background: $color-primary-grad;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 20px;
} }
.iconOffline { background: #eef0f4; } .familyEmoji {
font-size: 56px;
.iconText { line-height: 1;
font-size: 36px;
color: #ffffff;
} }
.iconOffline .iconText { color: #b5b9c0; } .familyText {
.name {
max-width: 100%;
font-size: 28px;
font-weight: 600;
color: #2a2d33;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusRow {
margin-top: 12px; margin-top: 12px;
font-size: 28px;
color: $color-text;
}
// ===== 未处理告警卡片 =====
.unhandledCard {
position: relative;
margin: 0 24px 24px;
padding: 32px;
background: $color-bg-white;
border-radius: $radius-lg;
box-shadow: $shadow-card;
}
.unhandledTag {
position: absolute;
top: -16px;
left: -8px;
padding: 6px 18px;
background: $color-danger-light-bg;
color: $color-danger-deep;
border: 2px solid $color-danger-deep;
border-radius: $radius-sm;
font-size: 22px;
transform: rotate(-4deg);
box-shadow: 0 2px 8px rgba(213, 59, 29, 0.15);
}
.unhandledTitle {
font-size: 32px;
font-weight: 600;
color: $color-text;
}
.unhandledRow {
margin-top: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.dot { .rowLabel {
width: 14px; font-size: 28px;
height: 14px; color: $color-text-secondary;
border-radius: 50%;
margin-right: 10px;
} }
.dotOn { background: #2ccb98; } .rowValue {
.dotOff { background: #c0c4cc; } font-size: 28px;
color: $color-text;
}
.statusText { font-size: 24px; color: #12a37a; } // ===== 告警消息列表 =====
.statusTextOff { color: #9ea1a6; } .messageList {
padding: 0 24px;
}
.fab { .messageCard {
position: fixed; margin-bottom: 24px;
right: 40px; padding: 32px;
bottom: 60px; background: $color-bg-white;
width: 112px; border-radius: $radius-lg;
height: 112px; box-shadow: $shadow-card;
border-radius: 50%; }
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
.messageHead {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
box-shadow: 0 8px 24px rgba(31, 202, 151, 0.4);
} }
.fabIcon { font-size: 48px; color: #ffffff; font-weight: 300; } .messageDot {
width: 16px;
height: 16px;
border-radius: $radius-circle;
background: $color-danger-deep;
margin-right: 12px;
}
.messageTitle {
font-size: 30px;
font-weight: 600;
color: $color-text;
}
.messageTime {
margin-top: 12px;
font-size: 24px;
color: $color-text-secondary;
}

View File

@ -1,105 +1,122 @@
import { useState, useCallback } from 'react' import { useState, useCallback, useMemo } from 'react'
import { useDidShow } from '@tarojs/taro' import { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components' 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 { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading' import { Loading } from '@/components/loading'
import { Empty } from '@/components/empty' import { Empty } from '@/components/empty'
import { getDeviceList } from '@/api/device' import { getEventList, type EventItem } from '@/api/event'
import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { navigateTo } from '@/utils/navigation' import { navigateTo } from '@/utils/navigation'
import { showToast } from '@/utils/toast'
import { splitAlerts } from './splitAlerts'
import styles from './index.module.scss' import styles from './index.module.scss'
const FALLBACK_ICON = '⌂'
export default function Index() { export default function Index() {
const dispatch = useAppDispatch() const [alertList, setAlertList] = useState<EventItem[]>([])
const scene = useAppSelector((s) => s.scene.current)
const deviceList = useAppSelector((s) => s.device.list)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false) const [loadError, setLoadError] = useState(false)
// 在线状态需每次进入页面都新鲜(spec 明确):useDidShow 覆盖 // 每次进入页面重拉全量告警(与事件页"全部"筛选同参数)
// 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景,与 uni-app onShow 等价 const loadAlerts = useCallback(async () => {
const loadDevices = useCallback(async () => {
setLoading(true) setLoading(true)
setLoadError(false) setLoadError(false)
try { try {
const data = await getDeviceList(scene?.id ?? null) const list = await getEventList({ sceneId: 0, status: 0, startDate: '', endDate: '' })
dispatch(setDeviceList(data)) setAlertList(list)
} catch { } catch {
setLoadError(true) setLoadError(true)
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [dispatch, scene?.id]) }, [])
useDidShow(() => { useDidShow(() => {
loadDevices() loadAlerts()
}) })
const goDetail = (device: Device) => { const { unhandled, messages } = useMemo(() => splitAlerts(alertList), [alertList])
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 const goFamily = () => navigateTo(ROUTES.FAMILY)
const goAlertDetail = () => showToast('告警详情即将上线')
return ( return (
<View className={styles.page}> <View className={styles.page}>
<Navbar title="智慧养老" /> <Navbar title="智慧养老" autoBack={false} />
<View className={styles.banner}>
<View className={styles.bannerLeft}>
<Text className={styles.bannerTitle}>智慧养老云助手</Text>
<Text className={styles.bannerSub}>一键开启智慧好生活</Text>
</View>
</View>
<View className={styles.siteRow} onClick={goSceneSelect}>
<Text className={styles.siteText}>{scene?.name || '请选择场所'}</Text>
<Text className={styles.siteArrow}>›</Text>
</View>
{loading ? ( {loading ? (
<View className={styles.stateWrap}><Loading /></View> <View className={styles.stateWrap}><Loading /></View>
) : loadError ? ( ) : loadError ? (
<View className={styles.stateWrap}> <View className={styles.stateWrap}>
<Empty text="加载失败,请检查网络" mode="wifi" /> <Empty text="加载失败,请检查网络" mode="wifi" />
<View className={styles.retryBtn} onClick={loadDevices}><Text>重试</Text></View> <View className={styles.retryBtn} onClick={loadAlerts}><Text>重试</Text></View>
</View>
) : deviceList.length === 0 ? (
<View className={styles.stateWrap}>
<Empty text="暂无设备" />
<View className={styles.retryBtn} onClick={goAdd}><Text>添加设备</Text></View>
</View> </View>
) : ( ) : (
<View className={styles.grid}> <>
{deviceList.map((device) => ( <FamilyEntry onClick={goFamily} />
<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}> {unhandled.length > 0 && (
<Text className={styles.fabIcon}>+</Text> <UnhandledAlert alert={unhandled[0]} onClick={goAlertDetail} />
</View> )}
{messages.length > 0 && (
<View className={styles.messageList}>
{messages.map((alert) => (
<AlertMessageItem key={alert.id} alert={alert} onClick={goAlertDetail} />
))}
</View>
)}
{unhandled.length === 0 && messages.length === 0 && (
<View className={styles.stateWrap}>
<Empty text="暂无告警消息" mode="message" />
</View>
)}
</>
)}
</View>
)
}
function FamilyEntry({ onClick }: { onClick: () => void }) {
return (
<View className={styles.familyEntry} onClick={onClick}>
<View className={styles.familyIcon}>
<Text className={styles.familyEmoji}>👨‍👩‍👧</Text>
</View>
<Text className={styles.familyText}>家庭圈</Text>
</View>
)
}
function UnhandledAlert({ alert, onClick }: { alert: EventItem; onClick: () => void }) {
return (
<View className={styles.unhandledCard} onClick={onClick}>
<View className={styles.unhandledTag}><Text>未处理</Text></View>
<Text className={styles.unhandledTitle}>{alert.deviceName || '未知设备'}</Text>
{alert.sceneName && (
<View className={styles.unhandledRow}>
<Text className={styles.rowLabel}>家庭圈:</Text>
<Text className={styles.rowValue}>{alert.sceneName}</Text>
</View>
)}
{alert.time && (
<View className={styles.unhandledRow}>
<Text className={styles.rowLabel}>时间:</Text>
<Text className={styles.rowValue}>{alert.time}</Text>
</View>
)}
</View>
)
}
function AlertMessageItem({ alert, onClick }: { alert: EventItem; onClick: () => void }) {
return (
<View className={styles.messageCard} onClick={onClick}>
<View className={styles.messageHead}>
<View className={styles.messageDot} />
<Text className={styles.messageTitle}>告警消息</Text>
</View>
{alert.time && <Text className={styles.messageTime}>{alert.time}</Text>}
</View> </View>
) )
} }