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')
+})