| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316 |
- <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>
- <!-- 提示/同步卡片 -->
- <view v-if="uiState !== 'result'" class="tip-box">
- <view class="tip-icon">i</view>
- <view class="tip-text">{{ tipText }}</view>
- </view>
- <view v-else-if="bpUiState === 'result' && activeTab === 'bp'" class="sync-card">
- <view class="sync-left">
- <view class="sync-title">数据已同步</view>
- <view class="sync-time">测量时间:{{ lastSyncTime }}</view>
- </view>
- <image class="sync-check" src="@/static/images/pages_watch/icons/remove_icon2.png"></image>
- </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>
- <!-- 已连接信息(截图底部样式) -->
- <!-- <view v-if="connected" class="conn-card">
- <view class="conn-row">
- <view class="conn-dot" />
- <text class="conn-text">{{ deviceDisplayName }}已通过蓝牙连接({{ connectionPercent }}%)</text>
- </view>
- <view v-if="lastSyncTime" class="conn-time">最后同步时间:{{ lastSyncTime }}</view>
- </view> -->
- <!-- 结果指标卡片(只在血压/心率Tab展示,保持布局一致) -->
- <view v-if="bpUiState === 'result' && activeTab === 'bp'" class="result-panel">
- <view class="result-grid">
- <view class="result-card">
- <view class="result-label">血压</view>
- <view class="result-value">
- {{ lastBloodPressure.systolic }}/{{ lastBloodPressure.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>
- <!-- <svg viewBox="0 0 120 40" width="100%" height="40">
- <polyline fill="none" stroke="#FF7700" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
- points="8,28 18,28 25,20 34,26 46,14 58,22 68,18 80,26 95,12 110,20" />
- </svg> -->
- </view>
- <view class="result-text" :class="bpResultText=='偏高'?'active':''">{{ bpResultText }}</view>
- </view>
- <view class="result-card">
- <view class="result-label">心率</view>
- <view class="result-value">
- {{ lastBloodPressure.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>
- <!-- <svg viewBox="0 0 120 40" width="100%" height="40">
- <polyline fill="none" stroke="#FF4D4F" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
- points="6,26 14,28 20,20 28,24 34,18 42,22 50,16 58,20 66,14 74,18 82,10 90,16 98,12 106,16" />
- </svg> -->
- </view>
- <view class="result-text" :class="pulseResultText=='偏高'?'active':''">{{ pulseResultText }}</view>
- </view>
- <view class="result-card">
- <view class="result-label">血糖</view>
- <view class="result-value">
- {{ lastGlucose.value }}
- <text class="result-unit">({{ lastGlucose.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>
- <!-- <svg viewBox="0 0 120 40" width="100%" height="40">
- <polyline fill="none" stroke="#52D087" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
- points="10,26 25,20 40,24 55,12 70,16 85,8 100,18 112,10" />
- <circle cx="25" cy="20" r="4" fill="#52D087" />
- <circle cx="55" cy="12" r="4" fill="#52D087" />
- <circle cx="85" cy="8" r="4" fill="#52D087" />
- </svg> -->
- </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">
- {{ 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>
- <!-- <svg viewBox="0 0 120 40" width="100%" height="40">
- <g fill="#FF7700">
- <rect x="10" y="22" width="6" height="16" rx="2" />
- <rect x="20" y="18" width="6" height="20" rx="2" />
- <rect x="30" y="12" width="6" height="26" rx="2" />
- <rect x="40" y="16" width="6" height="22" rx="2" />
- <rect x="50" y="10" width="6" height="28" rx="2" />
- <rect x="60" y="14" width="6" height="24" rx="2" />
- <rect x="70" y="6" width="6" height="32" rx="2" />
- <rect x="80" y="12" width="6" height="26" rx="2" />
- <rect x="90" y="9" width="6" height="29" rx="2" />
- <rect x="100" y="13" width="6" height="25" rx="2" />
- </g>
- </svg> -->
- </view>
- <view class="result-text" :class="uricResultText=='偏高'?'active':''">{{ uricResultText }}</view>
- </view>
- </view>
- </view>
- <!-- 连接/设备操作区(非截图区域:保持你现有连接逻辑) -->
- <view v-if="!connected" class="actions">
- <view v-if="boundDevice">
- <view class="list-title">已绑定设备</view>
- <view class="device-item active" style="margin-bottom: 24rpx; justify-content: space-between">
- <view class="device-info">
- <text class="device-name">{{ boundDevice.name }}</text>
- <text class="device-id">{{ boundDevice.deviceId }}</text>
- </view>
- </view>
- <button class="btn btn-connect" :disabled="connecting" :loading="connecting" @tap="connectDevice(false)">
- {{ connecting ? '连接中...' : '重试连接' }}
- </button>
- <button class="btn btn-warn" :disabled="disconnecting" @tap="unbindDevice">解绑</button>
- </view>
- <view v-else>
- <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"
- :class="{ active: selectedDeviceId === d.deviceId }"
- @tap="selectDevice(d)"
- >
- <radio :checked="selectedDeviceId === d.deviceId" color="#FF5C03" />
- <view class="device-info">
- <text class="device-name">{{ d.name || d.localName || '未知设备' }}</text>
- <text class="device-id">{{ d.deviceId }}</text>
- </view>
- </view>
- </view>
- <button class="btn btn-connect" :disabled="!selectedDeviceId || connecting" :loading="connecting" @tap="connectDevice(false)">
- {{ connecting ? '连接中...' : '连接设备' }}
- </button>
- </view>
- </view>
- <!-- 日志(调试用,可折叠) -->
- <view class="log-section">
- <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 v-if="footerPreText" class="footer-txt">
- <view class="pre"> {{ footerPreText }}</view>
- <view v-if="lastSyncTime" class="time">最后同步时间:{{ lastSyncTime }}</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;
- }
- 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
- },
- // 血糖测量状态提示(如:请滴入血样)
- 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
- },
- // 血压计已回复 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'
- };
- },
- 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') {
- 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.lastBloodPressure.time ?? this.lastGlucose.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.lastBloodPressure.systolic);
- const dbp = Number(this.lastBloodPressure.diastolic);
- if (!isFinite(sbp) || !isFinite(dbp) || this.lastBloodPressure.systolic == null || this.lastBloodPressure.diastolic == null) return '--';
- if (sbp < 90 || dbp < 60) return '偏低';
- if (sbp >= 140 || dbp >= 90) return '偏高';
- return '正常';
- },
- pulseResultText() {
- const pulse = Number(this.lastBloodPressure.pulse);
- if (!isFinite(pulse) || this.lastBloodPressure.pulse == null) return '--';
- if (pulse < 60) return '偏低';
- if (pulse > 100) return '偏高';
- return '正常';
- },
- glucoseResultText() {
- const rawValue = Number(this.lastGlucose.value);
- if (!isFinite(rawValue) || this.lastGlucose.value == null) return '--';
- const unit = String(this.lastGlucose.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() {
- 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;
- }
- });
- // const sys = uni.getSystemInfoSync();
- // this.statusBarHeight = sys.statusBarHeight;
- const bound = uni.getStorageSync('bound_health_device');
- if (bound) {
- try {
- this.boundDevice = JSON.parse(bound);
- this.selectedDeviceId = this.boundDevice.deviceId;
- this.selectedDeviceName = this.boundDevice.name;
- // 延迟启动自动连接
- setTimeout(() => {
- this.autoConnect();
- }, 500);
- } catch (e) {}
- }
- },
- 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() {
- uni.showModal({
- title: '解除绑定',
- content: '确定要解除绑定该设备吗?解绑后需重新扫描连接。',
- success: async (res) => {
- if (res.confirm) {
- if (this.connected) {
- await this.disconnect();
- }
- uni.removeStorageSync('bound_health_device');
- this.boundDevice = null;
- this.selectedDeviceId = '';
- this.selectedDeviceName = '';
- this.deviceList = [];
- this.log('设备已解绑');
- }
- }
- });
- },
- 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;
- };
- const newDevices = (res.devices || []).filter((d) => nameContains(d) && !this.deviceList.some((x) => x.deviceId === d.deviceId));
- 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 || '未知设备';
- },
- 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;
- // 连续发送几次以确保设备收到,旧设备握手+气泵启动可能需要较长时间,增加重试次数
- let retryCount = 0;
- const maxRetries = 8; // 增加到8次,给予约8秒的等待时间
- const sendTimer = setInterval(() => {
- if (this.bpMeasureStarted || retryCount >= maxRetries || !this.userRequestedMeasure) {
- clearInterval(sendTimer);
- if (retryCount >= maxRetries && !this.bpMeasureStarted) {
- this.log('设备未响应测量指令');
- this.bpProcessStep = '设备未响应,请重试';
- uni.showToast({ title: '设备未响应,请重试', icon: 'none' });
- }
- return;
- }
- this.sendData(buffer, '开始测量(FA 05)');
- // 延迟一点发送兼容指令
- setTimeout(() => {
- if (!this.bpMeasureStarted && this.userRequestedMeasure) {
- this.sendData(bufferV2D2, '开始测量(D2)');
- }
- }, 200);
- retryCount++;
- }, 1000);
-
- // 立即发送第一次
- this.sendData(buffer, '开始测量(FA 05)');
- setTimeout(() => {
- this.sendData(bufferV2D2, '开始测量(D2)');
- }, 200);
- 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.$forceUpdate();
- this.log(`血压(V2): ${systolic}/${diastolic} mmHg 脉率:${pulse}`);
- } 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.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.$forceUpdate();
- this.log(`血压: ${systolic}/${diastolic} mmHg`);
-
- // 按照文档要求回复 FA 60 确认指令,防止设备重复发送
- const replyBuffer = buildBpCommand(CMD_BP_CONNECT_FA, [0x60]);
- this.sendData(replyBuffer, '确认结果(FA 60)');
- } 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.glucoseStatus = '测量完成';
- this.log(`血糖结果: ${glucoseRes.value} ${glucoseRes.unit}`);
- } 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}`);
- } 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' });
- if (!this.boundDevice) {
- const deviceInfo = {
- deviceId: this.selectedDeviceId,
- name: this.selectedDeviceName
- };
- this.boundDevice = deviceInfo;
- uni.setStorageSync('bound_health_device', JSON.stringify(deviceInfo));
- this.log('已绑定设备: ' + deviceInfo.name);
- }
- } 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('已断开');
- },
- async closeBLE() {
- try {
- if (this.deviceId) {
- await promisifyUniApi('closeBLEConnection', { deviceId: this.deviceId });
- }
- } catch (e) {}
- try {
- await promisifyUniApi('closeBluetoothAdapter');
- } catch (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;
- }
- .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;
- // border-radius: 50%;
- // background: #07c160;
- // color: #fff;
- // display: flex;
- // align-items: center;
- // justify-content: center;
- // font-size: 28rpx;
- // font-weight: 700;
- }
- .result-panel {
- //margin-top: 18rpx;
- 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-text .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 {
- display: flex;
- align-items: center;
- padding: 20rpx 0;
- border-radius: 12rpx;
- }
- .device-item.active {
- background: #fff5f0;
- }
- .device-info {
- margin-left: 20rpx;
- display: flex;
- flex-direction: column;
- }
- .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>
|