Kaynağa Gözat

feat(mallApp): 支持无价格商品参与秒杀购买

- 秒杀商品统一展示活动价并开放加购与立即购买入口
- 商品详情、规格弹层及专区列表同步活动价格和库存限制
- 秒杀结束后恢复商品原价格类型与询价状态
shizhongqi 1 gün önce
ebeveyn
işleme
604e765bf7

+ 4 - 3
mallApp/src/components/home/activitySection.vue

@@ -2,7 +2,8 @@
   店铺首页-秒杀专区/团购专区
   按 layoutCols(1/2/3) 渲染活动商品,含倒计时;秒杀按钮为「立即抢购」,团购按钮为「去开团」;
   仅在活动有效期内(startTime~endTime)展示,未开始或结束后整块隐藏;
-  秒杀点击跳商品详情;团购点击跳团购详情页(开团/参团入口)
+  秒杀点击跳商品详情;团购点击跳团购详情页(开团/参团入口);
+  无价格秒杀商品只展示活动价,原价为 0 时不画划线价
 -->
 <template>
   <view v-if="isActive && data && data.enabled == 1 && displayGoods.length" class="home-activity">
@@ -43,7 +44,7 @@
           <view class="row-bottom">
             <view class="row-price-wrap">
               <text class="price">¥{{ formatPrice(g.price) }}</text>
-              <text class="origin-price">¥{{ formatPrice(g.originPrice) }}</text>
+              <text v-if="Number(g.originPrice) > 0" class="origin-price">¥{{ formatPrice(g.originPrice) }}</text>
             </view>
             <view class="action-btn">
               {{ actionText }}
@@ -67,7 +68,7 @@
         <text class="grid-name">{{ g.name }}</text>
         <view class="grid-price-row">
           <text class="price">¥{{ formatPrice(g.price) }}</text>
-          <text v-if="cols === 2" class="origin-price">¥{{ formatPrice(g.originPrice) }}</text>
+          <text v-if="cols === 2 && Number(g.originPrice) > 0" class="origin-price">¥{{ formatPrice(g.originPrice) }}</text>
         </view>
         <view class="grid-btn">{{ actionText }}<text v-if="cols === 2"> ></text></view>
       </view>

+ 24 - 11
mallApp/src/mixins/cgProduct.js

@@ -479,6 +479,7 @@ export default {
 		/**
 		 * 组装花束购物车行(property=0):仅做校验并构造行结构,不写入 Vuex/缓存。
 		 * 「立即购买」复用该行结构,保证与加购的计价、展示口径一致,同时不污染购物车。
+		 * 秒杀进行中允许无价格商品按活动价入行,priceType 记为 1,避免结算仍走询价拦截。
 		 * @param {Object} goods 主商品信息
 		 * @param {Object|null} spec 选中的规格子商品,单规格可为 null
 		 * @param {Number} num 数量,默认 1
@@ -491,16 +492,28 @@ export default {
 			}
 			const buyNum = Number(num) > 0 ? Number(num) : 1;
 			const saleGoods = spec && spec.id ? spec : goods;
-			const price = Number(saleGoods.price != null ? saleGoods.price : goods.price);
-			// 无价格类型(priceType=0):不能加购,引导向商家询价
-			if (Number(goods.priceType) === 0) {
-				this.$msg('请向商家咨询价格');
-				return null;
-			}
-			// 有价格类型且价格为0时,提示商品还没有价格
-			if ((Number(goods.priceType) === 1 && !(price > 0))) {
-				this.$msg('商品还没有价格哦');
-				return null;
+			const isSeckill = goods.activityType === 'seckill';
+			let price = Number(saleGoods.price != null ? saleGoods.price : goods.price);
+			// 秒杀进行中:无价格商品用活动价直购;规格自身无价时回退到主商品上已覆盖的秒杀价
+			if (isSeckill && !(price > 0)) {
+				price = Number(goods.price);
+			}
+			if (isSeckill) {
+				if (!(price > 0)) {
+					this.$msg('商品还没有价格哦');
+					return null;
+				}
+			} else {
+				// 无价格类型(priceType=0):不能加购,引导向商家询价
+				if (Number(goods.priceType) === 0) {
+					this.$msg('请向商家咨询价格');
+					return null;
+				}
+				// 有价格类型且价格为0时,提示商品还没有价格
+				if ((Number(goods.priceType) === 1 && !(price > 0))) {
+					this.$msg('商品还没有价格哦');
+					return null;
+				}
 			}
 			const stockRaw = saleGoods.stock != null ? saleGoods.stock : goods.stock;
 			const stockNum = Number(stockRaw);
@@ -520,7 +533,7 @@ export default {
 				price: price,
 				bigPrice: price,
 				smallPrice: price,
-				priceType: goods.priceType,
+				priceType: isSeckill ? 1 : goods.priceType,
 				freightType: goods.freightType,
 				weight: saleGoods.weight != null ? saleGoods.weight : (goods.weight || 0),
 				stock: stock,

+ 10 - 3
mallApp/src/pages/goods/components/buy-foot.vue

@@ -2,7 +2,8 @@
   商品详情底部操作栏
   用途:左侧购物车/咨询入口 + 右侧加购/购买(或询价)
   使用方:pages/goods/detail
-  加购/购买点击时先拦商品库存与秒杀活动库存,不足则不打开选规格弹层
+  加购/购买点击时先拦商品库存与秒杀活动库存,不足则不打开选规格弹层;
+  无价格商品若处于秒杀进行中,右侧展示加购/购买而不是询价
 -->
 <template>
 	<view class="app-footer">
@@ -23,11 +24,11 @@
             </view>
         </view>
         <view class="btn-right">
-            <template v-if="priceType == 1">
+            <template v-if="canDirectBuy">
                 <button class="button-com cart-btn" @click="cart">加入购物车</button>
                 <button class="button-com red" @click="buy">购买</button>
             </template>
-            <button v-if="priceType == 0" class="button-com red" @click.stop="goChat">询价</button>
+            <button v-else class="button-com red" @click.stop="goChat">询价</button>
         </view>
     </view>
 </template>
@@ -61,6 +62,12 @@ export default {
         }
     },
     computed: {
+        /**
+         * 是否可直接加购/购买:有价格,或秒杀进行中(无价格商品也用秒杀价,不走询价)
+         */
+        canDirectBuy() {
+            return Number(this.priceType) === 1 || this.isSeckill
+        },
         /** 角标文案:超过 99 显示 99+,与店铺底部 tab 一致 */
         cartCountText() {
             return this.cartCount > 99 ? '99+' : String(this.cartCount)

+ 17 - 1
mallApp/src/pages/goods/components/sel-popup.vue

@@ -1,6 +1,7 @@
 <!--
   商品详情选规格/数量弹层
-  用途:加购或立即购买前选择规格与数量;秒杀会按限购、活动库存收紧可选上限
+  用途:加购或立即购买前选择规格与数量;秒杀会按限购、活动库存收紧可选上限;
+  无价格商品作为秒杀时按活动价允许加购/购买,不拦截询价
   使用方:pages/goods/detail
 -->
 <template>
@@ -200,6 +201,21 @@ export default {
         return null;
       }
       const sale = spec || this.info;
+      // 秒杀进行中:即使原商品/规格无价格,也按活动价允许加购/购买,不走询价
+      if (this.isSeckill) {
+        const seckillPrice = Number(this.info.price) || 0;
+        if (!(seckillPrice > 0)) {
+          this.$msg("商品还没有价格哦");
+          return null;
+        }
+        if (!this.assertBuyQty(spec)) {
+          return null;
+        }
+        const pricedSpec = spec
+          ? { ...spec, price: seckillPrice, priceType: 1 }
+          : spec;
+        return { spec: pricedSpec, ok: true };
+      }
       // 无价格商品不能加购/购买,引导询价
       if (Number(sale.priceType) === 0) {
         this.$msg('请向商家咨询价格');

+ 80 - 27
mallApp/src/pages/goods/detail.vue

@@ -2,7 +2,8 @@
   商城商品详情
   谁用:mallApp 顾客查看花束并加购/购买
   解决问题:多规格 specList 首项即主记录可售规格,选中后 specGoodsId 可等于主商品 id;
-  顶部信息卡按标题/分享、副文案、价格已售、服务保障排布,秒杀条保留在价格上方
+  顶部信息卡按标题/分享、副文案、价格已售、服务保障排布,秒杀条保留在价格上方;
+  无价格商品作为秒杀时展示秒杀价,底部走加购/购买而不是询价
 -->
 <template>
   <view class="page-container">
@@ -37,14 +38,14 @@
         </view>
         <view class="price-sold-row">
           <view class="price-wrap">
-            <view class="app-price app-bold" v-if="data.priceType == 1">
+            <view class="app-price app-bold" v-if="showNumericPrice">
               <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>
@@ -221,6 +222,8 @@ export default {
       activityEndTime: 0,
       seckillCountdownText: '',
       _seckillTimer: null,
+      /** 覆盖秒杀价前的原 price/priceType,活动结束后还原,避免无价格商品仍显示可买 */
+      _originPriceSnapshot: null,
     };
   },
   onLoad() {
@@ -252,7 +255,7 @@ export default {
   },
   onShareAppMessage() {
     const path = this.buildGoodsSharePath()
-    const sharePrice = this.data && this.data.priceType == 1
+    const sharePrice = this.data && (this.data.priceType == 1 || this.isSeckillActive)
       ? (this.data.price ? parseFloat(this.data.price).toFixed(2) : 0)
       : '面议'
     return {
@@ -284,6 +287,12 @@ export default {
       const endTime = Number(this.activityEndTime) || 0;
       return endTime > 0 && endTime > Math.floor(Date.now() / 1000);
     },
+    /**
+     * 是否展示数字价:有价格商品,或无价格但秒杀进行中(用秒杀价直购)
+     */
+    showNumericPrice() {
+      return Number(this.data.priceType) === 1 || this.isSeckillActive;
+    },
     /** 底部购物车角标:扎数 + 支数(与分类页/首页 tab 统计口径一致) */
     cartBadgeCount() {
       if (!this.selectList || !this.selectList.length) {
@@ -393,6 +402,7 @@ export default {
       if (end <= 0 || now >= end) {
         this.seckillCountdownText = '';
         this.clearSeckillTimer();
+        this.restoreGoodsPriceAfterSeckill();
         return;
       }
       let diff = end - now;
@@ -406,20 +416,58 @@ export default {
       // ≥1 天时前置「X天」,避免出现 48:00:00 这类超大小时数
       this.seckillCountdownText = d > 0 ? `${d}天${hms}` : hms;
     },
-    /** 用秒杀价覆盖详情展示价格,原价保留用于划线展示;仅覆盖展示层,实际下单价格由后端独立核价 */
+    /**
+     * 用秒杀价覆盖详情展示价格与可售状态。
+     * 无价格商品(priceType=0)作为秒杀时同样覆盖:展示秒杀价、priceType 视为有价,
+     * 从而底部走加购/购买而不是询价。原价仅在原商品有数字价时用于划线。
+     * 仅覆盖展示与购物车标记,实际下单价格由后端独立核价。
+     */
     applySeckillPriceIfNeeded() {
       if (!this.isSeckillActive) return;
-      if (this.data.priceType != 1) return;
+      // 只在首次覆盖时记下原价类型,切规格不要覆盖这份快照
+      if (!this._originPriceSnapshot) {
+        this._originPriceSnapshot = {
+          priceType: Number(this.data.priceType),
+          price: this.data.price,
+        };
+      }
+      const alreadyApplied = this.data.activityType === 'seckill'
+        && Number(this.data.price) === Number(this.activityPrice)
+        && Number(this.data.priceType) === 1;
+      let originPrice = Number(this.data.originPrice) || 0;
+      // 尚未覆盖时:有价格商品用当前售价做划线原价;无价格商品没有可划线原价
+      if (!alreadyApplied) {
+        originPrice = Number(this.data.priceType) === 1
+          ? (parseFloat(this.data.price) || 0)
+          : 0;
+      }
       this.data = {
         ...this.data,
-        originPrice: this.data.price,
+        originPrice,
         price: this.activityPrice,
+        priceType: 1,
         activityType: 'seckill',
         activityEndTime: this.activityEndTime,
         activityLimit: this.activityLimit,
         activityStock: this.activityStock,
       };
     },
+    /**
+     * 秒杀结束后还原商品原价类型。
+     * 无价格商品覆盖时曾把 priceType 改成 1,不还原会继续显示加购/购买。
+     */
+    restoreGoodsPriceAfterSeckill() {
+      const snap = this._originPriceSnapshot;
+      if (!snap || !this.data) return;
+      this.data = {
+        ...this.data,
+        price: snap.price,
+        priceType: snap.priceType,
+        originPrice: 0,
+        activityType: '',
+      };
+      this._originPriceSnapshot = null;
+    },
     init() {
       if (!this.option || !this.option.id) {
         return;
@@ -603,6 +651,7 @@ export default {
         res.data.stock = res.data.stock ? parseInt(res.data.stock) : 0;
         this.data = res.data;
         this.customId = res.data.customId;
+        this._originPriceSnapshot = null;
         this.applySeckillPriceIfNeeded();
 
         //商品描述
@@ -620,8 +669,9 @@ export default {
         this.common_previewContent =
           typeof content === "string" ? JSON.parse(content) : content || [];
 
-        const sharePrice = res.data.priceType == 1
-          ? (res.data.price ? parseFloat(res.data.price).toFixed(2) : 0)
+        const shareGoods = this.data || res.data;
+        const sharePrice = shareGoods.priceType == 1
+          ? (shareGoods.price ? parseFloat(shareGoods.price).toFixed(2) : 0)
           : '面议';
         const applyShare = () => {
           const shareParams = {
@@ -657,6 +707,7 @@ export default {
     /**
      * 应用选中规格:更新价格/库存/选中标记,并把轮播图切到该规格自己的图片
      * specList 每项都由后端 formatGoodsImg 处理过,自带完整 imgList
+     * 秒杀进行中会在写回规格价后重新覆盖活动价,保证无价格规格仍可直购
      */
     applySpecSelection(index) {
       const spec = (this.data.specList || [])[index];
@@ -672,12 +723,18 @@ export default {
         selectedSpecGoodsId: spec.id,
         selectedSpecName: spec.specName,
       };
+      // 切规格会写回规格自身价格/类型,秒杀进行中要重新盖上活动价,避免无价格规格又变回询价
+      this.applySeckillPriceIfNeeded();
       if (Array.isArray(spec.imgList) && spec.imgList.length) {
         this.imgList = spec.imgList.map((img) => ({ img }));
       }
     },
-    /** 规格卡片价格文案:无价格商品与主价格区一致展示 ¥? */
+    /** 规格卡片价格文案:秒杀进行中统一展示秒杀价;无价格非秒杀与主价格区一致展示 ¥? */
     specPriceText(spec) {
+      if (this.isSeckillActive) {
+        const price = parseFloat(this.activityPrice) || 0;
+        return '¥' + (Number.isInteger(price) ? price : price.toFixed(2));
+      }
       if (Number(spec.priceType) !== 1) {
         return '¥?';
       }
@@ -699,11 +756,23 @@ export default {
         this.setSelectInfoByType({ type: this.pageType || 'cg', info: cached });
       }
     },
+    /**
+     * 打开选规格/数量弹层。未登录先弹登录;秒杀若购物车已达限购,加购和立即购买都在此拦截。
+     * 库存不足由底部栏点击时先行拦截;这里再拦限购是因为要看购物车已有数量。
+     */
     showPopup(mode = 'buy') {
       if (!this.ensureLogin()) {
         return;
       }
-      this.popupMode = mode === 'cart' ? 'cart' : 'buy';
+      const popupMode = mode === 'cart' ? 'cart' : 'buy';
+      if (this.isSeckillActive && Number(this.activityLimit) > 0) {
+        if (this.currentSeckillCartCount >= this.activityLimit) {
+          this.$msg('超过限购数量,每人限购' + this.activityLimit + '件');
+          return;
+        }
+      }
+      this.buyNum = 1;
+      this.popupMode = popupMode;
       this.popupShow = true;
     },
     /** 跳转店铺购物车页 */
@@ -718,22 +787,6 @@ export default {
         type: 2,
       });
     },
-    /**
-     * 打开选规格/数量弹层。秒杀若购物车已达限购,加购和立即购买都在此拦截。
-     * 库存不足由底部栏点击时先行拦截;这里再拦限购是因为要看购物车已有数量。
-     */
-    showPopup(mode = 'buy') {
-      const popupMode = mode === 'cart' ? 'cart' : 'buy';
-      if (this.isSeckillActive && Number(this.activityLimit) > 0) {
-        if (this.currentSeckillCartCount >= this.activityLimit) {
-          this.$msg('超过限购数量,每人限购' + this.activityLimit + '件');
-          return;
-        }
-      }
-      this.buyNum = 1;
-      this.popupMode = popupMode;
-      this.popupShow = true;
-    },
     /** 拼混合结算页地址(与花材详情立即购买一致) */
     getAffirmUrl() {
       const account = (this.option && this.option.account) || this.account || uni.getStorageSync('account') || 0;

+ 15 - 2
mallApp/src/pages/goods/section-list.vue

@@ -3,6 +3,7 @@
   从秒杀/团购/热门推荐/今日上新/下拉商品的「更多」进入;
   样式参照 goods/list.vue,本次不包含搜索与筛选功能。
   秒杀/团购保留活动价、原价划线与团购人数展示。
+  无价格商品作为秒杀时仍展示秒杀价,单规格可直接加购,不走询价。
   右侧购物车:单规格直接加购,多规格跳转详情选规格(与 list.vue 一致)
   左下角悬浮购物车:展示当前店铺购物车数量,点击跳转 pages/home/cart
 -->
@@ -177,11 +178,13 @@ export default {
         })
     },
     /**
-     * 无价格商品(priceType=0):列表不展示数字价,统一用 -- 提示需询价
+     * 无价格商品(priceType=0):列表不展示数字价,统一用 -- 提示需询价。
+     * 秒杀列表用活动价,不按原商品无价格走询价。
      * @param {Object} item 商品
      * @returns {boolean}
      */
     isNoPrice(item) {
+      if (this.isSeckill) return false
       return Number(item && item.priceType) === 0
     },
     formatPrice(val) {
@@ -251,10 +254,20 @@ export default {
       const goods = {
         ...item,
         id: item.id || item.goodsId,
-        priceType: item.priceType != null ? item.priceType : 1,
+        priceType: this.isSeckill ? 1 : (item.priceType != null ? item.priceType : 1),
         cover: item.coverUrl || item.smallCover || item.cover,
         smallCover: item.smallCover || item.coverUrl || item.cover
       }
+      // 秒杀更多列表直接加购:打上活动标记,无价格商品也能按秒杀价入购物车
+      if (this.isSeckill) {
+        goods.activityType = 'seckill'
+        goods.activityEndTime = this.activityEndTime
+        goods.activityLimit = item.limit || 0
+        goods.originPrice = item.originPrice
+        if (item.stock !== undefined && item.stock !== null && item.stock !== '') {
+          goods.activityStock = Number(item.stock) || 0
+        }
+      }
       const ok = this.addBouquetToCart(goods, null, 1)
       if (ok) {
         uni.showToast({