Przeglądaj źródła

feat(mallApp): 优化商品详情信息与规格选择

- 重构商品信息卡布局,补充已售数据与服务保障展示
- 新增页面规格卡片,并同步价格、库存、轮播图及购买弹层选中状态
shizhongqi 2 dni temu
rodzic
commit
d582446819

+ 13 - 0
mallApp/src/pages/goods/components/sel-popup.vue

@@ -88,6 +88,11 @@ export default {
       type: String,
       type: String,
       default: 'buy',
       default: 'buy',
     },
     },
+    /** 打开弹层时默认选中的规格下标:与详情页规格卡片选中保持一致 */
+    defaultSpecIndex: {
+      type: Number,
+      default: 0,
+    },
     /** 是否为进行中的秒杀:用于限购与活动库存校验 */
     /** 是否为进行中的秒杀:用于限购与活动库存校验 */
     isSeckill: {
     isSeckill: {
       type: Boolean,
       type: Boolean,
@@ -114,6 +119,14 @@ export default {
       selIndex: 0,
       selIndex: 0,
     };
     };
   },
   },
+  watch: {
+    /** 每次打开弹层都对齐详情页当前选中的规格 */
+    show(val) {
+      if (val) {
+        this.selIndex = Number(this.defaultSpecIndex) || 0;
+      }
+    },
+  },
   computed: {
   computed: {
     /**
     /**
      * 是否为挂在主商品下的规格 SKU(masterId/master 不为 0)
      * 是否为挂在主商品下的规格 SKU(masterId/master 不为 0)

+ 273 - 66
mallApp/src/pages/goods/detail.vue

@@ -1,7 +1,8 @@
 <!--
 <!--
   商城商品详情
   商城商品详情
   谁用:mallApp 顾客查看花束并加购/购买
   谁用:mallApp 顾客查看花束并加购/购买
-  解决问题:多规格 specList 首项即主记录可售规格,选中后 specGoodsId 可等于主商品 id
+  解决问题:多规格 specList 首项即主记录可售规格,选中后 specGoodsId 可等于主商品 id;
+  顶部信息卡按标题/分享、副文案、价格已售、服务保障排布,秒杀条保留在价格上方
 -->
 -->
 <template>
 <template>
   <view class="page-container">
   <view class="page-container">
@@ -12,42 +13,83 @@
         :list="imgList"
         :list="imgList"
       />
       />
       <view class="shop-intro">
       <view class="shop-intro">
-        <view v-if="isSeckillActive" class="seckill-bar">
-          <view class="seckill-bar-left">
-            <text class="seckill-badge">秒杀价</text>
-            <text v-if="seckillCountdownText" class="seckill-countdown">{{ seckillCountdownText }}后结束</text>
-          </view>
-          <text v-if="activityLimit > 0" class="seckill-limit">每人限购{{ activityLimit }}件</text>
-        </view>
-        <view class="app-price app-bold" v-if="data.priceType == 1">
-          <text>¥</text>
-          <text class="app-size-34">
-            {{ data.price ? parseFloat(data.price).toFixed(2) : 0}}
-          </text>
-          <text v-if="isSeckillActive && data.originPrice > 0" class="app-price-origin">¥{{ parseFloat(data.originPrice).toFixed(2) }}</text>
-        </view>
-        <!-- 无价格商品:不展示数字价,引导底部询价 -->
-        <view class="app-price app-bold" v-else>¥?</view>
+        <!-- 标题左、分享右;规格名作副文案,避免挤掉秒杀等原有信息 -->
         <view class="tui-pro-titbox">
         <view class="tui-pro-titbox">
           <view class="tui-pro-title-wrap">
           <view class="tui-pro-title-wrap">
-            <view class="tui-pro-title app-size-32">{{ data.name }}</view>
+            <view class="tui-pro-title">{{ data.name }}</view>
             <view
             <view
               v-if="displaySpecName"
               v-if="displaySpecName"
-              class="shop-spec app-color-3"
+              class="shop-spec"
             >{{ displaySpecName }}</view>
             >{{ displaySpecName }}</view>
           </view>
           </view>
           <button open-type="share" class="share-button" @click="shareFn">
           <button open-type="share" class="share-button" @click="shareFn">
-            <i class="iconfont iconfenxiang"></i>
+            <text class="iconfont iconfenxiang share-icon"></text>
             <text class="share-text">分享</text>
             <text class="share-text">分享</text>
           </button>
           </button>
         </view>
         </view>
-        <view class="shop-express app-color-3">
-          <view>线下门店</view>
-          <view>
-            <span class="prompt-text">快速发货</span>
+        <!-- 秒杀进行中才展示:倒计时 + 限购,紧贴价格区提醒活动价 -->
+        <view v-if="isSeckillActive" class="seckill-bar">
+          <view class="seckill-bar-left">
+            <text class="seckill-badge">秒杀价</text>
+            <text v-if="seckillCountdownText" class="seckill-countdown">{{ seckillCountdownText }}后结束</text>
+          </view>
+          <text v-if="activityLimit > 0" class="seckill-limit">每人限购{{ activityLimit }}件</text>
+        </view>
+        <view class="price-sold-row">
+          <view class="price-wrap">
+            <view class="app-price app-bold" v-if="data.priceType == 1">
+              <text class="price-symbol">¥</text>
+              <text class="price-num">
+                {{ data.price ? parseFloat(data.price).toFixed(2) : 0}}
+              </text>
+              <text v-if="isSeckillActive && data.originPrice > 0" class="app-price-origin">¥{{ parseFloat(data.originPrice).toFixed(2) }}</text>
+            </view>
+            <!-- 无价格商品:不展示数字价,引导底部询价 -->
+            <view class="app-price app-bold" v-else>¥?</view>
+          </view>
+          <text class="sold-text">已售 {{ displaySold }}</text>
+        </view>
+        <!-- 三项服务保障:保留原有文案,按设计稿改成图标+主副标题横条 -->
+        <view class="shop-express">
+          <view class="express-item">
+            <text class="iconfont icondianpu express-icon"></text>
+            <view class="express-text">
+              <text class="express-title">线下门店</text>
+              <text class="express-sub">到店自提</text>
+            </view>
+          </view>
+          <view class="express-item">
+            <text class="iconfont iconfahuo express-icon"></text>
+            <view class="express-text">
+              <text class="express-title">快速发货</text>
+              <text class="express-sub">最快1-2小时达</text>
+            </view>
+          </view>
+          <view class="express-item">
+            <text class="iconfont icondagou express-icon"></text>
+            <view class="express-text">
+              <text class="express-title">品质保证</text>
+              <text class="express-sub">100%满意服务</text>
+            </view>
           </view>
           </view>
-          <view>
-            <span class="prompt-text">品质保证</span>
+        </view>
+      </view>
+      <!-- 多规格:卡片选规格,切换时同步价格/库存与顶部轮播图 -->
+      <view
+        v-if="data.specEnabled == 1 && data.specList && data.specList.length"
+        class="module-wrap spec-module"
+      >
+        <view class="spec-module-tit app-size-32 app-bold">花束规格</view>
+        <view class="spec-card-list">
+          <view
+            v-for="(spec, index) in data.specList"
+            :key="spec.id"
+            class="spec-card"
+            :class="{ active: specIndex === index }"
+            @click="selectSpecOnPage(index)"
+          >
+            <view class="spec-card-name app-bold">{{ spec.specName }}</view>
+            <view class="spec-card-price">{{ specPriceText(spec) }}</view>
           </view>
           </view>
         </view>
         </view>
       </view>
       </view>
@@ -96,6 +138,7 @@
       :hdId="hdId"
       :hdId="hdId"
       :account="account"
       :account="account"
       :mode="popupMode"
       :mode="popupMode"
+      :default-spec-index="specIndex"
       :is-seckill="isSeckillActive"
       :is-seckill="isSeckillActive"
       :activity-limit="activityLimit"
       :activity-limit="activityLimit"
       :activity-stock="activityStock"
       :activity-stock="activityStock"
@@ -153,6 +196,8 @@ export default {
       autoLoad: false,
       autoLoad: false,
       data: {}, //商品数据
       data: {}, //商品数据
       imgList: [],
       imgList: [],
+      /** 当前选中规格下标(specList),默认首规格 */
+      specIndex: 0,
       buyNum: 1,
       buyNum: 1,
       popupShow: false,
       popupShow: false,
       popupMode: 'buy',
       popupMode: 'buy',
@@ -278,6 +323,16 @@ export default {
       }
       }
       return '';
       return '';
     },
     },
+    /**
+     * 已售文案数字:优先 sold,兼容列表口径 totalSold;过万缩成 x.x万+
+     */
+    displaySold() {
+      const n = parseInt(this.data.sold, 10) || parseInt(this.data.totalSold, 10) || 0;
+      if (n >= 10000) {
+        return (n / 10000).toFixed(1) + '万+';
+      }
+      return String(n);
+    },
   },
   },
   methods: {
   methods: {
     /** 按登录态驱动 wangCg:0 未登录弹层 / 1 已登录关闭 */
     /** 按登录态驱动 wangCg:0 未登录弹层 / 1 已登录关闭 */
@@ -543,6 +598,8 @@ export default {
           newArr.push({ img: res.data.imgList[i] });
           newArr.push({ img: res.data.imgList[i] });
         }
         }
         this.imgList = newArr;
         this.imgList = newArr;
+        // 重新拉详情后回到首规格(specList 首项即主记录自身)
+        this.specIndex = 0;
         res.data.stock = res.data.stock ? parseInt(res.data.stock) : 0;
         res.data.stock = res.data.stock ? parseInt(res.data.stock) : 0;
         this.data = res.data;
         this.data = res.data;
         this.customId = res.data.customId;
         this.customId = res.data.customId;
@@ -584,8 +641,48 @@ export default {
     changeNum(e) {
     changeNum(e) {
       this.buyNum = e.value;
       this.buyNum = e.value;
     },
     },
+    /** 弹层内切换规格:同步页面选中态(价格/轮播图一并更新) */
     changeSpec(spec) {
     changeSpec(spec) {
-      this.data = {...this.data, price: spec.price, priceType: spec.priceType, stock: spec.stock, selectedSpecGoodsId: spec.id, selectedSpecName: spec.specName};
+      const list = (this.data && this.data.specList) || [];
+      const index = list.findIndex((el) => String(el.id) === String(spec.id));
+      this.applySpecSelection(index >= 0 ? index : this.specIndex);
+    },
+    /** 页面规格卡片点击 */
+    selectSpecOnPage(index) {
+      if (this.specIndex === index) {
+        return;
+      }
+      this.applySpecSelection(index);
+    },
+    /**
+     * 应用选中规格:更新价格/库存/选中标记,并把轮播图切到该规格自己的图片
+     * specList 每项都由后端 formatGoodsImg 处理过,自带完整 imgList
+     */
+    applySpecSelection(index) {
+      const spec = (this.data.specList || [])[index];
+      if (!spec) {
+        return;
+      }
+      this.specIndex = index;
+      this.data = {
+        ...this.data,
+        price: spec.price,
+        priceType: spec.priceType,
+        stock: spec.stock ? parseInt(spec.stock) : 0,
+        selectedSpecGoodsId: spec.id,
+        selectedSpecName: spec.specName,
+      };
+      if (Array.isArray(spec.imgList) && spec.imgList.length) {
+        this.imgList = spec.imgList.map((img) => ({ img }));
+      }
+    },
+    /** 规格卡片价格文案:无价格商品与主价格区一致展示 ¥? */
+    specPriceText(spec) {
+      if (Number(spec.priceType) !== 1) {
+        return '¥?';
+      }
+      const price = parseFloat(spec.price) || 0;
+      return '¥' + (Number.isInteger(price) ? price : price.toFixed(2));
     },
     },
     /**
     /**
      * 从本地缓存恢复当前店铺购物车到 Vuex
      * 从本地缓存恢复当前店铺购物车到 Vuex
@@ -818,16 +915,20 @@ export default {
       }
       }
     }
     }
   }
   }
-  // 公共模块
+  // 商品信息卡:上圆角压住轮播,标题/价格/保障按设计稿分层
   .shop-intro {
   .shop-intro {
+    position: relative;
+    z-index: 1;
+    margin-top: -32upx;
+    padding: 32upx 28upx 28upx;
     background: #fff;
     background: #fff;
-    padding: 30upx 0 30upx 30upx;
+    border-radius: 32upx 32upx 0 0;
     .seckill-bar {
     .seckill-bar {
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
       justify-content: space-between;
       justify-content: space-between;
-      padding: 10upx 20upx 10upx 0;
-      margin-bottom: 10upx;
+      padding: 12upx 0 0;
+      margin-top: 8upx;
       .seckill-bar-left {
       .seckill-bar-left {
         display: flex;
         display: flex;
         align-items: center;
         align-items: center;
@@ -853,66 +954,172 @@ export default {
     .app-price-origin {
     .app-price-origin {
       margin-left: 12upx;
       margin-left: 12upx;
       font-size: 26upx;
       font-size: 26upx;
+      font-weight: 400;
       color: #bbb;
       color: #bbb;
       text-decoration: line-through;
       text-decoration: line-through;
     }
     }
     .tui-pro-titbox {
     .tui-pro-titbox {
-      margin: 20upx 0 20upx;
-      @include disFlex(center, space-between);
+      @include disFlex(flex-start, space-between);
       .tui-pro-title-wrap {
       .tui-pro-title-wrap {
-        width: 80%;
+        flex: 1;
+        padding-right: 16upx;
+        overflow: hidden;
       }
       }
       .tui-pro-title {
       .tui-pro-title {
         width: 100%;
         width: 100%;
+        font-size: 36upx;
+        font-weight: 700;
+        color: #222;
+        line-height: 1.4;
       }
       }
-      // 与 .shop-express 同色同字号,紧跟商品名称下方
       .shop-spec {
       .shop-spec {
-        margin-top: 8upx;
+        margin-top: 10upx;
+        font-size: 26upx;
+        color: #999;
+        line-height: 1.5;
       }
       }
       .share-button {
       .share-button {
         display: flex;
         display: flex;
+        flex-direction: column;
         align-items: center;
         align-items: center;
-        justify-content: center;
-        width: 168upx;
-        height: 84upx;
-        background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
-        border-radius: 12upx;
-        border: 1upx solid #e1e5e9;
-        box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.08);
-        transition: all 0.3s ease;
+        justify-content: flex-start;
+        flex-shrink: 0;
+        width: 72upx;
+        min-width: 72upx;
+        height: auto;
+        min-height: 0;
         margin: 0;
         margin: 0;
         padding: 0;
         padding: 0;
-
-        &:active {
-          background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
-          box-shadow: 0 1upx 4upx rgba(0, 0, 0, 0.12);
+        background: transparent;
+        border: none;
+        line-height: 1;
+        &::after {
+          border: none;
         }
         }
-
-        .iconfont {
-          font-size: 32upx;
-          color: #4a5568;
-          text-shadow: 0 1upx 2upx rgba(255, 255, 255, 0.8);
-          margin-right: 8upx;
-          display: flex;
-          align-items: center;
+        .share-icon {
+          font-size: 36upx;
+          color: #999;
           line-height: 1;
           line-height: 1;
         }
         }
-
         .share-text {
         .share-text {
-          font-size: 28upx;
-          color: #4a5568;
-          font-weight: 500;
-          display: flex;
-          align-items: center;
+          margin-top: 6upx;
+          font-size: 22upx;
+          color: #999;
           line-height: 1;
           line-height: 1;
         }
         }
       }
       }
     }
     }
+    .price-sold-row {
+      display: flex;
+      align-items: baseline;
+      justify-content: space-between;
+      margin-top: 24upx;
+      .price-wrap {
+        flex: 1;
+        overflow: hidden;
+      }
+      .app-price {
+        display: flex;
+        align-items: baseline;
+      }
+      .price-symbol {
+        font-size: 28upx;
+      }
+      .price-num {
+        font-size: 48upx;
+        line-height: 1;
+      }
+      .sold-text {
+        flex-shrink: 0;
+        margin-left: 16upx;
+        font-size: 24upx;
+        color: #999;
+      }
+    }
     .shop-express {
     .shop-express {
-      padding-right: 30upx;
-      @include disFlex(center, space-between);
-      .prompt-text {
-        margin-right: 4upx;
+      display: flex;
+      align-items: center;
+      margin-top: 24upx;
+      padding: 26upx 38upx;
+      background: #f7f7f7;
+      border-radius: 16upx;
+      .express-item {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        overflow: hidden;
+        .express-icon {
+          margin-right: 8upx;
+          font-size: 48upx;
+          color: #52c41a;
+          line-height: 1;
+        }
+        .express-text {
+          display: flex;
+          flex-direction: column;
+          overflow: hidden;
+        }
+        .express-title {
+          font-size: 22upx;
+          color: #333;
+          line-height: 1.3;
+          white-space: nowrap;
+        }
+        .express-sub {
+          margin-top: 4upx;
+          font-size: 20upx;
+          color: #999;
+          line-height: 1.3;
+          white-space: nowrap;
+        }
+      }
+      .express-item + .express-item {
+        margin-left: 8upx;
+      }
+    }
+  }
+  // 规格卡片模块:选中项绿色描边+绿色价格,不带角标
+  .spec-module {
+    $specGreen: #7ba23f;
+    padding: 24upx;
+    .spec-module-tit {
+      margin-bottom: 24upx;
+    }
+    .spec-card-list {
+      display: flex;
+      flex-wrap: wrap;
+    }
+    .spec-card {
+      width: calc((100% - 40upx) / 3);
+      margin: 0 20upx 20upx 0;
+      padding: 24upx;
+      box-sizing: border-box;
+      background: #f7f7f5;
+      border: 2upx solid transparent;
+      border-radius: 16upx;
+      overflow: hidden;
+      &:nth-child(3n) {
+        margin-right: 0;
+      }
+      .spec-card-name {
+        font-size: 30upx;
+        color: #333;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      .spec-card-price {
+        margin-top: 16upx;
+        font-size: 28upx;
+        color: #999;
+      }
+      &.active {
+        background: #fff;
+        border-color: $specGreen;
+        .spec-card-price {
+          color: $specGreen;
+          font-weight: 600;
+        }
       }
       }
     }
     }
   }
   }