前端接入设备系统更新
This commit is contained in:
@@ -194,6 +194,134 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-update-card {
|
||||||
|
margin: 0 24px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
background: #F0FDF4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-heading {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #14532D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #15803D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 132px;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #16A34A;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: #CBD5E1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-action-text {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-body {
|
||||||
|
padding: 6px 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 18px 0;
|
||||||
|
border-bottom: 1px solid #F3F4F6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 26px;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-value {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #1F2937;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
color: #15803D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-progress-track {
|
||||||
|
flex: 1;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #E5E7EB;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #16A34A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-update-progress-text {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 72px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
color: #15803D;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-item.disabled {
|
.menu-item.disabled {
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
unbindDevice,
|
unbindDevice,
|
||||||
} from '@/services/binding'
|
} from '@/services/binding'
|
||||||
import { Child, clearSelectedChildId, createChild, setSelectedChildId, updateChild } from '@/services/child'
|
import { Child, clearSelectedChildId, createChild, setSelectedChildId, updateChild } from '@/services/child'
|
||||||
|
import { DeviceFirmwareStatus, getDeviceFirmwareStatus, startDeviceFirmwareUpdate } from '@/services/device'
|
||||||
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
|
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -22,6 +23,17 @@ interface MenuItem {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFirmwareStatusLabel(status?: string | null): string {
|
||||||
|
const normalized = String(status || '').trim()
|
||||||
|
if (!normalized || normalized === 'idle') return '未更新'
|
||||||
|
if (normalized === 'sent') return '指令已发送'
|
||||||
|
if (normalized === 'accepted') return '设备已接收'
|
||||||
|
if (normalized === 'updating') return '更新中'
|
||||||
|
if (normalized === 'success' || normalized === 'completed') return '已更新'
|
||||||
|
if (normalized === 'failed') return '更新失败'
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
export default function Sleep() {
|
export default function Sleep() {
|
||||||
const systemBanner = useSystemBanner()
|
const systemBanner = useSystemBanner()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -29,6 +41,9 @@ export default function Sleep() {
|
|||||||
const [bindings, setBindings] = useState<BindingListItem[]>([])
|
const [bindings, setBindings] = useState<BindingListItem[]>([])
|
||||||
const [binding, setBinding] = useState<BindingListItem | null>(null)
|
const [binding, setBinding] = useState<BindingListItem | null>(null)
|
||||||
const [currentChild, setCurrentChild] = useState<Child | null>(null)
|
const [currentChild, setCurrentChild] = useState<Child | null>(null)
|
||||||
|
const [firmwareStatus, setFirmwareStatus] = useState<DeviceFirmwareStatus | null>(null)
|
||||||
|
const [isLoadingFirmware, setIsLoadingFirmware] = useState(false)
|
||||||
|
const [isUpdatingFirmware, setIsUpdatingFirmware] = useState(false)
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [showChildModal, setShowChildModal] = useState(false)
|
const [showChildModal, setShowChildModal] = useState(false)
|
||||||
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
|
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
|
||||||
@@ -49,11 +64,13 @@ export default function Sleep() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const context = await loadCurrentChildBindingContext()
|
const context = await loadCurrentChildBindingContext()
|
||||||
|
const nextBinding = (context.currentBinding as BindingListItem | null) || null
|
||||||
setChildren(context.children)
|
setChildren(context.children)
|
||||||
setBindings(context.bindings as BindingListItem[])
|
setBindings(context.bindings as BindingListItem[])
|
||||||
setCurrentChild(context.currentChild)
|
setCurrentChild(context.currentChild)
|
||||||
setBinding((context.currentBinding as BindingListItem | null) || null)
|
setBinding(nextBinding)
|
||||||
setParentInfo(Taro.getStorageSync('userInfo') || {})
|
setParentInfo(Taro.getStorageSync('userInfo') || {})
|
||||||
|
void loadFirmwareStatus(nextBinding?.device_id)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[manage] load failed:', error)
|
console.error('[manage] load failed:', error)
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -67,6 +84,30 @@ export default function Sleep() {
|
|||||||
|
|
||||||
const currentChildName = currentChild?.child_name || '未设置'
|
const currentChildName = currentChild?.child_name || '未设置'
|
||||||
|
|
||||||
|
const loadFirmwareStatus = async (deviceId?: string | null) => {
|
||||||
|
const normalizedDeviceId = String(deviceId || '').trim()
|
||||||
|
if (!normalizedDeviceId) {
|
||||||
|
setFirmwareStatus(null)
|
||||||
|
setIsLoadingFirmware(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoadingFirmware(true)
|
||||||
|
try {
|
||||||
|
const nextFirmwareStatus = await getDeviceFirmwareStatus(normalizedDeviceId)
|
||||||
|
setFirmwareStatus(nextFirmwareStatus)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[manage] firmware load failed:', error)
|
||||||
|
setFirmwareStatus(null)
|
||||||
|
Taro.showToast({
|
||||||
|
title: error?.message || '系统更新状态加载失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoadingFirmware(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleOpenModal = (type: 'add' | 'edit', child?: Child) => {
|
const handleOpenModal = (type: 'add' | 'edit', child?: Child) => {
|
||||||
setModalType(type)
|
setModalType(type)
|
||||||
setChildName(child?.child_name || '')
|
setChildName(child?.child_name || '')
|
||||||
@@ -90,6 +131,7 @@ export default function Sleep() {
|
|||||||
}
|
}
|
||||||
setCurrentChild(child)
|
setCurrentChild(child)
|
||||||
setBinding(nextBinding)
|
setBinding(nextBinding)
|
||||||
|
void loadFirmwareStatus(nextBinding?.device_id)
|
||||||
setShowChildModal(false)
|
setShowChildModal(false)
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '已切换当前孩子',
|
title: '已切换当前孩子',
|
||||||
@@ -130,6 +172,37 @@ export default function Sleep() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStartFirmwareUpdate = () => {
|
||||||
|
if (!binding?.device_id || !firmwareStatus?.can_update || isLoadingFirmware || isUpdatingFirmware) return
|
||||||
|
|
||||||
|
Taro.showModal({
|
||||||
|
title: '系统更新',
|
||||||
|
content: `确定要更新当前设备 ${binding.device_id} 吗?`,
|
||||||
|
confirmText: '更新',
|
||||||
|
confirmColor: '#16A34A',
|
||||||
|
success: async (result) => {
|
||||||
|
if (!result.confirm || !binding?.device_id) return
|
||||||
|
|
||||||
|
setIsUpdatingFirmware(true)
|
||||||
|
try {
|
||||||
|
const nextFirmwareStatus = await startDeviceFirmwareUpdate(binding.device_id)
|
||||||
|
setFirmwareStatus(nextFirmwareStatus)
|
||||||
|
Taro.showToast({
|
||||||
|
title: '更新指令已发送',
|
||||||
|
icon: 'success',
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: error?.message || '更新失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsUpdatingFirmware(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleUnbind = () => {
|
const handleUnbind = () => {
|
||||||
if (!binding) return
|
if (!binding) return
|
||||||
|
|
||||||
@@ -223,6 +296,24 @@ export default function Sleep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parentDisplayName = parentInfo.nickname?.trim() || '家长'
|
const parentDisplayName = parentInfo.nickname?.trim() || '家长'
|
||||||
|
const currentFirmwareLabel = firmwareStatus?.current_version || '--'
|
||||||
|
const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
|
||||||
|
const firmwareStatusLabel = binding?.device_id
|
||||||
|
? isLoadingFirmware
|
||||||
|
? '查询中'
|
||||||
|
: getFirmwareStatusLabel(firmwareStatus?.update_status)
|
||||||
|
: '未绑定设备'
|
||||||
|
const firmwareProgress = Math.max(0, Math.min(100, Math.round(Number(firmwareStatus?.progress || 0))))
|
||||||
|
const showFirmwareProgress = ['sent', 'accepted', 'updating'].includes(String(firmwareStatus?.update_status || ''))
|
||||||
|
const canUpdateFirmware = Boolean(binding?.device_id && firmwareStatus?.can_update && !isLoadingFirmware && !isUpdatingFirmware)
|
||||||
|
const firmwareActionText = isLoadingFirmware ? '查询中' : isUpdatingFirmware ? '发送中' : '更新'
|
||||||
|
const firmwareSubtitle = !binding?.device_id
|
||||||
|
? '绑定设备后可查看系统版本'
|
||||||
|
: isLoadingFirmware
|
||||||
|
? '正在查询当前设备版本'
|
||||||
|
: firmwareStatus?.update_available
|
||||||
|
? '发现可更新版本'
|
||||||
|
: '当前设备版本状态'
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||||||
@@ -299,6 +390,47 @@ export default function Sleep() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View className={`system-update-card ${binding?.device_id ? '' : 'disabled'}`}>
|
||||||
|
<View className='system-update-header'>
|
||||||
|
<View className='system-update-heading'>
|
||||||
|
<Text className='system-update-title'>系统更新</Text>
|
||||||
|
<Text className='system-update-subtitle'>{firmwareSubtitle}</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`system-update-action ${canUpdateFirmware ? '' : 'disabled'}`}
|
||||||
|
onClick={canUpdateFirmware ? handleStartFirmwareUpdate : undefined}
|
||||||
|
>
|
||||||
|
<Text className='system-update-action-text'>{firmwareActionText}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className='system-update-body'>
|
||||||
|
<View className='system-update-row'>
|
||||||
|
<Text className='system-update-label'>当前设备</Text>
|
||||||
|
<Text className='system-update-value'>{binding?.device_id || '未绑定'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='system-update-row'>
|
||||||
|
<Text className='system-update-label'>当前版本</Text>
|
||||||
|
<Text className='system-update-value'>{currentFirmwareLabel}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='system-update-row'>
|
||||||
|
<Text className='system-update-label'>最新版本</Text>
|
||||||
|
<Text className='system-update-value highlight'>{latestFirmwareLabel}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='system-update-row'>
|
||||||
|
<Text className='system-update-label'>更新状态</Text>
|
||||||
|
<Text className='system-update-value'>{firmwareStatusLabel}</Text>
|
||||||
|
</View>
|
||||||
|
{showFirmwareProgress && (
|
||||||
|
<View className='system-update-progress'>
|
||||||
|
<View className='system-update-progress-track'>
|
||||||
|
<View className='system-update-progress-bar' style={{ width: `${firmwareProgress}%` }}></View>
|
||||||
|
</View>
|
||||||
|
<Text className='system-update-progress-text'>{firmwareProgress}%</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View className='menu-card'>
|
<View className='menu-card'>
|
||||||
{menuItems.map((item, index) => (
|
{menuItems.map((item, index) => (
|
||||||
<View key={index}>
|
<View key={index}>
|
||||||
|
|||||||
@@ -63,6 +63,22 @@ export interface DeviceRemoteSleepWakeResponse {
|
|||||||
msg_id: string
|
msg_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceFirmwareStatus {
|
||||||
|
device_id: string
|
||||||
|
current_version?: string | null
|
||||||
|
latest_version?: string | null
|
||||||
|
update_available: boolean
|
||||||
|
can_update: boolean
|
||||||
|
update_status: string
|
||||||
|
progress: number
|
||||||
|
target_version?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceFirmwareUpdateResponse extends DeviceFirmwareStatus {
|
||||||
|
msg_id: string
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTimeValue(value?: string | null): string | null {
|
function normalizeTimeValue(value?: string | null): string | null {
|
||||||
if (value === null || value === undefined) return null
|
if (value === null || value === undefined) return null
|
||||||
const trimmed = String(value).trim()
|
const trimmed = String(value).trim()
|
||||||
@@ -153,6 +169,29 @@ export async function setDeviceRemoteSleepWake(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDeviceFirmwareStatus(deviceId?: string): Promise<DeviceFirmwareStatus | null> {
|
||||||
|
const resolvedDeviceId = await resolveDeviceId(deviceId)
|
||||||
|
if (!resolvedDeviceId) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await request<DeviceFirmwareStatus>(`/banban/devices/${resolvedDeviceId}/firmware`)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.status === 404) return null
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startDeviceFirmwareUpdate(deviceId?: string): Promise<DeviceFirmwareUpdateResponse> {
|
||||||
|
const resolvedDeviceId = await resolveDeviceId(deviceId)
|
||||||
|
if (!resolvedDeviceId) {
|
||||||
|
throw new Error('当前没有可用设备')
|
||||||
|
}
|
||||||
|
|
||||||
|
return request<DeviceFirmwareUpdateResponse>(`/banban/devices/${resolvedDeviceId}/firmware/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDeviceAlarms(
|
export async function getDeviceAlarms(
|
||||||
deviceId?: string,
|
deviceId?: string,
|
||||||
limit = 20
|
limit = 20
|
||||||
|
|||||||
Reference in New Issue
Block a user