detail.vue 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. <template>
  2. <view class="device-detail">
  3. <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
  4. <nav-top-bar background="linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%)">
  5. <view class="device-detail__header">
  6. <u-icon
  7. size="36"
  8. class="arrow-left"
  9. name="arrow-left"
  10. @click="handleBack"
  11. ></u-icon>
  12. {{ deviceInfo.name }}
  13. </view>
  14. </nav-top-bar>
  15. <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
  16. <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
  17. <view class="operation-background"></view>
  18. <view class="operation-content">
  19. <view class="operation-item" v-if="isShowPhoto" @click="handlePhotoClick">
  20. <image :src="photoIcon" class="operation-icon"></image>
  21. 拍照
  22. </view>
  23. <!-- <view class="operation-item" @click="handleServiceClick">
  24. <image :src="serviceIcon" class="operation-icon"></image>
  25. 维修
  26. </view> -->
  27. <view class="operation-item" @click="handleSettingClick">
  28. <image :src="settingIcon" class="operation-icon"></image>
  29. 设置
  30. </view>
  31. <view class="operation-item" @click="handleSimClick">
  32. <image :src="simIcon" class="operation-icon"></image>
  33. SIM卡
  34. </view>
  35. <view class="operation-item" @click="modification">
  36. <image :src="editIcon" class="operation-icon"></image>
  37. 修改
  38. </view>
  39. </view>
  40. </view>
  41. <view class="device-detail__body">
  42. <DeviceCard
  43. :dataSource="deviceInfo"
  44. :collectTime="time"
  45. :title="deviceInfo.name"
  46. :deviceType="deviceType"
  47. />
  48. <view class="tabs">
  49. <view class="tab-container" v-if="isShowTab">
  50. <view class="tab-item" :class="activeTab === 'viewImage'?'active':''" @click="handleTabClick('viewImage')">
  51. 查看图片
  52. </view>
  53. <view class="tab-item" :class="activeTab === 'pestAnalysis'?'active':''" @click="handleTabClick('pestAnalysis')">
  54. 孢子分析
  55. </view>
  56. <view class="tab-item" :class="activeTab === 'deviceData'?'active':''" @click="handleTabClick('deviceData')">
  57. 设备数据
  58. </view>
  59. </view>
  60. <view class="select-timer-container">
  61. <view class="select-year">
  62. <view class="select-year-item" @click="showPicker = true">
  63. {{ currentYear }}
  64. <u-icon name="arrow-down" size="18" class="arrow-down"></u-icon>
  65. </view>
  66. </view>
  67. <view class="tabs-timer-container" @click="showCalendar">
  68. <view class="tabs-timer-item">
  69. {{startDate}}
  70. </view>
  71. 至
  72. <view class="tabs-timer-item">
  73. {{endDate}}
  74. </view>
  75. <u-icon name="calendar" size="36" class="calendar-icon"></u-icon>
  76. </view>
  77. </view>
  78. </view>
  79. <view v-show="activeTab === 'viewImage'">
  80. <!-- 孢子仪2.1(device_code=1):文件夹列表,file_fold 作为名称 -->
  81. <view class="tabs-fold" v-if="isSporeTwo && folderList.length">
  82. <view
  83. class="tabs-fold-item"
  84. :class="{ active: index === folderIndex }"
  85. v-for="(item, index) in folderList"
  86. :key="index"
  87. @click="handleFolderSelect(index)"
  88. >
  89. <image src="https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/file.png" class="tabs-fold-icon" />
  90. <view>{{ formatFold(item.file_fold) }}</view>
  91. </view>
  92. </view>
  93. <photoImage
  94. :images="imageList"
  95. :pestList="pestList"
  96. :disableShow="disableShow"
  97. @changeTab="changeTab"
  98. :currentYear="currentYear"
  99. :deviceInfo="deviceInfo"
  100. :sporeTwo="isSporeTwo"
  101. :folderId="folderList[folderIndex] ? folderList[folderIndex].id : ''"
  102. />
  103. </view>
  104. <view v-show="activeTab === 'deviceData'">
  105. <DeviceData
  106. :deviceStatic="deviceStatic"
  107. :deviceInfo="deviceInfo"
  108. :polylineList="polylineList"
  109. :deviceHistoryList="deviceHistoryList"
  110. :totalPage="totalPage"
  111. :currentPage="page"
  112. :page_size="page_size"
  113. @prevPage="prevPage"
  114. @nextPage="nextPage"
  115. />
  116. </view>
  117. <view v-show="activeTab === 'pestAnalysis'" class="spore-analysis">
  118. <!-- 孢子识别 -->
  119. <view class="analysis-card">
  120. <view class="card-title">孢子识别</view>
  121. <view class="pest-rank">
  122. <view class="pest-rank__item" v-for="(item, index) in pestRankList" :key="index">
  123. <text class="pest-rank__name">{{ item.name }}</text>
  124. <view class="pest-rank__track">
  125. <view class="pest-rank__fill" :style="{ width: rankPercent(item) + '%', background: rankColors[index % rankColors.length] }"></view>
  126. </view>
  127. <text class="pest-rank__count">{{ item.count }}</text>
  128. </view>
  129. </view>
  130. </view>
  131. <!-- 孢子趋势 -->
  132. <view class="analysis-card">
  133. <view class="card-title">孢子趋势</view>
  134. <view class="chart-wrap--spore">
  135. <!-- 只在tab激活且有数据时挂载,保证量宽高时容器可见,避免"布局错误" -->
  136. <qiun-data-charts
  137. v-if="hasSporeSeries && activeTab === 'pestAnalysis'"
  138. type="line"
  139. :chartData="sporeChart"
  140. :opts="sporeOpts"
  141. :reshow="sporeRedraw"
  142. :canvas2d="true"
  143. :inScrollView="true"
  144. :ontouch="true"
  145. />
  146. <view v-if="!hasSporeSeries" class="chart-empty">
  147. <u-empty :text="sporeTrendLoading ? '加载中...' : '暂无数据'"></u-empty>
  148. </view>
  149. </view>
  150. </view>
  151. </view>
  152. </view>
  153. <u-calendar v-model="show" :mode="mode" range-color="#999" btn-type="success" active-bg-color="#0bbc58" range-bg-color="rgba(11,188,88,0.13)" @change="handleChange" :max-date="maxDate" :min-date="minDate"></u-calendar>
  154. <u-picker v-model="showPicker" mode="selector" :range="selectorRange" range-key="id" :default-selector="[0]" @confirm="confirmHandler"></u-picker>
  155. </view>
  156. </template>
  157. <script>
  158. import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
  159. import DeviceCard from './components/DeviceCard.vue';
  160. import PestDiscern from './components/pestDiscern.vue';
  161. import PestEchart from './components/pestEchart.vue';
  162. import PestArchive from './components/pestArchive.vue';
  163. import photoImage from './components/photoImage.vue';
  164. import photoImage2 from './components/photoImage2.vue';
  165. import DeviceData from './components/deviceData.vue';
  166. export default {
  167. components: {
  168. NavTopBar,
  169. DeviceCard,
  170. PestDiscern,
  171. PestEchart,
  172. PestArchive,
  173. photoImage,
  174. photoImage2,
  175. DeviceData,
  176. },
  177. data(){
  178. return {
  179. isShowTab:false,
  180. showPicker: false,
  181. disableShow: false,
  182. isShowOperation: false,
  183. photoIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/photoIcon.png',
  184. editIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/editIcon.png',
  185. serviceIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/serviceIcon.png',
  186. simIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/simIcon.png',
  187. settingIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/settingIcon.png',
  188. totalPage:0,
  189. currentYear: new Date().getFullYear(),
  190. selectorRange: [],
  191. maxDate: this.formatDate(new Date()),
  192. minDate: this.formatDate(new Date(new Date().getFullYear(), 0, 1)),
  193. show: false,
  194. showSim: false,
  195. // 当前日期向前推30天 格式2026-01-01 月份和日期小于10的时候前面加个0
  196. startDate: this.formatDate(new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000)),
  197. endDate: this.formatDate(new Date()),
  198. mode: 'range',
  199. imageList: [],
  200. pest_order:{},
  201. deviceInfo: {},
  202. pestInfo: {},
  203. time: '',
  204. activeTab: 'viewImage',
  205. deviceType: '',
  206. location: '',
  207. total: 0,
  208. day: [],
  209. pest: [],
  210. page:1,
  211. is_pest:'',
  212. page_size:24,
  213. pestList:[],
  214. pest_names:'',
  215. pests:[],
  216. // 孢子分析
  217. rankColors: ['#4FD8E0', '#FF7A45', '#5AD8A6', '#FFC53D', '#F8E71C', '#5B8FF9', '#E8684A', '#6DC8EC', '#FF9DC6', '#7EDB8F'],
  218. pestRankList: [],
  219. // 孢子趋势数据(bzy_analysis:categories + series)
  220. sporeChart: { categories: [], series: [] },
  221. // 孢子趋势加载中
  222. sporeTrendLoading: false,
  223. // 真机首次量宽高偶发为0,延迟翻转此值触发qiun组件重试绘制
  224. sporeRedraw: false,
  225. sporeOpts: {
  226. dataPointShape: true,
  227. // 图例置顶,点击可隐藏/显示对应线(同 PC 图例交互)
  228. legend: {
  229. show: true,
  230. fontSize: 10,
  231. itemGap: 12
  232. },
  233. xAxis: {
  234. disableGrid: false,
  235. gridType: 'dash',
  236. gridColor: '#EEEEEE',
  237. fontSize: 10,
  238. fontColor: '#666666',
  239. itemCount: 4,
  240. scrollShow: true
  241. },
  242. // 虫数单轴。注意:opts里的函数属性经小程序setData传给组件后会丢失,不能依赖formatter,
  243. // 刻度整数化靠 buildSporeChart 把 data[0].max 取整到 splitNumber 的整数倍
  244. // padding给y轴标题和左侧刻度留出空间,避免被裁剪/压住卡片标题
  245. padding: [20, 15, 0, 5],
  246. yAxis: {
  247. showTitle: true,
  248. disableGrid: false,
  249. gridType: 'dash',
  250. gridColor: '#EEEEEE',
  251. splitNumber: 4,
  252. data: [
  253. {
  254. title: '虫数:头',
  255. titleFontSize: 10,
  256. titleFontColor: '#666666',
  257. position: 'left',
  258. min: 0,
  259. max: 4
  260. }
  261. ]
  262. },
  263. extra: {
  264. line: {
  265. type: 'curve',
  266. width: 2
  267. },
  268. tooltip: { showBox: true }
  269. },
  270. enableScroll: true
  271. },
  272. polylineList:[],
  273. isShowPhoto:false,
  274. deviceHistoryList:[],
  275. deviceStatic:{},
  276. // 孢子仪2.1 文件夹列表
  277. folderList: [],
  278. folderIndex: 0,
  279. // 设备代号:1=孢子仪2.1,其他=2.0(走原接口)
  280. deviceCode: ''
  281. }
  282. },
  283. computed: {
  284. // 孢子仪2.1
  285. isSporeTwo() {
  286. return this.deviceCode == 1;
  287. },
  288. // 孢子趋势是否有数据(无数据时显示空状态,避免图表组件一直转loading)
  289. hasSporeSeries() {
  290. return !!(this.sporeChart && this.sporeChart.series && this.sporeChart.series.length);
  291. }
  292. },
  293. onLoad(options){
  294. this.deviceInfo = JSON.parse(options.info);
  295. console.log(this.deviceInfo,'deviceInfod2eviceInfo')
  296. // 列表跳转时带过来的设备代号:1=孢子仪2.1
  297. this.deviceCode = options.device_code || this.deviceInfo.device_code || '';
  298. // 诊断:确认列表带过来的 device_code 是否有值
  299. console.log('[bzy2] device_code =', options.device_code, ', deviceInfo =', JSON.stringify(this.deviceInfo));
  300. // 延迟初始化:等布局稳定后再测 canvas 尺寸
  301. setTimeout(() => {
  302. this.getSporeAnalysis();
  303. this.getSporeTrend();
  304. }, 300);
  305. // this.getPestAnalysis();
  306. this.isShow();
  307. const currentYear = new Date().getFullYear();
  308. this.selectorRange = [];
  309. for(let i = 0;i<50;i++){
  310. const item = {
  311. label: currentYear - i,
  312. id: currentYear - i
  313. }
  314. this.selectorRange.push(item);
  315. }
  316. },
  317. methods: {
  318. handleSimClick(){
  319. // this.showSim = true;
  320. //`/pages/deviceDetails/weatherStation/${type}?deviceInfo=${encodeURIComponent(JSON.stringify(this.deviceInfo))}`
  321. uni.navigateTo({
  322. url:
  323. '/pages/deviceDetails/weatherStation/simDetail?deviceInfo=' +
  324. encodeURIComponent(JSON.stringify(this.deviceInfo))
  325. });
  326. },
  327. modification() {
  328. uni.navigateTo({
  329. url:
  330. '/pages/equipList2/seabox/modification?data=' +
  331. JSON.stringify(this.deviceInfo) +
  332. '&id=' +
  333. this.deviceInfo.type,
  334. });
  335. },
  336. async handlePhotoClick(){
  337. const res = await this.$myRequest({
  338. url: '/api/api_gateway?method=forecast.send_control.admin_device_control',
  339. method: 'POST',
  340. data: {
  341. device_type_id: this.deviceInfo.type,
  342. d_id: this.deviceInfo.d_id,
  343. cmd: 'takephoto',
  344. },
  345. });
  346. if(res){
  347. this.$u.toast('拍照成功')
  348. }
  349. },
  350. handleServiceClick(){
  351. uni.navigateTo({
  352. url: '/pages/afterSale/addafter?d_id=' + this.deviceInfo.d_id +'&device_id='+this.deviceInfo.id + '&device_type=' + this.deviceInfo.type,
  353. })
  354. },
  355. handleSettingClick(){
  356. uni.navigateTo({
  357. url: '/pages/bzy2/deviceControl?deviceId=' + this.deviceInfo.id + '&d_id=' + this.deviceInfo.d_id + '&device_model=' + this.deviceInfo.device_model + '&device_code=' + (this.deviceCode || this.deviceInfo.device_code || ''),
  358. });
  359. },
  360. closeOperationHandler(){
  361. this.isShowOperation = false;
  362. },
  363. isShow(){
  364. // disable == 0 或者 device_model == 15 表示不可以查看
  365. if(!this.deviceInfo.d_id){
  366. return false;
  367. }
  368. let showStatus = true;
  369. if(this.deviceInfo.device_model == 15 || this.deviceInfo.device_model == 12){
  370. showStatus = false;
  371. }
  372. if(showStatus){
  373. this.activeTab = 'viewImage';
  374. }else{
  375. this.activeTab = 'deviceData';
  376. }
  377. this.initAction();
  378. this.isShowTab = showStatus;
  379. if(this.deviceInfo.disable == 0){
  380. this.disableShow = false;
  381. if(showStatus){
  382. this.activeTab = 'viewImage';
  383. this.handleTabClick('viewImage');
  384. }
  385. }else{
  386. this.disableShow = true;
  387. }
  388. },
  389. prevPage(e){
  390. if(e == 1){
  391. return
  392. }
  393. this.page = e-=1;
  394. this.getDeviceHistoryData();
  395. },
  396. nextPage(e){
  397. if(e * this.page_size >= this.totalPage){
  398. return
  399. }
  400. this.page = e+=1;
  401. this.getDeviceHistoryData();
  402. },
  403. changeTab(pestList){
  404. let pest_names = pestList.map(item => item.pest_name);
  405. this.pest_names = pest_names.join(',');
  406. this.initImageList();
  407. },
  408. confirmHandler(e){
  409. this.currentYear = this.selectorRange[e].id;
  410. if(this.currentYear == new Date().getFullYear()){
  411. // 结束日期为this.endDate的月份和日期加上选择的年份
  412. const timeDate = this.currentYear + '-' + this.endDate.split('-')[1] + '-' + this.endDate.split('-')[2];
  413. this.endDate = this.formatDate(new Date(timeDate));
  414. // 开始日期为结束日期前30天
  415. this.startDate = this.formatDate(new Date(new Date(this.endDate).getTime() - 30 * 24 * 60 * 60 * 1000));
  416. this.maxDate = this.formatDate(new Date());
  417. this.minDate = this.formatDate(new Date(new Date().getFullYear(), 0, 1));
  418. }else{
  419. // 结束日期为this.endDate的月份和日期加上选择的年份
  420. const timeDate = this.currentYear + '-' + this.endDate.split('-')[1] + '-' + this.endDate.split('-')[2];
  421. this.endDate = this.formatDate(new Date(timeDate));
  422. // 开始日期为结束日期前30天
  423. this.startDate = this.formatDate(new Date(new Date(this.endDate).getTime() - 30 * 24 * 60 * 60 * 1000));
  424. this.maxDate = this.formatDate(new Date(this.currentYear, 11, 31));
  425. this.minDate = this.formatDate(new Date(this.currentYear, 0, 1));
  426. }
  427. this.initAction();
  428. this.showPicker = false;
  429. },
  430. getInfo(info){
  431. this.pestInfo = info;
  432. },
  433. // 格式化日期为YYYY-MM-DD格式,月份和日期小于10时前面加0
  434. formatDate(date) {
  435. const year = date.getFullYear();
  436. const month = String(date.getMonth() + 1).padStart(2, '0');
  437. const day = String(date.getDate()).padStart(2, '0');
  438. return `${year}-${month}-${day}`;
  439. },
  440. formatTime(date){
  441. // iOS的JavaScriptCore不支持'YYYY-MM-DD HH:MM:SS'格式,统一替换成'/'分隔保证真机可用
  442. const time = new Date(date.replace(/-/g, '/')).getTime() / 1000;
  443. return time.toFixed(0);
  444. },
  445. handleChange(e){
  446. this.startDate = e.startDate;
  447. this.endDate = e.endDate;
  448. this.page = 1;
  449. this.initAction();
  450. },
  451. showCalendar(){
  452. this.show = true;
  453. },
  454. handleBack() {
  455. uni.navigateBack({
  456. delta: 1
  457. });
  458. },
  459. initAction(){
  460. this.pest_order = {}
  461. if(this.activeTab === 'viewImage'){
  462. // this.initPest();
  463. this.initImageList();
  464. }else if(this.activeTab === 'deviceData'){
  465. // this.getDeviceData();
  466. this.getPolylineData();
  467. this.getDeviceHistoryData();
  468. }else if(this.activeTab === 'pestAnalysis'){
  469. console.log('[bzy2] initAction → pestAnalysis');
  470. this.getSporeAnalysis();
  471. this.getSporeTrend();
  472. }
  473. },
  474. handleTabClick(tab) {
  475. this.activeTab = tab;
  476. // 复位后延迟翻转,触发图表在真机上重试绘制(首次量宽高偶发为0)
  477. this.sporeRedraw = false;
  478. this.initAction();
  479. if(tab === 'pestAnalysis'){
  480. setTimeout(() => {
  481. this.sporeRedraw = true;
  482. }, 300);
  483. }
  484. },
  485. // 拉取孢子识别数据(bzy_pest_pie:各物种总量,按数量降序)
  486. async getSporeAnalysis() {
  487. const res = await this.$myRequest({
  488. url: '/api/api_gateway?method=forecast.bzy.bzy_pest_pie',
  489. method: 'POST',
  490. data: {
  491. device_id: this.deviceInfo.id,
  492. start: this.formatTime(this.startDate + ' 00:00:00'),// 秒级时间戳
  493. end: this.formatTime(this.endDate + ' 23:59:59')// 秒级时间戳
  494. }
  495. });
  496. const list = (res && res.data) || [];
  497. this.pestRankList = list
  498. .map(item => ({ name: item.name, count: item.value || 0 }))
  499. .sort((a, b) => b.count - a.count);
  500. },
  501. // 孢子趋势:各物种每日数量(秒级时间戳)
  502. async getSporeTrend() {
  503. this.sporeTrendLoading = true;
  504. try {
  505. console.log('[bzy2] getSporeTrend start', this.deviceInfo.id, this.startDate, this.endDate);
  506. const res = await this.$myRequest({
  507. url: '/api/api_gateway?method=forecast.bzy.bzy_analysis',
  508. method: 'POST',
  509. data: {
  510. device_id: this.deviceInfo.id,
  511. start: this.formatTime(this.startDate + ' 00:00:00'),// 秒级时间戳
  512. end: this.formatTime(this.endDate + ' 23:59:59')// 秒级时间戳
  513. }
  514. });
  515. // $myRequest解包后应为{categories, series};若拿到的是完整返回体则取data字段
  516. const payload = res && res.series ? res : ((res && res.data) || {});
  517. console.log(payload,'payloadpayload');
  518. console.log('[bzy2] bzy_analysis payload:', JSON.stringify(payload).slice(0, 300));
  519. this.buildSporeChart(payload);
  520. } catch (e) {
  521. console.log('[bzy2] bzy_analysis error:', e);
  522. } finally {
  523. this.sporeTrendLoading = false;
  524. }
  525. },
  526. // 孢子趋势:categories/series → 折线图;左轴max取整到splitNumber的整数倍,保证虫数刻度无小数
  527. buildSporeChart(res) {
  528. const categories = res?.categories || [];
  529. const series = (res?.series || []).map(item => ({
  530. name: item.name,
  531. data: (item.data || []).map(v => v || 0)
  532. }));
  533. let maxCount = 0;
  534. series.forEach(item => {
  535. item.data.forEach(v => {
  536. if (v > maxCount) maxCount = v;
  537. });
  538. });
  539. this.sporeOpts.yAxis.data[0].max = Math.max(4, Math.ceil(maxCount / 4) * 4);
  540. this.sporeChart = { categories: categories, series: series };
  541. // 真机上挂载后首次量宽高偶发为0,稍后翻转reshow触发组件重试绘制
  542. this.sporeRedraw = false;
  543. this.$nextTick(() => {
  544. setTimeout(() => {
  545. this.sporeRedraw = true;
  546. }, 400);
  547. });
  548. },
  549. // 虫害识别进度条宽度:占总量百分比,保底 4% 便于查看
  550. rankPercent(item) {
  551. const total = this.pestRankList.reduce((sum, cur) => sum + cur.count, 0);
  552. if (!total) return 0;
  553. return Math.max(4, Math.round((item.count / total) * 100));
  554. },
  555. async getDeviceData(){
  556. const res = await this.$myRequest({
  557. url: '/api/api_gateway?method=forecast.worm_lamp.device_status_data',
  558. method: 'POST',
  559. data: {
  560. device_id: this.deviceInfo.id,
  561. },
  562. });
  563. this.deviceStatic = res;
  564. },
  565. async initImageList(){
  566. // 孢子仪2.1(device_code=1)走文件夹列表;2.0 走原接口
  567. if (!this.isSporeTwo) {
  568. const res = await this.$myRequest({
  569. url: '/api/api_gateway?method=forecast.forecast_system.equip_photo',
  570. method: 'POST',
  571. data: {
  572. page:this.page,
  573. page_number:this.page_size,
  574. device_id: this.deviceInfo.id,
  575. ret:'list',
  576. time_begin: this.formatTime(this.formatDate(new Date(this.startDate)) + ' 00:00:00'),// 格式化开始时间YYYY-MM-DD HH:MM:SS
  577. time_end: this.formatTime(this.formatDate(new Date(this.endDate)) + ' 23:59:59'),// 格式化结束时间YYYY-MM-DD HH:MM:SS
  578. },
  579. });
  580. const data = res?.data || [];
  581. this.imageList = data
  582. return;
  583. }
  584. await this.initFolderList();
  585. },
  586. // 孢子仪2.1:拉取文件夹列表,默认选中第一个并加载其图片
  587. async initFolderList(){
  588. uni.showLoading({ title: '加载中' });
  589. try {
  590. const res = await this.$myRequest({
  591. url: '/api/api_gateway?method=forecast.bzy.bzy_file_fold',
  592. method: 'POST',
  593. data: {
  594. device_id: this.deviceInfo.id,
  595. page: 1,
  596. size: 999,
  597. },
  598. });
  599. this.folderList = res?.data || [];
  600. // 默认选中第一个文件夹并加载其图片
  601. this.folderIndex = 0;
  602. if (this.folderList.length) {
  603. await this.loadFolderPhotos();
  604. } else {
  605. this.imageList = [];
  606. }
  607. } catch (e) {
  608. console.log('bzy_file_fold error:', e);
  609. this.folderList = [];
  610. } finally {
  611. uni.hideLoading();
  612. }
  613. },
  614. // 20260905110035 → 2026-09-05 11:00
  615. formatFold(str){
  616. const s = String(str || '');
  617. if (s.length < 12) return s;
  618. return `${s.slice(0, 4)}${s.slice(4, 6)}${s.slice(6, 8)}_${s.slice(8, 10)}${s.slice(10, 12)}`;
  619. },
  620. handleFolderSelect(index){
  621. if (index === this.folderIndex) return;
  622. this.folderIndex = index;
  623. this.loadFolderPhotos();
  624. },
  625. // 孢子仪2.1:拉取选中文件夹内的图片列表
  626. async loadFolderPhotos(){
  627. const folder = this.folderList[this.folderIndex] || {};
  628. if (!folder.id) {
  629. this.imageList = [];
  630. return;
  631. }
  632. uni.showLoading({ title: '加载中' });
  633. try {
  634. const res = await this.$myRequest({
  635. url: '/api/api_gateway?method=forecast.bzy.bzy_two_percent_one_photo',
  636. method: 'POST',
  637. data: {
  638. id: folder.id,
  639. page: 1,
  640. size: 32,
  641. },
  642. });
  643. console.log('bzy_two_percent_one_photo res:', JSON.stringify(res).slice(0, 500));
  644. this.imageList = res?.data || [];
  645. } catch (e) {
  646. console.log('bzy_two_percent_one_photo error:', e);
  647. this.imageList = [];
  648. } finally {
  649. uni.hideLoading();
  650. }
  651. },
  652. async initPest(){
  653. const res = await this.$myRequest({
  654. url: '/api/api_gateway?method=forecast.new_cbd.pest_type_list',
  655. method: 'POST',
  656. data:{
  657. page:1,
  658. page_size:999999,
  659. device_id: this.deviceInfo.id,
  660. identify_model: 'B',
  661. time_begin: this.formatTime(this.formatDate(new Date(this.startDate)) + ' 00:00:00'),// 格式化开始时间YYYY-MM-DD HH:MM:SS
  662. time_end: this.formatTime(this.formatDate(new Date(this.endDate)) + ' 23:59:59'),// 格式化结束时间YYYY-MM-DD HH:MM:SS
  663. },
  664. });
  665. const data = res?.data || [];
  666. this.pestList = data
  667. },
  668. async getPolylineData(){
  669. const res = await this.$myRequest({
  670. url: '/api/api_gateway?method=forecast.worm_lamp.device_polyline_data',
  671. method: 'POST',
  672. data: {
  673. device_type_id: this.deviceInfo.type_id,
  674. d_id: this.deviceInfo.d_id,
  675. start_time: new Date(this.startDate + ' 00:00:00').getTime()/1000,// 转成毫秒
  676. end_time: new Date(this.endDate + ' 23:59:59').getTime()/1000,// 转成毫秒
  677. },
  678. });
  679. const data = res || [];
  680. this.polylineList = data
  681. },
  682. async getDeviceHistoryData(){
  683. const res = await this.$myRequest({
  684. url: '/api/api_gateway?method=forecast.worm_lamp.device_history_data',
  685. method: 'POST',
  686. data: {
  687. device_type_id: this.deviceInfo.type_id,
  688. device_id: this.deviceInfo.id,
  689. start_time: new Date(this.startDate + ' 00:00:00').getTime()/1000,
  690. end_time: new Date(this.endDate + ' 23:59:59').getTime()/1000,
  691. page: this.page,
  692. page_size: this.page_size,
  693. },
  694. });
  695. const data = res?.data || [];
  696. this.totalPage = res.counts;
  697. this.deviceHistoryList = data
  698. },
  699. async getPestAnalysis(){
  700. const res = await this.$myRequest({
  701. url: '/api/api_gateway?method=forecast.cbd_analysis.analysis_pest_result',
  702. method: 'POST',
  703. data: {
  704. d_id: this.deviceInfo.d_id,
  705. start: this.startDate,
  706. end: this.endDate,
  707. model: 'B'
  708. },
  709. });
  710. const pest_order = res?.pest_order;
  711. this.getInfo({});
  712. let total = 0;
  713. this.pests = [];
  714. for(let key in pest_order){
  715. total += pest_order[key] || 0;
  716. this.pests.push({
  717. name: key,
  718. percent: (pest_order[key] / total) * 100
  719. })
  720. }
  721. this.pest_order = pest_order;
  722. this.day = res?.day || [];
  723. const pest = res?.pest || [];
  724. this.pest = pest;
  725. // pest.forEach(p =>{
  726. // for(let i = 0;i< p.length;i++){
  727. // this.pest.push(p[i]);
  728. // }
  729. // })
  730. this.total = total;
  731. }
  732. }
  733. }
  734. </script>
  735. <style scoped lang="scss">
  736. ::v-deep .u-calendar__action{
  737. display:flex;
  738. justify-content: space-between;
  739. }
  740. ::v-deep .u-hover-class{
  741. .u-calendar__content__item__inner{
  742. color:#aaa !important;
  743. }
  744. }
  745. .operation-container{
  746. position: fixed;
  747. right: 0;
  748. top: 0;
  749. width: 100%;
  750. height: 100%;
  751. z-index: 99;
  752. }
  753. .operation{
  754. position: fixed;
  755. top: 260rpx;
  756. right: 0;
  757. z-index: 999;
  758. width:48rpx;
  759. height: 100rpx;
  760. line-height: 50rpx;
  761. border-radius: 8px 0 0 8px;
  762. border-top: 4rpx solid #FFF;
  763. border-bottom: 4rpx solid #FFF;
  764. border-left: 4rpx solid #FFF;
  765. background: #dddfe6a3;
  766. color: #515153;
  767. text-align: center;
  768. font-family: "Source Han Sans CN VF";
  769. font-size: 24rpx;
  770. font-weight: 500;
  771. writing-mode: vertical-rl;
  772. }
  773. .operation-background{
  774. position: fixed;
  775. right: 0;
  776. top: 0;
  777. width: 100%;
  778. height: 100%;
  779. z-index: 998;
  780. background: #00000040;
  781. }
  782. .operation-content{
  783. position: fixed;
  784. top: 240rpx;
  785. right: 80rpx;
  786. height: 126rpx;
  787. display: flex;
  788. padding: 16rpx 32rpx;
  789. align-items: center;
  790. gap: 64rpx;
  791. z-index: 999;
  792. border-radius: 32rpx;
  793. border: 2rpx solid #FFF;
  794. background: #FFF;
  795. backdrop-filter: blur(8rpx);
  796. .operation-item{
  797. display: flex;
  798. flex-direction: column;
  799. align-items: center;
  800. justify-content: center;
  801. color: #333333;
  802. text-align: center;
  803. font-family: "Source Han Sans CN VF";
  804. font-size: 24rpx;
  805. font-weight: 400;
  806. }
  807. .operation-icon{
  808. width: 58rpx;
  809. height: 58rpx;
  810. }
  811. }
  812. .device-detail {
  813. display: flex;
  814. width: 100%;
  815. // 用页面原生滚动:CSS容器滚动(overflow)会导致canvas同层渲染在滚动时错位
  816. min-height: calc(100vh - 24rpx);
  817. // 状态栏高度由 nav-top-bar 的占位撑起,这里只留头部与内容的间距
  818. padding-top: 24rpx;
  819. flex-direction: column;
  820. align-items: center;
  821. // 孢子分析
  822. .spore-analysis {
  823. width: 100%;
  824. .analysis-card {
  825. width: 100%;
  826. padding: 32rpx;
  827. box-sizing: border-box;
  828. background: #fff;
  829. border-radius: 16rpx;
  830. margin-bottom: 24rpx;
  831. &:last-child {
  832. margin-bottom: 0;
  833. }
  834. }
  835. .card-title {
  836. font-size: 32rpx;
  837. color: #042118;
  838. font-family: 'Source Han Sans CN VF';
  839. font-weight: 700;
  840. margin-bottom: 24rpx;
  841. }
  842. .pest-rank {
  843. &__item {
  844. display: flex;
  845. align-items: center;
  846. padding: 14rpx 0;
  847. }
  848. &__name {
  849. width: 160rpx;
  850. flex-shrink: 0;
  851. font-size: 26rpx;
  852. color: #042118;
  853. font-family: 'Source Han Sans CN VF';
  854. overflow: hidden;
  855. text-overflow: ellipsis;
  856. white-space: nowrap;
  857. }
  858. &__track {
  859. flex: 1;
  860. height: 12rpx;
  861. border-radius: 6rpx;
  862. background: #F0F2F5;
  863. overflow: hidden;
  864. margin: 0 24rpx;
  865. }
  866. &__fill {
  867. height: 100%;
  868. border-radius: 6rpx;
  869. }
  870. &__count {
  871. width: 80rpx;
  872. flex-shrink: 0;
  873. text-align: right;
  874. font-size: 26rpx;
  875. color: #042118;
  876. font-family: 'Source Han Sans CN VF';
  877. }
  878. }
  879. .chart-wrap--spore {
  880. width: 100%;
  881. height: 420rpx;
  882. .chart-empty {
  883. display: flex;
  884. align-items: center;
  885. justify-content: center;
  886. height: 100%;
  887. }
  888. }
  889. }
  890. background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);
  891. .device-detail__header {
  892. width: 100%;
  893. font-size: 28rpx;
  894. color: #999;
  895. color: #042118;
  896. font-family: 'Source Han Sans CN VF';
  897. font-weight: 700;
  898. position: relative;
  899. text-align: center;
  900. .arrow-left {
  901. position: absolute;
  902. left: 32rpx;
  903. margin-right: 12rpx;
  904. }
  905. }
  906. .device-detail__body {
  907. width: calc(100% - 64rpx);
  908. margin: 0 auto;
  909. border-radius: 16rpx;
  910. overflow-x: hidden;
  911. // 隐藏滚动条
  912. -ms-overflow-style: none;
  913. scrollbar-width: none;
  914. }
  915. .tab-content{
  916. width: 100%;
  917. padding-bottom: 32rpx;
  918. }
  919. .tabs {
  920. background: #ffffff;
  921. margin: 24rpx 0;
  922. border-radius: 16rpx;
  923. padding: 16rpx 0;
  924. padding-top: 0;
  925. .tab-container{
  926. display: flex;
  927. width: 100%;
  928. height: 88rpx;
  929. line-height: 88rpx;
  930. text-align: center;
  931. font-size: 28rpx;
  932. font-weight: 700;
  933. color: #042118;
  934. font-family: 'Source Han Sans CN VF';
  935. }
  936. .select-timer-container{
  937. display:flex;
  938. align-items: center;
  939. padding-left: 32rpx;
  940. .select-year{
  941. width: 110rpx;
  942. text-align: center;
  943. height: 64rpx;
  944. border-radius: 32rpx;
  945. font-family: 'Source Han Sans CN VF';
  946. line-height: 64rpx;
  947. background: #F1F4F8;
  948. padding: 0 32rpx;
  949. .select-year-item{
  950. color: #656565;
  951. font-size: 24rpx;
  952. display: flex;
  953. align-items: center;
  954. justify-content: space-around;
  955. }
  956. }
  957. }
  958. .tabs-timer-container{
  959. display: flex;
  960. align-items: center;
  961. justify-content: center;
  962. width: calc(100% - 206rpx);
  963. height: 64rpx;
  964. line-height: 64rpx;
  965. text-align: center;
  966. font-size: 24rpx;
  967. font-weight: 500;
  968. font-family: 'Source Han Sans CN VF';
  969. border-radius: 32rpx;
  970. background: #F1F4F8;
  971. padding: 0 32rpx;
  972. color: #656565;
  973. margin: 16rpx;
  974. position: relative;
  975. white-space: nowrap;
  976. .tabs-timer-item{
  977. width: auto;
  978. flex-shrink: 0;
  979. color: #656565;
  980. text-align: center;
  981. font-family: "Source Han Sans CN VF";
  982. font-size: 24rpx;
  983. font-weight: 400;
  984. }
  985. .calendar-icon{
  986. margin-left: 24rpx;
  987. flex-shrink: 0;
  988. }
  989. }
  990. .tab-item {
  991. flex: 1;
  992. color: #999999;
  993. text-align: center;
  994. font-family: "Source Han Sans CN VF";
  995. font-size: 28rpx;
  996. font-weight: 400;
  997. }
  998. .active{
  999. position: relative;
  1000. color: #303133;
  1001. text-align: center;
  1002. font-family: "Source Han Sans CN VF";
  1003. font-size: 28rpx;
  1004. font-weight: 700;
  1005. &::after {
  1006. content: '';
  1007. position: absolute;
  1008. bottom: 0;
  1009. left: 50%;
  1010. -webkit-transform: translateX(-50%);
  1011. transform: translateX(-50%);
  1012. width: 36rpx;
  1013. height: 36rpx;
  1014. border: 6rpx solid #0bbc58;
  1015. border-radius: 50%;
  1016. border-color: transparent;
  1017. border-bottom-color: #0bbc58;
  1018. }
  1019. }
  1020. }
  1021. .tabs-fold{
  1022. display: flex;
  1023. align-items: center;
  1024. text-align: center;
  1025. font-size: 28rpx;
  1026. font-weight: 700;
  1027. color: #042118;
  1028. font-family: 'Source Han Sans CN VF';
  1029. // 滚动条
  1030. overflow-x: auto;
  1031. white-space: nowrap;
  1032. padding-bottom: 24rpx;
  1033. // 隐藏滚动条
  1034. -ms-overflow-style: none;
  1035. scrollbar-width: none;
  1036. .tabs-fold-item{
  1037. display: flex;
  1038. align-items: center;
  1039. justify-content: center;
  1040. flex-direction: column;
  1041. flex-shrink: 0; // 不压缩子项,超出容器宽度才能横向滚动
  1042. background: #FFFFFF;
  1043. border-radius: 24rpx;
  1044. border: 4rpx solid transparent;
  1045. box-sizing: border-box;
  1046. width: 180rpx;
  1047. padding: 16rpx 0;
  1048. &.active {
  1049. border-color: #0BBC58;
  1050. }
  1051. text-align: center;
  1052. font-size: 24rpx;
  1053. font-weight: 400;
  1054. color: #656565;
  1055. font-family: 'Source Han Sans CN VF';
  1056. margin: 0 14rpx;
  1057. border: 1rpx solid #E5E5E5;
  1058. .tabs-fold-icon{
  1059. width: 48rpx;
  1060. height: 48rpx;
  1061. margin-bottom: 12rpx;
  1062. }
  1063. }
  1064. .tabs-fold-item.active{
  1065. border-color: #0bbc58;
  1066. }
  1067. }
  1068. }
  1069. </style>