hookehuyr

feat(奖励详情): 实现优惠券详情页面的API对接和交互逻辑

- 对接获取优惠券详情和兑换优惠券的API
- 根据API响应动态渲染页面内容
- 添加积分不足状态下的按钮禁用和样式变化
- 优化页面加载和用户交互流程
......@@ -19,6 +19,7 @@ declare module 'vue' {
NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRow: typeof import('@nutui/nutui-taro')['Row']
......
......@@ -2,62 +2,71 @@
<view class="min-h-screen bg-white pb-24">
<!-- 分享按钮 -->
<button id="share" data-name="shareBtn" open-type="share" style="font-size: 0.83rem; line-height: 3; padding: 0; position: absolute; right: 40rpx; top: 40rpx; z-index: 1000;color: white; width: 70rpx; height: 70rpx; border-radius: 50%; background: rgba(0, 0, 0, 0.6);">分享</button>
<!-- Top Image -->
<view class="w-full h-48">
<image :src="reward.image" class="w-full h-full object-cover" />
</view>
<!-- 内容区域 -->
<!-- Top Image -->
<view class="w-full h-48">
<image :src="reward.banner" class="w-full h-full object-cover" />
</view>
<!-- Main Content -->
<view class="p-6">
<!-- Points and Title -->
<view class="text-center mb-8">
<view class="text-4xl font-bold text-blue-500 mb-2">
<text class="text-2xl">{{ reward.points }} 积分</text>
<!-- Main Content -->
<view class="p-6">
<!-- Points and Title -->
<view class="text-center mb-8">
<view class="text-4xl font-bold text-blue-500 mb-2">
<text class="text-2xl">{{ reward.points_cost }} 积分</text>
</view>
<h1 class="text-xl font-bold">{{ reward.title }}</h1>
</view>
<h1 class="text-xl font-bold">{{ reward.title }}</h1>
</view>
<!-- Details Sections -->
<view class="space-y-6">
<!-- Applicable Stores -->
<view>
<h2 class="text-lg font-medium mb-3">可用门店</h2>
<view class="space-y-2 text-gray-600">
<view v-for="store in reward.stores" :key="store" class="flex text-sm">
<span class="mr-2">·</span>
<p>{{ store }}</p>
<!-- Details Sections -->
<view class="space-y-6">
<!-- Applicable Stores -->
<view v-if="reward.applicable_stores && reward.applicable_stores.length > 0">
<h2 class="text-lg font-medium mb-3">可用门店</h2>
<view class="space-y-2 text-gray-600">
<view v-for="store in reward.applicable_stores" :key="store" class="flex text-sm">
<span class="mr-2">·</span>
<p>{{ store }}</p>
</view>
</view>
</view>
</view>
<!-- Redemption Rules -->
<view>
<h2 class="text-lg font-medium mb-3">兑换规则</h2>
<view class="space-y-2 text-gray-600">
<view v-for="rule in reward.redemption_rules" :key="rule" class="flex text-sm">
<span class="mr-2">·</span>
<p>{{ rule }}</p>
<!-- Redemption Rules -->
<view v-if="reward.redeem_rules && reward.redeem_rules.length > 0">
<h2 class="text-lg font-medium mb-3">兑换规则</h2>
<view class="space-y-2 text-gray-600">
<view v-for="rule in reward.redeem_rules" :key="rule" class="flex text-sm">
<span class="mr-2">·</span>
<p>{{ rule }}</p>
</view>
</view>
</view>
</view>
<!-- Usage Rules -->
<view>
<h2 class="text-lg font-medium mb-3">使用规则</h2>
<view class="space-y-2 text-gray-600">
<view v-for="rule in reward.usage_rules" :key="rule" class="flex text-sm">
<span class="mr-2">·</span>
<p>{{ rule }}</p>
<!-- Usage Rules -->
<view v-if="reward.usage_rules && reward.usage_rules.length > 0">
<h2 class="text-lg font-medium mb-3">使用规则</h2>
<view class="space-y-2 text-gray-600">
<view v-for="rule in reward.usage_rules" :key="rule" class="flex text-sm">
<span class="mr-2">·</span>
<p>{{ rule }}</p>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- Bottom Button -->
<view v-if="isCreator" class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<nut-button type="primary" size="large" block color="#3B82F6" @click="handleRedeem">
立即兑换
<nut-button
type="primary"
size="large"
block
:color="reward.can_redeem ? '#3B82F6' : '#D1D5DB'"
:disabled="!reward.can_redeem"
@click="handleRedeem"
>
{{ reward.can_redeem ? '立即兑换' : '积分不足' }}
</nut-button>
</view>
</view>
......@@ -66,75 +75,132 @@
<script setup>
import { ref } from 'vue';
import Taro, { useDidShow, useLoad } from '@tarojs/taro';
import AppHeader from '../../components/AppHeader.vue';
import { getUserProfileAPI } from '@/api/user';
import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect';
// 导入接口
import { getCouponDetailAPI, redeemCouponAPI } from '@/api/coupon';
const isCreator = ref(false);
const couponId = ref('');
// Mock reward data based on the image
// 优惠券详情数据
const reward = ref({
id: 1,
title: '吴良材眼镜店85折券',
points: 10,
image: 'https://placehold.co/800x400/e2f3ff/0369a1?text=LFX&font=roboto', // Placeholder image
stores: [
'吴良材眼镜店 (南京东路店)',
'吴良材眼镜店 (淮海中路店)',
'吴良材眼镜店 (徐家汇店)'
],
redemption_rules: [
'每人每月限兑换1次',
'兑换后30天内有效',
'兑换成功后不可退回积分'
],
usage_rules: [
'仅限店内正价商品使用',
'不可与其他优惠同时使用',
'特价商品不可使用',
'最终解释权归商家所有'
]
id: '',
title: '',
points_cost: 0,
banner: '',
applicable_stores: [],
redeem_rules: [],
usage_rules: [],
can_redeem: false
});
/**
* @description Handles the redemption of the coupon.
* 获取优惠券详情
*/
const handleRedeem = () => {
// Show a confirmation modal
Taro.showModal({
title: '确认兑换',
content: `将消耗 ${reward.value.points} 积分兑换此优惠券,是否确认?`,
success: (res) => {
if (res.confirm) {
// Simulate API call for redemption
Taro.showLoading({ title: '兑换中...' });
const fetchCouponDetail = async () => {
if (!couponId.value) {
console.error('缺少优惠券ID');
return;
}
try {
const response = await getCouponDetailAPI({ id: couponId.value });
if (response && response.data) {
reward.value = {
id: response.data.id || '',
title: response.data.title || '',
points_cost: response.data.points_cost || 0,
banner: response.data.banner || '',
applicable_stores: response.data.applicable_stores || [],
redeem_rules: response.data.redeem_rules || [],
usage_rules: response.data.usage_rules || [],
can_redeem: response.data.can_redeem || false
};
}
} catch (error) {
console.error('获取优惠券详情失败:', error);
Taro.showToast({
title: '获取详情失败',
icon: 'error'
});
}
};
/**
* 处理优惠券兑换
*/
const handleRedeem = async () => {
if (!reward.value.can_redeem) {
Taro.showToast({
title: '积分不足,无法兑换',
icon: 'none'
});
return;
}
try {
// 显示确认弹窗
const result = await Taro.showModal({
title: '确认兑换',
content: `确定要用${reward.value.points_cost}积分兑换这张优惠券吗?`,
confirmText: '确认兑换',
cancelText: '取消'
});
if (result.confirm) {
// 调用兑换API
const redeemResponse = await redeemCouponAPI({
coupon_id: reward.value.id
});
if (redeemResponse) {
// 兑换成功提示
await Taro.showToast({
title: '兑换成功',
icon: 'success',
duration: 2000
});
// 延迟跳转到我的优惠券页面
setTimeout(() => {
Taro.hideLoading();
Taro.showToast({
title: '兑换成功',
icon: 'success',
duration: 2000
Taro.navigateBack({
delta: 1
});
// After successful redemption, you might want to navigate the user to their coupons page
// For now, we'll just navigate back.
setTimeout(() => {
Taro.navigateBack();
}, 2000);
}, 1500);
}, 2000);
} else {
throw new Error(redeemResponse?.message || '兑换失败');
}
}
});
} catch (error) {
console.error('兑换失败:', error);
Taro.showToast({
title: error.message || '兑换失败,请重试',
icon: 'error'
});
}
};
const initData = async () => {
// 获取用户信息,判断是否为创建者
const initData = async (options = {}) => {
try {
// 获取URL参数中的优惠券ID
if (options.id) {
couponId.value = options.id;
}
// 获取用户信息,判断是否为创建者
const { code, data } = await getUserProfileAPI();
if (code) {
isCreator.value = data?.user?.is_creator || false;
}
// 获取优惠券详情
if (couponId.value) {
await fetchCouponDetail();
}
} catch (error) {
console.error('获取用户信息失败:', error);
console.error('初始化数据失败:', error);
isCreator.value = false;
}
};
......@@ -142,12 +208,20 @@ const initData = async () => {
useLoad((options) => {
// 处理分享页面的授权逻辑
handleSharePageAuth(options, () => {
initData();
initData(options);
});
});
useDidShow(() => {
initData();
// 页面显示时只获取用户信息,不重新获取优惠券详情
getUserProfileAPI().then(({ code, data }) => {
if (code) {
isCreator.value = data?.user?.is_creator || false;
}
}).catch(error => {
console.error('获取用户信息失败:', error);
isCreator.value = false;
});
});
......