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([]) const [bindings, setBindings] = useState([]) const [binding, setBinding] = useState(null) const [currentChild, setCurrentChild] = useState(null) const [firmwareStatus, setFirmwareStatus] = useState(null) const [showFirmwareProgress, setShowFirmwareProgress] = useState(false) const [firmwareRefreshError, setFirmwareRefreshError] = useState(false) const firmwareLoadGenerationRef = useRef(0) const firmwareDeviceIdRef = useRef('') const [deviceRole, setDeviceRole] = useState(null) const [roles, setRoles] = useState([]) const [currentCardGroup, setCurrentCardGroup] = useState(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(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(null) const [editingCard, setEditingCard] = useState(null) const [cardName, setCardName] = useState('') const [isUpdatingCardName, setIsUpdatingCardName] = useState(false) const [editingFamilyMember, setEditingFamilyMember] = useState(null) const [familyMemberDisplayName, setFamilyMemberDisplayName] = useState('') const [parentInfo, setParentInfo] = useState(null) const [wechatMpStatus, setWechatMpStatus] = useState(null) const [isGuest, setIsGuest] = useState(false) const [isLoadingWechatMp, setIsLoadingWechatMp] = useState(false) const [isStartingWechatMpBind, setIsStartingWechatMpBind] = useState(false) const [isAuthorizingPhone, setIsAuthorizingPhone] = useState(false) const firmwareStatusPollTimerRef = useRef | 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 => { 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 ( 加载中... {systemBanner} ) } if (isGuest) { return ( 设置与管理 ⚙️ 管理宝贝资料和设备 这里可以管理宝贝资料、设备绑定、卡片、休眠时间、系统更新和家庭成员。当前页面为功能预览,真实数据会在绑定设备后展示。 宝贝资料 为不同宝贝维护独立设备和会话。 设备绑定 扫码或 NFC 绑定伴伴设备。 休眠设置 设置设备休眠时间和远程控制。 系统更新 查看设备版本,并在设备在线时发起更新。 家庭共享 邀请家庭成员一起查看和管理同一台设备。 准备绑定设备 浏览功能不需要登录;创建宝贝资料、绑定设备或查看真实设备数据时,再进入登录流程。 { Taro.setStorageSync('postLoginRedirect', '/pages/sleep/index') Taro.navigateTo({ url: '/pages/login/index' }) }} > 去登录 / 绑定设备 {systemBanner} ) } 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 ( 设置与管理 {parentInfo?.avatar_url ? ( ) : ( 👤 )} {parentDisplayName} 短信通知:{parentPhoneLabel} {parentPhone ? ( 已开启 ) : ( )} 宝贝资料数 {children.length} 个 当前宝贝 {currentChildName} 当前设备 {binding?.device_id || '未绑定'} 系统更新 {firmwareSubtitle} setShowFirmwareProgress(true) : canUpdateFirmware ? handleStartFirmwareUpdate : undefined} > {firmwareActionText} 当前设备 {binding?.device_id || '未绑定'} 当前版本 {currentFirmwareLabel} 最新版本 {latestFirmwareLabel} {menuItems.map((item, index) => ( handleMenuClick(item)}> {item.name} {item.value && {item.value}} {item.arrow && ›} {index < menuItems.length - 1 && } ))} 退出登录 伴伴 Companion V1.1.0 {showFirmwareProgress && firmwareStatus?.device_id === binding?.device_id && ( setShowFirmwareProgress(false)} onRetry={canUpdateFirmware ? () => { setShowFirmwareProgress(false); handleStartFirmwareUpdate() } : undefined} /> )} {showChildModal && ( setShowChildModal(false)}> { event.stopPropagation() }} > 切换当前宝贝 {children.length === 0 ? ( 当前还没有宝贝资料 ) : ( {children.map((item) => { const isActive = currentChild?.child_id === item.child_id const itemBinding = bindings.find((bindingItem) => bindingItem.child_id === item.child_id) || null return ( handleSelectChild(item)} > {item.child_name} {isActive && 当前} {isActive && ( { event.stopPropagation() handleOpenChildNameModal(item) }} > 修改 )} { event.stopPropagation() handleDeleteChild(item) }} > 删除 {itemBinding?.device_id || '未绑定设备'} ) })} )} setShowChildModal(false)}> 关闭 )} {showAssignDeviceModal && ( setShowAssignDeviceModal(false)}> { event.stopPropagation() }} > 关联现有设备 {unassignedBindings.length === 0 ? ( 当前没有待关联设备 ) : ( {unassignedBindings.map((item) => ( handleAssignExistingDevice(item)} > {item.device_id} 待关联 { event.stopPropagation() handleDeleteUnassignedDevice(item) }} > 删除 未关联宝贝 ))} )} setShowAssignDeviceModal(false)}> 关闭 )} {showCardsModal && ( setShowCardsModal(false)}> { event.stopPropagation() }} > 我的卡片 {binding?.device_id || '当前未绑定设备'} {isLoadingCards ? '查询中' : `${currentCardGroup?.card_count || 0} 张`} {!binding?.device_id ? ( 绑定设备后可查看卡片 UUID ) : isLoadingCards ? ( 卡片加载中... ) : currentCardGroup?.cards.length ? ( {currentCardGroup.cards.map((card) => ( {card.card_name || '未命名卡片'} {card.card_uuid} {canEditCardName && ( handleOpenCardNameModal(card)}> 编辑 )} ))} ) : ( 当前设备暂无卡片 )} {binding?.device_id && ( 添加设备卡片 )} setShowCardsModal(false)}> 关闭 )} {showModal && ( 编辑宝贝资料 宝贝名称 setChildName(sanitizeChildNameInput(event.detail.value))} /> 取消 保存 )} {showCardNameModal && ( 编辑卡片名称 setCardName(event.detail.value)} /> 留空保存后会显示为未命名卡片 取消 {isUpdatingCardName ? '保存中...' : '确认'} )} {showRoleModal && ( setShowRoleModal(false)}> { event.stopPropagation() }} > 切换 AI 角色 当前角色:{deviceRole?.name || deviceRole?.role_key || '未设置'} {deviceRole?.description || '暂无角色简介'} {isLoadingRoles ? ( 角色加载中... ) : roles.length === 0 ? ( 当前没有可用角色 ) : ( {roles.map((item) => { const isActive = deviceRole?.role_key === item.role_key return ( handleSelectRole(item)} > {item.name || item.role_key} {isActive && 当前} {item.description || `角色标识:${item.role_key}`} ) })} )} setShowRoleModal(false)}> 关闭 )} {showFamilyModal && ( setShowFamilyModal(false)}> { event.stopPropagation() }} > 家庭成员 {isLoadingFamily ? ( 成员加载中... ) : !familyInfo ? ( 当前没有成员信息 ) : ( <> {familyInfo.total}/{familyInfo.max_members} 人 {isFamilyOwner ? '主成员可添加和移除成员' : '成员可查看和操作设备'} {familyInfo.items.map((member) => { const canRemove = isFamilyOwner && !member.is_owner && !isUpdatingFamily const canEditName = isFamilyOwner && !isUpdatingFamilyName const isMe = currentUserId === member.user_id return ( {member.avatar_url ? ( ) : ( 👤 )} {member.nickname || `成员 ${member.user_id}`} {member.is_owner && 主成员} {isMe && 我} {member.is_owner ? '设备归属账号' : '家庭共享成员'} {(canEditName || canRemove) && ( {canEditName && ( handleOpenFamilyNameModal(member)}> 编辑 )} {canRemove && ( handleRemoveFamilyMember(member)}> 移除 )} )} ) })} {isFamilyOwner ? ( {familyInvitePath ? ( <> 邀请有效期至 {familyInviteExpiresAt ? familyInviteExpiresAt.replace('T', ' ').slice(0, 16) : '24 小时内'} ) : ( = familyInfo.max_members || isCreatingInvite ? 'disabled' : ''}`} onClick={familyInfo.total < familyInfo.max_members && !isCreatingInvite ? handleCreateFamilyInvite : undefined} > {isCreatingInvite ? '生成中...' : '邀请微信好友'} )} ) : ( 退出家庭 )} )} setShowFamilyModal(false)}> 关闭 )} {showFamilyNameModal && ( 编辑家庭内显示名 setFamilyMemberDisplayName(event.detail.value)} /> 留空保存后会显示账号昵称 取消 {isUpdatingFamilyName ? '保存中...' : '确认'} )} {systemBanner} ) }