feat(event): 告警列表/详情接真实字段与新增告警详情页
- HomeAlarm 类型按 2026-09-23 联调实测重写:content/createTime/ deviceAlias/otherInfo 扩展等,status 枚举确认 0-待处理 1-已忽略 2-已处理 3-自动处理;splitAlerts 待处理判定改 status===0,单测同步 - 新增 pages/event/feature/detail 告警详情页(getAlarmDetail、状态徽章、 设备/房间/时间/扩展信息行),路由与分包注册,事件卡片与首页告警 点击均跳详情 - 事件页/首页改用 resolveCurrentHome 共享当前家庭,卡片展示 content、 deviceAlias 优先、两行截断与红点行标 - 家庭圈成员行点击改群昵称(showModal editable + updateMemberNickName), 转让/改昵称入口放开给所有成员(成员响应无 userId,isOwner 不可靠, 由后端 400 校验兜底) - ListRow value 推到行尾并支持 valueClass 自定义样式
This commit is contained in:
parent
6ec6fc7138
commit
82aa7d6925
|
|
@ -12,16 +12,46 @@ export interface AlarmListReq {
|
|||
pageSize?: number
|
||||
}
|
||||
|
||||
// 骨架:本期仅锁定接口路径,字段类型联调时修正(spec D7)
|
||||
// 2026-09-23 联调实测字段(.getList 真实响应):
|
||||
// - 无 homeId 字段;alarmType 是告警类型字典 ID(非文案),文案在 content
|
||||
// - 时间字段是 createTime / lastUpdateTime,无 alarmTime
|
||||
// - status: 0-待处理 1-已忽略 2-已处理 3-自动处理(2026-09-23 后端确认)
|
||||
// - level: 5(告警级别,语义待字典确认);category: 'DEVICE'
|
||||
export interface HomeAlarm {
|
||||
alarmId: string
|
||||
homeId: string
|
||||
roomId: string
|
||||
content: string // 告警文案,如「跌倒检测器 No.01于2026-09-23 14:19:08发生确认跌倒」
|
||||
level?: number
|
||||
status: number
|
||||
category?: string
|
||||
alarmType: string // 告警类型字典 ID,前端不直接展示
|
||||
deviceId: string
|
||||
deviceName: string
|
||||
alarmType: string
|
||||
alarmTime: string
|
||||
status: number // 告警状态:0-未处理 1-已处理 2-已忽略(待实测确认枚举)
|
||||
deviceName: string // 设备序列号
|
||||
deviceAlias?: string | null // 设备别名(如「跌倒检测器 No.01」),展示优先于 deviceName
|
||||
roomId: string
|
||||
roomName?: string | null
|
||||
productId?: string
|
||||
createTime: string
|
||||
lastUpdateTime?: string
|
||||
// 详情接口附加的告警扩展信息(列表接口为 null)
|
||||
otherInfo?: {
|
||||
appName?: string
|
||||
description?: string
|
||||
alarmName?: string // 告警名称,如「跌倒报警」
|
||||
deviceSn?: string
|
||||
installId?: string
|
||||
deviceName?: string
|
||||
content?: string
|
||||
realName?: string
|
||||
alertTypeName?: string // 告警分类名,如「设备告警消息」
|
||||
systemName?: string
|
||||
cameraId?: string
|
||||
appId?: string
|
||||
alertTime?: string
|
||||
cameraName?: string
|
||||
deviceLocation?: string
|
||||
} | null
|
||||
videoUrl?: string | null
|
||||
capturePicUrl?: string | null
|
||||
}
|
||||
|
||||
export function getAlarmList(params: AlarmListReq): Promise<HomeAlarm[]> {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ export default {
|
|||
{
|
||||
root: 'pages/family',
|
||||
pages: ['index', 'detail', 'contact/add', 'contact/detail']
|
||||
},
|
||||
{
|
||||
root: 'pages/event/feature',
|
||||
pages: ['detail']
|
||||
}
|
||||
],
|
||||
window: {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@
|
|||
}
|
||||
|
||||
.value {
|
||||
margin-left: 24px;
|
||||
// 推到行尾紧贴箭头:无箭头时贴右边距;否则三元素 space-between 会把 value 挤到中间
|
||||
margin-left: auto;
|
||||
font-size: 28px;
|
||||
color: $color-text;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import styles from './index.module.scss'
|
|||
interface ListRowProps {
|
||||
label: string
|
||||
value?: string
|
||||
valueClass?: string
|
||||
arrow?: boolean
|
||||
onClick?: () => void
|
||||
danger?: boolean
|
||||
}
|
||||
|
||||
export function ListRow({ label, value, arrow, onClick, danger }: ListRowProps) {
|
||||
export function ListRow({ label, value, valueClass, arrow, onClick, danger }: ListRowProps) {
|
||||
const cls = [styles.row, danger ? styles.rowDanger : ''].filter(Boolean).join(' ')
|
||||
return (
|
||||
<View
|
||||
|
|
@ -19,7 +20,7 @@ export function ListRow({ label, value, arrow, onClick, danger }: ListRowProps)
|
|||
onClick={onClick}
|
||||
>
|
||||
<Text className={styles.label}>{label}</Text>
|
||||
{value && <Text className={styles.value}>{value}</Text>}
|
||||
{value && <Text className={`${styles.value} ${valueClass ?? ''}`.trim()}>{value}</Text>}
|
||||
{arrow && <Text className={styles.arrow}>›</Text>}
|
||||
</View>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export const ROUTES = Object.freeze({
|
|||
LOGIN: '/pages/login/index',
|
||||
HOME: '/pages/index/index',
|
||||
EVENT: '/pages/event/index',
|
||||
EVENT_DETAIL: '/pages/event/feature/detail',
|
||||
MINE: '/pages/mine/index',
|
||||
DEVICE_LIST: '/pages/device/index',
|
||||
DEVICE_ADD: '/pages/device/feature/add',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.stateWrap {
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 告警头卡:白底渐变浮卡,左侧语义色竖条强调紧急度
|
||||
.headCard {
|
||||
margin: 24px;
|
||||
padding: 32px;
|
||||
background: $color-bg-white;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.headTop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.alarmName {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
// 状态徽章
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 6px 20px;
|
||||
border-radius: $radius-pill;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
// 待处理:红(需关注)
|
||||
.tagPending {
|
||||
background: $color-danger-light-bg;
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
// 已处理:绿
|
||||
.tagDone {
|
||||
background: $color-success-light-bg;
|
||||
color: $color-success;
|
||||
}
|
||||
|
||||
// 已忽略/未知:灰
|
||||
.tagIgnored {
|
||||
background: $color-bg-disabled;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28px;
|
||||
color: $color-text;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.headMeta {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metaText {
|
||||
font-size: 24px;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.metaDot {
|
||||
margin: 0 12px;
|
||||
font-size: 24px;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
// 现场抓拍图
|
||||
.capture {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 0 24px 24px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
color: $color-text-secondary;
|
||||
margin: 0 8px 16px;
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from '@tarojs/taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { Loading } from '@/components/loading'
|
||||
import { StateView } from '@/components/state-view'
|
||||
import { ListRow, ListRowGroup } from '@/components/list-row'
|
||||
import { getAlarmDetail, type HomeAlarm } from '@/api/homeAlarm'
|
||||
import { navigateTo } from '@/utils/navigation'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import styles from './detail.module.scss'
|
||||
|
||||
// 状态 → 徽章文案与语义色(2026-09-23 后端确认:0-待处理 1-已忽略 2-已处理 3-自动处理)
|
||||
const STATUS_META: Record<number, { label: string; cls: string }> = {
|
||||
0: { label: '待处理', cls: 'tagPending' },
|
||||
1: { label: '已忽略', cls: 'tagIgnored' },
|
||||
2: { label: '已处理', cls: 'tagDone' },
|
||||
3: { label: '自动处理', cls: 'tagIgnored' }
|
||||
}
|
||||
|
||||
export default function AlarmDetail() {
|
||||
const router = useRouter()
|
||||
const alarmId = router.params.id ?? ''
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [alarm, setAlarm] = useState<HomeAlarm | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!alarmId) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
getAlarmDetail(alarmId)
|
||||
.then(setAlarm)
|
||||
.catch(() => setAlarm(null))
|
||||
.finally(() => setLoading(false))
|
||||
}, [alarmId])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className={styles.page}>
|
||||
<Navbar title="告警详情" variant="hero" />
|
||||
<View className={styles.stateWrap}><Loading /></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!alarm) {
|
||||
return (
|
||||
<View className={styles.page}>
|
||||
<Navbar title="告警详情" variant="hero" />
|
||||
<StateView status="error" mode="list" text="该告警不存在或已删除" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = STATUS_META[alarm.status] ?? { label: '未知状态', cls: 'tagIgnored' }
|
||||
const deviceLabel = alarm.deviceAlias || alarm.deviceName || alarm.deviceId || '未知设备'
|
||||
const ext = alarm.otherInfo
|
||||
|
||||
const goDevice = () => {
|
||||
if (ext?.installId) navigateTo(`${ROUTES.DEVICE_DETAIL}?id=${ext.installId}&name=${encodeURIComponent(deviceLabel)}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.page}>
|
||||
<Navbar title="告警详情" variant="hero" />
|
||||
|
||||
{/* 告警头卡:名称 + 状态徽章 + 文案 + 时间 */}
|
||||
<View className={styles.headCard}>
|
||||
<View className={styles.headTop}>
|
||||
<Text className={styles.alarmName}>{ext?.alarmName || '设备告警'}</Text>
|
||||
<Text className={`${styles.tag} ${styles[meta.cls]}`}>{meta.label}</Text>
|
||||
</View>
|
||||
<Text className={styles.content}>{alarm.content}</Text>
|
||||
<View className={styles.headMeta}>
|
||||
<Text className={styles.metaText}>{ext?.alertTypeName || '设备告警消息'}</Text>
|
||||
<Text className={styles.metaDot}>·</Text>
|
||||
<Text className={styles.metaText}>{alarm.createTime}</Text>
|
||||
</View>
|
||||
|
||||
{/* 现场抓拍:capturePicUrl 存在时展示 */}
|
||||
{alarm.capturePicUrl && (
|
||||
<Image className={styles.capture} src={alarm.capturePicUrl} mode="aspectFill" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 告警信息组 */}
|
||||
<View className={styles.section}>
|
||||
<Text className={styles.sectionTitle}>告警信息</Text>
|
||||
<ListRowGroup>
|
||||
<ListRow label="告警时间" value={ext?.alertTime || alarm.createTime} />
|
||||
<ListRow label="更新时间" value={alarm.lastUpdateTime || alarm.createTime} />
|
||||
<ListRow label="告警设备" value={deviceLabel} arrow={!!ext?.installId} onClick={ext?.installId ? goDevice : undefined} />
|
||||
<ListRow label="所在房间" value={alarm.roomName || '--'} />
|
||||
<ListRow label="设备序列号" value={alarm.deviceName || '--'} />
|
||||
{ext?.deviceLocation && <ListRow label="设备位置" value={ext.deviceLocation} />}
|
||||
</ListRowGroup>
|
||||
</View>
|
||||
|
||||
{/* 现场视频:videoUrl 存在时提示跳转(视频播放器暂未接入) */}
|
||||
{alarm.videoUrl && (
|
||||
<View className={styles.section}>
|
||||
<Text className={styles.sectionTitle}>现场记录</Text>
|
||||
<ListRowGroup>
|
||||
<ListRow label="告警录像" value="点击查看" arrow onClick={() => navigateTo(`${ROUTES.DEVICE_DETAIL}?id=${alarm.deviceId}`)} />
|
||||
</ListRowGroup>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -78,6 +78,7 @@
|
|||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-card;
|
||||
padding: 28px 32px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
& + & { margin-top: 20px; }
|
||||
}
|
||||
|
|
@ -90,20 +91,52 @@
|
|||
}
|
||||
|
||||
.alarm-type {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
// 多行文案截断到两行:跌倒类告警 content 常带完整时间戳较长
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 告警类型左侧色条行标:红点提示设备类告警语义
|
||||
.alarm-type-dot { display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: $radius-circle;
|
||||
background: $color-danger;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alarm-tag {
|
||||
display: inline-block;
|
||||
font-size: 22px;
|
||||
padding: 4px 16px;
|
||||
padding: 6px 16px;
|
||||
border-radius: $radius-pill;
|
||||
color: $color-text-secondary;
|
||||
background: $color-bg-gray;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 状态枚举待实测,先按未处理红色、已处理绿色、已忽略灰的常见语义 */
|
||||
// 标题行:红点 + 两行截断文案
|
||||
.alarmTypeWrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alarmCardPressed {
|
||||
border-color: $color-primary-light-border;
|
||||
}
|
||||
|
||||
/* 状态语义色:0-待处理红 2-已处理绿 1/3-已忽略/自动处理灰(2026-09-23 后端确认枚举) */
|
||||
.tag1 {
|
||||
color: $color-danger;
|
||||
background: $color-danger-light-bg;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import { View, Text } from '@tarojs/components'
|
|||
import { Navbar } from '@/components/navbar'
|
||||
import { Empty } from '@/components/empty'
|
||||
import { getHomeList, type HomeInfo } from '@/api/home'
|
||||
import { resolveCurrentHome } from '@/store/currentHome'
|
||||
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
|
||||
import { getAlarmList, type HomeAlarm } from '@/api/homeAlarm'
|
||||
import { navigateTo } from '@/utils/navigation'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { CalendarDrawer } from './calendar-drawer'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
|
|
@ -14,11 +17,12 @@ interface FilterOption {
|
|||
name: string
|
||||
}
|
||||
|
||||
// 告警状态筛选枚举(后端 integer,待实测确认:1-未处理 2-已处理 3-已忽略)
|
||||
// 告警状态枚举(2026-09-23 后端确认:0-待处理 1-已忽略 2-已处理 3-自动处理)
|
||||
const ALARM_STATUS: Record<string, string> = {
|
||||
'1': '未处理',
|
||||
'0': '待处理',
|
||||
'1': '已忽略',
|
||||
'2': '已处理',
|
||||
'3': '已忽略'
|
||||
'3': '自动处理'
|
||||
}
|
||||
|
||||
export default function Event() {
|
||||
|
|
@ -37,16 +41,18 @@ export default function Event() {
|
|||
const [roomOptions, setRoomOptions] = useState<FilterOption[]>([{ id: '', name: '全部' }])
|
||||
const statusOptions: FilterOption[] = [
|
||||
{ id: '', name: '全部' },
|
||||
{ id: '1', name: '未处理' },
|
||||
{ id: '0', name: '待处理' },
|
||||
{ id: '1', name: '已忽略' },
|
||||
{ id: '2', name: '已处理' },
|
||||
{ id: '3', name: '已忽略' }
|
||||
{ id: '3', name: '自动处理' }
|
||||
]
|
||||
|
||||
// 默认家庭 + 其房间列表:失败时保留"全部"选项不阻塞筛选
|
||||
// 当前家庭 + 其房间列表:与智能设备页共享选择(resolveCurrentHome 读持久化),
|
||||
// 失败时保留"全部"选项不阻塞筛选
|
||||
useEffect(() => {
|
||||
getHomeList()
|
||||
.then(async (list) => {
|
||||
const first = list[0] ?? null
|
||||
const first = resolveCurrentHome(list)
|
||||
setHome(first)
|
||||
if (!first) return
|
||||
const rooms = await getRoomList(first.homeId)
|
||||
|
|
@ -135,9 +141,10 @@ export default function Event() {
|
|||
|
||||
// CSS Modules 动态 key 映射对象(避免索引签名问题)
|
||||
const tagClsMap: Record<string, string> = {
|
||||
'1': styles.tag1,
|
||||
'2': styles.tag2,
|
||||
'3': styles.tag3
|
||||
'0': styles.tag1, // 待处理-红
|
||||
'1': styles.tag3, // 已忽略-灰
|
||||
'2': styles.tag2, // 已处理-绿
|
||||
'3': styles.tag3 // 自动处理-灰
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -181,18 +188,26 @@ export default function Event() {
|
|||
) : (
|
||||
<View className={styles.alarmList}>
|
||||
{alarmList.map((alarm) => (
|
||||
<View key={alarm.alarmId} className={styles.alarmCard}>
|
||||
<View
|
||||
key={alarm.alarmId}
|
||||
className={styles.alarmCard}
|
||||
hoverClass={styles.alarmCardPressed}
|
||||
onClick={() => navigateTo(`${ROUTES.EVENT_DETAIL}?id=${alarm.alarmId}`)}
|
||||
>
|
||||
<View className={styles.alarmHead}>
|
||||
<Text className={styles.alarmType}>{alarm.alarmType}</Text>
|
||||
<View className={styles.alarmTypeWrap}>
|
||||
<View className={styles.alarmTypeDot} />
|
||||
<Text className={styles.alarmType}>{alarm.content || '告警消息'}</Text>
|
||||
</View>
|
||||
<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>
|
||||
<Text className={styles.alarmValue}>{alarm.deviceAlias || alarm.deviceName || alarm.deviceId}</Text>
|
||||
</View>
|
||||
<View className={styles.alarmRow}>
|
||||
<Text className={styles.alarmLabel}>时间</Text>
|
||||
<Text className={styles.alarmValue}>{alarm.alarmTime}</Text>
|
||||
<Text className={styles.alarmValue}>{alarm.createTime}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,21 @@
|
|||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
// 家庭成员行「群主」徽章:浅绿底胶囊,右贴箭头
|
||||
// margin-left:auto 在 flex 行里把它推到最右;Text 需 inline-block 撑高背景
|
||||
.valueTag {
|
||||
display: inline-block;
|
||||
line-height: 1.4;
|
||||
padding: 6px 20px;
|
||||
background: $color-primary-light-bg;
|
||||
border-radius: $radius-pill;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: $color-primary-deep;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 转让群主选人弹层(Modal 内容)
|
||||
.transferTitle {
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { StateView } from '@/components/state-view'
|
|||
import { FamilyCard } from '@/components/family-card'
|
||||
import { ListRow, ListRowGroup } from '@/components/list-row'
|
||||
import { Modal } from '@/components/modal'
|
||||
import { getHomeDetail, getHomeMemberList, joinHome, transferOwner, type HomeInfo, type HomeMember } from '@/api/home'
|
||||
import { getHomeDetail, getHomeMemberList, joinHome, transferOwner, updateMemberNickName, type HomeInfo, type HomeMember } from '@/api/home'
|
||||
import { getCareMemberList, type CareMember } from '@/api/careMember'
|
||||
import { MAX_CARED_PER_FAMILY } from '@/constants/family'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
|
|
@ -17,8 +17,22 @@ import { navigateBack, navigateTo, reLaunch } from '@/utils/navigation'
|
|||
import { showToast } from '@/utils/toast'
|
||||
import styles from './detail.module.scss'
|
||||
|
||||
// Taro 类型未跟进 wx.showModal 的 editable 扩展(微信 2.17.1+),运行时真实存在
|
||||
interface EditableModalOption extends Taro.showModal.Option {
|
||||
editable?: boolean
|
||||
placeholderText?: string
|
||||
}
|
||||
|
||||
interface EditableModalResult extends Taro.showModal.SuccessCallbackResult {
|
||||
content?: string
|
||||
}
|
||||
|
||||
const showEditableModal = (option: EditableModalOption) =>
|
||||
Taro.showModal(option as Taro.showModal.Option) as Promise<EditableModalResult>
|
||||
|
||||
// 成员显示名:家庭内昵称优先,未设置回落微信昵称
|
||||
const memberName = (m: HomeMember): string => m.nickName ?? m.userNickName ?? '成员'
|
||||
// 注意 2026-09-23 实测:清空昵称后端返回空串 '' 而非 null,需按空白处理
|
||||
const memberName = (m: HomeMember): string => m.nickName?.trim() || m.userNickName?.trim() || '成员'
|
||||
|
||||
// 关爱对象联系人摘要:最多显示前 2 位姓名,其余以 +N 概括;未填则不显示
|
||||
const contactsSummary = (c: CareMember): string => {
|
||||
|
|
@ -45,6 +59,8 @@ export default function FamilyDetail() {
|
|||
// 转让群主:底部入口仅群主可见,点击后弹选人 Modal
|
||||
const [transferOpen, setTransferOpen] = useState(false)
|
||||
const [transferring, setTransferring] = useState(false)
|
||||
// 改昵称:点击成员行触发
|
||||
const [renaming, setRenaming] = useState(false)
|
||||
|
||||
const load = async () => {
|
||||
// 分享卡直达本页且未登录:先去登录,登录后回跳本页
|
||||
|
|
@ -114,9 +130,13 @@ export default function FamilyDetail() {
|
|||
// 我在成员列表里的角色:转让入口只对群主开放
|
||||
const myMember = members.find((m) => m.memberId === myUserId) ?? null
|
||||
const isOwner = myMember?.role === 'OWNER'
|
||||
// 注意:成员响应无 userId 字段(2026-09-23 实测),memberId 是家庭圈成员记录 ID,
|
||||
// 与登录 userId 不同源,isOwner 恒为 false——仅作未来后端补字段后的预留,勿用于门禁
|
||||
|
||||
// 转让/改昵称入口全开:前端无法可靠识别自己,由后端权威校验
|
||||
// (transferOwner 实测已有 400「不能转让给自己」「成员不存在」等完整校验)
|
||||
const openTransfer = () => {
|
||||
if (!isOwner || transferring) return
|
||||
if (transferring) return
|
||||
setTransferOpen(true)
|
||||
}
|
||||
const closeTransfer = () => {
|
||||
|
|
@ -138,6 +158,37 @@ export default function FamilyDetail() {
|
|||
}
|
||||
}
|
||||
|
||||
// 点击成员行改昵称:wx.showModal editable 输入框,预填当前昵称
|
||||
const renameMember = async (target: HomeMember) => {
|
||||
if (renaming) return
|
||||
setRenaming(true)
|
||||
try {
|
||||
const { confirm, content } = await showEditableModal({
|
||||
title: '修改群昵称',
|
||||
editable: true,
|
||||
placeholderText: '请输入新的昵称',
|
||||
content: target.nickName ?? ''
|
||||
})
|
||||
if (!confirm) return
|
||||
const next = (content ?? '').trim()
|
||||
if (!next) {
|
||||
showToast('昵称不能为空')
|
||||
return
|
||||
}
|
||||
if (next === (target.nickName ?? '').trim()) {
|
||||
showToast('昵称未变化')
|
||||
return
|
||||
}
|
||||
await updateMemberNickName({ memberId: target.memberId, nickName: next })
|
||||
showToast('已修改')
|
||||
await load()
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : '修改失败')
|
||||
} finally {
|
||||
setRenaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className={styles.page}>
|
||||
|
|
@ -219,7 +270,14 @@ export default function FamilyDetail() {
|
|||
) : (
|
||||
<ListRowGroup>
|
||||
{members.map((m) => (
|
||||
<ListRow key={m.memberId} label={memberName(m)} value={m.role === 'OWNER' ? '群主' : '成员'} />
|
||||
<ListRow
|
||||
key={m.memberId}
|
||||
label={memberName(m)}
|
||||
value={m.role === 'OWNER' ? '群主' : undefined}
|
||||
valueClass={styles.valueTag}
|
||||
arrow
|
||||
onClick={() => renameMember(m)}
|
||||
/>
|
||||
))}
|
||||
</ListRowGroup>
|
||||
)}
|
||||
|
|
@ -237,22 +295,19 @@ export default function FamilyDetail() {
|
|||
<Text className={styles.shareLabel}>邀请他人</Text>
|
||||
<Text className={styles.shareArrow}>›</Text>
|
||||
</Button>
|
||||
<ListRow label="转让群主" arrow onClick={isOwner ? openTransfer : stub('仅群主可转让')} />
|
||||
<ListRow label="修改群昵称" arrow onClick={stub('修改昵称功能即将上线')} />
|
||||
<ListRow label="转让群主" arrow onClick={openTransfer} />
|
||||
</ListRowGroup>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 转让群主选人弹层:列出除自己外的成员,选中即调用 transferOwner */}
|
||||
{/* 转让群主选人弹层:全部成员可选(含自己),后端对「转让给自己」返回 400 由 toast 透出 */}
|
||||
<Modal visible={transferOpen} onClose={closeTransfer}>
|
||||
<Text className={styles.transferTitle}>转让群主给谁?</Text>
|
||||
<View className={styles.transferList}>
|
||||
{members.filter((m) => m.memberId !== myUserId).length === 0 ? (
|
||||
{members.length === 0 ? (
|
||||
<Text className={styles.transferEmpty}>暂无可转让的成员</Text>
|
||||
) : (
|
||||
members
|
||||
.filter((m) => m.memberId !== myUserId)
|
||||
.map((m) => (
|
||||
members.map((m) => (
|
||||
<View
|
||||
key={m.memberId}
|
||||
className={styles.transferItem}
|
||||
|
|
@ -261,7 +316,7 @@ export default function FamilyDetail() {
|
|||
>
|
||||
<Text className={styles.transferItemName}>{memberName(m)}</Text>
|
||||
<Text className={styles.transferItemMeta}>
|
||||
{m.role === 'OWNER' ? '群主' : '成员'} · 群昵称:{m.nickName ?? '未设置'}
|
||||
{m.role === 'OWNER' ? '群主' : '成员'} · 群昵称:{m.nickName || '未设置'}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { Loading } from '@/components/loading'
|
|||
import { Empty } from '@/components/empty'
|
||||
import { StateView } from '@/components/state-view'
|
||||
import { getHomeList, type HomeInfo } from '@/api/home'
|
||||
import { resolveCurrentHome } from '@/store/currentHome'
|
||||
import { getAlarmList, type HomeAlarm } from '@/api/homeAlarm'
|
||||
import { store } from '@/store'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { navigateTo, reLaunch } from '@/utils/navigation'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { splitAlerts } from './splitAlerts'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
|
|
@ -26,7 +26,8 @@ export default function Index() {
|
|||
setLoadError(false)
|
||||
try {
|
||||
const homes = await getHomeList()
|
||||
const first: HomeInfo | undefined = homes[0]
|
||||
// 与智能设备页共享当前家庭:优先持久化选择,失效回落第一条(resolveCurrentHome 内重新落盘)
|
||||
const first: HomeInfo | null = resolveCurrentHome(homes)
|
||||
if (!first) {
|
||||
setAlertList([])
|
||||
return
|
||||
|
|
@ -52,7 +53,7 @@ export default function Index() {
|
|||
const { unhandled, messages } = useMemo(() => splitAlerts(alertList), [alertList])
|
||||
|
||||
const goFamily = () => navigateTo(ROUTES.FAMILY)
|
||||
const goAlertDetail = () => showToast('告警详情即将上线')
|
||||
const goAlertDetail = (alarmId: string) => navigateTo(`${ROUTES.EVENT_DETAIL}?id=${alarmId}`)
|
||||
|
||||
return (
|
||||
<View className={styles.page}>
|
||||
|
|
@ -67,13 +68,13 @@ export default function Index() {
|
|||
<FamilyEntry onClick={goFamily} />
|
||||
|
||||
{unhandled.length > 0 && (
|
||||
<UnhandledAlert alert={unhandled[0]} onClick={goAlertDetail} />
|
||||
<UnhandledAlert alert={unhandled[0]} onClick={() => goAlertDetail(unhandled[0].alarmId)} />
|
||||
)}
|
||||
|
||||
{messages.length > 0 && (
|
||||
<View className={styles.messageList}>
|
||||
{messages.map((alert) => (
|
||||
<AlertMessageItem key={alert.alarmId} alert={alert} onClick={goAlertDetail} />
|
||||
<AlertMessageItem key={alert.alarmId} alert={alert} onClick={() => goAlertDetail(alert.alarmId)} />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -108,19 +109,19 @@ function UnhandledAlert({ alert, onClick }: { alert: HomeAlarm; onClick: () => v
|
|||
<View className={styles.unhandledIcon}>
|
||||
<Text className={styles.unhandledIconText}>!</Text>
|
||||
</View>
|
||||
<Text className={styles.unhandledTitle}>{alert.deviceName || alert.deviceId || '未知设备'}</Text>
|
||||
<Text className={styles.unhandledTitle}>{alert.deviceAlias || alert.deviceName || alert.deviceId || '未知设备'}</Text>
|
||||
<View className={styles.unhandledTag}>
|
||||
<Text className={styles.unhandledTagText}>未处理</Text>
|
||||
<Text className={styles.unhandledTagText}>待处理</Text>
|
||||
</View>
|
||||
</View>
|
||||
{alert.alarmType && (
|
||||
{alert.content && (
|
||||
<View className={styles.unhandledMeta}>
|
||||
<Text className={styles.unhandledMetaText}>{alert.alarmType}</Text>
|
||||
<Text className={styles.unhandledMetaText}>{alert.content}</Text>
|
||||
</View>
|
||||
)}
|
||||
{alert.alarmTime && (
|
||||
{alert.createTime && (
|
||||
<View className={styles.unhandledMeta}>
|
||||
<Text className={styles.unhandledMetaText}>{alert.alarmTime}</Text>
|
||||
<Text className={styles.unhandledMetaText}>{alert.createTime}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -135,11 +136,11 @@ function AlertMessageItem({ alert, onClick }: { alert: HomeAlarm; onClick: () =>
|
|||
</View>
|
||||
<View className={styles.messageBody}>
|
||||
<View className={styles.messageHead}>
|
||||
<Text className={styles.messageTitle}>{alert.alarmType || '告警消息'}</Text>
|
||||
{alert.alarmTime && <Text className={styles.messageTime}>{alert.alarmTime}</Text>}
|
||||
<Text className={styles.messageTitle}>{alert.content || '告警消息'}</Text>
|
||||
{alert.createTime && <Text className={styles.messageTime}>{alert.createTime}</Text>}
|
||||
</View>
|
||||
{alert.deviceName && (
|
||||
<Text className={styles.messageDevice}>{alert.deviceName}</Text>
|
||||
{(alert.deviceAlias || alert.deviceName) && (
|
||||
<Text className={styles.messageDevice}>{alert.deviceAlias || alert.deviceName}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { HomeAlarm } from '@/api/homeAlarm'
|
||||
|
||||
// 首页告警分流:status=1 未处理(首页只展示最新一条),其余全部进消息列表
|
||||
// 首页告警分流:status=0 待处理(首页只展示最新一条),其余全部进消息列表
|
||||
// (2026-09-23 后端确认:0-待处理 1-已忽略 2-已处理 3-自动处理)
|
||||
export function splitAlerts(list: HomeAlarm[]): {
|
||||
unhandled: HomeAlarm[]
|
||||
messages: HomeAlarm[]
|
||||
|
|
@ -8,7 +9,7 @@ export function splitAlerts(list: HomeAlarm[]): {
|
|||
const unhandled: HomeAlarm[] = []
|
||||
const messages: HomeAlarm[] = []
|
||||
for (const item of list) {
|
||||
if (item.status === 1) unhandled.push(item)
|
||||
if (item.status === 0) unhandled.push(item)
|
||||
else messages.push(item)
|
||||
}
|
||||
return { unhandled, messages }
|
||||
|
|
|
|||
|
|
@ -2,35 +2,37 @@ import { describe, it, expect } from 'vitest'
|
|||
import { splitAlerts } from '@/pages/index/splitAlerts'
|
||||
import type { HomeAlarm } from '@/api/homeAlarm'
|
||||
|
||||
// 字段对齐 2026-09-23 实测响应(content/createTime;status 0-待处理 1-已忽略 2-已处理 3-自动处理)
|
||||
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,
|
||||
deviceName: '1463937a07a5',
|
||||
deviceAlias: '跌倒检测器 No.01',
|
||||
content: '跌倒检测器 No.01发生确认跌倒',
|
||||
alarmType: '1299830603847491584',
|
||||
createTime: '2026-09-23 14:19:08',
|
||||
status: 1,
|
||||
...over
|
||||
})
|
||||
|
||||
describe('splitAlerts', () => {
|
||||
it('status=1 进入未处理组,其余进入消息组', () => {
|
||||
it('status=0 进入待处理组,其余进入消息组', () => {
|
||||
const list = [
|
||||
item({ alarmId: '1', status: 1 }),
|
||||
item({ alarmId: '2', status: 2 }),
|
||||
item({ alarmId: '3', status: 3 })
|
||||
item({ alarmId: '1', status: 0 }),
|
||||
item({ alarmId: '2', status: 1 }),
|
||||
item({ alarmId: '3', status: 2 })
|
||||
]
|
||||
const { unhandled, messages } = splitAlerts(list)
|
||||
expect(unhandled.map((i) => i.alarmId)).toEqual(['1'])
|
||||
expect(messages.map((i) => i.alarmId)).toEqual(['2', '3'])
|
||||
})
|
||||
|
||||
it('status 异常值(如 0、99)落入消息组不丢弃', () => {
|
||||
const list = [item({ alarmId: '1', status: 0 }), item({ alarmId: '2', status: 99 })]
|
||||
it('status 异常值(如 99)落入消息组不丢弃', () => {
|
||||
const list = [item({ alarmId: '1', status: 99 })]
|
||||
const { unhandled, messages } = splitAlerts(list)
|
||||
expect(unhandled).toEqual([])
|
||||
expect(messages.map((i) => i.alarmId)).toEqual(['1', '2'])
|
||||
expect(messages.map((i) => i.alarmId)).toEqual(['1'])
|
||||
})
|
||||
|
||||
it('空列表两组均为空', () => {
|
||||
|
|
@ -41,9 +43,9 @@ describe('splitAlerts', () => {
|
|||
|
||||
it('两组均保持原有相对顺序', () => {
|
||||
const list = [
|
||||
item({ alarmId: '3', status: 2 }),
|
||||
item({ alarmId: '1', status: 1 }),
|
||||
item({ alarmId: '2', status: 2 })
|
||||
item({ alarmId: '3', status: 1 }),
|
||||
item({ alarmId: '1', status: 0 }),
|
||||
item({ alarmId: '2', status: 1 })
|
||||
]
|
||||
const { unhandled, messages } = splitAlerts(list)
|
||||
expect(unhandled.map((i) => i.alarmId)).toEqual(['1'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue