diff --git a/src/api/event.ts b/src/api/event.ts deleted file mode 100644 index be3b6ac..0000000 --- a/src/api/event.ts +++ /dev/null @@ -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 { - if (USE_MOCK) { - return Promise.resolve([]) - } - return request.get('/event/list', { ..._query }) -} diff --git a/src/api/homeAlarm.ts b/src/api/homeAlarm.ts index 8baf6fb..55a7eb2 100644 --- a/src/api/homeAlarm.ts +++ b/src/api/homeAlarm.ts @@ -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): Promise { - return request.get('/yanglao/app/ylHomeAlarm/getList', params) +export function getAlarmList(params: AlarmListReq): Promise { + return request.post('/yanglao/app/ylHomeAlarm/getList', params as unknown as Record) } export function getAlarmDetail(alarmId: string): Promise { diff --git a/src/api/homeRoom.ts b/src/api/homeRoom.ts index 7f39dd6..1ea37b6 100644 --- a/src/api/homeRoom.ts +++ b/src/api/homeRoom.ts @@ -7,8 +7,9 @@ export interface HomeRoom { roomName: string } +// 文档路径为 POST + body { homeId }(YlHomeRoomListReq) export function getRoomList(homeId: string): Promise { - return request.get('/yanglao/app/ylHomeRoom/getList', { homeId }) + return request.post('/yanglao/app/ylHomeRoom/getList', { homeId }) } export function addRoom(params: Record): Promise { diff --git a/src/pages/event/index.module.scss b/src/pages/event/index.module.scss index 9c14066..58bb159 100644 --- a/src/pages/event/index.module.scss +++ b/src/pages/event/index.module.scss @@ -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; diff --git a/src/pages/event/index.tsx b/src/pages/event/index.tsx index 19d4e1e..3923d16 100644 --- a/src/pages/event/index.tsx +++ b/src/pages/event/index.tsx @@ -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 = { + '1': '未处理', + '2': '已处理', + '3': '已忽略' +} + export default function Event() { - // '' | 'scene' | 'status' | 'time' - const [drawer, setDrawer] = useState<'' | 'scene' | 'status' | 'time'>('') - // 事件列表:后端接口定型前 mock 返回空数组 → 空态 - const [eventList, setEventList] = useState([]) + // '' | 'room' | 'status' | 'time' + const [drawer, setDrawer] = useState<'' | 'room' | 'status' | 'time'>('') + const [alarmList, setAlarmList] = useState([]) + const [loading, setLoading] = useState(true) const [filters, setFilters] = useState({ - sceneId: 0, // 0 表示全部 - status: 0, + roomId: '', + status: '', startDate: '', endDate: '', timeText: '' }) - const [sceneOptions, setSceneOptions] = useState([{ id: 0, name: '全部' }]) + const [home, setHome] = useState(null) + const [roomOptions, setRoomOptions] = useState([{ 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 ( @@ -111,20 +131,29 @@ export default function Event() { ) } + const statusTagOf = (alarm: HomeAlarm) => ALARM_STATUS[String(alarm.status)] ?? '未知' + + // CSS Modules 动态 key 映射对象(避免索引签名问题) + const tagClsMap: Record = { + '1': styles.tag1, + '2': styles.tag2, + '3': styles.tag3 + } + return ( {/* 筛选栏:三格等分 */} - setDrawer(drawer === 'scene' ? '' : 'scene')}> - - {sceneLabel} + setDrawer(drawer === 'room' ? '' : 'room')}> + + {roomLabel} ▾ setDrawer(drawer === 'status' ? '' : 'status')}> - + {statusLabel} ▾ @@ -140,32 +169,49 @@ export default function Event() { {/* 数据层:抽屉锚点(筛选栏下缘展开,遮罩盖住本层全部) */} - {eventList.length === 0 ? ( + {alarmList.length === 0 ? ( <> - + - 调整筛选条件后的事件列表将在此展示 + 调整筛选条件后的告警列表将在此展示 ) : ( - - {/* 事件卡片列表:布局等后端接口定型后补(Task 5.1.c 说明) */} + + {alarmList.map((alarm) => ( + + + {alarm.alarmType} + {statusTagOf(alarm)} + + + 设备 + {alarm.deviceName || alarm.deviceId} + + + 时间 + {alarm.alarmTime} + + + ))} )} {drawer !== '' && } - {drawer === 'scene' && renderOptionDrawer('scene')} + {drawer === 'room' && renderOptionDrawer('room')} {drawer === 'status' && renderOptionDrawer('status')} {drawer === 'time' && ( + applyFilter({ ...filters, startDate: start, endDate: end, timeText: `${start} 至 ${end}` }) + } /> )} diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 08be2e2..e2cb537 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -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([]) + const [alertList, setAlertList] = useState([]) 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 && ( {messages.map((alert) => ( - + ))} )} @@ -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 ( ! - {alert.deviceName || '未知设备'} + {alert.deviceName || alert.deviceId || '未知设备'} 未处理 - {alert.sceneName && ( + {alert.alarmType && ( - {alert.sceneName} + {alert.alarmType} )} - {alert.time && ( + {alert.alarmTime && ( - {alert.time} + {alert.alarmTime} )} ) } -function AlertMessageItem({ alert, onClick }: { alert: EventItem; onClick: () => void }) { +function AlertMessageItem({ alert, onClick }: { alert: HomeAlarm; onClick: () => void }) { return ( @@ -128,8 +135,8 @@ function AlertMessageItem({ alert, onClick }: { alert: EventItem; onClick: () => - 告警消息 - {alert.time && {alert.time}} + {alert.alarmType || '告警消息'} + {alert.alarmTime && {alert.alarmTime}} {alert.deviceName && ( {alert.deviceName} diff --git a/src/pages/index/splitAlerts.ts b/src/pages/index/splitAlerts.ts index 39828c0..1a87da8 100644 --- a/src/pages/index/splitAlerts.ts +++ b/src/pages/index/splitAlerts.ts @@ -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) diff --git a/tests/unit/home/splitAlerts.spec.ts b/tests/unit/home/splitAlerts.spec.ts index 1a8a4bb..c5d6e9b 100644 --- a/tests/unit/home/splitAlerts.spec.ts +++ b/tests/unit/home/splitAlerts.spec.ts @@ -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 => ({ - id: 1, - sceneName: '客厅', +const item = (over: Partial): 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']) }) })