devicePhoto.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102
  1. <template>
  2. <view class="device-photo-page">
  3. <!-- 顶部导航栏 -->
  4. <view class="device-detail__header">
  5. <u-icon
  6. size="36"
  7. class="arrow-left"
  8. name="arrow-left"
  9. @click="handleBack"
  10. ></u-icon>
  11. {{ title }}
  12. </view>
  13. <!-- 日期选择器 -->
  14. <view class="date-container">
  15. <view class="date-picker" @click="show = true">
  16. <view class="date-input">
  17. <text class="date-label">{{ time_begin || '-' }}</text>
  18. </view>
  19. <u-icon name="calendar" class="calendar"></u-icon>
  20. </view>
  21. <view class="tags-swiper">
  22. <view :class="{'tag-icon-active-left': is_jpg == 1}" class="tag-icon-active">
  23. <image :src="is_jpg == 1 ? imageActiveIcon : imageIcon" class="tag-icon" @click="changeVideo(1)"/>
  24. </view>
  25. <view :class="{'tag-icon-active-right': is_jpg == 0}" class="tag-icon-active">
  26. <image :src="is_jpg == 0 ? videoActiveIcon : videoIcon" class="tag-icon" @click="changeVideo(2)"/>
  27. </view>
  28. </view>
  29. </view>
  30. <view class="fileList-preview">
  31. <view class="fileList-scroll">
  32. <view
  33. class="file-item"
  34. v-for="(item, index) in fileList"
  35. :key="index"
  36. :class="{ fileActive: fileIndex == index }"
  37. @click="selectFile(index)"
  38. >
  39. {{ item.event_id }}
  40. </view>
  41. </view>
  42. </view>
  43. <!-- 主图片区域 -->
  44. <view class="main-photo" v-if="currentImg.addr">
  45. <movable-area class="movable-area" :style="{ height: containerHeight }">
  46. <movable-view
  47. :key="currentImg.addr"
  48. class="movable-view"
  49. direction="all"
  50. scale="true"
  51. scale-min="1"
  52. scale-max="5"
  53. :scale-value="movableScale"
  54. :x="movableX"
  55. :y="movableY"
  56. @tap="onViewerTap"
  57. >
  58. <view class="image-container">
  59. <!-- 原图 -->
  60. <image
  61. v-if="is_jpg == 1"
  62. :src="currentImg.addr"
  63. class="main-photo-image"
  64. mode="widthFix"
  65. @load="onImageLoad"
  66. />
  67. <!-- 视频 -->
  68. <video
  69. v-if="is_jpg == 0"
  70. :src="currentImg.addr"
  71. class="main-photo-image"
  72. mode="widthFix"
  73. @load="onImageLoad"
  74. controls
  75. />
  76. <!-- 虫子标记层 -->
  77. <view class="bug-markers" v-if="bugMarkers.length > 0">
  78. <view
  79. v-for="(marker, index) in bugMarkers"
  80. :key="index"
  81. class="bug-marker"
  82. :style="{
  83. left: marker.x + '%',
  84. top: marker.y + '%',
  85. width: marker.width + '%',
  86. height: marker.height + '%',
  87. borderColor: marker.color
  88. }"
  89. >
  90. <view class="bug-label" :style="{ color: marker.color }">
  91. {{ getPestName(marker.id) }}
  92. </view>
  93. </view>
  94. </view>
  95. </view>
  96. </movable-view>
  97. </movable-area>
  98. <view class="photo-timestamp">{{ formatDate(currentImg.addtime) }}</view>
  99. </view>
  100. <!-- 缩略图预览 -->
  101. <view class="thumbnail-preview">
  102. <view class="thumbnail-scroll">
  103. <view
  104. class="thumbnail-item"
  105. v-for="(item, index) in thumbnails"
  106. :key="index"
  107. :class="{ active: img_id == item.id }"
  108. @click="selectThumbnail(item)"
  109. >
  110. <image
  111. v-if="is_jpg == 1"
  112. :src="item.addr"
  113. class="thumbnail-image"
  114. />
  115. <!-- 视频 -->
  116. <video
  117. v-if="is_jpg == 0"
  118. :src="item.addr"
  119. class="thumbnail-image"
  120. mode="widthFix"
  121. controls
  122. />
  123. </view>
  124. </view>
  125. </view>
  126. <!-- 识别结果 -->
  127. <view class="recognition-result" v-if="pestList.length">
  128. <view class="result-title">当前图片识别结果</view>
  129. <!-- 一类害虫 -->
  130. <view>
  131. <view class="pest-category" v-for="(pest,index) in pestList" :key="index">
  132. <!-- <view class="category-header">
  133. <text class="category-title">{{ pest[0] }}</text>
  134. <text class="category-count">数量</text>
  135. </view> -->
  136. <view class="pest-item">
  137. <view class="pest-info">
  138. <view class="pest-color" :style="{ backgroundColor: getPestColor(pest.pest_name) }"></view>
  139. <text class="pest-name">{{ pest.pest_name }}</text>
  140. </view>
  141. <text class="pest-count">{{ pest.pest_num }}</text>
  142. </view>
  143. </view>
  144. </view>
  145. <!-- <view v-else>
  146. <view class="pest-category" v-for="(pest,index) in pestList" :key="index">
  147. <view class="pest-item">
  148. <view class="pest-info">
  149. <view class="pest-color" :style="{ backgroundColor: getPestColor(pest[1].text) }"></view>
  150. <text class="pest-name">{{ pest[1].text }}</text>
  151. </view>
  152. <text class="pest-count">{{ pest[1].value }}</text>
  153. </view>
  154. </view>
  155. </view> -->
  156. </view>
  157. <u-empty v-else text="暂无数据"></u-empty>
  158. <u-calendar
  159. v-model="show"
  160. :mode="mode"
  161. @change="handleChange"
  162. range-color="#999"
  163. btn-type="success"
  164. active-bg-color="#0BBC58"
  165. range-bg-color="rgba(11,188,88,0.13)"
  166. :defaultDate="defaultDate"
  167. ></u-calendar>
  168. <u-picker v-model="showPicker" mode="selector" :range="selectorRange" range-key="id" :default-selector="[0]" @confirm="confirmHandler"></u-picker>
  169. </view>
  170. </template>
  171. <script>
  172. export default {
  173. data() {
  174. return {
  175. show: false,
  176. mode: 'date',
  177. maxDate: this.formatTime(new Date()),
  178. minDate: this.formatTime(new Date(new Date().getFullYear(), 0, 1)),
  179. defaultDate: this.formatTime(new Date(new Date().getFullYear(), 0, 1)),
  180. showPicker: false,
  181. selectorRange: [],
  182. title: '查看图片',
  183. currentThumbnail: 2,
  184. videoActiveIcon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/svg/videoIconActive.svg',
  185. imageActiveIcon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/svg/imageIconActive.svg',
  186. imageIcon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/svg/imageIcon.svg',
  187. videoIcon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/svg/videoIcon.svg',
  188. pestYype:{},
  189. fileList:[],
  190. fileTotle:0,
  191. fileIndex:0,
  192. is_mark:0,
  193. device_id: '',
  194. time_begin: this.formatTime(new Date(new Date().getFullYear(), 0, 1)),
  195. time_end: this.formatTime(new Date()),
  196. img_id: '',
  197. pest_list:[],
  198. pestList:[],
  199. colors:[
  200. '#FF5951',
  201. '#66EDED',
  202. '#E67B3E',
  203. '#6DE28B',
  204. '#FFC97A',
  205. '#E7EB4B',
  206. '#1561F3',
  207. '#FA73F5',
  208. '#159AFF',
  209. '#FA73F5'
  210. ],
  211. currentYear:'',
  212. event_id:'',
  213. event_page:1,
  214. event_page_size:100,
  215. pest_list_arr:[],
  216. thumbnails: [],
  217. addtime:'',
  218. currentImg:{},
  219. bugMarkers: [],
  220. imageWidth: 0,
  221. imageHeight: 0,
  222. camera_id:'',
  223. is_jpg:1,
  224. imgOld_id:'',
  225. containerHeight: '300px',
  226. // 主图缩放/位移受控值:双击复位 & 切图重置用(movable-view 缩放拖动后易视图卡住拖不回)
  227. movableX: 0,
  228. movableY: 0,
  229. movableScale: 1,
  230. lastTapTime: 0,
  231. pestDict: {}, // 虫子字典 {id: name}
  232. pestColorMap: {}, // 害虫颜色映射 {pestName: color}
  233. };
  234. },
  235. async onLoad(options) {
  236. const {id,img_id,event_id,currentYear,addtime} = options
  237. this.device_id = id
  238. this.imgOld_id = img_id
  239. this.event_id = event_id || ''
  240. this.currentYear = currentYear
  241. this.addtime = this.formatTime(Number(addtime)*1000);
  242. this.time_begin = this.addtime;
  243. this.defaultDate = this.addtime;
  244. this.time_end = this.addtime;
  245. this.camera_id = '';
  246. this.is_jpg = 1;
  247. this.selectorRange = [];
  248. const nowYear = new Date().getFullYear();
  249. for(let i = 0;i<50;i++){
  250. const item = {
  251. label: nowYear - i,
  252. id: nowYear - i
  253. }
  254. this.selectorRange.push(item);
  255. }
  256. this.pestList = [];
  257. await this.getEventList();
  258. // 定位到跳转时携带的 event_id,找不到就默认第一个
  259. const fileIndex = this.fileList.findIndex(file => file.event_id == this.event_id);
  260. this.fileIndex = fileIndex > -1 ? fileIndex : 0;
  261. const curFile = this.fileList[this.fileIndex];
  262. if (curFile) {
  263. this.event_id = curFile.event_id;
  264. this.camera_id = curFile.device_id;
  265. }
  266. await this.getPestDict();
  267. await this.getPestLevelMap();
  268. await this.getPestList();
  269. // this.getDevicePhotoDetails();
  270. },
  271. methods: {
  272. selectFile(index){
  273. this.fileIndex = index;
  274. const file = this.fileList[index];
  275. this.event_id = file.event_id;
  276. this.camera_id = file.device_id;
  277. this.thumbnails = [];
  278. this.getPestList();
  279. },
  280. changeVideo(type){
  281. this.is_jpg = type == 1 ? 1 : 0;
  282. this.getPestList();
  283. },
  284. getPestName(id){
  285. if(this.is_mark == 0){
  286. return this.pestDict[id] || id;
  287. }
  288. return id;
  289. },
  290. async confirmHandler(e){
  291. this.currentYear = this.selectorRange[e].id;
  292. if(this.currentYear == new Date().getFullYear()){
  293. this.time_begin = this.formatTime(new Date(new Date().getFullYear(), 0, 1));
  294. this.time_end = this.formatTime(new Date());
  295. this.maxDate = this.formatTime(new Date());
  296. this.minDate = this.formatTime(new Date(new Date().getFullYear(), 0, 1));
  297. }else{
  298. this.time_begin = this.formatTime(new Date(this.currentYear, 0, 1));
  299. this.time_end = this.formatTime(new Date(this.currentYear, 11, 31));
  300. this.maxDate = this.formatTime(new Date(this.currentYear, 11, 31));
  301. this.minDate = this.formatTime(new Date(this.currentYear, 0, 1));
  302. this.defaultDate = this.minDate;
  303. }
  304. this.showPicker = false;
  305. // 切换年份后先清空上一组数据,再按新年份重新拉取
  306. this.fileList = [];
  307. this.thumbnails = [];
  308. this.pestList = [];
  309. this.currentImg = {};
  310. this.event_id = '';
  311. this.camera_id = '';
  312. await this.getEventList();
  313. this.fileIndex = 0;
  314. const curFile = this.fileList[0];
  315. if (curFile) {
  316. this.event_id = curFile.event_id;
  317. this.camera_id = curFile.device_id;
  318. }
  319. await this.getPestList();
  320. },
  321. async handleChange(e){
  322. this.time_begin = e.result;
  323. this.time_end = e.result;
  324. // 切换日期后先清空上一组数据(事件列表/缩略图/识别结果),再按新日期重新拉取
  325. this.fileList = [];
  326. this.thumbnails = [];
  327. this.pestList = [];
  328. this.currentImg = {};
  329. this.event_id = '';
  330. this.camera_id = '';
  331. await this.getEventList();
  332. // 默认选中新日期下的第一个事件
  333. this.fileIndex = 0;
  334. const curFile = this.fileList[0];
  335. if (curFile) {
  336. this.event_id = curFile.event_id;
  337. this.camera_id = curFile.device_id;
  338. }
  339. await this.getPestList();
  340. },
  341. getPestColor(pestName) {
  342. // 如果该害虫已有颜色,直接返回
  343. if (this.pestColorMap[pestName]) {
  344. return this.pestColorMap[pestName];
  345. }
  346. // 为新害虫分配颜色
  347. const usedColors = Object.values(this.pestColorMap);
  348. // 找一个未使用的颜色
  349. let color;
  350. for (const c of this.colors) {
  351. if (!usedColors.includes(c)) {
  352. color = c;
  353. break;
  354. }
  355. }
  356. // 如果所有颜色都用完了,使用哈希值确定颜色
  357. if (!color) {
  358. const hash = pestName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
  359. color = this.colors[hash % this.colors.length];
  360. }
  361. this.pestColorMap[pestName] = color;
  362. return color;
  363. },
  364. getLevelDisplayName(level) {
  365. const levelMap = {
  366. '-1': '其他害虫',
  367. 1: '一类害虫',
  368. 2: '二类害虫',
  369. 3: '三类害虫',
  370. 4: '四类害虫',
  371. 5: '五类害虫'
  372. };
  373. return levelMap[String(level)] || `${level}`;
  374. },
  375. async getEventList(){
  376. const res = await this.$myRequest({
  377. url: '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_event_list',
  378. method: 'POST',
  379. data: {
  380. page:this.event_page,
  381. page_size:this.event_page_size,
  382. device_id: this.device_id,
  383. time_begin: new Date(this.time_begin + ' 00:00:00').getTime()/1000,//换成秒级时间戳
  384. time_end: new Date(this.time_end + ' 23:59:59').getTime()/1000,//换成秒级时间戳
  385. },
  386. });
  387. const resList = res?.data || [];
  388. this.fileList = resList;
  389. this.fileTotle = res?.total || 0;
  390. },
  391. async getPestList() {
  392. const res = await this.$myRequest({
  393. url: '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_photo_list',
  394. method: 'POST',
  395. data: {
  396. camera_id: this.camera_id,
  397. is_jpg:this.is_jpg,
  398. page:1,
  399. event_id: this.event_id,
  400. page_size:500,
  401. time_begin: new Date(this.time_begin + ' 00:00:00').getTime()/1000,
  402. time_end: new Date(this.time_end + ' 23:59:59').getTime()/1000,
  403. device_id: this.device_id,
  404. },
  405. });
  406. this.thumbnails = res?.data || [];
  407. this.currentImg = {};
  408. if (!this.thumbnails.length) {
  409. // 无数据时不再请求详情
  410. this.img_id = '';
  411. return;
  412. }
  413. //如果和旧的id相同,就用旧的id不然就用第一个
  414. const oldImg = this.thumbnails.find(item => item.id == this.imgOld_id);
  415. this.img_id = oldImg?.id || this.thumbnails[0]?.id || ''
  416. this.getDevicePhotoDetails();
  417. },
  418. formatTime(dateString) {
  419. const date = new Date(dateString);
  420. const year = date.getFullYear();
  421. const month = String(date.getMonth() + 1).padStart(2, '0');
  422. const day = String(date.getDate()).padStart(2, '0');
  423. return `${year}-${month}-${day}`;
  424. },
  425. async getPestLevelMap() {
  426. const res = await this.$myRequest({
  427. url: '/api/api_gateway?method=forecast.new_cbd.pest_level_map',
  428. method: 'POST',
  429. });
  430. const pestYype = {}
  431. for(let key in res){
  432. pestYype[this.getLevelDisplayName(key)] = res[key]
  433. }
  434. this.pestYype = pestYype
  435. },
  436. // 获取虫子字典
  437. async getPestDict() {
  438. try {
  439. const res = await this.$myRequest({
  440. url: '/api/api_gateway?method=forecast.pest_info.pest_dict',
  441. method: 'POST',
  442. data:{
  443. type_name: '1'
  444. }
  445. });
  446. this.pestDict = res || {};
  447. } catch (error) {
  448. this.pestDict = {};
  449. }
  450. },
  451. formatDate(dateString) {
  452. const date = new Date(dateString*1000);
  453. const year = date.getFullYear();
  454. const month = String(date.getMonth() + 1).padStart(2, '0');
  455. const day = String(date.getDate()).padStart(2, '0');
  456. const hour = String(date.getHours()).padStart(2, '0');
  457. const minute = String(date.getMinutes()).padStart(2, '0');
  458. const second = String(date.getSeconds()).padStart(2, '0');
  459. return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
  460. },
  461. async getDevicePhotoDetails() {
  462. const res = await this.$myRequest({
  463. url: '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_photo_details',
  464. method: 'POST',
  465. data: {
  466. img_id: this.img_id,
  467. },
  468. });
  469. this.currentImg = res?.photo || {};
  470. // 处理label参数生成bugMarkers
  471. this.processBugMarkers();
  472. // const pest_list = res.pest_list
  473. const pestArr = new Map()
  474. // const pesAlltList = []
  475. // for(let key in this.pestYype){
  476. // for(let j in this.pestYype[key]){
  477. // const pestName = this.pestYype[key][j]
  478. // pesAlltList?.push(pestName)
  479. // pest_list?.forEach(item => {
  480. // if(item.pest_name == pestName){
  481. // if(pestArr.has(key)){
  482. // pestArr.get(key).push(item)
  483. // }else{
  484. // pestArr.set(key, [item])
  485. // }
  486. // }
  487. // })
  488. // }
  489. // }
  490. // pest_list?.forEach(item => {
  491. // if(!pesAlltList.includes(item.pest_name)){
  492. // if(pestArr.has('其他害虫')){
  493. // pestArr.get('其他害虫').push(item)
  494. // }else{
  495. // pestArr.set('其他害虫', [item])
  496. // }
  497. // }
  498. // })
  499. const pestList = []
  500. // for(let key of pestArr){
  501. // pestList.push(key)
  502. // }
  503. if (this.currentImg.is_mark != 0) {
  504. this.processBugMarkers();
  505. for(let i = 0;i< this.currentImg.mark.length;i++){
  506. const key = this.currentImg.mark[i].text
  507. if(pestArr.has(key)){
  508. pestArr.get(key).pest_num++
  509. pestArr.set(key, pestArr.get(key))
  510. }else{
  511. pestArr.set(key, {
  512. pest_num: 1,
  513. })
  514. }
  515. }
  516. for(let key of pestArr){
  517. pestList.push({
  518. pest_name: key[0],
  519. pest_num: key[1].pest_num,
  520. })
  521. }
  522. }
  523. this.pestList = pestList
  524. },
  525. onImageLoad(e) {
  526. this.imageWidth = e.detail.width;
  527. this.imageHeight = e.detail.height;
  528. // 计算容器显示高度(根据屏幕宽度和图片宽高比)
  529. const systemInfo = uni.getSystemInfoSync();
  530. const screenWidth = systemInfo.windowWidth;
  531. const containerWidth = screenWidth - 24; // 减去左右margin (24rpx ≈ 12px)
  532. const displayHeight = (containerWidth / this.imageWidth) * this.imageHeight;
  533. this.containerHeight = displayHeight + 'px';
  534. // 图片加载完成后处理bugMarkers
  535. this.processBugMarkers();
  536. },
  537. processBugMarkers() {
  538. if (this.currentImg.is_mark === 0) {
  539. if (!this.currentImg.label) {
  540. this.bugMarkers = [];
  541. return;
  542. }
  543. // 如果没有图片尺寸,先设置默认值
  544. if (!this.imageWidth || !this.imageHeight) {
  545. this.imageWidth = 1000;
  546. this.imageHeight = 1000;
  547. }
  548. try {
  549. // 根据原图宽度计算缩放比例
  550. let scaleRatio;
  551. if (this.imageWidth >= 5000) {
  552. scaleRatio = 1;
  553. } else if (this.imageWidth >= 4000) {
  554. scaleRatio = 1;
  555. } else {
  556. scaleRatio = 1;
  557. }
  558. // 处理label参数,将单引号替换为双引号
  559. let labelStr = this.currentImg.label;
  560. labelStr = labelStr.replace(/'/g, '"');
  561. const label = JSON.parse(labelStr);
  562. const markers = [];
  563. label.forEach(item => {
  564. for (let key in item) {
  565. const [x1, y1, x2, y2] = item[key];
  566. // 使用缩放比例计算标记位置和尺寸
  567. const x = (x1 * scaleRatio / this.imageWidth) * 100;
  568. const y = (y1 * scaleRatio / this.imageHeight) * 100;
  569. const width = ((x2 - x1) * scaleRatio / this.imageWidth) * 100;
  570. const height = ((y2 - y1) * scaleRatio / this.imageHeight) * 100;
  571. // 从字典中获取虫子名字(尝试字符串和数字两种类型)
  572. const pestName = this.pestDict[key] || this.pestDict[String(key)] || key;
  573. const color = this.getPestColor(pestName);
  574. markers.push({
  575. id: pestName, // 显示虫子名字
  576. x,
  577. y,
  578. width,
  579. height,
  580. color
  581. });
  582. }
  583. });
  584. this.bugMarkers = markers;
  585. } catch (error) {
  586. this.bugMarkers = [];
  587. }
  588. } else {
  589. // this.pestList = [];
  590. const markers = [];
  591. // 根据原图宽度计算缩放比例
  592. let scaleRatio = 2.4;
  593. // if (this.imageWidth >= 5000) {
  594. // scaleRatio = 4;
  595. // } else if (this.imageWidth >= 4000) {
  596. // scaleRatio = 1;
  597. // } else {
  598. // scaleRatio = 2.5;
  599. // }
  600. // const pestArr = new Map()
  601. this.currentImg?.mark?.map((item, index) => {
  602. // if(pestArr.has(item.text)){
  603. // pestArr.set(item.text,{
  604. // value: pestArr.get(item.text).value+=1,
  605. // text: item.text
  606. // })
  607. // }else{
  608. // pestArr.set(item.text, {
  609. // value: 1,
  610. // text: item.text
  611. // })
  612. // }
  613. // const pestList = []
  614. // for(let key of pestArr){
  615. // pestList.push(key)
  616. // }
  617. // this.pestList = pestList
  618. this.is_mark = 1
  619. const { startX, startY,text } = item;
  620. // 使用缩放比例计算标记位置和尺寸
  621. const x = item.width > 0 ? (startX * scaleRatio / this.imageWidth) * 105 : ((startX + item.width) * scaleRatio / this.imageWidth) * 105;
  622. const y = item.height > 0 ? (startY * scaleRatio / this.imageHeight) * 105 : ((startY + item.height) * scaleRatio / this.imageHeight) * 105;
  623. const width = (item.width > 0 ? item.width : -item.width) * scaleRatio / this.imageWidth * 100;
  624. const height = (item.height > 0 ? item.height : -item.height) * scaleRatio / this.imageHeight * 100;
  625. // 从字典中获取虫子名字(尝试字符串和数字两种类型)
  626. const pestName = text;
  627. const color = this.getPestColor(text);
  628. markers.push({
  629. id: pestName, // 显示虫子名字
  630. x,
  631. y,
  632. width,
  633. height,
  634. color
  635. });
  636. });
  637. this.bugMarkers = markers;
  638. }
  639. },
  640. handleBack() {
  641. uni.navigateBack({
  642. delta: 1
  643. });
  644. },
  645. goBack() {
  646. uni.navigateBack();
  647. },
  648. selectThumbnail(item) {
  649. this.resetViewer();
  650. this.img_id = item.id;
  651. this.getDevicePhotoDetails();
  652. },
  653. // 双击主图复位缩放与位置:movable-view 缩放拖动后可能视图卡住、拖不回来
  654. onViewerTap() {
  655. const now = Date.now();
  656. if (now - this.lastTapTime < 300) {
  657. this.resetViewer();
  658. }
  659. this.lastTapTime = now;
  660. },
  661. resetViewer() {
  662. this.movableScale = 1;
  663. // 交替设一个极小值,保证属性变化能触发 movable-view 复位
  664. this.movableX = this.movableX === 0 ? 0.01 : 0;
  665. this.movableY = this.movableY === 0 ? 0.01 : 0;
  666. }
  667. }
  668. };
  669. </script>
  670. <style lang="scss" scoped>
  671. ::v-deep .u-calendar__action{
  672. display:flex;
  673. justify-content: space-between;
  674. }
  675. ::v-deep .u-hover-class{
  676. .u-calendar__content__item__inner{
  677. color:#aaa !important;
  678. }
  679. }
  680. .device-photo-page {
  681. background-color: #F5F5F5;
  682. min-height: 100vh;
  683. width: 100%;
  684. padding-top: 112rpx;
  685. background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);
  686. .device-detail__header {
  687. width: 100%;
  688. font-size: 28rpx;
  689. color: #999;
  690. color: #042118;
  691. font-family: 'Source Han Sans CN VF';
  692. font-weight: 700;
  693. position: relative;
  694. text-align: center;
  695. .arrow-left {
  696. position: absolute;
  697. left: 32rpx;
  698. margin-right: 12rpx;
  699. }
  700. }
  701. /* 顶部导航栏 */
  702. .nav-bar {
  703. display: flex;
  704. align-items: center;
  705. justify-content: space-between;
  706. padding: 24rpx;
  707. background-color: #E6F7EF;
  708. .nav-left {
  709. .back-icon {
  710. font-size: 36rpx;
  711. color: #042118;
  712. }
  713. }
  714. .nav-title {
  715. font-size: 32rpx;
  716. font-weight: 700;
  717. color: #042118;
  718. }
  719. .nav-right {
  720. display: flex;
  721. align-items: center;
  722. .more-icon {
  723. font-size: 32rpx;
  724. color: #042118;
  725. margin-right: 24rpx;
  726. }
  727. .eye-icon {
  728. font-size: 32rpx;
  729. }
  730. }
  731. }
  732. .date-container{
  733. padding: 0 20rpx;
  734. display: flex;
  735. align-items: center;
  736. justify-content: center;
  737. .select-year{
  738. width: 150rpx;
  739. height: 80rpx;
  740. display: flex;
  741. align-items: center;
  742. justify-content: space-around;
  743. color: #656565;
  744. font-size: 26rpx;
  745. line-height: 80rpx;
  746. text-align: center;
  747. border-radius: 48rpx;
  748. background-color: #FFFFFF;
  749. margin-right: 10rpx;
  750. padding: 0 26rpx;
  751. .select-year-item{
  752. width: 100%;
  753. display: flex;
  754. align-items: center;
  755. justify-content: space-around;
  756. }
  757. }
  758. }
  759. /* 日期选择器 */
  760. .date-picker {
  761. flex:2;
  762. display: flex;
  763. align-items: center;
  764. padding: 20rpx 24rpx;
  765. background-color: #FFFFFF;
  766. margin-bottom: 24rpx;
  767. width: 100%;
  768. margin:20rpx auto;
  769. border-radius: 48rpx;
  770. position: relative;
  771. .date-input {
  772. display: flex;
  773. flex-direction: column;
  774. width: 100%;
  775. text-align: center;
  776. .date-label {
  777. font-size: 24rpx;
  778. color: #999999;
  779. margin-bottom: 8rpx;
  780. }
  781. .date-value {
  782. font-size: 28rpx;
  783. color: #042118;
  784. font-weight: 500;
  785. }
  786. }
  787. .calendar {
  788. position: absolute;
  789. right: 24rpx;
  790. top: 50%;
  791. transform: translateY(-50%);
  792. font-size: 32rpx;
  793. color: #999999;
  794. }
  795. .date-separator {
  796. margin: 0 24rpx;
  797. font-size: 28rpx;
  798. color: #999999;
  799. }
  800. }
  801. .tags-swiper{
  802. flex:1;
  803. background-color: #FFFFFF;
  804. border-radius: 48rpx;
  805. padding: 0rpx 0rpx;
  806. margin-left: 20rpx;
  807. height: 80rpx;
  808. display: flex;
  809. align-items: center;
  810. justify-content: center;
  811. z-index:10000;
  812. .tag-icon{
  813. flex:1;
  814. width: 40rpx;
  815. height: 40rpx;
  816. }
  817. .tag-icon-active{
  818. flex:1;
  819. height: 80rpx;
  820. display: flex;
  821. align-items: center;
  822. justify-content: center;
  823. }
  824. .tag-icon-active-left{
  825. border-radius: 36rpx 0rpx 0rpx 36rpx;
  826. background-color: #0BBC58;
  827. }
  828. .tag-icon-active-right{
  829. border-radius: 0rpx 36rpx 36rpx 0rpx;
  830. background-color: #0BBC58;
  831. }
  832. }
  833. /* 主图片区域 */
  834. .main-photo {
  835. position: relative;
  836. margin: 0 24rpx 24rpx;
  837. background-color: #FFFFFF;
  838. border-radius: 16rpx;
  839. padding: 0;
  840. overflow: hidden;
  841. .movable-area {
  842. width: 100%;
  843. overflow: hidden;
  844. }
  845. .movable-view {
  846. width: 100%;
  847. // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
  848. height: 100%;
  849. }
  850. .image-container {
  851. position: relative;
  852. width: 100%;
  853. }
  854. .main-photo-image {
  855. width: 100%;
  856. display: block;
  857. }
  858. /* 虫子标记层 */
  859. .bug-markers {
  860. position: absolute;
  861. top: 0;
  862. left: 0;
  863. width: 100%;
  864. height: 100%;
  865. pointer-events: none;
  866. }
  867. /* 虫子标记 */
  868. .bug-marker {
  869. position: absolute;
  870. border: 2rpx solid;
  871. border-radius: 4rpx;
  872. }
  873. /* 虫子标记标签 */
  874. .bug-label {
  875. position: absolute;
  876. top: -24rpx;
  877. left: 0;
  878. padding: 2rpx 6rpx;
  879. border-radius: 4rpx;
  880. font-size: 20rpx;
  881. color: white;
  882. white-space: nowrap;
  883. }
  884. /* 虫子标记层 */
  885. .bug-markers {
  886. position: absolute;
  887. top: 0;
  888. left: 0;
  889. width: 100%;
  890. height: 100%;
  891. pointer-events: none;
  892. }
  893. /* 虫子标记 */
  894. .bug-marker {
  895. position: absolute;
  896. border: 2rpx solid;
  897. border-radius: 4rpx;
  898. pointer-events: auto;
  899. }
  900. /* 虫子标记标签 */
  901. .bug-label {
  902. position: absolute;
  903. top: -30rpx;
  904. left: 0;
  905. padding: 4rpx 8rpx;
  906. border-radius: 4rpx;
  907. font-size: 20rpx;
  908. font-weight: bold;
  909. }
  910. .photo-timestamp {
  911. position: absolute;
  912. bottom: 0rpx;
  913. left: 0rpx;
  914. width:100%;
  915. background-color: rgba(0, 0, 0, 0.6);
  916. color: #FFFFFF;
  917. padding: 8rpx 16rpx;
  918. border-radius: 8rpx;
  919. font-size: 24rpx;
  920. }
  921. }
  922. .fileList-preview{
  923. margin: 0 24rpx 24rpx;
  924. .fileList-scroll{
  925. gap: 16rpx;
  926. box-sizing: border-box;
  927. white-space: nowrap;
  928. overflow-x: auto;
  929. overflow-y: hidden;
  930. width: 100%;
  931. // 隐藏滚动条
  932. -ms-overflow-style: none;
  933. scrollbar-width: none;
  934. .file-item {
  935. width: 240rpx;
  936. height: 40rpx;
  937. line-height: 40rpx;
  938. text-align: center;
  939. font-size: 24rpx;
  940. color: #042118;
  941. font-weight: 500;
  942. border: 2rpx solid transparent;
  943. display: inline-block;
  944. box-sizing: border-box;
  945. margin-right: 10rpx;
  946. border-radius: 8rpx;
  947. &.fileActive {
  948. border-color: #0BBC58;
  949. }
  950. }
  951. }
  952. }
  953. /* 缩略图预览 */
  954. .thumbnail-preview {
  955. margin: 0 24rpx 24rpx;
  956. .thumbnail-scroll {
  957. gap: 16rpx;
  958. padding-bottom: 16rpx;
  959. box-sizing: border-box;
  960. white-space: nowrap;
  961. overflow-x: auto;
  962. overflow-y: hidden;
  963. width: 100%;
  964. // 隐藏滚动条
  965. -ms-overflow-style: none;
  966. scrollbar-width: none;
  967. .thumbnail-item {
  968. width: 120rpx;
  969. height: 120rpx;
  970. border-radius: 8rpx;
  971. overflow: hidden;
  972. border: 2rpx solid transparent;
  973. display: inline-block;
  974. box-sizing: border-box;
  975. margin-right: 10rpx;
  976. &.active {
  977. border-color: #0BBC58;
  978. }
  979. .thumbnail-image {
  980. width: 100%;
  981. height: 100%;
  982. object-fit: cover;
  983. }
  984. }
  985. }
  986. }
  987. /* 识别结果 */
  988. .recognition-result {
  989. margin: 0 24rpx 24rpx;
  990. background-color: #FFFFFF;
  991. border-radius: 16rpx;
  992. padding: 24rpx;
  993. .result-title {
  994. font-size: 28rpx;
  995. font-weight: 700;
  996. color: #042118;
  997. margin-bottom: 24rpx;
  998. }
  999. /* 害虫类别 */
  1000. .pest-category {
  1001. margin-bottom: 32rpx;
  1002. border: 1px solid #E4E7ED;
  1003. border-radius: 16rpx;
  1004. &:last-child {
  1005. margin-bottom: 0;
  1006. }
  1007. .category-header {
  1008. display: flex;
  1009. justify-content: space-between;
  1010. margin-bottom: 16rpx;
  1011. background: #F6F8FC;
  1012. padding: 18rpx 24rpx;
  1013. .category-title {
  1014. font-size: 24rpx;
  1015. font-weight: 500;
  1016. color: #042118;
  1017. }
  1018. .category-count {
  1019. font-size: 24rpx;
  1020. color: #999999;
  1021. }
  1022. }
  1023. .pest-item {
  1024. display: flex;
  1025. justify-content: space-between;
  1026. align-items: center;
  1027. margin-bottom: 12rpx;
  1028. padding: 6rpx 24rpx;
  1029. &:last-child {
  1030. margin-bottom: 0;
  1031. }
  1032. .pest-info {
  1033. display: flex;
  1034. align-items: center;
  1035. .pest-color {
  1036. width: 16rpx;
  1037. height: 16rpx;
  1038. border-radius: 50%;
  1039. margin-right: 12rpx;
  1040. }
  1041. .pest-name {
  1042. font-size: 24rpx;
  1043. color: #042118;
  1044. }
  1045. }
  1046. .pest-count {
  1047. font-size: 24rpx;
  1048. color: #042118;
  1049. }
  1050. }
  1051. }
  1052. }
  1053. }
  1054. </style>