devicePhoto2.vue 29 KB

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