合入家庭共享与设备消息能力
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user