feat(event): 首页/消息中心接入告警列表真实接口,移除事件 mock 层

This commit is contained in:
ozh 2026-09-17 14:30:53 +08:00
parent 7bef729e93
commit 5761a5fed2
8 changed files with 247 additions and 130 deletions

View File

@ -1,26 +0,0 @@
import { request } from '@/http'
const USE_MOCK = true
export interface EventItem {
id: number
sceneName: string
deviceName: string
status: number
time: string
}
export interface EventQuery {
sceneId: number
status: number
startDate: string
endDate: string
}
// 事件列表接口未定:mock 返回空数组,页面保持空态
export function getEventList(_query: EventQuery): Promise<EventItem[]> {
if (USE_MOCK) {
return Promise.resolve([])
}
return request.get<EventItem[]>('/event/list', { ..._query })
}

View File

@ -1,16 +1,30 @@
import { request } from '@/http'
// 入参对齐 apifox YlHomeAlarmListReq(homeId 必填,其余可选筛选)
export interface AlarmListReq {
homeId: string
roomId?: string // 传入则只查该房间告警
status?: number // 告警状态,不传则不筛选
beginTime?: string // yyyy-MM-dd HH:mm:ss
endTime?: string
pageIndex?: number
pageSize?: number
}
// 骨架:本期仅锁定接口路径,字段类型联调时修正(spec D7)
export interface HomeAlarm {
alarmId: string
homeId: string
roomId: string
deviceId: string
deviceName: string
alarmType: string
alarmTime: string
handled: boolean
status: number // 告警状态:0-未处理 1-已处理 2-已忽略(待实测确认枚举)
}
export function getAlarmList(params: Record<string, unknown>): Promise<HomeAlarm[]> {
return request.get('/yanglao/app/ylHomeAlarm/getList', params)
export function getAlarmList(params: AlarmListReq): Promise<HomeAlarm[]> {
return request.post('/yanglao/app/ylHomeAlarm/getList', params as unknown as Record<string, unknown>)
}
export function getAlarmDetail(alarmId: string): Promise<HomeAlarm> {

View File

@ -7,8 +7,9 @@ export interface HomeRoom {
roomName: string
}
// 文档路径为 POST + body { homeId }(YlHomeRoomListReq)
export function getRoomList(homeId: string): Promise<HomeRoom[]> {
return request.get('/yanglao/app/ylHomeRoom/getList', { homeId })
return request.post('/yanglao/app/ylHomeRoom/getList', { homeId })
}
export function addRoom(params: Record<string, unknown>): Promise<void> {

View File

@ -68,6 +68,78 @@
color: $color-text-tertiary;
}
/* 告警卡片列表 */
.alarm-list {
padding: 24px;
}
.alarm-card {
background: $color-bg-white;
border-radius: $radius-lg;
box-shadow: $shadow-card;
padding: 28px 32px;
& + & { margin-top: 20px; }
}
.alarm-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.alarm-type {
font-size: 30px;
font-weight: 600;
color: $color-text;
}
.alarm-tag {
font-size: 22px;
padding: 4px 16px;
border-radius: $radius-pill;
color: $color-text-secondary;
background: $color-bg-gray;
}
/* 状态枚举待实测,先按未处理红色、已处理绿色、已忽略灰的常见语义 */
.tag1 {
color: $color-danger;
background: $color-danger-light-bg;
}
.tag2 {
color: $color-primary-deep;
background: $color-primary-light-bg;
}
.tag3 {
color: $color-text-tertiary;
background: $color-bg-gray;
}
.alarm-row {
display: flex;
align-items: flex-start;
min-height: 48px;
}
.alarm-label {
width: 120px;
flex-shrink: 0;
font-size: 26px;
color: $color-text-secondary;
}
.alarm-value {
flex: 1;
min-width: 0;
font-size: 26px;
color: $color-text;
word-break: break-all;
}
/* 数据层:三抽屉的定位锚点 */
.content-area {
position: relative;

View File

@ -1,92 +1,112 @@
import { useState, useCallback, useEffect } from 'react'
import { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Navbar } from '@/components/navbar'
import { Empty } from '@/components/empty'
import { getSceneList } from '@/api/scene'
import { getEventList, type EventItem } from '@/api/event'
import { getHomeList, type HomeInfo } from '@/api/home'
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
import { getAlarmList, type HomeAlarm } from '@/api/homeAlarm'
import { CalendarDrawer } from './calendar-drawer'
import styles from './index.module.scss'
interface FilterOption {
id: number
id: string // '' 表示全部
name: string
}
// 告警状态筛选枚举(后端 integer,待实测确认:1-未处理 2-已处理 3-已忽略)
const ALARM_STATUS: Record<string, string> = {
'1': '未处理',
'2': '已处理',
'3': '已忽略'
}
export default function Event() {
// '' | 'scene' | 'status' | 'time'
const [drawer, setDrawer] = useState<'' | 'scene' | 'status' | 'time'>('')
// 事件列表:后端接口定型前 mock 返回空数组 → 空态
const [eventList, setEventList] = useState<EventItem[]>([])
// '' | 'room' | 'status' | 'time'
const [drawer, setDrawer] = useState<'' | 'room' | 'status' | 'time'>('')
const [alarmList, setAlarmList] = useState<HomeAlarm[]>([])
const [loading, setLoading] = useState(true)
const [filters, setFilters] = useState({
sceneId: 0, // 0 表示全部
status: 0,
roomId: '',
status: '',
startDate: '',
endDate: '',
timeText: ''
})
const [sceneOptions, setSceneOptions] = useState<FilterOption[]>([{ id: 0, name: '全部' }])
const [home, setHome] = useState<HomeInfo | null>(null)
const [roomOptions, setRoomOptions] = useState<FilterOption[]>([{ id: '', name: '全部' }])
const statusOptions: FilterOption[] = [
{ id: 0, name: '全部' },
{ id: 1, name: '未处理' },
{ id: 2, name: '已处理' },
{ id: 3, name: '已忽略' }
{ id: '', name: '全部' },
{ id: '1', name: '未处理' },
{ id: '2', name: '已处理' },
{ id: '3', name: '已忽略' }
]
// 场所列表:复用场景接口;失败时保留"全部"不阻塞筛选
// 默认家庭 + 其房间列表:失败时保留"全部"选项不阻塞筛选
useEffect(() => {
getSceneList()
.then((data) => {
setSceneOptions([{ id: 0, name: '全部' }, ...(data ?? [])])
getHomeList()
.then(async (list) => {
const first = list[0] ?? null
setHome(first)
if (!first) return
const rooms = await getRoomList(first.homeId)
setRoomOptions([{ id: '', name: '全部' }, ...rooms.map(toRoomOption)])
})
.catch(() => setSceneOptions([{ id: 0, name: '全部' }]))
.catch(() => setRoomOptions([{ id: '', name: '全部' }]))
}, [])
useDidShow(() => {
// 进页首拉:home 就绪与否都调一次,home 为空时后端会报错并保持空态
if (home) loadAlarms(filters)
})
const toRoomOption = (r: HomeRoom): FilterOption => ({ id: r.roomId, name: r.roomName })
const closeDrawer = useCallback(() => setDrawer(''), [])
const sceneLabel = sceneOptions.find((o) => o.id === filters.sceneId)?.name ?? '全部场所'
const roomLabel = roomOptions.find((o) => o.id === filters.roomId)?.name ?? '全部场所'
const statusLabel = statusOptions.find((o) => o.id === filters.status)?.name ?? '全部状态'
// 筛选变化:拉取事件列表(当前 mock 空,保持空态展示)
const handleFilter = useCallback(async (next: typeof filters) => {
// 拉取告警列表:home 未就绪时跳过(useEffect 到达后由 loadAlarms 首拉兜底)
const loadAlarms = useCallback(async (next: typeof filters, homeId?: string) => {
const targetHomeId = homeId ?? home?.homeId
if (!targetHomeId) return
setLoading(true)
try {
const list = await getEventList({
sceneId: next.sceneId,
status: next.status,
startDate: next.startDate,
endDate: next.endDate
const list = await getAlarmList({
homeId: targetHomeId,
roomId: next.roomId || undefined,
status: next.status ? Number(next.status) : undefined,
// 接口要求完整时分秒:起止日拼边界值覆盖整天
beginTime: next.startDate ? `${next.startDate} 00:00:00` : undefined,
endTime: next.endDate ? `${next.endDate} 23:59:59` : undefined
})
setEventList(list)
setAlarmList(list ?? [])
} catch {
// 失败保持现有列表,toast 由请求封装统一处理
} finally {
setLoading(false)
}
}, [])
}, [home?.homeId])
const selectScene = (id: number) => {
const next = { ...filters, sceneId: id }
// home 就绪后的首拉(初始 useDidShow 时 home 可能还没到)
useEffect(() => {
if (home) loadAlarms(filters, home.homeId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [home?.homeId])
const applyFilter = (next: typeof filters) => {
setFilters(next)
closeDrawer()
handleFilter(next)
loadAlarms(next)
}
const selectStatus = (id: number) => {
const next = { ...filters, status: id }
setFilters(next)
closeDrawer()
handleFilter(next)
}
const onTimeConfirm = (start: string, end: string) => {
const next = { ...filters, startDate: start, endDate: end, timeText: `${start} 至 ${end}` }
setFilters(next)
closeDrawer()
handleFilter(next)
}
const renderOptionDrawer = (kind: 'scene' | 'status') => {
const opts = kind === 'scene' ? sceneOptions : statusOptions
const currentId = kind === 'scene' ? filters.sceneId : filters.status
const title = kind === 'scene' ? '选择场所' : '选择状态'
const onSelect = kind === 'scene' ? selectScene : selectStatus
const renderOptionDrawer = (kind: 'room' | 'status') => {
const opts = kind === 'room' ? roomOptions : statusOptions
const currentId = kind === 'room' ? filters.roomId : filters.status
const title = kind === 'room' ? '选择场所' : '选择状态'
const onSelect = (id: string) =>
applyFilter(kind === 'room' ? { ...filters, roomId: id } : { ...filters, status: id })
return (
<View className={styles.dropPanelOptions}>
<View className={styles.drawerHeader}>
@ -111,20 +131,29 @@ export default function Event() {
)
}
const statusTagOf = (alarm: HomeAlarm) => ALARM_STATUS[String(alarm.status)] ?? '未知'
// CSS Modules 动态 key 映射对象(避免索引签名问题)
const tagClsMap: Record<string, string> = {
'1': styles.tag1,
'2': styles.tag2,
'3': styles.tag3
}
return (
<View className={styles.page}>
<Navbar title="消息中心" autoBack={false} variant="hero" />
{/* 筛选栏:三格等分 */}
<View className={styles.filters}>
<View className={styles.filterCell} onClick={() => setDrawer(drawer === 'scene' ? '' : 'scene')}>
<View className={`${styles.filterTrigger} ${filters.sceneId !== 0 ? styles.filterTriggerActive : ''}`}>
<Text className={styles.filterText}>{sceneLabel}</Text>
<View className={styles.filterCell} onClick={() => setDrawer(drawer === 'room' ? '' : 'room')}>
<View className={`${styles.filterTrigger} ${filters.roomId ? styles.filterTriggerActive : ''}`}>
<Text className={styles.filterText}>{roomLabel}</Text>
<Text className={styles.filterArrow}>▾</Text>
</View>
</View>
<View className={styles.filterCell} onClick={() => setDrawer(drawer === 'status' ? '' : 'status')}>
<View className={`${styles.filterTrigger} ${filters.status !== 0 ? styles.filterTriggerActive : ''}`}>
<View className={`${styles.filterTrigger} ${filters.status ? styles.filterTriggerActive : ''}`}>
<Text className={styles.filterText}>{statusLabel}</Text>
<Text className={styles.filterArrow}>▾</Text>
</View>
@ -140,32 +169,49 @@ export default function Event() {
{/* 数据层:抽屉锚点(筛选栏下缘展开,遮罩盖住本层全部) */}
<View className={styles.contentArea}>
<View className={styles.contentBody}>
{eventList.length === 0 ? (
{alarmList.length === 0 ? (
<>
<View className={styles.emptyArea}>
<Empty text="暂无事件数据" mode="message" />
<Empty text="暂无告警数据" mode="message" />
</View>
<View className={styles.emptyHint}>
<Text>调整筛选条件后的事件列表将在此展示</Text>
<Text>调整筛选条件后的告警列表将在此展示</Text>
</View>
</>
) : (
<View className={styles.eventList}>
{/* 事件卡片列表:布局等后端接口定型后补(Task 5.1.c 说明) */}
<View className={styles.alarmList}>
{alarmList.map((alarm) => (
<View key={alarm.alarmId} className={styles.alarmCard}>
<View className={styles.alarmHead}>
<Text className={styles.alarmType}>{alarm.alarmType}</Text>
<Text className={`${styles.alarmTag} ${tagClsMap[String(alarm.status)] ?? ''}`}>{statusTagOf(alarm)}</Text>
</View>
<View className={styles.alarmRow}>
<Text className={styles.alarmLabel}>设备</Text>
<Text className={styles.alarmValue}>{alarm.deviceName || alarm.deviceId}</Text>
</View>
<View className={styles.alarmRow}>
<Text className={styles.alarmLabel}>时间</Text>
<Text className={styles.alarmValue}>{alarm.alarmTime}</Text>
</View>
</View>
))}
</View>
)}
</View>
{drawer !== '' && <View className={styles.contentMask} onClick={closeDrawer} catchMove />}
{drawer === 'scene' && renderOptionDrawer('scene')}
{drawer === 'room' && renderOptionDrawer('room')}
{drawer === 'status' && renderOptionDrawer('status')}
{drawer === 'time' && (
<CalendarDrawer
initialStart={filters.startDate}
initialEnd={filters.endDate}
onClose={closeDrawer}
onConfirm={onTimeConfirm}
onConfirm={(start, end) =>
applyFilter({ ...filters, startDate: start, endDate: end, timeText: `${start} 至 ${end}` })
}
/>
)}
</View>

View File

@ -5,7 +5,8 @@ import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading'
import { Empty } from '@/components/empty'
import { StateView } from '@/components/state-view'
import { getEventList, type EventItem } from '@/api/event'
import { getHomeList, type HomeInfo } from '@/api/home'
import { getAlarmList, type HomeAlarm } from '@/api/homeAlarm'
import { store } from '@/store'
import { storage } from '@/utils/storage'
import { ROUTES } from '@/constants/routes'
@ -15,17 +16,23 @@ import { splitAlerts } from './splitAlerts'
import styles from './index.module.scss'
export default function Index() {
const [alertList, setAlertList] = useState<EventItem[]>([])
const [alertList, setAlertList] = useState<HomeAlarm[]>([])
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false)
// 每次进入页面重拉全量告警(与事件页"全部"筛选同参数)
// 每次进入页面重拉默认家庭的全部告警(与事件页"全部"筛选同参数)
const loadAlerts = useCallback(async () => {
setLoading(true)
setLoadError(false)
try {
const list = await getEventList({ sceneId: 0, status: 0, startDate: '', endDate: '' })
setAlertList(list)
const homes = await getHomeList()
const first: HomeInfo | undefined = homes[0]
if (!first) {
setAlertList([])
return
}
const list = await getAlarmList({ homeId: first.homeId })
setAlertList(list ?? [])
} catch {
setLoadError(true)
} finally {
@ -66,7 +73,7 @@ export default function Index() {
{messages.length > 0 && (
<View className={styles.messageList}>
{messages.map((alert) => (
<AlertMessageItem key={alert.id} alert={alert} onClick={goAlertDetail} />
<AlertMessageItem key={alert.alarmId} alert={alert} onClick={goAlertDetail} />
))}
</View>
)}
@ -94,33 +101,33 @@ function FamilyEntry({ onClick }: { onClick: () => void }) {
)
}
function UnhandledAlert({ alert, onClick }: { alert: EventItem; onClick: () => void }) {
function UnhandledAlert({ alert, onClick }: { alert: HomeAlarm; onClick: () => void }) {
return (
<View className={styles.unhandledCard} onClick={onClick}>
<View className={styles.unhandledHead}>
<View className={styles.unhandledIcon}>
<Text className={styles.unhandledIconText}>!</Text>
</View>
<Text className={styles.unhandledTitle}>{alert.deviceName || '未知设备'}</Text>
<Text className={styles.unhandledTitle}>{alert.deviceName || alert.deviceId || '未知设备'}</Text>
<View className={styles.unhandledTag}>
<Text className={styles.unhandledTagText}>未处理</Text>
</View>
</View>
{alert.sceneName && (
{alert.alarmType && (
<View className={styles.unhandledMeta}>
<Text className={styles.unhandledMetaText}>{alert.sceneName}</Text>
<Text className={styles.unhandledMetaText}>{alert.alarmType}</Text>
</View>
)}
{alert.time && (
{alert.alarmTime && (
<View className={styles.unhandledMeta}>
<Text className={styles.unhandledMetaText}>{alert.time}</Text>
<Text className={styles.unhandledMetaText}>{alert.alarmTime}</Text>
</View>
)}
</View>
)
}
function AlertMessageItem({ alert, onClick }: { alert: EventItem; onClick: () => void }) {
function AlertMessageItem({ alert, onClick }: { alert: HomeAlarm; onClick: () => void }) {
return (
<View className={styles.messageCard} onClick={onClick}>
<View className={styles.messageIcon}>
@ -128,8 +135,8 @@ function AlertMessageItem({ alert, onClick }: { alert: EventItem; onClick: () =>
</View>
<View className={styles.messageBody}>
<View className={styles.messageHead}>
<Text className={styles.messageTitle}>告警消息</Text>
{alert.time && <Text className={styles.messageTime}>{alert.time}</Text>}
<Text className={styles.messageTitle}>{alert.alarmType || '告警消息'}</Text>
{alert.alarmTime && <Text className={styles.messageTime}>{alert.alarmTime}</Text>}
</View>
{alert.deviceName && (
<Text className={styles.messageDevice}>{alert.deviceName}</Text>

View File

@ -1,12 +1,12 @@
import type { EventItem } from '@/api/event'
import type { HomeAlarm } from '@/api/homeAlarm'
// 首页告警分流:status=1 未处理(首页只展示最新一条),其余全部进消息列表
export function splitAlerts(list: EventItem[]): {
unhandled: EventItem[]
messages: EventItem[]
export function splitAlerts(list: HomeAlarm[]): {
unhandled: HomeAlarm[]
messages: HomeAlarm[]
} {
const unhandled: EventItem[] = []
const messages: EventItem[] = []
const unhandled: HomeAlarm[] = []
const messages: HomeAlarm[] = []
for (const item of list) {
if (item.status === 1) unhandled.push(item)
else messages.push(item)

View File

@ -1,33 +1,36 @@
import { describe, it, expect } from 'vitest'
import { splitAlerts } from '@/pages/index/splitAlerts'
import type { EventItem } from '@/api/event'
import type { HomeAlarm } from '@/api/homeAlarm'
const item = (over: Partial<EventItem>): EventItem => ({
id: 1,
sceneName: '客厅',
const item = (over: Partial<HomeAlarm>): HomeAlarm => ({
alarmId: '1',
homeId: 'h1',
roomId: 'r1',
deviceId: 'd1',
deviceName: '体征监测仪',
alarmType: '心率异常',
alarmTime: '2026-09-07 21:22:12',
status: 2,
time: '2026-09-07 21:22:12',
...over
})
describe('splitAlerts', () => {
it('status=1 进入未处理组,其余进入消息组', () => {
const list = [
item({ id: 1, status: 1 }),
item({ id: 2, status: 2 }),
item({ id: 3, status: 3 })
item({ alarmId: '1', status: 1 }),
item({ alarmId: '2', status: 2 }),
item({ alarmId: '3', status: 3 })
]
const { unhandled, messages } = splitAlerts(list)
expect(unhandled.map((i) => i.id)).toEqual([1])
expect(messages.map((i) => i.id)).toEqual([2, 3])
expect(unhandled.map((i) => i.alarmId)).toEqual(['1'])
expect(messages.map((i) => i.alarmId)).toEqual(['2', '3'])
})
it('status 异常值(如 0、99)落入消息组不丢弃', () => {
const list = [item({ id: 1, status: 0 }), item({ id: 2, status: 99 })]
const list = [item({ alarmId: '1', status: 0 }), item({ alarmId: '2', status: 99 })]
const { unhandled, messages } = splitAlerts(list)
expect(unhandled).toEqual([])
expect(messages.map((i) => i.id)).toEqual([1, 2])
expect(messages.map((i) => i.alarmId)).toEqual(['1', '2'])
})
it('空列表两组均为空', () => {
@ -38,12 +41,12 @@ describe('splitAlerts', () => {
it('两组均保持原有相对顺序', () => {
const list = [
item({ id: 3, status: 2 }),
item({ id: 1, status: 1 }),
item({ id: 2, status: 2 })
item({ alarmId: '3', status: 2 }),
item({ alarmId: '1', status: 1 }),
item({ alarmId: '2', status: 2 })
]
const { unhandled, messages } = splitAlerts(list)
expect(unhandled.map((i) => i.id)).toEqual([1])
expect(messages.map((i) => i.id)).toEqual([3, 2])
expect(unhandled.map((i) => i.alarmId)).toEqual(['1'])
expect(messages.map((i) => i.alarmId)).toEqual(['3', '2'])
})
})