diff --git a/banban-mini/package.json b/banban-mini/package.json index 8d57201..460bf25 100644 --- a/banban-mini/package.json +++ b/banban-mini/package.json @@ -7,7 +7,8 @@ "dev": "npm run dev:weapp", "build": "npm run build:weapp", "build:weapp": "NODE_OPTIONS=--openssl-legacy-provider taro build --type weapp", - "dev:weapp": "NODE_OPTIONS=--openssl-legacy-provider taro build --type weapp --watch" + "dev:weapp": "NODE_OPTIONS=--openssl-legacy-provider taro build --type weapp --watch", + "test:ui": "node --test tests/*.test.cjs" }, "dependencies": { "@tarojs/components": "^3.6.38", diff --git a/banban-mini/src/app.config.ts b/banban-mini/src/app.config.ts index 4b2d64e..7eb45b7 100644 --- a/banban-mini/src/app.config.ts +++ b/banban-mini/src/app.config.ts @@ -19,6 +19,7 @@ export default defineAppConfig({ navigationBarTextStyle: "black", }, tabBar: { + custom: true, color: "#B9A17F", selectedColor: "#B86E2F", backgroundColor: "#FFFDF7", diff --git a/banban-mini/src/assets/tab-icons/chat-icon.png b/banban-mini/src/assets/tab-icons/chat-icon.png index df2367f..78e23cb 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 cc98c11..64a1a60 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 39f9097..9dbbd0e 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 25a2f64..da0b495 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 1cb9677..8d4a559 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 3621bdf..cfb6c34 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 43e679a..4fdd3ff 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 5c45f47..6fe232b 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/feature-chat.png b/banban-mini/src/assets/ui/feature-chat.png new file mode 100644 index 0000000..fab9dde Binary files /dev/null and b/banban-mini/src/assets/ui/feature-chat.png differ diff --git a/banban-mini/src/assets/ui/feature-location.png b/banban-mini/src/assets/ui/feature-location.png new file mode 100644 index 0000000..7b67539 Binary files /dev/null and b/banban-mini/src/assets/ui/feature-location.png differ diff --git a/banban-mini/src/assets/ui/feature-status.png b/banban-mini/src/assets/ui/feature-status.png new file mode 100644 index 0000000..a4be5b5 Binary files /dev/null and b/banban-mini/src/assets/ui/feature-status.png differ diff --git a/banban-mini/src/assets/ui/login-hero.png b/banban-mini/src/assets/ui/login-hero.png index 62a5102..ae137e7 100644 Binary files a/banban-mini/src/assets/ui/login-hero.png and b/banban-mini/src/assets/ui/login-hero.png differ diff --git a/banban-mini/src/assets/ui/menu-binding.png b/banban-mini/src/assets/ui/menu-binding.png new file mode 100644 index 0000000..34b1146 Binary files /dev/null and b/banban-mini/src/assets/ui/menu-binding.png differ diff --git a/banban-mini/src/assets/ui/menu-cards.png b/banban-mini/src/assets/ui/menu-cards.png new file mode 100644 index 0000000..1ad1622 Binary files /dev/null and b/banban-mini/src/assets/ui/menu-cards.png differ diff --git a/banban-mini/src/assets/ui/menu-family.png b/banban-mini/src/assets/ui/menu-family.png new file mode 100644 index 0000000..5a8c572 Binary files /dev/null and b/banban-mini/src/assets/ui/menu-family.png differ diff --git a/banban-mini/src/assets/ui/menu-notice.png b/banban-mini/src/assets/ui/menu-notice.png new file mode 100644 index 0000000..b2c3e18 Binary files /dev/null and b/banban-mini/src/assets/ui/menu-notice.png differ diff --git a/banban-mini/src/assets/ui/menu-role.png b/banban-mini/src/assets/ui/menu-role.png new file mode 100644 index 0000000..e7822fb Binary files /dev/null and b/banban-mini/src/assets/ui/menu-role.png differ diff --git a/banban-mini/src/assets/ui/menu-sleep.png b/banban-mini/src/assets/ui/menu-sleep.png new file mode 100644 index 0000000..f605b97 Binary files /dev/null and b/banban-mini/src/assets/ui/menu-sleep.png differ diff --git a/banban-mini/src/assets/ui/menu-unbind.png b/banban-mini/src/assets/ui/menu-unbind.png new file mode 100644 index 0000000..868d45b Binary files /dev/null and b/banban-mini/src/assets/ui/menu-unbind.png differ diff --git a/banban-mini/src/assets/ui/menu-update.png b/banban-mini/src/assets/ui/menu-update.png new file mode 100644 index 0000000..61d0406 Binary files /dev/null and b/banban-mini/src/assets/ui/menu-update.png differ diff --git a/banban-mini/src/assets/ui/page-pattern.png b/banban-mini/src/assets/ui/page-pattern.png new file mode 100644 index 0000000..4b7fc52 Binary files /dev/null and b/banban-mini/src/assets/ui/page-pattern.png differ diff --git a/banban-mini/src/assets/ui/role-baike.png b/banban-mini/src/assets/ui/role-baike.png new file mode 100644 index 0000000..0332582 Binary files /dev/null and b/banban-mini/src/assets/ui/role-baike.png differ diff --git a/banban-mini/src/assets/ui/role-banban.png b/banban-mini/src/assets/ui/role-banban.png new file mode 100644 index 0000000..7564ac8 Binary files /dev/null and b/banban-mini/src/assets/ui/role-banban.png differ diff --git a/banban-mini/src/assets/ui/role-tuntunzai.png b/banban-mini/src/assets/ui/role-tuntunzai.png new file mode 100644 index 0000000..357bc68 Binary files /dev/null and b/banban-mini/src/assets/ui/role-tuntunzai.png differ diff --git a/banban-mini/src/assets/ui/role-zhuli.png b/banban-mini/src/assets/ui/role-zhuli.png new file mode 100644 index 0000000..5b65d33 Binary files /dev/null and b/banban-mini/src/assets/ui/role-zhuli.png differ diff --git a/banban-mini/src/components/firmware-progress/index.scss b/banban-mini/src/components/firmware-progress/index.scss new file mode 100644 index 0000000..33923d1 --- /dev/null +++ b/banban-mini/src/components/firmware-progress/index.scss @@ -0,0 +1,28 @@ +.firmware-progress-mask { z-index: 30; } +.firmware-progress-card { max-width: 620px; } +.firmware-progress-version, +.firmware-progress-message { + display: block; + color: #a39080; + font-size: 26px; + line-height: 1.6; +} +.firmware-progress-message { min-height: 84px; margin-top: 8px; } +.firmware-progress-art { display: block; width: 160px; height: 160px; margin: 20px auto -4px; } +.firmware-progress-track { + height: 22px; + padding: 4px; + border: 2px solid #ad681d; + border-radius: 30px; + background: #fff9e2; + overflow: hidden; +} +.firmware-progress-fill { + height: 100%; + border-radius: 22px; + background: #c4e52d; + transition: width 0.25s ease-out; +} +.firmware-progress-track.failed .firmware-progress-fill { background: #ed9a75; } +.firmware-progress-caption { display: flex; justify-content: space-between; margin-top: 14px; color: #ad681d; font-size: 26px; } +.firmware-progress-processing { display: block; margin-top: 24px; padding: 16px; border-radius: 18px; background: #ededed; color: #9f958c; text-align: center; font-size: 28px; } diff --git a/banban-mini/src/components/firmware-progress/index.tsx b/banban-mini/src/components/firmware-progress/index.tsx new file mode 100644 index 0000000..3b3b9e7 --- /dev/null +++ b/banban-mini/src/components/firmware-progress/index.tsx @@ -0,0 +1,40 @@ +import { Image, Text, View } from '@tarojs/components' +import type { DeviceFirmwareStatus } from '@/services/device' +import { firmwareProgressView } from './view-model' +import './index.scss' + +type Props = { + status: DeviceFirmwareStatus | null + refreshError: boolean + onClose: () => void + onRetry?: () => void +} + +export default function FirmwareProgress({ status, refreshError, onClose, onRetry }: Props) { + const view = firmwareProgressView(status, refreshError) + return ( + + + {view.title} + {view.version && 本次更新版本 {view.version}} + {view.message} + + + + + + {refreshError ? '上次更新进度' : '更新进度'} + {view.hasProgress ? `${view.percent}%` : '等待设备上报'} + + {!view.complete && !view.failed && 处理中……} + + {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 new file mode 100644 index 0000000..46f4510 --- /dev/null +++ b/banban-mini/src/components/firmware-progress/view-model.ts @@ -0,0 +1,21 @@ +import type { DeviceFirmwareStatus } from '@/services/device' + +export function firmwareProgressView(status: DeviceFirmwareStatus | null, refreshError = false) { + const state = String(status?.update_status || '').toLowerCase() + const complete = state === 'success' || state === 'completed' + const failed = state === 'failed' + 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 message = complete ? '设备已完成更新,可以继续使用。' + : failed ? '更新未完成,请确认设备电量和网络后重试。' + : refreshError ? '暂时无法刷新进度,正在重试。你可以先关闭此窗口。' + : state === 'sent' ? '更新指令已发送,等待设备确认。' + : progress >= 100 ? '进度已达 100%,等待设备确认更新结果。' + : hasProgress ? '请保持设备电量充足,并连接网络。' + : '设备正在处理更新,等待进度上报。' + return { title, message, version, progress, percent: Math.floor(progress), hasProgress, complete, failed } +} diff --git a/banban-mini/src/custom-tab-bar/index.config.ts b/banban-mini/src/custom-tab-bar/index.config.ts new file mode 100644 index 0000000..4e68c5a --- /dev/null +++ b/banban-mini/src/custom-tab-bar/index.config.ts @@ -0,0 +1 @@ +export default { component: true } diff --git a/banban-mini/src/custom-tab-bar/index.scss b/banban-mini/src/custom-tab-bar/index.scss new file mode 100644 index 0000000..e8742ce --- /dev/null +++ b/banban-mini/src/custom-tab-bar/index.scss @@ -0,0 +1,25 @@ +.companion-tab-bar { + position: fixed; + z-index: 10; + bottom: 0; + left: 0; + right: 0; + display: flex; + padding: 14px 20px calc(12px + env(safe-area-inset-bottom)); + background: #fffdf7; + border: 2px solid #df9e55; + border-bottom: 0; + border-radius: 42px 42px 0 0; + box-sizing: border-box; +} +.companion-tab { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + color: #d6b18a; +} +.companion-tab.active { color: #ad681d; } +.companion-tab-icon { width: 62px; height: 52px; } +.companion-tab-label { margin-top: 4px; font-size: 24px; font-weight: 600; } diff --git a/banban-mini/src/custom-tab-bar/index.tsx b/banban-mini/src/custom-tab-bar/index.tsx new file mode 100644 index 0000000..9ed2c41 --- /dev/null +++ b/banban-mini/src/custom-tab-bar/index.tsx @@ -0,0 +1,39 @@ +import { Image, Text, View } from '@tarojs/components' +import Taro, { useDidShow } from '@tarojs/taro' +import { useState } from 'react' +import './index.scss' + +const tabs = [ + { page: 'device', label: '首页', icon: require('../assets/tab-icons/device-icon.png'), selected: require('../assets/tab-icons/device-selected.png') }, + { page: 'chat', label: '玩伴', icon: require('../assets/tab-icons/chat-icon.png'), selected: require('../assets/tab-icons/chat-selected.png') }, + { page: 'location', label: '定位', icon: require('../assets/tab-icons/location-icon.png'), selected: require('../assets/tab-icons/location-selected.png') }, + { page: 'sleep', label: '管理', icon: require('../assets/tab-icons/manage-icon.png'), selected: require('../assets/tab-icons/manage-selected.png') }, +] + +function currentRoute() { + const pages = Taro.getCurrentPages() + return pages[pages.length - 1]?.route || 'pages/device/index' +} + +export default function CustomTabBar() { + const [route, setRoute] = useState(currentRoute) + useDidShow(() => setRoute(currentRoute())) + return ( + + {tabs.map((tab) => { + const path = `pages/${tab.page}/index` + const active = route === path + return ( + { + if (active) return + Taro.switchTab({ url: `/${path}`, success: () => setRoute(path) }) + }}> + + {tab.label} + + ) + })} + + ) +} diff --git a/banban-mini/src/pages/bind/index.scss b/banban-mini/src/pages/bind/index.scss index 8dd6d8a..0129003 100644 --- a/banban-mini/src/pages/bind/index.scss +++ b/banban-mini/src/pages/bind/index.scss @@ -383,3 +383,18 @@ } } } + +.bind-result { padding: 24px 48px 60px; text-align: center; } +.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-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; } +.bind-instruction-title { font-size: 34px; font-weight: 600; color: #895731; } +.bind-result-button { min-height: 92px; margin-top: 28px; border-radius: 46px; display: flex; align-items: center; justify-content: center; font-size: 30px; font-weight: 600; box-sizing: border-box; } +.bind-result-button::after { display: none; } +.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; } diff --git a/banban-mini/src/pages/bind/index.tsx b/banban-mini/src/pages/bind/index.tsx index 9738e87..6dec695 100644 --- a/banban-mini/src/pages/bind/index.tsx +++ b/banban-mini/src/pages/bind/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Image, Input, Button } from '@tarojs/components' import { useEffect, useRef, useState } from 'react' import Taro, { useDidShow, useDidHide, useRouter } from '@tarojs/taro' @@ -175,6 +176,7 @@ export default function Bind() { const [bindStatus, setBindStatus] = useState(null) const [cardUUID, setCardUUID] = useState('') const [bindHint, setBindHint] = useState('') + const [bindResultDestination, setBindResultDestination] = useState('/pages/device/index') const [bindExpiresAt, setBindExpiresAt] = useState('') const [remainingBindSeconds, setRemainingBindSeconds] = useState(0) const pollingRef = useRef | null>(null) @@ -370,13 +372,7 @@ export default function Bind() { : session.card_uuid ? `绑定完成,卡号 ${session.card_uuid},稍后可关联宝贝` : '绑定完成,稍后可关联宝贝' ) Taro.showToast({ title: isAddingCard ? '新卡添加成功' : '设备绑定成功', icon: 'success' }) - setTimeout(() => { - if (isAddingCard || session.child_id) { - goDevicePage() - } else { - goManagePage() - } - }, 500) + setBindResultDestination(isAddingCard || session.child_id ? '/pages/device/index' : '/pages/sleep/index') return } @@ -568,7 +564,7 @@ export default function Bind() { if (loading) { return ( - + 加载中... @@ -577,8 +573,68 @@ export default function Bind() { ) } + const sessionVisible = bindStatus !== null && [ + SESSION_STATUS_PENDING, SESSION_STATUS_COMPLETED, SESSION_STATUS_EXPIRED, + SESSION_STATUS_FAILED, SESSION_STATUS_CANCELLED, + ].includes(bindStatus) + + if (sessionVisible) { + const completed = bindStatus === SESSION_STATUS_COMPLETED + const pending = bindStatus === SESSION_STATUS_PENDING + const title = pending ? '请在设备上贴卡' : completed + ? isAddingCard ? '新卡添加完成' : '设备绑定成功' + : isExpiredBind ? '贴卡时间已过' : '贴卡未完成' + return ( + + + + + 返回 + + + + {pending ? ( + <> + {title} + + {formatRemainingTime(remainingBindSeconds)} + 剩余贴卡时间 + + + {isAddingCard ? '加卡指令已发送' : '绑卡指令已发送'} + {bindHint} + + + + ) : ( + <> + + {title} + {bindHint} + {completed ? ( + + ) : ( + <> + + + + )} + + )} + + {systemBanner} + + ) + } + return ( - + @@ -587,16 +643,6 @@ export default function Bind() { {pageTitle} - {bindStatus === SESSION_STATUS_PENDING && ( - - )} - {bindStatus === SESSION_STATUS_COMPLETED && ( - - )} - {(bindStatus === SESSION_STATUS_EXPIRED || bindStatus === SESSION_STATUS_FAILED || bindStatus === SESSION_STATUS_CANCELLED) && ( - - )} - {isAddingCard ? ( diff --git a/banban-mini/src/pages/chat/detail/index.scss b/banban-mini/src/pages/chat/detail/index.scss index 1e3a449..76b30c7 100644 --- a/banban-mini/src/pages/chat/detail/index.scss +++ b/banban-mini/src/pages/chat/detail/index.scss @@ -92,7 +92,8 @@ align-items: flex-start; &.user { - flex-direction: row-reverse; + flex-direction: row; + justify-content: flex-end; .message-main { align-items: flex-end; @@ -251,3 +252,7 @@ font-weight: 600; letter-spacing: 2px; } + +.chat-avatar-art { width: 100%; height: 100%; } +.user-avatar { margin-left: 14px; } +.message-item.user .message-bubble { margin: 0; } diff --git a/banban-mini/src/pages/chat/detail/index.tsx b/banban-mini/src/pages/chat/detail/index.tsx index 2f11672..a7e2281 100644 --- a/banban-mini/src/pages/chat/detail/index.tsx +++ b/banban-mini/src/pages/chat/detail/index.tsx @@ -1,3 +1,5 @@ +import { getParticipantArtwork } from '@/utils/artwork' +import { getPageStyle } from '@/utils/page-style' import { View, Text, ScrollView, Image } from '@tarojs/components' import { useEffect, useRef, useState } from 'react' import Taro, { useRouter } from '@tarojs/taro' @@ -747,14 +749,14 @@ export default function ChatDetail() { } return ( - + - {character.icon} + {character.name} @@ -797,13 +799,13 @@ export default function ChatDetail() { - {msg.senderType === 'parent' ? '👨' : '👦'} + ) : ( - {msg.senderType === 'child' ? '🧒' : character.icon} + {formatParticipantLabel(msg)} diff --git a/banban-mini/src/pages/chat/index.scss b/banban-mini/src/pages/chat/index.scss index 91fc769..b88c6f3 100644 --- a/banban-mini/src/pages/chat/index.scss +++ b/banban-mini/src/pages/chat/index.scss @@ -368,3 +368,6 @@ font-weight: 700; color: #1A1A1A; } + +.conversation-avatar-art { width: 100%; height: 100%; } +.conversation-avatar { background: #fff1d4; padding: 8px; box-sizing: border-box; } diff --git a/banban-mini/src/pages/chat/index.tsx b/banban-mini/src/pages/chat/index.tsx index 6815c24..43174f0 100644 --- a/banban-mini/src/pages/chat/index.tsx +++ b/banban-mini/src/pages/chat/index.tsx @@ -1,4 +1,6 @@ -import { View, Text, ScrollView } from '@tarojs/components' +import { getParticipantArtwork } from '@/utils/artwork' +import { getPageStyle } from '@/utils/page-style' +import { View, Text, ScrollView, Image } from '@tarojs/components' import { useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { getToken } from '@/services/auth' @@ -74,7 +76,7 @@ export default function Chat() { } return ( - + 玩伴 @@ -108,7 +110,7 @@ export default function Chat() { )} > - 伴 + AI 玩伴 @@ -132,7 +134,7 @@ export default function Chat() { )} > - 家 + 家长留言 @@ -175,7 +177,7 @@ export default function Chat() { onClick={() => handleConversationClick(conversation)} > - {conversation.avatar} + diff --git a/banban-mini/src/pages/device/index.tsx b/banban-mini/src/pages/device/index.tsx index 84039ca..2ef4dcd 100644 --- a/banban-mini/src/pages/device/index.tsx +++ b/banban-mini/src/pages/device/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Slider, Image } from '@tarojs/components' import { useRef, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' @@ -271,7 +272,7 @@ export default function Device() { if (isLoading) { return ( - + 加载中... @@ -282,7 +283,7 @@ export default function Device() { if (isGuest) { return ( - + @@ -334,7 +335,7 @@ export default function Device() { if (!child) { const hasPendingDevice = Boolean(pendingBinding?.device_id) return ( - + 设备首页 @@ -362,7 +363,7 @@ export default function Device() { if (!binding) { const hasPendingDevice = Boolean(pendingBinding?.device_id) return ( - + {child.child_name} 的伴伴 @@ -421,7 +422,7 @@ export default function Device() { const latestAlarmChildName = getAlarmChildLabel(latestAlarm, childName) return ( - + {pageTitle} @@ -460,7 +461,7 @@ export default function Device() { - + 定时休眠 @@ -476,7 +477,7 @@ export default function Device() { - + 立即休眠/唤醒 diff --git a/banban-mini/src/pages/family-invite/index.tsx b/banban-mini/src/pages/family-invite/index.tsx index f07862f..ffeb400 100644 --- a/banban-mini/src/pages/family-invite/index.tsx +++ b/banban-mini/src/pages/family-invite/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Image } from '@tarojs/components' import Taro, { useDidShow, useRouter } from '@tarojs/taro' import { useState } from 'react' @@ -74,7 +75,7 @@ export default function FamilyInvite() { const statusText = invitation?.status === 1 ? '等待确认加入' : '邀请已失效' return ( - + 家庭共享邀请 diff --git a/banban-mini/src/pages/location/index.tsx b/banban-mini/src/pages/location/index.tsx index dc17b04..478c40a 100644 --- a/banban-mini/src/pages/location/index.tsx +++ b/banban-mini/src/pages/location/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Map } from '@tarojs/components' import { useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' @@ -376,7 +377,7 @@ export default function Location() { : '最近点位' return ( - + 设备定位 diff --git a/banban-mini/src/pages/login/index.tsx b/banban-mini/src/pages/login/index.tsx index 8551cf8..46aba92 100644 --- a/banban-mini/src/pages/login/index.tsx +++ b/banban-mini/src/pages/login/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Button, Checkbox, CheckboxGroup, Image } from '@tarojs/components' import { useEffect, useState } from 'react' import Taro from '@tarojs/taro' @@ -131,7 +132,7 @@ export default function Login() { } return ( - + - + AI 智能对话 - + 实时定位追踪 - + 设备状态监控 diff --git a/banban-mini/src/pages/notification/index.tsx b/banban-mini/src/pages/notification/index.tsx index 467758f..45667a0 100644 --- a/banban-mini/src/pages/notification/index.tsx +++ b/banban-mini/src/pages/notification/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Image } from '@tarojs/components' import { useEffect, useState } from 'react' import Taro, { useRouter } from '@tarojs/taro' @@ -49,7 +50,7 @@ export default function NotificationEntry() { }, [token]) return ( - + 伴伴 diff --git a/banban-mini/src/pages/sleep-schedule/index.tsx b/banban-mini/src/pages/sleep-schedule/index.tsx index 7ce5702..9f0dc65 100644 --- a/banban-mini/src/pages/sleep-schedule/index.tsx +++ b/banban-mini/src/pages/sleep-schedule/index.tsx @@ -1,3 +1,4 @@ +import { getPageStyle } from '@/utils/page-style' import { View, Text, Picker, Image } from '@tarojs/components' import { useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' @@ -107,7 +108,7 @@ export default function SleepSchedule() { if (isLoading) { return ( - + 加载中... @@ -118,7 +119,7 @@ export default function SleepSchedule() { if (!child) { return ( - + 定时休眠 先绑定设备并选择宝贝,再为对应设备设置休眠时间 @@ -134,7 +135,7 @@ export default function SleepSchedule() { if (!binding) { return ( - + 定时休眠 {child.child_name} 还没有绑定设备 @@ -156,7 +157,7 @@ export default function SleepSchedule() { const childName = child.child_name || '当前宝贝' return ( - + 定时休眠 每天在设定时段内,设备将进入不可启动状态 diff --git a/banban-mini/src/pages/sleep/index.scss b/banban-mini/src/pages/sleep/index.scss index 2e7309a..3b7b073 100644 --- a/banban-mini/src/pages/sleep/index.scss +++ b/banban-mini/src/pages/sleep/index.scss @@ -1058,3 +1058,6 @@ color: #E5484D; font-weight: 700; } + +.system-update-title-row { display: flex; align-items: center; gap: 12px; } +.system-update-icon { width: 54px; height: 54px; } diff --git a/banban-mini/src/pages/sleep/index.tsx b/banban-mini/src/pages/sleep/index.tsx index 3749855..7628b12 100644 --- a/banban-mini/src/pages/sleep/index.tsx +++ b/banban-mini/src/pages/sleep/index.tsx @@ -1,3 +1,6 @@ +import FirmwareProgress from '@/components/firmware-progress' +import { getRoleArtwork } from '@/utils/artwork' +import { getPageStyle } from '@/utils/page-style' import { View, Text, Image, Input, Button } from '@tarojs/components' import { useEffect, useRef, useState } from 'react' import Taro, { useDidHide, useDidShow, useShareAppMessage } from '@tarojs/taro' @@ -82,6 +85,10 @@ export default function Sleep() { const [binding, setBinding] = useState(null) const [currentChild, setCurrentChild] = useState(null) const [firmwareStatus, setFirmwareStatus] = useState(null) + const [showFirmwareProgress, setShowFirmwareProgress] = useState(false) + const [firmwareRefreshError, setFirmwareRefreshError] = useState(false) + const firmwareLoadGenerationRef = useRef(0) + const firmwareDeviceIdRef = useRef('') const [deviceRole, setDeviceRole] = useState(null) const [roles, setRoles] = useState([]) const [currentCardGroup, setCurrentCardGroup] = useState(null) @@ -269,6 +276,7 @@ export default function Sleep() { if (firmwareStatusPollDeviceIdRef.current !== normalizedDeviceId) return setFirmwareStatus(nextFirmwareStatus) + setFirmwareRefreshError(false) if (isFirmwareUpdateInProgress(nextFirmwareStatus?.update_status)) { scheduleFirmwareStatusPoll(normalizedDeviceId) return @@ -283,6 +291,7 @@ export default function Sleep() { } } catch (error) { console.warn('[manage] firmware poll failed:', error) + if (firmwareStatusPollDeviceIdRef.current === normalizedDeviceId) setFirmwareRefreshError(true) if (firmwareStatusPollDeviceIdRef.current === normalizedDeviceId) { scheduleFirmwareStatusPoll(normalizedDeviceId) } @@ -293,6 +302,13 @@ export default function Sleep() { const loadFirmwareStatus = async (deviceId?: string | null) => { const normalizedDeviceId = String(deviceId || '').trim() + const generation = ++firmwareLoadGenerationRef.current + if (firmwareDeviceIdRef.current !== normalizedDeviceId) { + setShowFirmwareProgress(false) + setFirmwareStatus(null) + stopFirmwareStatusPolling() + } + firmwareDeviceIdRef.current = normalizedDeviceId if (!normalizedDeviceId) { setFirmwareStatus(null) setIsLoadingFirmware(false) @@ -303,9 +319,13 @@ export default function Sleep() { setIsLoadingFirmware(true) try { const nextFirmwareStatus = await getDeviceFirmwareStatus(normalizedDeviceId) + if (generation !== firmwareLoadGenerationRef.current) return setFirmwareStatus(nextFirmwareStatus) + setFirmwareRefreshError(false) + if (isFirmwareUpdateInProgress(nextFirmwareStatus?.update_status)) setShowFirmwareProgress(true) syncFirmwareStatusPolling(normalizedDeviceId, nextFirmwareStatus) } catch (error: any) { + if (generation !== firmwareLoadGenerationRef.current) return console.error('[manage] firmware load failed:', error) setFirmwareStatus(null) if (firmwareStatusPollDeviceIdRef.current === normalizedDeviceId) { @@ -316,7 +336,7 @@ export default function Sleep() { icon: 'none', }) } finally { - setIsLoadingFirmware(false) + if (generation === firmwareLoadGenerationRef.current) setIsLoadingFirmware(false) } } @@ -749,6 +769,9 @@ export default function Sleep() { } const nextFirmwareStatus = await startDeviceFirmwareUpdate(binding.device_id) + if (firmwareDeviceIdRef.current !== binding.device_id) return + setShowFirmwareProgress(true) + setFirmwareRefreshError(false) setFirmwareStatus(nextFirmwareStatus) syncFirmwareStatusPolling(binding.device_id, nextFirmwareStatus) Taro.showToast({ @@ -961,7 +984,7 @@ export default function Sleep() { if (loading) { return ( - + 加载中... @@ -972,7 +995,7 @@ export default function Sleep() { if (isGuest) { return ( - + 设置与管理 @@ -1054,7 +1077,7 @@ export default function Sleep() { : isUpdatingFirmware ? '发送中' : isFirmwareUpdatingByStatus - ? '更新中' + ? '查看进度' : firmwareStatus?.update_available ? '更新' : '已最新' @@ -1093,7 +1116,7 @@ export default function Sleep() { : `${currentCardGroup?.card_count || 0} 张` const menuItems: MenuItem[] = [ { - icon: require('../../assets/tab-icons/orange-robot.png'), + icon: require('../../assets/ui/menu-role.png'), iconBgClass: 'orange', name: '当前宝贝', value: currentChildName, @@ -1101,7 +1124,7 @@ export default function Sleep() { }, ...(unassignedBindings.length > 0 ? [{ - icon: require('../../assets/tab-icons/rings.png'), + icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green', name: '关联现有设备', value: `${unassignedBindings.length} 台待关联`, @@ -1109,14 +1132,14 @@ export default function Sleep() { }] : []), { - icon: require('../../assets/tab-icons/rings.png'), + icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green', name: '绑定设备', value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定', arrow: true, }, { - icon: require('../../assets/tab-icons/rings.png'), + icon: require('../../assets/ui/menu-cards.png'), iconBgClass: 'green', name: '我的卡片', value: cardValue, @@ -1124,7 +1147,7 @@ export default function Sleep() { disabled: !binding, }, { - icon: require('../../assets/tab-icons/orange-robot.png'), + icon: require('../../assets/ui/menu-role.png'), iconBgClass: 'green', name: 'AI 角色', value: roleValue, @@ -1132,7 +1155,7 @@ export default function Sleep() { disabled: !binding, }, { - icon: require('../../assets/tab-icons/rings.png'), + icon: require('../../assets/ui/menu-family.png'), iconBgClass: 'green', name: '家庭成员', value: familyValue, @@ -1140,7 +1163,7 @@ export default function Sleep() { disabled: !binding, }, { - icon: require('../../assets/tab-icons/rings.png'), + icon: require('../../assets/ui/menu-notice.png'), iconBgClass: 'green', name: '服务号通知', value: wechatMpValue, @@ -1148,7 +1171,7 @@ export default function Sleep() { disabled: isStartingWechatMpBind, }, { - icon: require('../../assets/tab-icons/broken-rings.png'), + icon: require('../../assets/ui/menu-unbind.png'), iconBgClass: 'red', name: '解除设备绑定', value: binding?.device_id ? `当前: ${binding.device_id}` : '', @@ -1158,7 +1181,7 @@ export default function Sleep() { ] return ( - + 设置与管理 @@ -1210,12 +1233,15 @@ export default function Sleep() { - 系统更新 + + + 系统更新 + {firmwareSubtitle} setShowFirmwareProgress(true) : canUpdateFirmware ? handleStartFirmwareUpdate : undefined} > {firmwareActionText} @@ -1266,6 +1292,12 @@ export default function Sleep() { 伴伴 Companion V1.1.0 + {showFirmwareProgress && firmwareStatus?.device_id === binding?.device_id && ( + setShowFirmwareProgress(false)} + onRetry={canUpdateFirmware ? () => { setShowFirmwareProgress(false); handleStartFirmwareUpdate() } : undefined} /> + )} + {showChildModal && ( setShowChildModal(false)}> - 修改宝贝姓名 + 编辑宝贝资料 + 宝贝名称 - 确认 + 保存 @@ -1520,6 +1553,7 @@ export default function Sleep() { className={`role-switch-item ${isActive ? 'active' : ''} ${isUpdatingRole ? 'disabled' : ''}`} onClick={isUpdatingRole ? undefined : () => handleSelectRole(item)} > + {item.name || item.role_key} {isActive && 当前} diff --git a/banban-mini/src/styles/ui-theme.scss b/banban-mini/src/styles/ui-theme.scss index 1c2d3b3..3ba8090 100644 --- a/banban-mini/src/styles/ui-theme.scss +++ b/banban-mini/src/styles/ui-theme.scss @@ -17,15 +17,18 @@ page { --bb-lime: #c4e52d; --bb-lime-deep: #93b516; --bb-danger: #ff6d61; + --bb-body-font: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; + --bb-navigation-top: 12px; + min-height: 100%; background-color: var(--bb-cream); color: var(--bb-brown); - font-family: 'MuyaoSoftbrush', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-family: var(--bb-body-font); } button, input, textarea { - font-family: 'MuyaoSoftbrush', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-family: var(--bb-body-font); } .login-page, @@ -40,12 +43,11 @@ textarea { .notification-page, .webview-empty { background-color: var(--bb-cream) !important; - background-image: - radial-gradient(circle at 16px 18px, rgba(241, 191, 116, 0.17) 0 3px, transparent 3.5px), - radial-gradient(circle at 27px 13px, rgba(241, 191, 116, 0.12) 0 2px, transparent 2.5px), - radial-gradient(circle at 7px 13px, rgba(241, 191, 116, 0.12) 0 2px, transparent 2.5px), - radial-gradient(circle at 16px 5px, rgba(241, 191, 116, 0.12) 0 2px, transparent 2.5px); - background-size: 48px 48px; + background-image: url('../assets/ui/page-pattern.png') !important; + background-size: 240px 240px !important; + min-height: 100vh !important; + box-sizing: border-box; + padding-top: var(--bb-navigation-top); } .page-header, @@ -56,7 +58,7 @@ textarea { } .page-header { - padding-top: 46px !important; + padding-top: 24px !important; padding-bottom: 22px !important; text-align: center; } @@ -80,7 +82,7 @@ textarea { } .bind-header { - padding-top: 46px !important; + padding-top: 24px !important; } .bind-header .title { @@ -463,30 +465,17 @@ textarea { /* Artwork placement follows the 750px reference boards while leaving all existing controls in their original document flow. */ .login-header .logo { - position: relative; - width: 600px !important; - height: 360px !important; - margin-bottom: -12px !important; - overflow: hidden; + width: 560px !important; + max-width: 88vw; + height: 488px !important; + margin-bottom: 12px !important; background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; } - -.login-header .logo .login-hero-art { - position: absolute; - top: -255px; - left: 50%; - width: 510px; - height: 675px; - transform: translateX(-50%); - pointer-events: none; -} - -.login-header .logo-icon { - display: none; -} +.login-header .logo .login-hero-art { width: 100%; height: 100%; } +.login-header .logo-icon { display: none; } .device-page, .bind-page, @@ -738,24 +727,72 @@ textarea { } @media (max-width: 360px) { - .login-header .logo { - width: 520px !important; - height: 320px !important; - } - - .login-header .logo .login-hero-art { - width: 455px; - height: 602px; - top: -230px; - } - - .device-avatar { - right: -2px; - width: 210px !important; - } - - .device-avatar .device-mascot { - width: 160px; - height: 155px; - } + .login-header .logo { width: 520px !important; height: 453px !important; } } + +/* Page corrections from the annotated companion reference. */ +page { background-image: url('../assets/ui/page-pattern.png') !important; background-size: 240px 240px !important; } +.login-header { padding-top: 12px !important; padding-bottom: 28px !important; } +.features { gap: 16px; } +.feature-item { + flex: 1; + min-width: 0; + padding: 20px 6px; + background: #fffdf7; + border: 2px solid #d5b088; + border-radius: 22px; +} +.feature-item .feature-icon-image { width: 72px; height: 64px; padding: 6px; background: transparent; margin-bottom: 12px; } +.feature-item .feature-text { font-size: 22px; text-align: center; } +.login-btn-wrapper .login-btn { border-radius: 48px !important; background: linear-gradient(#e4f598, #bad83b) !important; border-color: #97ad3b !important; } +.bind-header { display: flex; align-items: center; gap: 12px; padding: 22px 24px 32px !important; } +.bind-header .back-btn { position: static; flex-shrink: 0; padding: 10px 14px; } +.bind-header .title { flex: 1; min-width: 0; margin: 0; padding: 12px !important; font-size: 32px !important; } +.status-card { margin-top: 16px; min-height: 340px; padding: 32px !important; } +.status-card .status-badge { margin-bottom: 24px; } +.status-card .battery-section { max-width: 58%; } +.status-card .battery-percent { color: #fffdf7 !important; font-size: 82px; } +.status-card .status-meta { max-width: 100%; padding-right: 0; } +.status-card .status-pill { width: 42%; min-width: 0; box-sizing: border-box; background: rgba(255,255,255,.28) !important; } +.device-page .status-card .device-avatar { top: 20px; right: 24px; width: 240px !important; height: 220px !important; transform: none !important; } +.device-avatar .device-mascot { width: 100%; height: 100%; } +.quick-sleep-btn { min-height: 96px; border-radius: 36px !important; } +.quick-sleep-btn.sleep { background: linear-gradient(#eeeeed, #c4c4c1) !important; border-color: #a2a29d !important; } +.quick-sleep-btn.wake { background: linear-gradient(#e5f597, #beda3a) !important; border-color: #9eaf45 !important; } +.quick-sleep-btn.disabled { opacity: .55; } +.sleep-page .hero-card { align-items: center; gap: 16px; } +.sleep-page .hero-copy { flex: 1; min-width: 0; } +.sleep-page .hero-icon-wrap { width: 210px; height: 200px; flex-shrink: 0; margin: 0; background: transparent; } +.sleep-page .sleep-hero-mascot { width: 210px; height: 200px; margin: 0; } +.sleep-page .hero-icon-wrap .hero-icon { display: none; } +.chat-detail-page { padding-top: 0; min-height: 0 !important; } +.chat-detail-page .chat-header { padding-top: var(--bb-navigation-top); flex-shrink: 0; } +.chat-detail-page .chat-list { min-height: 0; } +.chat-detail-page .composer { flex-shrink: 0; } +.chat-detail-page .composer-record.recording { background: linear-gradient(#eeeeed, #d2d2cc) !important; border: 2px solid #aaa69c; box-shadow: inset 0 3px 6px rgba(90,70,40,.12) !important; } +.chat-detail-page .composer-record.recording .composer-record-text { color: #786b59 !important; } +.chat-detail-page .message-bubble, +.chat-detail-page .message-bubble text, +.manage-page .user-card, +.manage-page .summary-card { font-family: var(--bb-body-font); } +.manage-page .summary-value { word-break: break-all; } +.manage-page .menu-left .control-icon-img { width: 68px; height: 68px; } +.manage-page .menu-left .icon-bg { background: transparent; } +.modal-mask { box-sizing: border-box; padding: 40px; z-index: 20; } +.modal-card { border: 2px solid #d3bda8 !important; border-radius: 36px !important; padding: 36px; max-height: 82vh; overflow-y: auto; } +.modal-title { font-size: 36px; line-height: 1.35; } +.modal-input-wrap { padding: 0; background: transparent; } +.modal-input { box-sizing: border-box; height: 88px; padding: 0 22px; border: 2px solid #e6dfd7 !important; border-radius: 22px; background: #f0eeeb !important; } +.modal-field-label { display: block; font-size: 25px; color: #a98258; margin-bottom: 12px; } +.manage-page .modal-actions { display: flex; gap: 20px; margin-top: 28px; } +.manage-page .modal-action { flex: 1; margin: 0; min-height: 76px; display: flex; align-items: center; justify-content: center; padding: 8px 16px; box-sizing: border-box; border: 2px solid #ad681d; border-radius: 22px; text-align: center; } +.modal-action.cancel { background: #ededeb; color: #8d857b; border-color: #aaa39a; } +.modal-action.confirm { background: #fff8df !important; color: #ad681d !important; } +.modal-action.disabled { opacity: .5; } +.role-switch-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } +.role-switch-list .role-switch-item { margin: 0; padding: 20px 12px; border: 2px solid #d4b184; border-radius: 22px; background: #fff8df; text-align: center; } +.role-switch-list .role-switch-item.active { border-color: #ad681d; background: #f5edc8; } +.role-switch-art { display: block; width: 142px; height: 142px; margin: 0 auto 12px; } +.role-switch-head { flex-direction: column; gap: 8px; } +.role-switch-head .device-switch-tag { margin-left: 0; } +.role-switch-desc { font-size: 22px; } diff --git a/banban-mini/src/utils/artwork.ts b/banban-mini/src/utils/artwork.ts new file mode 100644 index 0000000..fb49bde --- /dev/null +++ b/banban-mini/src/utils/artwork.ts @@ -0,0 +1,16 @@ +const roleArtwork: Record = { + banban: require('../assets/ui/role-banban.png'), + baikeboshi: require('../assets/ui/role-baike.png'), + zhuli: require('../assets/ui/role-zhuli.png'), + tuntunzai: require('../assets/ui/role-tuntunzai.png'), +} + +export function getRoleArtwork(roleKey?: string): string { + return roleArtwork[String(roleKey || '').toLowerCase()] || roleArtwork.banban +} + +export function getParticipantArtwork(kind?: string, roleKey?: string): string { + if (kind === 'parent') return require('../assets/ui/dog-happy.png') + if (kind === 'child' || kind === 'device') return roleArtwork.banban + return getRoleArtwork(roleKey) +} diff --git a/banban-mini/src/utils/page-style.ts b/banban-mini/src/utils/page-style.ts new file mode 100644 index 0000000..7ac27ef --- /dev/null +++ b/banban-mini/src/utils/page-style.ts @@ -0,0 +1,18 @@ +import Taro from '@tarojs/taro' +import type { CSSProperties } from 'react' + +// Custom navigation must clear both the status bar and WeChat's capsule. +export function getPageStyle(customNavigation = true): CSSProperties { + let top = 12 + if (customNavigation) { + let statusBarHeight = 24 + try { + statusBarHeight = Taro.getSystemInfoSync().statusBarHeight || statusBarHeight + const capsule = Taro.getMenuButtonBoundingClientRect() + top = Math.max(statusBarHeight + 44, capsule?.bottom || 0) + 12 + } catch { + top = statusBarHeight + 56 + } + } + return { '--bb-navigation-top': `${top}px` } as CSSProperties +} diff --git a/banban-mini/tests/firmware-progress.test.cjs b/banban-mini/tests/firmware-progress.test.cjs new file mode 100644 index 0000000..34e3d36 --- /dev/null +++ b/banban-mini/tests/firmware-progress.test.cjs @@ -0,0 +1,79 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { test } = require('node:test') +const ts = require('typescript') + +function loadTypeScript(file, mocks = {}) { + const source = fs.readFileSync(path.join(__dirname, '../src', file), 'utf8') + const { outputText } = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, esModuleInterop: true }, + }) + const module = { exports: {} } + new Function('require', 'module', 'exports', outputText)( + (id) => mocks[id] || require(id), module, module.exports, + ) + return module.exports +} + +const { firmwareProgressView } = loadTypeScript('components/firmware-progress/view-model.ts') +const status = (state, progress) => ({ device_id: 'test-device', update_status: state, progress, latest_version: '1.8.2' }) + +test('accepted update without reported progress stays indeterminate', () => { + const view = firmwareProgressView(status('accepted', 0)) + assert.equal(view.hasProgress, false) + assert.match(view.message, /等待进度上报/) +}) +test('received progress is used directly, including fractional values', () => { + const view = firmwareProgressView(status('updating', 65.4)) + assert.equal(view.progress, 65.4) + assert.equal(view.percent, 65) + assert.equal(view.complete, false) +}) +test('nearly complete progress never rounds up to 100', () => { + assert.equal(firmwareProgressView(status('updating', 99.9)).percent, 99) +}) +test('100 percent still waits for the device success result', () => { + const view = firmwareProgressView(status('updating', 100)) + assert.equal(view.complete, false) + assert.match(view.message, /等待设备确认/) +}) +test('both backend completion statuses show a completed update', () => { + for (const state of ['success', 'completed']) { + const view = firmwareProgressView(status(state, 0)) + assert.equal(view.complete, true) + assert.equal(view.percent, 100) + } +}) +test('failed update keeps the last real progress and offers recovery copy', () => { + const view = firmwareProgressView(status('failed', 42)) + assert.equal(view.failed, true) + assert.equal(view.progress, 42) + assert.match(view.message, /重试/) +}) +test('temporary query failure is not reported as an OTA failure', () => { + const view = firmwareProgressView(status('updating', 42), true) + assert.equal(view.failed, false) + assert.equal(view.progress, 42) + assert.match(view.message, /暂时无法刷新/) +}) +test('invalid progress cannot overflow or break the display', () => { + for (const value of [undefined, NaN, Infinity, -10]) { + assert.equal(firmwareProgressView(status('updating', value)).hasProgress, false) + } + assert.equal(firmwareProgressView(status('updating', 120)).progress, 100) +}) +test('backend placeholder version is not displayed as a release number', () => { + const view = firmwareProgressView({ ...status('updating', 12), target_version: 'updating' }) + assert.equal(view.version, '1.8.2') +}) +test('custom pages clear the capsule while native pages only add content spacing', () => { + const { getPageStyle } = loadTypeScript('utils/page-style.ts', { + '@tarojs/taro': { + getSystemInfoSync: () => ({ statusBarHeight: 44 }), + getMenuButtonBoundingClientRect: () => ({ bottom: 90 }), + }, + }) + assert.equal(getPageStyle()['--bb-navigation-top'], '102px') + assert.equal(getPageStyle(false)['--bb-navigation-top'], '12px') +})