- 新增浏览记录配置文件,统一管理类型和分类枚举 - 添加可视性观察混入,支持多端元素可见性检测 - 创建浏览历史页面,展示用户浏览记录 - 移除API接口中的limit参数限制 - 优化动态列表项组件,集成浏览记录功能master
| @ -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 | |||||
| } | |||||
| } | |||||
| @ -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() | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,255 @@ | |||||
| <template> | |||||
| <view class="browse-history"> | |||||
| <navbar leftClick @leftClick="$utils.navigateBack" title="浏览历史" /> | |||||
| <!-- 筛选标签 --> | |||||
| <view class="filter-tabs"> | |||||
| <uv-tabs | |||||
| :list="filterTabs" | |||||
| :activeStyle="{color: '#000', fontWeight: 900, fontSize: '32rpx'}" | |||||
| lineColor="#5baaff" | |||||
| lineHeight="6rpx" | |||||
| lineWidth="50rpx" | |||||
| keyName="title" | |||||
| @click="onFilterChange" | |||||
| /> | |||||
| </view> | |||||
| <!-- 浏览记录列表 --> | |||||
| <view class="history-list" v-if="historyList.length > 0"> | |||||
| <view class="history-item" | |||||
| v-for="(item, index) in historyList" | |||||
| :key="index" | |||||
| @click="goToDetail(item)"> | |||||
| <view class="item-content"> | |||||
| <!-- 动态内容 --> | |||||
| <view class="dynamic-info"> | |||||
| <view class="title" v-html="$utils.stringFormatHtml(item.title)"></view> | |||||
| <view class="meta-info"> | |||||
| <text class="author">{{ item.userName }}</text> | |||||
| <text class="time">{{ $dayjs(item.createTime).format('MM-DD HH:mm') }}</text> | |||||
| </view> | |||||
| </view> | |||||
| <!-- 缩略图 --> | |||||
| <view class="thumbnail" v-if="item.image"> | |||||
| <image :src="item.image.split(',')[0]" mode="aspectFill" /> | |||||
| </view> | |||||
| </view> | |||||
| <!-- 浏览时间 --> | |||||
| <view class="browse-time"> | |||||
| 浏览于 {{ $dayjs(item.browseTime).format('YYYY-MM-DD HH:mm') }} | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <!-- 空状态 --> | |||||
| <view class="empty-state" v-else-if="!loading"> | |||||
| <uv-empty | |||||
| text="暂无浏览记录" | |||||
| icon="history" | |||||
| iconSize="120" | |||||
| /> | |||||
| </view> | |||||
| <!-- 加载更多 --> | |||||
| <uv-load-more | |||||
| :status="loadStatus" | |||||
| @loadmore="loadMore" | |||||
| v-if="historyList.length > 0" | |||||
| /> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import navbar from '@/components/base/navbar.vue' | |||||
| import mixinsList from '@/mixins/loadList.js' | |||||
| import { BROWSE_RECORD_CATEGORY } from '@/config/browseConfig.js' | |||||
| export default { | |||||
| mixins: [mixinsList], | |||||
| components: { | |||||
| navbar | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| mixinsListApi: 'getBrowseRecordPage', | |||||
| filterTabs: [ | |||||
| { id: '', title: '全部' }, | |||||
| { id: 0, title: '动态' }, | |||||
| { id: 1, title: '租房' }, | |||||
| { id: 2, title: '工作' }, | |||||
| { id: 5, title: '活动' }, | |||||
| { id: 8, title: '文章' } | |||||
| ], | |||||
| currentFilter: '', | |||||
| historyList: [], | |||||
| loading: false, | |||||
| loadStatus: 'loadmore' | |||||
| } | |||||
| }, | |||||
| onLoad() { | |||||
| this.loadHistoryData() | |||||
| }, | |||||
| onPullDownRefresh() { | |||||
| this.refreshData() | |||||
| }, | |||||
| onReachBottom() { | |||||
| this.loadMore() | |||||
| }, | |||||
| methods: { | |||||
| // 筛选类型改变 | |||||
| onFilterChange(item) { | |||||
| this.currentFilter = item.id | |||||
| this.queryParams.type = item.id === '' ? undefined : item.id | |||||
| this.refreshList() | |||||
| }, | |||||
| // 加载浏览历史数据 | |||||
| loadHistoryData() { | |||||
| this.loading = true | |||||
| this.$api('getBrowseRecordPage', { | |||||
| ...this.queryParams, | |||||
| category: BROWSE_RECORD_CATEGORY.BROWSE // 只获取浏览记录 | |||||
| }, res => { | |||||
| this.loading = false | |||||
| uni.stopPullDownRefresh() | |||||
| if (res.code === 200) { | |||||
| const newData = res.result.records || res.result || [] | |||||
| if (this.queryParams.pageNum === 1) { | |||||
| this.historyList = newData | |||||
| } else { | |||||
| this.historyList = [...this.historyList, ...newData] | |||||
| } | |||||
| // 更新加载状态 | |||||
| if (newData.length < this.queryParams.pageSize) { | |||||
| this.loadStatus = 'nomore' | |||||
| } else { | |||||
| this.loadStatus = 'loadmore' | |||||
| } | |||||
| } | |||||
| }) | |||||
| }, | |||||
| // 刷新数据 | |||||
| refreshData() { | |||||
| this.queryParams.pageNum = 1 | |||||
| this.loadStatus = 'loadmore' | |||||
| this.loadHistoryData() | |||||
| }, | |||||
| // 加载更多 | |||||
| loadMore() { | |||||
| if (this.loadStatus === 'loadmore') { | |||||
| this.queryParams.pageNum++ | |||||
| this.loadStatus = 'loading' | |||||
| this.loadHistoryData() | |||||
| } | |||||
| }, | |||||
| // 跳转到详情页 | |||||
| goToDetail(item) { | |||||
| const typeRouteMap = { | |||||
| 0: '/pages_order/post/postDetail', | |||||
| 1: '/pages_order/renting/rentingDetail', | |||||
| 2: '/pages_order/work/workDetail', | |||||
| 5: '/pages_order/activity/activityDetail', | |||||
| 8: '/pages_order/article/articleDetail' | |||||
| } | |||||
| const route = typeRouteMap[item.type] || '/pages_order/post/postDetail' | |||||
| uni.navigateTo({ | |||||
| url: `${route}?id=${item.orderId}` | |||||
| }) | |||||
| } | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .browse-history { | |||||
| background-color: #f5f5f5; | |||||
| min-height: 100vh; | |||||
| } | |||||
| .filter-tabs { | |||||
| background-color: #fff; | |||||
| padding: 20rpx 0; | |||||
| margin-bottom: 20rpx; | |||||
| } | |||||
| .history-list { | |||||
| padding: 0 20rpx; | |||||
| } | |||||
| .history-item { | |||||
| background-color: #fff; | |||||
| border-radius: 20rpx; | |||||
| padding: 30rpx; | |||||
| margin-bottom: 20rpx; | |||||
| box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); | |||||
| .item-content { | |||||
| display: flex; | |||||
| align-items: flex-start; | |||||
| .dynamic-info { | |||||
| flex: 1; | |||||
| margin-right: 20rpx; | |||||
| .title { | |||||
| font-size: 32rpx; | |||||
| color: #333; | |||||
| line-height: 1.5; | |||||
| margin-bottom: 15rpx; | |||||
| display: -webkit-box; | |||||
| -webkit-box-orient: vertical; | |||||
| -webkit-line-clamp: 2; | |||||
| overflow: hidden; | |||||
| text-overflow: ellipsis; | |||||
| } | |||||
| .meta-info { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| font-size: 26rpx; | |||||
| color: #999; | |||||
| .author { | |||||
| margin-right: 20rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| .thumbnail { | |||||
| width: 120rpx; | |||||
| height: 120rpx; | |||||
| border-radius: 12rpx; | |||||
| overflow: hidden; | |||||
| flex-shrink: 0; | |||||
| image { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| } | |||||
| .browse-time { | |||||
| margin-top: 20rpx; | |||||
| font-size: 24rpx; | |||||
| color: #999; | |||||
| text-align: right; | |||||
| } | |||||
| } | |||||
| .empty-state { | |||||
| padding: 100rpx 0; | |||||
| text-align: center; | |||||
| } | |||||
| </style> | |||||