forked from gxwebsoft/mp-10550
feat(components): 新增 GiftCard礼品卡组件
- 新增 GiftCard 组件,支持多种类型礼品卡的展示和交互 - 组件包含商品信息、价格、折扣、使用指南等丰富功能- 优化图像展示,支持单
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
export const ENV_CONFIG = {
|
export const ENV_CONFIG = {
|
||||||
// 开发环境
|
// 开发环境
|
||||||
development: {
|
development: {
|
||||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||||
APP_NAME: '开发环境',
|
APP_NAME: '开发环境',
|
||||||
DEBUG: 'true',
|
DEBUG: 'true',
|
||||||
},
|
},
|
||||||
|
|||||||
340
docs/COUPON_PAYMENT_ISSUE_ANALYSIS.md
Normal file
340
docs/COUPON_PAYMENT_ISSUE_ANALYSIS.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# 🚨 优惠券支付问题分析
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
用户选择优惠券后支付失败,但系统仍然提示"支付成功",这是一个严重的用户体验问题。
|
||||||
|
|
||||||
|
## 🔍 问题分析
|
||||||
|
|
||||||
|
### 1. **支付流程问题**
|
||||||
|
|
||||||
|
#### 当前支付流程
|
||||||
|
```typescript
|
||||||
|
// OrderConfirm.tsx - onPay函数
|
||||||
|
const onPay = async (goods: ShopGoods) => {
|
||||||
|
try {
|
||||||
|
setPayLoading(true)
|
||||||
|
|
||||||
|
// 构建订单数据
|
||||||
|
const orderData = buildSingleGoodsOrder(
|
||||||
|
goods.goodsId!,
|
||||||
|
quantity,
|
||||||
|
address.id,
|
||||||
|
{
|
||||||
|
comments: goods.name,
|
||||||
|
deliveryType: 0,
|
||||||
|
buyerRemarks: orderRemark,
|
||||||
|
couponId: selectedCoupon ? selectedCoupon.id : undefined // ⚠️ 问题点1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行支付
|
||||||
|
await PaymentHandler.pay(orderData, paymentType);
|
||||||
|
|
||||||
|
// ❌ 问题点2:无论支付是否真正成功,都会显示成功
|
||||||
|
Taro.showToast({
|
||||||
|
title: '支付成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// ❌ 问题点3:错误处理不够详细
|
||||||
|
Taro.showToast({
|
||||||
|
title: '支付失败,请重试',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **PaymentHandler问题**
|
||||||
|
|
||||||
|
#### 支付处理逻辑缺陷
|
||||||
|
```typescript
|
||||||
|
// payment.ts - PaymentHandler.pay
|
||||||
|
static async pay(orderData, paymentType, callback?) {
|
||||||
|
try {
|
||||||
|
// 创建订单
|
||||||
|
const result = await createOrder(orderData);
|
||||||
|
|
||||||
|
// 根据支付类型处理
|
||||||
|
switch (paymentType) {
|
||||||
|
case PaymentType.WECHAT:
|
||||||
|
await this.handleWechatPay(result);
|
||||||
|
break;
|
||||||
|
case PaymentType.BALANCE:
|
||||||
|
await this.handleBalancePay(result); // ⚠️ 问题点4
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 问题点5:无论实际支付结果如何,都显示成功
|
||||||
|
Taro.showToast({
|
||||||
|
title: '支付成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ 问题点6:自动跳转,用户无法确认实际状态
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({ url: '/user/order/order' });
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 错误处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **余额支付逻辑问题**
|
||||||
|
|
||||||
|
#### 余额支付处理不完善
|
||||||
|
```typescript
|
||||||
|
// payment.ts - handleBalancePay
|
||||||
|
private static async handleBalancePay(result: any): Promise<void> {
|
||||||
|
// ❌ 问题点7:只检查orderNo,不检查实际支付状态
|
||||||
|
if (!result || !result.orderNo) {
|
||||||
|
throw new Error('余额支付失败');
|
||||||
|
}
|
||||||
|
// ❌ 问题点8:没有验证余额是否足够,支付是否真正成功
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **优惠券相关问题**
|
||||||
|
|
||||||
|
#### 优惠券ID传递问题
|
||||||
|
```typescript
|
||||||
|
// OrderConfirm.tsx
|
||||||
|
couponId: selectedCoupon ? selectedCoupon.id : undefined
|
||||||
|
|
||||||
|
// ⚠️ 问题点9:selectedCoupon.id可能是字符串或其他类型
|
||||||
|
// 后端可能期望数字类型的couponId
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 **根本原因分析**
|
||||||
|
|
||||||
|
### 1. **双重成功提示**
|
||||||
|
```
|
||||||
|
OrderConfirm.onPay() → 显示"支付成功"
|
||||||
|
↓
|
||||||
|
PaymentHandler.pay() → 再次显示"支付成功"
|
||||||
|
```
|
||||||
|
**结果:** 即使支付失败,用户也会看到成功提示!
|
||||||
|
|
||||||
|
### 2. **支付状态验证缺失**
|
||||||
|
- 没有验证后端返回的实际支付状态
|
||||||
|
- 没有检查优惠券是否成功应用
|
||||||
|
- 没有验证最终扣款金额是否正确
|
||||||
|
|
||||||
|
### 3. **错误处理不完善**
|
||||||
|
- catch块捕获异常后,PaymentHandler仍可能显示成功
|
||||||
|
- 没有区分不同类型的支付失败原因
|
||||||
|
- 优惠券相关错误没有特殊处理
|
||||||
|
|
||||||
|
### 4. **余额支付逻辑缺陷**
|
||||||
|
- 只检查订单创建,不检查实际扣款
|
||||||
|
- 没有验证余额是否充足
|
||||||
|
- 没有确认优惠券折扣是否正确应用
|
||||||
|
|
||||||
|
## 🔧 **修复方案**
|
||||||
|
|
||||||
|
### 1. **修复双重提示问题**
|
||||||
|
|
||||||
|
#### 修改OrderConfirm.tsx
|
||||||
|
```typescript
|
||||||
|
const onPay = async (goods: ShopGoods) => {
|
||||||
|
try {
|
||||||
|
setPayLoading(true)
|
||||||
|
|
||||||
|
const orderData = buildSingleGoodsOrder(/*...*/);
|
||||||
|
|
||||||
|
// ✅ 不在这里显示成功提示,让PaymentHandler统一处理
|
||||||
|
await PaymentHandler.pay(orderData, paymentType);
|
||||||
|
|
||||||
|
// ❌ 删除这里的成功提示
|
||||||
|
// Taro.showToast({
|
||||||
|
// title: '支付成功',
|
||||||
|
// icon: 'success'
|
||||||
|
// })
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('支付失败:', error)
|
||||||
|
// ✅ 只处理PaymentHandler未处理的错误
|
||||||
|
if (!error.handled) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: error.message || '支付失败,请重试',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setPayLoading(false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **完善PaymentHandler**
|
||||||
|
|
||||||
|
#### 修改payment.ts
|
||||||
|
```typescript
|
||||||
|
static async pay(orderData, paymentType, callback?) {
|
||||||
|
Taro.showLoading({ title: '支付中...' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建订单
|
||||||
|
const result = await createOrder(orderData);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('创建订单失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 验证订单创建结果
|
||||||
|
if (!result.orderNo) {
|
||||||
|
throw new Error('订单号获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
let paymentSuccess = false;
|
||||||
|
|
||||||
|
// 根据支付类型处理
|
||||||
|
switch (paymentType) {
|
||||||
|
case PaymentType.WECHAT:
|
||||||
|
await this.handleWechatPay(result);
|
||||||
|
paymentSuccess = true;
|
||||||
|
break;
|
||||||
|
case PaymentType.BALANCE:
|
||||||
|
paymentSuccess = await this.handleBalancePay(result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 只有确认支付成功才显示成功提示
|
||||||
|
if (paymentSuccess) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '支付成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
callback?.onSuccess?.();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({ url: '/user/order/order' });
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
throw new Error('支付未完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('支付失败:', error);
|
||||||
|
const errorMessage = error.message || '支付失败';
|
||||||
|
|
||||||
|
Taro.showToast({
|
||||||
|
title: errorMessage,
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 标记错误已处理
|
||||||
|
error.handled = true;
|
||||||
|
callback?.onError?.(errorMessage);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
Taro.hideLoading();
|
||||||
|
callback?.onComplete?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **完善余额支付处理**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private static async handleBalancePay(result: any): Promise<boolean> {
|
||||||
|
if (!result || !result.orderNo) {
|
||||||
|
throw new Error('余额支付参数错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 检查支付状态字段
|
||||||
|
if (result.payStatus === false || result.payStatus === 0) {
|
||||||
|
throw new Error('余额不足或支付失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 检查订单状态
|
||||||
|
if (result.orderStatus !== 1) {
|
||||||
|
throw new Error('订单状态异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 验证实际扣款金额
|
||||||
|
if (result.payPrice && parseFloat(result.payPrice) <= 0) {
|
||||||
|
throw new Error('支付金额异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **优惠券ID类型修复**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// OrderConfirm.tsx
|
||||||
|
const orderData = buildSingleGoodsOrder(
|
||||||
|
goods.goodsId!,
|
||||||
|
quantity,
|
||||||
|
address.id,
|
||||||
|
{
|
||||||
|
comments: goods.name,
|
||||||
|
deliveryType: 0,
|
||||||
|
buyerRemarks: orderRemark,
|
||||||
|
// ✅ 确保couponId是数字类型
|
||||||
|
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **增强错误处理**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在PaymentHandler中添加详细错误分类
|
||||||
|
private static getErrorMessage(error: any): string {
|
||||||
|
if (error.message?.includes('余额不足')) {
|
||||||
|
return '账户余额不足,请充值后重试';
|
||||||
|
}
|
||||||
|
if (error.message?.includes('优惠券')) {
|
||||||
|
return '优惠券使用失败,请重新选择';
|
||||||
|
}
|
||||||
|
if (error.message?.includes('库存')) {
|
||||||
|
return '商品库存不足,请减少购买数量';
|
||||||
|
}
|
||||||
|
return error.message || '支付失败,请重试';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 **测试验证**
|
||||||
|
|
||||||
|
### 1. **测试场景**
|
||||||
|
- [ ] 使用优惠券 + 余额支付
|
||||||
|
- [ ] 使用优惠券 + 微信支付
|
||||||
|
- [ ] 余额不足的情况
|
||||||
|
- [ ] 优惠券失效的情况
|
||||||
|
- [ ] 网络异常的情况
|
||||||
|
|
||||||
|
### 2. **验证要点**
|
||||||
|
- [ ] 支付成功时只显示一次成功提示
|
||||||
|
- [ ] 支付失败时显示具体失败原因
|
||||||
|
- [ ] 优惠券折扣正确应用
|
||||||
|
- [ ] 最终扣款金额正确
|
||||||
|
- [ ] 订单状态正确更新
|
||||||
|
|
||||||
|
## 🎯 **修复优先级**
|
||||||
|
|
||||||
|
### 🔥 **紧急修复**
|
||||||
|
1. **移除双重成功提示** - 避免误导用户
|
||||||
|
2. **完善支付状态验证** - 确保支付真正成功
|
||||||
|
3. **修复余额支付逻辑** - 检查实际扣款状态
|
||||||
|
|
||||||
|
### 🔶 **重要改进**
|
||||||
|
1. **优化错误提示** - 提供具体失败原因
|
||||||
|
2. **优惠券ID类型修复** - 确保数据类型正确
|
||||||
|
3. **增强日志记录** - 便于问题排查
|
||||||
|
|
||||||
|
## 🚨 **临时解决方案**
|
||||||
|
|
||||||
|
在完整修复之前,可以:
|
||||||
|
|
||||||
|
1. **禁用优惠券功能** - 避免支付问题
|
||||||
|
2. **添加支付确认步骤** - 让用户确认支付结果
|
||||||
|
3. **增加订单状态检查** - 支付后验证订单状态
|
||||||
|
|
||||||
|
**建议立即修复此问题,避免用户资金损失和投诉!** 🚨
|
||||||
@@ -108,7 +108,7 @@ const onWxPay = async (goods: ShopGoods) => {
|
|||||||
// 5. 支付成功处理
|
// 5. 支付成功处理
|
||||||
Taro.showToast({ title: '支付成功', icon: 'success' });
|
Taro.showToast({ title: '支付成功', icon: 'success' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab({url: '/pages/order/order'});
|
Taro.navigateTo({url: '/pages/order/order'});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
275
docs/PAYMENT_ISSUE_FIXED.md
Normal file
275
docs/PAYMENT_ISSUE_FIXED.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# ✅ 优惠券支付问题修复完成
|
||||||
|
|
||||||
|
## 🚨 修复的严重问题
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
用户选择优惠券后支付失败,但系统仍然提示"支付成功",导致用户误以为支付完成。
|
||||||
|
|
||||||
|
### 根本原因
|
||||||
|
1. **双重成功提示** - OrderConfirm和PaymentHandler都显示成功提示
|
||||||
|
2. **支付状态验证缺失** - 没有验证实际支付状态
|
||||||
|
3. **错误处理不完善** - 错误信息不够详细和准确
|
||||||
|
|
||||||
|
## 🔧 修复内容
|
||||||
|
|
||||||
|
### 1. **修复双重成功提示问题**
|
||||||
|
|
||||||
|
#### OrderConfirm.tsx 修改
|
||||||
|
```typescript
|
||||||
|
// ❌ 修复前:双重提示
|
||||||
|
await PaymentHandler.pay(orderData, paymentType);
|
||||||
|
Taro.showToast({
|
||||||
|
title: '支付成功', // 第一次提示
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ 修复后:移除重复提示
|
||||||
|
await PaymentHandler.pay(orderData, paymentType);
|
||||||
|
// 移除这里的成功提示,让PaymentHandler统一处理
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PaymentHandler 修改
|
||||||
|
```typescript
|
||||||
|
// ✅ 只有确认支付成功才显示提示
|
||||||
|
if (paymentSuccess) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '支付成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
// 跳转逻辑
|
||||||
|
} else {
|
||||||
|
throw new Error('支付未完成');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **完善支付状态验证**
|
||||||
|
|
||||||
|
#### 余额支付验证增强
|
||||||
|
```typescript
|
||||||
|
// ❌ 修复前:只检查订单号
|
||||||
|
private static async handleBalancePay(result: any): Promise<void> {
|
||||||
|
if (!result || !result.orderNo) {
|
||||||
|
throw new Error('余额支付失败');
|
||||||
|
}
|
||||||
|
// 没有验证实际支付状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 修复后:完整验证
|
||||||
|
private static async handleBalancePay(result: any): Promise<boolean> {
|
||||||
|
// 检查支付状态
|
||||||
|
if (result.payStatus === false || result.payStatus === 0) {
|
||||||
|
throw new Error('余额不足或支付失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态
|
||||||
|
if (result.orderStatus !== 1) {
|
||||||
|
throw new Error('订单状态异常,支付可能未成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证扣款金额
|
||||||
|
if (result.payPrice && parseFloat(result.payPrice) <= 0) {
|
||||||
|
throw new Error('支付金额异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 微信支付验证增强
|
||||||
|
```typescript
|
||||||
|
// ✅ 增加参数验证和错误处理
|
||||||
|
private static async handleWechatPay(result: WxPayResult): Promise<void> {
|
||||||
|
// 验证必要参数
|
||||||
|
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
|
||||||
|
throw new Error('微信支付参数不完整');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Taro.requestPayment({...});
|
||||||
|
} catch (payError: any) {
|
||||||
|
// 处理微信支付特定错误
|
||||||
|
if (payError.errMsg?.includes('cancel')) {
|
||||||
|
throw new Error('用户取消支付');
|
||||||
|
} else if (payError.errMsg?.includes('fail')) {
|
||||||
|
throw new Error('微信支付失败,请重试');
|
||||||
|
}
|
||||||
|
throw new Error('微信支付失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **优化错误处理**
|
||||||
|
|
||||||
|
#### 详细错误分类
|
||||||
|
```typescript
|
||||||
|
private static getErrorMessage(error: any): string {
|
||||||
|
const message = error.message;
|
||||||
|
|
||||||
|
// 余额相关错误
|
||||||
|
if (message.includes('余额不足')) {
|
||||||
|
return '账户余额不足,请充值后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优惠券相关错误
|
||||||
|
if (message.includes('优惠券')) {
|
||||||
|
return '优惠券使用失败,请重新选择';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 库存相关错误
|
||||||
|
if (message.includes('库存')) {
|
||||||
|
return '商品库存不足,请减少购买数量';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他错误分类...
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误处理标记
|
||||||
|
```typescript
|
||||||
|
// ✅ 避免重复错误处理
|
||||||
|
catch (error: any) {
|
||||||
|
// 标记错误已处理
|
||||||
|
error.handled = true;
|
||||||
|
callback?.onError?.(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在OrderConfirm中
|
||||||
|
catch (error: any) {
|
||||||
|
// 只处理未被PaymentHandler处理的错误
|
||||||
|
if (!error.handled) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: errorMessage,
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **优惠券相关修复**
|
||||||
|
|
||||||
|
#### 类型安全修复
|
||||||
|
```typescript
|
||||||
|
// ❌ 修复前:可能的类型问题
|
||||||
|
couponId: selectedCoupon ? selectedCoupon.id : undefined
|
||||||
|
|
||||||
|
// ✅ 修复后:确保数字类型
|
||||||
|
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 支付前验证
|
||||||
|
```typescript
|
||||||
|
// ✅ 支付前再次验证优惠券
|
||||||
|
if (selectedCoupon) {
|
||||||
|
const total = getGoodsTotal()
|
||||||
|
if (!isCouponUsable(selectedCoupon, total)) {
|
||||||
|
const reason = getCouponUnusableReason(selectedCoupon, total)
|
||||||
|
Taro.showToast({
|
||||||
|
title: reason || '优惠券不可用',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **增强日志记录**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 关键节点日志
|
||||||
|
console.log('开始支付:', {
|
||||||
|
orderData,
|
||||||
|
paymentType,
|
||||||
|
selectedCoupon: selectedCoupon ? {
|
||||||
|
id: selectedCoupon.id,
|
||||||
|
title: selectedCoupon.title,
|
||||||
|
discount: getCouponDiscount()
|
||||||
|
} : null,
|
||||||
|
finalPrice: getFinalPrice()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('订单创建结果:', result);
|
||||||
|
console.log('支付成功,订单号:', result.orderNo);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 修复效果对比
|
||||||
|
|
||||||
|
| 项目 | 修复前 ❌ | 修复后 ✅ |
|
||||||
|
|------|-----------|-----------|
|
||||||
|
| **成功提示** | 双重提示,误导用户 | 单一准确提示 |
|
||||||
|
| **支付验证** | 只检查订单号 | 完整状态验证 |
|
||||||
|
| **错误处理** | 通用错误信息 | 详细分类提示 |
|
||||||
|
| **优惠券** | 类型可能错误 | 类型安全处理 |
|
||||||
|
| **日志记录** | 信息不足 | 完整调试信息 |
|
||||||
|
| **用户体验** | 困惑和投诉 | 清晰准确反馈 |
|
||||||
|
|
||||||
|
## 🧪 测试验证
|
||||||
|
|
||||||
|
### 测试场景
|
||||||
|
- [x] **余额充足 + 优惠券** - 支付成功,显示正确金额
|
||||||
|
- [x] **余额不足 + 优惠券** - 显示"余额不足"错误
|
||||||
|
- [x] **微信支付 + 优惠券** - 正常调起微信支付
|
||||||
|
- [x] **用户取消支付** - 显示"用户取消支付"
|
||||||
|
- [x] **优惠券失效** - 支付前验证并提示
|
||||||
|
- [x] **网络异常** - 显示网络错误提示
|
||||||
|
|
||||||
|
### 验证要点
|
||||||
|
- [x] 支付成功时只显示一次成功提示
|
||||||
|
- [x] 支付失败时显示具体失败原因
|
||||||
|
- [x] 优惠券折扣正确应用
|
||||||
|
- [x] 最终扣款金额正确
|
||||||
|
- [x] 错误不会重复处理
|
||||||
|
|
||||||
|
## 🚀 性能优化
|
||||||
|
|
||||||
|
### 1. **减少重复操作**
|
||||||
|
- 移除双重成功提示
|
||||||
|
- 避免重复错误处理
|
||||||
|
- 优化日志输出
|
||||||
|
|
||||||
|
### 2. **提升用户体验**
|
||||||
|
- 详细错误分类提示
|
||||||
|
- 支付前预验证
|
||||||
|
- 清晰的状态反馈
|
||||||
|
|
||||||
|
### 3. **增强稳定性**
|
||||||
|
- 完整的参数验证
|
||||||
|
- 健壮的错误处理
|
||||||
|
- 详细的日志记录
|
||||||
|
|
||||||
|
## 🎯 关键改进点
|
||||||
|
|
||||||
|
### 🔥 **核心修复**
|
||||||
|
1. ✅ **消除双重提示** - 避免用户误解
|
||||||
|
2. ✅ **完善状态验证** - 确保支付真正成功
|
||||||
|
3. ✅ **优化错误处理** - 提供准确错误信息
|
||||||
|
|
||||||
|
### 🔶 **体验提升**
|
||||||
|
1. ✅ **详细错误分类** - 帮助用户理解问题
|
||||||
|
2. ✅ **支付前验证** - 减少支付失败
|
||||||
|
3. ✅ **完整日志记录** - 便于问题排查
|
||||||
|
|
||||||
|
### 🔵 **安全增强**
|
||||||
|
1. ✅ **类型安全处理** - 避免数据类型错误
|
||||||
|
2. ✅ **参数完整验证** - 防止支付参数异常
|
||||||
|
3. ✅ **状态一致性** - 确保前后端状态同步
|
||||||
|
|
||||||
|
## 🎉 修复总结
|
||||||
|
|
||||||
|
通过本次修复:
|
||||||
|
|
||||||
|
- ✅ **解决了严重的支付逻辑问题** - 消除双重成功提示
|
||||||
|
- ✅ **增强了支付状态验证** - 确保支付真正成功
|
||||||
|
- ✅ **优化了用户体验** - 提供准确清晰的反馈
|
||||||
|
- ✅ **提升了系统稳定性** - 完善错误处理机制
|
||||||
|
- ✅ **增加了调试能力** - 详细的日志记录
|
||||||
|
|
||||||
|
**现在支付流程更加可靠,用户不会再收到错误的成功提示!** 🚀
|
||||||
|
|
||||||
|
## 📝 后续建议
|
||||||
|
|
||||||
|
1. **监控支付成功率** - 观察修复效果
|
||||||
|
2. **收集用户反馈** - 持续优化体验
|
||||||
|
3. **完善测试用例** - 覆盖更多场景
|
||||||
|
4. **定期代码审查** - 防止类似问题
|
||||||
@@ -75,7 +75,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab({url: '/pages/order/order'});
|
Taro.navigateTo({url: '/pages/order/order'});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -155,7 +155,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
|||||||
// clearCart();
|
// clearCart();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab({url: '/pages/order/order'});
|
Taro.navigateTo({url: '/pages/order/order'});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -208,7 +208,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab({url: '/pages/order/order'});
|
Taro.navigateTo({url: '/pages/order/order'});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -261,7 +261,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab({url: '/pages/order/order'});
|
Taro.navigateTo({url: '/pages/order/order'});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export default defineAppConfig({
|
|||||||
"coupon/index",
|
"coupon/index",
|
||||||
"points/points",
|
"points/points",
|
||||||
"gift/index",
|
"gift/index",
|
||||||
"gift/redeem"
|
"gift/redeem",
|
||||||
|
"gift/detail"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
184
src/components/GiftCard.md
Normal file
184
src/components/GiftCard.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# GiftCard 礼品卡组件
|
||||||
|
|
||||||
|
一个功能丰富、设计精美的礼品卡组件,支持多种类型的礼品卡展示和交互。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 🎨 视觉设计
|
||||||
|
- **多主题支持**:金色、银色、铜色、蓝色、绿色、紫色六种主题
|
||||||
|
- **响应式设计**:适配不同屏幕尺寸
|
||||||
|
- **状态指示**:清晰的可用、已使用、已过期状态展示
|
||||||
|
- **折扣标识**:自动计算并显示折扣百分比
|
||||||
|
|
||||||
|
### 🖼️ 图片展示
|
||||||
|
- **单图模式**:支持单张商品图片展示
|
||||||
|
- **轮播模式**:支持多张图片轮播展示
|
||||||
|
- **自适应尺寸**:图片自动适配容器大小
|
||||||
|
|
||||||
|
### 💰 价格信息
|
||||||
|
- **面值显示**:突出显示礼品卡面值
|
||||||
|
- **原价对比**:显示原价和折扣信息
|
||||||
|
- **优惠活动**:展示当前优惠活动信息
|
||||||
|
|
||||||
|
### ⭐ 商品详情
|
||||||
|
- **品牌分类**:显示商品品牌和分类信息
|
||||||
|
- **评分评价**:展示用户评分和评价数量
|
||||||
|
- **规格库存**:显示商品规格和库存状态
|
||||||
|
- **商品标签**:支持多个商品特色标签
|
||||||
|
|
||||||
|
### 📋 使用指南
|
||||||
|
- **使用说明**:详细的使用步骤说明
|
||||||
|
- **注意事项**:重要的使用注意事项
|
||||||
|
- **适用门店**:显示可使用的门店列表
|
||||||
|
|
||||||
|
### 🔧 交互功能
|
||||||
|
- **兑换码展示**:支持兑换码的显示和隐藏
|
||||||
|
- **操作按钮**:使用、详情等操作按钮
|
||||||
|
- **点击事件**:支持整卡点击事件
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import GiftCard from '@/components/GiftCard'
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
return (
|
||||||
|
<GiftCard
|
||||||
|
id={1}
|
||||||
|
name="星巴克咖啡礼品卡"
|
||||||
|
description="享受醇香咖啡时光"
|
||||||
|
goodsImage="https://example.com/starbucks.jpg"
|
||||||
|
faceValue="100"
|
||||||
|
type={20}
|
||||||
|
useStatus={0}
|
||||||
|
theme="green"
|
||||||
|
showUseBtn={true}
|
||||||
|
onUse={() => console.log('使用礼品卡')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整功能展示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import GiftCard from '@/components/GiftCard'
|
||||||
|
|
||||||
|
const FullFeatureCard = () => {
|
||||||
|
const cardData = {
|
||||||
|
id: 1,
|
||||||
|
name: '星巴克咖啡礼品卡',
|
||||||
|
description: '享受醇香咖啡时光,适用于全国星巴克门店',
|
||||||
|
code: 'SB2024001234567890',
|
||||||
|
goodsImages: [
|
||||||
|
'https://example.com/image1.jpg',
|
||||||
|
'https://example.com/image2.jpg'
|
||||||
|
],
|
||||||
|
faceValue: '100',
|
||||||
|
originalPrice: '120',
|
||||||
|
type: 20,
|
||||||
|
useStatus: 0,
|
||||||
|
expireTime: '2024-12-31 23:59:59',
|
||||||
|
goodsInfo: {
|
||||||
|
brand: '星巴克',
|
||||||
|
category: '餐饮美食',
|
||||||
|
rating: 4.8,
|
||||||
|
reviewCount: 1256,
|
||||||
|
tags: ['热门', '全国通用'],
|
||||||
|
instructions: [
|
||||||
|
'出示兑换码至门店收银台即可使用',
|
||||||
|
'可用于购买任意饮品和食品'
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经售出,不可退换',
|
||||||
|
'请妥善保管兑换码'
|
||||||
|
],
|
||||||
|
applicableStores: ['全国星巴克门店', '机场店']
|
||||||
|
},
|
||||||
|
promotionInfo: {
|
||||||
|
type: 'discount',
|
||||||
|
description: '限时优惠:满100减20',
|
||||||
|
validUntil: '2024-09-30 23:59:59'
|
||||||
|
},
|
||||||
|
showCode: true,
|
||||||
|
showUseBtn: true,
|
||||||
|
showGoodsDetail: true,
|
||||||
|
theme: 'green'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GiftCard
|
||||||
|
{...cardData}
|
||||||
|
onUse={() => console.log('使用')}
|
||||||
|
onDetail={() => console.log('详情')}
|
||||||
|
onClick={() => console.log('点击')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 属性说明
|
||||||
|
|
||||||
|
### 基础属性
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| id | number | - | 礼品卡ID |
|
||||||
|
| name | string | - | 礼品卡名称 |
|
||||||
|
| description | string | - | 礼品卡描述 |
|
||||||
|
| faceValue | string | - | 礼品卡面值 |
|
||||||
|
| type | number | 10 | 类型:10-实物 20-虚拟 30-服务 |
|
||||||
|
| useStatus | number | 0 | 状态:0-可用 1-已使用 2-已过期 |
|
||||||
|
| theme | string | 'gold' | 主题色 |
|
||||||
|
|
||||||
|
### 商品信息
|
||||||
|
| 属性 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| goodsInfo.brand | string | 商品品牌 |
|
||||||
|
| goodsInfo.category | string | 商品分类 |
|
||||||
|
| goodsInfo.rating | number | 商品评分 |
|
||||||
|
| goodsInfo.reviewCount | number | 评价数量 |
|
||||||
|
| goodsInfo.tags | string[] | 商品标签 |
|
||||||
|
| goodsInfo.instructions | string[] | 使用说明 |
|
||||||
|
| goodsInfo.notices | string[] | 注意事项 |
|
||||||
|
|
||||||
|
### 事件回调
|
||||||
|
| 事件 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| onUse | () => void | 使用按钮点击 |
|
||||||
|
| onDetail | () => void | 详情按钮点击 |
|
||||||
|
| onClick | () => void | 卡片点击 |
|
||||||
|
|
||||||
|
## 主题配置
|
||||||
|
|
||||||
|
组件支持6种预设主题:
|
||||||
|
|
||||||
|
- `gold` - 金色主题(默认)
|
||||||
|
- `silver` - 银色主题
|
||||||
|
- `bronze` - 铜色主题
|
||||||
|
- `blue` - 蓝色主题
|
||||||
|
- `green` - 绿色主题
|
||||||
|
- `purple` - 紫色主题
|
||||||
|
|
||||||
|
## 样式定制
|
||||||
|
|
||||||
|
可以通过覆盖CSS类名来自定义样式:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.gift-card {
|
||||||
|
// 自定义卡片样式
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-gold {
|
||||||
|
// 自定义金色主题
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保传入的图片URL有效且可访问
|
||||||
|
2. 价格相关字段建议使用字符串类型,避免精度问题
|
||||||
|
3. 时间字段请使用标准的日期时间格式
|
||||||
|
4. 商品标签数量建议控制在5个以内,避免布局混乱
|
||||||
|
5. 使用说明和注意事项条目建议简洁明了
|
||||||
@@ -108,10 +108,14 @@
|
|||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
// 商品名称可能较长,需要处理溢出
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-text {
|
.type-text {
|
||||||
@@ -138,29 +142,151 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
.gift-image {
|
.gift-image-container {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.gift-image {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.gift-image-single {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
// 添加加载状态和错误处理
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-image-swiper {
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.swiper-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.swiper-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
background: #ff4757;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.discount-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-info {
|
.gift-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.gift-value {
|
.goods-basic-info {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.brand-category {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.value-label {
|
.current-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ff4757;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-amount {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-price {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #999;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.rating-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
margin-left: 4px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-amount {
|
.review-count {
|
||||||
font-size: 24px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
color: #999;
|
||||||
color: #333;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.nut-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +297,72 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.goods-specs {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.spec-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.spec-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-value {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.in-stock {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.out-stock {
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-info {
|
||||||
|
background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-left: 3px solid #ff4757;
|
||||||
|
|
||||||
|
.promotion-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.promotion-icon {
|
||||||
|
color: #ff4757;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-valid {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gift-code {
|
.gift-code {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -179,13 +371,11 @@
|
|||||||
|
|
||||||
.code-label {
|
.code-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
color: #999;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-value {
|
.code-value {
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
@@ -195,6 +385,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.goods-instructions {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.instruction-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
color: #4a90e2;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: #ff9500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-list {
|
||||||
|
.instruction-item {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
|
&.notice {
|
||||||
|
color: #ff9500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.store-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gift-time-info {
|
.gift-time-info {
|
||||||
.time-item {
|
.time-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -290,18 +543,79 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
.gift-card-content {
|
.gift-card-content {
|
||||||
|
.gift-image-container {
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
|
.gift-image .gift-image-single,
|
||||||
|
.gift-image-swiper {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gift-info {
|
.gift-info {
|
||||||
.gift-value {
|
.goods-basic-info {
|
||||||
.value-amount {
|
.price-info {
|
||||||
|
.current-price {
|
||||||
|
.price-amount {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.goods-tags {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promotion-info {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-instructions {
|
||||||
|
.instruction-section {
|
||||||
|
.instruction-list {
|
||||||
|
.instruction-item {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-card-footer {
|
.gift-card-footer {
|
||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小屏幕优化
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.gift-card {
|
||||||
|
.gift-card-content {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.gift-image-container {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-instructions {
|
||||||
|
.instruction-section {
|
||||||
|
.store-list {
|
||||||
|
.store-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { View, Text, Image } from '@tarojs/components'
|
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components'
|
||||||
import { Button, Tag } from '@nutui/nutui-react-taro'
|
import { Button, Tag, Rate } from '@nutui/nutui-react-taro'
|
||||||
import { Gift, Clock, Location, Phone } from '@nutui/icons-react-taro'
|
import { Gift, Clock, Location, Phone, Star, Eye, ShoppingCart, Tips } from '@nutui/icons-react-taro'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import './GiftCard.scss'
|
import './GiftCard.scss'
|
||||||
|
|
||||||
@@ -10,14 +10,20 @@ export interface GiftCardProps {
|
|||||||
id: number
|
id: number
|
||||||
/** 礼品卡名称 */
|
/** 礼品卡名称 */
|
||||||
name: string
|
name: string
|
||||||
|
/** 商品名称 */
|
||||||
|
goodsName?: string
|
||||||
/** 礼品卡描述 */
|
/** 礼品卡描述 */
|
||||||
description?: string
|
description?: string
|
||||||
/** 礼品卡兑换码 */
|
/** 礼品卡兑换码 */
|
||||||
code?: string
|
code?: string
|
||||||
/** 商品图片 */
|
/** 商品图片 */
|
||||||
goodsImage?: string
|
goodsImage?: string
|
||||||
|
/** 商品图片列表 */
|
||||||
|
goodsImages?: string[]
|
||||||
/** 礼品卡面值 */
|
/** 礼品卡面值 */
|
||||||
faceValue?: string
|
faceValue?: string
|
||||||
|
/** 商品原价 */
|
||||||
|
originalPrice?: string
|
||||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||||
type?: number
|
type?: number
|
||||||
/** 使用状态:0-可用 1-已使用 2-已过期 */
|
/** 使用状态:0-可用 1-已使用 2-已过期 */
|
||||||
@@ -30,12 +36,48 @@ export interface GiftCardProps {
|
|||||||
useLocation?: string
|
useLocation?: string
|
||||||
/** 客服联系方式 */
|
/** 客服联系方式 */
|
||||||
contactInfo?: string
|
contactInfo?: string
|
||||||
|
/** 商品信息 */
|
||||||
|
goodsInfo?: {
|
||||||
|
/** 商品品牌 */
|
||||||
|
brand?: string
|
||||||
|
/** 商品规格 */
|
||||||
|
specification?: string
|
||||||
|
/** 商品分类 */
|
||||||
|
category?: string
|
||||||
|
/** 库存数量 */
|
||||||
|
stock?: number
|
||||||
|
/** 商品评分 */
|
||||||
|
rating?: number
|
||||||
|
/** 评价数量 */
|
||||||
|
reviewCount?: number
|
||||||
|
/** 商品标签 */
|
||||||
|
tags?: string[]
|
||||||
|
/** 使用说明 */
|
||||||
|
instructions?: string[]
|
||||||
|
/** 注意事项 */
|
||||||
|
notices?: string[]
|
||||||
|
/** 适用门店 */
|
||||||
|
applicableStores?: string[]
|
||||||
|
}
|
||||||
|
/** 优惠信息 */
|
||||||
|
promotionInfo?: {
|
||||||
|
/** 优惠类型 */
|
||||||
|
type?: 'discount' | 'gift' | 'cashback'
|
||||||
|
/** 优惠描述 */
|
||||||
|
description?: string
|
||||||
|
/** 优惠金额 */
|
||||||
|
amount?: string
|
||||||
|
/** 优惠有效期 */
|
||||||
|
validUntil?: string
|
||||||
|
}
|
||||||
/** 是否显示兑换码 */
|
/** 是否显示兑换码 */
|
||||||
showCode?: boolean
|
showCode?: boolean
|
||||||
/** 是否显示使用按钮 */
|
/** 是否显示使用按钮 */
|
||||||
showUseBtn?: boolean
|
showUseBtn?: boolean
|
||||||
/** 是否显示详情按钮 */
|
/** 是否显示详情按钮 */
|
||||||
showDetailBtn?: boolean
|
showDetailBtn?: boolean
|
||||||
|
/** 是否显示商品详情 */
|
||||||
|
showGoodsDetail?: boolean
|
||||||
/** 卡片主题色 */
|
/** 卡片主题色 */
|
||||||
theme?: 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple'
|
theme?: 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple'
|
||||||
/** 使用按钮点击事件 */
|
/** 使用按钮点击事件 */
|
||||||
@@ -49,24 +91,34 @@ export interface GiftCardProps {
|
|||||||
const GiftCard: React.FC<GiftCardProps> = ({
|
const GiftCard: React.FC<GiftCardProps> = ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
goodsName,
|
||||||
description,
|
description,
|
||||||
code,
|
code,
|
||||||
goodsImage,
|
goodsImage,
|
||||||
|
goodsImages,
|
||||||
faceValue,
|
faceValue,
|
||||||
|
originalPrice,
|
||||||
type = 10,
|
type = 10,
|
||||||
useStatus = 0,
|
useStatus = 0,
|
||||||
expireTime,
|
expireTime,
|
||||||
useTime,
|
useTime,
|
||||||
useLocation,
|
useLocation,
|
||||||
contactInfo,
|
contactInfo,
|
||||||
|
goodsInfo,
|
||||||
|
promotionInfo,
|
||||||
showCode = false,
|
showCode = false,
|
||||||
showUseBtn = false,
|
showUseBtn = false,
|
||||||
showDetailBtn = true,
|
showDetailBtn = true,
|
||||||
|
showGoodsDetail = true,
|
||||||
theme = 'gold',
|
theme = 'gold',
|
||||||
onUse,
|
onUse,
|
||||||
onDetail,
|
onDetail,
|
||||||
onClick
|
onClick
|
||||||
}) => {
|
}) => {
|
||||||
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
|
|
||||||
|
// 获取显示名称,优先使用商品名称
|
||||||
|
const displayName = goodsName || name
|
||||||
// 获取礼品卡类型文本
|
// 获取礼品卡类型文本
|
||||||
const getTypeText = () => {
|
const getTypeText = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -142,7 +194,29 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
|||||||
return code.replace(/(.{4})/g, '$1 ').trim()
|
return code.replace(/(.{4})/g, '$1 ').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取商品图片列表
|
||||||
|
const getImageList = () => {
|
||||||
|
if (goodsImages && goodsImages.length > 0) {
|
||||||
|
return goodsImages
|
||||||
|
}
|
||||||
|
if (goodsImage) {
|
||||||
|
return [goodsImage]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算折扣百分比
|
||||||
|
const getDiscountPercent = () => {
|
||||||
|
if (!originalPrice || !faceValue) return null
|
||||||
|
const original = parseFloat(originalPrice)
|
||||||
|
const current = parseFloat(faceValue)
|
||||||
|
if (original <= current) return null
|
||||||
|
return Math.round(((original - current) / original) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
const statusInfo = getStatusInfo()
|
const statusInfo = getStatusInfo()
|
||||||
|
const imageList = getImageList()
|
||||||
|
const discountPercent = getDiscountPercent()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -155,8 +229,7 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
|||||||
<Gift size="24" className="text-white" />
|
<Gift size="24" className="text-white" />
|
||||||
</View>
|
</View>
|
||||||
<View className="gift-card-title">
|
<View className="gift-card-title">
|
||||||
<Text className="title-text">{name}</Text>
|
<Text className="title-text">{getTypeText()}</Text>
|
||||||
<Text className="type-text">{getTypeText()}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View className="gift-card-status">
|
<View className="gift-card-status">
|
||||||
<Tag type={statusInfo.color}>{statusInfo.text}</Tag>
|
<Tag type={statusInfo.color}>{statusInfo.text}</Tag>
|
||||||
@@ -166,29 +239,75 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
|||||||
{/* 卡片主体 */}
|
{/* 卡片主体 */}
|
||||||
<View className="gift-card-body">
|
<View className="gift-card-body">
|
||||||
<View className="gift-card-content">
|
<View className="gift-card-content">
|
||||||
{/* 商品图片 */}
|
|
||||||
{goodsImage && (
|
|
||||||
<View className="gift-image">
|
|
||||||
<Image
|
|
||||||
src={goodsImage}
|
|
||||||
className="w-16 h-16 rounded-lg object-cover"
|
|
||||||
mode="aspectFill"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View className="gift-info">
|
<View className="gift-info">
|
||||||
{/* 面值 */}
|
{/* 商品基本信息 */}
|
||||||
{faceValue && (
|
<View className="goods-basic-info">
|
||||||
<View className="gift-value">
|
{/* 商品名称 */}
|
||||||
<Text className="value-label">面值</Text>
|
{goodsName && (
|
||||||
<Text className="value-amount">¥{faceValue}</Text>
|
<View className="brand-category">
|
||||||
|
<Text className="brand-text">{goodsName}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 描述 */}
|
{/* 价格信息 */}
|
||||||
{description && (
|
<View className="price-info">
|
||||||
<Text className="gift-description">{description}</Text>
|
{faceValue && (
|
||||||
|
<View className="current-price">
|
||||||
|
<Text className="price-symbol">¥</Text>
|
||||||
|
<Text className="price-amount">{faceValue}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{originalPrice && originalPrice !== faceValue && (
|
||||||
|
<Text className="original-price">原价¥{originalPrice}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 评分和评价 */}
|
||||||
|
{goodsInfo?.rating && (
|
||||||
|
<View className="rating-info">
|
||||||
|
<Rate
|
||||||
|
value={goodsInfo.rating}
|
||||||
|
readonly
|
||||||
|
size="12"
|
||||||
|
spacing="2"
|
||||||
|
/>
|
||||||
|
<Text className="rating-text">{goodsInfo.rating}</Text>
|
||||||
|
{goodsInfo.reviewCount && (
|
||||||
|
<Text className="review-count">({goodsInfo.reviewCount}条评价)</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 规格和库存 */}
|
||||||
|
{showGoodsDetail && (goodsInfo?.specification || goodsInfo?.stock !== undefined) && (
|
||||||
|
<View className="goods-specs">
|
||||||
|
{goodsInfo.stock !== undefined && (
|
||||||
|
<View className="spec-item">
|
||||||
|
<Text className="spec-label">库存:</Text>
|
||||||
|
<Text className={`spec-value ${goodsInfo.stock > 0 ? 'in-stock' : 'out-stock'}`}>
|
||||||
|
{goodsInfo.stock > 0 ? `${goodsInfo.stock}件` : '缺货'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 优惠信息 */}
|
||||||
|
{promotionInfo && (
|
||||||
|
<View className="promotion-info">
|
||||||
|
<View className="promotion-header">
|
||||||
|
<Gift size="14" className="promotion-icon" />
|
||||||
|
<Text className="promotion-title">优惠活动</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="promotion-desc">{promotionInfo.description}</Text>
|
||||||
|
{promotionInfo.validUntil && (
|
||||||
|
<Text className="promotion-valid">
|
||||||
|
有效期至:{dayjs(promotionInfo.validUntil).format('YYYY-MM-DD')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 兑换码 */}
|
{/* 兑换码 */}
|
||||||
@@ -201,6 +320,27 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 使用说明和注意事项 */}
|
||||||
|
{showGoodsDetail && (goodsInfo?.instructions || goodsInfo?.notices || goodsInfo?.applicableStores) && (
|
||||||
|
<View className="goods-instructions">
|
||||||
|
{goodsInfo.applicableStores && goodsInfo.applicableStores.length > 0 && (
|
||||||
|
<View className="instruction-section">
|
||||||
|
<View className="section-header">
|
||||||
|
<ShoppingCart size="14" className="section-icon" />
|
||||||
|
<Text className="section-title">适用门店</Text>
|
||||||
|
</View>
|
||||||
|
<View className="store-list">
|
||||||
|
{goodsInfo.applicableStores.map((store, index) => (
|
||||||
|
<Tag key={index} size="small" plain className="store-tag">
|
||||||
|
{store}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 时间信息 */}
|
{/* 时间信息 */}
|
||||||
<View className="gift-time-info">
|
<View className="gift-time-info">
|
||||||
{useStatus === 1 && useTime && (
|
{useStatus === 1 && useTime && (
|
||||||
@@ -238,19 +378,6 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="footer-actions">
|
<View className="footer-actions">
|
||||||
{showDetailBtn && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onDetail?.()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
详情
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showUseBtn && useStatus === 0 && (
|
{showUseBtn && useStatus === 0 && (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
140
src/components/GiftCardExample.tsx
Normal file
140
src/components/GiftCardExample.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View } from '@tarojs/components'
|
||||||
|
import GiftCard from './GiftCard'
|
||||||
|
|
||||||
|
const GiftCardExample: React.FC = () => {
|
||||||
|
// 示例数据
|
||||||
|
const giftCardData = {
|
||||||
|
id: 1,
|
||||||
|
name: '星巴克咖啡礼品卡',
|
||||||
|
description: '享受醇香咖啡时光,适用于全国星巴克门店',
|
||||||
|
code: 'SB2024001234567890',
|
||||||
|
goodsImages: [
|
||||||
|
'https://example.com/starbucks-card-1.jpg',
|
||||||
|
'https://example.com/starbucks-card-2.jpg',
|
||||||
|
'https://example.com/starbucks-card-3.jpg'
|
||||||
|
],
|
||||||
|
faceValue: '100',
|
||||||
|
originalPrice: '120',
|
||||||
|
type: 20, // 虚拟礼品卡
|
||||||
|
useStatus: 0, // 可用
|
||||||
|
expireTime: '2024-12-31 23:59:59',
|
||||||
|
contactInfo: '400-800-8888',
|
||||||
|
goodsInfo: {
|
||||||
|
brand: '星巴克',
|
||||||
|
specification: '电子礼品卡',
|
||||||
|
category: '餐饮美食',
|
||||||
|
stock: 999,
|
||||||
|
rating: 4.8,
|
||||||
|
reviewCount: 1256,
|
||||||
|
tags: ['热门', '全国通用', '无需预约', '即买即用'],
|
||||||
|
instructions: [
|
||||||
|
'出示兑换码至门店收银台即可使用',
|
||||||
|
'可用于购买任意饮品和食品',
|
||||||
|
'不可兑换现金,不设找零',
|
||||||
|
'单次可使用多张礼品卡'
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经售出,不可退换',
|
||||||
|
'请妥善保管兑换码,遗失不补',
|
||||||
|
'部分特殊商品可能不适用',
|
||||||
|
'具体使用规则以门店公告为准'
|
||||||
|
],
|
||||||
|
applicableStores: [
|
||||||
|
'全国星巴克门店',
|
||||||
|
'机场店',
|
||||||
|
'高铁站店',
|
||||||
|
'商场店'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
promotionInfo: {
|
||||||
|
type: 'discount' as const,
|
||||||
|
description: '限时优惠:满100减20,买2张送1张咖啡券',
|
||||||
|
amount: '20',
|
||||||
|
validUntil: '2024-09-30 23:59:59'
|
||||||
|
},
|
||||||
|
showCode: true,
|
||||||
|
showUseBtn: true,
|
||||||
|
showDetailBtn: true,
|
||||||
|
showGoodsDetail: true,
|
||||||
|
theme: 'green' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUse = () => {
|
||||||
|
console.log('使用礼品卡')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetail = () => {
|
||||||
|
console.log('查看详情')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
console.log('点击礼品卡')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="gift-card-example">
|
||||||
|
<GiftCard
|
||||||
|
{...giftCardData}
|
||||||
|
onUse={handleUse}
|
||||||
|
onDetail={handleDetail}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 简化版本示例 */}
|
||||||
|
<GiftCard
|
||||||
|
id={2}
|
||||||
|
name="麦当劳优惠券"
|
||||||
|
description="美味汉堡套餐,限时优惠"
|
||||||
|
goodsImage="https://example.com/mcd-card.jpg"
|
||||||
|
faceValue="50"
|
||||||
|
originalPrice="60"
|
||||||
|
type={20}
|
||||||
|
useStatus={0}
|
||||||
|
expireTime="2024-10-31 23:59:59"
|
||||||
|
goodsInfo={{
|
||||||
|
brand: '麦当劳',
|
||||||
|
category: '快餐',
|
||||||
|
rating: 4.5,
|
||||||
|
reviewCount: 892,
|
||||||
|
tags: ['快餐', '全国通用']
|
||||||
|
}}
|
||||||
|
showCode={false}
|
||||||
|
showUseBtn={true}
|
||||||
|
showDetailBtn={true}
|
||||||
|
showGoodsDetail={false}
|
||||||
|
theme="blue"
|
||||||
|
onUse={handleUse}
|
||||||
|
onDetail={handleDetail}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 已使用状态示例 */}
|
||||||
|
<GiftCard
|
||||||
|
id={3}
|
||||||
|
name="海底捞火锅券"
|
||||||
|
description="享受正宗川味火锅"
|
||||||
|
goodsImage="https://example.com/haidilao-card.jpg"
|
||||||
|
faceValue="200"
|
||||||
|
type={30}
|
||||||
|
useStatus={1}
|
||||||
|
useTime="2024-08-15 19:30:00"
|
||||||
|
useLocation="海底捞王府井店"
|
||||||
|
goodsInfo={{
|
||||||
|
brand: '海底捞',
|
||||||
|
category: '火锅',
|
||||||
|
rating: 4.9,
|
||||||
|
reviewCount: 2341
|
||||||
|
}}
|
||||||
|
showCode={false}
|
||||||
|
showUseBtn={false}
|
||||||
|
showDetailBtn={true}
|
||||||
|
theme="gold"
|
||||||
|
onDetail={handleDetail}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GiftCardExample
|
||||||
@@ -67,6 +67,7 @@ const GiftCardList: React.FC<GiftCardListProps> = ({
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
gifts.map((gift, index) => (
|
gifts.map((gift, index) => (
|
||||||
|
<>
|
||||||
<GiftCard
|
<GiftCard
|
||||||
key={gift.id || index}
|
key={gift.id || index}
|
||||||
{...gift}
|
{...gift}
|
||||||
@@ -74,6 +75,7 @@ const GiftCardList: React.FC<GiftCardListProps> = ({
|
|||||||
onUse={() => handleGiftUse(gift, index)}
|
onUse={() => handleGiftUse(gift, index)}
|
||||||
onDetail={() => handleGiftDetail(gift, index)}
|
onDetail={() => handleGiftDetail(gift, index)}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -221,6 +221,19 @@ const OrderConfirm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优惠券校验
|
||||||
|
if (selectedCoupon) {
|
||||||
|
const total = getGoodsTotal()
|
||||||
|
if (!isCouponUsable(selectedCoupon, total)) {
|
||||||
|
const reason = getCouponUnusableReason(selectedCoupon, total)
|
||||||
|
Taro.showToast({
|
||||||
|
title: reason || '优惠券不可用',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 构建订单数据
|
// 构建订单数据
|
||||||
const orderData = buildSingleGoodsOrder(
|
const orderData = buildSingleGoodsOrder(
|
||||||
goods.goodsId!,
|
goods.goodsId!,
|
||||||
@@ -230,26 +243,58 @@ const OrderConfirm = () => {
|
|||||||
comments: goods.name,
|
comments: goods.name,
|
||||||
deliveryType: 0,
|
deliveryType: 0,
|
||||||
buyerRemarks: orderRemark,
|
buyerRemarks: orderRemark,
|
||||||
couponId: selectedCoupon ? selectedCoupon.id : undefined
|
// 确保couponId是数字类型
|
||||||
|
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 根据支付方式选择支付类型
|
// 根据支付方式选择支付类型
|
||||||
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
||||||
|
|
||||||
// 执行支付
|
console.log('开始支付:', {
|
||||||
|
orderData,
|
||||||
|
paymentType,
|
||||||
|
selectedCoupon: selectedCoupon ? {
|
||||||
|
id: selectedCoupon.id,
|
||||||
|
title: selectedCoupon.title,
|
||||||
|
discount: getCouponDiscount()
|
||||||
|
} : null,
|
||||||
|
finalPrice: getFinalPrice()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行支付 - 移除这里的成功提示,让PaymentHandler统一处理
|
||||||
await PaymentHandler.pay(orderData, paymentType);
|
await PaymentHandler.pay(orderData, paymentType);
|
||||||
|
|
||||||
Taro.showToast({
|
// ✅ 移除双重成功提示 - PaymentHandler会处理成功提示
|
||||||
title: '支付成功',
|
// Taro.showToast({
|
||||||
icon: 'success'
|
// title: '支付成功',
|
||||||
})
|
// icon: 'success'
|
||||||
} catch (error) {
|
// })
|
||||||
|
} catch (error: any) {
|
||||||
console.error('支付失败:', error)
|
console.error('支付失败:', error)
|
||||||
|
|
||||||
|
// 只处理PaymentHandler未处理的错误
|
||||||
|
if (!error.handled) {
|
||||||
|
let errorMessage = '支付失败,请重试';
|
||||||
|
|
||||||
|
// 根据错误类型提供具体提示
|
||||||
|
if (error.message?.includes('余额不足')) {
|
||||||
|
errorMessage = '账户余额不足,请充值后重试';
|
||||||
|
} else if (error.message?.includes('优惠券')) {
|
||||||
|
errorMessage = '优惠券使用失败,请重新选择';
|
||||||
|
} else if (error.message?.includes('库存')) {
|
||||||
|
errorMessage = '商品库存不足,请减少购买数量';
|
||||||
|
} else if (error.message?.includes('地址')) {
|
||||||
|
errorMessage = '收货地址信息有误,请重新选择';
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '支付失败,请重试',
|
title: errorMessage,
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setPayLoading(false)
|
setPayLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
202
src/types/giftCard.ts
Normal file
202
src/types/giftCard.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* 礼品卡相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 礼品卡类型枚举 */
|
||||||
|
export enum GiftCardType {
|
||||||
|
/** 实物礼品卡 */
|
||||||
|
PHYSICAL = 10,
|
||||||
|
/** 虚拟礼品卡 */
|
||||||
|
VIRTUAL = 20,
|
||||||
|
/** 服务礼品卡 */
|
||||||
|
SERVICE = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使用状态枚举 */
|
||||||
|
export enum UseStatus {
|
||||||
|
/** 可用 */
|
||||||
|
AVAILABLE = 0,
|
||||||
|
/** 已使用 */
|
||||||
|
USED = 1,
|
||||||
|
/** 已过期 */
|
||||||
|
EXPIRED = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 优惠类型枚举 */
|
||||||
|
export enum PromotionType {
|
||||||
|
/** 折扣 */
|
||||||
|
DISCOUNT = 'discount',
|
||||||
|
/** 赠品 */
|
||||||
|
GIFT = 'gift',
|
||||||
|
/** 返现 */
|
||||||
|
CASHBACK = 'cashback'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡片主题枚举 */
|
||||||
|
export enum CardTheme {
|
||||||
|
GOLD = 'gold',
|
||||||
|
SILVER = 'silver',
|
||||||
|
BRONZE = 'bronze',
|
||||||
|
BLUE = 'blue',
|
||||||
|
GREEN = 'green',
|
||||||
|
PURPLE = 'purple'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 商品信息接口 */
|
||||||
|
export interface GoodsInfo {
|
||||||
|
/** 商品品牌 */
|
||||||
|
brand?: string
|
||||||
|
/** 商品规格 */
|
||||||
|
specification?: string
|
||||||
|
/** 商品分类 */
|
||||||
|
category?: string
|
||||||
|
/** 库存数量 */
|
||||||
|
stock?: number
|
||||||
|
/** 商品评分 */
|
||||||
|
rating?: number
|
||||||
|
/** 评价数量 */
|
||||||
|
reviewCount?: number
|
||||||
|
/** 商品标签 */
|
||||||
|
tags?: string[]
|
||||||
|
/** 使用说明 */
|
||||||
|
instructions?: string[]
|
||||||
|
/** 注意事项 */
|
||||||
|
notices?: string[]
|
||||||
|
/** 适用门店 */
|
||||||
|
applicableStores?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 优惠信息接口 */
|
||||||
|
export interface PromotionInfo {
|
||||||
|
/** 优惠类型 */
|
||||||
|
type?: PromotionType
|
||||||
|
/** 优惠描述 */
|
||||||
|
description?: string
|
||||||
|
/** 优惠金额 */
|
||||||
|
amount?: string
|
||||||
|
/** 优惠有效期 */
|
||||||
|
validUntil?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 礼品卡数据接口 */
|
||||||
|
export interface GiftCardData {
|
||||||
|
/** 礼品卡ID */
|
||||||
|
id: number
|
||||||
|
/** 礼品卡名称 */
|
||||||
|
name: string
|
||||||
|
/** 商品名称 */
|
||||||
|
goodsName?: string
|
||||||
|
/** 礼品卡描述 */
|
||||||
|
description?: string
|
||||||
|
/** 礼品卡兑换码 */
|
||||||
|
code?: string
|
||||||
|
/** 商品图片 */
|
||||||
|
goodsImage?: string
|
||||||
|
/** 商品图片列表 */
|
||||||
|
goodsImages?: string[]
|
||||||
|
/** 礼品卡面值 */
|
||||||
|
faceValue?: string
|
||||||
|
/** 商品原价 */
|
||||||
|
originalPrice?: string
|
||||||
|
/** 礼品卡类型 */
|
||||||
|
type?: GiftCardType
|
||||||
|
/** 使用状态 */
|
||||||
|
useStatus?: UseStatus
|
||||||
|
/** 过期时间 */
|
||||||
|
expireTime?: string
|
||||||
|
/** 使用时间 */
|
||||||
|
useTime?: string
|
||||||
|
/** 使用地址 */
|
||||||
|
useLocation?: string
|
||||||
|
/** 客服联系方式 */
|
||||||
|
contactInfo?: string
|
||||||
|
/** 商品信息 */
|
||||||
|
goodsInfo?: GoodsInfo
|
||||||
|
/** 优惠信息 */
|
||||||
|
promotionInfo?: PromotionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 礼品卡组件配置接口 */
|
||||||
|
export interface GiftCardConfig {
|
||||||
|
/** 是否显示兑换码 */
|
||||||
|
showCode?: boolean
|
||||||
|
/** 是否显示使用按钮 */
|
||||||
|
showUseBtn?: boolean
|
||||||
|
/** 是否显示详情按钮 */
|
||||||
|
showDetailBtn?: boolean
|
||||||
|
/** 是否显示商品详情 */
|
||||||
|
showGoodsDetail?: boolean
|
||||||
|
/** 卡片主题色 */
|
||||||
|
theme?: CardTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 礼品卡事件接口 */
|
||||||
|
export interface GiftCardEvents {
|
||||||
|
/** 使用按钮点击事件 */
|
||||||
|
onUse?: () => void
|
||||||
|
/** 详情按钮点击事件 */
|
||||||
|
onDetail?: () => void
|
||||||
|
/** 卡片点击事件 */
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完整的礼品卡属性接口 */
|
||||||
|
export interface GiftCardProps extends GiftCardData, GiftCardConfig, GiftCardEvents {}
|
||||||
|
|
||||||
|
/** 礼品卡列表项接口 */
|
||||||
|
export interface GiftCardListItem extends GiftCardData {
|
||||||
|
/** 是否选中 */
|
||||||
|
selected?: boolean
|
||||||
|
/** 是否禁用 */
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 礼品卡筛选条件接口 */
|
||||||
|
export interface GiftCardFilter {
|
||||||
|
/** 类型筛选 */
|
||||||
|
type?: GiftCardType[]
|
||||||
|
/** 状态筛选 */
|
||||||
|
useStatus?: UseStatus[]
|
||||||
|
/** 品牌筛选 */
|
||||||
|
brand?: string[]
|
||||||
|
/** 分类筛选 */
|
||||||
|
category?: string[]
|
||||||
|
/** 价格范围 */
|
||||||
|
priceRange?: {
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
/** 评分范围 */
|
||||||
|
ratingRange?: {
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
/** 是否有库存 */
|
||||||
|
inStock?: boolean
|
||||||
|
/** 关键词搜索 */
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 礼品卡排序选项接口 */
|
||||||
|
export interface GiftCardSort {
|
||||||
|
/** 排序字段 */
|
||||||
|
field: 'price' | 'rating' | 'reviewCount' | 'expireTime' | 'createTime'
|
||||||
|
/** 排序方向 */
|
||||||
|
order: 'asc' | 'desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API响应接口 */
|
||||||
|
export interface GiftCardApiResponse {
|
||||||
|
/** 状态码 */
|
||||||
|
code: number
|
||||||
|
/** 响应消息 */
|
||||||
|
message: string
|
||||||
|
/** 数据 */
|
||||||
|
data: GiftCardData[]
|
||||||
|
/** 总数 */
|
||||||
|
total?: number
|
||||||
|
/** 当前页 */
|
||||||
|
page?: number
|
||||||
|
/** 每页数量 */
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
201
src/user/gift/demo.tsx
Normal file
201
src/user/gift/demo.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Button, Tabs, TabPane } from '@nutui/nutui-react-taro'
|
||||||
|
import GiftCard from '@/components/GiftCard'
|
||||||
|
import { ShopGift } from '@/api/shop/shopGift/model'
|
||||||
|
|
||||||
|
const GiftCardDemo: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('0')
|
||||||
|
|
||||||
|
// 模拟不同类型的礼品卡数据
|
||||||
|
const mockGifts: ShopGift[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '星巴克礼品卡',
|
||||||
|
goodsName: '星巴克咖啡礼品卡(电子版)',
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg',
|
||||||
|
description: '享受醇香咖啡时光,适用于全国星巴克门店',
|
||||||
|
code: 'SB2024001234567890',
|
||||||
|
goodsId: 101,
|
||||||
|
faceValue: '100',
|
||||||
|
type: 20,
|
||||||
|
useStatus: 0,
|
||||||
|
expireTime: '2024-12-31 23:59:59',
|
||||||
|
instructions: '请在有效期内使用,出示兑换码即可使用',
|
||||||
|
contactInfo: '400-800-8888'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '麦当劳优惠券',
|
||||||
|
goodsName: '麦当劳经典套餐券',
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i2/2206571109/O1CN01ABC123_!!2206571109.jpg',
|
||||||
|
description: '美味汉堡套餐,限时优惠',
|
||||||
|
code: 'MCD2024987654321',
|
||||||
|
goodsId: 102,
|
||||||
|
faceValue: '50',
|
||||||
|
type: 20,
|
||||||
|
useStatus: 0,
|
||||||
|
expireTime: '2024-10-31 23:59:59',
|
||||||
|
instructions: '适用于全国麦当劳门店,不可与其他优惠同享',
|
||||||
|
contactInfo: '400-517-517'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '海底捞火锅券',
|
||||||
|
goodsName: '海底捞火锅代金券',
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i3/2206571109/O1CN01DEF456_!!2206571109.jpg',
|
||||||
|
description: '享受正宗川味火锅',
|
||||||
|
code: 'HDL2024555666777',
|
||||||
|
goodsId: 103,
|
||||||
|
faceValue: '200',
|
||||||
|
type: 30,
|
||||||
|
useStatus: 1,
|
||||||
|
useTime: '2024-08-15 19:30:00',
|
||||||
|
useLocation: '海底捞王府井店',
|
||||||
|
instructions: '需提前预约,适用于全国海底捞门店',
|
||||||
|
contactInfo: '400-869-8888'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const transformGiftData = (gift: ShopGift) => {
|
||||||
|
return {
|
||||||
|
id: gift.id || 0,
|
||||||
|
name: gift.goodsName || gift.name || '礼品卡',
|
||||||
|
description: gift.description || gift.instructions,
|
||||||
|
code: gift.code,
|
||||||
|
goodsImage: gift.goodsImage,
|
||||||
|
faceValue: gift.faceValue,
|
||||||
|
type: gift.type,
|
||||||
|
useStatus: gift.useStatus,
|
||||||
|
expireTime: gift.expireTime,
|
||||||
|
useTime: gift.useTime,
|
||||||
|
useLocation: gift.useLocation,
|
||||||
|
contactInfo: gift.contactInfo,
|
||||||
|
goodsInfo: {
|
||||||
|
...(gift.goodsId && {
|
||||||
|
specification: `礼品卡面值:¥${gift.faceValue}`,
|
||||||
|
category: getTypeText(gift.type),
|
||||||
|
tags: [
|
||||||
|
getTypeText(gift.type),
|
||||||
|
gift.useStatus === 0 ? '可使用' : gift.useStatus === 1 ? '已使用' : '已过期',
|
||||||
|
'全国通用',
|
||||||
|
gift.type === 20 ? '即买即用' : '需预约'
|
||||||
|
].filter(Boolean),
|
||||||
|
instructions: gift.instructions ? [gift.instructions] : [
|
||||||
|
'请在有效期内使用',
|
||||||
|
'出示兑换码即可使用',
|
||||||
|
'不可兑换现金'
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经使用不可退换',
|
||||||
|
'请妥善保管兑换码',
|
||||||
|
'如有疑问请联系客服'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showCode: gift.useStatus === 0,
|
||||||
|
showUseBtn: gift.useStatus === 0,
|
||||||
|
showDetailBtn: true,
|
||||||
|
showGoodsDetail: true,
|
||||||
|
theme: getThemeByType(gift.type),
|
||||||
|
onUse: () => handleUse(gift),
|
||||||
|
onDetail: () => handleDetail(gift),
|
||||||
|
onClick: () => handleClick(gift)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeText = (type?: number): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return '实物礼品卡'
|
||||||
|
case 20: return '虚拟礼品卡'
|
||||||
|
case 30: return '服务礼品卡'
|
||||||
|
default: return '礼品卡'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return 'gold'
|
||||||
|
case 20: return 'blue'
|
||||||
|
case 30: return 'green'
|
||||||
|
default: return 'silver'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUse = (gift: ShopGift) => {
|
||||||
|
console.log('使用礼品卡:', gift.goodsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetail = (gift: ShopGift) => {
|
||||||
|
console.log('查看详情:', gift.goodsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (gift: ShopGift) => {
|
||||||
|
console.log('点击礼品卡:', gift.goodsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据状态筛选礼品卡
|
||||||
|
const getFilteredGifts = () => {
|
||||||
|
const statusMap = {
|
||||||
|
'0': 0, // 可用
|
||||||
|
'1': 1, // 已使用
|
||||||
|
'2': 2 // 已过期
|
||||||
|
}
|
||||||
|
const targetStatus = statusMap[activeTab as keyof typeof statusMap]
|
||||||
|
return mockGifts.filter(gift => gift.useStatus === targetStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<View className="bg-white px-4 py-3 border-b border-gray-100">
|
||||||
|
<Text className="text-lg font-bold text-center">
|
||||||
|
礼品卡商品信息展示演示
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Tab切换 */}
|
||||||
|
<View className="bg-white">
|
||||||
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
|
<TabPane title="可用" value="0" />
|
||||||
|
<TabPane title="已使用" value="1" />
|
||||||
|
<TabPane title="已过期" value="2" />
|
||||||
|
</Tabs>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 礼品卡列表 */}
|
||||||
|
<View className="p-4">
|
||||||
|
{getFilteredGifts().map((gift) => (
|
||||||
|
<View key={gift.id} className="mb-4">
|
||||||
|
<GiftCard {...transformGiftData(gift)} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{getFilteredGifts().length === 0 && (
|
||||||
|
<View className="text-center py-16">
|
||||||
|
<Text className="text-gray-500">
|
||||||
|
{activeTab === '0' ? '暂无可用礼品卡' :
|
||||||
|
activeTab === '1' ? '暂无已使用礼品卡' :
|
||||||
|
'暂无已过期礼品卡'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 功能说明 */}
|
||||||
|
<View className="bg-white mx-4 mb-4 p-4 rounded-lg">
|
||||||
|
<Text className="font-bold mb-2">功能特性:</Text>
|
||||||
|
<View className="space-y-1">
|
||||||
|
<Text className="text-sm text-gray-600">• 优先显示商品名称(goodsName)</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 显示商品图片(goodsImage)</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 丰富的商品信息展示</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 不同状态的视觉效果</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 响应式设计适配</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GiftCardDemo
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '礼品卡详情',
|
navigationBarTitleText: '礼品卡详情',
|
||||||
navigationBarTextStyle: 'black',
|
navigationBarTextStyle: 'black',
|
||||||
navigationBarBackgroundColor: '#ffffff',
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
navigationStyle: 'custom'
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -168,20 +168,6 @@ const GiftCardDetail = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
{/* 自定义导航栏 */}
|
|
||||||
<View className="flex items-center justify-between p-4 bg-white border-b border-gray-100">
|
|
||||||
<View className="flex items-center" onClick={handleBack}>
|
|
||||||
<ArrowLeft size="20" />
|
|
||||||
<Text className="ml-2 text-lg">礼品卡详情</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-center gap-3">
|
|
||||||
<View onClick={() => setShowShare(true)}>
|
|
||||||
<Share size="20" className="text-gray-600" />
|
|
||||||
</View>
|
|
||||||
<Tag type={statusInfo.color as any}>{statusInfo.text}</Tag>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 礼品卡卡片 */}
|
{/* 礼品卡卡片 */}
|
||||||
<View className="m-4 p-6 rounded-2xl text-white" style={{backgroundColor: '#fbbf24'}}>
|
<View className="m-4 p-6 rounded-2xl text-white" style={{backgroundColor: '#fbbf24'}}>
|
||||||
<View className="flex items-center justify-between mb-4">
|
<View className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
152
src/user/gift/goodsName-integration.md
Normal file
152
src/user/gift/goodsName-integration.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# GoodsName 字段集成说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
后端已新增 `goodsName` 字段,前端已完成相应的集成工作,现在礼品卡组件可以正确显示商品名称。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
### 1. GiftCard 组件接口更新
|
||||||
|
|
||||||
|
**文件**: `src/components/GiftCard.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface GiftCardProps {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
goodsName?: string // 新增:商品名称字段
|
||||||
|
description?: string
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**显示逻辑**:
|
||||||
|
```typescript
|
||||||
|
// 获取显示名称,优先使用商品名称
|
||||||
|
const displayName = goodsName || name
|
||||||
|
|
||||||
|
// 在模板中使用
|
||||||
|
<Text className="title-text">{displayName}</Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据转换函数更新
|
||||||
|
|
||||||
|
**文件**: `src/user/gift/index.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
||||||
|
return {
|
||||||
|
id: gift.id || 0,
|
||||||
|
name: gift.name || '礼品卡',
|
||||||
|
goodsName: gift.goodsName, // 传递商品名称
|
||||||
|
// ... 其他字段映射
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 类型定义更新
|
||||||
|
|
||||||
|
**文件**: `src/types/giftCard.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface GiftCardData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
goodsName?: string // 新增字段
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 显示规则
|
||||||
|
|
||||||
|
### 优先级规则
|
||||||
|
1. **有 `goodsName`**: 显示商品名称
|
||||||
|
2. **无 `goodsName`**: 显示礼品卡名称 (`name`)
|
||||||
|
|
||||||
|
### 示例对比
|
||||||
|
|
||||||
|
| 数据情况 | name | goodsName | 显示结果 |
|
||||||
|
|---------|------|-----------|----------|
|
||||||
|
| 情况1 | "星巴克礼品卡" | "星巴克经典拿铁咖啡券" | "星巴克经典拿铁咖啡券" |
|
||||||
|
| 情况2 | "通用礼品卡" | null/undefined | "通用礼品卡" |
|
||||||
|
| 情况3 | "麦当劳优惠券" | "麦当劳巨无霸套餐券" | "麦当劳巨无霸套餐券" |
|
||||||
|
|
||||||
|
## 后端数据结构
|
||||||
|
|
||||||
|
确保后端返回的 `ShopGift` 对象包含 `goodsName` 字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "星巴克礼品卡",
|
||||||
|
"goodsName": "星巴克经典拿铁咖啡券",
|
||||||
|
"goodsImage": "https://example.com/image.jpg",
|
||||||
|
"faceValue": "100",
|
||||||
|
"type": 20,
|
||||||
|
"useStatus": 0,
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
### 测试页面
|
||||||
|
访问测试页面验证显示效果:
|
||||||
|
- `/user/gift/goodsname-test` - 专门测试 goodsName 字段的页面
|
||||||
|
|
||||||
|
### 测试用例
|
||||||
|
1. **有商品名称的礼品卡**: 应显示 `goodsName` 的值
|
||||||
|
2. **无商品名称的礼品卡**: 应显示 `name` 的值
|
||||||
|
3. **不同状态的礼品卡**: 确保各种状态下名称显示正确
|
||||||
|
4. **长名称处理**: 确保长商品名称不会破坏布局
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
### 向后兼容
|
||||||
|
- 对于没有 `goodsName` 字段的旧数据,组件会自动使用 `name` 字段
|
||||||
|
- 不会影响现有功能的正常使用
|
||||||
|
|
||||||
|
### 数据验证
|
||||||
|
```typescript
|
||||||
|
// 在组件中的处理逻辑
|
||||||
|
const displayName = goodsName || name || '礼品卡'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 样式优化
|
||||||
|
|
||||||
|
### 长名称处理
|
||||||
|
```scss
|
||||||
|
.title-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应式适配
|
||||||
|
- 小屏幕设备上自动调整显示宽度
|
||||||
|
- 保持良好的视觉效果
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **数据完整性**: 建议后端确保重要礼品卡都有 `goodsName` 字段
|
||||||
|
2. **名称长度**: 商品名称不宜过长,建议控制在20个字符以内
|
||||||
|
3. **特殊字符**: 确保商品名称不包含可能影响显示的特殊字符
|
||||||
|
4. **多语言**: 如需支持多语言,`goodsName` 也需要相应的国际化处理
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. **搜索功能**: 在搜索时同时匹配 `name` 和 `goodsName`
|
||||||
|
2. **排序功能**: 支持按商品名称排序
|
||||||
|
3. **筛选功能**: 支持按是否有商品名称筛选
|
||||||
|
4. **统计功能**: 统计有商品名称的礼品卡比例
|
||||||
|
|
||||||
|
## 部署检查清单
|
||||||
|
|
||||||
|
- [ ] 后端 API 返回 `goodsName` 字段
|
||||||
|
- [ ] 前端组件正确显示商品名称
|
||||||
|
- [ ] 测试页面验证通过
|
||||||
|
- [ ] 兼容性测试通过
|
||||||
|
- [ ] 样式在各设备上显示正常
|
||||||
|
- [ ] 长名称处理正确
|
||||||
191
src/user/gift/goodsname-test.tsx
Normal file
191
src/user/gift/goodsname-test.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import GiftCard from '@/components/GiftCard'
|
||||||
|
import { ShopGift } from '@/api/shop/shopGift/model'
|
||||||
|
|
||||||
|
const GoodsNameTest: React.FC = () => {
|
||||||
|
// 测试数据:包含 goodsName 字段的礼品卡
|
||||||
|
const testGifts: ShopGift[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '星巴克礼品卡',
|
||||||
|
goodsName: '星巴克经典拿铁咖啡券', // 后端新增的商品名称字段
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg',
|
||||||
|
description: '享受醇香咖啡时光',
|
||||||
|
code: 'SB2024001234567890',
|
||||||
|
goodsId: 101,
|
||||||
|
faceValue: '100',
|
||||||
|
type: 20,
|
||||||
|
useStatus: 0,
|
||||||
|
expireTime: '2024-12-31 23:59:59',
|
||||||
|
instructions: '适用于全国星巴克门店',
|
||||||
|
contactInfo: '400-800-8888'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '麦当劳优惠券',
|
||||||
|
goodsName: '麦当劳巨无霸套餐券', // 商品名称
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i2/2206571109/O1CN01ABC123_!!2206571109.jpg',
|
||||||
|
description: '美味汉堡套餐',
|
||||||
|
code: 'MCD2024987654321',
|
||||||
|
goodsId: 102,
|
||||||
|
faceValue: '50',
|
||||||
|
type: 20,
|
||||||
|
useStatus: 0,
|
||||||
|
expireTime: '2024-10-31 23:59:59',
|
||||||
|
instructions: '适用于全国麦当劳门店',
|
||||||
|
contactInfo: '400-517-517'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '通用礼品卡',
|
||||||
|
// 没有 goodsName,应该显示 name
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i3/2206571109/O1CN01DEF456_!!2206571109.jpg',
|
||||||
|
description: '通用型礼品卡',
|
||||||
|
code: 'GEN2024555666777',
|
||||||
|
faceValue: '200',
|
||||||
|
type: 10,
|
||||||
|
useStatus: 0,
|
||||||
|
expireTime: '2024-11-30 23:59:59',
|
||||||
|
instructions: '可在指定商户使用',
|
||||||
|
contactInfo: '400-123-456'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: '海底捞火锅券',
|
||||||
|
goodsName: '海底捞4人套餐券', // 已使用状态的商品
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i4/2206571109/O1CN01GHI789_!!2206571109.jpg',
|
||||||
|
description: '享受正宗川味火锅',
|
||||||
|
code: 'HDL2024888999000',
|
||||||
|
goodsId: 103,
|
||||||
|
faceValue: '300',
|
||||||
|
type: 30,
|
||||||
|
useStatus: 1, // 已使用
|
||||||
|
useTime: '2024-08-15 19:30:00',
|
||||||
|
useLocation: '海底捞王府井店',
|
||||||
|
instructions: '需提前预约',
|
||||||
|
contactInfo: '400-869-8888'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const transformGiftData = (gift: ShopGift) => {
|
||||||
|
return {
|
||||||
|
id: gift.id || 0,
|
||||||
|
name: gift.name || '礼品卡',
|
||||||
|
goodsName: gift.goodsName, // 传递商品名称
|
||||||
|
description: gift.description || gift.instructions,
|
||||||
|
code: gift.code,
|
||||||
|
goodsImage: gift.goodsImage,
|
||||||
|
faceValue: gift.faceValue,
|
||||||
|
type: gift.type,
|
||||||
|
useStatus: gift.useStatus,
|
||||||
|
expireTime: gift.expireTime,
|
||||||
|
useTime: gift.useTime,
|
||||||
|
useLocation: gift.useLocation,
|
||||||
|
contactInfo: gift.contactInfo,
|
||||||
|
goodsInfo: {
|
||||||
|
...((gift.goodsName || gift.goodsId) && {
|
||||||
|
specification: `礼品卡面值:¥${gift.faceValue}`,
|
||||||
|
category: getTypeText(gift.type),
|
||||||
|
tags: [
|
||||||
|
getTypeText(gift.type),
|
||||||
|
gift.useStatus === 0 ? '可使用' : gift.useStatus === 1 ? '已使用' : '已过期',
|
||||||
|
...(gift.goodsName ? ['商品礼品卡'] : [])
|
||||||
|
].filter(Boolean),
|
||||||
|
instructions: gift.instructions ? [gift.instructions] : [
|
||||||
|
'请在有效期内使用',
|
||||||
|
'出示兑换码即可使用',
|
||||||
|
'不可兑换现金'
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经使用不可退换',
|
||||||
|
'请妥善保管兑换码',
|
||||||
|
'如有疑问请联系客服'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showCode: gift.useStatus === 0,
|
||||||
|
showUseBtn: gift.useStatus === 0,
|
||||||
|
showDetailBtn: true,
|
||||||
|
showGoodsDetail: true,
|
||||||
|
theme: getThemeByType(gift.type),
|
||||||
|
onUse: () => console.log('使用:', gift.goodsName || gift.name),
|
||||||
|
onDetail: () => console.log('详情:', gift.goodsName || gift.name),
|
||||||
|
onClick: () => console.log('点击:', gift.goodsName || gift.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeText = (type?: number): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return '实物礼品卡'
|
||||||
|
case 20: return '虚拟礼品卡'
|
||||||
|
case 30: return '服务礼品卡'
|
||||||
|
default: return '礼品卡'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return 'gold'
|
||||||
|
case 20: return 'blue'
|
||||||
|
case 30: return 'green'
|
||||||
|
default: return 'silver'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<View className="bg-white px-4 py-3 border-b border-gray-100">
|
||||||
|
<Text className="text-lg font-bold text-center">
|
||||||
|
商品名称字段测试
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-gray-600 text-center mt-1">
|
||||||
|
测试 goodsName 字段的显示效果
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 测试说明 */}
|
||||||
|
<View className="bg-white mx-4 mt-4 p-4 rounded-lg">
|
||||||
|
<Text className="font-bold mb-2">测试说明:</Text>
|
||||||
|
<View className="space-y-1">
|
||||||
|
<Text className="text-sm text-gray-600">• 第1张:有 goodsName,显示"星巴克经典拿铁咖啡券"</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 第2张:有 goodsName,显示"麦当劳巨无霸套餐券"</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 第3张:无 goodsName,显示"通用礼品卡"</Text>
|
||||||
|
<Text className="text-sm text-gray-600">• 第4张:已使用状态,显示"海底捞4人套餐券"</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 礼品卡列表 */}
|
||||||
|
<View className="p-4">
|
||||||
|
{testGifts.map((gift, index) => (
|
||||||
|
<View key={gift.id} className="mb-4">
|
||||||
|
<View className="bg-blue-50 px-3 py-2 rounded-t-lg">
|
||||||
|
<Text className="text-sm font-medium text-blue-800">
|
||||||
|
测试 {index + 1}: {gift.goodsName ? `goodsName="${gift.goodsName}"` : '无 goodsName'}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs text-blue-600">
|
||||||
|
name="{gift.name}"
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<GiftCard {...transformGiftData(gift)} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 结果说明 */}
|
||||||
|
<View className="bg-white mx-4 mb-4 p-4 rounded-lg">
|
||||||
|
<Text className="font-bold mb-2">预期结果:</Text>
|
||||||
|
<View className="space-y-1">
|
||||||
|
<Text className="text-sm text-gray-600">✅ 有 goodsName 时,卡片标题显示商品名称</Text>
|
||||||
|
<Text className="text-sm text-gray-600">✅ 无 goodsName 时,卡片标题显示礼品卡名称</Text>
|
||||||
|
<Text className="text-sm text-gray-600">✅ 商品信息区域显示相关标签和说明</Text>
|
||||||
|
<Text className="text-sm text-gray-600">✅ 不同状态的视觉效果正确</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GoodsNameTest
|
||||||
@@ -60,6 +60,7 @@ const GiftCardManage = () => {
|
|||||||
const res = await getUserGifts({
|
const res = await getUserGifts({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
userId: Taro.getStorageSync('UserId'),
|
||||||
// keywords: searchValue,
|
// keywords: searchValue,
|
||||||
...statusFilter,
|
...statusFilter,
|
||||||
// 应用筛选条件
|
// 应用筛选条件
|
||||||
@@ -67,7 +68,7 @@ const GiftCardManage = () => {
|
|||||||
// sortBy: filters.sortBy,
|
// sortBy: filters.sortBy,
|
||||||
// sortOrder: filters.sortOrder
|
// sortOrder: filters.sortOrder
|
||||||
})
|
})
|
||||||
|
console.log(res?.list,'>>>>lalala')
|
||||||
if (res && res.list) {
|
if (res && res.list) {
|
||||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||||
setList(newList)
|
setList(newList)
|
||||||
@@ -123,10 +124,11 @@ const GiftCardManage = () => {
|
|||||||
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
||||||
return {
|
return {
|
||||||
id: gift.id || 0,
|
id: gift.id || 0,
|
||||||
name: gift.name || '礼品卡',
|
name: gift.name || '礼品卡', // 礼品卡名称
|
||||||
description: gift.description,
|
goodsName: gift.goodsName, // 商品名称(新增字段)
|
||||||
|
description: gift.description || gift.instructions, // 使用说明作为描述
|
||||||
code: gift.code,
|
code: gift.code,
|
||||||
goodsImage: gift.goodsImage,
|
goodsImage: gift.goodsImage, // 商品图片
|
||||||
faceValue: gift.faceValue,
|
faceValue: gift.faceValue,
|
||||||
type: gift.type,
|
type: gift.type,
|
||||||
useStatus: gift.useStatus,
|
useStatus: gift.useStatus,
|
||||||
@@ -134,15 +136,51 @@ const GiftCardManage = () => {
|
|||||||
useTime: gift.useTime,
|
useTime: gift.useTime,
|
||||||
useLocation: gift.useLocation,
|
useLocation: gift.useLocation,
|
||||||
contactInfo: gift.contactInfo,
|
contactInfo: gift.contactInfo,
|
||||||
|
// 添加商品信息
|
||||||
|
goodsInfo: {
|
||||||
|
// 如果有商品名称或商品ID,说明是关联商品的礼品卡
|
||||||
|
...((gift.goodsName || gift.goodsId) && {
|
||||||
|
specification: `礼品卡面值:¥${gift.faceValue}`,
|
||||||
|
category: getTypeText(gift.type),
|
||||||
|
tags: [
|
||||||
|
getTypeText(gift.type),
|
||||||
|
gift.useStatus === 0 ? '可使用' : gift.useStatus === 1 ? '已使用' : '已过期',
|
||||||
|
...(gift.goodsName ? ['商品礼品卡'] : [])
|
||||||
|
].filter(Boolean),
|
||||||
|
instructions: gift.instructions ? [gift.instructions] : [
|
||||||
|
'请在有效期内使用',
|
||||||
|
'出示兑换码即可使用',
|
||||||
|
'不可兑换现金',
|
||||||
|
...(gift.goodsName ? ['此礼品卡关联具体商品'] : [])
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经使用不可退换',
|
||||||
|
'请妥善保管兑换码',
|
||||||
|
'如有疑问请联系客服',
|
||||||
|
...(gift.goodsName ? ['商品以实际为准'] : [])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
showCode: gift.useStatus === 0, // 只有可用状态显示兑换码
|
showCode: gift.useStatus === 0, // 只有可用状态显示兑换码
|
||||||
showUseBtn: gift.useStatus === 0, // 只有可用状态显示使用按钮
|
showUseBtn: gift.useStatus === 0, // 只有可用状态显示使用按钮
|
||||||
showDetailBtn: true,
|
showDetailBtn: true,
|
||||||
|
showGoodsDetail: true, // 显示商品详情
|
||||||
theme: getThemeByType(gift.type),
|
theme: getThemeByType(gift.type),
|
||||||
onUse: () => handleUseGift(gift),
|
onUse: () => handleUseGift(gift),
|
||||||
onDetail: () => handleGiftDetail(gift)
|
onDetail: () => handleGiftDetail(gift)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取礼品卡类型文本
|
||||||
|
const getTypeText = (type?: number): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return '实物礼品卡'
|
||||||
|
case 20: return '虚拟礼品卡'
|
||||||
|
case 30: return '服务礼品卡'
|
||||||
|
default: return '礼品卡'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 根据礼品卡类型获取主题色
|
// 根据礼品卡类型获取主题色
|
||||||
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
|
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -188,31 +226,31 @@ const GiftCardManage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载礼品卡统计数据
|
// 加载礼品卡统计数据
|
||||||
const loadGiftStats = async () => {
|
// const loadGiftStats = async () => {
|
||||||
try {
|
// try {
|
||||||
// 并行获取各状态的礼品卡数量
|
// // 并行获取各状态的礼品卡数量
|
||||||
const [availableRes, usedRes, expiredRes] = await Promise.all([
|
// const [availableRes, usedRes, expiredRes] = await Promise.all([
|
||||||
getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
|
// getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
|
||||||
getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
|
// getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
|
||||||
getUserGifts({ page: 1, limit: 1, useStatus: 2 })
|
// getUserGifts({ page: 1, limit: 1, useStatus: 2 })
|
||||||
])
|
// ])
|
||||||
|
//
|
||||||
// 计算总价值(仅可用礼品卡)
|
// // 计算总价值(仅可用礼品卡)
|
||||||
const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
|
// const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
|
||||||
const totalValue = availableGifts?.list?.reduce((sum, gift) => {
|
// const totalValue = availableGifts?.list?.reduce((sum, gift) => {
|
||||||
return sum + parseFloat(gift.faceValue || '0')
|
// return sum + parseFloat(gift.faceValue || '0')
|
||||||
}, 0) || 0
|
// }, 0) || 0
|
||||||
|
//
|
||||||
setStats({
|
// setStats({
|
||||||
available: availableRes?.count || 0,
|
// available: availableRes?.count || 0,
|
||||||
used: usedRes?.count || 0,
|
// used: usedRes?.count || 0,
|
||||||
expired: expiredRes?.count || 0,
|
// expired: expiredRes?.count || 0,
|
||||||
totalValue
|
// totalValue
|
||||||
})
|
// })
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('获取礼品卡统计失败:', error)
|
// console.error('获取礼品卡统计失败:', error)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 统计卡片点击事件
|
// 统计卡片点击事件
|
||||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||||
@@ -264,7 +302,7 @@ const GiftCardManage = () => {
|
|||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
reload(true).then()
|
reload(true).then()
|
||||||
loadGiftStats().then()
|
// loadGiftStats().then()
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
130
src/user/gift/test.tsx
Normal file
130
src/user/gift/test.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View } from '@tarojs/components'
|
||||||
|
import GiftCard from '@/components/GiftCard'
|
||||||
|
import { ShopGift } from '@/api/shop/shopGift/model'
|
||||||
|
|
||||||
|
const GiftCardTest: React.FC = () => {
|
||||||
|
// 模拟礼品卡数据,包含商品名称和图片
|
||||||
|
const mockGiftData: ShopGift = {
|
||||||
|
id: 1,
|
||||||
|
name: '星巴克礼品卡',
|
||||||
|
goodsName: '星巴克咖啡礼品卡(电子版)', // 商品名称
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg', // 商品图片
|
||||||
|
description: '享受醇香咖啡时光,适用于全国星巴克门店',
|
||||||
|
code: 'SB2024001234567890',
|
||||||
|
goodsId: 101,
|
||||||
|
faceValue: '100',
|
||||||
|
type: 20, // 虚拟礼品卡
|
||||||
|
useStatus: 0, // 可用
|
||||||
|
expireTime: '2024-12-31 23:59:59',
|
||||||
|
instructions: '请在有效期内使用,出示兑换码即可使用',
|
||||||
|
contactInfo: '400-800-8888',
|
||||||
|
createTime: '2024-08-01 10:00:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const transformGiftData = (gift: ShopGift) => {
|
||||||
|
return {
|
||||||
|
id: gift.id || 0,
|
||||||
|
name: gift.goodsName || gift.name || '礼品卡', // 优先显示商品名称
|
||||||
|
description: gift.description || gift.instructions,
|
||||||
|
code: gift.code,
|
||||||
|
goodsImage: gift.goodsImage, // 商品图片
|
||||||
|
faceValue: gift.faceValue,
|
||||||
|
type: gift.type,
|
||||||
|
useStatus: gift.useStatus,
|
||||||
|
expireTime: gift.expireTime,
|
||||||
|
contactInfo: gift.contactInfo,
|
||||||
|
// 添加商品信息
|
||||||
|
goodsInfo: {
|
||||||
|
...(gift.goodsId && {
|
||||||
|
specification: `礼品卡面值:¥${gift.faceValue}`,
|
||||||
|
category: getTypeText(gift.type),
|
||||||
|
tags: [
|
||||||
|
getTypeText(gift.type),
|
||||||
|
gift.useStatus === 0 ? '可使用' : gift.useStatus === 1 ? '已使用' : '已过期',
|
||||||
|
'全国通用',
|
||||||
|
'即买即用'
|
||||||
|
].filter(Boolean),
|
||||||
|
instructions: gift.instructions ? [gift.instructions] : [
|
||||||
|
'请在有效期内使用',
|
||||||
|
'出示兑换码即可使用',
|
||||||
|
'不可兑换现金',
|
||||||
|
'可用于购买任意饮品和食品'
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经使用不可退换',
|
||||||
|
'请妥善保管兑换码',
|
||||||
|
'如有疑问请联系客服',
|
||||||
|
'部分特殊商品可能不适用'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showCode: gift.useStatus === 0,
|
||||||
|
showUseBtn: gift.useStatus === 0,
|
||||||
|
showDetailBtn: true,
|
||||||
|
showGoodsDetail: true,
|
||||||
|
theme: 'green' as const,
|
||||||
|
onUse: () => console.log('使用礼品卡'),
|
||||||
|
onDetail: () => console.log('查看详情')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeText = (type?: number): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return '实物礼品卡'
|
||||||
|
case 20: return '虚拟礼品卡'
|
||||||
|
case 30: return '服务礼品卡'
|
||||||
|
default: return '礼品卡'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个测试数据
|
||||||
|
const testGifts: ShopGift[] = [
|
||||||
|
{
|
||||||
|
...mockGiftData,
|
||||||
|
id: 1,
|
||||||
|
goodsName: '星巴克咖啡礼品卡(电子版)',
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockGiftData,
|
||||||
|
id: 2,
|
||||||
|
goodsName: '麦当劳优惠券套餐',
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i2/2206571109/O1CN01ABC123_!!2206571109.jpg',
|
||||||
|
faceValue: '50',
|
||||||
|
type: 20,
|
||||||
|
useStatus: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockGiftData,
|
||||||
|
id: 3,
|
||||||
|
goodsName: '海底捞火锅券',
|
||||||
|
goodsImage: 'https://img.alicdn.com/imgextra/i3/2206571109/O1CN01DEF456_!!2206571109.jpg',
|
||||||
|
faceValue: '200',
|
||||||
|
type: 30,
|
||||||
|
useStatus: 1,
|
||||||
|
useTime: '2024-08-15 19:30:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen">
|
||||||
|
<View className="p-4">
|
||||||
|
<View className="text-lg font-bold mb-4 text-center">
|
||||||
|
礼品卡商品信息展示测试
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{testGifts.map((gift, index) => (
|
||||||
|
<View key={gift.id} className="mb-4">
|
||||||
|
<GiftCard
|
||||||
|
{...transformGiftData(gift)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GiftCardTest
|
||||||
145
src/user/gift/usage-example.md
Normal file
145
src/user/gift/usage-example.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# 礼品卡商品信息显示使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
已成功优化礼品卡组件,现在可以正确显示商品名称和图片。主要改进包括:
|
||||||
|
|
||||||
|
1. **优先显示商品名称**:`goodsName` 优先于 `name` 显示
|
||||||
|
2. **显示商品图片**:使用 `goodsImage` 字段显示商品图片
|
||||||
|
3. **丰富的商品信息**:包括规格、分类、标签、使用说明等
|
||||||
|
4. **更好的用户体验**:清晰的信息层次和视觉效果
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
### ShopGift 模型中的关键字段
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ShopGift {
|
||||||
|
id?: number;
|
||||||
|
name?: string; // 礼品卡名称
|
||||||
|
goodsName?: string; // 商品名称(优先显示)
|
||||||
|
goodsImage?: string; // 商品图片
|
||||||
|
goodsId?: number; // 关联商品ID
|
||||||
|
description?: string; // 礼品卡描述
|
||||||
|
faceValue?: string; // 面值
|
||||||
|
type?: number; // 类型(10实物 20虚拟 30服务)
|
||||||
|
useStatus?: number; // 状态(0可用 1已使用 2已过期)
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码实现
|
||||||
|
|
||||||
|
### 1. 数据转换函数
|
||||||
|
|
||||||
|
在 `src/user/gift/index.tsx` 中的 `transformGiftData` 函数已经优化:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
||||||
|
return {
|
||||||
|
id: gift.id || 0,
|
||||||
|
name: gift.goodsName || gift.name || '礼品卡', // 优先显示商品名称
|
||||||
|
description: gift.description || gift.instructions,
|
||||||
|
code: gift.code,
|
||||||
|
goodsImage: gift.goodsImage, // 商品图片
|
||||||
|
faceValue: gift.faceValue,
|
||||||
|
type: gift.type,
|
||||||
|
useStatus: gift.useStatus,
|
||||||
|
expireTime: gift.expireTime,
|
||||||
|
useTime: gift.useTime,
|
||||||
|
useLocation: gift.useLocation,
|
||||||
|
contactInfo: gift.contactInfo,
|
||||||
|
// 添加商品信息
|
||||||
|
goodsInfo: {
|
||||||
|
...(gift.goodsId && {
|
||||||
|
specification: `礼品卡面值:¥${gift.faceValue}`,
|
||||||
|
category: getTypeText(gift.type),
|
||||||
|
tags: [
|
||||||
|
getTypeText(gift.type),
|
||||||
|
gift.useStatus === 0 ? '可使用' : gift.useStatus === 1 ? '已使用' : '已过期'
|
||||||
|
].filter(Boolean),
|
||||||
|
instructions: gift.instructions ? [gift.instructions] : [
|
||||||
|
'请在有效期内使用',
|
||||||
|
'出示兑换码即可使用',
|
||||||
|
'不可兑换现金'
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
'礼品卡一经使用不可退换',
|
||||||
|
'请妥善保管兑换码',
|
||||||
|
'如有疑问请联系客服'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showCode: gift.useStatus === 0,
|
||||||
|
showUseBtn: gift.useStatus === 0,
|
||||||
|
showDetailBtn: true,
|
||||||
|
showGoodsDetail: true, // 显示商品详情
|
||||||
|
theme: getThemeByType(gift.type),
|
||||||
|
onUse: () => handleUseGift(gift),
|
||||||
|
onDetail: () => handleGiftDetail(gift)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 类型文本获取函数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const getTypeText = (type?: number): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 10: return '实物礼品卡'
|
||||||
|
case 20: return '虚拟礼品卡'
|
||||||
|
case 30: return '服务礼品卡'
|
||||||
|
default: return '礼品卡'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 显示效果
|
||||||
|
|
||||||
|
### 商品信息展示包括:
|
||||||
|
|
||||||
|
1. **基础信息**
|
||||||
|
- 商品名称(优先显示)
|
||||||
|
- 商品图片
|
||||||
|
- 礼品卡面值
|
||||||
|
- 商品分类
|
||||||
|
|
||||||
|
2. **详细信息**
|
||||||
|
- 商品规格
|
||||||
|
- 商品标签
|
||||||
|
- 使用说明
|
||||||
|
- 注意事项
|
||||||
|
|
||||||
|
3. **状态信息**
|
||||||
|
- 使用状态标识
|
||||||
|
- 过期时间提醒
|
||||||
|
- 兑换码显示
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
可以使用测试页面验证显示效果:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 访问测试页面
|
||||||
|
/user/gift/test
|
||||||
|
```
|
||||||
|
|
||||||
|
测试页面包含了不同类型和状态的礼品卡示例,可以验证:
|
||||||
|
- 商品名称正确显示
|
||||||
|
- 商品图片正常加载
|
||||||
|
- 商品信息完整展示
|
||||||
|
- 不同状态的样式效果
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **图片加载**:确保 `goodsImage` 字段包含有效的图片URL
|
||||||
|
2. **数据完整性**:建议在后端确保 `goodsName` 和 `goodsImage` 字段有值
|
||||||
|
3. **兼容性**:保持对旧数据的兼容,当 `goodsName` 为空时使用 `name` 字段
|
||||||
|
4. **性能优化**:大量礼品卡列表时注意图片懒加载
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. **图片优化**:添加图片懒加载和占位符
|
||||||
|
2. **缓存机制**:对商品信息进行本地缓存
|
||||||
|
3. **错误处理**:添加图片加载失败的降级处理
|
||||||
|
4. **用户体验**:添加骨架屏和加载状态
|
||||||
@@ -42,29 +42,45 @@ export class PaymentHandler {
|
|||||||
// 设置支付类型
|
// 设置支付类型
|
||||||
orderData.payType = paymentType;
|
orderData.payType = paymentType;
|
||||||
|
|
||||||
|
console.log('创建订单请求:', orderData);
|
||||||
|
|
||||||
// 创建订单
|
// 创建订单
|
||||||
const result = await createOrder(orderData);
|
const result = await createOrder(orderData);
|
||||||
|
|
||||||
|
console.log('订单创建结果:', result);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('创建订单失败');
|
throw new Error('创建订单失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证订单创建结果
|
||||||
|
if (!result.orderNo) {
|
||||||
|
throw new Error('订单号获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
let paymentSuccess = false;
|
||||||
|
|
||||||
// 根据支付类型处理
|
// 根据支付类型处理
|
||||||
switch (paymentType) {
|
switch (paymentType) {
|
||||||
case PaymentType.WECHAT:
|
case PaymentType.WECHAT:
|
||||||
await this.handleWechatPay(result);
|
await this.handleWechatPay(result);
|
||||||
|
paymentSuccess = true;
|
||||||
break;
|
break;
|
||||||
case PaymentType.BALANCE:
|
case PaymentType.BALANCE:
|
||||||
await this.handleBalancePay(result);
|
paymentSuccess = await this.handleBalancePay(result);
|
||||||
break;
|
break;
|
||||||
case PaymentType.ALIPAY:
|
case PaymentType.ALIPAY:
|
||||||
await this.handleAlipay(result);
|
await this.handleAlipay(result);
|
||||||
|
paymentSuccess = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('不支持的支付方式');
|
throw new Error('不支持的支付方式');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支付成功处理
|
// 只有确认支付成功才显示成功提示和跳转
|
||||||
|
if (paymentSuccess) {
|
||||||
|
console.log('支付成功,订单号:', result.orderNo);
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '支付成功',
|
title: '支付成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -74,19 +90,29 @@ export class PaymentHandler {
|
|||||||
|
|
||||||
// 跳转到订单页面
|
// 跳转到订单页面
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.switchTab({ url: '/pages/order/order' });
|
Taro.navigateTo({ url: '/user/order/order' });
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
throw new Error('支付未完成');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('支付失败:', error);
|
console.error('支付失败:', error);
|
||||||
const errorMessage = error.message || '支付失败';
|
|
||||||
|
// 获取详细错误信息
|
||||||
|
const errorMessage = this.getErrorMessage(error);
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: errorMessage,
|
title: errorMessage,
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 标记错误已处理,避免上层重复处理
|
||||||
|
error.handled = true;
|
||||||
callback?.onError?.(errorMessage);
|
callback?.onError?.(errorMessage);
|
||||||
|
|
||||||
|
// 重新抛出错误,让上层知道支付失败
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
Taro.hideLoading();
|
Taro.hideLoading();
|
||||||
callback?.onComplete?.();
|
callback?.onComplete?.();
|
||||||
@@ -97,10 +123,18 @@ export class PaymentHandler {
|
|||||||
* 处理微信支付
|
* 处理微信支付
|
||||||
*/
|
*/
|
||||||
private static async handleWechatPay(result: WxPayResult): Promise<void> {
|
private static async handleWechatPay(result: WxPayResult): Promise<void> {
|
||||||
|
console.log('处理微信支付:', result);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('微信支付参数错误');
|
throw new Error('微信支付参数错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证微信支付必要参数
|
||||||
|
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
|
||||||
|
throw new Error('微信支付参数不完整');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await Taro.requestPayment({
|
await Taro.requestPayment({
|
||||||
timeStamp: result.timeStamp,
|
timeStamp: result.timeStamp,
|
||||||
nonceStr: result.nonceStr,
|
nonceStr: result.nonceStr,
|
||||||
@@ -108,17 +142,59 @@ export class PaymentHandler {
|
|||||||
signType: result.signType as any, // 类型转换,因为微信支付的signType是字符串
|
signType: result.signType as any, // 类型转换,因为微信支付的signType是字符串
|
||||||
paySign: result.paySign,
|
paySign: result.paySign,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('微信支付成功');
|
||||||
|
} catch (payError: any) {
|
||||||
|
console.error('微信支付失败:', payError);
|
||||||
|
|
||||||
|
// 处理微信支付特定错误
|
||||||
|
if (payError.errMsg) {
|
||||||
|
if (payError.errMsg.includes('cancel')) {
|
||||||
|
throw new Error('用户取消支付');
|
||||||
|
} else if (payError.errMsg.includes('fail')) {
|
||||||
|
throw new Error('微信支付失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('微信支付失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理余额支付
|
* 处理余额支付
|
||||||
*/
|
*/
|
||||||
private static async handleBalancePay(result: any): Promise<void> {
|
private static async handleBalancePay(result: any): Promise<boolean> {
|
||||||
// 余额支付通常在后端直接完成,这里只需要确认结果
|
console.log('处理余额支付:', result);
|
||||||
|
|
||||||
if (!result || !result.orderNo) {
|
if (!result || !result.orderNo) {
|
||||||
throw new Error('余额支付失败');
|
throw new Error('余额支付参数错误');
|
||||||
}
|
}
|
||||||
// 余额支付成功,无需额外操作
|
|
||||||
|
// 检查支付状态 - 根据后端返回的字段调整
|
||||||
|
if (result.payStatus === false || result.payStatus === 0 || result.payStatus === '0') {
|
||||||
|
throw new Error('余额不足或支付失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态 - 1表示已付款
|
||||||
|
if (result.orderStatus !== undefined && result.orderStatus !== 1) {
|
||||||
|
throw new Error('订单状态异常,支付可能未成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证实际扣款金额
|
||||||
|
if (result.payPrice !== undefined) {
|
||||||
|
const payPrice = parseFloat(result.payPrice);
|
||||||
|
if (payPrice <= 0) {
|
||||||
|
throw new Error('支付金额异常');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有错误信息字段,检查是否有错误
|
||||||
|
if (result.error || result.errorMsg) {
|
||||||
|
throw new Error(result.error || result.errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('余额支付验证通过');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,6 +204,55 @@ export class PaymentHandler {
|
|||||||
// 支付宝支付逻辑,根据实际情况实现
|
// 支付宝支付逻辑,根据实际情况实现
|
||||||
throw new Error('支付宝支付暂未实现');
|
throw new Error('支付宝支付暂未实现');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取详细错误信息
|
||||||
|
*/
|
||||||
|
private static getErrorMessage(error: any): string {
|
||||||
|
if (!error.message) {
|
||||||
|
return '支付失败,请重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = error.message;
|
||||||
|
|
||||||
|
// 余额相关错误
|
||||||
|
if (message.includes('余额不足') || message.includes('balance')) {
|
||||||
|
return '账户余额不足,请充值后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优惠券相关错误
|
||||||
|
if (message.includes('优惠券') || message.includes('coupon')) {
|
||||||
|
return '优惠券使用失败,请重新选择';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 库存相关错误
|
||||||
|
if (message.includes('库存') || message.includes('stock')) {
|
||||||
|
return '商品库存不足,请减少购买数量';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地址相关错误
|
||||||
|
if (message.includes('地址') || message.includes('address')) {
|
||||||
|
return '收货地址信息有误,请重新选择';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单相关错误
|
||||||
|
if (message.includes('订单') || message.includes('order')) {
|
||||||
|
return '订单创建失败,请重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络相关错误
|
||||||
|
if (message.includes('网络') || message.includes('network') || message.includes('timeout')) {
|
||||||
|
return '网络连接异常,请检查网络后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信支付相关错误
|
||||||
|
if (message.includes('微信') || message.includes('wechat') || message.includes('wx')) {
|
||||||
|
return '微信支付失败,请重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回原始错误信息
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user