gardenList.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <template>
  2. <view class="list-page">
  3. <view class="garden-list">
  4. <view class="garden-item" v-for="farm in gardenList" :key="farm.farmId || farm.id"
  5. >
  6. <view class="garden-l" v-if="farm.coverVideo">
  7. <video :id="`gl-video-${farm.farmId || farm.id}`" :src="farm.coverVideo" :show-progress="false" :show-play-btn="true"
  8. :show-center-play-btn="false" object-fit="cover" :show-fullscreen-btn="false"
  9. disable-picture-in-picture :autoplay="false" class="video-dom"
  10. loop muted>
  11. </video>
  12. </view>
  13. <view class="garden-l" v-else>
  14. <image class="garden-img" :src="`${config.BASIC_IMG}home/garden.png`" mode=""></image>
  15. <text class="img-text">无人机实拍视频</text>
  16. </view>
  17. <view class="garden-r" @click="goGardenItem(farm)">
  18. <view class="garden-title">
  19. {{ farm.name }}
  20. </view>
  21. <view class="garden-info">
  22. 品种:荔枝({{ farm.pz }})
  23. </view>
  24. <view class="garden-info">
  25. 位置:{{ (farm.cityName + farm.countyName) || '—' }}
  26. </view>
  27. <view class="garden-btn-group">
  28. <view class="btn-second">
  29. 有味指数
  30. <text>{{ farm.envScore || '—' }}分</text>
  31. </view>
  32. <view class="btn-primary" @click.stop="goSourceReport(farm)">
  33. 溯源报告
  34. </view>
  35. </view>
  36. </view>
  37. </view>
  38. </view>
  39. </view>
  40. </template>
  41. <script setup>
  42. import {
  43. onMounted,
  44. onUnmounted,
  45. ref,
  46. nextTick
  47. } from "vue";
  48. import config from "@/api/config.js"
  49. import HOME from '@/api/home'
  50. import { onPageScroll } from '@dcloudio/uni-app'
  51. onMounted(() => {
  52. getGardenList()
  53. // 初始计算
  54. nextTick(() => {
  55. setTimeout(updateCenterVideo, 50)
  56. })
  57. })
  58. onPageScroll(() => handlePageScroll())
  59. onUnmounted(() => {
  60. // 离开页面时全部暂停
  61. pauseAll()
  62. })
  63. const gardenList = ref([])
  64. const playingId = ref(null)
  65. function throttle(fn, wait = 150) {
  66. let lastTime = 0
  67. let timer = null
  68. return function(...args) {
  69. const now = Date.now()
  70. if (now - lastTime >= wait) {
  71. lastTime = now
  72. fn.apply(this, args)
  73. } else {
  74. clearTimeout(timer)
  75. timer = setTimeout(() => {
  76. lastTime = Date.now()
  77. fn.apply(this, args)
  78. }, wait - (now - lastTime))
  79. }
  80. }
  81. }
  82. const handlePageScroll = throttle(() => {
  83. updateCenterVideo()
  84. }, 200)
  85. function updateCenterVideo() {
  86. // 计算视口中心点附近的 video,播放该 video,其余暂停
  87. const query = uni.createSelectorQuery()
  88. query.selectAll('.video-dom').fields({ id: true, rect: true, size: true }, rects => {
  89. if (!rects || !rects.length) return
  90. const viewportHeight = uni.getSystemInfoSync().windowHeight
  91. const viewportCenter = viewportHeight / 2
  92. let minDistance = Number.POSITIVE_INFINITY
  93. let closestId = null
  94. rects.forEach(rect => {
  95. if (!rect) return
  96. const height = (typeof rect.height === 'number' ? rect.height : (rect.bottom - rect.top))
  97. const elementCenter = rect.top + height / 2
  98. // 仅考虑在可视区域内的元素
  99. if (rect.bottom < 0 || rect.top > viewportHeight) return
  100. const distance = Math.abs(elementCenter - viewportCenter)
  101. if (distance < minDistance) {
  102. minDistance = distance
  103. closestId = rect.id
  104. }
  105. })
  106. if (closestId && playingId.value !== closestId) {
  107. playOnly(closestId)
  108. }
  109. }).exec()
  110. }
  111. function playOnly(targetId) {
  112. // 暂停所有,再播放目标
  113. const ctxs = []
  114. gardenList.value.forEach(item => {
  115. if (!item.coverVideo) return
  116. const id = `gl-video-${item.farmId || item.id}`
  117. const ctx = uni.createVideoContext(id)
  118. ctxs.push({ id, ctx })
  119. })
  120. ctxs.forEach(({ id, ctx }) => {
  121. if (id === targetId) {
  122. try { ctx.play() } catch (e) {}
  123. playingId.value = id
  124. } else {
  125. try { ctx.pause() } catch (e) {}
  126. }
  127. })
  128. }
  129. function pauseAll() {
  130. gardenList.value.forEach(item => {
  131. if (!item.coverVideo) return
  132. const id = `gl-video-${item.farmId || item.id}`
  133. try { uni.createVideoContext(id).pause() } catch (e) {}
  134. })
  135. }
  136. function getGardenList() {
  137. HOME.getAllFarm().then(({
  138. data
  139. }) => {
  140. gardenList.value = data.filter(item => item.recommend !== null)
  141. nextTick(() => setTimeout(updateCenterVideo, 50))
  142. })
  143. }
  144. function goGardenItem(farm) {
  145. const id = farm?.farmId
  146. uni.navigateTo({
  147. url: `/pages/tabBar/home/subPages/gardenItem?farmId=${id}`
  148. });
  149. }
  150. function goSourceReport(farm) {
  151. const id = farm?.farmId
  152. uni.navigateTo({
  153. url: `/pages/tabBar/home/subPages/sourceReport?farmId=${id}`
  154. });
  155. }
  156. </script>
  157. <style lang="scss" scoped>
  158. .list-page {
  159. padding: 20rpx 40rpx;
  160. .garden-list {
  161. .garden-item {
  162. display: flex;
  163. .garden-l {
  164. position: relative;
  165. .video-dom {
  166. width: 234rpx;
  167. height: 200rpx;
  168. object-fit: cover;
  169. border-radius: 10rpx;
  170. }
  171. .garden-img {
  172. width: 254rpx;
  173. height: 200rpx;
  174. object-fit: cover;
  175. border-radius: 10rpx;
  176. }
  177. .img-text {
  178. position: absolute;
  179. left: 0%;
  180. top: 0%;
  181. background: rgba(0, 0, 0, 0.3);
  182. backdrop-filter: 8rpx;
  183. border-radius: 10rpx 0 10rpx 0;
  184. font-size: 20rpx;
  185. color: #FFFFFF;
  186. padding: 6rpx 14rpx;
  187. }
  188. }
  189. .garden-r {
  190. padding-left: 20rpx;
  191. .garden-title {
  192. font-size: 28rpx;
  193. line-height: 42rpx;
  194. color: #000000;
  195. font-weight: 600;
  196. padding-bottom: 10rpx;
  197. }
  198. .garden-info {
  199. color: rgba(0, 0, 0, 0.5);
  200. font-size: 24rpx;
  201. line-height: 36rpx;
  202. }
  203. .garden-btn-group {
  204. padding-top: 20rpx;
  205. display: flex;
  206. align-items: center;
  207. .btn-second {
  208. padding: 0 20rpx;
  209. border-radius: 40rpx;
  210. font-size: 24rpx;
  211. color: #C49600;
  212. background: rgba(255, 217, 94, 0.2);
  213. height: 56rpx;
  214. line-height: 56rpx;
  215. }
  216. .btn-primary {
  217. padding: 0 20rpx;
  218. border-radius: 40rpx;
  219. font-size: 24rpx;
  220. color: #000000;
  221. background: #FFD95E;
  222. font-weight: bold;
  223. height: 56rpx;
  224. line-height: 56rpx;
  225. margin-left: 20rpx;
  226. }
  227. }
  228. }
  229. }
  230. .garden-item+.garden-item {
  231. margin-top: 20rpx;
  232. }
  233. }
  234. }
  235. </style>