508 lines
17 KiB
TypeScript
508 lines
17 KiB
TypeScript
import { View, Text, Image, Input } from '@tarojs/components'
|
||
import { useState } from 'react'
|
||
import Taro, { useDidShow } from '@tarojs/taro'
|
||
import { clearToken, getToken } from '@/services/auth'
|
||
import {
|
||
BindingListItem,
|
||
clearSelectedBindingDeviceId,
|
||
loadCurrentChildBindingContext,
|
||
setSelectedBindingDeviceId,
|
||
unbindDevice,
|
||
} from '@/services/binding'
|
||
import { Child, clearSelectedChildId, createChild, setSelectedChildId, updateChild } from '@/services/child'
|
||
import { DeviceFirmwareStatus, getDeviceFirmwareStatus, startDeviceFirmwareUpdate } from '@/services/device'
|
||
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
|
||
}
|
||
|
||
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 [isLoadingFirmware, setIsLoadingFirmware] = useState(false)
|
||
const [isUpdatingFirmware, setIsUpdatingFirmware] = useState(false)
|
||
const [showModal, setShowModal] = useState(false)
|
||
const [showChildModal, setShowChildModal] = useState(false)
|
||
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
|
||
const [childName, setChildName] = useState('')
|
||
const [editingChildId, setEditingChildId] = useState<number | null>(null)
|
||
const [parentInfo, setParentInfo] = useState<{ nickname?: string; avatar_url?: string }>({})
|
||
|
||
useDidShow(() => {
|
||
void loadData()
|
||
})
|
||
|
||
const loadData = async () => {
|
||
if (!getToken()) {
|
||
Taro.reLaunch({ url: '/pages/login/index' })
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
try {
|
||
const context = await loadCurrentChildBindingContext()
|
||
const nextBinding = (context.currentBinding as BindingListItem | null) || null
|
||
setChildren(context.children)
|
||
setBindings(context.bindings as BindingListItem[])
|
||
setCurrentChild(context.currentChild)
|
||
setBinding(nextBinding)
|
||
setParentInfo(Taro.getStorageSync('userInfo') || {})
|
||
void loadFirmwareStatus(nextBinding?.device_id)
|
||
} catch (error: any) {
|
||
console.error('[manage] load failed:', error)
|
||
Taro.showToast({
|
||
title: error?.message || '加载失败,请稍后重试',
|
||
icon: 'none',
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const currentChildName = currentChild?.child_name || '未设置'
|
||
|
||
const loadFirmwareStatus = async (deviceId?: string | null) => {
|
||
const normalizedDeviceId = String(deviceId || '').trim()
|
||
if (!normalizedDeviceId) {
|
||
setFirmwareStatus(null)
|
||
setIsLoadingFirmware(false)
|
||
return
|
||
}
|
||
|
||
setIsLoadingFirmware(true)
|
||
try {
|
||
const nextFirmwareStatus = await getDeviceFirmwareStatus(normalizedDeviceId)
|
||
setFirmwareStatus(nextFirmwareStatus)
|
||
} catch (error: any) {
|
||
console.error('[manage] firmware load failed:', error)
|
||
setFirmwareStatus(null)
|
||
Taro.showToast({
|
||
title: error?.message || '系统更新状态加载失败',
|
||
icon: 'none',
|
||
})
|
||
} finally {
|
||
setIsLoadingFirmware(false)
|
||
}
|
||
}
|
||
|
||
const handleOpenModal = (type: 'add' | 'edit', child?: Child) => {
|
||
setModalType(type)
|
||
setChildName(child?.child_name || '')
|
||
setEditingChildId(child?.child_id || null)
|
||
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)
|
||
setShowChildModal(false)
|
||
Taro.showToast({
|
||
title: '已切换当前孩子',
|
||
icon: 'success',
|
||
})
|
||
}
|
||
|
||
const handleOpenAddChildFromSwitch = () => {
|
||
setShowChildModal(false)
|
||
handleOpenModal('add')
|
||
}
|
||
|
||
const handleSubmitModal = async () => {
|
||
const normalizedName = childName.trim()
|
||
if (!normalizedName) {
|
||
Taro.showToast({ title: '请输入儿童昵称', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (modalType === 'add') {
|
||
const createdChild = await createChild({ child_name: normalizedName })
|
||
setSelectedChildId(createdChild.child_id)
|
||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||
} else if (editingChildId) {
|
||
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 handleStartFirmwareUpdate = () => {
|
||
if (!binding?.device_id || !firmwareStatus?.can_update || isLoadingFirmware || isUpdatingFirmware) 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 nextFirmwareStatus = await startDeviceFirmwareUpdate(binding.device_id)
|
||
setFirmwareStatus(nextFirmwareStatus)
|
||
Taro.showToast({
|
||
title: '更新指令已发送',
|
||
icon: 'success',
|
||
})
|
||
} catch (error: any) {
|
||
Taro.showToast({
|
||
title: error?.message || '更新失败,请重试',
|
||
icon: 'none',
|
||
})
|
||
} finally {
|
||
setIsUpdatingFirmware(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')
|
||
Taro.reLaunch({ url: '/pages/login/index' })
|
||
},
|
||
})
|
||
}
|
||
|
||
const handleMenuClick = (item: MenuItem) => {
|
||
if (item.disabled) return
|
||
|
||
if (item.name === '当前孩子') {
|
||
setShowChildModal(true)
|
||
return
|
||
}
|
||
|
||
if (item.name === '编辑当前孩子') {
|
||
if (currentChild) {
|
||
handleOpenModal('edit', currentChild)
|
||
} else {
|
||
handleOpenModal('add')
|
||
}
|
||
return
|
||
}
|
||
|
||
if (item.name === '新增孩子') {
|
||
handleOpenModal('add')
|
||
return
|
||
}
|
||
|
||
if (item.name === '绑定设备') {
|
||
if (!currentChild) {
|
||
handleOpenModal('add')
|
||
Taro.showToast({
|
||
title: '请先添加儿童资料',
|
||
icon: 'none',
|
||
})
|
||
return
|
||
}
|
||
Taro.navigateTo({ url: '/pages/bind/index' })
|
||
return
|
||
}
|
||
|
||
if (item.name === '解除设备绑定') {
|
||
handleUnbind()
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<View className='manage-page'>
|
||
<View className='loading'>
|
||
<Text>加载中...</Text>
|
||
</View>
|
||
{systemBanner}
|
||
</View>
|
||
)
|
||
}
|
||
|
||
const parentDisplayName = parentInfo.nickname?.trim() || '家长'
|
||
const currentFirmwareLabel = firmwareStatus?.current_version || '--'
|
||
const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
|
||
const canUpdateFirmware = Boolean(binding?.device_id && firmwareStatus?.can_update && !isLoadingFirmware && !isUpdatingFirmware)
|
||
const firmwareActionText = isLoadingFirmware ? '查询中' : isUpdatingFirmware ? '发送中' : '更新'
|
||
const firmwareSubtitle = !binding?.device_id
|
||
? '绑定设备后可查看系统版本'
|
||
: isLoadingFirmware
|
||
? '正在查询当前设备版本'
|
||
: firmwareStatus?.update_available
|
||
? '发现可更新版本'
|
||
: '当前设备版本状态'
|
||
const menuItems: MenuItem[] = [
|
||
{
|
||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||
iconBgClass: 'orange',
|
||
name: '当前孩子',
|
||
value: currentChildName,
|
||
arrow: true,
|
||
},
|
||
{
|
||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||
iconBgClass: 'orange',
|
||
name: '新增孩子',
|
||
value: '',
|
||
arrow: true,
|
||
},
|
||
{
|
||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||
iconBgClass: 'orange',
|
||
name: '编辑当前孩子',
|
||
value: '',
|
||
arrow: true,
|
||
},
|
||
{
|
||
icon: require('../../assets/tab-icons/rings.png'),
|
||
iconBgClass: 'green',
|
||
name: '绑定设备',
|
||
value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定',
|
||
arrow: true,
|
||
},
|
||
{
|
||
icon: require('../../assets/tab-icons/broken-rings.png'),
|
||
iconBgClass: 'red',
|
||
name: '解除设备绑定',
|
||
value: binding?.device_id ? `当前: ${binding.device_id}` : '',
|
||
arrow: true,
|
||
disabled: !binding,
|
||
},
|
||
]
|
||
|
||
return (
|
||
<View className='manage-page'>
|
||
<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>
|
||
</View>
|
||
<View className='verified-badge'>
|
||
<Text>{currentChild ? '已选择当前孩子' : '未选择孩子'}</Text>
|
||
</View>
|
||
</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'>
|
||
<Text className='system-update-title'>系统更新</Text>
|
||
<Text className='system-update-subtitle'>{firmwareSubtitle}</Text>
|
||
</View>
|
||
<View
|
||
className={`system-update-action ${canUpdateFirmware ? '' : 'disabled'}`}
|
||
onClick={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>
|
||
|
||
{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>
|
||
{isActive && <Text className='device-switch-tag'>当前</Text>}
|
||
</View>
|
||
<Text className='device-switch-name'>{itemBinding?.device_id || '未绑定设备'}</Text>
|
||
</View>
|
||
)
|
||
})}
|
||
</View>
|
||
)}
|
||
<View className='device-switch-footer'>
|
||
<Text className='device-switch-add' onClick={handleOpenAddChildFromSwitch}>
|
||
+ 新增孩子
|
||
</Text>
|
||
</View>
|
||
<View className='modal-actions'>
|
||
<Text className='modal-action cancel' onClick={() => setShowChildModal(false)}>
|
||
关闭
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{showModal && (
|
||
<View className='modal-mask'>
|
||
<View className='modal-card'>
|
||
<Text className='modal-title'>{modalType === 'add' ? '新建儿童资料' : '修改儿童昵称'}</Text>
|
||
<View className='modal-input-wrap'>
|
||
<Input
|
||
className='modal-input'
|
||
value={childName}
|
||
placeholder='请输入儿童昵称'
|
||
focus
|
||
onInput={(event) => setChildName(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>
|
||
)}
|
||
{systemBanner}
|
||
</View>
|
||
)
|
||
}
|