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

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

View File

@@ -8,6 +8,7 @@ export default defineAppConfig({
"pages/location/index",
"pages/sleep/index",
"pages/sleep-schedule/index",
"pages/family-invite/index",
],
window: {
backgroundTextStyle: "light",

View File

@@ -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 {

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '家庭邀请',
})

View 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;
}

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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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;
}

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

View File

@@ -4,9 +4,11 @@ import { getCurrentUserId, getToken, handleUnauthorized } from './session'
const BASE_URL = API_BASE_URL
const REQUEST_TIMEOUT_MS = 10000
const DEVICE_UNAVAILABLE_MESSAGE = '设备不在线或处于休眠中,暂时无法执行该操作'
const ERROR_DETAIL_MAP: Record<string, string> = {
'Request failed': '请求失败',
'device is already bound, unbind it before binding again': '设备已绑定,请先解绑后再重新绑定',
'设备不在线或在休眠中': DEVICE_UNAVAILABLE_MESSAGE,
}
class ApiError extends Error {
@@ -71,4 +73,5 @@ async function request<T>(
}
export { request, ApiError, BASE_URL }
export { DEVICE_UNAVAILABLE_MESSAGE }
export { getCurrentUserId, getToken }

View File

@@ -6,6 +6,8 @@ export interface DeviceStatus {
child_id?: number | null
child_name?: string | null
sleep_mode?: number | null
manual_sleep_mode?: number | null
schedule_suppressed_until?: string | null
disable_time_start?: string | null
disable_time_end?: string | null
timezone?: string | null

View File

@@ -0,0 +1,71 @@
import { request } from './api'
export interface FamilyMember {
user_id: number
nickname?: string | null
avatar_url?: string | null
role: 'owner' | 'member'
is_owner: boolean
joined_at?: string | null
}
export interface FamilyMemberListResponse {
device_id: string
max_members: number
total: number
current_user_role: 'owner' | 'member'
items: FamilyMember[]
}
export interface FamilyInvitation {
invite_token: string
device_id: string
child_id?: number | null
child_name?: string | null
status: number
expires_at: string
}
export interface FamilyInvitationCreateResponse {
invite_token: string
device_id: string
expires_at: string
}
export interface FamilyInvitationAcceptResponse {
device_id: string
child_id?: number | null
child_name?: string | null
}
export async function getFamilyMembers(deviceId: string): Promise<FamilyMemberListResponse> {
return request<FamilyMemberListResponse>(`/banban/family/devices/${deviceId}/members`)
}
export async function createFamilyInvitation(deviceId: string): Promise<FamilyInvitationCreateResponse> {
return request<FamilyInvitationCreateResponse>(`/banban/family/devices/${deviceId}/invitations`, {
method: 'POST',
})
}
export async function getFamilyInvitation(inviteToken: string): Promise<FamilyInvitation> {
return request<FamilyInvitation>(`/banban/family/invitations/${inviteToken}`)
}
export async function acceptFamilyInvitation(inviteToken: string): Promise<FamilyInvitationAcceptResponse> {
return request<FamilyInvitationAcceptResponse>(`/banban/family/invitations/${inviteToken}/accept`, {
method: 'POST',
})
}
export async function removeFamilyMember(deviceId: string, memberUserId: number): Promise<void> {
return request<void>(`/banban/family/devices/${deviceId}/members/${memberUserId}`, {
method: 'DELETE',
})
}
export async function leaveFamily(deviceId: string): Promise<void> {
return request<void>(`/banban/family/devices/${deviceId}/me`, {
method: 'DELETE',
})
}