1714 lines
61 KiB
TypeScript
1714 lines
61 KiB
TypeScript
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>
|
||
)
|
||
}
|