SEC_Warehouse/src/pages/family/detail.tsx

344 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}