feat(小程序): 支持多设备绑定与管理

This commit is contained in:
stu2not
2026-04-22 17:25:31 +08:00
parent d7df6f565c
commit f21e0afcd0
8 changed files with 1078 additions and 670 deletions

View File

@@ -1,104 +1,65 @@
import { View, Text, Switch, Slider, Image } from '@tarojs/components'
import { useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { getCurrentBinding } from '@/services/binding'
import { useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { getToken } from '@/services/auth'
import { Binding, resolveActiveBinding } from '@/services/binding'
import { Child, getChild } from '@/services/child'
import './index.scss'
interface DeviceData {
id: string
name: string
status: 'online' | 'offline'
battery: number
daysLeft: number
sleepEnabled: boolean
sleepTime: string
volume: number
}
export default function Device() {
const [device, setDevice] = useState<DeviceData | null>(null)
const [binding, setBinding] = useState<Binding | null>(null)
const [child, setChild] = useState<Child | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [hasDevice, setHasDevice] = useState(false)
const [sleepEnabled, setSleepEnabled] = useState(true)
const [volume, setVolume] = useState(50)
useEffect(() => {
console.log('[Device] Checking login status...')
const token = Taro.getStorageSync('token')
console.log('[Device] Token exists:', !!token)
if (!token) {
console.log('[Device] No token, redirecting to login')
Taro.redirectTo({ url: '/pages/login/index' })
useDidShow(() => {
void loadDeviceInfo()
})
const loadDeviceInfo = async () => {
if (!getToken()) {
Taro.reLaunch({ url: '/pages/login/index' })
return
}
loadDeviceInfo()
}, [])
const loadDeviceInfo = async () => {
setIsLoading(true)
try {
const binding = await getCurrentBinding()
console.log('[Device] Got binding:', binding)
if (binding) {
if (!binding.child_id) {
setHasDevice(false)
Taro.showToast({ title: 'Please link child profile first', icon: 'none' })
setTimeout(() => {
Taro.redirectTo({ url: '/pages/bind/index' })
}, 600)
return
}
const activeBinding = await resolveActiveBinding()
setBinding(activeBinding)
setHasDevice(true)
Taro.setStorageSync('hasDevice', '1')
Taro.setStorageSync('deviceInfo', {
id: binding.device_id,
name: '伴伴设备',
status: 'online',
battery: 82
})
setDevice({
id: binding.device_id,
name: '伴伴设备',
status: 'online',
battery: 82,
daysLeft: 4,
sleepEnabled: true,
sleepTime: '每天 22:00 - 07:00',
volume: 50
})
if (activeBinding?.child_id) {
try {
const currentChild = await getChild(activeBinding.child_id)
setChild(currentChild)
} catch (error) {
console.error('[device] load child failed:', error)
setChild(null)
}
} else {
setHasDevice(false)
Taro.removeStorageSync('hasDevice')
Taro.removeStorageSync('deviceInfo')
}
} catch (err: any) {
console.log('[Device] No binding or error:', err?.message || err)
if (err?.status === 404) {
setHasDevice(false)
Taro.removeStorageSync('hasDevice')
Taro.removeStorageSync('deviceInfo')
setChild(null)
}
} catch (error: any) {
console.error('[device] load failed:', error)
Taro.showToast({
title: error?.message || '加载失败,请稍后重试',
icon: 'none',
})
} finally {
setIsLoading(false)
}
setIsLoading(false)
}
const handleSleepToggle = (e: any) => {
if (!device) return
setDevice({ ...device, sleepEnabled: e.detail.value })
setSleepEnabled(e.detail.value)
Taro.showToast({
title: e.detail.value ? '已开启定时休眠' : '已关闭定时休眠',
icon: 'success'
title: e.detail.value ? '休眠展示已开启' : '休眠展示已关闭',
icon: 'none',
})
}
const handleVolumeChange = (e: any) => {
if (!device) return
setDevice({ ...device, volume: e.detail.value })
}
const switchTab = (url: string) => {
Taro.switchTab({ url })
setVolume(e.detail.value)
}
if (isLoading) {
@@ -111,27 +72,48 @@ export default function Device() {
)
}
// 未绑定设备,跳转到绑定页面
if (!hasDevice || !device) {
Taro.redirectTo({ url: '/pages/bind/index' })
return null
if (!binding) {
return (
<View className='device-page'>
<View className='page-header'>
<Text className='page-title'></Text>
</View>
<View className='empty-card'>
<View className='empty-robot'>
<Image className='robot-img' src={require('../../assets/tab-icons/robot.png')} mode='aspectFit' />
</View>
<Text className='empty-title'></Text>
<View className='empty-btn' onClick={() => Taro.navigateTo({ url: '/pages/bind/index' })}>
<Text className='empty-btn-text'></Text>
</View>
</View>
</View>
)
}
const childName = child?.child_name || '未设置'
const pageTitle = child?.child_name ? `${child.child_name} 的伴伴` : '伴伴设备'
const battery = 82
const daysLeft = 4
return (
<View className='device-page'>
<View className='page-header'>
<Text className='page-title'>{device.name}</Text>
<Text className='page-subtitle'></Text>
<Text className='page-title'>{pageTitle}</Text>
</View>
<View className='status-card'>
<View className='status-badge'>
<View className='status-dot'></View>
<Text className='status-text'>4G 线</Text>
<Text className='status-text'>{binding.child_id ? '已完成绑定' : '待关联儿童'}</Text>
</View>
<View className='battery-section'>
<Text className='battery-percent'>{device.battery}<Text className='percent'>%</Text></Text>
<Text className='battery-label'> ( {device.daysLeft} )</Text>
<Text className='battery-percent'>
{battery}
<Text className='percent'>%</Text>
</Text>
<Text className='battery-label'> ( {daysLeft} )</Text>
</View>
<View className='device-avatar'>
<View className='robot-icon'>
@@ -140,6 +122,21 @@ export default function Device() {
</View>
</View>
<View className='detail-card'>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value'>{binding.device_id}</Text>
</View>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value'>{childName}</Text>
</View>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value'>{binding.bound_at || '--'}</Text>
</View>
</View>
<View className='section-title'></View>
<View className='control-card'>
<View className='control-item'>
@@ -153,14 +150,10 @@ export default function Device() {
</View>
<View className='control-info'>
<Text className='control-name'></Text>
<Text className='control-detail'>{device.sleepTime}</Text>
<Text className='control-detail'></Text>
</View>
</View>
<Switch
checked={device.sleepEnabled}
onChange={handleSleepToggle}
color='#FF8C42'
/>
<Switch checked={sleepEnabled} onChange={handleSleepToggle} color='#FF8C42' />
</View>
<View className='divider'></View>
@@ -188,7 +181,7 @@ export default function Device() {
className='slider'
min={0}
max={100}
value={device.volume}
value={volume}
onChange={handleVolumeChange}
activeColor='#FF8C42'
backgroundColor='#E5E5E5'
@@ -203,6 +196,11 @@ export default function Device() {
</View>
</View>
{!binding.child_id && (
<View className='helper-card' onClick={() => Taro.navigateTo({ url: '/pages/bind/index' })}>
<Text className='helper-title'></Text>
</View>
)}
</View>
)
}