feat(family): 关爱对象详情页(查看/删除/编辑),列表行接入跳转

- 新建 pages/family/contact/detail:getCareMemberDetail 展示姓名/身份证脱敏/地址/紧急联系人,删除走确认弹窗 + deleteCareMember
- add 页支持编辑模式:memberId 参数预填原数据,save 带 memberId 走修改
- 家庭详情页关爱对象行加箭头跳转详情
This commit is contained in:
ozh 2026-09-17 11:56:35 +08:00
parent 2832053569
commit a1901b5611
7 changed files with 310 additions and 6 deletions

View File

@ -12,6 +12,7 @@ export default {
'pages/family/index',
'pages/family/detail',
'pages/family/contact/add',
'pages/family/contact/detail',
'pages/event/index'
],
window: {

View File

@ -11,5 +11,6 @@ export const ROUTES = Object.freeze({
FAMILY: '/pages/family/index',
FAMILY_DETAIL: '/pages/family/detail',
FAMILY_CONTACT_ADD: '/pages/family/contact/add',
FAMILY_CONTACT_DETAIL: '/pages/family/contact/detail',
SCENE_SELECT: '/pages/scene/select'
})

View File

@ -5,6 +5,13 @@
padding-bottom: 40px;
}
.stateWrap {
margin-top: 80px;
display: flex;
flex-direction: column;
align-items: center;
}
.card,
.contactsCard {
margin: 24px 24px 0;

View File

@ -1,8 +1,9 @@
import { useState } from 'react'
import { useRouter } from '@tarojs/taro'
import { useRouter, useDidShow } from '@tarojs/taro'
import { View, Text, Input } from '@tarojs/components'
import { Navbar } from '@/components/navbar'
import { saveCareMember, type CareContact } from '@/api/careMember'
import { Loading } from '@/components/loading'
import { saveCareMember, getCareMemberDetail, type CareContact } from '@/api/careMember'
import { MAX_EMERGENCY_CONTACTS, MAX_FAMILY_NAME_LEN } from '@/constants/family'
import { navigateBack } from '@/utils/navigation'
import { showToast } from '@/utils/toast'
@ -17,16 +18,39 @@ const emptyContact = (): (CareContact & { tmpId: string }) => ({
relationShip: ''
})
// 编辑模式预填:后端 contacts 可能 null,统一补 tmpId
const toRows = (contacts: CareContact[] | null): (CareContact & { tmpId: string })[] =>
(contacts ?? []).map((c) => ({ ...c, tmpId: `tmp-${Date.now()}-${(tmpSeq += 1)}` }))
export default function ContactAdd() {
const router = useRouter()
const familyId = router.params.familyId ?? ''
const memberId = router.params.memberId ?? ''
const isEdit = memberId !== ''
const [loading, setLoading] = useState(isEdit)
const [name, setName] = useState('')
const [idNumber, setIdNumber] = useState('')
const [address, setAddress] = useState('')
const [contacts, setContacts] = useState<(CareContact & { tmpId: string })[]>([])
const [submitting, setSubmitting] = useState(false)
// 编辑模式进入时拉取原数据预填
useDidShow(() => {
if (!isEdit) return
getCareMemberDetail(memberId)
.then((m) => {
setName(m.name)
setIdNumber(m.idNumber)
setAddress(m.address)
setContacts(toRows(m.contacts))
})
.catch((e: unknown) => {
showToast(e instanceof Error ? e.message : '加载失败')
})
.finally(() => setLoading(false))
})
const patchContact = (id: string, key: keyof CareContact, value: string) => {
setContacts((prev) => prev.map((c) => (c.tmpId === id ? { ...c, [key]: value } : c)))
}
@ -46,13 +70,14 @@ export default function ContactAdd() {
setSubmitting(true)
try {
await saveCareMember({
memberId: memberId || undefined,
homeId: familyId,
name: name.trim(),
idNumber: idNumber.trim(),
address: address.trim(),
contacts: result.contacts
})
showToast('添加成功')
showToast(isEdit ? '修改成功' : '添加成功')
setTimeout(() => navigateBack(), 500)
} catch (error) {
showToast(error instanceof Error ? error.message : '添加失败')
@ -61,9 +86,18 @@ export default function ContactAdd() {
}
}
if (loading) {
return (
<View className={styles.page}>
<Navbar title="添加关爱对象" variant="hero" />
<Navbar title={isEdit ? '编辑关爱对象' : '添加关爱对象'} variant="hero" />
<View className={styles.stateWrap}><Loading /></View>
</View>
)
}
return (
<View className={styles.page}>
<Navbar title={isEdit ? '编辑关爱对象' : '添加关爱对象'} variant="hero" />
<View className={styles.card}>
<View className={styles.formRow}>
@ -151,7 +185,7 @@ export default function ContactAdd() {
hoverClass={submitting ? 'none' : styles.submitPressed}
onClick={submit}
>
<Text className={styles.submitText}>{submitting ? '提交中...' : '确认添加'}</Text>
<Text className={styles.submitText}>{submitting ? '提交中...' : isEdit ? '保存修改' : '确认添加'}</Text>
</View>
</View>
)

View File

@ -0,0 +1,109 @@
.page {
min-height: 100vh;
background: $color-bg;
padding-bottom: 40px;
}
.stateWrap {
margin-top: 80px;
display: flex;
flex-direction: column;
align-items: center;
}
.body {
padding: 24px;
}
// 顶部概览:圆头像(首字)+ 姓名,对齐 FamilyCard 头部风格
.profile {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0 32px;
}
.avatar {
width: 120px;
height: 120px;
border-radius: $radius-circle;
background: $color-primary-grad;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.avatarText {
font-size: 48px;
font-weight: 600;
color: #ffffff;
}
.name {
font-size: 34px;
font-weight: 600;
color: $color-text;
}
.section {
margin-bottom: 24px;
}
.sectionTitle {
display: block;
font-size: 26px;
color: $color-text-secondary;
margin: 8px 8px 16px;
}
.sectionEmpty {
display: block;
padding: 32px 8px;
font-size: 26px;
color: $color-text-tertiary;
}
// 编辑/删除双按钮:编辑主色实底、删除白底红字描边
.btnRow {
display: flex;
gap: 24px;
margin-top: 32px;
}
.editBtn {
flex: 1;
height: 96px;
background: $color-primary-grad;
border-radius: $radius-pill;
display: flex;
align-items: center;
justify-content: center;
}
.editBtnText {
font-size: 32px;
font-weight: 600;
color: #ffffff;
}
.delBtn {
flex: 1;
height: 96px;
background: $color-bg-white;
border: 2px solid $color-danger;
border-radius: $radius-pill;
display: flex;
align-items: center;
justify-content: center;
}
.delBtnText {
font-size: 32px;
font-weight: 600;
color: $color-danger;
}
.btnPressed {
opacity: 0.85;
}

View File

@ -0,0 +1,144 @@
import { useState } from 'react'
import { useRouter, useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading'
import { StateView } from '@/components/state-view'
import { ListRow, ListRowGroup } from '@/components/list-row'
import { getCareMemberDetail, deleteCareMember, type CareMember } from '@/api/careMember'
import { ROUTES } from '@/constants/routes'
import { navigateBack, navigateTo } from '@/utils/navigation'
import { showToast } from '@/utils/toast'
import styles from './detail.module.scss'
export default function ContactDetail() {
const router = useRouter()
const memberId = router.params.memberId ?? ''
const familyId = router.params.familyId ?? ''
const [loading, setLoading] = useState(true)
const [member, setMember] = useState<CareMember | null>(null)
const [deleting, setDeleting] = useState(false)
const load = async () => {
setLoading(true)
try {
setMember(await getCareMemberDetail(memberId))
} catch {
setMember(null)
} finally {
setLoading(false)
}
}
// 从编辑表单返回时刷新
useDidShow(() => {
load()
})
const goEdit = () => {
navigateTo(`${ROUTES.FAMILY_CONTACT_ADD}?familyId=${familyId}&memberId=${memberId}`)
}
const confirmDelete = () => {
if (deleting) return
Taro.showModal({
title: '删除关爱对象',
content: `确定删除「${member?.name ?? ''}」吗?删除后不可恢复。`,
confirmText: '删除',
confirmColor: '#EE4B4B'
}).then(async ({ confirm }) => {
if (!confirm) return
setDeleting(true)
try {
await deleteCareMember(memberId)
showToast('已删除')
setTimeout(() => navigateBack(), 500)
} catch (error) {
showToast(error instanceof Error ? error.message : '删除失败')
} finally {
setDeleting(false)
}
})
}
if (loading) {
return (
<View className={styles.page}>
<Navbar title="关爱对象详情" variant="hero" />
<View className={styles.stateWrap}><Loading /></View>
</View>
)
}
if (!member) {
return (
<View className={styles.page}>
<Navbar title="关爱对象详情" variant="hero" />
<StateView
status="error"
mode="list"
text="该关爱对象不存在"
retryText="返回"
onRetry={() => navigateBack()}
/>
</View>
)
}
const contacts = member.contacts ?? []
// 身份证号脱敏:保留前 4 后 4
const maskedId = member.idNumber
? `${member.idNumber.slice(0, 4)}**********${member.idNumber.slice(-4)}`
: '--'
return (
<View className={styles.page}>
<Navbar title="关爱对象详情" variant="hero" />
<View className={styles.body}>
<View className={styles.profile}>
<View className={styles.avatar}>
<Text className={styles.avatarText}>{member.name.slice(0, 1)}</Text>
</View>
<Text className={styles.name}>{member.name}</Text>
</View>
<View className={styles.section}>
<ListRowGroup>
<ListRow label="姓名" value={member.name} />
<ListRow label="身份证号" value={maskedId} />
<ListRow label="关爱地址" value={member.address || '--'} />
</ListRowGroup>
</View>
<View className={styles.section}>
<Text className={styles.sectionTitle}>紧急联系人({contacts.length})</Text>
{contacts.length === 0 ? (
<Text className={styles.sectionEmpty}>未填写紧急联系人</Text>
) : (
<ListRowGroup>
{contacts.map((c, i) => (
<ListRow key={`${c.name}-${i}`} label={`${i + 1}. ${c.name}`} value={c.relationShip ? `${c.relationShip} · ${c.phone}` : c.phone} />
))}
</ListRowGroup>
)}
</View>
<View className={styles.btnRow}>
<View className={styles.editBtn} hoverClass={styles.btnPressed} onClick={goEdit}>
<Text className={styles.editBtnText}>编辑信息</Text>
</View>
<View
className={styles.delBtn}
hoverClass={deleting ? 'none' : styles.btnPressed}
onClick={deleting ? undefined : confirmDelete}
>
<Text className={styles.delBtnText}>{deleting ? '删除中...' : '删除'}</Text>
</View>
</View>
</View>
</View>
)
}

View File

@ -123,7 +123,15 @@ export default function FamilyDetail() {
) : (
<ListRowGroup>
{caredList.map((c) => (
<ListRow key={c.memberId} label={c.name} value={contactsSummary(c)} />
<ListRow
key={c.memberId}
label={c.name}
value={contactsSummary(c)}
arrow
onClick={() =>
navigateTo(`${ROUTES.FAMILY_CONTACT_DETAIL}?familyId=${familyId}&memberId=${c.memberId}`)
}
/>
))}
</ListRowGroup>
)}