feat(family): 家庭圈列表/详情/关爱对象切换真实后端接口,移除 mock 数据层

This commit is contained in:
ozh 2026-09-16 17:37:21 +08:00
parent b1a9bef903
commit 6889e9b4fa
11 changed files with 145 additions and 392 deletions

46
src/api/careMember.ts Normal file
View File

@ -0,0 +1,46 @@
import { request } from '@/http'
// 关爱对象接口(apifox 2026-09-11 文档版)
// 紧急联系人(文档 YlHomeCareMemberContactItemReq,注意关系字段名为 relationShip)
export interface CareContact {
name: string
phone: string
relationShip: string
}
// 保存入参(文档 YlHomeCareMemberSaveReq:memberId 空=新增,有值=修改)
export interface CareMemberSaveParams {
memberId?: string
homeId: string
name: string // 最多 10 个字
idNumber: string
address: string
contacts: CareContact[] // 最多 5 名
}
// 列表项(响应字段文档未给出,按保存入参推断,联调修正)
export interface CareMember {
memberId: string
homeId: string
name: string
idNumber: string
address: string
contacts: CareContact[]
}
export function saveCareMember(params: CareMemberSaveParams): Promise<void> {
return request.post('/yanglao/app/ylHomeCareMember/save', params as unknown as Record<string, unknown>)
}
export function getCareMemberList(homeId: string): Promise<CareMember[]> {
return request.post('/yanglao/app/ylHomeCareMember/getList', { homeId })
}
export function getCareMemberDetail(memberId: string): Promise<CareMember> {
return request.get(`/yanglao/app/ylHomeCareMember/get/${memberId}`)
}
export function deleteCareMember(memberId: string): Promise<void> {
return request.post('/yanglao/app/ylHomeCareMember/delete', { memberId })
}

View File

@ -1,193 +0,0 @@
import { storage } from '@/utils/storage'
import {
FAMILY_DATA_STORAGE_KEY,
MAX_CARED_PER_FAMILY,
MOCK_ID_SEED
} from '@/constants/family'
export interface Family {
id: string
name: string
memberCount: number
caredCount: number
caredLabel: string
address: string
deviceCount: number
role?: '管理员' | '群主' // 当前用户在家庭圈的角色,弹层显示用
}
export interface EmergencyContact {
id: string
name: string
phone: string
relation: string
}
export interface CaredPerson {
id: string
familyId: string
name: string
idNumber: string
address: string
contacts: EmergencyContact[]
}
export interface FamilyMember {
id: string
familyId: string
name: string
role: 'owner' | 'member'
phone: string
notify: boolean
}
export interface FamilyDetailData {
family: Family
members: FamilyMember[]
cared: CaredPerson[]
}
interface FamilyData {
families: Family[]
cared: CaredPerson[]
members: FamilyMember[]
nextId: number
}
const SEED_DATA: FamilyData = {
families: [
{ id: '846443196293189', name: 'JollyPlace', memberCount: 6, caredCount: 1, caredLabel: 'Jay', address: '--', deviceCount: 24, role: '管理员' },
{ id: '735201884550361', name: '我的家庭圈', memberCount: 4, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 1, role: '群主' },
{ id: '628934071266852', name: '康乐之家', memberCount: 1, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 21, role: '群主' },
{ id: '519027468331740', name: '平安庭院', memberCount: 1, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 0, role: '群主' }
],
cared: [
{
id: '900001',
familyId: '846443196293189',
name: 'Jay',
idNumber: '440301195603150000',
address: '深圳市南山区科技园',
contacts: [
{ id: '910001', name: '张女士', phone: '13800138000', relation: '配偶' },
{ id: '910002', name: '李先生', phone: '13900139000', relation: '儿子' }
]
}
],
members: [
{ id: '900001', familyId: '846443196293189', name: 'Jay', role: 'owner', phone: '13800138000', notify: true },
{ id: '900002', familyId: '846443196293189', name: '小美', role: 'member', phone: '13900139000', notify: false }
],
nextId: MOCK_ID_SEED
}
// 读全量数据;storage 为空时写入样例(首次启动初始化)
function loadData(): FamilyData {
const saved = storage.get<FamilyData>(FAMILY_DATA_STORAGE_KEY)
if (saved) {
if (!saved.families || !Array.isArray(saved.families)) {
console.warn('familyData 存储结构异常,回退默认数据')
return resetData()
}
return saved
}
return resetData()
}
function resetData(): FamilyData {
const data: FamilyData = JSON.parse(JSON.stringify(SEED_DATA))
storage.set(FAMILY_DATA_STORAGE_KEY, data)
return data
}
function saveData(data: FamilyData): void {
storage.set(FAMILY_DATA_STORAGE_KEY, data)
}
// mock 异步语义:与真实 API 调用形式一致
const delay = <T>(value: T, ms = 100): Promise<T> =>
new Promise((resolve) => setTimeout(() => resolve(value), ms))
function nextIdOf(data: FamilyData): string {
const id = String(data.nextId)
data.nextId += 1
return id
}
export async function getFamilies(): Promise<Family[]> {
return delay(loadData().families)
}
export async function getFamilyDetail(id: string): Promise<FamilyDetailData | null> {
const data = loadData()
const family = data.families.find((f) => f.id === id)
if (!family) return delay(null)
const members = data.members.filter((m) => m.familyId === id)
const cared = data.cared.filter((c) => c.familyId === id)
return delay({ family, members, cared })
}
export async function addFamily(name: string): Promise<Family> {
const trimmed = name.trim()
if (!trimmed) throw new Error('请输入家庭名')
const data = loadData()
const family: Family = {
id: nextIdOf(data),
name: trimmed,
memberCount: 1,
caredCount: 0,
caredLabel: '--',
address: '--',
deviceCount: 0,
role: '群主'
}
data.families = [...data.families, family]
saveData(data)
return delay(family)
}
export async function joinFamily(code: string): Promise<Family> {
const trimmed = code.trim()
if (!trimmed) throw new Error('请输入家庭编号')
const data = loadData()
const family: Family = {
id: nextIdOf(data),
name: `加入的 ${trimmed.slice(-4)}`,
memberCount: 2,
caredCount: 0,
caredLabel: '--',
address: '--',
deviceCount: 0,
role: '群主'
}
data.families = [...data.families, family]
saveData(data)
return delay(family)
}
export async function canAddCared(familyId: string): Promise<boolean> {
const data = loadData()
const count = data.cared.filter((c) => c.familyId === familyId).length
return count < MAX_CARED_PER_FAMILY
}
export async function addCared(input: Omit<CaredPerson, 'id'>): Promise<CaredPerson> {
const data = loadData()
const count = data.cared.filter((c) => c.familyId === input.familyId).length
if (count >= MAX_CARED_PER_FAMILY) throw new Error('已达上限')
const person: CaredPerson = { ...input, id: nextIdOf(data) }
data.cared = [...data.cared, person]
// 同步家庭卡上的关爱对象摘要
const family = data.families.find((f) => f.id === input.familyId)
if (family) {
family.caredCount = count + 1
family.caredLabel = person.name
}
saveData(data)
return delay(person)
}
export async function getCaredByFamily(familyId: string): Promise<CaredPerson[]> {
return delay(loadData().cared.filter((c) => c.familyId === familyId))
}

View File

@ -1,21 +1,31 @@
import { request } from '@/http' import { request } from '@/http'
// 骨架:本期仅锁定接口路径,字段类型按文档目录推断,联调时逐个修正(spec D7) // 家庭圈接口(apifox 2026-09-11 文档版)
// 响应 Content 字段文档未给出(ResponseData 泛型占位),类型按命名推断,联调时逐个修正
export interface HomeInfo { export interface HomeInfo {
homeId: string homeId: string
homeName: string homeName: string
ownerUserId: string ownerUserId: string
memberCount: number memberCount: number
// 以下为列表卡展示推断字段,联调确认
careName?: string // 关爱对象名(caredLabel)
careCount?: number // 关爱对象数(上限校验用)
address?: string
deviceCount?: number
role?: string // 当前用户角色(群主/成员)
} }
export interface HomeMember { export interface HomeMember {
userId: string userId: string
nickName: string nickName: string
isOwner: boolean isOwner: boolean
mobile?: string
} }
export function addHome(params: Record<string, unknown>): Promise<void> { export function addHome(name: string): Promise<void> {
return request.post('/yanglao/app/ylHome/add', params) // name 可空,空则后端默认「我的家庭圈」(文档 YlHomeAddReq)
return request.post('/yanglao/app/ylHome/add', { name })
} }
export function updateHome(params: Record<string, unknown>): Promise<void> { export function updateHome(params: Record<string, unknown>): Promise<void> {
@ -27,11 +37,11 @@ export function dissolveHome(params: Record<string, unknown>): Promise<void> {
} }
export function getHomeList(): Promise<HomeInfo[]> { export function getHomeList(): Promise<HomeInfo[]> {
return request.get('/yanglao/app/ylHome/getList') return request.post('/yanglao/app/ylHome/getList')
} }
export function getHomeMemberList(homeId: string): Promise<HomeMember[]> { export function getHomeMemberList(homeId: string): Promise<HomeMember[]> {
return request.get('/yanglao/app/ylHome/getMemberList', { homeId }) return request.post('/yanglao/app/ylHome/getMemberList', { homeId })
} }
export function transferOwner(params: Record<string, unknown>): Promise<void> { export function transferOwner(params: Record<string, unknown>): Promise<void> {
@ -46,6 +56,7 @@ export function getHomeDetail(homeId: string): Promise<HomeInfo> {
return request.get(`/yanglao/app/ylHome/get/${homeId}`) return request.get(`/yanglao/app/ylHome/get/${homeId}`)
} }
export function joinHome(params: Record<string, unknown>): Promise<void> { export function joinHome(code: string): Promise<void> {
return request.post('/yanglao/app/ylHome/join', params) // code 为家庭编号(文档 YlHomeJoinReq)
return request.post('/yanglao/app/ylHome/join', { code })
} }

View File

@ -1,20 +1,20 @@
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import type { Family } from '@/api/family' import type { HomeInfo } from '@/api/home'
import styles from './index.module.scss' import styles from './index.module.scss'
interface Props { interface Props {
family: Family family: HomeInfo
onClick?: (id: string) => void onClick?: (id: string) => void
} }
export function FamilyCard({ family, onClick }: Props) { export function FamilyCard({ family, onClick }: Props) {
const first = family.name.slice(0, 1) const first = family.homeName.slice(0, 1)
return ( return (
<View <View
className={styles.card} className={styles.card}
hoverClass={onClick ? styles.cardPressed : 'none'} hoverClass={onClick ? styles.cardPressed : 'none'}
onClick={() => onClick?.(family.id)} onClick={() => onClick?.(family.homeId)}
> >
{/* 右上淡绿圆装饰 */} {/* 右上淡绿圆装饰 */}
<View className={styles.decor} /> <View className={styles.decor} />
@ -24,21 +24,21 @@ export function FamilyCard({ family, onClick }: Props) {
</View> </View>
<View className={styles.headMain}> <View className={styles.headMain}>
<View className={styles.nameRow}> <View className={styles.nameRow}>
<Text className={styles.name}>{family.name}</Text> <Text className={styles.name}>{family.homeName}</Text>
{family.role && <Text className={styles.roleBadge}>{family.role}</Text>} {family.role && <Text className={styles.roleBadge}>{family.role}</Text>}
</View> </View>
<Text className={styles.subCount}> <Text className={styles.subCount}>
{family.memberCount} 位成员 · {family.deviceCount} 台设备 {family.memberCount} 位成员 · {family.deviceCount ?? 0} 台设备
</Text> </Text>
</View> </View>
</View> </View>
<View className={styles.rows}> <View className={styles.rows}>
<Text className={styles.rowLabel}>关爱对象</Text> <Text className={styles.rowLabel}>关爱对象</Text>
<Text className={styles.rowValue}>{family.caredLabel}</Text> <Text className={styles.rowValue}>{family.careName || '--'}</Text>
</View> </View>
<View className={styles.rows}> <View className={styles.rows}>
<Text className={styles.rowLabel}>关爱地址</Text> <Text className={styles.rowLabel}>关爱地址</Text>
<Text className={styles.rowValue}>{family.address}</Text> <Text className={styles.rowValue}>{family.address || '--'}</Text>
</View> </View>
</View> </View>
) )

View File

@ -11,7 +11,7 @@ import { getDeviceList } from '@/api/device'
import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums' import { DEVICE_ICON_MAP, type DeviceModel } from '@/constants/enums'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { navigateTo } from '@/utils/navigation' import { navigateTo } from '@/utils/navigation'
import { getFamilies, type Family } from '@/api/family' import { getHomeList, type HomeInfo } from '@/api/home'
import styles from './index.module.scss' import styles from './index.module.scss'
const FALLBACK_ICON = '⌂' const FALLBACK_ICON = '⌂'
@ -24,10 +24,10 @@ export default function DeviceList() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false) const [loadError, setLoadError] = useState(false)
const [families, setFamilies] = useState<Family[]>([]) const [families, setFamilies] = useState<HomeInfo[]>([])
const [currentFamily, setCurrentFamily] = useState<Family | null>(null) const [currentFamily, setCurrentFamily] = useState<HomeInfo | null>(null)
const [familySheetOpen, setFamilySheetOpen] = useState(false) const [familySheetOpen, setFamilySheetOpen] = useState(false)
const [pendingFamily, setPendingFamily] = useState<Family | null>(null) const [pendingFamily, setPendingFamily] = useState<HomeInfo | null>(null)
// 在线状态需每次进入页面都新鲜:useDidShow 覆盖 // 在线状态需每次进入页面都新鲜:useDidShow 覆盖
// 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景 // 首次进入、从详情页 navigateBack 返回、tab 切换回三种场景
@ -46,7 +46,7 @@ export default function DeviceList() {
const loadFamilies = useCallback(async () => { const loadFamilies = useCallback(async () => {
try { try {
const list = await getFamilies() const list = await getHomeList()
setFamilies(list) setFamilies(list)
// 若当前未选中,默认选第一条 // 若当前未选中,默认选第一条
setCurrentFamily((prev) => prev ?? list[0] ?? null) setCurrentFamily((prev) => prev ?? list[0] ?? null)
@ -93,11 +93,11 @@ export default function DeviceList() {
<View className={styles.familyCard}> <View className={styles.familyCard}>
<View className={styles.familyRow}> <View className={styles.familyRow}>
<Text className={styles.familyLabel}>设备应用家庭</Text> <Text className={styles.familyLabel}>设备应用家庭</Text>
<Text className={styles.familyValue}>{currentFamily?.name ?? scene?.name ?? '未选择'}</Text> <Text className={styles.familyValue}>{currentFamily?.homeName ?? scene?.name ?? '未选择'}</Text>
</View> </View>
<View className={styles.familyRow}> <View className={styles.familyRow}>
<Text className={styles.familyLabel}>关爱对象</Text> <Text className={styles.familyLabel}>关爱对象</Text>
<Text className={styles.familyValue}>{currentFamily?.caredLabel ?? '--'}</Text> <Text className={styles.familyValue}>{currentFamily?.careName ?? '--'}</Text>
</View> </View>
<View className={styles.familyRow}> <View className={styles.familyRow}>
<Text className={styles.familyLabel}>关爱地址</Text> <Text className={styles.familyLabel}>关爱地址</Text>
@ -150,15 +150,15 @@ export default function DeviceList() {
<BottomSheet visible={familySheetOpen} title="切换家庭圈" onClose={closeFamilySheet}> <BottomSheet visible={familySheetOpen} title="切换家庭圈" onClose={closeFamilySheet}>
<View className={styles.sheetList}> <View className={styles.sheetList}>
{families.map((family) => { {families.map((family) => {
const active = pendingFamily?.id === family.id const active = pendingFamily?.homeId === family.homeId
return ( return (
<View <View
key={family.id} key={family.homeId}
className={`${styles.sheetItem} ${active ? styles.sheetItemActive : ''}`} className={`${styles.sheetItem} ${active ? styles.sheetItemActive : ''}`}
onClick={() => setPendingFamily(family)} onClick={() => setPendingFamily(family)}
> >
<View className={styles.sheetHead}> <View className={styles.sheetHead}>
<Text className={styles.sheetName}>{family.name}</Text> <Text className={styles.sheetName}>{family.homeName}</Text>
{family.role && ( {family.role && (
<View className={styles.sheetRole}> <View className={styles.sheetRole}>
<Text className={styles.sheetRoleText}>{family.role}</Text> <Text className={styles.sheetRoleText}>{family.role}</Text>
@ -166,8 +166,8 @@ export default function DeviceList() {
)} )}
</View> </View>
<View className={styles.sheetMeta}> <View className={styles.sheetMeta}>
<Text className={styles.sheetMetaItem}>关爱对象 {family.caredLabel}</Text> <Text className={styles.sheetMetaItem}>关爱对象 {family.careName || '--'}</Text>
<Text className={styles.sheetMetaItem}>设备数量 {family.deviceCount}台</Text> <Text className={styles.sheetMetaItem}>设备数量 {family.deviceCount ?? 0}台</Text>
</View> </View>
</View> </View>
) )

View File

@ -2,18 +2,19 @@ import { useState } from 'react'
import { useRouter } from '@tarojs/taro' import { useRouter } from '@tarojs/taro'
import { View, Text, Input } from '@tarojs/components' import { View, Text, Input } from '@tarojs/components'
import { Navbar } from '@/components/navbar' import { Navbar } from '@/components/navbar'
import { addCared, type EmergencyContact } from '@/api/family' import { saveCareMember, type CareContact } from '@/api/careMember'
import { MAX_EMERGENCY_CONTACTS, MAX_FAMILY_NAME_LEN } from '@/constants/family' import { MAX_EMERGENCY_CONTACTS, MAX_FAMILY_NAME_LEN } from '@/constants/family'
import { navigateBack } from '@/utils/navigation' import { navigateBack } from '@/utils/navigation'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
import { validateContactForm } from './validate' import { validateContactForm } from './validate'
import styles from './add.module.scss' import styles from './add.module.scss'
const emptyContact = (): EmergencyContact => ({ let tmpSeq = 0
id: `tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, const emptyContact = (): (CareContact & { tmpId: string }) => ({
tmpId: `tmp-${Date.now()}-${(tmpSeq += 1)}`,
name: '', name: '',
phone: '', phone: '',
relation: '' relationShip: ''
}) })
export default function ContactAdd() { export default function ContactAdd() {
@ -23,15 +24,15 @@ export default function ContactAdd() {
const [name, setName] = useState('') const [name, setName] = useState('')
const [idNumber, setIdNumber] = useState('') const [idNumber, setIdNumber] = useState('')
const [address, setAddress] = useState('') const [address, setAddress] = useState('')
const [contacts, setContacts] = useState<EmergencyContact[]>([]) const [contacts, setContacts] = useState<(CareContact & { tmpId: string })[]>([])
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const patchContact = (id: string, key: keyof EmergencyContact, value: string) => { const patchContact = (id: string, key: keyof CareContact, value: string) => {
setContacts((prev) => prev.map((c) => (c.id === id ? { ...c, [key]: value } : c))) setContacts((prev) => prev.map((c) => (c.tmpId === id ? { ...c, [key]: value } : c)))
} }
const removeContact = (id: string) => { const removeContact = (id: string) => {
setContacts((prev) => prev.filter((c) => c.id !== id)) setContacts((prev) => prev.filter((c) => c.tmpId !== id))
} }
const submit = async () => { const submit = async () => {
@ -44,8 +45,8 @@ export default function ContactAdd() {
setSubmitting(true) setSubmitting(true)
try { try {
await addCared({ await saveCareMember({
familyId, homeId: familyId,
name: name.trim(), name: name.trim(),
idNumber: idNumber.trim(), idNumber: idNumber.trim(),
address: address.trim(), address: address.trim(),
@ -107,8 +108,8 @@ export default function ContactAdd() {
<Text className={styles.contactsTip}>最多可添加 {MAX_EMERGENCY_CONTACTS} 位紧急联系人,可不填</Text> <Text className={styles.contactsTip}>最多可添加 {MAX_EMERGENCY_CONTACTS} 位紧急联系人,可不填</Text>
{contacts.map((c) => ( {contacts.map((c) => (
<View key={c.id} className={styles.contactItem}> <View key={c.tmpId} className={styles.contactItem}>
<View className={styles.contactDel} hoverClass={styles.contactDelPressed} onClick={() => removeContact(c.id)}> <View className={styles.contactDel} hoverClass={styles.contactDelPressed} onClick={() => removeContact(c.tmpId)}>
<View className={styles.contactDelInner}> <View className={styles.contactDelInner}>
<Text className={styles.contactDelText}>×</Text> <Text className={styles.contactDelText}>×</Text>
</View> </View>
@ -119,7 +120,7 @@ export default function ContactAdd() {
className={styles.input} className={styles.input}
placeholder="联系人姓名" placeholder="联系人姓名"
value={c.name} value={c.name}
onInput={(e) => patchContact(c.id, 'name', e.detail.value)} onInput={(e) => patchContact(c.tmpId, 'name', e.detail.value)}
/> />
</View> </View>
<View className={styles.formRow}> <View className={styles.formRow}>
@ -129,7 +130,7 @@ export default function ContactAdd() {
type="number" type="number"
placeholder="联系人电话" placeholder="联系人电话"
value={c.phone} value={c.phone}
onInput={(e) => patchContact(c.id, 'phone', e.detail.value)} onInput={(e) => patchContact(c.tmpId, 'phone', e.detail.value)}
/> />
</View> </View>
<View className={styles.formRow}> <View className={styles.formRow}>
@ -137,8 +138,8 @@ export default function ContactAdd() {
<Input <Input
className={styles.input} className={styles.input}
placeholder="如:配偶、儿子" placeholder="如:配偶、儿子"
value={c.relation} value={c.relationShip}
onInput={(e) => patchContact(c.id, 'relation', e.detail.value)} onInput={(e) => patchContact(c.tmpId, 'relationShip', e.detail.value)}
/> />
</View> </View>
</View> </View>

View File

@ -1,14 +1,14 @@
import type { EmergencyContact } from '@/api/family' import type { CareContact } from '@/api/careMember'
export interface ContactFormInput { export interface ContactFormInput {
name: string name: string
idNumber: string idNumber: string
address: string address: string
contacts: EmergencyContact[] contacts: (CareContact & { tmpId: string })[]
} }
export type ValidateResult = export type ValidateResult =
| { ok: true; contacts: EmergencyContact[] } | { ok: true; contacts: CareContact[] }
| { ok: false; error: string } | { ok: false; error: string }
// 必填项齐全且联系人卡要么空白(忽略)要么填全,才允许提交 // 必填项齐全且联系人卡要么空白(忽略)要么填全,才允许提交
@ -22,5 +22,7 @@ export function validateContactForm(input: ContactFormInput): ValidateResult {
return { ok: false, error: '请完整填写联系人' } return { ok: false, error: '请完整填写联系人' }
} }
return { ok: true, contacts: filled } // 提交前剥掉仅供 React key 用的 tmpId
const contacts = filled.map(({ tmpId: _tmpId, ...c }) => c)
return { ok: true, contacts }
} }

View File

@ -6,11 +6,7 @@ import { Loading } from '@/components/loading'
import { StateView } from '@/components/state-view' import { StateView } from '@/components/state-view'
import { FamilyCard } from '@/components/family-card' import { FamilyCard } from '@/components/family-card'
import { ListRow, ListRowGroup } from '@/components/list-row' import { ListRow, ListRowGroup } from '@/components/list-row'
import { import { getHomeDetail, getHomeMemberList, type HomeInfo, type HomeMember } from '@/api/home'
getFamilyDetail,
canAddCared,
type FamilyDetailData
} from '@/api/family'
import { MAX_CARED_PER_FAMILY } from '@/constants/family' import { MAX_CARED_PER_FAMILY } from '@/constants/family'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { navigateBack, navigateTo } from '@/utils/navigation' import { navigateBack, navigateTo } from '@/utils/navigation'
@ -22,29 +18,33 @@ export default function FamilyDetail() {
const familyId = router.params.id ?? '' const familyId = router.params.id ?? ''
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [detail, setDetail] = useState<FamilyDetailData | null>(null) const [family, setFamily] = useState<HomeInfo | null>(null)
const [members, setMembers] = useState<HomeMember[]>([])
const [caredCount, setCaredCount] = useState(0)
const load = async () => { const load = async () => {
setLoading(true) setLoading(true)
try { try {
setDetail(await getFamilyDetail(familyId)) const [home, memberList] = await Promise.all([
getHomeDetail(familyId),
getHomeMemberList(familyId)
])
setFamily(home)
setMembers(memberList)
setCaredCount(home.careCount ?? 0)
} catch { } catch {
setDetail(null) setFamily(null)
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
// 从添加表单返回时刷新(caredCount 变化) // 从添加表单返回时刷新
useDidShow(() => { useDidShow(() => {
load() load()
}) })
const addCared = async () => { const addCared = () => {
if (!(await canAddCared(familyId))) {
showToast('已达上限')
return
}
navigateTo(`${ROUTES.FAMILY_CONTACT_ADD}?familyId=${familyId}`) navigateTo(`${ROUTES.FAMILY_CONTACT_ADD}?familyId=${familyId}`)
} }
@ -57,7 +57,7 @@ export default function FamilyDetail() {
) )
} }
if (!detail) { if (!family) {
return ( return (
<View className={styles.page}> <View className={styles.page}>
<Navbar title="关爱对象信息" variant="hero" /> <Navbar title="关爱对象信息" variant="hero" />
@ -72,7 +72,7 @@ export default function FamilyDetail() {
) )
} }
const canAdd = detail.family.caredCount < MAX_CARED_PER_FAMILY const canAdd = caredCount < MAX_CARED_PER_FAMILY
const stub = (tip: string) => () => showToast(tip) const stub = (tip: string) => () => showToast(tip)
return ( return (
@ -80,7 +80,7 @@ export default function FamilyDetail() {
<Navbar title="关爱对象信息" variant="hero" /> <Navbar title="关爱对象信息" variant="hero" />
<View className={styles.body}> <View className={styles.body}>
<FamilyCard family={detail.family} /> <FamilyCard family={family} />
{/* 添加关爱对象卡:达上限后灰化静态 */} {/* 添加关爱对象卡:达上限后灰化静态 */}
<View <View
@ -102,15 +102,15 @@ export default function FamilyDetail() {
<View className={styles.section}> <View className={styles.section}>
<ListRowGroup> <ListRowGroup>
{detail.members.map((m) => ( {members.map((m) => (
<ListRow key={m.id} label={m.name} value={m.role === 'owner' ? '群主' : '成员'} /> <ListRow key={m.userId} label={m.nickName} value={m.isOwner ? '群主' : '成员'} />
))} ))}
</ListRowGroup> </ListRowGroup>
</View> </View>
<View className={styles.section}> <View className={styles.section}>
<ListRowGroup> <ListRowGroup>
<ListRow label="家庭编号" value={detail.family.id} onClick={stub('复制成功(占位)')} arrow /> <ListRow label="家庭编号" value={family.homeId} onClick={stub('复制成功(占位)')} arrow />
<ListRow label="邀请他人" arrow onClick={stub('邀请功能即将上线')} /> <ListRow label="邀请他人" arrow onClick={stub('邀请功能即将上线')} />
<ListRow label="转让群主" arrow onClick={stub('转让功能即将上线')} /> <ListRow label="转让群主" arrow onClick={stub('转让功能即将上线')} />
<ListRow label="修改群昵称" arrow onClick={stub('修改昵称功能即将上线')} /> <ListRow label="修改群昵称" arrow onClick={stub('修改昵称功能即将上线')} />

View File

@ -4,7 +4,7 @@ import { View, Text } from '@tarojs/components'
import { Navbar } from '@/components/navbar' import { Navbar } from '@/components/navbar'
import { Modal } from '@/components/modal' import { Modal } from '@/components/modal'
import { FamilyCard } from '@/components/family-card' import { FamilyCard } from '@/components/family-card'
import { getFamilies, addFamily, joinFamily, type Family } from '@/api/family' import { getHomeList, addHome, joinHome, type HomeInfo } from '@/api/home'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { navigateTo } from '@/utils/navigation' import { navigateTo } from '@/utils/navigation'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
@ -24,18 +24,18 @@ const showEditableModal = (option: EditableModalOption) =>
Taro.showModal(option as Taro.showModal.Option) as Promise<EditableModalResult> Taro.showModal(option as Taro.showModal.Option) as Promise<EditableModalResult>
export default function Family() { export default function Family() {
const [list, setList] = useState<Family[]>([]) const [list, setList] = useState<HomeInfo[]>([])
const [showCreateModal, setShowCreateModal] = useState(false) const [showCreateModal, setShowCreateModal] = useState(false)
const reload = async () => { const reload = async () => {
try { try {
setList(await getFamilies()) setList(await getHomeList())
} catch { } catch (e) {
showToast('加载失败') showToast(e instanceof Error ? e.message : '加载失败')
} }
} }
// 从详情页返回时刷新(caredCount 变化) // 从详情页返回时刷新(关爱对象变化)
useDidShow(() => { useDidShow(() => {
reload() reload()
}) })
@ -50,7 +50,7 @@ export default function Family() {
}).then(async ({ confirm, content }) => { }).then(async ({ confirm, content }) => {
if (!confirm) return if (!confirm) return
try { try {
await addFamily(content ?? '') await addHome(content ?? '')
setShowCreateModal(false) setShowCreateModal(false)
await reload() await reload()
} catch (error) { } catch (error) {
@ -67,7 +67,7 @@ export default function Family() {
}).then(async ({ confirm, content }) => { }).then(async ({ confirm, content }) => {
if (!confirm) return if (!confirm) return
try { try {
await joinFamily(content ?? '') await joinHome(content ?? '')
setShowCreateModal(false) setShowCreateModal(false)
await reload() await reload()
} catch (error) { } catch (error) {
@ -82,7 +82,7 @@ export default function Family() {
<View className={styles.list}> <View className={styles.list}>
{list.map((family) => ( {list.map((family) => (
<FamilyCard key={family.id} family={family} onClick={goDetail} /> <FamilyCard key={family.homeId} family={family} onClick={goDetail} />
))} ))}
{/* 底部加号卡:弹新建/加入 */} {/* 底部加号卡:弹新建/加入 */}

View File

@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { validateContactForm, type ContactFormInput } from '@/pages/family/contact/validate' import { validateContactForm, type ContactFormInput } from '@/pages/family/contact/validate'
import type { EmergencyContact } from '@/api/family' import type { CareContact } from '@/api/careMember'
const contact = (over: Partial<EmergencyContact> = {}): EmergencyContact => ({ const contact = (over: Partial<CareContact & { tmpId: string }> = {}): CareContact & { tmpId: string } => ({
id: 'c1', tmpId: 'c1',
name: '', name: '',
phone: '', phone: '',
relation: '', relationShip: '',
...over ...over
}) })
@ -47,10 +47,12 @@ describe('添加关爱对象表单校验', () => {
expect(!r2.ok && r2.error).toBe('请完整填写联系人') expect(!r2.ok && r2.error).toBe('请完整填写联系人')
}) })
it('姓名电话齐全的联系人保留在结果中', () => { it('姓名电话齐全的联系人保留,且提交前剥掉 tmpId', () => {
const c = contact({ id: 'c9', name: '张女士', phone: '13800138000', relation: '配偶' }) const c = contact({ tmpId: 'c9', name: '张女士', phone: '13800138000', relationShip: '配偶' })
const result = validateContactForm(form({ contacts: [c] })) const result = validateContactForm(form({ contacts: [c] }))
expect(result.ok).toBe(true) expect(result.ok).toBe(true)
if (result.ok) expect(result.contacts).toEqual([c]) if (result.ok) {
expect(result.contacts).toEqual([{ name: '张女士', phone: '13800138000', relationShip: '配偶' }])
}
}) })
}) })

View File

@ -1,116 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
// mock storage:内存 Map 语义,与 Taro.setStorageSync/getStorageSync 等价
const store = new Map<string, unknown>()
vi.mock('@/utils/storage', () => ({
storage: {
get: <T>(key: string): T | null => (store.has(key) ? (store.get(key) as T) : null),
set: <T>(key: string, value: T): void => { store.set(key, value) },
remove: (key: string): void => { store.delete(key) }
}
}))
import {
getFamilies,
getFamilyDetail,
addFamily,
joinFamily,
canAddCared,
addCared,
getCaredByFamily,
type CaredPerson
} from '@/api/family'
import { FAMILY_DATA_STORAGE_KEY } from '@/constants/family'
const caredInput = (over: Partial<CaredPerson> = {}): Omit<CaredPerson, 'id'> => ({
familyId: '735201884550361',
name: '王奶奶',
idNumber: '440301194001010000',
address: '深圳市南山区',
contacts: [],
...over
})
beforeEach(() => {
store.clear()
})
describe('家庭圈 mock 数据层', () => {
it('首次调用 getFamilies 写入样例并返回 4 条', async () => {
const list = await getFamilies()
expect(list).toHaveLength(4)
expect(store.has(FAMILY_DATA_STORAGE_KEY)).toBe(true)
})
it('addFamily 追加家庭且持久化,caredCount 从 0 开始', async () => {
await getFamilies()
const f = await addFamily('测试家庭')
expect(f.name).toBe('测试家庭')
expect(f.caredCount).toBe(0)
const again = await getFamilies()
expect(again).toHaveLength(5)
expect(again.some((x) => x.id === f.id)).toBe(true)
})
it('addFamily 空名抛错', async () => {
await getFamilies()
await expect(addFamily(' ')).rejects.toThrow('请输入家庭名')
})
it('joinFamily 按编号生成新家庭', async () => {
await getFamilies()
const f = await joinFamily('1234567890')
expect(f.name).toBe('加入的 7890')
const again = await getFamilies()
expect(again).toHaveLength(5)
})
it('getFamilyDetail 返回家庭 + 成员 + 关爱对象', async () => {
await getFamilies()
const detail = await getFamilyDetail('846443196293189')
expect(detail).not.toBeNull()
expect(detail!.family.name).toBe('JollyPlace')
expect(detail!.members.length).toBe(2)
expect(detail!.cared.length).toBe(1)
expect(detail!.cared[0].name).toBe('Jay')
})
it('getFamilyDetail 不存在返回 null', async () => {
await getFamilies()
expect(await getFamilyDetail('nope')).toBeNull()
})
it('canAddCared 未达上限 true,加满后 false', async () => {
await getFamilies()
// JollyPlace 已有 Jay(1/2),仍未达上限
expect(await canAddCared('846443196293189')).toBe(true)
expect(await canAddCared('735201884550361')).toBe(true)
await addCared(caredInput())
await addCared(caredInput({ name: '李爷爷' }))
expect(await canAddCared('735201884550361')).toBe(false)
})
it('addCared 写入并同步家庭 caredCount/caredLabel', async () => {
await getFamilies()
const saved = await addCared(caredInput())
expect(saved.id).toBeTruthy()
const detail = await getFamilyDetail('735201884550361')
expect(detail).not.toBeNull()
expect(detail!.family.caredCount).toBe(1)
expect(detail!.family.caredLabel).toBe('王奶奶')
expect(detail!.cared[0].contacts).toEqual([])
})
it('addCared 达上限抛错', async () => {
await getFamilies()
await addCared(caredInput())
await addCared(caredInput({ name: '李爷爷' }))
await expect(addCared(caredInput({ name: '第三人' }))).rejects.toThrow('已达上限')
})
it('getCaredByFamily 返回该家庭的关爱对象数组', async () => {
await getFamilies()
const list = await getCaredByFamily('846443196293189')
expect(list.map((c) => c.name)).toEqual(['Jay'])
})
})