Browse Source

feat(cart): 优化购物车价格明细显示与选择逻辑

- 在购物车类型定义中新增 amount、coupon 和 couponName 字段
- 调整开发环境接口地址配置,启用指定开发环境URL
- 在购物车组件 cart.vue 中新增价格明细弹窗,展示商品合计、总价、优惠券
- 设置弹窗显示逻辑,支持点击明细查看具体价格构成
- 在商品分类组件 classfiy.vue 中新增购物车商品多选功能,支持全选与反选
- 优化购物车弹窗展示,加入选择框及已选商品数量显示
- 实现清空购物车功能,带确认弹窗提示
- 动态刷新购物车商品选中状态,自动添加新商品到选中列表
- 调整价格获取逻辑,仅统计已选商品价格并更新总价显示
- 购物车弹窗中支持商品单独选择、删除、加减操作,并显示库存和删除状态
- 使用 badge 显示购物车商品总数,提升用户体验
- 增加步骤注释和界面微调提升可用性与美观度
zhangtao 1 week ago
parent
commit
e469f2afd5

+ 10 - 0
src/api/api.type.d.ts

@@ -1166,6 +1166,16 @@ namespace Api {
      * skuList
      */
     skuList: CartSkuVo[]
+    /**
+     * 金额
+     */
+    amount: number
+    /**
+     * 优惠券
+     */
+    coupon: number
+    couponName: string
+
     [property: string]: any
   }
   interface AppletOrderSkuVo {

+ 2 - 2
src/config/index.ts

@@ -9,11 +9,11 @@ const mapEnvVersion = {
   // develop: 'http://192.168.0.19:8080', // 邓
   // develop: 'http://192.168.0.217:8080', // 黄
   // develop: 'http://192.168.0.11:8080', // 王
-  // develop: 'http://192.168.1.89:8080', // 田
+  develop: 'http://192.168.1.89:8080', // 田
   // develop: 'http://74949mkfh190.vicp.fun', // 付
   // develop: 'http://47.109.84.152:8081',
   // develop: 'https://5ed0f7cc.r9.vip.cpolar.cn',
-  develop: 'https://smqjh.api.zswlgz.com',
+  // develop: 'https://smqjh.api.zswlgz.com',
   /**
    * 体验版
    */

+ 72 - 22
src/subPack-xsb/commonTab/components/cart.vue

@@ -7,6 +7,7 @@ const emit = defineEmits(['changeTab'])
 
 const { cartList, isCartAllChecked, totalProduct } = storeToRefs(useSmqjhCartStore())
 const cartStore = useSmqjhCartStore()
+const priceDetailPopup = ref(false)
 
 watch(() => cartList.value, async () => {
   cartStore.getCartTotalPrice()
@@ -125,34 +126,32 @@ onMounted(async () => {
         </scroll-view>
       </view>
     </view>
-    <view v-if="cartList.length" class="fixedShadow fixed bottom-60rpx left-0 z-99 box-border w-full flex items-center justify-between rounded-t-16rpx bg-white px24rpx pb60rpx pt10rpx">
+    <view class="fixedShadow fixed bottom-60rpx left-0 z-99 box-border w-full flex items-center justify-between rounded-t-16rpx bg-white px24rpx pb60rpx pt10rpx">
       <view class="ios w-full flex items-center justify-between">
         <view class="flex items-center">
-          <image
-            :src="`${StaticUrl}/cart-lanzi.png`"
-            class="h100rpx w100rpx"
-          />
-          <view class="ml16rpx flex items-center">
-            <wd-checkbox v-model="isCartAllChecked" size="large" @change="cartStore.cartAllChecked">
-              全选
-            </wd-checkbox>
-            <view class="ml10rpx text-24rpx text-#FF4A39" @click="cartStore.cartDeleteGoods">
-              删除
-            </view>
+          <view class="flex items-center">
+            <!-- <wd-badge :model-value="getTotalNum" :top="20"> -->
+            <image :src="`${StaticUrl}/cart-lanzi.png`" class="h100rpx w100rpx" />
+            <!-- </wd-badge> -->
           </view>
-        </view>
-        <view class="flex items-center">
-          <view class="flex items-center font-semibold">
-            <view class="text-22rpx text-#222">
-              总计:
+          <view class="ml40rpx">
+            <view class="flex items-center">
+              <view class="font-semibold">
+                ¥ {{ totalProduct?.amount || 0 }}
+              </view>
+              <view v-if="totalProduct?.coupon" class="ml10rpx text-24rpx text-#FF4A39">
+                共减¥{{ totalProduct?.coupon }}
+              </view>
+              <view v-if="totalProduct?.amount" class="ml10rpx text-24rpx text-gray" @click="priceDetailPopup = true">
+                明细 <wd-icon name="arrow-up" size="24rpx" color="#aaa" />
+              </view>
             </view>
-            <view class="flex items-baseline text-24rpx text-#FF4A39">
-              ¥
-              <text class="text-36rpx">
-                {{ totalProduct?.price || '0.00' }}
-              </text>
+            <view class="mt10rpx text-24rpx text-gray">
+              配送费:¥{{ totalProduct?.transfee || 0 }}
             </view>
           </view>
+        </view>
+        <view class="flex items-center">
           <view class="ml20rpx w160rpx">
             <wd-button block size="large" @click="cartStore.cartOrderConfirm">
               结算
@@ -161,6 +160,57 @@ onMounted(async () => {
         </view>
       </view>
     </view>
+    <!-- 价格明细弹窗 -->
+    <Zpopup v-model="priceDetailPopup" :zindex="10" bg="#fff">
+      <view class="ios box-border w-full px-40rpx pb-60rpx pt-36rpx">
+        <view class="mb-40rpx text-center text-32rpx font-semibold">
+          价格明细
+        </view>
+        <view class="flex items-center justify-between py-20rpx">
+          <view class="text-30rpx font-semibold">
+            商品合计
+          </view>
+          <view class="text-30rpx font-semibold">
+            ¥{{ totalProduct?.amount }}
+          </view>
+        </view>
+        <view class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            商品总价
+          </view>
+          <view class="text-28rpx text-#333">
+            ¥{{ totalProduct?.price }}
+          </view>
+        </view>
+        <template v-if="totalProduct?.coupon">
+          <view class="flex items-center justify-between py-20rpx">
+            <view class="text-28rpx text-#333">
+              下单用券共减
+            </view>
+            <view class="text-28rpx text-#FF4A39 font-semibold">
+              -¥{{ totalProduct?.coupon }}
+            </view>
+          </view>
+          <view v-if="totalProduct?.couponName" class="flex items-center justify-between pb-20rpx">
+            <view class="text-24rpx text-#AAAAAA">
+              {{ totalProduct?.couponName }}
+            </view>
+            <view class="text-24rpx text-#AAAAAA">
+              -¥{{ totalProduct?.coupon }}
+            </view>
+          </view>
+        </template>
+        <view class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            配送费
+          </view>
+          <view class="text-28rpx text-#333">
+            ¥{{ totalProduct?.transfee }}
+          </view>
+        </view>
+      </view>
+      <view class="h200rpx" />
+    </Zpopup>
   </view>
 </template>
 

+ 219 - 64
src/subPack-xsb/commonTab/components/classfiy.vue

@@ -8,11 +8,52 @@ const props = defineProps<{ categoryList: Api.xsbCategories[], hotText: Api.xsbS
 const { statusBarHeight, MenuButtonHeight } = storeToRefs(useSysStore())
 const { topNavActive, leftActive, SelectShopInfo } = storeToRefs(useSysXsbStore())
 const { userInfo, token } = storeToRefs(useUserStore())
+const { getTotalNum } = storeToRefs(useSmqjhCartStore())
 const classfiylist = computed(() => props.categoryList)
 const sortClassBtn = ref(1)
 const show = ref(false)
 const cartList = ref<Api.xsbCategoriesCartList[]>([])
+const cartPopup = ref(false)
+const totalProduct = ref<Api.shoppingCartOrderConfirm>()
+const priceDetailPopup = ref(false)
 const cartIds = computed(() => cartList.value.filter(it => it.isDelete !== '1' && it.shopSkuStocks !== '0').map(it => it.id))
+// 勾选状态:只计算勾选商品的价格
+const selectedIds = ref<number[]>([])
+const isAllSelected = computed(() => cartIds.value.length > 0 && cartIds.value.every(id => selectedIds.value.includes(id)))
+const selectedCount = computed(() => selectedIds.value.length)
+
+function toggleSelectAll() {
+  if (isAllSelected.value) {
+    selectedIds.value = []
+  }
+  else {
+    selectedIds.value = [...cartIds.value]
+  }
+  getGoodsPrice()
+}
+async function handleClearCart() {
+  useGlobalMessage().confirm({
+    title: '提示',
+    msg: '确认清空购物车?',
+    success: async () => {
+      uni.showLoading({ mask: true })
+      try {
+        const ids = cartList.value.map(it => it.id).join(',')
+        if (ids) {
+          await Apis.common.deleteShoppingCart({ pathParams: { ids } })
+        }
+        cartPopup.value = false
+        selectedIds.value = []
+        totalProduct.value = undefined
+        useSmqjhCartStore().getCartList('XSB')
+        await getCartCategorList()
+      }
+      finally {
+        uni.hideLoading()
+      }
+    },
+  })
+}
 const showball = ref(false)
 const _this = getCurrentInstance()
 const selectGoods = ref(false)
@@ -23,7 +64,6 @@ const goodsInTo = ref()
 const goodsInfo = ref<Api.xsbCategoryProductList | Api.xsbProductDetail | undefined>()
 
 const isTopLoading = ref(false)
-const cartPopup = ref(false)
 const basllObj = ref({
   left: 0,
   top: 0,
@@ -42,7 +82,6 @@ const goodsLoading = ref<LoadMoreState>()
 const navHeight = computed(() => {
   return (`${Number(MenuButtonHeight.value) + Number(statusBarHeight.value)}px`)
 })
-const totalProduct = ref<Api.shoppingCartOrderConfirm>()
 function handleTopNavChange(item: Api.xsbCategoriesChildren) {
   topNavActive.value = item.code
   if (!item.children)
@@ -65,6 +104,8 @@ const categoriesId = computed(() => {
 })
 async function getCartCategorList() {
   return new Promise((resolve, reject) => {
+    // 记录刷新前的 ID 集合,用于检测新增商品
+    const prevIdSet = new Set(cartList.value.map(it => it.id))
     Apis.xsb.myShoppingCartCategory({
       data: {
         businessType: 'XSB',
@@ -73,9 +114,23 @@ async function getCartCategorList() {
       },
     }).then((res) => {
       cartList.value = res.data
+      const validIds = res.data
+        .filter(it => it.isDelete !== '1' && it.shopSkuStocks !== '0')
+        .map(it => it.id)
+      // 保留仍有效的已选项 + 自动选中新添加的商品
+      const newlyAdded = validIds.filter(id => !prevIdSet.has(id))
+      const keepSelected = selectedIds.value.filter(id => validIds.includes(id))
+      selectedIds.value = [...new Set([...keepSelected, ...newlyAdded])]
+      // 第一次加载时全选
+      if (selectedIds.value.length === 0 && validIds.length > 0) {
+        selectedIds.value = [...validIds]
+      }
       if (cartList.value.length) {
         getGoodsPrice()
       }
+      else {
+        totalProduct.value = undefined
+      }
       resolve(res)
     }).catch((err) => {
       reject(err)
@@ -239,10 +294,13 @@ function handleGo(item: Api.xsbCategoryProductList) {
 }
 
 async function getGoodsPrice() {
-  if (cartIds.value.length) {
-    const res = await useSmqjhCartStore().getCartAddGoodsPrice(cartIds.value.join(','))
+  if (selectedIds.value.length) {
+    const res = await useSmqjhCartStore().getCartAddGoodsPrice(selectedIds.value.join(','))
     totalProduct.value = res
   }
+  else {
+    totalProduct.value = undefined
+  }
 }
 async function handleSub(item: Api.xsbCategoriesCartList) {
   if (item.num === 1) {
@@ -563,11 +621,31 @@ function handlePay() {
       class="fixedShadow fixed bottom-60rpx left-0 z-100 box-border w-full flex items-center justify-between rounded-t-16rpx bg-white px24rpx pb60rpx pt10rpx"
     >
       <view class="ios w-full flex items-center justify-between">
-        <view class="flex items-center" @click="cartPopup = true">
-          <image :src="`${StaticUrl}/cart-lanzi.png`" class="cart-box h100rpx w100rpx" />
+        <view class="flex items-center">
+          <view class="flex items-center" @click="cartPopup = true, priceDetailPopup = false">
+            <wd-badge :model-value="getTotalNum" :top="20">
+              <image :src="`${StaticUrl}/cart-lanzi.png`" class="cart-box h100rpx w100rpx" />
+            </wd-badge>
+          </view>
+          <view class="ml40rpx">
+            <view class="flex items-center">
+              <view class="font-semibold">
+                ¥ {{ totalProduct?.amount || 0 }}
+              </view>
+              <view v-if="totalProduct?.coupon" class="ml10rpx text-24rpx text-#FF4A39">
+                共减¥{{ totalProduct?.coupon }}
+              </view>
+              <view v-if="totalProduct?.amount" class="ml10rpx text-24rpx text-gray" @click="priceDetailPopup = true, cartPopup = false">
+                明细 <wd-icon name="arrow-up" size="24rpx" color="#aaa" />
+              </view>
+            </view>
+            <view class="mt10rpx text-24rpx text-gray">
+              配送费:¥{{ totalProduct?.transfee || 0 }}
+            </view>
+          </view>
         </view>
         <view class="flex items-center">
-          <view class="flex items-center font-semibold">
+          <!-- <view class="flex items-center font-semibold">
             <view class="text-22rpx text-#222">
               总计:
             </view>
@@ -577,7 +655,7 @@ function handlePay() {
                 {{ totalProduct?.price || '0.00' }}
               </text>
             </view>
-          </view>
+          </view> -->
           <view class="ml20rpx w160rpx">
             <wd-button block size="large" @click="handlePay">
               结算
@@ -586,70 +664,147 @@ function handlePay() {
         </view>
       </view>
     </view>
+    <!-- 价格明细弹窗 -->
+    <Zpopup v-model="priceDetailPopup" :zindex="10" bg="#fff">
+      <view class="ios box-border w-full px-40rpx pb-60rpx pt-36rpx">
+        <view class="mb-40rpx text-center text-32rpx font-semibold">
+          价格明细
+        </view>
+        <!-- 商品合计 -->
+        <view class="flex items-center justify-between py-20rpx">
+          <view class="text-30rpx font-semibold">
+            商品合计
+          </view>
+          <view class="text-30rpx font-semibold">
+            ¥{{ totalProduct?.amount }}
+          </view>
+        </view>
+        <!-- 商品总价 -->
+        <view class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            商品总价
+          </view>
+          <view class="text-28rpx text-#333">
+            ¥{{ totalProduct?.price }}
+          </view>
+        </view>
+        <!-- 下单用券共减 -->
+        <template v-if="totalProduct?.coupon">
+          <view class="flex items-center justify-between py-20rpx">
+            <view class="text-28rpx text-#333">
+              下单用券共减
+            </view>
+            <view class="text-28rpx text-#FF4A39 font-semibold">
+              -¥{{ totalProduct?.coupon }}
+            </view>
+          </view>
+          <!-- 券名称子行 -->
+          <view v-if="totalProduct?.couponName" class="flex items-center justify-between pb-20rpx">
+            <view class="text-24rpx text-#AAAAAA">
+              {{ totalProduct?.couponName }}
+            </view>
+            <view class="text-24rpx text-#AAAAAA">
+              -¥{{ totalProduct?.coupon }}
+            </view>
+          </view>
+        </template>
+        <!-- 配送费 -->
+        <view class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            配送费
+          </view>
+          <view class="text-28rpx text-#333">
+            ¥{{ totalProduct?.transfee }}
+          </view>
+        </view>
+      </view>
+      <view class="h200rpx" />
+    </Zpopup>
     <Zpopup v-model="cartPopup" :zindex="99">
-      <view class="ios h800rpx overflow-y-scroll">
-        <view class="p24rpx">
-          <view
-            v-for="item in cartList" :key="item.id" class="relative mt20rpx box-border flex items-center overflow-hidden rounded-16rpx bg-white p24rpx"
-          >
-            <view class="flex flex-1">
-              <image
-                :src="item.pic"
-                class="h206rpx w200rpx flex-shrink-0"
-                @click.stop="router.push({ name: 'xsb-goods', params: { id: String(item.prodId) } })"
-              />
-              <view class="ml20rpx flex-1">
-                <view class="text-left text-28rpx font-semibold">
-                  <!-- <view v-for="i in 2" :key="i" class="mr5px inline-block">
-                        <wd-tag type="danger">
-                          惊喜回馈
-                        </wd-tag>
-                      </view> -->
-                  {{ item.skuName }}
-                </view>
-                <view class="mt14rpx text-24rpx text-#AAAAAA">
-                  规格:{{ item.spec }}
-                </view>
-                <view class="mt14rpx flex items-center justify-between">
-                  <view class="text-36rpx text-#FF4A39 font-semibold">
-                    ¥{{ item.price }}
-                  </view>
-                  <!-- <wd-input-number v-model="item.num" disable-input @change="handleChangeNum($event, item)" /> -->
-                  <view v-if="item.shopSkuStocks !== '0' && item.isDelete !== '1'" class="flex items-center">
-                    <image
-                      :src="` ${StaticUrl}/sub-cart.png`"
-                      class="h44rpx w44rpx"
-                      @click.stop="handleSub(item)"
-                    />
-                    <view class="box-border h44rpx w84rpx flex items-center justify-center border border-#F0F0F0 border-solid text-24rpx text-#AAAAAA">
-                      {{ item.num }}
+      <view class="ios overflow-hidden">
+        <!-- 头部:全选 + 已选件数 + 清空购物车 -->
+        <view class="flex items-center justify-between border-b border-[#F5F5F5] border-solid px-24rpx py-24rpx">
+          <view class="flex items-center">
+            <wd-checkbox :model-value="isAllSelected" size="large" @change="toggleSelectAll">
+              全选
+            </wd-checkbox>
+            <text class="ml-16rpx text-24rpx text-[#AAAAAA]">
+              已选{{ selectedCount }}件
+            </text>
+          </view>
+          <view class="text-24rpx text-[#AAAAAA]" @click="handleClearCart">
+            <wd-icon name="delete" size="24rpx" /> 清空购物车
+          </view>
+        </view>
+        <!-- 商品列表 -->
+        <view class="h-700rpx overflow-y-scroll">
+          <view class="p-24rpx">
+            <wd-checkbox-group v-model="selectedIds" @change="getGoodsPrice">
+              <view
+                v-for="item in cartList" :key="item.id"
+                class="relative mt-20rpx box-border flex items-center overflow-hidden rounded-16rpx bg-white p-24rpx"
+              >
+                <!-- 复选框 -->
+                <wd-checkbox
+                  v-if="item.shopSkuStocks !== '0' && item.isDelete !== '1'"
+                  :model-value="item.id"
+                  size="large"
+                  class="mr-16rpx flex-shrink-0"
+                />
+                <view v-else class="mr-16rpx h-40rpx w-40rpx flex-shrink-0" />
+                <view class="flex flex-1">
+                  <image
+                    :src="item.pic"
+                    class="h206rpx w200rpx flex-shrink-0"
+                    @click.stop="router.push({ name: 'xsb-goods', params: { id: String(item.prodId) } })"
+                  />
+                  <view class="ml20rpx flex-1">
+                    <view class="text-left text-28rpx font-semibold">
+                      {{ item.skuName }}
+                    </view>
+                    <view class="mt14rpx text-24rpx text-#AAAAAA">
+                      规格:{{ item.spec }}
+                    </view>
+                    <view class="mt14rpx flex items-center justify-between">
+                      <view class="text-36rpx text-#FF4A39 font-semibold">
+                        ¥{{ item.price }}
+                      </view>
+                      <view v-if="item.shopSkuStocks !== '0' && item.isDelete !== '1'" class="flex items-center">
+                        <image
+                          :src="` ${StaticUrl}/sub-cart.png`"
+                          class="h44rpx w44rpx"
+                          @click.stop="handleSub(item)"
+                        />
+                        <view class="box-border h44rpx w84rpx flex items-center justify-center border border-#F0F0F0 border-solid text-24rpx text-#AAAAAA">
+                          {{ item.num }}
+                        </view>
+                        <image
+                          :src="` ${StaticUrl}/add-cart.png`"
+                          class="h44rpx w44rpx"
+                          @click.stop="handleAdd(item)"
+                        />
+                      </view>
                     </view>
-                    <image
-                      :src="` ${StaticUrl}/add-cart.png`"
-                      class="h44rpx w44rpx"
-                      @click.stop="handleAdd(item)"
-                    />
                   </view>
                 </view>
-              </view>
-            </view>
-            <view v-if="item.shopSkuStocks == '0' || item.isDelete == '1'" class="absolute left-0 top-0 z-1 h-full w-full bg-[rgba(255,255,255,.6)]">
-              <view class="relative w-full flex items-center justify-center">
-                <view class="rounded-16rpx bg-[rgba(0,0,0,.5)] p20rpx text-white">
-                  {{ item.shopSkuStocks == '0' ? '商品已售罄' : '商品已删除' }}
-                </view>
-                <view class="absolute bottom-20rpx right-20rpx">
-                  <wd-button @click="handleDelCartGoods(item)">
-                    删除商品
-                  </wd-button>
+                <view v-if="item.shopSkuStocks == '0' || item.isDelete == '1'" class="absolute left-0 top-0 z-1 h-full w-full bg-[rgba(255,255,255,.6)]">
+                  <view class="relative w-full flex items-center justify-center">
+                    <view class="rounded-16rpx bg-[rgba(0,0,0,.5)] p20rpx text-white">
+                      {{ item.shopSkuStocks == '0' ? '商品已售罄' : '商品已删除' }}
+                    </view>
+                    <view class="absolute bottom-20rpx right-20rpx">
+                      <wd-button @click="handleDelCartGoods(item)">
+                        删除商品
+                      </wd-button>
+                    </view>
+                  </view>
                 </view>
               </view>
-            </view>
+            </wd-checkbox-group>
           </view>
+          <StatusTip v-if="!cartList.length" tip="暂无内容" />
+          <view class="h320rpx" />
         </view>
-        <StatusTip v-if="!cartList.length" tip="暂无内容" />
-
-        <view class="h320rpx" />
       </view>
     </Zpopup>
     <selectSku v-model:show="selectGoods" v-model:sku-id="selectSkuId" v-model:select-goods-num="SelectGoodsNum" :goods-info="goodsInfo!">