671 lines
27 KiB
TypeScript
671 lines
27 KiB
TypeScript
import { getPageStyle } from '@/utils/page-style'
|
|
import { formatBatteryDisplay, getBatteryLevel } from '@/utils/battery'
|
|
import { setActiveTabRoute } from '@/utils/tab-bar'
|
|
import { View, Text, Slider, Image } from '@tarojs/components'
|
|
import { useRef, useState } from 'react'
|
|
import Taro, { useDidShow } from '@tarojs/taro'
|
|
import { getToken } from '@/services/auth'
|
|
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
|
|
import { Binding, loadCurrentChildBindingContext } from '@/services/binding'
|
|
import { Child } from '@/services/child'
|
|
import { DeviceAlarmItem, DeviceStatus, getDeviceAlarms, getDeviceStatus, getDeviceVolumeCommandStatus, setDeviceRemoteSleepWake, setDeviceVolume } from '@/services/device'
|
|
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
|
|
import { usePageShare } from '@/utils/page-share'
|
|
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 '弱'
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms)
|
|
})
|
|
}
|
|
|
|
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 goLoginWithRedirect(url = '/pages/device/index') {
|
|
Taro.setStorageSync('postLoginRedirect', url)
|
|
Taro.navigateTo({ url: '/pages/login/index' })
|
|
}
|
|
|
|
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 '未关联宝贝'
|
|
}
|
|
|
|
function formatAlarmCoordinates(alarm: DeviceAlarmItem | null): string {
|
|
if (!alarm || alarm.lat === null || alarm.lat === undefined || alarm.lng === null || alarm.lng === undefined) {
|
|
return ''
|
|
}
|
|
return `${Number(alarm.lat).toFixed(6)}, ${Number(alarm.lng).toFixed(6)}`
|
|
}
|
|
|
|
function getAlarmLocationLabel(alarm: DeviceAlarmItem | null): string {
|
|
const address = String(alarm?.address || '').trim()
|
|
if (address) return address
|
|
|
|
const coordinates = formatAlarmCoordinates(alarm)
|
|
if (coordinates) return coordinates
|
|
|
|
return '暂无告警位置'
|
|
}
|
|
|
|
function getAlarmLocationHint(alarm: DeviceAlarmItem | null): string {
|
|
if (!alarm?.location_updated_at) return '设备还没有可关联的位置上报'
|
|
const prefix = `位置更新于 ${formatAlarmTime(alarm.location_updated_at)}`
|
|
if (alarm.location_stale) return `${prefix},可能不是告警发生时的位置`
|
|
if (!String(alarm.address || '').trim() && alarm.address_resolve_status === 0) return `${prefix},地址解析中`
|
|
if (!String(alarm.address || '').trim() && alarm.address_resolve_status === 2) return `${prefix},地址解析失败`
|
|
return prefix
|
|
}
|
|
|
|
export default function Device() {
|
|
usePageShare()
|
|
const systemBanner = useSystemBanner()
|
|
const [binding, setBinding] = useState<Binding | null>(null)
|
|
const [pendingBinding, setPendingBinding] = 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 [isDetailExpanded, setIsDetailExpanded] = useState(false)
|
|
const [isSavingVolume, setIsSavingVolume] = useState(false)
|
|
const [sleepWakePending, setSleepWakePending] = useState<'on' | 'off' | null>(null)
|
|
const [volumeValue, setVolumeValue] = useState(0)
|
|
const [isGuest, setIsGuest] = useState(false)
|
|
const volumeCommandInFlightRef = useRef(false)
|
|
|
|
useDidShow(() => {
|
|
setActiveTabRoute('pages/device/index')
|
|
void loadDeviceInfo()
|
|
})
|
|
|
|
const loadDeviceInfo = async () => {
|
|
if (!getToken()) {
|
|
setBinding(null)
|
|
setPendingBinding(null)
|
|
setChild(null)
|
|
setDeviceStatus(null)
|
|
setDeviceAlarms([])
|
|
setVolumeValue(0)
|
|
setIsGuest(true)
|
|
setIsLoading(false)
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
setIsGuest(false)
|
|
try {
|
|
const context = await loadCurrentChildBindingContext()
|
|
setChild(context.currentChild)
|
|
setBinding(context.currentBinding)
|
|
setPendingBinding(context.pendingBinding)
|
|
|
|
if (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) {
|
|
console.error('[device] load failed:', error)
|
|
Taro.showToast({
|
|
title: error?.message || '加载失败,请稍后重试',
|
|
icon: 'none',
|
|
})
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleOpenSleepSchedule = () => {
|
|
Taro.navigateTo({ url: '/pages/sleep-schedule/index' })
|
|
}
|
|
|
|
const handleToggleDetail = () => {
|
|
setIsDetailExpanded((current) => !current)
|
|
}
|
|
|
|
const handleVolumeChanging = (e: any) => {
|
|
if (volumeCommandInFlightRef.current) return
|
|
setVolumeValue(Number(e.detail?.value || 0))
|
|
}
|
|
|
|
const handleVolumeCommit = async (e: any) => {
|
|
if (!binding?.device_id) return
|
|
if (volumeCommandInFlightRef.current) return
|
|
const nextLevel = Number(e.detail?.value || 0)
|
|
const fallbackLevel = deviceStatus?.volume ?? volumeValue ?? 0
|
|
volumeCommandInFlightRef.current = true
|
|
setVolumeValue(nextLevel)
|
|
setIsSavingVolume(true)
|
|
|
|
try {
|
|
const command = await setDeviceVolume(nextLevel, binding.device_id)
|
|
let completedLevel: number | null = null
|
|
let failedMessage = ''
|
|
for (let attempt = 0; attempt < 13; attempt += 1) {
|
|
await sleep(800)
|
|
const status = await getDeviceVolumeCommandStatus(binding.device_id, command.time)
|
|
if (status.status === 'completed') {
|
|
completedLevel = status.current_level ?? nextLevel
|
|
break
|
|
}
|
|
if (status.status === 'failed' || status.status === 'timeout') {
|
|
failedMessage = status.error || (status.status === 'timeout' ? '设备未确认音量调整,请稍后查看设备状态' : '设备音量调整失败')
|
|
break
|
|
}
|
|
}
|
|
if (completedLevel === null) {
|
|
throw new Error(failedMessage || '设备未确认音量调整,请稍后查看设备状态')
|
|
}
|
|
setVolumeValue(completedLevel)
|
|
setDeviceStatus((current) => (current ? { ...current, volume: completedLevel } : current))
|
|
Taro.showToast({
|
|
title: '音量已成功调整',
|
|
icon: 'success',
|
|
})
|
|
} catch (error: any) {
|
|
setVolumeValue(deviceStatus?.volume ?? fallbackLevel)
|
|
const message = error?.message === DEVICE_UNAVAILABLE_MESSAGE
|
|
? '设备不在线或处于休眠中,暂时无法调节音量'
|
|
: error?.message || '音量设置失败'
|
|
Taro.showToast({
|
|
title: message,
|
|
icon: 'none',
|
|
})
|
|
} finally {
|
|
volumeCommandInFlightRef.current = false
|
|
setIsSavingVolume(false)
|
|
}
|
|
}
|
|
|
|
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' style={getPageStyle(true)}>
|
|
<View className='loading'>
|
|
<Text>加载中...</Text>
|
|
</View>
|
|
{systemBanner}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
if (isGuest) {
|
|
return (
|
|
<View className='device-page' style={getPageStyle(true)}>
|
|
<View className='guest-hero'>
|
|
<View className='guest-device-icon'>
|
|
<Image className='guest-mascot' src={require('../../assets/ui/dog-happy.png')} mode='aspectFit' />
|
|
</View>
|
|
<Text className='guest-title'>先认识一下伴伴</Text>
|
|
<Text className='guest-subtitle'>无需登录即可浏览主要能力,绑定设备时再完成登录。</Text>
|
|
</View>
|
|
|
|
<View className='guest-feature-grid'>
|
|
<View className='guest-feature-card'>
|
|
<Image className='guest-feature-icon' src={require('../../assets/tab-icons/device-icon.png')} mode='aspectFit' />
|
|
<Text className='guest-feature-title'>设备状态</Text>
|
|
<Text className='guest-feature-desc'>查看电量、信号、版本和告警记录。</Text>
|
|
</View>
|
|
<View className='guest-feature-card'>
|
|
<Image className='guest-feature-icon' src={require('../../assets/tab-icons/location-icon.png')} mode='aspectFit' />
|
|
<Text className='guest-feature-title'>定位展示</Text>
|
|
<Text className='guest-feature-desc'>绑定后可查看设备当前位置。</Text>
|
|
</View>
|
|
<View className='guest-feature-card'>
|
|
<Image className='guest-feature-icon' src={require('../../assets/tab-icons/chat-icon.png')} mode='aspectFit' />
|
|
<Text className='guest-feature-title'>亲子互动</Text>
|
|
<Text className='guest-feature-desc'>查看宝贝和设备的消息会话。</Text>
|
|
</View>
|
|
<View className='guest-feature-card'>
|
|
<Image className='guest-feature-icon' src={require('../../assets/tab-icons/manage-icon.png')} mode='aspectFit' />
|
|
<Text className='guest-feature-title'>设备管理</Text>
|
|
<Text className='guest-feature-desc'>管理宝贝资料、绑定设备和休眠设置。</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className='guest-action-card'>
|
|
<Text className='guest-action-title'>准备绑定或管理设备</Text>
|
|
<Text className='guest-action-desc'>登录后可绑定设备,并在绑定流程中创建或选择宝贝资料。</Text>
|
|
<View className='guest-primary-btn' onClick={() => goLoginWithRedirect('/pages/device/index')}>
|
|
<Text className='guest-primary-btn-text'>去登录 / 绑定设备</Text>
|
|
</View>
|
|
</View>
|
|
<Image
|
|
className='guest-footer-art'
|
|
src={require('../../assets/ui/paw-dog-footer.png')}
|
|
mode='aspectFit'
|
|
/>
|
|
{systemBanner}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
if (!child) {
|
|
const hasPendingDevice = Boolean(pendingBinding?.device_id)
|
|
return (
|
|
<View className='device-page' style={getPageStyle(true)}>
|
|
<View className='page-header'>
|
|
<Text className='page-title'>设备首页</Text>
|
|
</View>
|
|
|
|
<View className='empty-card'>
|
|
<View className='empty-robot'>
|
|
<Image className='empty-mascot' src={require('../../assets/ui/dog-happy.png')} mode='aspectFit' />
|
|
</View>
|
|
<Text className='empty-title'>{hasPendingDevice ? '设备已绑定,待关联宝贝' : '先绑定一台伴伴设备'}</Text>
|
|
<Text className='empty-subtitle'>
|
|
{hasPendingDevice
|
|
? '这台设备还没有关联宝贝,关联后即可查看定位、聊天和设备状态'
|
|
: '绑定时可以选择已有宝贝,或直接新建宝贝姓名'}
|
|
</Text>
|
|
<View className='empty-btn' onClick={() => Taro.navigateTo({ url: hasPendingDevice ? '/pages/bind/index?mode=assign' : '/pages/bind/index' })}>
|
|
<Text className='empty-btn-text'>{hasPendingDevice ? '关联宝贝' : '添加设备'}</Text>
|
|
</View>
|
|
</View>
|
|
<Image className='guest-footer-art' src={require('../../assets/ui/paw-dog-footer.png')} mode='aspectFit' />
|
|
{systemBanner}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
if (!binding) {
|
|
const hasPendingDevice = Boolean(pendingBinding?.device_id)
|
|
return (
|
|
<View className='device-page' style={getPageStyle(true)}>
|
|
<View className='page-header'>
|
|
<Text className='page-title'>{child.child_name} 的伴伴</Text>
|
|
</View>
|
|
|
|
<View className='child-focus-card'>
|
|
<View className='child-focus-avatar'>
|
|
<Text className='child-focus-icon'>🧒</Text>
|
|
</View>
|
|
<View className='child-focus-info'>
|
|
<Text className='child-focus-name'>{child.child_name}</Text>
|
|
<Text className='child-focus-desc'>当前宝贝已选中,还没有绑定设备</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className='empty-card'>
|
|
<View className='empty-robot'>
|
|
<Image className='empty-mascot' src={require('../../assets/ui/dog-happy.png')} mode='aspectFit' />
|
|
</View>
|
|
<Text className='empty-title'>{hasPendingDevice ? '有设备待关联' : '给当前宝贝绑定一台设备'}</Text>
|
|
<Text className='empty-subtitle'>
|
|
{hasPendingDevice
|
|
? `设备 ${pendingBinding?.device_id} 还没有关联宝贝,可以关联到当前宝贝`
|
|
: '绑定后,这个宝贝的聊天记录、定位和设备状态都会显示在首页'}
|
|
</Text>
|
|
<View className='empty-btn' onClick={() => Taro.navigateTo({ url: hasPendingDevice ? '/pages/bind/index?mode=assign' : '/pages/bind/index' })}>
|
|
<Text className='empty-btn-text'>{hasPendingDevice ? '关联到当前宝贝' : '去绑定设备'}</Text>
|
|
</View>
|
|
</View>
|
|
<Image className='guest-footer-art' src={require('../../assets/ui/paw-dog-footer.png')} mode='aspectFit' />
|
|
{systemBanner}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const childName = child.child_name || '未设置'
|
|
const pageTitle = `${childName} 的伴伴`
|
|
const batteryValue = deviceStatus?.power ?? deviceStatus?.battery_pct ?? null
|
|
const batteryDisplay = formatBatteryDisplay(batteryValue)
|
|
const batteryLevel = getBatteryLevel(batteryValue)
|
|
const statusMascotSrc = {
|
|
unknown: require('../../assets/ui/dog-happy.png'),
|
|
full: require('../../assets/ui/dog-happy.png'),
|
|
high: require('../../assets/ui/dog-sparkle.png'),
|
|
medium: require('../../assets/ui/dog-lying.png'),
|
|
low: require('../../assets/ui/dog-question.png'),
|
|
}[batteryLevel]
|
|
const signalLabel = getSignalLabel(deviceStatus?.signal)
|
|
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' style={getPageStyle(true)}>
|
|
<View className='page-header bound-device-header'>
|
|
<Text className='page-title'>{pageTitle}</Text>
|
|
</View>
|
|
|
|
<View className={`status-card battery-${batteryLevel}`}>
|
|
<View className='status-badge'>
|
|
<View className='status-dot'></View>
|
|
<Text className='status-text'>已完成绑定</Text>
|
|
</View>
|
|
<View className='battery-section'>
|
|
<Text className='battery-percent'>
|
|
{batteryDisplay.text}
|
|
{batteryDisplay.showPercent && <Text className='percent'>%</Text>}
|
|
</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'>
|
|
<Image className='device-mascot' src={statusMascotSrc} mode='aspectFit' />
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
<View className='section-title'>设备控制</View>
|
|
<View className='control-card'>
|
|
<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/ui/menu-sleep.png')} mode='aspectFit' />
|
|
</View>
|
|
<View className='control-info'>
|
|
<Text className='control-name'>定时休眠</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/ui/menu-sleep.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>
|
|
</View>
|
|
|
|
<View className='divider'></View>
|
|
|
|
<View className='control-item volume-item'>
|
|
<View className='volume-header'>
|
|
<View className='control-left'>
|
|
<View className='icon-bg orange'>
|
|
<Image
|
|
className='control-icon-img'
|
|
src={require('../../assets/tab-icons/orange-speaker.png')}
|
|
mode='aspectFit'
|
|
/>
|
|
</View>
|
|
<View className='control-info'>
|
|
<Text className='control-name'>外放音量</Text>
|
|
<Text className='control-detail'>{isSavingVolume ? '调整中...' : `当前 ${volumeLabel}`}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
<View className='volume-slider'>
|
|
<Image
|
|
className='volume-icon'
|
|
src={require('../../assets/tab-icons/unspeaker.png')}
|
|
mode='aspectFit'
|
|
/>
|
|
<Slider
|
|
className='slider'
|
|
min={0}
|
|
max={100}
|
|
value={volumeValue}
|
|
disabled={isSavingVolume}
|
|
onChanging={handleVolumeChanging}
|
|
onChange={handleVolumeCommit}
|
|
activeColor='#FF8C42'
|
|
backgroundColor='#E5E5E5'
|
|
blockColor='#FF8C42'
|
|
/>
|
|
<Image
|
|
className='volume-icon'
|
|
src={require('../../assets/tab-icons/orange-speaker.png')}
|
|
mode='aspectFit'
|
|
/>
|
|
</View>
|
|
</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 className='alarm-row alarm-row-vertical'>
|
|
<Text className='alarm-label'>告警位置</Text>
|
|
<View className='alarm-location-value'>
|
|
<Text className='alarm-location-main'>{getAlarmLocationLabel(latestAlarm)}</Text>
|
|
<Text className='alarm-location-hint'>{getAlarmLocationHint(latestAlarm)}</Text>
|
|
</View>
|
|
</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>
|
|
<Text className='alarm-history-location'>{getAlarmLocationLabel(alarm)}</Text>
|
|
{alarm.location_stale ? (
|
|
<Text className='alarm-history-location-hint'>位置可能不是告警发生时的位置</Text>
|
|
) : null}
|
|
</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'>
|
|
<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 detail-row-highlight'>
|
|
<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?.location_updated_at)}</Text>
|
|
</View>
|
|
<View className='detail-row'>
|
|
<Text className='detail-label'>服务端时间</Text>
|
|
<Text className='detail-value'>{formatTime(deviceStatus?.server_time)}</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'>设备 ID</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'>{formatTime(binding.bound_at)}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
{systemBanner}
|
|
</View>
|
|
)
|
|
}
|