2 Комити 5d7236dcd9 ... 64b2cc4347

Аутор SHA1 Порука Датум
  zouzexu 64b2cc4347 ``` пре 3 недеља
  zouzexu 9f2f10bc70 ``` пре 3 недеља

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

@@ -93,6 +93,227 @@ namespace Api {
     icon: string
     children: xsbCategoriesChildren[]
   }
+  /**
+   * MemberAppInfoVO,小程序当前登录用户会员信息,用于会员中心、首页弹窗和权益展示。
+   */
+  export interface userMemberInfo {
+    /**
+     * 是否为有效会员。true 表示当前会员权益生效中。
+     */
+    active?: boolean
+    /**
+     * 会员权益配置 JSON。包含电商折扣 discountRate、充电每度立减 chargePerKwhDiscount 等配置。
+     */
+    benefitConfigJson?: string
+    /**
+     * 权益明细数组。每项至少包含 benefitType、benefitName、benefitDesc、useStatus。
+     */
+    benefitDetails?: MemberBenefitRecordVO[]
+    /**
+     * 优惠券自动发放配置 JSON。包含优惠券 ID、发放张数等配置。
+     */
+    couponConfigJson?: string
+    /**
+     * 会员生效时间。第三方回调确认后生效;为空时按系统规则生成。
+     */
+    effectiveTime?: Date
+    /**
+     * 会员到期时间。到期后会员权益失效。
+     */
+    expireTime?: Date
+    /**
+     * 自选赠品配置 JSON。包含可领取的赠品 SKU、数量等配置。
+     */
+    giftConfigJson?: string
+    /**
+     * 自选赠品是否已领取。true 表示已领取,不可重复领取。
+     */
+    giftReceived?: boolean
+    /**
+     * 开通后首次进入小程序展示弹窗用。接口返回 true 后会在服务端写入已读记录,后续再次调用返回 false。
+     */
+    isFirstLogin?: boolean
+    /**
+     * 会员账号 ID。一次会员开通对应一条会员账号记录。
+     */
+    memberAccountId?: number
+    /**
+     * 系统会员用户 ID。
+     */
+    memberId?: number
+    /**
+     * 会员状态枚举:ACTIVE=生效中,PENDING_EXPIRE=待失效,EXPIRED=已失效。
+     */
+    memberStatus?: string
+    /**
+     * 会员类型 ID,对应后台会员类型配置。
+     */
+    memberTypeId?: number
+    /**
+     * 会员类型名称。
+     */
+    memberTypeName?: string
+    /**
+     * 权益描述文案,用于后台列表或小程序展示。
+     */
+    rightsDesc?: string
+    [property: string]: any
+  }
+
+  /**
+   * MemberBenefitRecordVO,会员权益发放记录。包含优惠券、自选赠品发放结果及关联单号。
+   */
+  export interface MemberBenefitRecordVO {
+    /**
+     * 权益说明/权益发放备注,用于小程序或后台展示权益描述。
+     */
+    benefitDesc?: string
+    /**
+     * 权益名称。
+     */
+    benefitName?: string
+    /**
+     * 权益类型。COUPON 表示优惠券,GIFT 表示自选赠品。
+     */
+    benefitType?: string
+    /**
+     * 权益内容或配置摘要。
+     */
+    benefitValue?: string
+    /**
+     * 关联业务单号,如优惠券发放记录号、赠品订单号。
+     */
+    businessNo?: string
+    /**
+     * 创建时间。
+     */
+    createTime?: Date
+    /**
+     * 发放状态。1=发放成功,0=发放失败。
+     */
+    grantStatus?: number
+    /**
+     * 主键 ID。
+     */
+    id?: number
+    /**
+     * 会员账号 ID。一次会员开通对应一条会员账号记录。
+     */
+    memberAccountId?: number
+    /**
+     * 系统会员用户 ID。
+     */
+    memberId?: number
+    /**
+     * 备注说明。
+     */
+    remark?: string
+    /**
+     * 权益使用/发放状态。1=成功/可用,0=失败/不可用。
+     */
+    useStatus?: number
+    [property: string]: any
+  }
+  /**
+   * MemberGiftVO,会员自选赠品信息。用户只能在配置的赠品中多选一领取一次。
+   */
+  export interface giftsListModel {
+    /**
+     * 渠道/企业 ID。
+     */
+    channelId?: number
+    /**
+     * 赠品图片地址。
+     */
+    picUrl?: string
+    /**
+     * 商品原价/销售价,单位元。会员价根据该价格和 discountRate 计算。
+     */
+    price?: number
+    /**
+     * 领取数量或商品数量。赠品场景通常为配置的赠品数量。
+     */
+    quantity?: number | string
+    /**
+     * 当前赠品是否已领取。
+     */
+    received?: boolean
+    /**
+     * 门店 ID。
+     */
+    shopId?: number
+    /**
+     * 商品 SKU ID。
+     */
+    skuId?: number
+    /**
+     * SKU 名称/规格名称。
+     */
+    skuName?: string
+    /**
+     * 商品 SPU ID。
+     */
+    spuId?: number
+    /**
+     * SPU 商品名称。
+     */
+    spuName?: string
+    /**
+     * 库存数量。
+     */
+    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 {
     /**
      * 多规格

+ 3 - 0
src/api/apiDefinitions.ts

@@ -36,6 +36,9 @@ export default {
   'sys.updateUserInfo': ['PUT', '/smqjh-system/app-api/v1/members/{memberId}'],
   '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.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'],

+ 46 - 1
src/api/globals.d.ts

@@ -203,6 +203,51 @@ declare global {
       >(
         config: Config
       ): Alova2Method<listData<Api.sysDict>, 'sys.dictPage', Config>;
+
+      userVipInfo<
+        Config extends Alova2MethodConfig<apiResData<Api.userMemberInfo>> & {}
+      >(
+        config: Config
+      ): Alova2Method<apiResData<Api.userMemberInfo>, 'sys.userVipInfo', Config>;
+
+      giftsList<
+        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.giftsReceiveModel>, 'sys.giftsReceive', Config>;
     }
     xsb: {
       orderCoupons<
@@ -1323,7 +1368,7 @@ declare global {
       ): Alova2Method<apiResData<string>, 'refueling.getPayCode', Config>;
       cancelOrder<
         Config extends Alova2MethodConfig<apiResData<any>> & {
-         params: {
+          params: {
             /**
              * 订单ID
              */

+ 1 - 0
src/components.d.ts

@@ -42,6 +42,7 @@ declare module 'vue' {
     WdRadioGroup: typeof import('wot-design-uni/components/wd-radio-group/wd-radio-group.vue')['default']
     WdRate: typeof import('wot-design-uni/components/wd-rate/wd-rate.vue')['default']
     WdSearch: typeof import('wot-design-uni/components/wd-search/wd-search.vue')['default']
+    WdSegmented: typeof import('wot-design-uni/components/wd-segmented/wd-segmented.vue')['default']
     WdSkeleton: typeof import('wot-design-uni/components/wd-skeleton/wd-skeleton.vue')['default']
     WdSortButton: typeof import('wot-design-uni/components/wd-sort-button/wd-sort-button.vue')['default']
     WdStatusTip: typeof import('wot-design-uni/components/wd-status-tip/wd-status-tip.vue')['default']

+ 2 - 1
src/config/index.ts

@@ -12,8 +12,9 @@ const mapEnvVersion = {
   // develop: 'http://192.168.1.21:8080', // 田
   // develop: 'http://74949mkfh190.vicp.fun', // 付
   // 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',
+  // develop: 'https://smqjh.api.zswlgz.com',
   /**
    * 体验版
    */

+ 2 - 2
src/pages.json

@@ -297,7 +297,7 @@
         {
           "path": "giveawaysVip/giveawaysVip",
           "name": "smqjh-giveaways-vip",
-          "islogin": false,
+          "islogin": true,
           "style": {
             "navigationBarTitleText": "选择赠品"
           }
@@ -313,7 +313,7 @@
         {
           "path": "userVip/userVip",
           "name": "smqjh-user-vip",
-          "islogin": false,
+          "islogin": true,
           "style": {
             "navigationBarTitleText": "会员中心",
             "navigationStyle": "custom"

+ 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
+}

+ 39 - 5
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'
 
@@ -19,7 +20,7 @@ const { show } = useGlobalToast()
 const addressStore = useAddressStore()
 const { statusBarHeight, MenuButtonHeight, opcity, isOnlineAudit } = storeToRefs(useSysStore())
 const { name } = storeToRefs(addressStore)
-const { userInfo } = storeToRefs(useUserStore())
+const { userInfo, userMemberInfo } = storeToRefs(useUserStore())
 const xsbStore = ref<typeof import('@/subPack-xsb/store-xsb/sys')>()
 const { data: goodsList, isLastPage, page, reload, error, refresh } = usePagination((pageNum, pageSize) =>
   Apis.xsb.getSearchProductList({ data: { pageNum, pageSize, salesNum: 'DESC', shopId: xsbStore.value?.useSysXsbStore().SelectShopInfo.shopId || 2, channelId: userInfo.value.channelId || 1 } }), {
@@ -37,6 +38,7 @@ const currentIndex = ref(0)
 const loading = ref(true)
 onShow(async () => {
   useSysStore().getAudit()
+  useUserStore().getUserMemberInfo()
   useSmqjhCartStore().getCartList('XSB')
   getSelectZhUser()
   xsbStore.value = await AsyncImport('@/subPack-xsb/store-xsb/sys')
@@ -68,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
@@ -192,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"
             >
@@ -272,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>
@@ -328,11 +358,11 @@ function handleJyBanner() {
             </view>
           </view>
         </wd-overlay>
-        <wd-overlay :show="showoverlay2" @click="showoverlay2 = false">
+        <wd-overlay :show="showoverlay2 && userMemberInfo.isFirstLogin" @click="showoverlay2 = false">
           <view class="mt-280rpx flex items-center justify-center">
             <view class="relative h-906rpx w-644rpx flex flex-col items-center justify-center text-center" :style="{ backgroundImage: `url(${StaticUrl}/vip-index-popup.png)`, backgroundSize: 'cover', backgroundPosition: 'center' }">
               <view class="absolute top-120rpx w-406rpx border-[1rpx_solid_#FFFFFF] rounded-50rpx border-solid px-30rpx py-8rpx text-center text-24rpx text-#FFF">
-                您的会员权益已生效有效期至2027-04-08 23:59:59
+                您的会员权益已生效有效期至{{ userMemberInfo.expireTime }}
               </view>
               <image :src="`${StaticUrl}/vip-index-check.png`" class="absolute top-690rpx h-84rpx w-372rpx" @click="router.push({ name: 'smqjh-user-vip' })" />
               <image :src="`${StaticUrl}/vip-index-shopping.png`" class="absolute top-800rpx h-84rpx w-288rpx" @click="router.push({ name: 'xsb-homeTabbar' })" />
@@ -365,5 +395,9 @@ function handleJyBanner() {
     background: rgba(0, 0, 0, .3);
   }
 
+  .transition-height {
+    transition: height 0.3s ease;
+  }
+
 }
 </style>

+ 14 - 9
src/pages/my/index.vue

@@ -21,7 +21,7 @@ const tabList = ref([
 onMounted(() => {
   useUserStore().getUserInfo()
 })
-const { token, userInfo, getUserAvatar } = storeToRefs(useUserStore())
+const { token, userInfo, getUserAvatar, userMemberInfo } = storeToRefs(useUserStore())
 function handleLoginOut() {
   useGlobalMessage().confirm({
     title: '提示',
@@ -59,10 +59,10 @@ function handleGo(item: { name: string, status: string }) {
         </template>
         <template v-else>
           <view class="flex items-center">
-            <view class="relative" @click="router.push({ name: 'smqjh-user-vip' })">
-              <image :src="`${StaticUrl}/user-head-vip.png`" class="absolute right-[-20rpx] top-[-20rpx] h-50rpx w-50rpx" />
+            <view class="relative">
+              <image v-if="userMemberInfo.active" :src="`${StaticUrl}/user-head-vip.png`" class="absolute right-[-20rpx] top-[-20rpx] h-50rpx w-50rpx" />
               <image :src="getUserAvatar" alt="" class="h-100rpx w-100rpx flex-shrink-0 rounded-full" />
-              <view class="absolute left-[-6rpx] top-90rpx h-40rpx w-112rpx rounded-22rpx bg-[linear-gradient(132deg,#F0C568_0%,#FFF3B2_20.02%,#EBBA5E_44.19%,#FFF3B2_71.13%,#F0C26C_100%)] text-center text-24rpx text-#7F5935 line-height-40rpx">
+              <view v-if="userMemberInfo.active" class="absolute left-[-6rpx] top-90rpx h-40rpx w-112rpx rounded-22rpx bg-[linear-gradient(132deg,#F0C568_0%,#FFF3B2_20.02%,#EBBA5E_44.19%,#FFF3B2_71.13%,#F0C26C_100%)] text-center text-24rpx text-#7F5935 line-height-40rpx">
                 尊贵会员
               </view>
             </view>
@@ -75,13 +75,13 @@ function handleGo(item: { name: string, status: string }) {
               </view>
             </view>
           </view>
-          <view class="flex flex-col items-center" @click="router.push({ name: 'common-user-center' })">
+          <view class="flex flex-col items-center" @click="router.push({ name: 'smqjh-user-vip' })">
             <image
-              :src="`${StaticUrl}/user-setting.png`"
-              class="h-48rpx w-48rpx"
+              :src="`${StaticUrl}/vip-center-icon.png`"
+              class="h-56rpx w-66rpx"
             />
-            <view class="mt-12rpx text-24rpx text-[var(--them-color)]">
-              账户设置
+            <view class="mt-12rpx text-24rpx text-#7F5935">
+              会员中心
             </view>
           </view>
         </template>
@@ -131,6 +131,11 @@ function handleGo(item: { name: string, status: string }) {
               </view>
             </template>
           </wd-cell>
+          <wd-cell title="账户设置" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'common-user-center' })">
+            <template #icon>
+              <image :src="`${StaticUrl}/mine-setting-icon.png`" class="h-50rpx w-50rpx" />
+            </template>
+          </wd-cell>
         </wd-cell-group>
       </wd-card>
     </view>

+ 19 - 0
src/store/user.ts

@@ -27,6 +27,10 @@ interface userStroe {
    * 市民请集合购物车用户选中的收货地址
    */
   smqjhSelectedAddress: Api.addressList | null
+  /**
+   * 用户会员信息
+   */
+  userMemberInfo: Api.userMemberInfo
 }
 export const useUserStore = defineStore('user', {
   state: (): userStroe => ({
@@ -40,6 +44,11 @@ export const useUserStore = defineStore('user', {
     addresses: [],
     selectedAddress: null,
     smqjhSelectedAddress: null,
+    userMemberInfo: {
+      active: false,
+      isFirstLogin: false,
+      expireTime: undefined,
+    },
   }),
   getters: {
     getUserAvatar(): string {
@@ -54,11 +63,21 @@ export const useUserStore = defineStore('user', {
       if (this.token) {
         const { data } = await api.sys.userInfo({})
         this.userInfo = data
+        await this.getUserMemberInfo()
         await this.getuserAddresslist()
         this.getSelectedAddress()
         await useSmqjhCartStore().getCartList('XSB')
       }
     },
+    /**
+     * 获取用户会员信息
+     */
+    async getUserMemberInfo() {
+      if (this.token) {
+        const { data } = await Apis.sys.userVipInfo({})
+        this.userMemberInfo = data
+      }
+    },
     async updataUserInfo(data: Api.userInfo) {
       uni.showLoading({ mask: true })
       await Apis.sys.updateUserInfo({ pathParams: { memberId: data.id }, data })

+ 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

+ 142 - 0
src/subPack-smqjh/giveawaysVip/giveawaysVip.vue

@@ -0,0 +1,142 @@
+<script setup lang="ts">
+import router from '@/router'
+
+definePage({
+  name: 'smqjh-giveaways-vip',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '选择赠品',
+  },
+})
+onMounted(() => {
+  getGiftsList()
+})
+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 || []
+}
+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">
+        <wd-icon name="check-circle-filled" size="20px" color="#9ED605" />
+      </view>
+      <view class="flex items-center gap-20rpx">
+        <image
+          :src="item.picUrl"
+          class="h-200rpx w-200rpx rounded-16rpx"
+        />
+        <view>
+          <view class="text-32rpx font-bold line-height-40rpx">
+            <text class="mr-8rpx rounded-8rpx bg-#FF4D3A px-12rpx py-4rpx text-22rpx text-#FFF">
+              赠品
+            </text>
+            {{ item.spuName }}
+          </view>
+          <view class="mt-14rpx text-24rpx text-#AAA">
+            规格:{{ item.skuName }}
+          </view>
+          <view class="mt-14rpx flex items-center justify-between">
+            <view class="text-36rpx text-#FF4D3A font-800">
+              ¥{{ item.price }}
+            </view>
+            <wd-input-number :model-value="Number(item.quantity) || 1" disabled @change="handleChange" />
+          </view>
+        </view>
+      </view>
+    </view>
+    <StatusTip v-if="giveawaysList?.length === 0" tip="暂无内容" />
+    <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" :loading="isPicking" @click="pickUp">
+      免费领取
+    </wd-button>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 126 - 0
src/subPack-smqjh/userVip/userVip.vue

@@ -0,0 +1,126 @@
+<script setup lang="ts">
+import { rightsList } from './vip-data'
+import router from '@/router'
+import { StaticUrl } from '@/config'
+
+const { statusBarHeight, MenuButtonHeight, opcity } = storeToRefs(useSysStore())
+definePage({
+  name: 'smqjh-user-vip',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '会员中心',
+    navigationStyle: 'custom',
+  },
+
+})
+onMounted(() => {
+  opcity.value = 0
+})
+const { userMemberInfo } = storeToRefs(useUserStore())
+onPageScroll((e) => {
+  const calculatedOpacity = e.scrollTop / 100
+  opcity.value = Math.min(1, Math.max(0.1, calculatedOpacity))
+})
+
+function toUse(value: any) {
+  if (value.id === 1) {
+    wx.openOfficialAccountArticle({
+      url: 'https://mp.weixin.qq.com/s/lxpdZ6DUhgqg00AT9klu5Q',
+    })
+  }
+  if (value.route === '') {
+    useGlobalToast().show('该权益正在快马加鞭赶来~')
+  }
+  else {
+    router.push({ name: value.route })
+  }
+}
+</script>
+
+<template>
+  <view class="min-h-screen" :class="userMemberInfo.active ? 'bg-[linear-gradient(225deg,#13141D_0%,#55565D_34.72%,#15161F_100%)]' : ''">
+    <wd-navbar
+      title="会员中心" :custom-style="`background-color: rgba(226, 255, 145, ${opcity});color: ${opcity > 0.5 ? '#000' : '#FFF'};`"
+      :bordered="false" :z-index="999" safe-area-inset-top left-arrow fixed @click-left="router.back()"
+    />
+    <view :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" />
+    <view v-if="userMemberInfo.active" class="px24rpx">
+      <view class="mt-54rpx">
+        <image :src="`${StaticUrl}/vip-welcome.png`" class="h-46rpx w-339rpx" />
+        <view class="mt-36rpx text-28rpx text-#b5b5b5">
+          甄选权益,开启您的购物之旅
+        </view>
+      </view>
+      <view class="mt-36rpx h-370rpx pl-50rpx" :style="{ backgroundImage: `url(${StaticUrl}/vip-card.png)`, backgroundSize: 'cover', backgroundPosition: 'center' }">
+        <image :src="`${StaticUrl}/vip-tags.png`" class="mt-72rpx h-54rpx w-238rpx" />
+        <view class="mt-20rpx text-24rpx text-#b5b5b5">
+          <text v-if="userMemberInfo.memberStatus === 'ACTIVE'">
+            有效期至
+          </text>
+          <text v-if="userMemberInfo.memberStatus === 'PENDING_EXPIRE'">
+            会员已退订
+          </text>
+          <text v-if="userMemberInfo.memberStatus === 'EXPIRED'">
+            会员已失效
+          </text>
+        </view>
+        <view class="mt-20rpx text-28rpx text-#b5b5b5">
+          <text v-if="userMemberInfo.memberStatus === 'PENDING_EXPIRE'">
+            权益将于
+          </text>
+          <text v-if="userMemberInfo.memberStatus === 'EXPIRED'">
+            失效时间
+          </text>
+        </view>
+        <view class="mt-20rpx text-28rpx text-#b5b5b5">
+          {{ userMemberInfo.expireTime }}
+          <text v-if="userMemberInfo.memberStatus != 'ACTIVE'">
+            失效
+          </text>
+        </view>
+      </view>
+      <view class="mt-30rpx">
+        <view class="flex items-center justify-center gap-36rpx">
+          <view class="flex items-center gap-6rpx">
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(30deg);" />
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(30deg);" />
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(30deg);" />
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(30deg);" />
+          </view>
+          <view class="text-36rpx text-#FEE3AD font-800">
+            我的权益
+          </view>
+          <view class="flex items-center gap-6rpx">
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(-30deg);" />
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(-30deg);" />
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(-30deg);" />
+            <view class="h-16rpx w-8rpx bg-#FEE3AD" style="transform: skew(-30deg);" />
+          </view>
+        </view>
+      </view>
+      <view class="mt-44rpx flex flex-wrap gap-20rpx">
+        <view v-for="value in rightsList" :key="value.id" class="w-304rpx border-[1rpx_solid_#F4E5BD] rounded-32rpx bg-[rgba(255,255,255,0.3)] p-20rpx">
+          <view class="text-28rpx text-#FEE3AD font-bold">
+            {{ value.title }}
+          </view>
+          <view class="mt-20rpx flex items-center justify-between">
+            <view>
+              <view class="text-28rpx text-#b5b5b5">
+                {{ value.desc }}
+              </view>
+              <view class="mt-16rpx h-42rpx w-108rpx rounded-22rpx bg-#F4E5BD text-center text-24rpx text-#7F5935 line-height-42rpx" @click="toUse(value)">
+                去使用
+              </view>
+            </view>
+            <image :src="value.icon" class="h-80rpx w-80rpx" />
+          </view>
+        </view>
+      </view>
+    </view>
+    <StatusTip v-else image="vip-not-data.png" tip="敬请期待" />
+  </view>
+</template>
+
+<style lang="scss" scoped>
+
+</style>

+ 47 - 0
src/subPack-smqjh/userVip/vip-data.ts

@@ -0,0 +1,47 @@
+import { StaticUrl } from '@/config'
+
+const { userMemberInfo } = storeToRefs(useUserStore())
+export const rightsList = [
+  {
+    id: 1,
+    title: '全省中石化优惠',
+    desc: '每升立减0.3元',
+    icon: `${StaticUrl}/vip-zshyh.png`,
+    route: '',
+  },
+  {
+    id: 2,
+    title: '市民请集合平台快消品',
+    desc: '折扣率9.5折',
+    icon: `${StaticUrl}/vip-ptkxp.png`,
+    route: 'xsb-homeTabbar',
+  },
+  {
+    id: 3,
+    title: '全省贵阳城投充电桩优惠',
+    desc: '每度电立减0.03元',
+    icon: `${StaticUrl}/vip-cdzyh.png`,
+    route: 'charge-index',
+  },
+  {
+    id: 4,
+    title: '贵阳城投停车场优惠',
+    desc: '折扣率9.5折',
+    icon: `${StaticUrl}/vip-tccyh.png`,
+    route: '',
+  },
+  {
+    id: 5,
+    title: '优惠券',
+    desc: `优惠券${userMemberInfo.value.couponCount}张`,
+    icon: `${StaticUrl}/vip-yhq.png`,
+    route: 'xsb-coupon',
+  },
+  {
+    id: 6,
+    title: '自选赠品',
+    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">

+ 72 - 6
src/subPack-xsb/confirmOrder/index.vue

@@ -18,8 +18,17 @@ const remarks = ref('')
 const initialCouponId = ref<string | undefined>(undefined)
 const confirmedCouponId = ref<string | undefined>(undefined)
 const draftCouponId = ref<string | undefined>(undefined)
-
+const list = ref<string[]>(['即时配送', '自提'])
+const current = ref('即时配送')
+const model = reactive<{
+  value1: string
+  value2: string
+}>({
+  value1: '',
+  value2: '',
+})
 const deliveryType = ref(1)
+const isMemberGiftOrder = computed(() => !!orderInfo.value?.memberGiftItems?.length)
 
 // 优惠券选择
 const couponPopup = ref(false)
@@ -124,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,
@@ -164,8 +183,10 @@ async function handlePay() {
 
 <template>
   <view class="page px20rpx py20rpx">
+    <wd-segmented v-model:value="current" :options="list" />
     <view
-      class="mb20rpx rounded-16rpx bg-white p24rpx"
+      v-if="current === '即时配送'"
+      class="my20rpx rounded-16rpx bg-white p24rpx"
       @click="router.push({ name: 'common-addressList', params: { type: 'select' } })"
     >
       <view class="flex items-center justify-between">
@@ -197,6 +218,45 @@ async function handlePay() {
       </view>
     </view>
 
+    <view v-else class="my20rpx rounded-16rpx bg-white p24rpx">
+      <view class="flex items-center gap-50rpx">
+        <view>
+          <view class="text-28rpx font-semibold">
+            自提点:星闪豹(贵阳店)
+          </view>
+          <view class="mt-20rpx text-24rpx text-#AAAAAA">
+            贵州省贵阳市贵阳市观山湖区金麦安置小区12组团26号-28号门面
+          </view>
+        </view>
+        <view>
+          <view class="mb-20rpx text-24rpx text-#AAAAAA">
+            距你XXkm
+          </view>
+          <wd-icon name="location" size="22px" color="#9ED605" />
+        </view>
+      </view>
+      <view>
+        <wd-input
+          v-model="model.value1"
+          label="联系人"
+          label-width="80px"
+          prop="value1"
+          clearable
+          placeholder="请输入联系人"
+          :rules="[{ required: true, message: '请填写联系人' }]"
+        />
+        <wd-input
+          v-model="model.value2"
+          label="预留电话"
+          label-width="80px"
+          prop="value1"
+          clearable
+          placeholder="请输入预留电话"
+          :rules="[{ required: true, pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/, message: '请输入正确的手机号' }]"
+        />
+      </view>
+    </view>
+
     <view class="rounded-16rpx bg-white p24rpx">
       <view class="flex items-center text-28rpx font-semibold">
         {{ orderInfo?.shopName }}
@@ -244,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">
@@ -256,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 }}
@@ -272,12 +332,18 @@ 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">
         备注
       </view>
       <view class="flex-1">
-        <wd-input v-model="remarks" placeholder="选填,请先和商家协商一致,付款后商家可见" no-border clearable />
+        <wd-input v-model="remarks" placeholder="选填,请先和商家协商一致,付款后商家可见" clearable no-border />
       </view>
     </view>
     <view class="h250rpx" />
@@ -356,7 +422,7 @@ async function handlePay() {
         </view>
       </template>
     </Zpopup>
-    <view class="ios footer fixed bottom-0 left-0 box-border w-full rounded-t-16rpx bg-white px24rpx">
+    <view class="ios footer fixed bottom-0 left-0 z-1000 box-border w-full rounded-t-16rpx bg-white px24rpx">
       <view class="box-border w-full flex items-center justify-between py20rpx">
         <view class="flex items-center text-#FF4D3A">
           <view class="font-semibold10 flex items-baseline text-36rpx">

+ 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>