devicePhoto.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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. 查看图片
  12. </view>
  13. <!-- 主图片区域 -->
  14. <view class="main-photo" v-if="currentImg.addr">
  15. <movable-area class="movable-area" :style="{ height: containerHeight }">
  16. <movable-view
  17. class="movable-view"
  18. direction="all"
  19. scale="true"
  20. scale-min="1"
  21. scale-max="5"
  22. >
  23. <view class="image-container">
  24. <image
  25. :src="currentImg.addr"
  26. class="main-photo-image"
  27. mode="widthFix"
  28. @load="onImageLoad"
  29. />
  30. <!-- 识别标记层 -->
  31. <view class="bug-markers" v-if="bugMarkers.length">
  32. <view
  33. v-for="(marker, index) in bugMarkers"
  34. :key="index"
  35. class="bug-marker"
  36. :style="{
  37. left: marker.x + '%',
  38. top: marker.y + '%',
  39. width: marker.width + '%',
  40. height: marker.height + '%',
  41. borderColor: marker.color
  42. }"
  43. >
  44. <view class="bug-label" :style="{ color: marker.color }">
  45. {{ marker.id }}
  46. </view>
  47. </view>
  48. </view>
  49. </view>
  50. </movable-view>
  51. </movable-area>
  52. <view class="photo-timestamp">{{ formatDate(addtime) }}</view>
  53. </view>
  54. <view class="main-photo-empty" v-else>
  55. <u-empty text="暂无图片"></u-empty>
  56. </view>
  57. <!-- 缩略图预览:当前分组的所有图片 -->
  58. <view class="thumbnail-preview" v-if="thumbnails.length">
  59. <view class="thumbnail-scroll">
  60. <view
  61. class="thumbnail-item"
  62. v-for="(item, index) in thumbnails"
  63. :key="index"
  64. :class="{ active: currentIndex == index }"
  65. @click="selectThumbnail(index)"
  66. >
  67. <image :src="item.addr" class="thumbnail-image" mode="aspectFill" />
  68. </view>
  69. </view>
  70. </view>
  71. <!-- 识别结果 -->
  72. <view class="recognition-result">
  73. <view class="result-title">当前图片识别结果</view>
  74. <view class="result-table" v-if="pestList.length">
  75. <view class="result-header">
  76. <text class="header-name">鼠类名称</text>
  77. <text class="header-count">数 量</text>
  78. </view>
  79. <view class="result-row" v-for="(pest, index) in pestList" :key="index">
  80. <view class="pest-info">
  81. <view class="pest-color" :style="{ backgroundColor: pest.color }"></view>
  82. <view class="pest-text">
  83. <view class="pest-name">{{ pest.pest_name }}</view>
  84. <view class="pest-attrs" v-if="pest.attrs">{{ pest.attrs }}</view>
  85. </view>
  86. </view>
  87. <text class="pest-count">{{ pest.pest_num }}</text>
  88. </view>
  89. </view>
  90. <u-empty v-else text="暂无数据"></u-empty>
  91. </view>
  92. </view>
  93. </template>
  94. <script>
  95. export default {
  96. data() {
  97. return {
  98. title: '查看图片',
  99. addtime: '',
  100. group_id: '',
  101. // 该分组的所有图片
  102. thumbnails: [],
  103. currentIndex: 0,
  104. bugMarkers: [],
  105. imageWidth: 0,
  106. imageHeight: 0,
  107. containerHeight: '300px',
  108. pestList: [],
  109. colors: [
  110. '#FF5951',
  111. '#66EDED',
  112. '#E67B3E',
  113. '#6DE28B',
  114. '#FFC97A',
  115. '#E7EB4B',
  116. '#1561F3',
  117. '#FA73F5',
  118. '#159AFF',
  119. '#FA73F5'
  120. ],
  121. pestColorMap: {}, // 害虫颜色映射 {pestName: color}
  122. };
  123. },
  124. computed: {
  125. currentImg() {
  126. return this.thumbnails[this.currentIndex] || {};
  127. },
  128. },
  129. onLoad(options) {
  130. const { addtime, group_id } = options;
  131. this.addtime = Number(addtime) || 0;
  132. this.group_id = group_id || '';
  133. // 取出列表页带过来的该分组全部图片
  134. try {
  135. const imgs = JSON.parse(uni.getStorageSync('shuhai_group_imgs') || '[]');
  136. this.thumbnails = (imgs || []).filter(img => img && img.addr);
  137. } catch (error) {
  138. this.thumbnails = [];
  139. }
  140. uni.removeStorageSync('shuhai_group_imgs');
  141. this.currentIndex = 0;
  142. this.processBugMarkers();
  143. this.buildPestList();
  144. },
  145. methods: {
  146. selectThumbnail(index) {
  147. if (index === this.currentIndex) {
  148. return;
  149. }
  150. this.currentIndex = index;
  151. this.processBugMarkers();
  152. this.buildPestList();
  153. },
  154. // 根据当前图片的mark生成识别结果列表
  155. buildPestList() {
  156. const marks = this.currentImg.mark || [];
  157. const pestArr = new Map();
  158. marks.forEach(item => {
  159. const name = item.text || '未知';
  160. if (!pestArr.has(name)) {
  161. pestArr.set(name, {
  162. pest_name: name,
  163. pest_num: 0,
  164. attrs: this.getAttrs(item),
  165. });
  166. }
  167. pestArr.get(name).pest_num++;
  168. });
  169. this.pestList = [...pestArr.values()].map(item => ({
  170. ...item,
  171. color: this.getPestColor(item.pest_name),
  172. }));
  173. },
  174. // 拼接害虫属性信息(尾长/体长/重量,接口有字段才展示)
  175. getAttrs(mark) {
  176. const parts = [];
  177. if (mark.tail_length) parts.push('尾长:' + mark.tail_length);
  178. if (mark.body_length) parts.push('体长:' + mark.body_length);
  179. if (mark.weight) parts.push('重量:' + mark.weight);
  180. return parts.join(' ');
  181. },
  182. getPestColor(pestName) {
  183. // 如果该害虫已有颜色,直接返回
  184. if (this.pestColorMap[pestName]) {
  185. return this.pestColorMap[pestName];
  186. }
  187. // 为新害虫分配颜色
  188. const usedColors = Object.values(this.pestColorMap);
  189. let color;
  190. for (const c of this.colors) {
  191. if (!usedColors.includes(c)) {
  192. color = c;
  193. break;
  194. }
  195. }
  196. // 如果所有颜色都用完了,使用哈希值确定颜色
  197. if (!color) {
  198. const hash = pestName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
  199. color = this.colors[hash % this.colors.length];
  200. }
  201. this.pestColorMap[pestName] = color;
  202. return color;
  203. },
  204. onImageLoad(e) {
  205. this.imageWidth = e.detail.width;
  206. this.imageHeight = e.detail.height;
  207. // 计算容器显示高度(根据屏幕宽度和图片宽高比)
  208. const systemInfo = uni.getSystemInfoSync();
  209. const screenWidth = systemInfo.windowWidth;
  210. const containerWidth = screenWidth - 24; // 减去左右margin (24rpx ≈ 12px)
  211. const displayHeight = (containerWidth / this.imageWidth) * this.imageHeight;
  212. this.containerHeight = displayHeight + 'px';
  213. // 图片加载完成后处理bugMarkers
  214. this.processBugMarkers();
  215. },
  216. // 根据mark的坐标信息生成标记框
  217. processBugMarkers() {
  218. const marks = this.currentImg.mark || [];
  219. if (!marks.length) {
  220. this.bugMarkers = [];
  221. return;
  222. }
  223. // 图片未加载完成时先给默认值
  224. if (!this.imageWidth || !this.imageHeight) {
  225. this.imageWidth = 1000;
  226. this.imageHeight = 1000;
  227. }
  228. const scaleRatio = 2.4;
  229. const markers = [];
  230. marks.forEach(item => {
  231. if (item.startX === undefined) {
  232. return;
  233. }
  234. const { startX, startY, text } = item;
  235. // 使用缩放比例计算标记位置和尺寸(负宽高表示从右下角画的框,做翻转)
  236. const x = item.width > 0 ? (startX * scaleRatio / this.imageWidth) * 105 : ((startX + item.width) * scaleRatio / this.imageWidth) * 105;
  237. const y = item.height > 0 ? (startY * scaleRatio / this.imageHeight) * 105 : ((startY + item.height) * scaleRatio / this.imageHeight) * 105;
  238. const width = (item.width > 0 ? item.width : -item.width) * scaleRatio / this.imageWidth * 100;
  239. const height = (item.height > 0 ? item.height : -item.height) * scaleRatio / this.imageHeight * 100;
  240. markers.push({
  241. id: text,
  242. x,
  243. y,
  244. width,
  245. height,
  246. color: this.getPestColor(text)
  247. });
  248. });
  249. this.bugMarkers = markers;
  250. },
  251. // 格式化时间
  252. formatDate(dateString) {
  253. const date = new Date(dateString * 1000);
  254. const year = date.getFullYear();
  255. const month = String(date.getMonth() + 1).padStart(2, '0');
  256. const day = String(date.getDate()).padStart(2, '0');
  257. const hour = String(date.getHours()).padStart(2, '0');
  258. const minute = String(date.getMinutes()).padStart(2, '0');
  259. const second = String(date.getSeconds()).padStart(2, '0');
  260. return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
  261. },
  262. handleBack() {
  263. uni.navigateBack({
  264. delta: 1
  265. });
  266. },
  267. }
  268. };
  269. </script>
  270. <style lang="scss" scoped>
  271. .device-photo-page {
  272. background-color: #F5F5F5;
  273. min-height: 100vh;
  274. width: 100%;
  275. padding-top: 112rpx;
  276. padding-bottom: 32rpx;
  277. box-sizing: border-box;
  278. background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);
  279. .device-detail__header {
  280. width: 100%;
  281. font-size: 28rpx;
  282. color: #042118;
  283. font-family: 'Source Han Sans CN VF';
  284. font-weight: 700;
  285. position: relative;
  286. text-align: center;
  287. .arrow-left {
  288. position: absolute;
  289. left: 32rpx;
  290. margin-right: 12rpx;
  291. }
  292. }
  293. /* 主图片区域 */
  294. .main-photo {
  295. position: relative;
  296. margin: 24rpx;
  297. background-color: #FFFFFF;
  298. border-radius: 16rpx;
  299. padding: 0;
  300. overflow: hidden;
  301. .movable-area {
  302. width: 100%;
  303. overflow: hidden;
  304. }
  305. .movable-view {
  306. width: 100%;
  307. }
  308. .image-container {
  309. position: relative;
  310. width: 100%;
  311. }
  312. .main-photo-image {
  313. width: 100%;
  314. display: block;
  315. }
  316. /* 识别标记层 */
  317. .bug-markers {
  318. position: absolute;
  319. top: 0;
  320. left: 0;
  321. width: 100%;
  322. height: 100%;
  323. pointer-events: none;
  324. }
  325. /* 识别标记 */
  326. .bug-marker {
  327. position: absolute;
  328. border: 2rpx solid;
  329. border-radius: 4rpx;
  330. pointer-events: auto;
  331. }
  332. /* 识别标记标签 */
  333. .bug-label {
  334. position: absolute;
  335. top: -30rpx;
  336. left: 0;
  337. padding: 4rpx 8rpx;
  338. border-radius: 4rpx;
  339. font-size: 20rpx;
  340. font-weight: bold;
  341. white-space: nowrap;
  342. }
  343. .photo-timestamp {
  344. position: absolute;
  345. bottom: 0rpx;
  346. left: 0rpx;
  347. width: 100%;
  348. box-sizing: border-box;
  349. background-color: rgba(0, 0, 0, 0.6);
  350. color: #FFFFFF;
  351. padding: 12rpx 16rpx;
  352. font-size: 26rpx;
  353. }
  354. }
  355. .main-photo-empty {
  356. margin: 24rpx;
  357. padding: 120rpx 0;
  358. background-color: #FFFFFF;
  359. border-radius: 16rpx;
  360. }
  361. /* 缩略图预览 */
  362. .thumbnail-preview {
  363. margin: 0 24rpx 24rpx;
  364. .thumbnail-scroll {
  365. gap: 16rpx;
  366. padding-bottom: 16rpx;
  367. box-sizing: border-box;
  368. white-space: nowrap;
  369. overflow-x: auto;
  370. overflow-y: hidden;
  371. width: 100%;
  372. // 隐藏滚动条
  373. -ms-overflow-style: none;
  374. scrollbar-width: none;
  375. .thumbnail-item {
  376. width: 120rpx;
  377. height: 120rpx;
  378. border-radius: 8rpx;
  379. overflow: hidden;
  380. border: 4rpx solid transparent;
  381. display: inline-block;
  382. box-sizing: border-box;
  383. margin-right: 16rpx;
  384. &.active {
  385. border-color: #0BBC58;
  386. }
  387. .thumbnail-image {
  388. width: 100%;
  389. height: 100%;
  390. }
  391. }
  392. }
  393. }
  394. /* 识别结果 */
  395. .recognition-result {
  396. margin: 0 24rpx 24rpx;
  397. background-color: #FFFFFF;
  398. border-radius: 16rpx;
  399. padding: 24rpx;
  400. .result-title {
  401. font-size: 28rpx;
  402. font-weight: 700;
  403. color: #042118;
  404. margin-bottom: 24rpx;
  405. }
  406. .result-table {
  407. border: 1px solid #E4E7ED;
  408. border-radius: 12rpx;
  409. overflow: hidden;
  410. .result-header {
  411. display: flex;
  412. justify-content: space-between;
  413. align-items: center;
  414. background: #F6F8FC;
  415. padding: 18rpx 24rpx;
  416. font-size: 26rpx;
  417. font-weight: 700;
  418. color: #042118;
  419. }
  420. .result-row {
  421. display: flex;
  422. justify-content: space-between;
  423. align-items: center;
  424. padding: 18rpx 24rpx;
  425. border-top: 1px solid #EFF2F7;
  426. .pest-info {
  427. display: flex;
  428. align-items: center;
  429. .pest-color {
  430. width: 16rpx;
  431. height: 16rpx;
  432. border-radius: 50%;
  433. margin-right: 12rpx;
  434. flex-shrink: 0;
  435. }
  436. .pest-text {
  437. .pest-name {
  438. font-size: 26rpx;
  439. color: #042118;
  440. }
  441. .pest-attrs {
  442. font-size: 22rpx;
  443. color: #999999;
  444. margin-top: 6rpx;
  445. }
  446. }
  447. }
  448. .pest-count {
  449. font-size: 28rpx;
  450. font-weight: 500;
  451. color: #042118;
  452. }
  453. }
  454. }
  455. }
  456. }
  457. </style>