Browse Source

refactor(mall): 重构积分商城相关功能

- 优化商品详情页面布局和逻辑
- 更新购物车页面,增加商品状态显示
- 调整首页活动倒计时样式
- 重构产品列表组件,支持不同积分结算方式显示
- 更新相关请求和模型定义,提高代码可维护性
EvilDragon 3 months ago
parent
commit
3c58752101

+ 103 - 0
packages/app/src/core/libs/models.ts

@@ -502,6 +502,109 @@ export interface Banner {
   designDesc: any
   createTime: number
 }
+export interface Product {
+  /* id */
+  id: number
+
+  /* 商品名称 */
+  prodcutName: string
+
+  /* 商品id */
+  productId: string
+
+  /* 商品一级类目 */
+  oneCategory: string
+
+  /* 商品一级类目 */
+  oneCategoryName: string
+
+  /* 商品二级类目 */
+  secondCategory: string
+
+  /* 商品二级类目 */
+  secondCategoryName: string
+
+  /* 商品数量是否限制 0:否 1:是 */
+  isRestrict: number
+
+  /* 商品市场价 */
+  productPrice: string
+
+  /* 商品类别 */
+  productType: number
+
+  /* 商品库存 */
+  productRepertory: number
+
+  /* 供应商id */
+  vendorId: number
+
+  /* 供应商名称 */
+  vendorName: string
+
+  /* 供应商状态,可用值:0,1 */
+  vendorStatus: string
+
+  /* 是否需要积分 0:否 1:是 */
+  needPoints: number
+
+  /* 积分大小 */
+  points: number
+
+  /* 获取方式 1:到店核销 3:其他 */
+  gainType: number
+
+  /* 兑换说明 */
+  exchangeDesc: string
+
+  /* 商品封面地址 */
+  productCoverImgUrl: string
+
+  /* 商品详情地址 */
+  productDetailsImgUrl: string
+
+  /* 图文详情 */
+  contentDesc: string
+
+  /* 状态(0正常 1停用) */
+  status: number
+
+  /* */
+  statusStr: string
+
+  /* 兑换量 */
+  exchangeCount: number
+
+  /* 会员等级ids */
+  memberLevelIds: string
+
+  /* 会员等级名称 */
+  memberLevelName: string
+
+  /* 超值划算积分 */
+  favourablePoints: number
+
+  /* 是否展示超值划算 */
+  showFavourable: boolean
+
+  /* 超值划算截止时间 */
+  favourableEndDate: Record<string, unknown>
+
+  /* 是否限购 */
+  purchaseLimit: boolean
+
+  /* 限购数量 */
+  purchaseQuantity: number
+
+  /* 创建时间 */
+  createTime: Record<string, unknown>
+
+  /* 更新时间 */
+  updateTime: Record<string, unknown>
+
+  /* 排序 */
+  sort: number
+}
 export interface PointsOrder {
   id: number
   orderType: number

+ 3 - 31
packages/app/src/core/libs/requests.ts

@@ -21,7 +21,7 @@ import {
   Badge,
   Certificate,
   UserBasicInfo,
-  ActivitySignUp,
+  ActivitySignUp, Product,
 } from './models'
 import dayjs from 'dayjs'
 
@@ -408,36 +408,7 @@ export const getFavourableProducts = () =>
     }[]
   >('/app-api/member/product/listFavourableProduct')
 export const getProduct = (id: string) =>
-  httpGet<{
-    id: number
-    prodcutName: string
-    productId: string
-    oneCategory: any
-    oneCategoryName: any
-    secondCategory: string
-    secondCategoryName: any
-    isRestrict: number
-    productRepertory: any
-    productPrice: string
-    productType: number
-    vendorId: any
-    vendorName: string
-    needPoints: number
-    points: number
-    gainType: number
-    exchangeDesc: string
-    productCoverImgUrl: string
-    productDetailsImgUrl: string
-    contentDesc: string
-    status: number
-    exchangeCount: any
-    memberLevelId: any
-    memberLevelName: any
-    favourablePoints: any
-    favourableEndDate: any
-    createTime: number
-    updateTime: number
-  }>('/app-api/member/product/detail', { productId: id })
+  httpGet<Product>('/app-api/member/product/detail', { productId: id })
 export const getProductItemBuy = (query: { userId: number }) =>
   httpPost<{
     list: Partial<{
@@ -469,6 +440,7 @@ export const getProductItemBuy = (query: { userId: number }) =>
       createTime: any
       userId: number
       nums: number
+      deleted?: boolean
     }>[]
     total: number
   }>('/app-api/member/product-item-buy/select', query)

+ 29 - 11
packages/app/src/pages/home/mall/components/product.vue

@@ -6,6 +6,7 @@ import { requestToast } from '../../../../core/utils/common'
 import { createProductItemBuy } from '../../../../core/libs/requests'
 import { useUserStore } from '../../../../store'
 import { storeToRefs } from 'pinia'
+import { needLoginPages } from '@/utils'
 
 const props = defineProps({
   options: {
@@ -51,12 +52,13 @@ const handleAddToCart = async () => {
       ></wd-img>
     </div>
     <div class="flex">
-      <div class="text-black/90 text-base font-normal font-['PingFang_SC'] leading-normal">
+      <div class="text-black/90 text-base font-normal font-['PingFang_SC'] leading-normal line-clamp-1 text-ellipsis overflow-hidden">
         <!-- 阿芙佳朵 -->
         {{ options.prodcutName }}
       </div>
       <div class="flex-1"></div>
       <div
+        v-if="Number(options.productPrice)"
         class="w-[26px] text-black/30 text-xs font-normal font-['PingFang_SC'] line-through leading-normal"
       >
         <!-- ¥60 -->
@@ -64,17 +66,33 @@ const handleAddToCart = async () => {
       </div>
     </div>
     <div class="flex items-center mb-6">
-      <div class="flex items-end gap-1">
-        <div class="text-[#ef4343] text-[22px] font-normal font-['D-DIN_Exp'] leading-5.5">
-          <!-- 1000 -->
-          {{ options.points }}
+      <template v-if="String(options.needPoints) === '0'">
+        <div class="flex items-end gap-1">
+          <div class="text-[#ef4343] text-[22px] font-normal font-['D-DIN_Exp'] leading-5.5">
+            <!-- 1000 -->
+            {{ options.points }}
+          </div>
+          <div class="text-black/60 text-sm font-normal font-['PingFang_SC']">积分</div>
         </div>
-        <div class="text-black/60 text-sm font-normal font-['PingFang_SC']">积分</div>
-      </div>
-      <div class="flex-1"></div>
-      <div class="" @click.stop="handleAddToCart">
-        <wd-img width="32" height="32" :src="addBlack"></wd-img>
-      </div>
+        <div class="flex-1"></div>
+        <div class="" @click.stop="handleAddToCart">
+          <wd-img width="32" height="32" :src="addBlack"></wd-img>
+        </div>
+      </template>
+      <template v-if="String(options.needPoints) === '1'">
+        <div class="flex items-end gap-1">
+          <div class="text-[#ef4343] text-[22px] font-normal font-['D-DIN_Exp'] leading-5.5">
+            <!-- 1000 -->
+            {{ options.points }}
+          </div>
+          <div class="text-black/60 text-sm font-normal font-['PingFang_SC']">折</div>
+          <div class="text-black/60 text-sm font-normal font-['PingFang_SC']">(积分结算)</div>
+        </div>
+        <div class="flex-1"></div>
+        <!--        <div class="" @click.stop="handleAddToCart">-->
+        <!--          <wd-img width="32" height="32" :src="addBlack"></wd-img>-->
+        <!--        </div>-->
+      </template>
     </div>
   </div>
 </template>

+ 58 - 67
packages/app/src/pages/home/mall/detail/index.vue

@@ -8,11 +8,6 @@
 </route>
 
 <script setup lang="ts">
-import TiltedButton from '@/components/tilted-button.vue'
-import Product from '../components/product.vue'
-import { shoppingBag } from '@designer-hub/assets/src/assets/svgs/index'
-import InvertedTrapezoidButton from '@/components/inverted-trapezoid-button.vue'
-import TrapeziumButton from '@/components/trapezium-button.vue'
 import { useRouter } from '../../../../core/utils/router'
 import { createProductItemBuy, getProduct, productPlacing } from '../../../../core/libs/requests'
 import { requestToast } from '../../../../core/utils/common'
@@ -21,6 +16,7 @@ import { storeToRefs } from 'pinia'
 import BottomAppBar from '@/components/bottom-app-bar.vue'
 import ButtonEvo from '@/components/button-evo.vue'
 import { usePermissions } from '../../../../composables/permissions'
+import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html.vue'
 
 const { clickByPermission } = usePermissions()
 const userStore = useUserStore()
@@ -33,6 +29,8 @@ const type = ref<'add2Cart' | 'orderNow'>()
 const { data, run: setData } = useRequest(() => getProduct(id.value))
 
 const handleConfirm = async () => {
+  // 积分
+  const points = data.value?.showFavourable ? data.value?.favourablePoints : data.value?.points
   if (type.value === 'orderNow') {
     const { data: res, code } = await requestToast(() =>
       productPlacing({
@@ -42,7 +40,7 @@ const handleConfirm = async () => {
         list: [
           {
             productId: id.value,
-            points: data.value.points,
+            points,
             nums: nums.value,
             productName: data.value.prodcutName,
             orderImgUrl: data.value.productCoverImgUrl,
@@ -53,7 +51,7 @@ const handleConfirm = async () => {
       }),
     )
     if (code !== 0) return
-    router.push(`/pages/home/mall/confirm-order/index?data=${JSON.stringify(res)}`)
+    await router.push(`/pages/home/mall/confirm-order/index?data=${JSON.stringify(res)}`)
   }
   if (type.value === 'add2Cart') {
     await requestToast(
@@ -63,7 +61,7 @@ const handleConfirm = async () => {
             {
               userId: userInfo.value.userId,
               productId: data.value?.productId || '',
-              points: data.value?.points,
+              points,
               nums: nums.value,
             },
           ],
@@ -83,29 +81,35 @@ onLoad(async (query: { id: string }) => {
   <view class="flex-grow flex flex-col">
     <div class="aspect-[1.34/1] relative">
       <div class="absolute aspect-[1.26/1] top-0 w-full">
-        <wd-img width="100%" height="100%" :src="data?.productCoverImgUrl" />
+        <wd-img width="100%" height="100%" mode="aspectFill" :src="data?.productDetailsImgUrl" />
       </div>
     </div>
     <div class="relative flex-1 bg-white p-4 flex flex-col gap-4 rounded-tl-2xl rounded-tr-2xl">
-      <div class="flex items-center gap-1">
-        <div class="text-[#ef4343] text-[26px] font-normal font-['D-DIN_Exp'] leading-normal">
+      <div class="flex items-end gap-1">
+        <div class="text-[#ef4343] text-[26px] font-normal font-['D-DIN_Exp'] leading-[20px]">
           <!-- 1000 -->
-          {{ data?.points }}
-        </div>
-        <div class="text-black/60 text-base font-normal font-['PingFang_SC'] leading-[34px]">
-          积分
+          {{ data?.showFavourable ? data?.favourablePoints : data?.points }}
         </div>
+        <template v-if="String(data?.needPoints) === '0'">
+          <div class="text-black/60 text-base font-normal font-['PingFang_SC'] leading-4">积分</div>
+        </template>
+        <template v-if="String(data?.needPoints) === '1'">
+          <div class="text-black/60 text-base font-normal font-['PingFang_SC'] leading-4">
+            折(积分结算)
+          </div>
+        </template>
         <div
-          class="w-[66px] text-black/30 text-xs font-normal font-['PingFang_SC'] line-through leading-normal"
+          v-if="Number(data?.productPrice)"
+          class="w-[66px] text-black/30 text-xs font-normal font-['PingFang_SC'] leading-3"
         >
           <!-- ¥60 -->
           ¥{{ data?.productPrice }}
         </div>
         <div class="flex-1"></div>
-        <div class="text-[#999999] text-xs font-normal font-['PingFang_SC'] leading-[10.18px]">
-          <!-- 已售5件 -->
-          已售{{ data?.exchangeCount || 0 }}件
-        </div>
+        <!--        <div class="text-[#999999] text-xs font-normal font-['PingFang_SC']">-->
+        <!--          &lt;!&ndash; 已售5件 &ndash;&gt;-->
+        <!--          已售{{ data?.exchangeCount || 0 }}件-->
+        <!--        </div>-->
       </div>
       <div class="text-black text-xl font-normal font-['PingFang_SC']">
         <!-- 阿芙佳朵 -->
@@ -133,52 +137,38 @@ onLoad(async (query: { id: string }) => {
           商品详情
         </div>
       </wd-divider>
-      <wd-img width="100%" mode="widthFix" :src="data?.productDetailsImgUrl"></wd-img>
+      <mpHtml :content="data?.contentDesc"></mpHtml>
     </div>
-    <BottomAppBar fixed placeholder>
-      <div class="h-[63px] bg-white backdrop-blur-[20px] flex items-center justify-between gap-2">
-        <div class="flex-1">
-          <ButtonEvo
-            block
-            color="white"
-            location="right"
-            @click="((show = true), (type = 'add2Cart'))"
-          >
-            <span class="text-black/80">加入购物车</span>
-          </ButtonEvo>
-        </div>
-        <!-- <div @click="(show = true), (type = 'add2Cart')">
-          <InvertedTrapezoidButton>
-            <div
-              class="w-20 h-[22px] text-black text-base font-normal font-['PingFang_SC'] leading-tight"
+    <template v-if="String(data?.needPoints) === '0'">
+      <BottomAppBar fixed placeholder>
+        <div class="h-[63px] bg-white backdrop-blur-[20px] flex items-center justify-between gap-2">
+          <div class="flex-1">
+            <ButtonEvo
+              block
+              color="white"
+              location="right"
+              @click="((show = true), (type = 'add2Cart'))"
             >
-              加入购物车
-            </div>
-          </InvertedTrapezoidButton>
-        </div> -->
-        <div class="flex-1">
-          <!-- <TrapeziumButton size="large">
-            <div class="text-white text-base font-normal font-['PingFang_SC'] leading-tight">
-              <div class="text-white text-base font-normal font-['PingFang_SC'] leading-tight">
-                立即兑换
-              </div>
-            </div>
-          </TrapeziumButton> -->
-          <ButtonEvo
-            block
-            size="lg"
-            @click="
-              clickByPermission('mallExchange', () => {
-                show = true
-                type = 'orderNow'
-              })
-            "
-          >
-            立即兑换
-          </ButtonEvo>
+              <span class="text-black/80">加入购物车</span>
+            </ButtonEvo>
+          </div>
+          <div class="flex-1">
+            <ButtonEvo
+              block
+              size="lg"
+              @click="
+                clickByPermission('mallExchange', () => {
+                  show = true
+                  type = 'orderNow'
+                })
+              "
+            >
+              立即兑换
+            </ButtonEvo>
+          </div>
         </div>
-      </div>
-    </BottomAppBar>
+      </BottomAppBar>
+    </template>
     <wd-action-sheet v-model="show">
       <view class="px-7 py-11">
         <div class="flex gap-3 mb-13.5">
@@ -189,11 +179,12 @@ onLoad(async (query: { id: string }) => {
             <div class="text-black/40 text-base font-normal font-['PingFang_SC'] leading-normal">
               {{ data?.prodcutName }}
             </div>
-            <div class="flex items-center">
-              <div class="text-[#ef4343] text-[22px] font-normal font-['D-DIN_Exp'] leading-normal">
-                {{ data?.points }}
+            <div class="flex items-end gap-1">
+              <div class="text-[#ef4343] text-[22px] font-normal font-['D-DIN_Exp'] leading-4">
+                <!--                {{ data?.points }}-->
+                {{ data?.showFavourable ? data?.favourablePoints : data?.points }}
               </div>
-              <div class="text-black/40 text-sm font-normal font-['PingFang_SC'] leading-[34px]">
+              <div class="text-black/40 text-sm font-normal font-['PingFang_SC'] leading-3">
                 积分
               </div>
               <div class="flex-1"></div>

+ 73 - 24
packages/app/src/pages/home/mall/index.vue

@@ -56,7 +56,7 @@ const categories = computed(() => productCategories.value.find(({ id }) => id ==
 const time = computed(
   () =>
     dayjs(favourableProducts.value[current.value].favourableEndDate).diff(
-      dayjs(),
+      dayjs().toDate(),
       'milliseconds',
     ) || 0,
 )
@@ -94,15 +94,44 @@ onShow(async () => {
           >
             <wd-count-down :time="time">
               <template #default="{ current }">
-                <span
-                  class="text-center text-white text-base font-normal font-['PingFang_SC'] leading-relaxed"
-                >
-                  <span class="custom-count-down">{{ current.hours }}</span>
-                  <span class="custom-count-down-colon">:</span>
-                  <span class="custom-count-down">{{ current.minutes }}</span>
-                  <span class="custom-count-down-colon">:</span>
-                  <span class="custom-count-down">{{ current.seconds }}</span>
-                </span>
+                <!--                <span-->
+                <!--                  class="text-center text-white text-base font-normal font-['PingFang_SC'] leading-relaxed"-->
+                <!--                >-->
+                <!--                  <span class="custom-count-down">{{ current.hours }}</span>-->
+                <!--                  <span class="custom-count-down-colon">:</span>-->
+                <!--                  <span class="custom-count-down">{{ current.minutes }}</span>-->
+                <!--                  <span class="custom-count-down-colon">:</span>-->
+                <!--                  <span class="custom-count-down">{{ current.seconds }}</span>-->
+                <!--                </span>-->
+                <div v-if="time" class="flex h-full items-center gap-1.25 text-black/40 text-sm">
+                  <!--                  <div>距{{ { waiting: '报名开始', running: '报名结束' }[status] }}还有</div>-->
+                  <div
+                    v-if="current.days"
+                    class="w-4 h-4 bg-white/90 rounded text-black text-2.5 flex items-center justify-center"
+                  >
+                    {{ current.days }}
+                  </div>
+                  <span v-if="current.days" class="custom-count-down-colon text-white">天</span>
+                  <div
+                    class="w-4 h-4 bg-white/90 rounded text-black text-2.5 flex items-center justify-center"
+                  >
+                    {{ current.hours }}
+                  </div>
+                  <span class="custom-count-down-colon text-white">时</span>
+                  <div
+                    class="w-4 h-4 bg-white/90 rounded text-black text-2.5 flex items-center justify-center"
+                  >
+                    {{ current.minutes }}
+                  </div>
+                  <span class="custom-count-down-colon text-white">分</span>
+                  <div
+                    v-if="!current.days"
+                    class="w-4 h-4 bg-white/90 rounded text-black text-2.5 flex items-center justify-center"
+                  >
+                    {{ current.seconds }}
+                  </div>
+                  <span v-if="!current.days" class="custom-count-down-colon text-white">秒</span>
+                </div>
               </template>
             </wd-count-down>
             <!-- 17:02:18 -->
@@ -125,25 +154,45 @@ onShow(async () => {
               />
               <div class="flex-1">
                 <div
-                  class="w-[178px] text-black/90 text-base font-normal font-['PingFang_SC'] leading-normal"
+                  class="text-black/90 text-base font-normal font-['PingFang_SC'] leading-normal line-clamp-1 text-ellipsis overflow-hidden"
                 >
                   <!-- 海蓝之谜精华面霜60ml -->
                   {{ it.prodcutName }}
                 </div>
                 <div class="flex items-center gap-2.5">
-                  <div class="flex-1">
-                    <!-- {{ (it.exchangeCount || 0 / it.productRepertory || 0) * 100 }} -->
-                    <ProgressEvo
-                      :height="6"
-                      :model-value="(it.exchangeCount || 0 / it.productRepertory || 0) * 100"
-                      color="black"
-                    ></ProgressEvo>
-                  </div>
-                  <div
-                    class="text-black/40 text-[10px] font-normal font-['PingFang_SC'] leading-normal"
-                  >
-                    还剩{{ it.productRepertory - it.exchangeCount }}件
-                  </div>
+                  <template v-if="Number(it.isRestrict)">
+                    <div class="flex-1">
+                      <!-- {{ (it.exchangeCount || 0 / it.productRepertory || 0) * 100 }} -->
+                      <!--                      1-->
+                      <!--                      {{ it.isRestrict }}-->
+
+                      <ProgressEvo
+                        :height="6"
+                        :model-value="(it.exchangeCount || 0 / it.productRepertory || 0) * 100"
+                        color="black"
+                      ></ProgressEvo>
+                    </div>
+                    <div
+                      class="text-black/40 text-[10px] font-normal font-['PingFang_SC'] leading-normal"
+                    >
+                      还剩{{ it.productRepertory - it.exchangeCount }}件
+                    </div>
+                  </template>
+                  <template v-else>
+                    <div class="flex-1 h-6">
+                      <!--                      <ProgressEvo-->
+                      <!--                        :height="6"-->
+                      <!--                        :model-value="(it.exchangeCount || 0 / it.productRepertory || 0) * 100"-->
+                      <!--                        color="black"-->
+                      <!--                      ></ProgressEvo>-->
+                    </div>
+                    <div
+                      class="text-black/40 text-[10px] font-normal font-['PingFang_SC'] leading-normal"
+                    >
+                      <!--                      还剩{{ it.productRepertory - it.exchangeCount }}件-->
+                      数量不限
+                    </div>
+                  </template>
                 </div>
                 <div class="flex items-end gap-1 mt-5">
                   <div class="text-[#ef4343] text-[22px] font-normal font-['D-DIN_Exp'] pb-3">

+ 22 - 2
packages/app/src/pages/home/mall/shopping-cart/index.vue

@@ -104,7 +104,7 @@ const handleProductNumsChange = async (nums, product) => {
 }
 const handlePlaceOrder = async () => {
   if (!selected.value.length) {
-    uni.showToast({ title: '请选择商品', icon: 'none' })
+    await uni.showToast({ title: '请选择商品', icon: 'none' })
     return ''
   }
   const { code, data: res } = await requestToast(() =>
@@ -160,8 +160,28 @@ const handlePlaceOrder = async () => {
                     :class="`${selected.map((it) => it.productId).includes(it.productId) ? 'bg-black' : ''}`"
                   ></div>
                 </div>
-                <div class="w-[110px] h-[110px] bg-[#f6f6f6] rounded-2xl overflow-hidden">
+                <div class="w-[110px] h-[110px] bg-[#f6f6f6] rounded-2xl overflow-hidden relative">
                   <wd-img width="100%" height="100%" :src="it.productCoverImgUrl"></wd-img>
+                  <div
+                    v-if="it.status"
+                    class="absolute bottom-0 w-full h-5.5 bg-[#D7D7D7] flex items-center justify-center"
+                  >
+                    <div
+                      class="text-black/60 text-xs font-normal font-['PingFang_SC'] leading-normal"
+                    >
+                      已下架
+                    </div>
+                  </div>
+                  <div
+                    v-if="it.deleted"
+                    class="absolute bottom-0 w-full h-5.5 bg-[#D7D7D7] flex items-center justify-center"
+                  >
+                    <div
+                      class="text-black/60 text-xs font-normal font-['PingFang_SC'] leading-normal"
+                    >
+                      已失效
+                    </div>
+                  </div>
                 </div>
                 <div class="flex flex-col justify-between flex-1">
                   <div