Browse Source

蓝牙重新封装,使用pinia构建单例模式。

liyuliangjiazai 3 months ago
parent
commit
8cfbaee

+ 9 - 0
code/ajyApp/App.vue

@@ -1,8 +1,17 @@
 <script>
+	import { useBleStore } from '@/stores/ble'
+
 	export default {
+		globalData: {
+			bleManager: null
+		},
 		onLaunch: function() {
 			console.log('App Launch')
 			this.checkLoginState()
+			// 在 App.vue 上下文中绑定 BLE 系统监听器
+			// 确保回调关联的 taskCenter 永不销毁,避免页面切换后报错
+			const bleStore = useBleStore()
+			bleStore.bindGlobalListeners()
 		},
 		onShow: function() {
 			console.log('App Show')

+ 4 - 1
code/ajyApp/main.js

@@ -15,11 +15,14 @@ app.$mount()
 
 // #ifdef VUE3
 import { createSSRApp } from 'vue'
+import * as Pinia from 'pinia'
 export function createApp() {
   const app = createSSRApp(App)
+  app.use(Pinia.createPinia())
   app.mixin(toastMixin)
   return {
-    app
+    app,
+    Pinia // 此处必须将 Pinia 返回
   }
 }
 // #endif

+ 4 - 1
code/ajyApp/pages.json

@@ -89,7 +89,10 @@
 			"path": "pages/device/search/search",
 			"style": {
 				"navigationStyle": "custom",
-				"navigationBarTitleText": ""
+				"navigationBarTitleText": "",
+				"app-plus": {
+					"backgroundColor": "#cee7ec"
+				}
 			}
 		},
 		{

+ 30 - 23
code/ajyApp/pages/ble-demo/ble-demo.nvue

@@ -28,10 +28,11 @@
 </template>
 
 <script>
-import bleManager, {
-	BLE_STATE, MODE, SUB_MODE, TEMPERATURE,
-	ensureBlePrerequisite, openLocationSettings
-} from '@/utils/ble'
+import { useBleStore } from '@/stores/ble'
+import {
+	BLE_STATE, MODE, SUB_MODE, TEMPERATURE
+} from '@/utils/ble/constants.js'
+import { ensureBlePrerequisite, openLocationSettings } from '@/utils/ble/permission.js'
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
 
 export default {
@@ -45,20 +46,26 @@ export default {
 			logs: []
 		}
 	},
+	computed: {
+		bleStore() {
+			return useBleStore()
+		}
+	},
 	onLoad() {
-		bleManager.configure({ debug: true })
+		this.bleStore.configure({ debug: true })
 
-		this._unbinders = [
-			bleManager.on('state', s => { this.state = s }),
-			bleManager.on('connected', d => { this.device = d; this.log('已连接 ' + d.deviceId) }),
-			bleManager.on('disconnected', e => { this.log('已断开 ' + (e.reason || '')) }),
-			bleManager.on('reconnected', d => { this.log('自动重连成功') }),
-			bleManager.on('report:GROUP_1', d => { this.log('参数组1 ' + JSON.stringify(d)) }),
-			bleManager.on('report:GROUP_2', d => { this.log('参数组2 ' + JSON.stringify(d)) })
-		]
-	},
-	onUnload() {
-		this._unbinders.forEach(fn => fn && fn())
+		// 监听store状态变化
+		this.$watch(() => this.bleStore.bleState, (s) => {
+			this.state = s
+		})
+		this.$watch(() => this.bleStore.linked, (linked) => {
+			if (linked) {
+				this.device = this.bleStore.device
+				this.log('已连接 ' + (this.bleStore.device && this.bleStore.device.deviceId || ''))
+			} else if (this.device) {
+				this.log('已断开')
+			}
+		})
 	},
 	methods: {
 		log(msg) {
@@ -69,8 +76,8 @@ export default {
 		async onScanConnect() {
 			try {
 				await ensureBlePrerequisite()
-				await bleManager.init()
-				const dev = await bleManager.scanAndConnect({ timeout: 8000 })
+				await this.bleStore.init()
+				const dev = await this.bleStore.scanAndConnect({ timeout: 8000 })
 				this.device = dev
 			} catch (e) {
 				this.log('失败: ' + e.message)
@@ -84,18 +91,18 @@ export default {
 			}
 		},
 		async onDisconnect() {
-			await bleManager.disconnect()
+			await this.bleStore.disconnect()
 			this.device = null
 		},
-		async onPowerOn()  { await this._safe(() => bleManager.powerOn()) },
-		async onPowerOff() { await this._safe(() => bleManager.powerOff()) },
+		async onPowerOn()  { await this._safe(() => this.bleStore.powerOn()) },
+		async onPowerOff() { await this._safe(() => this.bleStore.powerOff()) },
 		async onStart() {
-			await this._safe(() => bleManager.startMoxi({
+			await this._safe(() => this.bleStore.startMoxi({
 				mode: MODE.BASIC, subMode: SUB_MODE.SUB_1,
 				temperature: TEMPERATURE.MID, duration: 30
 			}))
 		},
-		async onPause() { await this._safe(() => bleManager.pauseMoxi()) },
+		async onPause() { await this._safe(() => this.bleStore.pauseMoxi()) },
 
 		async _safe(fn) {
 			try { await fn(); this.log('指令已下发') }

+ 109 - 31
code/ajyApp/pages/device/detail/detail.nvue

@@ -28,7 +28,15 @@
 			</view>
 			<view class="menuitem">
 				<text>信号强度</text>
-				<text class="gray">{{rssiText}}</text>
+				<view class="signal-value-wrap">
+					<view class="signal-wrap">
+						<view class="signal-bar signal-bar1" :class="{'signal-active': signalLevel >= 1}"></view>
+						<view class="signal-bar signal-bar2" :class="{'signal-active': signalLevel >= 2}"></view>
+						<view class="signal-bar signal-bar3" :class="{'signal-active': signalLevel >= 3}"></view>
+						<view class="signal-bar signal-bar4" :class="{'signal-active': signalLevel >= 4}"></view>
+					</view>
+					<text class="gray" style="padding-right: 0;">{{rssiText}}</text>
+				</view>
 			</view>
 			<view class="menuitem">
 				<text>固件版本</text>
@@ -58,7 +66,8 @@
 </template>
 
 <script>
-import bleManager, { BLE_STATE } from '@/utils/ble'
+import { useBleStore } from '@/stores/ble'
+import { BLE_STATE } from '@/utils/ble/constants.js'
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
 
 export default {
@@ -79,12 +88,23 @@ export default {
 	},
 	computed: {
 		rssiText() {
+			console.log("信号强度",this.rssi)
 			if (!this.rssi) return '未知'
 			const val = Number(this.rssi)
-			if (val >= -50) return '极强 (' + this.rssi + 'dBm)'
-			if (val >= -65) return '强 (' + this.rssi + 'dBm)'
-			if (val >= -80) return '中 (' + this.rssi + 'dBm)'
-			return '弱 (' + this.rssi + 'dBm)'
+			if (val >= -50) return '非常强'
+			if (val >= -65) return '强'
+			if (val >= -80) return '中'
+			
+			return '弱'
+		},
+		signalLevel() {
+			if (!this.rssi) return 0
+			const val = Number(this.rssi)
+			if (val >= -50) return 4
+			if (val >= -65) return 3
+			if (val >= -80) return 2
+			if (val >= -95) return 1
+			return 1
 		}
 	},
 	onLoad(options) {
@@ -94,27 +114,42 @@ export default {
 		this.deviceId = options.deviceId || ''
 		this.rssi = options.rssi || ''
 	},
-	onShow() {
-		// 监听BLE状态
-		this._unbinders = [
-			bleManager.on('state', (s) => {
+	async onShow() {
+		const bleStore = useBleStore()
+		// 用bleState精确判断连接状态
+		this.connected = (bleStore.bleState === BLE_STATE.READY_COMM)
+		console.log("连接状态", this.connected, "bleState:", bleStore.bleState)
+		
+		// 已连接时获取实时RSSI(使用store中实际连接的deviceId)
+		const connectedId = bleStore.device && bleStore.device.deviceId
+		if (this.connected && connectedId) {
+			console.log("获取RSSI使用deviceId:", connectedId, "页面deviceId:", this.deviceId)
+			uni.getBLEDeviceRSSI({
+				deviceId: connectedId,
+				success: (res) => {
+					console.log("实时信号强度", res.RSSI)
+					this.rssi = res.RSSI
+				},
+				fail: (err) => {
+					console.log("获取RSSI失败", err)
+					// 连接实际已断开,同步状态
+					if (err.code === 10004) {
+						this.connected = false
+					}
+				}
+			})
+		}
+		
+		// 监听store状态变化(使用$watch)
+		this._stopWatch = this.$watch(
+			() => bleStore.bleState,
+			(s) => {
 				this.connected = (s === BLE_STATE.READY_COMM)
 				if (s === BLE_STATE.DISCONNECTED) {
 					this.connecting = false
 				}
-			}),
-			bleManager.on('disconnected', () => {
-				this.connected = false
-				this.connecting = false
-			}),
-			bleManager.on('report:GROUP_2', (data) => {
-				if (data && data.firmwareVersion) {
-					this.firmwareVersion = 'V' + data.firmwareVersion
-				}
-			})
-		]
-		// 检查当前连接状态
-		this.connected = bleManager.isConnected
+			}
+		)
 	},
 	onHide() {
 		this._cleanListeners()
@@ -127,17 +162,18 @@ export default {
 			uni.navigateBack()
 		},
 		_cleanListeners() {
-			if (this._unbinders && this._unbinders.length) {
-				this._unbinders.forEach(fn => fn && fn())
-				this._unbinders = []
+			if (this._stopWatch) {
+				this._stopWatch()
+				this._stopWatch = null
 			}
 		},
 		async connectDevice() {
 			if (this.connecting || this.connected) return
 			this.connecting = true
+			const bleStore = useBleStore()
 			try {
-				await bleManager.init()
-				await bleManager.connect(this.deviceId)
+				await bleStore.init()
+				await bleStore.connectDevice(this.deviceId)
 				this.connected = true
 				this.$refs.ayToast.success('连接成功')
 			} catch (e) {
@@ -147,8 +183,9 @@ export default {
 			}
 		},
 		async disconnectDevice() {
+			const bleStore = useBleStore()
 			try {
-				await bleManager.disconnect()
+				await bleStore.disconnect()
 				this.connected = false
 				this.$refs.ayToast.success('已断开连接')
 			} catch (e) {
@@ -156,6 +193,7 @@ export default {
 			}
 		},
 		delDevice() {
+			const bleStore = useBleStore()
 			uni.showModal({
 				title: '提示',
 				content: '此操作将删除该设备,是否继续?',
@@ -163,9 +201,8 @@ export default {
 				confirmText: '继续',
 				success: (res) => {
 					if (res.confirm) {
-						// 断开连接
 						if (this.connected) {
-							bleManager.disconnect().catch(() => {})
+							bleStore.disconnect().catch(() => {})
 						}
 						// 从本地存储中移除
 						try {
@@ -312,6 +349,47 @@ export default {
 	font-size: 22rpx;
 }
 
+.signal-value-wrap {
+	flex-direction: row;
+	align-items: center;
+	padding-right: 20rpx;
+}
+
+.signal-wrap {
+	flex-direction: row;
+	align-items: flex-end;
+	margin-right: 12rpx;
+	height: 38rpx;
+	padding-bottom: 4rpx;
+}
+
+.signal-bar {
+	width: 8rpx;
+	margin-left: 4rpx;
+	border-radius: 4rpx;
+	background-color: #ddd;
+}
+
+.signal-bar1 {
+	height: 14rpx;
+}
+
+.signal-bar2 {
+	height: 22rpx;
+}
+
+.signal-bar3 {
+	height: 30rpx;
+}
+
+.signal-bar4 {
+	height: 38rpx;
+}
+
+.signal-active {
+	background-color: #389588;
+}
+
 .arrowright {
 	width: 40rpx;
 	height: 40rpx;

+ 22 - 10
code/ajyApp/pages/device/deviceInfo/deviceInfo.nvue

@@ -322,7 +322,8 @@ import bleMixin from './mixins/ble-mixin.js'
 import audioMixin from './mixins/audio-mixin.js'
 import acupointMixin from './mixins/acupoint-mixin.js'
 import planMixin from './mixins/plan-mixin.js'
-import bleManager, { CHAIR_ANGLE } from '@/utils/ble'
+import { useBleStore } from '@/stores/ble'
+import { CHAIR_ANGLE } from '@/utils/ble/constants.js'
 
 export default {
 	components: {
@@ -337,19 +338,30 @@ export default {
 			deviceId: '',
 			deviceName: '',
 			rssi: '',
-			subTime: '00:00:00',
 			isShowDrawer2: false,
-			modeType: 0, // 0无艾灸 1专业 2自定义 3专家
 			modelist: ['无艾灸模式', '专业模式', '自定义模式', '专家模式'],
-			chairAngle: 90,
-			otherSetting: {
-				aijiuNum: '0',
-				lvxinNum: '0',
-				huishouNum: '0'
-			},
 			currentUserName: ''
 		}
 	},
+	computed: {
+		_bleStore() {
+			return useBleStore()
+		},
+		subTime() {
+			return this._bleStore.subTime
+		},
+		modeType: {
+			get() { return this._bleStore.modeType },
+			set(val) { this._bleStore.modeType = val }
+		},
+		chairAngle: {
+			get() { return this._bleStore.chairAngle },
+			set(val) { this._bleStore.chairAngle = val }
+		},
+		otherSetting() {
+			return this._bleStore.otherSetting
+		}
+	},
 	onLoad(options) {
 		const sysInfo = uni.getSystemInfoSync()
 		this.statusBarHeight = sysInfo.statusBarHeight || 44
@@ -412,7 +424,7 @@ export default {
 			}
 			const level = angleMap[this.chairAngle] || CHAIR_ANGLE.LEVEL_1
 			if (this.linked) {
-				bleManager.setChairAngle(level).catch(e => console.error('setAngle fail', e))
+				this._bleStore.setChairAngle(level).catch(e => console.error('setAngle fail', e))
 			}
 		}
 	}

+ 88 - 146
code/ajyApp/pages/device/deviceInfo/mixins/ble-mixin.js

@@ -1,29 +1,55 @@
 /**
  * BLE 蓝牙连接与设备控制 mixin
+ * 基于 Pinia Store 管理蓝牙单例状态
  * 负责:权限检查、连接/断连/重连、指令收发、设备状态管理
  */
-import bleManager, { BLE_STATE, MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble'
+import { useBleStore } from '@/stores/ble'
+import { MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble/constants.js'
 import { ensureBlePrerequisite } from '@/utils/ble/permission.js'
 
 export default {
 	data() {
 		return {
-			linked: false,
-			deviceStatus: 0, // 0停止 1预热 2点火 3艾灸 4灭火 5暂停
-			hotPercentage: '0%',
-			ispreHot: false,
-			reconnectDrawer: false,
-			reconnectCount: 0,
-			excepDrawer: false,
-			exceTxt: 0,
-			isShowConfirm: false,
-			_unbinders: []
+			isShowConfirm: false
+		}
+	},
+	computed: {
+		bleStore() {
+			return useBleStore()
+		},
+		linked() {
+			return this.bleStore.linked
+		},
+		deviceStatus: {
+			get() { return this.bleStore.deviceStatus },
+			set(val) { this.bleStore.deviceStatus = val }
+		},
+		hotPercentage() {
+			return this.bleStore.hotPercentage
+		},
+		ispreHot: {
+			get() { return this.bleStore.ispreHot },
+			set(val) { this.bleStore.ispreHot = val }
+		},
+		reconnectDrawer() {
+			return this.bleStore.reconnectDrawer
+		},
+		reconnectCount() {
+			return this.bleStore.reconnectCount
+		},
+		excepDrawer: {
+			get() { return this.bleStore.excepDrawer },
+			set(val) { this.bleStore.excepDrawer = val }
+		},
+		exceTxt: {
+			get() { return this.bleStore.exceTxt },
+			set(val) { this.bleStore.exceTxt = val }
 		}
 	},
 	methods: {
 		// ========== BLE 初始化 ==========
 		async initBle() {
-			// 1. Android权限检查(iOS由系统自动弹窗,无需手动处理)
+			// 1. Android权限检查
 			try {
 				await ensureBlePrerequisite()
 			} catch (e) {
@@ -37,135 +63,34 @@ export default {
 				}
 			}
 
-			// 2. 重置BLE适配器,清除search页面可能残留的扫描状态
-			try { await bleManager.destroy() } catch (_) {}
+			// 2. 断开旧连接并关闭适配器(不解绑全局监听器)
+			try { await this.bleStore.disconnect() } catch (_) {}
+			try { await this.bleStore.resetAdapter() } catch (_) {}
 
-			// 3. 重新初始化适配器
-			await bleManager.init()
-
-			// 4. 监听BLE事件(必须在destroy之后注册,因为destroy会清除所有监听器)
-			this._unbinders = [
-				bleManager.on('state', (s) => {
-					this.linked = (s === BLE_STATE.READY_COMM)
-				}),
-				bleManager.on('disconnected', (info) => {
-					this.linked = false
-					if (!info.manual) {
-						this.reconnectDrawer = true
-					}
-				}),
-				bleManager.on('reconnecting', ({ count }) => {
-					this.reconnectCount = count
-					this.reconnectDrawer = true
-				}),
-				bleManager.on('reconnected', () => {
-					this.reconnectDrawer = false
-					this.reconnectCount = 0
-					this.linked = true
-					this.$refs.ayToast.success('重连成功')
-				}),
-				bleManager.on('reconnectFailed', () => {
-					this.reconnectDrawer = false
-					this.$refs.ayToast.error('连接失败,请返回重试')
-					setTimeout(() => {
-						uni.navigateBack()
-					}, 1500)
-				}),
-				bleManager.on('report:GROUP_1', (data) => {
-					this._handleGroup1(data)
-				}),
-				bleManager.on('report:GROUP_2', (data) => {
-					this._handleGroup2(data)
-				})
-			]
-
-			// 5. 扫描并连接设备(与ble-demo一致的可靠方式:先扫描发现设备,再连接)
+			// 3. 扫描并连接设备(优先直连,跳过扫描避免 Android 限流)
 			try {
 				const scanOpt = { timeout: 8000 }
+				// 已有 deviceId 时优先直连,不需要扫描
+				if (this.deviceId) {
+					scanOpt.deviceId = this.deviceId
+				}
 				if (this.deviceName) {
 					scanOpt.deviceName = this.deviceName
 				}
-				await bleManager.scanAndConnect(scanOpt)
-				this.linked = true
+				await this.bleStore.scanAndConnect(scanOpt)
+				// BLE连接建立后需短暂等待设备就绪再发首条指令
+				await new Promise(r => setTimeout(r, 1500))
 				this._sendCurrentState()
 			} catch (e) {
 				console.error('BLE连接失败', e)
-				this.$refs.ayToast.error('连接失败: ' + (e.message || e.code || ''))
-			}
-		},
-
-		_cleanListeners() {
-			if (this._unbinders && this._unbinders.length) {
-				this._unbinders.forEach(fn => fn && fn())
-				this._unbinders = []
-			}
-		},
-
-		// ========== 设备上报数据解析 ==========
-		_handleGroup1(data) {
-			// 参数组1: 设备状态
-			switch (data.runtimeState) {
-				case 0x00:
-					this.deviceStatus = 0
-					break
-				case 0x01:
-				case 0x02:
-					this.deviceStatus = 1
-					break
-				case 0x03:
-					this.deviceStatus = 3
-					break
-				case 0x04:
-					this.deviceStatus = 4
-					break
-				case 0x05:
-					this.deviceStatus = 5
-					break
-			}
-			// 模式
-			switch (data.mode) {
-				case MODE.LEISURE:
-					this.modeType = 0; break
-				case MODE.PROFESSIONAL:
-					this.modeType = 1; break
-				case MODE.PERSONAL:
-					this.modeType = 2; break
-				case MODE.EXPERT:
-					this.modeType = 3; break
-			}
-			// 剩余时间
-			const m = data.remainMinute || 0
-			const s = data.remainSecond || 0
-			const h = Math.floor(m / 60)
-			const m1 = m % 60
-			this.subTime = `${h.toString().padStart(2,'0')}:${m1.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`
-
-			// 异常检测
-			if (data.foreignDetect === 0x02) {
-				this.excepDrawer = true
-				this.exceTxt = 1
-			}
-			// 耗材状态
-			this.otherSetting.aijiuNum = String(data.consumable || 0)
-			this.otherSetting.lvxinNum = String(data.filterPercent || 0)
-			this.otherSetting.huishouNum = String(data.recycleBinPct || 0)
-		},
-
-		_handleGroup2(data) {
-			// 参数组2: 预热/点火进度
-			if (data.preheatPercent < 100) {
-				this.ispreHot = false
-				this.hotPercentage = data.preheatPercent + '%'
-			} else {
-				this.ispreHot = true
-				this.hotPercentage = data.ignitePercent + '%'
+				this.$refs.ayToast.error(this._bleFriendlyMsg(e))
 			}
 		},
 
 		// ========== 发送指令 ==========
 		_sendCurrentState() {
 			const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
-			bleManager.sendBasic({
+			this.bleStore.sendBasic({
 				power: POWER.ON,
 				moxiState: MOXI_STATE.DONE,
 				preheatState: MOXI_STATE.DONE,
@@ -180,15 +105,14 @@ export default {
 
 		// ========== 开始艾灸 ==========
 		startDeviceEvt() {
-			this.deviceStatus = 1
-			this.ispreHot = false
+			this.bleStore.deviceStatus = 1
+			this.bleStore.ispreHot = false
 			let totalDuration = 0
 			this.curCase.forEach(item => {
 				totalDuration += item.time
 			})
-			// 发送开始指令
 			const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
-			bleManager.sendBasic({
+			this.bleStore.sendBasic({
 				power: POWER.ON,
 				moxiState: MOXI_STATE.START,
 				preheatState: MOXI_STATE.START,
@@ -200,27 +124,24 @@ export default {
 				duration: totalDuration || 30
 			}).catch(e => console.error('startDevice fail', e))
 
-			// 发送穴位坐标
 			if (this.curCase.length > 0) {
 				const points = this.curCase.map(item => ({
 					point: item.id,
 					x: item._x,
 					y: item._y
 				}))
-				bleManager.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e))
+				this.bleStore.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e))
 			}
 		},
 
 		// ========== 暂停/继续 ==========
 		stopAijiu() {
 			if (this.deviceStatus == 5) {
-				// 继续
-				this.deviceStatus = 3
-				bleManager.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
+				this.bleStore.deviceStatus = 3
+				this.bleStore.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
 			} else if (this.deviceStatus == 3) {
-				// 暂停
-				this.deviceStatus = 5
-				bleManager.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
+				this.bleStore.deviceStatus = 5
+				this.bleStore.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
 			}
 		},
 
@@ -229,9 +150,9 @@ export default {
 			this.isShowConfirm = true
 		},
 		confirmStop() {
-			this.deviceStatus = 0
+			this.bleStore.deviceStatus = 0
 			this.isShowConfirm = false
-			bleManager.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
+			this.bleStore.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
 		},
 
 		// ========== 模式切换 ==========
@@ -243,15 +164,14 @@ export default {
 				confirmText: '继续',
 				success: (res) => {
 					if (res.confirm) {
-						this.modeType = num
-						this.deviceStatus = 0
+						this.bleStore.modeType = num
+						this.bleStore.deviceStatus = 0
 						this.isShowDrawer2 = false
 						if (this._stopAudioOnModeChange) {
 							this._stopAudioOnModeChange()
 						}
-						// 发送模式切换指令
 						const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
-						bleManager.sendBasic({
+						this.bleStore.sendBasic({
 							power: POWER.ON,
 							moxiState: MOXI_STATE.DONE,
 							preheatState: MOXI_STATE.DONE,
@@ -269,8 +189,30 @@ export default {
 
 		// ========== 断开BLE ==========
 		disconnectBle() {
-			this._cleanListeners()
-			bleManager.disconnect().catch(() => {})
+			// 先停止扫描(防止页面销毁后 onBluetoothDeviceFound 回调找不到 taskCenter)
+			this.bleStore.stopScan()
+			this.bleStore.disconnect().catch(() => {})
+		},
+
+		// ========== BLE错误码转用户友好提示 ==========
+		_bleFriendlyMsg(e) {
+			const code = e && (e.code || e.message || '')
+			const originMsg = e && e.origin && e.origin.msg
+			const map = {
+				BLE_SCAN_FAIL: this.bleStore.scanThrottled
+					? '扫描过于频繁,请等待30秒后再试'
+					: (originMsg || '未搜索到设备,请确保设备已开机并靠近手机'),
+				BLE_CONNECT_FAIL: '连接设备失败,请确保设备在范围内并重试',
+				BLE_CONNECT_TIMEOUT: '连接超时,请靠近设备后重试',
+				BLE_ADAPTER_OFF: '蓝牙未开启,请先开启蓝牙',
+				BLE_PERMISSION_DENIED: '蓝牙权限未授予,请在设置中开启',
+				BLE_LOCATION_OFF: '位置服务未开启,请开启后重试',
+				BLE_DISCONNECTED: '设备已断开连接',
+				BLE_SERVICE_NOT_FOUND: '设备服务异常,请重启设备后重试',
+				BLE_WRITE_FAIL: '指令发送失败,请重试',
+				BLE_BUSY: '设备正忙,请稍后再试'
+			}
+			return map[code] || ('连接失败,请重试 (' + code + ')')
 		}
 	}
 }

+ 26 - 0
code/ajyApp/pages/device/list/list.nvue

@@ -32,6 +32,7 @@
 
 <script>
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
+import { useBleStore } from '@/stores/ble'
 
 export default {
 	components: {
@@ -47,6 +48,7 @@ export default {
 		const sysInfo = uni.getSystemInfoSync()
 		this.statusBarHeight = sysInfo.statusBarHeight || 44
 		this.init()
+		this.updateConnectedDeviceRSSI()
 	},
 	onPullDownRefresh() {
 		uni.stopPullDownRefresh()
@@ -75,6 +77,30 @@ export default {
 			uni.navigateTo({
 				url: '/pages/device/deviceInfo/deviceInfo?name=' + encodeURIComponent(item.name || '') + '&deviceId=' + encodeURIComponent(item.deviceId || '') + '&rssi=' + encodeURIComponent(item.RSSI || '')
 			})
+		},
+		/** 如果设备已连接,实时获取RSSI并更新缓存 */
+		updateConnectedDeviceRSSI() {
+			const bleStore = useBleStore()
+			if (!bleStore.linked || !bleStore.device || !bleStore.device.deviceId) return
+			const connectedDeviceId = bleStore.device.deviceId
+			uni.getBLEDeviceRSSI({
+				deviceId: connectedDeviceId,
+				success: (res) => {
+					console.log('实时RSSI更新:', res.RSSI)
+					// 更新列表中对应设备的RSSI
+					const idx = this.list.findIndex(item => item.deviceId === connectedDeviceId)
+					if (idx !== -1) {
+						this.list[idx].RSSI = res.RSSI
+						// 同步更新到本地缓存
+						try {
+							uni.setStorageSync('deviceList', this.list)
+						} catch (e) {}
+					}
+				},
+				fail: (err) => {
+					console.log('获取RSSI失败:', err)
+				}
+			})
 		}
 	}
 }

+ 64 - 68
code/ajyApp/pages/device/search/search.nvue

@@ -17,6 +17,11 @@
 			<text class="search-btn" @click="onRefresh">{{searching ? '停止' : '重新搜索'}}</text>
 		</view>
 
+		<!-- 扫描限流警告 -->
+		<view class="throttle-warning" v-if="scanThrottled">
+			<text class="throttle-text">扫描过于频繁,可能无法搜索到设备,请稍等片刻再试</text>
+		</view>
+
 		<!-- 设备列表 -->
 		<scroll-view class="device-scroll" scroll-y="true">
 			<view class="device-item" v-for="(item, index) in deviceList" :key="index" @click="onSelectDevice(item)">
@@ -46,9 +51,8 @@
 </template>
 
 <script>
-import bleManager, {
-	BLE_STATE, ensureBlePrerequisite, openLocationSettings
-} from '@/utils/ble'
+import { useBleStore } from '@/stores/ble'
+import { ensureBlePrerequisite, openLocationSettings, openBluetoothSettings } from '@/utils/ble/permission.js'
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
 
 export default {
@@ -57,65 +61,47 @@ export default {
 	},
 	data() {
 		return {
-			statusBarHeight: 44,
-			searching: false,
-			deviceList: [],
-			_unbinders: []
+			statusBarHeight: 44
+		}
+	},
+	computed: {
+		bleStore() {
+			return useBleStore()
+		},
+		searching() {
+			return this.bleStore.searching
+		},
+		deviceList() {
+			return this.bleStore.scannedDevices
+		},
+		scanThrottled() {
+			return this.bleStore.scanThrottled
 		}
 	},
 	onLoad() {
 		const sysInfo = uni.getSystemInfoSync()
 		this.statusBarHeight = sysInfo.statusBarHeight || 44
 
-		// 配置并监听事件
-		bleManager.configure({ debug: true })
-
-		this._unbinders = [
-			bleManager.on('state', s => {
-				this.searching = (s === BLE_STATE.SCANNING)
-			}),
-			bleManager.on('deviceFound', d => {
-				// 避免重复
-				const exists = this.deviceList.find(item => item.deviceId === d.deviceId)
-				if (!exists) {
-					this.deviceList.push({
-						deviceId: d.deviceId,
-						name: d.name || d.localName || '',
-						RSSI: d.RSSI || ''
-					})
-				}
-			})
-		]
-
+		// 配置并开始扫描
+		this.bleStore.configure({ debug: true })
 		this.startScan()
 	},
 	onUnload() {
-		// 离开页面停止扫描
-		this.stopScan()
-		if (this._unbinders && this._unbinders.length) {
-			this._unbinders.forEach(fn => fn && fn())
-		}
+		// 离开页面才真正停止底层BLE发现
+		this.bleStore.stopScan()
 	},
 	methods: {
 		goBack() {
 			uni.navigateBack()
 		},
 		async startScan() {
-			this.deviceList = []
 			try {
 				await ensureBlePrerequisite()
-				await bleManager.init()
-				// 使用returnAll模式,超时后resolve所有设备
-				bleManager.scan({ timeout: 15000, returnAll: true }).then(devices => {
-					// 扫描结束
-					this.searching = false
-				}).catch(e => {
-					this.searching = false
-					console.error('扫描异常', e)
-				})
+				// startScan 内部自动初始化适配器,扫描结果自动更新 store.scannedDevices
+				await this.bleStore.startScan({ timeout: 15000, returnAll: true })
 			} catch (e) {
-				this.searching = false
-				if (e.message === 'BLE_LOCATION_OFF') {
+				const msg = e && (e.message || e.code || '')
+				if (msg === 'BLE_LOCATION_OFF') {
 					uni.showModal({
 						title: '提示',
 						content: '请先开启位置服务以搜索蓝牙设备',
@@ -123,43 +109,42 @@ export default {
 							if (r.confirm) openLocationSettings()
 						}
 					})
-				} else if (e.message === 'BLE_ADAPTER_OFF') {
-					this.$refs.ayToast.error('请先开启蓝牙')
-				} else if (e.message === 'BLE_PERMISSION_DENIED') {
+				} else if (msg === 'BLE_ADAPTER_OFF' || msg === 'BLE_ADAPTER_OFF') {
+					uni.showModal({
+						title: '蓝牙未开启',
+						content: '请先开启手机蓝牙,才能搜索和连接艾灸椅设备',
+						confirmText: '去设置',
+						cancelText: '取消',
+						success: r => {
+							if (r.confirm) openBluetoothSettings()
+						}
+					})
+				} else if (msg === 'BLE_PERMISSION_DENIED') {
 					this.$refs.ayToast.error('请授权蓝牙权限')
 				} else {
-					this.$refs.ayToast.error('扫描失败: ' + (e.message || ''))
+					console.error('扫描异常', e)
+					this.$refs.ayToast.error('扫描失败: ' + (msg || ''))
 				}
 			}
 		},
-		stopScan() {
-			try {
-				uni.stopBluetoothDevicesDiscovery({
-					success() {},
-					fail() {}
-				})
-			} catch (e) {}
-		},
 		onRefresh() {
 			if (this.searching) {
-				this.stopScan()
-				this.searching = false
+				this.bleStore.stopScan()
 			} else {
 				this.startScan()
 			}
 		},
 		getSignalLevel(rssi) {
-			// RSSI转信号格数: 4格(强) / 3格 / 2格 / 1格(弱)
+			console.log("信号",rssi)
 			if (!rssi) return 0
 			const val = Number(rssi)
-			if (val >= -50) return 4   // 非常强
-			if (val >= -65) return 3   // 强
-			if (val >= -80) return 2   // 中
-			if (val >= -95) return 1   // 弱
+			if (val >= -50) return 4
+			if (val >= -65) return 3
+			if (val >= -80) return 2
+			if (val >= -95) return 1
 			return 1
 		},
 		onSelectDevice(device) {
-			// 将设备添加到本地存储的设备列表
 			let deviceList = []
 			try {
 				const data = uni.getStorageSync('deviceList')
@@ -168,14 +153,12 @@ export default {
 				}
 			} catch (e) {}
 
-			// 检查是否已添加
 			const exists = deviceList.find(item => item.deviceId === device.deviceId)
 			if (exists) {
 				this.$refs.ayToast.show('该设备已添加')
 				return
 			}
 
-			// 添加设备
 			deviceList.push({
 				deviceId: device.deviceId,
 				name: device.name || '艾灸椅',
@@ -184,9 +167,10 @@ export default {
 			uni.setStorageSync('deviceList', deviceList)
 			this.$refs.ayToast.success('设备添加成功')
 
-			// 延迟返回
 			setTimeout(() => {
-				uni.navigateBack()
+				uni.redirectTo({
+					url: `/pages/device/deviceInfo/deviceInfo?deviceId=${encodeURIComponent(device.deviceId)}&name=${encodeURIComponent(device.name || '艾灸椅')}&rssi=${device.RSSI || ''}`
+				})
 			}, 1000)
 		}
 	}
@@ -331,4 +315,16 @@ export default {
 	font-size: 28rpx;
 	color: #999;
 }
+.throttle-warning {
+	background-color: #FFF3E0;
+	padding: 16rpx 38rpx;
+	margin-left: 38rpx;
+	margin-right: 38rpx;
+	border-radius: 12rpx;
+	margin-bottom: 16rpx;
+}
+.throttle-text {
+	font-size: 24rpx;
+	color: #E65100;
+}
 </style>

+ 2 - 0
code/ajyApp/utils/ble/BleManager.js

@@ -78,6 +78,7 @@ class BleManager extends EventEmitter {
   /** 打开蓝牙适配器 */
   async init() {
     if (this._state !== BLE_STATE.IDLE && this._state !== BLE_STATE.DISCONNECTED) {
+		console.log("蓝牙已经初始化不用重复初始化")
       return
     }
     try {
@@ -90,6 +91,7 @@ class BleManager extends EventEmitter {
     }
     this._bindSystemListeners()
     this._setState(BLE_STATE.READY)
+	console.log("蓝牙始化")
   }
 
   /** 彻底释放 */

+ 6 - 16
code/ajyApp/utils/ble/index.js

@@ -1,25 +1,15 @@
 /**
- * BLE 模块出口 —— 直接拿到单例
+ * BLE 模块出口
  *
- * 用法:
- *   import bleManager, { BLE_STATE, BLE_ERROR, MODE, TEMPERATURE } from '@/utils/ble'
+ * 蓝牙状态管理已迁移至 Pinia Store,请使用:
+ *   import { useBleStore } from '@/stores/ble'
+ *   const bleStore = useBleStore()
  *
- *   bleManager.configure({ deviceNamePrefix: 'AJY-', debug: true })
- *   await bleManager.init()
- *   const dev = await bleManager.scanAndConnect()
- *   bleManager.on('report:GROUP_1', data => console.log('参数组1', data))
- *   bleManager.on('report:GROUP_2', data => console.log('参数组2', data))
- *   await bleManager.startMoxi({ mode: 2, subMode: 1, temperature: 2, duration: 30 })
+ * 本文件仅导出协议常量、编解码工具、权限检查等基础工具
  */
 
-import BleManager from './BleManager.js'
-
 export * from './constants.js'
 export * from './protocol.js'
 export * from './permission.js'
 export { default as EventEmitter } from './EventEmitter.js'
-export { BleManager }
-
-// 单例导出
-const bleManager = BleManager.getInstance()
-export default bleManager
+export { default as logger } from './logger.js'

+ 52 - 2
code/ajyApp/utils/ble/permission.js

@@ -99,13 +99,63 @@ export function requestAndroidPermissions() {
   })
 }
 
-/** 一键: 申请权限 + 校验定位服务. 失败抛异常 */
+/** 检查系统蓝牙是否已开启 (Android 使用原生 API,iOS 依赖 uni API) */
+export function isBluetoothEnabled() {
+  return new Promise(resolve => {
+    const platform = getPlatform()
+    if (!isAppPlus()) return resolve(true)
+    // #ifdef APP-PLUS
+    if (platform === 'android') {
+      try {
+        const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')
+        const adapter = BluetoothAdapter.getDefaultAdapter()
+        resolve(adapter != null && adapter.isEnabled())
+      } catch (e) {
+        // 无法检测时放行,让后续流程处理
+        resolve(true)
+      }
+    } else if (platform === 'ios') {
+      // iOS 无法直接通过原生 API 检测,依赖 uni.openBluetoothAdapter 的结果
+      resolve(true)
+    } else {
+      resolve(true)
+    }
+    // #endif
+    // #ifndef APP-PLUS
+    resolve(true)
+    // #endif
+  })
+}
+
+/** 跳转到系统蓝牙设置页面 */
+export function openBluetoothSettings() {
+  // #ifdef APP-PLUS
+  const platform = getPlatform()
+  if (platform === 'android') {
+    try {
+      const main = plus.android.runtimeMainActivity()
+      const Intent = plus.android.importClass('android.content.Intent')
+      const Settings = plus.android.importClass('android.provider.Settings')
+      const intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
+      main.startActivity(intent)
+    } catch (e) { console.error('跳转蓝牙设置失败', e) }
+  } else if (platform === 'ios') {
+    plus.runtime.openURL('App-Prefs:root=Bluetooth')
+  }
+  // #endif
+}
+
+/** 一键: 申请权限 + 校验定位服务 + 检测蓝牙开启. 失败抛异常 */
 export async function ensureBlePrerequisite() {
-  if (getPlatform() === 'android') {
+  const platform = getPlatform()
+  if (platform === 'android') {
     const ok = await requestAndroidPermissions()
     if (!ok) throw new Error('BLE_PERMISSION_DENIED')
     const loc = await isLocationEnabled()
     if (!loc) throw new Error('BLE_LOCATION_OFF')
   }
+  // 检测蓝牙是否开启(Android 原生检测,iOS 跳过此步)
+  const bleOn = await isBluetoothEnabled()
+  if (!bleOn) throw new Error('BLE_ADAPTER_OFF')
   return true
 }