Files
lucky_shop/pages_goods/public/js/detail.js
ZF sun e01ba29604 fix(鸿蒙兼容): 修复商品详情页骨架屏常驻问题
1. 兼容后端不返回goods_promotion字段的场景,避免直接取值抛错
2. 替换鸿蒙元服务下不可靠的nextTick和延迟关闭逻辑,改为同步关闭骨架屏,解决数据返回后骨架屏常驻的问题
2026-07-31 16:52:16 +08:00

617 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export default {
data() {
return {
goodsRoute: '/pages_goods/detail',
couponList: [], //优惠券列表
couponBtnSwitch: false, //获取优惠券防止重复提交
// 商品详情骨架屏:true 显示骨架,false 显示真实内容
// 首屏进入即为 true,商品详情数据首屏渲染完成后(与 loading-cover 同时机)置 false
loading: true,
posterApi: '/api/goods/poster',
//满减活动
manjian: {
type: 0,
manjian_name: "",
rule_json: null
},
//组合套餐
bundlingType: false,
bundling: [{
bundling_goods: {
bl_name: '',
sku_image: ''
}
}],
membercard: null, // 会员卡信息
hackReset: true,
cardOff: false
// 留言表单数据不再在 data() 维护:detail.vue 的 computed formDiyValue
// 直接从 goodsSkuDetail.goods_form / goods_form_config / goods_form_* 实时读取并打包成
// diy-form raw 模式期望的 value 结构。
}
},
computed: {
showDiscount() {
var flag = false;
if (
this.preview == 0 &&
this.addonIsExist.discount &&
this.goodsSkuDetail.promotion_type == 1 &&
this.goodsSkuDetail.discountTimeMachine &&
(!this.goodsSkuDetail.member_price || (this.goodsSkuDetail.member_price > 0 && Number(this.goodsSkuDetail.member_price) > Number(this.goodsSkuDetail.discount_price))
)
) {
flag = true;
}
return flag;
},
memberCardDiscount() {
let discount = 0,
showPrice = this.goodsSkuDetail.member_price > 0 && Number(this.goodsSkuDetail.member_price) < Number(this.goodsSkuDetail.discount_price) ? this.goodsSkuDetail.member_price : this.goodsSkuDetail.discount_price;
if (this.membercard && this.membercard.member_price > 0 && (parseFloat(showPrice) > parseFloat(this.membercard.member_price))) {
discount = parseFloat(showPrice) - parseFloat(this.membercard.member_price);
}
return discount.toFixed(2);
},
// 弹窗高度: 受限于视口, 内部表单 <scroll-view> 自带滚动, 不再按字段数无限加高
FormHeight() {
// 上限 65vh: 留足顶部状态栏/导航栏 + 底部 home indicator, 也不会显得"一打开就半屏遮脸"
// 字段多时通过内部 <scroll-view scroll-y> 自然滚动
const formList = (this.goodsSkuDetail && this.goodsSkuDetail.goods_form)
|| this.goodsForm;
let height = 45;
if (formList && formList.length) {
// 少量字段: 每个约 5vh; 字段多时递增放缓并封顶, 避免接近屏顶
// 5 个字段 → 45 + min(10, 25) = 55vh
// 10 个字段 → 45 + min(10, 50) = 55vh (已封顶)
const dynamic = Math.min(10, formList.length * 5);
height = Math.min(55, height + dynamic);
}
height += 'vh';
return height;
}
},
onLoad(data) {
// #ifdef MP-ALIPAY
let options = my.getLaunchOptionsSync();
options.query && Object.assign(data, options.query)
// #endif
this.skuId = data.sku_id || 0;
this.goodsId = data.goods_id || 0;
// 小程序扫码进入
if (data.scene) {
var sceneParams = decodeURIComponent(data.scene);
sceneParams = sceneParams.split('&');
if (sceneParams.length) {
sceneParams.forEach(item => {
if (item.indexOf('goods_id') != -1) this.goodsId = item.split('-')[1];
});
}
}
// #ifdef MP-WEIXIN
this.getShareImg();
// #endif
},
async onShow() {
// 同步获取商品详情
// 鸿蒙元服务兼容: 包 try/catch, 防止内部任何抛错导致整个 page 显示空白
try {
await this.getGoodsSkuDetail();
} catch (e) {
console.error('getGoodsSkuDetail failed:', e);
// 失败时不关闭骨架屏, 由视图层 hasRealData(goods_id>0) 自动保留占位,
// 避免错误地把页面切到"空详情"状态
uni.showToast({
title: this.$lang ? this.$lang('common.networkError') : '网络异常,请重试',
icon: 'none'
});
}
},
onHide() {
this.couponBtnSwitch = false;
},
methods: {
//拨打电话
phoneClick(mobile){
uni.makePhoneCall({
phoneNumber: mobile+'',
success(){},
fail(){}
})
},
setSkuId(val) {
if (val) {
this.skuId = val;
this.getBundling();
}
},
// 获取商品详情
async getGoodsSkuDetail() {
let res;
try {
// 修复鸿蒙元服务: 必须传 async: false !
// 当 App.vue 的 boot (lang/theme/remote-config) 未完成时,
// sendRequest 在 async !== false 模式下会直接 return undefined (不返回 promise),
// 导致 res.code undefined / res.data 抛 TypeError, 整个 onShow 失败.
res = await this.$api.sendRequest({
url: '/api/goodssku/detail',
async: false,
data: {
sku_id: this.skuId,
goods_id: this.goodsId,
}
});
// 防御: 即便 async:false 仍可能在极端情况下返回 undefined
if (!res || typeof res !== 'object') {
throw new Error('sendRequest returned non-object response (likely boot not complete)');
}
} catch (e) {
console.error('getGoodsSkuDetail sendRequest failed:', e);
throw e;
}
let data = res.data;
if (data.goods_sku_detail != null) {
if (data.goods_sku_detail.promotion_type == 'presale' && data.goods_sku_detail.presale_id) {
this.$util.redirectTo('/pages_promotion/presale/detail', {
id: data.goods_sku_detail.presale_id,
sku_id: this.skuId
}, 'reLaunch');
return;
}
if(uni.getStorageSync('lang') == 'en-us') data.goods_sku_detail.goods_name = data.goods_sku_detail.en_goods_name
// 关键改动: 先一次性赋值, 释放事件循环让 MP-Weixin 把首屏 (含媒体/价格/标题) 渲出来,
// 再 $nextTick 后才跑同步字段变换 (sku_images split / JSON.parse / 属性去重 等),
// 避免连续多次 Vue 响应式写触发同一帧内多次 vdom 重算, 显著改善"打开详情很慢"的体感。
this.goodsSkuDetail = data.goods_sku_detail;
if (!this.skuId) this.skuId = this.goodsSkuDetail.sku_id;
if (!this.goodsId) this.goodsId = this.goodsSkuDetail.goods_id;
// 分享参数、链接
this.shareQuery = 'goods_id=' + this.goodsSkuDetail.goods_id;
this.shareUrl = this.goodsRoute + '?' + this.shareQuery;
// 在线客服聊天参数
this.chatRoomParams = {
sku_id: this.goodsSkuDetail.sku_id
};
// 鸿蒙兼容: 后端在部分场景下可能不返回 goods_promotion, 缺字段直接 .[0] 会抛 TypeError,
// 整个 onShow 走 catch 路径无法关闭骨架屏, 表现为"数据已回来但永远显示骨架屏"
const promotionList = this.goodsSkuDetail?.goods_promotion || [];
let typeId = promotionList.length > 0 ? promotionList[0] : undefined;
if (typeId) {
// 限时折扣
if (typeId.discount_id) {
this.chatRoomParams.type = 'discount'
this.chatRoomParams.type_id = typeId.discount_id
}
}
// 海报参数
this.posterParams = {
goods_id: this.goodsId
};
// 让首屏主区域先绘制
// 兼容鸿蒙元服务: $nextTick 在某些 JSVM 实现下不可靠, 用 setTimeout 兜底
// #ifdef MP-HARMONY
await new Promise(resolve => setTimeout(resolve, 16));
// 修复鸿蒙元服务: 不依赖 setTimeout 关闭骨架屏.
// 鸿蒙 JSVM 在 onShow 早期窗口的 setTimeout 回调有时被推迟/丢弃, 表现为"数据已回来但永远显示骨架屏".
// 数据已赋值 + 首屏让步, 同步直接关闭骨架屏, 由 hasRealData 兜底避免空详情.
this.loading = false;
// #endif
// #ifndef MP-HARMONY
await this.$nextTick();
// 关闭商品详情骨架屏: 在真实数据首屏渲染完成后再隐藏骨架屏,
// 用户会看到骨架屏 → 真实详情 的清晰过渡
// 最短展示 300ms: 数据请求很快时也保留足够感知时间, 否则一闪而过用户根本看不到骨架屏
setTimeout(() => {
this.loading = false;
}, 300);
// #endif
// 处理商品数据 (同步字段变换 + 后台并发 getGoodsForm)
await this.handleGoodsSkuData();
// 限时折扣
if (this.goodsSkuDetail.promotion_type == 1 && this.addonIsExist.discount) {
//检测倒计时
if ((this.goodsSkuDetail.end_time - res.timestamp) > 0) {
this.goodsSkuDetail.discountTimeMachine = this.$util.countDown(this.goodsSkuDetail.end_time - res.timestamp);
} else {
this.goodsSkuDetail.promotion_type = 0;
}
}
if (this.goodsSkuDetail.promotion_type == 1 && this.goodsSkuDetail.discountTimeMachine) {
if (this.goodsSkuDetail.member_price > 0 && Number(this.goodsSkuDetail.member_price) <= Number(this.goodsSkuDetail.discount_price)) {
this.goodsSkuDetail.show_price = this.goodsSkuDetail.member_price;
} else {
this.goodsSkuDetail.show_price = this.goodsSkuDetail.discount_price;
}
} else {
if (this.goodsSkuDetail.member_price > 0) {
this.goodsSkuDetail.show_price = this.goodsSkuDetail.member_price;
} else {
this.goodsSkuDetail.show_price = this.goodsSkuDetail.price;
}
}
// 满减
if (this.goodsSkuDetail.manjian) {
this.getManjian(this.goodsSkuDetail.manjian);
}
// 会员卡
if (this.goodsSkuDetail.membercard) {
this.membercard = this.goodsSkuDetail.membercard;
}
// 优惠券
if (Array.isArray(this.goodsSkuDetail.coupon_list)) {
this.couponList = this.goodsSkuDetail.coupon_list;
this.couponList.forEach(v => {
if (v.count == v.lead_count) v.useState = 2;
else if (v.max_fetch != 0 && v.member_coupon_num && v.member_coupon_num >= v.max_fetch) v.useState = 1;
else v.useState = 0;
});
this.couponList = this.couponList.sort(this.sortBy('useState'))
}
// 组合套餐
// if (this.goodsSkuDetail.bundling_list) {
// this.handleBundlingData(this.goodsSkuDetail.bundling_list);
// }
} else {
this.$util.redirectTo('/pages_tool/goods/not_exist', {}, 'redirectTo');
}
},
choiceSku() {
this.$refs.goodsSku.show("buy_now", () => {
this.$store.dispatch('getCartNumber');
});
},
//商品留言打开
async showinformation(){
// 修复: 第一次打开时, uni-popup 内部 <scroll-view><diy-form> 还没完成首次渲染,
// 直接 open() 会让用户看到空白弹窗。这里:
// 1) 若商品表单后台请求尚未完成 (或根本没触发), 先发起并 await 该请求
// 2) 用 $nextTick 等组件挂载完成后再打开
try {
// 若页面已进入但 _goodsFormPromise 还没建 (如 sku 切换/数据未到等场景),
// 这里补一个并发请求; 若已存在, 复用旧 promise 不重复请求。
if (!this._goodsFormPromise || typeof this._goodsFormPromise.then !== 'function') {
const loader = (this.getGoodsForm && typeof this.getGoodsForm === 'function')
? this.getGoodsForm()
: Promise.resolve();
this._goodsFormPromise = loader;
await loader;
} else {
await this._goodsFormPromise;
}
} catch (e) {
console.warn('await goods form promise failed', e);
}
// 第一次强制刷新 popup 内部组件布局 (nextTick), 确保表单完整渲染
// 兼容鸿蒙元服务: $nextTick 不可靠时降级到 setTimeout
try { await this.$nextTick(); } catch (e) { /* 兜底 */ }
// 微信小程序下 scroll-view 在首次以 transform 隐藏后再显示时, 内部子节点布局可能未刷新
// 双 nextTick 兜底, 保证表单完整可见
try { await this.$nextTick(); } catch (e) { /* 兜底 */ }
if (this.$refs.informationPopup && typeof this.$refs.informationPopup.open === 'function') {
this.$refs.informationPopup.open();
}
},
//商品留言关闭
closeinformationPopup(){
this.$refs.informationPopup.close();
},
//保存商品留言
saveubfirnation(){
// ref 指向 <diy-form>:diy-form 只暴露 getFormData() 方法,无 formData 属性
if (!this.$refs.form.verify()) return;
// uni.setStorageSync('goodFormData', {
// goods_id: this.goodsSkuDetail.goods_id,
// form_data: this.$refs.form.getFormData()
// });
this.$api.sendRequest({
url: '/api/goods/information',
data: {
goods_id: this.goodsSkuDetail.goods_id,
form_data: JSON.stringify(this.$refs.form.getFormData())
},
success: res => {
var data = res.data;
if (res.code == 0) {
}else{
this.$util.showToast({
title: res.msg
});
}
},
});
},
// 加入购物车
joinCart() {
if (!this.storeToken && this.preview == 0) {
this.redirectTologin(this.shareUrl);
return;
}
if (this.goodsSkuDetail.is_virtual == 1) {
this.$refs.goodsSku.show("buy_now", () => {
this.$store.dispatch('getCartNumber');
});
} else {
this.$refs.goodsSku.show("join_cart", () => {
this.$store.dispatch('getCartNumber');
});
}
},
// 立即购买
buyNow() {
if (!this.storeToken && this.preview == 0) {
this.redirectTologin(this.shareUrl);
return;
}
this.$refs.goodsSku.show("buy_now", () => {
});
},
sortBy(field) {
//根据传过来的字段进行排序,y-x 得分从高到低,x-y 从低到高
return (y, x) => {
return y[field] - x[field]
}
},
// 打开优惠券弹出层
openCouponPopup() {
this.$refs.couponPopup.open();
},
// 关闭优惠券弹出层
closeCouponPopup() {
this.$refs.couponPopup.close();
},
// 领取优惠券
receiveCoupon(item) {
let that = this;
if (this.preview) return; // 开启预览,禁止任何操作和跳转
if (this.couponBtnSwitch) return;
this.couponBtnSwitch = true;
if (this.storeToken) {
this.$api.sendRequest({
url: '/coupon/api/coupon/receive',
data: {
coupon_type_id: item.coupon_type_id,
get_type: 2 //获取方式:1订单2.直接领取3.活动领取
},
success: res => {
var data = res.data;
let msg = '';
let list = this.couponList;
if (res.data.is_exist == 1 && res.code < 0) {
msg = this.$lang('goods.detail.couponAlreadyClaimed');
} else if (res.code == 0) {
msg = this.$lang('goods.detail.couponClaimSuccess');
} else {
msg = res.message;
}
if (res.data.is_exist == 1) {
for (let i = 0; i < list.length; i++) {
if (list[i].coupon_type_id == item.coupon_type_id) {
that.$set(that.couponList[i], 'useState', 1);
}
}
} else {
for (let i = 0; i < list.length; i++) {
if (list[i].coupon_type_id == item.coupon_type_id) {
that.$set(that.couponList[i], 'useState', 2);
}
}
}
this.$util.showToast({
title: msg
});
that.$forceUpdate()
this.hackReset = false;
this.$nextTick(() => {
this.hackReset = true;
})
this.couponBtnSwitch = false;
},
});
} else {
this.redirectTologin(this.shareUrl);
this.couponBtnSwitch = false;
}
},
//-------------------------------------满减-------------------------------------
//获取满减信息
getManjian(data) {
this.manjian = data;
let limit = data.type == 0 ? this.$lang('goods.detail.manjianUnitYuan') : this.$lang('goods.detail.manjianUnitPiece');
Object.keys(data.rule_json).forEach((key) => {
var item = data.rule_json[key];
if (item.coupon_data) {
for (var i = 0; i < item.coupon_data.length; i++) {
item.coupon_data[i].coupon_num = item.coupon_num[i]
}
}
item.limit = data.type == 0 ? parseFloat(item.limit).toFixed(2) : parseInt(item.limit);
// 满减
if (item.discount_money != undefined) {
let itemText = this.$lang('goods.detail.fullReductionItem', { limit: item.limit, unit: limit, money: item.discount_money });
if (this.manjian.manjian == undefined) {
this.manjian.manjian = itemText;
} else {
this.manjian.manjian += this.$lang('goods.detail.manjianItemSeparator') + itemText;
}
}
// 满送
if (item.point != undefined || item.coupon != undefined) {
let text = '';
if (item.point != undefined) {
text = this.$lang('goods.detail.fullGiftPoints', { point: item.point });
}
if (item.coupon != undefined && item.coupon_data != undefined) {
item.coupon_data.forEach((couponItem, couponIndex) => {
if (couponItem.type == 'discount') {
let couponText = this.$lang('goods.detail.fullGiftDiscountCoupon', { count: item.coupon_num[couponIndex], discount: parseFloat(couponItem.discount) });
if (text == '') text = couponText;
else text += this.$lang('goods.detail.fullGiftConnector') + couponText;
} else {
let couponText = this.$lang('goods.detail.fullGiftMoneyCoupon', { count: item.coupon_num[couponIndex], money: parseFloat(couponItem.money) });
if (text == '') text = couponText;
else text += this.$lang('goods.detail.fullGiftConnector') + couponText;
}
})
}
let giftItemText = this.$lang('goods.detail.fullGiftItem', { limit: item.limit, unit: limit, gift: text });
if (this.manjian.mansong == undefined) {
this.manjian.mansong = giftItemText;
} else {
this.manjian.mansong += this.$lang('goods.detail.manjianItemSeparator') + giftItemText;
}
}
// 包邮
if (item.free_shipping != undefined) {
let itemText = this.$lang('goods.detail.freeShippingItem', { limit: item.limit, unit: limit });
if (this.manjian.free_shipping == undefined) {
this.manjian.free_shipping = itemText;
} else {
this.manjian.free_shipping += this.$lang('goods.detail.manjianItemSeparator') + itemText;
}
}
});
},
openManjianPopup() {
this.$refs.manjianPopup.open();
},
closeManjianPopup() {
this.$refs.manjianPopup.close();
},
//-------------------------------------组合套餐-------------------------------------
//获取当前商品关联的组合套餐
getBundling() {
// 连锁门店没有营销活动
if (this.globalStoreConfig && this.globalStoreConfig.store_business == 'store') return;
this.$api.sendRequest({
url: "/bundling/api/bundling/lists",
data: {
sku_id: this.skuId
},
success: res => {
// this.handleBundlingData(res.data);
}
});
},
handleBundlingData(data) {
this.bundling = data;
if (this.bundling.length) {
for (var i = 0; i < this.bundling[0].bundling_goods.length; i++) {
if (this.bundling[0].bundling_goods[i].sku_id == this.skuId) {
this.bundlingType = true;
break;
} else {
this.bundlingType = false;
}
}
for (var i = 0; i < this.bundling.length; i++) {
for (var j = 0; j < this.bundling[i].bundling_goods.length; j++) {
if (this.bundling[i].bundling_goods[j].sku_id == this.skuId) {
this.bundling[i].bundling_goods.splice(j, 1);
}
}
}
}
},
// 打开组合套餐弹出层
openBundlingPopup() {
this.$refs.bundlingPopup.open();
},
// 关闭组合套餐弹出层
closeBundlingPopup() {
this.$refs.bundlingPopup.close();
},
imageError() {
this.goodsSkuDetail.sku_image = this.$util.getDefaultImage().goods;
this.$forceUpdate();
},
bundlingImageError(index, goods_index) {
this.bundling[index].bundling_goods[goods_index].sku_image = this.$util.getDefaultImage().goods;
this.$forceUpdate();
},
fenxiao() {
this.$refs.fenxiaoPopup.show()
},
toGoodsDetail(item) {
this.$util.redirectTo(this.goodsRoute, {
sku_id: item
});
},
toComoDetail(id) {
this.$util.redirectTo('/pages_promotion/bundling/detail', {
bl_id: id
});
},
/**
* 获取分享图
*/
getShareImg() {
let posterParams = {
goods_id: this.goodsId
};
this.$api.sendRequest({
url: '/api/goods/shareimg',
data: {
qrcode_param: JSON.stringify(posterParams)
},
success: res => {
if (res.code == 0) this.shareImg = res.data.path + '?no=' + parseInt((new Date()).getTime() / 1000);
}
})
},
/**
* 金额格式化输出
* @param {Object} money
*/
decimalPointFormat(money) {
if (isNaN(parseFloat(money))) return money;
let arr = money.toString().split(".");
let arr1 = arr[1].split("").reverse();
let arr2 = [];
arr1.forEach((item, index) => {
if (item > 1) {
arr2 = arr1.splice(index);
}
});
let str = arr2.length ? (arr[0] + "." + arr2.reverse().join("")) : arr[0];
return str;
}
}
}