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