| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768 |
- <template>
- <view class="page">
- <!-- 顶部导航 -->
- <!-- <view :style="{ height: statusBarHeight}"></view> -->
- <view class="nav-bar" :style="{ paddingTop: statusBarHeight }">
- <view class="nav-back" @tap="goBack">
- <!-- <text class="back-text">‹ 返回</text> -->
- <image class="back-text" src="@/static/images/pages_watch/icons/back_arrow_icon24.png"></image>
- </view>
- <view class="nav-title">小护士血压仪</view>
- <image class="next-text" src="@/static/images/pages_watch/icons/more_icon24.png"></image>
- </view>
- <view class="content">
- <!-- 顶部Tab -->
- <view class="tabs">
- <view class="tab" :class="{ active: activeTab === 'bp' }" @tap="activeTab = 'bp'">血压/心率</view>
- <view class="tab" :class="{ active: activeTab === 'glucose' }" @tap="activeTab = 'glucose'">血糖测量</view>
- <view class="tab" :class="{ active: activeTab === 'uric' }" @tap="activeTab = 'uric'">尿酸测量</view>
- </view>
- <!-- 圆形仪表 -->
- <view class="meter-wrap">
- <view class="meter-ring" :style="ringStyle">
- <view class="meter-center">
- <!-- 准备就绪 -->
- <view v-if="uiState === 'ready'" class="ready-view">
- <!-- <view class="ready-check">✓</view> -->
- <image class="ready-checkimg" src="@/static/images/pages_watch/icons/ok_icon.png"></image>
- <view class="ready-text">准备就绪</view>
- </view>
- <!-- 量测中 -->
- <view v-else-if="uiState === 'measuring'" class="measuring-view">
- <view class="bp-value">
- {{ displaySystolic }}/{{ displayDiastolic }}
- <text class="bp-value-unit">mmHg</text>
- </view>
- <view class="measuring-sub">正在自动加压..{{ ringProgressInt }}%</view>
- </view>
- <!-- 结果完成 -->
- <view v-else-if="uiState === 'result'" class="result-view">
- <view v-if="activeTab === 'bp'" class="result-view">
- <view class="bp-value">
- {{ lastBloodPressure.systolic }}/{{ lastBloodPressure.diastolic }}
- <text class="bp-value-unit">mmHg</text>
- </view>
- <view class="result-sub">测量完成</view>
- </view>
- <view v-else-if="activeTab === 'glucose'" class="result-view">
- <view class="bp-value">
- {{ lastGlucose.value }}
- <text class="bp-value-unit">{{ lastGlucose.unit }}</text>
- </view>
- <view class="result-sub">测量完成</view>
- </view>
- <view v-else class="result-view">
- <view class="bp-value">
- {{ lastUricAcid.value }}
- <text class="bp-value-unit">{{ lastUricAcid.unit }}</text>
- </view>
- <view class="result-sub">测量完成</view>
- </view>
- </view>
- <!-- 异常 -->
- <view v-else-if="uiState === 'error'" class="error-view">
- <view v-if="activeTab === 'bp'" class="result-view">
- <view class="bp-value error-num">
- --/--
- <text class="bp-value-unit">mmHg</text>
- </view>
- <view class="result-sub error-sub">测量错误</view>
- </view>
- <view v-else-if="activeTab === 'glucose'" class="result-view">
- <view class="bp-value error-num">
- --
- <text class="bp-value-unit">{{ lastGlucose.unit }}</text>
- </view>
- <view class="result-sub error-sub">{{ glucoseErrorMsg ? '血糖测量错误' : '测量错误' }}</view>
- </view>
- <view v-else class="result-view">
- <view class="bp-value error-num">
- --
- <text class="bp-value-unit">{{ lastUricAcid.unit }}</text>
- </view>
- <view class="result-sub error-sub">{{ uricAcidErrorMsg ? '尿酸测量错误' : '测量错误' }}</view>
- </view>
- </view>
- <!-- 未连接 -->
- <view v-else class="ready-view">
- <view class="ready-check disconnected-check">○</view>
- <view class="ready-text">未连接</view>
- </view>
- </view>
- </view>
- </view>
- <!-- 提示/同步卡片 v-if="uiState !== 'result'" -->
- <view v-if="uiState !== 'result'" class="tip-box">
- <view class="tip-icon">i</view>
- <view class="tip-text">{{ tipText }}</view>
- </view>
- <!-- 连接/设备操作区(非截图区域:保持你现有连接逻辑) -->
- <view class="actions">
- <view>
- <view class="list-title">已绑定设备</view>
- <!-- <button class="es-w-200 es-h-88" v-show="!isFamily" @click="editXHSDevice('FC:62:58:04:19:1E')">绑定</button>
- <button class="es-w-200 es-h-88" v-show="isFamily" @click="editMyfamily('FC:62:58:04:19:1E')">家人绑定</button>
- <button class="es-w-200 es-h-88" @click="xhsDataAddTest(0)">记录血压</button>
- <button class="es-w-200 es-h-88" @click="xhsDataAddTest(1)">记录血糖</button>
- <button class="es-w-200 es-h-88" @click="xhsDataAddTest(3)">记录尿酸</button> -->
- <view class="device-item active es-mb-20" v-for="(item, index) in xhsDeviceList" :key="index">
- <view class="x-f" style="margin-bottom: 24rpx; justify-content: space-between">
- <view class="device-info flex">
- <text class="device-name">{{ item.name }}</text>
- <text class="device-id">{{ item.deviceId }}</text>
- </view>
- <view class="es-mr-10" v-if="deviceId == item.deviceId">{{ statusText }}</view>
- </view>
- <view class="device-item-footer">
- <button class="device-connect-warn" :disabled="disconnecting" @tap="unbindDevice(item)">解绑</button>
- <button class="device-connect" v-if="deviceId == item.deviceId && !connected" :disabled="connecting" :loading="connecting" @tap="connectDevice(false)">
- {{ connecting ? '连接中...' : '重试连接' }}
- </button>
- </view>
- </view>
- </view>
- <view>
- <button class="btn btn-primary" :disabled="scanning" :loading="scanning" @tap="startScan">
- {{ scanning ? '扫描中...' : '添加新设备' }}
- </button>
- <view v-if="deviceList.length > 0" class="device-list">
- <view class="list-title">可选设备(请确保血压仪/血糖仪已开机)</view>
- <view
- v-for="d in deviceList"
- :key="d.deviceId"
- class="device-item x-f es-pl-20 es-pr-20"
- :class="{ active: selectedDeviceId === d.deviceId }"
- @tap="selectDevice(d)"
- >
- <!-- <radio :checked="selectedDeviceId === d.deviceId" color="#FF5C03" /> -->
- <image class="es-w-100 es-h-100" src="/static/images/pages_watch/icons/blood_pressure_monitor_img.png" mode="aspectFit"></image>
- <view class="device-info flex">
- <text class="device-name">{{ d.name || d.localName || '未知设备' }}</text>
- <text class="device-id">{{ d.deviceId }}</text>
- </view>
- <view>
- <button
- class="device-item-btn"
- :disabled="d.deviceId == selectedDeviceId && (!selectedDeviceId || connecting)"
- :loading="d.deviceId == selectedDeviceId && connecting"
- >
- {{ d.deviceId == selectedDeviceId && connecting ? '连接中...' : '连接设备' }}
- </button>
- </view>
- </view>
- </view>
- </view>
- </view>
- <!-- 主按钮 -->
- <view v-if="activeTab === 'bp' && (bpUiState === 'ready' || bpUiState === 'error')" class="primary-btn" @tap="onPrimaryActionTap">
- <text class="primary-btn-text">{{ bpUiState === 'error' ? '重新测量' : '开始测量' }}</text>
- </view>
- <view v-else-if="activeTab === 'bp' && bpUiState === 'measuring'" class="primary-btn primary-btn-disabled">
- <text class="primary-btn-text">测量中</text>
- </view>
- <!-- v-else-if="bpUiState === 'result' && activeTab === 'bp'" -->
- <view class="sync-card" v-show="boundDevice">
- <view class="sync-left">
- <view class="sync-title" :style="{ color: dataSync === 'success' ? '#22C55E' : dataSync === 'error' ? 'red' : dataSync === 'loading' ? '#999' : '#22C55E' }">
- {{ dataSync === 'success' ? '数据已同步' : dataSync === 'error' ? '数据同步失败' : dataSync === 'loading' ? '数据同步中' : '数据同步' }}
- </view>
- <view class="sync-time">{{ footerTimeText || '--' }}</view>
- </view>
- <image class="sync-check" v-show="dataSync == 'success'" src="@/static/images/pages_watch/icons/remove_icon2.png"></image>
- <button class="device-item-btn" v-show="dataSync != 'success'" :disabled="dataSync === 'loading'" :loading="dataSync === 'loading'" @click="xhsDataAdd(0)">
- {{ dataSync === 'loading' ? '同步中' : dataSync === 'error' ? '重新同步' : '同步成功' }}
- </button>
- </view>
- <!-- 结果指标卡片(只在血压/心率Tab展示,保持布局一致) -->
- <!-- v-if="bpUiState === 'result' && activeTab === 'bp'" -->
- <view class="result-panel" v-show="boundDevice">
- <view class="result-grid">
- <view class="result-card">
- <view class="result-label">血压</view>
- <view class="result-value x-f">
- {{ cardBloodPressure.systolic || '-' }}/{{ cardBloodPressure.diastolic || '-' }}
- <text class="result-unit">(mmHg)</text>
- </view>
- <view class="result-chart">
- <image class="chart-img" src="@/static/images/pages_watch/icons/bloodP.png"></image>
- </view>
- <view class="result-text" :class="bpResultText == '偏高' ? 'active' : ''">{{ bpResultText }}</view>
- </view>
- <view class="result-card">
- <view class="result-label">心率</view>
- <view class="result-value x-f">
- {{ cardBloodPressure.pulse || '-' }}
- <text class="result-unit">bpm</text>
- </view>
- <view class="result-chart">
- <image class="chart-img" src="@/static/images/pages_watch/icons/heart.png"></image>
- </view>
- <view class="result-text" :class="pulseResultText == '偏高' ? 'active' : ''">{{ pulseResultText }}</view>
- </view>
- <view class="result-card">
- <view class="result-label">血糖</view>
- <view class="result-value x-f">
- {{ cardGlucose.value || '-' }}
- <text class="result-unit">({{ cardGlucose.unit }})</text>
- </view>
- <view class="result-chart">
- <image
- class="chart-img"
- :src="glucoseResultText == '偏高' ? '/static/images/pages_watch/icons/bloods-top.png' : '/static/images/pages_watch/icons/bloods.png'"
- ></image>
- </view>
- <view class="result-text" :class="glucoseResultText == '偏高' ? 'active' : ''">{{ glucoseResultText }}</view>
- </view>
- <view class="result-card">
- <view class="result-label">
- 尿酸
- <!-- <text class="trend-badge">{{ uricTrendText }}</text> -->
- </view>
- <view class="result-value x-f">
- {{ lastUricAcid.value || '-' }}
- <text class="result-unit">({{ lastUricAcid.unit }})</text>
- </view>
- <view class="result-chart">
- <image
- class="chart-img"
- :src="uricResultText == '偏高' ? '/static/images/pages_watch/icons/acid-top.png' : '/static/images/pages_watch/icons/acid.png'"
- ></image>
- </view>
- <view class="result-text" :class="uricResultText == '偏高' ? 'active' : ''">{{ uricResultText }}</view>
- </view>
- </view>
- </view>
- <!-- 日志(调试用,可折叠) -->
- <view class="log-section" v-if="false">
- <view class="log-title" @tap="showLog = !showLog">
- <text>运行日志</text>
- <text class="log-toggle">{{ showLog ? '收起' : '展开' }}</text>
- </view>
- <scroll-view v-if="showLog" class="log-list" scroll-y>
- <text v-for="(line, i) in logs" :key="i" class="log-line">{{ line }}</text>
- </scroll-view>
- </view>
- </view>
- </view>
- </template>
- <script>
- /**
- * 将 uni-app 的回调式 API 包装为 Promise 风格,便于使用 async/await 避免回调地狱
- * @param {string} api - uni-app 的 API 名称 (如 'openBluetoothAdapter')
- * @param {Object} opts - 传递给 API 的参数对象
- * @returns {Promise} 成功时 resolve 返回结果,失败时 reject 返回错误信息
- */
- const promisifyUniApi = (api, opts = {}) =>
- new Promise((res, rej) => {
- opts.success = res;
- opts.fail = rej;
- uni[api](opts);
- });
- // 解析 BLE 血压测量特征值(Bluetooth GATT 0x2A18 格式)
- // 固定 UUID:接收用 0xFFF1,发送用 0xFFF2,同属 Service 0xFFF0
- // 血压协议:命令头 0xFD,0xFD;命令尾 0x0D,0x0A
- const BP_HEADER = [0xfd, 0xfd];
- const BP_TAIL = [0x0d, 0x0a];
- // 血压协议命令 (FD FD ... 0D 0A) - 新协议
- const CMD_BP_BROADCAST_A4 = 0xa4;
- const CMD_BP_CONNECT_FA = 0xfa; // FA 05
- const CMD_BP_CONFIRM_06 = 0x06;
- const CMD_BP_PRESSURE_FB = 0xfb;
- const CMD_BP_RESULT_FC = 0xfc;
- const CMD_BP_ERROR_FD = 0xfd;
- // 血糖/尿酸协议命令 (A5 A5 ... 5A 5A)
- const GU_HEADER = [0xa5, 0xa5];
- const GU_TAIL = [0x5a, 0x5a];
- const CMD_GU_START_B1 = 0xb1;
- const CMD_GU_GLUCOSE_RES_B2 = 0xb2;
- const CMD_GU_URIC_RES_B3 = 0xb3;
- const CMD_GU_GLUCOSE_ERR_B4 = 0xb4;
- const CMD_GU_URIC_ERR_B5 = 0xb5;
- const CMD_GU_REPLY_START_C1 = 0xc1;
- const CMD_GU_REPLY_GLUCOSE_RES_C2 = 0xc2;
- const CMD_GU_REPLY_GLUCOSE_ERR_C3 = 0xc3;
- const CMD_GU_REPLY_URIC_RES_C4 = 0xc4;
- const CMD_GU_REPLY_URIC_ERR_C5 = 0xc5;
- // 构造血压指令
- function buildBpCommand(cmd, data = []) {
- const buf = new Uint8Array(2 + 1 + data.length + 2);
- buf[0] = 0xfd;
- buf[1] = 0xfd;
- buf[2] = cmd;
- for (let i = 0; i < data.length; i++) {
- buf[3 + i] = data[i];
- }
- buf[buf.length - 2] = 0x0d;
- buf[buf.length - 1] = 0x0a;
- return buf.buffer;
- }
- // 构造血糖/尿酸指令
- function buildGuCommand(cmd, data = []) {
- const len = 1 + data.length + 1; // 帧长包括 指令类型、数据、校验和
- const buf = new Uint8Array(2 + 1 + len + 2); // 帧头(2) + 帧长(1) + len + 帧尾(2)
- buf[0] = 0xa5;
- buf[1] = 0xa5;
- buf[2] = len;
- buf[3] = cmd;
- let checksum = cmd;
- for (let i = 0; i < data.length; i++) {
- buf[4 + i] = data[i];
- checksum += data[i];
- }
- buf[buf.length - 3] = checksum & 0xff;
- buf[buf.length - 2] = 0x5a;
- buf[buf.length - 1] = 0x5a;
- return buf.buffer;
- }
- function parseBpPacket(arr) {
- // 查找 BP_HEADER (0xfd, 0xfd)
- let startIndex = -1;
- for (let i = 0; i < arr.length - 1; i++) {
- if (arr[i] === 0xfd && arr[i + 1] === 0xfd) {
- startIndex = i;
- break;
- }
- }
- if (startIndex === -1) return null;
- // 查找 BP_TAIL (0x0d, 0x0a)
- let endIndex = -1;
- for (let i = startIndex + 2; i < arr.length - 1; i++) {
- if (arr[i] === 0x0d && arr[i + 1] === 0x0a) {
- endIndex = i + 1; // 包含 0x0a
- break;
- }
- }
- if (endIndex === -1) return null;
- const cmd = arr[startIndex + 2];
- const data = arr.slice(startIndex + 3, endIndex - 1);
- return { cmd, data };
- }
- // 构造旧协议指令 (D1/D2/D3等)
- function buildV2Command(cmd, data = []) {
- const packetLen = 1 + data.length + 1; // Cmd(1) + Data(n) + Checksum(1)
- const buf = new Uint8Array(2 + 1 + packetLen + 2);
- buf[0] = 0xfd;
- buf[1] = 0xfd;
- buf[2] = packetLen;
- buf[3] = cmd;
- let checksum = cmd;
- for (let i = 0; i < data.length; i++) {
- buf[4 + i] = data[i];
- checksum += data[i];
- }
- buf[buf.length - 3] = checksum & 0xff;
- buf[buf.length - 2] = 0x0d;
- buf[buf.length - 1] = 0x0a;
- return buf.buffer;
- }
- function parseGuPacket(arr) {
- // 查找 GU_HEADER (0xa5, 0xa5)
- let startIndex = -1;
- for (let i = 0; i < arr.length - 1; i++) {
- if (arr[i] === 0xa5 && arr[i + 1] === 0xa5) {
- startIndex = i;
- break;
- }
- }
- if (startIndex === -1) return null;
- const len = arr[startIndex + 2];
- const expectedEndIndex = startIndex + 5 + len - 1;
- if (expectedEndIndex >= arr.length) return null;
- if (arr[expectedEndIndex - 1] !== 0x5a || arr[expectedEndIndex] !== 0x5a) return null;
- const cmd = arr[startIndex + 3];
- const dataLen = len - 2; // len = cmd(1) + data + CS(1)
- const data = arr.slice(startIndex + 4, startIndex + 4 + dataLen);
- const checksum = arr[startIndex + 4 + dataLen];
- let sum = cmd;
- for (let i = 0; i < data.length; i++) sum += data[i];
- if ((sum & 0xff) !== checksum) {
- console.log('GU Checksum error:', sum & 0xff, checksum);
- return null;
- }
- return { cmd, data };
- }
- // 解析血糖/尿酸数据包
- function parseGlucosePacket(buffer) {
- // 解析新协议 (A5 A5)
- const newPacket = parseGuPacket(new Uint8Array(buffer));
- if (newPacket) {
- const { cmd, data } = newPacket;
- // 1. 开始测量 (B1)
- if (cmd === CMD_GU_START_B1) {
- const type = data[1]; // 0x01=血糖, 0x02=尿酸
- if (type === 0x01) {
- return {
- type: 'glucose_start',
- user: data[0],
- replyCmd: buildGuCommand(CMD_GU_REPLY_START_C1)
- };
- } else if (type === 0x02) {
- return {
- type: 'uric_start',
- user: data[0],
- replyCmd: buildGuCommand(CMD_GU_REPLY_START_C1)
- };
- }
- }
- // 2. 血糖结果 (B2)
- if (cmd === CMD_GU_GLUCOSE_RES_B2) {
- if (data.length >= 4) {
- const valH = data[1];
- const valL = data[2];
- const unitByte = data[3];
- const value = (valH << 8) | valL;
- let finalValue = value;
- if (value === 0) {
- finalValue = 'LO';
- } else if (value === 0xffff) {
- finalValue = 'HI';
- } else {
- finalValue = (value / 10).toFixed(1);
- }
- return {
- type: 'glucose_result',
- value: finalValue,
- unit: unitByte === 0x01 ? 'mg/dL' : 'mmol/L',
- time: Date.now(),
- replyCmd: buildGuCommand(CMD_GU_REPLY_GLUCOSE_RES_C2)
- };
- }
- }
- // 3. 尿酸结果 (B3)
- if (cmd === CMD_GU_URIC_RES_B3) {
- if (data.length >= 4) {
- const valH = data[1];
- const valL = data[2];
- const unitByte = data[3];
- const value = (valH << 8) | valL;
- let finalValue = value;
- if (value === 0) {
- finalValue = 'LO';
- } else if (value === 0xffff) {
- finalValue = 'HI';
- }
- return {
- type: 'uric_result',
- value: finalValue,
- unit: unitByte === 0x01 ? 'mg/dL' : 'umol/L',
- time: Date.now(),
- replyCmd: buildGuCommand(CMD_GU_REPLY_URIC_RES_C4) // 文档中尿酸结果回复是C4
- };
- }
- }
- // 4. 报错 (B4/B5)
- if (cmd === CMD_GU_GLUCOSE_ERR_B4) {
- const errCode = data[1];
- return {
- type: 'glucose_error',
- msg: '血糖测量错误: ' + getErrorMsg(errCode),
- replyCmd: buildGuCommand(CMD_GU_REPLY_GLUCOSE_ERR_C3)
- };
- }
- if (cmd === CMD_GU_URIC_ERR_B5) {
- const errCode = data[1];
- return {
- type: 'uric_error',
- msg: '尿酸测量错误: ' + getErrorMsg(errCode),
- replyCmd: buildGuCommand(CMD_GU_REPLY_URIC_ERR_C5) // 尿酸报错回复C5
- };
- }
- }
- return null;
- }
- function getErrorMsg(code) {
- const msgs = {
- 0x01: '温度超范围',
- 0x02: '试纸已使用过',
- 0x03: '试纸损坏或中途拔出'
- // 其他错误码...
- };
- return msgs[code] || '错误码 ' + code;
- }
- import { getWatchUserInfo, editMyfamily } from '@/api/pages_watch/user.js';
- import { editXHSDevice, xhsDataAdd, removeXHSDevice, getLastData, xhsDataAddList } from '@/api/pages_watch/device.js';
- export default {
- data() {
- return {
- scanning: false,
- connecting: false,
- disconnecting: false,
- connected: false,
- deviceList: [],
- selectedDeviceId: '',
- selectedDeviceName: '',
- deviceId: '',
- serviceId: '',
- characteristicId: '',
- notifyServiceId: '',
- notifyCharId: '',
- writeServiceId: '',
- writeCharId: '',
- writeType: '', // 明确记录使用的写入类型: 'write' 或 'writeNoResponse'
- lastGlucose: {
- value: null,
- unit: 'mmol/L',
- time: null
- },
- cardGlucose: {},
- // 血糖测量状态提示(如:请滴入血样)
- glucoseStatus: '',
- // 血糖测量错误提示
- glucoseErrorMsg: '',
- lastUricAcid: {
- value: null,
- unit: 'umol/L',
- time: null
- },
- // 尿酸测量状态提示
- uricAcidStatus: '',
- // 尿酸测量错误提示
- uricAcidErrorMsg: '',
- lastBloodPressure: {
- systolic: null,
- diastolic: null,
- pulse: null,
- ihb: null, // 0 正常,1 心律不齐
- time: null
- },
- cardBloodPressure: {},
- // 血压计已回复 FD FD 06 0D 0A,已开始量测
- bpMeasureStarted: false,
- // 是否由用户主动点击开始测量
- userRequestedMeasure: false,
- bpWaitUser: 0, // A4广播中的用户号
- // 量测过程中实时压力值(FD FD FB PressureH PressureL 0D 0A),单位 mmHg,收到最终结果后清空
- bpCurrentPressure: null,
- // 量测错误提示(E-1/E-2/E-3/E-5/E-E/E-B),收到成功结果或断开时清空
- bpErrorMsg: '',
- // 新增:血压测量流程状态
- bpProcessStatus: '', // 'connecting', 'handshake', 'measuring', 'result', 'error'
- bpProcessStep: '', // 详细步骤描述
- logs: [],
- showLog: true,
- statusText: '未连接',
- statusBarHeight: uni.getSystemInfoSync().statusBarHeight + 'px',
- boundDevice: null,
- // 顶部Tab:血压/血糖/尿酸(只影响展示,不改变蓝牙业务逻辑)
- activeTab: 'bp',
- // 数据同步状态
- dataSync: 'success',
- dataSyncList: [],
- deviceType: 1, //0腕表1小护士2新表
- isFamily: false,
- selectUser: 0,
- xhsDeviceList: [],
- otherDevice: [],
- retryCount: 0
- };
- },
- computed: {
- deviceDisplayName() {
- return this.selectedDeviceName || (this.boundDevice && this.boundDevice.name) || '小护士血压仪';
- },
- bpUiState() {
- if (!this.connected) return 'disconnected';
- if (this.bpProcessStatus === 'result') return 'result';
- if (this.bpProcessStatus === 'measuring') return 'measuring';
- if (this.bpProcessStatus === 'error') return 'error';
- return 'ready';
- //return 'result';
- },
- // uiState:根据当前Tab展示对应的结果/提示状态(不改变蓝牙业务逻辑)
- uiState() {
- if (this.activeTab === 'bp') return this.bpUiState;
- if (!this.connected) return 'disconnected';
- if (this.activeTab === 'glucose') {
- if (this.glucoseErrorMsg) return 'error';
- if (this.lastGlucose.value !== null) return 'result';
- return 'ready';
- }
- if (this.activeTab === 'uric') {
- console.log("qxj uricAcidErrorMsg",this.uricAcidErrorMsg);
- console.log("qxj lastUricAcid",this.lastUricAcid);
- if (this.uricAcidErrorMsg) return 'error';
- if (this.lastUricAcid.value !== null) return 'result';
- return 'ready';
- }
- return 'ready';
- },
- // 环形进度:用实时压力估算(用于样式展示),结果完成时为100%
- ringProgressInt() {
- if (this.activeTab !== 'bp') return this.uiState === 'result' ? 100 : 0;
- if (this.bpUiState === 'result') return 100;
- if (this.bpUiState !== 'measuring') return 0;
- if (this.bpCurrentPressure == null) return 0;
- const p = (Number(this.bpCurrentPressure) - 50) / 100; // 假设 50~150mmHg 对应 0~100%
- return Math.max(0, Math.min(100, Math.round(p * 100)));
- },
- ringProgress() {
- return this.ringProgressInt;
- },
- ringStyle() {
- const track = '#EAEAEA';
- const tabArcColor = this.activeTab === 'glucose' ? '#52D087' : '#FF7700';
- const arcActive =
- (this.activeTab === 'bp' && (this.bpUiState === 'measuring' || this.bpUiState === 'result')) || (this.activeTab !== 'bp' && this.uiState === 'result');
- const arc = arcActive ? tabArcColor : '#EAEAEA';
- return {
- '--p': this.ringProgress + '%',
- '--arc': arc,
- '--track': track
- };
- },
- displaySystolic() {
- if (this.lastBloodPressure.systolic != null) return this.lastBloodPressure.systolic;
- if (this.bpCurrentPressure == null) return '--';
- return Math.round(Number(this.bpCurrentPressure));
- },
- displayDiastolic() {
- if (this.lastBloodPressure.diastolic != null) return this.lastBloodPressure.diastolic;
- if (this.bpCurrentPressure == null) return '--';
- // UI展示近似:根据实时压力估算舒张压占比
- return Math.round(Number(this.bpCurrentPressure) * 0.64);
- },
- connectionPercent() {
- return this.connected ? 85 : 0;
- },
- lastSyncTime() {
- // 优先用血压时间,其次血糖/尿酸
- return this.cardBloodPressure.time ?? this.cardGlucose.time ?? this.lastUricAcid.time ?? null;
- },
- footerPreText() {
- const deviceName = this.deviceDisplayName || '设备';
- if (this.connected) {
- return `小护士血压仪已通过蓝牙连接(${this.connectionPercent}%)`;
- }
- if (this.boundDevice) {
- return `小护士血压仪待连接`;
- }
- return '';
- },
- footerTimeText() {
- const timeVal = this.lastSyncTime;
- if (timeVal == null || timeVal === '') return '最后同步时间:--';
- // 统一显示为:YYYY-MM-DD HH:mm(不带秒,避免 toLocaleString 受地区影响)
- let d;
- if (typeof timeVal === 'number') {
- d = new Date(timeVal);
- } else {
- d = new Date(timeVal);
- }
- if (Number.isNaN(d.getTime())) return `最后同步时间:${String(timeVal)}`;
- const pad2 = (n) => String(n).padStart(2, '0');
- const formatted = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
- return `最后同步时间:${formatted}`;
- },
- uricTrendText() {
- // 1:偏低 0:正常 2:偏高(简单阈值用于样式标签)
- const v = Number(this.lastUricAcid.value);
- if (!isFinite(v) || this.lastUricAcid.value === null) return ' ';
- // 常用阈值:150~416umol/L(与项目页面一致)
- if (v < 150) return '偏低';
- if (v > 416) return '偏高';
- return '正常';
- },
- bpResultText() {
- const sbp = Number(this.cardBloodPressure.systolic);
- const dbp = Number(this.cardBloodPressure.diastolic);
- if (!isFinite(sbp) || !isFinite(dbp) || this.cardBloodPressure.systolic == null || this.cardBloodPressure.diastolic == null) return '--';
- if (sbp < 90 || dbp < 60) return '偏低';
- if (sbp >= 140 || dbp >= 90) return '偏高';
- return '正常';
- },
- pulseResultText() {
- const pulse = Number(this.cardBloodPressure.pulse);
- if (!isFinite(pulse) || this.cardBloodPressure.pulse == null) return '--';
- if (pulse < 60) return '偏低';
- if (pulse > 100) return '偏高';
- return '正常';
- },
- glucoseResultText() {
- const rawValue = Number(this.cardGlucose.value);
- if (!isFinite(rawValue) || this.cardGlucose.value === null) return '--';
- const unit = String(this.cardGlucose.unit || '').toLowerCase();
- // 统一换算到 mmol/L 后再判断,避免不同单位阈值不一致
- const mmolValue = unit.includes('mg/dl') ? rawValue / 18 : rawValue;
- if (!isFinite(mmolValue)) return '--';
- if (mmolValue < 3.9) return '偏低';
- if (mmolValue > 6.1) return '偏高';
- return '正常';
- },
- uricResultText() {
- const rawValue = Number(this.lastUricAcid.value);
- if (!isFinite(rawValue) || this.lastUricAcid.value === null) return '--';
- const unit = String(this.lastUricAcid.unit || '').toLowerCase();
- // 尿酸统一按 umol/L 判断:mg/dL × 59.48 = umol/L
- const uricUmol = unit.includes('mg/dl') ? rawValue * 59.48 : rawValue;
- if (!isFinite(uricUmol)) return '--';
- if (uricUmol < 150) return '偏低';
- if (uricUmol > 416) return '偏高';
- return '正常';
- },
- tipText() {
- if (this.activeTab === 'bp') return '请保持坐姿,手臂平放桌面,袖带边缘距肘节2-3厘米。';
- if (this.activeTab === 'glucose') {
- if (this.glucoseErrorMsg) return '⚠️ ' + this.glucoseErrorMsg;
- if (this.glucoseStatus) return this.glucoseStatus;
- return '请滴入样本...';
- }
- if (this.activeTab === 'uric') {
- if (this.uricAcidErrorMsg) return '⚠️ ' + this.uricAcidErrorMsg;
- if (this.uricAcidStatus) return this.uricAcidStatus;
- return '请滴入样本...';
- }
- return '';
- }
- },
- onLoad(option) {
- this.selectUser = Number(option.selectUser || 0);
- this.isFamily = option.selectUser != '0';
- this.getUser();
- uni.onBLEConnectionStateChange((res) => {
- console.log(`[BLE Connection] deviceId: ${res.deviceId}, connected: ${res.connected}`);
- if (res.deviceId === this.deviceId && !res.connected) {
- this.connected = false;
- this.statusText = '连接已断开';
- this.bpProcessStatus = 'idle';
- this.bpProcessStep = '设备连接已断开,请重新开启设备';
- this.log('设备蓝牙连接已断开');
- this.userRequestedMeasure = false;
- this.bpMeasureStarted = false;
- this.bpCurrentPressure = null;
- }
- });
- },
- onUnload() {
- this.closeBLE();
- },
- methods: {
- /**
- * 页面进入后尝试自动连接已绑定设备,仅做连接与握手,不触发测量
- * - 读取本地绑定信息,打开蓝牙适配器
- * - 调用 connectDevice(false) 建立连接
- */
- goBack() {
- uni.navigateBack({ delta: 1 });
- },
- onPrimaryActionTap() {
- // 准备/异常时允许点击:不改动原有的测量逻辑,只做UI状态保护
- if (!this.connected) {
- uni.showToast({ title: '未连接', icon: 'none' });
- return;
- }
- if (this.uiState === 'measuring') return;
- if (typeof this.startBpMeasurement === 'function') this.startBpMeasurement();
- },
- async autoConnect() {
- if (!this.boundDevice) return;
- this.log('尝试自动连接绑定设备...');
- try {
- await promisifyUniApi('openBluetoothAdapter');
- await this.connectDevice(false);
- } catch (e) {
- this.log('蓝牙未开启或异常,自动连接取消');
- }
- },
- //开启后台扫描寻找绑定设备...
- async startBackgroundScanForBoundDevice() {
- if (this.scanning) return;
- this.log('开启后台扫描寻找绑定设备...');
- this.scanning = true;
- try {
- await promisifyUniApi('startBluetoothDevicesDiscovery', {
- allowDuplicatesKey: false
- });
- uni.onBluetoothDeviceFound((res) => {
- const newDevices = res.devices || [];
- const found = newDevices.find((d) => d.deviceId === this.boundDevice.deviceId);
- if (found) {
- this.log('扫描到绑定设备,发起连接...');
- this.stopScan();
- this.connectDevice(true);
- }
- });
- } catch (e) {
- this.scanning = false;
- this.log('后台扫描启动失败');
- }
- },
- unbindDevice(item) {
- uni.showModal({
- title: '解除绑定',
- content: '确定要解除绑定该设备吗?解绑后需重新扫描连接。',
- success: async (res) => {
- if (res.confirm) {
- if (this.deviceId == item.item && this.connected) {
- await this.disconnect();
- }
- if (!this.isFamily) {
- this.removeXHSDevice(item.deviceId);
- } else {
- this.removeXHSDevicefamily(item.deviceId);
- }
- const boundDeviceStr = uni.getStorageSync('bound_health_device');
- let boundDevice = null;
- try {
- // 解析缓存设备
- if (boundDeviceStr) boundDevice = JSON.parse(boundDeviceStr);
- // 校验:缓存设备是否在当前列表中
- const isDeviceValid = boundDevice && boundDevice.deviceId == item.deviceId;
- if (isDeviceValid) {
- uni.removeStorageSync('bound_health_device');
- }
- } catch (e) {}
- }
- }
- });
- },
- // 小护士解除绑定设备(自己)
- removeXHSDevice(deviceId) {
- removeXHSDevice({ xhsDeviceId: deviceId, deviceType: this.deviceType })
- .then(async (res) => {
- if (res.code == 200) {
- this.log('设备已解绑(接口)');
- this.xhsDeviceList = this.xhsDeviceList.filter((item) => item.deviceId != deviceId);
- uni.showToast({ title: '解绑成功', icon: 'success' });
- uni.$emit('scanFitWatch');
- } else {
- uni.showToast({
- title: res.msg,
- icon: 'none'
- });
- this.log('设备解绑失败(接口)');
- }
- })
- .catch((err) => {
- uni.showToast({
- title: '设备解绑失败',
- icon: 'none'
- });
- this.log('设备解绑失败err(接口)');
- });
- },
- removeXHSDevicefamily(deviceId) {
- let xhsDeviceId = this.xhsDeviceList.map((item) => item.deviceId);
- xhsDeviceId = xhsDeviceId.filter((item) => item != deviceId);
- this.otherDevice[this.selectUser - 1].xhsDeviceId = xhsDeviceId.join(',');
- console.log('==this.otherDevice=', this.otherDevice);
- editMyfamily({ otherDevice: JSON.stringify(this.otherDevice) })
- .then(async (res) => {
- if (res.code == 200) {
- this.log('设备已解绑(接口)');
- this.xhsDeviceList = this.xhsDeviceList.filter((item) => item.deviceId != deviceId);
- uni.showToast({ title: '解绑成功', icon: 'success' });
- uni.$emit('scanFitWatch');
- } else {
- uni.showToast({
- title: res.msg,
- icon: 'none'
- });
- this.log('设备绑定接口失败');
- }
- })
- .catch(async (err) => {
- uni.showToast({
- title: '设备解绑失败',
- icon: 'none'
- });
- this.log('设备解绑失败err(接口)');
- });
- },
- log(msg) {
- const line = `${new Date().toLocaleTimeString()} ${msg}`;
- this.logs.unshift(line);
- if (this.logs.length > 100) this.logs.pop();
- },
- async startScan() {
- this.deviceList = [];
- this.selectedDeviceId = '';
- this.log('打开蓝牙适配器...');
- console.log('打开蓝牙适配器...');
- try {
- await promisifyUniApi('openBluetoothAdapter');
- this.log('开始扫描低功耗蓝牙设备...');
- await promisifyUniApi('startBluetoothDevicesDiscovery', {
- allowDuplicatesKey: false
- });
- this.scanning = true;
- this.statusText = '正在扫描...';
- uni.onBluetoothDeviceFound((res) => {
- const nameContains = (d) => {
- const name = (d.name || d.localName || '').toLowerCase();
- return name.indexOf('bluetooth') !== -1;
- };
- // 已绑定的设备 ID 数组
- const boundIds = this.xhsDeviceList.map((item) => item.deviceId);
- // 过滤:只保留 名称符合 + 不在已绑定列表里 的设备
- const newDevices = (res.devices || []).filter((d) => {
- const isNameValid = nameContains(d);
- const isNotBound = !boundIds.includes(d.deviceId);
- const isNotExist = !this.deviceList.some((x) => x.deviceId === d.deviceId);
- return isNameValid && isNotBound && isNotExist;
- });
- if (newDevices.length) {
- this.deviceList = [...this.deviceList, ...newDevices];
- }
- });
- setTimeout(() => this.stopScan(), 10000);
- } catch (e) {
- this.scanning = false;
- this.statusText = '未连接';
- const msg = (e.errMsg || e.message || String(e)).toLowerCase();
- if (msg.indexOf('bluetooth') !== -1 || msg.indexOf('adapter') !== -1) {
- uni.showToast({ title: '请开启手机蓝牙', icon: 'none' });
- }
- this.log('扫描失败:' + (e.errMsg || e));
- }
- },
- async stopScan() {
- if (!this.scanning) return;
- try {
- await promisifyUniApi('stopBluetoothDevicesDiscovery');
- } catch (e) {}
- this.scanning = false;
- this.statusText = this.connected ? '已连接' : '未连接';
- this.log('扫描已停止,共发现 ' + this.deviceList.length + ' 个设备');
- },
- selectDevice(d) {
- this.selectedDeviceId = d.deviceId;
- this.selectedDeviceName = d.name || d.localName || '未知设备';
- this.connectDevice(false);
- },
- async sendData(buffer, description = '数据') {
- if (!this.deviceId || !this.writeServiceId || !this.writeCharId) return;
- const arr = new Uint8Array(buffer);
- const hex = Array.from(arr)
- .map((b) => ('0' + b.toString(16)).slice(-2))
- .join(' ');
- console.log(`[BLE Send Raw] ${description} Hex:`, hex);
- // 默认使用检测到的 writeType,如果没有则默认为 'write'
- let currentType = this.writeType;
- if (!currentType) {
- // 如果没有检测到明确的类型,尝试先用 write,如果失败会自动切
- currentType = 'write';
- }
- // 特殊处理:如果是 iOS 且没有明确 writeType,有些设备可能必须用 writeNoResponse
- const sys = uni.getSystemInfoSync();
- if (sys.platform === 'ios' && !this.writeType) {
- // iOS 下对某些外设,writeNoResponse 更稳定
- // currentType = 'writeNoResponse';
- }
- try {
- await promisifyUniApi('writeBLECharacteristicValue', {
- deviceId: this.deviceId,
- serviceId: this.writeServiceId,
- characteristicId: this.writeCharId,
- value: buffer,
- writeType: currentType
- });
- this.log(`已发送${description}: ${hex}`);
- } catch (e) {
- console.error(`[BLE Send Error] Type=${currentType}:`, e);
- // 不管什么错误,尝试切换 writeType 重试一次
- const retryType = currentType === 'write' ? 'writeNoResponse' : 'write';
- this.log(`发送失败 (${e.errCode || e.errMsg || e}),尝试切换为 ${retryType} 重试...`);
- try {
- await promisifyUniApi('writeBLECharacteristicValue', {
- deviceId: this.deviceId,
- serviceId: this.writeServiceId,
- characteristicId: this.writeCharId,
- value: buffer,
- writeType: retryType
- });
- this.writeType = retryType; // 如果重试成功,更新默认类型
- this.log(`重试成功 (${retryType}): ${hex}`);
- } catch (retryErr) {
- console.error(`[BLE Retry Error] Type=${retryType}:`, retryErr);
- // 还是失败,尝试不传 writeType (让系统自动判断)
- this.log(`二次重试失败,尝试不传 writeType...`);
- try {
- await promisifyUniApi('writeBLECharacteristicValue', {
- deviceId: this.deviceId,
- serviceId: this.writeServiceId,
- characteristicId: this.writeCharId,
- value: buffer
- // 不传 writeType
- });
- this.log(`三次重试成功 (Auto): ${hex}`);
- } catch (finalErr) {
- console.error(`[BLE Final Error]:`, finalErr);
- this.log(`发送最终失败: ${finalErr.errMsg || finalErr}`);
- }
- }
- }
- },
- // 手动开始血压测量
- /**
- * 用户点击后主动发起血压测量请求
- * - 要求蓝牙已经连接且写入特征可用
- * - 发送新协议开始测量指令 (FA 05)
- */
- startBpMeasurement() {
- if (!this.writeCharId) {
- uni.showToast({ title: '蓝牙未连接', icon: 'none' });
- return;
- }
- this.userRequestedMeasure = true;
- this.bpMeasureStarted = false;
- this.bpErrorMsg = '';
- this.bpProcessStatus = 'handshake';
- this.bpProcessStep = '正在请求开始测量...';
- // 新协议指令 (V2/V5协议)
- const bufferV2D2 = buildV2Command(0xd2); // D2为V5协议的开始测量指令
- // 旧协议指令
- const buffer = new Uint8Array([0xfa, 0x05, 0x00, 0x00, 0x00, 0x00, 0x05, 0x04]).buffer;
- // 连续发送几次以确保设备收到,旧设备握手+气泵启动可能需要较长时间,增加重试次数
- this.retryCount = 0;
- const maxRetries = 8; // 增加到8次,给予约8秒的等待时间
- // console.log("this.bpMeasureStarted====",this.bpMeasureStarted,this.retryCount,maxRetries,!this.userRequestedMeasure)
- this.log(`外面${this.bpMeasureStarted},${this.retryCount}---${maxRetries},${!this.userRequestedMeasure}`);
- const sendTimer = setInterval(() => {
- this.log(
- `里面${this.bpMeasureStarted},${this.retryCount}---${maxRetries},${!this.userRequestedMeasure}====${
- this.bpMeasureStarted || this.retryCount >= maxRetries || !this.userRequestedMeasure
- }`
- );
- if (this.bpMeasureStarted || this.retryCount >= maxRetries || !this.userRequestedMeasure) {
- clearInterval(sendTimer);
- if (this.retryCount >= maxRetries && !this.bpMeasureStarted) {
- this.log('设备未响应测量指令');
- this.bpProcessStep = '设备未响应,请重试';
- uni.showToast({ title: '设备未响应,请重试', icon: 'none' });
- }
- return;
- }
- this.log('=======开始测量12====');
- this.sendData(buffer, '开始测量(FA 05)==1');
- // 延迟一点发送兼容指令
- setTimeout(() => {
- if (!this.bpMeasureStarted && this.userRequestedMeasure) {
- this.sendData(bufferV2D2, '开始测量(D2)==1');
- }
- }, 200);
- this.retryCount++;
- }, 2000);
- // 立即发送第一次
- this.sendData(buffer, '开始测量(FA 05)==2');
- setTimeout(() => {
- this.sendData(bufferV2D2, '开始测量(D2)==2');
- }, 200);
- this.retryCount++;
- },
- // 进入血检模式
- enterBloodTest() {
- if (!this.writeCharId) {
- uni.showToast({ title: '蓝牙未连接', icon: 'none' });
- return;
- }
- this.log('请求进入血检模式...');
- const bufferV2D3 = buildV2Command(0xd3); // D3为进入血检界面指令
- this.sendData(bufferV2D3, '进入血检(D3)');
- },
- async connectDevice(isAutoConnect = false) {
- if (!this.selectedDeviceId) {
- uni.showToast({ title: '请先选择设备', icon: 'none' });
- return;
- }
- this.userRequestedMeasure = false; // 连接时重置状态,等待用户点击开始测量
- this.connecting = true;
- this.log('正在连接 ' + this.selectedDeviceName + '...');
- this.statusText = '连接中...';
- try {
- await promisifyUniApi('createBLEConnection', {
- deviceId: this.selectedDeviceId,
- timeout: 10000
- });
- this.deviceId = this.selectedDeviceId;
- this.connected = true;
- this.statusText = '已连接';
- this.log('连接成功,正在发现服务...');
- await new Promise((r) => setTimeout(r, 600));
- const { services } = await promisifyUniApi('getBLEDeviceServices', {
- deviceId: this.deviceId
- });
- if (!services || services.length === 0) {
- this.log('未发现服务');
- this.connecting = false;
- return;
- }
- this.log('发现服务: ' + services.length + ' 个');
- services.forEach((s, i) => {
- console.log(`[Service ${i}] UUID: ${s.uuid}, IsPrimary: ${s.isPrimary}`);
- this.log(`S${i}: ${s.uuid.slice(4, 8)}`);
- });
- // 优先尝试 FFF0 (回到最初的起点)
- let svc = services.find((s) => s.uuid.toLowerCase().includes('fff0'));
- if (!svc) {
- // 如果没有 FFF0,尝试 FF10
- svc = services.find((s) => s.uuid.toLowerCase().includes('ff10'));
- }
- if (svc) {
- this.serviceId = svc.uuid;
- this.log('使用服务: ' + this.serviceId.slice(4, 8));
- // 如果是 FF10,特征值可能也是 FF11/FF12,或者其他
- // 需要重新获取该服务的特征值
- } else {
- this.log('错误: 未找到有效服务 (FF10/FFF0)');
- this.connecting = false;
- return;
- }
- const { characteristics } = await promisifyUniApi('getBLEDeviceCharacteristics', {
- deviceId: this.deviceId,
- serviceId: this.serviceId
- });
- // 打印所有特征值的详细属性,便于排查
- this.log('发现特征值: ' + characteristics.length + ' 个');
- characteristics.forEach((c, index) => {
- const props = [];
- if (c.properties.read) props.push('Read');
- if (c.properties.write) props.push('Write');
- if (c.properties.writeNoResponse) props.push('WriteNoResp');
- if (c.properties.notify) props.push('Notify');
- if (c.properties.indicate) props.push('Indicate');
- console.log(`[Char ${index}] UUID: ${c.uuid}, Props: ${props.join('|')}`);
- this.log(`C${index}: ${c.uuid.slice(4, 8)} [${props.join('|')}]`);
- });
- // 1. 查找接收特征 (Notify/Indicate)
- // 尝试匹配 FFF1, FF11 或任何 Notify
- let notifyChar = characteristics.find(
- (c) => (c.uuid.toLowerCase().includes('fff1') || c.uuid.toLowerCase().includes('ff11')) && (c.properties.notify || c.properties.indicate)
- );
- // 如果没找到,尝试只匹配 UUID
- if (!notifyChar) {
- notifyChar = characteristics.find((c) => c.uuid.toLowerCase().includes('fff1') || c.uuid.toLowerCase().includes('ff11'));
- }
- // 如果还是没找到,回退到任意具备 Notify/Indicate 属性的特征
- if (!notifyChar) {
- this.log('未找到标准接收特征,尝试回退...');
- notifyChar = characteristics.find((c) => c.properties.notify || c.properties.indicate);
- }
- if (notifyChar) {
- this.notifyCharId = notifyChar.uuid;
- this.notifyServiceId = this.serviceId;
- } else {
- this.log('错误: 未找到任何可接收数据的特征值');
- this.connecting = false;
- return;
- }
- // 2. 查找发送特征 (Write/WriteNoResponse)
- // 尝试匹配 FFF2, FF12 或任何 Write
- let writeChar = characteristics.find(
- (c) => (c.uuid.toLowerCase().includes('fff2') || c.uuid.toLowerCase().includes('ff12')) && (c.properties.write || c.properties.writeNoResponse)
- );
- // 如果没找到,回退到任意具备 Write/WriteNoResponse 属性的特征
- if (!writeChar) {
- this.log('未找到标准发送特征,尝试回退...');
- writeChar = characteristics.find((c) => c.properties.write || c.properties.writeNoResponse);
- }
- if (writeChar) {
- this.writeServiceId = this.serviceId;
- this.writeCharId = writeChar.uuid;
- // 确定写入类型:优先使用 write (带响应),如果不支持则用 writeNoResponse
- // 注意:有些设备虽然标明 write,但实际需要 writeNoResponse,反之亦然
- // 我们这里记录首选类型,在 sendData 中会实现自动降级重试
- if (writeChar.properties.write) {
- this.writeType = 'write';
- } else if (writeChar.properties.writeNoResponse) {
- this.writeType = 'writeNoResponse';
- } else {
- this.writeType = 'write'; // 默认
- }
- this.log(`发送特征就绪 (Type: ${this.writeType})`);
- } else {
- this.log('警告: 未找到可写特征,无法发送指令');
- }
- this.log(`最终配置: Notify=${this.notifyCharId.slice(4, 8)}, Write=${this.writeCharId ? this.writeCharId.slice(4, 8) : '无'}`);
- await promisifyUniApi('notifyBLECharacteristicValueChange', {
- deviceId: this.deviceId,
- serviceId: this.notifyServiceId,
- characteristicId: this.notifyCharId,
- state: true
- });
- this.log('已开启数据通知');
- // 不再自动发送任何握手指令,等待用户点击开始测量
- if (this.writeCharId) {
- this.log('连接成功,等待用户操作');
- this.bpProcessStatus = 'connected';
- this.bpProcessStep = '已连接,请点击开始测量';
- }
- this.log('已开启数据通知');
- uni.onBLECharacteristicValueChange((res) => {
- if (res.deviceId !== this.deviceId) return;
- const buf = res.value;
- const arr = new Uint8Array(buf);
- const hex = Array.from(arr)
- .map((b) => ('0' + b.toString(16)).slice(-2))
- .join(' ');
- console.log('[BLE Recv Raw] Hex:', hex);
- console.log('[BLE Recv Raw] Bytes:', arr);
- // this.log('收到数据: ' + hex);
- // 1. 尝试解析血压协议 (FD FD)
- // 尝试解析 V2 格式 (FD FD Len Cmd ... CS 0D 0A)
- // V2格式特点:第2字节(索引2)是Len,最后一个字节是0A,倒数第二个是0D,倒数第三个是CS
- let isV2 = false;
- let v2Cmd = null;
- let v2Data = null;
- if (arr.length >= 6 && arr[0] === 0xfd && arr[1] === 0xfd) {
- const len = arr[2];
- if (arr.length >= 2 + 1 + len + 2) {
- const content = arr.slice(3, 3 + len);
- v2Cmd = content[0];
- v2Data = content.slice(1, content.length - 1);
- const checksum = content[content.length - 1];
- let sum = v2Cmd;
- for (let i = 0; i < v2Data.length; i++) sum += v2Data[i];
- if ((sum & 0xff) === checksum) {
- isV2 = true;
- }
- }
- }
- // 处理旧协议 V2 逻辑
- if (isV2) {
- // 1. 握手: A0 -> D1
- if (v2Cmd === 0xa0) {
- this.log('收到旧协议广播 (A0),发送连接请求 (D1)...');
- this.bpProcessStatus = 'handshake';
- this.bpProcessStep = '收到广播,请求连接 (D1)...';
- this.sendData(buildV2Command(0xd1), '连接请求(D1)');
- return;
- }
- // 1.1 握手确认: D1 -> D1 (设备回显)
- if (v2Cmd === 0xd1) {
- this.log('收到 D1 回显,等待 AF...');
- this.bpProcessStatus = 'handshake';
- this.bpProcessStep = '设备已确认请求,等待连接成功(AF)...';
- return;
- }
- // 2. 握手成功: AF
- if (v2Cmd === 0xaf) {
- this.log('收到连接建立成功信号 (AF)');
- this.bpProcessStatus = 'handshake';
- if (this.userRequestedMeasure) {
- this.bpProcessStep = '连接成功,发送开始测量指令 (D2)...';
- this.sendData(buildV2Command(0xd2), '开始测量(D2)');
- } else {
- this.bpProcessStep = '设备已就绪,等待用户点击开始测量';
- this.log('设备已就绪,等待用户点击开始测量,暂不发送D2');
- }
- return;
- }
- // 2.1 血压测量确认: A1 (设备回复)
- if (v2Cmd === 0xa1) {
- this.log('设备已确认测量请求 (A1)');
- this.bpProcessStatus = 'measuring';
- this.bpProcessStep = '设备已就绪,等待加压...';
- this.bpMeasureStarted = true;
- return;
- }
- // 2.2 进入血检确认: D4 (设备回复)
- if (v2Cmd === 0xd4) {
- this.log('设备已进入血检界面 (D4)');
- this.bpProcessStatus = 'idle';
- this.bpProcessStep = '请插入试纸...';
- uni.showToast({ title: '请插入试纸', icon: 'none' });
- return;
- }
- // 5. 血压测量过程 (A2)
- if (v2Cmd === 0xa2) {
- this.bpProcessStatus = 'measuring';
- if (v2Data.length >= 2) {
- const pressure = v2Data[0] * 256 + v2Data[1];
- this.bpCurrentPressure = pressure;
- this.bpMeasureStarted = true;
- this.bpProcessStep = `测量中... ${pressure} mmHg`;
- }
- return;
- }
- // 测试结果 (A3)
- if (v2Cmd === 0xa3) {
- // [0xFD,0xFD, 0x07, 0xA3, SYS, DIA, 单位, PUL, IHB, Checksum, 0X0D, 0x0A]
- // v2Data: [SYS, DIA, 单位, PUL, IHB]
- if (v2Data.length >= 5) {
- const systolic = v2Data[0];
- const diastolic = v2Data[1];
- const unitByte = v2Data[2]; // 00=mmHg, 01=kPa (通常是00)
- const pulse = v2Data[3];
- const ihb = v2Data[4];
- this.userRequestedMeasure = false;
- this.bpMeasureStarted = false; // 测量结束,重置开始标志
- if (systolic > 0 && diastolic > 0) {
- this.bpCurrentPressure = null;
- this.bpErrorMsg = '';
- this.bpProcessStatus = 'result';
- this.bpProcessStep = '测量完成';
- this.lastBloodPressure = {
- systolic,
- diastolic,
- pulse,
- ihb: ihb === 0x01 ? 1 : 0,
- time: Date.now()
- };
- this.cardBloodPressure = this.lastBloodPressure;
- this.$forceUpdate();
- this.log(`血压(V2): ${systolic}/${diastolic} mmHg 脉率:${pulse}`);
- this.xhsDataAdd(1, 0, this.lastBloodPressure);
- } else {
- this.bpCurrentPressure = null;
- this.bpErrorMsg = '测量结果异常 (数据为0)';
- this.bpProcessStatus = 'error';
- this.bpProcessStep = '测量失败';
- this.log('血压计错误(V2): 测量结果为0');
- uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
- }
- }
- return;
- }
- // 6. 血压测量错误 (A4)
- if (v2Cmd === 0xa4) {
- const errCode = v2Data[0] || 0;
- this.userRequestedMeasure = false; // 测量结束,重置状态
- this.bpMeasureStarted = false; // 测量结束,重置开始标志
- this.bpErrorMsg = '测量错误请重试';
- this.bpProcessStatus = 'error';
- this.bpProcessStep = '测量失败';
- this.log(`血压计测量错误(V2): 错误码 ${errCode.toString(16).toUpperCase()}`);
- uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
- return;
- }
- // 7. 血糖测量开始 (BB)
- if (v2Cmd === 0xbb) {
- this.log('收到血糖测量开始指令 (BB),回复 D9...');
- this.glucoseStatus = '请滴入样本...';
- this.glucoseErrorMsg = '';
- this.sendData(buildV2Command(0xd9), '回复血糖开始(D9)');
- return;
- }
- // 8. 血糖测量结果 (B2)
- if (v2Cmd === 0xb2) {
- if (v2Data.length >= 3) {
- const valH = v2Data[0];
- const valL = v2Data[1];
- const unitByte = v2Data[2];
- const value = (valH << 8) | valL;
- let displayVal = '0.0';
- if (valH === 0xff && valL === 0xff) {
- displayVal = 'HIGH';
- } else if (valH === 0x00 && valL === 0x00) {
- displayVal = 'LOW';
- } else {
- displayVal = (value / 10).toFixed(1);
- }
- this.lastGlucose = {
- value: displayVal,
- unit: unitByte === 0x01 ? 'mg/dL' : 'mmol/L',
- time: Date.now()
- };
- this.cardGlucose = this.lastGlucose;
- this.glucoseStatus = '测量完成';
- this.log(`血糖结果(V2): ${displayVal} ${this.lastGlucose.unit}`);
- this.sendData(buildV2Command(0xda), '回复血糖结果(DA)');
- }
- return;
- }
- // 9. 血糖报错 (B3)
- if (v2Cmd === 0xb3) {
- const errCode = v2Data[0];
- this.glucoseStatus = '';
- this.glucoseErrorMsg = `测量错误: 错误码 ${errCode.toString(16).toUpperCase()}`;
- this.log(`血糖报错(V2): ${errCode.toString(16).toUpperCase()}`);
- this.sendData(buildV2Command(0xdb), '回复血糖报错(DB)');
- return;
- }
- // 10. 尿酸测量开始 (CC)
- if (v2Cmd === 0xcc) {
- this.log('收到尿酸测量开始指令 (CC),回复 DC...');
- this.uricAcidStatus = '请滴入样本...';
- this.uricAcidErrorMsg = '';
- this.sendData(buildV2Command(0xdc), '回复尿酸开始(DC)');
- return;
- }
- // 11. 尿酸测量结果 (C2)
- if (v2Cmd === 0xc2) {
- if (v2Data.length >= 3) {
- const valH = v2Data[0];
- const valL = v2Data[1];
- const unitByte = v2Data[2];
- const value = (valH << 8) | valL;
- let displayVal = '0';
- if (valH === 0xff && valL === 0xff) {
- displayVal = 'HIGH';
- } else if (valH === 0x00 && valL === 0x00) {
- displayVal = 'LOW';
- } else {
- displayVal = value.toString();
- }
- this.lastUricAcid = {
- value: displayVal,
- unit: unitByte === 0x01 ? 'mg/dL' : 'umol/L',
- time: Date.now()
- };
- this.uricAcidStatus = '测量完成';
- this.log(`尿酸结果(V2): ${displayVal} ${this.lastUricAcid.unit}`);
- this.sendData(buildV2Command(0xdd), '回复尿酸结果(DD)');
- }
- return;
- }
- // 12. 尿酸报错 (C3)
- if (v2Cmd === 0xc3) {
- const errCode = v2Data[0];
- this.uricAcidStatus = '';
- this.uricAcidErrorMsg = `测量错误: 错误码 ${errCode.toString(16).toUpperCase()}`;
- this.log(`尿酸报错(V2): ${errCode.toString(16).toUpperCase()}`);
- this.sendData(buildV2Command(0xde), '回复尿酸报错(DE)');
- return;
- }
- }
- // 新协议解析 (FD FD ...)
- const bpPacket = parseBpPacket(arr);
- if (bpPacket) {
- const { cmd, data } = bpPacket;
- // 兼容:旧协议的 A0 广播 (FD FD 02 A0 A0 0D 0A) 或 D1 (FD FD 02 D1 D1 0D 0A)
- // 如果 isV2 为 true 且是 A0,已经在上面处理过了。这里作为备用。
- if (cmd === 0x02 && data.length >= 2 && data[0] === 0xa0 && data[1] === 0xa0) {
- if (!isV2) {
- this.log('收到旧协议广播 (A0)[fallback],发送 D1');
- this.sendData(buildV2Command(0xd1), '连接请求(D1)');
- }
- return;
- }
- // A4 广播 (等待连接)
- if (cmd === CMD_BP_BROADCAST_A4) {
- if (data.length >= 3) {
- this.bpWaitUser = data[2]; // 用户号在第3个字节(索引2)
- }
- this.log('收到血压计唤醒请求 (A4)');
- // 如果用户已点击开始测量,持续回复 FA 05 直到设备确认并停止发送 A4
- if (this.userRequestedMeasure) {
- this.bpProcessStep = '设备唤醒中,正在同步指令...';
- const buffer = buildBpCommand(CMD_BP_CONNECT_FA, [0x05]);
- this.sendData(buffer, '响应A4(FA 05)');
- } else {
- this.bpProcessStep = '设备就绪,等待用户开始测量';
- }
- return;
- }
- // 06 确认开始测量
- if (cmd === CMD_BP_CONFIRM_06 || (cmd === CMD_BP_CONNECT_FA && data[0] === 0x06)) {
- this.log('设备已确认测量请求 (06)');
- this.bpProcessStatus = 'measuring';
- this.bpProcessStep = '设备已就绪,等待加压...';
- this.bpMeasureStarted = true;
- return;
- }
- // FB 测量中压力
- if (cmd === CMD_BP_PRESSURE_FB) {
- this.bpProcessStatus = 'measuring';
- if (data.length >= 2) {
- const pressure = data[0] * 256 + data[1];
- this.bpCurrentPressure = pressure;
- this.bpMeasureStarted = true;
- this.bpProcessStep = `测量中... ${pressure} mmHg`;
- }
- return;
- }
- // FC 测试结果
- if (cmd === CMD_BP_RESULT_FC) {
- // data: [SYS, DIA, PUL, IHB]
- if (data.length >= 4) {
- const systolic = data[0];
- const diastolic = data[1];
- const pulse = data[2];
- const ihb = data[3];
- this.userRequestedMeasure = false;
- this.bpMeasureStarted = false; // 测量结束,重置开始标志
- if (systolic > 0 && diastolic > 0) {
- this.bpCurrentPressure = null;
- this.bpErrorMsg = '';
- this.bpProcessStatus = 'result';
- this.bpProcessStep = '测量完成';
- this.lastBloodPressure = {
- systolic,
- diastolic,
- pulse,
- ihb: ihb === 0x01 ? 1 : 0,
- time: Date.now()
- };
- this.cardBloodPressure = this.lastBloodPressure;
- this.$forceUpdate();
- this.log(`血压: ${systolic}/${diastolic} mmHg`);
- // 按照文档要求回复 FA 60 确认指令,防止设备重复发送
- const replyBuffer = buildBpCommand(CMD_BP_CONNECT_FA, [0x60]);
- this.sendData(replyBuffer, '确认结果(FA 60)');
- this.xhsDataAdd(1, 0, this.lastBloodPressure);
- } else {
- this.bpCurrentPressure = null;
- this.bpErrorMsg = '测量结果异常 (数据为0)';
- this.bpProcessStatus = 'error';
- this.bpProcessStep = '测量失败';
- this.log('血压计错误: 测量结果为0');
- uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
- }
- }
- return;
- }
- // FD 测量错误
- if (cmd === CMD_BP_ERROR_FD) {
- const errCode = data[0] || 0;
- this.userRequestedMeasure = false; // 测量结束,重置状态
- this.bpMeasureStarted = false; // 测量结束,重置开始标志
- this.bpCurrentPressure = null;
- this.bpErrorMsg = '测量错误请重试';
- this.bpProcessStatus = 'error';
- this.bpProcessStep = '测量失败';
- this.log(`血压计测量错误: 错误码 ${errCode.toString(16).toUpperCase()}`);
- uni.showToast({ title: '测量错误请重试', icon: 'none', duration: 3000 });
- return;
- }
- return;
- }
- // 2. 尝试解析血糖/尿酸协议 (A5 A5)
- const glucoseRes = parseGlucosePacket(buf);
- if (glucoseRes) {
- if (glucoseRes.type === 'glucose_start') {
- this.glucoseStatus = '请滴入样本...';
- this.glucoseErrorMsg = '';
- this.log('血糖测量开始');
- } else if (glucoseRes.type === 'uric_start') {
- this.uricAcidStatus = '请滴入样本...';
- this.uricAcidErrorMsg = '';
- this.log('尿酸测量开始');
- } else if (glucoseRes.type === 'glucose_result') {
- this.lastGlucose = {
- value: glucoseRes.value,
- unit: glucoseRes.unit,
- time: glucoseRes.time
- };
- this.cardGlucose = this.lastGlucose;
- this.glucoseStatus = '测量完成';
- this.log(`血糖结果: ${glucoseRes.value} ${glucoseRes.unit}`);
- this.xhsDataAdd(1, 1, this.lastGlucose);
- } else if (glucoseRes.type === 'uric_result') {
- this.lastUricAcid = {
- value: glucoseRes.value,
- unit: glucoseRes.unit,
- time: glucoseRes.time
- };
- this.uricAcidStatus = '测量完成';
- this.log(`尿酸结果: ${glucoseRes.value} ${glucoseRes.unit}`);
- this.xhsDataAdd(1, 3, this.lastUricAcid);
- } else if (glucoseRes.type === 'glucose_error') {
- this.glucoseErrorMsg = glucoseRes.msg;
- this.glucoseStatus = '';
- this.log(glucoseRes.msg);
- } else if (glucoseRes.type === 'uric_error') {
- this.uricAcidErrorMsg = glucoseRes.msg;
- this.uricAcidStatus = '';
- this.log(glucoseRes.msg);
- }
- if (glucoseRes.replyCmd) {
- this.sendData(glucoseRes.replyCmd, '回复确认');
- }
- return;
- }
- });
- uni.showToast({ title: '连接成功', icon: 'success' });
- console.log('!this.boundDevice===', !this.boundDevice);
- if (!this.boundDevice) {
- if (this.isFamily) {
- this.editMyfamily(this.selectedDeviceId);
- return;
- }
- this.editXHSDevice(this.selectedDeviceId);
- }
- } catch (e) {
- this.connected = false;
- if (!this.boundDevice) {
- this.deviceId = '';
- }
- this.statusText = '未连接';
- const errMsg = e.errMsg || e.message || String(e);
- this.log('连接失败: ' + errMsg);
- uni.showToast({ title: '连接失败', icon: 'none' });
- if (isAutoConnect === true && this.boundDevice) {
- this.log('直接连接失败,尝试后台扫描...');
- this.startBackgroundScanForBoundDevice();
- }
- }
- this.connecting = false;
- },
- async disconnect() {
- this.disconnecting = true;
- this.log('断开连接...');
- await this.closeBLE();
- this.connected = false;
- this.deviceId = '';
- this.serviceId = '';
- this.characteristicId = '';
- this.notifyServiceId = '';
- this.notifyCharId = '';
- this.writeServiceId = '';
- this.writeCharId = '';
- this.lastGlucose = { value: null, unit: 'mmol/L', time: null };
- this.glucoseStatus = '';
- this.glucoseErrorMsg = '';
- this.lastUricAcid = { value: null, unit: 'umol/L', time: null };
- this.uricAcidStatus = '';
- this.uricAcidErrorMsg = '';
- this.lastBloodPressure = { systolic: null, diastolic: null, pulse: null, ihb: null, time: null };
- this.bpMeasureStarted = false;
- this.userRequestedMeasure = false;
- this.bpCurrentPressure = null;
- this.bpErrorMsg = '';
- this.statusText = '未连接';
- this.disconnecting = false;
- this.log('已断开');
- const boundDeviceStr = uni.getStorageSync('bound_health_device');
- let boundDevice = null;
- try {
- // 解析缓存设备
- if (boundDeviceStr) boundDevice = JSON.parse(boundDeviceStr);
- // 校验:缓存设备是否在当前列表中
- const isDeviceValid = boundDevice && boundDevice.deviceId == item.deviceId;
- if (isDeviceValid) {
- uni.removeStorageSync('bound_health_device');
- }
- } catch (e) {}
- },
- async closeBLE() {
- try {
- if (this.deviceId) {
- await promisifyUniApi('closeBLEConnection', { deviceId: this.deviceId });
- }
- } catch (e) {}
- try {
- await promisifyUniApi('closeBluetoothAdapter');
- } catch (e) {}
- },
- // 获取接口数据
- getLastData() {
- this.dataSync = 'loading';
- const param = {
- deviceId: this.selectedDeviceId,
- deviceType: this.deviceType
- };
- getLastData(param).then((res) => {
- if (res.code == 200) {
- // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- // 状态标记
- this.dataSync = 'success';
- // 安全获取数据列表
- const { data = [] } = res;
- // 一次性提取所有需要的数据(只遍历一次数组,性能更好)
- const bloodPressure = data.find((item) => item.recordType === 0);
- const pulse = data.find((item) => item.recordType === 2);
- const glucose = data.find((item) => item.recordType === 1);
- const uricAcid = data.find((item) => item.recordType === 3);
- // 血压数据解析(安全处理)
- let bpData = {};
- try {
- bpData = bloodPressure?.recordValue ? JSON.parse(bloodPressure.recordValue) : {};
- } catch (e) {
- bpData = {};
- }
- // 统一赋值(带默认值,防止 undefined 报错)
- this.cardGlucose = {
- value: glucose?.recordValue || '',
- unit: 'mmol/L',
- time: glucose?.createTime || ''
- };
- this.lastUricAcid = {
- value: uricAcid?.recordValue || null,
- unit: 'umol/L',
- time: uricAcid?.createTime || ''
- };
- this.cardBloodPressure = {
- systolic: bpData.sdb || '',
- diastolic: bpData.dbp || '',
- pulse: pulse?.recordValue || '',
- ihb: null,
- time: bloodPressure?.createTime || ''
- };
- console.log('this.cardBloodPressure=====', this.cardBloodPressure);
- } else {
- this.dataSync = 'error';
- this.log('数据同步失败');
- }
- })
- .catch(() => {
- this.dataSync = 'error';
- this.log('数据同步失败');
- });
- },
- editMyfamily(selectedDeviceId, type) {
- if (!selectedDeviceId || this.xhsDeviceList.some((item) => item.deviceId == selectedDeviceId)) return;
- let xhsDeviceId = this.xhsDeviceList.map((item) => item.deviceId);
- xhsDeviceId.push(selectedDeviceId);
- this.otherDevice[this.selectUser - 1].xhsDeviceId = xhsDeviceId.join(',');
- editMyfamily({ otherDevice: JSON.stringify(this.otherDevice) })
- .then(async (res) => {
- if (res.code == 200) {
- this.xhsDeviceList.push({ name: 'Bluetooth BP', deviceId: selectedDeviceId });
- this.selectedDeviceId = selectedDeviceId;
- this.selectedDeviceName = 'Bluetooth BP';
- this.boundDevice = {
- deviceId: this.selectedDeviceId,
- name: this.selectedDeviceName
- };
- console.log('已绑定设备============' + this.boundDevice.deviceId);
- this.log('已绑定设备: ' + this.boundDevice.deviceId);
- uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
- this.getLastData();
- uni.$emit('scanFitWatch');
- } else {
- uni.showToast({
- title: res.msg,
- icon: 'none'
- });
- if (this.connected) {
- await this.disconnect();
- }
- uni.removeStorageSync('bound_health_device');
- this.log('设备绑定接口失败');
- }
- })
- .catch(async (err) => {
- this.log('设备绑定接口失败');
- if (this.connected) {
- await this.disconnect();
- }
- uni.removeStorageSync('bound_health_device');
- });
- },
- editXHSDevice(selectedDeviceId) {
- if (!selectedDeviceId || this.xhsDeviceList.some((item) => item.deviceId == selectedDeviceId)) return;
- const xhsDeviceId = this.xhsDeviceList.map((item) => item.deviceId);
- xhsDeviceId.push(selectedDeviceId);
- const param = {
- xhsDeviceId: xhsDeviceId.join(','), // 设备唯一MAC
- deviceType: this.deviceType
- };
- editXHSDevice(param)
- .then(async (res) => {
- if (res.code == 200) {
- this.xhsDeviceList.push({ name: 'Bluetooth BP', deviceId: selectedDeviceId });
- this.selectedDeviceId = selectedDeviceId;
- this.selectedDeviceName = 'Bluetooth BP';
- this.boundDevice = {
- deviceId: this.selectedDeviceId,
- name: this.selectedDeviceName
- };
- console.log('已绑定设备============' + this.boundDevice.deviceId);
- this.log('已绑定设备: ' + this.boundDevice.deviceId);
- uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
- uni.$emit('scanFitWatch');
- this.getLastData();
- } else {
- uni.showToast({
- title: res.msg,
- icon: 'none'
- });
- if (this.connected) {
- await this.disconnect();
- }
- uni.removeStorageSync('bound_health_device');
- this.log('设备绑定接口失败');
- }
- })
- .catch(async (err) => {
- this.log('设备绑定接口失败');
- if (this.connected) {
- await this.disconnect();
- }
- uni.removeStorageSync('bound_health_device');
- });
- },
- xhsDataAddTest(recordType) {
- let recordValue = '';
- let param = '';
- if (recordType == 0) {
- recordValue = {
- sdb: '90',
- dbp: '140'
- };
- param = [
- {
- recordType: 0, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: JSON.stringify(recordValue),
- deviceId: 'FC:62:58:04:19:1E',
- deviceType: this.deviceType
- },
- {
- recordType: 2, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: 80,
- deviceId: 'FC:62:58:04:19:1E',
- deviceType: this.deviceType
- }
- ];
- } else if (recordType == 1) {
- param = [
- {
- recordType: 1, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: 5,
- deviceId: 'FC:62:58:04:19:1E',
- deviceType: this.deviceType
- }
- ];
- } else if (recordType == 3) {
- param = [
- {
- recordType: 3, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: '200',
- deviceId: 'FC:62:58:04:19:1E',
- deviceType: this.deviceType
- }
- ];
- }
- this.dataSync = 'loading';
- xhsDataAddList(param)
- .then((res) => {
- if (res.code == 200) {
- this.dataSync = 'success';
- // this.getLastData()
- uni.$emit('scanFitWatch');
- } else {
- this.dataSync = 'error';
- this.log('数据同步失败');
- }
- })
- .catch(() => {
- this.dataSync = 'error';
- this.log('数据同步失败');
- });
- },
- // 接口数据同步
- xhsDataAdd(type, recordType, data) {
- if (type == 1) {
- let recordValue = '';
- this.dataSyncFunInfo = '';
- if (recordType == 0) {
- recordValue = {
- sdb: data.systolic,
- dbp: data.diastolic
- };
- this.dataSyncFunInfo = [
- {
- recordType: 0, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: JSON.stringify(recordValue),
- deviceId: this.deviceId,
- deviceType: this.deviceType
- },
- {
- recordType: 2, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: data.pulse,
- deviceId: this.deviceId,
- deviceType: this.deviceType
- }
- ];
- } else if (recordType == 1) {
- this.dataSyncFunInfo = [
- {
- recordType: 1, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: data.value,
- deviceId: this.deviceId,
- deviceType: this.deviceType
- }
- ];
- } else if (recordType == 3) {
- this.dataSyncFunInfo = [
- {
- recordType: 3, // 0:血压 1:血糖 2:心率 3尿酸 4血氧
- recordValue: data.value,
- deviceId: this.deviceId,
- deviceType: this.deviceType
- }
- ];
- }
- }
- this.dataSync = 'loading';
- console.log('param===', this.dataSyncFunInfo);
- xhsDataAddList(this.dataSyncFunInfo)
- .then((res) => {
- if (res.code == 200) {
- this.dataSync = 'success';
- // this.getLastData()
- uni.$emit('scanFitWatch');
- } else {
- this.dataSync = 'error';
- this.log('数据同步失败');
- }
- })
- .catch(() => {
- this.dataSync = 'error';
- this.log('数据同步失败');
- });
- },
- getUser() {
- getWatchUserInfo({ isFamily: false }).then((res) => {
- if (res.code == 200) {
- let userInfo = res.user;
- uni.setStorageSync('userWatchInfo', JSON.stringify(res.user));
- // 1. 处理设备列表:家庭成员 / 个人模式
- if (this.isFamily) {
- // 安全解析家庭成员设备
- this.otherDevice = userInfo.otherDevice ? JSON.parse(userInfo.otherDevice) : [];
- // 格式化设备ID为对象数组
- const formattedDevices = this.otherDevice.map((item) => ({
- ...item,
- xhsDeviceId: item.xhsDeviceId ? item.xhsDeviceId.split(',').map((id) => ({ deviceId: id, name: 'Bluetooth BP' })) : []
- }));
- // 安全取当前选中成员的设备(防越界)
- const index = this.selectUser - 1;
- this.xhsDeviceList = formattedDevices[index]?.xhsDeviceId || [];
- } else {
- // 个人设备格式化
- const deviceIds = userInfo.xhsDeviceId ? userInfo.xhsDeviceId.split(',') : [];
- this.xhsDeviceList = deviceIds.map((id) => ({ deviceId: id, name: 'Bluetooth BP' }));
- }
- // 2. 设备绑定 & 自动连接逻辑(统一处理,无重复代码)
- if (!this.xhsDeviceList || this.xhsDeviceList.length === 0) {
- // 无设备:清空绑定缓存
- uni.removeStorageSync('bound_health_device');
- this.boundDevice = null;
- } else {
- // 有设备:安全获取绑定设备
- const boundDeviceStr = uni.getStorageSync('bound_health_device');
- let boundDevice = null;
- try {
- // 解析缓存设备
- if (boundDeviceStr) boundDevice = JSON.parse(boundDeviceStr);
- // 校验:缓存设备是否在当前列表中
- const isDeviceValid = boundDevice && this.xhsDeviceList.some((item) => item.deviceId == boundDevice.deviceId);
- // 确定最终绑定设备
- this.boundDevice = isDeviceValid ? boundDevice : this.xhsDeviceList[0];
- // 持久化保存
- uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
- // 赋值选中设备信息
- this.selectedDeviceId = this.boundDevice.deviceId;
- this.selectedDeviceName = this.boundDevice.name;
- this.deviceId = this.selectedDeviceId;
- // 自动连接 & 获取数据
- setTimeout(() => this.autoConnect(), 500);
- this.getLastData();
- } catch (e) {
- // 异常兜底:绑定第一个设备
- this.boundDevice = this.xhsDeviceList[0];
- uni.setStorageSync('bound_health_device', JSON.stringify(this.boundDevice));
- // 赋值选中设备信息
- this.selectedDeviceId = this.boundDevice.deviceId;
- this.selectedDeviceName = this.boundDevice.name;
- this.deviceId = this.selectedDeviceId;
- // 自动连接 & 获取数据
- setTimeout(() => this.autoConnect(), 500);
- this.getLastData();
- console.error('设备绑定异常:', e);
- }
- }
- }
- });
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .page {
- min-height: 100vh;
- background: #ffffff;
- }
- .footer-txt {
- position: absolute;
- bottom: 40rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
- .pre {
- width: 450rpx;
- height: 56rpx;
- line-height: 56rpx;
- text-align: center;
- background: #f0fdf4;
- border-radius: 28rpx 28rpx 28rpx 28rpx;
- border: 1rpx solid rgba(34, 197, 94, 0.1);
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 24rpx;
- color: #22c55e;
- }
- .time {
- margin-top: 16rpx;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 24rpx;
- color: #999999;
- line-height: 40rpx;
- text-align: center;
- }
- }
- .nav-bar {
- position: sticky;
- top: 0;
- z-index: 10;
- display: flex;
- align-items: center;
- justify-content: center;
- justify-content: space-between;
- // height: 88rpx;
- margin: 0 24rpx;
- background: #fff;
- }
- .nav-title {
- font-size: 34rpx;
- font-weight: 600;
- color: #333;
- }
- .nav-back {
- // position: absolute;
- // left: 24rpx;
- // padding: 10rpx 0;
- }
- .back-text {
- width: 64rpx;
- height: 64rpx;
- }
- .next-text {
- width: 48rpx;
- height: 48rpx;
- }
- .content {
- padding: 24rpx;
- padding-bottom: 60rpx;
- padding-top: 40rpx;
- }
- /* ============ 截图风格UI ============ */
- .tabs {
- display: flex;
- gap: 18rpx;
- margin-bottom: 50rpx;
- }
- .tab {
- flex: 1;
- height: 72rpx;
- line-height: 72rpx;
- text-align: center;
- border-radius: 36rpx;
- background: #f5f7fa;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 28rpx;
- color: #67686f;
- }
- .tab.active {
- background: #ff7700;
- color: #fff;
- font-weight: 600;
- }
- .meter-wrap {
- display: flex;
- justify-content: center;
- margin-top: 12rpx;
- }
- .meter-ring {
- position: relative;
- width: 510rpx;
- height: 510rpx;
- border-radius: 50%;
- background: conic-gradient(var(--arc, #eaeaea) var(--p, 0%), var(--track, #eaeaea) 0);
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .meter-ring::after {
- content: '';
- position: absolute;
- inset: 24rpx;
- background: #ffffff;
- border-radius: 50%;
- }
- .meter-center {
- position: absolute;
- z-index: 1;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- }
- .ready-view {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- }
- .ready-checkimg {
- width: 104rpx;
- height: 104rpx;
- }
- .ready-check {
- width: 104rpx;
- height: 104rpx;
- border-radius: 50%;
- background: #ff7700;
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 56rpx;
- font-weight: 700;
- margin-bottom: 18rpx;
- }
- .disconnected-check {
- background: #eaeaea;
- color: #999;
- }
- .ready-text {
- font-size: 28rpx;
- color: #666;
- font-weight: 500;
- }
- .bp-value {
- font-size: 76rpx;
- font-weight: 700;
- color: #111;
- line-height: 88rpx;
- }
- .bp-value-unit {
- font-size: 28rpx;
- font-weight: 500;
- color: #999;
- margin-left: 10rpx;
- vertical-align: middle;
- }
- .measuring-sub,
- .result-sub {
- margin-top: 10rpx;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 28rpx;
- color: #757575;
- }
- .error-num {
- color: #999;
- }
- .error-sub {
- color: #e64340;
- }
- .tip-box {
- margin: 40rpx 0 0 0;
- padding: 20rpx 30rpx;
- background: #fff6f1;
- border-radius: 16rpx;
- display: flex;
- align-items: flex-start;
- gap: 14rpx;
- background: rgba(255, 119, 0, 0.05);
- border-radius: 16rpx 16rpx 16rpx 16rpx;
- border: 2rpx solid rgba(255, 119, 0, 0.1);
- }
- .tip-icon {
- width: 34rpx;
- height: 34rpx;
- border-radius: 50%;
- background: #ff7700;
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22rpx;
- font-weight: 700;
- flex-shrink: 0;
- line-height: 34rpx;
- }
- .tip-text {
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 24rpx;
- color: #67686f;
- line-height: 36rpx;
- }
- .primary-btn {
- margin-top: 26rpx;
- height: 96rpx;
- border-radius: 18rpx;
- background: linear-gradient(90deg, #f8551f 0%, #ff9501 100%);
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 12rpx 30rpx rgba(255, 119, 0, 0.25);
- }
- .primary-btn-disabled {
- background: #ff8a3a;
- box-shadow: none;
- opacity: 0.55;
- }
- .primary-btn-text {
- color: #fff;
- font-size: 34rpx;
- font-weight: 700;
- }
- .conn-card {
- margin-top: 22rpx;
- padding: 0 6rpx;
- }
- .conn-row {
- display: flex;
- align-items: center;
- gap: 14rpx;
- }
- .conn-dot {
- width: 12rpx;
- height: 12rpx;
- border-radius: 50%;
- background: #07c160;
- }
- .conn-text {
- font-size: 24rpx;
- color: #666;
- }
- .conn-time {
- margin-top: 10rpx;
- font-size: 22rpx;
- color: #999;
- }
- .sync-card {
- margin-top: 26rpx;
- background: #f5f7fa;
- border-radius: 16rpx 16rpx 0 0;
- padding: 24rpx 30rpx;
- display: flex;
- align-items: center;
- justify-content: space-between;
- .device-item-btn {
- background: red;
- color: #fff;
- padding: 16rpx 24rpx;
- border-radius: 44rpx;
- font-size: 28rpx;
- margin: 0;
- min-width: 160rpx;
- }
- }
- .sync-left {
- display: flex;
- flex-direction: column;
- }
- .sync-title {
- font-family: PingFang SC, PingFang SC;
- font-weight: 600;
- font-size: 36rpx;
- color: #22c55e;
- line-height: 40rpx;
- margin-bottom: 10rpx;
- }
- .sync-time {
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 24rpx;
- color: #999999;
- line-height: 40rpx;
- }
- .sync-check {
- width: 48rpx;
- height: 48rpx;
- }
- .result-panel {
- background: #f5f7fa;
- border-radius: 0 0 16rpx 16rpx;
- padding: 20rpx 16rpx;
- }
- .result-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 16rpx;
- }
- .result-card {
- width: 100%;
- min-width: 0;
- background: #ffffff;
- border-radius: 14rpx;
- padding: 18rpx 16rpx;
- box-sizing: border-box;
- }
- .result-label {
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 28rpx;
- color: #333333;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .result-text {
- margin-top: 16rpx;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 24rpx;
- color: #757575;
- }
- .result-card .active {
- color: #ff7700;
- }
- .trend-badge {
- font-size: 20rpx;
- color: #ff7700;
- font-weight: 700;
- }
- .result-value {
- margin-top: 8rpx;
- font-family: DINPro, DINPro;
- font-weight: bold;
- font-size: 48rpx;
- color: #333333;
- }
- .result-unit {
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 26rpx;
- color: #757575;
- margin-left: 10rpx;
- }
- .result-chart {
- margin-top: 6rpx;
- height: 80rpx;
- .chart-img {
- width: 100%;
- height: 100%;
- }
- }
- /* ============ 旧样式(保留,给断连/设备选择区使用) ============ */
- .status-card {
- background: #fff;
- border-radius: 24rpx;
- padding: 48rpx;
- margin-bottom: 32rpx;
- text-align: center;
- border: 2rpx solid #eee;
- }
- .status-card.connected {
- border-color: #ff5c03;
- background: #fffaf7;
- }
- .status-icon {
- font-size: 56rpx;
- color: #999;
- margin-bottom: 16rpx;
- }
- .status-card.connected .status-icon {
- color: #ff5c03;
- }
- .status-text {
- font-size: 28rpx;
- color: #666;
- margin-bottom: 24rpx;
- }
- .bp-wait-hint {
- font-size: 24rpx;
- color: #999;
- margin-bottom: 16rpx;
- }
- .bp-measure-ok {
- color: #07c160;
- }
- .bp-pressure-live {
- font-size: 28rpx;
- color: #ff5c03;
- font-weight: 600;
- margin-bottom: 16rpx;
- }
- .bp-error-msg {
- font-size: 24rpx;
- color: #e64340;
- background: #fff5f5;
- padding: 16rpx;
- border-radius: 12rpx;
- margin-bottom: 16rpx;
- line-height: 1.5;
- }
- .value-ihb {
- display: block;
- color: #e64340;
- margin-top: 4rpx;
- }
- .measure-value {
- margin: 24rpx 0 8rpx;
- }
- .value-label {
- display: block;
- font-size: 24rpx;
- color: #999;
- margin-bottom: 4rpx;
- }
- .value-num {
- font-size: 72rpx;
- font-weight: 700;
- color: #ff5c03;
- }
- .value-unit {
- font-size: 28rpx;
- color: #999;
- margin-left: 8rpx;
- }
- .value-extra {
- display: block;
- font-size: 26rpx;
- color: #666;
- margin-top: 8rpx;
- }
- .measure-time {
- font-size: 24rpx;
- color: #999;
- }
- .actions {
- background: #fff;
- border-radius: 24rpx;
- padding: 32rpx;
- margin-bottom: 24rpx;
- }
- .btn {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- border-radius: 44rpx;
- font-size: 32rpx;
- margin-bottom: 24rpx;
- }
- .btn:last-child {
- margin-bottom: 0;
- }
- .btn-primary {
- background: #ff5c03;
- color: #fff;
- border: none;
- }
- .btn-connect {
- background: #07c160;
- color: #fff;
- border: none;
- }
- .btn-warn {
- background: #fff;
- color: #e64340;
- border: 2rpx solid #e64340;
- }
- .device-list {
- margin: 24rpx 0;
- padding: 0 0 16rpx;
- border-bottom: 1rpx solid #eee;
- }
- .list-title {
- font-size: 26rpx;
- color: #999;
- margin-bottom: 16rpx;
- }
- .device-item {
- padding: 20rpx 0;
- border-radius: 12rpx;
- .device-item-btn {
- background: #07c160;
- color: #fff;
- padding: 16rpx 24rpx;
- border-radius: 44rpx;
- font-size: 28rpx;
- margin: 0;
- min-width: 160rpx;
- }
- }
- .device-item.active {
- background: #fff5f0;
- }
- .device-info {
- margin-left: 20rpx;
- display: flex;
- flex-direction: column;
- }
- .device-item-footer {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- .device-connect {
- background: #07c160;
- border: 1rpx solid #07c160;
- color: #fff;
- padding: 16rpx 24rpx;
- border-radius: 44rpx;
- font-size: 28rpx;
- margin: 0;
- min-width: 160rpx;
- }
- .device-connect-warn {
- padding: 16rpx 24rpx;
- background: #fff;
- color: #e64340;
- border: 1rpx solid #e64340;
- border-radius: 44rpx;
- font-size: 28rpx;
- margin: 0 20rpx 0 0;
- min-width: 160rpx;
- }
- }
- .device-name {
- font-size: 30rpx;
- color: #333;
- }
- .device-id {
- font-size: 22rpx;
- color: #999;
- margin-top: 4rpx;
- }
- .log-section {
- background: #fff;
- border-radius: 24rpx;
- padding: 24rpx;
- }
- .log-title {
- display: flex;
- justify-content: space-between;
- font-size: 28rpx;
- color: #666;
- padding: 8rpx 0;
- }
- .log-toggle {
- color: #ff5c03;
- font-size: 26rpx;
- }
- .log-list {
- max-height: 360rpx;
- margin-top: 16rpx;
- padding: 16rpx;
- background: #f5f5f5;
- border-radius: 12rpx;
- }
- .log-line {
- display: block;
- font-size: 22rpx;
- color: #666;
- line-height: 1.6;
- word-break: break-all;
- }
- </style>
|