healthMeter.vue 81 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768
  1. <template>
  2. <view class="page">
  3. <!-- 顶部导航 -->
  4. <!-- <view :style="{ height: statusBarHeight}"></view> -->
  5. <view class="nav-bar" :style="{ paddingTop: statusBarHeight }">
  6. <view class="nav-back" @tap="goBack">
  7. <!-- <text class="back-text">‹ 返回</text> -->
  8. <image class="back-text" src="@/static/images/pages_watch/icons/back_arrow_icon24.png"></image>
  9. </view>
  10. <view class="nav-title">小护士血压仪</view>
  11. <image class="next-text" src="@/static/images/pages_watch/icons/more_icon24.png"></image>
  12. </view>
  13. <view class="content">
  14. <!-- 顶部Tab -->
  15. <view class="tabs">
  16. <view class="tab" :class="{ active: activeTab === 'bp' }" @tap="activeTab = 'bp'">血压/心率</view>
  17. <view class="tab" :class="{ active: activeTab === 'glucose' }" @tap="activeTab = 'glucose'">血糖测量</view>
  18. <view class="tab" :class="{ active: activeTab === 'uric' }" @tap="activeTab = 'uric'">尿酸测量</view>
  19. </view>
  20. <!-- 圆形仪表 -->
  21. <view class="meter-wrap">
  22. <view class="meter-ring" :style="ringStyle">
  23. <view class="meter-center">
  24. <!-- 准备就绪 -->
  25. <view v-if="uiState === 'ready'" class="ready-view">
  26. <!-- <view class="ready-check">✓</view> -->
  27. <image class="ready-checkimg" src="@/static/images/pages_watch/icons/ok_icon.png"></image>
  28. <view class="ready-text">准备就绪</view>
  29. </view>
  30. <!-- 量测中 -->
  31. <view v-else-if="uiState === 'measuring'" class="measuring-view">
  32. <view class="bp-value">
  33. {{ displaySystolic }}/{{ displayDiastolic }}
  34. <text class="bp-value-unit">mmHg</text>
  35. </view>
  36. <view class="measuring-sub">正在自动加压..{{ ringProgressInt }}%</view>
  37. </view>
  38. <!-- 结果完成 -->
  39. <view v-else-if="uiState === 'result'" class="result-view">
  40. <view v-if="activeTab === 'bp'" class="result-view">
  41. <view class="bp-value">
  42. {{ lastBloodPressure.systolic }}/{{ lastBloodPressure.diastolic }}
  43. <text class="bp-value-unit">mmHg</text>
  44. </view>
  45. <view class="result-sub">测量完成</view>
  46. </view>
  47. <view v-else-if="activeTab === 'glucose'" class="result-view">
  48. <view class="bp-value">
  49. {{ lastGlucose.value }}
  50. <text class="bp-value-unit">{{ lastGlucose.unit }}</text>
  51. </view>
  52. <view class="result-sub">测量完成</view>
  53. </view>
  54. <view v-else class="result-view">
  55. <view class="bp-value">
  56. {{ lastUricAcid.value }}
  57. <text class="bp-value-unit">{{ lastUricAcid.unit }}</text>
  58. </view>
  59. <view class="result-sub">测量完成</view>
  60. </view>
  61. </view>
  62. <!-- 异常 -->
  63. <view v-else-if="uiState === 'error'" class="error-view">
  64. <view v-if="activeTab === 'bp'" class="result-view">
  65. <view class="bp-value error-num">
  66. --/--
  67. <text class="bp-value-unit">mmHg</text>
  68. </view>
  69. <view class="result-sub error-sub">测量错误</view>
  70. </view>
  71. <view v-else-if="activeTab === 'glucose'" class="result-view">
  72. <view class="bp-value error-num">
  73. --
  74. <text class="bp-value-unit">{{ lastGlucose.unit }}</text>
  75. </view>
  76. <view class="result-sub error-sub">{{ glucoseErrorMsg ? '血糖测量错误' : '测量错误' }}</view>
  77. </view>
  78. <view v-else class="result-view">
  79. <view class="bp-value error-num">
  80. --
  81. <text class="bp-value-unit">{{ lastUricAcid.unit }}</text>
  82. </view>
  83. <view class="result-sub error-sub">{{ uricAcidErrorMsg ? '尿酸测量错误' : '测量错误' }}</view>
  84. </view>
  85. </view>
  86. <!-- 未连接 -->
  87. <view v-else class="ready-view">
  88. <view class="ready-check disconnected-check">○</view>
  89. <view class="ready-text">未连接</view>
  90. </view>
  91. </view>
  92. </view>
  93. </view>
  94. <!-- 提示/同步卡片 v-if="uiState !== 'result'" -->
  95. <view v-if="uiState !== 'result'" class="tip-box">
  96. <view class="tip-icon">i</view>
  97. <view class="tip-text">{{ tipText }}</view>
  98. </view>
  99. <!-- 连接/设备操作区(非截图区域:保持你现有连接逻辑) -->
  100. <view class="actions">
  101. <view>
  102. <view class="list-title">已绑定设备</view>
  103. <!-- <button class="es-w-200 es-h-88" v-show="!isFamily" @click="editXHSDevice('FC:62:58:04:19:1E')">绑定</button>
  104. <button class="es-w-200 es-h-88" v-show="isFamily" @click="editMyfamily('FC:62:58:04:19:1E')">家人绑定</button>
  105. <button class="es-w-200 es-h-88" @click="xhsDataAddTest(0)">记录血压</button>
  106. <button class="es-w-200 es-h-88" @click="xhsDataAddTest(1)">记录血糖</button>
  107. <button class="es-w-200 es-h-88" @click="xhsDataAddTest(3)">记录尿酸</button> -->
  108. <view class="device-item active es-mb-20" v-for="(item, index) in xhsDeviceList" :key="index">
  109. <view class="x-f" style="margin-bottom: 24rpx; justify-content: space-between">
  110. <view class="device-info flex">
  111. <text class="device-name">{{ item.name }}</text>
  112. <text class="device-id">{{ item.deviceId }}</text>
  113. </view>
  114. <view class="es-mr-10" v-if="deviceId == item.deviceId">{{ statusText }}</view>
  115. </view>
  116. <view class="device-item-footer">
  117. <button class="device-connect-warn" :disabled="disconnecting" @tap="unbindDevice(item)">解绑</button>
  118. <button class="device-connect" v-if="deviceId == item.deviceId && !connected" :disabled="connecting" :loading="connecting" @tap="connectDevice(false)">
  119. {{ connecting ? '连接中...' : '重试连接' }}
  120. </button>
  121. </view>
  122. </view>
  123. </view>
  124. <view>
  125. <button class="btn btn-primary" :disabled="scanning" :loading="scanning" @tap="startScan">
  126. {{ scanning ? '扫描中...' : '添加新设备' }}
  127. </button>
  128. <view v-if="deviceList.length > 0" class="device-list">
  129. <view class="list-title">可选设备(请确保血压仪/血糖仪已开机)</view>
  130. <view
  131. v-for="d in deviceList"
  132. :key="d.deviceId"
  133. class="device-item x-f es-pl-20 es-pr-20"
  134. :class="{ active: selectedDeviceId === d.deviceId }"
  135. @tap="selectDevice(d)"
  136. >
  137. <!-- <radio :checked="selectedDeviceId === d.deviceId" color="#FF5C03" /> -->
  138. <image class="es-w-100 es-h-100" src="/static/images/pages_watch/icons/blood_pressure_monitor_img.png" mode="aspectFit"></image>
  139. <view class="device-info flex">
  140. <text class="device-name">{{ d.name || d.localName || '未知设备' }}</text>
  141. <text class="device-id">{{ d.deviceId }}</text>
  142. </view>
  143. <view>
  144. <button
  145. class="device-item-btn"
  146. :disabled="d.deviceId == selectedDeviceId && (!selectedDeviceId || connecting)"
  147. :loading="d.deviceId == selectedDeviceId && connecting"
  148. >
  149. {{ d.deviceId == selectedDeviceId && connecting ? '连接中...' : '连接设备' }}
  150. </button>
  151. </view>
  152. </view>
  153. </view>
  154. </view>
  155. </view>
  156. <!-- 主按钮 -->
  157. <view v-if="activeTab === 'bp' && (bpUiState === 'ready' || bpUiState === 'error')" class="primary-btn" @tap="onPrimaryActionTap">
  158. <text class="primary-btn-text">{{ bpUiState === 'error' ? '重新测量' : '开始测量' }}</text>
  159. </view>
  160. <view v-else-if="activeTab === 'bp' && bpUiState === 'measuring'" class="primary-btn primary-btn-disabled">
  161. <text class="primary-btn-text">测量中</text>
  162. </view>
  163. <!-- v-else-if="bpUiState === 'result' && activeTab === 'bp'" -->
  164. <view class="sync-card" v-show="boundDevice">
  165. <view class="sync-left">
  166. <view class="sync-title" :style="{ color: dataSync === 'success' ? '#22C55E' : dataSync === 'error' ? 'red' : dataSync === 'loading' ? '#999' : '#22C55E' }">
  167. {{ dataSync === 'success' ? '数据已同步' : dataSync === 'error' ? '数据同步失败' : dataSync === 'loading' ? '数据同步中' : '数据同步' }}
  168. </view>
  169. <view class="sync-time">{{ footerTimeText || '--' }}</view>
  170. </view>
  171. <image class="sync-check" v-show="dataSync == 'success'" src="@/static/images/pages_watch/icons/remove_icon2.png"></image>
  172. <button class="device-item-btn" v-show="dataSync != 'success'" :disabled="dataSync === 'loading'" :loading="dataSync === 'loading'" @click="xhsDataAdd(0)">
  173. {{ dataSync === 'loading' ? '同步中' : dataSync === 'error' ? '重新同步' : '同步成功' }}
  174. </button>
  175. </view>
  176. <!-- 结果指标卡片(只在血压/心率Tab展示,保持布局一致) -->
  177. <!-- v-if="bpUiState === 'result' && activeTab === 'bp'" -->
  178. <view class="result-panel" v-show="boundDevice">
  179. <view class="result-grid">
  180. <view class="result-card">
  181. <view class="result-label">血压</view>
  182. <view class="result-value x-f">
  183. {{ cardBloodPressure.systolic || '-' }}/{{ cardBloodPressure.diastolic || '-' }}
  184. <text class="result-unit">(mmHg)</text>
  185. </view>
  186. <view class="result-chart">
  187. <image class="chart-img" src="@/static/images/pages_watch/icons/bloodP.png"></image>
  188. </view>
  189. <view class="result-text" :class="bpResultText == '偏高' ? 'active' : ''">{{ bpResultText }}</view>
  190. </view>
  191. <view class="result-card">
  192. <view class="result-label">心率</view>
  193. <view class="result-value x-f">
  194. {{ cardBloodPressure.pulse || '-' }}
  195. <text class="result-unit">bpm</text>
  196. </view>
  197. <view class="result-chart">
  198. <image class="chart-img" src="@/static/images/pages_watch/icons/heart.png"></image>
  199. </view>
  200. <view class="result-text" :class="pulseResultText == '偏高' ? 'active' : ''">{{ pulseResultText }}</view>
  201. </view>
  202. <view class="result-card">
  203. <view class="result-label">血糖</view>
  204. <view class="result-value x-f">
  205. {{ cardGlucose.value || '-' }}
  206. <text class="result-unit">({{ cardGlucose.unit }})</text>
  207. </view>
  208. <view class="result-chart">
  209. <image
  210. class="chart-img"
  211. :src="glucoseResultText == '偏高' ? '/static/images/pages_watch/icons/bloods-top.png' : '/static/images/pages_watch/icons/bloods.png'"
  212. ></image>
  213. </view>
  214. <view class="result-text" :class="glucoseResultText == '偏高' ? 'active' : ''">{{ glucoseResultText }}</view>
  215. </view>
  216. <view class="result-card">
  217. <view class="result-label">
  218. 尿酸
  219. <!-- <text class="trend-badge">{{ uricTrendText }}</text> -->
  220. </view>
  221. <view class="result-value x-f">
  222. {{ lastUricAcid.value || '-' }}
  223. <text class="result-unit">({{ lastUricAcid.unit }})</text>
  224. </view>
  225. <view class="result-chart">
  226. <image
  227. class="chart-img"
  228. :src="uricResultText == '偏高' ? '/static/images/pages_watch/icons/acid-top.png' : '/static/images/pages_watch/icons/acid.png'"
  229. ></image>
  230. </view>
  231. <view class="result-text" :class="uricResultText == '偏高' ? 'active' : ''">{{ uricResultText }}</view>
  232. </view>
  233. </view>
  234. </view>
  235. <!-- 日志(调试用,可折叠) -->
  236. <view class="log-section" v-if="false">
  237. <view class="log-title" @tap="showLog = !showLog">
  238. <text>运行日志</text>
  239. <text class="log-toggle">{{ showLog ? '收起' : '展开' }}</text>
  240. </view>
  241. <scroll-view v-if="showLog" class="log-list" scroll-y>
  242. <text v-for="(line, i) in logs" :key="i" class="log-line">{{ line }}</text>
  243. </scroll-view>
  244. </view>
  245. </view>
  246. </view>
  247. </template>
  248. <script>
  249. /**
  250. * 将 uni-app 的回调式 API 包装为 Promise 风格,便于使用 async/await 避免回调地狱
  251. * @param {string} api - uni-app 的 API 名称 (如 'openBluetoothAdapter')
  252. * @param {Object} opts - 传递给 API 的参数对象
  253. * @returns {Promise} 成功时 resolve 返回结果,失败时 reject 返回错误信息
  254. */
  255. const promisifyUniApi = (api, opts = {}) =>
  256. new Promise((res, rej) => {
  257. opts.success = res;
  258. opts.fail = rej;
  259. uni[api](opts);
  260. });
  261. // 解析 BLE 血压测量特征值(Bluetooth GATT 0x2A18 格式)
  262. // 固定 UUID:接收用 0xFFF1,发送用 0xFFF2,同属 Service 0xFFF0
  263. // 血压协议:命令头 0xFD,0xFD;命令尾 0x0D,0x0A
  264. const BP_HEADER = [0xfd, 0xfd];
  265. const BP_TAIL = [0x0d, 0x0a];
  266. // 血压协议命令 (FD FD ... 0D 0A) - 新协议
  267. const CMD_BP_BROADCAST_A4 = 0xa4;
  268. const CMD_BP_CONNECT_FA = 0xfa; // FA 05
  269. const CMD_BP_CONFIRM_06 = 0x06;
  270. const CMD_BP_PRESSURE_FB = 0xfb;
  271. const CMD_BP_RESULT_FC = 0xfc;
  272. const CMD_BP_ERROR_FD = 0xfd;
  273. // 血糖/尿酸协议命令 (A5 A5 ... 5A 5A)
  274. const GU_HEADER = [0xa5, 0xa5];
  275. const GU_TAIL = [0x5a, 0x5a];
  276. const CMD_GU_START_B1 = 0xb1;
  277. const CMD_GU_GLUCOSE_RES_B2 = 0xb2;
  278. const CMD_GU_URIC_RES_B3 = 0xb3;
  279. const CMD_GU_GLUCOSE_ERR_B4 = 0xb4;
  280. const CMD_GU_URIC_ERR_B5 = 0xb5;
  281. const CMD_GU_REPLY_START_C1 = 0xc1;
  282. const CMD_GU_REPLY_GLUCOSE_RES_C2 = 0xc2;
  283. const CMD_GU_REPLY_GLUCOSE_ERR_C3 = 0xc3;
  284. const CMD_GU_REPLY_URIC_RES_C4 = 0xc4;
  285. const CMD_GU_REPLY_URIC_ERR_C5 = 0xc5;
  286. // 构造血压指令
  287. function buildBpCommand(cmd, data = []) {
  288. const buf = new Uint8Array(2 + 1 + data.length + 2);
  289. buf[0] = 0xfd;
  290. buf[1] = 0xfd;
  291. buf[2] = cmd;
  292. for (let i = 0; i < data.length; i++) {
  293. buf[3 + i] = data[i];
  294. }
  295. buf[buf.length - 2] = 0x0d;
  296. buf[buf.length - 1] = 0x0a;
  297. return buf.buffer;
  298. }
  299. // 构造血糖/尿酸指令
  300. function buildGuCommand(cmd, data = []) {
  301. const len = 1 + data.length + 1; // 帧长包括 指令类型、数据、校验和
  302. const buf = new Uint8Array(2 + 1 + len + 2); // 帧头(2) + 帧长(1) + len + 帧尾(2)
  303. buf[0] = 0xa5;
  304. buf[1] = 0xa5;
  305. buf[2] = len;
  306. buf[3] = cmd;
  307. let checksum = cmd;
  308. for (let i = 0; i < data.length; i++) {
  309. buf[4 + i] = data[i];
  310. checksum += data[i];
  311. }
  312. buf[buf.length - 3] = checksum & 0xff;
  313. buf[buf.length - 2] = 0x5a;
  314. buf[buf.length - 1] = 0x5a;
  315. return buf.buffer;
  316. }
  317. function parseBpPacket(arr) {
  318. // 查找 BP_HEADER (0xfd, 0xfd)
  319. let startIndex = -1;
  320. for (let i = 0; i < arr.length - 1; i++) {
  321. if (arr[i] === 0xfd && arr[i + 1] === 0xfd) {
  322. startIndex = i;
  323. break;
  324. }
  325. }
  326. if (startIndex === -1) return null;
  327. // 查找 BP_TAIL (0x0d, 0x0a)
  328. let endIndex = -1;
  329. for (let i = startIndex + 2; i < arr.length - 1; i++) {
  330. if (arr[i] === 0x0d && arr[i + 1] === 0x0a) {
  331. endIndex = i + 1; // 包含 0x0a
  332. break;
  333. }
  334. }
  335. if (endIndex === -1) return null;
  336. const cmd = arr[startIndex + 2];
  337. const data = arr.slice(startIndex + 3, endIndex - 1);
  338. return { cmd, data };
  339. }
  340. // 构造旧协议指令 (D1/D2/D3等)
  341. function buildV2Command(cmd, data = []) {
  342. const packetLen = 1 + data.length + 1; // Cmd(1) + Data(n) + Checksum(1)
  343. const buf = new Uint8Array(2 + 1 + packetLen + 2);
  344. buf[0] = 0xfd;
  345. buf[1] = 0xfd;
  346. buf[2] = packetLen;
  347. buf[3] = cmd;
  348. let checksum = cmd;
  349. for (let i = 0; i < data.length; i++) {
  350. buf[4 + i] = data[i];
  351. checksum += data[i];
  352. }
  353. buf[buf.length - 3] = checksum & 0xff;
  354. buf[buf.length - 2] = 0x0d;
  355. buf[buf.length - 1] = 0x0a;
  356. return buf.buffer;
  357. }
  358. function parseGuPacket(arr) {
  359. // 查找 GU_HEADER (0xa5, 0xa5)
  360. let startIndex = -1;
  361. for (let i = 0; i < arr.length - 1; i++) {
  362. if (arr[i] === 0xa5 && arr[i + 1] === 0xa5) {
  363. startIndex = i;
  364. break;
  365. }
  366. }
  367. if (startIndex === -1) return null;
  368. const len = arr[startIndex + 2];
  369. const expectedEndIndex = startIndex + 5 + len - 1;
  370. if (expectedEndIndex >= arr.length) return null;
  371. if (arr[expectedEndIndex - 1] !== 0x5a || arr[expectedEndIndex] !== 0x5a) return null;
  372. const cmd = arr[startIndex + 3];
  373. const dataLen = len - 2; // len = cmd(1) + data + CS(1)
  374. const data = arr.slice(startIndex + 4, startIndex + 4 + dataLen);
  375. const checksum = arr[startIndex + 4 + dataLen];
  376. let sum = cmd;
  377. for (let i = 0; i < data.length; i++) sum += data[i];
  378. if ((sum & 0xff) !== checksum) {
  379. console.log('GU Checksum error:', sum & 0xff, checksum);
  380. return null;
  381. }
  382. return { cmd, data };
  383. }
  384. // 解析血糖/尿酸数据包
  385. function parseGlucosePacket(buffer) {
  386. // 解析新协议 (A5 A5)
  387. const newPacket = parseGuPacket(new Uint8Array(buffer));
  388. if (newPacket) {
  389. const { cmd, data } = newPacket;
  390. // 1. 开始测量 (B1)
  391. if (cmd === CMD_GU_START_B1) {
  392. const type = data[1]; // 0x01=血糖, 0x02=尿酸
  393. if (type === 0x01) {
  394. return {
  395. type: 'glucose_start',
  396. user: data[0],
  397. replyCmd: buildGuCommand(CMD_GU_REPLY_START_C1)
  398. };
  399. } else if (type === 0x02) {
  400. return {
  401. type: 'uric_start',
  402. user: data[0],
  403. replyCmd: buildGuCommand(CMD_GU_REPLY_START_C1)
  404. };
  405. }
  406. }
  407. // 2. 血糖结果 (B2)
  408. if (cmd === CMD_GU_GLUCOSE_RES_B2) {
  409. if (data.length >= 4) {
  410. const valH = data[1];
  411. const valL = data[2];
  412. const unitByte = data[3];
  413. const value = (valH << 8) | valL;
  414. let finalValue = value;
  415. if (value === 0) {
  416. finalValue = 'LO';
  417. } else if (value === 0xffff) {
  418. finalValue = 'HI';
  419. } else {
  420. finalValue = (value / 10).toFixed(1);
  421. }
  422. return {
  423. type: 'glucose_result',
  424. value: finalValue,
  425. unit: unitByte === 0x01 ? 'mg/dL' : 'mmol/L',
  426. time: Date.now(),
  427. replyCmd: buildGuCommand(CMD_GU_REPLY_GLUCOSE_RES_C2)
  428. };
  429. }
  430. }
  431. // 3. 尿酸结果 (B3)
  432. if (cmd === CMD_GU_URIC_RES_B3) {
  433. if (data.length >= 4) {
  434. const valH = data[1];
  435. const valL = data[2];
  436. const unitByte = data[3];
  437. const value = (valH << 8) | valL;
  438. let finalValue = value;
  439. if (value === 0) {
  440. finalValue = 'LO';
  441. } else if (value === 0xffff) {
  442. finalValue = 'HI';
  443. }
  444. return {
  445. type: 'uric_result',
  446. value: finalValue,
  447. unit: unitByte === 0x01 ? 'mg/dL' : 'umol/L',
  448. time: Date.now(),
  449. replyCmd: buildGuCommand(CMD_GU_REPLY_URIC_RES_C4) // 文档中尿酸结果回复是C4
  450. };
  451. }
  452. }
  453. // 4. 报错 (B4/B5)
  454. if (cmd === CMD_GU_GLUCOSE_ERR_B4) {
  455. const errCode = data[1];
  456. return {
  457. type: 'glucose_error',
  458. msg: '血糖测量错误: ' + getErrorMsg(errCode),
  459. replyCmd: buildGuCommand(CMD_GU_REPLY_GLUCOSE_ERR_C3)
  460. };
  461. }
  462. if (cmd === CMD_GU_URIC_ERR_B5) {
  463. const errCode = data[1];
  464. return {
  465. type: 'uric_error',
  466. msg: '尿酸测量错误: ' + getErrorMsg(errCode),
  467. replyCmd: buildGuCommand(CMD_GU_REPLY_URIC_ERR_C5) // 尿酸报错回复C5
  468. };
  469. }
  470. }
  471. return null;
  472. }
  473. function getErrorMsg(code) {
  474. const msgs = {
  475. 0x01: '温度超范围',
  476. 0x02: '试纸已使用过',
  477. 0x03: '试纸损坏或中途拔出'
  478. // 其他错误码...
  479. };
  480. return msgs[code] || '错误码 ' + code;
  481. }
  482. import { getWatchUserInfo, editMyfamily } from '@/api/pages_watch/user.js';
  483. import { editXHSDevice, xhsDataAdd, removeXHSDevice, getLastData, xhsDataAddList } from '@/api/pages_watch/device.js';
  484. export default {
  485. data() {
  486. return {
  487. scanning: false,
  488. connecting: false,
  489. disconnecting: false,
  490. connected: false,
  491. deviceList: [],
  492. selectedDeviceId: '',
  493. selectedDeviceName: '',
  494. deviceId: '',
  495. serviceId: '',
  496. characteristicId: '',
  497. notifyServiceId: '',
  498. notifyCharId: '',
  499. writeServiceId: '',
  500. writeCharId: '',
  501. writeType: '', // 明确记录使用的写入类型: 'write' 或 'writeNoResponse'
  502. lastGlucose: {
  503. value: null,
  504. unit: 'mmol/L',
  505. time: null
  506. },
  507. cardGlucose: {},
  508. // 血糖测量状态提示(如:请滴入血样)
  509. glucoseStatus: '',
  510. // 血糖测量错误提示
  511. glucoseErrorMsg: '',
  512. lastUricAcid: {
  513. value: null,
  514. unit: 'umol/L',
  515. time: null
  516. },
  517. // 尿酸测量状态提示
  518. uricAcidStatus: '',
  519. // 尿酸测量错误提示
  520. uricAcidErrorMsg: '',
  521. lastBloodPressure: {
  522. systolic: null,
  523. diastolic: null,
  524. pulse: null,
  525. ihb: null, // 0 正常,1 心律不齐
  526. time: null
  527. },
  528. cardBloodPressure: {},
  529. // 血压计已回复 FD FD 06 0D 0A,已开始量测
  530. bpMeasureStarted: false,
  531. // 是否由用户主动点击开始测量
  532. userRequestedMeasure: false,
  533. bpWaitUser: 0, // A4广播中的用户号
  534. // 量测过程中实时压力值(FD FD FB PressureH PressureL 0D 0A),单位 mmHg,收到最终结果后清空
  535. bpCurrentPressure: null,
  536. // 量测错误提示(E-1/E-2/E-3/E-5/E-E/E-B),收到成功结果或断开时清空
  537. bpErrorMsg: '',
  538. // 新增:血压测量流程状态
  539. bpProcessStatus: '', // 'connecting', 'handshake', 'measuring', 'result', 'error'
  540. bpProcessStep: '', // 详细步骤描述
  541. logs: [],
  542. showLog: true,
  543. statusText: '未连接',
  544. statusBarHeight: uni.getSystemInfoSync().statusBarHeight + 'px',
  545. boundDevice: null,
  546. // 顶部Tab:血压/血糖/尿酸(只影响展示,不改变蓝牙业务逻辑)
  547. activeTab: 'bp',
  548. // 数据同步状态
  549. dataSync: 'success',
  550. dataSyncList: [],
  551. deviceType: 1, //0腕表1小护士2新表
  552. isFamily: false,
  553. selectUser: 0,
  554. xhsDeviceList: [],
  555. otherDevice: [],
  556. retryCount: 0
  557. };
  558. },
  559. computed: {
  560. deviceDisplayName() {
  561. return this.selectedDeviceName || (this.boundDevice && this.boundDevice.name) || '小护士血压仪';
  562. },
  563. bpUiState() {
  564. if (!this.connected) return 'disconnected';
  565. if (this.bpProcessStatus === 'result') return 'result';
  566. if (this.bpProcessStatus === 'measuring') return 'measuring';
  567. if (this.bpProcessStatus === 'error') return 'error';
  568. return 'ready';
  569. //return 'result';
  570. },
  571. // uiState:根据当前Tab展示对应的结果/提示状态(不改变蓝牙业务逻辑)
  572. uiState() {
  573. if (this.activeTab === 'bp') return this.bpUiState;
  574. if (!this.connected) return 'disconnected';
  575. if (this.activeTab === 'glucose') {
  576. if (this.glucoseErrorMsg) return 'error';
  577. if (this.lastGlucose.value !== null) return 'result';
  578. return 'ready';
  579. }
  580. if (this.activeTab === 'uric') {
  581. console.log("qxj uricAcidErrorMsg",this.uricAcidErrorMsg);
  582. console.log("qxj lastUricAcid",this.lastUricAcid);
  583. if (this.uricAcidErrorMsg) return 'error';
  584. if (this.lastUricAcid.value !== null) return 'result';
  585. return 'ready';
  586. }
  587. return 'ready';
  588. },
  589. // 环形进度:用实时压力估算(用于样式展示),结果完成时为100%
  590. ringProgressInt() {
  591. if (this.activeTab !== 'bp') return this.uiState === 'result' ? 100 : 0;
  592. if (this.bpUiState === 'result') return 100;
  593. if (this.bpUiState !== 'measuring') return 0;
  594. if (this.bpCurrentPressure == null) return 0;
  595. const p = (Number(this.bpCurrentPressure) - 50) / 100; // 假设 50~150mmHg 对应 0~100%
  596. return Math.max(0, Math.min(100, Math.round(p * 100)));
  597. },
  598. ringProgress() {
  599. return this.ringProgressInt;
  600. },
  601. ringStyle() {
  602. const track = '#EAEAEA';
  603. const tabArcColor = this.activeTab === 'glucose' ? '#52D087' : '#FF7700';
  604. const arcActive =
  605. (this.activeTab === 'bp' && (this.bpUiState === 'measuring' || this.bpUiState === 'result')) || (this.activeTab !== 'bp' && this.uiState === 'result');
  606. const arc = arcActive ? tabArcColor : '#EAEAEA';
  607. return {
  608. '--p': this.ringProgress + '%',
  609. '--arc': arc,
  610. '--track': track
  611. };
  612. },
  613. displaySystolic() {
  614. if (this.lastBloodPressure.systolic != null) return this.lastBloodPressure.systolic;
  615. if (this.bpCurrentPressure == null) return '--';
  616. return Math.round(Number(this.bpCurrentPressure));
  617. },
  618. displayDiastolic() {
  619. if (this.lastBloodPressure.diastolic != null) return this.lastBloodPressure.diastolic;
  620. if (this.bpCurrentPressure == null) return '--';
  621. // UI展示近似:根据实时压力估算舒张压占比
  622. return Math.round(Number(this.bpCurrentPressure) * 0.64);
  623. },
  624. connectionPercent() {
  625. return this.connected ? 85 : 0;
  626. },
  627. lastSyncTime() {
  628. // 优先用血压时间,其次血糖/尿酸
  629. return this.cardBloodPressure.time ?? this.cardGlucose.time ?? this.lastUricAcid.time ?? null;
  630. },
  631. footerPreText() {
  632. const deviceName = this.deviceDisplayName || '设备';
  633. if (this.connected) {
  634. return `小护士血压仪已通过蓝牙连接(${this.connectionPercent}%)`;
  635. }
  636. if (this.boundDevice) {
  637. return `小护士血压仪待连接`;
  638. }
  639. return '';
  640. },
  641. footerTimeText() {
  642. const timeVal = this.lastSyncTime;
  643. if (timeVal == null || timeVal === '') return '最后同步时间:--';
  644. // 统一显示为:YYYY-MM-DD HH:mm(不带秒,避免 toLocaleString 受地区影响)
  645. let d;
  646. if (typeof timeVal === 'number') {
  647. d = new Date(timeVal);
  648. } else {
  649. d = new Date(timeVal);
  650. }
  651. if (Number.isNaN(d.getTime())) return `最后同步时间:${String(timeVal)}`;
  652. const pad2 = (n) => String(n).padStart(2, '0');
  653. const formatted = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
  654. return `最后同步时间:${formatted}`;
  655. },
  656. uricTrendText() {
  657. // 1:偏低 0:正常 2:偏高(简单阈值用于样式标签)
  658. const v = Number(this.lastUricAcid.value);
  659. if (!isFinite(v) || this.lastUricAcid.value === null) return ' ';
  660. // 常用阈值:150~416umol/L(与项目页面一致)
  661. if (v < 150) return '偏低';
  662. if (v > 416) return '偏高';
  663. return '正常';
  664. },
  665. bpResultText() {
  666. const sbp = Number(this.cardBloodPressure.systolic);
  667. const dbp = Number(this.cardBloodPressure.diastolic);
  668. if (!isFinite(sbp) || !isFinite(dbp) || this.cardBloodPressure.systolic == null || this.cardBloodPressure.diastolic == null) return '--';
  669. if (sbp < 90 || dbp < 60) return '偏低';
  670. if (sbp >= 140 || dbp >= 90) return '偏高';
  671. return '正常';
  672. },
  673. pulseResultText() {
  674. const pulse = Number(this.cardBloodPressure.pulse);
  675. if (!isFinite(pulse) || this.cardBloodPressure.pulse == null) return '--';
  676. if (pulse < 60) return '偏低';
  677. if (pulse > 100) return '偏高';
  678. return '正常';
  679. },
  680. glucoseResultText() {
  681. const rawValue = Number(this.cardGlucose.value);
  682. if (!isFinite(rawValue) || this.cardGlucose.value === null) return '--';
  683. const unit = String(this.cardGlucose.unit || '').toLowerCase();
  684. // 统一换算到 mmol/L 后再判断,避免不同单位阈值不一致
  685. const mmolValue = unit.includes('mg/dl') ? rawValue / 18 : rawValue;
  686. if (!isFinite(mmolValue)) return '--';
  687. if (mmolValue < 3.9) return '偏低';
  688. if (mmolValue > 6.1) return '偏高';
  689. return '正常';
  690. },
  691. uricResultText() {
  692. const rawValue = Number(this.lastUricAcid.value);
  693. if (!isFinite(rawValue) || this.lastUricAcid.value === null) return '--';
  694. const unit = String(this.lastUricAcid.unit || '').toLowerCase();
  695. // 尿酸统一按 umol/L 判断:mg/dL × 59.48 = umol/L
  696. const uricUmol = unit.includes('mg/dl') ? rawValue * 59.48 : rawValue;
  697. if (!isFinite(uricUmol)) return '--';
  698. if (uricUmol < 150) return '偏低';
  699. if (uricUmol > 416) return '偏高';
  700. return '正常';
  701. },
  702. tipText() {
  703. if (this.activeTab === 'bp') return '请保持坐姿,手臂平放桌面,袖带边缘距肘节2-3厘米。';
  704. if (this.activeTab === 'glucose') {
  705. if (this.glucoseErrorMsg) return '⚠️ ' + this.glucoseErrorMsg;
  706. if (this.glucoseStatus) return this.glucoseStatus;
  707. return '请滴入样本...';
  708. }
  709. if (this.activeTab === 'uric') {
  710. if (this.uricAcidErrorMsg) return '⚠️ ' + this.uricAcidErrorMsg;
  711. if (this.uricAcidStatus) return this.uricAcidStatus;
  712. return '请滴入样本...';
  713. }
  714. return '';
  715. }
  716. },
  717. onLoad(option) {
  718. this.selectUser = Number(option.selectUser || 0);
  719. this.isFamily = option.selectUser != '0';
  720. this.getUser();
  721. uni.onBLEConnectionStateChange((res) => {
  722. console.log(`[BLE Connection] deviceId: ${res.deviceId}, connected: ${res.connected}`);
  723. if (res.deviceId === this.deviceId && !res.connected) {
  724. this.connected = false;
  725. this.statusText = '连接已断开';
  726. this.bpProcessStatus = 'idle';
  727. this.bpProcessStep = '设备连接已断开,请重新开启设备';
  728. this.log('设备蓝牙连接已断开');
  729. this.userRequestedMeasure = false;
  730. this.bpMeasureStarted = false;
  731. this.bpCurrentPressure = null;
  732. }
  733. });
  734. },
  735. onUnload() {
  736. this.closeBLE();
  737. },
  738. methods: {
  739. /**
  740. * 页面进入后尝试自动连接已绑定设备,仅做连接与握手,不触发测量
  741. * - 读取本地绑定信息,打开蓝牙适配器
  742. * - 调用 connectDevice(false) 建立连接
  743. */
  744. goBack() {
  745. uni.navigateBack({ delta: 1 });
  746. },
  747. onPrimaryActionTap() {
  748. // 准备/异常时允许点击:不改动原有的测量逻辑,只做UI状态保护
  749. if (!this.connected) {
  750. uni.showToast({ title: '未连接', icon: 'none' });
  751. return;
  752. }
  753. if (this.uiState === 'measuring') return;
  754. if (typeof this.startBpMeasurement === 'function') this.startBpMeasurement();
  755. },
  756. async autoConnect() {
  757. if (!this.boundDevice) return;
  758. this.log('尝试自动连接绑定设备...');
  759. try {
  760. await promisifyUniApi('openBluetoothAdapter');
  761. await this.connectDevice(false);
  762. } catch (e) {
  763. this.log('蓝牙未开启或异常,自动连接取消');
  764. }
  765. },
  766. //开启后台扫描寻找绑定设备...
  767. async startBackgroundScanForBoundDevice() {
  768. if (this.scanning) return;
  769. this.log('开启后台扫描寻找绑定设备...');
  770. this.scanning = true;
  771. try {
  772. await promisifyUniApi('startBluetoothDevicesDiscovery', {
  773. allowDuplicatesKey: false
  774. });
  775. uni.onBluetoothDeviceFound((res) => {
  776. const newDevices = res.devices || [];
  777. const found = newDevices.find((d) => d.deviceId === this.boundDevice.deviceId);
  778. if (found) {
  779. this.log('扫描到绑定设备,发起连接...');
  780. this.stopScan();
  781. this.connectDevice(true);
  782. }
  783. });
  784. } catch (e) {
  785. this.scanning = false;
  786. this.log('后台扫描启动失败');
  787. }
  788. },
  789. unbindDevice(item) {
  790. uni.showModal({
  791. title: '解除绑定',
  792. content: '确定要解除绑定该设备吗?解绑后需重新扫描连接。',
  793. success: async (res) => {
  794. if (res.confirm) {
  795. if (this.deviceId == item.item && this.connected) {
  796. await this.disconnect();
  797. }
  798. if (!this.isFamily) {
  799. this.removeXHSDevice(item.deviceId);
  800. } else {
  801. this.removeXHSDevicefamily(item.deviceId);
  802. }
  803. const boundDeviceStr = uni.getStorageSync('bound_health_device');
  804. let boundDevice = null;
  805. try {
  806. // 解析缓存设备
  807. if (boundDeviceStr) boundDevice = JSON.parse(boundDeviceStr);
  808. // 校验:缓存设备是否在当前列表中
  809. const isDeviceValid = boundDevice && boundDevice.deviceId == item.deviceId;
  810. if (isDeviceValid) {
  811. uni.removeStorageSync('bound_health_device');
  812. }
  813. } catch (e) {}
  814. }
  815. }
  816. });
  817. },
  818. // 小护士解除绑定设备(自己)
  819. removeXHSDevice(deviceId) {
  820. removeXHSDevice({ xhsDeviceId: deviceId, deviceType: this.deviceType })
  821. .then(async (res) => {
  822. if (res.code == 200) {
  823. this.log('设备已解绑(接口)');
  824. this.xhsDeviceList = this.xhsDeviceList.filter((item) => item.deviceId != deviceId);
  825. uni.showToast({ title: '解绑成功', icon: 'success' });
  826. uni.$emit('scanFitWatch');
  827. } else {
  828. uni.showToast({
  829. title: res.msg,
  830. icon: 'none'
  831. });
  832. this.log('设备解绑失败(接口)');
  833. }
  834. })
  835. .catch((err) => {
  836. uni.showToast({
  837. title: '设备解绑失败',
  838. icon: 'none'
  839. });
  840. this.log('设备解绑失败err(接口)');
  841. });
  842. },
  843. removeXHSDevicefamily(deviceId) {
  844. let xhsDeviceId = this.xhsDeviceList.map((item) => item.deviceId);
  845. xhsDeviceId = xhsDeviceId.filter((item) => item != deviceId);
  846. this.otherDevice[this.selectUser - 1].xhsDeviceId = xhsDeviceId.join(',');
  847. console.log('==this.otherDevice=', this.otherDevice);
  848. editMyfamily({ otherDevice: JSON.stringify(this.otherDevice) })
  849. .then(async (res) => {
  850. if (res.code == 200) {
  851. this.log('设备已解绑(接口)');
  852. this.xhsDeviceList = this.xhsDeviceList.filter((item) => item.deviceId != deviceId);
  853. uni.showToast({ title: '解绑成功', icon: 'success' });
  854. uni.$emit('scanFitWatch');
  855. } else {
  856. uni.showToast({
  857. title: res.msg,
  858. icon: 'none'
  859. });
  860. this.log('设备绑定接口失败');
  861. }
  862. })
  863. .catch(async (err) => {
  864. uni.showToast({
  865. title: '设备解绑失败',
  866. icon: 'none'
  867. });
  868. this.log('设备解绑失败err(接口)');
  869. });
  870. },
  871. log(msg) {
  872. const line = `${new Date().toLocaleTimeString()} ${msg}`;
  873. this.logs.unshift(line);
  874. if (this.logs.length > 100) this.logs.pop();
  875. },
  876. async startScan() {
  877. this.deviceList = [];
  878. this.selectedDeviceId = '';
  879. this.log('打开蓝牙适配器...');
  880. console.log('打开蓝牙适配器...');
  881. try {
  882. await promisifyUniApi('openBluetoothAdapter');
  883. this.log('开始扫描低功耗蓝牙设备...');
  884. await promisifyUniApi('startBluetoothDevicesDiscovery', {
  885. allowDuplicatesKey: false
  886. });
  887. this.scanning = true;
  888. this.statusText = '正在扫描...';
  889. uni.onBluetoothDeviceFound((res) => {
  890. const nameContains = (d) => {
  891. const name = (d.name || d.localName || '').toLowerCase();
  892. return name.indexOf('bluetooth') !== -1;
  893. };
  894. // 已绑定的设备 ID 数组
  895. const boundIds = this.xhsDeviceList.map((item) => item.deviceId);
  896. // 过滤:只保留 名称符合 + 不在已绑定列表里 的设备
  897. const newDevices = (res.devices || []).filter((d) => {
  898. const isNameValid = nameContains(d);
  899. const isNotBound = !boundIds.includes(d.deviceId);
  900. const isNotExist = !this.deviceList.some((x) => x.deviceId === d.deviceId);
  901. return isNameValid && isNotBound && isNotExist;
  902. });
  903. if (newDevices.length) {
  904. this.deviceList = [...this.deviceList, ...newDevices];
  905. }
  906. });
  907. setTimeout(() => this.stopScan(), 10000);
  908. } catch (e) {
  909. this.scanning = false;
  910. this.statusText = '未连接';
  911. const msg = (e.errMsg || e.message || String(e)).toLowerCase();
  912. if (msg.indexOf('bluetooth') !== -1 || msg.indexOf('adapter') !== -1) {
  913. uni.showToast({ title: '请开启手机蓝牙', icon: 'none' });
  914. }
  915. this.log('扫描失败:' + (e.errMsg || e));
  916. }
  917. },
  918. async stopScan() {
  919. if (!this.scanning) return;
  920. try {
  921. await promisifyUniApi('stopBluetoothDevicesDiscovery');
  922. } catch (e) {}
  923. this.scanning = false;
  924. this.statusText = this.connected ? '已连接' : '未连接';
  925. this.log('扫描已停止,共发现 ' + this.deviceList.length + ' 个设备');
  926. },
  927. selectDevice(d) {
  928. this.selectedDeviceId = d.deviceId;
  929. this.selectedDeviceName = d.name || d.localName || '未知设备';
  930. this.connectDevice(false);
  931. },
  932. async sendData(buffer, description = '数据') {
  933. if (!this.deviceId || !this.writeServiceId || !this.writeCharId) return;
  934. const arr = new Uint8Array(buffer);
  935. const hex = Array.from(arr)
  936. .map((b) => ('0' + b.toString(16)).slice(-2))
  937. .join(' ');
  938. console.log(`[BLE Send Raw] ${description} Hex:`, hex);
  939. // 默认使用检测到的 writeType,如果没有则默认为 'write'
  940. let currentType = this.writeType;
  941. if (!currentType) {
  942. // 如果没有检测到明确的类型,尝试先用 write,如果失败会自动切
  943. currentType = 'write';
  944. }
  945. // 特殊处理:如果是 iOS 且没有明确 writeType,有些设备可能必须用 writeNoResponse
  946. const sys = uni.getSystemInfoSync();
  947. if (sys.platform === 'ios' && !this.writeType) {
  948. // iOS 下对某些外设,writeNoResponse 更稳定
  949. // currentType = 'writeNoResponse';
  950. }
  951. try {
  952. await promisifyUniApi('writeBLECharacteristicValue', {
  953. deviceId: this.deviceId,
  954. serviceId: this.writeServiceId,
  955. characteristicId: this.writeCharId,
  956. value: buffer,
  957. writeType: currentType
  958. });
  959. this.log(`已发送${description}: ${hex}`);
  960. } catch (e) {
  961. console.error(`[BLE Send Error] Type=${currentType}:`, e);
  962. // 不管什么错误,尝试切换 writeType 重试一次
  963. const retryType = currentType === 'write' ? 'writeNoResponse' : 'write';
  964. this.log(`发送失败 (${e.errCode || e.errMsg || e}),尝试切换为 ${retryType} 重试...`);
  965. try {
  966. await promisifyUniApi('writeBLECharacteristicValue', {
  967. deviceId: this.deviceId,
  968. serviceId: this.writeServiceId,
  969. characteristicId: this.writeCharId,
  970. value: buffer,
  971. writeType: retryType
  972. });
  973. this.writeType = retryType; // 如果重试成功,更新默认类型
  974. this.log(`重试成功 (${retryType}): ${hex}`);
  975. } catch (retryErr) {
  976. console.error(`[BLE Retry Error] Type=${retryType}:`, retryErr);
  977. // 还是失败,尝试不传 writeType (让系统自动判断)
  978. this.log(`二次重试失败,尝试不传 writeType...`);
  979. try {
  980. await promisifyUniApi('writeBLECharacteristicValue', {
  981. deviceId: this.deviceId,
  982. serviceId: this.writeServiceId,
  983. characteristicId: this.writeCharId,
  984. value: buffer
  985. // 不传 writeType
  986. });
  987. this.log(`三次重试成功 (Auto): ${hex}`);
  988. } catch (finalErr) {
  989. console.error(`[BLE Final Error]:`, finalErr);
  990. this.log(`发送最终失败: ${finalErr.errMsg || finalErr}`);
  991. }
  992. }
  993. }
  994. },
  995. // 手动开始血压测量
  996. /**
  997. * 用户点击后主动发起血压测量请求
  998. * - 要求蓝牙已经连接且写入特征可用
  999. * - 发送新协议开始测量指令 (FA 05)
  1000. */
  1001. startBpMeasurement() {
  1002. if (!this.writeCharId) {
  1003. uni.showToast({ title: '蓝牙未连接', icon: 'none' });
  1004. return;
  1005. }
  1006. this.userRequestedMeasure = true;
  1007. this.bpMeasureStarted = false;
  1008. this.bpErrorMsg = '';
  1009. this.bpProcessStatus = 'handshake';
  1010. this.bpProcessStep = '正在请求开始测量...';
  1011. // 新协议指令 (V2/V5协议)
  1012. const bufferV2D2 = buildV2Command(0xd2); // D2为V5协议的开始测量指令
  1013. // 旧协议指令
  1014. const buffer = new Uint8Array([0xfa, 0x05, 0x00, 0x00, 0x00, 0x00, 0x05, 0x04]).buffer;
  1015. // 连续发送几次以确保设备收到,旧设备握手+气泵启动可能需要较长时间,增加重试次数
  1016. this.retryCount = 0;
  1017. const maxRetries = 8; // 增加到8次,给予约8秒的等待时间
  1018. // console.log("this.bpMeasureStarted====",this.bpMeasureStarted,this.retryCount,maxRetries,!this.userRequestedMeasure)
  1019. this.log(`外面${this.bpMeasureStarted},${this.retryCount}---${maxRetries},${!this.userRequestedMeasure}`);
  1020. const sendTimer = setInterval(() => {
  1021. this.log(
  1022. `里面${this.bpMeasureStarted},${this.retryCount}---${maxRetries},${!this.userRequestedMeasure}====${
  1023. this.bpMeasureStarted || this.retryCount >= maxRetries || !this.userRequestedMeasure
  1024. }`
  1025. );
  1026. if (this.bpMeasureStarted || this.retryCount >= maxRetries || !this.userRequestedMeasure) {
  1027. clearInterval(sendTimer);
  1028. if (this.retryCount >= maxRetries && !this.bpMeasureStarted) {
  1029. this.log('设备未响应测量指令');
  1030. this.bpProcessStep = '设备未响应,请重试';
  1031. uni.showToast({ title: '设备未响应,请重试', icon: 'none' });
  1032. }
  1033. return;
  1034. }
  1035. this.log('=======开始测量12====');
  1036. this.sendData(buffer, '开始测量(FA 05)==1');
  1037. // 延迟一点发送兼容指令
  1038. setTimeout(() => {
  1039. if (!this.bpMeasureStarted && this.userRequestedMeasure) {
  1040. this.sendData(bufferV2D2, '开始测量(D2)==1');
  1041. }
  1042. }, 200);
  1043. this.retryCount++;
  1044. }, 2000);
  1045. // 立即发送第一次
  1046. this.sendData(buffer, '开始测量(FA 05)==2');
  1047. setTimeout(() => {
  1048. this.sendData(bufferV2D2, '开始测量(D2)==2');
  1049. }, 200);
  1050. this.retryCount++;
  1051. },
  1052. // 进入血检模式
  1053. enterBloodTest() {
  1054. if (!this.writeCharId) {
  1055. uni.showToast({ title: '蓝牙未连接', icon: 'none' });
  1056. return;
  1057. }
  1058. this.log('请求进入血检模式...');
  1059. const bufferV2D3 = buildV2Command(0xd3); // D3为进入血检界面指令
  1060. this.sendData(bufferV2D3, '进入血检(D3)');
  1061. },
  1062. async connectDevice(isAutoConnect = false) {
  1063. if (!this.selectedDeviceId) {
  1064. uni.showToast({ title: '请先选择设备', icon: 'none' });
  1065. return;
  1066. }
  1067. this.userRequestedMeasure = false; // 连接时重置状态,等待用户点击开始测量
  1068. this.connecting = true;
  1069. this.log('正在连接 ' + this.selectedDeviceName + '...');
  1070. this.statusText = '连接中...';
  1071. try {
  1072. await promisifyUniApi('createBLEConnection', {
  1073. deviceId: this.selectedDeviceId,
  1074. timeout: 10000
  1075. });
  1076. this.deviceId = this.selectedDeviceId;
  1077. this.connected = true;
  1078. this.statusText = '已连接';
  1079. this.log('连接成功,正在发现服务...');
  1080. await new Promise((r) => setTimeout(r, 600));
  1081. const { services } = await promisifyUniApi('getBLEDeviceServices', {
  1082. deviceId: this.deviceId
  1083. });
  1084. if (!services || services.length === 0) {
  1085. this.log('未发现服务');
  1086. this.connecting = false;
  1087. return;
  1088. }
  1089. this.log('发现服务: ' + services.length + ' 个');
  1090. services.forEach((s, i) => {
  1091. console.log(`[Service ${i}] UUID: ${s.uuid}, IsPrimary: ${s.isPrimary}`);
  1092. this.log(`S${i}: ${s.uuid.slice(4, 8)}`);
  1093. });
  1094. // 优先尝试 FFF0 (回到最初的起点)
  1095. let svc = services.find((s) => s.uuid.toLowerCase().includes('fff0'));
  1096. if (!svc) {
  1097. // 如果没有 FFF0,尝试 FF10
  1098. svc = services.find((s) => s.uuid.toLowerCase().includes('ff10'));
  1099. }
  1100. if (svc) {
  1101. this.serviceId = svc.uuid;
  1102. this.log('使用服务: ' + this.serviceId.slice(4, 8));
  1103. // 如果是 FF10,特征值可能也是 FF11/FF12,或者其他
  1104. // 需要重新获取该服务的特征值
  1105. } else {
  1106. this.log('错误: 未找到有效服务 (FF10/FFF0)');
  1107. this.connecting = false;
  1108. return;
  1109. }
  1110. const { characteristics } = await promisifyUniApi('getBLEDeviceCharacteristics', {
  1111. deviceId: this.deviceId,
  1112. serviceId: this.serviceId
  1113. });
  1114. // 打印所有特征值的详细属性,便于排查
  1115. this.log('发现特征值: ' + characteristics.length + ' 个');
  1116. characteristics.forEach((c, index) => {
  1117. const props = [];
  1118. if (c.properties.read) props.push('Read');
  1119. if (c.properties.write) props.push('Write');
  1120. if (c.properties.writeNoResponse) props.push('WriteNoResp');
  1121. if (c.properties.notify) props.push('Notify');
  1122. if (c.properties.indicate) props.push('Indicate');
  1123. console.log(`[Char ${index}] UUID: ${c.uuid}, Props: ${props.join('|')}`);
  1124. this.log(`C${index}: ${c.uuid.slice(4, 8)} [${props.join('|')}]`);
  1125. });
  1126. // 1. 查找接收特征 (Notify/Indicate)
  1127. // 尝试匹配 FFF1, FF11 或任何 Notify
  1128. let notifyChar = characteristics.find(
  1129. (c) => (c.uuid.toLowerCase().includes('fff1') || c.uuid.toLowerCase().includes('ff11')) && (c.properties.notify || c.properties.indicate)
  1130. );
  1131. // 如果没找到,尝试只匹配 UUID
  1132. if (!notifyChar) {
  1133. notifyChar = characteristics.find((c) => c.uuid.toLowerCase().includes('fff1') || c.uuid.toLowerCase().includes('ff11'));
  1134. }
  1135. // 如果还是没找到,回退到任意具备 Notify/Indicate 属性的特征
  1136. if (!notifyChar) {
  1137. this.log('未找到标准接收特征,尝试回退...');
  1138. notifyChar = characteristics.find((c) => c.properties.notify || c.properties.indicate);
  1139. }
  1140. if (notifyChar) {
  1141. this.notifyCharId = notifyChar.uuid;
  1142. this.notifyServiceId = this.serviceId;
  1143. } else {
  1144. this.log('错误: 未找到任何可接收数据的特征值');
  1145. this.connecting = false;
  1146. return;
  1147. }
  1148. // 2. 查找发送特征 (Write/WriteNoResponse)
  1149. // 尝试匹配 FFF2, FF12 或任何 Write
  1150. let writeChar = characteristics.find(
  1151. (c) => (c.uuid.toLowerCase().includes('fff2') || c.uuid.toLowerCase().includes('ff12')) && (c.properties.write || c.properties.writeNoResponse)
  1152. );
  1153. // 如果没找到,回退到任意具备 Write/WriteNoResponse 属性的特征
  1154. if (!writeChar) {
  1155. this.log('未找到标准发送特征,尝试回退...');
  1156. writeChar = characteristics.find((c) => c.properties.write || c.properties.writeNoResponse);
  1157. }
  1158. if (writeChar) {
  1159. this.writeServiceId = this.serviceId;
  1160. this.writeCharId = writeChar.uuid;
  1161. // 确定写入类型:优先使用 write (带响应),如果不支持则用 writeNoResponse
  1162. // 注意:有些设备虽然标明 write,但实际需要 writeNoResponse,反之亦然
  1163. // 我们这里记录首选类型,在 sendData 中会实现自动降级重试
  1164. if (writeChar.properties.write) {
  1165. this.writeType = 'write';
  1166. } else if (writeChar.properties.writeNoResponse) {
  1167. this.writeType = 'writeNoResponse';
  1168. } else {
  1169. this.writeType = 'write'; // 默认
  1170. }
  1171. this.log(`发送特征就绪 (Type: ${this.writeType})`);
  1172. } else {
  1173. this.log('警告: 未找到可写特征,无法发送指令');
  1174. }
  1175. this.log(`最终配置: Notify=${this.notifyCharId.slice(4, 8)}, Write=${this.writeCharId ? this.writeCharId.slice(4, 8) : '无'}`);
  1176. await promisifyUniApi('notifyBLECharacteristicValueChange', {
  1177. deviceId: this.deviceId,
  1178. serviceId: this.notifyServiceId,
  1179. characteristicId: this.notifyCharId,
  1180. state: true
  1181. });
  1182. this.log('已开启数据通知');
  1183. // 不再自动发送任何握手指令,等待用户点击开始测量
  1184. if (this.writeCharId) {
  1185. this.log('连接成功,等待用户操作');
  1186. this.bpProcessStatus = 'connected';
  1187. this.bpProcessStep = '已连接,请点击开始测量';
  1188. }
  1189. this.log('已开启数据通知');
  1190. uni.onBLECharacteristicValueChange((res) => {
  1191. if (res.deviceId !== this.deviceId) return;
  1192. const buf = res.value;
  1193. const arr = new Uint8Array(buf);
  1194. const hex = Array.from(arr)
  1195. .map((b) => ('0' + b.toString(16)).slice(-2))
  1196. .join(' ');
  1197. console.log('[BLE Recv Raw] Hex:', hex);
  1198. console.log('[BLE Recv Raw] Bytes:', arr);
  1199. // this.log('收到数据: ' + hex);
  1200. // 1. 尝试解析血压协议 (FD FD)
  1201. // 尝试解析 V2 格式 (FD FD Len Cmd ... CS 0D 0A)
  1202. // V2格式特点:第2字节(索引2)是Len,最后一个字节是0A,倒数第二个是0D,倒数第三个是CS
  1203. let isV2 = false;
  1204. let v2Cmd = null;
  1205. let v2Data = null;
  1206. if (arr.length >= 6 && arr[0] === 0xfd && arr[1] === 0xfd) {
  1207. const len = arr[2];
  1208. if (arr.length >= 2 + 1 + len + 2) {
  1209. const content = arr.slice(3, 3 + len);
  1210. v2Cmd = content[0];
  1211. v2Data = content.slice(1, content.length - 1);
  1212. const checksum = content[content.length - 1];
  1213. let sum = v2Cmd;
  1214. for (let i = 0; i < v2Data.length; i++) sum += v2Data[i];
  1215. if ((sum & 0xff) === checksum) {
  1216. isV2 = true;
  1217. }
  1218. }
  1219. }
  1220. // 处理旧协议 V2 逻辑
  1221. if (isV2) {
  1222. // 1. 握手: A0 -> D1
  1223. if (v2Cmd === 0xa0) {
  1224. this.log('收到旧协议广播 (A0),发送连接请求 (D1)...');
  1225. this.bpProcessStatus = 'handshake';
  1226. this.bpProcessStep = '收到广播,请求连接 (D1)...';
  1227. this.sendData(buildV2Command(0xd1), '连接请求(D1)');
  1228. return;
  1229. }
  1230. // 1.1 握手确认: D1 -> D1 (设备回显)
  1231. if (v2Cmd === 0xd1) {
  1232. this.log('收到 D1 回显,等待 AF...');
  1233. this.bpProcessStatus = 'handshake';
  1234. this.bpProcessStep = '设备已确认请求,等待连接成功(AF)...';
  1235. return;
  1236. }
  1237. // 2. 握手成功: AF
  1238. if (v2Cmd === 0xaf) {
  1239. this.log('收到连接建立成功信号 (AF)');
  1240. this.bpProcessStatus = 'handshake';
  1241. if (this.userRequestedMeasure) {
  1242. this.bpProcessStep = '连接成功,发送开始测量指令 (D2)...';
  1243. this.sendData(buildV2Command(0xd2), '开始测量(D2)');
  1244. } else {
  1245. this.bpProcessStep = '设备已就绪,等待用户点击开始测量';
  1246. this.log('设备已就绪,等待用户点击开始测量,暂不发送D2');
  1247. }
  1248. return;
  1249. }
  1250. // 2.1 血压测量确认: A1 (设备回复)
  1251. if (v2Cmd === 0xa1) {
  1252. this.log('设备已确认测量请求 (A1)');
  1253. this.bpProcessStatus = 'measuring';
  1254. this.bpProcessStep = '设备已就绪,等待加压...';
  1255. this.bpMeasureStarted = true;
  1256. return;
  1257. }
  1258. // 2.2 进入血检确认: D4 (设备回复)
  1259. if (v2Cmd === 0xd4) {
  1260. this.log('设备已进入血检界面 (D4)');
  1261. this.bpProcessStatus = 'idle';
  1262. this.bpProcessStep = '请插入试纸...';
  1263. uni.showToast({ title: '请插入试纸', icon: 'none' });
  1264. return;
  1265. }
  1266. // 5. 血压测量过程 (A2)
  1267. if (v2Cmd === 0xa2) {
  1268. this.bpProcessStatus = 'measuring';
  1269. if (v2Data.length >= 2) {
  1270. const pressure = v2Data[0] * 256 + v2Data[1];
  1271. this.bpCurrentPressure = pressure;
  1272. this.bpMeasureStarted = true;
  1273. this.bpProcessStep = `测量中... ${pressure} mmHg`;
  1274. }
  1275. return;
  1276. }
  1277. // 测试结果 (A3)
  1278. if (v2Cmd === 0xa3) {
  1279. // [0xFD,0xFD, 0x07, 0xA3, SYS, DIA, 单位, PUL, IHB, Checksum, 0X0D, 0x0A]
  1280. // v2Data: [SYS, DIA, 单位, PUL, IHB]
  1281. if (v2Data.length >= 5) {
  1282. const systolic = v2Data[0];
  1283. const diastolic = v2Data[1];
  1284. const unitByte = v2Data[2]; // 00=mmHg, 01=kPa (通常是00)
  1285. const pulse = v2Data[3];
  1286. const ihb = v2Data[4];
  1287. this.userRequestedMeasure = false;
  1288. this.bpMeasureStarted = false; // 测量结束,重置开始标志
  1289. if (systolic > 0 && diastolic > 0) {
  1290. this.bpCurrentPressure = null;
  1291. this.bpErrorMsg = '';
  1292. this.bpProcessStatus = 'result';
  1293. this.bpProcessStep = '测量完成';
  1294. this.lastBloodPressure = {
  1295. systolic,
  1296. diastolic,
  1297. pulse,
  1298. ihb: ihb === 0x01 ? 1 : 0,
  1299. time: Date.now()
  1300. };
  1301. this.cardBloodPressure = this.lastBloodPressure;
  1302. this.$forceUpdate();
  1303. this.log(`血压(V2): ${systolic}/${diastolic} mmHg 脉率:${pulse}`);
  1304. this.xhsDataAdd(1, 0, this.lastBloodPressure);
  1305. } else {
  1306. this.bpCurrentPressure = null;
  1307. this.bpErrorMsg = '测量结果异常 (数据为0)';
  1308. this.bpProcessStatus = 'error';
  1309. this.bpProcessStep = '测量失败';
  1310. this.log('血压计错误(V2): 测量结果为0');
  1311. uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
  1312. }
  1313. }
  1314. return;
  1315. }
  1316. // 6. 血压测量错误 (A4)
  1317. if (v2Cmd === 0xa4) {
  1318. const errCode = v2Data[0] || 0;
  1319. this.userRequestedMeasure = false; // 测量结束,重置状态
  1320. this.bpMeasureStarted = false; // 测量结束,重置开始标志
  1321. this.bpErrorMsg = '测量错误请重试';
  1322. this.bpProcessStatus = 'error';
  1323. this.bpProcessStep = '测量失败';
  1324. this.log(`血压计测量错误(V2): 错误码 ${errCode.toString(16).toUpperCase()}`);
  1325. uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
  1326. return;
  1327. }
  1328. // 7. 血糖测量开始 (BB)
  1329. if (v2Cmd === 0xbb) {
  1330. this.log('收到血糖测量开始指令 (BB),回复 D9...');
  1331. this.glucoseStatus = '请滴入样本...';
  1332. this.glucoseErrorMsg = '';
  1333. this.sendData(buildV2Command(0xd9), '回复血糖开始(D9)');
  1334. return;
  1335. }
  1336. // 8. 血糖测量结果 (B2)
  1337. if (v2Cmd === 0xb2) {
  1338. if (v2Data.length >= 3) {
  1339. const valH = v2Data[0];
  1340. const valL = v2Data[1];
  1341. const unitByte = v2Data[2];
  1342. const value = (valH << 8) | valL;
  1343. let displayVal = '0.0';
  1344. if (valH === 0xff && valL === 0xff) {
  1345. displayVal = 'HIGH';
  1346. } else if (valH === 0x00 && valL === 0x00) {
  1347. displayVal = 'LOW';
  1348. } else {
  1349. displayVal = (value / 10).toFixed(1);
  1350. }
  1351. this.lastGlucose = {
  1352. value: displayVal,
  1353. unit: unitByte === 0x01 ? 'mg/dL' : 'mmol/L',
  1354. time: Date.now()
  1355. };
  1356. this.cardGlucose = this.lastGlucose;
  1357. this.glucoseStatus = '测量完成';
  1358. this.log(`血糖结果(V2): ${displayVal} ${this.lastGlucose.unit}`);
  1359. this.sendData(buildV2Command(0xda), '回复血糖结果(DA)');
  1360. }
  1361. return;
  1362. }
  1363. // 9. 血糖报错 (B3)
  1364. if (v2Cmd === 0xb3) {
  1365. const errCode = v2Data[0];
  1366. this.glucoseStatus = '';
  1367. this.glucoseErrorMsg = `测量错误: 错误码 ${errCode.toString(16).toUpperCase()}`;
  1368. this.log(`血糖报错(V2): ${errCode.toString(16).toUpperCase()}`);
  1369. this.sendData(buildV2Command(0xdb), '回复血糖报错(DB)');
  1370. return;
  1371. }
  1372. // 10. 尿酸测量开始 (CC)
  1373. if (v2Cmd === 0xcc) {
  1374. this.log('收到尿酸测量开始指令 (CC),回复 DC...');
  1375. this.uricAcidStatus = '请滴入样本...';
  1376. this.uricAcidErrorMsg = '';
  1377. this.sendData(buildV2Command(0xdc), '回复尿酸开始(DC)');
  1378. return;
  1379. }
  1380. // 11. 尿酸测量结果 (C2)
  1381. if (v2Cmd === 0xc2) {
  1382. if (v2Data.length >= 3) {
  1383. const valH = v2Data[0];
  1384. const valL = v2Data[1];
  1385. const unitByte = v2Data[2];
  1386. const value = (valH << 8) | valL;
  1387. let displayVal = '0';
  1388. if (valH === 0xff && valL === 0xff) {
  1389. displayVal = 'HIGH';
  1390. } else if (valH === 0x00 && valL === 0x00) {
  1391. displayVal = 'LOW';
  1392. } else {
  1393. displayVal = value.toString();
  1394. }
  1395. this.lastUricAcid = {
  1396. value: displayVal,
  1397. unit: unitByte === 0x01 ? 'mg/dL' : 'umol/L',
  1398. time: Date.now()
  1399. };
  1400. this.uricAcidStatus = '测量完成';
  1401. this.log(`尿酸结果(V2): ${displayVal} ${this.lastUricAcid.unit}`);
  1402. this.sendData(buildV2Command(0xdd), '回复尿酸结果(DD)');
  1403. }
  1404. return;
  1405. }
  1406. // 12. 尿酸报错 (C3)
  1407. if (v2Cmd === 0xc3) {
  1408. const errCode = v2Data[0];
  1409. this.uricAcidStatus = '';
  1410. this.uricAcidErrorMsg = `测量错误: 错误码 ${errCode.toString(16).toUpperCase()}`;
  1411. this.log(`尿酸报错(V2): ${errCode.toString(16).toUpperCase()}`);
  1412. this.sendData(buildV2Command(0xde), '回复尿酸报错(DE)');
  1413. return;
  1414. }
  1415. }
  1416. // 新协议解析 (FD FD ...)
  1417. const bpPacket = parseBpPacket(arr);
  1418. if (bpPacket) {
  1419. const { cmd, data } = bpPacket;
  1420. // 兼容:旧协议的 A0 广播 (FD FD 02 A0 A0 0D 0A) 或 D1 (FD FD 02 D1 D1 0D 0A)
  1421. // 如果 isV2 为 true 且是 A0,已经在上面处理过了。这里作为备用。
  1422. if (cmd === 0x02 && data.length >= 2 && data[0] === 0xa0 && data[1] === 0xa0) {
  1423. if (!isV2) {
  1424. this.log('收到旧协议广播 (A0)[fallback],发送 D1');
  1425. this.sendData(buildV2Command(0xd1), '连接请求(D1)');
  1426. }
  1427. return;
  1428. }
  1429. // A4 广播 (等待连接)
  1430. if (cmd === CMD_BP_BROADCAST_A4) {
  1431. if (data.length >= 3) {
  1432. this.bpWaitUser = data[2]; // 用户号在第3个字节(索引2)
  1433. }
  1434. this.log('收到血压计唤醒请求 (A4)');
  1435. // 如果用户已点击开始测量,持续回复 FA 05 直到设备确认并停止发送 A4
  1436. if (this.userRequestedMeasure) {
  1437. this.bpProcessStep = '设备唤醒中,正在同步指令...';
  1438. const buffer = buildBpCommand(CMD_BP_CONNECT_FA, [0x05]);
  1439. this.sendData(buffer, '响应A4(FA 05)');
  1440. } else {
  1441. this.bpProcessStep = '设备就绪,等待用户开始测量';
  1442. }
  1443. return;
  1444. }
  1445. // 06 确认开始测量
  1446. if (cmd === CMD_BP_CONFIRM_06 || (cmd === CMD_BP_CONNECT_FA && data[0] === 0x06)) {
  1447. this.log('设备已确认测量请求 (06)');
  1448. this.bpProcessStatus = 'measuring';
  1449. this.bpProcessStep = '设备已就绪,等待加压...';
  1450. this.bpMeasureStarted = true;
  1451. return;
  1452. }
  1453. // FB 测量中压力
  1454. if (cmd === CMD_BP_PRESSURE_FB) {
  1455. this.bpProcessStatus = 'measuring';
  1456. if (data.length >= 2) {
  1457. const pressure = data[0] * 256 + data[1];
  1458. this.bpCurrentPressure = pressure;
  1459. this.bpMeasureStarted = true;
  1460. this.bpProcessStep = `测量中... ${pressure} mmHg`;
  1461. }
  1462. return;
  1463. }
  1464. // FC 测试结果
  1465. if (cmd === CMD_BP_RESULT_FC) {
  1466. // data: [SYS, DIA, PUL, IHB]
  1467. if (data.length >= 4) {
  1468. const systolic = data[0];
  1469. const diastolic = data[1];
  1470. const pulse = data[2];
  1471. const ihb = data[3];
  1472. this.userRequestedMeasure = false;
  1473. this.bpMeasureStarted = false; // 测量结束,重置开始标志
  1474. if (systolic > 0 && diastolic > 0) {
  1475. this.bpCurrentPressure = null;
  1476. this.bpErrorMsg = '';
  1477. this.bpProcessStatus = 'result';
  1478. this.bpProcessStep = '测量完成';
  1479. this.lastBloodPressure = {
  1480. systolic,
  1481. diastolic,
  1482. pulse,
  1483. ihb: ihb === 0x01 ? 1 : 0,
  1484. time: Date.now()
  1485. };
  1486. this.cardBloodPressure = this.lastBloodPressure;
  1487. this.$forceUpdate();
  1488. this.log(`血压: ${systolic}/${diastolic} mmHg`);
  1489. // 按照文档要求回复 FA 60 确认指令,防止设备重复发送
  1490. const replyBuffer = buildBpCommand(CMD_BP_CONNECT_FA, [0x60]);
  1491. this.sendData(replyBuffer, '确认结果(FA 60)');
  1492. this.xhsDataAdd(1, 0, this.lastBloodPressure);
  1493. } else {
  1494. this.bpCurrentPressure = null;
  1495. this.bpErrorMsg = '测量结果异常 (数据为0)';
  1496. this.bpProcessStatus = 'error';
  1497. this.bpProcessStep = '测量失败';
  1498. this.log('血压计错误: 测量结果为0');
  1499. uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
  1500. }
  1501. }
  1502. return;
  1503. }
  1504. // FD 测量错误
  1505. if (cmd === CMD_BP_ERROR_FD) {
  1506. const errCode = data[0] || 0;
  1507. this.userRequestedMeasure = false; // 测量结束,重置状态
  1508. this.bpMeasureStarted = false; // 测量结束,重置开始标志
  1509. this.bpCurrentPressure = null;
  1510. this.bpErrorMsg = '测量错误请重试';
  1511. this.bpProcessStatus = 'error';
  1512. this.bpProcessStep = '测量失败';
  1513. this.log(`血压计测量错误: 错误码 ${errCode.toString(16).toUpperCase()}`);
  1514. uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
  1515. return;
  1516. }
  1517. return;
  1518. }
  1519. // 2. 尝试解析血糖/尿酸协议 (A5 A5)
  1520. const glucoseRes = parseGlucosePacket(buf);
  1521. if (glucoseRes) {
  1522. if (glucoseRes.type === 'glucose_start') {
  1523. this.glucoseStatus = '请滴入样本...';
  1524. this.glucoseErrorMsg = '';
  1525. this.log('血糖测量开始');
  1526. } else if (glucoseRes.type === 'uric_start') {
  1527. this.uricAcidStatus = '请滴入样本...';
  1528. this.uricAcidErrorMsg = '';
  1529. this.log('尿酸测量开始');
  1530. } else if (glucoseRes.type === 'glucose_result') {
  1531. this.lastGlucose = {
  1532. value: glucoseRes.value,
  1533. unit: glucoseRes.unit,
  1534. time: glucoseRes.time
  1535. };
  1536. this.cardGlucose = this.lastGlucose;
  1537. this.glucoseStatus = '测量完成';
  1538. this.log(`血糖结果: ${glucoseRes.value} ${glucoseRes.unit}`);
  1539. this.xhsDataAdd(1, 1, this.lastGlucose);
  1540. } else if (glucoseRes.type === 'uric_result') {
  1541. this.lastUricAcid = {
  1542. value: glucoseRes.value,
  1543. unit: glucoseRes.unit,
  1544. time: glucoseRes.time
  1545. };
  1546. this.uricAcidStatus = '测量完成';
  1547. this.log(`尿酸结果: ${glucoseRes.value} ${glucoseRes.unit}`);
  1548. this.xhsDataAdd(1, 3, this.lastUricAcid);
  1549. } else if (glucoseRes.type === 'glucose_error') {
  1550. this.glucoseErrorMsg = glucoseRes.msg;
  1551. this.glucoseStatus = '';
  1552. this.log(glucoseRes.msg);
  1553. } else if (glucoseRes.type === 'uric_error') {
  1554. this.uricAcidErrorMsg = glucoseRes.msg;
  1555. this.uricAcidStatus = '';
  1556. this.log(glucoseRes.msg);
  1557. }
  1558. if (glucoseRes.replyCmd) {
  1559. this.sendData(glucoseRes.replyCmd, '回复确认');
  1560. }
  1561. return;
  1562. }
  1563. });
  1564. uni.showToast({ title: '连接成功', icon: 'success' });
  1565. console.log('!this.boundDevice===', !this.boundDevice);
  1566. if (!this.boundDevice) {
  1567. if (this.isFamily) {
  1568. this.editMyfamily(this.selectedDeviceId);
  1569. return;
  1570. }
  1571. this.editXHSDevice(this.selectedDeviceId);
  1572. }
  1573. } catch (e) {
  1574. this.connected = false;
  1575. if (!this.boundDevice) {
  1576. this.deviceId = '';
  1577. }
  1578. this.statusText = '未连接';
  1579. const errMsg = e.errMsg || e.message || String(e);
  1580. this.log('连接失败: ' + errMsg);
  1581. uni.showToast({ title: '连接失败', icon: 'none' });
  1582. if (isAutoConnect === true && this.boundDevice) {
  1583. this.log('直接连接失败,尝试后台扫描...');
  1584. this.startBackgroundScanForBoundDevice();
  1585. }
  1586. }
  1587. this.connecting = false;
  1588. },
  1589. async disconnect() {
  1590. this.disconnecting = true;
  1591. this.log('断开连接...');
  1592. await this.closeBLE();
  1593. this.connected = false;
  1594. this.deviceId = '';
  1595. this.serviceId = '';
  1596. this.characteristicId = '';
  1597. this.notifyServiceId = '';
  1598. this.notifyCharId = '';
  1599. this.writeServiceId = '';
  1600. this.writeCharId = '';
  1601. this.lastGlucose = { value: null, unit: 'mmol/L', time: null };
  1602. this.glucoseStatus = '';
  1603. this.glucoseErrorMsg = '';
  1604. this.lastUricAcid = { value: null, unit: 'umol/L', time: null };
  1605. this.uricAcidStatus = '';
  1606. this.uricAcidErrorMsg = '';
  1607. this.lastBloodPressure = { systolic: null, diastolic: null, pulse: null, ihb: null, time: null };
  1608. this.bpMeasureStarted = false;
  1609. this.userRequestedMeasure = false;
  1610. this.bpCurrentPressure = null;
  1611. this.bpErrorMsg = '';
  1612. this.statusText = '未连接';
  1613. this.disconnecting = false;
  1614. this.log('已断开');
  1615. const boundDeviceStr = uni.getStorageSync('bound_health_device');
  1616. let boundDevice = null;
  1617. try {
  1618. // 解析缓存设备
  1619. if (boundDeviceStr) boundDevice = JSON.parse(boundDeviceStr);
  1620. // 校验:缓存设备是否在当前列表中
  1621. const isDeviceValid = boundDevice && boundDevice.deviceId == item.deviceId;
  1622. if (isDeviceValid) {
  1623. uni.removeStorageSync('bound_health_device');
  1624. }
  1625. } catch (e) {}
  1626. },
  1627. async closeBLE() {
  1628. try {
  1629. if (this.deviceId) {
  1630. await promisifyUniApi('closeBLEConnection', { deviceId: this.deviceId });
  1631. }
  1632. } catch (e) {}
  1633. try {
  1634. await promisifyUniApi('closeBluetoothAdapter');
  1635. } catch (e) {}
  1636. },
  1637. // 获取接口数据
  1638. getLastData() {
  1639. this.dataSync = 'loading';
  1640. const param = {
  1641. deviceId: this.selectedDeviceId,
  1642. deviceType: this.deviceType
  1643. };
  1644. getLastData(param).then((res) => {
  1645. if (res.code == 200) {
  1646. // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1647. // 状态标记
  1648. this.dataSync = 'success';
  1649. // 安全获取数据列表
  1650. const { data = [] } = res;
  1651. // 一次性提取所有需要的数据(只遍历一次数组,性能更好)
  1652. const bloodPressure = data.find((item) => item.recordType === 0);
  1653. const pulse = data.find((item) => item.recordType === 2);
  1654. const glucose = data.find((item) => item.recordType === 1);
  1655. const uricAcid = data.find((item) => item.recordType === 3);
  1656. // 血压数据解析(安全处理)
  1657. let bpData = {};
  1658. try {
  1659. bpData = bloodPressure?.recordValue ? JSON.parse(bloodPressure.recordValue) : {};
  1660. } catch (e) {
  1661. bpData = {};
  1662. }
  1663. // 统一赋值(带默认值,防止 undefined 报错)
  1664. this.cardGlucose = {
  1665. value: glucose?.recordValue || '',
  1666. unit: 'mmol/L',
  1667. time: glucose?.createTime || ''
  1668. };
  1669. this.lastUricAcid = {
  1670. value: uricAcid?.recordValue || null,
  1671. unit: 'umol/L',
  1672. time: uricAcid?.createTime || ''
  1673. };
  1674. this.cardBloodPressure = {
  1675. systolic: bpData.sdb || '',
  1676. diastolic: bpData.dbp || '',
  1677. pulse: pulse?.recordValue || '',
  1678. ihb: null,
  1679. time: bloodPressure?.createTime || ''
  1680. };
  1681. console.log('this.cardBloodPressure=====', this.cardBloodPressure);
  1682. } else {
  1683. this.dataSync = 'error';
  1684. this.log('数据同步失败');
  1685. }
  1686. })
  1687. .catch(() => {
  1688. this.dataSync = 'error';
  1689. this.log('数据同步失败');
  1690. });
  1691. },
  1692. editMyfamily(selectedDeviceId, type) {
  1693. if (!selectedDeviceId || this.xhsDeviceList.some((item) => item.deviceId == selectedDeviceId)) return;
  1694. let xhsDeviceId = this.xhsDeviceList.map((item) => item.deviceId);
  1695. xhsDeviceId.push(selectedDeviceId);
  1696. this.otherDevice[this.selectUser - 1].xhsDeviceId = xhsDeviceId.join(',');
  1697. editMyfamily({ otherDevice: JSON.stringify(this.otherDevice) })
  1698. .then(async (res) => {
  1699. if (res.code == 200) {
  1700. this.xhsDeviceList.push({ name: 'Bluetooth BP', deviceId: selectedDeviceId });
  1701. this.selectedDeviceId = selectedDeviceId;
  1702. this.selectedDeviceName = 'Bluetooth BP';
  1703. this.boundDevice = {
  1704. deviceId: this.selectedDeviceId,
  1705. name: this.selectedDeviceName
  1706. };
  1707. console.log('已绑定设备============' + this.boundDevice.deviceId);
  1708. this.log('已绑定设备: ' + this.boundDevice.deviceId);
  1709. uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
  1710. this.getLastData();
  1711. uni.$emit('scanFitWatch');
  1712. } else {
  1713. uni.showToast({
  1714. title: res.msg,
  1715. icon: 'none'
  1716. });
  1717. if (this.connected) {
  1718. await this.disconnect();
  1719. }
  1720. uni.removeStorageSync('bound_health_device');
  1721. this.log('设备绑定接口失败');
  1722. }
  1723. })
  1724. .catch(async (err) => {
  1725. this.log('设备绑定接口失败');
  1726. if (this.connected) {
  1727. await this.disconnect();
  1728. }
  1729. uni.removeStorageSync('bound_health_device');
  1730. });
  1731. },
  1732. editXHSDevice(selectedDeviceId) {
  1733. if (!selectedDeviceId || this.xhsDeviceList.some((item) => item.deviceId == selectedDeviceId)) return;
  1734. const xhsDeviceId = this.xhsDeviceList.map((item) => item.deviceId);
  1735. xhsDeviceId.push(selectedDeviceId);
  1736. const param = {
  1737. xhsDeviceId: xhsDeviceId.join(','), // 设备唯一MAC
  1738. deviceType: this.deviceType
  1739. };
  1740. editXHSDevice(param)
  1741. .then(async (res) => {
  1742. if (res.code == 200) {
  1743. this.xhsDeviceList.push({ name: 'Bluetooth BP', deviceId: selectedDeviceId });
  1744. this.selectedDeviceId = selectedDeviceId;
  1745. this.selectedDeviceName = 'Bluetooth BP';
  1746. this.boundDevice = {
  1747. deviceId: this.selectedDeviceId,
  1748. name: this.selectedDeviceName
  1749. };
  1750. console.log('已绑定设备============' + this.boundDevice.deviceId);
  1751. this.log('已绑定设备: ' + this.boundDevice.deviceId);
  1752. uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
  1753. uni.$emit('scanFitWatch');
  1754. this.getLastData();
  1755. } else {
  1756. uni.showToast({
  1757. title: res.msg,
  1758. icon: 'none'
  1759. });
  1760. if (this.connected) {
  1761. await this.disconnect();
  1762. }
  1763. uni.removeStorageSync('bound_health_device');
  1764. this.log('设备绑定接口失败');
  1765. }
  1766. })
  1767. .catch(async (err) => {
  1768. this.log('设备绑定接口失败');
  1769. if (this.connected) {
  1770. await this.disconnect();
  1771. }
  1772. uni.removeStorageSync('bound_health_device');
  1773. });
  1774. },
  1775. xhsDataAddTest(recordType) {
  1776. let recordValue = '';
  1777. let param = '';
  1778. if (recordType == 0) {
  1779. recordValue = {
  1780. sdb: '90',
  1781. dbp: '140'
  1782. };
  1783. param = [
  1784. {
  1785. recordType: 0, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1786. recordValue: JSON.stringify(recordValue),
  1787. deviceId: 'FC:62:58:04:19:1E',
  1788. deviceType: this.deviceType
  1789. },
  1790. {
  1791. recordType: 2, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1792. recordValue: 80,
  1793. deviceId: 'FC:62:58:04:19:1E',
  1794. deviceType: this.deviceType
  1795. }
  1796. ];
  1797. } else if (recordType == 1) {
  1798. param = [
  1799. {
  1800. recordType: 1, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1801. recordValue: 5,
  1802. deviceId: 'FC:62:58:04:19:1E',
  1803. deviceType: this.deviceType
  1804. }
  1805. ];
  1806. } else if (recordType == 3) {
  1807. param = [
  1808. {
  1809. recordType: 3, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1810. recordValue: '200',
  1811. deviceId: 'FC:62:58:04:19:1E',
  1812. deviceType: this.deviceType
  1813. }
  1814. ];
  1815. }
  1816. this.dataSync = 'loading';
  1817. xhsDataAddList(param)
  1818. .then((res) => {
  1819. if (res.code == 200) {
  1820. this.dataSync = 'success';
  1821. // this.getLastData()
  1822. uni.$emit('scanFitWatch');
  1823. } else {
  1824. this.dataSync = 'error';
  1825. this.log('数据同步失败');
  1826. }
  1827. })
  1828. .catch(() => {
  1829. this.dataSync = 'error';
  1830. this.log('数据同步失败');
  1831. });
  1832. },
  1833. // 接口数据同步
  1834. xhsDataAdd(type, recordType, data) {
  1835. if (type == 1) {
  1836. let recordValue = '';
  1837. this.dataSyncFunInfo = '';
  1838. if (recordType == 0) {
  1839. recordValue = {
  1840. sdb: data.systolic,
  1841. dbp: data.diastolic
  1842. };
  1843. this.dataSyncFunInfo = [
  1844. {
  1845. recordType: 0, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1846. recordValue: JSON.stringify(recordValue),
  1847. deviceId: this.deviceId,
  1848. deviceType: this.deviceType
  1849. },
  1850. {
  1851. recordType: 2, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1852. recordValue: data.pulse,
  1853. deviceId: this.deviceId,
  1854. deviceType: this.deviceType
  1855. }
  1856. ];
  1857. } else if (recordType == 1) {
  1858. this.dataSyncFunInfo = [
  1859. {
  1860. recordType: 1, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1861. recordValue: data.value,
  1862. deviceId: this.deviceId,
  1863. deviceType: this.deviceType
  1864. }
  1865. ];
  1866. } else if (recordType == 3) {
  1867. this.dataSyncFunInfo = [
  1868. {
  1869. recordType: 3, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  1870. recordValue: data.value,
  1871. deviceId: this.deviceId,
  1872. deviceType: this.deviceType
  1873. }
  1874. ];
  1875. }
  1876. }
  1877. this.dataSync = 'loading';
  1878. console.log('param===', this.dataSyncFunInfo);
  1879. xhsDataAddList(this.dataSyncFunInfo)
  1880. .then((res) => {
  1881. if (res.code == 200) {
  1882. this.dataSync = 'success';
  1883. // this.getLastData()
  1884. uni.$emit('scanFitWatch');
  1885. } else {
  1886. this.dataSync = 'error';
  1887. this.log('数据同步失败');
  1888. }
  1889. })
  1890. .catch(() => {
  1891. this.dataSync = 'error';
  1892. this.log('数据同步失败');
  1893. });
  1894. },
  1895. getUser() {
  1896. getWatchUserInfo({ isFamily: false }).then((res) => {
  1897. if (res.code == 200) {
  1898. let userInfo = res.user;
  1899. uni.setStorageSync('userWatchInfo', JSON.stringify(res.user));
  1900. // 1. 处理设备列表:家庭成员 / 个人模式
  1901. if (this.isFamily) {
  1902. // 安全解析家庭成员设备
  1903. this.otherDevice = userInfo.otherDevice ? JSON.parse(userInfo.otherDevice) : [];
  1904. // 格式化设备ID为对象数组
  1905. const formattedDevices = this.otherDevice.map((item) => ({
  1906. ...item,
  1907. xhsDeviceId: item.xhsDeviceId ? item.xhsDeviceId.split(',').map((id) => ({ deviceId: id, name: 'Bluetooth BP' })) : []
  1908. }));
  1909. // 安全取当前选中成员的设备(防越界)
  1910. const index = this.selectUser - 1;
  1911. this.xhsDeviceList = formattedDevices[index]?.xhsDeviceId || [];
  1912. } else {
  1913. // 个人设备格式化
  1914. const deviceIds = userInfo.xhsDeviceId ? userInfo.xhsDeviceId.split(',') : [];
  1915. this.xhsDeviceList = deviceIds.map((id) => ({ deviceId: id, name: 'Bluetooth BP' }));
  1916. }
  1917. // 2. 设备绑定 & 自动连接逻辑(统一处理,无重复代码)
  1918. if (!this.xhsDeviceList || this.xhsDeviceList.length === 0) {
  1919. // 无设备:清空绑定缓存
  1920. uni.removeStorageSync('bound_health_device');
  1921. this.boundDevice = null;
  1922. } else {
  1923. // 有设备:安全获取绑定设备
  1924. const boundDeviceStr = uni.getStorageSync('bound_health_device');
  1925. let boundDevice = null;
  1926. try {
  1927. // 解析缓存设备
  1928. if (boundDeviceStr) boundDevice = JSON.parse(boundDeviceStr);
  1929. // 校验:缓存设备是否在当前列表中
  1930. const isDeviceValid = boundDevice && this.xhsDeviceList.some((item) => item.deviceId == boundDevice.deviceId);
  1931. // 确定最终绑定设备
  1932. this.boundDevice = isDeviceValid ? boundDevice : this.xhsDeviceList[0];
  1933. // 持久化保存
  1934. uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
  1935. // 赋值选中设备信息
  1936. this.selectedDeviceId = this.boundDevice.deviceId;
  1937. this.selectedDeviceName = this.boundDevice.name;
  1938. this.deviceId = this.selectedDeviceId;
  1939. // 自动连接 & 获取数据
  1940. setTimeout(() => this.autoConnect(), 500);
  1941. this.getLastData();
  1942. } catch (e) {
  1943. // 异常兜底:绑定第一个设备
  1944. this.boundDevice = this.xhsDeviceList[0];
  1945. uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
  1946. // 赋值选中设备信息
  1947. this.selectedDeviceId = this.boundDevice.deviceId;
  1948. this.selectedDeviceName = this.boundDevice.name;
  1949. this.deviceId = this.selectedDeviceId;
  1950. // 自动连接 & 获取数据
  1951. setTimeout(() => this.autoConnect(), 500);
  1952. this.getLastData();
  1953. console.error('设备绑定异常:', e);
  1954. }
  1955. }
  1956. }
  1957. });
  1958. }
  1959. }
  1960. };
  1961. </script>
  1962. <style lang="scss" scoped>
  1963. .page {
  1964. min-height: 100vh;
  1965. background: #ffffff;
  1966. }
  1967. .footer-txt {
  1968. position: absolute;
  1969. bottom: 40rpx;
  1970. display: flex;
  1971. align-items: center;
  1972. justify-content: center;
  1973. flex-direction: column;
  1974. .pre {
  1975. width: 450rpx;
  1976. height: 56rpx;
  1977. line-height: 56rpx;
  1978. text-align: center;
  1979. background: #f0fdf4;
  1980. border-radius: 28rpx 28rpx 28rpx 28rpx;
  1981. border: 1rpx solid rgba(34, 197, 94, 0.1);
  1982. font-family: PingFang SC, PingFang SC;
  1983. font-weight: 400;
  1984. font-size: 24rpx;
  1985. color: #22c55e;
  1986. }
  1987. .time {
  1988. margin-top: 16rpx;
  1989. font-family: PingFang SC, PingFang SC;
  1990. font-weight: 400;
  1991. font-size: 24rpx;
  1992. color: #999999;
  1993. line-height: 40rpx;
  1994. text-align: center;
  1995. }
  1996. }
  1997. .nav-bar {
  1998. position: sticky;
  1999. top: 0;
  2000. z-index: 10;
  2001. display: flex;
  2002. align-items: center;
  2003. justify-content: center;
  2004. justify-content: space-between;
  2005. // height: 88rpx;
  2006. margin: 0 24rpx;
  2007. background: #fff;
  2008. }
  2009. .nav-title {
  2010. font-size: 34rpx;
  2011. font-weight: 600;
  2012. color: #333;
  2013. }
  2014. .nav-back {
  2015. // position: absolute;
  2016. // left: 24rpx;
  2017. // padding: 10rpx 0;
  2018. }
  2019. .back-text {
  2020. width: 64rpx;
  2021. height: 64rpx;
  2022. }
  2023. .next-text {
  2024. width: 48rpx;
  2025. height: 48rpx;
  2026. }
  2027. .content {
  2028. padding: 24rpx;
  2029. padding-bottom: 60rpx;
  2030. padding-top: 40rpx;
  2031. }
  2032. /* ============ 截图风格UI ============ */
  2033. .tabs {
  2034. display: flex;
  2035. gap: 18rpx;
  2036. margin-bottom: 50rpx;
  2037. }
  2038. .tab {
  2039. flex: 1;
  2040. height: 72rpx;
  2041. line-height: 72rpx;
  2042. text-align: center;
  2043. border-radius: 36rpx;
  2044. background: #f5f7fa;
  2045. font-family: PingFang SC, PingFang SC;
  2046. font-weight: 400;
  2047. font-size: 28rpx;
  2048. color: #67686f;
  2049. }
  2050. .tab.active {
  2051. background: #ff7700;
  2052. color: #fff;
  2053. font-weight: 600;
  2054. }
  2055. .meter-wrap {
  2056. display: flex;
  2057. justify-content: center;
  2058. margin-top: 12rpx;
  2059. }
  2060. .meter-ring {
  2061. position: relative;
  2062. width: 510rpx;
  2063. height: 510rpx;
  2064. border-radius: 50%;
  2065. background: conic-gradient(var(--arc, #eaeaea) var(--p, 0%), var(--track, #eaeaea) 0);
  2066. display: flex;
  2067. align-items: center;
  2068. justify-content: center;
  2069. }
  2070. .meter-ring::after {
  2071. content: '';
  2072. position: absolute;
  2073. inset: 24rpx;
  2074. background: #ffffff;
  2075. border-radius: 50%;
  2076. }
  2077. .meter-center {
  2078. position: absolute;
  2079. z-index: 1;
  2080. left: 0;
  2081. right: 0;
  2082. top: 0;
  2083. bottom: 0;
  2084. display: flex;
  2085. flex-direction: column;
  2086. align-items: center;
  2087. justify-content: center;
  2088. text-align: center;
  2089. }
  2090. .ready-view {
  2091. display: flex;
  2092. flex-direction: column;
  2093. align-items: center;
  2094. justify-content: center;
  2095. }
  2096. .ready-checkimg {
  2097. width: 104rpx;
  2098. height: 104rpx;
  2099. }
  2100. .ready-check {
  2101. width: 104rpx;
  2102. height: 104rpx;
  2103. border-radius: 50%;
  2104. background: #ff7700;
  2105. color: #fff;
  2106. display: flex;
  2107. align-items: center;
  2108. justify-content: center;
  2109. font-size: 56rpx;
  2110. font-weight: 700;
  2111. margin-bottom: 18rpx;
  2112. }
  2113. .disconnected-check {
  2114. background: #eaeaea;
  2115. color: #999;
  2116. }
  2117. .ready-text {
  2118. font-size: 28rpx;
  2119. color: #666;
  2120. font-weight: 500;
  2121. }
  2122. .bp-value {
  2123. font-size: 76rpx;
  2124. font-weight: 700;
  2125. color: #111;
  2126. line-height: 88rpx;
  2127. }
  2128. .bp-value-unit {
  2129. font-size: 28rpx;
  2130. font-weight: 500;
  2131. color: #999;
  2132. margin-left: 10rpx;
  2133. vertical-align: middle;
  2134. }
  2135. .measuring-sub,
  2136. .result-sub {
  2137. margin-top: 10rpx;
  2138. font-family: PingFang SC, PingFang SC;
  2139. font-weight: 400;
  2140. font-size: 28rpx;
  2141. color: #757575;
  2142. }
  2143. .error-num {
  2144. color: #999;
  2145. }
  2146. .error-sub {
  2147. color: #e64340;
  2148. }
  2149. .tip-box {
  2150. margin: 40rpx 0 0 0;
  2151. padding: 20rpx 30rpx;
  2152. background: #fff6f1;
  2153. border-radius: 16rpx;
  2154. display: flex;
  2155. align-items: flex-start;
  2156. gap: 14rpx;
  2157. background: rgba(255, 119, 0, 0.05);
  2158. border-radius: 16rpx 16rpx 16rpx 16rpx;
  2159. border: 2rpx solid rgba(255, 119, 0, 0.1);
  2160. }
  2161. .tip-icon {
  2162. width: 34rpx;
  2163. height: 34rpx;
  2164. border-radius: 50%;
  2165. background: #ff7700;
  2166. color: #fff;
  2167. display: flex;
  2168. align-items: center;
  2169. justify-content: center;
  2170. font-size: 22rpx;
  2171. font-weight: 700;
  2172. flex-shrink: 0;
  2173. line-height: 34rpx;
  2174. }
  2175. .tip-text {
  2176. font-family: PingFang SC, PingFang SC;
  2177. font-weight: 400;
  2178. font-size: 24rpx;
  2179. color: #67686f;
  2180. line-height: 36rpx;
  2181. }
  2182. .primary-btn {
  2183. margin-top: 26rpx;
  2184. height: 96rpx;
  2185. border-radius: 18rpx;
  2186. background: linear-gradient(90deg, #f8551f 0%, #ff9501 100%);
  2187. display: flex;
  2188. align-items: center;
  2189. justify-content: center;
  2190. box-shadow: 0 12rpx 30rpx rgba(255, 119, 0, 0.25);
  2191. }
  2192. .primary-btn-disabled {
  2193. background: #ff8a3a;
  2194. box-shadow: none;
  2195. opacity: 0.55;
  2196. }
  2197. .primary-btn-text {
  2198. color: #fff;
  2199. font-size: 34rpx;
  2200. font-weight: 700;
  2201. }
  2202. .conn-card {
  2203. margin-top: 22rpx;
  2204. padding: 0 6rpx;
  2205. }
  2206. .conn-row {
  2207. display: flex;
  2208. align-items: center;
  2209. gap: 14rpx;
  2210. }
  2211. .conn-dot {
  2212. width: 12rpx;
  2213. height: 12rpx;
  2214. border-radius: 50%;
  2215. background: #07c160;
  2216. }
  2217. .conn-text {
  2218. font-size: 24rpx;
  2219. color: #666;
  2220. }
  2221. .conn-time {
  2222. margin-top: 10rpx;
  2223. font-size: 22rpx;
  2224. color: #999;
  2225. }
  2226. .sync-card {
  2227. margin-top: 26rpx;
  2228. background: #f5f7fa;
  2229. border-radius: 16rpx 16rpx 0 0;
  2230. padding: 24rpx 30rpx;
  2231. display: flex;
  2232. align-items: center;
  2233. justify-content: space-between;
  2234. .device-item-btn {
  2235. background: red;
  2236. color: #fff;
  2237. padding: 16rpx 24rpx;
  2238. border-radius: 44rpx;
  2239. font-size: 28rpx;
  2240. margin: 0;
  2241. min-width: 160rpx;
  2242. }
  2243. }
  2244. .sync-left {
  2245. display: flex;
  2246. flex-direction: column;
  2247. }
  2248. .sync-title {
  2249. font-family: PingFang SC, PingFang SC;
  2250. font-weight: 600;
  2251. font-size: 36rpx;
  2252. color: #22c55e;
  2253. line-height: 40rpx;
  2254. margin-bottom: 10rpx;
  2255. }
  2256. .sync-time {
  2257. font-family: PingFang SC, PingFang SC;
  2258. font-weight: 400;
  2259. font-size: 24rpx;
  2260. color: #999999;
  2261. line-height: 40rpx;
  2262. }
  2263. .sync-check {
  2264. width: 48rpx;
  2265. height: 48rpx;
  2266. }
  2267. .result-panel {
  2268. background: #f5f7fa;
  2269. border-radius: 0 0 16rpx 16rpx;
  2270. padding: 20rpx 16rpx;
  2271. }
  2272. .result-grid {
  2273. display: grid;
  2274. grid-template-columns: repeat(2, minmax(0, 1fr));
  2275. gap: 16rpx;
  2276. }
  2277. .result-card {
  2278. width: 100%;
  2279. min-width: 0;
  2280. background: #ffffff;
  2281. border-radius: 14rpx;
  2282. padding: 18rpx 16rpx;
  2283. box-sizing: border-box;
  2284. }
  2285. .result-label {
  2286. font-family: PingFang SC, PingFang SC;
  2287. font-weight: 500;
  2288. font-size: 28rpx;
  2289. color: #333333;
  2290. display: flex;
  2291. align-items: center;
  2292. justify-content: space-between;
  2293. }
  2294. .result-text {
  2295. margin-top: 16rpx;
  2296. font-family: PingFang SC, PingFang SC;
  2297. font-weight: 400;
  2298. font-size: 24rpx;
  2299. color: #757575;
  2300. }
  2301. .result-card .active {
  2302. color: #ff7700;
  2303. }
  2304. .trend-badge {
  2305. font-size: 20rpx;
  2306. color: #ff7700;
  2307. font-weight: 700;
  2308. }
  2309. .result-value {
  2310. margin-top: 8rpx;
  2311. font-family: DINPro, DINPro;
  2312. font-weight: bold;
  2313. font-size: 48rpx;
  2314. color: #333333;
  2315. }
  2316. .result-unit {
  2317. font-family: PingFang SC, PingFang SC;
  2318. font-weight: 400;
  2319. font-size: 26rpx;
  2320. color: #757575;
  2321. margin-left: 10rpx;
  2322. }
  2323. .result-chart {
  2324. margin-top: 6rpx;
  2325. height: 80rpx;
  2326. .chart-img {
  2327. width: 100%;
  2328. height: 100%;
  2329. }
  2330. }
  2331. /* ============ 旧样式(保留,给断连/设备选择区使用) ============ */
  2332. .status-card {
  2333. background: #fff;
  2334. border-radius: 24rpx;
  2335. padding: 48rpx;
  2336. margin-bottom: 32rpx;
  2337. text-align: center;
  2338. border: 2rpx solid #eee;
  2339. }
  2340. .status-card.connected {
  2341. border-color: #ff5c03;
  2342. background: #fffaf7;
  2343. }
  2344. .status-icon {
  2345. font-size: 56rpx;
  2346. color: #999;
  2347. margin-bottom: 16rpx;
  2348. }
  2349. .status-card.connected .status-icon {
  2350. color: #ff5c03;
  2351. }
  2352. .status-text {
  2353. font-size: 28rpx;
  2354. color: #666;
  2355. margin-bottom: 24rpx;
  2356. }
  2357. .bp-wait-hint {
  2358. font-size: 24rpx;
  2359. color: #999;
  2360. margin-bottom: 16rpx;
  2361. }
  2362. .bp-measure-ok {
  2363. color: #07c160;
  2364. }
  2365. .bp-pressure-live {
  2366. font-size: 28rpx;
  2367. color: #ff5c03;
  2368. font-weight: 600;
  2369. margin-bottom: 16rpx;
  2370. }
  2371. .bp-error-msg {
  2372. font-size: 24rpx;
  2373. color: #e64340;
  2374. background: #fff5f5;
  2375. padding: 16rpx;
  2376. border-radius: 12rpx;
  2377. margin-bottom: 16rpx;
  2378. line-height: 1.5;
  2379. }
  2380. .value-ihb {
  2381. display: block;
  2382. color: #e64340;
  2383. margin-top: 4rpx;
  2384. }
  2385. .measure-value {
  2386. margin: 24rpx 0 8rpx;
  2387. }
  2388. .value-label {
  2389. display: block;
  2390. font-size: 24rpx;
  2391. color: #999;
  2392. margin-bottom: 4rpx;
  2393. }
  2394. .value-num {
  2395. font-size: 72rpx;
  2396. font-weight: 700;
  2397. color: #ff5c03;
  2398. }
  2399. .value-unit {
  2400. font-size: 28rpx;
  2401. color: #999;
  2402. margin-left: 8rpx;
  2403. }
  2404. .value-extra {
  2405. display: block;
  2406. font-size: 26rpx;
  2407. color: #666;
  2408. margin-top: 8rpx;
  2409. }
  2410. .measure-time {
  2411. font-size: 24rpx;
  2412. color: #999;
  2413. }
  2414. .actions {
  2415. background: #fff;
  2416. border-radius: 24rpx;
  2417. padding: 32rpx;
  2418. margin-bottom: 24rpx;
  2419. }
  2420. .btn {
  2421. width: 100%;
  2422. height: 88rpx;
  2423. line-height: 88rpx;
  2424. border-radius: 44rpx;
  2425. font-size: 32rpx;
  2426. margin-bottom: 24rpx;
  2427. }
  2428. .btn:last-child {
  2429. margin-bottom: 0;
  2430. }
  2431. .btn-primary {
  2432. background: #ff5c03;
  2433. color: #fff;
  2434. border: none;
  2435. }
  2436. .btn-connect {
  2437. background: #07c160;
  2438. color: #fff;
  2439. border: none;
  2440. }
  2441. .btn-warn {
  2442. background: #fff;
  2443. color: #e64340;
  2444. border: 2rpx solid #e64340;
  2445. }
  2446. .device-list {
  2447. margin: 24rpx 0;
  2448. padding: 0 0 16rpx;
  2449. border-bottom: 1rpx solid #eee;
  2450. }
  2451. .list-title {
  2452. font-size: 26rpx;
  2453. color: #999;
  2454. margin-bottom: 16rpx;
  2455. }
  2456. .device-item {
  2457. padding: 20rpx 0;
  2458. border-radius: 12rpx;
  2459. .device-item-btn {
  2460. background: #07c160;
  2461. color: #fff;
  2462. padding: 16rpx 24rpx;
  2463. border-radius: 44rpx;
  2464. font-size: 28rpx;
  2465. margin: 0;
  2466. min-width: 160rpx;
  2467. }
  2468. }
  2469. .device-item.active {
  2470. background: #fff5f0;
  2471. }
  2472. .device-info {
  2473. margin-left: 20rpx;
  2474. display: flex;
  2475. flex-direction: column;
  2476. }
  2477. .device-item-footer {
  2478. display: flex;
  2479. align-items: center;
  2480. justify-content: flex-end;
  2481. .device-connect {
  2482. background: #07c160;
  2483. border: 1rpx solid #07c160;
  2484. color: #fff;
  2485. padding: 16rpx 24rpx;
  2486. border-radius: 44rpx;
  2487. font-size: 28rpx;
  2488. margin: 0;
  2489. min-width: 160rpx;
  2490. }
  2491. .device-connect-warn {
  2492. padding: 16rpx 24rpx;
  2493. background: #fff;
  2494. color: #e64340;
  2495. border: 1rpx solid #e64340;
  2496. border-radius: 44rpx;
  2497. font-size: 28rpx;
  2498. margin: 0 20rpx 0 0;
  2499. min-width: 160rpx;
  2500. }
  2501. }
  2502. .device-name {
  2503. font-size: 30rpx;
  2504. color: #333;
  2505. }
  2506. .device-id {
  2507. font-size: 22rpx;
  2508. color: #999;
  2509. margin-top: 4rpx;
  2510. }
  2511. .log-section {
  2512. background: #fff;
  2513. border-radius: 24rpx;
  2514. padding: 24rpx;
  2515. }
  2516. .log-title {
  2517. display: flex;
  2518. justify-content: space-between;
  2519. font-size: 28rpx;
  2520. color: #666;
  2521. padding: 8rpx 0;
  2522. }
  2523. .log-toggle {
  2524. color: #ff5c03;
  2525. font-size: 26rpx;
  2526. }
  2527. .log-list {
  2528. max-height: 360rpx;
  2529. margin-top: 16rpx;
  2530. padding: 16rpx;
  2531. background: #f5f5f5;
  2532. border-radius: 12rpx;
  2533. }
  2534. .log-line {
  2535. display: block;
  2536. font-size: 22rpx;
  2537. color: #666;
  2538. line-height: 1.6;
  2539. word-break: break-all;
  2540. }
  2541. </style>