优惠卷列表完成

main
格调main 2026-03-21 21:38:13 +08:00
parent 4605e3538d
commit 80c1a21db0
5 changed files with 467 additions and 302 deletions

View File

@ -139,6 +139,13 @@
"navigationBarTitleText": "隐私政策", "navigationBarTitleText": "隐私政策",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "myCoupons",
"style": {
"navigationBarTitleText": "我的优惠卷",
"navigationStyle": "custom"
}
} }
] ]
} }

View File

@ -101,29 +101,22 @@
<!-- 底部结算栏固定 --> <!-- 底部结算栏固定 -->
<view class="checkout-footer"> <view class="checkout-footer">
<view class="footer-left"> <view class="total-info">
<view class="price-section"> <text class="total-label">总金额¥</text>
<view class="main-price"> <text class="total-amount">{{ totalAmount.toFixed(2) }}</text>
<view class="price-left"> <view class="member-benefit">
<text class="price-label">合计:</text> <image
<text class="price-symbol">¥</text> class="crown-icon"
<text class="price-num">{{ totalAmount.toFixed(2) }}</text> src="/static/service/crown-icon.png"
</view> mode="aspectFill"
<view class="member-benefit" v-if="hasMemberDiscount"> ></image>
<image class="crown-icon" src="/static/service/crown-icon.png" mode="aspectFill"></image> <text class="benefit-text">{{ memberLevelName }}优惠</text>
<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>
</view> </view>
<button <button
class="checkout-btn" class="checkout-btn"
:class="{ disabled: totalAmountBeforeDiscount <= 0 }" :class="{ disabled: totalAmount <= 0 }"
:disabled="totalAmountBeforeDiscount <= 0" :disabled="totalAmount <= 0"
@click="handleCheckout" @click="handleCheckout"
> >
去结算 去结算
@ -138,8 +131,7 @@ import {
getGuildStoreDetail, getGuildStoreDetail,
getGuildVoucher, getGuildVoucher,
getPaySign, getPaySign,
appBuy, appBuy
getLuCouponPage
} from "@/api/service"; } from "@/api/service";
import NavHeader from "@/components/NavHeader/NavHeader.vue"; import NavHeader from "@/components/NavHeader/NavHeader.vue";
@ -157,37 +149,26 @@ export default {
userInfo: {}, userInfo: {},
distance: null, distance: null,
paying: false, paying: false,
coupons: [], //
selectedCoupon: null, //
}; };
}, },
computed: { computed: {
headerHeight() { headerHeight() {
return this.statusBarHeight + 44; return this.statusBarHeight + 44;
}, },
// // 0
totalAmountBeforeDiscount() { totalAmount() {
return this.menuList.reduce((total, item) => { return this.menuList.reduce((total, item) => {
if (item.selected && item.quantity > 0) { if (item.selected && item.quantity > 0) {
//
const price = (item.salePrice || 0) / 100; const price = (item.salePrice || 0) / 100;
return total + price * item.quantity; return total + price * item.quantity;
} }
return total; return total;
}, 0); }, 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() { hasMemberDiscount() {
// level level return this.menuList.some((item) => item.selected && item.discount);
return !!(this.userInfo && this.userInfo.level && this.userInfo.level.name);
}, },
// 访 // 访
memberLevelName() { memberLevelName() {
@ -197,15 +178,6 @@ export default {
); );
}, },
}, },
watch: {
//
menuList: {
handler() {
this.autoSelectBestCoupon();
},
deep: true
}
},
onLoad(options) { onLoad(options) {
const systemInfo = uni.getSystemInfoSync(); const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0; this.statusBarHeight = systemInfo.statusBarHeight || 0;
@ -263,11 +235,10 @@ export default {
// //
async loadStoreData() { async loadStoreData() {
try { try {
// //
await Promise.all([ await Promise.all([
this.loadStoreDetail(this.shopId), this.loadStoreDetail(this.shopId),
this.loadStoreMenu(this.shopId), this.loadStoreMenu(this.shopId),
this.loadCoupons()
]); ]);
} catch (error) { } catch (error) {
console.error("加载店铺数据失败:", error); console.error("加载店铺数据失败:", error);
@ -310,153 +281,6 @@ 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) { toggleMenuItem(index) {
const item = this.menuList[index]; const item = this.menuList[index];
@ -471,7 +295,6 @@ export default {
// 0 1 // 0 1
item.quantity = 1; item.quantity = 1;
} }
this.autoSelectBestCoupon();
}, },
// //
@ -487,7 +310,6 @@ export default {
} }
// //
item.quantity = (item.quantity || 0) + 1; item.quantity = (item.quantity || 0) + 1;
this.autoSelectBestCoupon();
}, },
// //
@ -506,7 +328,6 @@ export default {
item.selected = false; item.selected = false;
} }
} }
this.autoSelectBestCoupon();
}, },
// 100 // 100
@ -549,19 +370,11 @@ export default {
console.log("购买信息: " + voucherStr); console.log("购买信息: " + voucherStr);
console.log("金额:" + trxamt); console.log("金额:" + trxamt);
const res = await appBuy({
const buyParams = {
shopId: this.shopId, shopId: this.shopId,
voucherData: voucherStr, voucherData: voucherStr,
payableAmount: trxamt, payableAmount: trxamt,
}; });
// ID
if (this.selectedCoupon) {
buyParams.couponId = this.selectedCoupon.id;
}
const res = await appBuy(buyParams);
console.log(res); console.log(res);
if (!res) { if (!res) {
uni.showToast({ uni.showToast({
@ -949,118 +762,64 @@ export default {
left: 0; left: 0;
right: 0; right: 0;
background-color: #fff; background-color: #fff;
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom)); padding: 25rpx 20rpx 25rpx 48rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05); border-top: 1rpx solid #e2e8f1;
z-index: 100; z-index: 100;
.footer-left { .total-info {
flex: 1;
display: flex; display: flex;
align-items: center; align-items: baseline;
.price-section { .total-label {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 30rpx;
color: #d51c3c;
}
.total-amount {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 50rpx;
color: #d51c3c;
}
.member-benefit {
display: flex; display: flex;
flex-direction: column; align-items: center;
gap: 8rpx;
margin-left: 12rpx;
background: rgba(255, 107, 0, 0.1);
padding: 4rpx 12rpx;
border-radius: 8rpx;
.main-price { .crown-icon {
display: flex; width: 23rpx;
align-items: center; height: 20rpx;
flex-wrap: wrap;
gap: 8rpx;
.price-left {
display: flex;
align-items: baseline;
}
.price-label {
font-size: 26rpx;
color: #333;
margin-right: 8rpx;
}
.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: inline-flex;
align-items: center;
gap: 6rpx;
margin-left: 12rpx;
background: rgba(255, 107, 0, 0.1);
padding: 4rpx 12rpx;
border-radius: 8rpx;
white-space: nowrap;
flex-shrink: 0;
.crown-icon {
width: 24rpx;
height: 24rpx;
display: block;
flex-shrink: 0;
}
.benefit-text {
font-weight: 500;
font-size: 20rpx;
color: #ff6b00;
line-height: 1;
}
}
} }
.discount-section { .benefit-text {
display: inline-flex; font-family: PingFang-SC, PingFang-SC;
align-items: center; font-weight: 500;
margin-top: 4rpx; font-size: 20rpx;
padding: 4rpx 12rpx; color: #ff6b00;
background-color: rgba(213, 28, 60, 0.08);
border-radius: 20rpx;
.discount-text {
font-size: 22rpx;
color: #d51c3c;
margin-right: 4rpx;
}
} }
} }
} }
.checkout-btn { .checkout-btn {
width: 240rpx; color: #ffffff;
height: 80rpx; font-family: PingFang-SC, PingFang-SC;
line-height: 80rpx;
text-align: center;
background: #004294;
border-radius: 40rpx;
color: #fff;
font-size: 30rpx;
font-weight: bold; font-weight: bold;
margin: 0; font-size: 28rpx;
padding: 0;
border: none; border: none;
background: #004294;
&.disabled { border-radius: 35rpx;
background: #ccc; position: absolute;
color: #fff; right: 20rpx;
}
&::after {
border: none;
}
} }
} }
</style> </style>

View File

@ -110,6 +110,13 @@
<text class="menu-text">投诉建议</text> <text class="menu-text">投诉建议</text>
<text class="menu-arrow"></text> <text class="menu-arrow"></text>
</view> </view>
<view class="menu-item" @tap="goToMyCoupons">
<view class="menu-icon"
><image src="/static/profile/my_icon.png" mode="aspectFill"></image
></view>
<text class="menu-text">我的优惠卷</text>
<text class="menu-arrow"></text>
</view>
<!-- <view class="menu-item" @tap="goToPostMessage"> <!-- <view class="menu-item" @tap="goToPostMessage">
<view class="menu-icon" <view class="menu-icon"
><image ><image
@ -483,6 +490,11 @@ export default {
url: "/pages/activities/complaints", url: "/pages/activities/complaints",
}); });
}, },
goToMyCoupons() {
uni.navigateTo({
url: "/pages/profileSub/myCoupons",
});
},
goToPostMessage() { goToPostMessage() {
uni.navigateTo({ uni.navigateTo({
url: "/pages/activities/postMessage", url: "/pages/activities/postMessage",

View File

@ -0,0 +1,387 @@
<template>
<view class="my-coupons-page">
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="我的优惠卷" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<!-- Tab 切换 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === '0' }"
@click="switchTab('0')"
>
<text class="tab-text">待使用</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === '1' }"
@click="switchTab('1')"
>
<text class="tab-text">已使用</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === '2' }"
@click="switchTab('2')"
>
<text class="tab-text">已过期</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'all' }"
@click="switchTab('all')"
>
<text class="tab-text">全部</text>
</view>
</view>
<!-- 列表内容 -->
<scroll-view
class="coupon-list"
scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="handleLoadMore"
:lower-threshold="100"
>
<!-- 空数据提示 -->
<view class="empty-state" v-if="!loading && coupons.length === 0">
<image
class="empty-icon"
src="/static/home/entry_icon.png"
mode="aspectFit"
></image>
<text class="empty-text">暂无优惠卷</text>
</view>
<view
class="coupon-item"
v-for="(item, index) in coupons"
:key="index"
:class="{ disabled: isCouponDisabled(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>
<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-status-stamp" v-if="item.status === 1">使</view>
<view class="coupon-status-stamp" v-else-if="item.status === 2">已过期</view>
<view class="coupon-status-stamp" v-else-if="item.status === 3">已作废</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import NavHeader from "@/components/NavHeader/NavHeader.vue";
import { getLuCouponPage } from "@/api/service.js";
import { formatTime } from "@/utils/date.js";
export default {
components: {
NavHeader,
},
data() {
return {
statusBarHeight: 0,
currentTab: '0', // 使
coupons: [],
pageNo: 1,
pageSize: 10,
total: 0,
loading: false,
refreshing: false,
hasMore: true,
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
this.loadData();
},
methods: {
switchTab(tab) {
if (this.currentTab === tab) return;
this.currentTab = tab;
this.refreshData();
},
async loadData() {
if (this.loading || !this.hasMore) return;
this.loading = true;
try {
const params = {
pageNo: this.pageNo,
pageSize: this.pageSize
};
if (this.currentTab !== 'all') {
params.status = Number(this.currentTab);
}
const res = await getLuCouponPage(params);
if (res && res.list) {
if (this.pageNo === 1) {
this.coupons = res.list;
} else {
this.coupons = this.coupons.concat(res.list);
}
this.total = res.total || 0;
this.hasMore = this.coupons.length < this.total;
this.pageNo++;
} else {
this.hasMore = false;
}
} catch (error) {
console.error("加载优惠卷失败:", error);
uni.showToast({
title: "加载失败",
icon: "none"
});
} finally {
this.loading = false;
this.refreshing = false;
}
},
refreshData() {
this.pageNo = 1;
this.hasMore = true;
this.coupons = [];
this.loadData();
},
handleRefresh() {
if (this.refreshing) return;
this.refreshing = true;
this.pageNo = 1;
this.hasMore = true;
this.loadData();
},
handleLoadMore() {
this.loadData();
},
isCouponDisabled(item) {
return item.status === 1 || item.status === 2 || item.status === 3;
},
formatTimeStr(timestamp) {
if (!timestamp) return '';
return formatTime(timestamp, 'YYYY-MM-DD HH:mm:ss');
},
//
formatAmount(amount) {
if (!amount) return '0';
const yuan = amount / 100;
return Number.isInteger(yuan) ? yuan.toString() : yuan.toFixed(2).replace(/\.?0+$/, '');
}
}
}
</script>
<style lang="scss" scoped>
.my-coupons-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;
}
.tab-bar {
display: flex;
background-color: #fff;
height: 88rpx;
border-bottom: 1rpx solid #f0f0f0;
.tab-item {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
}
&.active {
.tab-text {
color: #d51c3c;
font-weight: 500;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 6rpx;
background-color: #d51c3c;
border-radius: 4rpx;
}
}
}
}
.coupon-list {
flex: 1;
padding: 20rpx;
box-sizing: border-box;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
.coupon-item {
position: relative;
display: flex;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 30rpx;
align-items: center;
overflow: hidden;
&.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: 20rpx;
display: flex;
flex-direction: column;
justify-content: center;
.coupon-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.coupon-time {
font-size: 22rpx;
color: #999;
}
}
.coupon-status-stamp {
position: absolute;
right: -20rpx;
top: 20rpx;
width: 120rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
background-color: rgba(153, 153, 153, 0.1);
color: #999;
font-size: 20rpx;
transform: rotate(45deg);
transform-origin: center;
border: 1rpx solid #999;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB