hookehuyr

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

- 对接获取优惠券详情和兑换优惠券的API
- 根据API响应动态渲染页面内容
- 添加积分不足状态下的按钮禁用和样式变化
- 优化页面加载和用户交互流程
...@@ -19,6 +19,7 @@ declare module 'vue' { ...@@ -19,6 +19,7 @@ declare module 'vue' {
19 NutDialog: typeof import('@nutui/nutui-taro')['Dialog'] 19 NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
20 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] 20 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
21 NutInput: typeof import('@nutui/nutui-taro')['Input'] 21 NutInput: typeof import('@nutui/nutui-taro')['Input']
22 + NutLoading: typeof import('@nutui/nutui-taro')['Loading']
22 NutPicker: typeof import('@nutui/nutui-taro')['Picker'] 23 NutPicker: typeof import('@nutui/nutui-taro')['Picker']
23 NutPopup: typeof import('@nutui/nutui-taro')['Popup'] 24 NutPopup: typeof import('@nutui/nutui-taro')['Popup']
24 NutRow: typeof import('@nutui/nutui-taro')['Row'] 25 NutRow: typeof import('@nutui/nutui-taro')['Row']
......
...@@ -2,62 +2,71 @@ ...@@ -2,62 +2,71 @@
2 <view class="min-h-screen bg-white pb-24"> 2 <view class="min-h-screen bg-white pb-24">
3 <!-- 分享按钮 --> 3 <!-- 分享按钮 -->
4 <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> 4 <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>
5 - <!-- Top Image --> 5 + <!-- 内容区域 -->
6 - <view class="w-full h-48"> 6 + <!-- Top Image -->
7 - <image :src="reward.image" class="w-full h-full object-cover" /> 7 + <view class="w-full h-48">
8 - </view> 8 + <image :src="reward.banner" class="w-full h-full object-cover" />
9 + </view>
9 10
10 - <!-- Main Content --> 11 + <!-- Main Content -->
11 - <view class="p-6"> 12 + <view class="p-6">
12 - <!-- Points and Title --> 13 + <!-- Points and Title -->
13 - <view class="text-center mb-8"> 14 + <view class="text-center mb-8">
14 - <view class="text-4xl font-bold text-blue-500 mb-2"> 15 + <view class="text-4xl font-bold text-blue-500 mb-2">
15 - <text class="text-2xl">{{ reward.points }} 积分</text> 16 + <text class="text-2xl">{{ reward.points_cost }} 积分</text>
17 + </view>
18 + <h1 class="text-xl font-bold">{{ reward.title }}</h1>
16 </view> 19 </view>
17 - <h1 class="text-xl font-bold">{{ reward.title }}</h1>
18 - </view>
19 20
20 - <!-- Details Sections --> 21 + <!-- Details Sections -->
21 - <view class="space-y-6"> 22 + <view class="space-y-6">
22 - <!-- Applicable Stores --> 23 + <!-- Applicable Stores -->
23 - <view> 24 + <view v-if="reward.applicable_stores && reward.applicable_stores.length > 0">
24 - <h2 class="text-lg font-medium mb-3">可用门店</h2> 25 + <h2 class="text-lg font-medium mb-3">可用门店</h2>
25 - <view class="space-y-2 text-gray-600"> 26 + <view class="space-y-2 text-gray-600">
26 - <view v-for="store in reward.stores" :key="store" class="flex text-sm"> 27 + <view v-for="store in reward.applicable_stores" :key="store" class="flex text-sm">
27 - <span class="mr-2">·</span> 28 + <span class="mr-2">·</span>
28 - <p>{{ store }}</p> 29 + <p>{{ store }}</p>
30 + </view>
29 </view> 31 </view>
30 </view> 32 </view>
31 - </view>
32 33
33 - <!-- Redemption Rules --> 34 + <!-- Redemption Rules -->
34 - <view> 35 + <view v-if="reward.redeem_rules && reward.redeem_rules.length > 0">
35 - <h2 class="text-lg font-medium mb-3">兑换规则</h2> 36 + <h2 class="text-lg font-medium mb-3">兑换规则</h2>
36 - <view class="space-y-2 text-gray-600"> 37 + <view class="space-y-2 text-gray-600">
37 - <view v-for="rule in reward.redemption_rules" :key="rule" class="flex text-sm"> 38 + <view v-for="rule in reward.redeem_rules" :key="rule" class="flex text-sm">
38 - <span class="mr-2">·</span> 39 + <span class="mr-2">·</span>
39 - <p>{{ rule }}</p> 40 + <p>{{ rule }}</p>
41 + </view>
40 </view> 42 </view>
41 </view> 43 </view>
42 - </view>
43 44
44 - <!-- Usage Rules --> 45 + <!-- Usage Rules -->
45 - <view> 46 + <view v-if="reward.usage_rules && reward.usage_rules.length > 0">
46 - <h2 class="text-lg font-medium mb-3">使用规则</h2> 47 + <h2 class="text-lg font-medium mb-3">使用规则</h2>
47 - <view class="space-y-2 text-gray-600"> 48 + <view class="space-y-2 text-gray-600">
48 - <view v-for="rule in reward.usage_rules" :key="rule" class="flex text-sm"> 49 + <view v-for="rule in reward.usage_rules" :key="rule" class="flex text-sm">
49 - <span class="mr-2">·</span> 50 + <span class="mr-2">·</span>
50 - <p>{{ rule }}</p> 51 + <p>{{ rule }}</p>
52 + </view>
51 </view> 53 </view>
52 </view> 54 </view>
53 </view> 55 </view>
54 </view> 56 </view>
55 - </view> 57 +
56 58
57 <!-- Bottom Button --> 59 <!-- Bottom Button -->
58 <view v-if="isCreator" class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100"> 60 <view v-if="isCreator" class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
59 - <nut-button type="primary" size="large" block color="#3B82F6" @click="handleRedeem"> 61 + <nut-button
60 - 立即兑换 62 + type="primary"
63 + size="large"
64 + block
65 + :color="reward.can_redeem ? '#3B82F6' : '#D1D5DB'"
66 + :disabled="!reward.can_redeem"
67 + @click="handleRedeem"
68 + >
69 + {{ reward.can_redeem ? '立即兑换' : '积分不足' }}
61 </nut-button> 70 </nut-button>
62 </view> 71 </view>
63 </view> 72 </view>
...@@ -66,75 +75,132 @@ ...@@ -66,75 +75,132 @@
66 <script setup> 75 <script setup>
67 import { ref } from 'vue'; 76 import { ref } from 'vue';
68 import Taro, { useDidShow, useLoad } from '@tarojs/taro'; 77 import Taro, { useDidShow, useLoad } from '@tarojs/taro';
69 -import AppHeader from '../../components/AppHeader.vue';
70 import { getUserProfileAPI } from '@/api/user'; 78 import { getUserProfileAPI } from '@/api/user';
71 import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'; 79 import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect';
80 +// 导入接口
81 +import { getCouponDetailAPI, redeemCouponAPI } from '@/api/coupon';
72 82
73 const isCreator = ref(false); 83 const isCreator = ref(false);
84 +const couponId = ref('');
74 85
75 -// Mock reward data based on the image 86 +// 优惠券详情数据
76 const reward = ref({ 87 const reward = ref({
77 - id: 1, 88 + id: '',
78 - title: '吴良材眼镜店85折券', 89 + title: '',
79 - points: 10, 90 + points_cost: 0,
80 - image: 'https://placehold.co/800x400/e2f3ff/0369a1?text=LFX&font=roboto', // Placeholder image 91 + banner: '',
81 - stores: [ 92 + applicable_stores: [],
82 - '吴良材眼镜店 (南京东路店)', 93 + redeem_rules: [],
83 - '吴良材眼镜店 (淮海中路店)', 94 + usage_rules: [],
84 - '吴良材眼镜店 (徐家汇店)' 95 + can_redeem: false
85 - ],
86 - redemption_rules: [
87 - '每人每月限兑换1次',
88 - '兑换后30天内有效',
89 - '兑换成功后不可退回积分'
90 - ],
91 - usage_rules: [
92 - '仅限店内正价商品使用',
93 - '不可与其他优惠同时使用',
94 - '特价商品不可使用',
95 - '最终解释权归商家所有'
96 - ]
97 }); 96 });
98 97
99 /** 98 /**
100 - * @description Handles the redemption of the coupon. 99 + * 获取优惠券详情
101 */ 100 */
102 -const handleRedeem = () => { 101 +const fetchCouponDetail = async () => {
103 - // Show a confirmation modal 102 + if (!couponId.value) {
104 - Taro.showModal({ 103 + console.error('缺少优惠券ID');
105 - title: '确认兑换', 104 + return;
106 - content: `将消耗 ${reward.value.points} 积分兑换此优惠券,是否确认?`, 105 + }
107 - success: (res) => { 106 +
108 - if (res.confirm) { 107 + try {
109 - // Simulate API call for redemption 108 + const response = await getCouponDetailAPI({ id: couponId.value });
110 - Taro.showLoading({ title: '兑换中...' }); 109 +
110 + if (response && response.data) {
111 + reward.value = {
112 + id: response.data.id || '',
113 + title: response.data.title || '',
114 + points_cost: response.data.points_cost || 0,
115 + banner: response.data.banner || '',
116 + applicable_stores: response.data.applicable_stores || [],
117 + redeem_rules: response.data.redeem_rules || [],
118 + usage_rules: response.data.usage_rules || [],
119 + can_redeem: response.data.can_redeem || false
120 + };
121 + }
122 + } catch (error) {
123 + console.error('获取优惠券详情失败:', error);
124 + Taro.showToast({
125 + title: '获取详情失败',
126 + icon: 'error'
127 + });
128 + }
129 +};
130 +
131 +/**
132 + * 处理优惠券兑换
133 + */
134 +const handleRedeem = async () => {
135 + if (!reward.value.can_redeem) {
136 + Taro.showToast({
137 + title: '积分不足,无法兑换',
138 + icon: 'none'
139 + });
140 + return;
141 + }
142 +
143 + try {
144 + // 显示确认弹窗
145 + const result = await Taro.showModal({
146 + title: '确认兑换',
147 + content: `确定要用${reward.value.points_cost}积分兑换这张优惠券吗?`,
148 + confirmText: '确认兑换',
149 + cancelText: '取消'
150 + });
151 +
152 + if (result.confirm) {
153 + // 调用兑换API
154 + const redeemResponse = await redeemCouponAPI({
155 + coupon_id: reward.value.id
156 + });
157 +
158 + if (redeemResponse) {
159 + // 兑换成功提示
160 + await Taro.showToast({
161 + title: '兑换成功',
162 + icon: 'success',
163 + duration: 2000
164 + });
165 +
166 + // 延迟跳转到我的优惠券页面
111 setTimeout(() => { 167 setTimeout(() => {
112 - Taro.hideLoading(); 168 + Taro.navigateBack({
113 - Taro.showToast({ 169 + delta: 1
114 - title: '兑换成功',
115 - icon: 'success',
116 - duration: 2000
117 }); 170 });
118 - // After successful redemption, you might want to navigate the user to their coupons page 171 + }, 2000);
119 - // For now, we'll just navigate back. 172 + } else {
120 - setTimeout(() => { 173 + throw new Error(redeemResponse?.message || '兑换失败');
121 - Taro.navigateBack();
122 - }, 2000);
123 - }, 1500);
124 } 174 }
125 } 175 }
126 - }); 176 + } catch (error) {
177 + console.error('兑换失败:', error);
178 + Taro.showToast({
179 + title: error.message || '兑换失败,请重试',
180 + icon: 'error'
181 + });
182 + }
127 }; 183 };
128 184
129 -const initData = async () => { 185 +const initData = async (options = {}) => {
130 - // 获取用户信息,判断是否为创建者
131 try { 186 try {
187 + // 获取URL参数中的优惠券ID
188 + if (options.id) {
189 + couponId.value = options.id;
190 + }
191 +
192 + // 获取用户信息,判断是否为创建者
132 const { code, data } = await getUserProfileAPI(); 193 const { code, data } = await getUserProfileAPI();
133 if (code) { 194 if (code) {
134 isCreator.value = data?.user?.is_creator || false; 195 isCreator.value = data?.user?.is_creator || false;
135 } 196 }
197 +
198 + // 获取优惠券详情
199 + if (couponId.value) {
200 + await fetchCouponDetail();
201 + }
136 } catch (error) { 202 } catch (error) {
137 - console.error('获取用户信息失败:', error); 203 + console.error('初始化数据失败:', error);
138 isCreator.value = false; 204 isCreator.value = false;
139 } 205 }
140 }; 206 };
...@@ -142,12 +208,20 @@ const initData = async () => { ...@@ -142,12 +208,20 @@ const initData = async () => {
142 useLoad((options) => { 208 useLoad((options) => {
143 // 处理分享页面的授权逻辑 209 // 处理分享页面的授权逻辑
144 handleSharePageAuth(options, () => { 210 handleSharePageAuth(options, () => {
145 - initData(); 211 + initData(options);
146 }); 212 });
147 }); 213 });
148 214
149 useDidShow(() => { 215 useDidShow(() => {
150 - initData(); 216 + // 页面显示时只获取用户信息,不重新获取优惠券详情
217 + getUserProfileAPI().then(({ code, data }) => {
218 + if (code) {
219 + isCreator.value = data?.user?.is_creator || false;
220 + }
221 + }).catch(error => {
222 + console.error('获取用户信息失败:', error);
223 + isCreator.value = false;
224 + });
151 }); 225 });
152 226
153 227
......