mrr.sj.front/pages/jingxuan/selected-skills.vue

580 lines
17 KiB
Vue
Raw Permalink Normal View History

<template>
<view class="skill-market-page">
2026-06-05 13:09:01 +08:00
<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" class="nav-img-size">
</custom-navbar>
2026-06-01 13:54:01 +08:00
<view class="sticky-header-container" :style="{ top: navHeight + 'px' }">
2026-06-05 13:09:01 +08:00
2026-06-01 13:54:01 +08:00
<view class="official-author-block" v-if="authorInfo.show">
<view class="author-left">
<image class="author-avatar" :src="authorInfo.avatar" mode="aspectFill"></image>
<view class="author-info">
<text class="author-name">{{ authorInfo.name }}</text>
<text class="author-desc">发布了{{ authorInfo.videoCount }}条技能</text>
</view>
</view>
2026-06-05 13:09:01 +08:00
2026-06-01 13:54:01 +08:00
<view class="view-more-btn" v-if="!authorInfo.isOfficial" @click="goToBrandDetail">
<text>查看更多</text>
<view class="arrow-right"></view>
</view>
</view>
2026-06-01 13:54:01 +08:00
<view class="sort-bar-wrap">
<view class="sort-bar">
<view class="left-sorts">
<view class="sort-item" @tap="toggleSort('time')">
<text class="sort-text" :class="{ active: orderType === 1 || orderType === 2 }">发布时间</text>
<view class="arrows">
2026-06-15 14:01:12 +08:00
<image class="arrow-icon" :src="orderType === 2 ? iconPinkUp : iconGreyUp"></image>
<image class="arrow-icon" :src="orderType === 1 ? iconPinkDown : iconGreyDown"></image>
2026-06-01 13:54:01 +08:00
</view>
</view>
2026-06-05 13:09:01 +08:00
2026-06-01 13:54:01 +08:00
<view class="sort-item" @tap="toggleSort('view')">
<text class="sort-text" :class="{ active: orderType === 3 || orderType === 4 }">浏览量</text>
<view class="arrows">
2026-06-15 14:01:12 +08:00
<image class="arrow-icon" :src="orderType === 4 ? iconPinkUp : iconGreyUp"></image>
<image class="arrow-icon" :src="orderType === 3 ? iconPinkDown : iconGreyDown"></image>
2026-06-01 13:54:01 +08:00
</view>
</view>
</view>
2026-06-08 11:47:07 +08:00
<view class="right-filter" @tap="toggleFilterPanel" v-if="!authorInfo.show">
2026-06-01 13:54:01 +08:00
<text class="filter-text">筛选</text>
<image class="filter-icon" :src="filterIcon" :class="{ 'rotate': filterVisible }"></image>
</view>
</view>
2026-06-01 13:54:01 +08:00
<view class="filter-panel" :class="{ 'show': filterVisible }" catchtouchmove="true">
<view class="filter-mask" @tap="closeFilterPanel"></view>
<view class="filter-container">
<text class="group-title">课程类型</text>
<view class="course-filter">
<view class="course-type-item" :class="{ active: linkType === '' }" @tap="selectAndApply('')">全部</view>
<view class="course-type-item" :class="{ active: linkType === 1 }" @tap="selectAndApply(1)">平台课程</view>
<view class="course-type-item" :class="{ active: linkType === 2 }" @tap="selectAndApply(2)">品牌课程</view>
</view>
</view>
</view>
2026-06-05 13:09:01 +08:00
</view>
</view>
<view class="list-container">
<view v-if="skillList.length > 0">
2026-06-05 13:09:01 +08:00
<skill-card v-for="(item, index) in skillList" :key="item.id || index" :item="item"
@click="goToDetail(item)"></skill-card>
</view>
2026-06-05 13:09:01 +08:00
2026-05-28 16:09:50 +08:00
<view v-else>
<noData></noData>
</view>
</view>
2026-06-05 13:09:01 +08:00
<view style="height: 60rpx;"></view>
2026-06-05 13:09:01 +08:00
</view>
</template>
<script>
import request from "@/utils/request"
2026-06-05 13:09:01 +08:00
import SkillCard from "@/pages/home/components/skill-card.vue"
2026-05-28 16:09:50 +08:00
import noData from "@/components/noData/noData.vue";
const USE_REAL_API = true
export default {
2026-05-28 16:09:50 +08:00
components: { SkillCard, noData },
data() {
return {
navHeight: 0,
2026-06-05 13:09:01 +08:00
orderType: 0,
linkType: '',
2026-06-01 13:54:01 +08:00
// 【新增】:统一管理作者/官方/品牌信息
authorInfo: {
show: false, // 控制是否显示作者头部
isOfficial: false, // 区分是官方还是普通品牌
brandId: null, // 品牌ID
name: '', // 作者/品牌名称
avatar: '', // 头像
videoCount: 0 // 视频数量
},
2026-06-05 13:09:01 +08:00
filterVisible: false,
2026-06-05 13:09:01 +08:00
skillList: [],
page: 1,
limit: 10,
isFinished: false,
2026-06-05 13:09:01 +08:00
iconGreyUp: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e00ab2ac-bccf-42d8-820c-65727327e279.png',
iconGreyDown: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/dfffb601-6877-4608-a3eb-88695b05c6c2.png',
iconPinkUp: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/5bc84dff-e120-4d4b-a33e-25d68e065654.png',
iconPinkDown: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/ff2c2c9d-694a-4adf-b737-66b07f4a63ae.png',
2026-06-05 13:09:01 +08:00
mockDatabase: []
}
},
computed: {
filterIcon() {
2026-06-05 13:09:01 +08:00
return this.filterVisible
? 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e00ab2ac-bccf-42d8-820c-65727327e279.png'
: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/bfe7a104-bef8-4432-9487-42f05e3e9035.png'
}
},
2026-06-01 13:54:01 +08:00
// 接收页面跳转传过来的作者参数
onLoad(options) {
if (options && options.showAuthor === '1') {
this.authorInfo.show = true;
this.authorInfo.isOfficial = options.isOfficial === '1';
2026-06-05 13:09:01 +08:00
this.authorInfo.brandId = options.linkId || null;
2026-06-01 13:54:01 +08:00
// 用 decodeURIComponent 把转义的字符解析回正常的中文和链接
this.authorInfo.name = options.authorName ? decodeURIComponent(options.authorName) : '未知作者';
this.authorInfo.avatar = options.authorAvatar ? decodeURIComponent(options.authorAvatar) : 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/bbe832bf-aae6-4579-b3cc-246d252488c3';
this.authorInfo.videoCount = options.videoCount || 0;
// 强制设置筛选类型
2026-06-05 13:09:01 +08:00
this.linkType = this.authorInfo.isOfficial ? 1 : 2;
2026-06-01 13:54:01 +08:00
} else if (options && options.linkType !== undefined) {
this.linkType = Number(options.linkType);
}
const sysInfo = uni.getSystemInfoSync()
const sbh = sysInfo.statusBarHeight || 0
2026-06-05 13:09:01 +08:00
this.navHeight = sbh
this.initMockData()
this.fetchData(true)
},
onReachBottom() {
if (!this.isFinished) {
this.fetchData(false)
}
},
methods: {
goToSearch() {
uni.navigateTo({
2026-06-01 13:54:01 +08:00
url: '/pages/search_common/search?type=skill'
});
},
2026-06-04 13:54:54 +08:00
// 跳转到品牌详情页面
2026-06-01 13:54:01 +08:00
goToBrandDetail() {
if (this.authorInfo.brandId) {
uni.navigateTo({
2026-06-04 13:54:54 +08:00
url: `/pages/jingxuan/brand-detail?id=${this.authorInfo.brandId}`
});
2026-06-01 13:54:01 +08:00
} else {
uni.showToast({ title: '缺少品牌信息', icon: 'none' });
}
},
2026-06-05 13:09:01 +08:00
toggleSort(type) {
if (type === 'time') {
if (this.orderType === 1) this.orderType = 2
2026-06-15 14:01:12 +08:00
else if (this.orderType === 2) this.orderType = 0
else this.orderType = 1
} else if (type === 'view') {
if (this.orderType === 3) this.orderType = 4
2026-06-15 14:01:12 +08:00
else if (this.orderType === 4) this.orderType = 0
else this.orderType = 3
}
this.fetchData(true)
},
2026-06-05 13:09:01 +08:00
toggleFilterPanel() {
this.filterVisible = !this.filterVisible
},
closeFilterPanel() {
this.filterVisible = false
},
2026-06-05 13:09:01 +08:00
selectAndApply(typeVal) {
this.linkType = typeVal;
2026-06-01 13:54:01 +08:00
// 当通过本地筛选器切换时,关闭“专属展示区”,确保纯净筛选
2026-06-05 13:09:01 +08:00
this.authorInfo.show = false;
this.closeFilterPanel();
this.fetchData(true);
},
2026-06-05 13:09:01 +08:00
async fetchData(isReset = false) {
if (isReset) {
this.page = 1
this.isFinished = false
this.skillList = []
}
2026-06-05 13:09:01 +08:00
if (USE_REAL_API) {
uni.showLoading({ title: '加载中...' })
try {
const params = {
page: this.page,
2026-05-28 16:09:50 +08:00
limit: this.limit
}
2026-06-01 13:54:01 +08:00
if (this.orderType !== 0) params.order_type = this.orderType;
if (this.linkType !== '') params.link_type = this.linkType;
2026-06-05 13:09:01 +08:00
2026-06-01 13:54:01 +08:00
// 【核心接口预留】如果当前在看某个品牌的专属页需要传link_id给后端过滤
if (this.authorInfo.show && !this.authorInfo.isOfficial && this.authorInfo.brandId) {
params.link_id = this.authorInfo.brandId;
}
2026-06-05 13:09:01 +08:00
const res = await request.post('/sj/skill/list', params)
if ((res.code === 200 || res.state === 1) && res.data && res.data.list) {
const list = res.data.list.map(item => this.formatApiData(item))
if (list.length < this.limit) this.isFinished = true
this.skillList = isReset ? list : this.skillList.concat(list)
this.page++
} else {
this.isFinished = true
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
} else {
uni.showLoading({ title: '模拟加载中...' })
setTimeout(() => {
let list = [...this.mockDatabase]
2026-06-05 13:09:01 +08:00
2026-05-28 16:09:50 +08:00
if (this.linkType !== '') {
list = list.filter(item => item.link_type === this.linkType)
}
2026-06-01 13:54:01 +08:00
// 模拟品牌过滤
if (this.authorInfo.show && !this.authorInfo.isOfficial && this.authorInfo.brandId) {
2026-06-05 13:09:01 +08:00
list = list.filter(item => item.brand_id == this.authorInfo.brandId)
2026-06-01 13:54:01 +08:00
}
2026-06-05 13:09:01 +08:00
2026-05-28 16:09:50 +08:00
if (this.orderType !== 0) {
list.sort((a, b) => {
2026-06-05 13:09:01 +08:00
if (this.orderType === 1) return b.timestamp - a.timestamp
if (this.orderType === 2) return a.timestamp - b.timestamp
if (this.orderType === 3) return b.views - a.views
if (this.orderType === 4) return a.views - b.views
2026-05-28 16:09:50 +08:00
return 0
})
}
2026-06-05 13:09:01 +08:00
const start = (this.page - 1) * this.limit
const end = start + this.limit
const pageData = list.slice(start, end)
2026-06-05 13:09:01 +08:00
if (pageData.length < this.limit) this.isFinished = true
this.skillList = isReset ? pageData : this.skillList.concat(pageData)
this.page++
uni.hideLoading()
}, 500)
}
},
2026-06-05 13:09:01 +08:00
formatApiData(item) {
return {
id: item.id,
title: item.name,
coverSrc: item.cover_img || 'https://dummyimage.com/300x200/ccc/fff.png',
type: item.link_name || (item.link_type === 1 ? '平台课程' : '品牌课程'),
link_type: item.link_type,
views: item.browse_num || 0,
2026-06-05 13:09:01 +08:00
time: item.create_time ? this.formatTimeAgo(item.create_time) : '',
typeIcon: item.link_logo || ''
}
},
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]
},
goToDetail(item) {
if (item.id) {
uni.navigateTo({ url: `/pages/skill/detail?id=${item.id}` })
} else {
uni.showToast({ title: '详情页开发中', icon: 'none' })
}
},
initMockData() {
this.mockDatabase = [
2026-06-05 13:09:01 +08:00
{ id: 1, title: '穿戴甲的操作视频扫码验券操作教程', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '平台课程', link_type: 1, views: 836000, time: '7小时前', timestamp: Date.now() - 7 * 3600000 },
{ id: 2, title: '脸部按摩操作步骤详情', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '品牌课程', link_type: 2, brand_id: 101, views: 8334, time: '1天前', timestamp: Date.now() - 24 * 3600000 },
{ id: 3, title: '全身SPA的操作视频操作教程', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '平台课程', link_type: 1, views: 2344, time: '3天前', timestamp: Date.now() - 3 * 24 * 3600000 },
{ id: 4, title: '高级面部护理手法详解最新版', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '平台课程', link_type: 1, views: 836, time: '4月26日', timestamp: Date.now() - 30 * 24 * 3600000 },
{ id: 5, title: '光疗美甲进阶全方位解析', coverSrc: 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/e2220528-fdf6-4cad-a25d-ec6cbee1501e.jpg', type: '品牌课程', link_type: 2, brand_id: 101, views: 12000, time: '2小时前', timestamp: Date.now() - 2 * 3600000 }
]
}
}
}
</script>
<style lang="scss" scoped>
.skill-market-page {
min-height: 100vh;
background: #fff;
}
2026-06-01 13:54:01 +08:00
/* 统一吸顶容器 */
.sticky-header-container {
position: sticky;
background: #fff;
z-index: 101;
/* 确保整体吸顶时下方有稍微一点阴影或边框分割更好看,可根据 UI 需求开启 */
// border-bottom: 1rpx solid #F5F5F5;
}
/* 官方/品牌 作者展示块样式 */
.official-author-block {
display: flex;
align-items: center;
2026-06-01 13:54:01 +08:00
justify-content: space-between;
padding: 24rpx 30rpx;
background: #fff;
border-bottom: 10rpx solid #F5F5F5;
2026-06-01 13:54:01 +08:00
.author-left {
display: flex;
2026-06-01 13:54:01 +08:00
align-items: center;
2026-06-05 13:09:01 +08:00
2026-06-01 13:54:01 +08:00
.author-avatar {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
margin-right: 20rpx;
}
2026-06-01 13:54:01 +08:00
.author-info {
display: flex;
flex-direction: column;
.author-name {
font-weight: 500;
font-size: 28rpx;
color: #333333;
line-height: 40rpx;
margin-bottom: 4rpx;
}
.author-desc {
font-weight: 400;
font-size: 20rpx;
color: #999999;
line-height: 28rpx;
}
}
2026-06-01 13:54:01 +08:00
}
2026-06-01 13:54:01 +08:00
.view-more-btn {
display: flex;
align-items: center;
font-size: 24rpx;
2026-06-05 13:09:01 +08:00
color: #FF4767;
border: 2rpx solid #FF4767;
2026-06-04 13:54:54 +08:00
padding: 8rpx 20rpx;
border-radius: 45rpx;
2026-06-05 13:09:01 +08:00
2026-06-01 13:54:01 +08:00
.arrow-right {
width: 10rpx;
height: 10rpx;
2026-06-05 13:09:01 +08:00
border-top: 2rpx solid #FF4767;
border-right: 2rpx solid #FF4767;
2026-06-01 13:54:01 +08:00
transform: rotate(45deg);
margin-left: 6rpx;
}
}
}
.sort-bar-wrap {
2026-06-01 13:54:01 +08:00
/* 移除单独的 sticky由外层 container 统一控制 */
background: #fff;
2026-06-05 13:09:01 +08:00
position: relative;
}
.sort-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
2026-06-05 13:09:01 +08:00
.left-sorts {
display: flex;
align-items: center;
2026-05-28 13:32:28 +08:00
gap: 100rpx;
2026-06-05 13:09:01 +08:00
.sort-item {
display: flex;
align-items: center;
2026-06-05 13:09:01 +08:00
.sort-text {
font-size: 28rpx;
color: #333333;
margin-right: 6rpx;
font-weight: 400;
2026-06-05 13:09:01 +08:00
&.active {
color: #FF4767;
font-weight: 500;
}
}
2026-06-05 13:09:01 +08:00
.arrows {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
2026-06-05 13:09:01 +08:00
.arrow-icon {
width: 14rpx;
height: 8rpx;
margin: 2rpx 0;
}
}
}
}
.right-filter {
display: flex;
align-items: center;
2026-06-05 13:09:01 +08:00
.filter-text {
font-size: 26rpx;
color: #666666;
margin-right: 8rpx;
}
2026-06-05 13:09:01 +08:00
.filter-icon {
2026-06-05 13:09:01 +08:00
width: 24rpx;
height: 24rpx;
transition: transform 0.3s ease;
2026-06-05 13:09:01 +08:00
&.rotate {
transform: rotate(180deg);
2026-06-05 13:09:01 +08:00
width: 18rpx;
height: 12rpx;
}
}
}
}
.list-container {
padding: 0 24rpx 30rpx;
}
.filter-panel {
position: absolute;
2026-06-05 13:09:01 +08:00
top: 100%;
left: 0;
right: 0;
height: 100vh;
z-index: 100;
pointer-events: none;
overflow: hidden;
&.show {
pointer-events: auto;
2026-06-05 13:09:01 +08:00
.filter-mask {
opacity: 1;
}
.filter-container {
transform: translateY(0);
}
}
.filter-mask {
position: absolute;
2026-06-05 13:09:01 +08:00
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
2026-06-05 13:09:01 +08:00
.filter-container {
position: absolute;
2026-06-05 13:09:01 +08:00
top: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 0 0 24rpx 24rpx;
padding: 30rpx 30rpx 40rpx;
display: flex;
flex-direction: column;
transform: translateY(-100%);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.group-title {
font-weight: 500;
font-size: 28rpx;
color: #333333;
margin-bottom: 24rpx;
display: block;
}
.course-filter {
display: flex;
gap: 20rpx;
2026-06-05 13:09:01 +08:00
.course-type-item {
background: #F5F5F5;
color: #333333;
font-size: 26rpx;
padding: 12rpx 36rpx;
border-radius: 12rpx;
border: 1rpx solid transparent;
transition: all 0.2s;
2026-06-05 13:09:01 +08:00
display: flex;
align-items: center;
justify-content: center;
2026-06-05 13:09:01 +08:00
&.active {
background: #FFF0F2;
color: #FF4767;
border-color: #FFB0BD;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
2026-06-05 13:09:01 +08:00
.empty-icon {
width: 346rpx;
height: 373rpx;
}
}
2026-06-05 13:09:01 +08:00
::v-deep .icon-headle {
2026-06-05 13:09:01 +08:00
width: 32rpx !important;
height: 32rpx !important;
}
2026-06-05 13:09:01 +08:00
::v-deep .back-icon {
2026-06-05 13:09:01 +08:00
width: 28rpx !important;
height: 36rpx !important;
}
.nav-img-size::v-deep .icon-headle {
width: 36rpx !important;
height: 36rpx !important;
}
2026-06-15 14:01:12 +08:00
</style>