Files
banban/banban-mini/src/pages/sleep/index.tsx
T

1714 lines
61 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 FirmwareProgress from '@/components/firmware-progress'
import { getRoleArtwork } from '@/utils/artwork'
import { getPageStyle } from '@/utils/page-style'
import { setActiveTabRoute } from '@/utils/tab-bar'
import { View, Text, Image, Input, Button } from '@tarojs/components'
import { useEffect, useRef, useState } from 'react'
import Taro, { useDidHide, useDidShow, useShareAppMessage } from '@tarojs/taro'
import { authorizeParentPhone, clearToken, getParent, getToken, Parent } from '@/services/auth'
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
import { getCurrentUserId } from '@/services/session'
import {
BindingListItem,
BindingCardItem,
BindingCardGroupItem,
clearSelectedBindingDeviceId,
getBindingCardGroups,
loadCurrentChildBindingContext,
setSelectedBindingDeviceId,
updateBindingCardName,
unbindDevice,
} from '@/services/binding'
import {
Child,
CHILD_NAME_RULE_MESSAGE,
clearSelectedChildId,
deleteChild,
sanitizeChildNameInput,
setSelectedChildId,
updateChild,
validateChildNameInput,
} from '@/services/child'
import {
createFamilyInvitation,
FamilyMember,
FamilyMemberListResponse,
getFamilyMembers,
leaveFamily,
removeFamilyMember,
updateFamilyMemberDisplayName,
} from '@/services/family'
import {
DeviceCurrentRole,
DeviceFirmwareStatus,
DeviceRoleSummary,
getDeviceFirmwareStatus,
getDeviceOnlineStatus,
getDeviceRole,
getDeviceRoles,
startDeviceFirmwareUpdate,
updateDeviceRole,
} from '@/services/device'
import { getWechatMpBindStatus, startWechatMpBind, WechatMpBindStatus } from '@/services/wechat-mp'
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
import './index.scss'
interface MenuItem {
icon: string
iconBgClass: string
name: string
value: string
arrow?: boolean
disabled?: boolean
}
const FIRMWARE_STATUS_POLL_INTERVAL_MS = 3000
const FIRMWARE_UPDATE_IN_PROGRESS_STATUSES = new Set(['sent', 'accepted', 'updating'])
const FIRMWARE_UPDATE_SUCCESS_STATUSES = new Set(['success', 'completed'])
function normalizeFirmwareUpdateStatus(status?: string | null): string {
return String(status || '').trim().toLowerCase()
}
function isFirmwareUpdateInProgress(status?: string | null): boolean {
return FIRMWARE_UPDATE_IN_PROGRESS_STATUSES.has(normalizeFirmwareUpdateStatus(status))
}
function isFirmwareUpdateSuccessful(status?: string | null): boolean {
return FIRMWARE_UPDATE_SUCCESS_STATUSES.has(normalizeFirmwareUpdateStatus(status))
}
export default function Sleep() {
const systemBanner = useSystemBanner()
const [loading, setLoading] = useState(true)
const [children, setChildren] = useState<Child[]>([])
const [bindings, setBindings] = useState<BindingListItem[]>([])
const [binding, setBinding] = useState<BindingListItem | null>(null)
const [currentChild, setCurrentChild] = useState<Child | null>(null)
const [firmwareStatus, setFirmwareStatus] = useState<DeviceFirmwareStatus | null>(null)
const [showFirmwareProgress, setShowFirmwareProgress] = useState(false)
const [firmwareRefreshError, setFirmwareRefreshError] = useState(false)
const firmwareLoadGenerationRef = useRef(0)
const firmwareDeviceIdRef = useRef('')
const [deviceRole, setDeviceRole] = useState<DeviceCurrentRole | null>(null)
const [roles, setRoles] = useState<DeviceRoleSummary[]>([])
const [currentCardGroup, setCurrentCardGroup] = useState<BindingCardGroupItem | null>(null)
const [isLoadingCards, setIsLoadingCards] = useState(false)
const [isLoadingFirmware, setIsLoadingFirmware] = useState(false)
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 [isUpdatingFamilyName, setIsUpdatingFamilyName] = useState(false)
const [familyInvitePath, setFamilyInvitePath] = useState('')
const [familyInviteExpiresAt, setFamilyInviteExpiresAt] = useState('')
const [showModal, setShowModal] = useState(false)
const [showChildModal, setShowChildModal] = useState(false)
const [showRoleModal, setShowRoleModal] = useState(false)
const [showFamilyModal, setShowFamilyModal] = useState(false)
const [showFamilyNameModal, setShowFamilyNameModal] = useState(false)
const [showAssignDeviceModal, setShowAssignDeviceModal] = useState(false)
const [showCardsModal, setShowCardsModal] = useState(false)
const [showCardNameModal, setShowCardNameModal] = useState(false)
const [childName, setChildName] = useState('')
const [editingChildId, setEditingChildId] = useState<number | null>(null)
const [editingCard, setEditingCard] = useState<BindingCardItem | null>(null)
const [cardName, setCardName] = useState('')
const [isUpdatingCardName, setIsUpdatingCardName] = useState(false)
const [editingFamilyMember, setEditingFamilyMember] = useState<FamilyMember | null>(null)
const [familyMemberDisplayName, setFamilyMemberDisplayName] = useState('')
const [parentInfo, setParentInfo] = useState<Parent | null>(null)
const [wechatMpStatus, setWechatMpStatus] = useState<WechatMpBindStatus | null>(null)
const [isGuest, setIsGuest] = useState(false)
const [isLoadingWechatMp, setIsLoadingWechatMp] = useState(false)
const [isStartingWechatMpBind, setIsStartingWechatMpBind] = useState(false)
const [isAuthorizingPhone, setIsAuthorizingPhone] = useState(false)
const firmwareStatusPollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const firmwareStatusPollDeviceIdRef = useRef('')
const isFirmwareStatusPollingRef = useRef(false)
useDidShow(() => {
setActiveTabRoute('pages/sleep/index')
void loadData()
})
useDidHide(() => {
stopFirmwareStatusPolling()
})
useEffect(() => () => {
stopFirmwareStatusPolling()
}, [])
useShareAppMessage(() => {
const targetName = currentChild?.child_name || binding?.device_id || '宝贝'
return {
title: `邀请你加入${targetName}的家庭共享`,
path: familyInvitePath || '/pages/device/index',
}
})
const loadData = async () => {
if (!getToken()) {
setChildren([])
setBindings([])
setBinding(null)
setCurrentChild(null)
setParentInfo(null)
setFirmwareStatus(null)
setDeviceRole(null)
setRoles([])
setCurrentCardGroup(null)
setFamilyInfo(null)
setWechatMpStatus(null)
setIsGuest(true)
setLoading(false)
return
}
setIsGuest(false)
setLoading(true)
try {
const context = await loadCurrentChildBindingContext()
const currentUserId = getCurrentUserId()
const currentParent = currentUserId ? await loadCurrentParent(currentUserId) : null
const nextBinding = (context.currentBinding as BindingListItem | null) || null
setChildren(context.children)
setBindings(context.bindings as BindingListItem[])
setCurrentChild(context.currentChild)
setBinding(nextBinding)
setParentInfo(currentParent)
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
void loadCurrentDeviceCards(nextBinding?.device_id)
void loadWechatMpStatus()
} catch (error: any) {
console.error('[manage] load failed:', error)
Taro.showToast({
title: error?.message || '加载失败,请稍后重试',
icon: 'none',
})
} finally {
setLoading(false)
}
}
const loadWechatMpStatus = async () => {
setIsLoadingWechatMp(true)
try {
setWechatMpStatus(await getWechatMpBindStatus())
} catch (error) {
console.warn('[manage] wechat mp bind status load failed:', error)
setWechatMpStatus(null)
} finally {
setIsLoadingWechatMp(false)
}
}
const loadCurrentParent = async (userId: number): Promise<Parent | null> => {
try {
return await getParent(userId)
} catch (error) {
console.warn('[manage] parent profile load failed:', error)
return null
}
}
const currentChildName = currentChild?.child_name || '未设置'
function clearFirmwareStatusPollTimer() {
if (firmwareStatusPollTimerRef.current) {
clearTimeout(firmwareStatusPollTimerRef.current)
firmwareStatusPollTimerRef.current = null
}
}
function stopFirmwareStatusPolling() {
clearFirmwareStatusPollTimer()
firmwareStatusPollDeviceIdRef.current = ''
isFirmwareStatusPollingRef.current = false
}
function scheduleFirmwareStatusPoll(deviceId: string) {
clearFirmwareStatusPollTimer()
firmwareStatusPollTimerRef.current = setTimeout(() => {
void pollFirmwareStatus(deviceId)
}, FIRMWARE_STATUS_POLL_INTERVAL_MS)
}
function startFirmwareStatusPolling(deviceId: string) {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) return
firmwareStatusPollDeviceIdRef.current = normalizedDeviceId
scheduleFirmwareStatusPoll(normalizedDeviceId)
}
function syncFirmwareStatusPolling(deviceId: string, nextFirmwareStatus?: DeviceFirmwareStatus | null) {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) {
stopFirmwareStatusPolling()
return
}
if (isFirmwareUpdateInProgress(nextFirmwareStatus?.update_status)) {
startFirmwareStatusPolling(normalizedDeviceId)
} else {
stopFirmwareStatusPolling()
}
}
async function pollFirmwareStatus(deviceId: string) {
const normalizedDeviceId = String(deviceId || '').trim()
if (
!normalizedDeviceId ||
firmwareStatusPollDeviceIdRef.current !== normalizedDeviceId ||
isFirmwareStatusPollingRef.current
) {
return
}
isFirmwareStatusPollingRef.current = true
try {
const nextFirmwareStatus = await getDeviceFirmwareStatus(normalizedDeviceId)
if (firmwareStatusPollDeviceIdRef.current !== normalizedDeviceId) return
setFirmwareStatus(nextFirmwareStatus)
setFirmwareRefreshError(false)
if (isFirmwareUpdateInProgress(nextFirmwareStatus?.update_status)) {
scheduleFirmwareStatusPoll(normalizedDeviceId)
return
}
stopFirmwareStatusPolling()
const nextStatus = normalizeFirmwareUpdateStatus(nextFirmwareStatus?.update_status)
if (isFirmwareUpdateSuccessful(nextStatus)) {
Taro.showToast({ title: '系统更新完成', icon: 'success' })
} else if (nextStatus === 'failed') {
Taro.showToast({ title: '系统更新失败,可重试', icon: 'none' })
}
} catch (error) {
console.warn('[manage] firmware poll failed:', error)
if (firmwareStatusPollDeviceIdRef.current === normalizedDeviceId) setFirmwareRefreshError(true)
if (firmwareStatusPollDeviceIdRef.current === normalizedDeviceId) {
scheduleFirmwareStatusPoll(normalizedDeviceId)
}
} finally {
isFirmwareStatusPollingRef.current = false
}
}
const loadFirmwareStatus = async (deviceId?: string | null) => {
const normalizedDeviceId = String(deviceId || '').trim()
const generation = ++firmwareLoadGenerationRef.current
if (firmwareDeviceIdRef.current !== normalizedDeviceId) {
setShowFirmwareProgress(false)
setFirmwareStatus(null)
stopFirmwareStatusPolling()
}
firmwareDeviceIdRef.current = normalizedDeviceId
if (!normalizedDeviceId) {
setFirmwareStatus(null)
setIsLoadingFirmware(false)
stopFirmwareStatusPolling()
return
}
setIsLoadingFirmware(true)
try {
const nextFirmwareStatus = await getDeviceFirmwareStatus(normalizedDeviceId)
if (generation !== firmwareLoadGenerationRef.current) return
setFirmwareStatus(nextFirmwareStatus)
setFirmwareRefreshError(false)
if (isFirmwareUpdateInProgress(nextFirmwareStatus?.update_status)) setShowFirmwareProgress(true)
syncFirmwareStatusPolling(normalizedDeviceId, nextFirmwareStatus)
} catch (error: any) {
if (generation !== firmwareLoadGenerationRef.current) return
console.error('[manage] firmware load failed:', error)
setFirmwareStatus(null)
if (firmwareStatusPollDeviceIdRef.current === normalizedDeviceId) {
stopFirmwareStatusPolling()
}
Taro.showToast({
title: error?.message || '系统更新状态加载失败',
icon: 'none',
})
} finally {
if (generation === firmwareLoadGenerationRef.current) setIsLoadingFirmware(false)
}
}
const loadRoleData = async (deviceId?: string | null) => {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) {
setDeviceRole(null)
setRoles([])
setIsLoadingRoles(false)
return
}
setIsLoadingRoles(true)
try {
const [nextRoles, nextDeviceRole] = await Promise.all([
getDeviceRoles(),
getDeviceRole(normalizedDeviceId),
])
setRoles(nextRoles)
setDeviceRole(nextDeviceRole)
} catch (error: any) {
console.error('[manage] role load failed:', error)
setRoles([])
setDeviceRole(null)
Taro.showToast({
title: error?.message || '角色信息加载失败',
icon: 'none',
})
} finally {
setIsLoadingRoles(false)
}
}
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 loadCurrentDeviceCards = async (deviceId?: string | null) => {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) {
setCurrentCardGroup(null)
setIsLoadingCards(false)
return
}
setIsLoadingCards(true)
try {
const nextCardGroups = await getBindingCardGroups()
const nextCurrentGroup = nextCardGroups.items.find((item) => item.device_id === normalizedDeviceId) || null
setCurrentCardGroup(nextCurrentGroup)
} catch (error: any) {
console.error('[manage] card groups load failed:', error)
setCurrentCardGroup(null)
Taro.showToast({
title: error?.message || '卡片信息加载失败',
icon: 'none',
})
} finally {
setIsLoadingCards(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 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 handleOpenFamilyNameModal = (member: FamilyMember) => {
if (!binding?.device_id || !isFamilyOwner) return
setEditingFamilyMember(member)
setFamilyMemberDisplayName(member.display_name || '')
setShowFamilyNameModal(true)
}
const handleCloseFamilyNameModal = () => {
if (isUpdatingFamilyName) return
setShowFamilyNameModal(false)
setEditingFamilyMember(null)
setFamilyMemberDisplayName('')
}
const handleSubmitFamilyName = async () => {
if (!binding?.device_id || !editingFamilyMember || isUpdatingFamilyName) return
const normalizedName = familyMemberDisplayName.trim()
if (normalizedName.length > 64) {
Taro.showToast({ title: '显示名最多 64 个字', icon: 'none' })
return
}
setIsUpdatingFamilyName(true)
try {
await updateFamilyMemberDisplayName(binding.device_id, editingFamilyMember.user_id, {
display_name: normalizedName || null,
})
Taro.showToast({ title: '已保存', icon: 'success' })
setShowFamilyNameModal(false)
setEditingFamilyMember(null)
setFamilyMemberDisplayName('')
await loadFamilyData(binding.device_id)
} catch (error: any) {
Taro.showToast({
title: error?.message || '显示名保存失败',
icon: 'none',
})
} finally {
setIsUpdatingFamilyName(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 handleOpenChildNameModal = (child: Child) => {
setChildName(child.child_name || '')
setEditingChildId(child.child_id)
setShowChildModal(false)
setShowModal(true)
}
const handleCloseModal = () => {
setShowModal(false)
setChildName('')
setEditingChildId(null)
}
const handleSelectChild = (child: Child) => {
setSelectedChildId(child.child_id)
const nextBinding = bindings.find((item) => item.child_id === child.child_id) || null
if (nextBinding?.device_id) {
setSelectedBindingDeviceId(nextBinding.device_id)
} else {
clearSelectedBindingDeviceId()
}
setCurrentChild(child)
setBinding(nextBinding)
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
void loadCurrentDeviceCards(nextBinding?.device_id)
setShowChildModal(false)
Taro.showToast({
title: '已切换当前宝贝',
icon: 'success',
})
}
const handleOpenCardNameModal = (card: BindingCardItem) => {
setEditingCard(card)
setCardName(card.card_name || '')
setShowCardNameModal(true)
}
const handleCloseCardNameModal = () => {
if (isUpdatingCardName) return
setShowCardNameModal(false)
setEditingCard(null)
setCardName('')
}
const handleSubmitCardName = async () => {
if (!editingCard || isUpdatingCardName) return
const normalizedName = cardName.trim()
if (normalizedName.length > 64) {
Taro.showToast({ title: '卡片名称最多 64 个字', icon: 'none' })
return
}
setIsUpdatingCardName(true)
try {
await updateBindingCardName(editingCard.card_id, {
card_name: normalizedName || null,
})
Taro.showToast({ title: '已保存', icon: 'success' })
setShowCardNameModal(false)
setEditingCard(null)
setCardName('')
await loadCurrentDeviceCards(binding?.device_id)
} catch (error: any) {
Taro.showToast({
title: error?.message || '卡片名称保存失败',
icon: 'none',
})
} finally {
setIsUpdatingCardName(false)
}
}
const handleAssignExistingDevice = (targetBinding: BindingListItem) => {
if (!targetBinding?.device_id) return
setSelectedBindingDeviceId(targetBinding.device_id)
setShowAssignDeviceModal(false)
Taro.navigateTo({ url: '/pages/bind/index?mode=assign' })
}
const handleDeleteUnassignedDevice = (targetBinding: BindingListItem) => {
if (!targetBinding?.device_id) return
Taro.showModal({
title: '删除设备',
content: `确定要从当前账号删除设备 ${targetBinding.device_id} 吗?删除后需要重新扫码绑定。`,
confirmText: '删除',
confirmColor: '#E5484D',
success: async (result) => {
if (!result.confirm) return
try {
await unbindDevice(targetBinding.device_id)
if (binding?.device_id === targetBinding.device_id) {
clearSelectedBindingDeviceId()
}
Taro.showToast({ title: '已删除设备', icon: 'success' })
await loadData()
} catch (error: any) {
Taro.showToast({
title: error?.message || '删除设备失败,请重试',
icon: 'none',
})
}
},
})
}
const handleOpenAddCard = () => {
if (!binding?.device_id) return
setShowCardsModal(false)
Taro.navigateTo({ url: `/pages/bind/index?mode=card&device_id=${encodeURIComponent(binding.device_id)}` })
}
const handleSubmitModal = async () => {
const normalizedName = childName.trim()
const childNameError = validateChildNameInput(normalizedName)
if (childNameError) {
Taro.showToast({ title: childNameError, icon: 'none' })
return
}
try {
if (!editingChildId) {
Taro.showToast({ title: '当前没有可修改的宝贝', icon: 'none' })
return
}
await updateChild(editingChildId, { child_name: normalizedName })
Taro.showToast({ title: '修改成功', icon: 'success' })
handleCloseModal()
await loadData()
} catch (error: any) {
console.error('[manage] save child failed:', error)
Taro.showToast({
title: error?.message || '保存失败,请重试',
icon: 'none',
})
}
}
const handleDeleteChild = (child: Child) => {
const childBinding = bindings.find((item) => item.child_id === child.child_id) || null
const isCurrentChild = currentChild?.child_id === child.child_id
const content = childBinding?.device_id
? '删除后,' + child.child_name + ' 将不再显示,设备 ' + childBinding.device_id + ' 会保留但不再分配给该宝贝。确定删除吗?'
: '删除后,' + child.child_name + ' 将不再显示。确定删除吗?'
Taro.showModal({
title: '删除宝贝',
content,
confirmText: '删除',
confirmColor: '#E5484D',
success: async (result) => {
if (!result.confirm) return
try {
await deleteChild(child.child_id)
if (isCurrentChild) {
clearSelectedChildId()
}
if (isCurrentChild && childBinding?.device_id && binding?.device_id === childBinding.device_id) {
clearSelectedBindingDeviceId()
}
setShowChildModal(false)
Taro.showToast({ title: '已删除', icon: 'success' })
await loadData()
} catch (error: any) {
console.error('[manage] delete child failed:', error)
Taro.showToast({
title: error?.message || '删除失败,请重试',
icon: 'none',
})
}
},
})
}
const handleStartFirmwareUpdate = () => {
if (
!binding?.device_id ||
!firmwareStatus?.can_update ||
isLoadingFirmware ||
isUpdatingFirmware ||
isFirmwareUpdateInProgress(firmwareStatus.update_status)
) {
return
}
Taro.showModal({
title: '系统更新',
content: `确定要更新当前设备 ${binding.device_id} 吗?`,
confirmText: '更新',
confirmColor: '#16A34A',
success: async (result) => {
if (!result.confirm || !binding?.device_id) return
setIsUpdatingFirmware(true)
try {
const onlineStatus = await getDeviceOnlineStatus(binding.device_id)
if (onlineStatus && !onlineStatus.online) {
Taro.showToast({
title: '设备不在线或处于休眠中,暂时无法发送更新指令',
icon: 'none',
})
return
}
const nextFirmwareStatus = await startDeviceFirmwareUpdate(binding.device_id)
if (firmwareDeviceIdRef.current !== binding.device_id) return
setShowFirmwareProgress(true)
setFirmwareRefreshError(false)
setFirmwareStatus(nextFirmwareStatus)
syncFirmwareStatusPolling(binding.device_id, nextFirmwareStatus)
Taro.showToast({
title: '更新指令已发送',
icon: 'success',
})
} catch (error: any) {
const message = error?.message === DEVICE_UNAVAILABLE_MESSAGE
? '设备不在线或处于休眠中,暂时无法发送更新指令'
: error?.message || '更新失败,请重试'
Taro.showToast({
title: message,
icon: 'none',
})
} finally {
setIsUpdatingFirmware(false)
}
},
})
}
const handleOpenRoleModal = () => {
if (!binding?.device_id) {
Taro.showToast({ title: '当前没有可用设备', icon: 'none' })
return
}
setShowRoleModal(true)
if (roles.length === 0) {
void loadRoleData(binding.device_id)
}
}
const handleSelectRole = async (role: DeviceRoleSummary) => {
if (!binding?.device_id || isUpdatingRole) return
setIsUpdatingRole(true)
try {
const language = deviceRole?.preferred_language || role.default_language || role.languages?.[0] || null
const nextRole = await updateDeviceRole(role.role_key, language, binding.device_id)
setDeviceRole(nextRole)
setShowRoleModal(false)
Taro.showToast({ title: 'AI 角色已切换', icon: 'success' })
} catch (error: any) {
Taro.showToast({
title: error?.message || '角色切换失败',
icon: 'none',
})
} finally {
setIsUpdatingRole(false)
}
}
const handleUnbind = () => {
if (!binding) return
Taro.showModal({
title: '解除设备绑定',
content: `确定要解除 ${currentChildName} 当前绑定的设备吗?`,
confirmColor: '#FF8C42',
success: async (result) => {
if (!result.confirm) return
try {
await unbindDevice(binding.device_id)
clearSelectedBindingDeviceId()
Taro.showToast({ title: '已解绑', icon: 'success' })
await loadData()
} catch (error: any) {
Taro.showToast({
title: error?.message || '解绑失败,请重试',
icon: 'none',
})
}
},
})
}
const handleLogout = () => {
Taro.showModal({
title: '退出登录',
content: '确定要退出当前账号吗?',
confirmColor: '#FF8C42',
success: (result) => {
if (!result.confirm) return
clearToken()
clearSelectedBindingDeviceId()
clearSelectedChildId()
Taro.removeStorageSync('userInfo')
void loadData()
},
})
}
const handleAuthorizePhone = async (event: any) => {
if (isAuthorizingPhone) return
const phoneCode = String(event?.detail?.code || '').trim()
if (!phoneCode) {
const errMsg = String(event?.detail?.errMsg || '')
Taro.showToast({
title: errMsg.includes('deny') || errMsg.includes('fail') ? '未授权手机号' : '未获取到手机号授权',
icon: 'none',
})
return
}
setIsAuthorizingPhone(true)
Taro.showLoading({ title: '保存中...' })
try {
const parent = await authorizeParentPhone(phoneCode)
setParentInfo(parent)
const cachedUserInfo = (Taro.getStorageSync('userInfo') || {}) as {
nickname?: string
avatar_url?: string
}
Taro.setStorageSync('userInfo', {
...cachedUserInfo,
nickname: parent.nickname || cachedUserInfo.nickname,
avatar_url: parent.avatar_url || cachedUserInfo.avatar_url,
})
Taro.showToast({ title: '短信通知已开启', icon: 'success' })
} catch (error: any) {
console.error('[manage] phone authorization failed:', error)
Taro.showToast({
title: error?.message || '手机号保存失败',
icon: 'none',
})
} finally {
Taro.hideLoading()
setIsAuthorizingPhone(false)
}
}
const handleStartWechatMpBind = async () => {
if (isStartingWechatMpBind) return
setIsStartingWechatMpBind(true)
try {
const result = await startWechatMpBind()
const bindUrl = String(result.url || '').trim()
if (!bindUrl) {
throw new Error('未获取到服务号绑定链接')
}
Taro.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(bindUrl)}`,
fail: () => {
Taro.setClipboardData({
data: bindUrl,
success: () => {
Taro.showModal({
title: '服务号通知',
content: '绑定链接已复制,请在微信里打开完成授权。',
showCancel: false,
})
},
})
},
})
} catch (error: any) {
Taro.showToast({
title: error?.message || '服务号绑定失败',
icon: 'none',
})
} finally {
setIsStartingWechatMpBind(false)
}
}
const handleMenuClick = (item: MenuItem) => {
if (item.disabled) return
if (item.name === '当前宝贝') {
setShowChildModal(true)
return
}
if (item.name === '关联现有设备') {
setShowAssignDeviceModal(true)
return
}
if (item.name === '绑定设备') {
Taro.navigateTo({ url: '/pages/bind/index' })
return
}
if (item.name === '我的卡片') {
setShowCardsModal(true)
return
}
if (item.name === 'AI 角色') {
handleOpenRoleModal()
return
}
if (item.name === '家庭成员') {
handleOpenFamilyModal()
return
}
if (item.name === '服务号通知') {
handleStartWechatMpBind()
return
}
if (item.name === '解除设备绑定') {
handleUnbind()
}
}
if (loading) {
return (
<View className='manage-page' style={getPageStyle(true)}>
<View className='loading'>
<Text>加载中...</Text>
</View>
{systemBanner}
</View>
)
}
if (isGuest) {
return (
<View className='manage-page' style={getPageStyle(true)}>
<View className='page-header'>
<Text className='page-title'>设置与管理</Text>
</View>
<View className='guest-manage-card'>
<Text className='guest-manage-icon'>⚙️</Text>
<Text className='guest-manage-title'>管理宝贝资料和设备</Text>
<Text className='guest-manage-desc'>这里可以管理宝贝资料、设备绑定、卡片、休眠时间、系统更新和家庭成员。当前页面为功能预览,真实数据会在绑定设备后展示。</Text>
</View>
<View className='guest-manage-list'>
<View className='guest-manage-item'>
<Text className='guest-manage-item-title'>宝贝资料</Text>
<Text className='guest-manage-item-desc'>为不同宝贝维护独立设备和会话。</Text>
</View>
<View className='guest-manage-item'>
<Text className='guest-manage-item-title'>设备绑定</Text>
<Text className='guest-manage-item-desc'>扫码或 NFC 绑定伴伴设备。</Text>
</View>
<View className='guest-manage-item'>
<Text className='guest-manage-item-title'>休眠设置</Text>
<Text className='guest-manage-item-desc'>设置设备休眠时间和远程控制。</Text>
</View>
<View className='guest-manage-item'>
<Text className='guest-manage-item-title'>系统更新</Text>
<Text className='guest-manage-item-desc'>查看设备版本,并在设备在线时发起更新。</Text>
</View>
<View className='guest-manage-item'>
<Text className='guest-manage-item-title'>家庭共享</Text>
<Text className='guest-manage-item-desc'>邀请家庭成员一起查看和管理同一台设备。</Text>
</View>
</View>
<View className='guest-manage-card action-card'>
<Text className='guest-manage-title small'>准备绑定设备</Text>
<Text className='guest-manage-desc'>浏览功能不需要登录;创建宝贝资料、绑定设备或查看真实设备数据时,再进入登录流程。</Text>
<View
className='guest-manage-btn secondary'
onClick={() => {
Taro.setStorageSync('postLoginRedirect', '/pages/sleep/index')
Taro.navigateTo({ url: '/pages/login/index' })
}}
>
<Text className='guest-manage-btn-text'>去登录 / 绑定设备</Text>
</View>
</View>
<Image className='manage-footer-art' src={require('../../assets/ui/paw-dog-footer.png')} mode='aspectFit' />
{systemBanner}
</View>
)
}
const parentDisplayName = parentInfo?.nickname?.trim() || '家长'
const parentPhone = String(parentInfo?.phone || '').trim()
const parentPhoneLabel = parentPhone ? `${parentPhone.slice(0, 3)}****${parentPhone.slice(-4)}` : '未开启'
const unassignedBindings = bindings.filter((item) => item.device_id && item.child_id === null)
const currentFirmwareLabel = firmwareStatus?.current_version || '--'
const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
const currentUserId = getCurrentUserId()
const isFamilyOwner = familyInfo?.current_user_role === 'owner'
const canEditCardName = Boolean(
binding?.device_id &&
currentCardGroup?.device_id === binding.device_id &&
familyInfo?.device_id === binding.device_id &&
isFamilyOwner
)
const isFirmwareUpdatingByStatus = isFirmwareUpdateInProgress(firmwareStatus?.update_status)
const canUpdateFirmware = Boolean(
binding?.device_id &&
firmwareStatus?.can_update &&
!isLoadingFirmware &&
!isUpdatingFirmware &&
!isFirmwareUpdatingByStatus
)
const firmwareActionText = !binding?.device_id
? '更新'
: isLoadingFirmware
? '查询中'
: isUpdatingFirmware
? '发送中'
: isFirmwareUpdatingByStatus
? '查看进度'
: firmwareStatus?.update_available
? '更新'
: '已最新'
const roleValue = !binding?.device_id
? '未绑定'
: isLoadingRoles
? '查询中'
: deviceRole?.name || '未设置'
const familyValue = !binding?.device_id
? '未绑定'
: isLoadingFamily
? '查询中'
: `${familyInfo?.total || 1}/4 人`
const wechatMpValue = isStartingWechatMpBind
? '打开中'
: isLoadingWechatMp
? '查询中'
: wechatMpStatus?.bound
? '已开启'
: '未绑定'
const firmwareSubtitle = !binding?.device_id
? '绑定设备后可查看系统版本'
: isLoadingFirmware
? '正在查询当前设备版本'
: isFirmwareUpdatingByStatus
? '设备正在更新,请稍候'
: normalizeFirmwareUpdateStatus(firmwareStatus?.update_status) === 'failed'
? '更新失败,可重试'
: firmwareStatus?.update_available
? '发现可更新版本'
: '当前设备版本状态'
const cardValue = !binding?.device_id
? '未绑定'
: isLoadingCards
? '查询中'
: `${currentCardGroup?.card_count || 0} 张`
const menuItems: MenuItem[] = [
{
icon: require('../../assets/ui/menu-role.png'),
iconBgClass: 'orange',
name: '当前宝贝',
value: currentChildName,
arrow: true,
},
...(unassignedBindings.length > 0
? [{
icon: require('../../assets/ui/menu-binding.png'),
iconBgClass: 'green',
name: '关联现有设备',
value: `${unassignedBindings.length} 台待关联`,
arrow: true,
}]
: []),
{
icon: require('../../assets/ui/menu-binding.png'),
iconBgClass: 'green',
name: '绑定设备',
value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定',
arrow: true,
},
{
icon: require('../../assets/ui/menu-cards.png'),
iconBgClass: 'green',
name: '我的卡片',
value: cardValue,
arrow: true,
disabled: !binding,
},
{
icon: require('../../assets/ui/menu-role.png'),
iconBgClass: 'green',
name: 'AI 角色',
value: roleValue,
arrow: true,
disabled: !binding,
},
{
icon: require('../../assets/ui/menu-family.png'),
iconBgClass: 'green',
name: '家庭成员',
value: familyValue,
arrow: true,
disabled: !binding,
},
{
icon: require('../../assets/ui/menu-notice.png'),
iconBgClass: 'green',
name: '服务号通知',
value: wechatMpValue,
arrow: true,
disabled: isStartingWechatMpBind,
},
{
icon: require('../../assets/ui/menu-unbind.png'),
iconBgClass: 'red',
name: '解除设备绑定',
value: binding?.device_id ? `当前: ${binding.device_id}` : '',
arrow: true,
disabled: !binding,
},
]
return (
<View className='manage-page' style={getPageStyle(true)}>
<View className='page-header'>
<Text className='page-title'>设置与管理</Text>
</View>
<View className='user-card'>
<View className='user-avatar'>
{parentInfo?.avatar_url ? (
<Image className='avatar-img image-avatar' src={parentInfo.avatar_url} mode='aspectFill' />
) : (
<Text className='avatar-img'>👤</Text>
)}
</View>
<View className='user-info'>
<Text className='user-name'>{parentDisplayName}</Text>
<Text className='user-phone'>短信通知:{parentPhoneLabel}</Text>
</View>
{parentPhone ? (
<View className='verified-badge'>
<Text>已开启</Text>
</View>
) : (
<Button
className='phone-auth-btn'
openType='getPhoneNumber'
loading={isAuthorizingPhone}
disabled={isAuthorizingPhone}
onGetPhoneNumber={handleAuthorizePhone}
>
开启短信
</Button>
)}
</View>
<View className='summary-card'>
<View className='summary-row'>
<Text className='summary-label'>宝贝资料数</Text>
<Text className='summary-value'>{children.length} 个</Text>
</View>
<View className='summary-row'>
<Text className='summary-label'>当前宝贝</Text>
<Text className='summary-value'>{currentChildName}</Text>
</View>
<View className='summary-row'>
<Text className='summary-label'>当前设备</Text>
<Text className='summary-value'>{binding?.device_id || '未绑定'}</Text>
</View>
</View>
<View className={`system-update-card ${binding?.device_id ? '' : 'disabled'}`}>
<View className='system-update-header'>
<View className='system-update-heading'>
<View className='system-update-title-row'>
<Image className='system-update-icon' src={require('../../assets/ui/menu-update.png')} mode='aspectFit' />
<Text className='system-update-title'>系统更新</Text>
</View>
<Text className='system-update-subtitle'>{firmwareSubtitle}</Text>
</View>
<View
className={`system-update-action ${canUpdateFirmware || isFirmwareUpdatingByStatus ? '' : 'disabled'}`}
onClick={isFirmwareUpdatingByStatus ? () => setShowFirmwareProgress(true) : canUpdateFirmware ? handleStartFirmwareUpdate : undefined}
>
<Text className='system-update-action-text'>{firmwareActionText}</Text>
</View>
</View>
<View className='system-update-body'>
<View className='system-update-row'>
<Text className='system-update-label'>当前设备</Text>
<Text className='system-update-value'>{binding?.device_id || '未绑定'}</Text>
</View>
<View className='system-update-row'>
<Text className='system-update-label'>当前版本</Text>
<Text className='system-update-value'>{currentFirmwareLabel}</Text>
</View>
<View className='system-update-row'>
<Text className='system-update-label'>最新版本</Text>
<Text className='system-update-value highlight'>{latestFirmwareLabel}</Text>
</View>
</View>
</View>
<View className='menu-card'>
{menuItems.map((item, index) => (
<View key={index}>
<View className={`menu-item ${item.disabled ? 'disabled' : ''}`} onClick={() => handleMenuClick(item)}>
<View className='menu-left'>
<View className={`icon-bg ${item.iconBgClass}`}>
<Image className='control-icon-img' src={item.icon} mode='aspectFit' />
</View>
<Text className='menu-name'>{item.name}</Text>
</View>
<View className='menu-right'>
{item.value && <Text className='menu-value'>{item.value}</Text>}
{item.arrow && <Text className='arrow'>›</Text>}
</View>
</View>
{index < menuItems.length - 1 && <View className='divider'></View>}
</View>
))}
</View>
<View className='logout-section'>
<View className='logout-btn' onClick={handleLogout}>
<Text className='logout-text'>退出登录</Text>
</View>
</View>
<View className='version-info'>
<Text className='version-text'>伴伴 Companion V1.1.0</Text>
</View>
{showFirmwareProgress && firmwareStatus?.device_id === binding?.device_id && (
<FirmwareProgress status={firmwareStatus} refreshError={firmwareRefreshError}
onClose={() => setShowFirmwareProgress(false)}
onRetry={canUpdateFirmware ? () => { setShowFirmwareProgress(false); handleStartFirmwareUpdate() } : undefined} />
)}
{showChildModal && (
<View className='modal-mask' onClick={() => setShowChildModal(false)}>
<View
className='modal-card device-switch-card'
onClick={(event) => {
event.stopPropagation()
}}
>
<Text className='modal-title'>切换当前宝贝</Text>
{children.length === 0 ? (
<Text className='device-switch-empty'>当前还没有宝贝资料</Text>
) : (
<View className='device-switch-list'>
{children.map((item) => {
const isActive = currentChild?.child_id === item.child_id
const itemBinding = bindings.find((bindingItem) => bindingItem.child_id === item.child_id) || null
return (
<View
key={item.child_id}
className={`device-switch-item ${isActive ? 'active' : ''}`}
onClick={() => handleSelectChild(item)}
>
<View className='device-switch-head'>
<Text className='device-switch-id'>{item.child_name}</Text>
<View className='device-switch-tags'>
{isActive && <Text className='device-switch-tag'>当前</Text>}
{isActive && (
<Text
className='device-switch-edit'
onClick={(event) => {
event.stopPropagation()
handleOpenChildNameModal(item)
}}
>
修改
</Text>
)}
<Text
className='device-switch-delete'
onClick={(event) => {
event.stopPropagation()
handleDeleteChild(item)
}}
>
删除
</Text>
</View>
</View>
<Text className='device-switch-name'>{itemBinding?.device_id || '未绑定设备'}</Text>
</View>
)
})}
</View>
)}
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={() => setShowChildModal(false)}>
关闭
</Text>
</View>
</View>
</View>
)}
{showAssignDeviceModal && (
<View className='modal-mask' onClick={() => setShowAssignDeviceModal(false)}>
<View
className='modal-card device-switch-card'
onClick={(event) => {
event.stopPropagation()
}}
>
<Text className='modal-title'>关联现有设备</Text>
{unassignedBindings.length === 0 ? (
<Text className='device-switch-empty'>当前没有待关联设备</Text>
) : (
<View className='device-switch-list'>
{unassignedBindings.map((item) => (
<View
key={item.device_id}
className='device-switch-item'
onClick={() => handleAssignExistingDevice(item)}
>
<View className='device-switch-head'>
<Text className='device-switch-id'>{item.device_id}</Text>
<View className='device-switch-tags'>
<Text className='device-switch-tag'>待关联</Text>
<Text
className='device-switch-delete'
onClick={(event) => {
event.stopPropagation()
handleDeleteUnassignedDevice(item)
}}
>
删除
</Text>
</View>
</View>
<Text className='device-switch-name'>未关联宝贝</Text>
</View>
))}
</View>
)}
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={() => setShowAssignDeviceModal(false)}>
关闭
</Text>
</View>
</View>
</View>
)}
{showCardsModal && (
<View className='modal-mask' onClick={() => setShowCardsModal(false)}>
<View
className='modal-card cards-modal-card'
onClick={(event) => {
event.stopPropagation()
}}
>
<Text className='modal-title'>我的卡片</Text>
<View className='cards-modal-summary'>
<Text className='cards-modal-device'>{binding?.device_id || '当前未绑定设备'}</Text>
<Text className='bound-cards-count'>
{isLoadingCards ? '查询中' : `${currentCardGroup?.card_count || 0} 张`}
</Text>
</View>
{!binding?.device_id ? (
<View className='bound-cards-empty'>
<Text className='bound-cards-empty-text'>绑定设备后可查看卡片 UUID</Text>
</View>
) : isLoadingCards ? (
<View className='bound-cards-empty'>
<Text className='bound-cards-empty-text'>卡片加载中...</Text>
</View>
) : currentCardGroup?.cards.length ? (
<View className='bound-card-list cards-modal-list'>
{currentCardGroup.cards.map((card) => (
<View className='bound-card-item' key={`${currentCardGroup.device_id}-${card.card_id}`}>
<View className='bound-card-main'>
<Text className={`bound-card-name ${card.card_name ? '' : 'empty'}`}>
{card.card_name || '未命名卡片'}
</Text>
<Text className='bound-card-uuid'>{card.card_uuid}</Text>
</View>
{canEditCardName && (
<Text className='bound-card-edit' onClick={() => handleOpenCardNameModal(card)}>
编辑
</Text>
)}
</View>
))}
</View>
) : (
<View className='bound-cards-empty'>
<Text className='bound-cards-empty-text'>当前设备暂无卡片</Text>
</View>
)}
{binding?.device_id && (
<View className='cards-modal-add' onClick={handleOpenAddCard}>
<Text className='cards-modal-add-text'>添加设备卡片</Text>
</View>
)}
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={() => setShowCardsModal(false)}>
关闭
</Text>
</View>
</View>
</View>
)}
{showModal && (
<View className='modal-mask'>
<View className='modal-card'>
<Text className='modal-title'>编辑宝贝资料</Text>
<Text className='modal-field-label'>宝贝名称</Text>
<View className='modal-input-wrap'>
<Input
className='modal-input'
value={childName}
placeholder={CHILD_NAME_RULE_MESSAGE}
focus
maxlength={5}
onInput={(event) => setChildName(sanitizeChildNameInput(event.detail.value))}
/>
</View>
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={handleCloseModal}>
取消
</Text>
<Text className='modal-action confirm' onClick={handleSubmitModal}>
保存
</Text>
</View>
</View>
</View>
)}
{showCardNameModal && (
<View className='modal-mask'>
<View className='modal-card'>
<Text className='modal-title'>编辑卡片名称</Text>
<View className='modal-input-wrap'>
<Input
className='modal-input'
value={cardName}
placeholder='例如 奶奶的卡'
focus
disabled={isUpdatingCardName}
onInput={(event) => setCardName(event.detail.value)}
/>
</View>
<Text className='card-name-modal-tip'>留空保存后会显示为未命名卡片</Text>
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={handleCloseCardNameModal}>
取消
</Text>
<Text className={`modal-action confirm ${isUpdatingCardName ? 'disabled' : ''}`} onClick={handleSubmitCardName}>
{isUpdatingCardName ? '保存中...' : '确认'}
</Text>
</View>
</View>
</View>
)}
{showRoleModal && (
<View className='modal-mask' onClick={() => setShowRoleModal(false)}>
<View
className='modal-card role-switch-card'
onClick={(event) => {
event.stopPropagation()
}}
>
<Text className='modal-title'>切换 AI 角色</Text>
<View className='current-role-intro'>
<Text className='current-role-label'>当前角色:{deviceRole?.name || deviceRole?.role_key || '未设置'}</Text>
<Text className='current-role-desc'>
{deviceRole?.description || '暂无角色简介'}
</Text>
</View>
{isLoadingRoles ? (
<Text className='device-switch-empty'>角色加载中...</Text>
) : roles.length === 0 ? (
<Text className='device-switch-empty'>当前没有可用角色</Text>
) : (
<View className='role-switch-list'>
{roles.map((item) => {
const isActive = deviceRole?.role_key === item.role_key
return (
<View
key={item.role_key}
className={`role-switch-item ${isActive ? 'active' : ''} ${isUpdatingRole ? 'disabled' : ''}`}
onClick={isUpdatingRole ? undefined : () => handleSelectRole(item)}
>
<Image className='role-switch-art' src={getRoleArtwork(item.role_key)} mode='aspectFit' />
<View className='role-switch-head'>
<Text className='role-switch-name'>{item.name || item.role_key}</Text>
{isActive && <Text className='device-switch-tag'>当前</Text>}
</View>
<Text className='role-switch-desc'>
{item.description || `角色标识:${item.role_key}`}
</Text>
</View>
)
})}
</View>
)}
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={() => setShowRoleModal(false)}>
关闭
</Text>
</View>
</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 canEditName = isFamilyOwner && !isUpdatingFamilyName
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>
{(canEditName || canRemove) && (
<View className='family-member-actions'>
{canEditName && (
<Text className='family-member-edit' onClick={() => handleOpenFamilyNameModal(member)}>
编辑
</Text>
)}
{canRemove && (
<Text className='family-member-remove' onClick={() => handleRemoveFamilyMember(member)}>
移除
</Text>
)}
</View>
)}
</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>
</>
) : (
<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>
)}
{showFamilyNameModal && (
<View className='modal-mask'>
<View className='modal-card'>
<Text className='modal-title'>编辑家庭内显示名</Text>
<View className='modal-input-wrap'>
<Input
className='modal-input'
value={familyMemberDisplayName}
placeholder='例如 奶奶'
focus
disabled={isUpdatingFamilyName}
onInput={(event) => setFamilyMemberDisplayName(event.detail.value)}
/>
</View>
<Text className='family-name-modal-tip'>留空保存后会显示账号昵称</Text>
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={handleCloseFamilyNameModal}>
取消
</Text>
<Text className={`modal-action confirm ${isUpdatingFamilyName ? 'disabled' : ''}`} onClick={handleSubmitFamilyName}>
{isUpdatingFamilyName ? '保存中...' : '确认'}
</Text>
</View>
</View>
</View>
)}
{systemBanner}
</View>
)
}