合入家庭共享与设备消息能力

This commit is contained in:
stu2not
2026-05-26 17:12:51 +08:00
parent 555a2ddc16
commit 050bc17f79
40 changed files with 3072 additions and 165 deletions
+281 -4
View File
@@ -1,7 +1,9 @@
import { View, Text, Image, Input } from '@tarojs/components'
import { View, Text, Image, Input, Button } from '@tarojs/components'
import { useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
import { clearToken, getToken } from '@/services/auth'
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
import { getCurrentUserId } from '@/services/session'
import {
BindingListItem,
clearSelectedBindingDeviceId,
@@ -10,6 +12,14 @@ import {
unbindDevice,
} from '@/services/binding'
import { Child, clearSelectedChildId, createChild, deleteChild, setSelectedChildId, updateChild } from '@/services/child'
import {
createFamilyInvitation,
FamilyMember,
FamilyMemberListResponse,
getFamilyMembers,
leaveFamily,
removeFamilyMember,
} from '@/services/family'
import {
DeviceCurrentRole,
DeviceFirmwareStatus,
@@ -47,10 +57,17 @@ export default function Sleep() {
const [isUpdatingFirmware, setIsUpdatingFirmware] = useState(false)
const [isLoadingRoles, setIsLoadingRoles] = useState(false)
const [isUpdatingRole, setIsUpdatingRole] = useState(false)
const [familyInfo, setFamilyInfo] = useState<FamilyMemberListResponse | null>(null)
const [isLoadingFamily, setIsLoadingFamily] = useState(false)
const [isCreatingInvite, setIsCreatingInvite] = useState(false)
const [isUpdatingFamily, setIsUpdatingFamily] = useState(false)
const [familyInvitePath, setFamilyInvitePath] = useState('')
const [familyInviteExpiresAt, setFamilyInviteExpiresAt] = useState('')
const [showModal, setShowModal] = useState(false)
const [showChildModal, setShowChildModal] = useState(false)
const [showDeleteChildModal, setShowDeleteChildModal] = useState(false)
const [showRoleModal, setShowRoleModal] = useState(false)
const [showFamilyModal, setShowFamilyModal] = useState(false)
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
const [childName, setChildName] = useState('')
const [editingChildId, setEditingChildId] = useState<number | null>(null)
@@ -60,6 +77,14 @@ export default function Sleep() {
void loadData()
})
useShareAppMessage(() => {
const targetName = currentChild?.child_name || binding?.device_id || '孩子'
return {
title: `邀请你加入${targetName}的家庭共享`,
path: familyInvitePath || '/pages/device/index',
}
})
const loadData = async () => {
if (!getToken()) {
Taro.reLaunch({ url: '/pages/login/index' })
@@ -77,6 +102,7 @@ export default function Sleep() {
setParentInfo(Taro.getStorageSync('userInfo') || {})
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
} catch (error: any) {
console.error('[manage] load failed:', error)
Taro.showToast({
@@ -144,6 +170,135 @@ export default function Sleep() {
}
}
const loadFamilyData = async (deviceId?: string | null) => {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) {
setFamilyInfo(null)
setFamilyInvitePath('')
setFamilyInviteExpiresAt('')
setIsLoadingFamily(false)
return
}
setFamilyInvitePath('')
setFamilyInviteExpiresAt('')
setIsLoadingFamily(true)
try {
const nextFamilyInfo = await getFamilyMembers(normalizedDeviceId)
setFamilyInfo(nextFamilyInfo)
} catch (error: any) {
console.error('[manage] family load failed:', error)
setFamilyInfo(null)
setFamilyInvitePath('')
setFamilyInviteExpiresAt('')
if (showFamilyModal) {
Taro.showToast({
title: error?.message || '家庭成员加载失败',
icon: 'none',
})
}
} finally {
setIsLoadingFamily(false)
}
}
const handleOpenFamilyModal = () => {
if (!binding?.device_id) {
Taro.showToast({ title: '当前没有可用设备', icon: 'none' })
return
}
setShowFamilyModal(true)
void loadFamilyData(binding.device_id)
}
const handleCreateFamilyInvite = async () => {
if (!binding?.device_id || isCreatingInvite) return
setIsCreatingInvite(true)
try {
const invitation = await createFamilyInvitation(binding.device_id)
const invitePath = `/pages/family-invite/index?token=${invitation.invite_token}`
setFamilyInvitePath(invitePath)
setFamilyInviteExpiresAt(invitation.expires_at)
Taro.showToast({ title: '邀请已生成,请发送', icon: 'none' })
} catch (error: any) {
Taro.showToast({
title: error?.message || '创建邀请失败',
icon: 'none',
})
} finally {
setIsCreatingInvite(false)
}
}
const handleCopyFamilyInvite = () => {
if (!familyInvitePath) return
Taro.setClipboardData({
data: familyInvitePath,
success: () => {
Taro.showToast({ title: '邀请路径已复制', icon: 'success' })
},
})
}
const handleRemoveFamilyMember = (member: FamilyMember) => {
if (!binding?.device_id || member.is_owner || isUpdatingFamily) return
Taro.showModal({
title: '移除成员',
content: `确定要移除 ${member.nickname || '该成员'} 吗?`,
confirmText: '移除',
confirmColor: '#E5484D',
success: async (result) => {
if (!result.confirm || !binding?.device_id) return
setIsUpdatingFamily(true)
try {
await removeFamilyMember(binding.device_id, member.user_id)
Taro.showToast({ title: '已移除', icon: 'success' })
await loadFamilyData(binding.device_id)
} catch (error: any) {
Taro.showToast({
title: error?.message || '移除失败',
icon: 'none',
})
} finally {
setIsUpdatingFamily(false)
}
},
})
}
const handleLeaveFamily = () => {
if (!binding?.device_id || isUpdatingFamily) return
Taro.showModal({
title: '退出家庭',
content: '退出后将不能继续查看和操作这台设备。',
confirmText: '退出',
confirmColor: '#E5484D',
success: async (result) => {
if (!result.confirm || !binding?.device_id) return
setIsUpdatingFamily(true)
try {
await leaveFamily(binding.device_id)
clearSelectedBindingDeviceId()
Taro.showToast({ title: '已退出', icon: 'success' })
setShowFamilyModal(false)
await loadData()
} catch (error: any) {
Taro.showToast({
title: error?.message || '退出失败',
icon: 'none',
})
} finally {
setIsUpdatingFamily(false)
}
},
})
}
const handleOpenModal = (type: 'add' | 'edit', child?: Child) => {
setModalType(type)
setChildName(child?.child_name || '')
@@ -169,6 +324,7 @@ export default function Sleep() {
setBinding(nextBinding)
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
setShowChildModal(false)
Taro.showToast({
title: '已切换当前孩子',
@@ -262,7 +418,7 @@ export default function Sleep() {
const onlineStatus = await getDeviceOnlineStatus(binding.device_id)
if (onlineStatus && !onlineStatus.online) {
Taro.showToast({
title: '设备不在线,暂时无法发送更新指令',
title: '设备不在线或处于休眠中,暂时无法发送更新指令',
icon: 'none',
})
return
@@ -275,8 +431,11 @@ export default function Sleep() {
icon: 'success',
})
} catch (error: any) {
const message = error?.message === DEVICE_UNAVAILABLE_MESSAGE
? '设备不在线或处于休眠中,暂时无法发送更新指令'
: error?.message || '更新失败,请重试'
Taro.showToast({
title: error?.message || '更新失败,请重试',
title: message,
icon: 'none',
})
} finally {
@@ -403,6 +562,11 @@ export default function Sleep() {
return
}
if (item.name === '家庭成员') {
handleOpenFamilyModal()
return
}
if (item.name === '解除设备绑定') {
handleUnbind()
}
@@ -422,6 +586,8 @@ export default function Sleep() {
const parentDisplayName = parentInfo.nickname?.trim() || '家长'
const currentFirmwareLabel = firmwareStatus?.current_version || '--'
const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
const currentUserId = getCurrentUserId()
const isFamilyOwner = familyInfo?.current_user_role === 'owner'
const canUpdateFirmware = Boolean(binding?.device_id && firmwareStatus?.can_update && !isLoadingFirmware && !isUpdatingFirmware)
const firmwareActionText = isLoadingFirmware ? '查询中' : isUpdatingFirmware ? '发送中' : '更新'
const roleValue = !binding?.device_id
@@ -429,6 +595,11 @@ export default function Sleep() {
: isLoadingRoles
? '查询中'
: deviceRole?.name || '未设置'
const familyValue = !binding?.device_id
? '未绑定'
: isLoadingFamily
? '查询中'
: `${familyInfo?.total || 1}/4 人`
const firmwareSubtitle = !binding?.device_id
? '绑定设备后可查看系统版本'
: isLoadingFirmware
@@ -481,6 +652,14 @@ export default function Sleep() {
arrow: true,
disabled: !binding,
},
{
icon: require('../../assets/tab-icons/rings.png'),
iconBgClass: 'green',
name: '家庭成员',
value: familyValue,
arrow: true,
disabled: !binding,
},
{
icon: require('../../assets/tab-icons/broken-rings.png'),
iconBgClass: 'red',
@@ -744,6 +923,104 @@ export default function Sleep() {
</View>
</View>
)}
{showFamilyModal && (
<View className='modal-mask' onClick={() => setShowFamilyModal(false)}>
<View
className='modal-card family-card'
onClick={(event) => {
event.stopPropagation()
}}
>
<Text className='modal-title'>家庭成员</Text>
{isLoadingFamily ? (
<Text className='device-switch-empty'>成员加载中...</Text>
) : !familyInfo ? (
<Text className='device-switch-empty'>当前没有成员信息</Text>
) : (
<>
<View className='family-summary'>
<Text className='family-summary-text'>{familyInfo.total}/{familyInfo.max_members} 人</Text>
<Text className='family-summary-subtitle'>
{isFamilyOwner ? '主成员可添加和移除成员' : '成员可查看和操作设备'}
</Text>
</View>
<View className='family-list'>
{familyInfo.items.map((member) => {
const canRemove = isFamilyOwner && !member.is_owner && !isUpdatingFamily
const isMe = currentUserId === member.user_id
return (
<View key={member.user_id} className='family-member-item'>
<View className='family-member-avatar'>
{member.avatar_url ? (
<Image className='family-member-avatar-img' src={member.avatar_url} mode='aspectFill' />
) : (
<Text className='family-member-avatar-text'>👤</Text>
)}
</View>
<View className='family-member-main'>
<View className='family-member-head'>
<Text className='family-member-name'>{member.nickname || `成员 ${member.user_id}`}</Text>
<View className='device-switch-tags'>
{member.is_owner && <Text className='device-switch-tag'>主成员</Text>}
{isMe && <Text className='device-switch-tag muted'>我</Text>}
</View>
</View>
<Text className='family-member-role'>{member.is_owner ? '设备归属账号' : '家庭共享成员'}</Text>
</View>
{canRemove && (
<Text className='family-member-remove' onClick={() => handleRemoveFamilyMember(member)}>
移除
</Text>
)}
</View>
)
})}
</View>
{isFamilyOwner ? (
<View className='family-invite-actions'>
{familyInvitePath ? (
<>
<Button
className='family-primary-action family-share-button'
openType='share'
hoverClass='none'
>
<Text className='family-primary-action-text'>发送给微信好友</Text>
</Button>
<Text className='family-invite-tip'>
邀请有效期至 {familyInviteExpiresAt ? familyInviteExpiresAt.replace('T', ' ').slice(0, 16) : '24 小时内'}
</Text>
<Text className='family-copy-action' onClick={handleCopyFamilyInvite}>
复制邀请路径
</Text>
</>
) : (
<View
className={`family-primary-action ${familyInfo.total >= familyInfo.max_members || isCreatingInvite ? 'disabled' : ''}`}
onClick={familyInfo.total < familyInfo.max_members && !isCreatingInvite ? handleCreateFamilyInvite : undefined}
>
<Text className='family-primary-action-text'>{isCreatingInvite ? '生成中...' : '邀请微信好友'}</Text>
</View>
)}
</View>
) : (
<View
className={`family-danger-action ${isUpdatingFamily ? 'disabled' : ''}`}
onClick={isUpdatingFamily ? undefined : handleLeaveFamily}
>
<Text className='family-danger-action-text'>退出家庭</Text>
</View>
)}
</>
)}
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={() => setShowFamilyModal(false)}>
关闭
</Text>
</View>
</View>
</View>
)}
{systemBanner}
</View>
)