feat:绑定设备和设备详情
This commit is contained in:
44
src/utils/auth-token.ts
Normal file
44
src/utils/auth-token.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
const TOKEN_KEY = 'token'
|
||||
|
||||
/** 从登录响应中提取 access_token(兼容 RuoYi-Vue-Plus) */
|
||||
export function extractAuthToken(payload: unknown): string {
|
||||
if (payload == null) return ''
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
return payload.replace(/^Bearer\s+/i, '').trim()
|
||||
}
|
||||
|
||||
if (typeof payload === 'object') {
|
||||
const record = payload as Record<string, unknown>
|
||||
for (const key of ['access_token', 'accessToken', 'token']) {
|
||||
const value = record[key]
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value.replace(/^Bearer\s+/i, '').trim()
|
||||
}
|
||||
}
|
||||
if (record.data != null) {
|
||||
const nested = extractAuthToken(record.data)
|
||||
if (nested) return nested
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getStoredToken(): string {
|
||||
const raw = (uni.getStorageSync(TOKEN_KEY) as string) || ''
|
||||
return raw.replace(/^Bearer\s+/i, '').trim()
|
||||
}
|
||||
|
||||
export function saveStoredToken(token: string): string {
|
||||
const normalized = token.replace(/^Bearer\s+/i, '').trim()
|
||||
if (!normalized) {
|
||||
throw new Error('empty token')
|
||||
}
|
||||
uni.setStorageSync(TOKEN_KEY, normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function clearStoredToken(): void {
|
||||
uni.removeStorageSync(TOKEN_KEY)
|
||||
}
|
||||
292
src/utils/bluetooth.ts
Normal file
292
src/utils/bluetooth.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
export class BluetoothError extends Error {
|
||||
errCode?: number
|
||||
|
||||
constructor(message: string, errCode?: number) {
|
||||
super(message)
|
||||
this.name = 'BluetoothError'
|
||||
this.errCode = errCode
|
||||
}
|
||||
}
|
||||
|
||||
type UniFail = UniApp.GeneralCallbackResult
|
||||
|
||||
function toBluetoothError(err: UniFail, fallback: string): BluetoothError {
|
||||
const msg = err.errMsg || fallback
|
||||
return new BluetoothError(getBluetoothErrorMessage({ errCode: err.errCode, errMsg: msg }), err.errCode)
|
||||
}
|
||||
|
||||
export function isBluetoothApiSupported(): boolean {
|
||||
return (
|
||||
typeof uni.openBluetoothAdapter === 'function'
|
||||
&& typeof uni.startBluetoothDevicesDiscovery === 'function'
|
||||
&& typeof uni.onBluetoothDeviceFound === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
function openAdapter(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.openBluetoothAdapter({
|
||||
success: () => resolve(),
|
||||
fail: (err) => reject(toBluetoothError(err, '蓝牙初始化失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getAdapterState(): Promise<UniApp.GetBluetoothAdapterStateSuccess> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getBluetoothAdapterState({
|
||||
success: (res) => resolve(res),
|
||||
fail: (err) => reject(toBluetoothError(err, '获取蓝牙状态失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化蓝牙适配器并确认可用 */
|
||||
export async function ensureBluetoothReady(): Promise<void> {
|
||||
if (!isBluetoothApiSupported()) {
|
||||
throw new BluetoothError('当前环境不支持蓝牙,请使用 App 或微信小程序')
|
||||
}
|
||||
|
||||
try {
|
||||
await openAdapter()
|
||||
} catch (err) {
|
||||
const btErr = err as BluetoothError
|
||||
if (btErr.errCode !== 10001) {
|
||||
try {
|
||||
const state = await getAdapterState()
|
||||
if (state.available) return
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const state = await getAdapterState()
|
||||
if (!state.available) {
|
||||
throw new BluetoothError('本机不支持蓝牙', 10009)
|
||||
}
|
||||
}
|
||||
|
||||
export function startBluetoothDiscovery(): Promise<void> {
|
||||
if (typeof uni.startBluetoothDevicesDiscovery !== 'function') {
|
||||
return Promise.reject(new BluetoothError('当前环境不支持蓝牙扫描'))
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.startBluetoothDevicesDiscovery({
|
||||
allowDuplicatesKey: false,
|
||||
success: () => resolve(),
|
||||
fail: (err) => reject(toBluetoothError(err, '开始扫描失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function stopBluetoothDiscovery(): Promise<void> {
|
||||
if (typeof uni.stopBluetoothDevicesDiscovery !== 'function') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.stopBluetoothDevicesDiscovery({
|
||||
success: () => resolve(),
|
||||
fail: (err) => reject(toBluetoothError(err, '停止扫描失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function getDiscoveredBluetoothDevices(): Promise<UniApp.BluetoothDeviceInfo[]> {
|
||||
if (typeof uni.getBluetoothDevices !== 'function') {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getBluetoothDevices({
|
||||
success: (res) => resolve(res.devices ?? []),
|
||||
fail: (err) => reject(toBluetoothError(err, '获取已发现设备失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function subscribeBluetoothDevicesFound(
|
||||
onFound: (devices: UniApp.BluetoothDeviceInfo[]) => void,
|
||||
): () => void {
|
||||
if (typeof uni.onBluetoothDeviceFound !== 'function') {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
const handler = (res: UniApp.OnBluetoothDeviceFoundCallbackResult) => {
|
||||
if (res.devices?.length) onFound(res.devices)
|
||||
}
|
||||
uni.onBluetoothDeviceFound(handler)
|
||||
|
||||
return () => {
|
||||
if (typeof uni.offBluetoothDeviceFound !== 'function') return
|
||||
uni.offBluetoothDeviceFound(handler)
|
||||
}
|
||||
}
|
||||
|
||||
const MAC_PATTERN = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/
|
||||
|
||||
/** 规范化为大写 MAC(AA:BB:CC:DD:EE:FF) */
|
||||
export function normalizeMacAddress(value: string): string | undefined {
|
||||
const trimmed = value.trim()
|
||||
if (MAC_PATTERN.test(trimmed)) return trimmed.toUpperCase()
|
||||
const compact = trimmed.replace(/[^0-9A-Fa-f]/g, '').toUpperCase()
|
||||
if (compact.length !== 12) return undefined
|
||||
return compact.replace(/(.{2})(?=.)/g, '$1:')
|
||||
}
|
||||
|
||||
/** 去掉分隔符的大写 MAC */
|
||||
export function compactMacAddress(value: string): string {
|
||||
return value.replace(/[^0-9A-Fa-f]/g, '').toUpperCase()
|
||||
}
|
||||
|
||||
export function deviceMatchesMac(
|
||||
device: UniApp.BluetoothDeviceInfo,
|
||||
mac: string,
|
||||
): boolean {
|
||||
const target = compactMacAddress(mac)
|
||||
if (!target) return false
|
||||
|
||||
const deviceMac = normalizeMacAddress(device.deviceId)
|
||||
if (deviceMac && compactMacAddress(deviceMac) === target) return true
|
||||
|
||||
const compactId = compactMacAddress(device.deviceId)
|
||||
return compactId === target
|
||||
}
|
||||
|
||||
/** RSSI → signalLevel:0离线 1弱 2一般 3强 */
|
||||
export function rssiToSignalLevel(rssi?: number): string {
|
||||
if (rssi == null) return '0'
|
||||
if (rssi >= -55) return '3'
|
||||
if (rssi >= -70) return '2'
|
||||
if (rssi >= -85) return '1'
|
||||
return '0'
|
||||
}
|
||||
|
||||
export function createBleConnection(bleDeviceId: string): Promise<void> {
|
||||
if (typeof uni.createBLEConnection !== 'function') {
|
||||
return Promise.reject(new BluetoothError('当前环境不支持蓝牙连接'))
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.createBLEConnection({
|
||||
deviceId: bleDeviceId,
|
||||
timeout: 10000,
|
||||
success: () => resolve(),
|
||||
fail: (err) => reject(toBluetoothError(err, '连接设备失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function closeBleConnection(bleDeviceId: string): Promise<void> {
|
||||
if (typeof uni.closeBLEConnection !== 'function') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.closeBLEConnection({
|
||||
deviceId: bleDeviceId,
|
||||
success: () => resolve(),
|
||||
fail: (err) => reject(toBluetoothError(err, '断开连接失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function subscribeBleConnectionStateChange(
|
||||
onChange: (res: { deviceId: string; connected: boolean }) => void,
|
||||
): () => void {
|
||||
if (typeof uni.onBLEConnectionStateChange !== 'function') {
|
||||
return () => {}
|
||||
}
|
||||
uni.onBLEConnectionStateChange(onChange)
|
||||
return () => {
|
||||
if (typeof uni.offBLEConnectionStateChange !== 'function') return
|
||||
uni.offBLEConnectionStateChange(onChange)
|
||||
}
|
||||
}
|
||||
|
||||
/** 扫描并匹配指定 MAC 的 BLE 设备 */
|
||||
export async function scanForBleDeviceByMac(
|
||||
mac: string,
|
||||
options: { timeoutMs?: number } = {},
|
||||
): Promise<UniApp.BluetoothDeviceInfo | null> {
|
||||
const timeoutMs = options.timeoutMs ?? 15000
|
||||
const normalizedMac = normalizeMacAddress(mac)
|
||||
if (!normalizedMac) return null
|
||||
|
||||
await ensureBluetoothReady()
|
||||
|
||||
let matched: UniApp.BluetoothDeviceInfo | null = null
|
||||
|
||||
const pickMatch = (devices: UniApp.BluetoothDeviceInfo[]) => {
|
||||
for (const device of devices) {
|
||||
if (deviceMatchesMac(device, normalizedMac)) {
|
||||
matched = device
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const existing = await getDiscoveredBluetoothDevices()
|
||||
if (pickMatch(existing)) {
|
||||
return matched
|
||||
}
|
||||
|
||||
const unsubscribe = subscribeBluetoothDevicesFound((devices) => {
|
||||
pickMatch(devices)
|
||||
})
|
||||
|
||||
try {
|
||||
await startBluetoothDiscovery()
|
||||
} catch (err) {
|
||||
unsubscribe()
|
||||
throw err
|
||||
}
|
||||
|
||||
const startedAt = Date.now()
|
||||
while (!matched && Date.now() - startedAt < timeoutMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
const cached = await getDiscoveredBluetoothDevices()
|
||||
if (pickMatch(cached)) break
|
||||
}
|
||||
|
||||
unsubscribe()
|
||||
await stopBluetoothDiscovery().catch(() => {})
|
||||
return matched
|
||||
}
|
||||
|
||||
export function getBluetoothErrorMessage(err: unknown): string {
|
||||
if (err instanceof BluetoothError) return err.message
|
||||
|
||||
const code = (err as UniFail)?.errCode
|
||||
const msg = (err as UniFail)?.errMsg
|
||||
|
||||
switch (code) {
|
||||
case 10001:
|
||||
return '请先打开手机蓝牙后再试'
|
||||
case 10002:
|
||||
return '没有找到指定设备'
|
||||
case 10003:
|
||||
return '连接失败,请靠近设备后重试'
|
||||
case 10004:
|
||||
return '没有找到指定服务'
|
||||
case 10005:
|
||||
return '没有找到指定特征值'
|
||||
case 10006:
|
||||
return '当前连接已断开'
|
||||
case 10007:
|
||||
return '当前特征值不支持此操作'
|
||||
case 10008:
|
||||
return '蓝牙操作失败,请重试'
|
||||
case 10009:
|
||||
return '本机不支持低功耗蓝牙'
|
||||
case 10010:
|
||||
return '设备已连接'
|
||||
case 10011:
|
||||
return '配对设备需要配对码'
|
||||
case 10012:
|
||||
return '连接超时,请重试'
|
||||
case 10013:
|
||||
return '设备标识无效'
|
||||
default:
|
||||
return msg || '蓝牙不可用,请检查权限与系统设置'
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,12 @@ import iconPhone from '@/static/icon-phone@2x.png'
|
||||
import iconCode from '@/static/icon-code@2x.png'
|
||||
import iconChecked from '@/static/icon-checked@2x.png'
|
||||
import iconPassword from '@/static/路径.png'
|
||||
import iconDeviceDetailSettings from '@/static/路径1.png'
|
||||
import deviceDetailHeroBg from '@/static/组 15226@2x.png'
|
||||
import iconRunModeInfo from '@/static/组 15227@2x.png'
|
||||
import iconHomeSelector from '@/static/图标 4.png'
|
||||
import iconHomeArrow from '@/static/组 9.png'
|
||||
import iconHomeSelected from '@/static/完成@2x.png'
|
||||
import iconNotify from '@/static/组 27.png'
|
||||
import iconDeviceEmpty from '@/static/图标 8.png'
|
||||
import iconDeviceNameEdit from '@/static/图标 9.png'
|
||||
@@ -33,10 +37,18 @@ export const staticAssets = {
|
||||
iconChecked,
|
||||
/** 路径.png — 密码框 */
|
||||
iconPassword,
|
||||
/** 路径1.png — 设备详情右上角设置 */
|
||||
iconDeviceDetailSettings,
|
||||
/** 组 15226@2x.png — 设备详情页上半部分背景 */
|
||||
deviceDetailHeroBg,
|
||||
/** 组 15227@2x.png — 设备详情「运行模式」旁提示图标 */
|
||||
iconRunModeInfo,
|
||||
/** 图标 4.png — 首页「我的家」 */
|
||||
iconHomeSelector,
|
||||
/** 组 9.png — 「我的家」下拉箭头 */
|
||||
iconHomeArrow,
|
||||
/** 完成@2x.png — 「我的家」下拉选中图标 */
|
||||
iconHomeSelected,
|
||||
/** 组 27.png — 设备页右上角通知 */
|
||||
iconNotify,
|
||||
/** 图标 8.png — 设备空状态中心图标 */
|
||||
|
||||
Reference in New Issue
Block a user