Przeglądaj źródła

Merge branch 'refs/heads/dev' into jiapu_aijiuyi

# Conflicts:
#	code/ajyApp/pages/device/deviceInfo/mixins/ble-mixin.js
#	code/ajyApp/pages/device/list/list.nvue
#	code/ajyApp/pages/device/search/search.nvue
jiapu 3 miesięcy temu
rodzic
commit
7bc5371e9c

+ 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

+ 360 - 0
code/ajyApp/package-lock.json

@@ -0,0 +1,360 @@
+{
+  "name": "ajyapp",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "ajyapp",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "pinia": "^2.1.7"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
+      "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/shared": "3.5.35",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
+      "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-core": "3.5.35",
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
+      "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/compiler-core": "3.5.35",
+        "@vue/compiler-dom": "3.5.35",
+        "@vue/compiler-ssr": "3.5.35",
+        "@vue/shared": "3.5.35",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.15",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
+      "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.35",
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
+      "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
+      "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.5.35",
+        "@vue/shared": "3.5.35"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
+      "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.5.35",
+        "@vue/runtime-core": "3.5.35",
+        "@vue/shared": "3.5.35",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
+      "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.35",
+        "@vue/shared": "3.5.35"
+      },
+      "peerDependencies": {
+        "vue": "3.5.35"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
+      "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "peer": true,
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.12",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC",
+      "peer": true
+    },
+    "node_modules/pinia": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+      "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.4.4",
+        "vue": "^2.7.0 || ^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.15",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+      "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "nanoid": "^3.3.12",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.35",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
+      "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.35",
+        "@vue/compiler-sfc": "3.5.35",
+        "@vue/runtime-dom": "3.5.35",
+        "@vue/server-renderer": "3.5.35",
+        "@vue/shared": "3.5.35"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 14 - 0
code/ajyApp/package.json

@@ -0,0 +1,14 @@
+{
+  "name": "ajyapp",
+  "version": "1.0.0",
+  "description": "",
+  "main": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+  }
+}

+ 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: {
@@ -338,19 +339,30 @@ export default {
 			deviceCode: '',
 			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
@@ -426,7 +438,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))
 			}
 		}
 	}

+ 89 - 216
code/ajyApp/pages/device/deviceInfo/mixins/ble-mixin.js

@@ -1,30 +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'
-import { selectDevice } from '@/utils/api/device'
 
 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) {
@@ -38,135 +63,34 @@ export default {
 				}
 			}
 
-			// 2. 重置BLE适配器,清除search页面可能残留的扫描状态
-			try { await bleManager.destroy() } catch (_) {}
-
-			// 3. 重新初始化适配器
-			await bleManager.init()
+			// 2. 断开旧连接并关闭适配器(不解绑全局监听器)
+			try { await this.bleStore.disconnect() } catch (_) {}
+			try { await this.bleStore.resetAdapter() } catch (_) {}
 
-			// 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,19 +104,15 @@ export default {
 		},
 
 		// ========== 开始艾灸 ==========
-		async startDeviceEvt() {
-			const ready = await this.ensureMoxibustionReady()
-			if (!ready) return
-
-			this.deviceStatus = 1
-			this.ispreHot = false
+		startDeviceEvt() {
+			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,
@@ -204,92 +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))
 			}
 		},
 
-		async ensureMoxibustionReady() {
-			const token = uni.getStorageSync('user_token')
-			if (!token) {
-				this.$refs.ayToast.error('请先登录')
-				uni.navigateTo({ url: '/pages/login/login' })
-				return false
-			}
-
-			const currentDevice = uni.getStorageSync('current_device') || {}
-			const currentUser = uni.getStorageSync('current_manage_user') || {}
-			const deviceCode = currentDevice.deviceCode || this.deviceCode || this.deviceName || this.deviceId
-			if (!deviceCode) {
-				this.$refs.ayToast.error('请先选择设备')
-				return false
-			}
-
-			const profileId = currentUser.profileId || currentUser.id || currentDevice.profileId
-			if (!profileId) {
-				this.$refs.ayToast.error('请先选择用户')
-				uni.navigateTo({ url: '/pages/device/userManage/userManage' })
-				return false
-			}
-
-			let selected
-			try {
-				selected = await selectDevice({
-					deviceCode,
-					groupId: currentDevice.groupId || null,
-					profileId
-				})
-			} catch (e) {
-				console.error('设备使用权限校验失败', e)
-				return false
-			}
-
-			const profile = selected.profile || currentUser
-			uni.setStorageSync('current_device', { ...currentDevice, ...selected })
-			uni.setStorageSync('current_manage_user', profile)
-			uni.setStorageSync('current_manage_user_id', profile.profileId || profile.id)
-
-			if (!this.isProfileCompleteForMoxibustion(profile)) {
-				this.$refs.ayToast.error('请先补全用户资料')
-				setTimeout(() => {
-					uni.navigateTo({
-						url: '/pages/device/bodyParams/bodyParams?mode=edit&userId=' + encodeURIComponent(profile.profileId || profile.id || '')
-					})
-				}, 600)
-				return false
-			}
-			return true
-		},
-
-		isProfileCompleteForMoxibustion(profile) {
-			if (!profile) return false
-			const bodyHeight = profile.bodyHeight || profile.shoulderHeight
-			return !!(
-				profile.name &&
-				profile.gender &&
-				profile.age &&
-				profile.shoulderWidth &&
-				bodyHeight &&
-				profile.spineLength
-			)
-		},
-
 		// ========== 暂停/继续 ==========
 		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(() => {})
 			}
 		},
 
@@ -298,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(() => {})
 		},
 
 		// ========== 模式切换 ==========
@@ -312,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,
@@ -338,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 + ')')
 		}
 	}
 }

+ 30 - 42
code/ajyApp/pages/device/list/list.nvue

@@ -32,7 +32,7 @@
 
 <script>
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
-import { getDeviceList, selectDevice } from '@/utils/api/device'
+import { useBleStore } from '@/stores/ble'
 
 export default {
 	components: {
@@ -48,27 +48,15 @@ export default {
 		const sysInfo = uni.getSystemInfoSync()
 		this.statusBarHeight = sysInfo.statusBarHeight || 44
 		this.init()
+		this.updateConnectedDeviceRSSI()
 	},
 	onPullDownRefresh() {
 		uni.stopPullDownRefresh()
 		this.init()
 	},
 	methods: {
-		async init() {
-			try {
-				const data = await getDeviceList()
-				this.list = (data || []).map(item => ({
-					...item,
-					deviceId: item.deviceCode || item.deviceId,
-					name: item.deviceName || item.name || item.deviceCode,
-					RSSI: item.RSSI || ''
-				}))
-				uni.setStorageSync('deviceList', this.list)
-				return
-			} catch (e) {
-				console.error('加载设备列表失败,使用本地缓存', e)
-			}
-
+		init() {
+			// 从本地存储读取设备列表
 			try {
 				const data = uni.getStorageSync('deviceList')
 				if (data && data.length > 0) {
@@ -85,33 +73,33 @@ export default {
 				url: '/pages/device/search/search'
 			})
 		},
-		async goDeviceInfo(item) {
-			try {
-				const selected = await selectDevice({
-					deviceCode: item.deviceCode || item.deviceId,
-					groupId: item.groupId || null,
-					profileId: item.profileId || null
-				})
-				const merged = {
-					...item,
-					...selected,
-					deviceId: item.deviceId || selected.deviceCode,
-					name: selected.deviceName || item.name
-				}
-				uni.setStorageSync('current_device', merged)
-				if (selected.profile) {
-					uni.setStorageSync('current_manage_user_id', selected.profile.profileId || selected.profile.id)
-					uni.setStorageSync('current_manage_user', selected.profile)
-				}
-			} catch (e) {
-				console.error('选择设备失败', e)
-				return
-			}
+		goDeviceInfo(item) {
 			uni.navigateTo({
-				url: '/pages/device/deviceInfo/deviceInfo?name=' + encodeURIComponent(item.name || '') +
-					'&deviceId=' + encodeURIComponent(item.deviceId || '') +
-					'&deviceCode=' + encodeURIComponent(item.deviceCode || item.deviceId || '') +
-					'&rssi=' + encodeURIComponent(item.RSSI || '')
+				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)
+				}
 			})
 		}
 	}

+ 69 - 100
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,11 +51,9 @@
 </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'
-import { bindDevice } from '@/utils/api/device'
 
 export default {
 	components: {
@@ -58,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: '请先开启位置服务以搜索蓝牙设备',
@@ -124,55 +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
 		},
-		async onSelectDevice(device) {
-			const deviceCode = device.name || device.localName || device.deviceId
-			if (!deviceCode) {
-				this.$refs.ayToast.error('未识别到设备编号')
-				return
-			}
-			let bindResult
-			try {
-				bindResult = await bindDevice({ deviceCode })
-			} catch (e) {
-				console.error('绑定设备失败', e)
-				return
-			}
-
+		onSelectDevice(device) {
 			let deviceList = []
 			try {
 				const data = uni.getStorageSync('deviceList')
@@ -181,39 +153,24 @@ export default {
 				}
 			} catch (e) {}
 
-			// 检查是否已添加
-			const exists = deviceList.find(item =>
-				item.deviceId === device.deviceId || item.deviceCode === bindResult.deviceCode
-			)
+			const exists = deviceList.find(item => item.deviceId === device.deviceId)
 			if (exists) {
 				this.$refs.ayToast.show('该设备已添加')
 				return
 			}
 
-			const newDevice = {
+			deviceList.push({
 				deviceId: device.deviceId,
-				bleDeviceName: device.name || '',
-				name: bindResult.deviceName || device.name || '艾灸椅',
-				deviceCode: bindResult.deviceCode || deviceCode,
-				deviceType: bindResult.deviceType || 1,
-				groupId: bindResult.groupId || null,
-				profileId: bindResult.profileId || null,
-				profileComplete: !!bindResult.profileComplete,
-				profile: bindResult.profile || null,
+				name: device.name || '艾灸椅',
 				RSSI: device.RSSI
-			}
-			deviceList.push(newDevice)
+			})
 			uni.setStorageSync('deviceList', deviceList)
-			uni.setStorageSync('current_device', newDevice)
-			if (bindResult.profile) {
-				uni.setStorageSync('current_manage_user_id', bindResult.profile.profileId || bindResult.profile.id)
-				uni.setStorageSync('current_manage_user', bindResult.profile)
-			}
 			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)
 		}
 	}
@@ -358,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>

+ 798 - 0
code/ajyApp/stores/ble.js

@@ -0,0 +1,798 @@
+/**
+ * 艾灸椅 BLE Pinia Store
+ * 真正的应用级单例,跨页面共享蓝牙状态
+ *
+ * 用法:
+ *   import { useBleStore } from '@/stores/ble'
+ *   const bleStore = useBleStore()
+ *
+ *   // 扫描
+ *   await bleStore.startScan({ timeout: 15000, returnAll: true })
+ *
+ *   // 连接
+ *   await bleStore.connectDevice(deviceId)
+ *
+ *   // 发送指令
+ *   await bleStore.sendBasic({ power: 1, moxiState: 1 })
+ *
+ *   // 读取状态(自动响应式)
+ *   bleStore.linked / bleStore.searching / bleStore.deviceStatus
+ */
+
+import { defineStore } from 'pinia'
+import logger from '@/utils/ble/logger.js'
+import {
+	DEFAULT_CONFIG, BLE_STATE, BLE_ERROR,
+	POWER, MOXI_STATE, MUTE, MODE, SUB_MODE, TEMPERATURE, CHAIR_ANGLE
+} from '@/utils/ble/constants.js'
+import {
+	encodeBasic, encodeModeParam1, encodeModeParam2, encodeAcupoints,
+	bufferToArrayBuffer, arrayBufferToU8, bytesToHex, FrameParser
+} from '@/utils/ble/protocol.js'
+import { ensureBlePrerequisite, openLocationSettings } from '@/utils/ble/permission.js'
+
+// ========== 跨 nvue 页面共享内部变量(通过 globalData 确保单例) ==========
+function _getShared() {
+	const app = getApp()
+	if (!app.globalData) app.globalData = {}
+	if (!app.globalData._bleInternal) {
+		app.globalData._bleInternal = {
+			instanceId: Date.now() + '_' + Math.random().toString(36).slice(2, 6),
+			serviceId: null,
+			writeCharId: null,
+			notifyCharId: null,
+			reconnectTimer: null,
+			reconnectCount: 0,
+			reconnectGeneration: 0,  // 重连代数,用于丢弃僵尸回调
+			writeLock: Promise.resolve(),
+			bound: false,
+			cancelScan: null,
+			scanAborted: false,      // 标记扫描已被外部中止,防止僵尸扫描
+			onDeviceFound: null,     // 当前扫描的设备发现回调
+			scanStartHistory: [],    // 近期 startDiscovery 调用时间戳,用于检测 Android 限流
+			intentionalDisconnect: false, // 标记主动断开,防止系统回调误触发重连
+			parser: null,
+			config: { ...DEFAULT_CONFIG }
+		}
+		console.log(`[BLE Store] 首次创建共享实例,instanceId = ${app.globalData._bleInternal.instanceId}`)
+	}
+	return app.globalData._bleInternal
+}
+
+// 兼容模块加载阶段(getApp()可能未就绪),延迟到首次调用时获取
+let _shared = null
+function _S() {
+	if (!_shared) _shared = _getShared()
+	return _shared
+}
+
+// 便捷访问
+function _instanceId() { return _S().instanceId }
+
+// ========== 工具函数 ==========
+function _invoke(apiFn, params) {
+	return new Promise((resolve, reject) => {
+		apiFn({ ...params, success: resolve, fail: reject, complete: () => {} })
+	})
+}
+
+function _err(code, origin) {
+	const err = new Error(code)
+	err.code = code
+	if (origin) err.origin = origin
+	return err
+}
+
+function _uuidEq(a, b) {
+	return String(a || '').toLowerCase() === String(b || '').toLowerCase()
+}
+
+export const useBleStore = defineStore('ble', {
+	state: () => ({
+		// ===== 连接状态 =====
+		bleState: BLE_STATE.IDLE,
+		device: null, // { deviceId, name, RSSI }
+		linked: false,
+
+		// ===== 蓝牙开关状态 =====
+		bleAdapterOff: false, // 蓝牙适配器未开启,页面可监听此状态展示提示
+
+		// ===== 扫描 =====
+		searching: false,
+		scannedDevices: [],
+		scanThrottled: false, // 检测到 Android 扫描限流时为 true,页面可监听此状态提示用户
+
+		// ===== 设备运行状态(来自上报) =====
+		deviceStatus: 0, // 0停止 1预热 2点火 3艾灸 4灭火 5暂停
+		hotPercentage: '0%',
+		ispreHot: false,
+		subTime: '00:00:00',
+		modeType: 0, // 0无艾灸 1专业 2自定义 3专家
+		chairAngle: 90,
+
+		// ===== 重连 =====
+		reconnectDrawer: false,
+		reconnectCount: 0,
+
+		// ===== 异常 =====
+		excepDrawer: false,
+		exceTxt: 0,
+
+		// ===== 耗材状态 =====
+		otherSetting: {
+			aijiuNum: '0',
+			lvxinNum: '0',
+			huishouNum: '0'
+		}
+	}),
+
+	actions: {
+		// ============== 配置 ==============
+		configure(opt = {}) {
+			const s = _S()
+			s.config = { ...s.config, ...opt }
+			logger.setEnabled(s.config.debug)
+		},
+
+		// ============== 内部状态管理 ==============
+		_setState(s) {
+			if (this.bleState === s) return
+			this.bleState = s
+			this.linked = (s === BLE_STATE.READY_COMM)
+			this.searching = (s === BLE_STATE.SCANNING)
+			logger.info(`[instanceId=${_instanceId()}] state ->`, s)
+		},
+
+		// ============== 初始化 / 释放 ==============
+		async init() {
+			console.log(`[BLE Store] init() called, instanceId = ${_instanceId()}`)
+			if (this.bleState !== BLE_STATE.IDLE && this.bleState !== BLE_STATE.DISCONNECTED) {
+				logger.info('蓝牙已初始化,跳过重复初始化')
+				return
+			}
+			try {
+				await _invoke(uni.openBluetoothAdapter, {})
+				this.bleAdapterOff = false
+			} catch (e) {
+				logger.error('openBluetoothAdapter fail', e)
+				const code = e && (e.errCode || e.code)
+				const isOff = code === 10001
+				if (isOff) {
+					this.bleAdapterOff = true
+					this._showBleOffPrompt('请先开启手机蓝牙,才能连接艾灸椅设备')
+				}
+				throw _err(isOff ? BLE_ERROR.ADAPTER_OFF : BLE_ERROR.NOT_SUPPORT, e)
+			}
+			this._bindSystemListeners()
+			this._setState(BLE_STATE.READY)
+			logger.info('蓝牙适配器已初始化')
+		},
+
+		async destroy() {
+			this._clearReconnect()
+			const s = _S()
+			if (s.cancelScan) { s.cancelScan(); s.cancelScan = null }
+			s.onDeviceFound = null
+			try { await this.disconnect() } catch (_) {}
+			try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
+			this._unbindSystemListeners()
+			this._setState(BLE_STATE.IDLE)
+			if (s.parser) s.parser.reset()
+		},
+
+		/**
+		 * 轻量级重置:只关闭适配器并重置状态,不解绑全局监听器
+		 * 适用于页面切换时清除残留扫描/连接状态
+		 */
+		async resetAdapter() {
+			const s = _S()
+			s.intentionalDisconnect = true
+			this._clearReconnect()
+			if (s.cancelScan) { s.cancelScan(); s.cancelScan = null }
+			s.onDeviceFound = null
+			try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
+			this._setState(BLE_STATE.IDLE)
+			if (s.parser) s.parser.reset()
+			s.intentionalDisconnect = false
+		},
+
+		/**
+		 * 绑定BLE系统监听器 - 必须在 App.vue onLaunch 中调用
+		 * 确保回调绑定到 App.vue 的 taskCenter(永不销毁)
+		 */
+		bindGlobalListeners() {
+			if (_S().bound) return
+			this._bindSystemListeners()
+		},
+
+		_bindSystemListeners() {
+			const s = _S()
+			if (s.bound) return
+
+			// 初始化帧解析器
+			if (!s.parser) {
+				s.parser = new FrameParser(
+					(decoded, frame) => this._onFrame(decoded, frame),
+					(err, frame) => logger.warn('frame parse error', err.message, bytesToHex(frame))
+				)
+			}
+
+			uni.onBluetoothAdapterStateChange(this._onAdapterStateChange = (res) => {
+				logger.info('adapterStateChange', res)
+				const s = _S()
+				if (!res.available) {
+					// 主动关闭适配器时不显示提示且不触发重连
+					if (s.intentionalDisconnect) return
+					this.bleAdapterOff = true
+					this._showBleOffPrompt('蓝牙已关闭,设备连接已断开。请重新开启蓝牙以继续使用')
+					this._handleDisconnected(BLE_ERROR.ADAPTER_OFF)
+				} else {
+					this.bleAdapterOff = false
+					// 蓝牙重新开启:如果之前有连接过的设备且当前处于断连状态,自动发起重连
+					this._onAdapterRestored()
+				}
+			})
+			uni.onBLEConnectionStateChange(this._onConnStateChange = (res) => {
+				logger.info('connectionStateChange', res)
+				if (!res.connected && this.device && res.deviceId === this.device.deviceId) {
+					this._handleDisconnected(BLE_ERROR.DISCONNECTED)
+				}
+			})
+			uni.onBLECharacteristicValueChange(this._onCharChange = (res) => {
+				const u8 = arrayBufferToU8(res.value)
+				logger.log('<=', bytesToHex(u8))
+				if (s.parser) s.parser.feed(u8)
+			})
+			// 永久注册设备发现监听器(不可反复 on/off,否则多次后系统丢失监听)
+			uni.onBluetoothDeviceFound((res) => {
+				if (s.onDeviceFound) s.onDeviceFound(res)
+			})
+			s.bound = true
+			logger.info(`[instanceId=${s.instanceId}] BLE系统监听器已全局绑定(含onBluetoothDeviceFound)`)
+		},
+
+		_unbindSystemListeners() {
+			// 全局监听器不再主动解绑,防止 taskCenter 丢失
+			// 仅在极端情况下(如蓝牙完全不再使用)才解绑
+		},
+
+		// ============== 确保就绪 ==============
+		async _ensureReady() {
+			if (this.bleState === BLE_STATE.IDLE) await this.init()
+			try {
+				const res = await _invoke(uni.getBluetoothAdapterState, {})
+				if (!res.available) {
+					this.bleAdapterOff = true
+					this._showBleOffPrompt('请先开启手机蓝牙,才能连接艾灸椅设备')
+					throw _err(BLE_ERROR.ADAPTER_OFF)
+				}
+				this.bleAdapterOff = false
+			} catch (e) {
+				if (e && e.code && String(e.code).startsWith('BLE_')) throw e
+				throw _err(BLE_ERROR.ADAPTER_OFF, e)
+			}
+		},
+
+		// ============== 扫描 ==============
+		async startScan(opt = {}) {
+			const s = _S()
+			console.log(`[BLE Store] startScan() called, instanceId = ${s.instanceId}`)
+			// 重置中止标记
+			s.scanAborted = false
+			this.scanThrottled = false
+			// 取消上一次残留扫描
+			if (s.cancelScan) { s.cancelScan(); s.cancelScan = null }
+			// 停止上一次发现
+			await _invoke(uni.stopBluetoothDevicesDiscovery, {}).catch(() => {})
+
+			await this._ensureReady()
+
+			// 如果在 await 期间页面已卸载并调用了 stopScan,直接中止
+			if (s.scanAborted) {
+				logger.info('startScan aborted (page already unloaded)')
+				return opt.returnAll ? [] : null
+			}
+
+			const {
+				namePrefix = s.config.deviceNamePrefix,
+				deviceName,
+				services,
+				timeout = s.config.scanTimeout,
+				returnAll = false
+			} = opt
+
+			// 清空上次扫描结果
+			this.scannedDevices = []
+
+			// Android 限流检测(30秒内最多5次,iOS 无此限制)
+			const platform = uni.getSystemInfoSync().platform
+			if (platform === 'android') {
+				const now = Date.now()
+				s.scanStartHistory = s.scanStartHistory.filter(t => now - t < 30000)
+				if (s.scanStartHistory.length >= 4) {
+					this.scanThrottled = true
+					logger.warn(`BLE scan throttle: ${s.scanStartHistory.length + 1} starts in 30s, system may ignore`)
+				}
+				s.scanStartHistory.push(now)
+			}
+
+			const devices = new Map()
+			const matched = []
+
+			return new Promise(async (resolve, reject) => {
+				let finished = false
+
+				const onFound = (res) => {
+					if (finished) return
+					for (const d of res.devices) {
+						if (devices.has(d.deviceId)) continue
+						devices.set(d.deviceId, d)
+						const name = d.name || d.localName || ''
+						const hit =
+							(deviceName && name === deviceName) ||
+							(namePrefix && name.startsWith(namePrefix)) ||
+							(!deviceName && !namePrefix)
+						if (hit) {
+							matched.push(d)
+							const exists = this.scannedDevices.find(item => item.deviceId === d.deviceId)
+							if (!exists) {
+								this.scannedDevices.push({
+									deviceId: d.deviceId,
+									name: name,
+									RSSI: d.RSSI || ''
+								})
+							}
+							if (!returnAll) { finish(null, d); return }
+						}
+					}
+				}
+
+				const timer = setTimeout(() => {
+					if (returnAll) finish(null, matched)
+					else if (matched.length) finish(null, matched[0])
+					else finish(_err(BLE_ERROR.SCAN_FAIL, { msg: '扫描超时,未发现目标设备' }))
+				}, timeout)
+
+				const finish = (err, data) => {
+					if (finished) return
+					finished = true
+					s.cancelScan = null
+					s.onDeviceFound = null
+					clearTimeout(timer)
+					_invoke(uni.stopBluetoothDevicesDiscovery, {}).catch(() => {})
+					this._setState(BLE_STATE.READY)
+					err ? reject(err) : resolve(data)
+				}
+
+				s.cancelScan = () => finish(null, returnAll ? matched : (matched[0] || null))
+
+				// 通过共享回调分发事件(监听器已在 _bindSystemListeners 中永久注册)
+				s.onDeviceFound = onFound
+				this._setState(BLE_STATE.SCANNING)
+
+				try {
+					await _invoke(uni.startBluetoothDevicesDiscovery, {
+						allowDuplicatesKey: false,
+						interval: 0,
+						services
+					})
+				} catch (e) {
+					finish(_err(BLE_ERROR.SCAN_FAIL, e))
+				}
+			})
+		},
+
+		/** 停止扫描 */
+		stopScan() {
+			const s = _S()
+			s.scanAborted = true
+			s.onDeviceFound = null
+			if (s.cancelScan) {
+				s.cancelScan()
+				s.cancelScan = null
+			} else {
+				try {
+					uni.stopBluetoothDevicesDiscovery({ success() {}, fail() {} })
+				} catch (e) {}
+			}
+			this.searching = false
+			this.scanThrottled = false
+			if (this.bleState === BLE_STATE.SCANNING) {
+				this._setState(BLE_STATE.READY)
+			}
+		},
+
+		// ============== 连接 ==============
+		async connectDevice(deviceId) {
+			const s = _S()
+			console.log(`[BLE Store] connectDevice() called, instanceId = ${s.instanceId}`)
+			if (!deviceId) throw _err(BLE_ERROR.CONNECT_FAIL, { msg: 'deviceId 不能为空' })
+			await this._ensureReady()
+			if (this.bleState === BLE_STATE.CONNECTING) throw _err(BLE_ERROR.BUSY)
+			this._setState(BLE_STATE.CONNECTING)
+			this.device = { deviceId, name: '' }
+
+			try {
+				await _invoke(uni.createBLEConnection, {
+					deviceId,
+					timeout: s.config.connectTimeout
+				})
+				this._setState(BLE_STATE.CONNECTED)
+
+				// Android 提升 MTU
+				// #ifdef APP-PLUS
+				if (uni.getSystemInfoSync().platform === 'android' && uni.setBLEMTU) {
+					try { await _invoke(uni.setBLEMTU, { deviceId, mtu: 185 }) } catch (_) {}
+				}
+				// #endif
+
+				await this._discoverAndSubscribe(deviceId)
+				this._setState(BLE_STATE.READY_COMM)
+				s.reconnectCount = 0
+			} catch (e) {
+				this._setState(BLE_STATE.DISCONNECTED)
+				this.device = null // 连接失败,清空 device 防止延迟回调误触发重连
+				try { await _invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
+				const code = e && (e.errCode || e.code)
+				if (code === 10003 || code === -1) {
+					throw _err(BLE_ERROR.CONNECT_TIMEOUT, e)
+				}
+				throw _err(BLE_ERROR.CONNECT_FAIL, e)
+			}
+		},
+
+		/** 扫描 + 连接一步到位(支持 deviceId 直连优先,跳过扫描) */
+		async scanAndConnect(opt = {}) {
+			const { deviceId: directId, deviceName, ...restOpt } = opt
+
+			// 策略:如果已有 deviceId,先尝试直连(不扫描),失败后回退扫描
+			if (directId) {
+				try {
+					logger.info(`directConnect attempt, deviceId=${directId}`)
+					await this.connectDevice(directId)
+					this.device.name = deviceName || this.device.name || ''
+					logger.info('directConnect success, scan skipped')
+					return this.device
+				} catch (e) {
+					logger.warn('directConnect failed, fallback to scan', e.message || e.code)
+					// connectDevice 失败时已清空 this.device 并关闭连接,无需额外处理
+				}
+			}
+
+			// 回退:扫描 + 连接
+			const scanOpt = { ...restOpt }
+			if (deviceName) scanOpt.deviceName = deviceName
+			const device = await this.startScan(scanOpt)
+			await this.connectDevice(device.deviceId)
+			this.device.name = device.name || device.localName || ''
+			return this.device
+		},
+
+		/** 主动断开 */
+		async disconnect() {
+			const s = _S()
+			s.intentionalDisconnect = true
+			this._clearReconnect()
+			if (!this.device) {
+				s.intentionalDisconnect = false
+				return
+			}
+			const { deviceId } = this.device
+			// 先置空 device,防止 onBLEConnectionStateChange 回调误匹配
+			this.device = null
+			this._setState(BLE_STATE.DISCONNECTED)
+			try { await _invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
+			s.intentionalDisconnect = false
+		},
+
+		/** 发现服务并订阅通知 */
+		async _discoverAndSubscribe(deviceId) {
+			const s = _S()
+			const svcRes = await _invoke(uni.getBLEDeviceServices, { deviceId })
+			const services = svcRes.services || []
+			const targetSvc = services.find(sv => _uuidEq(sv.uuid, s.config.serviceId))
+				|| services.find(sv => sv.isPrimary)
+				|| services[0]
+			if (!targetSvc) throw _err(BLE_ERROR.SERVICE_NOT_FOUND)
+			s.serviceId = targetSvc.uuid
+
+			const charRes = await _invoke(uni.getBLEDeviceCharacteristics, {
+				deviceId, serviceId: s.serviceId
+			})
+			const chars = charRes.characteristics || []
+
+			const writeChar = chars.find(c => _uuidEq(c.uuid, s.config.writeCharId))
+				|| chars.find(c => c.properties && (c.properties.write || c.properties.writeNoResponse || c.properties.writeDefault))
+			const notifyChar = chars.find(c => _uuidEq(c.uuid, s.config.notifyCharId))
+				|| chars.find(c => c.properties && (c.properties.notify || c.properties.indicate))
+
+			if (!writeChar) throw _err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到写特征' })
+			if (!notifyChar) throw _err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到通知特征' })
+
+			s.writeCharId = writeChar.uuid
+			s.notifyCharId = notifyChar.uuid
+
+			await _invoke(uni.notifyBLECharacteristicValueChange, {
+				deviceId,
+				serviceId: s.serviceId,
+				characteristicId: s.notifyCharId,
+				state: true
+			})
+		},
+
+		// ============== 断连处理 & 自动重连 ==============
+		_handleDisconnected(reason) {
+			const s = _S()
+			// 主动断开/重置时不触发重连
+			if (s.intentionalDisconnect) return
+			if (this.bleState === BLE_STATE.DISCONNECTED || this.bleState === BLE_STATE.IDLE) return
+			const device = this.device
+			this._setState(BLE_STATE.DISCONNECTED)
+			if (s.parser) s.parser.reset()
+			this._attemptReconnect(device, reason)
+		},
+
+		_attemptReconnect(device, reason) {
+			const s = _S()
+			if (!s.config.autoReconnect || !device || s.reconnectCount >= s.config.maxReconnect) {
+				if (s.reconnectCount >= s.config.maxReconnect) {
+					logger.warn('reconnect max reached, waiting for adapter restore')
+					s.reconnectCount = 0
+					this.reconnectDrawer = false
+					this.reconnectCount = 0
+				}
+				return
+			}
+			s.reconnectCount++
+			this.reconnectCount = s.reconnectCount
+			this.reconnectDrawer = true
+			const delay = Math.min(1000 * s.reconnectCount, 5000)
+			logger.warn(`reconnect in ${delay}ms (${s.reconnectCount}/${s.config.maxReconnect})`)
+
+			const gen = s.reconnectGeneration
+			s.reconnectTimer = setTimeout(async () => {
+				if (s.reconnectGeneration !== gen) {
+					logger.info('reconnect callback aborted (generation mismatch)')
+					return
+				}
+				try {
+					// 先关闭适配器清除脏状态(加标志防止回调干扰)
+					s.intentionalDisconnect = true
+					try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
+					s.intentionalDisconnect = false
+					this._setState(BLE_STATE.IDLE)
+					if (s.reconnectGeneration !== gen) return
+					// 优先使用 deviceId 直连,避免不必要的扫描触发 Android 限流
+					const scanOpt = { timeout: 8000 }
+					if (device.deviceId) scanOpt.deviceId = device.deviceId
+					if (device.name) scanOpt.deviceName = device.name
+					await this.scanAndConnect(scanOpt)
+					if (s.reconnectGeneration !== gen) return
+					this.reconnectDrawer = false
+					this.reconnectCount = 0
+					logger.info('reconnect success')
+				} catch (e) {
+					if (s.reconnectGeneration !== gen) return
+					logger.error('reconnect fail', e)
+					this._attemptReconnect(device, reason)
+				}
+			}, delay)
+		},
+
+		_clearReconnect() {
+			const s = _S()
+			if (s.reconnectTimer) { clearTimeout(s.reconnectTimer); s.reconnectTimer = null }
+			s.reconnectCount = 0
+			s.reconnectGeneration++ // 递增代数,使正在执行的僵尸回调自动失效
+			this.reconnectCount = 0
+			this.reconnectDrawer = false
+		},
+
+		/**
+		 * 蓝牙适配器从关闭恢复为开启时调用
+		 * 如果之前有连接过的设备且当前处于断连状态,自动扫描并重新连接
+		 */
+		_onAdapterRestored() {
+			const device = this.device
+			if (!device || !device.deviceId) return
+			if (this.bleState !== BLE_STATE.DISCONNECTED && this.bleState !== BLE_STATE.IDLE) return
+			logger.info('adapter restored, auto reconnecting to', device.name || device.deviceId)
+			// 重置重连计数,发起新一轮重连
+			this._clearReconnect()
+			this._setState(BLE_STATE.IDLE)
+			// 延迟 1.5s 等待适配器完全就绪(Android 蓝牙状态延迟)
+			const s = _S()
+			const gen = s.reconnectGeneration
+			this.reconnectDrawer = true
+			this.reconnectCount = 1
+			s.reconnectTimer = setTimeout(async () => {
+				if (s.reconnectGeneration !== gen) return
+				try {
+					// 关闭旧适配器,确保重新打开时状态干净
+					s.intentionalDisconnect = true
+					try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
+					s.intentionalDisconnect = false
+					if (s.reconnectGeneration !== gen) return
+					// 优先直连,避免扫描触发限流
+					const scanOpt = { timeout: 8000 }
+					if (device.deviceId) scanOpt.deviceId = device.deviceId
+					if (device.name) scanOpt.deviceName = device.name
+					await this.scanAndConnect(scanOpt)
+					if (s.reconnectGeneration !== gen) return
+					this.reconnectDrawer = false
+					this.reconnectCount = 0
+					logger.info('adapter restore reconnect success')
+				} catch (e) {
+					if (s.reconnectGeneration !== gen) return
+					logger.error('adapter restore reconnect fail', e)
+					// 失败后进入常规重连流程(还有2次机会)
+					s.reconnectCount = 1
+					this._attemptReconnect(device, BLE_ERROR.ADAPTER_OFF)
+				}
+			}, 1500)
+		},
+
+		// ============== 收包处理 ==============
+		_onFrame(decoded, frame) {
+			logger.info('frame received', decoded.funcCode)
+			if (decoded.parsed) {
+				if (decoded.parsed.type === 'GROUP_1') {
+					this._handleGroup1(decoded.parsed)
+				} else if (decoded.parsed.type === 'GROUP_2') {
+					this._handleGroup2(decoded.parsed)
+				}
+			}
+		},
+
+		/** 解析参数组1 */
+		_handleGroup1(data) {
+			// 设备运行状态
+			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)
+		},
+
+		/** 解析参数组2 */
+		_handleGroup2(data) {
+			if (data.preheatPercent < 100) {
+				this.ispreHot = false
+				this.hotPercentage = data.preheatPercent + '%'
+			} else {
+				this.ispreHot = true
+				this.hotPercentage = data.ignitePercent + '%'
+			}
+			// 椅子角度
+			if (data.chairAngle) {
+				const angleMap = {
+					1: 90, 2: 105, 3: 120, 4: 135, 5: 150
+				}
+				if (angleMap[data.chairAngle]) {
+					this.chairAngle = angleMap[data.chairAngle]
+				}
+			}
+		},
+
+		// ============== 发送指令 ==============
+		async writeRaw(u8) {
+			if (!this.linked) throw _err(BLE_ERROR.DISCONNECTED)
+			const s = _S()
+			const payload = bufferToArrayBuffer(u8)
+			s.writeLock = s.writeLock.then(async () => {
+				logger.log('=>', bytesToHex(u8))
+				logger.log('write params:', {
+					deviceId: this.device.deviceId,
+					serviceId: s.serviceId,
+					characteristicId: s.writeCharId,
+					valueByteLength: payload.byteLength
+				})
+				try {
+					await _invoke(uni.writeBLECharacteristicValue, {
+						deviceId: this.device.deviceId,
+						serviceId: s.serviceId,
+						characteristicId: s.writeCharId,
+						value: payload
+					})
+				} catch (e) {
+					logger.error('writeBLE origin error:', JSON.stringify(e))
+					throw _err(BLE_ERROR.WRITE_FAIL, e)
+				}
+			})
+			return s.writeLock
+		},
+
+		/** 下发基本功能指令 (0x01) */
+		sendBasic(opt) { return this.writeRaw(encodeBasic(opt)) },
+
+		/** 下发模式参数 1 (步骤 1-7) */
+		sendModeParam1(opt) { return this.writeRaw(encodeModeParam1(opt)) },
+
+		/** 下发模式参数 2 (步骤 8-14) */
+		sendModeParam2(opt) { return this.writeRaw(encodeModeParam2(opt)) },
+
+		/** 下发穴位坐标 */
+		async sendAcupoints(points = []) {
+			for (let i = 0; i < points.length; i += 2) {
+				const pair = points.slice(i, i + 2)
+				await this.writeRaw(encodeAcupoints(pair))
+			}
+		},
+
+		// ---- 常用快捷方法 ----
+		powerOn() { return this.sendBasic({ power: POWER.ON }) },
+		powerOff() { return this.sendBasic({ power: POWER.OFF }) },
+		startMoxi(opt = {}) { return this.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.START, ...opt }) },
+		pauseMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.PAUSE }) },
+		stopMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.DONE }) },
+		setMute(on) { return this.sendBasic({ mute: on ? MUTE.ON : MUTE.OFF }) },
+		setTemperature(v) { return this.sendBasic({ temperature: v }) },
+		setChairAngle(v) { return this.sendBasic({ angle: v }) },
+
+		// ============== 蓝牙关闭提示 ==============
+		/**
+		 * 弹窗提示用户蓝牙未开启,引导用户前往设置开启
+		 * 内部做防抖,避免短时间内重复弹窗
+		 */
+		_showBleOffPrompt(message) {
+			const s = _S()
+			// 防抖:5秒内不重复弹窗
+			const now = Date.now()
+			if (s._lastBleOffPromptTime && now - s._lastBleOffPromptTime < 5000) return
+			s._lastBleOffPromptTime = now
+
+			uni.showModal({
+				title: '蓝牙未开启',
+				content: message || '请开启手机蓝牙后重试',
+				confirmText: '去设置',
+				cancelText: '取消',
+				success: (res) => {
+					if (res.confirm) {
+						// #ifdef APP-PLUS
+						const platform = uni.getSystemInfoSync().platform
+						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) {
+								logger.error('跳转蓝牙设置失败', e)
+							}
+						} else if (platform === 'ios') {
+							// iOS 可以打开 App 设置页
+							plus.runtime.openURL('App-Prefs:root=Bluetooth')
+						}
+						// #endif
+					}
+				}
+			})
+		}
+	}
+})

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