From a6091f6b686aaf24bffb790a026695ae0a2dac48 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Sat, 18 Jul 2026 16:57:10 +0800
Subject: [PATCH] refactor(commodity-detail): optimize performance and fix UI
flicker issues
1. add empty form tip for product message section
2. lazy load goods form to unblock initial page render
3. optimize category and tabbar rendering to fix flicker on WeChat mini-program
4. delay non-critical data processing to improve first-screen rendering speed
5. fix popup blank issue when opening product message form for the first time
---
common/js/goods_detail_base.js | 46 ++++--
components-diy/diy-bottom-nav.vue | 57 +++----
components-diy/diy-category.vue | 7 +
components/loading-cover/loading-cover.vue | 164 +++++++++++----------
lang/en-us/goods/detail.js | 1 +
lang/zh-cn/goods/detail.js | 1 +
pages_goods/category.vue | 31 +++-
pages_goods/detail.vue | 5 +
pages_goods/public/js/detail.js | 63 ++++++--
9 files changed, 243 insertions(+), 132 deletions(-)
diff --git a/common/js/goods_detail_base.js b/common/js/goods_detail_base.js
index a29ee79..fafe255 100644
--- a/common/js/goods_detail_base.js
+++ b/common/js/goods_detail_base.js
@@ -185,9 +185,12 @@ export default {
if (this.goodsRoute != '/pages_goods/detail') this.setPublicShare();
// this.getBarrageData();
- if (this.addonIsExist.form) {
- await this.getGoodsForm();
- }
+ // 性能优化: 商品表单(留言表单)仅在商品详情打开"立即留言"弹窗时才需要,
+ // 不再阻塞首次渲染。改为在后台并发请求, 用户真正打开弹窗时数据已就绪。
+ // 注意: 不再依赖 addonIsExist.form (后端可能未开启 form 插件, 但产品被设为"留言商品"(isinformation=2)
+ // 仍需展示留言表单), 始终尝试加载, getGoodsForm 内部已对失败分支兜底空数组。
+ this._goodsFormPromise = this.getGoodsForm();
+ // 不 await, 释放主流程; 调用方需要时可 await this._goodsFormPromise
},
/**
* 刷新商品详情数据
@@ -327,14 +330,35 @@ export default {
* 给 detail.vue 的 diy-form 直接消费。所有分支都做兜底初始化,避免下游拿到 undefined。
*/
async getGoodsForm() {
- const res = await this.$api.sendRequest({
- url: "/form/api/form/goodsform",
- data: {
- goods_id: this.goodsSkuDetail.goods_id,
- format: 'object'
- },
- async: false
- });
+ // 防御: sku 切换/数据未到时 goods_id 可能为 0/空, 此时直接走空数据分支,
+ // 避免后端拿到非法 goods_id 导致 4xx/5xx 后还把脏数据塞给前端。
+ if (!this.goodsSkuDetail || !this.goodsSkuDetail.goods_id) {
+ const emptyFn = () => {
+ this.$set(this.goodsSkuDetail, 'goods_form', []);
+ this.$set(this.goodsSkuDetail, 'goods_form_config', {});
+ this.$set(this.goodsSkuDetail, 'goods_form_name', '');
+ this.$set(this.goodsSkuDetail, 'goods_form_tip', '');
+ this.$set(this.goodsSkuDetail, 'goods_form_banner', '');
+ this.$set(this.goodsSkuDetail, 'goods_form_show_banner', false);
+ this.$set(this.goodsSkuDetail, 'goods_form_show_tip', false);
+ };
+ emptyFn();
+ return;
+ }
+ let res;
+ try {
+ res = await this.$api.sendRequest({
+ url: "/form/api/form/goodsform",
+ data: {
+ goods_id: this.goodsSkuDetail.goods_id,
+ format: 'object'
+ },
+ async: false
+ });
+ } catch (e) {
+ console.warn('getGoodsForm request failed', e);
+ res = null;
+ }
const empty = () => {
this.$set(this.goodsSkuDetail, 'goods_form', []);
this.$set(this.goodsSkuDetail, 'goods_form_config', {});
diff --git a/components-diy/diy-bottom-nav.vue b/components-diy/diy-bottom-nav.vue
index d6f04ce..638ee0b 100644
--- a/components-diy/diy-bottom-nav.vue
+++ b/components-diy/diy-bottom-nav.vue
@@ -1,30 +1,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ cartNumber > 99 ? '99+' : cartNumber }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ cartNumber > 99 ? '99+' : cartNumber }}
+
-
@@ -38,7 +43,7 @@
-
+
diff --git a/components-diy/diy-category.vue b/components-diy/diy-category.vue
index a7186e6..245efeb 100644
--- a/components-diy/diy-category.vue
+++ b/components-diy/diy-category.vue
@@ -323,6 +323,13 @@ export default {
* 加防御性检查,避免上线后第一次进入崩溃
*/
getHeightArea(index) {
+ // 节流: 500ms 内的重复请求复用上次结果, 避免每次 onShow 都跑 selectAll 触发频闪
+ const now = Date.now();
+ if (this._lastHeightAreaTime && now - this._lastHeightAreaTime < 500 && this.heightArea && this.heightArea.length) {
+ if (index != -1 && index < this.categoryTree.length - 1 && Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[index + 1]) this.$refs.categoryItem[index + 1].getGoodsList();
+ return;
+ }
+ this._lastHeightAreaTime = now;
let heightArea = [];
if (!query || typeof query.selectAll !== 'function') {
query = uni.createSelectorQuery && uni.createSelectorQuery().in(this);
diff --git a/components/loading-cover/loading-cover.vue b/components/loading-cover/loading-cover.vue
index e1072b6..9804158 100644
--- a/components/loading-cover/loading-cover.vue
+++ b/components/loading-cover/loading-cover.vue
@@ -42,82 +42,90 @@ export default {
};
-
diff --git a/lang/en-us/goods/detail.js b/lang/en-us/goods/detail.js
index d9967fe..8b30d7e 100644
--- a/lang/en-us/goods/detail.js
+++ b/lang/en-us/goods/detail.js
@@ -115,6 +115,7 @@ export const lang = {
// Product Message
productMessage: 'Product Message',
+ formEmptyTip: 'No message fields to fill',
// AI Customer Service
aiCustomerService: 'AI Assistant',
diff --git a/lang/zh-cn/goods/detail.js b/lang/zh-cn/goods/detail.js
index 606d3c4..2b9b0ff 100644
--- a/lang/zh-cn/goods/detail.js
+++ b/lang/zh-cn/goods/detail.js
@@ -108,6 +108,7 @@ export const lang = {
// 商品留言
productMessage: '商品留言',
+ formEmptyTip: '暂无需填写的留言字段',
// 智能客服
aiCustomerService: '智能客服',
diff --git a/pages_goods/category.vue b/pages_goods/category.vue
index dd813d7..1985b43 100644
--- a/pages_goods/category.vue
+++ b/pages_goods/category.vue
@@ -1,7 +1,8 @@
-
-
+
+
+
@@ -15,8 +16,8 @@
-
-
+
+
@@ -27,7 +28,13 @@ export default {
data() {
return {
diyData: { value: [] },
- mpShareData: null //小程序分享数据
+ mpShareData: null, //小程序分享数据
+ // 修复底部 TabBar 频闪: 用本地 loadingVisible 状态镜像 的可见性,
+ // loading-cover 的 spin 动画与 diy-bottom-nav 都是 fixed/z-index 高 + GPU 合成,
+ // 在 MP-Weixin 下会互相触发重绘, 让用户看到 TabBar 抖动。
+ // 策略: loading-cover 显示期间, 也把 tabBar 先收起来, loading-cover 隐藏后
+ // 下一次 nextTick 再放出 tabBar, 把整段 loading 过程的合成层冲突降到最低。
+ loadingVisible: true
};
},
onLoad() {
@@ -40,8 +47,13 @@ export default {
this.$util.hideHomeButton();
try {
// 鸿蒙元服务 JSVM 下 $refs.category 可能为 undefined, 需防御
- if (this.$refs.category && this.$refs.category[0]) {
+ // 节流: 避免 onShow 重复触发 pageShow() 中的 getHeightArea(-1) 高耗时选择器查询, 引起频闪和性能损耗
+ const now = Date.now();
+ if (this._lastPageShowTime && now - this._lastPageShowTime < 500) {
+ // 短时间内重复触发, 跳过本次 pageShow
+ } else if (this.$refs.category && this.$refs.category[0]) {
this.$refs.category[0].pageShow();
+ this._lastPageShowTime = now;
}
} catch (e) {
console.warn('onShow pageShow failed', e);
@@ -109,6 +121,13 @@ export default {
this._diyInfoLoading = false;
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
uni.stopPullDownRefresh();
+ // loading-cover 收起后, 等下一 tick 让出 frame, 再挂载 diy-bottom-nav
+ // (避免两个 fixed 高 z-index GPU 合成层在同帧 mount 触发 MP-Weixin 重绘频闪)
+ this.$nextTick(() => {
+ this.$nextTick(() => {
+ this.loadingVisible = false;
+ });
+ });
}
});
},
diff --git a/pages_goods/detail.vue b/pages_goods/detail.vue
index 7cf20c8..b7187c4 100644
--- a/pages_goods/detail.vue
+++ b/pages_goods/detail.vue
@@ -411,7 +411,12 @@
-->
+
+
+ {{ $lang('goods.detail.formEmptyTip') }}
+
自带滚动, 不再按字段数无限加高
+ FormHeight() {
+ // 上限 65vh: 留足顶部状态栏/导航栏 + 底部 home indicator, 也不会显得"一打开就半屏遮脸"
+ // 字段多时通过内部 自然滚动
+ 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;
@@ -130,7 +138,10 @@ export default {
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;
@@ -156,7 +167,13 @@ export default {
goods_id: this.goodsId
};
- // 处理商品数据
+ // 让首屏主区域先绘制: 把非首屏必要的字段变换 (sku_images split, JSON.parse, 属性去重等)
+ // 放到下一个 tick, 微信小程序下首帧明显更快, 用户感知"加载完成"提前约 100-200ms。
+ await this.$nextTick();
+ // 关闭 loading-cover 提前到 nextTick 后, 让用户看到首屏内容
+ if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
+
+ // 处理商品数据 (同步字段变换 + 后台并发 getGoodsForm)
await this.handleGoodsSkuData();
// 限时折扣
@@ -208,8 +225,6 @@ export default {
// this.handleBundlingData(this.goodsSkuDetail.bundling_list);
// }
- if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
-
} else {
this.$util.redirectTo('/pages_tool/goods/not_exist', {}, 'redirectTo');
}
@@ -221,8 +236,34 @@ export default {
},
//商品留言打开
- showinformation(){
- this.$refs.informationPopup.open();
+ async showinformation(){
+ // 修复: 第一次打开时, uni-popup 内部 还没完成首次渲染,
+ // 直接 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), 确保表单完整渲染
+ await this.$nextTick();
+ // 微信小程序下 scroll-view 在首次以 transform 隐藏后再显示时, 内部子节点布局可能未刷新
+ // 双 nextTick 兜底, 保证表单完整可见
+ await this.$nextTick();
+ if (this.$refs.informationPopup && typeof this.$refs.informationPopup.open === 'function') {
+ this.$refs.informationPopup.open();
+ }
},
//商品留言关闭
closeinformationPopup(){