Просмотр исходного кода

💄 style(goods-detail): 重构商品详情页底部操作栏样式与交互

- 重构底部操作栏布局,采用垂直排列,优化视觉层次
- 重新设计数量选择器,使用输入框替代纯文本显示,支持直接输入
- 优化按钮样式,添加渐变背景、阴影和点击反馈效果
- 为按钮添加禁用状态样式,提升用户体验

✨ feat(goods-detail): 增强商品数量选择功能

- 新增数量输入验证逻辑,支持键盘直接输入数量
- 添加库存和最大购买数量限制验证(最多999件)
- 实现输入框失焦自动校正功能(空输入默认为1)
- 优化增减按钮交互,添加库存不足提示

🐛 fix(goods-detail): 修复数量处理逻辑

- 修复数量为0时的处理逻辑,确保添加到购物车和立即购买时使用正确数量
- 统一数量处理逻辑,避免因输入框空值导致的错误
yourname 1 месяц назад
Родитель
Сommit
30e34d1a74
2 измененных файлов с 218 добавлено и 70 удалено
  1. 88 40
      mini/src/pages/goods-detail/index.css
  2. 130 30
      mini/src/pages/goods-detail/index.tsx

+ 88 - 40
mini/src/pages/goods-detail/index.css

@@ -278,82 +278,129 @@
   bottom: 0;
   bottom: 0;
   left: 0;
   left: 0;
   right: 0;
   right: 0;
-  height: 120rpx;
   background: white;
   background: white;
-  border-top: 1rpx solid #e8e8e8;
+  
   display: flex;
   display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 24rpx;
-}
-
-.action-left {
-  flex: 1;
+  flex-direction: column;
+  padding: 24rpx 32rpx 32rpx;
+  box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.1);
+  z-index: 100;
 }
 }
 
 
-.quantity-selector {
+.quantity-section {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+  justify-content: space-between;
+  margin-bottom: 24rpx;
+  padding-bottom: 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
 }
 }
 
 
 .quantity-label {
 .quantity-label {
-  font-size: 24rpx;
-  color: #666;
-  margin-right: 16rpx;
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 600;
+  line-height: 64rpx;
+  height: 64rpx;
+  display: flex;
+  align-items: center;
 }
 }
 
 
 .quantity-controls {
 .quantity-controls {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  border: 1rpx solid #e8e8e8;
-  border-radius: 4rpx;
+}
+
+.button-section {
+  display: flex;
+  gap: 24rpx;
 }
 }
 
 
 .quantity-btn {
 .quantity-btn {
-  width: 48rpx;
-  height: 48rpx;
+  width: 64rpx;
+  height: 64rpx;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  background: #f8f8f8;
+  background: transparent;
   border: none;
   border: none;
-  color: #333;
-  font-size: 24rpx;
+  color: #ff9500;
+  font-size: 32rpx;
+  font-weight: bold;
+  padding: 0;
 }
 }
 
 
-.quantity-value {
-  width: 60rpx;
-  height: 48rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 24rpx;
+.quantity-btn:active {
+  opacity: 0.7;
+}
+
+.quantity-input {
+  width: 300rpx;
+  height: 64rpx;
+  border: none;
+  font-size: 32rpx;
   color: #333;
   color: #333;
-  border-left: 1rpx solid #e8e8e8;
-  border-right: 1rpx solid #e8e8e8;
+  font-weight: 600;
+  text-align: center;
+  background: transparent;
+  padding: 0;
+  margin: 0;
 }
 }
 
 
-.action-right {
-  display: flex;
-  gap: 16rpx;
+.quantity-input:focus {
+  outline: none;
 }
 }
 
 
 .add-cart-btn {
 .add-cart-btn {
-  background: #ff9500;
+  flex: 1;
+  background: linear-gradient(135deg, #ff9500, #ff7b00);
   color: white;
   color: white;
   border: none;
   border: none;
-  padding: 16rpx 32rpx;
-  border-radius: 24rpx;
-  font-size: 28rpx;
+  padding: 24rpx 0;
+  border-radius: 40rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  box-shadow: 0 6rpx 16rpx rgba(255, 149, 0, 0.4);
+  transition: all 0.2s ease;
+  text-align: center;
+}
+
+.add-cart-btn:active {
+  transform: translateY(2rpx);
+  box-shadow: 0 3rpx 10rpx rgba(255, 149, 0, 0.4);
+}
+
+.add-cart-btn:disabled {
+  background: #cccccc;
+  color: #999;
+  box-shadow: none;
+  transform: none;
 }
 }
 
 
 .buy-now-btn {
 .buy-now-btn {
-  background: #fa4126;
+  flex: 1;
+  background: linear-gradient(135deg, #fa4126, #e6391e);
   color: white;
   color: white;
   border: none;
   border: none;
-  padding: 16rpx 32rpx;
-  border-radius: 24rpx;
-  font-size: 28rpx;
+  padding: 24rpx 0;
+  border-radius: 40rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  box-shadow: 0 6rpx 16rpx rgba(250, 65, 38, 0.4);
+  transition: all 0.2s ease;
+  text-align: center;
+}
+
+.buy-now-btn:active {
+  transform: translateY(2rpx);
+  box-shadow: 0 3rpx 10rpx rgba(250, 65, 38, 0.4);
+}
+
+.buy-now-btn:disabled {
+  background: #cccccc;
+  color: #999;
+  box-shadow: none;
+  transform: none;
 }
 }
 
 
 /* 规格选择弹窗 */
 /* 规格选择弹窗 */
@@ -446,6 +493,7 @@
   color: #999;
   color: #999;
 }
 }
 
 
+/* 数量选择器 */
 .quantity-section {
 .quantity-section {
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;

+ 130 - 30
mini/src/pages/goods-detail/index.tsx

@@ -1,4 +1,4 @@
-import { View, ScrollView, Text, RichText } from '@tarojs/components'
+import { View, ScrollView, Text, RichText, Input } from '@tarojs/components'
 import { useQuery } from '@tanstack/react-query'
 import { useQuery } from '@tanstack/react-query'
 import { useState, useEffect } from 'react'
 import { useState, useEffect } from 'react'
 import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
 import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
@@ -140,6 +140,96 @@ export default function GoodsDetailPage() {
     }
     }
   })
   })
 
 
+  // 获取最大可购买数量
+  const getMaxQuantity = () => {
+    return Math.min(goods?.stock || 999, 999)
+  }
+
+  // 处理减少数量
+  const handleDecrease = () => {
+    const currentQty = quantity === 0 ? 1 : quantity
+    const newQuantity = Math.max(1, currentQty - 1)
+    setQuantity(newQuantity)
+  }
+
+  // 处理增加数量
+  const handleIncrease = () => {
+    const currentQty = quantity === 0 ? 1 : quantity
+    const maxQuantity = getMaxQuantity()
+    if (currentQty >= maxQuantity) {
+      if (maxQuantity === goods?.stock) {
+        Taro.showToast({
+          title: `库存只有${goods?.stock}件`,
+          icon: 'none',
+          duration: 1500
+        })
+      } else {
+        Taro.showToast({
+          title: '单次最多购买999件',
+          icon: 'none',
+          duration: 1500
+        })
+      }
+      return
+    }
+    setQuantity(currentQty + 1)
+  }
+
+  // 处理数量输入变化
+  const handleQuantityChange = (value: string) => {
+    // 清除非数字字符
+    const cleanedValue = value.replace(/[^\d]/g, '')
+
+    // 如果输入为空,设为空字符串(允许用户删除)
+    if (cleanedValue === '') {
+      setQuantity(0) // 设为0表示空输入
+      return
+    }
+
+    const numValue = parseInt(cleanedValue)
+
+    // 验证最小值
+    if (numValue < 1) {
+      setQuantity(1)
+      Taro.showToast({
+        title: '数量不能小于1',
+        icon: 'none',
+        duration: 1500
+      })
+      return
+    }
+
+    // 验证最大值
+    const maxQuantity = getMaxQuantity()
+    if (numValue > maxQuantity) {
+      setQuantity(maxQuantity)
+      if (maxQuantity === goods?.stock) {
+        Taro.showToast({
+          title: `库存只有${goods?.stock}件`,
+          icon: 'none',
+          duration: 1500
+        })
+      } else {
+        Taro.showToast({
+          title: '单次最多购买999件',
+          icon: 'none',
+          duration: 1500
+        })
+      }
+      return
+    }
+
+    setQuantity(numValue)
+  }
+
+  // 处理输入框失去焦点(完成输入)
+  const handleQuantityBlur = () => {
+    // 如果数量为0(表示空输入),设为1
+    if (quantity === 0) {
+      setQuantity(1)
+    }
+  }
+
   // 添加到购物车
   // 添加到购物车
   const handleAddToCart = () => {
   const handleAddToCart = () => {
     if (!goods) return
     if (!goods) return
@@ -147,7 +237,9 @@ export default function GoodsDetailPage() {
     const currentPrice = goods.price
     const currentPrice = goods.price
     const currentStock = goods.stock
     const currentStock = goods.stock
 
 
-    if (quantity > currentStock) {
+    const finalQuantity = quantity === 0 ? 1 : quantity
+
+    if (finalQuantity > currentStock) {
       Taro.showToast({
       Taro.showToast({
         title: '库存不足',
         title: '库存不足',
         icon: 'none'
         icon: 'none'
@@ -161,7 +253,7 @@ export default function GoodsDetailPage() {
       price: currentPrice,
       price: currentPrice,
       image: goods.imageFile?.fullUrl || '',
       image: goods.imageFile?.fullUrl || '',
       stock: currentStock,
       stock: currentStock,
-      quantity,
+      quantity: finalQuantity,
       spec: ''
       spec: ''
     })
     })
 
 
@@ -177,8 +269,9 @@ export default function GoodsDetailPage() {
 
 
     const currentPrice = goods.price
     const currentPrice = goods.price
     const currentStock = goods.stock
     const currentStock = goods.stock
+    const finalQuantity = quantity === 0 ? 1 : quantity
 
 
-    if (quantity > currentStock) {
+    if (finalQuantity > currentStock) {
       Taro.showToast({
       Taro.showToast({
         title: '库存不足',
         title: '库存不足',
         icon: 'none'
         icon: 'none'
@@ -188,7 +281,7 @@ export default function GoodsDetailPage() {
 
 
     Taro.removeStorageSync('buyNow')
     Taro.removeStorageSync('buyNow')
     Taro.removeStorageSync('checkoutItems')
     Taro.removeStorageSync('checkoutItems')
-    
+
     // 将商品信息存入临时存储,跳转到订单确认页
     // 将商品信息存入临时存储,跳转到订单确认页
     Taro.setStorageSync('buyNow', {
     Taro.setStorageSync('buyNow', {
       goods: {
       goods: {
@@ -196,10 +289,10 @@ export default function GoodsDetailPage() {
         name: goods.name,
         name: goods.name,
         price: currentPrice,
         price: currentPrice,
         image: goods.imageFile?.fullUrl || '',
         image: goods.imageFile?.fullUrl || '',
-        quantity,
+        quantity: finalQuantity,
         spec: ''
         spec: ''
       },
       },
-      totalAmount: currentPrice * quantity
+      totalAmount: currentPrice * finalQuantity
     })
     })
 
 
     // const buyNowData = Taro.getStorageSync('buyNow')
     // const buyNowData = Taro.getStorageSync('buyNow')
@@ -309,32 +402,39 @@ export default function GoodsDetailPage() {
 
 
       {/* 底部操作栏 */}
       {/* 底部操作栏 */}
       <View className="bottom-action-bar">
       <View className="bottom-action-bar">
-        <View className="action-left">
-          <View className="quantity-selector">
-            <Text className="quantity-label">数量:</Text>
-            <View className="quantity-controls">
-              <Button
-                size="sm"
-                variant="ghost"
-                className="quantity-btn"
-                onClick={() => setQuantity(Math.max(1, quantity - 1))}
-              >
-                -
-              </Button>
-              <Text className="quantity-value">{quantity}</Text>
-              <Button
-                size="sm"
-                variant="ghost"
-                className="quantity-btn"
-                onClick={() => setQuantity(Math.min(goods.stock, quantity + 1))}
-              >
-                +
-              </Button>
-            </View>
+        <View className="quantity-section">
+          <Text className="quantity-label">数量</Text>
+          <View className="quantity-controls">
+            <Button
+              size="sm"
+              variant="ghost"
+              className="quantity-btn"
+              onClick={handleDecrease}
+            >
+              -
+            </Button>
+            <Input
+              className="quantity-input"
+              type="number"
+              value={quantity === 0 ? '' : quantity.toString()}
+              onInput={(e) => handleQuantityChange(e.detail.value)}
+              onBlur={handleQuantityBlur}
+              placeholder="1"
+              maxlength={3}
+              confirmType="done"
+            />
+            <Button
+              size="sm"
+              variant="ghost"
+              className="quantity-btn"
+              onClick={handleIncrease}
+            >
+              +
+            </Button>
           </View>
           </View>
         </View>
         </View>
 
 
-        <View className="action-right">
+        <View className="button-section">
           <Button
           <Button
             className="add-cart-btn"
             className="add-cart-btn"
             onClick={handleAddToCart}
             onClick={handleAddToCart}