瑶都万能墙
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

204 lines
5.2 KiB

  1. /**
  2. * 可见性监听混入
  3. * 提供元素可见性监听功能支持H5微信小程序App端
  4. * 用于实现浏览记录等功能
  5. */
  6. import { VIEWPORT_CONFIG, createBrowseRecordParams } from '@/config/browseConfig.js'
  7. export default {
  8. data() {
  9. return {
  10. hasRecordedView: false, // 标记是否已记录浏览
  11. viewTimer: null, // 浏览计时器
  12. isInViewport: false, // 是否在可视区域内
  13. observer: null, // 观察者实例
  14. }
  15. },
  16. mounted() {
  17. // 组件挂载后,使用Intersection Observer监听可见性
  18. this.observeVisibility()
  19. },
  20. beforeDestroy() {
  21. // 组件销毁前清理observer和计时器
  22. this.cleanupObserver()
  23. },
  24. methods: {
  25. /**
  26. * 监听元素可见性
  27. * @param {String} selector - 要监听的元素选择器默认为'.works'
  28. * @param {Object} config - 配置参数
  29. */
  30. observeVisibility(selector = '.works', config = {}) {
  31. const {
  32. threshold = VIEWPORT_CONFIG.THRESHOLD,
  33. dwellTime = VIEWPORT_CONFIG.DWELL_TIME,
  34. margins = VIEWPORT_CONFIG.VIEWPORT_MARGINS
  35. } = config
  36. // #ifdef H5
  37. if (typeof IntersectionObserver !== 'undefined') {
  38. this.observer = new IntersectionObserver((entries) => {
  39. entries.forEach(entry => {
  40. if (entry.isIntersecting && !this.hasRecordedView) {
  41. // 元素进入可视区域且未记录过浏览
  42. this.handleViewportEnter(dwellTime)
  43. } else if (!entry.isIntersecting && this.isInViewport) {
  44. // 元素离开可视区域
  45. this.handleViewportLeave()
  46. }
  47. })
  48. }, {
  49. threshold: threshold
  50. })
  51. this.observer.observe(this.$el)
  52. } else {
  53. // 降级方案:延迟记录浏览
  54. setTimeout(() => {
  55. if (!this.hasRecordedView) {
  56. this.recordBrowseView()
  57. }
  58. }, dwellTime)
  59. }
  60. // #endif
  61. // #ifdef MP-WEIXIN
  62. // 使用微信小程序原生的IntersectionObserver API
  63. this.observer = wx.createIntersectionObserver(this, {
  64. thresholds: [threshold],
  65. initialRatio: 0,
  66. observeAll: false
  67. })
  68. // 指定页面显示区域作为参照区域
  69. this.observer.relativeToViewport(margins)
  70. // 开始监听目标节点
  71. this.observer.observe(selector, (res) => {
  72. if (res.intersectionRatio > threshold && !this.hasRecordedView) {
  73. // 当元素达到配置阈值以上进入可视区域且未记录过浏览时,处理进入可视区域事件
  74. this.handleViewportEnter(dwellTime)
  75. } else if (res.intersectionRatio <= threshold && this.isInViewport) {
  76. // 元素离开可视区域
  77. this.handleViewportLeave()
  78. }
  79. })
  80. // #endif
  81. // #ifdef APP-PLUS
  82. // App端使用uni-app的createIntersectionObserver
  83. this.observer = uni.createIntersectionObserver(this, {
  84. thresholds: [threshold],
  85. initialRatio: 0,
  86. observeAll: false
  87. })
  88. this.observer.relativeToViewport(margins)
  89. this.observer.observe(selector, (res) => {
  90. if (res.intersectionRatio > threshold && !this.hasRecordedView) {
  91. this.handleViewportEnter(dwellTime)
  92. } else if (res.intersectionRatio <= threshold && this.isInViewport) {
  93. this.handleViewportLeave()
  94. }
  95. })
  96. // #endif
  97. },
  98. /**
  99. * 处理进入可视区域事件
  100. * @param {Number} dwellTime - 停留时间
  101. */
  102. handleViewportEnter(dwellTime = VIEWPORT_CONFIG.DWELL_TIME) {
  103. if (this.hasRecordedView) return
  104. this.isInViewport = true
  105. // 设置延迟计时器,确保用户真正浏览了内容
  106. this.viewTimer = setTimeout(() => {
  107. if (this.isInViewport && !this.hasRecordedView) {
  108. this.recordBrowseView()
  109. }
  110. }, dwellTime)
  111. },
  112. /**
  113. * 处理离开可视区域事件
  114. */
  115. handleViewportLeave() {
  116. this.isInViewport = false
  117. // 清除计时器,如果用户快速滚动过去,不记录浏览
  118. if (this.viewTimer) {
  119. clearTimeout(this.viewTimer)
  120. this.viewTimer = null
  121. }
  122. },
  123. /**
  124. * 记录浏览行为
  125. * @param {String} itemType - 项目类型默认为'dynamic'
  126. * @param {String} itemId - 项目ID默认从this.item.id获取
  127. */
  128. recordBrowseView(itemType = 'dynamic', itemId = null) {
  129. const id = itemId || (this.item && this.item.id)
  130. if (this.hasRecordedView || !id) return
  131. this.hasRecordedView = true
  132. // 清除计时器
  133. if (this.viewTimer) {
  134. clearTimeout(this.viewTimer)
  135. this.viewTimer = null
  136. }
  137. // 使用配置文件中的参数创建API请求参数
  138. const params = createBrowseRecordParams(id, itemType)
  139. // 调用浏览记录API
  140. this.$api('addBrowseRecord', params, res => {
  141. if (res.code === 200) {
  142. console.log('浏览记录已保存:', id)
  143. }
  144. })
  145. },
  146. /**
  147. * 清理观察者和计时器
  148. */
  149. cleanupObserver() {
  150. if (this.observer) {
  151. // #ifdef H5
  152. if (typeof this.observer.disconnect === 'function') {
  153. this.observer.disconnect()
  154. }
  155. // #endif
  156. // #ifdef MP-WEIXIN || APP-PLUS
  157. if (typeof this.observer.disconnect === 'function') {
  158. this.observer.disconnect()
  159. }
  160. // #endif
  161. this.observer = null
  162. }
  163. if (this.viewTimer) {
  164. clearTimeout(this.viewTimer)
  165. this.viewTimer = null
  166. }
  167. },
  168. /**
  169. * 重置浏览记录状态
  170. */
  171. resetBrowseRecord() {
  172. this.hasRecordedView = false
  173. this.isInViewport = false
  174. this.cleanupObserver()
  175. }
  176. }
  177. }