diff --git a/banban-mini/src/assets/tab-icons/chat-icon.png b/banban-mini/src/assets/tab-icons/chat-icon.png index 78e23cb..cd6b4db 100644 Binary files a/banban-mini/src/assets/tab-icons/chat-icon.png and b/banban-mini/src/assets/tab-icons/chat-icon.png differ diff --git a/banban-mini/src/assets/tab-icons/chat-selected.png b/banban-mini/src/assets/tab-icons/chat-selected.png index 64a1a60..c0e8416 100644 Binary files a/banban-mini/src/assets/tab-icons/chat-selected.png and b/banban-mini/src/assets/tab-icons/chat-selected.png differ diff --git a/banban-mini/src/assets/tab-icons/device-icon.png b/banban-mini/src/assets/tab-icons/device-icon.png index 9dbbd0e..2c96ff0 100644 Binary files a/banban-mini/src/assets/tab-icons/device-icon.png and b/banban-mini/src/assets/tab-icons/device-icon.png differ diff --git a/banban-mini/src/assets/tab-icons/device-selected.png b/banban-mini/src/assets/tab-icons/device-selected.png index da0b495..752546c 100644 Binary files a/banban-mini/src/assets/tab-icons/device-selected.png and b/banban-mini/src/assets/tab-icons/device-selected.png differ diff --git a/banban-mini/src/assets/tab-icons/location-icon.png b/banban-mini/src/assets/tab-icons/location-icon.png index 8d4a559..ab03c21 100644 Binary files a/banban-mini/src/assets/tab-icons/location-icon.png and b/banban-mini/src/assets/tab-icons/location-icon.png differ diff --git a/banban-mini/src/assets/tab-icons/location-selected.png b/banban-mini/src/assets/tab-icons/location-selected.png index cfb6c34..60e4f35 100644 Binary files a/banban-mini/src/assets/tab-icons/location-selected.png and b/banban-mini/src/assets/tab-icons/location-selected.png differ diff --git a/banban-mini/src/assets/tab-icons/manage-icon.png b/banban-mini/src/assets/tab-icons/manage-icon.png index 4fdd3ff..91f3091 100644 Binary files a/banban-mini/src/assets/tab-icons/manage-icon.png and b/banban-mini/src/assets/tab-icons/manage-icon.png differ diff --git a/banban-mini/src/assets/tab-icons/manage-selected.png b/banban-mini/src/assets/tab-icons/manage-selected.png index 6fe232b..dda9854 100644 Binary files a/banban-mini/src/assets/tab-icons/manage-selected.png and b/banban-mini/src/assets/tab-icons/manage-selected.png differ diff --git a/banban-mini/src/assets/ui/avatar-parent.png b/banban-mini/src/assets/ui/avatar-parent.png new file mode 100644 index 0000000..df07e60 Binary files /dev/null and b/banban-mini/src/assets/ui/avatar-parent.png differ diff --git a/banban-mini/src/assets/ui/heading-chat.png b/banban-mini/src/assets/ui/heading-chat.png new file mode 100644 index 0000000..7ceed64 Binary files /dev/null and b/banban-mini/src/assets/ui/heading-chat.png differ diff --git a/banban-mini/src/assets/ui/heading-location.png b/banban-mini/src/assets/ui/heading-location.png new file mode 100644 index 0000000..9186c72 Binary files /dev/null and b/banban-mini/src/assets/ui/heading-location.png differ diff --git a/banban-mini/src/assets/ui/heading-manage.png b/banban-mini/src/assets/ui/heading-manage.png new file mode 100644 index 0000000..bcdc9e7 Binary files /dev/null and b/banban-mini/src/assets/ui/heading-manage.png differ diff --git a/banban-mini/src/components/firmware-progress/index.tsx b/banban-mini/src/components/firmware-progress/index.tsx index 3b3b9e7..9bdd48d 100644 --- a/banban-mini/src/components/firmware-progress/index.tsx +++ b/banban-mini/src/components/firmware-progress/index.tsx @@ -16,20 +16,20 @@ export default function FirmwareProgress({ status, refreshError, onClose, onRetr {view.title} - {view.version && 本次更新版本 {view.version}} + {view.version && {view.current ? '当前版本' : '本次更新版本'} {view.version}} {view.message} - + {!view.current && - - + } + {!view.current && {refreshError ? '上次更新进度' : '更新进度'} {view.hasProgress ? `${view.percent}%` : '等待设备上报'} - - {!view.complete && !view.failed && 处理中……} + } + {!view.complete && !view.failed && !view.current && 处理中……} {view.complete ? '完成' : '关闭'} {view.failed && onRetry && 重试} diff --git a/banban-mini/src/components/firmware-progress/view-model.ts b/banban-mini/src/components/firmware-progress/view-model.ts index 46f4510..576bb05 100644 --- a/banban-mini/src/components/firmware-progress/view-model.ts +++ b/banban-mini/src/components/firmware-progress/view-model.ts @@ -1,21 +1,24 @@ import type { DeviceFirmwareStatus } from '@/services/device' +import { isFirmwareCurrent, isFirmwareUpdateInProgress } from '../../utils/firmware-status' export function firmwareProgressView(status: DeviceFirmwareStatus | null, refreshError = false) { - const state = String(status?.update_status || '').toLowerCase() + const state = String(status?.update_status || '').trim().toLowerCase() const complete = state === 'success' || state === 'completed' - const failed = state === 'failed' + const current = !complete && !isFirmwareUpdateInProgress(state) && isFirmwareCurrent(status) + const failed = state === 'failed' && !current const value = Number(status?.progress) const progress = complete ? 100 : Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0 const hasProgress = complete || progress > 0 const target = status?.target_version - const version = target && !['unknown', 'updating'].includes(target) ? target : status?.latest_version - const title = complete ? '固件更新完成' : failed ? '固件更新失败' : '固件更新中' + const version = current ? status?.current_version : target && !['unknown', 'updating'].includes(target) ? target : status?.latest_version + const title = complete ? '固件更新完成' : current ? '当前已是最新版本' : failed ? '固件更新失败' : '固件更新中' const message = complete ? '设备已完成更新,可以继续使用。' - : failed ? '更新未完成,请确认设备电量和网络后重试。' - : refreshError ? '暂时无法刷新进度,正在重试。你可以先关闭此窗口。' - : state === 'sent' ? '更新指令已发送,等待设备确认。' - : progress >= 100 ? '进度已达 100%,等待设备确认更新结果。' - : hasProgress ? '请保持设备电量充足,并连接网络。' - : '设备正在处理更新,等待进度上报。' - return { title, message, version, progress, percent: Math.floor(progress), hasProgress, complete, failed } + : current ? '当前设备已运行最新版本,无需更新。' + : failed ? '更新未完成,请确认设备电量和网络后重试。' + : refreshError ? '暂时无法刷新进度,正在重试。你可以先关闭此窗口。' + : state === 'sent' ? '更新指令已发送,等待设备确认。' + : progress >= 100 ? '进度已达 100%,等待设备确认更新结果。' + : hasProgress ? '请保持设备电量充足,并连接网络。' + : '设备正在处理更新,等待进度上报。' + return { title, message, version, progress, percent: Math.floor(progress), hasProgress, complete, failed, current } } diff --git a/banban-mini/src/components/page-title/index.tsx b/banban-mini/src/components/page-title/index.tsx new file mode 100644 index 0000000..5824192 --- /dev/null +++ b/banban-mini/src/components/page-title/index.tsx @@ -0,0 +1,13 @@ +import { Image } from '@tarojs/components' + +const artwork = { + 玩伴: require('../../assets/ui/heading-chat.png'), + 设备定位: require('../../assets/ui/heading-location.png'), + 设备管理: require('../../assets/ui/heading-manage.png'), +} + +// Bundle the supplied design's lettering so headings do not depend on font loading. +export default function PageTitle({ title }: { title: keyof typeof artwork }) { + return +} diff --git a/banban-mini/src/pages/bind/index.scss b/banban-mini/src/pages/bind/index.scss index 0129003..916824b 100644 --- a/banban-mini/src/pages/bind/index.scss +++ b/banban-mini/src/pages/bind/index.scss @@ -388,7 +388,25 @@ .bind-result-title { display: block; color: #895731; font-size: 46px; font-weight: 700; line-height: 1.35; } .bind-result-art { display: block; width: 310px; height: 290px; margin: 12px auto 20px; } .bind-result-description { display: block; margin-top: 18px; color: #a68d74; font-size: 27px; line-height: 1.65; word-break: break-word; } -.bind-countdown-ring { width: 220px; height: 220px; border: 12px solid #f9d96c; border-top-color: #ffa24f; border-radius: 50%; margin: 44px auto; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fffdf7; } +.bind-countdown-ring { + position: relative; + width: 244px; + height: 244px; + border-radius: 50%; + margin: 44px auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #fffdf7; + box-shadow: inset 0 0 0 12px #f9d96c; +} +.bind-countdown-half { position: absolute; top: 0; width: 50%; height: 100%; overflow: hidden; pointer-events: none; } +.bind-countdown-half.right { right: 0; } +.bind-countdown-half.left { left: 0; } +.bind-countdown-arc { position: absolute; top: 0; width: 100%; height: 100%; box-sizing: border-box; border: 12px solid #ffa24f; transition: transform .8s linear; } +.bind-countdown-half.right .bind-countdown-arc { left: -100%; border-right: 0; border-radius: 122px 0 0 122px; transform-origin: 100% 50%; } +.bind-countdown-half.left .bind-countdown-arc { left: 100%; border-left: 0; border-radius: 0 122px 122px 0; transform-origin: 0 50%; } .bind-countdown-value { font-size: 62px; font-weight: 700; color: #ec913e; } .bind-countdown-label { margin-top: 12px; font-size: 22px; color: #aa8154; } .bind-instruction { padding: 28px; border: 2px solid #d2b48c; border-radius: 46px; background: #fffdf7; } @@ -398,3 +416,8 @@ .bind-result-button.primary { background: linear-gradient(#ffce8d, #efa05a); border: 2px solid #cf935a; color: #895731; } .bind-result-button.secondary { background: #fffdf7; border: 2px solid #cf935a; color: #ad8054; } .bind-result-button.waiting { background: linear-gradient(#eeede9, #c9c7c1); border: 2px solid #aaa69c; color: #777168; } +.bind-device-summary { margin-top: 32px; padding: 12px 28px; border: 2px solid #d2b48c; border-radius: 28px; background: #fffdf7; text-align: left; } +.bind-device-row { display: flex; align-items: center; justify-content: space-between; padding: 20px 0; color: #a68d74; font-size: 26px; } +.bind-device-row + .bind-device-row { border-top: 1px solid #eee3d5; } +.bind-device-value { color: #895731; font-weight: 600; } +.bind-device-value.online { color: #93b516; } diff --git a/banban-mini/src/pages/bind/index.tsx b/banban-mini/src/pages/bind/index.tsx index 6dec695..a0119b9 100644 --- a/banban-mini/src/pages/bind/index.tsx +++ b/banban-mini/src/pages/bind/index.tsx @@ -1,9 +1,11 @@ import { getPageStyle } from '@/utils/page-style' +import { formatBatteryDisplay } from '@/utils/battery' import { View, Text, Image, Input, Button } from '@tarojs/components' import { useEffect, useRef, useState } from 'react' import Taro, { useDidShow, useDidHide, useRouter } from '@tarojs/taro' import { getToken } from '@/services/auth' +import { DeviceOnlineStatus, DeviceStatus, getDeviceOnlineStatus, getDeviceStatus } from '@/services/device' import { getNFCBindSession, reportBindInitialLocation, @@ -179,11 +181,19 @@ export default function Bind() { const [bindResultDestination, setBindResultDestination] = useState('/pages/device/index') const [bindExpiresAt, setBindExpiresAt] = useState('') const [remainingBindSeconds, setRemainingBindSeconds] = useState(0) + const [totalBindSeconds, setTotalBindSeconds] = useState(0) + const [boundDeviceId, setBoundDeviceId] = useState('') + const [boundDeviceStatus, setBoundDeviceStatus] = useState(null) + const [boundDeviceOnline, setBoundDeviceOnline] = useState(null) + const [loadingBoundDevice, setLoadingBoundDevice] = useState(false) const pollingRef = useRef | null>(null) + const pollGenerationRef = useRef(0) + const pageVisibleRef = useRef(true) const countdownRef = useRef | null>(null) const appliedLaunchQrRef = useRef('') useDidShow(() => { + pageVisibleRef.current = true void loadPageData() if (bindStatus === SESSION_STATUS_PENDING && bindToken) { schedulePoll(bindToken) @@ -191,14 +201,32 @@ export default function Bind() { }) useDidHide(() => { + pageVisibleRef.current = false stopPolling() }) useEffect(() => () => { + pageVisibleRef.current = false stopPolling() stopCountdown() }, []) + useEffect(() => { + if (bindStatus !== SESSION_STATUS_COMPLETED || !boundDeviceId) return + let cancelled = false + setLoadingBoundDevice(true) + void Promise.all([ + getDeviceStatus(boundDeviceId).catch(() => null), + getDeviceOnlineStatus(boundDeviceId).catch(() => null), + ]).then(([status, online]) => { + if (cancelled) return + setBoundDeviceStatus(status) + setBoundDeviceOnline(online) + setLoadingBoundDevice(false) + }) + return () => { cancelled = true } + }, [bindStatus, boundDeviceId]) + const hasSelectedOrNewChild = selectedChildId !== null || Boolean(newChildName.trim()) const isPendingBinding = Boolean(pendingDeviceId) const isAddingCard = isCardMode && Boolean(deviceId.trim()) @@ -245,6 +273,7 @@ export default function Bind() { } const stopPolling = () => { + pollGenerationRef.current += 1 if (pollingRef.current) { clearTimeout(pollingRef.current) pollingRef.current = null @@ -260,8 +289,10 @@ export default function Bind() { const schedulePoll = (nextBindToken?: string) => { stopPolling() + if (!pageVisibleRef.current) return + const generation = pollGenerationRef.current pollingRef.current = setTimeout(() => { - void pollBindSession(nextBindToken) + void pollBindSession(nextBindToken, generation) }, 1500) } @@ -274,6 +305,10 @@ export default function Bind() { setBindHint('') setBindExpiresAt('') setRemainingBindSeconds(0) + setTotalBindSeconds(0) + setBoundDeviceId('') + setBoundDeviceStatus(null) + setBoundDeviceOnline(null) } const applyLaunchQrPayload = () => { @@ -336,12 +371,13 @@ export default function Bind() { return () => stopCountdown() }, [bindStatus, bindExpiresAt]) - const pollBindSession = async (nextBindToken?: string) => { + const pollBindSession = async (nextBindToken?: string, generation = pollGenerationRef.current) => { const activeBindToken = (nextBindToken || bindToken).trim() - if (!activeBindToken) return + if (!activeBindToken || !pageVisibleRef.current || generation !== pollGenerationRef.current) return try { const session = await getNFCBindSession(activeBindToken) + if (!pageVisibleRef.current || generation !== pollGenerationRef.current) return setBindStatus(session.status) setCardUUID(session.card_uuid || '') setBindExpiresAt(session.expires_at || '') @@ -360,6 +396,7 @@ export default function Bind() { stopCountdown() if (session.status === SESSION_STATUS_COMPLETED) { + setBoundDeviceId(session.device_id) setSelectedBindingDeviceId(session.device_id) if (!isAddingCard && session.child_id) { void uploadBindInitialLocation(session.device_id) @@ -391,8 +428,10 @@ export default function Bind() { return } } catch (error: any) { + if (!pageVisibleRef.current || generation !== pollGenerationRef.current) return console.error('[bind] poll bind session failed:', error) - setBindHint(error?.message || '查询绑定状态失败,请重试') + setBindHint('暂时无法查询绑定结果,正在重试,请稍候') + schedulePoll(activeBindToken) } } @@ -526,6 +565,7 @@ export default function Bind() { setBindStatus(session.status) setBindExpiresAt(session.expires_at) setRemainingBindSeconds(getRemainingBindSeconds(session.expires_at)) + setTotalBindSeconds(getRemainingBindSeconds(session.expires_at)) setBindHint('已发送加卡指令,请在倒计时结束前去设备上贴新卡') schedulePoll(session.bind_token) Taro.showToast({ title: '请去设备上贴新卡', icon: 'none' }) @@ -546,6 +586,7 @@ export default function Bind() { setBindStatus(session.status) setBindExpiresAt(session.expires_at) setRemainingBindSeconds(getRemainingBindSeconds(session.expires_at)) + setTotalBindSeconds(getRemainingBindSeconds(session.expires_at)) setBindHint(childId ? '已发送绑卡指令,请在倒计时结束前去设备上贴自己的卡' : '已发送绑卡指令,贴卡后设备会先进入待关联状态') schedulePoll(session.bind_token) Taro.showToast({ title: '请去设备上贴卡', icon: 'none' }) @@ -581,6 +622,8 @@ export default function Bind() { if (sessionVisible) { const completed = bindStatus === SESSION_STATUS_COMPLETED const pending = bindStatus === SESSION_STATUS_PENDING + const countdownAngle = totalBindSeconds > 0 ? Math.max(0, Math.min(360, remainingBindSeconds / totalBindSeconds * 360)) : 0 + const resultBattery = formatBatteryDisplay(boundDeviceStatus?.power ?? boundDeviceStatus?.battery_pct) const title = pending ? '请在设备上贴卡' : completed ? isAddingCard ? '新卡添加完成' : '设备绑定成功' : isExpiredBind ? '贴卡时间已过' : '贴卡未完成' @@ -597,6 +640,12 @@ export default function Bind() { <> {title} + + + + + + {formatRemainingTime(remainingBindSeconds)} 剩余贴卡时间 @@ -613,9 +662,23 @@ export default function Bind() { {title} {bindHint} {completed ? ( - + <> + + + 设备状态 + + {loadingBoundDevice ? '查询中' : boundDeviceOnline ? boundDeviceOnline.online ? '在线' : '离线' : '--'} + + + + 当前电量 + {loadingBoundDevice ? '查询中' : `${resultBattery.text}${resultBattery.showPercent ? '%' : ''}`} + + + + ) : ( <>