forked from gxwebsoft/mp-10550
feat(admin): 从文章详情页面改为文章管理页面
- 修改页面配置,设置新的导航栏标题和样式 - 重新设计页面布局,增加搜索栏、文章列表和操作按钮 - 添加文章搜索、分页加载和删除功能 - 优化文章列表项的样式和交互 - 新增礼品卡相关API和组件 - 更新优惠券组件,增加到期提醒和筛选功能
This commit is contained in:
@@ -2,12 +2,18 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
height: 120px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
@@ -15,7 +21,7 @@
|
||||
|
||||
.coupon-left {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
width: 110px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -24,23 +30,23 @@
|
||||
position: relative;
|
||||
|
||||
&.theme-red {
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||
}
|
||||
|
||||
&.theme-orange {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||
}
|
||||
|
||||
&.theme-blue {
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
|
||||
}
|
||||
|
||||
&.theme-purple {
|
||||
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
||||
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
|
||||
}
|
||||
|
||||
&.theme-green {
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||
}
|
||||
|
||||
.amount-wrapper {
|
||||
@@ -49,22 +55,23 @@
|
||||
margin-bottom: 8px;
|
||||
|
||||
.currency {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 30px;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.condition {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
opacity: 0.9;
|
||||
margin-top: 2px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,21 +119,23 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
|
||||
.coupon-info {
|
||||
flex: 1;
|
||||
|
||||
.coupon-title {
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.coupon-validity {
|
||||
font-size: 24px;
|
||||
font-size: 26px;
|
||||
color: #6b7280;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,38 +145,45 @@
|
||||
align-items: center;
|
||||
|
||||
.coupon-btn {
|
||||
min-width: 48px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 24px;
|
||||
min-width: 120px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
font-size: 26px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.theme-red {
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||
}
|
||||
|
||||
&.theme-orange {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||
}
|
||||
|
||||
&.theme-blue {
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
|
||||
}
|
||||
|
||||
&.theme-purple {
|
||||
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
||||
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
|
||||
}
|
||||
|
||||
&.theme-green {
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
color: #6b7280;
|
||||
padding: 4px 8px;
|
||||
font-size: 26px;
|
||||
color: #9ca3af;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +79,41 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
|
||||
// 获取使用条件文本
|
||||
const getConditionText = () => {
|
||||
if (type === 3) return '无门槛'
|
||||
if (type === 3) return '免费使用' // 免费券
|
||||
if (minAmount && minAmount > 0) {
|
||||
return `满${minAmount}可用`
|
||||
return `满${minAmount}元可用`
|
||||
}
|
||||
return '无门槛'
|
||||
}
|
||||
|
||||
// 格式化有效期显示
|
||||
const formatValidityPeriod = () => {
|
||||
if (!startTime || !endTime) return ''
|
||||
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
const now = new Date()
|
||||
|
||||
// 如果还未开始
|
||||
if (now < start) {
|
||||
return `${start.getMonth() + 1}.${start.getDate()} 开始生效`
|
||||
}
|
||||
|
||||
// 计算剩余天数
|
||||
const diffTime = end.getTime() - now.getTime()
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays <= 0) {
|
||||
return '已过期'
|
||||
} else if (diffDays <= 3) {
|
||||
return `${diffDays}天后过期`
|
||||
} else {
|
||||
return `${end.getMonth() + 1}.${end.getDate()} 过期`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return ''
|
||||
@@ -108,8 +136,8 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
{/* 左侧金额区域 */}
|
||||
<View className={`coupon-left ${themeClass}`}>
|
||||
<View className="amount-wrapper">
|
||||
<Text className="currency">¥</Text>
|
||||
<Text className="amount">{amount}</Text>
|
||||
{type !== 3 && <Text className="currency">¥</Text>}
|
||||
<Text className="amount">{formatAmount()}</Text>
|
||||
</View>
|
||||
<View className="condition">
|
||||
{getConditionText()}
|
||||
@@ -130,7 +158,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')}
|
||||
</View>
|
||||
<View className="coupon-validity">
|
||||
有效期:{getValidityText()}
|
||||
{formatValidityPeriod()}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -151,7 +179,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
size="small"
|
||||
onClick={onUse}
|
||||
>
|
||||
使用
|
||||
立即使用
|
||||
</Button>
|
||||
)}
|
||||
{status !== 0 && (
|
||||
|
||||
174
src/components/CouponExpireNotice.tsx
Normal file
174
src/components/CouponExpireNotice.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Clock, Close, Agenda } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface ExpiringSoon {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
amount: string
|
||||
minAmount?: string
|
||||
endTime: string
|
||||
daysLeft: number
|
||||
}
|
||||
|
||||
export interface CouponExpireNoticeProps {
|
||||
/** 是否显示提醒 */
|
||||
visible: boolean
|
||||
/** 即将过期的优惠券列表 */
|
||||
expiringSoonCoupons: ExpiringSoon[]
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
/** 使用优惠券回调 */
|
||||
onUseCoupon: (coupon: ExpiringSoon) => void
|
||||
}
|
||||
|
||||
const CouponExpireNotice: React.FC<CouponExpireNoticeProps> = ({
|
||||
visible,
|
||||
expiringSoonCoupons,
|
||||
onClose,
|
||||
onUseCoupon
|
||||
}) => {
|
||||
// 获取优惠券金额显示
|
||||
const getCouponAmountDisplay = (coupon: ExpiringSoon) => {
|
||||
switch (coupon.type) {
|
||||
case 10: // 满减券
|
||||
return `¥${coupon.amount}`
|
||||
case 20: // 折扣券
|
||||
return `${coupon.amount}折`
|
||||
case 30: // 免费券
|
||||
return '免费'
|
||||
default:
|
||||
return `¥${coupon.amount}`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取使用条件文本
|
||||
const getConditionText = (coupon: ExpiringSoon) => {
|
||||
if (coupon.type === 30) return '无门槛使用'
|
||||
if (coupon.minAmount && parseFloat(coupon.minAmount) > 0) {
|
||||
return `满${coupon.minAmount}元可用`
|
||||
}
|
||||
return '无门槛使用'
|
||||
}
|
||||
|
||||
// 获取到期时间显示
|
||||
const getExpireTimeDisplay = (coupon: ExpiringSoon) => {
|
||||
if (coupon.daysLeft === 0) {
|
||||
return '今天到期'
|
||||
} else if (coupon.daysLeft === 1) {
|
||||
return '明天到期'
|
||||
} else {
|
||||
return `${coupon.daysLeft}天后到期`
|
||||
}
|
||||
}
|
||||
|
||||
// 去购物
|
||||
const handleGoShopping = () => {
|
||||
onClose()
|
||||
Taro.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
style={{ width: '90%', maxWidth: '400px' }}
|
||||
round
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<View className="flex items-center">
|
||||
<Clock size="24" className="text-orange-500 mr-2" />
|
||||
<Text className="text-lg font-semibold text-gray-900">
|
||||
优惠券即将过期
|
||||
</Text>
|
||||
</View>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-500" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 提醒文案 */}
|
||||
<View className="text-center mb-6">
|
||||
<Text className="text-gray-600">
|
||||
您有 {expiringSoonCoupons.length} 张优惠券即将过期,请及时使用
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 优惠券列表 */}
|
||||
<View className="max-h-80 overflow-y-auto mb-6">
|
||||
{expiringSoonCoupons.map((coupon, _) => (
|
||||
<View
|
||||
key={coupon.id}
|
||||
className="flex items-center justify-between p-3 mb-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center mb-1">
|
||||
<Text className="font-semibold text-gray-900 mr-2">
|
||||
{coupon.name}
|
||||
</Text>
|
||||
<View className="px-2 py-1 bg-red-100 rounded text-red-600 text-xs">
|
||||
{getCouponAmountDisplay(coupon)}
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-sm text-gray-600 mb-1">
|
||||
{getConditionText(coupon)}
|
||||
</Text>
|
||||
<Text className={`text-xs ${
|
||||
coupon.daysLeft === 0 ? 'text-red-500' :
|
||||
coupon.daysLeft === 1 ? 'text-orange-500' : 'text-gray-500'
|
||||
}`}>
|
||||
{getExpireTimeDisplay(coupon)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => onUseCoupon(coupon)}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="flex gap-3">
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={onClose}
|
||||
>
|
||||
稍后提醒
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
icon={<Agenda />}
|
||||
onClick={handleGoShopping}
|
||||
>
|
||||
去购物
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<View className="text-center mt-4">
|
||||
<Text className="text-xs text-gray-400">
|
||||
过期的优惠券将无法使用,请及时关注有效期
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponExpireNotice
|
||||
210
src/components/CouponFilter.tsx
Normal file
210
src/components/CouponFilter.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup, Radio, RadioGroup, Divider } from '@nutui/nutui-react-taro'
|
||||
import { Filter, Close } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface CouponFilterProps {
|
||||
/** 是否显示筛选器 */
|
||||
visible: boolean
|
||||
/** 当前筛选条件 */
|
||||
filters: {
|
||||
type?: number[]
|
||||
minAmount?: number
|
||||
sortBy?: 'createTime' | 'amount' | 'expireTime'
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
/** 筛选条件变更回调 */
|
||||
onFiltersChange: (filters: any) => void
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CouponFilter: React.FC<CouponFilterProps> = ({
|
||||
visible,
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onClose
|
||||
}) => {
|
||||
const [tempFilters, setTempFilters] = useState(filters)
|
||||
|
||||
// 优惠券类型选项
|
||||
const typeOptions = [
|
||||
{ label: '全部类型', value: '' },
|
||||
{ label: '满减券', value: '10' },
|
||||
{ label: '折扣券', value: '20' },
|
||||
{ label: '免费券', value: '30' }
|
||||
]
|
||||
|
||||
// 最低金额选项
|
||||
const minAmountOptions = [
|
||||
{ label: '不限', value: '' },
|
||||
{ label: '10元以上', value: '10' },
|
||||
{ label: '50元以上', value: '50' },
|
||||
{ label: '100元以上', value: '100' },
|
||||
{ label: '200元以上', value: '200' }
|
||||
]
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = [
|
||||
{ label: '创建时间', value: 'createTime' },
|
||||
{ label: '优惠金额', value: 'amount' },
|
||||
{ label: '到期时间', value: 'expireTime' }
|
||||
]
|
||||
|
||||
// 排序方向选项
|
||||
const sortOrderOptions = [
|
||||
{ label: '升序', value: 'asc' },
|
||||
{ label: '降序', value: 'desc' }
|
||||
]
|
||||
|
||||
// 重置筛选条件
|
||||
const handleReset = () => {
|
||||
const resetFilters = {
|
||||
type: [],
|
||||
minAmount: undefined,
|
||||
sortBy: 'createTime' as const,
|
||||
sortOrder: 'desc' as const
|
||||
}
|
||||
setTempFilters(resetFilters)
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
const handleApply = () => {
|
||||
onFiltersChange(tempFilters)
|
||||
onClose()
|
||||
}
|
||||
|
||||
// 更新临时筛选条件
|
||||
const updateTempFilters = (key: string, value: any) => {
|
||||
setTempFilters(prev => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="right"
|
||||
style={{ width: '80%', height: '100%' }}
|
||||
>
|
||||
<View className="h-full flex flex-col">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<View className="flex items-center">
|
||||
<Filter size="20" className="text-gray-600 mr-2" />
|
||||
<Text className="text-lg font-semibold">筛选条件</Text>
|
||||
</View>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-600" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 筛选内容 */}
|
||||
<View className="flex-1 overflow-y-auto p-4">
|
||||
{/* 优惠券类型 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
优惠券类型
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.type?.[0]?.toString() || ''}
|
||||
onChange={(value) => {
|
||||
updateTempFilters('type', value ? [parseInt(value)] : [])
|
||||
}}
|
||||
>
|
||||
{typeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 最低消费金额 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
最低消费金额
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.minAmount?.toString() || ''}
|
||||
onChange={(value) => {
|
||||
updateTempFilters('minAmount', value ? parseInt(value) : undefined)
|
||||
}}
|
||||
>
|
||||
{minAmountOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 排序方式 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
排序方式
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.sortBy || 'createTime'}
|
||||
onChange={(value) => updateTempFilters('sortBy', value)}
|
||||
>
|
||||
{sortOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 排序方向 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
排序方向
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.sortOrder || 'desc'}
|
||||
onChange={(value) => updateTempFilters('sortOrder', value)}
|
||||
>
|
||||
{sortOrderOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="p-4 border-t border-gray-100">
|
||||
<View className="flex gap-3">
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={handleApply}
|
||||
>
|
||||
应用
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponFilter
|
||||
159
src/components/CouponGuide.tsx
Normal file
159
src/components/CouponGuide.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Ask, Ticket, Clock, Gift } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface CouponGuideProps {
|
||||
/** 是否显示指南 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CouponGuide: React.FC<CouponGuideProps> = ({
|
||||
visible,
|
||||
onClose
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const guideSteps = [
|
||||
{
|
||||
title: '如何获取优惠券?',
|
||||
icon: <Gift size="24" className="text-red-500" />,
|
||||
content: [
|
||||
'1. 点击"领取"按钮浏览可领取的优惠券',
|
||||
'2. 关注商家活动和促销信息',
|
||||
'3. 完成指定任务获得优惠券奖励',
|
||||
'4. 邀请好友注册获得推荐奖励'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '如何使用优惠券?',
|
||||
icon: <Ticket size="24" className="text-green-500" />,
|
||||
content: [
|
||||
'1. 选择心仪商品加入购物车',
|
||||
'2. 在结算页面选择可用优惠券',
|
||||
'3. 确认优惠金额后完成支付',
|
||||
'4. 优惠券使用后不可退回'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '优惠券使用规则',
|
||||
icon: <Clock size="24" className="text-blue-500" />,
|
||||
content: [
|
||||
'1. 每张优惠券只能使用一次',
|
||||
'2. 优惠券有使用期限,过期作废',
|
||||
'3. 满减券需达到最低消费金额',
|
||||
'4. 部分优惠券仅限指定商品使用'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '常见问题解答',
|
||||
icon: <Ask size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'Q: 优惠券可以叠加使用吗?',
|
||||
'A: 一般情况下不支持叠加,具体以活动规则为准',
|
||||
'Q: 优惠券过期了怎么办?',
|
||||
'A: 过期优惠券无法使用,请及时关注有效期',
|
||||
'Q: 退款时优惠券会退回吗?',
|
||||
'A: 已使用的优惠券不会退回,请谨慎使用'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < guideSteps.length - 1) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const currentGuide = guideSteps[currentStep]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable={false}
|
||||
style={{ width: '90%', maxWidth: '400px' }}
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="text-center mb-6">
|
||||
<View className="flex justify-center mb-3">
|
||||
{currentGuide.icon}
|
||||
</View>
|
||||
<Text className="text-xl font-bold text-gray-900">
|
||||
{currentGuide.title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className="mb-6">
|
||||
{currentGuide.content.map((item, index) => (
|
||||
<View key={index} className="mb-3">
|
||||
<Text className="text-gray-700 leading-relaxed">
|
||||
{item}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 进度指示器 */}
|
||||
<View className="flex justify-center mb-6">
|
||||
{guideSteps.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full mx-1 ${
|
||||
index === currentStep ? 'bg-red-500' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="flex justify-between">
|
||||
<View className="flex gap-2">
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handleSkip}
|
||||
>
|
||||
跳过
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
>
|
||||
{currentStep === guideSteps.length - 1 ? '完成' : '下一步'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponGuide
|
||||
182
src/components/CouponShare.tsx
Normal file
182
src/components/CouponShare.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Share, Wechat, QQ, Weibo, Link, Close } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface CouponShareProps {
|
||||
/** 是否显示分享弹窗 */
|
||||
visible: boolean
|
||||
/** 优惠券信息 */
|
||||
coupon: {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
amount: string
|
||||
minAmount?: string
|
||||
description?: string
|
||||
}
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CouponShare: React.FC<CouponShareProps> = ({
|
||||
visible,
|
||||
coupon,
|
||||
onClose
|
||||
}) => {
|
||||
// 生成分享文案
|
||||
const generateShareText = () => {
|
||||
const typeText = coupon.type === 10 ? '满减券' : coupon.type === 20 ? '折扣券' : '免费券'
|
||||
const amountText = coupon.type === 10 ? `¥${coupon.amount}` :
|
||||
coupon.type === 20 ? `${coupon.amount}折` : '免费'
|
||||
const conditionText = coupon.minAmount ? `满${coupon.minAmount}元可用` : '无门槛'
|
||||
|
||||
return `🎁 ${coupon.name}\n💰 ${amountText} ${typeText}\n📋 ${conditionText}\n快来领取吧!`
|
||||
}
|
||||
|
||||
// 生成分享链接
|
||||
const generateShareUrl = () => {
|
||||
// 这里应该是实际的分享链接,包含优惠券ID等参数
|
||||
return `https://your-domain.com/coupon/share?id=${coupon.id}`
|
||||
}
|
||||
|
||||
// 微信分享
|
||||
const handleWechatShare = () => {
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
const handleCopyLink = () => {
|
||||
const shareUrl = generateShareUrl()
|
||||
const shareText = generateShareText()
|
||||
const fullText = `${shareText}\n\n${shareUrl}`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: fullText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存图片分享
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
// 这里可以生成优惠券图片并保存到相册
|
||||
// 实际实现需要canvas绘制优惠券图片
|
||||
Taro.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const shareOptions = [
|
||||
{
|
||||
icon: <Wechat size="32" className="text-green-500" />,
|
||||
label: '微信好友',
|
||||
onClick: handleWechatShare
|
||||
},
|
||||
{
|
||||
icon: <Link size="32" className="text-blue-500" />,
|
||||
label: '复制链接',
|
||||
onClick: handleCopyLink
|
||||
},
|
||||
{
|
||||
icon: <Share size="32" className="text-purple-500" />,
|
||||
label: '保存图片',
|
||||
onClick: handleSaveImage
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
style={{ height: 'auto' }}
|
||||
round
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between mb-6">
|
||||
<Text className="text-lg font-semibold">分享优惠券</Text>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-500" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 优惠券预览 */}
|
||||
<View className="bg-gradient-to-r from-red-400 to-red-500 rounded-xl p-4 mb-6 text-white">
|
||||
<Text className="text-xl font-bold mb-2">{coupon.name}</Text>
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold">
|
||||
{coupon.type === 10 ? `¥${coupon.amount}` :
|
||||
coupon.type === 20 ? `${coupon.amount}折` : '免费'}
|
||||
</Text>
|
||||
<Text className="text-sm opacity-90">
|
||||
{coupon.minAmount ? `满${coupon.minAmount}元可用` : '无门槛使用'}
|
||||
</Text>
|
||||
</View>
|
||||
<Share size="24" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 分享选项 */}
|
||||
<View className="grid grid-cols-3 gap-4 mb-4">
|
||||
{shareOptions.map((option, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className="flex flex-col items-center py-4 bg-gray-50 rounded-lg"
|
||||
onClick={option.onClick}
|
||||
>
|
||||
<View className="mb-2">{option.icon}</View>
|
||||
<Text className="text-sm text-gray-700">{option.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 分享文案预览 */}
|
||||
<View className="bg-gray-50 rounded-lg p-3">
|
||||
<Text className="text-xs text-gray-500 mb-2">分享文案预览:</Text>
|
||||
<Text className="text-sm text-gray-700 leading-relaxed">
|
||||
{generateShareText()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponShare
|
||||
71
src/components/CouponStats.tsx
Normal file
71
src/components/CouponStats.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Gift, Voucher, Clock } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface CouponStatsProps {
|
||||
/** 可用优惠券数量 */
|
||||
availableCount: number
|
||||
/** 已使用优惠券数量 */
|
||||
usedCount: number
|
||||
/** 已过期优惠券数量 */
|
||||
expiredCount: number
|
||||
/** 点击统计项的回调 */
|
||||
onStatsClick?: (type: 'available' | 'used' | 'expired') => void
|
||||
}
|
||||
|
||||
const CouponStats: React.FC<CouponStatsProps> = ({
|
||||
availableCount,
|
||||
usedCount,
|
||||
expiredCount,
|
||||
onStatsClick
|
||||
}) => {
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired') => {
|
||||
onStatsClick?.(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-white mx-4 my-3 rounded-xl p-4">
|
||||
<Text className="text-lg font-semibold mb-4 text-gray-800">优惠券统计</Text>
|
||||
|
||||
<View className="flex justify-between">
|
||||
{/* 可用优惠券 */}
|
||||
<View
|
||||
className="flex-1 text-center py-3 mx-1 bg-red-50 rounded-lg"
|
||||
onClick={() => handleStatsClick('available')}
|
||||
>
|
||||
<View className="flex justify-center mb-2">
|
||||
<Gift size="24" className="text-red-500" />
|
||||
</View>
|
||||
<Text className="text-2xl font-bold text-red-500 block">{availableCount}</Text>
|
||||
<Text className="text-sm text-gray-600 mt-1">可用</Text>
|
||||
</View>
|
||||
|
||||
{/* 已使用优惠券 */}
|
||||
<View
|
||||
className="flex-1 text-center py-3 mx-1 bg-green-50 rounded-lg"
|
||||
onClick={() => handleStatsClick('used')}
|
||||
>
|
||||
<View className="flex justify-center mb-2">
|
||||
<Voucher size="24" className="text-green-500" />
|
||||
</View>
|
||||
<Text className="text-2xl font-bold text-green-500 block">{usedCount}</Text>
|
||||
<Text className="text-sm text-gray-600 mt-1">已使用</Text>
|
||||
</View>
|
||||
|
||||
{/* 已过期优惠券 */}
|
||||
<View
|
||||
className="flex-1 text-center py-3 mx-1 bg-gray-50 rounded-lg"
|
||||
onClick={() => handleStatsClick('expired')}
|
||||
>
|
||||
<View className="flex justify-center mb-2">
|
||||
<Clock size="24" className="text-gray-500" />
|
||||
</View>
|
||||
<Text className="text-2xl font-bold text-gray-500 block">{expiredCount}</Text>
|
||||
<Text className="text-sm text-gray-600 mt-1">已过期</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponStats
|
||||
162
src/components/CouponUsageRecord.tsx
Normal file
162
src/components/CouponUsageRecord.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Tag } from '@nutui/nutui-react-taro'
|
||||
import { Voucher, Clock, Agenda } from '@nutui/icons-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export interface CouponUsageRecordProps {
|
||||
/** 优惠券ID */
|
||||
couponId: number
|
||||
/** 优惠券名称 */
|
||||
couponName: string
|
||||
/** 优惠券类型 */
|
||||
couponType: number
|
||||
/** 优惠券金额 */
|
||||
couponAmount: string
|
||||
/** 使用时间 */
|
||||
usedTime: string
|
||||
/** 订单号 */
|
||||
orderNo?: string
|
||||
/** 订单金额 */
|
||||
orderAmount?: string
|
||||
/** 节省金额 */
|
||||
savedAmount?: string
|
||||
/** 使用状态:1-已使用 2-已过期 */
|
||||
status: 1 | 2
|
||||
/** 点击事件 */
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const CouponUsageRecord: React.FC<CouponUsageRecordProps> = ({
|
||||
couponName,
|
||||
couponType,
|
||||
couponAmount,
|
||||
usedTime,
|
||||
orderNo,
|
||||
orderAmount,
|
||||
savedAmount,
|
||||
status,
|
||||
onClick
|
||||
}) => {
|
||||
// 获取优惠券类型文本
|
||||
const getCouponTypeText = () => {
|
||||
switch (couponType) {
|
||||
case 10: return '满减券'
|
||||
case 20: return '折扣券'
|
||||
case 30: return '免费券'
|
||||
default: return '优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠券金额显示
|
||||
const getCouponAmountDisplay = () => {
|
||||
switch (couponType) {
|
||||
case 10: // 满减券
|
||||
return `¥${couponAmount}`
|
||||
case 20: // 折扣券
|
||||
return `${couponAmount}折`
|
||||
case 30: // 免费券
|
||||
return '免费'
|
||||
default:
|
||||
return `¥${couponAmount}`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = () => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return {
|
||||
text: '已使用',
|
||||
color: 'success' as const,
|
||||
icon: <Voucher size="16" className="text-green-500" />
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
text: '已过期',
|
||||
color: 'danger' as const,
|
||||
icon: <Clock size="16" className="text-red-500" />
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: '未知',
|
||||
color: 'default' as const,
|
||||
icon: <Clock size="16" className="text-gray-500" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo()
|
||||
|
||||
return (
|
||||
<View
|
||||
className="bg-white mx-4 mb-3 rounded-xl p-4 border border-gray-100"
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 头部信息 */}
|
||||
<View className="flex items-center justify-between mb-3">
|
||||
<View className="flex items-center">
|
||||
<View className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<Text className="text-red-500 font-bold text-lg">
|
||||
{getCouponAmountDisplay()}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-900 text-base">{couponName}</Text>
|
||||
<Text className="text-gray-500 text-sm">{getCouponTypeText()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center">
|
||||
{statusInfo.icon}
|
||||
<Tag type={statusInfo.color} className="ml-2">
|
||||
{statusInfo.text}
|
||||
</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 使用详情 */}
|
||||
<View className="bg-gray-50 rounded-lg p-3">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-gray-600 text-sm">使用时间</Text>
|
||||
<Text className="text-gray-900 text-sm">
|
||||
{dayjs(usedTime).format('YYYY-MM-DD HH:mm')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{orderNo && (
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-gray-600 text-sm">订单号</Text>
|
||||
<Text className="text-gray-900 text-sm">{orderNo}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{orderAmount && (
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-gray-600 text-sm">订单金额</Text>
|
||||
<Text className="text-gray-900 text-sm">¥{orderAmount}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{savedAmount && (
|
||||
<View className="flex items-center justify-between">
|
||||
<Text className="text-gray-600 text-sm">节省金额</Text>
|
||||
<Text className="text-red-500 text-sm font-semibold">¥{savedAmount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部操作 */}
|
||||
{orderNo && (
|
||||
<View className="flex justify-end mt-3">
|
||||
<View className="flex items-center text-blue-500 text-sm">
|
||||
<Agenda size="14" className="mr-1" />
|
||||
<Text>查看订单</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponUsageRecord
|
||||
307
src/components/GiftCard.scss
Normal file
307
src/components/GiftCard.scss
Normal file
@@ -0,0 +1,307 @@
|
||||
.gift-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// 主题色彩
|
||||
&.gift-card-gold {
|
||||
.gift-card-header {
|
||||
background: #ffd700;
|
||||
}
|
||||
.use-btn {
|
||||
background: #ffd700;
|
||||
border: none;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-silver {
|
||||
.gift-card-header {
|
||||
background: #c0c0c0;
|
||||
}
|
||||
.use-btn {
|
||||
background: #c0c0c0;
|
||||
border: none;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-bronze {
|
||||
.gift-card-header {
|
||||
background: #cd7f32;
|
||||
}
|
||||
.use-btn {
|
||||
background: #cd7f32;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-blue {
|
||||
.gift-card-header {
|
||||
background: #4a90e2;
|
||||
}
|
||||
.use-btn {
|
||||
background: #4a90e2;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-green {
|
||||
.gift-card-header {
|
||||
background: #5cb85c;
|
||||
}
|
||||
.use-btn {
|
||||
background: #5cb85c;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-purple {
|
||||
.gift-card-header {
|
||||
background: #9b59b6;
|
||||
}
|
||||
.use-btn {
|
||||
background: #9b59b6;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
|
||||
.gift-card-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gift-card-title {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
|
||||
.title-text {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-status {
|
||||
.nut-tag {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #333;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-body {
|
||||
padding: 20px;
|
||||
|
||||
.gift-card-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.gift-image {
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gift-info {
|
||||
flex: 1;
|
||||
|
||||
.gift-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.value-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value-amount {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.gift-code {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 1px dashed #ddd;
|
||||
|
||||
.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;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-time-info {
|
||||
.time-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px 20px;
|
||||
|
||||
.footer-info {
|
||||
.contact-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.contact-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.use-btn {
|
||||
min-width: 80px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
.overlay-badge {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 礼品卡基础样式
|
||||
.gift-card {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
// 使用按钮效果
|
||||
.use-btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.gift-card {
|
||||
.gift-card-body {
|
||||
padding: 16px;
|
||||
|
||||
.gift-card-content {
|
||||
.gift-info {
|
||||
.gift-value {
|
||||
.value-amount {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-footer {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
282
src/components/GiftCard.tsx
Normal file
282
src/components/GiftCard.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
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 dayjs from 'dayjs'
|
||||
import './GiftCard.scss'
|
||||
|
||||
export interface GiftCardProps {
|
||||
/** 礼品卡ID */
|
||||
id: number
|
||||
/** 礼品卡名称 */
|
||||
name: string
|
||||
/** 礼品卡描述 */
|
||||
description?: string
|
||||
/** 礼品卡兑换码 */
|
||||
code?: string
|
||||
/** 商品图片 */
|
||||
goodsImage?: string
|
||||
/** 礼品卡面值 */
|
||||
faceValue?: string
|
||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
type?: number
|
||||
/** 使用状态:0-可用 1-已使用 2-已过期 */
|
||||
useStatus?: number
|
||||
/** 过期时间 */
|
||||
expireTime?: string
|
||||
/** 使用时间 */
|
||||
useTime?: string
|
||||
/** 使用地址 */
|
||||
useLocation?: string
|
||||
/** 客服联系方式 */
|
||||
contactInfo?: string
|
||||
/** 是否显示兑换码 */
|
||||
showCode?: boolean
|
||||
/** 是否显示使用按钮 */
|
||||
showUseBtn?: boolean
|
||||
/** 是否显示详情按钮 */
|
||||
showDetailBtn?: boolean
|
||||
/** 卡片主题色 */
|
||||
theme?: 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple'
|
||||
/** 使用按钮点击事件 */
|
||||
onUse?: () => void
|
||||
/** 详情按钮点击事件 */
|
||||
onDetail?: () => void
|
||||
/** 卡片点击事件 */
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const GiftCard: React.FC<GiftCardProps> = ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
code,
|
||||
goodsImage,
|
||||
faceValue,
|
||||
type = 10,
|
||||
useStatus = 0,
|
||||
expireTime,
|
||||
useTime,
|
||||
useLocation,
|
||||
contactInfo,
|
||||
showCode = false,
|
||||
showUseBtn = false,
|
||||
showDetailBtn = true,
|
||||
theme = 'gold',
|
||||
onUse,
|
||||
onDetail,
|
||||
onClick
|
||||
}) => {
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取使用状态信息
|
||||
const getStatusInfo = () => {
|
||||
switch (useStatus) {
|
||||
case 0:
|
||||
return {
|
||||
text: '可使用',
|
||||
color: 'success' as const,
|
||||
bgColor: 'bg-green-100',
|
||||
textColor: 'text-green-600'
|
||||
}
|
||||
case 1:
|
||||
return {
|
||||
text: '已使用',
|
||||
color: 'warning' as const,
|
||||
bgColor: 'bg-gray-100',
|
||||
textColor: 'text-gray-600'
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
text: '已过期',
|
||||
color: 'danger' as const,
|
||||
bgColor: 'bg-red-100',
|
||||
textColor: 'text-red-600'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: '未知',
|
||||
color: 'default' as const,
|
||||
bgColor: 'bg-gray-100',
|
||||
textColor: 'text-gray-600'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取主题样式类名
|
||||
const getThemeClass = () => {
|
||||
return `gift-card-${theme}`
|
||||
}
|
||||
|
||||
// 格式化过期时间显示
|
||||
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) {
|
||||
return '今天过期'
|
||||
} else if (diffDays <= 7) {
|
||||
return `${diffDays}天后过期`
|
||||
} else {
|
||||
return expire.format('YYYY-MM-DD 过期')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化兑换码显示
|
||||
const formatCode = () => {
|
||||
if (!code) return ''
|
||||
if (!showCode) return code.replace(/(.{4})/g, '$1 ').trim()
|
||||
return code.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo()
|
||||
|
||||
return (
|
||||
<View
|
||||
className={`gift-card ${getThemeClass()} ${useStatus !== 0 ? 'disabled' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 卡片头部 */}
|
||||
<View className="gift-card-header">
|
||||
<View className="gift-card-logo">
|
||||
<Gift size="24" className="text-white" />
|
||||
</View>
|
||||
<View className="gift-card-title">
|
||||
<Text className="title-text">{name}</Text>
|
||||
<Text className="type-text">{getTypeText()}</Text>
|
||||
</View>
|
||||
<View className="gift-card-status">
|
||||
<Tag type={statusInfo.color}>{statusInfo.text}</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 卡片主体 */}
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
{description && (
|
||||
<Text className="gift-description">{description}</Text>
|
||||
)}
|
||||
|
||||
{/* 兑换码 */}
|
||||
{code && (
|
||||
<View className="gift-code">
|
||||
<Text className="code-label">兑换码</Text>
|
||||
<Text className="code-value">{formatCode()}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 时间信息 */}
|
||||
<View className="gift-time-info">
|
||||
{useStatus === 1 && useTime && (
|
||||
<View className="time-item">
|
||||
<Clock size="14" className="text-gray-400" />
|
||||
<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" />
|
||||
<Text className="time-text">使用地址:{useLocation}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 卡片底部操作 */}
|
||||
<View className="gift-card-footer">
|
||||
<View className="footer-info">
|
||||
{contactInfo && (
|
||||
<View className="contact-info">
|
||||
<Phone size="12" className="text-gray-400" />
|
||||
<Text className="contact-text">{contactInfo}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="footer-actions">
|
||||
{showDetailBtn && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDetail?.()
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showUseBtn && useStatus === 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className={`use-btn ${getThemeClass()}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onUse?.()
|
||||
}}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 状态遮罩 */}
|
||||
{useStatus !== 0 && (
|
||||
<View className="gift-card-overlay">
|
||||
<View className="overlay-badge">
|
||||
{statusInfo.text}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCard
|
||||
169
src/components/GiftCardGuide.tsx
Normal file
169
src/components/GiftCardGuide.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Gift, QrCode, Voucher, Service } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface GiftCardGuideProps {
|
||||
/** 是否显示指南 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
|
||||
visible,
|
||||
onClose
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const guideSteps = [
|
||||
{
|
||||
title: '如何获取礼品卡?',
|
||||
icon: <Gift size="24" className="text-yellow-500" />,
|
||||
content: [
|
||||
'1. 通过兑换码兑换礼品卡',
|
||||
'2. 扫描二维码快速兑换',
|
||||
'3. 参与活动获得礼品卡奖励',
|
||||
'4. 朋友赠送的礼品卡'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '如何兑换礼品卡?',
|
||||
icon: <QrCode size="24" className="text-blue-500" />,
|
||||
content: [
|
||||
'1. 点击"兑换"按钮进入兑换页面',
|
||||
'2. 输入礼品卡兑换码或扫码输入',
|
||||
'3. 验证兑换码有效性',
|
||||
'4. 确认兑换,礼品卡添加到账户'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '如何使用礼品卡?',
|
||||
icon: <Voucher size="24" className="text-green-500" />,
|
||||
content: [
|
||||
'1. 选择可用状态的礼品卡',
|
||||
'2. 点击"立即使用"按钮',
|
||||
'3. 填写使用信息(地址、备注等)',
|
||||
'4. 确认使用,完成礼品卡消费'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '礼品卡类型说明',
|
||||
icon: <Gift size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'🎁 实物礼品卡:需到指定地址领取商品',
|
||||
'💻 虚拟礼品卡:自动发放到账户余额',
|
||||
'🛎️ 服务礼品卡:联系客服预约服务',
|
||||
'⏰ 注意查看有效期,过期无法使用'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '常见问题解答',
|
||||
icon: <Service size="24" className="text-red-500" />,
|
||||
content: [
|
||||
'Q: 礼品卡可以转赠他人吗?',
|
||||
'A: 未使用的礼品卡可以通过分享功能转赠',
|
||||
'Q: 礼品卡过期了怎么办?',
|
||||
'A: 过期礼品卡无法使用,请及时关注有效期',
|
||||
'Q: 使用礼品卡后可以退款吗?',
|
||||
'A: 已使用的礼品卡不支持退款操作'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < guideSteps.length - 1) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const currentGuide = guideSteps[currentStep]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable={false}
|
||||
style={{ width: '90%', maxWidth: '400px' }}
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="text-center mb-6">
|
||||
<View className="flex justify-center mb-3">
|
||||
{currentGuide.icon}
|
||||
</View>
|
||||
<Text className="text-xl font-bold text-gray-900">
|
||||
{currentGuide.title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className="mb-6">
|
||||
{currentGuide.content.map((item, index) => (
|
||||
<View key={index} className="mb-3">
|
||||
<Text className="text-gray-700 leading-relaxed">
|
||||
{item}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 进度指示器 */}
|
||||
<View className="flex justify-center mb-6">
|
||||
{guideSteps.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full mx-1 ${
|
||||
index === currentStep ? 'bg-yellow-500' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="flex justify-between">
|
||||
<View className="flex gap-2">
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handleSkip}
|
||||
>
|
||||
跳过
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
>
|
||||
{currentStep === guideSteps.length - 1 ? '完成' : '下一步'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardGuide
|
||||
173
src/components/GiftCardList.tsx
Normal file
173
src/components/GiftCardList.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React from 'react'
|
||||
import { View, ScrollView } from '@tarojs/components'
|
||||
import GiftCard, { GiftCardProps } from './GiftCard'
|
||||
|
||||
export interface GiftCardListProps {
|
||||
/** 礼品卡列表数据 */
|
||||
gifts: GiftCardProps[]
|
||||
/** 列表标题 */
|
||||
title?: string
|
||||
/** 布局方式:vertical-垂直布局 horizontal-水平滚动 grid-网格布局 */
|
||||
layout?: 'vertical' | 'horizontal' | 'grid'
|
||||
/** 网格列数(仅在grid布局时有效) */
|
||||
columns?: number
|
||||
/** 是否显示空状态 */
|
||||
showEmpty?: boolean
|
||||
/** 空状态文案 */
|
||||
emptyText?: string
|
||||
/** 礼品卡点击事件 */
|
||||
onGiftClick?: (gift: GiftCardProps, index: number) => void
|
||||
/** 礼品卡使用事件 */
|
||||
onGiftUse?: (gift: GiftCardProps, index: number) => void
|
||||
/** 礼品卡详情事件 */
|
||||
onGiftDetail?: (gift: GiftCardProps, index: number) => void
|
||||
}
|
||||
|
||||
const GiftCardList: React.FC<GiftCardListProps> = ({
|
||||
gifts = [],
|
||||
title,
|
||||
layout = 'vertical',
|
||||
columns = 2,
|
||||
showEmpty = true,
|
||||
emptyText = '暂无礼品卡',
|
||||
onGiftClick,
|
||||
onGiftUse,
|
||||
onGiftDetail
|
||||
}) => {
|
||||
const handleGiftClick = (gift: GiftCardProps, index: number) => {
|
||||
onGiftClick?.(gift, index)
|
||||
}
|
||||
|
||||
const handleGiftUse = (gift: GiftCardProps, index: number) => {
|
||||
onGiftUse?.(gift, index)
|
||||
}
|
||||
|
||||
const handleGiftDetail = (gift: GiftCardProps, index: number) => {
|
||||
onGiftDetail?.(gift, index)
|
||||
}
|
||||
|
||||
// 垂直布局
|
||||
if (layout === 'vertical') {
|
||||
return (
|
||||
<View className="p-4">
|
||||
{title && (
|
||||
<View className="font-semibold text-gray-800 mb-4 text-lg">{title}</View>
|
||||
)}
|
||||
|
||||
{gifts.length === 0 ? (
|
||||
showEmpty && (
|
||||
<View className="text-center py-16 px-5">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<View className="text-2xl">🎁</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-gray-500 text-base">{emptyText}</View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
gifts.map((gift, index) => (
|
||||
<GiftCard
|
||||
key={gift.id || index}
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 网格布局
|
||||
if (layout === 'grid') {
|
||||
return (
|
||||
<View className="p-4">
|
||||
{title && (
|
||||
<View className="font-semibold text-gray-800 mb-4 text-lg">{title}</View>
|
||||
)}
|
||||
|
||||
{gifts.length === 0 ? (
|
||||
showEmpty && (
|
||||
<View className="text-center py-16 px-5">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<View className="text-2xl">🎁</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-gray-500 text-base">{emptyText}</View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<View
|
||||
className="flex flex-wrap"
|
||||
style={{gap: '12px'}}
|
||||
>
|
||||
{gifts.map((gift, index) => (
|
||||
<View
|
||||
key={gift.id || index}
|
||||
className="w-full"
|
||||
style={{width: `calc(${100/columns}% - ${12*(columns-1)/columns}px)`}}
|
||||
>
|
||||
<GiftCard
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 水平滚动布局
|
||||
return (
|
||||
<View>
|
||||
{title && (
|
||||
<View className="font-semibold text-gray-800 mb-4 pl-4 text-lg">
|
||||
{title}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{gifts.length === 0 ? (
|
||||
showEmpty && (
|
||||
<View className="text-center py-16 px-5">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<View className="text-2xl">🎁</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-gray-500 text-base">{emptyText}</View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<ScrollView
|
||||
scrollX
|
||||
className="flex p-4 gap-3 overflow-x-auto"
|
||||
showScrollbar={false}
|
||||
style={{}}
|
||||
>
|
||||
{gifts.map((gift, index) => (
|
||||
<View
|
||||
key={gift.id || index}
|
||||
className="flex-shrink-0 w-80 mb-0"
|
||||
>
|
||||
<GiftCard
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardList
|
||||
227
src/components/GiftCardShare.tsx
Normal file
227
src/components/GiftCardShare.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Share, Wechat, QQ, Weibo, Link, Close, Gift } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface GiftCardShareProps {
|
||||
/** 是否显示分享弹窗 */
|
||||
visible: boolean
|
||||
/** 礼品卡信息 */
|
||||
giftCard: {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
faceValue: string
|
||||
code?: string
|
||||
description?: string
|
||||
}
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const GiftCardShare: React.FC<GiftCardShareProps> = ({
|
||||
visible,
|
||||
giftCard,
|
||||
onClose
|
||||
}) => {
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (giftCard.type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
}
|
||||
}
|
||||
|
||||
// 生成分享文案
|
||||
const generateShareText = () => {
|
||||
const typeText = getTypeText()
|
||||
const valueText = `¥${giftCard.faceValue}`
|
||||
|
||||
return `🎁 ${giftCard.name}\n💰 面值 ${valueText}\n🏷️ ${typeText}\n${giftCard.description ? `📝 ${giftCard.description}\n` : ''}快来领取这份礼品卡吧!`
|
||||
}
|
||||
|
||||
// 生成分享链接
|
||||
const generateShareUrl = () => {
|
||||
// 这里应该是实际的分享链接,包含礼品卡ID等参数
|
||||
return `https://your-domain.com/gift/share?id=${giftCard.id}`
|
||||
}
|
||||
|
||||
// 微信分享
|
||||
const handleWechatShare = () => {
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
const handleCopyLink = () => {
|
||||
const shareUrl = generateShareUrl()
|
||||
const shareText = generateShareText()
|
||||
const fullText = `${shareText}\n\n${shareUrl}`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: fullText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制兑换码
|
||||
const handleCopyCode = () => {
|
||||
if (!giftCard.code) {
|
||||
Taro.showToast({
|
||||
title: '暂无兑换码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: giftCard.code,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '兑换码已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存图片分享
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
// 这里可以生成礼品卡图片并保存到相册
|
||||
// 实际实现需要canvas绘制礼品卡图片
|
||||
Taro.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const shareOptions = [
|
||||
{
|
||||
icon: <Wechat size="32" className="text-green-500" />,
|
||||
label: '微信好友',
|
||||
onClick: handleWechatShare
|
||||
},
|
||||
{
|
||||
icon: <Link size="32" className="text-blue-500" />,
|
||||
label: '复制链接',
|
||||
onClick: handleCopyLink
|
||||
},
|
||||
{
|
||||
icon: <Gift size="32" className="text-purple-500" />,
|
||||
label: '复制兑换码',
|
||||
onClick: handleCopyCode,
|
||||
disabled: !giftCard.code
|
||||
},
|
||||
{
|
||||
icon: <Share size="32" className="text-orange-500" />,
|
||||
label: '保存图片',
|
||||
onClick: handleSaveImage
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
style={{ height: 'auto' }}
|
||||
round
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between mb-6">
|
||||
<Text className="text-lg font-semibold">分享礼品卡</Text>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-500" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡预览 */}
|
||||
<View className="rounded-xl p-4 mb-6 text-white" style={{backgroundColor: '#fbbf24'}}>
|
||||
<Text className="text-xl font-bold mb-2">{giftCard.name}</Text>
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold">¥{giftCard.faceValue}</Text>
|
||||
<Text className="text-sm opacity-90">{getTypeText()}</Text>
|
||||
</View>
|
||||
<Gift size="24" />
|
||||
</View>
|
||||
{giftCard.code && (
|
||||
<View className="mt-3 p-2 bg-white bg-opacity-20 rounded">
|
||||
<Text className="text-xs opacity-80">兑换码</Text>
|
||||
<Text className="font-mono font-bold">{giftCard.code}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 分享选项 */}
|
||||
<View className="flex justify-between mb-4" style={{gap: '16px'}}>
|
||||
{shareOptions.map((option, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`flex-1 flex flex-col items-center py-4 bg-gray-50 rounded-lg ${
|
||||
option.disabled ? 'opacity-50' : ''
|
||||
}`}
|
||||
onClick={option.disabled ? undefined : option.onClick}
|
||||
>
|
||||
<View className="mb-2">{option.icon}</View>
|
||||
<Text className="text-sm text-gray-700">{option.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 分享文案预览 */}
|
||||
<View className="bg-gray-50 rounded-lg p-3">
|
||||
<Text className="text-xs text-gray-500 mb-2">分享文案预览:</Text>
|
||||
<Text className="text-sm text-gray-700 leading-relaxed">
|
||||
{generateShareText()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardShare
|
||||
72
src/components/GiftCardStats.tsx
Normal file
72
src/components/GiftCardStats.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Gift, Voucher, Clock } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface GiftCardStatsProps {
|
||||
/** 可用礼品卡数量 */
|
||||
availableCount: number
|
||||
/** 已使用礼品卡数量 */
|
||||
usedCount: number
|
||||
/** 已过期礼品卡数量 */
|
||||
expiredCount: number
|
||||
/** 礼品卡总价值 */
|
||||
totalValue?: number
|
||||
/** 点击统计项的回调 */
|
||||
onStatsClick?: (type: 'available' | 'used' | 'expired' | 'total') => void
|
||||
}
|
||||
|
||||
const GiftCardStats: React.FC<GiftCardStatsProps> = ({
|
||||
availableCount,
|
||||
usedCount,
|
||||
expiredCount,
|
||||
onStatsClick
|
||||
}) => {
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||
onStatsClick?.(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-white mx-4 my-3 rounded-xl p-3 shadow-sm">
|
||||
{/* 紧凑的统计卡片 - 2x2 网格 */}
|
||||
<View className="grid grid-cols-3 gap-2">
|
||||
{/* 可用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-2 bg-yellow-50 rounded-lg border border-yellow-200"
|
||||
onClick={() => handleStatsClick('available')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Gift size="16" className="text-yellow-600 mr-1" />
|
||||
<Text className="text-sm text-gray-600">可用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-yellow-600">{availableCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已使用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-2 bg-green-50 rounded-lg border border-green-200"
|
||||
onClick={() => handleStatsClick('used')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Voucher size="16" className="text-green-600 mr-1" />
|
||||
<Text className="text-sm text-gray-600">已使用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-green-600">{usedCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已过期礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-2 bg-gray-50 rounded-lg border border-gray-200"
|
||||
onClick={() => handleStatsClick('expired')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Clock size="16" className="text-gray-500 mr-1" />
|
||||
<Text className="text-sm text-gray-600">已过期</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-gray-500">{expiredCount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardStats
|
||||
87
src/components/GiftCardStatsMax.tsx
Normal file
87
src/components/GiftCardStatsMax.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Gift, Voucher, Clock, Star } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface GiftCardStatsProps {
|
||||
/** 可用礼品卡数量 */
|
||||
availableCount: number
|
||||
/** 已使用礼品卡数量 */
|
||||
usedCount: number
|
||||
/** 已过期礼品卡数量 */
|
||||
expiredCount: number
|
||||
/** 礼品卡总价值 */
|
||||
totalValue?: number
|
||||
/** 点击统计项的回调 */
|
||||
onStatsClick?: (type: 'available' | 'used' | 'expired' | 'total') => void
|
||||
}
|
||||
|
||||
const GiftCardStats: React.FC<GiftCardStatsProps> = ({
|
||||
availableCount,
|
||||
usedCount,
|
||||
expiredCount,
|
||||
totalValue,
|
||||
onStatsClick
|
||||
}) => {
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||
onStatsClick?.(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-white mx-4 my-3 rounded-xl p-3 shadow-sm">
|
||||
{/* 紧凑的统计卡片 - 2x2 网格 */}
|
||||
<View className="grid grid-cols-2 gap-2">
|
||||
{/* 可用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-yellow-50 rounded-lg border border-yellow-200"
|
||||
onClick={() => handleStatsClick('available')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Gift size="20" className="text-yellow-600 mr-2" />
|
||||
<Text className="text-sm text-gray-600">可用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-yellow-600">{availableCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已使用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200"
|
||||
onClick={() => handleStatsClick('used')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Voucher size="20" className="text-green-600 mr-2" />
|
||||
<Text className="text-sm text-gray-600">已使用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-green-600">{usedCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已过期礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200"
|
||||
onClick={() => handleStatsClick('expired')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Clock size="20" className="text-gray-500 mr-2" />
|
||||
<Text className="text-sm text-gray-600">已过期</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-gray-500">{expiredCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 总价值 */}
|
||||
{totalValue !== undefined && (
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-purple-50 rounded-lg border border-purple-200"
|
||||
onClick={() => handleStatsClick('total')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Star size="20" className="text-purple-600 mr-2" />
|
||||
<Text className="text-sm text-gray-600">总价值</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-purple-600">¥{totalValue}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardStats
|
||||
Reference in New Issue
Block a user