提交小程序合入代码
This commit is contained in:
@@ -164,11 +164,21 @@
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
|
||||
&.sleep-item:active {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
&.volume-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
|
||||
&.quick-sleep-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-left {
|
||||
@@ -220,6 +230,14 @@
|
||||
color: #FF8C42;
|
||||
}
|
||||
|
||||
.control-link {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #FF8C42;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0 24px;
|
||||
@@ -236,6 +254,195 @@
|
||||
}
|
||||
}
|
||||
|
||||
.quick-sleep-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.quick-sleep-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.quick-sleep-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 82px;
|
||||
border-radius: 18px;
|
||||
|
||||
&.sleep {
|
||||
background: #1F2937;
|
||||
}
|
||||
|
||||
&.wake {
|
||||
background: #FF8C42;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-sleep-btn-text {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.alarm-card {
|
||||
margin: 0 24px 24px;
|
||||
border-radius: 20px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alarm-header {
|
||||
padding: 24px 24px 18px;
|
||||
background: linear-gradient(135deg, #FFF1F2 0%, #FFE4E6 100%);
|
||||
}
|
||||
|
||||
.alarm-title {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #9F1239;
|
||||
}
|
||||
|
||||
.alarm-subtitle {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.6;
|
||||
color: #BE123C;
|
||||
}
|
||||
|
||||
.alarm-body {
|
||||
padding: 6px 24px 10px;
|
||||
}
|
||||
|
||||
.alarm-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid #F7E4E8;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-label {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.alarm-value {
|
||||
max-width: 60%;
|
||||
font-size: 28px;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.alarm-empty {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.alarm-empty-text {
|
||||
font-size: 26px;
|
||||
line-height: 1.7;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.alarm-history {
|
||||
padding: 0 24px 20px;
|
||||
border-top: 1px solid #F7E4E8;
|
||||
}
|
||||
|
||||
.alarm-history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22px 0 18px;
|
||||
}
|
||||
|
||||
.alarm-history-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.alarm-history-count {
|
||||
font-size: 24px;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.alarm-history-item {
|
||||
padding: 18px 0;
|
||||
border-top: 1px solid #F5F5F5;
|
||||
|
||||
&:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-history-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.alarm-history-time {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.alarm-history-type {
|
||||
flex-shrink: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #BE123C;
|
||||
}
|
||||
|
||||
.alarm-history-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.alarm-history-child,
|
||||
.alarm-history-device {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 24px;
|
||||
color: #6B7280;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.alarm-history-device {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alarm-history-empty {
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
.alarm-history-empty-text {
|
||||
font-size: 24px;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { View, Text, Slider, Image, Switch } from '@tarojs/components'
|
||||
import { View, Text, Slider, Image } 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 { DeviceAlarmItem, DeviceStatus, getDeviceAlarms, getDeviceStatus, setDeviceRemoteSleepWake, setDeviceVolume } from '@/services/device'
|
||||
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
|
||||
import './index.scss'
|
||||
|
||||
function formatTime(value?: string | null): string {
|
||||
@@ -39,14 +40,53 @@ function getSignalLabel(value?: number | null): string {
|
||||
return '弱'
|
||||
}
|
||||
|
||||
function formatSleepRange(status: DeviceStatus | null): string {
|
||||
const start = status?.disable_time_start?.trim()
|
||||
const end = status?.disable_time_end?.trim()
|
||||
if (!start || !end) return '未设置'
|
||||
return `${start}-${end}`
|
||||
}
|
||||
|
||||
function formatAlarmTime(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')
|
||||
const second = `${date.getSeconds()}`.padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
function getAlarmTypeLabel(value?: string | null): string {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) return '--'
|
||||
if (normalized === '010') return '紧急告警'
|
||||
return normalized
|
||||
}
|
||||
|
||||
function getAlarmChildLabel(alarm: DeviceAlarmItem | null, fallbackChildName?: string | null): string {
|
||||
const alarmChildName = String(alarm?.child_name || '').trim()
|
||||
if (alarmChildName) return alarmChildName
|
||||
|
||||
const fallbackName = String(fallbackChildName || '').trim()
|
||||
if (fallbackName) return fallbackName
|
||||
|
||||
if (alarm?.child_id) return `儿童 ${alarm.child_id}`
|
||||
return '未关联儿童'
|
||||
}
|
||||
|
||||
export default function Device() {
|
||||
const systemBanner = useSystemBanner()
|
||||
const [binding, setBinding] = useState<Binding | null>(null)
|
||||
const [child, setChild] = useState<Child | null>(null)
|
||||
const [deviceStatus, setDeviceStatus] = useState<DeviceStatus | null>(null)
|
||||
const [deviceAlarms, setDeviceAlarms] = useState<DeviceAlarmItem[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [sleepEnabled, setSleepEnabled] = useState(true)
|
||||
const [isDetailExpanded, setIsDetailExpanded] = useState(false)
|
||||
const [isSavingVolume, setIsSavingVolume] = useState(false)
|
||||
const [sleepWakePending, setSleepWakePending] = useState<'on' | 'off' | null>(null)
|
||||
const [volumeValue, setVolumeValue] = useState(0)
|
||||
|
||||
useDidShow(() => {
|
||||
@@ -66,11 +106,16 @@ export default function Device() {
|
||||
setBinding(context.currentBinding)
|
||||
|
||||
if (context.currentBinding?.device_id) {
|
||||
const nextStatus = await getDeviceStatus(context.currentBinding.device_id)
|
||||
const [nextStatus, nextAlarms] = await Promise.all([
|
||||
getDeviceStatus(context.currentBinding.device_id),
|
||||
getDeviceAlarms(context.currentBinding.device_id, 20),
|
||||
])
|
||||
setDeviceStatus(nextStatus)
|
||||
setDeviceAlarms(nextAlarms.items || [])
|
||||
setVolumeValue(nextStatus?.volume ?? 0)
|
||||
} else {
|
||||
setDeviceStatus(null)
|
||||
setDeviceAlarms([])
|
||||
setVolumeValue(0)
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -84,13 +129,8 @@ export default function Device() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSleepToggle = (e: any) => {
|
||||
const nextValue = Boolean(e.detail?.value)
|
||||
setSleepEnabled(nextValue)
|
||||
Taro.showToast({
|
||||
title: nextValue ? '休眠已开启' : '休眠已关闭',
|
||||
icon: 'none',
|
||||
})
|
||||
const handleOpenSleepSchedule = () => {
|
||||
Taro.navigateTo({ url: '/pages/sleep-schedule/index' })
|
||||
}
|
||||
|
||||
const handleToggleDetail = () => {
|
||||
@@ -126,12 +166,33 @@ export default function Device() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoteSleepWake = async (switchValue: 'on' | 'off') => {
|
||||
if (!binding?.device_id || sleepWakePending) return
|
||||
|
||||
setSleepWakePending(switchValue)
|
||||
try {
|
||||
await setDeviceRemoteSleepWake(switchValue, binding.device_id)
|
||||
Taro.showToast({
|
||||
title: switchValue === 'off' ? '休眠指令已发送' : '唤醒指令已发送',
|
||||
icon: 'success',
|
||||
})
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error?.message || '操作失败,请重试',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
setSleepWakePending(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className='device-page'>
|
||||
<View className='loading'>
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
{systemBanner}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -153,6 +214,7 @@ export default function Device() {
|
||||
<Text className='empty-btn-text'>去添加孩子</Text>
|
||||
</View>
|
||||
</View>
|
||||
{systemBanner}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -184,6 +246,7 @@ export default function Device() {
|
||||
<Text className='empty-btn-text'>去绑定设备</Text>
|
||||
</View>
|
||||
</View>
|
||||
{systemBanner}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -196,6 +259,10 @@ export default function Device() {
|
||||
const versionLabel = deviceStatus?.version || '--'
|
||||
const coordinateLabel = formatCoordinates(deviceStatus)
|
||||
const volumeLabel = `${volumeValue}%`
|
||||
const sleepRangeLabel = formatSleepRange(deviceStatus)
|
||||
const latestAlarm = deviceAlarms[0] || null
|
||||
const historyAlarms = latestAlarm ? deviceAlarms.slice(1) : []
|
||||
const latestAlarmChildName = getAlarmChildLabel(latestAlarm, childName)
|
||||
|
||||
return (
|
||||
<View className='device-page'>
|
||||
@@ -234,17 +301,53 @@ export default function Device() {
|
||||
|
||||
<View className='section-title'>设备控制</View>
|
||||
<View className='control-card'>
|
||||
<View className='control-item'>
|
||||
<View className='control-item sleep-item' onClick={handleOpenSleepSchedule}>
|
||||
<View className='control-left'>
|
||||
<View className='icon-bg purple'>
|
||||
<Image className='control-icon-img' src={require('../../assets/tab-icons/moon.png')} mode='aspectFit' />
|
||||
</View>
|
||||
<View className='control-info'>
|
||||
<Text className='control-name'>定时休眠</Text>
|
||||
<Text className='control-detail'>开启后进入休眠状态</Text>
|
||||
<Text className='control-detail'>{sleepRangeLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='control-link'>设置</Text>
|
||||
</View>
|
||||
|
||||
<View className='divider'></View>
|
||||
|
||||
<View className='control-item quick-sleep-item'>
|
||||
<View className='quick-sleep-header'>
|
||||
<View className='control-left'>
|
||||
<View className='icon-bg purple'>
|
||||
<Image className='control-icon-img' src={require('../../assets/tab-icons/moon.png')} mode='aspectFit' />
|
||||
</View>
|
||||
<View className='control-info'>
|
||||
<Text className='control-name'>立即休眠/唤醒</Text>
|
||||
<Text className='control-detail'>
|
||||
{sleepWakePending === 'off'
|
||||
? '正在发送休眠指令...'
|
||||
: sleepWakePending === 'on'
|
||||
? '正在发送唤醒指令...'
|
||||
: '立即控制当前设备'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='quick-sleep-actions'>
|
||||
<View
|
||||
className={`quick-sleep-btn sleep ${sleepWakePending ? 'disabled' : ''}`}
|
||||
onClick={sleepWakePending ? undefined : () => handleRemoteSleepWake('off')}
|
||||
>
|
||||
<Text className='quick-sleep-btn-text'>立即休眠</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`quick-sleep-btn wake ${sleepWakePending ? 'disabled' : ''}`}
|
||||
onClick={sleepWakePending ? undefined : () => handleRemoteSleepWake('on')}
|
||||
>
|
||||
<Text className='quick-sleep-btn-text'>立即唤醒</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch checked={sleepEnabled} onChange={handleSleepToggle} color='#FF8C42' />
|
||||
</View>
|
||||
|
||||
<View className='divider'></View>
|
||||
@@ -291,6 +394,70 @@ export default function Device() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='alarm-card'>
|
||||
<View className='alarm-header'>
|
||||
<Text className='alarm-title'>最近告警</Text>
|
||||
<Text className='alarm-subtitle'>
|
||||
{latestAlarm ? '已收到设备主动上报的告警,可继续查看以往记录' : '暂时没有收到新的设备告警'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{latestAlarm ? (
|
||||
<View>
|
||||
<View className='alarm-body'>
|
||||
<View className='alarm-row'>
|
||||
<Text className='alarm-label'>告警时间</Text>
|
||||
<Text className='alarm-value'>{formatAlarmTime(latestAlarm.created_at)}</Text>
|
||||
</View>
|
||||
<View className='alarm-row'>
|
||||
<Text className='alarm-label'>来源儿童</Text>
|
||||
<Text className='alarm-value'>{latestAlarmChildName}</Text>
|
||||
</View>
|
||||
<View className='alarm-row'>
|
||||
<Text className='alarm-label'>来源设备</Text>
|
||||
<Text className='alarm-value'>{latestAlarm.device_id}</Text>
|
||||
</View>
|
||||
<View className='alarm-row'>
|
||||
<Text className='alarm-label'>上报类型</Text>
|
||||
<Text className='alarm-value'>{getAlarmTypeLabel(latestAlarm.source_msg_id)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='alarm-history'>
|
||||
<View className='alarm-history-header'>
|
||||
<Text className='alarm-history-title'>以往告警</Text>
|
||||
<Text className='alarm-history-count'>最近 {historyAlarms.length} 条</Text>
|
||||
</View>
|
||||
|
||||
{historyAlarms.length > 0 ? (
|
||||
historyAlarms.map((alarm) => (
|
||||
<View className='alarm-history-item' key={alarm.alarm_id}>
|
||||
<View className='alarm-history-main'>
|
||||
<Text className='alarm-history-time'>{formatAlarmTime(alarm.created_at)}</Text>
|
||||
<Text className='alarm-history-type'>{getAlarmTypeLabel(alarm.source_msg_id)}</Text>
|
||||
</View>
|
||||
<View className='alarm-history-meta'>
|
||||
<Text className='alarm-history-child'>
|
||||
来源儿童:{getAlarmChildLabel(alarm, childName)}
|
||||
</Text>
|
||||
<Text className='alarm-history-device'>{alarm.device_id}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<View className='alarm-history-empty'>
|
||||
<Text className='alarm-history-empty-text'>还没有更早的告警记录。</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='alarm-empty'>
|
||||
<Text className='alarm-empty-text'>设备触发长按告警后,会在这里显示最近一条记录。</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className='fold-card'>
|
||||
<View className='fold-header' onClick={handleToggleDetail}>
|
||||
<View className='fold-heading'>
|
||||
@@ -329,6 +496,7 @@ export default function Device() {
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{systemBanner}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user