Feat:体验券领取

This commit is contained in:
linxi
2026-03-12 18:05:38 +08:00
parent 6b849c5300
commit 605cd4f054
4 changed files with 497 additions and 1 deletions
+5 -1
View File
@@ -159,4 +159,8 @@ export function hadView(data) {
export function getShareImg(id) {
return request.get('/landmarkGoods/poster/' + id, {}, {login: false})
}
}
export function getLatestExperienceCoupon() {
return request.get('/experienceCoupon/latest', {}, {login: true})
}
+7
View File
@@ -737,6 +737,13 @@
"navigationBarTitleText": "时令尝鲜"
}
},
{
"path": "views/experienceCoupon",
"style": {
"navigationBarTitleText": "体验券详情",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
{
"path": "views/submitOrder",
"style": {
+31
View File
@@ -85,6 +85,17 @@
</view>
</scroll-view>
</view>
<view
v-if="hasExperienceCoupon && experienceCouponImage"
class="experience-coupon-banner"
@click="goExperienceCoupon"
>
<image
:src="experienceCouponImage"
mode="widthFix"
class="experience-coupon-img"
/>
</view>
<!-- 千县名品和地标好物 -->
<view
v-if="landmarkGoodsImage.image || countyFamousImage.image"
@@ -439,6 +450,10 @@ export default {
bastRightList: [],
// 尖货推荐横幅
bannerList: [],
// 体验券
hasExperienceCoupon: 0,
hasUnusedExperienceCoupon: 0,
experienceCouponImage: '',
// 老用户弹窗
isOldUser: true,
oldUserPopup: {},
@@ -1381,6 +1396,9 @@ export default {
toNewZone() {
this.$yrouter.push('/pages/home/newZone')
},
goExperienceCoupon() {
this.$yrouter.push('/pkg_product/views/experienceCoupon')
},
init() {
sharpBannar().then(res => {
const { data } = res
@@ -1429,6 +1447,9 @@ export default {
_this.$set(_this, 'contentHomestay', data.contentHomestayV2 || [])
_this.$set(_this, 'contentFestival', data.contentFestivalV2 || [])
_this.$set(_this, 'newZone', data.newGoods || [])
_this.$set(_this, 'hasExperienceCoupon', data.hasExperienceCoupon || 0)
_this.$set(_this, 'hasUnusedExperienceCoupon', data.hasUnusedExperienceCoupon || 0)
_this.$set(_this, 'experienceCouponImage', data.experienceCouponImage || '')
_this.$set(_this, 'isOldUserPopupShow', data.isOldUserPopupShow || 0)
_this.indexVideo = data.indexVideo ? data.indexVideo.videoUrl : ''
_this.bastLeftList = []
@@ -1802,6 +1823,16 @@ export default {
}
}
}
.experience-coupon-banner {
position: relative;
z-index: 5;
padding-top: 16rpx;
.experience-coupon-img {
display: block;
width: 100%;
border-radius: 20rpx;
}
}
.hotel-list-wrap {
position: relative;
width: 344rpx;
+454
View File
@@ -0,0 +1,454 @@
<script>
import { getLatestExperienceCoupon } from '@/api/product'
import { getExperienceStoreCategoryList } from '@/api/join'
export default {
data() {
const webUrl = this.$VUE_APP_RESOURCES_URL
return {
webUrl,
headerImage: webUrl + '/experienceCoupon/header.png',
totalCount: 0,
selectedCount: 0,
remainNum: 0,
startTime: '',
endTime: '',
expireDate: '',
categoryList: [
{ id: 'all', name: '全部体验' }
],
activeCategoryId: 'all',
experienceList: [
{
id: 1,
title: 'Wave Restaurant',
subTitle: '一种咖啡 两种体验',
distance: '距选地点1.8km',
businessTime: '09:30-02:00',
tags: '两种体验咖啡',
categoryId: 'all',
image: webUrl + '/experienceCoupon/experience-1.png',
selected: true
},
{
id: 2,
title: '老信食堂',
subTitle: '云南特色卤菜',
distance: '距选地点1.8km',
businessTime: '09:30-02:00',
tags: '云南特色美食',
categoryId: 'all',
image: webUrl + '/experienceCoupon/experience-2.png',
selected: true
},
{
id: 3,
title: '桃矢小馆·缘味小融合',
subTitle: '会员可享受氛围小店',
distance: '距选地点1.8km',
businessTime: '09:30-02:00',
tags: '氛围小店美食',
categoryId: 'all',
image: webUrl + '/experienceCoupon/experience-3.png',
selected: false
},
{
id: 4,
title: '哇猫博物馆',
subTitle: '非遗猫文化体验',
distance: '距选地点1.8km',
businessTime: '09:30-02:00',
tags: '非遗手作体验',
categoryId: 'all',
image: webUrl + '/experienceCoupon/experience-4.png',
selected: false
}
]
}
},
computed: {
filteredList() {
if (this.activeCategoryId === 'all') {
return this.experienceList
}
return this.experienceList.filter(item => item.categoryId === this.activeCategoryId)
},
remainCount() {
if (this.remainNum != null) {
return this.remainNum
}
return this.totalCount - this.selectedCount
}
},
onLoad() {
this.initPage()
},
methods: {
initPage() {
this.fetchExperienceCoupon()
this.fetchCategoryList()
},
fetchExperienceCoupon() {
getLatestExperienceCoupon().then(({ data }) => {
if (!data) {
return
}
this.totalCount = data.totalNum || 0
const remain = data.remainNum != null ? data.remainNum : 0
this.remainNum = remain
this.selectedCount = Math.max(this.totalCount - remain, 0)
this.startTime = data.startTime || ''
this.endTime = data.endTime || ''
this.expireDate = this.endTime || this.expireDate
this.headerImage = data.experienceCouponImage || this.headerImage
}).catch(() => {})
},
fetchCategoryList() {
getExperienceStoreCategoryList().then(({ data }) => {
const list = data || []
this.categoryList = [
{ id: 'all', name: '全部体验' },
...list.map(item => ({
id: item.id,
name: item.name
}))
]
if (list.length > 0) {
const ids = list.map(item => item.id)
if (this.experienceList[0]) {
this.experienceList[0].categoryId = ids[0]
}
if (this.experienceList[1]) {
this.experienceList[1].categoryId = ids[Math.min(1, ids.length - 1)]
}
if (this.experienceList[2]) {
this.experienceList[2].categoryId = ids[Math.min(2, ids.length - 1)]
}
if (this.experienceList[3]) {
this.experienceList[3].categoryId = ids[Math.min(3, ids.length - 1)]
}
}
}).catch(() => {})
},
changeCategory(id) {
this.activeCategoryId = id
},
toggleSelect(item) {
if (!item.selected && this.remainCount === 0) {
uni.showToast({
title: '体验券数量已达上限',
icon: 'none'
})
return
}
item.selected = !item.selected
this.selectedCount = this.experienceList.filter(it => it.selected).length
},
handleContinueSelect() {
uni.showToast({
title: '请继续选择体验券',
icon: 'none'
})
},
handleGoExperience() {
uni.showToast({
title: '请前往体验门店使用',
icon: 'none'
})
},
handleConfirm() {
uni.showToast({
title: '已确认体验券选择',
icon: 'none'
})
}
}
}
</script>
<template>
<view class="experience-coupon-page">
<view class="header">
<image
:src="headerImage"
class="header-bg"
mode="aspectFill"
/>
<view class="header-mask" />
<view class="header-content">
<view class="header-tag">
选择您的免费体验券
</view>
<view class="header-main">
<view class="line">
根据您的套票您有
<text class="strong">{{ totalCount }}</text>
张体验券可以选择
</view>
<view class="line flex jc-between ai-center">
<view>
已选清单
<text class="strong">{{ selectedCount }}/{{ totalCount }}</text>
</view>
</view>
<view class="line">
请在
<text class="strong">{{ expireDate }}</text>
内选择使用
</view>
</view>
</view>
</view>
<view class="content">
<view class="section-header flex jc-between ai-center">
<view class="title">
可用体验券库
</view>
</view>
<view class="tabs flex">
<view
v-for="item in categoryList"
:key="item.id"
class="tab-item"
:class="{ active: activeCategoryId === item.id }"
@click="changeCategory(item.id)"
>
{{ item.name }}
</view>
</view>
<view class="card-list">
<view
v-for="item in filteredList"
:key="item.id"
class="card"
>
<image
:src="item.image"
class="card-image"
mode="aspectFill"
/>
<view class="card-body">
<view class="card-title">
{{ item.title }}
</view>
<view class="card-subtitle more-t">
{{ item.subTitle }}
</view>
<view class="card-meta flex jc-between">
<text class="meta-distance">{{ item.distance }}</text>
<text class="meta-time">营业时间{{ item.businessTime }}</text>
</view>
<view class="card-footer flex jc-between ai-center">
<view class="card-tags more-t">
{{ item.tags }}
</view>
<view
class="card-btn"
:class="{ selected: item.selected }"
@click.stop="toggleSelect(item)"
>
{{ item.selected ? '取消选择' : '立即选择' }}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="bottom-bar">
<view
v-if="remainCount > 0"
class="bottom-group flex jc-between ai-center"
>
<view
class="btn-outline"
@click="handleContinueSelect"
>
继续选择还可以选{{ remainCount }}
</view>
<view
class="btn-primary"
@click="handleGoExperience"
>
先去体验
</view>
</view>
<view
v-else
class="bottom-single"
>
<view
class="btn-primary"
@click="handleConfirm"
>
确认
</view>
</view>
</view>
</view>
</template>
<style scoped lang="less">
.experience-coupon-page {
min-height: 100vh;
padding-bottom: 140rpx;
background: #f5f5f5;
color: #333;
}
.header {
position: relative;
height: 360rpx;
overflow: hidden;
}
.header-bg {
width: 100%;
height: 100%;
}
.header-mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 220rpx;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
}
.header-content {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 32rpx 32rpx 40rpx;
box-sizing: border-box;
color: #fff;
}
.header-tag {
display: inline-block;
padding: 6rpx 24rpx;
margin-bottom: 16rpx;
border-radius: 40rpx;
background: #C52733;
font-size: 22rpx;
}
.header-main .line {
margin-top: 8rpx;
font-size: 24rpx;
}
.header-main .strong {
margin: 0 4rpx;
font-size: 28rpx;
font-weight: 600;
}
.content {
padding: 24rpx 24rpx 0;
}
.section-header .title {
font-size: 30rpx;
font-weight: 600;
}
.tabs {
margin-top: 24rpx;
padding: 4rpx;
border-radius: 40rpx;
background: #f0f0f0;
}
.tab-item {
flex: 1;
padding: 16rpx 0;
text-align: center;
font-size: 24rpx;
color: #666;
border-radius: 32rpx;
}
.tab-item.active {
background: #C52733;
color: #fff;
}
.card-list {
margin-top: 24rpx;
}
.card {
margin-bottom: 24rpx;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
.card-image {
width: 100%;
height: 260rpx;
}
.card-body {
padding: 20rpx 24rpx 24rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
}
.card-subtitle {
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
.card-meta {
margin-top: 12rpx;
font-size: 22rpx;
color: #999;
}
.card-footer {
margin-top: 16rpx;
}
.card-tags {
flex: 1;
margin-right: 16rpx;
font-size: 22rpx;
color: #999;
}
.card-btn {
min-width: 160rpx;
padding: 12rpx 24rpx;
border-radius: 40rpx;
text-align: center;
font-size: 24rpx;
color: #C52733;
border: 1rpx solid #C52733;
}
.card-btn.selected {
background: #C52733;
color: #fff;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 16rpx 24rpx 32rpx;
box-sizing: border-box;
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.bottom-group {
column-gap: 16rpx;
}
.btn-outline {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
border: 1rpx solid #C52733;
text-align: center;
font-size: 26rpx;
line-height: 80rpx;
color: #C52733;
background: #fff;
}
.btn-primary {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
text-align: center;
font-size: 28rpx;
line-height: 80rpx;
color: #fff;
background: #C52733;
}
.bottom-single .btn-primary {
width: 100%;
}
</style>