add:新增会员、优惠券

This commit is contained in:
2026-01-19 09:16:09 +08:00
parent dbf7fa0c95
commit b0daa7b59b
23 changed files with 1912 additions and 58 deletions
+877
View File
@@ -0,0 +1,877 @@
<template>
<view class="purchase-page">
<!-- Tab 切换 -->
<view class="tab-container">
<view class="tab-item" :class="{ active: currentTab === 'card' }" @click="switchTab('card')">
<text class="tab-text">{{ $t('purchase.memberCard') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'coupon' }" @click="switchTab('coupon')">
<text class="tab-text">{{ $t('purchase.coupon') }}</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-area" scroll-y>
<!-- 会员卡列表 -->
<view v-if="currentTab === 'card'" class="product-list">
<view v-for="item in memberCards" :key="item.id" class="product-card"
:class="{ selected: selectedProduct?.id === item.id }" @click="selectProduct(item)">
<view class="card-content">
<view class="card-left">
<text class="card-name">{{ item.name }}</text>
<text class="card-desc">{{ item.description }}</text>
</view>
<view class="card-right">
<text class="card-price">¥{{ item.price }}</text>
<text class="card-original-price" v-if="item.originalPrice">¥{{ item.originalPrice }}</text>
</view>
</view>
</view>
<!-- 空数据提示 -->
<uv-empty v-if="memberCards.length === 0" mode="car" :text="$t('purchase.noCards')"></uv-empty>
</view>
<!-- 优惠券列表 -->
<view v-if="currentTab === 'coupon'" class="product-list">
<view v-for="item in coupons" :key="item.id" class="coupon-card-wrapper">
<view class="coupon-card"
:class="{ selected: selectedProduct?.id === item.id }" @click="selectProduct(item)">
<!-- 左侧圆形缺口 -->
<view class="coupon-circle-left"></view>
<!-- 右侧圆形缺口 -->
<view class="coupon-circle-right"></view>
<view class="coupon-left">
<text class="coupon-value">{{ item.type === 'discount' ? item.discount + '折' : '¥' + item.value
}}</text>
<text class="coupon-condition">{{ item.condition }}</text>
<text class="coupon-validity">{{ item.validity }}</text>
</view>
<view class="coupon-divider"></view>
<view class="coupon-right">
<view class="coupon-price">
<text class="price-label">¥</text>
<text class="price-value">{{ item.price }}</text>
</view>
</view>
</view>
</view>
<!-- 空数据提示 -->
<uv-empty v-if="coupons.length === 0" mode="coupon" :text="$t('purchase.noCoupons')"></uv-empty>
</view>
<!-- 说明部分 -->
<view class="description-section">
<text class="description-title">{{ currentTab === 'card' ? $t('purchase.cardDescription') :
$t('purchase.couponDescription') }}</text>
<view class="description-content">
<view v-for="(desc, index) in descriptions" :key="index" class="description-item">
<text class="description-subtitle">{{ desc.title }}</text>
<text class="description-text">{{ desc.content }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="my-products-wrapper" @click="goToMyProducts">
<view class="" style="display: flex;" >
<view class="my-products" >
<image class="my-products-icon" src="/static/couponList.png" mode="aspectFit">
</image>
<text class="my-products-text">{{ currentTab === 'card' ? $t('purchase.myCards') :
$t('purchase.myCoupons') }}</text>
</view>
</view>
<view class="" style="width: 40rpx;height: 40rpx;">
<image src="/static/gotoBuy.png" mode="aspectFill" style="width: 40rpx;height: 40rpx;"></image>
</view>
</view>
<view class="action-wrapper">
<view class="price-info">
<text class="current-price">¥{{ selectedProduct?.price || '0.00' }}</text>
<text class="original-price" v-if="selectedProduct?.originalPrice">¥{{
selectedProduct?.originalPrice
}}</text>
</view>
<view class="buy-button" :class="{ disabled: !selectedProduct }" @click="handleBuy">
<text class="buy-button-text">{{ $t('purchase.buyNow') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
useI18n
} from '@/utils/i18n.js'
import {
getCouponsByPosition,
createCouponPayment,
cancelCouponPayment
} from '@/config/api/coupon.js'
// import {
// cancelMemberCardPayment
// } from '@/config/api/member.js'
import {
createMemberCardPayment,
getMemberCardsByPosition,
cancelMemberCardPayment
} from '@/config/api/member.js'
const {
t: $t
} = useI18n()
// 当前选中的 Tab
const currentTab = ref('card') // 'card' 或 'coupon'
// 选中的商品
const selectedProduct = ref(null)
// 当前场地ID(从路由参数获取)
const positionId = ref(null)
// 生命周期 onLoad 钩子 - 获取路由参数
onLoad((options) => {
if (options.positionId) {
positionId.value = options.positionId
console.log('获取到场地ID:', positionId.value)
}
})
// 会员卡列表(从后端加载)
const memberCards = ref([])
// 优惠券列表(从后端加载)
const coupons = ref([])
// 说明内容
const descriptions = computed(() => {
if (currentTab.value === 'card') {
return [
{
title: $t('purchase.cardUseInstruction'),
content: $t('purchase.cardUseDescription')
},
{
title: $t('purchase.cardValidityPeriod'),
content: $t('purchase.cardValidityDescription')
},
{
title: $t('purchase.cardRefundPolicy'),
content: $t('purchase.cardRefundDescription')
}
]
} else {
return [
{
title: $t('purchase.couponUseInstruction'),
content: $t('purchase.couponUseDescription')
},
{
title: $t('purchase.couponValidityPeriod'),
content: $t('purchase.couponValidityDescription')
},
{
title: $t('purchase.couponUsageScope'),
content: $t('purchase.couponUsageDescription')
}
]
}
})
// 加载会员卡列表
const loadMemberCards = async () => {
try {
uni.showLoading({
title: '加载中...'
})
const res = await getMemberCardsByPosition(positionId.value)
uni.hideLoading()
if (res.code === 200 && res.data) {
// 转换为页面需要的格式
memberCards.value = res.data.map(item => ({
id: item.memberCardId,
name: item.cardName,
type: item.cardType,
description: item.positionName || '无描述',
price: item.purchasePrice ? item.purchasePrice.toString() : '0.00',
originalPrice: null,
availablePositions: item.positionName,
cycleDays: item.cycleDays,
dailyLimitCount: item.dailyLimitCount,
singleLimitMinutes: item.singleLimitMinutes,
validDays: item.validDays,
totalCount: item.totalCount,
purchaseQuantity: item.purchaseQuantity
}))
}
} catch (error) {
uni.hideLoading()
console.error('加载会员卡失败:', error)
uni.showToast({
title: '加载会员卡失败',
icon: 'none'
})
}
}
// 加载优惠券列表
const loadCoupons = async () => {
try {
uni.showLoading({
title: '加载中...'
})
const res = await getCouponsByPosition(positionId.value)
uni.hideLoading()
if (res.code === 200 && res.data) {
// 转换为页面需要的格式
coupons.value = res.data.map(item => ({
id: item.couponId,
couponId: item.couponId,
name: item.couponName,
type: item.couponType === '1' ? 'discount' : 'cash',
discount: item.discountRate ? (item.discountRate * 10).toFixed(0) : null,
value: item.deductAmount ? item.deductAmount.toString() : null,
condition: item.usableCondition > 0 ? `${item.usableCondition}元可用` : '无门槛',
validity: item.validDays > 0 ? `从购买日起 有效期${item.validDays}` : '永久有效',
price: item.purchasePrice ? item.purchasePrice.toString() : '0.00',
originalPrice: null,
usablePositions: item.usablePositionNameMap,
remark: item.remark
}))
}
} catch (error) {
uni.hideLoading()
console.error('加载优惠券失败:', error)
uni.showToast({
title: '加载优惠券失败',
icon: 'none'
})
}
}
// 初始化
onMounted(async () => {
// 加载会员卡列表
if (positionId.value) {
await loadMemberCards()
}
// 加载优惠券列表
if (positionId.value) {
await loadCoupons()
}
// 默认选中第一个会员卡(如果有的话)
if (memberCards.value.length > 0) {
selectedProduct.value = memberCards.value[0]
}
})
const switchTab = (tab) => {
currentTab.value = tab
selectedProduct.value = null
}
// 选择商品
const selectProduct = (product) => {
selectedProduct.value = product
}
const orderNo = ref('')
// 处理购买
const handleBuy = async () => {
if (!selectedProduct.value) {
uni.showToast({
title: $t('purchase.pleaseSelect'),
icon: 'none'
})
return
}
// 会员卡购买
if (currentTab.value === 'card') {
try {
uni.showLoading({
title: '正在创建订单...'
})
const res = await createMemberCardPayment(selectedProduct.value.id)
uni.hideLoading()
if (res.code === 200 && res.data) {
orderNo.value = res.data.OutOrderNo;
// 调起微信支付
uni.requestPayment({
timeStamp: res.data.timeStamp,
nonceStr: res.data.nonceStr,
package: res.data.packageValue || res.data.package,
signType: res.data.signType || 'MD5',
paySign: res.data.paySign,
success: (payRes) => {
uni.showToast({
title: '支付成功',
icon: 'success'
})
// 支付成功后,跳转到我的会员卡页面
setTimeout(() => {
uni.navigateTo({
url: '/pages/my/card'
})
}, 1500)
},
fail: (err) => {
console.error('支付失败:', err)
console.log('支付失败详细信息:', err.errMsg.includes('cancel'));
if (err.errMsg && err.errMsg.includes('cancel')) {
if (orderNo.value) {
cancelMemberCardPayment(orderNo.value)
.then(cancelRes => {
console.log('取消支付订单成功:', cancelRes);
})
.catch(cancelErr => {
console.error('取消支付订单失败:', cancelErr);
});
}
uni.showToast({
title: '已取消支付',
icon: 'none'
})
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
})
}
}
})
} else {
uni.showToast({
title: res.msg || '创建订单失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('购买失败:', error)
uni.showToast({
title: '购买失败,请稍后重试',
icon: 'none'
})
}
return
}
// 优惠券购买
if (currentTab.value === 'coupon') {
try {
uni.showLoading({
title: '正在创建订单...'
})
const res = await createCouponPayment(selectedProduct.value.couponId)
uni.hideLoading()
if (res.code === 200 && res.data) {
// 调起微信支付
uni.requestPayment({
timeStamp: res.data.timeStamp,
nonceStr: res.data.nonceStr,
package: res.data.packageValue || res.data.package,
signType: res.data.signType || 'MD5',
paySign: res.data.paySign,
success: (payRes) => {
uni.showToast({
title: '支付成功',
icon: 'success'
})
// 支付成功后,跳转到我的优惠券页面
setTimeout(() => {
uni.navigateTo({
url: '/pages/my/coupon'
})
}, 1500)
},
fail: (err) => {
console.error('支付失败:', err)
if (err.errMsg && err.errMsg.includes('cancel')) {
// 用户取消支付,调用取消接口
const orderNo = res.data.OutOrderNo;
if (orderNo) {
cancelCouponPayment(orderNo)
.then(cancelRes => {
console.log('取消支付订单成功:', cancelRes);
})
.catch(cancelErr => {
console.error('取消支付订单失败:', cancelErr);
});
}
uni.showToast({
title: '已取消支付',
icon: 'none'
})
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
})
}
}
})
} else {
uni.showToast({
title: res.msg || '创建订单失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('购买失败:', error)
uni.showToast({
title: '购买失败,请稍后重试',
icon: 'none'
})
}
}
}
// 查看我的商品
const goToMyProducts = () => {
if (currentTab.value === 'card') {
uni.navigateTo({
url: '/pages/my/card'
})
} else if (currentTab.value === 'coupon') {
uni.navigateTo({
url: '/pages/my/coupon'
})
}
}
</script>
<style lang="scss" scoped>
.purchase-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 180rpx;
}
/* Tab 切换 */
.tab-container {
top: 0;
position: fixed;
left: 0;
right: 0;
display: flex;
background-color: #ffffff;
z-index: 999;
padding: 20rpx 0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
&.active {
.tab-text {
color: #333;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 6rpx;
background-color: #FFA928;
border-radius: 3rpx;
}
}
}
/* 内容区域 */
.content-area {
height: calc(100vh - 180rpx);
padding: 20rpx;
padding-top: 120rpx;
box-sizing: border-box;
}
/* 商品列表 */
.product-list {
margin-bottom: 20rpx;
margin-top: 20rpx;
}
/* 会员卡卡片 */
.product-card {
background-color: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
box-sizing: border-box;
&:last-child {
margin-bottom: 0;
}
&.selected {
border-color: #FFA928;
box-shadow: 0 4rpx 20rpx rgba(255, 169, 40, 0.2);
}
.card-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.card-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.card-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.card-desc {
font-size: 24rpx;
color: #999;
line-height: 1.6;
}
.card-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.card-price {
font-size: 36rpx;
font-weight: 600;
color: #FF6B00;
}
.card-original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
}
/* 优惠券卡片 */
.coupon-card-wrapper {
position: relative;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.coupon-card {
background: linear-gradient(135deg, #FFF4E6 0%, #FFE8CC 100%);
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex;
align-items: stretch;
position: relative;
box-sizing: border-box;
border: 2rpx solid transparent;
transition: all 0.3s;
overflow: visible;
min-height: 180rpx;
&.selected {
border-color: #B8741A;
box-shadow: 0 4rpx 20rpx rgba(184, 116, 26, 0.2);
.coupon-circle-left,
.coupon-circle-right {
background-color: #FFF4E6;
border: 2rpx solid #B8741A;
}
}
}
/* 左侧圆形缺口 */
.coupon-circle-left {
position: absolute;
left: -16rpx;
top: 50%;
transform: translateY(-50%);
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background-color: #f5f5f5;
z-index: 10;
transition: all 0.3s;
box-sizing: border-box;
}
/* 右侧圆形缺口 */
.coupon-circle-right {
position: absolute;
right: -16rpx;
top: 50%;
transform: translateY(-50%);
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background-color: #f5f5f5;
z-index: 10;
transition: all 0.3s;
box-sizing: border-box;
}
.coupon-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.coupon-value {
font-size: 48rpx;
font-weight: 700;
color: #B8741A;
}
.coupon-condition {
font-size: 28rpx;
color: #333;
}
.coupon-validity {
font-size: 24rpx;
color: #999;
}
.coupon-divider {
width: 2rpx;
height: 100%;
position: absolute;
left: 65%;
transform: translateX(-50%);
top: 0;
bottom: 0;
// min-height: 160rpx;
align-self: stretch;
background: repeating-linear-gradient(to bottom,
#B8741A 0rpx,
#B8741A 8rpx,
transparent 8rpx,
transparent 16rpx);
margin: 0 30rpx;
}
.coupon-right {
display: flex;
align-items: center;
justify-content: center;
.coupon-price {
display: flex;
align-items: baseline;
gap: 4rpx;
.price-label {
font-size: 24rpx;
color: #B8741A;
font-weight: 600;
}
.price-value {
font-size: 36rpx;
color: #B8741A;
font-weight: 700;
}
}
}
/* 说明部分 */
.description-section {
background-color: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-top: 20rpx;
box-sizing: border-box;
}
.description-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.description-content {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.description-item {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.description-subtitle {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.description-text {
font-size: 24rpx;
color: #666;
line-height: 1.8;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding-bottom: 20rpx;
border-radius: 40rpx 40rpx 0 0;
// padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 1000;
}
.my-products-wrapper {
display: flex;
justify-content: space-between;
flex-direction: row;
background: #FFF4E3;
border-radius: 40rpx;
padding: 16rpx 32rpx;
border-radius: 40rpx 40rpx 0 0;
}
.action-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
margin-left: 20rpx;
margin-right: 20rpx;
padding-top: 20rpx;
}
.my-products {
display: flex;
// flex-direction: column;
align-items: center;
justify-content: center;
gap: 4rpx;
min-width: 120rpx;
cursor: pointer;
&.full {
flex-direction: row;
gap: 8rpx;
padding: 16rpx 32rpx;
background-color: #f5f5f5;
border-radius: 40rpx;
}
.my-products-icon {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
}
.my-products-text {
margin-left: 10rpx;
font-size: 22rpx;
color: #A16300;
white-space: nowrap;
}
}
.price-info {
display: flex;
flex-direction: column;
// align-items: flex-end;
gap: 4rpx;
flex: 1;
margin: 0 20rpx;
}
.current-price {
font-size: 36rpx;
font-weight: 600;
color: #FF6B00;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
.buy-button {
padding: 24rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
transition: all 0.3s;
&.disabled {
background-color: #ccc;
opacity: 0.6;
}
.buy-button-text {
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
}
}
</style>