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

@@ -14,15 +14,8 @@
font-weight: 700;
color: #1A1A1A;
display: block;
margin-bottom: 12px;
line-height: 1.2;
}
.page-subtitle {
font-size: 28px;
color: #666666;
line-height: 1.4;
}
}
.status-card {
@@ -289,3 +282,99 @@
color: #999999;
}
}
.detail-card {
background: #FFFFFF;
border-radius: 20px;
margin: 0 24px 24px;
padding: 10px 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #F3F3F3;
&:last-child {
border-bottom: none;
}
}
.detail-label {
font-size: 28px;
color: #666666;
}
.detail-value {
max-width: 60%;
font-size: 28px;
color: #1A1A1A;
text-align: right;
word-break: break-all;
}
.empty-card {
background: #FFFFFF;
border-radius: 24px;
margin: 0 24px;
padding: 48px 32px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
text-align: center;
}
.empty-robot {
width: 132px;
height: 132px;
background: #FEF3E8;
border-radius: 50%;
margin: 0 auto 24px;
display: flex;
align-items: center;
justify-content: center;
.robot-img {
width: 68px;
height: 68px;
}
}
.empty-title {
display: block;
font-size: 34px;
font-weight: 700;
color: #1A1A1A;
}
.empty-btn {
margin-top: 28px;
height: 92px;
border-radius: 16px;
background: #FF8C42;
display: flex;
align-items: center;
justify-content: center;
}
.empty-btn-text {
font-size: 30px;
color: #FFFFFF;
font-weight: 600;
}
.helper-card {
background: #FFFFFF;
border-radius: 20px;
margin: 0 24px 24px;
padding: 28px 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.helper-title {
display: block;
font-size: 30px;
font-weight: 600;
color: #1A1A1A;
}

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>
)
}