forked from gxwebsoft/mp-10550
feat(orderConfirm): 优化订单确认页面功能和样式
- 添加优惠券选择功能 - 增加商品数量选择 - 完善订单信息展示 - 优化支付流程 - 添加错误状态和加载状态处理 - 新增 OrderConfirmSkeleton 组件用于加载骨架屏
This commit is contained in:
253
src/components/CouponCard.scss
Normal file
253
src/components/CouponCard.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
168
src/components/CouponCard.tsx
Normal file
168
src/components/CouponCard.tsx
Normal file
@@ -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
|
||||
96
src/components/CouponList.tsx
Normal file
96
src/components/CouponList.tsx
Normal file
@@ -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
|
||||
78
src/components/OrderConfirmSkeleton.scss
Normal file
78
src/components/OrderConfirmSkeleton.scss
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
80
src/components/OrderConfirmSkeleton.tsx
Normal file
80
src/components/OrderConfirmSkeleton.tsx
Normal file
@@ -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
|
||||
121
src/components/QuantitySelector.scss
Normal file
121
src/components/QuantitySelector.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/components/QuantitySelector.tsx
Normal file
88
src/components/QuantitySelector.tsx
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user