huangye-parking/miniapp/pages/points/index.vue
2026-03-11 19:39:07 +08:00

493 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="points-page">
<NavBar title="我的积分" :showBack="false" />
<!-- 积分卡片 -->
<view class="balance-card">
<image v-if="pointsBgUrl" :src="pointsBgUrl" class="balance-bg" mode="aspectFill" />
<text class="balance-label">我的积分</text>
<text class="balance-value">{{ balance }}</text>
<view class="balance-actions">
<view class="action-btn" @click="showPointsDesc">
<text class="action-btn-text">积分说明</text>
</view>
<view class="action-btn" @click="goDetail">
<text class="action-btn-text">积分明细</text>
</view>
</view>
</view>
<!-- 门店筛选 -->
<view class="filter-row">
<picker :range="storeOptions" range-key="label" @change="onStoreChange">
<view class="store-filter">
<text class="filter-label">适用门店:{{ currentStoreName }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<!-- 可兑换优惠券列表 -->
<view class="coupon-list">
<view class="coupon-card" v-for="item in filteredList" :key="item.id">
<!-- 背景图 -->
<image class="coupon-card-bg" src="/static/item_bg2.png" mode="scaleToFill" />
<!-- 左侧标签 -->
<view class="coupon-left">
<template v-if="item.couponType === 'discount'">
<text class="coupon-price">¥{{ item.discountAmount }}</text>
<text class="coupon-type-label">折扣券</text>
</template>
<template v-else>
<text class="coupon-price">免费</text>
<text class="coupon-type-label">洗车券</text>
</template>
</view>
<!-- 右侧信息 -->
<view class="coupon-right">
<text class="coupon-name">{{ item.name }}</text>
<text
class="coupon-info">有效期:{{ isPermanent(item) ? '永久有效' : formatDate(item.validStart) + '至' + formatDate(item.validEnd) }}</text>
<text class="coupon-info">剩余数量:{{ item.remainingCount }} 张</text>
<text class="coupon-info" v-if="item.source === 'ygl'">使用方式:请在驿公里洗车小程序中使用</text>
<view class="coupon-bottom">
<view class="exchange-btn" @click="onExchange(item)"
:class="{ disabled: item.remainingCount <= 0 }">
<text class="exchange-btn-text">{{ item.pointsCost }} 积分兑换</text>
</view>
</view>
</view>
</view>
<view v-if="!filteredList.length && !loading" class="empty-state">
<image class="empty-icon" src="/static/ic_none.png" mode="aspectFit"></image>
<text class="empty-text">暂无可兑换优惠券</text>
</view>
</view>
<!-- 积分说明弹窗 -->
<RichContentPopup :visible="pointsDescVisible" title="积分说明" :content="pointsDescContent"
icon="/static/ic_points.png" @close="pointsDescVisible = false" />
<!-- 兑换确认弹窗 -->
<ConfirmDialog :visible="confirmVisible" title="确认兑换" :message="confirmMessage" @confirm="doExchange"
@cancel="confirmVisible = false" />
<!-- 驿公里券提示弹窗 -->
<ConfirmDialog :visible="yglTipVisible" title="温馨提示" message="该优惠券为驿公里券,兑换后需在驿公里洗车小程序中使用,确定兑换吗?"
@confirm="doExchangeYgl" @cancel="yglTipVisible = false" />
</view>
</template>
<script>
import {
getBalance
} from '@/api/points'
import {
getAvailable,
exchange
} from '@/api/coupon'
import {
getStores
} from '@/api/store'
import {
getPopup
} from '@/api/config'
import RichContentPopup from '@/components/RichContentPopup.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import NavBar from '@/components/NavBar.vue'
export default {
components: {
RichContentPopup,
ConfirmDialog,
NavBar
},
data() {
return {
loading: false,
balance: 0,
availableList: [],
storeOptions: [{
label: '全部',
value: ''
}],
currentStoreId: '',
currentStoreName: '全部',
pointsDescVisible: false,
pointsDescContent: '',
confirmVisible: false,
confirmMessage: '',
yglTipVisible: false,
currentItem: null,
selectedStoreId: null,
pointsBgUrl: ''
}
},
computed: {
filteredList() {
if (!this.currentStoreId) return this.availableList
// 筛选包含该门店的优惠券
return this.availableList.filter(item => {
if (item.stores && item.stores.length) {
return item.stores.some(s => s.id == this.currentStoreId)
}
return false
})
}
},
onShow() {
this.loadData()
},
methods: {
formatDate(d) {
if (!d) return ''
return d.substring(0, 10)
},
isPermanent(item) {
if (!item.validEnd) return false
return new Date(item.validEnd).getFullYear() >= 2099
},
async loadData() {
this.loading = true
try {
const [balanceRes, availableRes, storesRes] = await Promise.all([
getBalance(), getAvailable(), getStores()
])
this.balance = balanceRes.balance ?? balanceRes.data?.balance ?? 0
this.availableList = availableRes.data || availableRes || []
const stores = storesRes.data || storesRes || []
this.storeOptions = [{
label: '全部',
value: ''
},
...stores.map(s => ({
label: s.name,
value: s.id
}))
]
// 加载积分页背景图配置
try {
const bgRes = await getPopup('points-bg')
this.pointsBgUrl = bgRes.content || bgRes.data?.content || ''
} catch (e) {
console.error('加载背景图失败:', e)
}
} catch (err) {
console.error('加载积分数据失败:', err)
} finally {
this.loading = false
}
},
onStoreChange(e) {
const idx = e.detail.value
const opt = this.storeOptions[idx]
this.currentStoreId = opt.value
this.currentStoreName = opt.label
},
async showPointsDesc() {
try {
const res = await getPopup('points-desc')
this.pointsDescContent = res.content || res.data?.content || ''
this.pointsDescVisible = true
} catch (err) {
console.error('获取积分说明失败:', err)
}
},
goDetail() {
uni.navigateTo({
url: '/pages/points/detail'
})
},
onExchange(item) {
if (item.remainingCount <= 0) return
if (item.pointsCost > 0 && this.balance < item.pointsCost) {
uni.showToast({
title: '积分不足',
icon: 'none'
})
return
}
this.currentItem = item
// 多门店时需要先选择门店
const stores = item.stores || []
if (stores.length === 0) {
uni.showToast({ title: '该优惠券未配置门店', icon: 'none' })
return
}
if (stores.length === 1) {
// 只有一个门店,直接使用
this.selectedStoreId = stores[0].id
this.showExchangeConfirm(item)
} else {
// 多个门店,弹出选择
const names = stores.map(s => s.name)
uni.showActionSheet({
itemList: names,
success: (res) => {
this.selectedStoreId = stores[res.tapIndex].id
this.showExchangeConfirm(item)
}
})
}
},
showExchangeConfirm(item) {
if (item.source === 'ygl') {
this.yglTipVisible = true
} else {
this.confirmMessage = `确定使用 ${item.pointsCost} 积分兑换「${item.name}」吗?`
this.confirmVisible = true
}
},
async doExchange() {
this.confirmVisible = false
await this.executeExchange()
},
async doExchangeYgl() {
this.yglTipVisible = false
await this.executeExchange()
},
async executeExchange() {
if (!this.currentItem || !this.selectedStoreId) return
try {
await exchange({
templateId: this.currentItem.id,
storeId: this.selectedStoreId
})
uni.showToast({
title: '兑换成功',
icon: 'success'
})
this.loadData()
} catch (err) {
console.error('兑换失败:', err)
}
}
}
}
</script>
<style scoped>
.points-page {
background: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 积分卡片 */
.balance-card {
margin: 20rpx 24rpx 0;
background: linear-gradient(135deg, #F4E0CB, #EBCDB4);
border-radius: 24rpx;
padding: 48rpx 40rpx 40rpx;
text-align: center;
position: relative;
overflow: hidden;
}
.balance-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.balance-card::after {
content: '';
position: absolute;
right: -40rpx;
bottom: -20rpx;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
.balance-label {
font-size: 28rpx;
color: #AF6547;
position: relative;
z-index: 1;
}
.balance-value {
display: block;
font-size: 80rpx;
font-weight: bold;
color: #AF6547;
margin: 12rpx 0 32rpx;
position: relative;
z-index: 1;
}
.balance-actions {
display: flex;
justify-content: center;
gap: 32rpx;
position: relative;
z-index: 1;
}
.action-btn {
background: #FAE6D4;
border: 2rpx solid #FFF4E5;
border-radius: 40rpx;
padding: 12rpx 40rpx;
}
.action-btn:active {
opacity: 0.7;
}
.action-btn-text {
font-size: 26rpx;
color: #AF6547;
font-weight: 500;
}
/* 门店筛选 */
.filter-row {
padding: 24rpx 24rpx 8rpx;
}
.store-filter {
display: flex;
align-items: center;
gap: 8rpx;
}
.filter-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.filter-arrow {
font-size: 24rpx;
color: #999;
}
/* 优惠券列表 */
.coupon-list {
padding: 0 24rpx;
}
.coupon-card {
display: flex;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
}
.coupon-card-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* 左侧标签 */
.coupon-left {
width: 160rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx 0 24rpx 20rpx;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.coupon-price {
font-size: 65.13rpx;
font-weight: bold;
color: #E8C7AA;
line-height: 1.2;
}
.coupon-type-label {
font-size: 32.57rpx;
color: #E8C7AA;
margin-top: 6rpx;
}
/* 右侧信息 */
.coupon-right {
flex: 1;
padding: 24rpx 24rpx 20rpx;
display: flex;
margin-left: 20rpx;
flex-direction: column;
min-width: 0;
position: relative;
z-index: 1;
}
.coupon-name {
font-size: 35.71rpx;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.coupon-info {
font-size: 25.79rpx;
color: #FFFFFF;
line-height: 1.6;
}
.coupon-bottom {
display: flex;
justify-content: flex-end;
margin-top: 12rpx;
}
.exchange-btn {
background: #FFF8E8;
border-radius: 10rpx;
padding: 10rpx 28rpx;
}
.exchange-btn:active {
opacity: 0.8;
}
.exchange-btn.disabled {
background: #ccc;
}
.exchange-btn-text {
font-size: 24rpx;
color: #E8C7AA;
font-weight: 500;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
</style>