523 lines
12 KiB
Vue
523 lines
12 KiB
Vue
<template>
|
||
<view
|
||
class="container"
|
||
:class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]"
|
||
>
|
||
<navBar />
|
||
|
||
<view class="content">
|
||
<view class="header-section">
|
||
<text class="page-title">
|
||
{{ $t('pointsMall.title') }}
|
||
</text>
|
||
</view>
|
||
|
||
<!-- 积分卡片 -->
|
||
<view class="points-card">
|
||
<view class="points-row" @click="openExchangeRecord">
|
||
<view class="points-left">
|
||
<text class="points-label">
|
||
{{ $t('pointsMall.myPoints') }}
|
||
</text>
|
||
<text class="points-num">{{ userInfo.points }}</text>
|
||
</view>
|
||
|
||
<!-- 右侧箭头 -->
|
||
<view class="points-arrow"><uv-icon name="arrow-right" color="#FFFFFF" size="28"></uv-icon></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 商品列表 -->
|
||
<view class="goods-container">
|
||
<view
|
||
class="goods-item"
|
||
v-for="(item, index) in goodsList"
|
||
:key="item.id ?? index"
|
||
>
|
||
<image :src="item.image" mode="aspectFill" class="goods-image" />
|
||
|
||
<view class="goods-info">
|
||
<text class="goods-title">{{ item.name }}</text>
|
||
|
||
<text class="goods-stock">
|
||
{{ $t('pointsMall.stock') }}:{{ item.stock }}
|
||
</text>
|
||
|
||
<text class="goods-desc">{{ item.desc }}</text>
|
||
|
||
<view class="goods-footer">
|
||
<text class="goods-price">
|
||
{{ getNeedPoints(item) }} {{ $t('pointsMall.pointsUnit') }}
|
||
</text>
|
||
|
||
<view
|
||
class="exchange-btn"
|
||
:class="{ disabled: (item.stock ?? 0) <= 0 }"
|
||
@click="(item.stock ?? 0) > 0 && handleExchange(item)"
|
||
>
|
||
{{ $t('pointsMall.exchange') }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 兑换确认弹窗 -->
|
||
<myModal
|
||
v-model="showModal"
|
||
:title="$t('pointsMall.exchangeConfirmTitle')"
|
||
:confirmText="$t('common.confirm')"
|
||
:cancelText="$t('common.cancel')"
|
||
:noClose="true"
|
||
@confirm="submitExchange"
|
||
@close="closeModal"
|
||
>
|
||
<view class="confirm-box">
|
||
<text class="confirm-text">
|
||
{{
|
||
$t('pointsMall.exchangeConfirmTip').replace('{points}', currentNeedPoints)
|
||
}}
|
||
</text>
|
||
|
||
<view class="confirm-goods" v-if="currentGoods">
|
||
<image :src="currentGoods.image" mode="aspectFill" class="confirm-image" />
|
||
<view class="confirm-info">
|
||
<text class="confirm-title">{{ currentGoods.name }}</text>
|
||
<text class="confirm-points">
|
||
{{ currentNeedPoints }} {{ $t('pointsMall.pointsUnit') }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</myModal>
|
||
|
||
<!-- 领取成功弹窗 -->
|
||
<myModal
|
||
v-model="showSuccessModal"
|
||
:title="$t('pointsMall.successTitle')"
|
||
:confirmText="$t('common.confirm')"
|
||
:noClose="true"
|
||
@confirm="closeSuccessModal"
|
||
@close="closeSuccessModal"
|
||
>
|
||
<view class="success-box">
|
||
<text class="success-text">
|
||
{{ $t('pointsMall.successTip') }}
|
||
</text>
|
||
</view>
|
||
</myModal>
|
||
<myModal
|
||
v-model="showRecordModal"
|
||
:title="$t('pointsMall.exchangeRecordTitle')"
|
||
:confirmShow="false"
|
||
>
|
||
<view class="record-box">
|
||
<!-- 加载中 -->
|
||
<!-- <view v-if="recordLoading" class="record-loading">
|
||
{{ $t('common.loading') }}
|
||
</view> -->
|
||
|
||
<!-- 空状态 -->
|
||
<view v-if="recordList.length === 0" class="record-empty" style="text-align: center;">
|
||
{{ $t('pointsMall.noExchangeRecord') }}
|
||
</view>
|
||
|
||
<!-- 列表 -->
|
||
<view v-else class="record-list">
|
||
<view class="record-item" v-for="(r, idx) in recordList" :key="r.id ?? idx">
|
||
<view class="record-top">
|
||
<text class="record-name">{{ r.giftName || r.name || '-' }}</text>
|
||
<!-- <text class="record-status">
|
||
已兑换
|
||
</text> -->
|
||
</view>
|
||
<view class="record-bottom">
|
||
<text class="record-time">{{ r.exchangeTime?.substring(0, 10) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view class="record-more" v-if="recordHasMore" @click="loadMoreRecord">
|
||
{{ recordMoreLoading ? $t('common.loading') : $t('common.loadMore') }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</myModal>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed } from "vue";
|
||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||
import navBar from "@/components/navBar.vue";
|
||
import { useMainStore } from "@/store/index.js";
|
||
import myModal from "@/components/myModal.vue";
|
||
import { useLoginApi } from "@/Apis/login.js";
|
||
import { baseImageUrl } from "@/config/index.js";
|
||
import { useI18n } from 'vue-i18n'
|
||
const { t } = useI18n()
|
||
const getApi = useLoginApi();
|
||
const { themeInfo } = useMainStore();
|
||
|
||
// 用户积分
|
||
const userInfo = reactive({
|
||
points: 0
|
||
});
|
||
|
||
// 商品列表
|
||
const goodsList = ref([]);
|
||
const recordList = ref([]);
|
||
const showRecordModal = ref(false);
|
||
|
||
// 弹窗控制:确认弹窗
|
||
const showModal = ref(false);
|
||
const currentGoods = ref(null);
|
||
|
||
// 弹窗控制:成功弹窗
|
||
const showSuccessModal = ref(false);
|
||
|
||
|
||
// 打开弹窗
|
||
const openExchangeRecord = () => {
|
||
showRecordModal.value = true
|
||
// 每次打开都刷新第一页
|
||
GetCustomerExchangeGift()
|
||
}
|
||
|
||
// 统一计算商品所需积分:优先 required,其次 points,兜底 0
|
||
const getNeedPoints = (item) => {
|
||
return item?.required ?? item?.points ?? 0;
|
||
};
|
||
|
||
// 当前选中商品所需积分(用于弹窗显示)
|
||
const currentNeedPoints = computed(() => {
|
||
return getNeedPoints(currentGoods.value);
|
||
});
|
||
|
||
const GetCustomerExchangeGift = () => {
|
||
uni.showLoading({
|
||
mask: true
|
||
});
|
||
getApi.GetCustomerExchangeGift().then((res) => {
|
||
uni.hideLoading();
|
||
if (res.code == 200) {
|
||
recordList.value = res.data || [];
|
||
}
|
||
});
|
||
}
|
||
|
||
const getDataList = () => {
|
||
uni.showLoading({
|
||
mask: true
|
||
});
|
||
getApi.GetGiftList().then((res) => {
|
||
uni.hideLoading();
|
||
if (res.code == 200) {
|
||
goodsList.value = (res.data || []).map((item) => {
|
||
item.image = baseImageUrl + item.imageUrl;
|
||
return item;
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
// 获取用户积分
|
||
const GetCustomerPoint = () => {
|
||
userInfo.points = 0;
|
||
getApi.GetCustomerPoint().then((res) => {
|
||
if (res.code === 200) {
|
||
userInfo.points = res.data?.availablePoints || 0;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 点击兑换:打开确认弹窗
|
||
const handleExchange = (item) => {
|
||
if ((item.stock ?? 0) <= 0) {
|
||
uni.showToast({ title: t('pointsMall.toastOutOfStock'), icon: "none" });
|
||
return;
|
||
}
|
||
|
||
const need = getNeedPoints(item);
|
||
if (userInfo.points < need) {
|
||
uni.showToast({ title: t('pointsMall.toastNotEnoughPoints'), icon: "none" });
|
||
return;
|
||
}
|
||
|
||
currentGoods.value = item;
|
||
showModal.value = true;
|
||
};
|
||
|
||
// 提交兑换:确认后调用接口
|
||
const submitExchange = () => {
|
||
if (!currentGoods.value) return;
|
||
|
||
const need = currentNeedPoints.value;
|
||
if (userInfo.points < need) {
|
||
uni.showToast({ title: t('pointsMall.toastNotEnoughPoints'), icon: "none" });
|
||
closeModal();
|
||
return;
|
||
}
|
||
|
||
uni.showLoading({ title: t('pointsMall.toastExchanging') });
|
||
|
||
getApi.CustomerExchangeGift({ giftId: currentGoods.value.id }).then((res) => {
|
||
uni.hideLoading();
|
||
|
||
// 这里你原来判断的是 res.data,我保留逻辑;如果你实际是 res.code===200 更合理可改
|
||
if (res.code == 200) {
|
||
// 关闭确认弹窗
|
||
closeModal();
|
||
|
||
// 刷新数据
|
||
getDataList();
|
||
GetCustomerPoint();
|
||
|
||
// 打开成功弹窗
|
||
showSuccessModal.value = true;
|
||
} else {
|
||
uni.showToast({ title: t('pointsMall.toastExchangeFailed'), icon: "none" });
|
||
}
|
||
}).catch(() => {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: t('pointsMall.toastExchangeFailed'), icon: "none" });
|
||
});
|
||
};
|
||
|
||
// 关闭确认弹窗
|
||
const closeModal = () => {
|
||
showModal.value = false;
|
||
currentGoods.value = null;
|
||
};
|
||
|
||
// 关闭成功弹窗
|
||
const closeSuccessModal = () => {
|
||
showSuccessModal.value = false;
|
||
};
|
||
|
||
onShow(() => {
|
||
getDataList();
|
||
GetCustomerPoint();
|
||
});
|
||
|
||
onLoad(() => {});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import "@/static/style/theme.scss";
|
||
|
||
.container {
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
padding-bottom: 40rpx;
|
||
background: linear-gradient(
|
||
to bottom,
|
||
var(--right-linear),
|
||
var(--left-linear2)
|
||
);
|
||
.content {
|
||
padding: 0 30rpx;
|
||
}
|
||
}
|
||
|
||
.header-section {
|
||
padding: 20rpx 0;
|
||
.page-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
}
|
||
.record-box{
|
||
padding: 20rpx;
|
||
width: 100%;
|
||
.record-item{
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
.points-card {
|
||
background: linear-gradient(135deg, #FF5722 0%, #FF9800 100%);
|
||
border-radius: 20rpx;
|
||
padding: 40rpx;
|
||
margin-bottom: 30rpx;
|
||
color: #fff;
|
||
box-shadow: 0 4rpx 12rpx rgba(255, 154, 158, 0.3);
|
||
.points-row{
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:space-between;
|
||
}
|
||
|
||
.points-left{
|
||
display:flex;
|
||
flex-direction:column;
|
||
}
|
||
|
||
.points-arrow{
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
opacity: 0.9;
|
||
padding-left: 20rpx;
|
||
}
|
||
|
||
|
||
.points-label {
|
||
font-size: 28rpx;
|
||
opacity: 0.9;
|
||
display: block;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.points-num {
|
||
font-size: 60rpx;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.goods-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.goods-item {
|
||
display: flex;
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 20rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||
|
||
.goods-image {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
border-radius: 12rpx;
|
||
background-color: #eee;
|
||
margin-right: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.goods-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
|
||
.goods-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.goods-stock {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.goods-desc {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
overflow: hidden;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.goods-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.goods-price {
|
||
font-size: 32rpx;
|
||
color: #ff5a5f;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.exchange-btn {
|
||
padding: 10rpx 30rpx;
|
||
background-color: #007aff;
|
||
color: #fff;
|
||
font-size: 26rpx;
|
||
border-radius: 30rpx;
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
&.disabled {
|
||
background-color: #c8c9cc;
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 兑换确认弹窗样式 */
|
||
.confirm-box {
|
||
padding: 20rpx 0;
|
||
|
||
.confirm-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.6;
|
||
display: block;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.confirm-goods {
|
||
display: flex;
|
||
align-items: center;
|
||
background: #f7f7f7;
|
||
border-radius: 14rpx;
|
||
padding: 16rpx;
|
||
|
||
.confirm-image {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 10rpx;
|
||
margin-right: 16rpx;
|
||
flex-shrink: 0;
|
||
background: #eee;
|
||
}
|
||
|
||
.confirm-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
|
||
.confirm-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.confirm-points {
|
||
font-size: 26rpx;
|
||
color: #ff5a5f;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 领取成功弹窗样式 */
|
||
.success-box {
|
||
padding: 30rpx 0;
|
||
|
||
.success-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.7;
|
||
}
|
||
}
|
||
</style>
|