diff --git a/banban-mini/src/pages/sleep/index.scss b/banban-mini/src/pages/sleep/index.scss index 50d5fa8..abc226b 100644 --- a/banban-mini/src/pages/sleep/index.scss +++ b/banban-mini/src/pages/sleep/index.scss @@ -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; } diff --git a/banban-mini/src/pages/sleep/index.tsx b/banban-mini/src/pages/sleep/index.tsx index a40d7b6..07edc8b 100644 --- a/banban-mini/src/pages/sleep/index.tsx +++ b/banban-mini/src/pages/sleep/index.tsx @@ -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([]) const [binding, setBinding] = useState(null) const [currentChild, setCurrentChild] = useState(null) + const [firmwareStatus, setFirmwareStatus] = useState(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() { + + + + 系统更新 + {firmwareSubtitle} + + + {firmwareActionText} + + + + + 当前设备 + {binding?.device_id || '未绑定'} + + + 当前版本 + {currentFirmwareLabel} + + + 最新版本 + {latestFirmwareLabel} + + + 更新状态 + {firmwareStatusLabel} + + {showFirmwareProgress && ( + + + + + {firmwareProgress}% + + )} + + + {menuItems.map((item, index) => ( diff --git a/banban-mini/src/services/device.ts b/banban-mini/src/services/device.ts index e4cb29c..ff78d1e 100644 --- a/banban-mini/src/services/device.ts +++ b/banban-mini/src/services/device.ts @@ -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 { + const resolvedDeviceId = await resolveDeviceId(deviceId) + if (!resolvedDeviceId) return null + + try { + return await request(`/banban/devices/${resolvedDeviceId}/firmware`) + } catch (error: any) { + if (error?.status === 404) return null + throw error + } +} + +export async function startDeviceFirmwareUpdate(deviceId?: string): Promise { + const resolvedDeviceId = await resolveDeviceId(deviceId) + if (!resolvedDeviceId) { + throw new Error('当前没有可用设备') + } + + return request(`/banban/devices/${resolvedDeviceId}/firmware/update`, { + method: 'POST', + }) +} + export async function getDeviceAlarms( deviceId?: string, limit = 20