浏览代码

【功能完善】积分商城

puhui999 10 月之前
父节点
当前提交
d2f516e1ae

+ 11 - 0
pages.json

@@ -120,6 +120,17 @@
 						"group": "商品"
 					}
 				},
+                {
+                    "path": "point",
+                    "style": {
+                      "navigationBarTitleText": "积分商品"
+                    },
+                    "meta": {
+                      "sync": true,
+                      "title": "积分商品",
+                      "group": "商品"
+                    }
+                },
 				{
 					"path": "list",
 					"style": {

+ 77 - 0
pages/activity/point/list.vue

@@ -0,0 +1,77 @@
+<!-- 页面  -->
+<template>
+  <s-layout title="积分商城">
+    <view class="ss-p-20">
+      <view v-for="item in state.pagination.data" :key="item.id" class="ss-m-b-20">
+        <s-point-card
+          size="sl"
+          :data="item"
+          priceColor="#FF3000"
+          @tap="sheep.$router.go('/pages/goods/point', { id: item.id })"
+        ></s-point-card>
+      </view>
+    </view>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/goods-empty.png"
+      text="暂无积分商品"
+    ></s-empty>
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadmore"
+    />
+  </s-layout>
+</template>
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash';
+
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+  });
+  async function getData(page = 1, list_rows = 5) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.app.scoreShop.list({
+      list_rows,
+      page,
+    });
+    if (res.error === 0) {
+      let couponlist = _.concat(state.pagination.data, res.data.data);
+      state.pagination = {
+        ...res.data,
+        data: couponlist,
+      };
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getData(state.pagination.current_page + 1);
+    }
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadmore();
+  });
+  onLoad(() => {
+    getData();
+  });
+</script>

+ 368 - 0
pages/goods/point.vue

@@ -0,0 +1,368 @@
+<template>
+  <view>
+    <s-layout :onShareAppMessage="state.shareInfo" navbar="goods">
+      <!-- 标题栏 -->
+      <detailNavbar />
+      <detailSkeleton v-if="state.skeletonLoading" />
+      <!-- 空置页 -->
+
+      <s-empty
+        v-else-if="state.goodsInfo === null"
+        text="商品不存在或已下架"
+        icon="/static/soldout-empty.png"
+        showAction
+        actionText="再逛逛"
+        actionUrl="/pages/goods/list"
+      />
+      <block v-else>
+        <!-- 商品轮播图  -->
+        <su-swiper
+          class="ss-m-b-14 detail-swiper-selector"
+          isPreview
+          :list="state.goodsSwiper"
+          dotStyle="tag"
+          imageMode="widthFix"
+          dotCur="bg-mask-40"
+          :seizeHeight="750"
+        />
+
+        <!-- 价格+标题 -->
+        <view class="title-card detail-card ss-p-y-40 ss-p-x-20">
+          <view class="ss-flex ss-row-between ss-col-center ss-m-b-18">
+            <view class="price-box ss-flex ss-col-bottom">
+              <view v-if="goodsPrice.price > 0" class="price-text"> ¥{{ goodsPrice.price }} </view>
+              <text v-if="goodsPrice.price > 0 && goodsPrice.score > 0">+</text>
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                class="score-img"
+              ></image>
+              <view class="score-text ss-m-r-16">
+                {{ goodsPrice.score }}
+              </view>
+            </view>
+            <view class="sales-text">
+              {{ formatExchange(state.goodsInfo.sales_show_type, state.goodsInfo.sales) }}
+            </view>
+          </view>
+          <view class="origin-price-text ss-m-b-60" v-if="state.goodsInfo.original_price">
+            原价:¥{{ state.selectedSkuPrice.original_price || state.goodsInfo.original_price }}
+          </view>
+          <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.title }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.subtitle }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <detail-cell-sku
+            v-model="state.selectedSkuPrice.goods_sku_text"
+            :skus="state.goodsInfo.skus"
+            @tap="state.showSelectSku = true"
+          />
+          <detail-cell-service v-model="state.goodsInfo.service" />
+          <detail-cell-params v-model="state.goodsInfo.params" />
+        </view>
+        <!-- 规格与数量弹框 -->
+        <s-select-sku
+          :goodsInfo="state.goodsInfo"
+          :show="state.showSelectSku"
+          :isScore="true"
+          @addCart="onAddCart"
+          @buy="onBuy"
+          @change="onSkuChange"
+          @close="state.showSelectSku = false"
+        />
+
+        <!-- 评价 -->
+        <view class="detail-comment-selector">
+          <detail-comment-card :goodsId="state.goodsId" />
+        </view>
+
+        <!-- 详情 -->
+        <view class="detail-content-selector"></view>
+        <detail-content-card :content="state.goodsInfo.content" />
+
+        <!-- 详情tabbar -->
+        <detail-tabbar v-model="state.goodsInfo" :shareIcon="false" :collectIcon="false">
+          <!-- TODO: 缺货中 已售罄 判断 设计-->
+          <view class="buy-box ss-flex ss-col-center ss-p-r-20" v-if="state.goodsInfo.stock > 0">
+            <button class="ss-reset-button buy-btn" @tap="state.showSelectSku = true">
+              立即兑换
+            </button>
+          </view>
+          <view class="buy-box ss-flex ss-col-center ss-p-r-20" v-else>
+            <button class="ss-reset-button disabled-btn" disabled> 已兑完 </button>
+          </view>
+        </detail-tabbar>
+      </block>
+    </s-layout>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, computed } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash';
+  import { formatExchange, formatGoodsSwiper } from '@/sheep/hooks/useGoods';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailCellService from './components/detail/detail-cell-service.vue';
+  import detailCellParams from './components/detail/detail-cell-params.vue';
+  import detailTabbar from './components/detail/detail-tabbar.vue';
+  import detailSkeleton from './components/detail/detail-skeleton.vue';
+  import detailCommentCard from './components/detail/detail-comment-card.vue';
+  import detailContentCard from './components/detail/detail-content-card.vue';
+
+  const headerBg = sheep.$url.css('/static/img/shop/goods/score-bg.png');
+  const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+  const grouponBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+
+  onPageScroll(() => {});
+
+  const state = reactive({
+    goodsId: 0,
+    skeletonLoading: true,
+    goodsInfo: {},
+    showSelectSku: false,
+    goodsSwiper: [],
+    selectedSkuPrice: {},
+    shareInfo: {},
+    showModel: false,
+    total: 0,
+    couponInfo: [],
+  });
+
+  const goodsPrice = computed(() => {
+    let price, score;
+    if (isEmpty(state.selectedSkuPrice)) {
+      price = state.goodsInfo.price[0];
+      score = state.goodsInfo.score || 0;
+    } else {
+      price = state.selectedSkuPrice.price;
+      score = state.selectedSkuPrice.score || 0;
+    }
+    return { price, score };
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+  // 格式化价格
+  function formatPrice(e) {
+    if (Number(e[0]) > 0) {
+      return e.length === 1 ? e[0] : e.join('~');
+    } else {
+      return '';
+    }
+  }
+  // 添加购物车
+  function onAddCart(e) {
+    sheep.$store('cart').add(e);
+  }
+  // 立即购买
+  function onBuy(e) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'score',
+        goods_list: [
+          {
+            goods_id: e.goods_id,
+            goods_num: e.goods_num,
+            goods_sku_price_id: e.id,
+          },
+        ],
+      }),
+    });
+  }
+
+  onLoad((options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+    state.goodsId = options.id;
+    // 加载商品信息
+    sheep.$api.app.scoreShop.detail(state.goodsId).then((res) => {
+      state.skeletonLoading = false;
+      if (res.error === 0) {
+        state.goodsInfo = res.data;
+        state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.images);
+      } else {
+        // 未找到商品
+        state.goodsInfo = null;
+      }
+    });
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-card {
+    background-color: #ffff;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+
+  // 价格标题卡片
+  .title-card {
+    width: 710rpx;
+    box-sizing: border-box;
+    background-size: 100% 100%;
+    border-radius: 10rpx;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+    .price-box {
+      .score-img {
+        width: 36rpx;
+        height: 36rpx;
+        margin: 0 4rpx;
+      }
+      .score-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 36rpx;
+        font-family: OPPOSANS;
+      }
+      .price-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 36rpx;
+        font-family: OPPOSANS;
+      }
+    }
+    .origin-price-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      text-decoration: line-through;
+      color: $gray-c;
+      font-family: OPPOSANS;
+    }
+
+    .sales-text {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $gray-c;
+    }
+
+    .discounts-box {
+      .discounts-tag {
+        padding: 4rpx 10rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        border-radius: 4rpx;
+        color: var(--ui-BG-Main);
+        // background: rgba(#2aae67, 0.05);
+        background: var(--ui-BG-Main-tag);
+      }
+
+      .discounts-title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: var(--ui-BG-Main);
+        line-height: normal;
+      }
+
+      .cicon-forward {
+        color: var(--ui-BG-Main);
+        font-size: 24rpx;
+        line-height: normal;
+        margin-top: 4rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      line-height: 42rpx;
+    }
+
+    .subtitle-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: $dark-9;
+      line-height: 42rpx;
+    }
+  }
+
+  // 购买
+  .buy-box {
+    .buy-btn {
+      width: 630rpx;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      color: $white;
+    }
+    .disabled-btn {
+      width: 630rpx;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background: #999999;
+      color: $white;
+    }
+  }
+
+  //秒杀卡片
+  .seckill-box {
+    background: v-bind(seckillBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  .groupon-box {
+    background: v-bind(grouponBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  //活动卡片
+  .activity-box {
+    width: 100%;
+    height: 80rpx;
+    box-sizing: border-box;
+    margin-bottom: 10rpx;
+
+    .activity-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: 42rpx;
+
+      .activity-icon {
+        width: 38rpx;
+        height: 38rpx;
+      }
+    }
+
+    .activity-go {
+      width: 70rpx;
+      height: 32rpx;
+      background: #ffffff;
+      border-radius: 16rpx;
+      font-weight: 500;
+      color: #ff6000;
+      font-size: 24rpx;
+      line-height: normal;
+    }
+  }
+
+  .model-box {
+    height: 60vh;
+    .model-content {
+      height: 56vh;
+    }
+    .title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+</style>

+ 9 - 10
pages/order/pickUpVerify.vue

@@ -19,15 +19,15 @@
       <view class="num">{{ orderInfo.pickUpVerifyCode }}</view>
       <view class="rules">
         <!-- TODO puhui999: 需要后端放回:使用 receiveTime 即可 -->
-<!--        <view class="item">-->
-<!--          <view class="rulesTitle flex flex-wrap align-center">-->
-<!--            核销时间-->
-<!--          </view>-->
-<!--          <view class="info">-->
-<!--            每日:-->
-<!--            <text class="time">2020-2-+52</text>-->
-<!--          </view>-->
-<!--        </view>-->
+        <view class="item">
+          <view class="rulesTitle flex flex-wrap align-center">
+            核销时间
+          </view>
+          <view class="info">
+            每日:
+            <text class="time">2020-2-+52</text>
+          </view>
+        </view>
         <view class="item">
           <view class="rulesTitle flex flex-wrap align-center">
             <text class="iconfont icon-shuoming1"></text>
@@ -138,7 +138,6 @@
 </script>
 
 <style scoped lang="scss">
-  // TODO puhui999: 样式需要调整有 bug
   .borRadius14 {
     border-radius: 14rpx !important;
   }

+ 34 - 0
sheep/api/promotion/point.js

@@ -0,0 +1,34 @@
+import request from '@/sheep/request';
+
+const PointApi = {
+  // 获得积分商城活动分页
+  getPointActivityPage: (params) => {
+    return request({ url: 'promotion/point-activity/page', method: 'GET', params });
+  },
+
+  // 获得积分商城活动列表,基于活动编号数组
+  getPointActivityListByIds: (ids) => {
+    return request({
+      url: '/promotion/point-activity/list-by-ids',
+      method: 'GET',
+      params: {
+        ids,
+      },
+    });
+  },
+
+  /**
+   * 获得积分商城活动明细
+   * @param {number} id 积分商城活动编号
+   * @return {*}
+   */
+  getPointActivity: (id) => {
+    return request({
+      url: 'promotion/point-activity/get-detail',
+      method: 'GET',
+      params: { id },
+    });
+  },
+};
+
+export default PointApi;

+ 2 - 0
sheep/components/s-block-item/s-block-item.vue

@@ -39,6 +39,8 @@
     <s-groupon-block v-if="type === 'PromotionCombination'" :data="data" :styles="styles" />
     <!-- 营销组件:秒杀 -->
     <s-seckill-block v-if="type === 'PromotionSeckill'" :data="data" :styles="styles" />
+    <!-- 营销组件:积分商城(模式不一样,无法适配) -->
+    <s-point-block v-if="type === 'PromotionPoint'" :data="data" :styles="styles" />
     <!-- 营销组件:小程序直播(暂时没有这个功能) -->
     <s-live-block v-if="type === 'MpLive'" :data="data" :styles="styles" />
     <!-- 营销组件:优惠券 -->

+ 10 - 2
sheep/components/s-goods-item/s-goods-item.vue

@@ -28,6 +28,14 @@
             >
               ¥{{ fen2yuan(price) }}
             </view>
+            <view v-if="point && Number(price) > 0">+</view>
+            <view class="price-text ss-flex ss-col-center" v-if="point">
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                class="point-img"
+              ></image>
+              <view>{{ point }}</view>
+            </view>
             <view v-if="num" class="total-text ss-flex ss-col-center">x {{ num }}</view>
             <slot name="priceSuffix"></slot>
           </view>
@@ -88,7 +96,7 @@
       type: [String, Number],
       default: 0,
     },
-    score: {
+    point: {
       type: [String, Number],
       default: '',
     },
@@ -113,7 +121,7 @@
 </script>
 
 <style lang="scss" scoped>
-  .score-img {
+  .point-img {
     width: 36rpx;
     height: 36rpx;
     margin: 0 4rpx;

+ 202 - 0
sheep/components/s-point-block/s-point-block.vue

@@ -0,0 +1,202 @@
+<template>
+  <view>
+    <view
+      v-if="mode === 1 && state.scoreList.length"
+      class="goods-md-wrap ss-flex ss-flex-wrap ss-col-top"
+    >
+      <view class="goods-list-box">
+        <view
+          class="left-list"
+          :style="[{ paddingRight: data.space + 'rpx', marginBottom: data.space + 'px' }]"
+          v-for="item in state.leftScoreList"
+          :key="item.id"
+        >
+          <s-point-card
+            class="goods-md-box"
+            size="md"
+            :goodsFields="goodsFields"
+            :data="item"
+            :titleColor="goodsFields.title?.color"
+            :subTitleColor="goodsFields.subtitle.color"
+            :topRadius="data.borderRadiusTop"
+            :bottomRadius="data.borderRadiusBottom"
+            :titleWidth="330 - marginLeft - marginRight"
+            @click="sheep.$router.go('/pages/goods/score', { id: item.id })"
+            @getHeight="mountMasonry($event, 'left')"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+                {{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
+              </button>
+            </template>
+          </s-point-card>
+        </view>
+      </view>
+      <view class="goods-list-box">
+        <view
+          class="right-list"
+          :style="[{ paddingLeft: data.space + 'rpx', marginBottom: data.space + 'px' }]"
+          v-for="item in state.rightScoreList"
+          :key="item.id"
+        >
+          <s-score-card
+            class="goods-md-box"
+            size="md"
+            :goodsFields="goodsFields"
+            :data="item"
+            :titleColor="goodsFields.title?.color"
+            :subTitleColor="goodsFields.subtitle.color"
+            :topRadius="data.borderRadiusTop"
+            :bottomRadius="data.borderRadiusBottom"
+            :titleWidth="330 - marginLeft - marginRight"
+            @click="sheep.$router.go('/pages/goods/score', { id: item.id })"
+            @getHeight="mountMasonry($event, 'right')"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+                {{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
+              </button>
+            </template>
+          </s-score-card>
+        </view>
+      </view>
+
+      <!-- <view class="goods-hack" v-if="state.scoreList.length % 2 == 1" style="width: 345rpx"></view> -->
+    </view>
+    <view v-if="mode === 2 && state.scoreList.length" class="goods-lg-box">
+      <view
+        class="goods-box"
+        :style="[{ marginBottom: data.space + 'px' }]"
+        v-for="item in state.scoreList"
+        :key="item.id"
+      >
+        <s-score-card
+          class="goods-card"
+          size="lg"
+          :goodsFields="goodsFields"
+          :data="item"
+          :titleColor="goodsFields.title?.color"
+          :subTitleColor="goodsFields.subtitle.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @tap="sheep.$router.go('/pages/goods/score', { id: item.id })"
+        >
+          <template v-slot:cart>
+            <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+              {{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
+            </button>
+          </template>
+        </s-score-card>
+      </view>
+    </view>
+  </view>
+</template>
+<script setup>
+  import { computed, reactive, onMounted } from 'vue';
+  import sheep from '@/sheep';
+
+  const state = reactive({
+    scoreList: [],
+    leftScoreList: [],
+    rightScoreList: [],
+  });
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+  const { mode, buyNowStyle, goodsFields, goodsIds } = props.data ?? {};
+  const { marginLeft, marginRight } = props.styles ?? {};
+  async function getScoreListByIds(ids) {
+    let { data } = await sheep.$api.app.scoreShop.ids({ ids });
+    return data;
+  }
+
+  onMounted(async () => {
+    state.scoreList = await getScoreListByIds(goodsIds.join(','));
+    if (mode === 1) {
+      mountMasonry();
+    }
+  });
+
+  // 加载瀑布流
+  let count = 0;
+  let leftHeight = 0;
+  let rightHeight = 0;
+
+  function mountMasonry(height = 0, where = 'left') {
+    if (!state.scoreList[count]) return;
+    if (where === 'left') leftHeight += height;
+    if (where === 'right') rightHeight += height;
+    if (leftHeight <= rightHeight) {
+      state.leftScoreList.push(state.scoreList[count]);
+    } else {
+      state.rightScoreList.push(state.scoreList[count]);
+    }
+    count++;
+  }
+  // 购买按钮样式
+  const buyStyle = computed(() => {
+    if (buyNowStyle.mode == 1) {
+      // button
+      return {
+        background: `linear-gradient(to right, ${buyNowStyle.color1}, ${buyNowStyle.color2})`,
+      };
+    }
+
+    if (buyNowStyle.mode == 2) {
+      // image
+      return {
+        width: '54rpx',
+        height: '54rpx',
+        background: `url(${sheep.$url.cdn(buyNowStyle.src)}) no-repeat`,
+        backgroundSize: '100% 100%',
+      };
+    }
+  });
+</script>
+<style lang="scss" scoped>
+  .goods-md-wrap {
+    width: 100%;
+  }
+
+  .goods-list-box {
+    width: 50%;
+    box-sizing: border-box;
+    .left-list {
+      &:nth-last-child(1) {
+        margin-bottom: 0 !important;
+      }
+    }
+  }
+
+  .goods-box {
+    &:nth-last-of-type(1) {
+      margin-bottom: 0 !important;
+    }
+  }
+
+  .goods-md-box,
+  .goods-sl-box,
+  .goods-lg-box {
+    position: relative;
+
+    .cart-btn {
+      position: absolute;
+      bottom: 10rpx;
+      right: 20rpx;
+      z-index: 11;
+      height: 50rpx;
+      line-height: 50rpx;
+      padding: 0 20rpx;
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #fff;
+    }
+  }
+</style>

+ 458 - 0
sheep/components/s-point-card/s-point-card.vue

@@ -0,0 +1,458 @@
+<template>
+  <view>
+    <!-- md卡片:竖向,一行放两个,图上内容下 -->
+    <view v-if="size === 'md'" class="md-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
+      <image
+        class="md-img-box"
+        :src="sheep.$url.cdn(data.image)"
+        mode="widthFix"
+        @load="calculatePanelHeight"
+      ></image>
+      <view
+        class="md-goods-content ss-flex-col ss-row-around ss-p-b-20 ss-p-t-20 ss-p-x-16"
+        :id="elId"
+      >
+        <view
+          v-if="goodsFields.title?.show"
+          class="md-goods-title ss-line-1"
+          :style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
+        >
+          {{ data.title }}
+        </view>
+        <view
+          v-if="goodsFields.subtitle?.show"
+          class="md-goods-subtitle ss-m-t-16 ss-line-1"
+          :style="[{ color: subTitleColor }]"
+        >
+          {{ data.subtitle }}
+        </view>
+        <view class="ss-col-bottom">
+          <view
+            v-if="goodsFields.score_price?.show"
+            class="md-goods-price ss-m-t-16 font-OPPOSANS ss-m-r-10 ss-flex"
+            :style="[{ color: goodsFields.score_price.color }]"
+          >
+            <view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
+            <image
+              :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+              class="score-img"
+            ></image>
+            {{ data.score }}
+          </view>
+
+          <view
+            v-if="goodsFields.price?.show && data.original_price > 0"
+            class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
+            :style="[{ color: goodsFields.price.color }]"
+          >
+            <text class="price-unit ss-font-20">{{ priceUnit }}</text>
+            <view class="ss-m-l-8">{{ data.original_price }}</view>
+          </view>
+        </view>
+
+        <view class="ss-m-t-16 ss-flex ss-col-center ss-flex-wrap">
+          <view class="sales-text">{{ salesAndStock }}</view>
+        </view>
+      </view>
+
+      <slot name="cart">
+        <view class="cart-box ss-flex ss-col-center ss-row-center">
+          <image class="cart-icon" src="/static/img/shop/tabbar/category2.png" mode=""></image>
+        </view>
+      </slot>
+    </view>
+    <!-- lg卡片:横向型,一行放一个,图片左内容右边  -->
+    <view
+      v-if="size === 'lg'"
+      class="lg-goods-card ss-flex ss-col-stretch"
+      :style="[elStyles]"
+      @tap="onClick"
+    >
+      <image class="lg-img-box" :src="sheep.$url.cdn(data.image)" mode="aspectFill"></image>
+      <view class="lg-goods-content ss-flex-1 ss-flex-col ss-row-between ss-p-b-10 ss-p-t-20">
+        <view class="ss-m-r-20">
+          <view
+            v-if="goodsFields.title?.show"
+            class="lg-goods-title ss-line-2"
+            :style="[{ color: titleColor }]"
+          >
+            {{ data.title }}
+          </view>
+          <view
+            v-if="goodsFields.subtitle?.show"
+            class="lg-goods-subtitle ss-m-t-10 ss-line-1"
+            :style="[{ color: subTitleColor }]"
+          >
+            {{ data.subtitle }}
+          </view>
+        </view>
+        <view>
+          <view class="ss-m-t-10">
+            <view
+              v-if="goodsFields.score_price?.show"
+              class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS"
+              :style="[{ color: goodsFields.score_price.color }]"
+            >
+              <view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                class="score-img"
+              ></image>
+              {{ data.score }}
+            </view>
+            <view
+              v-if="goodsFields.price?.show && data.original_price > 0"
+              class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS"
+              :style="[{ color: goodsFields.price.color }]"
+            >
+              <text class="price-unit ss-font-20">{{ priceUnit }}</text>
+              <view class="ss-m-l-8">{{ data.original_price }}</view>
+            </view>
+          </view>
+          <view class="ss-m-t-16 ss-flex ss-col-center ss-flex-wrap">
+            <view class="sales-text">{{ salesAndStock }}</view>
+          </view>
+        </view>
+      </view>
+
+      <slot name="cart"
+      ><view class="buy-box ss-flex ss-col-center ss-row-center">去兑换</view></slot
+      >
+    </view>
+    <!-- sl卡片:竖向型,一行放一个,图片上内容下边 -->
+    <view v-if="size === 'sl'" class="sl-goods-card ss-flex-col" @tap="onClick">
+      <image class="sl-img-box" :src="sheep.$url.cdn(data.image)" mode="aspectFill"></image>
+
+      <view class="sl-goods-content ss-flex-col ss-row-between ss-p-b-20 ss-p-t-20">
+        <view class="ss-m-b-20">
+          <view class="sl-goods-title ss-line-1 ss-p-l-16 ss-p-r-16">
+            {{ data.title }}
+          </view>
+          <view v-if="data.subtitle" class="sl-goods-subtitle ss-p-l-16 ss-p-r-16 ss-m-t-16">
+            {{ data.subtitle }}
+          </view>
+        </view>
+        <view>
+          <slot name="activity">
+            <view
+              v-if="data.promos?.length"
+              class="tag-box ss-flex ss-col-center ss-flex-wrap ss-p-l-16 ss-p-r-16"
+            >
+              <view
+                class="activity-tag ss-m-r-10 ss-m-t-16"
+                v-for="item in data.promos"
+                :key="item.id"
+              >
+                {{ item.title }}
+              </view>
+            </view>
+          </slot>
+          <view class="ss-flex ss-col-bottom ss-p-l-16 ss-p-r-16 font-OPPOSANS">
+            <view class="sl-goods-price ss-m-r-12 ss-flex">
+              <view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                class="score-img"
+              ></image>
+              <view>{{ data.score ? data.score : '' }}</view>
+            </view>
+            <view
+              v-if="data.original_price > 0"
+              class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
+            >
+              <text class="price-unit ss-font-20">¥</text>
+              <view class="ss-m-l-8">{{ data.original_price }}</view>
+            </view>
+          </view>
+          <view class="ss-p-l-16 ss-p-r-16 ss-m-t-16 ss-flex ss-flex-wrap">
+            <view class="sales-text">{{ salesAndStock }}</view>
+          </view>
+        </view>
+      </view>
+
+      <slot name="cart"
+      ><view class="buy-box ss-flex ss-col-center ss-row-center">去兑换</view></slot
+      >
+    </view>
+  </view>
+</template>
+<script setup>
+  import { computed, getCurrentInstance } from 'vue';
+  import sheep from '@/sheep';
+  import { formatSales } from '@/sheep/hooks/useGoods';
+  import { formatStock } from '@/sheep/hooks/useGoods';
+  /**
+   * 订单卡片
+   *
+   * @property {String} img 											- 图片
+   * @property {String} title 										- 标题
+   * @property {Number} titleWidth = 0								- 标题宽度,默认0,单位rpx
+   * @property {String} skuText 										- 规格
+   * @property {String | Number} score 								- 积分
+   * @property {String | Number} price 								- 价格
+   * @property {String | Number} originalPrice 						- 单购价
+   * @property {String} priceColor 									- 价格颜色
+   * @property {Number | String} num									- 数量
+   *
+   */
+  const props = defineProps({
+    goodsFields: {
+      type: [Array, Object],
+      default() {
+        return {
+          title: { show: true },
+          subtitle: { show: true },
+          price: { show: true },
+          original_price: { show: true },
+          sales: { show: true },
+          stock: { show: true },
+        };
+      },
+    },
+    tagStyle: {
+      type: Object,
+      default: {},
+    },
+    data: {
+      type: Object,
+      default: {},
+    },
+    size: {
+      type: String,
+      default: 'sl',
+    },
+    background: {
+      type: String,
+      default: '',
+    },
+    topRadius: {
+      type: Number,
+      default: 0,
+    },
+    bottomRadius: {
+      type: Number,
+      default: 0,
+    },
+    titleWidth: {
+      type: Number,
+      default: 0,
+    },
+    titleColor: {
+      type: String,
+      default: '#333',
+    },
+    priceUnit: {
+      type: String,
+      default: '¥',
+    },
+    subTitleColor: {
+      type: String,
+      default: '#999999',
+    },
+  });
+  // 组件样式
+  const elStyles = computed(() => {
+    return {
+      background: props.background,
+      'border-top-left-radius': props.topRadius + 'px',
+      'border-top-right-radius': props.topRadius + 'px',
+      'border-bottom-left-radius': props.bottomRadius + 'px',
+      'border-bottom-right-radius': props.bottomRadius + 'px',
+    };
+  });
+  const emits = defineEmits(['click', 'getHeight']);
+  const onClick = () => {
+    emits('click');
+  };
+  // 格式化销量、库存信息
+  const salesAndStock = computed(() => {
+    let text = [];
+    text.push(formatSales(props.data.sales_show_type, props.data.sales));
+    text.push(formatStock(props.data.stock_show_type, props.data.stock));
+    return text.join(' | ');
+  });
+  // 获取实时卡片高度
+  const { proxy } = getCurrentInstance();
+  const elId = `sheep_${Math.ceil(Math.random() * 10e5).toString(36)}`;
+  function calculatePanelHeight(e) {
+    if (props.size === 'md') {
+      const view = uni.createSelectorQuery().in(proxy);
+      view.select(`#${elId}`).fields({ size: true, scrollOffset: true });
+      view.exec((data) => {
+        const goodsPriceCard = data[0];
+        const card = {
+          width: goodsPriceCard.width,
+          height: (goodsPriceCard.width / e.detail.width) * e.detail.height + goodsPriceCard.height,
+        };
+        emits('getHeight', card.height);
+      });
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .price-unit {
+    margin-right: -4px;
+  }
+  .sales-text {
+    display: table;
+    font-size: 24rpx;
+    transform: scale(0.8);
+    margin-left: -16rpx;
+    color: #c4c4c4;
+  }
+
+  // md
+  .md-goods-card {
+    overflow: hidden;
+    width: 100%;
+    position: relative;
+    z-index: 1;
+    background-color: $white;
+    position: relative;
+
+    .md-img-box {
+      width: 100%;
+    }
+
+    .md-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      width: 100%;
+    }
+    .md-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+
+    .md-goods-price {
+      font-size: 30rpx;
+      color: $red;
+      line-height: 36rpx;
+    }
+
+    .cart-box {
+      width: 54rpx;
+      height: 54rpx;
+      background: linear-gradient(90deg, #fe8900, #ff5e00);
+      border-radius: 50%;
+      position: absolute;
+      bottom: 50rpx;
+      right: 20rpx;
+      z-index: 2;
+
+      .cart-icon {
+        width: 30rpx;
+        height: 30rpx;
+      }
+    }
+  }
+
+  // lg
+  .lg-goods-card {
+    overflow: hidden;
+    position: relative;
+    z-index: 1;
+    background-color: $white;
+    height: 280rpx;
+
+    .lg-img-box {
+      width: 280rpx;
+      height: 280rpx;
+      margin-right: 20rpx;
+    }
+
+    .lg-goods-title {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+      // line-height: 36rpx;
+      // width: 410rpx;
+    }
+    .lg-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+      line-height: 30rpx;
+      // width: 410rpx;
+    }
+
+    .lg-goods-price {
+      font-size: 30rpx;
+      color: $red;
+      line-height: 36rpx;
+    }
+
+    .buy-box {
+      position: absolute;
+      bottom: 20rpx;
+      right: 20rpx;
+      z-index: 2;
+      width: 120rpx;
+      height: 50rpx;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #ffffff;
+    }
+    .tag-box {
+      width: 100%;
+    }
+  }
+  .sl-goods-card {
+    overflow: hidden;
+    position: relative;
+    z-index: 1;
+    width: 100%;
+    background-color: $white;
+
+    .sl-img-box {
+      width: 100%;
+      height: 360rpx;
+    }
+
+    .sl-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      width: 100%;
+      box-sizing: border-box;
+    }
+    .sl-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+      line-height: 30rpx;
+      width: 100%;
+      box-sizing: border-box;
+    }
+
+    .sl-goods-price {
+      font-size: 30rpx;
+      color: $red;
+    }
+
+    .buy-box {
+      position: absolute;
+      bottom: 20rpx;
+      right: 20rpx;
+      z-index: 2;
+      width: 148rpx;
+      height: 50rpx;
+      background: linear-gradient(90deg, #fe8900, #ff5e00);
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #ffffff;
+    }
+  }
+  .goods-origin-price {
+    font-size: 20rpx;
+    color: #c4c4c4;
+    text-decoration: line-through;
+  }
+  .score-img {
+    width: 36rpx;
+    height: 36rpx;
+    margin: 0 4rpx;
+  }
+</style>