mrr.sj.front/pages/my/myFavorite.vue

678 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="collection-page">
<custom-navbar title="我的收藏" :showBack="true" backgroundColor="#FFFFFF" :show-headle="true" borderBottom="none"
headleSrc="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/51ebca5d-95c5-4fb8-8aa3-560a79100d25.png"
@onHeadleClick="goToSearch">
</custom-navbar>
<view class="nav-placeholder" :style="{ height: navHeight + 'px' }"></view>
<view class="tab-bar-wrap" :style="{ top: navHeight + 'px' }">
<view class="tab-bar">
<view class="tab-items">
<view class="tab-item" :class="{ active: currentTab === 'brand' }" @tap="switchTab('brand')">
收藏的品牌
</view>
<view class="tab-item" :class="{ active: currentTab === 'course' }" @tap="switchTab('course')">
收藏的课程
</view>
</view>
<view class="filter-area" @tap="toggleFilterPanel">
<text class="filter-text">筛选</text>
<image class="filter-icon" :src="filterIcon" mode="aspectFit" :class="{ 'rotate': filterVisible }"></image>
</view>
</view>
</view>
<view class="filter-panel" :class="{ 'show': filterVisible }" :style="{ top: filterTop + 'px' }"
catchtouchmove="true">
<view class="filter-mask" @tap="closeFilterPanel"></view>
<view class="filter-container">
<scroll-view scroll-y class="filter-scroll" v-if="currentTab === 'brand'">
<view class="filter-group" v-for="group in brandFilterData" :key="group.id">
<view class="group-title">{{ group.title }}</view>
<view class="pill-list">
<view class="pill-item" v-for="sub in group.children" :key="sub.id"
:class="{ active: selectedSecondIds.includes(sub.id) }" @tap="toggleSecondSelect(sub.id)">
{{ sub.title }}
</view>
</view>
</view>
</scroll-view>
<view v-else class="course-filter">
<view class="course-type-item" :class="{ active: selectedCourseType === 'platform' }"
@tap="selectCourseType('platform')">
平台课程
</view>
<view class="course-type-item" :class="{ active: selectedCourseType === 'brand' }"
@tap="selectCourseType('brand')">
品牌课程
</view>
</view>
<view class="filter-footer">
<view class="reset-btn" @tap="resetFilter">重置</view>
<view class="confirm-btn" @tap="applyFilterAndClose">确定</view>
</view>
</view>
</view>
<view class="collection-content" v-if="currentTab === 'brand'">
<view class="brand-grid" v-if="filteredBrandList.length > 0">
<brand-card class="brand-card-wrap" v-for="(item, index) in filteredBrandList" :key="index" :item="item"
:tagKeys="item.tagKeys" @click="goToBrandDetail(item)"></brand-card>
</view>
<view class="empty-state" v-else>
<image class="empty-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/da814ede-1adc-4f90-8cdf-5357a12fab27.png">
</image>
</view>
</view>
<view class="collection-content" v-else>
<view class="course-list" v-if="filteredCourseList.length > 0">
<skill-card v-for="(item, index) in filteredCourseList" :key="index" :item="item"></skill-card>
</view>
<view class="empty-state" v-else>
<image class="empty-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/da814ede-1adc-4f90-8cdf-5357a12fab27.png">
</image>
</view>
</view>
<view style="height: 120rpx;"></view>
<TabBar :selected="3"></TabBar>
</view>
</template>
<script>
import TabBar from "@/components/tabbar/tabbar.vue"
import BrandCard from "@/pages/home/components/brand-card.vue"
import SkillCard from "@/pages/home/components/skill-card.vue"
import request from "@/utils/request"
// 数据模式开关true=调用真实接口false=使用模拟数据
const USE_REAL_API = true
export default {
components: {
TabBar,
BrandCard,
SkillCard
},
data() {
return {
navHeight: 0, // 动态计算的导航栏高度
filterTop: 0, // 动态计算的筛选框顶部起点
currentTab: 'brand',
// 筛选相关
filterVisible: false,
// 品牌筛选下拉数据 (合并一级和二级)
brandFilterData: [],
selectedSecondIds: [], // 选中的二级ID数组
// 课程筛选
selectedCourseType: '', // 'platform' 或 'brand'
// 原始数据(收藏的数据)
rawBrandList: [],
rawCourseList: [],
// 展示数据(过滤后)
filteredBrandList: [],
filteredCourseList: []
}
},
computed: {
filterIcon() {
// 这里的图标链接可以统一成一个配合CSS的旋转动画更好看
return 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e00ab2ac-bccf-42d8-820c-65727327e279.png'
}
},
onLoad() {
// 精准计算吸顶高度与下拉框起点防止蒙版遮挡白色的Tab区
const sysInfo = uni.getSystemInfoSync()
const sbh = sysInfo.statusBarHeight || 0
this.navHeight = sbh
// Tab栏自身的高度大约为 106rpx转化为 px 叠加上去
this.filterTop = this.navHeight + uni.upx2px(106)
this.loadFilterCategories()
this.loadData()
},
methods: {
// 跳转搜索页
goToSearch() {
// this.currentTab 的值刚好是 'brand' 或 'course'
uni.navigateTo({
url: `/pages/search_common/search?type=collection_${this.currentTab}`
});
},
goBack() {
uni.navigateBack()
},
addAddress() {
// 外部方法占位
},
switchTab(tab) {
if (this.currentTab === tab) return
this.currentTab = tab
// 重置筛选不同tab筛选条件独立
if (tab === 'brand') {
this.resetBrandFilter()
} else {
this.resetCourseFilter()
}
this.loadData()
this.closeFilterPanel()
},
// 筛选面板控制
toggleFilterPanel() {
this.filterVisible = !this.filterVisible
},
closeFilterPanel() {
this.filterVisible = false
},
// 加载一级分类并自动组装其对应的二级分类,兼容 state === 1
async loadFilterCategories() {
try {
const res = await request.post('/sj/firstclass', {})
if ((res.code === 200 || res.state === 1) && res.data) {
const firstClasses = res.data;
// 并发请求所有的二级分类
const promises = firstClasses.map(async (first) => {
try {
const subRes = await request.post('/sj/secondclass', { id: first.id })
return {
id: first.id,
title: first.title,
children: ((subRes.code === 200 || subRes.state === 1) && subRes.data) ? subRes.data : []
}
} catch (e) {
return { id: first.id, title: first.title, children: [] }
}
});
const filterData = await Promise.all(promises);
// 过滤掉没有子分类的项,避免面板出现光秃秃的标题
this.brandFilterData = filterData.filter(group => group.children.length > 0);
} else {
this.brandFilterData = []
}
} catch (error) {
console.error('获取分类字典失败', error)
}
},
toggleSecondSelect(secId) {
const index = this.selectedSecondIds.indexOf(secId)
if (index === -1) {
this.selectedSecondIds.push(secId)
} else {
this.selectedSecondIds.splice(index, 1)
}
},
// 课程筛选
selectCourseType(type) {
// 点击已选中的则取消选中
if (this.selectedCourseType === type) {
this.selectedCourseType = ''
} else {
this.selectedCourseType = type
}
},
// 重置品牌筛选
resetBrandFilter() {
this.selectedSecondIds = []
this.applyBrandFilter()
},
resetCourseFilter() {
this.selectedCourseType = ''
this.applyCourseFilter()
},
resetFilter() {
if (this.currentTab === 'brand') {
this.resetBrandFilter()
} else {
this.resetCourseFilter()
}
},
applyFilterAndClose() {
if (this.currentTab === 'brand') {
this.applyBrandFilter()
} else {
this.applyCourseFilter()
}
this.closeFilterPanel()
},
// 品牌过滤逻辑根据二级分类ID相交判断
applyBrandFilter() {
if (!this.rawBrandList.length) {
this.filteredBrandList = []
return
}
if (this.selectedSecondIds.length === 0) {
this.filteredBrandList = [...this.rawBrandList]
} else {
this.filteredBrandList = this.rawBrandList.filter(brand => {
return brand.secondClassIds && brand.secondClassIds.some(id => this.selectedSecondIds.includes(id))
})
}
},
// 课程过滤逻辑(根据课程类型)
applyCourseFilter() {
if (!this.rawCourseList.length) {
this.filteredCourseList = []
return
}
if (!this.selectedCourseType) {
this.filteredCourseList = [...this.rawCourseList]
} else {
this.filteredCourseList = this.rawCourseList.filter(course => {
if (this.selectedCourseType === 'platform') return course.type === '平台课程'
if (this.selectedCourseType === 'brand') return course.type === '品牌课程'
return true
})
}
},
// 加载原始数据根据当前Tab
loadData() {
if (this.currentTab === 'brand') {
this.loadBrandList()
} else {
this.loadCourseList()
}
},
async loadBrandList() {
if (USE_REAL_API) {
await this.fetchRealBrandList()
} else {
this.mockBrandList()
}
this.applyBrandFilter()
},
async loadCourseList() {
if (USE_REAL_API) {
await this.fetchRealCourseList()
} else {
this.mockCourseList()
}
this.applyCourseFilter()
},
// ============ 真实接口调用 ============
async fetchRealBrandList() {
try {
// 1. 接口路径换成真实的品牌收藏列表
const res = await request.post('/sj/brand/collectList', { page: 1, limit: 100 })
if ((res.code === 200 || res.state === 1) && res.data && res.data.list) {
// 2. 不需要前端再做 filter(item => item.collect_state === 1) 了,直接用!
this.rawBrandList = res.data.list.map(item => {
let tags = []
if (item.franchise_state == 1) tags.push('franchise')
if (item.student_state == 1) tags.push('recruit')
if (tags.length === 0) tags.push('more')
return {
id: item.id,
name: item.name,
bgSrc: item.cover_img || 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/default.jpg',
avatar: item.logo_img || 'https://dummyimage.com/100x100/ccc/fff.png&text=Avatar',
tagKeys: tags,
// 3. 强制转字符串,防止后端返回纯数字导致 split 报错,同时兼容后端没返回此字段的情况
secondClassIds: item.second_class_ids ? String(item.second_class_ids).split(',').map(Number) : []
}
})
console.log('成功获取品牌收藏列表:', this.rawBrandList)
} else {
this.rawBrandList = []
}
} catch (error) {
console.error('获取品牌收藏失败详情:', error)
}
},
async fetchRealCourseList() {
try {
// 1. 接口路径换成真实的技能收藏列表
const res = await request.post('/sj/skill/collectList', { page: 1, limit: 100 })
if ((res.code === 200 || res.state === 1) && res.data && res.data.list) {
// 2. 同样不需要 filter直接 map 映射数据
this.rawCourseList = res.data.list.map(item => {
return {
id: item.id,
title: item.name,
coverSrc: item.cover_img,
type: item.link_name || (item.link_type == 1 ? '平台课程' : '品牌课程'),
link_type: item.link_type,
views: item.browse_num || 0,
// 3. 使用安全的时间格式化
time: item.create_time ? this.formatTimeAgo(item.create_time) : '',
typeIcon: item.link_logo || ''
}
})
console.log('成功获取课程收藏列表:', this.rawCourseList)
} else {
this.rawCourseList = []
}
} catch (error) {
console.error('获取课程收藏失败', error)
}
},
formatTimeAgo(timeStr) {
if (!timeStr) return '';
const timeStrSafe = String(timeStr);
const now = new Date()
const time = new Date(timeStrSafe.replace(/-/g, '/'))
const diff = Math.floor((now - time) / 1000)
if (diff < 3600) return Math.floor(diff / 60) + '分钟前'
if (diff < 86400) return Math.floor(diff / 3600) + '小时前'
if (diff < 604800) return Math.floor(diff / 86400) + '天前'
return timeStrSafe.split(' ')[0]
},
goToBrandDetail(item) {
if (item.id) {
uni.navigateTo({ url: `/pages/brand/detail?id=${item.id}` })
}
},
// ============ 模拟数据兜底 ============
mockBrandList() {
this.rawBrandList = [
{
id: 1, name: '脸缘品牌', bgSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg',
avatar: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg',
tagKeys: ['franchise', 'recruit'], secondClassIds: [76, 77]
},
{
id: 2, name: '美肌工坊', bgSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg',
avatar: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg',
tagKeys: ['franchise'], secondClassIds: [78]
},
{
id: 3, name: '悦容堂', bgSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg',
avatar: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg',
tagKeys: ['recruit'], secondClassIds: [201, 202]
}
]
},
mockCourseList() {
this.rawCourseList = [
{ id: 1, title: '穿戴甲的操作视频扫码验券操作教程', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '平台课程', views: 836000, time: '7小时前' },
{ id: 2, title: '全身SPA的操作视频操作教程', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '平台课程', views: 2344, time: '3天前' },
{ id: 3, title: '高级面部护理手法详解', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '品牌课程', views: 56700, time: '1周前' }
]
}
}
}
</script>
<style lang="scss" scoped>
.collection-page {
min-height: 100vh;
background: #fff;
}
/* 将页面左右 padding 转移到内容区,保证 Tab 背景贯穿全屏 */
.tab-bar-wrap {
position: sticky;
/* 吸顶,保证滑动页面时一直显示在最前面 */
background: #fff;
padding: 10rpx 24rpx;
z-index: 101;
}
.tab-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: transparent;
padding: 10rpx 0;
.tab-items {
display: flex;
align-items: center;
.tab-item {
font-weight: 400;
font-size: 26rpx;
color: #777777;
background: #f5f5f5;
border-radius: 27rpx;
margin-right: 40rpx;
padding: 9rpx 24rpx;
transition: all 0.3s ease;
&.active {
color: #FF4767;
background: #FFECEF;
border: 1rpx solid #FFB0BD;
border-radius: 29rpx;
}
}
}
.filter-area {
display: flex;
align-items: center;
padding: 10rpx 0;
.filter-text {
font-size: 24rpx;
color: #666666;
margin-right: 6rpx;
}
.filter-icon {
width: 16rpx;
height: 10rpx;
transition: transform 0.3s ease;
/* 展开时图标倒转 */
&.rotate {
transform: rotate(180deg);
}
}
}
}
/* 核心抽屉动画布局 */
.filter-panel {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
/* 低于 Tab区的101保证隐藏时躲在其后 */
pointer-events: none;
/* 面板关闭时,穿透点击到底部列表 */
overflow: hidden;
/* 防止抽屉向上滑出时外溢 */
&.show {
pointer-events: auto;
/* 打开时恢复点击拦截 */
.filter-mask {
opacity: 1;
}
.filter-container {
transform: translateY(50rpx);
/* 滑出 */
}
}
.filter-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.filter-container {
position: absolute;
top: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 0 0 30rpx 30rpx;
padding: 40rpx 30rpx;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.05);
transform: translateY(-100%);
/* 默认隐藏在上方即Tab背后 */
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
/* 垂直分组设计的样式图3 */
.filter-scroll {
max-height: 50vh;
}
.filter-group {
margin-bottom: 30rpx;
.group-title {
font-weight: 500;
font-size: 28rpx;
color: #333333;
line-height: 40rpx;
margin-bottom: 20rpx;
}
.pill-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
}
/* 通用胶囊按钮 */
.pill-item {
width: calc(25% - 16rpx);
/* 25%是一行四个,减去间距 */
height: 58rpx;
background: #F5F5F5;
font-weight: 400;
font-size: 26rpx;
color: #333333;
line-height: 37rpx;
/* 使用 flex 居中 */
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
transition: all 0.3s;
box-sizing: border-box;
/* 防止 padding 会撑大盒子 */
&.active {
background: #FFF0F2;
color: #FF4767;
border: 1rpx solid #FFB0BD;
}
}
.course-filter {
display: flex;
gap: 20rpx;
/* padding: 20rpx 0 40rpx; */
.course-type-item {
background: #F5F5F5;
color: #666666;
font-size: 26rpx;
padding: 12rpx 40rpx;
border-radius: 12rpx;
&.active {
background: #FFF0F2;
color: #FF4767;
}
}
}
.filter-footer {
display: flex;
justify-content: space-between;
margin-top: 20rpx;
padding-top: 20rpx;
/* border-top: 1rpx solid #F5F5F5; */
.reset-btn,
.confirm-btn {
width: 47%;
text-align: center;
padding: 20rpx 0;
border-radius: 40rpx;
font-size: 28rpx;
}
.reset-btn {
background: #F5F5F5;
color: #666;
}
.confirm-btn {
background: #FF4767;
color: #fff;
}
}
/* 内容区缩进补充 */
.collection-content {
padding: 0 24rpx;
margin-top: 20rpx;
}
.brand-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.brand-card-wrap {
width: 49.2%;
margin-bottom: 20rpx;
}
}
.course-list {
display: flex;
flex-direction: column;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 346rpx;
height: 373rpx;
}
}
::v-deep .icon-headle {
width: 32rpx !important;
height: 32rpx !important;
}
</style>