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; }
.banner {
min-height: 180px;
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;
.page {
min-height: 100vh;
background: $color-bg;
padding-bottom: 40px;
}
.stateWrap {
@ -54,7 +15,7 @@
margin-top: 40px;
width: 240px;
height: 80px;
background: #2ccb98;
background: $color-primary;
border-radius: 40px;
display: flex;
align-items: center;
@ -63,86 +24,116 @@
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: #ffffff;
border-radius: 24px;
// ===== 家庭圈入口 =====
.familyEntry {
margin: 24px;
padding-left: 48px;
display: flex;
flex-direction: column;
align-items: center;
}
.icon {
.familyIcon {
width: 108px;
height: 108px;
border-radius: 50%;
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
border-radius: $radius-circle;
background: $color-primary-grad;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.iconOffline { background: #eef0f4; }
.iconText {
font-size: 36px;
color: #ffffff;
.familyEmoji {
font-size: 56px;
line-height: 1;
}
.iconOffline .iconText { color: #b5b9c0; }
.name {
max-width: 100%;
font-size: 28px;
font-weight: 600;
color: #2a2d33;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusRow {
.familyText {
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;
align-items: center;
}
.dot {
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 10px;
.rowLabel {
font-size: 28px;
color: $color-text-secondary;
}
.dotOn { background: #2ccb98; }
.dotOff { background: #c0c4cc; }
.rowValue {
font-size: 28px;
color: $color-text;
}
.statusText { font-size: 24px; color: #12a37a; }
.statusTextOff { color: #9ea1a6; }
// ===== 告警消息列表 =====
.messageList {
padding: 0 24px;
}
.fab {
position: fixed;
right: 40px;
bottom: 60px;
width: 112px;
height: 112px;
border-radius: 50%;
background: linear-gradient(135deg, #1fca97 0%, #2dbf9c 100%);
.messageCard {
margin-bottom: 24px;
padding: 32px;
background: $color-bg-white;
border-radius: $radius-lg;
box-shadow: $shadow-card;
}
.messageHead {
display: flex;
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 { 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 { getEventList, type EventItem } from '@/api/event'
import { ROUTES } from '@/constants/routes'
import { navigateTo } from '@/utils/navigation'
import { showToast } from '@/utils/toast'
import { splitAlerts } from './splitAlerts'
import styles from './index.module.scss'
const FALLBACK_ICON = '⌂'
export default function Index() {
const dispatch = useAppDispatch()
const scene = useAppSelector((s) => s.scene.current)
const deviceList = useAppSelector((s) => s.device.list)
const [alertList, setAlertList] = useState<EventItem[]>([])
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false)
// 在线状态需每次进入页面都新鲜(spec 明确):useDidShow 覆盖
// 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景,与 uni-app onShow 等价
const loadDevices = useCallback(async () => {
// 每次进入页面重拉全量告警(与事件页"全部"筛选同参数)
const loadAlerts = useCallback(async () => {
setLoading(true)
setLoadError(false)
try {
const data = await getDeviceList(scene?.id ?? null)
dispatch(setDeviceList(data))
const list = await getEventList({ sceneId: 0, status: 0, startDate: '', endDate: '' })
setAlertList(list)
} catch {
setLoadError(true)
} finally {
setLoading(false)
}
}, [dispatch, scene?.id])
}, [])
useDidShow(() => {
loadDevices()
loadAlerts()
})
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 { unhandled, messages } = useMemo(() => splitAlerts(alertList), [alertList])
const iconOf = (model: DeviceModel) => DEVICE_ICON_MAP[model] ?? FALLBACK_ICON
const goFamily = () => navigateTo(ROUTES.FAMILY)
const goAlertDetail = () => showToast('告警详情即将上线')
return (
<View className={styles.page}>
<Navbar title="智慧养老" />
<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>
<Navbar title="智慧养老" autoBack={false} />
{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 className={styles.retryBtn} onClick={loadAlerts}><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>
<>
<FamilyEntry onClick={goFamily} />
{unhandled.length > 0 && (
<UnhandledAlert alert={unhandled[0]} onClick={goAlertDetail} />
)}
{messages.length > 0 && (
<View className={styles.messageList}>
{messages.map((alert) => (
<AlertMessageItem key={alert.id} alert={alert} onClick={goAlertDetail} />
))}
</View>
)}
<View className={styles.fab} onClick={goAdd}>
<Text className={styles.fabIcon}>+</Text>
{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>
)
}