fitWatch.vue 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351
  1. <template>
  2. <view class="page">
  3. <view class="nav-bar" :style="{ paddingTop: statusBarHeight}">
  4. <view class="nav-back" @tap="goBack">
  5. <!-- <text class="back-text">‹ 返回</text> -->
  6. <image class="back-text" src="@/static/images/pages_watch/icons/back_arrow_icon24.png"></image>
  7. </view>
  8. <view class="nav-title">FitCloud智能手表</view>
  9. <image class="next-text" src="@/static/images/pages_watch/icons/more_icon24.png"></image>
  10. </view>
  11. <view class="content">
  12. <view class="section-header">当前设备</view>
  13. <view class="status-card connected">
  14. <image class="deviceimg" src="/static/images/pages_watch/icons/smartwatch_img.png" mode="aspectFit">
  15. </image>
  16. <view class="x-f flex">
  17. <view class="flex">
  18. <view class="device-name">{{newXhsDeviceId}}</view>
  19. <view class="status-row">
  20. <!-- <text class="status-label ">连接状态</text> -->
  21. <text class="status-value" :class="(connectionState || '').toLowerCase()">
  22. {{ connectionText}}
  23. </text>
  24. </view>
  25. </view>
  26. <button class="device-connect" :disabled="disconnecting" :loading="disconnecting" @tap="unbindDevice()">解绑</button>
  27. <button class="device-connect" @tap="connectDevice()">连接</button>
  28. </view>
  29. </view>
  30. <!-- connectionState === 'CONNECTED' -->
  31. <view class="panel" v-if="connectedDeviceMac" style="margin-top: 24rpx;">
  32. <view class="panel-title x-bc">
  33. <text>实时健康数据</text>
  34. <text style="color: #07c160;" v-show="dataSync=='success'">同步成功</text>
  35. <button class="device-item-btn" v-show="dataSync!='success'" :disabled="dataSync=== 'loading'"
  36. :loading="dataSync=== 'loading'" @click="xhsDataAdd(connectedDeviceMac,1)">
  37. {{ dataSync=== 'loading' ? '同步中' : dataSync === 'error'? '重新同步': '同步成功'}}
  38. </button>
  39. </view>
  40. <view class="health-monitoring">
  41. <view class="health-monitoring-item">
  42. <view class="health-monitoring-title">
  43. <view>
  44. <view class="health-monitoring-maintitle">心率</view>
  45. <view>心脏健康管理</view>
  46. </view>
  47. <image src="/static/images/pages_watch/icons/heart_rate_icon.png" mode="aspectFill"></image>
  48. </view>
  49. <view class="health-monitoring-res resnum">{{ healthData.heartRate}}</view>
  50. </view>
  51. <view class="health-monitoring-item">
  52. <view class="health-monitoring-title">
  53. <view>
  54. <view class="health-monitoring-maintitle">血氧</view>
  55. <view>血氧风险管控</view>
  56. </view>
  57. <image src="/static/images/pages_watch/icons/blood_oxygen_icon.png" mode="aspectFill">
  58. </image>
  59. </view>
  60. <view class="health-monitoring-res resnum">{{ healthData.oxygen}}</view>
  61. </view>
  62. <view class="health-monitoring-item">
  63. <view class="health-monitoring-title">
  64. <view>
  65. <view class="health-monitoring-maintitle">血压</view>
  66. <view>血压健康监测</view>
  67. </view>
  68. <image src="/static/images/pages_watch/icons/blood_pressure_icon.png" mode="aspectFill"></image>
  69. </view>
  70. <view class="health-monitoring-res resnum">
  71. {{ healthData.systolicPressure }}/{{ healthData.diastolicPressure }}</view>
  72. </view>
  73. </view>
  74. <template v-if="connectionState == 'CONNECTED'">
  75. <view class="btn-row">
  76. <button class="btn btn-primary" @click="startRealTimeData" :disabled="isMeasuring">开始测量</button>
  77. <button class="btn btn-danger" @click="stopRealTimeData" :disabled="!isMeasuring">停止测量</button>
  78. </view>
  79. <!-- <view class="btn-row">
  80. <button class="btn btn-default" @click="disconnect">断开连接</button>
  81. </view> -->
  82. </template>
  83. </view>
  84. <!-- connectionState === 'CONNECTED' v-if="connectedDeviceMac"-->
  85. <view class="panel es-mt-20" v-if="connectedDeviceMac">
  86. <view class="panel-title x-bc">
  87. <text>其他数据</text>
  88. <text style="color: #07c160;" v-show="dataOtherSync=='success'&&!isSyncing">同步成功</text>
  89. <button class="device-item-btn" v-show="dataOtherSync!='success'" :disabled="dataOtherSync=== 'loading'"
  90. :loading="dataOtherSync=== 'loading'" @click="syncData()">
  91. {{ dataOtherSync=== 'loading' ? '同步中' : dataOtherSync === 'error'||dataOtherSync === 'errorSync'? '重新同步': '同步成功'}}
  92. </button>
  93. </view>
  94. <view class="history-section" v-if="sleepDataList && sleepDataList.length > 0">
  95. <view class="section-title">睡眠记录 (最新{{ sleepDataList.length }}条)</view>
  96. <scroll-view scroll-y class="history-list">
  97. <view class="history-item" v-for="(item, index) in sleepDataList" :key="index">
  98. <view class="history-header">
  99. <text class="date">{{ formatTime(item.timestamp) }}</text>
  100. <text class="count">{{ item.items ? item.items.length : 0 }}段</text>
  101. </view>
  102. <view class="history-detail" v-if="item.items && item.items.length > 0">
  103. <view class="detail-row" v-for="(detail, dIndex) in item.items" :key="dIndex">
  104. <text class="time">{{ formatTimeOnly(detail.startTime) }} - {{ formatTimeOnly(detail.endTime) }}</text>
  105. <text class="status" :class="'status-' + detail.status">
  106. {{ getSleepStatus(detail.status) }}
  107. </text>
  108. </view>
  109. </view>
  110. </view>
  111. </scroll-view>
  112. </view>
  113. <view class="empty-tip" v-else>暂无睡眠数据</view>
  114. <!-- 运动数据展示 -->
  115. <view class="history-section" v-if="sportDataList && sportDataList.length > 0">
  116. <view class="section-title">锻炼记录 (最新{{ sportDataList.length }}条)</view>
  117. <scroll-view scroll-y class="history-list">
  118. <view class="history-item" v-for="(item, index) in sportDataList" :key="index">
  119. <view class="history-header">
  120. <text class="date">{{ formatTime(item.timestamp) }}</text>
  121. <text class="count">类型: {{ item.sportType }}</text>
  122. </view>
  123. <view class="history-detail">
  124. <view class="detail-row">
  125. <text class="time">时长: {{ item.duration }}秒</text>
  126. <text class="status">消耗: {{ item.calories }}千卡</text>
  127. </view>
  128. <view class="detail-row">
  129. <text class="time">步数: {{ item.steps }}步</text>
  130. <text class="status">距离: {{ item.distance }}公里</text>
  131. </view>
  132. </view>
  133. </view>
  134. </scroll-view>
  135. </view>
  136. <view class="empty-tip" v-else>暂无锻炼记录</view>
  137. <!-- 计步数据展示 -->
  138. <view class="history-section" v-if="stepDataList && stepDataList.length > 0">
  139. <view class="section-title">计步记录 (最新{{ stepDataList.length }}条)</view>
  140. <scroll-view scroll-y class="history-list">
  141. <view class="history-item" v-for="(item, index) in stepDataList" :key="index">
  142. <view class="history-header">
  143. <text class="date">{{ formatTime(item.timestamp) }}</text>
  144. </view>
  145. <view class="history-detail">
  146. <view class="detail-row">
  147. <text class="time">步数: {{ item.steps }}步</text>
  148. <text class="status">消耗: {{ item.calories }}千卡</text>
  149. </view>
  150. <view class="detail-row">
  151. <text class="time">距离: {{ item.distance }}公里</text>
  152. </view>
  153. </view>
  154. </view>
  155. </scroll-view>
  156. </view>
  157. <view class="empty-tip" v-else>暂无计步记录</view>
  158. </view>
  159. </view>
  160. </view>
  161. </template>
  162. <script>
  163. import {
  164. getWatchUserInfo,
  165. editMyfamily
  166. } from "@/api/pages_watch/user.js";
  167. import {
  168. editXHSDevice,
  169. xhsDataAddList,
  170. removeXHSDevice,
  171. getLastData
  172. } from "@/api/pages_watch/device.js"
  173. // 引入原生插件
  174. const fitCloudWatch = uni.requireNativePlugin('fitCloudWatch');
  175. export default {
  176. data() {
  177. return {
  178. disconnecting: false,
  179. isScanning: false,
  180. deviceList: [],
  181. connectionState: 'DISCONNECTED', // CONNECTED, DISCONNECTED, CONNECTING
  182. connectedDeviceMac: '',
  183. isMeasuring: false,
  184. isAutoConnecting: false, // 避免收到原生旧状态干扰的标识
  185. healthData: {
  186. heartRate: 0,
  187. oxygen: 0,
  188. diastolicPressure: 0,
  189. systolicPressure: 0,
  190. respiratoryRate: 0
  191. },
  192. isSyncing: false,
  193. sleepDataList: [],
  194. sportDataList: [],
  195. stepDataList: [],
  196. statusBarHeight: uni.getSystemInfoSync().statusBarHeight + 'px',
  197. xhsDeviceList: [],
  198. fitCloudWatchUser: {},
  199. deviceType: 2, //0腕表1小护士2新表
  200. isFamily: false,
  201. selectUser: 0,
  202. boundDevice: null,
  203. dataSync: 'success',
  204. dataSyncFunInfo: "",
  205. dataOtherSync: 'success',
  206. dataOtherSyncFunInfo: "",
  207. otherDevice: [],
  208. newXhsDeviceId: ''
  209. }
  210. },
  211. computed: {
  212. connectionText() {
  213. const map = {
  214. 'CONNECTED': '已连接',
  215. 'DISCONNECTED': '未连接',
  216. 'CONNECTING': '连接中...'
  217. };
  218. return map[this.connectionState]
  219. }
  220. },
  221. watch: {
  222. connectionState(newVal) {
  223. this.updateXhsDeviceStatus();
  224. },
  225. connectedDeviceMac(newVal){
  226. this.updateXhsDeviceStatus();
  227. },
  228. },
  229. onLoad(option) {
  230. this.selectUser = Number(option.selectUser || 0)
  231. this.isFamily = option.selectUser != '0';
  232. this.newXhsDeviceId = uni.getStorageSync('fitCloudWatch_mac');
  233. this.getUser()
  234. const sys = uni.getSystemInfoSync();
  235. },
  236. onShow() {
  237. // 页面显示时注册事件和检查状态,保证从后台切回或从其他页面返回时都能自动重连
  238. this.registerGlobalEvents();
  239. if(this.newXhsDeviceId) {
  240. this.checkCurrentConnectionState();
  241. }
  242. },
  243. onHide() {
  244. // 页面隐藏时清理事件,避免多实例问题
  245. this.removeGlobalEvents();
  246. },
  247. onUnload() {
  248. // 页面卸载时停止扫描和测量,防止内存泄漏
  249. if (this.isScanning) {
  250. this.stopScan();
  251. }
  252. if (this.isMeasuring) {
  253. this.stopRealTimeData();
  254. }
  255. this.removeGlobalEvents();
  256. },
  257. methods: {
  258. goScan() {
  259. uni.navigateTo({
  260. url: '/pages_bluetooth/scanFitWatch?selectUser='+this.selectUser
  261. })
  262. },
  263. // 检查当前连接状态
  264. checkCurrentConnectionState() {
  265. if (!fitCloudWatch) return;
  266. try {
  267. fitCloudWatch.getConnectionState({}, (res) => {
  268. console.log(type,'当前连接状态:', res);
  269. if (res.state === 'CONNECTED' && res.mac) {
  270. // 原生层已经连接,直接恢复 UI 状态
  271. // 校验:当前设备是否等于连接的设备
  272. if(res.mac==this.newXhsDeviceId) {
  273. this.connectionState = 'CONNECTED';
  274. this.connectedDeviceMac = res.mac;
  275. // uni.showToast({ title: '已恢复连接状态', icon: 'none' });
  276. } else {
  277. // 尝试自动连接已绑定的设备
  278. this.disconnect()
  279. this.checkAutoConnect();
  280. }
  281. } else {
  282. // 原生层未连接,尝试自动连接已绑定的设备
  283. this.checkAutoConnect();
  284. }
  285. });
  286. } catch (e) {
  287. console.error('调用 getConnectionState 失败:', e);
  288. // 降级处理,直接尝试自动连接
  289. this.checkAutoConnect();
  290. }
  291. },
  292. updateXhsDeviceStatus() {
  293. this.xhsDeviceList.forEach((item, index) => {
  294. if (item.newXhsDeviceId === this.connectedDeviceMac) {
  295. this.$set(this.xhsDeviceList[index], 'connectionState', this.connectionState);
  296. } else {
  297. this.$set(this.xhsDeviceList[index], 'connectionState', '');
  298. }
  299. });
  300. },
  301. goBack() {
  302. uni.navigateBack({
  303. delta: 1
  304. });
  305. },
  306. // 注册插件的全局事件监听
  307. registerGlobalEvents() {
  308. // 先移除旧的监听,防止重复注册
  309. this.removeGlobalEvents();
  310. // 监听扫描到的设备
  311. plus.globalEvent.addEventListener('onFitCloudDeviceFound', this.onDeviceFound);
  312. // 监听连接状态变化
  313. plus.globalEvent.addEventListener('onFitCloudConnectionStateChanged', this.onConnectionStateChanged);
  314. // 监听连接错误
  315. plus.globalEvent.addEventListener('onFitCloudConnectionError', this.onConnectionError);
  316. // 监听实时健康数据
  317. plus.globalEvent.addEventListener('onFitCloudRealTimeData', this.onRealTimeData);
  318. // 监听实时测量错误
  319. plus.globalEvent.addEventListener('onFitCloudRealTimeDataError', this.onRealTimeDataError);
  320. // 监听睡眠数据同步
  321. plus.globalEvent.addEventListener('onFitCloudSleepData', this.onSleepData);
  322. // 监听运动数据同步
  323. plus.globalEvent.addEventListener('onFitCloudSportData', this.onSportData);
  324. // 监听计步数据同步
  325. plus.globalEvent.addEventListener('onFitCloudStepData', this.onStepData);
  326. },
  327. removeGlobalEvents() {
  328. plus.globalEvent.removeEventListener('onFitCloudDeviceFound', this.onDeviceFound);
  329. plus.globalEvent.removeEventListener('onFitCloudConnectionStateChanged', this.onConnectionStateChanged);
  330. plus.globalEvent.removeEventListener('onFitCloudConnectionError', this.onConnectionError);
  331. plus.globalEvent.removeEventListener('onFitCloudRealTimeData', this.onRealTimeData);
  332. plus.globalEvent.removeEventListener('onFitCloudRealTimeDataError', this.onRealTimeDataError);
  333. plus.globalEvent.removeEventListener('onFitCloudSleepData', this.onSleepData);
  334. plus.globalEvent.removeEventListener('onFitCloudSportData', this.onSportData);
  335. plus.globalEvent.removeEventListener('onFitCloudStepData', this.onStepData);
  336. },
  337. onDeviceFound(device) {
  338. console.log('扫描到设备:', JSON.stringify(device));
  339. },
  340. onConnectionStateChanged(res) {
  341. console.log('状态改变:', res.state);
  342. if (res.state === 'DISCONNECTED') {
  343. if (this.isAutoConnecting) {
  344. // 自动连接中收到的断开状态很可能是原生的旧状态,忽略它
  345. console.log('忽略自动连接中的断开状态事件');
  346. return;
  347. }
  348. this.connectionState = res.state;
  349. uni.showToast({ title: '已断开连接', icon: 'none' });
  350. this.isMeasuring = false;
  351. } else if (res.state === 'CONNECTED') {
  352. // 如果 res 里面带了 mac,更新一下
  353. if (res.mac==this.newXhsDeviceId) {
  354. this.connectionState = res.state;
  355. this.isAutoConnecting = false; // 连接成功,立即重置
  356. uni.showToast({ title: '连接成功', icon: 'success' });
  357. }
  358. } else {
  359. this.connectionState = res.state;
  360. }
  361. },
  362. onConnectionError(res) {
  363. console.log('连接错误:', res);
  364. // 如果是 SDK 内部在重试,可以不弹窗打扰用户
  365. if (res.isRetry) {
  366. console.log('正在重试连接...');
  367. return;
  368. }
  369. // 只有当前不在已连接状态,或者不是重试时才提示
  370. if (this.connectionState !== 'CONNECTED') {
  371. uni.showToast({
  372. title: '连接异常: ' + (res.error || '未知错误'),
  373. icon: 'none',
  374. duration: 3000
  375. });
  376. this.connectionState = 'DISCONNECTED';
  377. this.isAutoConnecting = false;
  378. }
  379. },
  380. onRealTimeData(data) {
  381. this.healthData = {
  382. ...this.healthData,
  383. ...data
  384. };
  385. },
  386. onRealTimeDataError(error) {
  387. uni.showToast({ title: '测量异常: ' + error.error, icon: 'none' });
  388. this.isMeasuring = false;
  389. },
  390. onSleepData(res) {
  391. console.log('接收睡眠数据:', res);
  392. if (res.sleepData && res.sleepData.length > 0) {
  393. this.sleepDataList = res.sleepData;
  394. this.xhsOtherDataAdd(this.connectedDeviceMac)
  395. }
  396. },
  397. onSportData(res) {
  398. console.log('接收到运动(锻炼)数据:', res);
  399. if (res.sportData && res.sportData.length > 0) {
  400. this.sportDataList = res.sportData;
  401. this.xhsOtherDataAdd(this.connectedDeviceMac)
  402. }
  403. },
  404. onStepData(res) {
  405. console.log('接收到计步数据:', res);
  406. if (res.stepData && res.stepData.length > 0) {
  407. this.stepDataList = res.stepData;
  408. this.xhsOtherDataAdd(this.connectedDeviceMac)
  409. }
  410. },
  411. // 自动连接
  412. checkAutoConnect() {
  413. if (!fitCloudWatch) return;
  414. const savedMac = this.newXhsDeviceId;
  415. if (savedMac) {
  416. this.connectionState = 'CONNECTING';
  417. this.connectedDeviceMac = savedMac;
  418. // 标记正在自动连接中,避免被原生的初始状态事件(DISCONNECTED)干扰
  419. this.isAutoConnecting = true;
  420. uni.showLoading({ title: '自动连接中...' });
  421. const userId = this.fitCloudWatchUser.userId + '_' + savedMac
  422. fitCloudWatch.connect({
  423. mac: savedMac,
  424. userId: userId, // 测试用户ID
  425. isBind: true, // 测试绑定模式
  426. sex: this.fitCloudWatchUser.sex || true, // 男
  427. age: this.fitCloudWatchUser.age || 25,
  428. height: this.fitCloudWatchUser.height || 175,
  429. weight: this.fitCloudWatchUser.weight || 70
  430. }, (res) => {
  431. uni.hideLoading();
  432. if (res.code !== 0) {
  433. this.connectionState = 'DISCONNECTED';
  434. this.isAutoConnecting = false;
  435. uni.showToast({ title: '自动连接指令发送失败', icon: 'none' });
  436. } else {
  437. // 连接指令发送成功,重置标记,让状态监听正常接管
  438. setTimeout(() => {
  439. this.isAutoConnecting = false;
  440. }, 1500); // 延迟 1.5 秒重置,跳过旧状态的干扰
  441. }
  442. });
  443. }
  444. },
  445. // 1. 开始扫描
  446. async startScan() {
  447. // if (!fitCloudWatch) {
  448. // uni.showToast({ title: '原生插件未加载,请在自定义基座或原生App中运行', icon: 'none' });
  449. // return;
  450. // }
  451. // // Android 平台必须动态申请定位和蓝牙权限
  452. // if (plus.os.name === 'Android') {
  453. // const permissions = [
  454. // 'android.permission.ACCESS_FINE_LOCATION',
  455. // 'android.permission.ACCESS_COARSE_LOCATION'
  456. // ];
  457. // // 适配 Android 12+ 蓝牙权限
  458. // if (parseInt(plus.os.version) >= 12) {
  459. // permissions.push('android.permission.BLUETOOTH_SCAN');
  460. // permissions.push('android.permission.BLUETOOTH_CONNECT');
  461. // }
  462. // for (let i = 0; i < permissions.length; i++) {
  463. // const result = await new Promise(resolve => {
  464. // plus.android.requestPermissions([permissions[i]], (res) => {
  465. // resolve(res.granted.length > 0);
  466. // }, (e) => {
  467. // resolve(false);
  468. // });
  469. // });
  470. // if (!result) {
  471. // uni.showToast({ title: '需要定位和蓝牙权限才能扫描设备', icon: 'none' });
  472. // return;
  473. // }
  474. // }
  475. // // 检查蓝牙是否开启
  476. // const main = plus.android.runtimeMainActivity();
  477. // const BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
  478. // const BAdapter = BluetoothAdapter.getDefaultAdapter();
  479. // if(!BAdapter.isEnabled()){
  480. // uni.showModal({
  481. // title: '提示',
  482. // content: '请先打开蓝牙',
  483. // showCancel: false
  484. // });
  485. // return;
  486. // }
  487. // }
  488. // this.deviceList = [];
  489. // this.isScanning = true;
  490. // // 将保存的 MAC 传给原生,让原生直接过滤
  491. // const savedMac = uni.getStorageSync('fitCloudWatch_mac');
  492. // fitCloudWatch.startScan({ mac: savedMac || '' }, (res) => {
  493. // if (res.code !== 0) {
  494. // this.isScanning = false;
  495. // uni.showToast({ title: '启动扫描失败: ' + res.message, icon: 'none' });
  496. // }
  497. // });
  498. },
  499. // 2. 停止扫描
  500. stopScan() {
  501. if (!fitCloudWatch) return;
  502. fitCloudWatch.stopScan({}, (res) => {
  503. this.isScanning = false;
  504. });
  505. },
  506. // 3. 连接设备
  507. connectDevice() {
  508. if(!this.newXhsDeviceId) return
  509. // 连接状态检测
  510. if (this.connectionState === 'CONNECTED') {
  511. if (this.connectedDeviceMac === this.newXhsDeviceId) {
  512. uni.showToast({ title: '该设备已连接,无需重复连接', icon: 'none' });
  513. return;
  514. } else {
  515. uni.showToast({ title: '请先断开当前设备后再连接新设备', icon: 'none' });
  516. return;
  517. }
  518. }
  519. if (this.isScanning) {
  520. this.stopScan();
  521. }
  522. this.connectionState = 'CONNECTING';
  523. this.connectedDeviceMac = this.newXhsDeviceId;
  524. // 判断 xhsDeviceList 中是否已存在当前设备
  525. const existIndex = this.xhsDeviceList.findIndex(item => item.newXhsDeviceId === this.newXhsDeviceId);
  526. if (existIndex !== -1) {
  527. // 已存在 → 更新连接状态
  528. this.$set(this.xhsDeviceList[existIndex], 'connectionState', 'CONNECTING');
  529. }
  530. uni.showLoading({
  531. title: '连接中...'
  532. });
  533. const userId = this.fitCloudWatchUser.userId + '_' + this.newXhsDeviceId
  534. fitCloudWatch.connect({
  535. mac: this.newXhsDeviceId,
  536. userId: userId, // 测试用户ID
  537. isBind: true, // 测试绑定模式
  538. sex: this.fitCloudWatchUser.sex || true, // 男
  539. age: this.fitCloudWatchUser.age || 25,
  540. height: this.fitCloudWatchUser.height || 175,
  541. weight: this.fitCloudWatchUser.weight || 70
  542. }, (res) => {
  543. uni.hideLoading();
  544. if (res.code !== 0) {
  545. this.connectionState = 'DISCONNECTED';
  546. uni.showToast({
  547. title: '连接指令发送失败,请检查蓝牙和定位是否打开',
  548. icon: 'none'
  549. });
  550. }
  551. });
  552. },
  553. // 4. 断开连接
  554. disconnect() {
  555. fitCloudWatch.disconnect({}, (res) => {
  556. console.log('断开指令发送', res);
  557. });
  558. },
  559. getUser() {
  560. this.fitCloudWatchUser = uni.getStorageSync("userWatchInfo") ? JSON.parse(uni.getStorageSync("userWatchInfo")) : ''
  561. if(this.fitCloudWatchUser) {
  562. if (this.isFamily) {
  563. // 安全解析家庭成员设备
  564. this.otherDevice = this.fitCloudWatchUser.otherDevice ? JSON.parse(this.fitCloudWatchUser.otherDevice) : [];
  565. // 格式化设备ID为对象数组
  566. const formattedDevices = this.otherDevice.map(item => ({
  567. ...item,
  568. newXhsDeviceId: item.newXhsDeviceId ?
  569. item.newXhsDeviceId.split(',').map(id => ({
  570. newXhsDeviceId: id,
  571. connectionState: ''
  572. })) : []
  573. }));
  574. // 安全取当前选中成员的设备(防越界)
  575. const index = this.selectUser - 1;
  576. this.loading = false
  577. this.xhsDeviceList = formattedDevices[index]?.newXhsDeviceId || [];
  578. } else {
  579. // 个人设备格式化
  580. const deviceIds = this.fitCloudWatchUser.newXhsDeviceId ? this.fitCloudWatchUser.newXhsDeviceId.split(',') : [];
  581. this.loading = false
  582. this.xhsDeviceList = deviceIds.map(id => ({
  583. newXhsDeviceId: id,
  584. connectionState: ''
  585. }));
  586. }
  587. // 2. 设备绑定 & 自动连接逻辑(统一处理,无重复代码)
  588. if (!this.xhsDeviceList || this.xhsDeviceList.length === 0) {
  589. // 无设备:清空绑定缓存
  590. uni.removeStorageSync('fitCloudWatch_mac');
  591. this.boundDevice = null;
  592. } else {
  593. this.newXhsDeviceId = uni.getStorageSync('fitCloudWatch_mac');
  594. // 自动连接 & 获取数据
  595. this.checkCurrentConnectionState()
  596. }
  597. }
  598. },
  599. // 解绑设备
  600. unbindDevice() {
  601. uni.showModal({
  602. title: '解绑确认',
  603. content: '确定要解绑当前手表吗?',
  604. success: (res) => {
  605. if (res.confirm) {
  606. // 统一删除设备方法
  607. const removeDevice = () => {
  608. this.isFamily
  609. ? this.removeXHSDevicefamily(this.newXhsDeviceId)
  610. : this.removeXHSDevice(this.newXhsDeviceId);
  611. };
  612. try {
  613. fitCloudWatch.unbind({}, (unbindRes) => {
  614. // 无论解绑成功/失败,都执行删除
  615. if (unbindRes.code === 0) {
  616. this.disconnect();
  617. }
  618. removeDevice();
  619. });
  620. } catch (error) {
  621. // 出错了也强制删除设备
  622. removeDevice();
  623. console.error('解绑异常:', error);
  624. }
  625. }
  626. }
  627. });
  628. },
  629. // 5. 开始测量实时数据
  630. startRealTimeData() {
  631. this.isMeasuring = true;
  632. // 重置数据展示
  633. this.healthData = {
  634. heartRate: 0,
  635. oxygen: 0,
  636. diastolicPressure: 0,
  637. systolicPressure: 0,
  638. respiratoryRate: 0
  639. };
  640. fitCloudWatch.startRealTimeData({
  641. heartRate: true,
  642. oxygen: true,
  643. bloodPressure: true,
  644. respiratoryRate: true
  645. }, (res) => {
  646. if (res.code !== 0) {
  647. this.isMeasuring = false;
  648. uni.showToast({
  649. title: '开启测量失败',
  650. icon: 'none'
  651. });
  652. }
  653. });
  654. },
  655. // 6. 停止测量实时数据
  656. stopRealTimeData() {
  657. this.xhsDataAdd(this.connectedDeviceMac)
  658. fitCloudWatch.stopRealTimeData({}, (res) => {
  659. this.isMeasuring = false;
  660. });
  661. },
  662. xhsDataAdd(deviceId, type) {
  663. // 无设备ID直接返回
  664. if (!deviceId) return;
  665. // 条件一致时不重新构造数据
  666. if (type === 1 && this.dataSyncFunInfo?.[0]?.deviceId === deviceId) {
  667. // 不处理,直接走接口
  668. } else {
  669. // 解构防止 healthData 不存在报错
  670. const { systolicPressure, diastolicPressure, heartRate, oxygen } = this.healthData || {};
  671. // 构造参数(0:血压 1:血糖 2:心率 3尿酸 4血氧 5步数 6运动)
  672. this.dataSyncFunInfo = [{
  673. recordType: 0, // 血压
  674. recordValue: JSON.stringify({
  675. sdb: systolicPressure,
  676. dbp: diastolicPressure
  677. }),
  678. deviceId: deviceId,
  679. deviceType: this.deviceType
  680. }, {
  681. recordType: 2, // 心率
  682. recordValue: heartRate,
  683. deviceId: deviceId,
  684. deviceType: this.deviceType
  685. }, {
  686. recordType: 4, // 血氧
  687. recordValue: oxygen,
  688. deviceId: deviceId,
  689. deviceType: this.deviceType
  690. }];
  691. }
  692. this.dataSync = 'loading'
  693. xhsDataAddList(this.dataSyncFunInfo).then(res => {
  694. if (res.code == 200) {
  695. this.dataSync = 'success'
  696. this.dataSyncFunInfo = []
  697. } else {
  698. this.dataSync = 'error'
  699. uni.showToast({
  700. title: '数据同步失败' + JSON.stringify(res.msg),
  701. icon: 'none'
  702. })
  703. }
  704. }).catch(() => {
  705. this.dataSync = 'error'
  706. uni.showToast({
  707. title: '数据同步失败',
  708. icon: 'none'
  709. })
  710. })
  711. },
  712. // 同步睡眠、步数,运动
  713. xhsOtherDataAdd(deviceId, type) {
  714. // 无设备ID直接返回
  715. if (!deviceId) return;
  716. // 条件一致时不重新构造数据
  717. if (type === 1 && this.dataOtherSyncFunInfo?.[0]?.deviceId === deviceId) {
  718. // 不处理,直接走接口
  719. } else {
  720. // 构造参数(0:血压 1:血糖 2:心率 3尿酸 4血氧 5步数 6运动 7睡眠)
  721. this.dataOtherSyncFunInfo = [{
  722. recordType: 5, // 步数
  723. recordValue: JSON.stringify(this.stepDataList),
  724. deviceId: deviceId,
  725. deviceType: this.deviceType
  726. }, {
  727. recordType: 6, // 运动
  728. recordValue: JSON.stringify(this.sportDataList),
  729. deviceId: deviceId,
  730. deviceType: this.deviceType
  731. }, {
  732. recordType: 7, // 睡眠
  733. recordValue: JSON.stringify(this.sleepDataList),
  734. deviceId: deviceId,
  735. deviceType: this.deviceType
  736. }];
  737. }
  738. this.dataOtherSync = 'loading'
  739. xhsDataAddList(this.dataOtherSyncFunInfo).then(res => {
  740. if (res.code == 200) {
  741. this.dataOtherSync = 'success'
  742. this.dataOtherSyncFunInfo = []
  743. } else {
  744. this.dataOtherSync = 'error'
  745. uni.showToast({
  746. title: '数据同步失败' + JSON.stringify(res.msg),
  747. icon: 'none'
  748. })
  749. }
  750. }).catch(() => {
  751. this.dataOtherSync = 'error'
  752. uni.showToast({
  753. title: '数据同步失败',
  754. icon: 'none'
  755. })
  756. })
  757. },
  758. // 7. 同步历史数据 (包含睡眠、运动、计步等)
  759. syncData() {
  760. if(this.dataOtherSync == 'error'){
  761. this.xhsOtherDataAdd(this.connectedDeviceMac, 1)
  762. return
  763. }
  764. console.log("同步历史数据this.isSyncing",this.isSyncing)
  765. if (this.isSyncing) return;
  766. this.isSyncing = true;
  767. // 清理旧数据
  768. this.sleepDataList = [];
  769. this.sportDataList = [];
  770. this.stepDataList = [];
  771. uni.showLoading({ title: '同步中...' });
  772. this.dataOtherSync = 'loading'
  773. try {
  774. fitCloudWatch.syncData({}, (res) => {
  775. this.isSyncing = false;
  776. uni.hideLoading();
  777. if (res.code === 0) {
  778. uni.showToast({ title: '同步指令已发送', icon: 'success' });
  779. } else {
  780. this.dataOtherSync = 'errorSync'
  781. uni.showToast({ title: '同步失败: ' + (res.message || '未知错误'), icon: 'none' });
  782. }
  783. });
  784. } catch (err) {
  785. console.error("同步失败:", err);
  786. this.isSyncing = false;
  787. this.dataOtherSync = 'errorSync';
  788. uni.hideLoading();
  789. uni.showToast({ title: "同步异常,请重试", icon: "none" });
  790. }
  791. },
  792. formatTime(timestamp) {
  793. if (!timestamp) return '--';
  794. const date = new Date(timestamp);
  795. const y = date.getFullYear();
  796. const m = (date.getMonth() + 1).toString().padStart(2, '0');
  797. const d = date.getDate().toString().padStart(2, '0');
  798. return `${y}-${m}-${d}`;
  799. },
  800. // 辅助方法:状态转换
  801. getSleepStatus(status) {
  802. const statusMap = {
  803. 1: '深睡',
  804. 2: '浅睡',
  805. 3: '清醒'
  806. };
  807. return statusMap[status] || '未知';
  808. },
  809. formatTimeOnly(timestamp) {
  810. if (!timestamp) return '';
  811. const date = new Date(timestamp);
  812. const h = String(date.getHours()).padStart(2, '0');
  813. const m = String(date.getMinutes()).padStart(2, '0');
  814. return `${h}:${m}`;
  815. },
  816. // 小护士解除绑定设备(自己)
  817. removeXHSDevice(newXhsDeviceId) {
  818. uni.showLoading({
  819. title: '解绑中'
  820. })
  821. removeXHSDevice({
  822. newXhsDeviceId: newXhsDeviceId,
  823. deviceType: this.deviceType
  824. }).then(async res => {
  825. uni.hideLoading()
  826. if (res.code == 200) {
  827. this.xhsDeviceList = this.xhsDeviceList.filter(item => item.newXhsDeviceId != newXhsDeviceId)
  828. this.connectionState = ''
  829. this.connectedDeviceMac = ''
  830. this.newXhsDeviceId = ''
  831. uni.removeStorageSync('fitCloudWatch_mac');
  832. uni.showToast({
  833. title: '解绑成功',
  834. icon: 'success'
  835. });
  836. } else {
  837. uni.showToast({
  838. title: res.msg,
  839. icon: 'none'
  840. })
  841. }
  842. }).catch(err => {
  843. uni.hideLoading()
  844. uni.showToast({
  845. title: '设备解绑失败',
  846. icon: 'none'
  847. })
  848. })
  849. },
  850. removeXHSDevicefamily(deviceId) {
  851. let newXhsDeviceId = this.xhsDeviceList.map(item => item.newXhsDeviceId);
  852. newXhsDeviceId = newXhsDeviceId.filter(item => item != deviceId);
  853. this.otherDevice[this.selectUser - 1].newXhsDeviceId = newXhsDeviceId.join(',')
  854. uni.showLoading({
  855. title: '解绑中'
  856. })
  857. editMyfamily({
  858. otherDevice: JSON.stringify(this.otherDevice)
  859. }).then(async res => {
  860. uni.hideLoading()
  861. if (res.code == 200) {
  862. this.xhsDeviceList = this.xhsDeviceList.filter(item => item.newXhsDeviceId != deviceId)
  863. this.connectionState = ''
  864. this.connectedDeviceMac = ''
  865. this.newXhsDeviceId = ''
  866. uni.removeStorageSync('fitCloudWatch_mac');
  867. uni.showToast({
  868. title: '解绑成功',
  869. icon: 'success'
  870. });
  871. } else {
  872. uni.showToast({
  873. title: res.msg,
  874. icon: 'none'
  875. })
  876. // if (this.connected) {
  877. // await this.disconnect();
  878. // }
  879. uni.removeStorageSync('fitCloudWatch_mac');
  880. }
  881. }).catch(async err => {
  882. uni.hideLoading()
  883. uni.showToast({
  884. title: '设备解绑失败',
  885. icon: 'none'
  886. })
  887. });
  888. },
  889. // 获取接口数据
  890. getLastData(selectedDeviceId) {
  891. const param = {
  892. deviceId: selectedDeviceId,
  893. deviceType: this.deviceType
  894. }
  895. getLastData(param).then(res => {
  896. if (res.code == 200) {
  897. // // 0:血压 1:血糖 2:心率 3尿酸 4血氧
  898. // // 状态标记
  899. // this.dataSync = 'success';
  900. // 安全获取数据列表
  901. const {
  902. data = []
  903. } = res;
  904. // 一次性提取所有需要的数据(只遍历一次数组,性能更好)
  905. const bloodPressure = data.find(item => item.recordType === 0);
  906. const pulse = data.find(item => item.recordType === 2);
  907. const oxygen = data.find(item => item.recordType === 4);
  908. const glucose = data.find(item => item.recordType === 1);
  909. const uricAcid = data.find(item => item.recordType === 3);
  910. // 血压数据解析(安全处理)
  911. let bpData = {};
  912. try {
  913. bpData = bloodPressure?.recordValue ? JSON.parse(bloodPressure.recordValue) : {};
  914. } catch (e) {
  915. bpData = {};
  916. }
  917. // 统一赋值(带默认值,防止 undefined 报错)
  918. this.cardGlucose = {
  919. value: glucose?.recordValue || '',
  920. unit: 'mmol/L',
  921. time: glucose?.createTime || ''
  922. };
  923. this.healthData = {
  924. heartRate: pulse?.recordValue,
  925. oxygen: oxygen?.recordValue,
  926. diastolicPressure: bpData.dbp,
  927. systolicPressure: bpData.sdb,
  928. respiratoryRate: null
  929. }
  930. } else {
  931. uni.showToast({
  932. title: res.msg,
  933. icon: 'none'
  934. })
  935. }
  936. })
  937. },
  938. }
  939. }
  940. </script>
  941. <style lang="scss" scoped>
  942. .page {
  943. min-height: 100vh;
  944. background: #ffffff;
  945. }
  946. .deviceimg {
  947. width: 120rpx;
  948. height: 120rpx;
  949. margin-right: 24rpx;
  950. }
  951. .nav-bar {
  952. position: sticky;
  953. top: 0;
  954. z-index: 10;
  955. display: flex;
  956. align-items: center;
  957. justify-content: space-between;
  958. margin: 0 24rpx;
  959. background: #fff;
  960. }
  961. .back-text {
  962. width: 64rpx;
  963. height: 64rpx;
  964. }
  965. .next-text {
  966. width: 48rpx;
  967. height: 48rpx;
  968. }
  969. .nav-back {
  970. width: 84rpx;
  971. height: 64rpx;
  972. display: flex;
  973. align-items: center;
  974. justify-content: flex-start;
  975. }
  976. .back-text {
  977. font-size: 30rpx;
  978. color: #ff5c03;
  979. }
  980. .nav-title {
  981. font-size: 34rpx;
  982. font-weight: 600;
  983. color: #333;
  984. }
  985. .content {
  986. padding: 32rpx 24rpx 48rpx;
  987. }
  988. .section-header {
  989. font-size: 32rpx;
  990. font-weight: 700;
  991. color: #333;
  992. margin-bottom: 24rpx;
  993. padding-left: 8rpx;
  994. }
  995. .status-card {
  996. background: #fff;
  997. border-radius: 24rpx;
  998. padding: 32rpx;
  999. border: 2rpx solid #f0f0f0;
  1000. margin-bottom: 24rpx;
  1001. display: flex;
  1002. align-items: center;
  1003. }
  1004. .device-name {
  1005. font-size: 30rpx;
  1006. font-weight: 600;
  1007. color: #222;
  1008. margin-bottom: 16rpx;
  1009. }
  1010. .status-card.connected {
  1011. background: #fffaf7;
  1012. border-color: rgba(255, 92, 3, 0.15);
  1013. box-shadow: 0 4rpx 12rpx rgba(255, 92, 3, 0.05);
  1014. }
  1015. .status-row {
  1016. display: flex;
  1017. align-items: center;
  1018. justify-content: space-between;
  1019. }
  1020. .status-row:not(:last-child) {
  1021. margin-bottom: 12rpx;
  1022. }
  1023. .status-label {
  1024. font-size: 26rpx;
  1025. color: #999;
  1026. }
  1027. .status-value {
  1028. font-size: 28rpx;
  1029. font-weight: 600;
  1030. color: #333;
  1031. }
  1032. .status-value.connected {
  1033. color: #07c160;
  1034. }
  1035. .status-value.disconnected {
  1036. color: #999;
  1037. }
  1038. .status-value.connecting {
  1039. color: #ff9501;
  1040. }
  1041. .panel {
  1042. background: #fff;
  1043. border-radius: 24rpx;
  1044. padding: 24rpx;
  1045. margin-bottom: 24rpx;
  1046. border: 2rpx solid #f5f5f5;
  1047. }
  1048. .panel-title {
  1049. font-size: 30rpx;
  1050. font-weight: 600;
  1051. color: #333;
  1052. margin-bottom: 20rpx;
  1053. }
  1054. .health-monitoring {
  1055. display: flex;
  1056. align-items: center;
  1057. justify-content: flex-start;
  1058. flex-wrap: wrap;
  1059. margin-right: -16rpx;
  1060. &-item {
  1061. width: calc(50% - 16rpx);
  1062. min-height: 264rpx;
  1063. margin: 0 16rpx 16rpx 0;
  1064. overflow: hidden;
  1065. background: #F5f7fa;
  1066. border-radius: 16rpx 16rpx 16rpx 16rpx;
  1067. padding: 24rpx 34rpx 24rpx 24rpx;
  1068. box-sizing: border-box;
  1069. }
  1070. &-maintitle {
  1071. margin-bottom: 4rpx;
  1072. font-weight: 500;
  1073. font-size: 30rpx;
  1074. color: #333333;
  1075. }
  1076. &-title {
  1077. display: flex;
  1078. align-items: center;
  1079. justify-content: space-between;
  1080. font-weight: 400;
  1081. font-size: 24rpx;
  1082. color: #999999;
  1083. image {
  1084. width: 72rpx;
  1085. height: 72rpx;
  1086. flex-shrink: 0;
  1087. }
  1088. }
  1089. .resnum {
  1090. font-family: DIN, DIN;
  1091. font-weight: 500;
  1092. font-size: 64rpx;
  1093. }
  1094. &-res {
  1095. height: 78rpx;
  1096. margin: 20rpx 0 6rpx;
  1097. font-family: PingFang SC, PingFang SC;
  1098. font-weight: 600;
  1099. font-size: 48rpx;
  1100. color: #333333;
  1101. }
  1102. }
  1103. .btn-row {
  1104. display: flex;
  1105. gap: 16rpx;
  1106. margin-bottom: 16rpx;
  1107. }
  1108. .btn-row:last-child {
  1109. margin-bottom: 0;
  1110. }
  1111. .btn {
  1112. flex: 1;
  1113. height: 84rpx;
  1114. line-height: 84rpx;
  1115. border-radius: 42rpx;
  1116. font-size: 28rpx;
  1117. border: none;
  1118. }
  1119. .btn-primary {
  1120. background: linear-gradient(90deg, #f8551f 0%, #ff9501 100%);
  1121. color: #fff;
  1122. }
  1123. .btn-default {
  1124. background: #f5f7fa;
  1125. color: #67686f;
  1126. }
  1127. .btn-danger {
  1128. background: #e64340;
  1129. color: #fff;
  1130. }
  1131. .device-list {
  1132. margin-top: 8rpx;
  1133. background: #f5f7fa;
  1134. border-radius: 16rpx;
  1135. padding: 16rpx;
  1136. }
  1137. .list-title {
  1138. font-size: 24rpx;
  1139. color: #666;
  1140. margin-bottom: 16rpx;
  1141. }
  1142. .scroll-y {
  1143. max-height: 520rpx;
  1144. }
  1145. .device-item {
  1146. display: flex;
  1147. align-items: center;
  1148. justify-content: space-between;
  1149. padding: 18rpx 8rpx;
  1150. border-bottom: 1rpx solid #ebedf0;
  1151. }
  1152. .device-item:last-child {
  1153. border-bottom: none;
  1154. }
  1155. .device-info {
  1156. display: flex;
  1157. flex-direction: column;
  1158. }
  1159. .device-mac {
  1160. margin-top: 6rpx;
  1161. font-size: 22rpx;
  1162. color: #999;
  1163. }
  1164. .device-connectbox {
  1165. display: flex;
  1166. flex-direction: column;
  1167. align-items: center;
  1168. }
  1169. .device-connect {
  1170. font-family: PingFang SC, PingFang SC;
  1171. font-weight: 500;
  1172. font-size: 26rpx;
  1173. color: #fff;
  1174. padding: 16rpx 32rpx;
  1175. background: #FF7700;
  1176. border-radius: 26rpx 26rpx 26rpx 26rpx;
  1177. // 已连接
  1178. &.connected {
  1179. background: #07c160;
  1180. }
  1181. // 连接中
  1182. &.connecting {
  1183. background: #ff9501;
  1184. }
  1185. }
  1186. .empty-tip {
  1187. text-align: center;
  1188. font-size: 24rpx;
  1189. color: #999;
  1190. padding: 28rpx 0 18rpx;
  1191. }
  1192. .device-item-btn {
  1193. background: #07c160;
  1194. color: #fff;
  1195. padding: 16rpx 24rpx;
  1196. border-radius: 44rpx;
  1197. font-size: 28rpx;
  1198. margin: 0;
  1199. min-width: 160rpx;
  1200. }
  1201. .history-section {
  1202. margin-top: 30rpx;
  1203. background: #fff;
  1204. border-radius: 12rpx;
  1205. padding: 16rpx;
  1206. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
  1207. .section-title {
  1208. font-size: 28rpx;
  1209. font-weight: bold;
  1210. color: #333;
  1211. margin-bottom: 16rpx;
  1212. border-left: 6rpx solid #007aff;
  1213. padding-left: 12rpx;
  1214. }
  1215. .history-list {
  1216. max-height: 360rpx;
  1217. }
  1218. .history-item {
  1219. background: #f9f9f9;
  1220. border-radius: 8rpx;
  1221. padding: 16rpx;
  1222. margin-bottom: 16rpx;
  1223. }
  1224. .history-header {
  1225. display: flex;
  1226. justify-content: space-between;
  1227. align-items: center;
  1228. margin-bottom: 12rpx;
  1229. border-bottom: 2rpx dashed #eee;
  1230. padding-bottom: 8rpx;
  1231. }
  1232. .history-header .date {
  1233. font-size: 26rpx;
  1234. color: #333;
  1235. font-weight: bold;
  1236. }
  1237. .history-header .count {
  1238. font-size: 24rpx;
  1239. color: #007aff;
  1240. background: rgba(0, 122, 255, 0.1);
  1241. padding: 4rpx 12rpx;
  1242. border-radius: 20rpx;
  1243. }
  1244. .history-detail {
  1245. display: flex;
  1246. flex-direction: column;
  1247. gap: 8rpx;
  1248. }
  1249. .detail-row {
  1250. display: flex;
  1251. justify-content: space-between;
  1252. font-size: 24rpx;
  1253. color: #666;
  1254. }
  1255. .status-1 { color: #5B8FF9; } /* 深睡 */
  1256. .status-2 { color: #5AD8A6; } /* 浅睡 */
  1257. .status-3 { color: #F6BD16; } /* 清醒 */
  1258. }
  1259. </style>