344 lines
13 KiB
TypeScript
344 lines
13 KiB
TypeScript
import { useState } from 'react'
|
||
import { useRouter, useDidShow, useShareAppMessage } from '@tarojs/taro'
|
||
import Taro from '@tarojs/taro'
|
||
import { View, Text, Button } from '@tarojs/components'
|
||
import { useAppSelector } from '@/store/hooks'
|
||
import { Navbar } from '@/components/navbar'
|
||
import { Loading } from '@/components/loading'
|
||
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, 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'
|
||
import { 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>
|
||
|
||
// 成员显示名:家庭内昵称优先,未设置回落微信昵称
|
||
// 注意 2026-09-23 实测:清空昵称后端返回空串 '' 而非 null,需按空白处理
|
||
const memberName = (m: HomeMember): string => m.nickName?.trim() || m.userNickName?.trim() || '成员'
|
||
|
||
// 关爱对象联系人摘要:最多显示前 2 位姓名,其余以 +N 概括;未填则不显示
|
||
const contactsSummary = (c: CareMember): string => {
|
||
const list = c.contacts ?? []
|
||
if (list.length === 0) return ''
|
||
const head = list.slice(0, 2).map((ct) => ct.name).join('、')
|
||
const rest = list.length > 2 ? ` 等 ${list.length} 人` : ''
|
||
return `${head}${rest}`
|
||
}
|
||
|
||
export default function FamilyDetail() {
|
||
const router = useRouter()
|
||
const familyId = router.params.id ?? ''
|
||
const isInviteEnter = router.params.invite === '1'
|
||
const token = useAppSelector((s) => s.auth.token)
|
||
const myUserId = useAppSelector((s) => s.auth.userId)
|
||
|
||
const [loading, setLoading] = useState(true)
|
||
const [joining, setJoining] = useState(false)
|
||
const [family, setFamily] = useState<HomeInfo | null>(null)
|
||
const [members, setMembers] = useState<HomeMember[]>([])
|
||
const [caredList, setCaredList] = useState<CareMember[]>([])
|
||
|
||
// 转让群主:底部入口仅群主可见,点击后弹选人 Modal
|
||
const [transferOpen, setTransferOpen] = useState(false)
|
||
const [transferring, setTransferring] = useState(false)
|
||
// 改昵称:点击成员行触发
|
||
const [renaming, setRenaming] = useState(false)
|
||
|
||
// 拉取家庭数据:详情必须成功;成员/关爱对象列表失败降级为空(受邀者尚未入圈,
|
||
// 这两个接口会报权限错,不能因此把整个页面判成「家庭不存在」)
|
||
// 返回本次拉到的成员列表(供 autoJoin 用最新数据判断,避免 stale state)
|
||
const load = async (): Promise<HomeMember[] | null> => {
|
||
// 分享卡直达本页且未登录:先去登录,登录后回跳本页
|
||
if (!token) {
|
||
const target = encodeURIComponent(`${ROUTES.FAMILY_DETAIL}?id=${familyId}&invite=1`)
|
||
reLaunch(`${ROUTES.LOGIN}?redirect=${target}`)
|
||
return null
|
||
}
|
||
setLoading(true)
|
||
try {
|
||
const home = await getHomeDetail(familyId)
|
||
const [memberList, careList] = await Promise.all([
|
||
getHomeMemberList(familyId).catch(() => [] as HomeMember[]),
|
||
getCareMemberList(familyId).catch(() => [] as CareMember[])
|
||
])
|
||
setFamily(home)
|
||
setMembers(memberList)
|
||
// 上限判定以关爱对象实际列表为准,家庭卡计数仅展示
|
||
setCaredList(careList)
|
||
return memberList
|
||
} catch {
|
||
setFamily(null)
|
||
return null
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 从添加表单返回时刷新;受邀进入且尚未是成员时自动加入(跳过手动点击步骤)
|
||
useDidShow(() => {
|
||
load().then((memberList) => {
|
||
if (
|
||
!isInviteEnter || joining || myUserId == null ||
|
||
!memberList || memberList.length === 0 || memberList.some((m) => m.memberId === myUserId)
|
||
) return
|
||
autoJoin(familyId)
|
||
})
|
||
})
|
||
|
||
// 受邀直达:已登录但非成员时自动加入,无需手点
|
||
const autoJoin = async (homeId: string) => {
|
||
if (joining) return
|
||
setJoining(true)
|
||
try {
|
||
await joinHome(homeId)
|
||
showToast('已加入家庭')
|
||
await load()
|
||
} catch (e) {
|
||
// 加入失败(如接口异常)也要让页面可退出:详情能拉到就展示,拉不到走「家庭不存在」分支(带返回按钮)
|
||
showToast(e instanceof Error ? e.message : '加入失败')
|
||
if (!family) await load()
|
||
} finally {
|
||
setJoining(false)
|
||
}
|
||
}
|
||
|
||
// 邀请分享:接收方点分享卡直达本页,登录后自动加入
|
||
const inviterName = () => {
|
||
const me = members.find((m) => m.memberId === myUserId)
|
||
return memberName(me ?? members[0])
|
||
}
|
||
useShareAppMessage(() => ({
|
||
title: `${inviterName()} 邀请你加入家庭圈「${family?.name ?? ''}」`,
|
||
path: `${ROUTES.FAMILY_DETAIL}?id=${familyId}&invite=1`
|
||
}))
|
||
|
||
const addCared = () => {
|
||
navigateTo(`${ROUTES.FAMILY_CONTACT_ADD}?familyId=${familyId}`)
|
||
}
|
||
|
||
// 我在成员列表里的角色:转让入口只对群主开放
|
||
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 (transferring) return
|
||
setTransferOpen(true)
|
||
}
|
||
const closeTransfer = () => {
|
||
if (transferring) return
|
||
setTransferOpen(false)
|
||
}
|
||
const doTransfer = async (target: HomeMember) => {
|
||
if (transferring) return
|
||
setTransferring(true)
|
||
try {
|
||
await transferOwner({ memberId: target.memberId })
|
||
showToast('已转让群主')
|
||
setTransferOpen(false)
|
||
await load()
|
||
} catch (e) {
|
||
showToast(e instanceof Error ? e.message : '转让失败')
|
||
} finally {
|
||
setTransferring(false)
|
||
}
|
||
}
|
||
|
||
// 点击成员行改昵称: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}>
|
||
<Navbar title="关爱对象信息" variant="hero" />
|
||
<View className={styles.stateWrap}><Loading /></View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
if (!family) {
|
||
return (
|
||
<View className={styles.page}>
|
||
<Navbar title="关爱对象信息" variant="hero" />
|
||
<StateView
|
||
status="error"
|
||
mode="list"
|
||
text="无法获取家庭信息"
|
||
retryText="重新加载"
|
||
onRetry={() => load()}
|
||
/>
|
||
{/* 分享卡直达时页面栈只有本页,navigateBack 无处可退会卡死——退回首页 */}
|
||
<View className={styles.fallbackBtn} hoverClass={styles.fallbackBtnPressed} onClick={() => reLaunch(ROUTES.HOME)}>
|
||
<Text className={styles.fallbackBtnText}>回首页</Text>
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
const caredCount = caredList.length
|
||
const canAdd = caredCount < MAX_CARED_PER_FAMILY
|
||
const stub = (tip: string) => () => showToast(tip)
|
||
|
||
return (
|
||
<View className={styles.page}>
|
||
<Navbar title="关爱对象信息" variant="hero" />
|
||
|
||
<View className={styles.body}>
|
||
<FamilyCard family={family} />
|
||
|
||
{/* 添加关爱对象卡:达上限后整卡隐藏(占位无意义),上限信息改由标题计数表达 */}
|
||
{canAdd && (
|
||
<View
|
||
className={styles.addCared}
|
||
hoverClass={styles.addCaredPressed}
|
||
onClick={addCared}
|
||
>
|
||
<View className={styles.addCaredIcon}>
|
||
<Text className={styles.addCaredIconText}>+</Text>
|
||
</View>
|
||
<Text className={styles.addCaredText}>+ 最多可添加 {MAX_CARED_PER_FAMILY} 人</Text>
|
||
</View>
|
||
)}
|
||
|
||
<View className={styles.section}>
|
||
<View className={styles.sectionTitleRow}>
|
||
<Text className={styles.sectionTitle}>关爱对象</Text>
|
||
<Text className={styles.sectionCount}>
|
||
{caredCount}/{MAX_CARED_PER_FAMILY}
|
||
</Text>
|
||
</View>
|
||
{caredCount === 0 ? (
|
||
<Text className={styles.sectionEmpty}>暂无关爱对象,点击上方卡片添加</Text>
|
||
) : (
|
||
<ListRowGroup>
|
||
{caredList.map((c) => (
|
||
<ListRow
|
||
key={c.memberId}
|
||
label={c.name}
|
||
value={contactsSummary(c)}
|
||
arrow
|
||
onClick={() =>
|
||
navigateTo(`${ROUTES.FAMILY_CONTACT_DETAIL}?familyId=${familyId}&memberId=${c.memberId}`)
|
||
}
|
||
/>
|
||
))}
|
||
</ListRowGroup>
|
||
)}
|
||
</View>
|
||
|
||
<View className={styles.section}>
|
||
<Text className={styles.sectionTitle}>家庭成员</Text>
|
||
{members.length === 0 ? (
|
||
<Text className={styles.sectionEmpty}>暂无其他成员</Text>
|
||
) : (
|
||
<ListRowGroup>
|
||
{members.map((m) => (
|
||
<ListRow
|
||
key={m.memberId}
|
||
label={memberName(m)}
|
||
value={m.role === 'OWNER' ? '群主' : undefined}
|
||
valueClass={styles.valueTag}
|
||
arrow
|
||
onClick={() => renameMember(m)}
|
||
/>
|
||
))}
|
||
</ListRowGroup>
|
||
)}
|
||
</View>
|
||
|
||
<View className={styles.section}>
|
||
<ListRowGroup>
|
||
<ListRow label="家庭编号" value={family.homeId} onClick={stub('复制成功(占位)')} arrow />
|
||
{/* open-type=share 的 Button 铺满整行做命中区,样式与普通 ListRow 一致 */}
|
||
<Button
|
||
className={styles.shareRow}
|
||
hoverClass={styles.shareRowPressed}
|
||
openType="share"
|
||
>
|
||
<Text className={styles.shareLabel}>邀请他人</Text>
|
||
<Text className={styles.shareArrow}>›</Text>
|
||
</Button>
|
||
<ListRow label="转让群主" arrow onClick={openTransfer} />
|
||
</ListRowGroup>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 转让群主选人弹层:全部成员可选(含自己),后端对「转让给自己」返回 400 由 toast 透出 */}
|
||
<Modal visible={transferOpen} onClose={closeTransfer}>
|
||
<Text className={styles.transferTitle}>转让群主给谁?</Text>
|
||
<View className={styles.transferList}>
|
||
{members.length === 0 ? (
|
||
<Text className={styles.transferEmpty}>暂无可转让的成员</Text>
|
||
) : (
|
||
members.map((m) => (
|
||
<View
|
||
key={m.memberId}
|
||
className={styles.transferItem}
|
||
hoverClass={styles.transferItemPressed}
|
||
onClick={() => doTransfer(m)}
|
||
>
|
||
<Text className={styles.transferItemName}>{memberName(m)}</Text>
|
||
<Text className={styles.transferItemMeta}>
|
||
{m.role === 'OWNER' ? '群主' : '成员'} · 群昵称:{m.nickName || '未设置'}
|
||
</Text>
|
||
</View>
|
||
))
|
||
)}
|
||
</View>
|
||
<View className={styles.transferCancel} hoverClass={styles.transferCancelPressed} onClick={closeTransfer}>
|
||
<Text className={styles.transferCancelText}>取消</Text>
|
||
</View>
|
||
</Modal>
|
||
</View>
|
||
)
|
||
}
|