优惠卷页面选择开发

main
格调main 2026-03-21 19:16:08 +08:00
parent 9ab3c29168
commit 33e9728400
6 changed files with 564 additions and 67 deletions

View File

@ -4,9 +4,9 @@
*/
// 基础URL配置注意末尾不要加斜杠
const BASE_URL = 'https://guangsh.manage.hschengtai.com'
// const BASE_URL = 'https://guangsh.manage.hschengtai.com'
// const BASE_URL = 'http://192.168.0.97:48085'
// const BASE_URL = 'http://192.168.5.134:48085'
const BASE_URL = 'http://192.168.5.135:48085'
// 是否正在刷新token防止并发刷新
let isRefreshing = false
// 等待刷新完成的请求队列
@ -330,7 +330,8 @@ export function request(options = {}) {
uni.showModal({
title: '提示',
content: errorMsg,
showCancel: false,
showCancel: true,
cancelText: '取消',
confirmText: '去登录',
success: (modalRes) => {
if (modalRes.confirm) {

View File

@ -183,4 +183,13 @@ export function cancelOrder(params = {}){
})
}
// 获得优惠卷分页
export function getLuCouponPage(params = {}) {
return request({
url: '/app-api/member/lu-coupon/page',
method: 'GET',
data: params,
})
}

View File

@ -52,6 +52,13 @@
"navigationStyle": "custom"
}
},
{
"path": "selectCoupon",
"style": {
"navigationBarTitleText": "选择优惠卷",
"navigationStyle": "custom"
}
},
{
"path": "mapDetail",
"style": {

View File

@ -0,0 +1,253 @@
<template>
<view class="select-coupon-page">
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="选择优惠卷" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view class="coupon-list" scroll-y="true">
<view
class="coupon-item"
v-for="(item, index) in coupons"
:key="index"
:class="{ disabled: !item.isApplicable }"
@click="selectCoupon(item)"
>
<view class="coupon-left">
<!-- 折扣类 -->
<view class="coupon-price" v-if="item.type === 2">
<text class="amount">{{ (item.discountPercent / 10).toFixed(1).replace(/\.0$/, '') }}</text>
<text class="symbol" style="font-size: 24rpx; margin-left: 4rpx;"></text>
</view>
<!-- 金额类 -->
<view class="coupon-price" v-else>
<text class="symbol">¥</text>
<!-- 检查返回的字段是 discountPrice, discountAmount 还是抵扣金额相关字段如果是金额类应该有值 -->
<text class="amount">{{ formatAmount(item.discountAmount || item.discountPrice || item.price || 0) }}</text>
</view>
<view class="coupon-condition" v-if="item.usePrice">
{{ formatAmount(item.usePrice) }}可用
</view>
<view class="coupon-condition" v-else>
无门槛
</view>
</view>
<view class="coupon-right">
<view class="coupon-name">{{ item.name }}</view>
<view class="coupon-time" v-if="item.type === 2 && item.discountLimit > 0" style="margin-bottom: 6rpx;">: ¥{{ formatAmount(item.discountLimit) }}</view>
<view class="coupon-time" v-if="item.validEndTime">: {{ formatTimeStr(item.validEndTime) }}</view>
</view>
<view class="coupon-radio">
<radio :checked="selectedCouponId === item.id" :disabled="!item.isApplicable" color="#d51c3c" style="transform:scale(0.8)" />
</view>
</view>
<view v-if="coupons.length === 0" class="empty-tip"></view>
</scroll-view>
<!-- 不使用优惠卷按钮 -->
<view class="bottom-bar">
<button class="no-coupon-btn" @click="selectNone">使</button>
</view>
</view>
</view>
</template>
<script>
import NavHeader from "@/components/NavHeader/NavHeader.vue";
import { formatTime } from "@/utils/date.js";
export default {
components: {
NavHeader,
},
data() {
return {
statusBarHeight: 0,
coupons: [],
selectedCouponId: null,
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
//
const eventChannel = this.getOpenerEventChannel();
if (eventChannel && eventChannel.on) {
eventChannel.on('acceptDataFromOpenerPage', (data) => {
this.coupons = data.coupons || [];
this.selectedCouponId = data.selectedCouponId || null;
});
}
},
methods: {
selectCoupon(item) {
if (!item.isApplicable) {
return;
}
this.selectedCouponId = item.id;
this.confirmSelection(item);
},
selectNone() {
this.selectedCouponId = null;
this.confirmSelection(null);
},
confirmSelection(coupon) {
const eventChannel = this.getOpenerEventChannel();
if (eventChannel && eventChannel.emit) {
eventChannel.emit('acceptDataFromOpenedPage', { coupon });
}
uni.navigateBack();
},
formatTimeStr(timestamp) {
if (!timestamp) return '';
return formatTime(timestamp, 'YYYY-MM-DD HH:mm:ss');
},
//
formatAmount(amount) {
if (!amount) return '0';
const yuan = amount / 100;
// 0
return Number.isInteger(yuan) ? yuan.toString() : yuan.toFixed(2).replace(/\.?0+$/, '');
}
}
}
</script>
<style lang="scss" scoped>
.select-coupon-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
background-color: #fff;
}
.main-wrap {
display: flex;
flex-direction: column;
height: 100vh;
box-sizing: border-box;
}
.coupon-list {
flex: 1;
padding: 20rpx;
box-sizing: border-box;
}
.coupon-item {
display: flex;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 30rpx;
align-items: center;
&.disabled {
opacity: 0.6;
background-color: #fafafa;
.coupon-price {
color: #999 !important;
}
.coupon-name {
color: #999 !important;
}
}
.coupon-left {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 160rpx;
border-right: 2rpx dashed #eee;
padding-right: 20rpx;
.coupon-price {
color: #d51c3c;
.symbol {
font-size: 24rpx;
font-weight: bold;
}
.amount {
font-size: 48rpx;
font-weight: bold;
}
}
.coupon-condition {
font-size: 20rpx;
color: #666;
margin-top: 10rpx;
}
}
.coupon-right {
flex: 1;
padding-left: 30rpx;
display: flex;
flex-direction: column;
justify-content: center;
.coupon-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.coupon-time {
font-size: 22rpx;
color: #999;
}
}
.coupon-radio {
margin-left: 20rpx;
}
}
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #999;
font-size: 28rpx;
}
.bottom-bar {
padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
border-top: 1rpx solid #eee;
.no-coupon-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background-color: #f5f5f5;
color: #333;
font-size: 32rpx;
border-radius: 44rpx;
border: none;
&::after {
border: none;
}
}
}
</style>

View File

@ -101,22 +101,27 @@
<!-- 底部结算栏固定 -->
<view class="checkout-footer">
<view class="total-info">
<text class="total-label">总金额¥</text>
<text class="total-amount">{{ totalAmount.toFixed(2) }}</text>
<view class="member-benefit">
<image
class="crown-icon"
src="/static/service/crown-icon.png"
mode="aspectFill"
></image>
<view class="footer-left">
<view class="price-section">
<view class="main-price">
<text class="price-label">合计:</text>
<text class="price-symbol">¥</text>
<text class="price-num">{{ totalAmount.toFixed(2) }}</text>
<view class="member-benefit" v-if="hasMemberDiscount">
<image class="crown-icon" src="/static/service/crown-icon.png" mode="aspectFill"></image>
<text class="benefit-text">{{ memberLevelName }}优惠</text>
</view>
</view>
<view class="discount-section" @click="openCouponPopup">
<text class="discount-text">{{ selectedCoupon ? '已优惠 ¥' + (calculateCouponDiscount(selectedCoupon) / 100).toFixed(2) : '选择优惠卷' }}</text>
<uni-icons type="right" size="12" color="#d51c3c"></uni-icons>
</view>
</view>
</view>
<button
class="checkout-btn"
:class="{ disabled: totalAmount <= 0 }"
:disabled="totalAmount <= 0"
:class="{ disabled: totalAmountBeforeDiscount <= 0 }"
:disabled="totalAmountBeforeDiscount <= 0"
@click="handleCheckout"
>
去结算
@ -131,7 +136,8 @@ import {
getGuildStoreDetail,
getGuildVoucher,
getPaySign,
appBuy
appBuy,
getLuCouponPage
} from "@/api/service";
import NavHeader from "@/components/NavHeader/NavHeader.vue";
@ -149,23 +155,33 @@ export default {
userInfo: {},
distance: null,
paying: false,
coupons: [], //
selectedCoupon: null, //
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
// 0
totalAmount() {
//
totalAmountBeforeDiscount() {
return this.menuList.reduce((total, item) => {
if (item.selected && item.quantity > 0) {
//
const price = (item.salePrice || 0) / 100;
return total + price * item.quantity;
}
return total;
}, 0);
},
// 0
totalAmount() {
let amount = this.totalAmountBeforeDiscount;
if (this.selectedCoupon) {
const discount = this.calculateCouponDiscount(this.selectedCoupon) / 100;
amount = Math.max(0, amount - discount);
}
return amount;
},
//
hasMemberDiscount() {
return this.menuList.some((item) => item.selected && item.discount);
@ -178,6 +194,15 @@ export default {
);
},
},
watch: {
//
menuList: {
handler() {
this.autoSelectBestCoupon();
},
deep: true
}
},
onLoad(options) {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
@ -235,10 +260,11 @@ export default {
//
async loadStoreData() {
try {
//
//
await Promise.all([
this.loadStoreDetail(this.shopId),
this.loadStoreMenu(this.shopId),
this.loadCoupons()
]);
} catch (error) {
console.error("加载店铺数据失败:", error);
@ -281,6 +307,153 @@ export default {
}
},
//
async loadCoupons() {
try {
const res = await getLuCouponPage({ pageNo: 1, pageSize: 100, status: 0 });
if (res && res.list) {
this.coupons = res.list;
this.autoSelectBestCoupon();
}
} catch (error) {
console.error("加载优惠卷失败:", error);
}
},
//
// menuList selected true > 0
isCouponApplicable(coupon) {
if (!coupon) return false;
// 1.
// 使usePrice
const usePrice = coupon.usePrice || 0;
if (this.totalAmountBeforeDiscount * 100 < usePrice) {
return false;
}
// 2.
// 0 ID
const selectedItemIds = this.menuList
.filter(item => item.selected && item.quantity > 0)
.map(item => item.id);
//
if (selectedItemIds.length === 0) return false;
// 3. productScope
// 1
if (coupon.productScope === 1) {
return true;
}
// 2 voucherIdsID
let vIds = coupon.voucherIds || [];
if (typeof vIds === 'string') {
vIds = vIds.split(',').map(id => Number(id));
}
// ID ID voucherIds
if (vIds.length > 0) {
const hasMatch = selectedItemIds.some(id => vIds.includes(id) || vIds.includes(String(id)));
if (!hasMatch) return false;
} else if (coupon.productScope === 2) {
// voucherIds
return false;
}
return true;
},
//
autoSelectBestCoupon() {
//
const applicableCoupons = this.coupons.filter(c => this.isCouponApplicable(c));
if (applicableCoupons.length === 0) {
this.selectedCoupon = null;
return;
}
//
applicableCoupons.sort((a, b) => {
const discountA = this.calculateCouponDiscount(a);
const discountB = this.calculateCouponDiscount(b);
return discountB - discountA;
});
//
this.selectedCoupon = applicableCoupons[0];
},
//
calculateCouponDiscount(coupon) {
if (!coupon) return 0;
// 1-
if (coupon.type === 1 || !coupon.type) {
return coupon.discountAmount || coupon.discountPrice || coupon.price || 0;
}
// 2-
if (coupon.type === 2) {
let applicableAmount = 0; //
if (coupon.productScope === 1) {
//
applicableAmount = this.totalAmountBeforeDiscount * 100;
} else {
//
let vIds = coupon.voucherIds || [];
if (typeof vIds === 'string') {
vIds = vIds.split(',').map(id => Number(id));
}
this.menuList.forEach(item => {
if (item.selected && item.quantity > 0) {
if (vIds.includes(item.id) || vIds.includes(String(item.id))) {
applicableAmount += (item.sellPrice || 0) * item.quantity;
}
}
});
}
// * (100 - ) / 100
// 808 20%
const percent = coupon.discountPercent || 100;
let discount = Math.floor(applicableAmount * (100 - percent) / 100);
//
if (coupon.discountLimit && coupon.discountLimit > 0) {
discount = Math.min(discount, coupon.discountLimit);
}
return discount;
}
return 0;
},
//
openCouponPopup() {
// isApplicable
const couponsWithStatus = this.coupons.map(c => ({
...c,
isApplicable: this.isCouponApplicable(c)
}));
uni.navigateTo({
url: '/pages/detail/selectCoupon',
success: (res) => {
res.eventChannel.emit('acceptDataFromOpenerPage', {
coupons: couponsWithStatus,
selectedCouponId: this.selectedCoupon ? this.selectedCoupon.id : null
});
},
events: {
acceptDataFromOpenedPage: (data) => {
this.selectedCoupon = data.coupon;
}
}
});
},
//
toggleMenuItem(index) {
const item = this.menuList[index];
@ -295,6 +468,7 @@ export default {
// 0 1
item.quantity = 1;
}
this.autoSelectBestCoupon();
},
//
@ -310,6 +484,7 @@ export default {
}
//
item.quantity = (item.quantity || 0) + 1;
this.autoSelectBestCoupon();
},
//
@ -328,6 +503,7 @@ export default {
item.selected = false;
}
}
this.autoSelectBestCoupon();
},
// 100
@ -370,11 +546,19 @@ export default {
console.log("购买信息: " + voucherStr);
console.log("金额:" + trxamt);
const res = await appBuy({
const buyParams = {
shopId: this.shopId,
voucherData: voucherStr,
payableAmount: trxamt,
});
};
// ID
if (this.selectedCoupon) {
buyParams.memberCouponId = this.selectedCoupon.id;
}
const res = await appBuy(buyParams);
console.log(res);
if (!res) {
uni.showToast({
@ -762,39 +946,54 @@ export default {
left: 0;
right: 0;
background-color: #fff;
padding: 25rpx 20rpx 25rpx 48rpx;
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid #e2e8f1;
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05);
z-index: 100;
.total-info {
.footer-left {
flex: 1;
display: flex;
align-items: center;
.price-section {
display: flex;
flex-direction: column;
.main-price {
display: flex;
align-items: baseline;
.total-label {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 30rpx;
color: #d51c3c;
.price-label {
font-size: 26rpx;
color: #333;
margin-right: 8rpx;
}
.total-amount {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 50rpx;
.price-symbol {
font-size: 28rpx;
color: #d51c3c;
font-weight: bold;
}
.price-num {
font-size: 44rpx;
color: #d51c3c;
font-weight: bold;
font-family: DINAlternate-Bold, DINAlternate;
}
.member-benefit {
display: flex;
align-items: center;
gap: 8rpx;
gap: 6rpx;
margin-left: 12rpx;
background: rgba(255, 107, 0, 0.1);
padding: 4rpx 12rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
white-space: nowrap;
.crown-icon {
width: 23rpx;
@ -802,7 +1001,6 @@ export default {
}
.benefit-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 20rpx;
color: #ff6b00;
@ -810,16 +1008,45 @@ export default {
}
}
.discount-section {
display: inline-flex;
align-items: center;
margin-top: 4rpx;
padding: 4rpx 12rpx;
background-color: rgba(213, 28, 60, 0.08);
border-radius: 20rpx;
.discount-text {
font-size: 22rpx;
color: #d51c3c;
margin-right: 4rpx;
}
}
}
}
.checkout-btn {
color: #ffffff;
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 28rpx;
border: none;
width: 240rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: #004294;
border-radius: 35rpx;
position: absolute;
right: 20rpx;
border-radius: 40rpx;
color: #fff;
font-size: 30rpx;
font-weight: bold;
margin: 0;
padding: 0;
border: none;
&.disabled {
background: #ccc;
color: #fff;
}
&::after {
border: none;
}
}
}
</style>

View File

@ -18,14 +18,14 @@
:class="{ active: currentTab === 'pending_verification' }"
@click="switchTab('pending_verification')"
>
<text class="tab-text">已完成</text>
<text class="tab-text">待核销</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'chargeback' }"
@click="switchTab('chargeback')"
:class="{ active: currentTab === 'completed' }"
@click="switchTab('completed')"
>
<text class="tab-text">退款</text>
<text class="tab-text">完成</text>
</view>
<view
class="tab-item"