feat(components): 新增 GiftCard礼品卡组件
- 新增 GiftCard 组件,支持多种类型礼品卡的展示和交互 - 组件包含商品信息、价格、折扣、使用指南等丰富功能- 优化图像展示,支持单
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '开发环境',
|
||||
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. 支付成功处理
|
||||
Taro.showToast({ title: '支付成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({url: '/pages/order/order'});
|
||||
Taro.navigateTo({url: '/pages/order/order'});
|
||||
}, 2000);
|
||||
}
|
||||
} 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. **定期代码审查** - 防止类似问题
|
||||
@@ -17,10 +17,10 @@ interface OrderExampleProps {
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
goods,
|
||||
address,
|
||||
quantity = 1
|
||||
const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
goods,
|
||||
address,
|
||||
quantity = 1
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -37,7 +37,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
// 1. 构建订单请求数据
|
||||
const orderData: OrderCreateRequest = {
|
||||
@@ -57,7 +57,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
|
||||
// 2. 调用创建订单API
|
||||
const result = await createOrder(orderData);
|
||||
|
||||
|
||||
if (result && result.prepayId) {
|
||||
// 3. 调用微信支付
|
||||
await Taro.requestPayment({
|
||||
@@ -67,15 +67,15 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
signType: result.signType,
|
||||
paySign: result.paySign,
|
||||
});
|
||||
|
||||
|
||||
// 4. 支付成功处理
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({url: '/pages/order/order'});
|
||||
Taro.navigateTo({url: '/pages/order/order'});
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -110,7 +110,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
// 1. 构建订单商品列表
|
||||
const goodsItems: OrderGoodsItem[] = cartItems.map(item => ({
|
||||
@@ -134,7 +134,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
|
||||
// 4. 调用创建订单API
|
||||
const result = await createOrder(orderData);
|
||||
|
||||
|
||||
if (result && result.prepayId) {
|
||||
// 5. 调用微信支付
|
||||
await Taro.requestPayment({
|
||||
@@ -144,18 +144,18 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
signType: result.signType,
|
||||
paySign: result.paySign,
|
||||
});
|
||||
|
||||
|
||||
// 6. 支付成功处理
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
|
||||
// 7. 清空购物车(可选)
|
||||
// clearCart();
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({url: '/pages/order/order'});
|
||||
Taro.navigateTo({url: '/pages/order/order'});
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -174,7 +174,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
*/
|
||||
const handleSelfPickupOrder = async (merchantId: number) => {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
const orderData: OrderCreateRequest = {
|
||||
goodsItems: [
|
||||
@@ -192,7 +192,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
};
|
||||
|
||||
const result = await createOrder(orderData);
|
||||
|
||||
|
||||
if (result && result.prepayId) {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
@@ -201,14 +201,14 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
signType: result.signType,
|
||||
paySign: result.paySign,
|
||||
});
|
||||
|
||||
|
||||
Taro.showToast({
|
||||
title: '下单成功,请到店自提',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({url: '/pages/order/order'});
|
||||
Taro.navigateTo({url: '/pages/order/order'});
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -227,7 +227,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
*/
|
||||
const handleOrderWithCoupon = async (couponId: number) => {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
const orderData: OrderCreateRequest = {
|
||||
goodsItems: [
|
||||
@@ -245,7 +245,7 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
};
|
||||
|
||||
const result = await createOrder(orderData);
|
||||
|
||||
|
||||
if (result && result.prepayId) {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
@@ -254,14 +254,14 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
signType: result.signType,
|
||||
paySign: result.paySign,
|
||||
});
|
||||
|
||||
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({url: '/pages/order/order'});
|
||||
Taro.navigateTo({url: '/pages/order/order'});
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -277,16 +277,16 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleSingleGoodsOrder}
|
||||
>
|
||||
立即购买
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="success"
|
||||
|
||||
<Button
|
||||
type="success"
|
||||
loading={loading}
|
||||
onClick={() => handleCartOrder([
|
||||
{goodsId: goods.goodsId!, quantity: 1, goodsName: goods.name!}
|
||||
@@ -294,17 +294,17 @@ const OrderExample: React.FC<OrderExampleProps> = ({
|
||||
>
|
||||
购物车下单
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="warning"
|
||||
|
||||
<Button
|
||||
type="warning"
|
||||
loading={loading}
|
||||
onClick={() => handleSelfPickupOrder(1)}
|
||||
>
|
||||
自提下单
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="info"
|
||||
|
||||
<Button
|
||||
type="info"
|
||||
loading={loading}
|
||||
onClick={() => handleOrderWithCoupon(123)}
|
||||
>
|
||||
|
||||
@@ -41,7 +41,8 @@ export default defineAppConfig({
|
||||
"coupon/index",
|
||||
"points/points",
|
||||
"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 {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
// 商品名称可能较长,需要处理溢出
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
@@ -138,29 +142,151 @@
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.gift-image {
|
||||
.gift-image-container {
|
||||
margin-right: 16px;
|
||||
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 {
|
||||
flex: 1;
|
||||
|
||||
.gift-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
.goods-basic-info {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.value-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.value-amount {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
.price-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
|
||||
.review-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
@@ -179,13 +371,11 @@
|
||||
|
||||
.code-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.code-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
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 {
|
||||
.time-item {
|
||||
display: flex;
|
||||
@@ -290,10 +543,42 @@
|
||||
padding: 16px;
|
||||
|
||||
.gift-card-content {
|
||||
.gift-image-container {
|
||||
margin-right: 12px;
|
||||
|
||||
.gift-image .gift-image-single,
|
||||
.gift-image-swiper {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-info {
|
||||
.gift-value {
|
||||
.value-amount {
|
||||
font-size: 20px;
|
||||
.goods-basic-info {
|
||||
.price-info {
|
||||
.current-price {
|
||||
.price-amount {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-tags {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.promotion-info {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-instructions {
|
||||
.instruction-section {
|
||||
.instruction-list {
|
||||
.instruction-item {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,6 +587,35 @@
|
||||
|
||||
.gift-card-footer {
|
||||
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 { View, Text, Image } from '@tarojs/components'
|
||||
import { Button, Tag } from '@nutui/nutui-react-taro'
|
||||
import { Gift, Clock, Location, Phone } from '@nutui/icons-react-taro'
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components'
|
||||
import { Button, Tag, Rate } from '@nutui/nutui-react-taro'
|
||||
import { Gift, Clock, Location, Phone, Star, Eye, ShoppingCart, Tips } from '@nutui/icons-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
import './GiftCard.scss'
|
||||
|
||||
@@ -10,14 +10,20 @@ export interface GiftCardProps {
|
||||
id: number
|
||||
/** 礼品卡名称 */
|
||||
name: string
|
||||
/** 商品名称 */
|
||||
goodsName?: string
|
||||
/** 礼品卡描述 */
|
||||
description?: string
|
||||
/** 礼品卡兑换码 */
|
||||
code?: string
|
||||
/** 商品图片 */
|
||||
goodsImage?: string
|
||||
/** 商品图片列表 */
|
||||
goodsImages?: string[]
|
||||
/** 礼品卡面值 */
|
||||
faceValue?: string
|
||||
/** 商品原价 */
|
||||
originalPrice?: string
|
||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
type?: number
|
||||
/** 使用状态:0-可用 1-已使用 2-已过期 */
|
||||
@@ -30,12 +36,48 @@ export interface GiftCardProps {
|
||||
useLocation?: 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
|
||||
/** 是否显示使用按钮 */
|
||||
showUseBtn?: boolean
|
||||
/** 是否显示详情按钮 */
|
||||
showDetailBtn?: boolean
|
||||
/** 是否显示商品详情 */
|
||||
showGoodsDetail?: boolean
|
||||
/** 卡片主题色 */
|
||||
theme?: 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple'
|
||||
/** 使用按钮点击事件 */
|
||||
@@ -49,24 +91,34 @@ export interface GiftCardProps {
|
||||
const GiftCard: React.FC<GiftCardProps> = ({
|
||||
id,
|
||||
name,
|
||||
goodsName,
|
||||
description,
|
||||
code,
|
||||
goodsImage,
|
||||
goodsImages,
|
||||
faceValue,
|
||||
originalPrice,
|
||||
type = 10,
|
||||
useStatus = 0,
|
||||
expireTime,
|
||||
useTime,
|
||||
useLocation,
|
||||
contactInfo,
|
||||
goodsInfo,
|
||||
promotionInfo,
|
||||
showCode = false,
|
||||
showUseBtn = false,
|
||||
showDetailBtn = true,
|
||||
showGoodsDetail = true,
|
||||
theme = 'gold',
|
||||
onUse,
|
||||
onDetail,
|
||||
onClick
|
||||
}) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
|
||||
// 获取显示名称,优先使用商品名称
|
||||
const displayName = goodsName || name
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
@@ -119,11 +171,11 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
// 格式化过期时间显示
|
||||
const formatExpireTime = () => {
|
||||
if (!expireTime) return ''
|
||||
|
||||
|
||||
const expire = dayjs(expireTime)
|
||||
const now = dayjs()
|
||||
const diffDays = expire.diff(now, 'day')
|
||||
|
||||
|
||||
if (diffDays < 0) {
|
||||
return '已过期'
|
||||
} else if (diffDays === 0) {
|
||||
@@ -142,10 +194,32 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
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 imageList = getImageList()
|
||||
const discountPercent = getDiscountPercent()
|
||||
|
||||
return (
|
||||
<View
|
||||
<View
|
||||
className={`gift-card ${getThemeClass()} ${useStatus !== 0 ? 'disabled' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -155,8 +229,7 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
<Gift size="24" className="text-white" />
|
||||
</View>
|
||||
<View className="gift-card-title">
|
||||
<Text className="title-text">{name}</Text>
|
||||
<Text className="type-text">{getTypeText()}</Text>
|
||||
<Text className="title-text">{getTypeText()}</Text>
|
||||
</View>
|
||||
<View className="gift-card-status">
|
||||
<Tag type={statusInfo.color}>{statusInfo.text}</Tag>
|
||||
@@ -166,31 +239,77 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
{/* 卡片主体 */}
|
||||
<View className="gift-card-body">
|
||||
<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">
|
||||
{/* 面值 */}
|
||||
{faceValue && (
|
||||
<View className="gift-value">
|
||||
<Text className="value-label">面值</Text>
|
||||
<Text className="value-amount">¥{faceValue}</Text>
|
||||
{/* 商品基本信息 */}
|
||||
<View className="goods-basic-info">
|
||||
{/* 商品名称 */}
|
||||
{goodsName && (
|
||||
<View className="brand-category">
|
||||
<Text className="brand-text">{goodsName}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 价格信息 */}
|
||||
<View className="price-info">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
{description && (
|
||||
<Text className="gift-description">{description}</Text>
|
||||
|
||||
{/* 优惠信息 */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
|
||||
{/* 兑换码 */}
|
||||
{code && (
|
||||
<View className="gift-code">
|
||||
@@ -201,6 +320,27 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
</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">
|
||||
{useStatus === 1 && useTime && (
|
||||
@@ -209,14 +349,14 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
<Text className="time-text">使用时间:{dayjs(useTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
{useStatus === 0 && expireTime && (
|
||||
<View className="time-item">
|
||||
<Clock size="14" className="text-orange-500" />
|
||||
<Text className="time-text">{formatExpireTime()}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
{useLocation && (
|
||||
<View className="time-item">
|
||||
<Location size="14" className="text-gray-400" />
|
||||
@@ -236,21 +376,8 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
<View className="footer-actions">
|
||||
{showDetailBtn && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDetail?.()
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showUseBtn && useStatus === 0 && (
|
||||
<Button
|
||||
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,13 +67,15 @@ const GiftCardList: React.FC<GiftCardListProps> = ({
|
||||
)
|
||||
) : (
|
||||
gifts.map((gift, index) => (
|
||||
<GiftCard
|
||||
key={gift.id || index}
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
<>
|
||||
<GiftCard
|
||||
key={gift.id || index}
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -221,6 +221,19 @@ const OrderConfirm = () => {
|
||||
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(
|
||||
goods.goodsId!,
|
||||
@@ -230,26 +243,58 @@ const OrderConfirm = () => {
|
||||
comments: goods.name,
|
||||
deliveryType: 0,
|
||||
buyerRemarks: orderRemark,
|
||||
couponId: selectedCoupon ? selectedCoupon.id : undefined
|
||||
// 确保couponId是数字类型
|
||||
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
|
||||
}
|
||||
);
|
||||
|
||||
// 根据支付方式选择支付类型
|
||||
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);
|
||||
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
// ✅ 移除双重成功提示 - PaymentHandler会处理成功提示
|
||||
// Taro.showToast({
|
||||
// title: '支付成功',
|
||||
// icon: 'success'
|
||||
// })
|
||||
} catch (error: any) {
|
||||
console.error('支付失败:', error)
|
||||
Taro.showToast({
|
||||
title: '支付失败,请重试',
|
||||
icon: '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({
|
||||
title: errorMessage,
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
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({
|
||||
navigationBarTitleText: '礼品卡详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationStyle: 'custom'
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
|
||||
@@ -168,20 +168,6 @@ const GiftCardDetail = () => {
|
||||
|
||||
return (
|
||||
<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="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({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
userId: Taro.getStorageSync('UserId'),
|
||||
// keywords: searchValue,
|
||||
...statusFilter,
|
||||
// 应用筛选条件
|
||||
@@ -67,7 +68,7 @@ const GiftCardManage = () => {
|
||||
// sortBy: filters.sortBy,
|
||||
// sortOrder: filters.sortOrder
|
||||
})
|
||||
|
||||
console.log(res?.list,'>>>>lalala')
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
@@ -123,10 +124,11 @@ const GiftCardManage = () => {
|
||||
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡',
|
||||
description: gift.description,
|
||||
name: gift.name || '礼品卡', // 礼品卡名称
|
||||
goodsName: gift.goodsName, // 商品名称(新增字段)
|
||||
description: gift.description || gift.instructions, // 使用说明作为描述
|
||||
code: gift.code,
|
||||
goodsImage: gift.goodsImage,
|
||||
goodsImage: gift.goodsImage, // 商品图片
|
||||
faceValue: gift.faceValue,
|
||||
type: gift.type,
|
||||
useStatus: gift.useStatus,
|
||||
@@ -134,15 +136,51 @@ const GiftCardManage = () => {
|
||||
useTime: gift.useTime,
|
||||
useLocation: gift.useLocation,
|
||||
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, // 只有可用状态显示兑换码
|
||||
showUseBtn: gift.useStatus === 0, // 只有可用状态显示使用按钮
|
||||
showDetailBtn: true,
|
||||
showGoodsDetail: true, // 显示商品详情
|
||||
theme: getThemeByType(gift.type),
|
||||
onUse: () => handleUseGift(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' => {
|
||||
switch (type) {
|
||||
@@ -188,31 +226,31 @@ const GiftCardManage = () => {
|
||||
}
|
||||
|
||||
// 加载礼品卡统计数据
|
||||
const loadGiftStats = async () => {
|
||||
try {
|
||||
// 并行获取各状态的礼品卡数量
|
||||
const [availableRes, usedRes, expiredRes] = await Promise.all([
|
||||
getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
|
||||
getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
|
||||
getUserGifts({ page: 1, limit: 1, useStatus: 2 })
|
||||
])
|
||||
|
||||
// 计算总价值(仅可用礼品卡)
|
||||
const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
|
||||
const totalValue = availableGifts?.list?.reduce((sum, gift) => {
|
||||
return sum + parseFloat(gift.faceValue || '0')
|
||||
}, 0) || 0
|
||||
|
||||
setStats({
|
||||
available: availableRes?.count || 0,
|
||||
used: usedRes?.count || 0,
|
||||
expired: expiredRes?.count || 0,
|
||||
totalValue
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡统计失败:', error)
|
||||
}
|
||||
}
|
||||
// const loadGiftStats = async () => {
|
||||
// try {
|
||||
// // 并行获取各状态的礼品卡数量
|
||||
// const [availableRes, usedRes, expiredRes] = await Promise.all([
|
||||
// getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
|
||||
// getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
|
||||
// getUserGifts({ page: 1, limit: 1, useStatus: 2 })
|
||||
// ])
|
||||
//
|
||||
// // 计算总价值(仅可用礼品卡)
|
||||
// const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
|
||||
// const totalValue = availableGifts?.list?.reduce((sum, gift) => {
|
||||
// return sum + parseFloat(gift.faceValue || '0')
|
||||
// }, 0) || 0
|
||||
//
|
||||
// setStats({
|
||||
// available: availableRes?.count || 0,
|
||||
// used: usedRes?.count || 0,
|
||||
// expired: expiredRes?.count || 0,
|
||||
// totalValue
|
||||
// })
|
||||
// } catch (error) {
|
||||
// console.error('获取礼品卡统计失败:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 统计卡片点击事件
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||
@@ -264,7 +302,7 @@ const GiftCardManage = () => {
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
loadGiftStats().then()
|
||||
// loadGiftStats().then()
|
||||
});
|
||||
|
||||
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,51 +42,77 @@ export class PaymentHandler {
|
||||
// 设置支付类型
|
||||
orderData.payType = paymentType;
|
||||
|
||||
console.log('创建订单请求:', orderData);
|
||||
|
||||
// 创建订单
|
||||
const result = await createOrder(orderData);
|
||||
|
||||
console.log('订单创建结果:', result);
|
||||
|
||||
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:
|
||||
await this.handleBalancePay(result);
|
||||
paymentSuccess = await this.handleBalancePay(result);
|
||||
break;
|
||||
case PaymentType.ALIPAY:
|
||||
await this.handleAlipay(result);
|
||||
paymentSuccess = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error('不支持的支付方式');
|
||||
}
|
||||
|
||||
// 支付成功处理
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 只有确认支付成功才显示成功提示和跳转
|
||||
if (paymentSuccess) {
|
||||
console.log('支付成功,订单号:', result.orderNo);
|
||||
|
||||
callback?.onSuccess?.();
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/order/order' });
|
||||
}, 2000);
|
||||
callback?.onSuccess?.();
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
Taro.navigateTo({ url: '/user/order/order' });
|
||||
}, 2000);
|
||||
} else {
|
||||
throw new Error('支付未完成');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('支付失败:', error);
|
||||
const errorMessage = error.message || '支付失败';
|
||||
|
||||
// 获取详细错误信息
|
||||
const errorMessage = this.getErrorMessage(error);
|
||||
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error'
|
||||
});
|
||||
|
||||
// 标记错误已处理,避免上层重复处理
|
||||
error.handled = true;
|
||||
callback?.onError?.(errorMessage);
|
||||
|
||||
// 重新抛出错误,让上层知道支付失败
|
||||
throw error;
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
callback?.onComplete?.();
|
||||
@@ -97,28 +123,78 @@ export class PaymentHandler {
|
||||
* 处理微信支付
|
||||
*/
|
||||
private static async handleWechatPay(result: WxPayResult): Promise<void> {
|
||||
console.log('处理微信支付:', result);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('微信支付参数错误');
|
||||
}
|
||||
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
nonceStr: result.nonceStr,
|
||||
package: result.package,
|
||||
signType: result.signType as any, // 类型转换,因为微信支付的signType是字符串
|
||||
paySign: result.paySign,
|
||||
});
|
||||
// 验证微信支付必要参数
|
||||
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
|
||||
throw new Error('微信支付参数不完整');
|
||||
}
|
||||
|
||||
try {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
nonceStr: result.nonceStr,
|
||||
package: result.package,
|
||||
signType: result.signType as any, // 类型转换,因为微信支付的signType是字符串
|
||||
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) {
|
||||
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('支付宝支付暂未实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细错误信息
|
||||
*/
|
||||
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