Selaa lähdekoodia

```
feat: 添加会员赠品领取功能并优化首页导航栏

- 扩展Api.quantity类型支持string格式
- 新增giftsReceive接口定义和相关数据模型(giftsReceiveItem, giftsReceiveForm, giftsReceiveModel)
- 添加系统赠品领取API接口映射
- 更新giftsList返回类型为数组格式
- 实现giftsReceive方法调用接口
- 更改开发环境配置地址
- 修复manifest.json格式问题
- 将会员赠品页面设为需要登录访问
- 在购物车页面添加会员折扣金额显示
- 新增导航栏滑块高度计算工具函数calcNavSwiperHeight
- 实现首页金刚区动态高度调整和会员价显示
- 在充电页面添加停车费说明
- 实现会员赠品领取逻辑和表单验证
- 更新VIP权益页面显示实际优惠券和赠品数量
- 在分类和搜索页面添加会员价展示
- 实现确认订单页面赠品订单处理逻辑
- 在订单详情页面添加会员权益折扣显示
- 优化样式和交互体验

BREAKING CHANGE: giftsList接口返回类型由单个对象改为数组格式
```

zouzexu 2 päivää sitten
vanhempi
commit
64b2cc4347

+ 51 - 1
src/api/api.type.d.ts

@@ -233,7 +233,7 @@ namespace Api {
     /**
      * 领取数量或商品数量。赠品场景通常为配置的赠品数量。
      */
-    quantity?: number
+    quantity?: number | string
     /**
      * 当前赠品是否已领取。
      */
@@ -264,6 +264,56 @@ namespace Api {
     stock?: number
     [property: string]: any
   }
+  /**
+   * MemberGiftReceiveVO,会员赠品领取结果。返回会员账号和生成的赠品订单号。
+   */
+  export interface giftsReceiveItem {
+    /**
+     * 渠道/企业 ID。
+     */
+    channelId: number
+    /**
+     * 领取数量。
+     */
+    quantity: number
+    /**
+     * 门店 ID。
+     */
+    shopId: number
+    /**
+     * 商品 SKU ID。
+     */
+    skuId: number
+  }
+  export interface giftsReceiveForm {
+    /**
+     * 用户收货地址 ID。
+     */
+    addressId: number
+    /**
+     * 多商品合单领取明细。
+     */
+    items: giftsReceiveItem[]
+  }
+  export interface giftsReceiveModel {
+    /**
+     * 会员账号 ID。一次会员开通对应一条会员账号记录。
+     */
+    memberAccountId?: number
+    /**
+     * 订单编号。赠品领取成功后返回 0 元赠品订单号。
+     */
+    orderNumber?: string
+    /**
+     * 商品 SKU ID。
+     */
+    skuId?: number
+    /**
+     * 合单领取的 SKU ID 列表。
+     */
+    skuIds?: number[]
+    [property: string]: any
+  }
   interface xsbCategoryProductList {
     /**
      * 多规格

+ 2 - 1
src/api/apiDefinitions.ts

@@ -37,7 +37,8 @@ export default {
   'sys.selectZhUser': ['GET', '/smqjh-system/app-api/v1/claim/select'],
   'sys.zhUserReceived': ['POST', '/smqjh-system/app-api/v1/claim/received'],
   'sys.userVipInfo':['GET', '/smqjh-system/app-api/v1/member/current'],
-  'sys.giftsList':['GET', '/smqjh-system/app-api/v1/member/gifts'],
+  'sys.giftsList': ['GET', '/smqjh-system/app-api/v1/member/gifts'],
+  'sys.giftsReceive': ['POST', '/smqjh-system/app-api/v1/member/gifts/receive'],
 
   'xsb.categories':['GET', '/smqjh-pms/app-api/v1/categories'],
   'xsb.getCategoryProductList':['POST', '/smqjh-pms/app-api/v1/spu/getCategoryProductList'],

+ 36 - 2
src/api/globals.d.ts

@@ -209,11 +209,45 @@ declare global {
       >(
         config: Config
       ): Alova2Method<apiResData<Api.userMemberInfo>, 'sys.userVipInfo', Config>;
+
       giftsList<
-        Config extends Alova2MethodConfig<apiResData<Api.giftsListModel>> & {}
+        Config extends Alova2MethodConfig<apiResData<Api.giftsListModel[]>> & {}
+      >(
+        config: Config
+      ): Alova2Method<apiResData<Api.giftsListModel[]>, 'sys.giftsList', Config>;
+
+      giftsReceive<
+        Config extends Alova2MethodConfig<apiResData<Api.giftsReceiveModel>> & {
+          data: {
+            /**
+ * 用户收货地址 ID,用于创建赠品订单。
+ */
+            addressId: number;
+            /**
+             * 渠道/企业 ID。
+             */
+            channelId?: number;
+            /**
+             * 领取数量或商品数量。赠品场景通常为配置的赠品数量。
+             */
+            quantity?: number;
+            /**
+             * 门店 ID。
+             */
+            shopId?: number;
+            /**
+             * 商品 SKU ID。
+             */
+            skuId?: number;
+            /**
+             * 多商品合单领取明细。
+             */
+            items: Api.giftsReceiveItem[];
+          }
+        }
       >(
         config: Config
-      ): Alova2Method<apiResData<Api.giftsListModel>, 'sys.giftsList', Config>;
+      ): Alova2Method<apiResData<Api.giftsReceiveModel>, 'sys.giftsReceive', Config>;
     }
     xsb: {
       orderCoupons<

+ 2 - 1
src/config/index.ts

@@ -11,7 +11,8 @@ const mapEnvVersion = {
   // develop: 'http://192.168.0.11:8080', // 王
   // develop: 'http://192.168.1.21:8080', // 田
   // develop: 'http://74949mkfh190.vicp.fun', // 付
-  develop: 'http://47.109.84.152:8081',
+  // develop: 'http://47.109.84.152:8081',
+  develop: 'http://192.168.1.242:8080',
   // develop: 'https://5ed0f7cc.r9.vip.cpolar.cn',
   // develop: 'https://smqjh.api.zswlgz.com',
   /**

+ 1 - 1
src/pages.json

@@ -297,7 +297,7 @@
         {
           "path": "giveawaysVip/giveawaysVip",
           "name": "smqjh-giveaways-vip",
-          "islogin": false,
+          "islogin": true,
           "style": {
             "navigationBarTitleText": "选择赠品"
           }

+ 8 - 0
src/pages/cart/index.vue

@@ -242,6 +242,14 @@ async function handleSelectAddress() {
             </view>
           </view>
         </template>
+        <view v-if="totalProduct?.memberDiscountAmount" class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            {{ totalProduct?.memberBenefitDesc }}
+          </view>
+          <view class="text-28rpx text-#FF4A39 font-semibold">
+            -¥{{ totalProduct?.memberDiscountAmount }}
+          </view>
+        </view>
         <view class="flex items-center justify-between py-20rpx">
           <view class="text-28rpx text-[#333]">
             配送费

+ 31 - 0
src/pages/index/calcNavSwiperHeight.ts

@@ -0,0 +1,31 @@
+/**
+ * 计算金刚区 swiper 高度(单位:rpx)
+ *
+ * @param totalItems 可显示的总项数(已过滤 show)
+ * @param currentPageIndex 当前页面索引(从 0 开始)
+ * @param options 可选配置,支持覆盖每页项数、列数、单项高度等
+ */
+export interface CalcOptions {
+  itemsPerPage?: number
+  columns?: number
+  perImageHeight?: number
+  titleHeight?: number
+  rowGap?: number
+  extraSpace?: number
+}
+
+export function calcNavSwiperHeight(totalItems: number, currentPageIndex: number, options: CalcOptions = {}): number {
+  const itemsPerPage = options.itemsPerPage ?? 8
+  const columns = options.columns ?? 4
+  const perImageHeight = options.perImageHeight ?? 120
+  const titleHeight = options.titleHeight ?? 24
+  const rowGap = options.rowGap ?? 12
+  const extraSpace = options.extraSpace ?? 40
+
+  const start = currentPageIndex * itemsPerPage
+  const remaining = Math.max(0, totalItems - start)
+  const pageItems = Math.min(itemsPerPage, remaining)
+  const rows = Math.max(1, Math.ceil(pageItems / columns))
+
+  return rows * (perImageHeight + titleHeight) + Math.max(0, rows - 1) * rowGap + extraSpace
+}

+ 35 - 2
src/pages/index/index.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { calcNavSwiperHeight } from './calcNavSwiperHeight'
 import { StaticUrl } from '@/config'
 import router from '@/router'
 
@@ -69,6 +70,14 @@ const navList = computed(() => {
   ]
   return list
 })
+const itemsPerPage = 8
+const columnsCount = 4
+const visibleNavList = computed(() => navList.value.filter(i => i.show))
+const swiperHeight = computed(() => {
+  const total = visibleNavList.value.length
+  return calcNavSwiperHeight(total, currentIndex.value, { itemsPerPage, columns: columnsCount })
+})
+const swiperStyle = computed(() => ({ height: `${swiperHeight.value}rpx` }))
 onMounted(() => {
   addressStore.getLocation()
   opcity.value = 0
@@ -193,7 +202,8 @@ function handleJyBanner() {
 
           ]"
         >
-          <swiper :duration="300" class="h340rpx" @change="handleChangeSwiper">
+          <!-- h340rpx -->
+          <swiper :duration="300" :style="swiperStyle" class="transition-height" @change="handleChangeSwiper">
             <swiper-item
               v-for="pageIndex in Math.ceil(navList.filter(i => i.show).length / 8)" :key="pageIndex"
             >
@@ -273,7 +283,26 @@ function handleJyBanner() {
                           {{ item.prodName }}
                         </view>
                       </view>
-                      <view class="mt-20rpx flex items-end text-[#FF4D3A]">
+                      <view v-if="item.isMember" class="mt-20rpx flex items-center gap-16rpx">
+                        <view class="flex items-end text-[#FF4D3A]">
+                          <view class="text-24rpx">
+                            ¥
+                          </view>
+                          <view class="text-36rpx line-height-[36rpx]">
+                            {{ item.memberPrice }}
+                          </view>
+                          <view class="text-24rpx">
+                            元
+                          </view>
+                        </view>
+                        <view class="rounded-8rpx bg-#FF4A39 px-8rpx py-4rpx text-22rpx text-#FFF">
+                          会员价
+                        </view>
+                        <view class="text-24rpx text-#AAA line-through">
+                          ¥{{ item.channelProdPrice }}
+                        </view>
+                      </view>
+                      <view v-else class="mt-20rpx flex items-end text-[#FF4D3A]">
                         <view class="text-24rpx">
                         </view>
@@ -366,5 +395,9 @@ function handleJyBanner() {
     background: rgba(0, 0, 0, .3);
   }
 
+  .transition-height {
+    transition: height 0.3s ease;
+  }
+
 }
 </style>

+ 3 - 0
src/subPack-charge/chargePlateList/chargePlateList.vue

@@ -92,6 +92,9 @@ function handleDelete(id: number) {
         <view class="mt-10rpx">
           4.车牌绑定未按正确操作流程或车牌未对应现场充电车辆导致无法减免停车费,因此产生的一切损失与本平台无关。
         </view>
+        <view class="mt-10rpx">
+          5.部分充电站以停车场收费规则为准,暂不支持充电免停车费,请留意场站页面停车规则提示。
+        </view>
       </view>
     </view>
     <view

+ 85 - 8
src/subPack-smqjh/giveawaysVip/giveawaysVip.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
-const isChecked = ref(false)
+import router from '@/router'
+
 definePage({
   name: 'smqjh-giveaways-vip',
   islogin: true,
@@ -10,23 +11,99 @@ definePage({
 onMounted(() => {
   getGiftsList()
 })
-
-const giveawaysList = ref<Api.giftsListModel>()
+const { selectedAddress } = storeToRefs(useUserStore())
+const giveawaysList = ref<Api.giftsListModel[]>([])
+const isPicking = ref(false)
 async function getGiftsList() {
   const res = await Apis.sys.giftsList({})
-  giveawaysList.value = res.data
+  giveawaysList.value = res.data || []
 }
 function handleChange(value: any) {
   console.log(value)
 }
+
+async function pickUp() {
+  const addressId = selectedAddress.value?.id
+  if (!addressId) {
+    useGlobalToast().show({ msg: '请选择收货地址' })
+    return
+  }
+  if (!giveawaysList.value.length) {
+    useGlobalToast().show({ msg: '暂无可领取赠品' })
+    return
+  }
+
+  const firstGift = giveawaysList.value[0]
+  const hasInvalidGift = giveawaysList.value.some(item => !item.skuId || !item.shopId || !item.channelId)
+  if (hasInvalidGift) {
+    useGlobalToast().show({ msg: '赠品数据异常' })
+    return
+  }
+
+  const hasDifferentShopOrChannel = giveawaysList.value.some((item) => {
+    return item.shopId !== firstGift.shopId || item.channelId !== firstGift.channelId
+  })
+  if (hasDifferentShopOrChannel) {
+    useGlobalToast().show({ msg: '赠品需来自同一门店和渠道' })
+    return
+  }
+
+  if (isPicking.value) {
+    return
+  }
+  isPicking.value = true
+  try {
+    const items = giveawaysList.value.map((item) => {
+      return {
+        skuId: item.skuId!,
+        quantity: Number(item.quantity || 1),
+        shopId: item.shopId!,
+        channelId: item.channelId!,
+      }
+    })
+    const orderInfo = {
+      totalPrice: 0,
+      transfee: 0,
+      offsetPoints: 0,
+      shopName: firstGift.shopName || '',
+      price: 0,
+      amount: 0,
+      coupon: 0,
+      couponName: '',
+      orderCouponItemDTOS: [],
+      memberGiftAddressId: addressId,
+      memberGiftItems: items,
+      skuList: giveawaysList.value.map((item) => {
+        return {
+          prodId: item.spuId || 0,
+          num: Number(item.quantity || 1),
+          pic: item.picUrl,
+          price: '0',
+          shopId: item.shopId!,
+          shopSkuStocks: String(item.stock ?? ''),
+          skuId: item.skuId!,
+          skuName: item.spuName || item.skuName,
+          spec: item.skuName,
+        }
+      }),
+    }
+    router.push({
+      name: 'xsb-confirmOrder',
+      params: { data: JSON.stringify(orderInfo) },
+    })
+  }
+  finally {
+    isPicking.value = false
+  }
+}
 </script>
 
 <template>
   <view class="px24rpx">
     <view class="h-20rpx" />
     <view v-for="item in giveawaysList" :key="item.channelId" class="mb-20rpx flex items-center gap-20rpx rounded-16rpx bg-#FFF p-24rpx">
-      <view class="w-32rpx" @click="isChecked = !isChecked">
-        <wd-icon :name="isChecked ? 'check-circle-filled' : 'circle1'" size="20px" color="#9ED605" />
+      <view class="w-32rpx">
+        <wd-icon name="check-circle-filled" size="20px" color="#9ED605" />
       </view>
       <view class="flex items-center gap-20rpx">
         <image
@@ -47,7 +124,7 @@ function handleChange(value: any) {
             <view class="text-36rpx text-#FF4D3A font-800">
               ¥{{ item.price }}
             </view>
-            <wd-input-number v-model="item.quantity" disabled @change="handleChange" />
+            <wd-input-number :model-value="Number(item.quantity) || 1" disabled @change="handleChange" />
           </view>
         </view>
       </view>
@@ -56,7 +133,7 @@ function handleChange(value: any) {
     <view class="h-180rpx" />
   </view>
   <view class="fixed bottom-0 left-0 right-0 z-9999 h-140rpx w-702rpx rounded-16rpx bg-#FFF p-24rpx shadow-[0rpx_-6rpx_12rpx_2rpx_rgba(0,0,0,0.05)]">
-    <wd-button custom-class="h-80rpx w-full">
+    <wd-button custom-class="h-80rpx w-full" :loading="isPicking" @click="pickUp">
       免费领取
     </wd-button>
   </view>

+ 3 - 3
src/subPack-smqjh/userVip/vip-data.ts

@@ -1,6 +1,6 @@
 import { StaticUrl } from '@/config'
 
-// const { userMemberInfo } = storeToRefs(useUserStore())
+const { userMemberInfo } = storeToRefs(useUserStore())
 export const rightsList = [
   {
     id: 1,
@@ -33,14 +33,14 @@ export const rightsList = [
   {
     id: 5,
     title: '优惠券',
-    desc: '优惠券3张',
+    desc: `优惠券${userMemberInfo.value.couponCount}张`,
     icon: `${StaticUrl}/vip-yhq.png`,
     route: 'xsb-coupon',
   },
   {
     id: 6,
     title: '自选赠品',
-    desc: ``, // 自选赠品${userMemberInfo.value.}个
+    desc: `自选赠品${userMemberInfo.value.giftCount}个`,
     icon: `${StaticUrl}/vip-zxzp.png`,
     route: 'smqjh-giveaways-vip',
   },

+ 8 - 0
src/subPack-xsb/commonTab/components/cart.vue

@@ -211,6 +211,14 @@ onMounted(async () => {
             </view>
           </view>
         </template>
+        <view v-if="totalProduct?.memberDiscountAmount" class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            {{ totalProduct?.memberBenefitDesc }}
+          </view>
+          <view class="text-28rpx text-#FF4A39 font-semibold">
+            -¥{{ totalProduct?.memberDiscountAmount }}
+          </view>
+        </view>
         <view class="flex items-center justify-between py-20rpx">
           <view class="text-28rpx text-#333">
             配送费

+ 22 - 1
src/subPack-xsb/commonTab/components/classfiy.vue

@@ -570,7 +570,20 @@ export default {
                       </view>
                     </view>
                     <view class="flex items-center justify-between">
-                      <view class="text-#FF4D3A font-semibold">
+                      <view v-if="item.isMember" class="flex items-center gap-8rpx text-#FF4D3A">
+                        <text class="text-24rpx">
+                          ¥
+                        </text> <text class="text-30rpx">
+                          {{ item.memberPrice }}
+                        </text>
+                        <view class="rounded-8rpx bg-#FF4A39 px-8rpx py-4rpx text-22rpx text-#FFF">
+                          会员价
+                        </view>
+                        <text class="text-24rpx text-#AAA line-through">
+                          {{ item.channelProdPrice }}
+                        </text>
+                      </view>
+                      <view v-else class="text-#FF4D3A">
                         <text class="text-24rpx">
                         </text> <text class="text-36rpx">
@@ -746,6 +759,14 @@ export default {
             </view>
           </view>
         </template>
+        <view v-if="totalProduct?.memberDiscountAmount" class="flex items-center justify-between py-20rpx">
+          <view class="text-28rpx text-#333">
+            {{ totalProduct?.memberBenefitDesc }}
+          </view>
+          <view class="text-28rpx text-#FF4A39 font-semibold">
+            -¥{{ totalProduct?.memberDiscountAmount }}
+          </view>
+        </view>
         <!-- 配送费 -->
         <view class="flex items-center justify-between py-20rpx">
           <view class="text-28rpx text-#333">

+ 19 - 2
src/subPack-xsb/confirmOrder/index.vue

@@ -28,6 +28,7 @@ const model = reactive<{
   value2: '',
 })
 const deliveryType = ref(1)
+const isMemberGiftOrder = computed(() => !!orderInfo.value?.memberGiftItems?.length)
 
 // 优惠券选择
 const couponPopup = ref(false)
@@ -132,6 +133,16 @@ async function handlePay() {
   }
   isPay.value = true
   try {
+    if (isMemberGiftOrder.value) {
+      await Apis.sys.giftsReceive({
+        data: {
+          addressId: selectedAddress.value.id,
+          items: orderInfo.value.memberGiftItems,
+        },
+      })
+      await useUserStore().paySuccess('xsb-order', 'subPack-xsb/commonTab/index')
+      return
+    }
     const orderItemList = orderInfo.value?.skuList.map((it) => {
       return {
         prodCount: it.num,
@@ -293,7 +304,7 @@ async function handlePay() {
           ¥{{ orderInfo?.transfee }}
         </view>
       </view>
-      <view class="mb28rpx flex items-center justify-between text-28rpx" @click="openCouponPopup">
+      <view v-if="!isMemberGiftOrder" class="mb28rpx flex items-center justify-between text-28rpx" @click="openCouponPopup">
         <view>优惠券</view>
         <view class="flex items-center">
           <view v-if="currentCouponDiscount > 0" class="text-[#FF4D3A] font-semibold">
@@ -305,7 +316,7 @@ async function handlePay() {
           <wd-icon name="arrow-right" size="18px" color="#aaa" class="ml10rpx" />
         </view>
       </view>
-      <view class="flex items-center justify-between text-28rpx">
+      <view v-if="!isMemberGiftOrder" class="flex items-center justify-between text-28rpx">
         <view>积分({{ offsetPoints }})</view>
         <view class="text-#FF4D3A font-semibold">
           - ¥{{ offsetPoints / 100 }}
@@ -321,6 +332,12 @@ async function handlePay() {
         </view>
       </view>
     </view>
+    <view v-if="isMemberGiftOrder" class="mt20rpx flex items-center gap-20rpx rounded-16rpx bg-white p24rpx">
+      <wd-icon name="warning" size="16px" color="#FF4D3A" />
+      <text class="text-24rpx">
+        本商品为会员权益赠品,购买后不支持售后
+      </text>
+    </view>
     <view class="mt20rpx flex items-center rounded-16rpx bg-white p24rpx">
       <view class="w80rpx">
         备注

+ 16 - 2
src/subPack-xsb/orderDetaile/index.vue

@@ -458,16 +458,24 @@ function handleRefundDetail(item: any) {
           </view>
         </view>
         <view class="mt-24rpx flex items-center justify-between">
-          <view v-if="orderInfo.dvyType == 3">
+          <view v-if="orderInfo.dvyType == 3" class="text-28rpx">
             配送费(即时配送)
           </view>
-          <view v-if="orderInfo.dvyType == 1">
+          <view v-if="orderInfo.dvyType == 1" class="text-28rpx">
             快递
           </view>
           <view class="text-[#FF4A39] font-semibold">
             ¥{{ orderInfo?.freightAmount }}
           </view>
         </view>
+        <view v-if="orderInfo?.giftOrder" class="mt-24rpx flex items-center justify-between">
+          <view class="text-28rpx">
+            会员权益(9.5折)
+          </view>
+          <view class="text-[#FF4A39] font-semibold">
+            ¥{{ orderInfo?.memberDiscountAmount }}
+          </view>
+        </view>
         <view class="mt-24rpx flex items-center justify-between">
           <view class="text-28rpx">
             优惠券
@@ -525,6 +533,12 @@ function handleRefundDetail(item: any) {
           </view>
         </view>
       </view>
+      <view v-if="orderInfo?.giftOrder" class="mt20rpx flex items-center gap-20rpx rounded-16rpx bg-white p24rpx">
+        <wd-icon name="warning" size="16px" color="#FF4D3A" />
+        <text class="text-24rpx">
+          本商品为会员权益赠品,购买后不支持售后
+        </text>
+      </view>
       <view class="mt-20rpx rounded-16rpx bg-white p-24rpx">
         <view class="mb-24rpx text-28rpx font-semibold">
           订单信息

+ 12 - 1
src/subPack-xsb/search/index.vue

@@ -167,7 +167,18 @@ function handleSearchText(text: string) {
             <view class="text-32rpx font-semibold">
               {{ item.prodName }}
             </view>
-            <view class="mt14rpx text-36rpx text-#FF4A39 font-semibold">
+            <view v-if="item.isMember" class="mt14rpx flex items-center gap-16rpx">
+              <view class="text-36rpx text-#FF4A39 font-semibold">
+                ¥{{ item.memberPrice }}
+              </view>
+              <view class="rounded-8rpx bg-#FF4A39 px-8rpx py-4rpx text-22rpx text-#FFF">
+                会员价
+              </view>
+              <view class="text-24rpx text-#AAA line-through">
+                ¥{{ item.channelProdPrice }}
+              </view>
+            </view>
+            <view v-else class="mt14rpx text-36rpx text-#FF4A39 font-semibold">
               ¥{{ item.channelProdPrice }}
             </view>
           </view>