Browse Source
- 添加优惠券选择功能 - 增加商品数量选择 - 完善订单信息展示 - 优化支付流程 - 添加错误状态和加载状态处理 - 新增 OrderConfirmSkeleton 组件用于加载骨架屏master
10 changed files with 1313 additions and 85 deletions
@ -0,0 +1,253 @@ |
|||
.coupon-card { |
|||
position: relative; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100px; |
|||
margin-bottom: 12px; |
|||
border-radius: 8px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
|||
background: #fff; |
|||
|
|||
&--disabled { |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
// 主题颜色 |
|||
&--red { |
|||
.coupon-card__left { |
|||
background: linear-gradient(135deg, #ff6b6b, #ff5252); |
|||
} |
|||
.coupon-card__btn--receive, |
|||
.coupon-card__btn--use { |
|||
background: linear-gradient(135deg, #ff6b6b, #ff5252); |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
&--orange { |
|||
.coupon-card__left { |
|||
background: linear-gradient(135deg, #ffa726, #ff9800); |
|||
} |
|||
.coupon-card__btn--receive, |
|||
.coupon-card__btn--use { |
|||
background: linear-gradient(135deg, #ffa726, #ff9800); |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
&--blue { |
|||
.coupon-card__left { |
|||
background: linear-gradient(135deg, #42a5f5, #2196f3); |
|||
} |
|||
.coupon-card__btn--receive, |
|||
.coupon-card__btn--use { |
|||
background: linear-gradient(135deg, #42a5f5, #2196f3); |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
&--purple { |
|||
.coupon-card__left { |
|||
background: linear-gradient(135deg, #ab47bc, #9c27b0); |
|||
} |
|||
.coupon-card__btn--receive, |
|||
.coupon-card__btn--use { |
|||
background: linear-gradient(135deg, #ab47bc, #9c27b0); |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
&--green { |
|||
.coupon-card__left { |
|||
background: linear-gradient(135deg, #66bb6a, #4caf50); |
|||
} |
|||
.coupon-card__btn--receive, |
|||
.coupon-card__btn--use { |
|||
background: linear-gradient(135deg, #66bb6a, #4caf50); |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
// 左侧金额区域 |
|||
&__left { |
|||
flex: 0 0 100px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
color: #fff; |
|||
position: relative; |
|||
} |
|||
|
|||
&__amount { |
|||
display: flex; |
|||
align-items: baseline; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
&__currency { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
margin-right: 1px; |
|||
} |
|||
|
|||
&__value { |
|||
font-size: 28px; |
|||
font-weight: bold; |
|||
line-height: 1; |
|||
} |
|||
|
|||
&__condition { |
|||
font-size: 11px; |
|||
opacity: 0.9; |
|||
margin-top: 2px; |
|||
} |
|||
|
|||
// 分割线区域 |
|||
&__divider { |
|||
flex: 0 0 2px; |
|||
position: relative; |
|||
background: #f0f0f0; |
|||
} |
|||
|
|||
&__divider-line { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: repeating-linear-gradient( |
|||
to bottom, |
|||
transparent 0px, |
|||
transparent 4px, |
|||
#ddd 4px, |
|||
#ddd 8px |
|||
); |
|||
} |
|||
|
|||
&__circle { |
|||
position: absolute; |
|||
width: 16px; |
|||
height: 16px; |
|||
background: #f5f5f5; |
|||
border-radius: 50%; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
|
|||
&--top { |
|||
top: -8px; |
|||
} |
|||
|
|||
&--bottom { |
|||
bottom: -8px; |
|||
} |
|||
} |
|||
|
|||
// 右侧信息区域 |
|||
&__right { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: space-between; |
|||
padding: 12px; |
|||
} |
|||
|
|||
&__info { |
|||
flex: 1; |
|||
} |
|||
|
|||
&__title { |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
&__validity { |
|||
font-size: 11px; |
|||
color: #999; |
|||
} |
|||
|
|||
&__action { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
} |
|||
|
|||
&__btn { |
|||
min-width: 50px; |
|||
height: 24px; |
|||
border-radius: 12px; |
|||
font-size: 11px; |
|||
border: none; |
|||
|
|||
&--receive, |
|||
&--use { |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
&__status { |
|||
font-size: 12px; |
|||
color: #999; |
|||
padding: 4px 8px; |
|||
} |
|||
|
|||
// 状态遮罩 |
|||
&__mask { |
|||
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: 1; |
|||
} |
|||
|
|||
&__mask-text { |
|||
background: rgba(0, 0, 0, 0.6); |
|||
color: #fff; |
|||
padding: 4px 12px; |
|||
border-radius: 12px; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
} |
|||
} |
|||
|
|||
// 优惠券列表容器 |
|||
.coupon-list { |
|||
padding: 16px; |
|||
|
|||
&__title { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
&__empty { |
|||
text-align: center; |
|||
padding: 40px 20px; |
|||
color: #999; |
|||
font-size: 14px; |
|||
} |
|||
} |
|||
|
|||
// 优惠券横向滚动容器 |
|||
.coupon-scroll { |
|||
display: flex; |
|||
padding: 16px; |
|||
gap: 8px; |
|||
overflow-x: auto; |
|||
-webkit-overflow-scrolling: touch; |
|||
|
|||
&::-webkit-scrollbar { |
|||
display: none; |
|||
} |
|||
|
|||
.coupon-card { |
|||
flex: 0 0 240px; |
|||
margin-bottom: 0; |
|||
} |
|||
} |
@ -0,0 +1,168 @@ |
|||
import React from 'react' |
|||
import { View, Text } from '@tarojs/components' |
|||
import { Button } from '@nutui/nutui-react-taro' |
|||
import './CouponCard.scss' |
|||
|
|||
export interface CouponCardProps { |
|||
/** 优惠券金额 */ |
|||
amount: number |
|||
/** 最低消费金额 */ |
|||
minAmount?: number |
|||
/** 优惠券类型:1-满减券 2-折扣券 3-免费券 */ |
|||
type?: 1 | 2 | 3 |
|||
/** 优惠券状态:0-未使用 1-已使用 2-已过期 */ |
|||
status?: 0 | 1 | 2 |
|||
/** 优惠券标题 */ |
|||
title?: string |
|||
/** 有效期开始时间 */ |
|||
startTime?: string |
|||
/** 有效期结束时间 */ |
|||
endTime?: string |
|||
/** 是否显示领取按钮 */ |
|||
showReceiveBtn?: boolean |
|||
/** 是否显示使用按钮 */ |
|||
showUseBtn?: boolean |
|||
/** 领取按钮点击事件 */ |
|||
onReceive?: () => void |
|||
/** 使用按钮点击事件 */ |
|||
onUse?: () => void |
|||
/** 优惠券样式主题:red | orange | blue | purple | green */ |
|||
theme?: 'red' | 'orange' | 'blue' | 'purple' | 'green' |
|||
} |
|||
|
|||
const CouponCard: React.FC<CouponCardProps> = ({ |
|||
amount, |
|||
minAmount, |
|||
type = 1, |
|||
status = 0, |
|||
title, |
|||
startTime, |
|||
endTime, |
|||
showReceiveBtn = false, |
|||
showUseBtn = false, |
|||
onReceive, |
|||
onUse, |
|||
theme = 'red' |
|||
}) => { |
|||
// 格式化优惠券金额显示
|
|||
const formatAmount = () => { |
|||
switch (type) { |
|||
case 1: // 满减券
|
|||
return `¥${amount}` |
|||
case 2: // 折扣券
|
|||
return `${amount}折` |
|||
case 3: // 免费券
|
|||
return '免费' |
|||
default: |
|||
return `¥${amount}` |
|||
} |
|||
} |
|||
|
|||
// 获取优惠券状态文本
|
|||
const getStatusText = () => { |
|||
switch (status) { |
|||
case 0: |
|||
return '未使用' |
|||
case 1: |
|||
return '已使用' |
|||
case 2: |
|||
return '已过期' |
|||
default: |
|||
return '未使用' |
|||
} |
|||
} |
|||
|
|||
// 获取使用条件文本
|
|||
const getConditionText = () => { |
|||
if (type === 3) return '无门槛' |
|||
if (minAmount && minAmount > 0) { |
|||
return `满${minAmount}可用` |
|||
} |
|||
return '无门槛' |
|||
} |
|||
|
|||
// 格式化日期
|
|||
const formatDate = (dateStr?: string) => { |
|||
if (!dateStr) return '' |
|||
const date = new Date(dateStr) |
|||
return `${date.getMonth() + 1}.${date.getDate()}` |
|||
} |
|||
|
|||
// 获取有效期文本
|
|||
const getValidityText = () => { |
|||
if (startTime && endTime) { |
|||
return `${formatDate(startTime)}-${formatDate(endTime)}` |
|||
} |
|||
return '' |
|||
} |
|||
|
|||
return ( |
|||
<View className={`coupon-card coupon-card--${theme} ${status !== 0 ? 'coupon-card--disabled' : ''}`}> |
|||
{/* 左侧金额区域 */} |
|||
<View className="coupon-card__left"> |
|||
<View className="coupon-card__amount"> |
|||
<Text className="coupon-card__currency">¥</Text> |
|||
<Text className="coupon-card__value">{amount}</Text> |
|||
</View> |
|||
<View className="coupon-card__condition"> |
|||
{getConditionText()} |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 中间分割线 */} |
|||
<View className="coupon-card__divider"> |
|||
<View className="coupon-card__divider-line"></View> |
|||
<View className="coupon-card__circle coupon-card__circle--top"></View> |
|||
<View className="coupon-card__circle coupon-card__circle--bottom"></View> |
|||
</View> |
|||
|
|||
{/* 右侧信息区域 */} |
|||
<View className="coupon-card__right"> |
|||
<View className="coupon-card__info"> |
|||
<View className="coupon-card__title"> |
|||
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')} |
|||
</View> |
|||
<View className="coupon-card__validity"> |
|||
有效期:{getValidityText()} |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 按钮区域 */} |
|||
<View className="coupon-card__action"> |
|||
{showReceiveBtn && status === 0 && ( |
|||
<Button |
|||
className="coupon-card__btn coupon-card__btn--receive" |
|||
size="small" |
|||
onClick={onReceive} |
|||
> |
|||
领取 |
|||
</Button> |
|||
)} |
|||
{showUseBtn && status === 0 && ( |
|||
<Button |
|||
className="coupon-card__btn coupon-card__btn--use" |
|||
size="small" |
|||
onClick={onUse} |
|||
> |
|||
使用 |
|||
</Button> |
|||
)} |
|||
{status !== 0 && ( |
|||
<View className="coupon-card__status"> |
|||
{getStatusText()} |
|||
</View> |
|||
)} |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 状态遮罩 */} |
|||
{status !== 0 && ( |
|||
<View className="coupon-card__mask"> |
|||
<Text className="coupon-card__mask-text">{getStatusText()}</Text> |
|||
</View> |
|||
)} |
|||
</View> |
|||
) |
|||
} |
|||
|
|||
export default CouponCard |
@ -0,0 +1,96 @@ |
|||
import React from 'react' |
|||
import { View, ScrollView } from '@tarojs/components' |
|||
import CouponCard, { CouponCardProps } from './CouponCard' |
|||
import './CouponCard.scss' |
|||
|
|||
export interface CouponListProps { |
|||
/** 优惠券列表数据 */ |
|||
coupons: CouponCardProps[] |
|||
/** 列表标题 */ |
|||
title?: string |
|||
/** 布局方式:vertical-垂直布局 horizontal-水平滚动 */ |
|||
layout?: 'vertical' | 'horizontal' |
|||
/** 是否显示空状态 */ |
|||
showEmpty?: boolean |
|||
/** 空状态文案 */ |
|||
emptyText?: string |
|||
/** 优惠券点击事件 */ |
|||
onCouponClick?: (coupon: CouponCardProps, index: number) => void |
|||
} |
|||
|
|||
const CouponList: React.FC<CouponListProps> = ({ |
|||
coupons = [], |
|||
title, |
|||
layout = 'vertical', |
|||
showEmpty = true, |
|||
emptyText = '暂无优惠券', |
|||
onCouponClick |
|||
}) => { |
|||
const handleCouponClick = (coupon: CouponCardProps, index: number) => { |
|||
onCouponClick?.(coupon, index) |
|||
} |
|||
|
|||
// 垂直布局
|
|||
if (layout === 'vertical') { |
|||
return ( |
|||
<View className="coupon-list"> |
|||
{title && ( |
|||
<View className="coupon-list__title">{title}</View> |
|||
)} |
|||
|
|||
{coupons.length === 0 ? ( |
|||
showEmpty && ( |
|||
<View className="coupon-list__empty"> |
|||
{emptyText} |
|||
</View> |
|||
) |
|||
) : ( |
|||
coupons.map((coupon, index) => ( |
|||
<View |
|||
key={index} |
|||
onClick={() => handleCouponClick(coupon, index)} |
|||
> |
|||
<CouponCard {...coupon} /> |
|||
</View> |
|||
)) |
|||
)} |
|||
</View> |
|||
) |
|||
} |
|||
|
|||
// 水平滚动布局
|
|||
return ( |
|||
<View> |
|||
{title && ( |
|||
<View className="coupon-list__title" style={{ paddingLeft: '16px' }}> |
|||
{title} |
|||
</View> |
|||
)} |
|||
|
|||
{coupons.length === 0 ? ( |
|||
showEmpty && ( |
|||
<View className="coupon-list__empty"> |
|||
{emptyText} |
|||
</View> |
|||
) |
|||
) : ( |
|||
<ScrollView |
|||
scrollX |
|||
className="coupon-scroll" |
|||
showScrollbar={false} |
|||
> |
|||
{coupons.map((coupon, index) => ( |
|||
<View |
|||
key={index} |
|||
onClick={() => handleCouponClick(coupon, index)} |
|||
> |
|||
<CouponCard {...coupon} /> |
|||
</View> |
|||
))} |
|||
</ScrollView> |
|||
)} |
|||
</View> |
|||
) |
|||
} |
|||
|
|||
export default CouponList |
@ -0,0 +1,78 @@ |
|||
.order-confirm-skeleton { |
|||
padding: 0; |
|||
background: #f5f5f5; |
|||
|
|||
.skeleton-section { |
|||
background: #fff; |
|||
margin-bottom: 8px; |
|||
padding: 16px; |
|||
} |
|||
|
|||
.skeleton-address { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
|
|||
&-content { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
} |
|||
|
|||
.skeleton-goods { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
|
|||
&-content { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
&-price { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
} |
|||
} |
|||
|
|||
.skeleton-payment { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.skeleton-price-item { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 12px; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.skeleton-remark { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.skeleton-bottom { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background: #fff; |
|||
padding: 16px; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); |
|||
} |
|||
} |
@ -0,0 +1,80 @@ |
|||
import React from 'react' |
|||
import { View } from '@tarojs/components' |
|||
import { Skeleton } from '@nutui/nutui-react-taro' |
|||
import './OrderConfirmSkeleton.scss' |
|||
|
|||
const OrderConfirmSkeleton: React.FC = () => { |
|||
return ( |
|||
<View className="order-confirm-skeleton"> |
|||
{/* 收货地址骨架 */} |
|||
<View className="skeleton-section"> |
|||
<View className="skeleton-address"> |
|||
<Skeleton width="20px" height="20px" animated /> |
|||
<View className="skeleton-address-content"> |
|||
<Skeleton width="200px" height="16px" animated /> |
|||
<Skeleton width="120px" height="14px" animated /> |
|||
</View> |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 商品信息骨架 */} |
|||
<View className="skeleton-section"> |
|||
<View className="skeleton-goods"> |
|||
<Skeleton width="80px" height="80px" animated /> |
|||
<View className="skeleton-goods-content"> |
|||
<Skeleton width="180px" height="16px" animated /> |
|||
<Skeleton width="60px" height="14px" animated /> |
|||
<View className="skeleton-goods-price"> |
|||
<Skeleton width="80px" height="16px" animated /> |
|||
<Skeleton width="40px" height="14px" animated /> |
|||
</View> |
|||
</View> |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 支付方式骨架 */} |
|||
<View className="skeleton-section"> |
|||
<View className="skeleton-payment"> |
|||
<Skeleton width="60px" height="16px" animated /> |
|||
<Skeleton width="80px" height="16px" animated /> |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 价格明细骨架 */} |
|||
<View className="skeleton-section"> |
|||
<View className="skeleton-price-item"> |
|||
<Skeleton width="100px" height="16px" animated /> |
|||
<Skeleton width="60px" height="16px" animated /> |
|||
</View> |
|||
<View className="skeleton-price-item"> |
|||
<Skeleton width="60px" height="16px" animated /> |
|||
<Skeleton width="80px" height="16px" animated /> |
|||
</View> |
|||
<View className="skeleton-price-item"> |
|||
<Skeleton width="60px" height="16px" animated /> |
|||
<Skeleton width="60px" height="16px" animated /> |
|||
</View> |
|||
<View className="skeleton-price-item"> |
|||
<Skeleton width="120px" height="18px" animated /> |
|||
<Skeleton width="80px" height="18px" animated /> |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 订单备注骨架 */} |
|||
<View className="skeleton-section"> |
|||
<View className="skeleton-remark"> |
|||
<Skeleton width="60px" height="16px" animated /> |
|||
<Skeleton width="200px" height="32px" animated /> |
|||
</View> |
|||
</View> |
|||
|
|||
{/* 底部按钮骨架 */} |
|||
<View className="skeleton-bottom"> |
|||
<Skeleton width="120px" height="20px" animated /> |
|||
<Skeleton width="100px" height="40px" animated /> |
|||
</View> |
|||
</View> |
|||
) |
|||
} |
|||
|
|||
export default OrderConfirmSkeleton |
@ -0,0 +1,121 @@ |
|||
.quantity-selector { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
gap: 4px; |
|||
|
|||
&__controls { |
|||
display: flex; |
|||
align-items: center; |
|||
border: 1px solid #e5e5e5; |
|||
border-radius: 4px; |
|||
overflow: hidden; |
|||
background: #fff; |
|||
} |
|||
|
|||
&__btn { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border: none; |
|||
background: #f8f8f8; |
|||
color: #666; |
|||
transition: all 0.2s ease; |
|||
|
|||
&:active { |
|||
background: #e5e5e5; |
|||
} |
|||
|
|||
&--disabled { |
|||
background: #f5f5f5 !important; |
|||
color: #ccc !important; |
|||
cursor: not-allowed; |
|||
} |
|||
} |
|||
|
|||
&__input { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: #fff; |
|||
border-left: 1px solid #e5e5e5; |
|||
border-right: 1px solid #e5e5e5; |
|||
} |
|||
|
|||
&__value { |
|||
font-size: 14px; |
|||
color: #333; |
|||
font-weight: 500; |
|||
text-align: center; |
|||
} |
|||
|
|||
&__stock { |
|||
margin-top: 2px; |
|||
} |
|||
|
|||
&__stock-text { |
|||
font-size: 12px; |
|||
color: #999; |
|||
} |
|||
|
|||
// 尺寸变体 |
|||
&--small { |
|||
.quantity-selector__controls { |
|||
height: 24px; |
|||
} |
|||
|
|||
.quantity-selector__btn { |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
|
|||
.quantity-selector__input { |
|||
width: 32px; |
|||
height: 24px; |
|||
} |
|||
|
|||
.quantity-selector__value { |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
|
|||
&--medium { |
|||
.quantity-selector__controls { |
|||
height: 32px; |
|||
} |
|||
|
|||
.quantity-selector__btn { |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
|
|||
.quantity-selector__input { |
|||
width: 40px; |
|||
height: 32px; |
|||
} |
|||
|
|||
.quantity-selector__value { |
|||
font-size: 14px; |
|||
} |
|||
} |
|||
|
|||
&--large { |
|||
.quantity-selector__controls { |
|||
height: 40px; |
|||
} |
|||
|
|||
.quantity-selector__btn { |
|||
width: 40px; |
|||
height: 40px; |
|||
} |
|||
|
|||
.quantity-selector__input { |
|||
width: 48px; |
|||
height: 40px; |
|||
} |
|||
|
|||
.quantity-selector__value { |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,88 @@ |
|||
import React from 'react' |
|||
import { View, Text } from '@tarojs/components' |
|||
import { Button } from '@nutui/nutui-react-taro' |
|||
import { Minus, Plus } from '@nutui/icons-react-taro' |
|||
import './QuantitySelector.scss' |
|||
|
|||
export interface QuantitySelectorProps { |
|||
/** 当前数量 */ |
|||
value: number |
|||
/** 最小数量 */ |
|||
min?: number |
|||
/** 最大数量(库存) */ |
|||
max?: number |
|||
/** 是否禁用 */ |
|||
disabled?: boolean |
|||
/** 数量变化回调 */ |
|||
onChange?: (value: number) => void |
|||
/** 尺寸 */ |
|||
size?: 'small' | 'medium' | 'large' |
|||
/** 是否显示库存提示 */ |
|||
showStock?: boolean |
|||
/** 库存数量 */ |
|||
stock?: number |
|||
} |
|||
|
|||
const QuantitySelector: React.FC<QuantitySelectorProps> = ({ |
|||
value, |
|||
min = 1, |
|||
max = 999, |
|||
disabled = false, |
|||
onChange, |
|||
size = 'medium', |
|||
showStock = false, |
|||
stock |
|||
}) => { |
|||
const handleDecrease = () => { |
|||
if (disabled || value <= min) return |
|||
const newValue = value - 1 |
|||
onChange?.(newValue) |
|||
} |
|||
|
|||
const handleIncrease = () => { |
|||
if (disabled || value >= max) return |
|||
const newValue = value + 1 |
|||
onChange?.(newValue) |
|||
} |
|||
|
|||
const canDecrease = !disabled && value > min |
|||
const canIncrease = !disabled && value < max |
|||
|
|||
return ( |
|||
<View className={`quantity-selector quantity-selector--${size}`}> |
|||
<View className="quantity-selector__controls"> |
|||
<Button |
|||
className={`quantity-selector__btn quantity-selector__btn--minus ${!canDecrease ? 'quantity-selector__btn--disabled' : ''}`} |
|||
size="small" |
|||
onClick={handleDecrease} |
|||
disabled={!canDecrease} |
|||
> |
|||
<Minus size={size === 'small' ? 12 : size === 'large' ? 16 : 14} /> |
|||
</Button> |
|||
|
|||
<View className="quantity-selector__input"> |
|||
<Text className="quantity-selector__value">{value}</Text> |
|||
</View> |
|||
|
|||
<Button |
|||
className={`quantity-selector__btn quantity-selector__btn--plus ${!canIncrease ? 'quantity-selector__btn--disabled' : ''}`} |
|||
size="small" |
|||
onClick={handleIncrease} |
|||
disabled={!canIncrease} |
|||
> |
|||
<Plus size={size === 'small' ? 12 : size === 'large' ? 16 : 14} /> |
|||
</Button> |
|||
</View> |
|||
|
|||
{showStock && stock !== undefined && ( |
|||
<View className="quantity-selector__stock"> |
|||
<Text className="quantity-selector__stock-text"> |
|||
库存 {stock} 件 |
|||
</Text> |
|||
</View> |
|||
)} |
|||
</View> |
|||
) |
|||
} |
|||
|
|||
export default QuantitySelector |
Loading…
Reference in new issue