合入家庭共享与设备消息能力
This commit is contained in:
@@ -2,6 +2,7 @@ import { View, Text, Slider, Image } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { getToken } from '@/services/auth'
|
||||
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
|
||||
import { Binding, loadCurrentChildBindingContext } from '@/services/binding'
|
||||
import { Child } from '@/services/child'
|
||||
import { DeviceAlarmItem, DeviceStatus, getDeviceAlarms, getDeviceStatus, setDeviceRemoteSleepWake, setDeviceVolume } from '@/services/device'
|
||||
@@ -157,8 +158,11 @@ export default function Device() {
|
||||
})
|
||||
} catch (error: any) {
|
||||
setVolumeValue(deviceStatus?.volume ?? fallbackLevel)
|
||||
const message = error?.message === DEVICE_UNAVAILABLE_MESSAGE
|
||||
? '设备不在线或处于休眠中,暂时无法调节音量'
|
||||
: error?.message || '音量设置失败'
|
||||
Taro.showToast({
|
||||
title: error?.message || '音量设置失败',
|
||||
title: message,
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
|
||||
3
banban-mini/src/pages/family-invite/index.config.ts
Normal file
3
banban-mini/src/pages/family-invite/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '家庭邀请',
|
||||
})
|
||||
76
banban-mini/src/pages/family-invite/index.scss
Normal file
76
banban-mini/src/pages/family-invite/index.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
.family-invite-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
padding: 96px 24px 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.invite-panel {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.invite-title {
|
||||
display: block;
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.invite-desc {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
line-height: 1.6;
|
||||
color: #666666;
|
||||
|
||||
&.danger {
|
||||
color: #E5484D;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 22px 0;
|
||||
border-bottom: 1px solid #F2F2F2;
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.invite-value {
|
||||
min-width: 0;
|
||||
font-size: 29px;
|
||||
color: #1A1A1A;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.invite-action {
|
||||
height: 92px;
|
||||
margin-top: 36px;
|
||||
border-radius: 18px;
|
||||
background: #FF8C42;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.disabled {
|
||||
background: #CBD5E1;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-action-text {
|
||||
font-size: 30px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 700;
|
||||
}
|
||||
103
banban-mini/src/pages/family-invite/index.tsx
Normal file
103
banban-mini/src/pages/family-invite/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { getToken } from '@/services/auth'
|
||||
import { acceptFamilyInvitation, FamilyInvitation, getFamilyInvitation } from '@/services/family'
|
||||
import './index.scss'
|
||||
|
||||
export default function FamilyInvite() {
|
||||
const router = useRouter()
|
||||
const inviteToken = String(router.params?.token || '').trim()
|
||||
const redirectUrl = `/pages/family-invite/index?token=${inviteToken}`
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [joining, setJoining] = useState(false)
|
||||
const [invitation, setInvitation] = useState<FamilyInvitation | null>(null)
|
||||
const [errorText, setErrorText] = useState('')
|
||||
|
||||
useDidShow(() => {
|
||||
void loadInvitation()
|
||||
})
|
||||
|
||||
const loadInvitation = async () => {
|
||||
if (!getToken()) {
|
||||
Taro.setStorageSync('postLoginRedirect', redirectUrl)
|
||||
Taro.reLaunch({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
if (!inviteToken) {
|
||||
setErrorText('邀请无效')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setErrorText('')
|
||||
try {
|
||||
const nextInvitation = await getFamilyInvitation(inviteToken)
|
||||
setInvitation(nextInvitation)
|
||||
} catch (error: any) {
|
||||
setInvitation(null)
|
||||
setErrorText(error?.message || '邀请加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAccept = async () => {
|
||||
if (!inviteToken || joining) return
|
||||
|
||||
setJoining(true)
|
||||
try {
|
||||
await acceptFamilyInvitation(inviteToken)
|
||||
Taro.showToast({ title: '已加入家庭', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/device/index' })
|
||||
}, 600)
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error?.message || '加入失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
setJoining(false)
|
||||
}
|
||||
}
|
||||
|
||||
const targetName = invitation?.child_name || invitation?.device_id || '--'
|
||||
const statusText = invitation?.status === 1 ? '等待确认加入' : '邀请已失效'
|
||||
|
||||
return (
|
||||
<View className='family-invite-page'>
|
||||
<View className='invite-panel'>
|
||||
<Text className='invite-title'>家庭共享邀请</Text>
|
||||
{loading ? (
|
||||
<Text className='invite-desc'>加载中...</Text>
|
||||
) : errorText ? (
|
||||
<Text className='invite-desc danger'>{errorText}</Text>
|
||||
) : (
|
||||
<>
|
||||
<View className='invite-row'>
|
||||
<Text className='invite-label'>共享对象</Text>
|
||||
<Text className='invite-value'>{targetName}</Text>
|
||||
</View>
|
||||
<View className='invite-row'>
|
||||
<Text className='invite-label'>设备</Text>
|
||||
<Text className='invite-value'>{invitation?.device_id || '--'}</Text>
|
||||
</View>
|
||||
<View className='invite-row'>
|
||||
<Text className='invite-label'>状态</Text>
|
||||
<Text className='invite-value'>{statusText}</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`invite-action ${invitation?.status === 1 && !joining ? '' : 'disabled'}`}
|
||||
onClick={invitation?.status === 1 && !joining ? handleAccept : undefined}
|
||||
>
|
||||
<Text className='invite-action-text'>{joining ? '加入中...' : '加入家庭'}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { View, Text, Map } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { getToken } from '@/services/auth'
|
||||
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
|
||||
import { loadCurrentChildBindingContext } from '@/services/binding'
|
||||
import { getDeviceOnlineStatus } from '@/services/device'
|
||||
import {
|
||||
@@ -248,8 +249,11 @@ export default function Location() {
|
||||
}
|
||||
setLoading(false)
|
||||
console.error('[location] load failed:', error)
|
||||
const message = error?.message === DEVICE_UNAVAILABLE_MESSAGE
|
||||
? '设备不在线或处于休眠中,暂时无法获取位置'
|
||||
: error?.message || '位置更新失败'
|
||||
Taro.showToast({
|
||||
title: error?.message || '位置更新失败',
|
||||
title: message,
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,12 @@ export default function Login() {
|
||||
|
||||
useEffect(() => {
|
||||
if (getToken()) {
|
||||
const redirectUrl = String(Taro.getStorageSync('postLoginRedirect') || '').trim()
|
||||
if (redirectUrl) {
|
||||
Taro.removeStorageSync('postLoginRedirect')
|
||||
Taro.reLaunch({ url: redirectUrl })
|
||||
return
|
||||
}
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
return
|
||||
}
|
||||
@@ -64,6 +70,12 @@ export default function Login() {
|
||||
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
const redirectUrl = String(Taro.getStorageSync('postLoginRedirect') || '').trim()
|
||||
if (redirectUrl) {
|
||||
Taro.removeStorageSync('postLoginRedirect')
|
||||
Taro.reLaunch({ url: redirectUrl })
|
||||
return
|
||||
}
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}, 300)
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -384,6 +384,10 @@
|
||||
max-height: 74vh;
|
||||
}
|
||||
|
||||
.family-card {
|
||||
max-height: 76vh;
|
||||
}
|
||||
|
||||
.device-switch-empty {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
@@ -401,6 +405,11 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.family-list {
|
||||
max-height: 44vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.device-switch-footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -514,6 +523,10 @@
|
||||
&.danger {
|
||||
background: #E5484D;
|
||||
}
|
||||
|
||||
&.muted {
|
||||
background: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.device-switch-name {
|
||||
@@ -541,3 +554,163 @@
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.family-summary {
|
||||
padding: 20px 22px;
|
||||
border-radius: 18px;
|
||||
background: #F7F8FA;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.family-summary-text {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
color: #1A1A1A;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.family-summary-subtitle {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.family-member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #F2F2F2;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.family-member-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 18px;
|
||||
background: #FEF3E8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.family-member-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.family-member-avatar-text {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.family-member-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.family-member-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.family-member-name {
|
||||
min-width: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.family-member-role {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.family-member-remove {
|
||||
flex-shrink: 0;
|
||||
font-size: 26px;
|
||||
color: #E5484D;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.family-primary-action,
|
||||
.family-danger-action {
|
||||
height: 84px;
|
||||
margin-top: 22px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.family-primary-action {
|
||||
background: #FFF2E7;
|
||||
}
|
||||
|
||||
.family-invite-actions {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.family-share-button {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
line-height: normal;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.family-invite-actions .family-primary-action {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.family-invite-tip {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.family-copy-action {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
font-size: 26px;
|
||||
color: #FF8C42;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.family-danger-action {
|
||||
background: #FFF1F2;
|
||||
}
|
||||
|
||||
.family-primary-action-text {
|
||||
font-size: 28px;
|
||||
color: #FF8C42;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.family-danger-action-text {
|
||||
font-size: 28px;
|
||||
color: #E5484D;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -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