detail-collector.vue 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180
  1. <template>
  2. <view class="container">
  3. <!-- 设备头部信息区域 -->
  4. <view class="device-header">
  5. <view class="device-info-row">
  6. <view class="device-name-container">
  7. <text class="device-name">{{ deviceInfo.name }}</text>
  8. <view
  9. class="status-tag"
  10. :class="deviceInfo.status === 'online' ? 'status-online' : 'status-offline'"
  11. >
  12. <view class="status-dot" :class="{'offline-dot': deviceInfo.status === 'offline'}"></view>
  13. {{ deviceInfo.status === 'online' ? '在线' : '离线' }}
  14. </view>
  15. </view>
  16. <view class="refresh-btn" :class="{'refreshing': isRefreshing}" @tap="refreshData">
  17. <svg width="22" height="22" viewBox="0 0 24 24">
  18. <path fill="#3BB44A" d="M17.65,6.35C16.2,4.9,14.21,4,12,4c-4.42,0-7.99,3.58-7.99,8s3.57,8,7.99,8c3.73,0,6.84-2.55,7.73-6h-2.08c-0.82,2.33-3.04,4-5.65,4c-3.31,0-6-2.69-6-6s2.69-6,6-6c1.66,0,3.14,0.69,4.22,1.78L13,11h7V4L17.65,6.35z"/>
  19. </svg>
  20. </view>
  21. </view>
  22. <view class="device-meta-row">
  23. <view class="device-meta-item">
  24. <view class="meta-icon">
  25. <svg width="16" height="16" viewBox="0 0 24 24">
  26. <path fill="#3BB44A" d="M22,3H2C0.9,3,0,3.9,0,5v14c0,1.1,0.9,2,2,2h20c1.1,0,1.99-0.9,1.99-2L24,5C24,3.9,23.1,3,22,3z M9,17H6.5v-2.5h2.5V17z M9,13H6.5v-2.5h2.5V13z M9,9H6.5V6.5h2.5V9z M13.5,17H11v-2.5h2.5V17z M13.5,13H11v-2.5h2.5V13z M13.5,9H11V6.5h2.5V9z M18,17h-2.5v-2.5H18V17z M18,13h-2.5v-2.5H18V13z M18,9h-2.5V6.5H18V9z"/>
  27. </svg>
  28. </view>
  29. <text class="meta-label">设备编号:</text>
  30. <text class="meta-value">{{ deviceInfo.deviceId }}</text>
  31. </view>
  32. <view class="device-meta-item">
  33. <view class="meta-icon">
  34. <svg width="16" height="16" viewBox="0 0 24 24">
  35. <path fill="#3BB44A" d="M12,2C8.13,2,5,5.13,5,9c0,5.25,7,13,7,13s7-7.75,7-13C19,5.13,15.87,2,12,2z M12,11.5c-1.38,0-2.5-1.12-2.5-2.5s1.12-2.5,2.5-2.5s2.5,1.12,2.5,2.5S13.38,11.5,12,11.5z"/>
  36. </svg>
  37. </view>
  38. <text class="meta-label">安装位置:</text>
  39. <text class="meta-value">{{ deviceInfo.location }}</text>
  40. </view>
  41. <view class="device-meta-item">
  42. <view class="meta-icon">
  43. <svg width="16" height="16" viewBox="0 0 24 24">
  44. <path fill="#3BB44A" d="M11.99,2C6.47,2,2,6.48,2,12s4.47,10,9.99,10C17.52,22,22,17.52,22,12S17.52,2,11.99,2z M12,20c-4.42,0-8-3.58-8-8s3.58-8,8-8s8,3.58,8,8S16.42,20,12,20z M12.5,7H11v6l5.25,3.15l0.75-1.23l-4.5-2.67V7z"/>
  45. </svg>
  46. </view>
  47. <text class="meta-label">最近更新:</text>
  48. <text class="meta-value">{{ deviceInfo.lastUpdate }}</text>
  49. </view>
  50. </view>
  51. </view>
  52. <!-- 采集数据展示区域 - 气象站 -->
  53. <view class="data-section" v-if="deviceInfo.deviceType === 'weather'">
  54. <view class="section-title">
  55. <text>气象数据</text>
  56. </view>
  57. <view class="data-grid">
  58. <!-- 气温 -->
  59. <view class="data-item">
  60. <view class="data-icon" :class="{'no-data': !weatherData.temperature}">
  61. <svg width="28" height="28" viewBox="0 0 24 24">
  62. <path fill="#3BB44A" d="M15,13V5c0-1.66-1.34-3-3-3S9,3.34,9,5v8c-1.21,0.91-2,2.37-2,4c0,2.76,2.24,5,5,5s5-2.24,5-5C17,15.37,16.21,13.91,15,13z M11,5c0-0.55,0.45-1,1-1s1,0.45,1,1v3h-2V5z M12,19.5c-1.38,0-2.5-1.12-2.5-2.5c0-0.94,0.53-1.76,1.31-2.17L11.5,14.5v-4h1v4l0.69,0.33c0.78,0.41,1.31,1.23,1.31,2.17C14.5,18.38,13.38,19.5,12,19.5z"/>
  63. </svg>
  64. </view>
  65. <text class="data-label">气温</text>
  66. <view class="data-value-container">
  67. <template v-if="weatherData.temperature">
  68. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.temperature}">{{ weatherData.temperature }}</text>
  69. <text class="data-unit">℃</text>
  70. </template>
  71. <text v-else class="data-value no-data">暂无数据</text>
  72. </view>
  73. <text class="data-time" v-if="weatherData.temperature">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  74. </view>
  75. <!-- 湿度 -->
  76. <view class="data-item">
  77. <view class="data-icon" :class="{'no-data': !weatherData.humidity}">
  78. <svg width="28" height="28" viewBox="0 0 24 24">
  79. <path fill="#3BB44A" d="M12,2c-5.33,4.55-8,8.48-8,11.8c0,4.98,3.8,8.2,8,8.2s8-3.22,8-8.2C20,10.48,17.33,6.55,12,2z M12,20c-3.35,0-6-2.57-6-6.2c0-2.34,1.95-5.44,6-9.14c4.05,3.7,6,6.79,6,9.14C18,17.43,15.35,20,12,20z M7.83,14c0.37,0,0.67,0.26,0.74,0.62c0.41,2.22,2.28,2.98,3.64,2.87c0.43-0.02,0.79,0.32,0.79,0.75c0,0.4-0.32,0.73-0.72,0.75c-2.13,0.13-4.62-1.09-5.19-4.12C7.01,14.42,7.37,14,7.83,14z"/>
  80. </svg>
  81. </view>
  82. <text class="data-label">湿度</text>
  83. <view class="data-value-container">
  84. <template v-if="weatherData.humidity">
  85. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.humidity}">{{ weatherData.humidity }}</text>
  86. <text class="data-unit">%</text>
  87. </template>
  88. <text v-else class="data-value no-data">暂无数据</text>
  89. </view>
  90. <text class="data-time" v-if="weatherData.humidity">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  91. </view>
  92. <!-- 降雨量 -->
  93. <view class="data-item">
  94. <view class="data-icon" :class="{'no-data': !weatherData.rainfall}">
  95. <svg width="28" height="28" viewBox="0 0 24 24">
  96. <path fill="#3BB44A" d="M12.01,6c2.61,0,4.89,1.86,5.4,4.43l0.3,1.5l1.52,0.11c1.56,0.11,2.78,1.41,2.78,2.96c0,1.65-1.35,3-3,3h-13c-2.21,0-4-1.79-4-4c0-2.05,1.53-3.76,3.56-3.97l1.07-0.11l0.5-0.95C8.08,7.14,9.95,6,12.01,6 M12,4C9.2,4,6.78,5.64,5.67,8.04C2.68,8.16,0.24,10.7,0.24,13.76c0,3.08,2.49,5.58,5.56,5.58h13.19c2.49,0,4.51-2.03,4.51-4.53c0-2.34-1.77-4.25-4.05-4.5C18.4,6.75,15.49,4,12,4L12,4z M14,11l-2-2l-2,2H8.5l3.5,3.5l3.5-3.5H14z"/>
  97. </svg>
  98. </view>
  99. <text class="data-label">降雨量</text>
  100. <view class="data-value-container">
  101. <template v-if="weatherData.rainfall">
  102. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.rainfall}">{{ weatherData.rainfall }}</text>
  103. <text class="data-unit">mm</text>
  104. </template>
  105. <text v-else class="data-value no-data">暂无数据</text>
  106. </view>
  107. <text class="data-time" v-if="weatherData.rainfall">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  108. </view>
  109. <!-- 风向 -->
  110. <view class="data-item">
  111. <view class="data-icon" :class="{'no-data': !weatherData.windDirection}">
  112. <svg width="28" height="28" viewBox="0 0 24 24">
  113. <path fill="#3BB44A" d="M4,10H3L4,6L5,10H4z M22,10v2h-8v9l-2-1.6V19H9v1.4L7,22v-2.5c0-5.2,0-9.5,0-9.5H6v-2h16z M17,12v-1.5C17,9.7,16.3,9,15.5,9S14,9.7,14,10.5V12H17z M9,7.5C9,9.43,7.43,11,5.5,11S2,9.43,2,7.5S3.57,4,5.5,4S9,5.57,9,7.5z M6.5,7.5C6.5,8.33,6.02,9,5.5,9S4.5,8.33,4.5,7.5S4.98,6,5.5,6S6.5,6.67,6.5,7.5z"/>
  114. </svg>
  115. </view>
  116. <text class="data-label">风向</text>
  117. <view class="data-value-container">
  118. <template v-if="weatherData.windDirection">
  119. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.windDirection}">{{ weatherData.windDirection }}</text>
  120. <text class="data-unit"></text>
  121. </template>
  122. <text v-else class="data-value no-data">暂无数据</text>
  123. </view>
  124. <text class="data-time" v-if="weatherData.windDirection">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  125. </view>
  126. <!-- 风速 -->
  127. <view class="data-item">
  128. <view class="data-icon" :class="{'no-data': !weatherData.windSpeed}">
  129. <svg width="28" height="28" viewBox="0 0 24 24">
  130. <path fill="#3BB44A" d="M17.66,4.53L18.11,4.5c2.05,0.14,3.74,1.86,3.74,3.93c0,1.66-1.04,3.12-2.6,3.7H14v-2h5.56c0.47,0,0.87-0.39,0.87-0.89c0-0.5-0.4-0.89-0.87-0.89H8.5c-0.64,0-1.22,0.18-1.72,0.49c-0.53-0.34-1.14-0.5-1.66-0.51c-0.35,0-0.69,0.04-1.02,0.13c-0.97,0.37-1.69,1.33-1.69,2.38c0,1.44,1.22,2.61,2.72,2.61H9v2H5.14c-2.36,0-4.26-1.88-4.26-4.24c0-1.75,0.91-3.17,2.62-3.88C3.99,5.01,4.81,4.5,5.78,4.5c1.17,0,2.29,0.58,2.96,1.59c0.51-0.25,1.11-0.41,1.76-0.41h5.77L17.66,4.53z M21.11,14.5H17v-2h4.11c2.01,0,3.89,1.63,3.89,3.83c0,1.56-0.88,2.87-2.27,3.44c-0.38,0.14-0.77,0.23-1.21,0.23c-0.84,0-1.75-0.35-2.43-1.14l1.58-1.23c0.27,0.37,0.66,0.57,1.08,0.57c0.76,0,1.38-0.64,1.38-1.43C23.11,15.33,22.2,14.5,21.11,14.5z M13,18H4.69c-2.06,0-3.69-1.69-3.69-3.72c0-1.17,0.58-2.27,1.5-2.97C3.14,11.37,4,12.23,5,12.72V14h7v2H8v2h5V18z"/>
  131. </svg>
  132. </view>
  133. <text class="data-label">风速</text>
  134. <view class="data-value-container">
  135. <template v-if="weatherData.windSpeed">
  136. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.windSpeed}">{{ weatherData.windSpeed }}</text>
  137. <text class="data-unit">m/s</text>
  138. </template>
  139. <text v-else class="data-value no-data">暂无数据</text>
  140. </view>
  141. <text class="data-time" v-if="weatherData.windSpeed">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  142. </view>
  143. <!-- 气压 -->
  144. <view class="data-item">
  145. <view class="data-icon" :class="{'no-data': !weatherData.pressure}">
  146. <svg width="28" height="28" viewBox="0 0 24 24">
  147. <path fill="#3BB44A" d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8c0-4.41,3.59-8,8-8s8,3.59,8,8C20,16.41,16.41,20,12,20z M13,8h-2v2h-2v2h2v2h2v-2h2v-2h-2V8z M14.5,14.5v-1h-1v1H14.5z M9.5,14.5v-1h-1v1H9.5z M14.5,10.5V10h-1v0.5H14.5z M9.5,10.5V10h-1v0.5H9.5z"/>
  148. </svg>
  149. </view>
  150. <text class="data-label">气压</text>
  151. <view class="data-value-container">
  152. <template v-if="weatherData.pressure">
  153. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.pressure}">{{ weatherData.pressure }}</text>
  154. <text class="data-unit">hPa</text>
  155. </template>
  156. <text v-else class="data-value no-data">暂无数据</text>
  157. </view>
  158. <text class="data-time" v-if="weatherData.pressure">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  159. </view>
  160. <!-- 光照 -->
  161. <view class="data-item">
  162. <view class="data-icon" :class="{'no-data': !weatherData.illumination}">
  163. <svg width="28" height="28" viewBox="0 0 24 24">
  164. <path fill="#3BB44A" d="M6.76,4.84l-1.8-1.79-1.41,1.41l1.79,1.79L6.76,4.84z M4,10.5h-2v2h2V10.5z M13,0.55h-2V3.5h2V0.55z M20.45,4.46l-1.41-1.41-1.79,1.79l1.41,1.41L20.45,4.46z M17.24,18.16l1.79,1.8l1.41-1.41l-1.8-1.79L17.24,18.16z M20,10.5v2h2v-2H20z M12,5.5c-3.31,0-6,2.69-6,6s2.69,6,6,6s6-2.69,6-6S15.31,5.5,12,5.5z M11,22.45h2v-2.95h-2V22.45z M3.55,18.54l1.41,1.41l1.79-1.8l-1.41-1.41L3.55,18.54z"/>
  165. </svg>
  166. </view>
  167. <text class="data-label">光照</text>
  168. <view class="data-value-container">
  169. <template v-if="weatherData.illumination">
  170. <text class="data-value" :class="{'updated': updatedFields.weather && updatedFields.weather.illumination}">{{ weatherData.illumination }}</text>
  171. <text class="data-unit">lux</text>
  172. </template>
  173. <text v-else class="data-value no-data">暂无数据</text>
  174. </view>
  175. <text class="data-time" v-if="weatherData.illumination">{{ weatherData.updateTime || '2分钟前更新' }}</text>
  176. </view>
  177. </view>
  178. </view>
  179. <!-- 采集数据展示区域 - 土壤墒情 -->
  180. <view class="data-section" v-if="deviceInfo.deviceType === 'soil'">
  181. <view class="section-title">
  182. <text>土壤数据</text>
  183. </view>
  184. <view class="data-grid">
  185. <!-- 土壤温度 -->
  186. <view class="data-item">
  187. <view class="data-icon" :class="{'no-data': !soilData.temperature}">
  188. <svg width="28" height="28" viewBox="0 0 24 24">
  189. <path fill="#3BB44A" d="M15,13V5c0-1.66-1.34-3-3-3S9,3.34,9,5v8c-1.21,0.91-2,2.37-2,4c0,2.76,2.24,5,5,5s5-2.24,5-5C17,15.37,16.21,13.91,15,13z M12,20c-1.65,0-3-1.35-3-3c0-1.12,0.61-2.1,1.5-2.61V5c0-0.83,0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5v9.38c0.9,0.52,1.5,1.49,1.5,2.62C15,18.65,13.65,20,12,20z"/>
  190. </svg>
  191. </view>
  192. <text class="data-label">土壤温度</text>
  193. <view class="data-value-container">
  194. <template v-if="soilData.temperature">
  195. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.temperature}">{{ soilData.temperature }}</text>
  196. <text class="data-unit">℃</text>
  197. </template>
  198. <text v-else class="data-value no-data">暂无数据</text>
  199. </view>
  200. <text class="data-time" v-if="soilData.temperature">{{ soilData.updateTime || '5分钟前更新' }}</text>
  201. </view>
  202. <!-- 土壤湿度 -->
  203. <view class="data-item">
  204. <view class="data-icon" :class="{'no-data': !soilData.moisture}">
  205. <svg width="28" height="28" viewBox="0 0 24 24">
  206. <path fill="#3BB44A" d="M12,2c-5.33,4.55-8,8.48-8,11.8c0,4.98,3.8,8.2,8,8.2s8-3.22,8-8.2C20,10.48,17.33,6.55,12,2z M12,20c-3.35,0-6-2.57-6-6.2c0-2.34,1.95-5.44,6-9.14c4.05,3.7,6,6.79,6,9.14C18,17.43,15.35,20,12,20z"/>
  207. </svg>
  208. </view>
  209. <text class="data-label">土壤湿度</text>
  210. <view class="data-value-container">
  211. <template v-if="soilData.moisture">
  212. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.moisture}">{{ soilData.moisture }}</text>
  213. <text class="data-unit">%</text>
  214. </template>
  215. <text v-else class="data-value no-data">暂无数据</text>
  216. </view>
  217. <text class="data-time" v-if="soilData.moisture">{{ soilData.updateTime || '5分钟前更新' }}</text>
  218. </view>
  219. <!-- 氮含量 -->
  220. <view class="data-item">
  221. <view class="data-icon" :class="{'no-data': !soilData.nitrogen}">
  222. <svg width="28" height="28" viewBox="0 0 24 24">
  223. <path fill="#3BB44A" d="M12,2L4.5,20.29l0.71,0.71L12,18l6.79,3l0.71-0.71L12,2z M12,15.3L7.46,16.97L12,6.69l4.54,10.28L12,15.3z"/>
  224. </svg>
  225. </view>
  226. <text class="data-label">氮含量</text>
  227. <view class="data-value-container">
  228. <template v-if="soilData.nitrogen">
  229. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.nitrogen}">{{ soilData.nitrogen }}</text>
  230. <text class="data-unit">mg/kg</text>
  231. </template>
  232. <text v-else class="data-value no-data">暂无数据</text>
  233. </view>
  234. <text class="data-time" v-if="soilData.nitrogen">{{ soilData.updateTime || '5分钟前更新' }}</text>
  235. </view>
  236. <!-- 磷含量 -->
  237. <view class="data-item">
  238. <view class="data-icon" :class="{'no-data': !soilData.phosphorus}">
  239. <svg width="28" height="28" viewBox="0 0 24 24">
  240. <path fill="#3BB44A" d="M11,15H6.83l3.59-3.59L9,10l-6,6l6,6l1.41-1.41L6.83,17H11c4.42,0,8-3.58,8-8V3h-2v6C17,12.42,14.42,15,11,15z"/>
  241. </svg>
  242. </view>
  243. <text class="data-label">磷含量</text>
  244. <view class="data-value-container">
  245. <template v-if="soilData.phosphorus">
  246. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.phosphorus}">{{ soilData.phosphorus }}</text>
  247. <text class="data-unit">mg/kg</text>
  248. </template>
  249. <text v-else class="data-value no-data">暂无数据</text>
  250. </view>
  251. <text class="data-time" v-if="soilData.phosphorus">{{ soilData.updateTime || '5分钟前更新' }}</text>
  252. </view>
  253. <!-- 钾含量 -->
  254. <view class="data-item">
  255. <view class="data-icon" :class="{'no-data': !soilData.potassium}">
  256. <svg width="28" height="28" viewBox="0 0 24 24">
  257. <path fill="#3BB44A" d="M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M13.97,7.17l3.92,7.85C17.96,15.31,18.18,16,18.5,16c0.44,0,0.75-0.53,0.5-1l-4-8h-2l-2,4h-1.5L11,7H8.5L7,11H5.5l1-2l-2-4h10.72C16.42,5,16.97,6.17,13.97,7.17z"/>
  258. </svg>
  259. </view>
  260. <text class="data-label">钾含量</text>
  261. <view class="data-value-container">
  262. <template v-if="soilData.potassium">
  263. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.potassium}">{{ soilData.potassium }}</text>
  264. <text class="data-unit">mg/kg</text>
  265. </template>
  266. <text v-else class="data-value no-data">暂无数据</text>
  267. </view>
  268. <text class="data-time" v-if="soilData.potassium">{{ soilData.updateTime || '5分钟前更新' }}</text>
  269. </view>
  270. <!-- 土壤电导率 -->
  271. <view class="data-item">
  272. <view class="data-icon" :class="{'no-data': !soilData.conductivity}">
  273. <svg width="28" height="28" viewBox="0 0 24 24">
  274. <path fill="#3BB44A" d="M14.69,6.63L14.69,6.63c-0.35-0.35-0.92-0.35-1.27,0l-6.75,6.73c-0.35,0.35-0.35,0.92,0,1.27l0,0c0.35,0.35,0.92,0.35,1.27,0l6.75-6.73C15.04,7.55,15.04,6.98,14.69,6.63z M16.21,15.83l1.56-1.56c0.2-0.2,0.2-0.51,0-0.71c-0.2-0.2-0.51-0.2-0.71,0l-1.56,1.56c-0.2,0.2-0.2,0.51,0,0.71C15.7,16.02,16.02,16.02,16.21,15.83z M12.41,14.27c-0.2-0.2-0.51-0.2-0.71,0l-1.56,1.56c-0.2,0.2-0.2,0.51,0,0.71c0.2,0.2,0.51,0.2,0.71,0l1.56-1.56C12.61,14.78,12.61,14.47,12.41,14.27z M19,6.5C19,4.01,16.99,2,14.5,2c-1.82,0-3.53,1.15-4.27,2.64c-0.4-0.19-0.86-0.3-1.33-0.3C7.17,4.35,6,5.53,6,7c0,0.6,0.13,1.22,0.38,1.79C4.88,9.15,4,10.67,4,12.43C4,15.52,6.49,18,9.56,18h10.81c0.44,0,0.69-0.54,0.39-0.85l-1.56-1.56c-0.2-0.2-0.51-0.2-0.71,0c-0.2,0.2-0.2,0.51,0,0.71l0.63,0.63H9.56c-2.52,0-4.56-2.05-4.56-4.57c0-1.37,0.61-2.64,1.67-3.51L7.4,8.47c0.33,0.48,0.8,0.8,1.35,0.95C8.24,8.67,8,7.86,8,7c0-0.54,0.42-0.98,0.95-1c0.36-0.01,0.71,0.08,1.03,0.28l0.56,0.29l0.15-0.62C11.13,4.39,12.74,3,14.5,3C16.43,3,18,4.57,18,6.5v0.58c0,0.17,0.13,0.29,0.29,0.29c0.51,0.04,1.1,0.24,1.53,0.55C19.37,7.35,19,6.42,19,6.5z"/>
  275. </svg>
  276. </view>
  277. <text class="data-label">电导率</text>
  278. <view class="data-value-container">
  279. <template v-if="soilData.conductivity">
  280. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.conductivity}">{{ soilData.conductivity }}</text>
  281. <text class="data-unit">μS/cm</text>
  282. </template>
  283. <text v-else class="data-value no-data">暂无数据</text>
  284. </view>
  285. <text class="data-time" v-if="soilData.conductivity">{{ soilData.updateTime || '5分钟前更新' }}</text>
  286. </view>
  287. <!-- PH值 -->
  288. <view class="data-item">
  289. <view class="data-icon" :class="{'no-data': !soilData.ph}">
  290. <svg width="28" height="28" viewBox="0 0 24 24">
  291. <path fill="#3BB44A" d="M12,2c-5.33,4.55-8,8.48-8,11.8c0,4.98,3.8,8.2,8,8.2s8-3.22,8-8.2C20,10.48,17.33,6.55,12,2z M8.83,16.29c-1.03-1.46-1.74-3.39-1.26-5.37c0.17-0.71,0.49-1.34,0.9-1.92c0.17-0.24,0.35-0.47,0.55-0.67c0.18-0.21,0.38-0.36,0.58-0.54c0.72-0.49,1.45-0.89,2.1-1.39c0.35-0.22,0.7-0.43,1.05-0.61c0.43-0.23,0.93-0.35,1.26-0.68c0.21-0.21,0.35-0.47,0.45-0.77c0.1-0.28,0.17-0.64,0.25-0.91C14.84,3.16,14.99,3,15.16,3c0.55,0,0.59,0.41,0.61,0.44l0.05,0.15c0.04,0.11,0.21,0.71,0.21,1.03c0,0.53-0.23,0.94-0.44,1.31c-0.52,0.88-1.31,1.25-1.92,1.84c-0.36,0.38-0.66,0.85-0.91,1.34c-0.27,0.5-0.47,1.07-0.49,1.66c-0.02,0.42,0.05,0.86,0.27,1.26c0.23,0.43,0.65,0.65,1.06,0.65c0.71,0,1.24-0.46,1.58-0.97c0.46-0.66,0.74-1.48,0.95-2.27c0.11-0.46,0.18-0.93,0.23-1.35c0.14-1.16,0.27-2.25,0.95-2.33c0.17-0.02,0.3,0.01,0.42,0.12c0.32,0.29,0.31,1.11,0.32,1.38c0,0.51-0.01,1.19-0.14,1.83c-0.07,0.32-0.17,0.64-0.28,0.95c-0.19,0.48-0.4,0.95-0.66,1.34C15.85,12.85,15.29,13.16,14.7,13.29z M12,19.8c-3.46,0-6-2.55-6-6c0-0.34,0.03-0.67,0.08-1l0.93,0.7c0.19,0.47,0.55,0.96,1.28,1.23c0.67,0.25,1.37,0.05,1.86-0.33c0.27-0.21,0.48-0.46,0.65-0.71c0.62-0.95,0.89-2.85,0.62-4.04C11.32,9.32,11.17,9.05,11,8.88c0.03-0.06,0.13-0.16,0.3-0.22c0.37-0.14,0.64,0.11,0.82,0.37c0.28,0.42,0.46,0.82,0.55,1.24c0.02,0.08,0.04,0.18,0.05,0.24c0.12,0.52,0.33,1.72-0.25,3.07c-0.52,1.2-1.45,2.25-2.92,2.25c-1.66,0-2.52-1.24-2.67-2.03c-0.19-1.01,0.04-2.33,0.55-3.37c0.44-0.89,1-1.69,1.68-2.35c0.82-0.79,1.73-1.45,2.73-1.99c0.29-0.16,0.6-0.32,0.84-0.44c-0.14,0.45-0.37,0.76-0.87,1.07c-0.54,0.36-1.17,0.67-1.71,1.01c-0.65,0.36-1.26,0.76-1.81,1.18c-0.38,0.28-0.78,0.59-1.15,0.97c-1.1,1.06-1.84,2.23-2.2,3.41c-0.27,0.86-0.37,1.75-0.29,2.6c0.09,0.92,0.38,1.81,0.88,2.58c0.91,1.38,2.45,2.28,4.28,2.28c4.12,0,6-4.27,6-7c0-0.45-0.04-0.85-0.12-1.22c-0.08-0.36-0.21-0.69-0.4-0.97c0,0,0.18-0.07,0.35,0c0.23,0.09,0.39,0.29,0.52,0.67c0.18,0.56,0.25,1.12,0.25,1.68C18,17.41,15.4,19.8,12,19.8z"/>
  292. </svg>
  293. </view>
  294. <text class="data-label">PH值</text>
  295. <view class="data-value-container">
  296. <template v-if="soilData.ph">
  297. <text class="data-value" :class="{'updated': updatedFields.soil && updatedFields.soil.ph}">{{ soilData.ph }}</text>
  298. <text class="data-unit"></text>
  299. </template>
  300. <text v-else class="data-value no-data">暂无数据</text>
  301. </view>
  302. <text class="data-time" v-if="soilData.ph">{{ soilData.updateTime || '5分钟前更新' }}</text>
  303. </view>
  304. </view>
  305. </view>
  306. <!-- 告警信息列表 -->
  307. <view class="alerts-section">
  308. <view class="section-title">
  309. <text>告警信息</text>
  310. <view class="alert-badge" v-if="getUnhandledAlerts.length > 0">{{ getUnhandledAlerts.length }}</view>
  311. </view>
  312. <view class="alerts-list" v-if="getUnhandledAlerts.length > 0">
  313. <view
  314. v-for="(item, index) in getUnhandledAlerts.slice(0, 3)"
  315. :key="index"
  316. class="alert-item"
  317. :class="{
  318. 'alert-urgent': item.level === 'high',
  319. 'alert-warning': item.level === 'medium',
  320. 'alert-info': item.level === 'low'
  321. }"
  322. >
  323. <view class="alert-item-icon">
  324. <svg v-if="item.level === 'high'" width="24" height="24" viewBox="0 0 24 24">
  325. <path fill="#F56C6C" d="M12,2L1,21h22L12,2z M12,6l7.53,13H4.47L12,6z M11,10v4h2v-4H11z M11,16v2h2v-2H11z" />
  326. </svg>
  327. <svg v-else-if="item.level === 'medium'" width="24" height="24" viewBox="0 0 24 24">
  328. <path fill="#E6A23C" d="M11,15h2v2h-2V15z M11,7h2v6h-2V7z M11.99,2C6.47,2,2,6.48,2,12s4.47,10,9.99,10C17.52,22,22,17.52,22,12S17.52,2,11.99,2z M12,20c-4.42,0-8-3.58-8-8s3.58-8,8-8s8,3.58,8,8S16.42,20,12,20z" />
  329. </svg>
  330. <svg v-else width="24" height="24" viewBox="0 0 24 24">
  331. <path fill="#67C23A" d="M11,7h2v2h-2V7z M11,11h2v6h-2V11z M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8c0-4.41,3.59-8,8-8s8,3.59,8,8C20,16.41,16.41,20,12,20z" />
  332. </svg>
  333. </view>
  334. <view class="alert-item-info">
  335. <text class="alert-item-type">{{ item.type }}</text>
  336. <text class="alert-item-level">
  337. {{ item.level === 'high' ? '紧急' : item.level === 'medium' ? '警告' : '提示' }}
  338. </text>
  339. </view>
  340. <view class="alert-item-time">{{ item.time }}</view>
  341. </view>
  342. </view>
  343. <view v-else class="empty-alert">
  344. <text class="empty-text">暂无告警信息</text>
  345. </view>
  346. </view>
  347. </view>
  348. </template>
  349. <script>
  350. export default {
  351. data() {
  352. return {
  353. deviceInfo: {
  354. deviceId: 'COL1002',
  355. name: '采集设备-1',
  356. status: 'online',
  357. location: '西区B2地块',
  358. lastUpdate: '5分钟前',
  359. deviceType: 'weather' // 'weather' 或 'soil'
  360. },
  361. // 气象站数据
  362. weatherData: {
  363. temperature: '26.5',
  364. humidity: '68',
  365. rainfall: '0.0',
  366. windDirection: '东北',
  367. windSpeed: '3.2',
  368. pressure: '1013.5',
  369. illumination: '45000',
  370. updateTime: '2分钟前更新'
  371. },
  372. // 土壤墒情数据
  373. soilData: {
  374. temperature: '22.3',
  375. moisture: '35.8',
  376. nitrogen: '138.5',
  377. phosphorus: '42.7',
  378. potassium: '185.2',
  379. conductivity: '0.35',
  380. ph: '6.8',
  381. updateTime: '5分钟前更新'
  382. },
  383. // 模拟告警数据
  384. alertHistory: [
  385. { id: 1, time: '今天 13:05', type: '数据异常', status: '未处理', level: 'high' },
  386. { id: 2, time: '今天 09:30', type: '设备离线', status: '未处理', level: 'medium' },
  387. { id: 3, time: '昨天 15:45', type: '通信错误', status: '已处理', level: 'low' },
  388. { id: 4, time: '昨天 10:20', type: '电量不足', status: '未处理', level: 'low' }
  389. ],
  390. // 刷新状态
  391. isRefreshing: false,
  392. // 数值更新动画
  393. updatedFields: {
  394. weather: {},
  395. soil: {}
  396. }
  397. }
  398. },
  399. computed: {
  400. // 获取所有未处理的告警
  401. getUnhandledAlerts() {
  402. return this.alertHistory.filter(alert => alert.status === '未处理');
  403. }
  404. },
  405. onReady() {
  406. // 设置页面标题
  407. uni.setNavigationBarTitle({
  408. title: this.deviceInfo.name
  409. })
  410. },
  411. onLoad(options) {
  412. // 如果有传入设备ID,则获取设备信息
  413. if (options && options.id) {
  414. let deviceId = options.id;
  415. let deviceCode = options.code || '';
  416. // 如果有传入设备编码,优先使用编码判断设备类型
  417. if (deviceCode) {
  418. this.fetchDeviceInfoWithCode(deviceId, deviceCode);
  419. } else {
  420. this.fetchDeviceInfo(deviceId);
  421. }
  422. }
  423. },
  424. methods: {
  425. // 获取设备信息(带编码)
  426. fetchDeviceInfoWithCode(deviceId, deviceCode) {
  427. // 这里应该是API请求,暂时用模拟数据
  428. console.log('获取设备信息:', deviceId, deviceCode);
  429. // 模拟异步获取数据
  430. setTimeout(() => {
  431. // 根据设备编码判断是气象站还是土壤墒情
  432. if (deviceCode.startsWith('W')) {
  433. this.deviceInfo.deviceType = 'weather';
  434. // 保存旧数据用于比较
  435. const oldData = JSON.parse(JSON.stringify(this.weatherData));
  436. // 模拟更新气象数据
  437. this.weatherData = {
  438. temperature: (Math.random() * 10 + 20).toFixed(1),
  439. humidity: (Math.random() * 30 + 50).toFixed(0),
  440. rainfall: Math.random() < 0.3 ? (Math.random() * 5).toFixed(1) : '0.0',
  441. windDirection: ['东北', '东南', '西北', '西南', '东', '南', '西', '北'][Math.floor(Math.random() * 8)],
  442. windSpeed: (Math.random() * 5 + 1).toFixed(1),
  443. pressure: (Math.random() * 20 + 1000).toFixed(1),
  444. illumination: Math.floor(Math.random() * 50000 + 10000).toString(),
  445. updateTime: '刚刚更新'
  446. };
  447. // 检查哪些字段发生了变化
  448. this.updatedFields.weather = {};
  449. Object.keys(this.weatherData).forEach(key => {
  450. if (key !== 'updateTime' && this.weatherData[key] !== oldData[key]) {
  451. this.updatedFields.weather[key] = true;
  452. // 1秒后清除动画标记
  453. setTimeout(() => {
  454. this.$set(this.updatedFields.weather, key, false);
  455. }, 1000);
  456. }
  457. });
  458. } else if (deviceCode.startsWith('S')) {
  459. this.deviceInfo.deviceType = 'soil';
  460. // 保存旧数据用于比较
  461. const oldData = JSON.parse(JSON.stringify(this.soilData));
  462. // 模拟更新土壤数据
  463. this.soilData = {
  464. temperature: (Math.random() * 10 + 18).toFixed(1),
  465. moisture: (Math.random() * 30 + 20).toFixed(1),
  466. nitrogen: (Math.random() * 100 + 100).toFixed(1),
  467. phosphorus: (Math.random() * 50 + 20).toFixed(1),
  468. potassium: (Math.random() * 100 + 150).toFixed(1),
  469. conductivity: (Math.random() * 0.5 + 0.2).toFixed(2),
  470. ph: (Math.random() * 2 + 5.5).toFixed(1),
  471. updateTime: '刚刚更新'
  472. };
  473. // 检查哪些字段发生了变化
  474. this.updatedFields.soil = {};
  475. Object.keys(this.soilData).forEach(key => {
  476. if (key !== 'updateTime' && this.soilData[key] !== oldData[key]) {
  477. this.updatedFields.soil[key] = true;
  478. // 1秒后清除动画标记
  479. setTimeout(() => {
  480. this.$set(this.updatedFields.soil, key, false);
  481. }, 1000);
  482. }
  483. });
  484. } else {
  485. // 如果编码无法判断类型,使用原有判断逻辑
  486. this.fetchDeviceInfo(deviceId);
  487. return;
  488. }
  489. // 更新设备基本信息
  490. this.deviceInfo.deviceId = deviceCode;
  491. this.deviceInfo.name = `采集设备-${deviceCode.slice(-4)}`;
  492. this.deviceInfo.lastUpdate = '刚刚';
  493. // 实际应该是API请求结果
  494. }, 500);
  495. },
  496. // 获取设备信息
  497. fetchDeviceInfo(deviceId) {
  498. // 这里应该是API请求,暂时用模拟数据
  499. console.log('获取设备信息:', deviceId)
  500. // 模拟异步获取数据
  501. setTimeout(() => {
  502. // 根据设备ID判断是气象站还是土壤墒情
  503. if (deviceId.startsWith('W') || deviceId.includes('weather')) {
  504. this.deviceInfo.deviceType = 'weather';
  505. // 保存旧数据用于比较
  506. const oldData = JSON.parse(JSON.stringify(this.weatherData));
  507. // 模拟更新气象数据
  508. this.weatherData = {
  509. temperature: (Math.random() * 10 + 20).toFixed(1),
  510. humidity: (Math.random() * 30 + 50).toFixed(0),
  511. rainfall: Math.random() < 0.3 ? (Math.random() * 5).toFixed(1) : '0.0',
  512. windDirection: ['东北', '东南', '西北', '西南', '东', '南', '西', '北'][Math.floor(Math.random() * 8)],
  513. windSpeed: (Math.random() * 5 + 1).toFixed(1),
  514. pressure: (Math.random() * 20 + 1000).toFixed(1),
  515. illumination: Math.floor(Math.random() * 50000 + 10000).toString(),
  516. updateTime: Math.floor(Math.random() * 10 + 1) + '分钟前更新'
  517. };
  518. // 检查哪些字段发生了变化
  519. this.updatedFields.weather = {};
  520. Object.keys(this.weatherData).forEach(key => {
  521. if (key !== 'updateTime' && this.weatherData[key] !== oldData[key]) {
  522. this.updatedFields.weather[key] = true;
  523. // 1秒后清除动画标记
  524. setTimeout(() => {
  525. this.$set(this.updatedFields.weather, key, false);
  526. }, 1000);
  527. }
  528. });
  529. } else if (deviceId.startsWith('S') || deviceId.includes('soil')) {
  530. this.deviceInfo.deviceType = 'soil';
  531. // 保存旧数据用于比较
  532. const oldData = JSON.parse(JSON.stringify(this.soilData));
  533. // 模拟更新土壤数据
  534. this.soilData = {
  535. temperature: (Math.random() * 10 + 18).toFixed(1),
  536. moisture: (Math.random() * 30 + 20).toFixed(1),
  537. nitrogen: (Math.random() * 100 + 100).toFixed(1),
  538. phosphorus: (Math.random() * 50 + 20).toFixed(1),
  539. potassium: (Math.random() * 100 + 150).toFixed(1),
  540. conductivity: (Math.random() * 0.5 + 0.2).toFixed(2),
  541. ph: (Math.random() * 2 + 5.5).toFixed(1),
  542. updateTime: Math.floor(Math.random() * 10 + 1) + '分钟前更新'
  543. };
  544. // 检查哪些字段发生了变化
  545. this.updatedFields.soil = {};
  546. Object.keys(this.soilData).forEach(key => {
  547. if (key !== 'updateTime' && this.soilData[key] !== oldData[key]) {
  548. this.updatedFields.soil[key] = true;
  549. // 1秒后清除动画标记
  550. setTimeout(() => {
  551. this.$set(this.updatedFields.soil, key, false);
  552. }, 1000);
  553. }
  554. });
  555. } else {
  556. // 如果ID无法判断类型,根据偶数/奇数ID模拟不同类型
  557. const idNum = parseInt(deviceId.replace(/\D/g, ''));
  558. this.deviceInfo.deviceType = idNum % 2 === 0 ? 'weather' : 'soil';
  559. if (this.deviceInfo.deviceType === 'weather') {
  560. // 模拟更新气象数据
  561. this.weatherData = {
  562. temperature: (Math.random() * 10 + 20).toFixed(1),
  563. humidity: (Math.random() * 30 + 50).toFixed(0),
  564. rainfall: Math.random() < 0.3 ? (Math.random() * 5).toFixed(1) : '0.0',
  565. windDirection: ['东北', '东南', '西北', '西南', '东', '南', '西', '北'][Math.floor(Math.random() * 8)],
  566. windSpeed: (Math.random() * 5 + 1).toFixed(1),
  567. pressure: (Math.random() * 20 + 1000).toFixed(1),
  568. illumination: Math.floor(Math.random() * 50000 + 10000).toString(),
  569. updateTime: Math.floor(Math.random() * 10 + 1) + '分钟前更新'
  570. };
  571. } else {
  572. // 模拟更新土壤数据
  573. this.soilData = {
  574. temperature: (Math.random() * 10 + 18).toFixed(1),
  575. moisture: (Math.random() * 30 + 20).toFixed(1),
  576. nitrogen: (Math.random() * 100 + 100).toFixed(1),
  577. phosphorus: (Math.random() * 50 + 20).toFixed(1),
  578. potassium: (Math.random() * 100 + 150).toFixed(1),
  579. conductivity: (Math.random() * 0.5 + 0.2).toFixed(2),
  580. ph: (Math.random() * 2 + 5.5).toFixed(1),
  581. updateTime: Math.floor(Math.random() * 10 + 1) + '分钟前更新'
  582. };
  583. }
  584. }
  585. // 更新设备基本信息
  586. this.deviceInfo.deviceId = deviceId;
  587. this.deviceInfo.name = `采集设备-${deviceId.slice(-4)}`;
  588. this.deviceInfo.lastUpdate = (Math.floor(Math.random() * 5) + 1) + '分钟前';
  589. // 实际应该是API请求结果
  590. }, 500)
  591. },
  592. // 刷新数据
  593. refreshData() {
  594. if (this.isRefreshing) return;
  595. this.isRefreshing = true;
  596. uni.showLoading({
  597. title: '数据刷新中...'
  598. });
  599. setTimeout(() => {
  600. if (this.deviceInfo.deviceType === 'weather') {
  601. // 保存旧数据用于比较
  602. const oldData = JSON.parse(JSON.stringify(this.weatherData));
  603. // 模拟更新气象数据
  604. this.weatherData = {
  605. temperature: (Math.random() * 10 + 20).toFixed(1),
  606. humidity: (Math.random() * 30 + 50).toFixed(0),
  607. rainfall: Math.random() < 0.3 ? (Math.random() * 5).toFixed(1) : '0.0',
  608. windDirection: ['东北', '东南', '西北', '西南', '东', '南', '西', '北'][Math.floor(Math.random() * 8)],
  609. windSpeed: (Math.random() * 5 + 1).toFixed(1),
  610. pressure: (Math.random() * 20 + 1000).toFixed(1),
  611. illumination: Math.floor(Math.random() * 50000 + 10000).toString(),
  612. updateTime: '刚刚更新'
  613. };
  614. // 检查哪些字段发生了变化
  615. this.updatedFields.weather = {};
  616. Object.keys(this.weatherData).forEach(key => {
  617. if (key !== 'updateTime' && this.weatherData[key] !== oldData[key]) {
  618. this.updatedFields.weather[key] = true;
  619. // 1秒后清除动画标记
  620. setTimeout(() => {
  621. this.$set(this.updatedFields.weather, key, false);
  622. }, 1000);
  623. }
  624. });
  625. } else {
  626. // 保存旧数据用于比较
  627. const oldData = JSON.parse(JSON.stringify(this.soilData));
  628. // 模拟更新土壤数据
  629. this.soilData = {
  630. temperature: (Math.random() * 10 + 18).toFixed(1),
  631. moisture: (Math.random() * 30 + 20).toFixed(1),
  632. nitrogen: (Math.random() * 100 + 100).toFixed(1),
  633. phosphorus: (Math.random() * 50 + 20).toFixed(1),
  634. potassium: (Math.random() * 100 + 150).toFixed(1),
  635. conductivity: (Math.random() * 0.5 + 0.2).toFixed(2),
  636. ph: (Math.random() * 2 + 5.5).toFixed(1),
  637. updateTime: '刚刚更新'
  638. };
  639. // 检查哪些字段发生了变化
  640. this.updatedFields.soil = {};
  641. Object.keys(this.soilData).forEach(key => {
  642. if (key !== 'updateTime' && this.soilData[key] !== oldData[key]) {
  643. this.updatedFields.soil[key] = true;
  644. // 1秒后清除动画标记
  645. setTimeout(() => {
  646. this.$set(this.updatedFields.soil, key, false);
  647. }, 1000);
  648. }
  649. });
  650. }
  651. this.deviceInfo.lastUpdate = '刚刚';
  652. uni.hideLoading();
  653. uni.showToast({
  654. title: '数据已更新',
  655. icon: 'success'
  656. });
  657. // 停止刷新动画
  658. setTimeout(() => {
  659. this.isRefreshing = false;
  660. }, 500);
  661. }, 1500);
  662. }
  663. }
  664. }
  665. </script>
  666. <style>
  667. /* 基础样式 */
  668. .container {
  669. display: flex;
  670. flex-direction: column;
  671. min-height: 100vh;
  672. background-color: #F8FCF9;
  673. padding-bottom: 30rpx;
  674. }
  675. /* 设备头部样式 */
  676. .device-header {
  677. background-color: #FFFFFF;
  678. border-radius: 20rpx;
  679. padding: 30rpx;
  680. margin: 30rpx 30rpx 30rpx;
  681. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  682. border: 1rpx solid rgba(210, 237, 217, 0.5);
  683. }
  684. .device-info-row {
  685. display: flex;
  686. justify-content: space-between;
  687. align-items: center;
  688. margin-bottom: 28rpx;
  689. }
  690. .device-name-container {
  691. display: flex;
  692. flex-direction: column;
  693. align-items: flex-start;
  694. }
  695. .refresh-btn {
  696. width: 48rpx;
  697. height: 48rpx;
  698. background-color: rgba(76, 175, 80, 0.1);
  699. border-radius: 50%;
  700. display: flex;
  701. align-items: center;
  702. justify-content: center;
  703. padding: 12rpx;
  704. transition: transform 0.3s ease;
  705. }
  706. .refresh-btn:active {
  707. transform: rotate(180deg);
  708. }
  709. .device-name {
  710. font-size: 36rpx;
  711. color: #333333;
  712. font-weight: 600;
  713. margin-bottom: 12rpx;
  714. }
  715. .status-tag {
  716. padding: 6rpx 16rpx 6rpx 28rpx;
  717. border-radius: 30rpx;
  718. font-size: 24rpx;
  719. font-weight: 500;
  720. flex-shrink: 0;
  721. position: relative;
  722. overflow: hidden;
  723. }
  724. .status-online {
  725. background-color: rgba(76, 175, 80, 0.1);
  726. color: #3BB44A;
  727. }
  728. .status-offline {
  729. background-color: rgba(245, 108, 108, 0.1);
  730. color: #F56C6C;
  731. padding-left: 28rpx;
  732. }
  733. .status-dot {
  734. position: absolute;
  735. width: 6rpx;
  736. height: 6rpx;
  737. background-color: #3BB44A;
  738. border-radius: 50%;
  739. top: 50%;
  740. left: 12rpx;
  741. transform: translateY(-50%);
  742. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  743. animation: blink 1.5s infinite;
  744. display: inline-block;
  745. }
  746. .status-dot.offline-dot {
  747. background-color: #F56C6C;
  748. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  749. }
  750. @keyframes blink {
  751. 0% {
  752. opacity: 0.4;
  753. }
  754. 50% {
  755. opacity: 1;
  756. }
  757. 100% {
  758. opacity: 0.4;
  759. }
  760. }
  761. .device-meta-row {
  762. display: flex;
  763. flex-direction: column;
  764. background-color: #F9FCFA;
  765. padding: 20rpx 24rpx;
  766. border-radius: 16rpx;
  767. border: 1rpx solid rgba(210, 237, 217, 0.8);
  768. }
  769. .device-meta-item {
  770. display: flex;
  771. align-items: center;
  772. font-size: 28rpx;
  773. margin-top: 18rpx;
  774. }
  775. .device-meta-item:first-child {
  776. margin-top: 0;
  777. }
  778. .meta-icon {
  779. width: 36rpx;
  780. height: 36rpx;
  781. display: flex;
  782. align-items: center;
  783. justify-content: center;
  784. margin-right: 12rpx;
  785. }
  786. .meta-label {
  787. color: #777777;
  788. min-width: 140rpx;
  789. font-size: 28rpx;
  790. }
  791. .meta-value {
  792. color: #333333;
  793. flex: 1;
  794. font-size: 28rpx;
  795. font-weight: 500;
  796. }
  797. /* 数据展示区域 */
  798. .data-section {
  799. margin: 0 30rpx 30rpx;
  800. background-color: #FFFFFF;
  801. border-radius: 20rpx;
  802. padding: 30rpx 24rpx;
  803. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  804. border: 1rpx solid rgba(210, 237, 217, 0.5);
  805. }
  806. .section-title {
  807. font-size: 32rpx;
  808. font-weight: 600;
  809. color: #333333;
  810. margin-bottom: 30rpx;
  811. padding: 0 10rpx 16rpx;
  812. display: flex;
  813. align-items: center;
  814. position: relative;
  815. border-bottom: 2rpx solid rgba(59, 180, 74, 0.1);
  816. }
  817. .section-title::before {
  818. content: '';
  819. position: absolute;
  820. left: 0;
  821. top: 50%;
  822. transform: translateY(-50%);
  823. width: 8rpx;
  824. height: 32rpx;
  825. background-color: #3BB44A;
  826. border-radius: 4rpx;
  827. margin-right: 10rpx;
  828. }
  829. .section-title text {
  830. margin-left: 20rpx;
  831. }
  832. .alert-badge {
  833. background-color: #F56C6C;
  834. color: #FFFFFF;
  835. font-size: 22rpx;
  836. border-radius: 30rpx;
  837. padding: 2rpx 12rpx;
  838. margin-left: 12rpx;
  839. font-weight: normal;
  840. min-width: 32rpx;
  841. text-align: center;
  842. }
  843. .data-grid {
  844. display: flex;
  845. flex-wrap: wrap;
  846. padding: 10rpx 0;
  847. width: 100%;
  848. margin: 0 -10rpx;
  849. }
  850. .data-item {
  851. width: calc(50% - 20rpx);
  852. margin: 0 10rpx 20rpx;
  853. display: flex;
  854. flex-direction: column;
  855. align-items: center;
  856. background-color: #F6FCF7;
  857. border-radius: 16rpx;
  858. padding: 24rpx 16rpx;
  859. box-sizing: border-box;
  860. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  861. position: relative;
  862. border: 1rpx solid rgba(210, 237, 217, 0.6);
  863. transition: all 0.25s ease;
  864. overflow: hidden;
  865. }
  866. .data-item::before {
  867. content: '';
  868. position: absolute;
  869. top: 0;
  870. left: 0;
  871. width: 6rpx;
  872. height: 100%;
  873. background: linear-gradient(to bottom, #4CAF50, #8BC34A);
  874. opacity: 0.8;
  875. }
  876. .data-item:active {
  877. transform: translateY(2rpx);
  878. box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.1);
  879. border-color: rgba(76, 175, 80, 0.8);
  880. }
  881. .data-icon {
  882. width: 40rpx;
  883. height: 40rpx;
  884. display: flex;
  885. align-items: center;
  886. justify-content: center;
  887. margin-bottom: 16rpx;
  888. background-color: rgba(76, 175, 80, 0.08);
  889. border-radius: 50%;
  890. padding: 20rpx;
  891. transition: transform 0.3s ease;
  892. }
  893. .data-icon:active {
  894. transform: scale(1.1);
  895. }
  896. .data-icon.no-data {
  897. opacity: 0.5;
  898. }
  899. .data-label {
  900. font-size: 26rpx;
  901. color: #555555;
  902. margin-bottom: 12rpx;
  903. font-weight: 500;
  904. text-align: center;
  905. }
  906. .data-value-container {
  907. display: flex;
  908. align-items: flex-end;
  909. justify-content: center;
  910. margin-bottom: 10rpx;
  911. }
  912. .data-value {
  913. font-size: 36rpx;
  914. color: #333333;
  915. font-weight: 600;
  916. }
  917. .data-value.no-data {
  918. font-size: 26rpx;
  919. color: #999999;
  920. font-weight: normal;
  921. }
  922. .data-unit {
  923. font-size: 22rpx;
  924. color: #888888;
  925. margin-left: 4rpx;
  926. padding-bottom: 6rpx;
  927. }
  928. .data-time {
  929. font-size: 20rpx;
  930. color: #AAAAAA;
  931. margin-top: 8rpx;
  932. text-align: right;
  933. width: 100%;
  934. }
  935. /* 告警信息列表 */
  936. .alerts-section {
  937. margin: 0 30rpx 30rpx;
  938. background-color: #FFFFFF;
  939. border-radius: 20rpx;
  940. padding: 30rpx 24rpx;
  941. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  942. border: 1rpx solid rgba(210, 237, 217, 0.5);
  943. }
  944. .alerts-list {
  945. display: flex;
  946. flex-direction: column;
  947. }
  948. .alert-item {
  949. display: flex;
  950. align-items: center;
  951. padding: 24rpx 20rpx;
  952. border-radius: 12rpx;
  953. margin-bottom: 16rpx;
  954. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  955. position: relative;
  956. border: 1rpx solid transparent;
  957. transition: transform 0.2s ease;
  958. }
  959. .alert-item:active {
  960. transform: scale(0.98);
  961. }
  962. .alert-urgent {
  963. background-color: #FEF3F3;
  964. border-left: 4rpx solid #F56C6C;
  965. border-color: rgba(245, 108, 108, 0.2);
  966. }
  967. .alert-warning {
  968. background-color: #FFF8E6;
  969. border-left: 4rpx solid #E6A23C;
  970. border-color: rgba(230, 162, 60, 0.2);
  971. }
  972. .alert-info {
  973. background-color: #F2FAF5;
  974. border-left: 4rpx solid #67C23A;
  975. border-color: rgba(103, 194, 58, 0.2);
  976. }
  977. .alert-item-icon {
  978. width: 50rpx;
  979. height: 50rpx;
  980. display: flex;
  981. align-items: center;
  982. justify-content: center;
  983. margin-right: 16rpx;
  984. }
  985. .alert-item-info {
  986. flex: 1;
  987. display: flex;
  988. flex-direction: column;
  989. }
  990. .alert-item-type {
  991. font-size: 28rpx;
  992. color: #333333;
  993. font-weight: 500;
  994. margin-bottom: 8rpx;
  995. }
  996. .alert-item-level {
  997. font-size: 24rpx;
  998. color: #999999;
  999. }
  1000. .alert-item-time {
  1001. font-size: 24rpx;
  1002. color: #999999;
  1003. margin-left: 16rpx;
  1004. min-width: 100rpx;
  1005. text-align: right;
  1006. }
  1007. .empty-alert {
  1008. padding: 60rpx 0;
  1009. display: flex;
  1010. justify-content: center;
  1011. align-items: center;
  1012. }
  1013. .empty-text {
  1014. font-size: 28rpx;
  1015. color: #999999;
  1016. }
  1017. /* 数据更新动画 */
  1018. @keyframes fadeNumberChange {
  1019. 0% {
  1020. opacity: 0.6;
  1021. transform: scale(0.95);
  1022. }
  1023. 50% {
  1024. opacity: 1;
  1025. transform: scale(1.05);
  1026. }
  1027. 100% {
  1028. opacity: 1;
  1029. transform: scale(1);
  1030. }
  1031. }
  1032. .data-value.updated {
  1033. animation: fadeNumberChange 0.8s ease;
  1034. }
  1035. /* 刷新按钮旋转动画 */
  1036. @keyframes spin {
  1037. 0% {
  1038. transform: rotate(0deg);
  1039. }
  1040. 100% {
  1041. transform: rotate(360deg);
  1042. }
  1043. }
  1044. .refresh-btn.refreshing {
  1045. animation: spin 1.2s linear infinite;
  1046. }
  1047. </style>