前端接入设备系统更新

This commit is contained in:
stu2not
2026-05-08 16:56:39 +08:00
parent 9f38965a79
commit 9a42db11cb
3 changed files with 300 additions and 1 deletions

View File

@@ -194,6 +194,134 @@
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 {
opacity: 0.45;
}

View File

@@ -10,6 +10,7 @@ import {
unbindDevice,
} from '@/services/binding'
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 './index.scss'
@@ -22,6 +23,17 @@ interface MenuItem {
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() {
const systemBanner = useSystemBanner()
const [loading, setLoading] = useState(true)
@@ -29,6 +41,9 @@ export default function Sleep() {
const [bindings, setBindings] = useState<BindingListItem[]>([])
const [binding, setBinding] = useState<BindingListItem | 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 [showChildModal, setShowChildModal] = useState(false)
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
@@ -49,11 +64,13 @@ export default function Sleep() {
setLoading(true)
try {
const context = await loadCurrentChildBindingContext()
const nextBinding = (context.currentBinding as BindingListItem | null) || null
setChildren(context.children)
setBindings(context.bindings as BindingListItem[])
setCurrentChild(context.currentChild)
setBinding((context.currentBinding as BindingListItem | null) || null)
setBinding(nextBinding)
setParentInfo(Taro.getStorageSync('userInfo') || {})
void loadFirmwareStatus(nextBinding?.device_id)
} catch (error: any) {
console.error('[manage] load failed:', error)
Taro.showToast({
@@ -67,6 +84,30 @@ export default function Sleep() {
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) => {
setModalType(type)
setChildName(child?.child_name || '')
@@ -90,6 +131,7 @@ export default function Sleep() {
}
setCurrentChild(child)
setBinding(nextBinding)
void loadFirmwareStatus(nextBinding?.device_id)
setShowChildModal(false)
Taro.showToast({
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 = () => {
if (!binding) return
@@ -223,6 +296,24 @@ export default function Sleep() {
}
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[] = [
{
icon: require('../../assets/tab-icons/orange-robot.png'),
@@ -299,6 +390,47 @@ export default function Sleep() {
</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'>
{menuItems.map((item, index) => (
<View key={index}>

View File

@@ -63,6 +63,22 @@ export interface DeviceRemoteSleepWakeResponse {
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 {
if (value === null || value === undefined) return null
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(
deviceId?: string,
limit = 20