我的收藏页面+技能集市组件

This commit is contained in:
BAKEYi 2026-05-26 18:06:07 +08:00
parent f6ed231d12
commit 2f9055454c
5 changed files with 859 additions and 31 deletions

View File

@ -985,7 +985,8 @@
{
"path": "pages/my/myFavorite",
"style": {
"navigationBarTitleText": "我的收藏"
"navigationBarTitleText": "我的收藏",
"navigationStyle": "custom"
}
}

View File

@ -0,0 +1,190 @@
<template>
<view class="skill-item" @click="handleClick">
<view class="cover-box">
<image class="cover" :src="item.coverSrc" mode="aspectFill"></image>
<image
class="play-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/5c1ddfef-a200-424a-a9dc-03d9395568f6.png"
></image>
</view>
<view class="skill-info">
<view class="skill-title line-2">{{ item.title }}</view>
<view class="skill-type" :class="typeClass">
<image class="type-icon" :src="item.typeIcon || defaultTypeIcon"></image>
<text class="type-text">{{ item.type || '平台课程' }}</text>
</view>
<view class="skill-stats">
<image
class="view-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/2409b64a-56bb-4d9e-a4cc-5b3444fcf097.png"
></image>
{{ formatViews(item.views) }}观看 · {{ item.time }}
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SkillItem',
props: {
item: {
type: Object,
required: true,
default: () => ({
id: null,
title: '',
coverSrc: '',
type: '平台课程',
views: 0,
time: '',
typeIcon: ''
})
}
},
data() {
return {
defaultTypeIcon: 'https://dummyimage.com/30x30/ccc/fff.png&text=I' //
}
},
computed: {
typeClass() {
const typeMap = {
'平台课程': 'platform-course',
'脸缘品牌': 'brand-course',
'品牌课程': 'brand-course'
}
return typeMap[this.item.type] || 'platform-course'
}
},
methods: {
formatViews(views) {
if (!views) return '0'
if (views >= 10000) {
return (views / 10000).toFixed(1) + 'w'
}
return views.toString()
},
handleClick() {
// /
if (this.item.id) {
uni.navigateTo({
url: `/pages/skill/detail?id=${this.item.id}`
})
} else {
uni.showToast({ title: '详情页开发中', icon: 'none' })
}
}
}
}
</script>
<style lang="scss" scoped>
.skill-item {
display: flex;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.cover-box {
position: relative;
width: 224rpx;
height: 126rpx;
border-radius: 16rpx;
overflow: hidden;
margin-right: 24rpx;
flex-shrink: 0;
.cover {
width: 100%;
height: 100%;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 39rpx;
height: 39rpx;
}
}
.skill-info {
flex: 1;
display: flex;
min-width: 0;
flex-direction: column;
justify-content: space-between;
.skill-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
font-size: 28rpx;
color: #333;
font-weight: 400;
line-height: 40rpx;
}
.skill-type {
display: flex;
align-items: center;
font-size: 22rpx;
margin-top: 10rpx;
.type-icon {
width: 26rpx;
height: 26rpx;
margin-right: 8rpx;
border-radius: 50%;
}
.type-text {
border-radius: 7rpx;
font-weight: 400;
font-size: 24rpx;
line-height: 33rpx;
padding: 2rpx 10rpx;
}
&.platform-course .type-text {
background: #f9d3d760;
color: #E55463;
}
&.brand-course .type-text {
background: #e6f7ff;
color: #1890ff;
}
}
.skill-stats {
font-size: 22rpx;
color: #999;
margin-top: 10rpx;
display: flex;
align-items: center;
.view-icon {
width: 26rpx;
height: 22rpx;
margin-right: 8rpx;
margin-top: -2rpx;
}
}
}
}
.line-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>

View File

@ -129,20 +129,11 @@
<view class="header-right">更多技能 <image class="more-right-icon" src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/ee119e24-1cb2-43df-86e6-61af8e9fd0ad.png"></image> </view>
</view>
<view class="skill-list">
<view class="skill-item" v-for="(item, index) in skills" :key="index">
<view class="cover-box">
<image class="cover" :src="item.coverSrc"></image>
<image class="play-icon" src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/5c1ddfef-a200-424a-a9dc-03d9395568f6.png"></image>
</view>
<view class="skill-info">
<view class="skill-title line-2">{{ item.title }}</view>
<view class="skill-type" :class="item.typeClass">
<image class="type-icon" src="https://dummyimage.com/30x30/ccc/fff.png&text=I"></image>
<text class="type-text">{{ item.type }}</text>
</view>
<view class="skill-stats"> <image class="view-icon" src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/2409b64a-56bb-4d9e-a4cc-5b3444fcf097.png"></image>{{ item.views }}观看 · {{ item.time }}</view>
</view>
</view>
<skill-card
v-for="(item, index) in skills"
:key="index"
:item="item"
></skill-card>
</view>
</view>
@ -158,6 +149,7 @@ import request from "@/utils/request";
import filters from "./components/filters.vue";
import CustomNavbar from "@/components/custom-navbar/custom-navbar.vue";
import BrandCard from "./components/brand-card.vue";
import SkillCard from "./components/skill-card.vue";
export default {
components: {
@ -165,7 +157,8 @@ export default {
projectTabs,
CustomNavbar,
TabBar,
BrandCard
BrandCard,
SkillCard
},
data() {
return {

View File

@ -175,7 +175,7 @@ export default {
},
{
title: "店铺装修",
url: "https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/4b558456-2e38-4185-8713-d1f0e6645820.png",
url: "https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/4b558456-2e38-4185-8713-d1f0e6645820.png",
// https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e6e72039-eec4-414c-8a86-aad404602962.png
path: "/pages/shop/staff/distribution-service",
go_id_type: 1,

View File

@ -1,22 +1,666 @@
<template>
<view>
</view>
<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="addAddress">
</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>
export default {
data() {
return {
}
},
methods: {
}
}
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: {
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 syrId = uni.getStorageSync('syrId') || 1;
const res = await request.post('/sj/firstclass', { id: syrId })
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 {
const res = await request.post('/sj/brand/list', { page: 1, limit: 100 })
if ((res.code === 200 || res.state === 1) && res.data && res.data.list) {
const collectedBrands = res.data.list.filter(item => item.collect_state === 1)
this.rawBrandList = collectedBrands.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,
secondClassIds: item.second_class_ids ? item.second_class_ids.split(',').map(Number) : []
}
})
} else {
this.rawBrandList = []
}
} catch (error) {
console.error('获取品牌收藏失败', error)
}
},
async fetchRealCourseList() {
try {
const res = await request.post('/sj/skill/list', { page: 1, limit: 100 })
if ((res.code === 200 || res.state === 1) && res.data && res.data.list) {
const collectedCourses = res.data.list.filter(item => item.collect_state === 1)
this.rawCourseList = collectedCourses.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,
time: item.create_time ? this.formatTimeAgo(item.create_time) : '',
typeIcon: item.link_logo || ''
}
})
} else {
this.rawCourseList = []
}
} catch (error) {
console.error('获取课程收藏失败', error)
}
},
formatTimeAgo(timeStr) {
const now = new Date()
const time = new Date(timeStr.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 timeStr.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>
<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>