Procházet zdrojové kódy

feat(coupon): 新增优惠券模块及购物车价格明细弹窗

- 新增AppMemberCouponVO接口定义优惠券数据结构
- 添加memberCouponPage接口用于分页获取优惠券列表
- 新增优惠券页面路由及组件,实现优惠券列表展示与状态分类
- 购物车页新增价格明细弹窗,显示商品合计、优惠券金额、配送费等
- 购物车页调整总计区域样式,增加明细按钮触发弹窗
- 购物车页调整全选与删除功能相关代码
- 优化分类组件注释,暂时屏蔽部分购物车商品处理逻辑
- 我的页面新增优惠券入口,替代原评价入口
- 配置文件调整开发环境地址注释
- 修复manifest.json末尾格式问题
zhangtao před 1 týdnem
rodič
revize
50daae5275

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

@@ -3041,4 +3041,33 @@ namespace Api {
     [property: string]: any
   }
 
+  /** 优惠券领取列表 */
+  interface AppMemberCouponVO {
+    /** 第三方id */
+    allowanceId?: string
+    /** 门槛(单位(元)),满减时的需要达到什么金额,0或undefined表示无门槛 */
+    amountMoney?: number
+    /** 面额(单位(元)),优惠的金额 */
+    discountMoney?: number
+    /** 生效时间 */
+    effectiveTime?: string
+    /** 过期时间 */
+    expirationTime?: string
+    /** id */
+    id?: string
+    /** 锁定的订单ID */
+    lockOrderId?: string
+    /** 锁定状态:0-未锁定/已释放 1-已锁定 */
+    lockStatus?: number
+    /** 券名称 */
+    couponName?: string
+    /** 使用范围描述 */
+    scopeDesc?: string
+    /** 订单使用时间 */
+    orderCreateTime?: string
+    /** 使用状态:1-已使用 2-可使用 5-已过期 7-未生效 */
+    useStatus?: number
+    [property: string]: any
+  }
+
 }

+ 1 - 0
src/api/apiDefinitions.ts

@@ -65,6 +65,7 @@ export default {
   'xsb.refundCancel':['GET', '/smqjh-oms/app-api/v1/refund/cancel'],
   'xsb.refundDetails':['GET', '/smqjh-oms/app-api/v1/refund/findByDetails'],
   'xsb.popupConfig':['GET', '/smqjh-system/app-api/v1/appAdvertInfo/popupConfig'],
+  'xsb.memberCouponPage':['GET', '/smqjh-system/app-api/memberCoupon/getPageList'],
 
   'common.myShoppingCart':['GET', '/smqjh-oms/app-api/v1/shoppingCart/myShoppingCart'],
   'common.addShoppingCart':['POST', '/smqjh-oms/app-api/v1/shoppingCart/addShoppingCart'],

+ 13 - 0
src/api/globals.d.ts

@@ -547,6 +547,19 @@ declare global {
       >(
         config: Config
       ): Alova2Method<any, 'xsb.confirmReceipt', Config>;
+
+      memberCouponPage<
+        Config extends Alova2MethodConfig<listData<Api.AppMemberCouponVO>> & {
+          data: {
+            pageNum?: number;
+            pageSize?: number;
+            useStatus?: number;
+            lockStatus?: number;
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<listData<Api.AppMemberCouponVO>, 'xsb.memberCouponPage', Config>;
     }
     common: {
       myShoppingCart<

+ 2 - 2
src/config/index.ts

@@ -6,10 +6,10 @@ const mapEnvVersion = {
   // develop: 'http://192.168.1.101:8080',
   // develop: 'http://192.168.0.157:8080',
   // develop: 'http://192.168.1.253:8080',
-  // develop: 'http://192.168.0.19:8080', // 邓
+  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',

+ 8 - 0
src/pages.json

@@ -120,6 +120,14 @@
             "navigationBarTitleText": "提交订单"
           }
         },
+        {
+          "path": "coupon/index",
+          "name": "xsb-coupon",
+          "islogin": true,
+          "style": {
+            "navigationBarTitleText": "优惠券列表"
+          }
+        },
         {
           "path": "goods/index",
           "name": "xsb-goods",

+ 70 - 22
src/pages/cart/index.vue

@@ -14,9 +14,10 @@ definePage({
     disableScroll: true,
   },
 })
-const { cartList, isCartAllChecked, totalProduct } = storeToRefs(useSmqjhCartStore())
+const { cartList, totalProduct } = storeToRefs(useSmqjhCartStore())
 const { smqjhSelectedAddress } = storeToRefs(useUserStore())
 const cartStore = useSmqjhCartStore()
+const priceDetailPopup = ref(false)
 const tab = ref(0)
 const selectAddress = ref(false)
 const navList = ref([
@@ -161,31 +162,27 @@ async function handleSelectAddress() {
     <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 px-24rpx pb-60rpx pt-10rpx">
       <view class="ios w-full flex items-center justify-between">
         <view class="flex items-center">
-          <image
-            :src="`${StaticUrl}/cart-lanzi.png`"
-            class="h-100rpx w-100rpx"
-          />
-          <view class="ml-16rpx flex items-center">
-            <wd-checkbox v-model="isCartAllChecked" size="large" @change="cartStore.cartAllChecked">
-              全选
-            </wd-checkbox>
-            <view class="ml-10rpx text-24rpx text-[#FF4A39]" @click="cartStore.cartDeleteGoods">
-              删除
-            </view>
+          <view class="flex items-center">
+            <image :src="`${StaticUrl}/cart-lanzi.png`" class="h-100rpx w-100rpx" />
           </view>
-        </view>
-        <view class="flex items-center">
-          <view class="flex items-center font-semibold">
-            <view class="text-22rpx text-[#222]">
-              总计:
+          <view class="ml-40rpx">
+            <view class="flex items-center">
+              <view class="font-semibold">
+                ¥ {{ totalProduct?.amount || 0 }}
+              </view>
+              <view v-if="totalProduct?.coupon" class="ml-10rpx text-24rpx text-[#FF4A39]">
+                共减¥{{ totalProduct?.coupon }}
+              </view>
+              <view v-if="totalProduct?.amount" class="ml-10rpx 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="mt-10rpx text-24rpx text-gray">
+              配送费:¥{{ totalProduct?.transfee || 0 }}
             </view>
           </view>
+        </view>
+        <view class="flex items-center">
           <view class="ml-20rpx w-160rpx">
             <wd-button block size="large" @click="cartStore.cartOrderConfirm">
               结算
@@ -194,6 +191,57 @@ async function handleSelectAddress() {
         </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="h-200rpx" />
+    </Zpopup>
     <selectAddressTemplate v-model="selectAddress" />
   </view>
 </template>

+ 19 - 19
src/subPack-xsb/commonTab/components/classfiy.vue

@@ -105,7 +105,7 @@ const categoriesId = computed(() => {
 async function getCartCategorList() {
   return new Promise((resolve, reject) => {
     // 记录刷新前的 ID 集合,用于检测新增商品
-    const prevIdSet = new Set(cartList.value.map(it => it.id))
+    // const prevIdSet = new Set(cartList.value.map(it => it.id))
     Apis.xsb.myShoppingCartCategory({
       data: {
         businessType: 'XSB',
@@ -114,23 +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
-      }
+      // 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)
@@ -811,7 +811,7 @@ function handlePay() {
       <template #footer>
         <view class="box-border w-full flex items-center justify-between py20rpx">
           <view class="w-48%">
-            <wd-button hairline plain block @click="selectGoods = false">
+            <wd-button plain hairline block @click="selectGoods = false">
               取消
             </wd-button>
           </view>

+ 6 - 1
src/subPack-xsb/commonTab/components/my.vue

@@ -116,10 +116,15 @@ function handleGo(item: { name: string, status: string }) {
               <image :src="`${StaticUrl}/7.png`" class="h50rpx w50rpx" />
             </template>
           </wd-cell>
-          <wd-cell title="评价" custom-title-class="cell-title" clickable is-link>
+          <!-- <wd-cell title="评价" custom-title-class="cell-title" clickable is-link>
             <template #icon>
               <image :src="`${StaticUrl}/8.png`" class="h50rpx w50rpx" />
             </template>
+          </wd-cell> -->
+          <wd-cell title="优惠券" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'xsb-coupon' })">
+            <template #icon>
+              <image :src="`${StaticUrl}/xsb-my-coupon.png`" class="h50rpx w50rpx" />
+            </template>
           </wd-cell>
         </wd-cell-group>
       </wd-card>

+ 85 - 0
src/subPack-xsb/components/coupon/index.vue

@@ -0,0 +1,85 @@
+<script setup lang="ts">
+import { dayjs } from 'wot-design-uni'
+import { StaticUrl } from '@/config'
+import router from '@/router'
+
+defineProps<{
+  itemcoupon: Api.AppMemberCouponVO
+  /** 是否显示"去使用"按钮,默认 true */
+  showUseBtn?: boolean
+}>()
+function handleUseCoupon() {
+  router.replace({ name: 'xsb-homeTabbar', params: { name: 'xsb-classfiy' } })
+}
+</script>
+
+<template>
+  <view class="relative box-border h164rpx w-full flex overflow-hidden rounded-16rpx">
+    <image
+      v-if="itemcoupon.useStatus !== 2"
+      :src="`${StaticUrl}/xsb-coupon-bg-un.png`"
+      class="h-full w-full"
+    />
+    <image
+      v-else
+      :src="`${StaticUrl}/xsb-coupon-bg-item.png`"
+      class="h-full w-full"
+    />
+    <view class="absolute left-0 top-0 h-full w-full flex items-center">
+      <view class="w-184rpx flex flex-shrink-0 flex-col items-center justify-center py-36rpx">
+        <view class="flex items-end leading-none" :class="[itemcoupon.useStatus == 2 ? 'text-[#FF4A39]' : 'text-[#AAAAAA]']">
+          <text class="mb-6rpx text-24rpx font-semibold">
+            ¥
+          </text>
+          <text class="text-48rpx font-bold">
+            {{ itemcoupon.discountMoney ?? 0 }}
+          </text>
+        </view>
+        <view class="mt-12rpx text-24rpx text-[#AAAAAA]">
+          {{ Number(itemcoupon.amountMoney) > 0 ? `满${itemcoupon.amountMoney}可用` : '无门槛' }}
+        </view>
+      </view>
+      <view class="flex flex-1 items-center px-24rpx py-28rpx">
+        <view class="flex-1 pr-20rpx">
+          <view class="text-28rpx text-[#333] font-semibold">
+            {{ itemcoupon.activityName || '优惠券' }}
+          </view>
+          <template v-if="[2, 5].includes(itemcoupon.useStatus || 2)">
+            <view v-if="itemcoupon.expirationTime" class="mt-12rpx text-22rpx text-[#AAAAAA]">
+              有效期:{{ dayjs(itemcoupon.expirationTime).format('YYYY-MM-DD') }}
+            </view>
+            <view v-if="itemcoupon.scopeDesc" class="mt-8rpx text-22rpx text-[#AAAAAA]">
+              限:{{ itemcoupon.scopeDesc }}
+            </view>
+          </template>
+          <view v-if="itemcoupon.orderCreateTime" class="mt-8rpx text-22rpx text-[#AAAAAA]">
+            使用时间: {{ itemcoupon.orderCreateTime }}
+          </view>
+          <view v-if="itemcoupon.useStatus === 1" class="mt-8rpx text-22rpx text-[#AAAAAA]">
+            说明:部分退款,优惠卷不退还
+          </view>
+          <view v-if="itemcoupon.useStatus === 7" class="mt-8rpx text-22rpx text-[#AAAAAA]">
+            冻结原因:该券已被占用,取消可恢复,有效期内可用。
+          </view>
+        </view>
+        <view
+          v-if="showUseBtn !== false"
+          class="flex-shrink-0 rounded-full bg-[#FF4A39] px-28rpx py-16rpx text-26rpx text-white"
+          @click="handleUseCoupon"
+        >
+          去使用
+        </view>
+        <wd-button v-if="itemcoupon.useStatus === 1" hairline plain size="small" type="info" @click="router.replace({ name: 'xsb-orderDetaile', params: { id: itemcoupon.lockOrderId } })">
+          查看订单
+        </wd-button>
+        <wd-button v-if="itemcoupon.useStatus === 7" hairline plain size="small" type="info" @click="router.replace({ name: 'xsb-orderDetaile', params: { id: itemcoupon.lockOrderId } })">
+          去取消订单
+        </wd-button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+
+</style>

+ 74 - 0
src/subPack-xsb/coupon/index.vue

@@ -0,0 +1,74 @@
+<script setup lang="ts">
+import CouponItem from '../components/coupon/index.vue'
+
+definePage({ name: 'xsb-coupon', islogin: true, style: { navigationBarTitleText: '优惠券列表' } })
+
+const tabList = [
+  { label: '待使用', useStatus: 2 },
+  { label: '已使用', useStatus: 1 },
+  { label: '已过期', useStatus: 5 },
+  { label: '冻结', useStatus: 7 },
+]
+
+const activeTab = ref(0)
+
+const { data: couponList, isLastPage, page, reload } = usePagination(
+  (pageNum, pageSize) => Apis.xsb.memberCouponPage({
+    data: {
+      pageNum,
+      pageSize,
+      useStatus: tabList[activeTab.value].useStatus,
+    },
+  }),
+  {
+    immediate: true,
+    pageNum: 1,
+    pageSize: 10,
+    initialData: [],
+    data: res => res.data?.list ?? [],
+    append: true,
+  },
+)
+
+function switchTab(index: number) {
+  if (activeTab.value === index)
+    return
+  activeTab.value = index
+  couponList.value = []
+  reload()
+}
+
+onReachBottom(() => {
+  if (!isLastPage.value) {
+    page.value++
+  }
+})
+</script>
+
+<template>
+  <view class="min-h-screen bg-white">
+    <!-- Tab 导航 -->
+    <view class="sticky top-0 z-10 bg-white">
+      <wd-tabs v-model="activeTab" @change="switchTab($event.index)">
+        <wd-tab v-for="tab in tabList" :key="tab.useStatus" :title="tab.label" />
+      </wd-tabs>
+    </view>
+
+    <!-- 列表 -->
+    <view class="p-24rpx">
+      <view v-for="item in couponList" :key="item.id" class="mb-20rpx">
+        <CouponItem
+          :itemcoupon="item"
+          :show-use-btn="item.useStatus === 2"
+        />
+      </view>
+
+      <!-- 空状态 -->
+      <StatusTip v-if="couponList.length === 0" tip="暂无内容" />
+      <!-- 底部占位 -->
+      <view class="h-40rpx" />
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 1 - 0
src/uni-pages.d.ts

@@ -7,6 +7,7 @@ interface NavigateToOptions {
   url: "/pages/login/index" |
        "/subPack-xsb/commonTab/index" |
        "/subPack-xsb/confirmOrder/index" |
+       "/subPack-xsb/coupon/index" |
        "/subPack-xsb/goods/index" |
        "/subPack-xsb/order/index" |
        "/subPack-xsb/orderDetaile/index" |