设备首页接入真实状态与音量控制
This commit is contained in:
@@ -10,71 +10,74 @@
|
||||
padding: 80px 32px 24px;
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 48px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||
border-radius: 28px;
|
||||
padding: 40px 36px;
|
||||
margin: 0 24px 32px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 24px 32px;
|
||||
padding: 40px 36px;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: -20px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 24px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4CD964;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4CD964;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.battery-percent {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 72px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
line-height: 1;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
.percent {
|
||||
margin-left: 4px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,44 +87,75 @@
|
||||
}
|
||||
}
|
||||
|
||||
.status-meta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
min-width: 140px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.status-pill-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 22px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.status-pill-value {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.device-avatar {
|
||||
position: absolute;
|
||||
right: 36px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
right: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.robot-icon {
|
||||
.robot-img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
margin: 0 24px 20px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
margin: 0 24px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.control-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
margin: 0 24px 24px;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.control-item {
|
||||
@@ -135,62 +169,70 @@
|
||||
align-items: stretch;
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
.control-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.control-left {
|
||||
flex: 1;
|
||||
}
|
||||
.icon-bg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin-right: 20px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.purple {
|
||||
background: #F0E6FF;
|
||||
}
|
||||
|
||||
&.orange {
|
||||
background: #FFF0E6;
|
||||
}
|
||||
|
||||
.control-icon-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.control-name {
|
||||
margin-bottom: 6px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.control-detail {
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
color: #FF8C42;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0 24px;
|
||||
background: #F0F0F0;
|
||||
}
|
||||
|
||||
.volume-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.control-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.icon-bg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
|
||||
&.purple {
|
||||
background: #F0E6FF;
|
||||
}
|
||||
|
||||
&.orange {
|
||||
background: #FFF0E6;
|
||||
}
|
||||
|
||||
.control-icon-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.control-name {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.control-detail {
|
||||
font-size: 26px;
|
||||
color: #FF8C42;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,9 +242,9 @@
|
||||
padding: 0 8px;
|
||||
|
||||
.volume-icon {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
@@ -211,90 +253,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #F0F0F0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
.fold-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.bg-orange {
|
||||
background: #FEF3E8;
|
||||
}
|
||||
|
||||
&.bg-green {
|
||||
background: #E8F8F0;
|
||||
}
|
||||
|
||||
&.bg-blue {
|
||||
background: #E8F0FE;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-text {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 32px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
margin: 0 24px 24px;
|
||||
padding: 10px 24px;
|
||||
border-radius: 20px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.fold-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.fold-heading {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.fold-title {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.fold-subtitle {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
color: #666666;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fold-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 88px;
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
border-radius: 999px;
|
||||
background: #FEF3E8;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #FF8C42;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
padding: 0 24px 10px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #F3F3F3;
|
||||
|
||||
@@ -311,29 +327,41 @@
|
||||
.detail-value {
|
||||
max-width: 60%;
|
||||
font-size: 28px;
|
||||
color: #1A1A1A;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
|
||||
text {
|
||||
font-size: 32px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24px;
|
||||
margin: 0 24px;
|
||||
padding: 48px 32px;
|
||||
border-radius: 24px;
|
||||
background: #FFFFFF;
|
||||
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;
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
margin: 0 auto 24px;
|
||||
border-radius: 50%;
|
||||
background: #FEF3E8;
|
||||
|
||||
.robot-img {
|
||||
width: 68px;
|
||||
@@ -357,55 +385,40 @@
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
margin-top: 28px;
|
||||
height: 92px;
|
||||
border-radius: 16px;
|
||||
background: #FF8C42;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 92px;
|
||||
margin-top: 28px;
|
||||
border-radius: 16px;
|
||||
background: #FF8C42;
|
||||
}
|
||||
|
||||
.empty-btn-text {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
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;
|
||||
}
|
||||
|
||||
.child-focus-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24px;
|
||||
margin: 0 24px 24px;
|
||||
padding: 28px 24px;
|
||||
border-radius: 24px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.child-focus-avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
background: #FEF3E8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-right: 20px;
|
||||
border-radius: 50%;
|
||||
background: #FEF3E8;
|
||||
}
|
||||
|
||||
.child-focus-icon {
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
import { View, Text, Switch, Slider, Image } from '@tarojs/components'
|
||||
import { View, Text, Slider, Image, Switch } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { getToken } from '@/services/auth'
|
||||
import { Binding, loadCurrentChildBindingContext } from '@/services/binding'
|
||||
import { Child } from '@/services/child'
|
||||
import { DeviceStatus, getDeviceStatus, setDeviceVolume } from '@/services/device'
|
||||
import './index.scss'
|
||||
|
||||
function formatTime(value?: string | null): string {
|
||||
if (!value) return '--'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
const hour = `${date.getHours()}`.padStart(2, '0')
|
||||
const minute = `${date.getMinutes()}`.padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
function formatCoordinates(status: DeviceStatus | null): string {
|
||||
if (!status || status.lat === null || status.lat === undefined || status.lng === null || status.lng === undefined) {
|
||||
return '--'
|
||||
}
|
||||
return `${status.lat.toFixed(6)}, ${status.lng.toFixed(6)}`
|
||||
}
|
||||
|
||||
function getSignalLabel(value?: number | null): string {
|
||||
if (value === null || value === undefined) return '--'
|
||||
const normalized = Number(value)
|
||||
if (!Number.isFinite(normalized)) return '--'
|
||||
if (normalized <= 4) {
|
||||
if (normalized >= 3) return '强'
|
||||
if (normalized >= 2) return '中'
|
||||
return '弱'
|
||||
}
|
||||
if (normalized >= 67) return '强'
|
||||
if (normalized >= 34) return '中'
|
||||
return '弱'
|
||||
}
|
||||
|
||||
export default function Device() {
|
||||
const [binding, setBinding] = useState<Binding | null>(null)
|
||||
const [child, setChild] = useState<Child | null>(null)
|
||||
const [deviceStatus, setDeviceStatus] = useState<DeviceStatus | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [sleepEnabled, setSleepEnabled] = useState(true)
|
||||
const [volume, setVolume] = useState(50)
|
||||
const [isDetailExpanded, setIsDetailExpanded] = useState(false)
|
||||
const [isSavingVolume, setIsSavingVolume] = useState(false)
|
||||
const [volumeValue, setVolumeValue] = useState(0)
|
||||
|
||||
useDidShow(() => {
|
||||
void loadDeviceInfo()
|
||||
@@ -28,6 +64,15 @@ export default function Device() {
|
||||
const context = await loadCurrentChildBindingContext()
|
||||
setChild(context.currentChild)
|
||||
setBinding(context.currentBinding)
|
||||
|
||||
if (context.currentBinding?.device_id) {
|
||||
const nextStatus = await getDeviceStatus(context.currentBinding.device_id)
|
||||
setDeviceStatus(nextStatus)
|
||||
setVolumeValue(nextStatus?.volume ?? 0)
|
||||
} else {
|
||||
setDeviceStatus(null)
|
||||
setVolumeValue(0)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[device] load failed:', error)
|
||||
Taro.showToast({
|
||||
@@ -40,15 +85,45 @@ export default function Device() {
|
||||
}
|
||||
|
||||
const handleSleepToggle = (e: any) => {
|
||||
setSleepEnabled(e.detail.value)
|
||||
const nextValue = Boolean(e.detail?.value)
|
||||
setSleepEnabled(nextValue)
|
||||
Taro.showToast({
|
||||
title: e.detail.value ? '休眠展示已开启' : '休眠展示已关闭',
|
||||
title: nextValue ? '休眠展示已开启' : '休眠展示已关闭',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
|
||||
const handleVolumeChange = (e: any) => {
|
||||
setVolume(e.detail.value)
|
||||
const handleToggleDetail = () => {
|
||||
setIsDetailExpanded((current) => !current)
|
||||
}
|
||||
|
||||
const handleVolumeChanging = (e: any) => {
|
||||
setVolumeValue(Number(e.detail?.value || 0))
|
||||
}
|
||||
|
||||
const handleVolumeCommit = async (e: any) => {
|
||||
if (!binding?.device_id) return
|
||||
const nextLevel = Number(e.detail?.value || 0)
|
||||
const fallbackLevel = deviceStatus?.volume ?? volumeValue ?? 0
|
||||
setVolumeValue(nextLevel)
|
||||
setIsSavingVolume(true)
|
||||
|
||||
try {
|
||||
await setDeviceVolume(nextLevel, binding.device_id)
|
||||
setDeviceStatus((current) => (current ? { ...current, volume: nextLevel } : current))
|
||||
Taro.showToast({
|
||||
title: '音量指令已发送',
|
||||
icon: 'success',
|
||||
})
|
||||
} catch (error: any) {
|
||||
setVolumeValue(deviceStatus?.volume ?? fallbackLevel)
|
||||
Taro.showToast({
|
||||
title: error?.message || '音量设置失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
setIsSavingVolume(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -115,8 +190,12 @@ export default function Device() {
|
||||
|
||||
const childName = child.child_name || '未设置'
|
||||
const pageTitle = `${childName} 的伴伴`
|
||||
const battery = 82
|
||||
const daysLeft = 4
|
||||
const batteryValue = deviceStatus?.power ?? deviceStatus?.battery_pct ?? null
|
||||
const batteryDisplay = batteryValue === null || batteryValue === undefined ? '--' : String(batteryValue)
|
||||
const signalLabel = getSignalLabel(deviceStatus?.signal)
|
||||
const versionLabel = deviceStatus?.version || '--'
|
||||
const coordinateLabel = formatCoordinates(deviceStatus)
|
||||
const volumeLabel = `${volumeValue}%`
|
||||
|
||||
return (
|
||||
<View className='device-page'>
|
||||
@@ -131,10 +210,20 @@ export default function Device() {
|
||||
</View>
|
||||
<View className='battery-section'>
|
||||
<Text className='battery-percent'>
|
||||
{battery}
|
||||
<Text className='percent'>%</Text>
|
||||
{batteryDisplay}
|
||||
{batteryDisplay !== '--' && <Text className='percent'>%</Text>}
|
||||
</Text>
|
||||
<Text className='battery-label'>模拟电量展示 (预计续航 {daysLeft} 天)</Text>
|
||||
<Text className='battery-label'>当前电量</Text>
|
||||
</View>
|
||||
<View className='status-meta'>
|
||||
<View className='status-pill'>
|
||||
<Text className='status-pill-label'>信号</Text>
|
||||
<Text className='status-pill-value'>{signalLabel}</Text>
|
||||
</View>
|
||||
<View className='status-pill'>
|
||||
<Text className='status-pill-label'>版本</Text>
|
||||
<Text className='status-pill-value'>{versionLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='device-avatar'>
|
||||
<View className='robot-icon'>
|
||||
@@ -143,31 +232,12 @@ export default function Device() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='detail-card'>
|
||||
<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.device_id}</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='section-title'>设备控制</View>
|
||||
<View className='control-card'>
|
||||
<View className='control-item'>
|
||||
<View className='control-left'>
|
||||
<View className='icon-bg purple'>
|
||||
<Image
|
||||
className='control-icon-img'
|
||||
src={require('../../assets/tab-icons/moon.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
<Image className='control-icon-img' src={require('../../assets/tab-icons/moon.png')} mode='aspectFit' />
|
||||
</View>
|
||||
<View className='control-info'>
|
||||
<Text className='control-name'>定时休眠</Text>
|
||||
@@ -189,7 +259,10 @@ export default function Device() {
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
<Text className='control-name'>外放音量</Text>
|
||||
<View className='control-info'>
|
||||
<Text className='control-name'>外放音量</Text>
|
||||
<Text className='control-detail'>{isSavingVolume ? '发送中...' : `当前 ${volumeLabel}`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='volume-slider'>
|
||||
@@ -202,8 +275,9 @@ export default function Device() {
|
||||
className='slider'
|
||||
min={0}
|
||||
max={100}
|
||||
value={volume}
|
||||
onChange={handleVolumeChange}
|
||||
value={volumeValue}
|
||||
onChanging={handleVolumeChanging}
|
||||
onChange={handleVolumeCommit}
|
||||
activeColor='#FF8C42'
|
||||
backgroundColor='#E5E5E5'
|
||||
blockColor='#FF8C42'
|
||||
@@ -216,6 +290,45 @@ export default function Device() {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='fold-card'>
|
||||
<View className='fold-header' onClick={handleToggleDetail}>
|
||||
<View className='fold-heading'>
|
||||
<Text className='fold-title'>设备详情</Text>
|
||||
<Text className='fold-subtitle'>{binding.device_id}</Text>
|
||||
</View>
|
||||
<Text className='fold-action'>{isDetailExpanded ? '收起' : '展开'}</Text>
|
||||
</View>
|
||||
|
||||
{isDetailExpanded && (
|
||||
<View className='detail-body'>
|
||||
<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.device_id}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>绑定时间</Text>
|
||||
<Text className='detail-value'>{formatTime(binding.bound_at)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>当前坐标</Text>
|
||||
<Text className='detail-value'>{coordinateLabel}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>状态更新时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.settings_updated_at)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>定位更新时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.location_updated_at)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
62
banban-mini/src/services/device.ts
Normal file
62
banban-mini/src/services/device.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { request } from './api'
|
||||
import { loadCurrentChildBindingContext } from './binding'
|
||||
|
||||
export interface DeviceStatus {
|
||||
device_id: string
|
||||
child_id?: number | null
|
||||
child_name?: string | null
|
||||
power?: number | null
|
||||
volume?: number | null
|
||||
signal?: number | null
|
||||
version?: string | null
|
||||
settings_updated_at?: string | null
|
||||
coord_type?: string | null
|
||||
lat?: number | null
|
||||
lng?: number | null
|
||||
accuracy_m?: number | null
|
||||
altitude_m?: number | null
|
||||
speed_mps?: number | null
|
||||
heading_deg?: number | null
|
||||
source?: number | null
|
||||
battery_pct?: number | null
|
||||
device_time?: string | null
|
||||
server_time?: string | null
|
||||
location_updated_at?: string | null
|
||||
}
|
||||
|
||||
export interface DeviceVolumeUpdateResponse {
|
||||
device_id: string
|
||||
level: number
|
||||
msg_id: string
|
||||
}
|
||||
|
||||
async function resolveDeviceId(deviceId?: string): Promise<string> {
|
||||
const normalizedDeviceId = String(deviceId || '').trim()
|
||||
if (normalizedDeviceId) return normalizedDeviceId
|
||||
const context = await loadCurrentChildBindingContext()
|
||||
return context.currentBinding?.device_id || ''
|
||||
}
|
||||
|
||||
export async function getDeviceStatus(deviceId?: string): Promise<DeviceStatus | null> {
|
||||
const resolvedDeviceId = await resolveDeviceId(deviceId)
|
||||
if (!resolvedDeviceId) return null
|
||||
|
||||
try {
|
||||
return await request<DeviceStatus>(`/banban/devices/${resolvedDeviceId}/status`)
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function setDeviceVolume(level: number, deviceId?: string): Promise<DeviceVolumeUpdateResponse> {
|
||||
const resolvedDeviceId = await resolveDeviceId(deviceId)
|
||||
if (!resolvedDeviceId) {
|
||||
throw new Error('当前没有可用设备')
|
||||
}
|
||||
|
||||
return request<DeviceVolumeUpdateResponse>(`/banban/devices/${resolvedDeviceId}/volume`, {
|
||||
method: 'POST',
|
||||
data: { level },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user