From ecfbdc02868b94950957bb1953c4f3d294233406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sun, 17 Aug 2025 10:01:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=A4=BC=E5=93=81=E5=8D=A1):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=A2=9C=E8=89=B2=E4=B8=BB=E9=A2=98=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=A0=B8=E9=94=80=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改礼品卡颜色主题,使用渐变色提升视觉效果 - 添加礼品卡核销功能,包括生成和验证核销码 -优化礼品卡组件,增加状态显示和使用说明 - 新增礼品卡颜色测试页面,用于验证颜色 --- src/api/shop/shopGift/index.ts | 65 ++++ src/api/shop/shopGift/model/index.ts | 7 +- src/app.config.ts | 4 +- src/components/GiftCard.scss | 30 +- src/components/GiftCard.tsx | 52 ++- src/components/GiftCardQRCode.tsx | 333 +++++++++++++++++ src/pages/user/components/UserCard.tsx | 14 +- src/types/giftCard.ts | 18 +- src/user/gift/api-test.tsx | 228 ++++++++++++ src/user/gift/color-fix-summary.md | 137 +++++++ src/user/gift/color-test.tsx | 213 +++++++++++ src/user/gift/debug-tab.tsx | 226 ++++++++++++ src/user/gift/detail.tsx | 45 +-- src/user/gift/index.tsx | 96 +++-- src/user/gift/qrcode-demo.tsx | 221 +++++++++++ .../qrcode-verification-implementation.md | 212 +++++++++++ src/user/gift/redeem.tsx | 30 +- src/user/gift/status-field-migration.md | 237 ++++++++++++ src/user/gift/status-test.tsx | 219 +++++++++++ src/user/gift/tab-switch-fix.md | 198 ++++++++++ src/user/store/verification.config.ts | 4 + src/user/store/verification.tsx | 348 ++++++++++++++++++ 22 files changed, 2821 insertions(+), 116 deletions(-) create mode 100644 src/components/GiftCardQRCode.tsx create mode 100644 src/user/gift/api-test.tsx create mode 100644 src/user/gift/color-fix-summary.md create mode 100644 src/user/gift/color-test.tsx create mode 100644 src/user/gift/debug-tab.tsx create mode 100644 src/user/gift/qrcode-demo.tsx create mode 100644 src/user/gift/qrcode-verification-implementation.md create mode 100644 src/user/gift/status-field-migration.md create mode 100644 src/user/gift/status-test.tsx create mode 100644 src/user/gift/tab-switch-fix.md create mode 100644 src/user/store/verification.config.ts create mode 100644 src/user/store/verification.tsx diff --git a/src/api/shop/shopGift/index.ts b/src/api/shop/shopGift/index.ts index 00608ac..c106eeb 100644 --- a/src/api/shop/shopGift/index.ts +++ b/src/api/shop/shopGift/index.ts @@ -113,6 +113,22 @@ export async function getShopGift(id: number) { return Promise.reject(new Error(res.message)); } +/** + * 根据code查询礼品卡 + * @param code + */ +export async function getShopGiftByCode(code: string) { + const res = await request.get>( + '/shop/shop-gift/by-code/' + code + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + + + /** * 兑换礼品卡 */ @@ -178,3 +194,52 @@ export async function exportShopGift(ids?: number[]) { } return Promise.reject(new Error(res.message)); } + +/** + * 生成礼品卡核销码 + */ +export async function generateVerificationCode(giftId: number) { + const res = await request.post>( + '/shop/shop-gift/generate-verification-code', + { giftId } + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 验证核销码 + */ +export async function verifyGiftCard(params: { verificationCode?: string; giftCode?: string }) { + const res = await request.post>( + '/shop/shop-gift/verify', + params + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 完成礼品卡核销 + */ +export async function completeVerification(params: { + giftId: number; + verificationCode: string; + storeId?: number; + storeName?: string; + operatorId?: number; + operatorName?: string; +}) { + const res = await request.post>( + '/shop/shop-gift/complete-verification', + params + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopGift/model/index.ts b/src/api/shop/shopGift/model/index.ts index 13b4e9b..ce2de42 100644 --- a/src/api/shop/shopGift/model/index.ts +++ b/src/api/shop/shopGift/model/index.ts @@ -36,8 +36,6 @@ export interface ShopGift { isShow?: string; // 状态 (0未使用 1已使用 2已过期 3已失效) status?: number; - // 使用状态 (0可用 1已使用 2已过期) - useStatus?: number; // 备注 comments?: string; // 使用说明 @@ -74,12 +72,11 @@ export interface ShopGift { export interface ShopGiftParam extends PageParam { id?: number; keywords?: string; + code?: string; // 礼品卡类型筛选 type?: number; - // 状态筛选 + // 状态筛选 (0未使用 1已使用 2失效) status?: number; - // 使用状态筛选 - useStatus?: number; // 用户ID筛选 userId?: number; // 商品ID筛选 diff --git a/src/app.config.ts b/src/app.config.ts index 06d4ad4..bbe79cf 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -42,7 +42,9 @@ export default defineAppConfig({ "points/points", "gift/index", "gift/redeem", - "gift/detail" + "gift/detail", + "gift/qrcode-demo", + "store/verification" ] }, { diff --git a/src/components/GiftCard.scss b/src/components/GiftCard.scss index c04cfcd..8130a0f 100644 --- a/src/components/GiftCard.scss +++ b/src/components/GiftCard.scss @@ -20,21 +20,22 @@ // 主题色彩 &.gift-card-gold { .gift-card-header { - background: #ffd700; + background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); } .use-btn { - background: #ffd700; + background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: none; color: #333; + font-weight: 600; } } &.gift-card-silver { .gift-card-header { - background: #c0c0c0; + background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); } .use-btn { - background: #c0c0c0; + background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); border: none; color: #333; } @@ -42,45 +43,49 @@ &.gift-card-bronze { .gift-card-header { - background: #cd7f32; + background: linear-gradient(135deg, #cd7f32 0%, #b8722c 100%); } .use-btn { - background: #cd7f32; + background: linear-gradient(135deg, #cd7f32 0%, #b8722c 100%); border: none; color: #fff; + font-weight: 600; } } &.gift-card-blue { .gift-card-header { - background: #4a90e2; + background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); } .use-btn { - background: #4a90e2; + background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); border: none; color: #fff; + font-weight: 600; } } &.gift-card-green { .gift-card-header { - background: #5cb85c; + background: linear-gradient(135deg, #5cb85c 0%, #449d44 100%); } .use-btn { - background: #5cb85c; + background: linear-gradient(135deg, #5cb85c 0%, #449d44 100%); border: none; color: #fff; + font-weight: 600; } } &.gift-card-purple { .gift-card-header { - background: #9b59b6; + background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); } .use-btn { - background: #9b59b6; + background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); border: none; color: #fff; + font-weight: 600; } } @@ -519,7 +524,6 @@ color: #fff; padding: 8px 16px; border-radius: 20px; - font-size: 16px; font-weight: 600; } } diff --git a/src/components/GiftCard.tsx b/src/components/GiftCard.tsx index 7406cd0..421c739 100644 --- a/src/components/GiftCard.tsx +++ b/src/components/GiftCard.tsx @@ -1,8 +1,10 @@ import React, { useState } from 'react' -import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components' +import { View, Text } 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 { Gift, Clock, Location, Phone, ShoppingCart, Tips, QrCode } from '@nutui/icons-react-taro' +import Taro from '@tarojs/taro' import dayjs from 'dayjs' +import GiftCardQRCode from './GiftCardQRCode' import './GiftCard.scss' export interface GiftCardProps { @@ -26,8 +28,8 @@ export interface GiftCardProps { originalPrice?: string /** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */ type?: number - /** 使用状态:0-可用 1-已使用 2-已过期 */ - useStatus?: number + /** 状态:0-未使用 1-已使用 2-失效 */ + status?: number /** 过期时间 */ expireTime?: string /** 使用时间 */ @@ -99,7 +101,7 @@ const GiftCard: React.FC = ({ faceValue, originalPrice, type = 10, - useStatus = 0, + status = 0, expireTime, useTime, useLocation, @@ -116,6 +118,7 @@ const GiftCard: React.FC = ({ onClick }) => { const [currentImageIndex, setCurrentImageIndex] = useState(0) + const [showQRCode, setShowQRCode] = useState(false) // 获取显示名称,优先使用商品名称 const displayName = goodsName || name @@ -129,12 +132,12 @@ const GiftCard: React.FC = ({ } } - // 获取使用状态信息 + // 获取状态信息 const getStatusInfo = () => { - switch (useStatus) { + switch (status) { case 0: return { - text: '可使用', + text: '未使用', color: 'success' as const, bgColor: 'bg-green-100', textColor: 'text-green-600' @@ -148,7 +151,7 @@ const GiftCard: React.FC = ({ } case 2: return { - text: '已过期', + text: '失效', color: 'danger' as const, bgColor: 'bg-red-100', textColor: 'text-red-600' @@ -220,7 +223,7 @@ const GiftCard: React.FC = ({ return ( {/* 卡片头部 */} @@ -230,6 +233,7 @@ const GiftCard: React.FC = ({ {getTypeText()} + {name} {statusInfo.text} @@ -343,14 +347,14 @@ const GiftCard: React.FC = ({ {/* 时间信息 */} - {useStatus === 1 && useTime && ( + {status === 1 && useTime && ( 使用时间:{dayjs(useTime).format('YYYY-MM-DD HH:mm')} )} - {useStatus === 0 && expireTime && ( + {status === 0 && expireTime && ( {formatExpireTime()} @@ -378,14 +382,15 @@ const GiftCard: React.FC = ({ - {showUseBtn && useStatus === 0 && ( + {showUseBtn && status === 0 && ( + + ) : ( + + 生成失败 + + )} + + + {/* 核销码 */} + {verificationCode && ( + + + + 核销码 + + {verificationCode} + + + + + + )} + + {/* 兑换码 */} + {giftCard.code && ( + + + + 兑换码 + + {giftCard.code} + + + + + + )} + + {/* 操作按钮 */} + + + + + + {/* 使用说明 */} + + 使用说明: + + • 请向门店工作人员出示此二维码或核销码 + • 工作人员扫码后即可完成核销 + • 每次使用会生成新的核销码,确保安全 + • 如有问题请联系客服:{giftCard.contactInfo || '400-800-8888'} + + + + + ) +} + +export default GiftCardQRCode diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx index 5e3268f..a9a1c09 100644 --- a/src/pages/user/components/UserCard.tsx +++ b/src/pages/user/components/UserCard.tsx @@ -46,13 +46,13 @@ function UserCard() { }) // 加载积分数量 - getUserPointsStats(userId) - .then((res: any) => { - setPointsCount(res.currentPoints || 0) - }) - .catch((error: any) => { - console.error('Points stats error:', error) - }) + // getUserPointsStats(userId) + // .then((res: any) => { + // setPointsCount(res.currentPoints || 0) + // }) + // .catch((error: any) => { + // console.error('Points stats error:', error) + // }) // 加载礼品劵数量 setGiftCount(0) // pageUserGiftLog({userId, page: 1, limit: 1}).then(res => { diff --git a/src/types/giftCard.ts b/src/types/giftCard.ts index 8494a8c..b8a672d 100644 --- a/src/types/giftCard.ts +++ b/src/types/giftCard.ts @@ -12,14 +12,14 @@ export enum GiftCardType { SERVICE = 30 } -/** 使用状态枚举 */ -export enum UseStatus { - /** 可用 */ - AVAILABLE = 0, +/** 状态枚举 */ +export enum GiftStatus { + /** 未使用 */ + UNUSED = 0, /** 已使用 */ USED = 1, - /** 已过期 */ - EXPIRED = 2 + /** 失效 */ + INVALID = 2 } /** 优惠类型枚举 */ @@ -100,8 +100,8 @@ export interface GiftCardData { originalPrice?: string /** 礼品卡类型 */ type?: GiftCardType - /** 使用状态 */ - useStatus?: UseStatus + /** 状态 */ + status?: GiftStatus /** 过期时间 */ expireTime?: string /** 使用时间 */ @@ -156,7 +156,7 @@ export interface GiftCardFilter { /** 类型筛选 */ type?: GiftCardType[] /** 状态筛选 */ - useStatus?: UseStatus[] + status?: GiftStatus[] /** 品牌筛选 */ brand?: string[] /** 分类筛选 */ diff --git a/src/user/gift/api-test.tsx b/src/user/gift/api-test.tsx new file mode 100644 index 0000000..82fc35d --- /dev/null +++ b/src/user/gift/api-test.tsx @@ -0,0 +1,228 @@ +import React, { useState } from 'react' +import { View, Text } from '@tarojs/components' +import { Button } from '@nutui/nutui-react-taro' +import { getUserGifts } from '@/api/shop/shopGift' +import Taro from '@tarojs/taro' + +const ApiTest: React.FC = () => { + const [loading, setLoading] = useState(false) + const [results, setResults] = useState([]) + const [logs, setLogs] = useState([]) + + const addLog = (message: string) => { + const timestamp = new Date().toLocaleTimeString() + setLogs(prev => [`[${timestamp}] ${message}`, ...prev]) + } + + const testApiCall = async (status: number, statusName: string) => { + setLoading(true) + addLog(`开始测试 status=${status} (${statusName})`) + + try { + const params = { + page: 1, + limit: 10, + userId: Taro.getStorageSync('UserId'), + status: status + } + + addLog(`API参数: ${JSON.stringify(params)}`) + + const res = await getUserGifts(params) + + addLog(`API返回: ${res?.list?.length || 0} 条数据`) + + if (res?.list && res.list.length > 0) { + const statusCounts = res.list.reduce((acc: any, item: any) => { + const itemStatus = item.status + acc[itemStatus] = (acc[itemStatus] || 0) + 1 + return acc + }, {}) + + addLog(`返回数据状态分布: ${JSON.stringify(statusCounts)}`) + + // 检查是否所有返回的数据都是期望的状态 + const allCorrectStatus = res.list.every((item: any) => item.status === status) + if (allCorrectStatus) { + addLog(`✅ 状态筛选正确: 所有数据都是 status=${status}`) + } else { + addLog(`❌ 状态筛选错误: 返回了其他状态的数据`) + } + } else { + addLog(`ℹ️ 无数据返回`) + } + + setResults(prev => [...prev, { + status, + statusName, + count: res?.list?.length || 0, + data: res?.list || [], + success: true + }]) + + } catch (error) { + addLog(`❌ API调用失败: ${error}`) + setResults(prev => [...prev, { + status, + statusName, + count: 0, + data: [], + success: false, + error: String(error) + }]) + } finally { + setLoading(false) + } + } + + const clearResults = () => { + setResults([]) + setLogs([]) + } + + const testAllStatus = async () => { + clearResults() + await testApiCall(0, '未使用') + await new Promise(resolve => setTimeout(resolve, 1000)) // 延迟1秒 + await testApiCall(1, '已使用') + await new Promise(resolve => setTimeout(resolve, 1000)) // 延迟1秒 + await testApiCall(2, '失效') + } + + return ( + + {/* 页面标题 */} + + + API 状态参数测试 + + + 测试 getUserGifts API 的 status 参数传递 + + + + {/* 测试按钮 */} + + 测试操作: + + + + + + + + + + + + {/* 测试结果 */} + {results.length > 0 && ( + + 测试结果: + {results.map((result, index) => ( + + + + status={result.status} ({result.statusName}) + + + {result.success ? '成功' : '失败'} + + + + 返回数据: {result.count} 条 + + {result.error && ( + + 错误: {result.error} + + )} + {result.data.length > 0 && ( + + 示例数据: + {result.data.slice(0, 2).map((item: any, idx: number) => ( + + ID:{item.id}, status:{item.status}, name:{item.goodsName || item.name} + + ))} + + )} + + ))} + + )} + + {/* 调试日志 */} + + + 调试日志: + + + + {logs.length > 0 ? ( + logs.map((log, index) => ( + + {log} + + )) + ) : ( + 暂无日志 + )} + + + + {/* 使用说明 */} + + 使用说明: + + 1. 点击测试按钮调用 getUserGifts API + 2. 检查返回的数据状态是否与请求参数一致 + 3. 查看调试日志了解详细的API调用过程 + 4. 如果状态筛选不正确,说明后端或前端有问题 + + + + ) +} + +export default ApiTest diff --git a/src/user/gift/color-fix-summary.md b/src/user/gift/color-fix-summary.md new file mode 100644 index 0000000..5343a65 --- /dev/null +++ b/src/user/gift/color-fix-summary.md @@ -0,0 +1,137 @@ +# 礼品卡颜色主题修复说明 + +## 问题描述 + +用户反馈礼品卡显示为灰色,视觉效果不佳。经检查发现: + +1. **默认主题问题**:当礼品卡类型不是 10、20、30 时,默认使用 `silver` 主题 +2. **银色主题颜色**:银色主题使用的是 `#c0c0c0` 纯灰色,视觉效果较差 +3. **缺乏视觉层次**:所有主题色都是纯色,缺乏现代感 + +## 修复方案 + +### 1. 默认主题优化 + +**修改前**: +```typescript +default: return 'silver' // 默认灰色 +``` + +**修改后**: +```typescript +default: return 'purple' // 默认紫色,更美观 +``` + +### 2. 主题色渐变化 + +将所有主题色从纯色改为渐变色,提升视觉效果: + +#### 金色主题 (实物礼品卡) +```scss +background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); +``` + +#### 蓝色主题 (虚拟礼品卡) +```scss +background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); +``` + +#### 绿色主题 (服务礼品卡) +```scss +background: linear-gradient(135deg, #5cb85c 0%, #449d44 100%); +``` + +#### 紫色主题 (默认) +```scss +background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); +``` + +#### 银色主题 (优化后) +```scss +background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); +``` + +#### 铜色主题 +```scss +background: linear-gradient(135deg, #cd7f32 0%, #b8722c 100%); +``` + +### 3. 按钮样式优化 + +为所有主题的按钮添加字体加粗: +```scss +.use-btn { + font-weight: 600; // 新增 +} +``` + +## 主题色映射规则 + +| 礼品卡类型 | type值 | 主题色 | 视觉效果 | +|-----------|--------|--------|----------| +| 实物礼品卡 | 10 | gold | 金色渐变 | +| 虚拟礼品卡 | 20 | blue | 蓝色渐变 | +| 服务礼品卡 | 30 | green | 绿色渐变 | +| 其他/未知 | 其他值 | purple | 紫色渐变 | + +## 修改的文件 + +### 1. `src/user/gift/index.tsx` +- 修改 `getThemeByType` 函数 +- 将默认主题从 `silver` 改为 `purple` + +### 2. `src/components/GiftCard.scss` +- 优化所有主题色为渐变效果 +- 添加按钮字体加粗 +- 改进银色主题的颜色搭配 + +### 3. 新增测试文件 +- `src/user/gift/color-test.tsx` - 颜色测试页面 + +## 测试验证 + +### 测试页面 +访问 `/user/gift/color-test` 可以看到: +- 不同类型礼品卡的颜色效果 +- 渐变色的视觉表现 +- 按钮样式的改进效果 + +### 测试用例 +1. **type=10**: 应显示金色渐变 +2. **type=20**: 应显示蓝色渐变 +3. **type=30**: 应显示绿色渐变 +4. **type=其他**: 应显示紫色渐变 +5. **type=undefined**: 应显示紫色渐变 + +## 视觉效果对比 + +### 修复前 +- ❌ 默认显示灰色,视觉效果差 +- ❌ 纯色背景,缺乏层次感 +- ❌ 按钮样式平淡 + +### 修复后 +- ✅ 默认显示紫色,更加美观 +- ✅ 渐变色背景,富有层次感 +- ✅ 按钮字体加粗,更加突出 + +## 兼容性说明 + +- ✅ 完全向后兼容,不影响现有功能 +- ✅ 所有现有的礼品卡都会自动应用新的颜色主题 +- ✅ 不需要修改数据结构或API接口 + +## 后续优化建议 + +1. **动态主题**: 可以考虑根据商品分类动态选择主题色 +2. **自定义主题**: 允许用户或管理员自定义主题色 +3. **季节主题**: 根据季节或节日使用特殊主题色 +4. **品牌主题**: 根据商品品牌使用对应的品牌色 + +## 部署检查 + +- [ ] 样式文件更新正确 +- [ ] 主题选择逻辑正确 +- [ ] 测试页面验证通过 +- [ ] 各种设备上显示正常 +- [ ] 不同状态下颜色正确 diff --git a/src/user/gift/color-test.tsx b/src/user/gift/color-test.tsx new file mode 100644 index 0000000..f025d01 --- /dev/null +++ b/src/user/gift/color-test.tsx @@ -0,0 +1,213 @@ +import React from 'react' +import { View, Text } from '@tarojs/components' +import GiftCard from '@/components/GiftCard' +import { ShopGift } from '@/api/shop/shopGift/model' + +const ColorTest: React.FC = () => { + // 测试不同类型的礼品卡颜色 + const testGifts: ShopGift[] = [ + { + id: 1, + name: '实物礼品卡', + goodsName: '杜尔伯特草原奶香牛上脑(2kg,分4小包)', + goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg', + description: '实物商品礼品卡', + code: 'GOLD001234567890', + goodsId: 101, + faceValue: '200', + type: 10, // 实物礼品卡 - 应该显示金色 + 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: 'BLUE001234567890', + goodsId: 102, + faceValue: '100', + type: 20, // 虚拟礼品卡 - 应该显示蓝色 + useStatus: 0, + expireTime: '2024-12-31 23:59:59', + instructions: '适用于虚拟商品兑换', + contactInfo: '400-800-8888' + }, + { + id: 3, + name: '服务礼品卡', + goodsName: '海底捞4人套餐券', + goodsImage: 'https://img.alicdn.com/imgextra/i3/2206571109/O1CN01DEF456_!!2206571109.jpg', + description: '服务类礼品卡', + code: 'GREEN01234567890', + goodsId: 103, + faceValue: '300', + type: 30, // 服务礼品卡 - 应该显示绿色 + useStatus: 0, + expireTime: '2024-12-31 23:59:59', + instructions: '适用于服务类商品兑换', + contactInfo: '400-800-8888' + }, + { + id: 4, + name: '未知类型礼品卡', + goodsName: '通用商品券', + goodsImage: 'https://img.alicdn.com/imgextra/i4/2206571109/O1CN01GHI789_!!2206571109.jpg', + description: '未知类型礼品卡', + code: 'PURPLE1234567890', + goodsId: 104, + faceValue: '150', + type: 99, // 未知类型 - 应该显示紫色(默认) + useStatus: 0, + expireTime: '2024-12-31 23:59:59', + instructions: '通用礼品卡', + contactInfo: '400-800-8888' + }, + { + id: 5, + name: '银色主题测试', + goodsName: '银色主题礼品卡', + goodsImage: 'https://img.alicdn.com/imgextra/i5/2206571109/O1CN01JKL012_!!2206571109.jpg', + description: '测试银色主题', + code: 'SILVER1234567890', + goodsId: 105, + faceValue: '80', + type: undefined, // 无类型 - 应该显示紫色(默认) + useStatus: 0, + expireTime: '2024-12-31 23:59:59', + instructions: '银色主题测试', + contactInfo: '400-800-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, + contactInfo: gift.contactInfo, + goodsInfo: { + ...((gift.goodsName || gift.goodsId) && { + specification: `礼品卡面值:¥${gift.faceValue}`, + category: getTypeText(gift.type), + tags: [ + getTypeText(gift.type), + '可使用', + '测试卡片' + ].filter(Boolean), + instructions: [ + '这是颜色测试卡片', + '请检查主题色是否正确', + '不可兑换现金' + ], + notices: [ + '这是测试数据', + '请勿实际使用', + '仅用于颜色测试' + ] + }) + }, + showCode: true, + showUseBtn: true, + 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 'purple' // 默认使用紫色主题 + } + } + + const getExpectedColor = (type?: number): string => { + switch (type) { + case 10: return '金色渐变' + case 20: return '蓝色渐变' + case 30: return '绿色渐变' + default: return '紫色渐变' + } + } + + return ( + + {/* 页面标题 */} + + + 礼品卡颜色主题测试 + + + 验证不同类型礼品卡的主题色显示 + + + + {/* 颜色说明 */} + + 主题色说明: + + • type=10 (实物礼品卡) → 金色渐变 + • type=20 (虚拟礼品卡) → 蓝色渐变 + • type=30 (服务礼品卡) → 绿色渐变 + • 其他/未知类型 → 紫色渐变(默认) + + + + {/* 礼品卡列表 */} + + {testGifts.map((gift, index) => ( + + + + 测试 {index + 1}: type={gift.type || 'undefined'} → 预期: {getExpectedColor(gift.type)} + + + {gift.goodsName} + + + + + ))} + + + {/* 修复说明 */} + + 修复内容: + + ✅ 默认主题从银色改为紫色 + ✅ 所有主题色使用渐变效果 + ✅ 按钮字体加粗提升视觉效果 + ✅ 银色主题优化为更美观的灰色渐变 + + + + ) +} + +export default ColorTest diff --git a/src/user/gift/debug-tab.tsx b/src/user/gift/debug-tab.tsx new file mode 100644 index 0000000..97d2b71 --- /dev/null +++ b/src/user/gift/debug-tab.tsx @@ -0,0 +1,226 @@ +import React, { useState } from 'react' +import { View, Text } from '@tarojs/components' +import { Tabs, TabPane, Button } from '@nutui/nutui-react-taro' +import GiftCard from '@/components/GiftCard' +import { ShopGift } from '@/api/shop/shopGift/model' + +const DebugTab: React.FC = () => { + const [activeTab, setActiveTab] = useState('0') + const [debugInfo, setDebugInfo] = useState([]) + + // 模拟不同状态的礼品卡数据 + const mockData: { [key: string]: ShopGift[] } = { + '0': [ // 未使用 + { + id: 1, + name: '未使用礼品卡1', + goodsName: '星巴克咖啡券', + goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg', + code: 'UNUSED001', + faceValue: '100', + type: 20, + status: 0, + expireTime: '2024-12-31 23:59:59' + } + ], + '1': [ // 已使用 + { + id: 2, + name: '已使用礼品卡1', + goodsName: '麦当劳套餐券', + goodsImage: 'https://img.alicdn.com/imgextra/i2/2206571109/O1CN01ABC123_!!2206571109.jpg', + code: 'USED001', + faceValue: '50', + type: 20, + status: 1, + useTime: '2024-08-15 14:30:00' + } + ], + '2': [ // 失效 + { + id: 3, + name: '失效礼品卡1', + goodsName: '海底捞火锅券', + goodsImage: 'https://img.alicdn.com/imgextra/i3/2206571109/O1CN01DEF456_!!2206571109.jpg', + code: 'INVALID001', + faceValue: '200', + type: 30, + status: 2, + expireTime: '2024-07-31 23:59:59' + } + ] + } + + // 获取状态过滤条件 + const getStatusFilter = () => { + switch (String(activeTab)) { + case '0': return { status: 0 } + case '1': return { status: 1 } + case '2': return { status: 2 } + default: return {} + } + } + + // 根据传入值获取状态过滤条件 + const getStatusFilterByValue = (value: string | number) => { + switch (String(value)) { + case '0': return { status: 0 } + case '1': return { status: 1 } + case '2': return { status: 2 } + default: return {} + } + } + + // Tab切换处理 + const handleTabChange = (value: string | number) => { + const timestamp = new Date().toLocaleTimeString() + const statusFilter = getStatusFilterByValue(value) + + const newDebugInfo = [ + `[${timestamp}] Tab切换到: ${value}`, + `[${timestamp}] 状态过滤: ${JSON.stringify(statusFilter)}`, + `[${timestamp}] 预期显示: ${getStatusText(Number(value))}状态的礼品卡`, + '---' + ] + + setDebugInfo(prev => [...newDebugInfo, ...prev]) + setActiveTab(value) + } + + const getStatusText = (status: number): string => { + switch (status) { + case 0: return '未使用' + case 1: return '已使用' + case 2: return '失效' + default: return '未知' + } + } + + // 转换数据 + const transformGiftData = (gift: ShopGift) => { + return { + id: gift.id || 0, + name: gift.name || '礼品卡', + goodsName: gift.goodsName, + description: `状态: ${getStatusText(gift.status || 0)}`, + code: gift.code, + goodsImage: gift.goodsImage, + faceValue: gift.faceValue, + type: gift.type, + status: gift.status, + expireTime: gift.expireTime, + useTime: gift.useTime, + showCode: gift.status === 0, + showUseBtn: gift.status === 0, + showDetailBtn: true, + theme: 'blue' as const, + onUse: () => console.log('使用'), + onDetail: () => console.log('详情') + } + } + + const currentData = mockData[String(activeTab)] || [] + + return ( + + {/* 页面标题 */} + + + Tab切换调试页面 + + + + {/* 当前状态信息 */} + + 当前状态信息: + + activeTab: {String(activeTab)} + 状态过滤: {JSON.stringify(getStatusFilter())} + 显示数据条数: {currentData.length} + 预期状态: {getStatusText(Number(activeTab))} + + + + {/* Tab切换 */} + + + + + + + + + + + + {/* 礼品卡展示 */} + + + 当前显示: {getStatusText(Number(activeTab))}状态礼品卡 + + + {currentData.length > 0 ? ( + currentData.map((gift) => ( + + + + 礼品卡ID: {gift.id}, status: {gift.status}, 预期: {getStatusText(gift.status || 0)} + + + + + )) + ) : ( + + + 暂无{getStatusText(Number(activeTab))}状态的礼品卡 + + + )} + + + {/* 调试日志 */} + + + 调试日志: + + + + {debugInfo.length > 0 ? ( + debugInfo.map((info, index) => ( + + {info} + + )) + ) : ( + 暂无调试信息 + )} + + + + {/* 测试按钮 */} + + 快速测试: + + + + + + + + ) +} + +export default DebugTab diff --git a/src/user/gift/detail.tsx b/src/user/gift/detail.tsx index 03a5658..61560ca 100644 --- a/src/user/gift/detail.tsx +++ b/src/user/gift/detail.tsx @@ -1,7 +1,7 @@ import {useState, useEffect} from "react"; import {useRouter} from '@tarojs/taro' -import {Button, ConfigProvider, Tag, Divider, Image} from '@nutui/nutui-react-taro' -import {ArrowLeft, Gift, Clock, Location, Phone, Share, Copy} from '@nutui/icons-react-taro' +import {Button, ConfigProvider, Divider} from '@nutui/nutui-react-taro' +import {Gift, Clock, Location, Phone, Copy, QrCode} from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import {View, Text} from '@tarojs/components' import {ShopGift} from "@/api/shop/shopGift/model"; @@ -87,7 +87,7 @@ const GiftCardDetail = () => { const getGiftStatus = () => { if (!gift) return { status: 0, text: '未知', color: 'default' } - switch (gift.useStatus) { + switch (gift.status) { case 0: return { status: 0, text: '可使用', color: 'success' } case 1: @@ -103,18 +103,7 @@ const GiftCardDetail = () => { const handleUseGift = () => { if (!gift) return - Taro.showModal({ - title: '使用礼品卡', - content: `确定要使用"${gift.name}"吗?`, - success: (res) => { - if (res.confirm) { - // 跳转到礼品卡使用页面 - Taro.navigateTo({ - url: `/user/gift/use?id=${gift.id}` - }) - } - } - }) + } // 复制兑换码 @@ -171,30 +160,22 @@ const GiftCardDetail = () => { {/* 礼品卡卡片 */} - + {getGiftValueDisplay()} {getGiftTypeText(gift.type)} - {gift.goodsImage ? ( - - ) : ( - - )} + {gift.name} - {gift.description || getUsageText()} + {gift.description || getUsageText()} {/* 兑换码 */} {gift.code && ( - 兑换码 + 兑换码 {gift.code} + + + {/* 技术说明 */} + + 技术实现: + + • 二维码包含礼品卡ID、核销码等信息 + • 每次生成随机6位数字核销码 + • 支持扫码和手动输入两种验证方式 + • 核销后礼品卡状态更新为已使用 + + + + ) +} + +export default QRCodeDemo diff --git a/src/user/gift/qrcode-verification-implementation.md b/src/user/gift/qrcode-verification-implementation.md new file mode 100644 index 0000000..2ae4505 --- /dev/null +++ b/src/user/gift/qrcode-verification-implementation.md @@ -0,0 +1,212 @@ +# 礼品卡二维码核销功能实现说明 + +## 功能概述 + +将礼品卡的"立即使用"功能改为生成二维码,供门店工作人员扫码核销,提升用户体验和核销效率。 + +## 核心功能 + +### 1. 用户端功能 +- **生成核销二维码**:点击"立即使用"按钮生成包含核销信息的二维码 +- **显示核销码**:同时显示6位数字核销码,支持手动输入 +- **复制功能**:支持复制核销码和兑换码 +- **刷新功能**:可重新生成新的核销码 + +### 2. 门店端功能 +- **扫码核销**:使用摄像头扫描用户的二维码 +- **手动输入**:支持手动输入核销码进行验证 +- **信息验证**:显示礼品卡详细信息供确认 +- **完成核销**:确认后完成核销操作 + +## 技术实现 + +### 1. 组件结构 + +#### GiftCardQRCode 组件 +```typescript +interface GiftCardQRCodeProps { + visible: boolean + onClose: () => void + giftCard: { + id: number + name?: string + goodsName?: string + code?: string + faceValue?: string + type?: number + status?: number + expireTime?: string + contactInfo?: string + } +} +``` + +**主要功能**: +- 生成核销二维码 +- 显示礼品卡信息 +- 提供复制和分享功能 + +#### StoreVerification 页面 +**主要功能**: +- 扫码识别二维码 +- 手动输入核销码 +- 验证礼品卡信息 +- 完成核销操作 + +### 2. 数据流程 + +#### 二维码数据结构 +```json +{ + "type": "gift_card_verification", + "giftId": 123, + "giftCode": "SB2024001234567890", + "verificationCode": "123456", + "faceValue": "100", + "timestamp": 1692123456789, + "expireTime": "2024-12-31 23:59:59", + "codeExpireTime": "2024-08-16 15:30:00" +} +``` + +#### 核销流程 +1. **生成核销码** + ```typescript + POST /shop/shop-gift/generate-verification-code + Body: { giftId: number } + Response: { verificationCode: string, expireTime: string } + ``` + +2. **验证核销码** + ```typescript + POST /shop/shop-gift/verify + Body: { verificationCode?: string, giftCode?: string } + Response: ShopGift + ``` + +3. **完成核销** + ```typescript + POST /shop/shop-gift/complete-verification + Body: { + giftId: number, + verificationCode: string, + storeId?: number, + storeName?: string, + operatorId?: number, + operatorName?: string + } + ``` + +### 3. 安全机制 + +#### 核销码安全 +- **时效性**:核销码有效期15分钟 +- **一次性**:每次生成新的核销码 +- **随机性**:6位随机数字,防止猜测 + +#### 验证机制 +- **双重验证**:支持二维码和手动输入 +- **信息确认**:显示完整礼品卡信息供确认 +- **状态检查**:验证礼品卡状态和有效期 + +## 文件结构 + +### 新增文件 +``` +src/ +├── components/ +│ └── GiftCardQRCode.tsx # 二维码核销弹窗组件 +├── pages/ +│ └── store/ +│ └── verification.tsx # 门店核销页面 +├── user/gift/ +│ ├── qrcode-demo.tsx # 二维码功能演示页面 +│ └── qrcode-verification-implementation.md +└── api/shop/shopGift/ + └── index.ts # 新增核销相关API +``` + +### 修改文件 +``` +src/ +└── components/ + └── GiftCard.tsx # 修改"立即使用"按钮功能 +``` + +## 用户体验优化 + +### 1. 界面设计 +- **清晰的二维码显示**:大尺寸二维码,易于扫描 +- **信息完整展示**:显示礼品卡所有关键信息 +- **操作便捷**:一键复制、刷新等快捷操作 + +### 2. 交互优化 +- **即时反馈**:生成、验证过程有明确的加载状态 +- **错误处理**:友好的错误提示和重试机制 +- **状态管理**:清晰的状态流转和视觉反馈 + +### 3. 兼容性考虑 +- **多种核销方式**:支持扫码和手动输入 +- **设备适配**:适配不同尺寸的移动设备 +- **网络容错**:网络异常时的降级处理 + +## 业务流程 + +### 用户使用流程 +1. 用户在礼品卡列表点击"立即使用" +2. 系统生成核销二维码和核销码 +3. 用户向门店工作人员出示二维码 +4. 工作人员扫码或输入核销码 +5. 系统验证信息并显示礼品卡详情 +6. 工作人员确认后完成核销 +7. 礼品卡状态更新为已使用 + +### 门店操作流程 +1. 打开门店核销页面 +2. 扫描用户二维码或手动输入核销码 +3. 系统验证并显示礼品卡信息 +4. 确认信息无误后点击"确认核销" +5. 完成核销,用户礼品卡状态更新 + +## 测试验证 + +### 1. 功能测试 +- **二维码生成**:验证二维码正确生成和显示 +- **核销码生成**:验证6位数字核销码的随机性 +- **扫码识别**:验证二维码能被正确解析 +- **手动输入**:验证手动输入核销码的准确性 +- **核销完成**:验证核销后状态正确更新 + +### 2. 安全测试 +- **时效性测试**:验证核销码过期机制 +- **重复使用测试**:验证核销码不能重复使用 +- **状态验证**:验证只有未使用的礼品卡能生成核销码 + +### 3. 用户体验测试 +- **界面响应**:验证各种操作的响应速度 +- **错误处理**:验证各种异常情况的处理 +- **设备兼容**:验证在不同设备上的显示效果 + +## 部署注意事项 + +### 1. 后端API +- 确保核销相关API已部署 +- 配置核销码有效期(建议15分钟) +- 设置适当的并发限制 + +### 2. 权限配置 +- 门店核销页面需要相应权限 +- 核销操作需要记录操作人信息 + +### 3. 监控告警 +- 监控核销成功率 +- 监控异常核销请求 +- 设置核销量异常告警 + +## 后续优化建议 + +1. **批量核销**:支持一次核销多张礼品卡 +2. **核销统计**:提供核销数据统计和报表 +3. **离线核销**:支持网络异常时的离线核销 +4. **核销记录**:详细的核销历史记录查询 +5. **多门店支持**:支持多门店的核销管理 diff --git a/src/user/gift/redeem.tsx b/src/user/gift/redeem.tsx index e19f1c5..f3437ec 100644 --- a/src/user/gift/redeem.tsx +++ b/src/user/gift/redeem.tsx @@ -5,7 +5,7 @@ import {ArrowLeft, QrCode, Gift, Voucher} from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import {View, Text} from '@tarojs/components' import {ShopGift} from "@/api/shop/shopGift/model"; -import {validateGiftCode, redeemGift} from "@/api/shop/shopGift"; +import {validateGiftCode, redeemGift, pageShopGift, updateShopGift} from "@/api/shop/shopGift"; import GiftCard from "@/components/GiftCard"; const GiftCardRedeem = () => { @@ -38,14 +38,22 @@ const GiftCardRedeem = () => { setValidating(true) try { - const gift = await validateGiftCode(codeToValidate.trim()) - if(gift){ - setValidGift(gift) + const gifts = await pageShopGift({code: codeToValidate,status: 0}) + if(gifts?.count == 0){ + Taro.showToast({ + title: '兑换码无效或已使用', + icon: 'none' + }) + return + } + const item = gifts?.list[0]; + if(item){ + setValidGift(item) + Taro.showToast({ + title: '验证成功', + icon: 'success' + }) } - Taro.showToast({ - title: '验证成功', - icon: 'success' - }) } catch (error) { console.error('验证兑换码失败:', error) setValidGift(null) @@ -64,8 +72,9 @@ const GiftCardRedeem = () => { setLoading(true) try { - await redeemGift({ - code: code.trim() + await updateShopGift({ + ...validGift, + userId: Taro.getStorageSync('UserId') }) setRedeemSuccess(true) @@ -125,7 +134,6 @@ const GiftCardRedeem = () => { goodsImage: gift.goodsImage, faceValue: gift.faceValue, type: gift.type, - useStatus: gift.useStatus, expireTime: gift.expireTime, useTime: gift.useTime, useLocation: gift.useLocation, diff --git a/src/user/gift/status-field-migration.md b/src/user/gift/status-field-migration.md new file mode 100644 index 0000000..40f067e --- /dev/null +++ b/src/user/gift/status-field-migration.md @@ -0,0 +1,237 @@ +# 礼品卡状态字段迁移说明 + +## 概述 + +根据后端字段调整,将礼品卡的状态字段从 `useStatus` 迁移到 `status`,并更新状态值的含义。 + +## 字段变更 + +### 旧字段 (已废弃) +```typescript +useStatus?: number // 使用状态:0-可用 1-已使用 2-已过期 +``` + +### 新字段 +```typescript +status?: number // 状态:0-未使用 1-已使用 2-失效 +``` + +## 状态值映射 + +| 状态值 | 旧含义 | 新含义 | 显示文本 | 说明 | +|--------|--------|--------|----------|------| +| 0 | 可用 | 未使用 | "未使用" | 可以使用的礼品卡 | +| 1 | 已使用 | 已使用 | "已使用" | 已经使用过的礼品卡 | +| 2 | 已过期 | 失效 | "失效" | 失效的礼品卡(包括过期等情况) | + +## 修改内容 + +### 1. GiftCard 组件 (`src/components/GiftCard.tsx`) + +#### 接口定义更新 +```typescript +// 修改前 +useStatus?: number // 使用状态:0-可用 1-已使用 2-已过期 + +// 修改后 +status?: number // 状态:0-未使用 1-已使用 2-失效 +``` + +#### 状态信息函数更新 +```typescript +// 修改前 +const getStatusInfo = () => { + switch (useStatus) { + case 0: return { text: '可使用', color: 'success' } + case 1: return { text: '已使用', color: 'warning' } + case 2: return { text: '已过期', color: 'danger' } + } +} + +// 修改后 +const getStatusInfo = () => { + switch (status) { + case 0: return { text: '未使用', color: 'success' } + case 1: return { text: '已使用', color: 'warning' } + case 2: return { text: '失效', color: 'danger' } + } +} +``` + +#### 条件判断更新 +```typescript +// 所有 useStatus 相关的条件判断都改为 status +showCode: status === 0 +showUseBtn: status === 0 +className: status !== 0 ? 'disabled' : '' +``` + +### 2. 礼品卡页面 (`src/user/gift/index.tsx`) + +#### 筛选条件更新 +```typescript +// 修改前 +const getStatusFilter = () => { + switch (String(activeTab)) { + case '0': return { useStatus: 0 } // 可用 + case '1': return { useStatus: 1 } // 已使用 + case '2': return { useStatus: 2 } // 已过期 + } +} + +// 修改后 +const getStatusFilter = () => { + switch (String(activeTab)) { + case '0': return { status: 0 } // 未使用 + case '1': return { status: 1 } // 已使用 + case '2': return { status: 2 } // 失效 + } +} +``` + +#### 数据转换更新 +```typescript +// 修改前 +const transformGiftData = (gift: ShopGift) => ({ + useStatus: gift.useStatus, + showCode: gift.useStatus === 0, + showUseBtn: gift.useStatus === 0, +}) + +// 修改后 +const transformGiftData = (gift: ShopGift) => ({ + status: gift.status, + showCode: gift.status === 0, + showUseBtn: gift.status === 0, +}) +``` + +#### Tab标签更新 +```typescript +// 修改前 + + + + +// 修改后 + + + +``` + +#### 空状态文本更新 +```typescript +// 修改前 +activeTab === '0' ? "暂无可用礼品卡" : +activeTab === '1' ? "暂无已使用礼品卡" : +"暂无已过期礼品卡" + +// 修改后 +activeTab === '0' ? "暂无未使用礼品卡" : +activeTab === '1' ? "暂无已使用礼品卡" : +"暂无失效礼品卡" +``` + +### 3. 类型定义 (`src/types/giftCard.ts`) + +#### 枚举更新 +```typescript +// 修改前 +export enum UseStatus { + AVAILABLE = 0, // 可用 + USED = 1, // 已使用 + EXPIRED = 2 // 已过期 +} + +// 修改后 +export enum GiftStatus { + UNUSED = 0, // 未使用 + USED = 1, // 已使用 + INVALID = 2 // 失效 +} +``` + +#### 接口更新 +```typescript +// 修改前 +export interface GiftCardData { + useStatus?: UseStatus +} + +// 修改后 +export interface GiftCardData { + status?: GiftStatus +} +``` + +## 业务逻辑变更 + +### 显示逻辑 +1. **未使用 (status=0)**: + - 显示绿色"未使用"标签 + - 显示兑换码 + - 显示"立即使用"按钮 + - 显示过期时间提醒 + +2. **已使用 (status=1)**: + - 显示灰色"已使用"标签 + - 不显示兑换码和使用按钮 + - 显示使用时间和地址 + - 卡片呈现禁用状态 + +3. **失效 (status=2)**: + - 显示红色"失效"标签 + - 不显示兑换码和使用按钮 + - 显示状态遮罩 + - 卡片呈现禁用状态 + +### API 调用变更 +```typescript +// 修改前 +getUserGifts({ useStatus: 0 }) // 获取可用礼品卡 + +// 修改后 +getUserGifts({ status: 0 }) // 获取未使用礼品卡 +``` + +## 测试验证 + +### 测试页面 +访问 `/user/gift/status-test` 可以验证: +- 不同状态值的显示效果 +- 状态标签文本正确性 +- 按钮和兑换码的显示逻辑 +- 卡片的禁用状态 + +### 测试用例 +1. **status=0**: 显示"未使用",有兑换码和使用按钮 +2. **status=1**: 显示"已使用",显示使用时间,无按钮 +3. **status=2**: 显示"失效",有状态遮罩,无按钮 + +## 兼容性说明 + +### 数据兼容 +- 如果后端同时返回 `useStatus` 和 `status`,优先使用 `status` +- 建议后端逐步迁移,确保数据一致性 + +### 前端兼容 +- 前端已完全切换到 `status` 字段 +- 不再处理 `useStatus` 字段 +- 所有相关逻辑都已更新 + +## 部署检查清单 + +- [ ] 后端 API 返回 `status` 字段而非 `useStatus` +- [ ] 状态值含义正确:0未使用 1已使用 2失效 +- [ ] Tab标签显示正确文本 +- [ ] 状态标签显示正确文本 +- [ ] 筛选功能使用正确的字段名 +- [ ] 空状态提示文本正确 +- [ ] 测试页面验证通过 + +## 注意事项 + +1. **数据一致性**:确保后端和前端对状态值的理解一致 +2. **用户体验**:状态文本更改可能需要用户适应 +3. **API 文档**:更新相关 API 文档,说明字段变更 +4. **监控告警**:关注迁移后的错误日志和用户反馈 diff --git a/src/user/gift/status-test.tsx b/src/user/gift/status-test.tsx new file mode 100644 index 0000000..bc4f1f2 --- /dev/null +++ b/src/user/gift/status-test.tsx @@ -0,0 +1,219 @@ +import React from 'react' +import { View, Text } from '@tarojs/components' +import GiftCard from '@/components/GiftCard' +import { ShopGift } from '@/api/shop/shopGift/model' + +const StatusTest: React.FC = () => { + // 测试不同状态的礼品卡 + const testGifts: ShopGift[] = [ + { + id: 1, + name: '未使用礼品卡', + goodsName: '杜尔伯特草原奶香牛上脑(2kg,分4小包)', + goodsImage: 'https://img.alicdn.com/imgextra/i1/2206571109/O1CN01QZxQJJ1Uw8QZxQJJ_!!2206571109.jpg', + description: '未使用状态的礼品卡', + code: 'UNUSED1234567890', + goodsId: 101, + faceValue: '200', + type: 10, + status: 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: 'USED001234567890', + goodsId: 102, + faceValue: '100', + type: 20, + status: 1, // 已使用 + useTime: '2024-08-15 14:30:00', + useLocation: '星巴克王府井店', + instructions: '适用于虚拟商品兑换', + contactInfo: '400-800-8888' + }, + { + id: 3, + name: '失效礼品卡', + goodsName: '海底捞4人套餐券', + goodsImage: 'https://img.alicdn.com/imgextra/i3/2206571109/O1CN01DEF456_!!2206571109.jpg', + description: '失效状态的礼品卡', + code: 'INVALID1234567890', + goodsId: 103, + faceValue: '300', + type: 30, + status: 2, // 失效 + expireTime: '2024-07-31 23:59:59', + instructions: '适用于服务类商品兑换', + contactInfo: '400-800-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, + status: gift.status, // 使用 status 字段 + 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), + getStatusText(gift.status), + '测试卡片' + ].filter(Boolean), + instructions: [ + '这是状态测试卡片', + '请检查状态显示是否正确', + '不可兑换现金' + ], + notices: [ + '这是测试数据', + '请勿实际使用', + '仅用于状态测试' + ] + }) + }, + showCode: gift.status === 0, // 只有未使用状态显示兑换码 + showUseBtn: gift.status === 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 getStatusText = (status?: number): string => { + switch (status) { + case 0: return '未使用' + case 1: return '已使用' + case 2: 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 'purple' + } + } + + const getExpectedBehavior = (status?: number): string => { + switch (status) { + case 0: return '显示兑换码和使用按钮' + case 1: return '显示使用时间,无兑换码和使用按钮' + case 2: return '显示失效状态,无兑换码和使用按钮' + default: return '未知行为' + } + } + + return ( + + {/* 页面标题 */} + + + 礼品卡状态字段测试 + + + 验证 status 字段替代 useStatus 字段 + + + + {/* 状态说明 */} + + 状态字段说明: + + • status=0 (未使用) → 显示"未使用"标签 + • status=1 (已使用) → 显示"已使用"标签 + • status=2 (失效) → 显示"失效"标签 + + + + {/* 修改说明 */} + + 字段修改说明: + + ✅ useStatus 字段已废弃 + ✅ 使用 status 字段替代 + ✅ 状态文本更新:可用→未使用,已过期→失效 + ✅ Tab标签文本同步更新 + + + + {/* 礼品卡列表 */} + + {testGifts.map((gift, index) => ( + + + + 测试 {index + 1}: status={gift.status} → {getStatusText(gift.status)} + + + 预期行为: {getExpectedBehavior(gift.status)} + + + + + ))} + + + {/* 验证清单 */} + + 验证清单: + + □ status=0 的卡片显示"未使用"标签 + □ status=0 的卡片显示兑换码和使用按钮 + □ status=1 的卡片显示"已使用"标签 + □ status=1 的卡片显示使用时间 + □ status=1 的卡片不显示兑换码和使用按钮 + □ status=2 的卡片显示"失效"标签 + □ status=2 的卡片有状态遮罩 + □ 所有非0状态的卡片都有disabled样式 + + + + {/* API 参数说明 */} + + API 参数更新: + + • 筛选参数:useStatus → status + • 返回字段:useStatus → status + • 状态值:0未使用 1已使用 2失效 + + + + ) +} + +export default StatusTest diff --git a/src/user/gift/tab-switch-fix.md b/src/user/gift/tab-switch-fix.md new file mode 100644 index 0000000..34e9e04 --- /dev/null +++ b/src/user/gift/tab-switch-fix.md @@ -0,0 +1,198 @@ +# Tab切换状态传值修复说明 + +## 问题描述 + +用户反馈:点击"未使用"标签时,显示的却是"已使用"状态的礼品卡,说明Tab切换时传递的状态值不正确。 + +## 问题分析 + +经过代码检查,发现可能的问题点: + +1. **异步状态更新问题**:`handleTabChange` 中使用 `setTimeout` 延迟执行,可能导致状态更新不及时 +2. **API参数接口混乱**:`ShopGiftParam` 接口中同时存在 `status` 和 `useStatus` 字段 +3. **状态获取时机问题**:`getStatusFilter()` 可能获取到的是旧的 `activeTab` 值 + +## 修复方案 + +### 1. 优化Tab切换逻辑 + +**修改前**: +```typescript +const handleTabChange = (value: string | number) => { + setActiveTab(value) + setPage(1) + setList([]) + setHasMore(true) + // 延迟执行reload,确保状态更新完成 + setTimeout(() => { + reload(true) + }, 100) +} +``` + +**修改后**: +```typescript +const handleTabChange = (value: string | number) => { + console.log('Tab切换到:', value) + setActiveTab(value) + setPage(1) + setList([]) + setHasMore(true) + + // 直接传递状态值,避免异步状态更新问题 + const statusFilter = getStatusFilterByValue(value) + console.log('状态过滤条件:', statusFilter) + + // 立即加载数据 + loadGiftsByStatus(statusFilter) +} +``` + +### 2. 新增辅助函数 + +```typescript +// 根据传入的值获取状态过滤条件 +const getStatusFilterByValue = (value: string | number) => { + switch (String(value)) { + case '0': return { status: 0 } // 未使用 + case '1': return { status: 1 } // 已使用 + case '2': return { status: 2 } // 失效 + default: return {} + } +} + +// 根据状态过滤条件加载礼品卡 +const loadGiftsByStatus = async (statusFilter: any) => { + setLoading(true) + try { + const res = await getUserGifts({ + page: 1, + limit: 10, + userId: Taro.getStorageSync('UserId'), + ...statusFilter + }) + console.log('API返回数据:', res?.list) + if (res && res.list) { + setList(res.list) + setHasMore(res.list.length === 10) + setPage(2) + } else { + setList([]) + setHasMore(false) + } + } catch (error) { + console.error('获取礼品卡失败:', error) + Taro.showToast({ + title: '获取礼品卡失败', + icon: 'error' + }) + } finally { + setLoading(false) + } +} +``` + +### 3. 清理API接口定义 + +**修改前**: +```typescript +export interface ShopGiftParam extends PageParam { + // 状态筛选 + status?: number; + // 使用状态筛选 + useStatus?: number; // 冗余字段,容易造成混乱 +} +``` + +**修改后**: +```typescript +export interface ShopGiftParam extends PageParam { + // 状态筛选 (0未使用 1已使用 2失效) + status?: number; + // 移除了 useStatus 字段 +} +``` + +### 4. 增强调试功能 + +在关键函数中添加了 `console.log` 调试信息: +```typescript +console.log('Tab切换到:', value) +console.log('状态过滤条件:', statusFilter) +console.log('API返回数据:', res?.list) +``` + +## 修改的文件 + +1. **`src/user/gift/index.tsx`** + - 优化 `handleTabChange` 函数 + - 新增 `getStatusFilterByValue` 函数 + - 新增 `loadGiftsByStatus` 函数 + - 增强调试日志 + +2. **`src/api/shop/shopGift/model/index.ts`** + - 移除 `ShopGiftParam` 接口中的 `useStatus` 字段 + - 明确 `status` 字段的含义 + +3. **新增测试文件** + - `src/user/gift/debug-tab.tsx` - Tab切换调试页面 + - `src/user/gift/api-test.tsx` - API参数测试页面 + +## 测试验证 + +### 1. 调试页面测试 +访问 `/user/gift/debug-tab` 可以: +- 模拟Tab切换操作 +- 查看实时的状态变化 +- 验证数据显示是否正确 + +### 2. API参数测试 +访问 `/user/gift/api-test` 可以: +- 直接测试API调用 +- 验证 `status` 参数传递 +- 检查返回数据的状态一致性 + +### 3. 实际功能测试 +在主页面 `/user/gift/index` 中: +- 点击不同Tab标签 +- 查看控制台调试信息 +- 验证显示的数据状态是否正确 + +## 预期效果 + +修复后的预期效果: + +1. **Tab切换立即生效**: + - 点击"未使用"显示 `status=0` 的礼品卡 + - 点击"已使用"显示 `status=1` 的礼品卡 + - 点击"失效"显示 `status=2` 的礼品卡 + +2. **状态显示一致**: + - Tab标签文本与显示的数据状态一致 + - 不再出现点击"未使用"显示"已使用"数据的问题 + +3. **调试信息清晰**: + - 控制台输出详细的状态切换信息 + - 便于排查问题和验证修复效果 + +## 回滚方案 + +如果修复后出现问题,可以: + +1. **恢复原有的 `handleTabChange` 函数** +2. **保留 `setTimeout` 延迟执行逻辑** +3. **在 `ShopGiftParam` 中恢复 `useStatus` 字段** + +## 注意事项 + +1. **后端兼容性**:确保后端API支持 `status` 参数筛选 +2. **数据一致性**:确保后端返回的数据中 `status` 字段值正确 +3. **缓存清理**:如有必要,清理相关的缓存数据 +4. **用户体验**:修复后用户操作应该更加流畅和准确 + +## 后续优化 + +1. **错误处理**:增强API调用失败时的错误处理 +2. **加载状态**:优化Tab切换时的加载状态显示 +3. **性能优化**:考虑添加数据缓存机制 +4. **用户反馈**:收集用户使用反馈,持续优化体验 diff --git a/src/user/store/verification.config.ts b/src/user/store/verification.config.ts new file mode 100644 index 0000000..77afd68 --- /dev/null +++ b/src/user/store/verification.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '门店核销', + navigationBarTextStyle: 'black' +}) diff --git a/src/user/store/verification.tsx b/src/user/store/verification.tsx new file mode 100644 index 0000000..1fc3661 --- /dev/null +++ b/src/user/store/verification.tsx @@ -0,0 +1,348 @@ +import React, { useState } from 'react' +import { View, Text } from '@tarojs/components' +import { Button, Input, Tag, Divider } from '@nutui/nutui-react-taro' +import { Scan, Search } from '@nutui/icons-react-taro' +import Taro from '@tarojs/taro' +import dayjs from 'dayjs' +import {getShopGiftByCode, updateShopGift} from "@/api/shop/shopGift"; + +interface VerificationData { + type: string + giftId: number + giftCode: string + verificationCode: string + faceValue: string + timestamp: number + expireTime?: string +} + +interface GiftCardInfo { + id: number + name: string + goodsName?: string + faceValue: string + type: number + status: number + expireTime?: string + code: string +} + +const StoreVerification: React.FC = () => { + const [scanResult, setScanResult] = useState('') + const [verificationCode, setVerificationCode] = useState('') + const [giftInfo, setGiftInfo] = useState(null) + const [verificationResult, setVerificationResult] = useState<'success' | 'failed' | null>(null) + const [loading, setLoading] = useState(false) + + // 扫码功能 + const handleScan = () => { + Taro.scanCode({ + success: (res) => { + console.log('扫码结果:', res.result) + setScanResult(res.result) + parseQRCode(res.result) + }, + fail: (err) => { + console.error('扫码失败:', err) + Taro.showToast({ + title: '扫码失败', + icon: 'error' + }) + } + }) + } + + // 解析二维码数据 + const parseQRCode = (qrData: string) => { + try { + const data: VerificationData = JSON.parse(qrData) + + if (data.type === 'gift_card_verification') { + setVerificationCode(data.verificationCode) + // 模拟获取礼品卡信息 + mockGetGiftInfo(data) + } else { + throw new Error('无效的二维码格式') + } + } catch (error) { + console.error('解析二维码失败:', error) + Taro.showToast({ + title: '无效的二维码', + icon: 'error' + }) + } + } + + // 模拟获取礼品卡信息(实际应该调用API) + const mockGetGiftInfo = (data: VerificationData) => { + // 这里应该调用后端API验证礼品卡信息 + const mockGiftInfo: GiftCardInfo = { + id: data.giftId, + name: '礼品卡', + goodsName: '星巴克咖啡券', + faceValue: data.faceValue, + type: 20, + status: 0, + expireTime: data.expireTime, + code: data.giftCode + } + + setGiftInfo(mockGiftInfo) + } + + // 手动输入核销码验证 + const handleManualVerification = async () => { + if (!verificationCode.trim()) { + Taro.showToast({ + title: '请输入核销码', + icon: 'none' + }) + return + } + + setLoading(true) + try { + // 这里应该调用后端API验证核销码 + const giftCard = await getShopGiftByCode(verificationCode.trim()) + await updateShopGift({ + ...giftCard, + status: 1 + }) + Taro.showToast({ + title: '核销成功', + icon: 'success' + }) + } catch (error) { + console.error('验证失败:', error) + } finally { + setLoading(false) + } + } + + // 模拟验证核销码 + const mockVerifyCode = async (code: string) => { + // 模拟API调用延迟 + await new Promise(resolve => setTimeout(resolve, 1000)) + + // 模拟验证逻辑 + if (code.length === 6 && /^\d+$/.test(code)) { + setVerificationResult('success') + setGiftInfo({ + id: 1, + name: '礼品卡', + goodsName: '星巴克咖啡券', + faceValue: '100', + type: 20, + status: 0, + expireTime: '2024-12-31 23:59:59', + code: 'SB2024001234567890' + }) + Taro.showToast({ + title: '验证成功', + icon: 'success' + }) + } else { + setVerificationResult('failed') + Taro.showToast({ + title: '核销码无效', + icon: 'error' + }) + } + } + + // 确认核销 + const handleConfirmVerification = async () => { + if (!giftInfo) return + + setLoading(true) + try { + // 这里应该调用后端API完成核销 + await mockCompleteVerification(giftInfo.id) + + Taro.showToast({ + title: '核销成功', + icon: 'success' + }) + + // 重置状态 + setTimeout(() => { + resetForm() + }, 2000) + } catch (error) { + console.error('核销失败:', error) + Taro.showToast({ + title: '核销失败', + icon: 'error' + }) + } finally { + setLoading(false) + } + } + + // 模拟完成核销 + const mockCompleteVerification = async (giftId: number) => { + await new Promise(resolve => setTimeout(resolve, 1000)) + console.log('核销礼品卡:', giftId) + } + + // 重置表单 + const resetForm = () => { + setScanResult('') + setVerificationCode('') + setGiftInfo(null) + setVerificationResult(null) + } + + // 获取类型文本 + const getTypeText = (type: number) => { + switch (type) { + case 10: return '实物礼品卡' + case 20: return '虚拟礼品卡' + case 30: return '服务礼品卡' + default: return '礼品卡' + } + } + + return ( + + {/* 页面标题 */} + + + 礼品卡核销 + + + 门店工作人员核销页面 + + + + {/* 扫码区域 */} + + + 扫码核销 + + + + {scanResult && ( + + 扫码结果: + + {scanResult} + + + )} + + + {/* 手动输入区域 */} + + 手动输入核销码 + + + + + + + {/* 礼品卡信息 */} + {giftInfo && ( + + + 礼品卡信息 + {verificationResult === 'success' && ( + + + 验证成功 + + )} + {verificationResult === 'failed' && ( + + + 验证失败 + + )} + + + + + 商品名称 + + {giftInfo.goodsName || giftInfo.name} + + + + + 面值 + + ¥{giftInfo.faceValue} + + + + + 类型 + {getTypeText(giftInfo.type)} + + + + 兑换码 + {giftInfo.code} + + + {giftInfo.expireTime && ( + + 有效期至 + + {dayjs(giftInfo.expireTime).format('YYYY-MM-DD')} + + + )} + + + + + {verificationResult === 'success' && ( + + )} + + )} + + {/* 使用说明 */} + + 操作说明: + + 1. 用户出示礼品卡二维码 + 2. 点击"扫描二维码"进行扫码 + 3. 或手动输入用户提供的核销码 + + + + ) +} + +export default StoreVerification