deviceDetail.vue 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501
  1. <template>
  2. <!-- 设备详情 -->
  3. <view class="content">
  4. <view class="name_box">
  5. <view class="name_content">{{ devInfo.devName }}</view>
  6. </view>
  7. <view class="radar-box">
  8. <view
  9. :style="{
  10. width: `${length / 200}px`,
  11. height: `${width / 200}px`,
  12. position: 'relative',
  13. overflow: 'hidden',
  14. }"
  15. :class="[
  16. width < 25500 && length < 25000 ? 'tranStyle' : 'center',
  17. ]"
  18. >
  19. <view
  20. v-for="(item, index) in modules"
  21. :key="index"
  22. class="moduleContent"
  23. >
  24. <view
  25. :class="item.type"
  26. :style="{
  27. width: `${item.width / 2}px`,
  28. height: `${item.length / 2}px`,
  29. top: `${item.top / 2}px`,
  30. left: `${item.left / 2}px`,
  31. transform: `rotate(${item.rotate}deg)`,
  32. 'transform-origin': 'center center',
  33. }"
  34. >
  35. <image
  36. class="module-img"
  37. :src="`../../static/furnitures/${item.type}.png`"
  38. mode=""
  39. />
  40. </view>
  41. </view>
  42. <template v-if="devType == 'LNB'">
  43. <view v-for="item in targetPoints" :key="item.id">
  44. <image
  45. class="action-icon-M"
  46. :style="{
  47. position: 'absolute',
  48. transform: `translate3d(${
  49. item.displayX / 2
  50. }px, ${
  51. -item.displayY / 2
  52. }px, 0) translate(-50%, -50%)`,
  53. zIndex: 9999,
  54. transition: 'transform 1s linear',
  55. willChange: 'transform',
  56. }"
  57. :src="`../../static/${lnbAction}.png`"
  58. mode=""
  59. />
  60. </view>
  61. </template>
  62. <template v-else>
  63. <image
  64. v-if="actionName"
  65. class="action-icon-G"
  66. :style="{
  67. top: `${top / 200}px`,
  68. left: `${left / 200}px`,
  69. }"
  70. :src="`../../images/furnitures/${actionName}.png`"
  71. mode=""
  72. />
  73. </template>
  74. </view>
  75. <!-- <image
  76. class="redar-pic"
  77. src="../../static/rander.png"
  78. mode=""
  79. :style="{
  80. transform:
  81. 'translate(' +
  82. -xOffset / 100 +
  83. 'rpx,' +
  84. -yOffset / 100 +
  85. 'rpx)',
  86. }"
  87. /> -->
  88. </view>
  89. <view class="switchBox">
  90. <text class="name">呼吸灯</text>
  91. <switch
  92. :checked="statusLight == 1"
  93. @change="handleLightChange"
  94. :active-value="1"
  95. :inactive-value="0"
  96. size="24px"
  97. active-color="#07c160"
  98. inactive-color="#eeeff1"
  99. style="transform: scale(0.8)"
  100. />
  101. </view>
  102. <view class="notice-info">
  103. <view class="notice_title" @click="discrepancy">
  104. <text style="color: #95a4b3; font-size: 28rpx">{{
  105. todayDate
  106. }}</text>
  107. <image src="../../static/rightArrow.png"></image>
  108. </view>
  109. <view
  110. @click="getFrenEcharts()"
  111. class="title"
  112. v-if="
  113. devInfo.installPosition == 'Bedroom' ||
  114. (breathRate !== 0 && breathRate !== '')
  115. "
  116. >
  117. <view
  118. class="title-text"
  119. style="color: #111111; font-size: 30rpx"
  120. >当前呼吸率</view
  121. >
  122. <view
  123. style="
  124. display: flex;
  125. justify-content: center;
  126. align-items: center;
  127. "
  128. >
  129. <view
  130. class="title-btn"
  131. style="color: #111111; font-size: 30rpx"
  132. v-if="breathRate !== ''"
  133. >
  134. {{ breathRate }}次/分钟</view
  135. >
  136. <view
  137. v-else
  138. class="title-btn"
  139. style="color: #111111; font-size: 30rpx"
  140. >
  141. 暂无
  142. </view>
  143. <image
  144. src="../../static/rightArrow.png"
  145. style="margin-left: auto; width: 30rpx; height: 30rpx"
  146. ></image>
  147. </view>
  148. </view>
  149. <view class="title" v-if="devInfo.installPosition == 'Toilet'">
  150. <view class="title-text" style="color: #22dea7"
  151. >今日卫生间使用频次</view
  152. >
  153. <view class="title-btn" style="color: #22dea7"
  154. >{{
  155. freQuenceList.length == "" ? "0" : freQuenceList.length
  156. }}次</view
  157. >
  158. </view>
  159. <view class="title" v-if="devInfo.installPosition != 'Toilet'">
  160. <view class="title-text" style="color: #22dea7"
  161. >今日进出频次</view
  162. >
  163. <view class="title-btn" style="color: #22dea7"
  164. >{{
  165. freQuenceList.length == "" ? "0" : freQuenceList.length
  166. }}次</view
  167. >
  168. </view>
  169. <view
  170. class="stayDetail"
  171. v-if="freQuenceList.length > 0 && freQuenceList.length == 1"
  172. >
  173. <view v-for="item in freQuenceList" :key="item.id">
  174. <view class="stayDetail-item">
  175. <view class="stayDetail-text"
  176. >{{
  177. item.info.start_time.slice(11, 16)
  178. }}目标进入</view
  179. >
  180. <view class="stayDetail-btn"
  181. >{{
  182. item.info.end_time.slice(11, 16)
  183. }}目标离开</view
  184. >
  185. </view>
  186. </view>
  187. </view>
  188. <view
  189. class="stayDetail"
  190. v-if="freQuenceList.length > 0 && freQuenceList.length > 1"
  191. >
  192. <swiper
  193. class="auto-scroll-swiper"
  194. :indicator-dots="false"
  195. :autoplay="false"
  196. :interval="3000"
  197. :circular="true"
  198. :display-multiple-items="1"
  199. :vertical="false"
  200. :current="currentIndex"
  201. @change="onSwiperChange"
  202. >
  203. <swiper-item
  204. v-for="item in freQuenceList"
  205. :key="item.id"
  206. class="stayDetail-item"
  207. >
  208. <view class="stayDetail-text"
  209. >{{
  210. item.info.start_time.slice(11, 16)
  211. }}目标进入</view
  212. >
  213. <view class="stayDetail-btn"
  214. >{{
  215. item.info.end_time.slice(11, 16)
  216. }}目标离开</view
  217. >
  218. </swiper-item>
  219. </swiper>
  220. </view>
  221. <!-- <view class="title" v-if="devInfo.installPosition == 'Toilet'">
  222. <view class="title-text" style="color: #ff976a"
  223. >昨日卫生间使用频次</view
  224. >
  225. <view class="title-btn" style="color: #ff976a"
  226. >{{
  227. freQuenceList.length == "" ? "0" : freQuenceList.length
  228. }}次</view
  229. >
  230. </view>
  231. <view class="title" v-else>
  232. <view class="title-text" style="color: #ff976a"
  233. >昨日进出频次</view
  234. >
  235. <view class="title-btn" style="color: #ff976a"
  236. >{{
  237. freQuenceList.length == "" ? "0" : freQuenceList.length
  238. }}次</view
  239. >
  240. </view> -->
  241. </view>
  242. <view class="box" v-if="!breathShow">
  243. <view class="handle-btn">
  244. <view class="btn1" @click="shareDevice">分享</view>
  245. <view class="btn1" @click="gotoSetting">设置</view>
  246. <view class="btn2" @click="healthAlarm">守护计划</view>
  247. </view>
  248. </view>
  249. <view class="box" v-else>
  250. <view class="handle-btn">
  251. <view class="closeBreath" @click="breathShow = false"
  252. >关闭呼吸率曲线图</view
  253. >
  254. <view class="btn2" @click="saveBreath"> 保存呼吸率曲线图 </view>
  255. </view>
  256. </view>
  257. <!-- 遮罩层 -->
  258. <view class="mask" v-if="shareModel" @click="shareModel = false"></view>
  259. <!-- 弹窗内容 -->
  260. <view class="share-modal" v-if="shareModel">
  261. <view class="modal-header">
  262. <text class="cancel-btn" @click="shareModel = false">取消</text>
  263. <text class="modal-title">分享</text>
  264. <text class="confirm-btn" @click="onShareConfirm()">确认</text>
  265. </view>
  266. <view class="modal-body">
  267. <view class="info-row">
  268. <view class="label">设备序列号:</view>
  269. <view class="value">{{ devInfo.clientId }}</view>
  270. </view>
  271. <view class="info-row">
  272. <view class="label">设备名称:</view>
  273. <view class="value">{{ devInfo.devName }}</view>
  274. </view>
  275. <view class="input-row phoneInfo">
  276. <view>被分享人</view>
  277. <input
  278. class="input"
  279. placeholder="请输入手机号"
  280. type="number"
  281. v-model="sharedPhone"
  282. />
  283. </view>
  284. <view class="funChoice">
  285. <view class="funItem" @click="smChange()">
  286. <label class="simple-radio">
  287. <radio
  288. :value="messageFlag"
  289. color="#7c5345"
  290. :checked="messageFlag == true"
  291. class="hide-original"
  292. />
  293. <text>短信权限</text>
  294. </label>
  295. </view>
  296. <view class="funItem">
  297. <label class="simple-radio" @click="snChange()">
  298. <radio
  299. :value="serviceNumberFlag"
  300. color="#7c5345"
  301. :checked="serviceNumberFlag == true"
  302. class="hide-original"
  303. />
  304. <text>服务号通知</text>
  305. </label>
  306. </view>
  307. <view class="funItem">
  308. <label class="simple-radio" @click="vfChange()">
  309. <radio
  310. :value="voipFlag"
  311. color="#7c5345"
  312. :checked="voipFlag == true"
  313. class="hide-original"
  314. />
  315. <text>语音通话</text>
  316. </label>
  317. </view>
  318. </view>
  319. </view>
  320. </view>
  321. <view class="modal-mask" v-if="choiceVisible" @click="closeChoice">
  322. <view class="modal-container">
  323. <view class="modal-header">
  324. <text class="title">请选择分享方式</text>
  325. </view>
  326. <view class="modal-buttons">
  327. <!-- 手机号分析按钮 -->
  328. <button class="btn phone-btn" @click="handlePhoneAnalysis">
  329. <text>手机号分享</text>
  330. </button>
  331. <!-- 链接分享按钮 -->
  332. <button class="btn link-btn" @click="handleLinkShare">
  333. <text>链接分享</text>
  334. </button>
  335. </view>
  336. </view>
  337. </view>
  338. <!-- ECharts图标模块 -->
  339. <view v-if="breathShow" class="echartsClass">
  340. <image
  341. src="../../static/closePng.png"
  342. class="closePng"
  343. @click="breathShow = false"
  344. ></image>
  345. <l-echart
  346. id="chart-canvas"
  347. canvas-id="chart-canvas"
  348. ref="chartRef"
  349. @finished="initChart"
  350. v-if="breathShow"
  351. @click="breathShow = false"
  352. ></l-echart>
  353. </view>
  354. <alarModel v-if="showModle" ref="alarmModel" />
  355. </view>
  356. </template>
  357. <script>
  358. import * as echarts from "../../uni_modules/lime-echart/static/echarts.min";
  359. import MqttService from "../../utils/globalMqtt.js";
  360. export default {
  361. data() {
  362. return {
  363. clientId: "",
  364. width: 0, //检测区域宽度
  365. length: 0, //检测区域长度
  366. xOffset: 0,
  367. yOffset: 0,
  368. devInfo: "",
  369. actionName: "",
  370. startDate: "",
  371. endDate: "",
  372. softWare: "",
  373. statusLight: 0,
  374. currentDate: new Date().getTime(),
  375. lnbAction: "action8",
  376. wsj: false,
  377. todayWcTimes: "",
  378. stayDetail: "",
  379. todayDate: "",
  380. dev_id: "",
  381. nowTime: "",
  382. devName: "",
  383. devType: "",
  384. localPhone: uni.getStorageSync("phone"),
  385. startArr: [],
  386. freQuenceList: [],
  387. // mqtt相关
  388. currentIndex: 0,
  389. modules: [],
  390. autoPlayinterval: "",
  391. choiceVisible: "",
  392. // 手机号分享授权模块
  393. shareModel: false,
  394. alarmModel: false,
  395. sharedPhone: "",
  396. messageFlag: true,
  397. serviceNumberFlag: true,
  398. voipFlag: true,
  399. // mqtt模块
  400. targetPoints: {},
  401. inactivityTimer: null,
  402. left: 0,
  403. top: 0,
  404. clientIdProp: null,
  405. breathRate: "",
  406. breathShow: false,
  407. breathRpmList: [],
  408. option: {
  409. title: [
  410. {
  411. text: "呼吸率曲线",
  412. left: "center",
  413. top: 20,
  414. textStyle: { fontSize: 14 },
  415. },
  416. {
  417. text: "BPM: 0 次/分钟",
  418. left: "center",
  419. top: 50,
  420. textStyle: { fontSize: 12, color: "#805246" },
  421. },
  422. {
  423. text: "时间 (秒)",
  424. left: "right",
  425. top: "bottom",
  426. textStyle: { fontSize: 12, color: "#888888" },
  427. },
  428. ],
  429. grid: { left: 60, right: 20, top: 80, bottom: 50 },
  430. xAxis: {
  431. type: "category",
  432. data: Array.from({ length: 60 }, (_, i) => i + 1),
  433. boundaryGap: false,
  434. splitLine: { show: false },
  435. },
  436. yAxis: {
  437. type: "value",
  438. min: 0,
  439. max: 40,
  440. name: "呼吸率(次)",
  441. nameLocation: "end",
  442. nameGap: 20,
  443. nameTextStyle: { fontSize: 12, color: "#888888" },
  444. splitLine: { show: true },
  445. },
  446. series: [
  447. {
  448. type: "line",
  449. smooth: true,
  450. symbol: "none",
  451. lineStyle: { color: "#805246", width: 2 },
  452. data: Array(60).fill(0),
  453. },
  454. ],
  455. animation: true,
  456. animationDuration: 100,
  457. },
  458. index: 0,
  459. showModle: false,
  460. };
  461. },
  462. computed: {},
  463. methods: {
  464. getdevInfo(devId) {
  465. this.$http
  466. .get(`wap/device/queryDeviceInfoById`, {
  467. devId: devId,
  468. })
  469. .then((res) => {
  470. if (res.data.data) {
  471. this.devInfo = res.data.data;
  472. this.devType = this.devInfo.devType;
  473. this.width =
  474. Math.abs(
  475. this.devInfo.yyEnd - this.devInfo.yyStart
  476. ) * 100;
  477. this.length =
  478. Math.abs(
  479. this.devInfo.xxEnd - this.devInfo.xxStart
  480. ) * 100;
  481. this.xOffset =
  482. (this.devInfo.xxStart + this.devInfo.xxEnd) * 50;
  483. this.yOffset =
  484. -(this.devInfo.yyStart + this.devInfo.yyEnd) * 50;
  485. this.statusLight = this.devInfo.statusLight;
  486. }
  487. })
  488. .catch((err) => {});
  489. },
  490. getdevRoomInfo(devId) {
  491. this.$http
  492. .get(`wap/room/readRoom`, {
  493. devId: devId,
  494. })
  495. .then((res) => {
  496. if (res.data.data) {
  497. this.modules = res.data.data.furnitures;
  498. }
  499. });
  500. },
  501. handleLightChange(e) {
  502. let newValue = e.detail.value == true ? 1 : 0;
  503. this.$http
  504. .post(`wap/device/statusLight`, {
  505. statusLight: newValue,
  506. devId: this.devInfo.devId,
  507. })
  508. .then((res) => {
  509. if (res.data.code == 200) {
  510. uni.showToast({
  511. title: "操作成功",
  512. icon: "success",
  513. });
  514. this.statusLight = newValue;
  515. } else {
  516. wx.showToast({
  517. title: res.data.message,
  518. icon: "none",
  519. });
  520. }
  521. });
  522. },
  523. // 分享功能模块
  524. shareDevice() {
  525. this.choiceVisible = true;
  526. },
  527. onShareConfirm() {
  528. if (!this.sharedPhone) {
  529. uni.showModal({
  530. content: "请填写手机号!",
  531. showCancel: false,
  532. });
  533. return;
  534. }
  535. let reg_tel =
  536. /^(13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\d{8}$/;
  537. if (!reg_tel.test(this.sharedPhone)) {
  538. uni.showModal({
  539. content: "请填写正确手机号!",
  540. showCancel: false,
  541. });
  542. return;
  543. }
  544. let shareParam = {
  545. sharerUserId: uni.getStorageSync("userId"),
  546. devId: this.devInfo.devId,
  547. sharerPhone: uni.getStorageSync("phone"),
  548. sharedUserId: "",
  549. sharedPhone: this.sharedPhone,
  550. messageFlag: this.messageFlag == true ? 0 : 1,
  551. serviceNumberFlag: this.serviceNumberFlag == true ? 0 : 1,
  552. voipFlag: this.voipFlag == true ? 0 : 1,
  553. };
  554. this.$http
  555. .post("wap/share/deviceShare", shareParam, {
  556. header: {
  557. "Content-Type": "application/json;charset=UTF-8",
  558. },
  559. })
  560. .then((res) => {
  561. if (res.data.code == 200) {
  562. uni.showToast({
  563. title: "分享成功",
  564. icon: "success",
  565. duration: 1500,
  566. });
  567. } else {
  568. uni.showToast({
  569. title: res.data.message,
  570. icon: "none",
  571. duration: 1500,
  572. });
  573. }
  574. });
  575. this.shareModel = false;
  576. },
  577. closeChoice() {
  578. this.choiceVisible = false;
  579. },
  580. handlePhoneAnalysis() {
  581. this.choiceVisible = false;
  582. this.shareModel = true;
  583. },
  584. handleLinkShare() {
  585. this.choiceVisible = false;
  586. uni.navigateTo({
  587. url:
  588. "/pagesA/linkShare/linkShare?devInfo=" +
  589. JSON.stringify(this.devInfo),
  590. });
  591. },
  592. smChange() {
  593. this.messageFlag = !this.messageFlag;
  594. },
  595. snChange() {
  596. this.serviceNumberFlag = !this.serviceNumberFlag;
  597. },
  598. vfChange() {
  599. this.voipFlag = !this.voipFlag;
  600. },
  601. gotoSetting() {
  602. if (this.devInfo.online == 0) {
  603. uni.showToast({
  604. title: "离线设备不支持设置",
  605. icon: "none",
  606. });
  607. return;
  608. }
  609. uni.navigateTo({
  610. url:
  611. "/pagesA/deviceSetting/deviceSetting?devInfo=" +
  612. JSON.stringify(this.devInfo),
  613. });
  614. },
  615. getFrequency(clientId) {
  616. this.$http
  617. .post(`wap/stats/alarmEventsQuery`, {
  618. clientId: clientId,
  619. createTimeStart: this.$time(new Date()),
  620. createTimeEnd: this.$time(new Date()),
  621. eventType: 1,
  622. })
  623. .then((res) => {
  624. if (res.data.code == 200) {
  625. if (res.data.data.rows.length > 0) {
  626. this.freQuenceList = this.parseJsonToObjects(
  627. res.data.data.rows
  628. );
  629. } else {
  630. this.freQuenceList = [];
  631. }
  632. } else {
  633. wx.showToast({
  634. title: res.data.message,
  635. icon: "none",
  636. });
  637. }
  638. });
  639. },
  640. parseJsonToObjects(jsonArray) {
  641. return jsonArray.map((item) => {
  642. // 解析info字段的JSON字符串
  643. let infoData = {};
  644. try {
  645. infoData = JSON.parse(item.info);
  646. } catch (e) {
  647. console.error("Failed to parse info JSON:", e);
  648. }
  649. return {
  650. id: item.id,
  651. clientId: item.clientId,
  652. tenantId: item.tenantId,
  653. devName: item.devName,
  654. uuid: item.uuid,
  655. planUuid: item.planUuid,
  656. eventType: item.eventType,
  657. info: infoData,
  658. isHandle: item.isHandle,
  659. createTime: item.createTime,
  660. remark: item.remark,
  661. };
  662. });
  663. },
  664. // 健康闹钟方法
  665. healthAlarm() {
  666. uni.navigateTo({
  667. url:
  668. "/pagesA/healthAlarm/healthAlarm?devInfo=" +
  669. JSON.stringify(this.devInfo),
  670. });
  671. },
  672. onSwiperChange(event) {
  673. const current = event.detail.current;
  674. const totalItems = this.freQuenceList.length;
  675. if (current === totalItems - 1) {
  676. this.autoplay = false;
  677. }
  678. this.currentIndex = current;
  679. },
  680. autoSwipe() {
  681. if (this.freQuenceList && this.freQuenceList.length > 0) {
  682. setInterval(() => {
  683. let nextIndex = this.currentIndex + 1;
  684. if (nextIndex >= this.freQuenceList.length) {
  685. nextIndex = 0; // 循环到第一个项目
  686. }
  687. this.currentIndex = nextIndex;
  688. }, 3000); // 每3秒自动滚动一次
  689. }
  690. },
  691. discrepancy() {
  692. uni.navigateTo({
  693. url:
  694. "/pagesA/discrepancy/discrepancy?freQuenceList=" +
  695. JSON.stringify(this.freQuenceList),
  696. });
  697. },
  698. getCurrentDate() {
  699. const now = new Date();
  700. this.currentDate = `${now.getFullYear()}-${
  701. now.getMonth() + 1
  702. }月${now.getDate().toString().padStart(2, "0")}日`;
  703. },
  704. receptionChange(val) {
  705. this.targetPoints = val;
  706. },
  707. receptHealth(val) {
  708. this.breathRate = val;
  709. this.setEcharts(val);
  710. },
  711. // echarts图表模块
  712. getOption(list) {
  713. // 固定 X 轴 [0 ~ 60]
  714. const xData = Array.from({ length: 61 }, (_, i) => i);
  715. const recent = list.slice(-61);
  716. const data = new Array(61).fill(null);
  717. for (let i = 0; i < recent.length; i++) {
  718. data[i] = recent[i];
  719. }
  720. return {
  721. title: [
  722. { text: "呼吸率曲线", left: "center", top: 20 },
  723. {
  724. text: `BPM: ${recent[recent.length - 1] || 0} 次/分钟`,
  725. left: "center",
  726. top: 50,
  727. textStyle: { fontSize: 12, color: "#805246" },
  728. },
  729. {
  730. text: "时间 (秒)",
  731. left: "right",
  732. top: "bottom",
  733. textStyle: {
  734. fontSize: 10,
  735. color: "#888888",
  736. fontWeight: "normal",
  737. },
  738. },
  739. ],
  740. grid: { left: 60, right: 20, top: 80, bottom: 50 },
  741. xAxis: {
  742. type: "category",
  743. data: xData,
  744. boundaryGap: false,
  745. },
  746. yAxis: {
  747. type: "value",
  748. min: 0,
  749. max: 40,
  750. name: "呼吸率(次)",
  751. nameLocation: "end",
  752. nameGap: 20,
  753. nameTextStyle: {
  754. fontSize: 10,
  755. color: "#888888",
  756. fontWeight: "normal",
  757. },
  758. splitLine: { show: true },
  759. },
  760. series: [
  761. {
  762. type: "line",
  763. smooth: true,
  764. symbol: "none",
  765. data: data,
  766. showSymbol: false,
  767. lineStyle: { color: "#7a4e42", width: 2 },
  768. },
  769. ],
  770. };
  771. },
  772. getFrenEcharts() {
  773. if (this.breathRate === "" || this.breathRate === null) {
  774. uni.showToast({
  775. title: "暂无呼吸率",
  776. icon: "none",
  777. duration: 1500,
  778. });
  779. return;
  780. }
  781. this.breathShow = true;
  782. this.$nextTick(() => {
  783. this.initChart();
  784. });
  785. },
  786. initChart() {
  787. if (!this.$refs.chartRef) return;
  788. if (this.chartInstance) {
  789. this.chartInstance.dispose();
  790. this.chartInstance = null;
  791. }
  792. this.$refs.chartRef.init(echarts, (chart) => {
  793. this.chartInstance = chart;
  794. chart.setOption(this.option, true);
  795. });
  796. },
  797. setEcharts(val) {
  798. if (!Array.isArray(this.breathRpmList)) {
  799. this.breathRpmList = [];
  800. }
  801. this.breathRpmList.push(val);
  802. if (!this.chartInstance) return;
  803. const option = this.getOption(this.breathRpmList);
  804. this.chartInstance.setOption(option, { notMerge: true });
  805. },
  806. saveBreath() {
  807. const chart = this.$refs.chartRef;
  808. if (!chart) {
  809. uni.showToast({ title: "图表未渲染", icon: "none" });
  810. return;
  811. }
  812. chart.canvasToTempFilePath({
  813. success: (res) => {
  814. uni.saveImageToPhotosAlbum({
  815. filePath: res.tempFilePath,
  816. success: () => {
  817. uni.showToast({
  818. title: "保存成功",
  819. icon: "success",
  820. });
  821. },
  822. fail: (err) => {
  823. uni.showToast({
  824. title: "保存失败",
  825. icon: "none",
  826. });
  827. },
  828. });
  829. },
  830. fail: (err) => {
  831. uni.showToast({
  832. title: "导出失败",
  833. icon: "none",
  834. });
  835. },
  836. });
  837. },
  838. handleMessage(topic, message, clientId) {
  839. // 清除不活动定时器
  840. clearTimeout(this.inactivityTimer);
  841. this.inactivityTimer = setTimeout(() => {
  842. this.targetPoints = {};
  843. console.log("长时间没有点位消除");
  844. }, 1500);
  845. // 验证topic格式
  846. const match = topic.match(/^\/dev\/(.+)\/tracker_targets$/);
  847. if (!match || match[1] !== clientId) return;
  848. try {
  849. const data = JSON.parse(message.toString());
  850. if (data.health) {
  851. if (
  852. data.health.breath_rpm ||
  853. data.health.breath_rpm === 0
  854. ) {
  855. this.receptHealth(Math.floor(data.health.breath_rpm));
  856. } else {
  857. }
  858. }
  859. this.processTrackerData(data.tracker_targets);
  860. // console.log(data.tracker_targets, "MQTT消息解析成功22222");
  861. } catch (e) {
  862. console.error("MQTT消息解析失败", e);
  863. }
  864. },
  865. processTrackerData(arr) {
  866. if (Array.isArray(arr) && arr.length > 0 && Array.isArray(arr[0])) {
  867. this.targetPoints = {};
  868. const currentIds = new Set();
  869. const newTargetPoints = {};
  870. // 处理每个追踪目标
  871. arr.forEach((item) => {
  872. if (!Array.isArray(item) || item.length < 4) return;
  873. const [x, y, z, id] = item;
  874. currentIds.add(id.toString());
  875. // 处理新点或更新现有点
  876. if (!this.targetPoints[id]) {
  877. newTargetPoints[id] = this.createNewTargetPoint(
  878. x,
  879. y,
  880. z,
  881. id
  882. );
  883. } else {
  884. newTargetPoints[id] = this.updateExistingTargetPoint(
  885. this.targetPoints[id],
  886. x,
  887. y,
  888. z,
  889. 2
  890. );
  891. }
  892. });
  893. // 移除不存在的点
  894. Object.keys(this.targetPoints).forEach((id) => {
  895. if (!currentIds.has(id)) {
  896. delete this.targetPoints[id];
  897. }
  898. });
  899. // 更新目标点
  900. this.targetPoints = {
  901. ...this.targetPoints,
  902. ...newTargetPoints,
  903. };
  904. if (Array.isArray(this.targetPoints)) {
  905. this.targetPoints = this.targetPoints.filter(
  906. (item) => item !== null && item !== undefined
  907. );
  908. }
  909. }
  910. },
  911. createNewTargetPoint(x, y, z, id) {
  912. console.log(
  913. this.devInfo.xxStart,
  914. this.devInfo.yyEnd,
  915. " this.devInfo.yyEnd"
  916. );
  917. return {
  918. x,
  919. y,
  920. z,
  921. id,
  922. displayX: x - Number(this.devInfo.xxStart),
  923. displayY: y - Number(this.devInfo.yyEnd),
  924. lastX: x,
  925. lastY: y,
  926. };
  927. },
  928. updateExistingTargetPoint(existingPoint, x, y, z, THRESHOLD) {
  929. const dx = x - existingPoint.lastX;
  930. const dy = y - existingPoint.lastY;
  931. const distance = Math.sqrt(dx * dx + dy * dy);
  932. if (distance > THRESHOLD) {
  933. return {
  934. ...existingPoint,
  935. x,
  936. y,
  937. z,
  938. lastX: x,
  939. lastY: y,
  940. displayX: x - Number(this.devInfo.xxStart),
  941. displayY: y - Number(this.devInfo.yyEnd),
  942. };
  943. }
  944. return existingPoint;
  945. },
  946. },
  947. onShow() {
  948. this.showModle = true;
  949. this.todayDate = this.$time(new Date(), 2);
  950. },
  951. onLoad(options) {
  952. const devItem = JSON.parse(options.devItem);
  953. const { devId, clientId } = devItem;
  954. this.getFrequency(clientId);
  955. this.getdevInfo(devId);
  956. this.getdevRoomInfo(devId);
  957. this.autoPlayinterval = setTimeout(() => {
  958. this.autoSwipe();
  959. }, 3000);
  960. this.getCurrentDate();
  961. this.clientId = clientId;
  962. console.log(this.clientId, "clientIDetail");
  963. this.clientId = clientId;
  964. let userId = uni.getStorageSync("userId");
  965. MqttService.connectData(userId).then((client) => {
  966. const topic = `/dev/${this.clientId}/tracker_targets`;
  967. // 使用 MqttService.subscribe 管理订阅
  968. this.unsubscribeFn = MqttService.subscribe(
  969. "DATA",
  970. topic,
  971. (message, msgTopic) => {
  972. console.log(`接收到 ${msgTopic} 消息:`, message);
  973. // 处理消息
  974. const dataMatch = msgTopic.match(
  975. /^\/dev\/(.+)\/tracker_targets$/
  976. );
  977. const cmdMatch = msgTopic.match(/^\/mps\/wx_(.+)\/notice$/);
  978. if (dataMatch && dataMatch[1] === this.clientId) {
  979. this.handleMessage(msgTopic, message, this.clientId);
  980. } else if (cmdMatch) {
  981. this.$refs.alarmModel.hanOtherMessage(
  982. msgTopic,
  983. message
  984. );
  985. }
  986. }
  987. );
  988. if (this.unsubscribeFn) {
  989. console.log(`✅ 已成功订阅主题: ${topic}`);
  990. }
  991. });
  992. },
  993. onUnload() {
  994. this.inactivityTimer = null;
  995. this.autoPlayinterval = null;
  996. if (this.unsubscribeFn) {
  997. this.unsubscribeFn();
  998. this.unsubscribeFn = null;
  999. }
  1000. // 关闭 DATA 连接
  1001. MqttService.disconnectData();
  1002. },
  1003. onHide() {
  1004. this.showModle = false;
  1005. },
  1006. beforeDestroy() {
  1007. console.log("组件销毁时调用");
  1008. },
  1009. };
  1010. </script>
  1011. <style lang="less" scoped>
  1012. .content {
  1013. height: 100vh;
  1014. background: linear-gradient(180deg, #faede2 0%, #f4f4f4 100%);
  1015. box-sizing: border-box;
  1016. padding-top: 20rpx;
  1017. .name_box {
  1018. display: flex;
  1019. justify-content: center;
  1020. align-items: center;
  1021. margin: 0 auto 37rpx 0;
  1022. .name_content {
  1023. display: inline-flex;
  1024. background: rgba(255, 255, 255, 0.65);
  1025. border-radius: 37rpx;
  1026. padding: 6rpx 18rpx;
  1027. }
  1028. }
  1029. .radar-box {
  1030. margin: 0 auto;
  1031. position: relative;
  1032. display: flex;
  1033. align-items: center;
  1034. justify-content: center;
  1035. width: 710rpx;
  1036. height: 710rpx;
  1037. background: #ffffff;
  1038. border-radius: 37.5rpx;
  1039. box-sizing: border-box;
  1040. .center {
  1041. position: absolute;
  1042. background-color: #fff;
  1043. border: 14rpx solid #333333;
  1044. background-image: url("https://hflnxx.oss-cn-shanghai.aliyuncs.com/IMAGE/20250919/toilet_bg.png");
  1045. background-repeat: no-repeat;
  1046. background-position: center;
  1047. transform: scale(1.3);
  1048. .moduleContent {
  1049. position: relative;
  1050. view {
  1051. position: absolute;
  1052. }
  1053. .module-img {
  1054. width: 100%;
  1055. height: 100%;
  1056. display: block;
  1057. }
  1058. }
  1059. }
  1060. .tranStyle {
  1061. position: absolute;
  1062. background-color: #fff;
  1063. border: 9rpx solid #333333;
  1064. background-image: url("https://hflnxx.oss-cn-shanghai.aliyuncs.com/IMAGE/20250919/toilet_bg.png");
  1065. background-repeat: no-repeat;
  1066. background-position: center;
  1067. transform: scale(2.2);
  1068. .moduleContent {
  1069. overflow: hidden;
  1070. // position: relative;
  1071. view {
  1072. position: absolute;
  1073. }
  1074. .module-img {
  1075. width: 100%;
  1076. height: 100%;
  1077. display: block;
  1078. }
  1079. }
  1080. }
  1081. .action-icon-G {
  1082. position: absolute;
  1083. width: 100rpx;
  1084. height: 100rpx;
  1085. }
  1086. .action-icon-M {
  1087. // position: absolute;
  1088. width: 50rpx;
  1089. height: 50rpx;
  1090. }
  1091. .redar-pic {
  1092. position: absolute;
  1093. left: 50%;
  1094. top: 50%;
  1095. width: 40rpx;
  1096. height: 40rpx;
  1097. transform: translate(-50%, -50%); /* 先居中 */
  1098. }
  1099. }
  1100. .switchBox {
  1101. width: 710rpx;
  1102. height: 94rpx;
  1103. margin: 18rpx auto;
  1104. padding: 0 37rpx;
  1105. display: flex;
  1106. align-items: center;
  1107. background: #ffffff;
  1108. border-radius: 37rpx;
  1109. box-sizing: border-box;
  1110. .name {
  1111. color: #111111;
  1112. font-size: 32rpx;
  1113. }
  1114. }
  1115. .notice-info {
  1116. width: 710rpx;
  1117. max-height: 300rpx;
  1118. overflow: hidden;
  1119. padding: 30rpx 37rpx 10rpx 37rpx;
  1120. background: #ffffff;
  1121. border-radius: 37rpx;
  1122. box-sizing: border-box;
  1123. margin: 0 auto;
  1124. .notice_title {
  1125. display: flex;
  1126. align-items: space-between;
  1127. image {
  1128. margin-left: auto;
  1129. width: 30rpx;
  1130. height: 30rpx;
  1131. }
  1132. }
  1133. .title {
  1134. margin-top: 10rpx;
  1135. margin-bottom: 10rpx;
  1136. font-family: PingFang SC, PingFang SC;
  1137. font-weight: 500;
  1138. font-size: 34rpx;
  1139. color: #1757dd;
  1140. display: flex;
  1141. align-items: center;
  1142. justify-content: space-between;
  1143. .title-text {
  1144. font-size: 28rpx;
  1145. font-weight: 500;
  1146. color: #1757dd;
  1147. }
  1148. .title-btn {
  1149. font-size: 28rpx;
  1150. font-weight: 500;
  1151. color: #5a5a5a;
  1152. }
  1153. }
  1154. .stayDetail {
  1155. height: 80rpx;
  1156. overflow: hidden;
  1157. margin-top: 10rpx;
  1158. margin-bottom: 10rpx;
  1159. padding-left: 20rpx;
  1160. font-family: PingFang SC, PingFang SC;
  1161. font-weight: 500;
  1162. font-size: 34rpx;
  1163. .stayDetail-item {
  1164. display: flex;
  1165. padding-bottom: 5rpx !important;
  1166. padding-top: 10rpx !important;
  1167. box-sizing: border-box;
  1168. line-height: 60rpx;
  1169. .stayDetail-text {
  1170. width: 40%;
  1171. font-size: 28rpx;
  1172. height: 30rpx !important;
  1173. }
  1174. .stayDetail-btn {
  1175. padding-left: 30rpx;
  1176. font-size: 28rpx;
  1177. height: 30rpx !important;
  1178. }
  1179. }
  1180. }
  1181. }
  1182. .box {
  1183. position: fixed;
  1184. bottom: 0;
  1185. left: 0;
  1186. width: 100vw;
  1187. height: 168rpx;
  1188. padding: 0 37rpx;
  1189. background: #ffffff;
  1190. box-sizing: border-box;
  1191. .handle-btn {
  1192. margin-top: 40rpx;
  1193. display: flex;
  1194. align-items: center;
  1195. justify-content: space-between;
  1196. .btn1 {
  1197. display: flex;
  1198. align-items: center;
  1199. justify-content: center;
  1200. width: 155rpx;
  1201. height: 94rpx;
  1202. background: #ffebe4;
  1203. border-radius: 28rpx;
  1204. font-weight: 500;
  1205. color: #111111;
  1206. font-size: 32rpx;
  1207. text-align: center;
  1208. }
  1209. .btn2 {
  1210. display: flex;
  1211. align-items: center;
  1212. justify-content: center;
  1213. width: 328rpx;
  1214. height: 94rpx;
  1215. background: linear-gradient(
  1216. 105.95deg,
  1217. #a27867 0%,
  1218. #74483d 100%
  1219. );
  1220. border-radius: 28rpx;
  1221. box-shadow: 0rpx 4.69rpx 18.75rpx rgba(72, 41, 29, 0.15),
  1222. 0rpx 9.38rpx 9.38rpx rgba(154, 132, 89, 0.2),
  1223. 0rpx -4.69rpx 28.13rpx 4.69rpx #a16647 inset;
  1224. font-family: MiSans;
  1225. font-weight: 500;
  1226. color: #ffffff;
  1227. font-size: 32rpx;
  1228. }
  1229. .closeBreath {
  1230. display: flex;
  1231. align-items: center;
  1232. justify-content: center;
  1233. width: 328rpx;
  1234. height: 94rpx;
  1235. background: #ffebe4;
  1236. border-radius: 28rpx;
  1237. font-weight: 500;
  1238. color: #111111;
  1239. font-size: 32rpx;
  1240. text-align: center;
  1241. }
  1242. }
  1243. }
  1244. /* 弹窗部分样式 */
  1245. .mask {
  1246. position: fixed;
  1247. left: 0;
  1248. top: 0;
  1249. right: 0;
  1250. bottom: 0;
  1251. background: rgba(0, 0, 0, 0.3);
  1252. z-index: 100;
  1253. }
  1254. .share-modal {
  1255. position: fixed;
  1256. left: 0;
  1257. right: 0;
  1258. bottom: 0;
  1259. background: #fff;
  1260. border-radius: 24rpx 24rpx 0 0;
  1261. z-index: 101;
  1262. height: 750rpx;
  1263. .modal-header {
  1264. display: flex;
  1265. justify-content: space-between;
  1266. align-items: center;
  1267. padding: 32rpx 32rpx 0 32rpx;
  1268. font-size: 30rpx;
  1269. .cancel-btn {
  1270. color: #888;
  1271. font-size: 32rpx;
  1272. }
  1273. .modal-title {
  1274. color: #111111;
  1275. font-size: 36rpx;
  1276. }
  1277. .confirm-btn {
  1278. color: #996e5f;
  1279. font-size: 32rpx;
  1280. }
  1281. }
  1282. .modal-body {
  1283. padding: 50rpx;
  1284. .info-row {
  1285. display: flex;
  1286. margin-bottom: 24rpx;
  1287. font-size: 28rpx;
  1288. padding: 0 20rpx;
  1289. .label {
  1290. color: #111111;
  1291. width: 200rpx;
  1292. font-size: 32rpx;
  1293. text-align-last: justify;
  1294. }
  1295. .value {
  1296. text-align: right;
  1297. width: 65%;
  1298. color: #111111;
  1299. font-size: 32rpx;
  1300. }
  1301. }
  1302. .input-row {
  1303. display: flex;
  1304. align-items: center;
  1305. margin-top: 32rpx;
  1306. border-radius: 40rpx;
  1307. background: #f7f7f7;
  1308. padding: 0 24rpx;
  1309. height: 80rpx;
  1310. .input {
  1311. text-align: right;
  1312. flex: 1;
  1313. border: none;
  1314. background: transparent;
  1315. font-size: 28rpx;
  1316. color: #333;
  1317. }
  1318. }
  1319. .funChoice {
  1320. .funItem {
  1321. .simple-radio {
  1322. display: flex;
  1323. align-items: center;
  1324. padding: 20rpx 0;
  1325. }
  1326. }
  1327. }
  1328. }
  1329. }
  1330. // 分享弹窗样式
  1331. .modal-mask {
  1332. position: fixed;
  1333. top: 0;
  1334. left: 0;
  1335. right: 0;
  1336. bottom: 0;
  1337. background: rgba(0, 0, 0, 0.5);
  1338. display: flex;
  1339. justify-content: center;
  1340. align-items: center;
  1341. z-index: 333;
  1342. .modal-container {
  1343. width: 80%;
  1344. background: #fff;
  1345. border-radius: 16rpx;
  1346. overflow: hidden;
  1347. animation: fadeIn 0.3s;
  1348. .modal-header {
  1349. padding: 30rpx;
  1350. text-align: center;
  1351. border-bottom: 1rpx solid #f5f5f5;
  1352. .title {
  1353. font-size: 36rpx;
  1354. display: block;
  1355. margin-bottom: 10rpx;
  1356. }
  1357. }
  1358. .modal-buttons {
  1359. display: flex;
  1360. flex-direction: column;
  1361. padding: 20rpx;
  1362. .btn {
  1363. flex: 1;
  1364. height: 90rpx;
  1365. margin: 15rpx 0;
  1366. border-radius: 45rpx;
  1367. display: flex;
  1368. align-items: center;
  1369. justify-content: center;
  1370. font-size: 32rpx;
  1371. border: none;
  1372. background: none;
  1373. position: relative;
  1374. }
  1375. .btn-icon {
  1376. width: 40rpx;
  1377. height: 40rpx;
  1378. margin-right: 15rpx;
  1379. }
  1380. .phone-btn {
  1381. background: linear-gradient(
  1382. 105.95deg,
  1383. #ba978a 0%,
  1384. #a27867 100%
  1385. );
  1386. color: white;
  1387. }
  1388. .link-btn {
  1389. background: linear-gradient(
  1390. 105.95deg,
  1391. #a27867 0%,
  1392. #74483d 100%
  1393. );
  1394. color: white;
  1395. }
  1396. }
  1397. }
  1398. }
  1399. .echartsClass {
  1400. position: absolute;
  1401. width: 650rpx;
  1402. height: 600rpx;
  1403. top: -8%;
  1404. left: -37%;
  1405. transform: translate(50%, 50%);
  1406. background: #ffffff;
  1407. .closePng {
  1408. width: 40rpx;
  1409. height: 40rpx;
  1410. position: fixed;
  1411. top: 10rpx;
  1412. right: 10rpx;
  1413. z-index: 333;
  1414. }
  1415. }
  1416. }
  1417. </style>