From a6634d61a8950ac6b389a026eef229f228c4e61f Mon Sep 17 00:00:00 2001 From: lzx_win <2602107437@qq.com> Date: Tue, 21 Oct 2025 18:09:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B5=8F=E8=A7=88=E8=AE=B0=E5=BD=95):=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=B5=8F=E8=A7=88=E8=AE=B0=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增浏览记录配置文件,统一管理类型和分类枚举 - 添加可视性观察混入,支持多端元素可见性检测 - 创建浏览历史页面,展示用户浏览记录 - 移除API接口中的limit参数限制 - 优化动态列表项组件,集成浏览记录功能 --- api/model/browseRecord.js | 1 - components/list/dynamic/dynamicItem.vue | 6 +- config/browseConfig.js | 128 ++++++++++++++++ mixins/visibilityObserver.js | 205 +++++++++++++++++++++++++ pages.json | 8 + pages_order/mine/browseHistory.vue | 255 ++++++++++++++++++++++++++++++++ 6 files changed, 599 insertions(+), 4 deletions(-) create mode 100644 config/browseConfig.js create mode 100644 mixins/visibilityObserver.js create mode 100644 pages_order/mine/browseHistory.vue diff --git a/api/model/browseRecord.js b/api/model/browseRecord.js index b4f3d4c..98799d5 100644 --- a/api/model/browseRecord.js +++ b/api/model/browseRecord.js @@ -5,7 +5,6 @@ const api = { addBrowseRecord: { url: '/city/browseRecord/addBrowseRecord', method: 'POST', - limit : 1000, auth : true, showLoading : true, }, diff --git a/components/list/dynamic/dynamicItem.vue b/components/list/dynamic/dynamicItem.vue index 7911672..a4d4a75 100644 --- a/components/list/dynamic/dynamicItem.vue +++ b/components/list/dynamic/dynamicItem.vue @@ -30,7 +30,10 @@ import dynamicToShop from '@/components/list/dynamic/dynamicToShop.vue' import statisticalDataInfo from '@/components/list/statisticalDataInfo.vue' import commentList from '@/components/list/dynamic/commentList.vue' + import visibilityObserver from '@/mixins/visibilityObserver.js' + export default { + mixins: [visibilityObserver], components: { userHeadItem, daynamicInfo, @@ -41,9 +44,6 @@ props: { item: {}, }, - data() { - return {} - }, methods: { handleGoToDetail() { // 触发父组件事件,传递当前item数据 diff --git a/config/browseConfig.js b/config/browseConfig.js new file mode 100644 index 0000000..b50c4db --- /dev/null +++ b/config/browseConfig.js @@ -0,0 +1,128 @@ +/** + * 浏览记录配置文件 + * 统一管理浏览记录相关的API参数和配置 + */ + +// 浏览记录类型枚举 +export const BROWSE_RECORD_TYPE = { + DYNAMIC: 0, // 帖子/动态 + RENTAL: 1, // 租房 + JOB: 2, // 工作 + SCENIC_SPOT: 3, // 景点 + GOURMET: 4, // 美食 + ACTIVITY: 5, // 活动 + CAR_FIND_PERSON: 6, // 人找车 + PERSON_FIND_CAR: 7, // 车找人 + ARTICLE: 8 // 文章 +} + +// 浏览记录分类枚举 +export const BROWSE_RECORD_CATEGORY = { + BROWSE: 0, // 浏览 + LIKE: 1, // 点赞 + FORWARD: 2, // 转发 + REWARDED_VIDEO: 3, // 激励视频 + COVER_AD: 4 // 封面广告 +} + +// 可视区域检测配置 +export const VIEWPORT_CONFIG = { + // 触发阈值(元素可见比例) + THRESHOLD: 0.8, + + // 停留时间(毫秒)- 确保用户真正浏览了内容 + DWELL_TIME: 1000, + + // 参照区域边距 + VIEWPORT_MARGINS: { + top: 0, + bottom: 0, + left: 0, + right: 0 + } +} + +// 浏览记录API参数配置 +export const BROWSE_RECORD_CONFIG = { + // 动态列表项浏览记录 + DYNAMIC_ITEM: { + type: BROWSE_RECORD_TYPE.DYNAMIC, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 租房列表项浏览记录 + RENTAL_ITEM: { + type: BROWSE_RECORD_TYPE.RENTAL, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 工作列表项浏览记录 + JOB_ITEM: { + type: BROWSE_RECORD_TYPE.JOB, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 景点列表项浏览记录 + SCENIC_SPOT_ITEM: { + type: BROWSE_RECORD_TYPE.SCENIC_SPOT, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 美食列表项浏览记录 + GOURMET_ITEM: { + type: BROWSE_RECORD_TYPE.GOURMET, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 活动列表项浏览记录 + ACTIVITY_ITEM: { + type: BROWSE_RECORD_TYPE.ACTIVITY, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 人找车列表项浏览记录 + CAR_FIND_PERSON_ITEM: { + type: BROWSE_RECORD_TYPE.CAR_FIND_PERSON, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 车找人列表项浏览记录 + PERSON_FIND_CAR_ITEM: { + type: BROWSE_RECORD_TYPE.PERSON_FIND_CAR, + category: BROWSE_RECORD_CATEGORY.BROWSE + }, + + // 文章列表项浏览记录 + ARTICLE_ITEM: { + type: BROWSE_RECORD_TYPE.ARTICLE, + category: BROWSE_RECORD_CATEGORY.BROWSE + } +} + +// 获取浏览记录配置的工具函数 +export function getBrowseRecordConfig(itemType) { + const configMap = { + 'dynamic': BROWSE_RECORD_CONFIG.DYNAMIC_ITEM, + 'rental': BROWSE_RECORD_CONFIG.RENTAL_ITEM, + 'job': BROWSE_RECORD_CONFIG.JOB_ITEM, + 'scenicSpot': BROWSE_RECORD_CONFIG.SCENIC_SPOT_ITEM, + 'gourmet': BROWSE_RECORD_CONFIG.GOURMET_ITEM, + 'activity': BROWSE_RECORD_CONFIG.ACTIVITY_ITEM, + 'carFindPerson': BROWSE_RECORD_CONFIG.CAR_FIND_PERSON_ITEM, + 'personFindCar': BROWSE_RECORD_CONFIG.PERSON_FIND_CAR_ITEM, + 'article': BROWSE_RECORD_CONFIG.ARTICLE_ITEM + } + + return configMap[itemType] || BROWSE_RECORD_CONFIG.DYNAMIC_ITEM +} + +// 创建浏览记录参数的工具函数 +export function createBrowseRecordParams(orderId, itemType = 'dynamic') { + const config = getBrowseRecordConfig(itemType) + + return { + orderId: orderId, + type: config.type, + category: config.category + } +} \ No newline at end of file diff --git a/mixins/visibilityObserver.js b/mixins/visibilityObserver.js new file mode 100644 index 0000000..2fa98d2 --- /dev/null +++ b/mixins/visibilityObserver.js @@ -0,0 +1,205 @@ +/** + * 可见性监听混入 + * 提供元素可见性监听功能,支持H5、微信小程序、App端 + * 用于实现浏览记录等功能 + */ +import { VIEWPORT_CONFIG, createBrowseRecordParams } from '@/config/browseConfig.js' + +export default { + data() { + return { + hasRecordedView: false, // 标记是否已记录浏览 + viewTimer: null, // 浏览计时器 + isInViewport: false, // 是否在可视区域内 + observer: null, // 观察者实例 + } + }, + + mounted() { + // 组件挂载后,使用Intersection Observer监听可见性 + this.observeVisibility() + }, + + beforeDestroy() { + // 组件销毁前清理observer和计时器 + this.cleanupObserver() + }, + + methods: { + /** + * 监听元素可见性 + * @param {String} selector - 要监听的元素选择器,默认为'.works' + * @param {Object} config - 配置参数 + */ + observeVisibility(selector = '.works', config = {}) { + const { + threshold = VIEWPORT_CONFIG.THRESHOLD, + dwellTime = VIEWPORT_CONFIG.DWELL_TIME, + margins = VIEWPORT_CONFIG.VIEWPORT_MARGINS + } = config + + // #ifdef H5 + if (typeof IntersectionObserver !== 'undefined') { + this.observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && !this.hasRecordedView) { + // 元素进入可视区域且未记录过浏览 + this.handleViewportEnter(dwellTime) + } else if (!entry.isIntersecting && this.isInViewport) { + // 元素离开可视区域 + this.handleViewportLeave() + } + }) + }, { + threshold: threshold + }) + + this.observer.observe(this.$el) + } else { + // 降级方案:延迟记录浏览 + setTimeout(() => { + if (!this.hasRecordedView) { + this.recordBrowseView() + } + }, dwellTime) + } + // #endif + + // #ifdef MP-WEIXIN + // 使用微信小程序原生的IntersectionObserver API + this.observer = wx.createIntersectionObserver(this, { + thresholds: [threshold], + initialRatio: 0, + observeAll: false + }) + + // 指定页面显示区域作为参照区域 + this.observer.relativeToViewport(margins) + + // 开始监听目标节点 + this.observer.observe(selector, (res) => { + if (res.intersectionRatio > threshold && !this.hasRecordedView) { + // 当元素达到配置阈值以上进入可视区域且未记录过浏览时,处理进入可视区域事件 + this.handleViewportEnter(dwellTime) + } else if (res.intersectionRatio <= threshold && this.isInViewport) { + // 元素离开可视区域 + this.handleViewportLeave() + } + }) + // #endif + + // #ifdef APP-PLUS + // App端使用uni-app的createIntersectionObserver + this.observer = uni.createIntersectionObserver(this, { + thresholds: [threshold], + initialRatio: 0, + observeAll: false + }) + + this.observer.relativeToViewport(margins) + + this.observer.observe(selector, (res) => { + if (res.intersectionRatio > threshold && !this.hasRecordedView) { + this.handleViewportEnter(dwellTime) + } else if (res.intersectionRatio <= threshold && this.isInViewport) { + this.handleViewportLeave() + } + }) + // #endif + }, + + /** + * 处理进入可视区域事件 + * @param {Number} dwellTime - 停留时间 + */ + handleViewportEnter(dwellTime = VIEWPORT_CONFIG.DWELL_TIME) { + if (this.hasRecordedView) return + + this.isInViewport = true + + // 设置延迟计时器,确保用户真正浏览了内容 + this.viewTimer = setTimeout(() => { + if (this.isInViewport && !this.hasRecordedView) { + this.recordBrowseView() + } + }, dwellTime) + }, + + /** + * 处理离开可视区域事件 + */ + handleViewportLeave() { + this.isInViewport = false + + // 清除计时器,如果用户快速滚动过去,不记录浏览 + if (this.viewTimer) { + clearTimeout(this.viewTimer) + this.viewTimer = null + } + }, + + /** + * 记录浏览行为 + * @param {String} itemType - 项目类型,默认为'dynamic' + * @param {String} itemId - 项目ID,默认从this.item.id获取 + */ + recordBrowseView(itemType = 'dynamic', itemId = null) { + const id = itemId || (this.item && this.item.id) + + if (this.hasRecordedView || !id) return + + this.hasRecordedView = true + + // 清除计时器 + if (this.viewTimer) { + clearTimeout(this.viewTimer) + this.viewTimer = null + } + + // 使用配置文件中的参数创建API请求参数 + const params = createBrowseRecordParams(id, itemType) + + // 调用浏览记录API + this.$api('addBrowseRecord', params, res => { + if (res.code === 200) { + console.log('浏览记录已保存:', id) + } + }) + }, + + /** + * 清理观察者和计时器 + */ + cleanupObserver() { + if (this.observer) { + // #ifdef H5 + if (typeof this.observer.disconnect === 'function') { + this.observer.disconnect() + } + // #endif + + // #ifdef MP-WEIXIN || APP-PLUS + if (typeof this.observer.disconnect === 'function') { + this.observer.disconnect() + } + // #endif + + this.observer = null + } + + if (this.viewTimer) { + clearTimeout(this.viewTimer) + this.viewTimer = null + } + }, + + /** + * 重置浏览记录状态 + */ + resetBrowseRecord() { + this.hasRecordedView = false + this.isInViewport = false + this.cleanupObserver() + } + } +} \ No newline at end of file diff --git a/pages.json b/pages.json index 8031522..d288324 100644 --- a/pages.json +++ b/pages.json @@ -209,6 +209,14 @@ "enablePullDownRefresh": false } }, + { + "path": "mine/browseHistory", + "style": { + "navigationBarTitleText": "浏览历史", + "enablePullDownRefresh": true, + "navigationStyle": "custom" + } + }, { "path": "profile/userProfile", "style": { diff --git a/pages_order/mine/browseHistory.vue b/pages_order/mine/browseHistory.vue new file mode 100644 index 0000000..7fa9e95 --- /dev/null +++ b/pages_order/mine/browseHistory.vue @@ -0,0 +1,255 @@ + + + + + \ No newline at end of file