refactor(device): BLE 配网页改名为 ble-config,为新的 add 页让出路径

原 pages/device/add.tsx 迁移为 pages/device/ble-config.tsx(git mv 保留历史),路由常量新增 DEVICE_BLE_CONFIG 并注册 app.config.ts。pages/device/add 路径将由新"添加设备"页(手动输入+扫码)占用。
This commit is contained in:
ozh 2026-09-09 16:40:05 +08:00
parent 8243dfa134
commit 1f3c9c842a
4 changed files with 314 additions and 0 deletions

View File

@ -8,6 +8,7 @@ export default {
'pages/device/detail', 'pages/device/detail',
'pages/device/add', 'pages/device/add',
'pages/device/config', 'pages/device/config',
'pages/device/ble-config',
'pages/family/index', 'pages/family/index',
'pages/family/detail', 'pages/family/detail',
'pages/family/contact/add', 'pages/family/contact/add',

View File

@ -5,6 +5,7 @@ export const ROUTES = Object.freeze({
MINE: '/pages/mine/index', MINE: '/pages/mine/index',
DEVICE_LIST: '/pages/device/index', DEVICE_LIST: '/pages/device/index',
DEVICE_ADD: '/pages/device/add', DEVICE_ADD: '/pages/device/add',
DEVICE_BLE_CONFIG: '/pages/device/ble-config',
DEVICE_CONFIG: '/pages/device/config', DEVICE_CONFIG: '/pages/device/config',
DEVICE_DETAIL: '/pages/device/detail', DEVICE_DETAIL: '/pages/device/detail',
FAMILY: '/pages/family/index', FAMILY: '/pages/family/index',

View File

@ -0,0 +1,168 @@
.page {
min-height: 100vh;
background: #e9eaed;
}
.search-wrap {
height: 500px;
background: $color-bg-white;
display: flex;
justify-content: center;
align-items: center;
}
.search-btn {
width: 260px;
height: 260px;
border-radius: 130px;
background: $color-primary;
color: #ffffff;
font-size: 56px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.search-btn:active {
transform: scale(0.97);
}
.search-btn.is-searching {
animation: searchPulse 1.2s ease-in-out infinite;
}
@keyframes searchPulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(67, 201, 150, 0.42);
}
70% {
transform: scale(1.04);
box-shadow: 0 0 0 30px rgba(67, 201, 150, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(67, 201, 150, 0);
}
}
.tip-row {
min-height: 78px;
padding: 12px 16px;
background: #efeff1;
color: $color-danger;
font-size: 34px;
font-weight: 600;
display: flex;
align-items: center;
}
.list-header {
height: 116px;
background: $color-bg-white;
display: flex;
align-items: center;
padding: 0 38px;
}
.list-title {
font-size: 52px;
color: #101217;
font-weight: 700;
}
.list-body {
background: #e9eaed;
padding-bottom: 30px;
}
.empty-row {
padding: 36px;
text-align: center;
color: #8c9097;
font-size: 30px;
}
.device-card {
background: $color-bg-white;
margin-top: 12px;
padding: 24px;
display: flex;
gap: 20px;
}
.device-icon {
width: 92px;
height: 92px;
border-radius: 46px;
border: 2px solid #d8d8d8;
box-shadow: inset 0 0 0 10px #f1f1f1;
flex-shrink: 0;
}
.device-main {
flex: 1;
min-width: 0;
}
.device-name-row {
display: flex;
align-items: center;
gap: 12px;
}
.device-name {
flex: 1;
min-width: 0;
font-size: 38px;
color: #30343a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.device-tag {
font-size: 22px;
color: $color-primary-deep;
border: 1px solid $color-primary-deep;
border-radius: 20px;
padding: 2px 10px;
}
.name-tag {
color: $color-info-light;
border-color: $color-info-light;
}
.device-id-text {
display: block;
margin-top: 8px;
font-size: 30px;
color: #8a8f98;
word-break: break-all;
}
.device-actions {
display: flex;
gap: 10px;
margin-top: 16px;
}
.action-btn {
padding: 8px 16px;
border: 2px solid $color-primary;
color: $color-primary-deep;
font-size: 30px;
border-radius: 6px;
background: #f8fffc;
}
.action-btn.disabled {
border-color: $color-primary-light-border;
color: #7db89e;
background: #f5faf7;
}

View File

@ -0,0 +1,144 @@
import { useEffect, useState } from 'react'
import Taro from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Navbar } from '@/components/navbar'
import { useBle } from '@/ble/hooks/useBle'
import { navigateTo } from '@/utils/navigation'
import { ROUTES } from '@/constants/routes'
import { getErrorMessage } from '@/utils/error'
import { DEVICE_ACTION_KEYS, ACTION_REGISTRY, isEp832Device } from '@/constants/deviceActions'
import type { BleDevice } from '@/ble/hooks/useBle'
import styles from './ble-config.module.scss'
export default function DeviceBleConfig() {
const { state, discovery } = useBle()
const [preparingId, setPreparingId] = useState('')
// 页面隐藏/卸载时清理搜索(对齐旧版 onHide/onUnload),连接保持供配网页使用
useEffect(() => {
return () => {
if (discovery.getState().isSearching) {
discovery.stopSearch(false)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const startScan = async () => {
if (state.searching) {
await discovery.stopSearch()
return
}
// startSearch 内部自动完成:Android 定位权限检查 + 适配器初始化 +
// 服务 UUID 过滤扫描(00F4/F400/FFF4)+ 4s 无结果兜底全扫 + 30s 自动停止
try {
await discovery.startSearch()
} catch {
// startSearch 内部已 toast 具体错误码,此处不再重复提示
}
}
// 点击"网络配置":连接 → 发现服务特征 → 订阅 F301(厂家确认无需鉴权)→ 打开配网页
const openWifiPanel = async (deviceId: string) => {
if (preparingId) return
setPreparingId(deviceId)
Taro.showLoading({ title: '连接设备中', mask: true })
let prepError: unknown = null
try {
await discovery.prepareWifiConfig(deviceId)
navigateTo(ROUTES.DEVICE_CONFIG)
} catch (error) {
prepError = error
} finally {
setPreparingId('')
// showLoading 与 showToast 共用同一遮罩层,必须先 hideLoading 再弹提示
Taro.hideLoading()
}
if (prepError) {
// 连接/订阅失败用模态弹窗,需用户主动确认,确保错误诊断信息可见可读
Taro.showModal({
title: '设备连接失败',
content: getErrorMessage(prepError),
showCancel: false,
confirmText: '知道了'
})
}
}
const invokeAction = (key: string, deviceId: string) => {
if (key === 'WIFI') openWifiPanel(deviceId)
// PARAMS/SERVER 仅占位,后续接入
}
const isActionBusy = (key: string, deviceId: string) =>
key === 'WIFI' && preparingId === deviceId
const renderDevice = (device: BleDevice) => {
const actions = (DEVICE_ACTION_KEYS[isEp832Device(device) ? 'EP832' : 'DEFAULT'] ?? DEVICE_ACTION_KEYS.DEFAULT)
.map((key) => ({ key, ...ACTION_REGISTRY[key] }))
return (
<View key={device.deviceId} className={styles.deviceCard}>
<View className={styles.deviceIcon} />
<View className={styles.deviceMain}>
<View className={styles.deviceNameRow}>
<Text className={styles.deviceName}>{String(device.showName ?? device.deviceId)}</Text>
{device.hasTargetService ? (
<Text className={styles.deviceTag}>00F4</Text>
) : device.matchedByName || device.matchedByPayload ? (
<Text className={`${styles.deviceTag} ${styles.nameTag}`}>名称匹配</Text>
) : null}
</View>
<Text className={styles.deviceIdText}>{String(device.deviceId)}</Text>
<View className={styles.deviceActions}>
{actions.map((action) => {
const busy = isActionBusy(action.key, device.deviceId)
return (
<View
key={action.key}
className={`${styles.actionBtn} ${busy ? styles.disabled : ''}`}
onClick={() => !busy && invokeAction(action.key, device.deviceId)}
>
<Text>{busy ? action.busyLabel : action.label}</Text>
</View>
)
})}
</View>
</View>
</View>
)
}
return (
<View className={styles.page}>
<Navbar title="设备配置" variant="hero" />
<View className={styles.searchWrap}>
<View
className={`${styles.searchBtn} ${state.searching ? styles.isSearching : ''}`}
onClick={startScan}
>
<Text>{state.searching ? '搜索中' : '搜索'}</Text>
</View>
</View>
<View className={styles.tipRow}>
<Text>*手机需开启蓝牙功能,Android手机还需开启定位功能</Text>
</View>
<View className={styles.listHeader}>
<Text className={styles.listTitle}>搜索到的设备列表</Text>
</View>
<View className={styles.listBody}>
{state.deviceList.length === 0 ? (
<View className={styles.emptyRow}><Text>暂无搜索结果</Text></View>
) : (
state.deviceList.map(renderDevice)
)}
</View>
</View>
)
}