145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
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>
|
||
)
|
||
}
|