| .. | .. |
|---|
| 13 | 13 | <scroll-view scroll-y="true" scroll-with-animation='true' |
|---|
| 14 | 14 | :style="{height: (height-280/750*windowWidth) + 'px'}" :scroll-into-view="'menu_item'+navActive"> |
|---|
| 15 | 15 | <view class='item acea-row row-center-wrapper menu_item' :id="'menu_item'+index" |
|---|
| 16 | | - :class='index==navActive?"on":""' v-for="(item,index) in categoryList" :key="item._key" |
|---|
| 16 | + :class='index==navActive?"on":""' v-for="(item,index) in categoryList" :key="index" |
|---|
| 17 | 17 | @click='tap(index,"b"+index)'> |
|---|
| 18 | 18 | <image v-if="item.extra" class="img" :src="item.extra" mode="aspectFill"></image> |
|---|
| 19 | 19 | <view class="text">{{item.name}}</view> |
|---|
| .. | .. |
|---|
| 25 | 25 | <scroll-view :scroll-into-view="'subMenu_item'+subActive" scroll-x="true" scroll-with-animation='true' |
|---|
| 26 | 26 | style="width: 100%;"> |
|---|
| 27 | 27 | <view class="subMenu_wrapper"> |
|---|
| 28 | | - <block v-for="(itemn,indexn) in categoryList[navActive].child" :key="itemn._key"><text |
|---|
| 28 | + <block v-for="(itemn,indexn) in categoryList[navActive].child" :key="indexn"><text |
|---|
| 29 | 29 | class="subMenu_item" :id="'subMenu_item'+indexn" :class='indexn==subActive?"on":""' |
|---|
| 30 | 30 | @click='tapSub(indexn)'>{{itemn.name}}</text></block> |
|---|
| 31 | 31 | </view> |
|---|
| .. | .. |
|---|
| 35 | 35 | :style="{top: (categoryList[navActive].show2Category==0?0:118) + 'rpx',height: (height-(categoryList[navActive].show2Category==0?0:(122/750*windowWidth))) + 'px'}"> |
|---|
| 36 | 36 | <scroll-view :scroll-top="scrollTop" class="goodlistScroll" scroll-y="true" style="height:100%;" |
|---|
| 37 | 37 | :scroll-into-view="scrollIntoView" @scroll="handleScroll" :throttle="false"> |
|---|
| 38 | | - <block v-for="(aitem,aindex) in categoryList" :key="aitem._key"> |
|---|
| 39 | | - <view class="list" v-if="aitem.child.length>0" v-for="(sitem,sindex) in aitem.child" :key="sitem._key" |
|---|
| 38 | + <block v-for="(aitem,aindex) in categoryList" :key="aindex"> |
|---|
| 39 | + <view class="list" v-if="aitem.child.length>0" v-for="sitem,sindex in aitem.child" :key="sindex" |
|---|
| 40 | 40 | :style="(aindex==categoryList.length-1&&sindex==aitem.child.length-1)?('min-height:'+(height-370/750*windowWidth)+'px'):''" |
|---|
| 41 | 41 | :id="'list'+aindex+'_'+sindex"> |
|---|
| 42 | 42 | <view class="list_title">{{sitem.name}}</view> |
|---|
| 43 | 43 | <view class="list_wrapper cf"> |
|---|
| 44 | 44 | <view class="list_column cfl"> |
|---|
| 45 | 45 | <block style="float:left" v-if="sitem.productArr.length>0" |
|---|
| 46 | | - v-for="(gitem,gindex) in sitem.productArr" :key="gitem._key"> |
|---|
| 46 | + v-for="(gitem,gindex) in sitem.productArr" :key="gindex"> |
|---|
| 47 | 47 | <GoodBlock v-if="gindex%2==0" :size="'small'" :pageName="'分类'" |
|---|
| 48 | 48 | :moduleName="'分类商品列表'" :careBeanRate="careBeanRate" :goodsInfo="gitem"> |
|---|
| 49 | 49 | </GoodBlock> |
|---|
| .. | .. |
|---|
| 51 | 51 | </view> |
|---|
| 52 | 52 | <view class="list_column cfr"> |
|---|
| 53 | 53 | <block style="float:left" v-if="sitem.productArr.length>0" |
|---|
| 54 | | - v-for="(gitem,gindex) in sitem.productArr" :key="gitem._key"> |
|---|
| 54 | + v-for="(gitem,gindex) in sitem.productArr" :key="gindex"> |
|---|
| 55 | 55 | <GoodBlock v-if="gindex%2==1" :size="'small'" :pageName="'分类'" |
|---|
| 56 | 56 | :moduleName="'分类商品列表'" :careBeanRate="careBeanRate" :goodsInfo="gitem"> |
|---|
| 57 | 57 | </GoodBlock> |
|---|
| .. | .. |
|---|
| 66 | 66 | <view class="list_wrapper cf"> |
|---|
| 67 | 67 | <view class="list_column cfl"> |
|---|
| 68 | 68 | <block v-if="aitem.productArr.length>0" v-for="(gitem,gindex) in aitem.productArr" |
|---|
| 69 | | - :key="gitem._key"> |
|---|
| 69 | + :key="gindex"> |
|---|
| 70 | 70 | <GoodBlock v-if="gindex%2==0" :pageName="'分类'" :moduleName="'分类商品列表'" |
|---|
| 71 | 71 | :careBeanRate="careBeanRate" :goodsInfo="gitem"></GoodBlock> |
|---|
| 72 | 72 | </block> |
|---|
| 73 | 73 | </view> |
|---|
| 74 | 74 | <view class="list_column cfr"> |
|---|
| 75 | 75 | <block v-if="aitem.productArr.length>0" v-for="(gitem,gindex) in aitem.productArr" |
|---|
| 76 | | - :key="gitem._key"> |
|---|
| 76 | + :key="gindex"> |
|---|
| 77 | 77 | <GoodBlock v-if="gindex%2==1" :pageName="'分类'" :moduleName="'分类商品列表'" |
|---|
| 78 | 78 | :careBeanRate="careBeanRate" :goodsInfo="gitem"></GoodBlock> |
|---|
| 79 | 79 | </block> |
|---|
| .. | .. |
|---|
| 144 | 144 | totalGetNum: 0, |
|---|
| 145 | 145 | // 首屏优化:当前分类的待完成请求数 |
|---|
| 146 | 146 | catePending: [], |
|---|
| 147 | + // 标记各分类是否已派发过请求,避免切换分类时重复请求 |
|---|
| 148 | + cateStarted: [], |
|---|
| 147 | 149 | firstScreenReady: true, |
|---|
| 150 | + // 后台加载标记:首屏渲染并关闭loading后,才开始后台加载其余分类 |
|---|
| 151 | + bgLoaded: false, |
|---|
| 148 | 152 | // 渲染优化:非首屏分类数据缓冲队列,批量渲染减少setData次数 |
|---|
| 149 | 153 | deferredLists: [], |
|---|
| 150 | 154 | deferTimer: null, |
|---|
| .. | .. |
|---|
| 162 | 166 | let that = this; |
|---|
| 163 | 167 | that.menuTop = uni.getMenuButtonBoundingClientRect().top; |
|---|
| 164 | 168 | that.menuHeight = uni.getMenuButtonBoundingClientRect().height; |
|---|
| 169 | + // 尽早计算页面布局高度,避免首屏数据就绪后因 height 为 0 而白屏 |
|---|
| 170 | + that.infoScroll(); |
|---|
| 165 | 171 | if (that.$store.getters.careBeanRate) { |
|---|
| 166 | 172 | that.$nextTick(() => { |
|---|
| 167 | 173 | that.$set(that, 'careBeanRate', that.$store.getters.careBeanRate) |
|---|
| .. | .. |
|---|
| 176 | 182 | }); |
|---|
| 177 | 183 | }, |
|---|
| 178 | 184 | mounted() {}, |
|---|
| 185 | + onReady() { |
|---|
| 186 | + // 页面渲染完成后复核一次布局尺寸(含滚动区域 scrollHeight) |
|---|
| 187 | + this.infoScroll(); |
|---|
| 188 | + }, |
|---|
| 179 | 189 | onHide() { |
|---|
| 180 | 190 | let that = this; |
|---|
| 181 | 191 | if (that.$store.getters.isSystemHide) { |
|---|
| .. | .. |
|---|
| 339 | 349 | that.isClicking = true; |
|---|
| 340 | 350 | that.navActive = index; |
|---|
| 341 | 351 | that.subActive = 0; |
|---|
| 352 | + // loading期间切到尚未发起请求的分类时,立即补发请求,避免loading卡住 |
|---|
| 353 | + if (!that.cateStarted[index]) { |
|---|
| 354 | + that.loadCateByIndex(index); |
|---|
| 355 | + } |
|---|
| 342 | 356 | // loading期间切换分类,若目标分类已加载完成则立即关闭loading |
|---|
| 343 | 357 | that.checkFirstScreen(index); |
|---|
| 344 | 358 | that.$nextTick(() => { |
|---|
| .. | .. |
|---|
| 352 | 366 | let that = this; |
|---|
| 353 | 367 | that.isClicking = true; |
|---|
| 354 | 368 | that.subActive = index; |
|---|
| 369 | + // 当前分类尚未发起请求时补发,避免loading卡住 |
|---|
| 370 | + if (!that.cateStarted[that.navActive]) { |
|---|
| 371 | + that.loadCateByIndex(that.navActive); |
|---|
| 372 | + } |
|---|
| 355 | 373 | that.checkFirstScreen(that.navActive); |
|---|
| 356 | 374 | that.$nextTick(() => { |
|---|
| 357 | 375 | that.scrollIntoView = 'list' + that.navActive + '_' + that.subActive; |
|---|
| .. | .. |
|---|
| 391 | 409 | let that = this; |
|---|
| 392 | 410 | that.firstScreenReady = false; |
|---|
| 393 | 411 | that.allRequestsDone = false; |
|---|
| 412 | + that.bgLoaded = false; |
|---|
| 394 | 413 | that.deferredLists = []; |
|---|
| 395 | 414 | if (that.deferTimer) { |
|---|
| 396 | 415 | clearTimeout(that.deferTimer); |
|---|
| 397 | 416 | that.deferTimer = null; |
|---|
| 398 | 417 | } |
|---|
| 418 | + // 暂不考虑传入cid的情况:一律从第一个分类开始加载首屏 |
|---|
| 419 | + that.navActive = 0; |
|---|
| 420 | + that.subActive = 0; |
|---|
| 399 | 421 | uni.showLoading(); |
|---|
| 400 | 422 | getCategoryList().then(res => { |
|---|
| 401 | | - that.categoryList = res.data; |
|---|
| 402 | | - // [P0-优化] 装配阶段为所有分类节点打 _key,供模板 :key 使用 |
|---|
| 403 | | - that.ensureCategoryKey(that.categoryList); |
|---|
| 423 | + that.categoryList = res.data || []; |
|---|
| 404 | 424 | // 重置计数,避免重复进入页面时累积 |
|---|
| 405 | 425 | that.totalGetNum = 0; |
|---|
| 406 | 426 | that.finishNum = 0; |
|---|
| 407 | 427 | that.catePending = []; |
|---|
| 428 | + that.cateStarted = []; |
|---|
| 408 | 429 | that.categoryList.forEach((item, index) => { |
|---|
| 409 | | - |
|---|
| 410 | 430 | if (item.child && item.child.length > 0) { |
|---|
| 431 | + item.child.sort((a, b) => { |
|---|
| 432 | + return a.sort - b.sort; |
|---|
| 433 | + }); |
|---|
| 411 | 434 | that.totalGetNum += item.child.length; |
|---|
| 412 | 435 | that.catePending[index] = item.child.length; |
|---|
| 413 | 436 | } else { |
|---|
| .. | .. |
|---|
| 415 | 438 | that.catePending[index] = 1; |
|---|
| 416 | 439 | } |
|---|
| 417 | 440 | }) |
|---|
| 418 | | - |
|---|
| 419 | | - // 先标记当前选中分类,再发起请求 |
|---|
| 420 | | - that.categoryList.forEach((item, index) => { |
|---|
| 421 | | - if (that.optionId && that.optionId == item.id) { |
|---|
| 422 | | - that.navActive = index; |
|---|
| 423 | | - that.subActive = 0; |
|---|
| 424 | | - } |
|---|
| 425 | | - if (item.child && item.child.length > 0) { |
|---|
| 426 | | - item.child.sort((a, b) => { |
|---|
| 427 | | - return a.sort - b.sort; |
|---|
| 428 | | - }); |
|---|
| 429 | | - item.child.forEach((item2, index2) => { |
|---|
| 430 | | - if (that.optionId && that.optionId == item2.id) { |
|---|
| 431 | | - that.navActive = index; |
|---|
| 432 | | - that.subActive = index2; |
|---|
| 433 | | - } |
|---|
| 434 | | - }) |
|---|
| 435 | | - } |
|---|
| 441 | + // 分类为空时直接关闭loading,避免一直停留在loading态 |
|---|
| 442 | + if (!that.categoryList.length) { |
|---|
| 443 | + that.firstScreenReady = true; |
|---|
| 444 | + that.isLoading = false; |
|---|
| 445 | + uni.hideLoading(); |
|---|
| 446 | + return; |
|---|
| 447 | + } |
|---|
| 448 | + // 首屏优化:先只请求第一个分类,保证首屏数据最快返回并直接渲染 |
|---|
| 449 | + that.loadCateByIndex(that.navActive); |
|---|
| 450 | + // 其余分类待首屏渲染、loading关闭后,由 loadRestCategory 后台错峰加载 |
|---|
| 451 | + }) |
|---|
| 452 | + }, |
|---|
| 453 | + // 按索引加载某个分类下的商品数据(同一分类只派发一次) |
|---|
| 454 | + loadCateByIndex(index) { |
|---|
| 455 | + let that = this; |
|---|
| 456 | + let item = that.categoryList[index]; |
|---|
| 457 | + if (!item) return; |
|---|
| 458 | + if (that.cateStarted[index]) return; |
|---|
| 459 | + that.cateStarted[index] = true; |
|---|
| 460 | + if (item.child && item.child.length > 0) { |
|---|
| 461 | + item.child.forEach((item2, index2) => { |
|---|
| 462 | + that.getAllProductList('isSub', index, index2); |
|---|
| 436 | 463 | }) |
|---|
| 437 | | - |
|---|
| 438 | | - const loadCate = (index) => { |
|---|
| 439 | | - let item = that.categoryList[index]; |
|---|
| 440 | | - if (item.child && item.child.length > 0) { |
|---|
| 441 | | - item.child.forEach((item2, index2) => { |
|---|
| 442 | | - that.getAllProductList('isSub', index, index2); |
|---|
| 443 | | - }) |
|---|
| 444 | | - } else { |
|---|
| 445 | | - that.getAllProductList(false, index); |
|---|
| 446 | | - } |
|---|
| 447 | | - }; |
|---|
| 448 | | - // [P0-优化] 首屏分类(当前navActive):立即并发发起,不走任何队列,最大限度缩短首屏 loading 时长 |
|---|
| 449 | | - loadCate(that.navActive); |
|---|
| 450 | | - // [P0-优化] 其余分类:构建任务列表,走下方并发限流池,避免一次性占满 wx.request queue 拖慢首屏 |
|---|
| 451 | | - let otherTasks = []; |
|---|
| 452 | | - that.categoryList.forEach((it, idx) => { |
|---|
| 453 | | - if (idx !== that.navActive) otherTasks.push({ index: idx }); |
|---|
| 454 | | - }); |
|---|
| 455 | | - that.runCategoryTasksWithLimit(otherTasks, 4); |
|---|
| 456 | | - |
|---|
| 457 | | - |
|---|
| 458 | | - }) |
|---|
| 459 | | - }, |
|---|
| 460 | | - // [P0-优化] 并发限流工具:除首屏分类外,其余分类的请求经由此处发起 |
|---|
| 461 | | - runCategoryTasksWithLimit(tasks, limit) { |
|---|
| 462 | | - let that = this; |
|---|
| 463 | | - if (!tasks || !tasks.length) return Promise.resolve(); |
|---|
| 464 | | - const queue = tasks.slice(); |
|---|
| 465 | | - const executing = []; |
|---|
| 466 | | - const launch = () => { |
|---|
| 467 | | - while (queue.length && executing.length < limit) { |
|---|
| 468 | | - const task = queue.shift(); |
|---|
| 469 | | - const item = that.categoryList[task.index]; |
|---|
| 470 | | - if (!item) continue; |
|---|
| 471 | | - let promise; |
|---|
| 472 | | - if (item.child && item.child.length > 0) { |
|---|
| 473 | | - const subPromises = []; |
|---|
| 474 | | - item.child.forEach((sub, subIdx) => { |
|---|
| 475 | | - subPromises.push(that.getAllProductList('isSub', task.index, subIdx)); |
|---|
| 476 | | - }); |
|---|
| 477 | | - promise = Promise.all(subPromises); |
|---|
| 478 | | - } else { |
|---|
| 479 | | - promise = that.getAllProductList(false, task.index); |
|---|
| 480 | | - } |
|---|
| 481 | | - // 用 .catch 包一层避免单个失败拖垮整池 |
|---|
| 482 | | - const tracked = promise.catch(() => {}); |
|---|
| 483 | | - tracked.finally(() => { |
|---|
| 484 | | - const idx = executing.indexOf(tracked); |
|---|
| 485 | | - if (idx >= 0) executing.splice(idx, 1); |
|---|
| 486 | | - }).catch(() => {}); |
|---|
| 487 | | - executing.push(tracked); |
|---|
| 488 | | - } |
|---|
| 489 | | - }; |
|---|
| 490 | | - const next = () => { |
|---|
| 491 | | - launch(); |
|---|
| 492 | | - if (executing.length === 0) return Promise.resolve(); |
|---|
| 493 | | - return Promise.race(executing).then(next, next); |
|---|
| 494 | | - }; |
|---|
| 495 | | - return next(); |
|---|
| 496 | | - }, |
|---|
| 497 | | - // [P0-优化] 为分类节点打 _key:非 H5 平台的 v-for :key 必须为简单变量, |
|---|
| 498 | | - // 故在数据装配阶段预先在 item 上加 _key,模板用 :key="item._key" 引用 |
|---|
| 499 | | - ensureCategoryKey: function(list) { |
|---|
| 500 | | - if (!Array.isArray(list)) return; |
|---|
| 501 | | - list.forEach((item, i) => { |
|---|
| 502 | | - if (!item) return; |
|---|
| 503 | | - if (item.id != null) item._key = 'c-' + item.id; |
|---|
| 504 | | - else if (!item._key) item._key = 'c0-' + i; |
|---|
| 505 | | - if (item.child && item.child.length) { |
|---|
| 506 | | - item.child.forEach((sub, j) => { |
|---|
| 507 | | - if (!sub) return; |
|---|
| 508 | | - if (sub.id != null) sub._key = 's-' + sub.id; |
|---|
| 509 | | - else if (!sub._key) sub._key = 's0-' + i + '-' + j; |
|---|
| 510 | | - }); |
|---|
| 511 | | - } |
|---|
| 512 | | - }); |
|---|
| 513 | | - }, |
|---|
| 514 | | - // [P0-优化] 为商品节点打 _key |
|---|
| 515 | | - ensureProductKey: function(list) { |
|---|
| 516 | | - if (!Array.isArray(list)) return; |
|---|
| 517 | | - list.forEach((g, k) => { |
|---|
| 518 | | - if (!g) return; |
|---|
| 519 | | - if (g.id != null && !g._key) g._key = 'p-' + g.id; |
|---|
| 520 | | - else if (!g._key) g._key = 'pk-' + k; |
|---|
| 521 | | - }); |
|---|
| 522 | | - }, |
|---|
| 523 | | - // 当前分类数据全部就绪后立即关闭全屏loading,不必等待所有分类 |
|---|
| 524 | | - checkFirstScreen(index1) { |
|---|
| 464 | + } else { |
|---|
| 465 | + that.getAllProductList(false, index); |
|---|
| 466 | + } |
|---|
| 467 | + }, |
|---|
| 468 | + // 首屏渲染并关闭loading后,后台错峰加载其余分类,避免与首屏抢请求/抢渲染 |
|---|
| 469 | + loadRestCategory() { |
|---|
| 470 | + let that = this; |
|---|
| 471 | + if (that.bgLoaded) return; |
|---|
| 472 | + that.bgLoaded = true; |
|---|
| 473 | + let delay = 0; |
|---|
| 474 | + that.categoryList.forEach((item, index) => { |
|---|
| 475 | + if (index == that.navActive) return; |
|---|
| 476 | + // 每个分类错开触发,逐步在后台铺开,避免一次性铺满请求队列 |
|---|
| 477 | + delay += 50; |
|---|
| 478 | + setTimeout(() => { |
|---|
| 479 | + that.loadCateByIndex(index); |
|---|
| 480 | + }, delay); |
|---|
| 481 | + }) |
|---|
| 482 | + }, |
|---|
| 483 | + // 当前分类数据全部就绪后立即关闭全屏loading,不必等待所有分类 |
|---|
| 484 | + checkFirstScreen(index1) { |
|---|
| 525 | 485 | let that = this; |
|---|
| 526 | 486 | if (that.firstScreenReady || index1 != that.navActive) return; |
|---|
| 527 | 487 | if ((that.catePending[index1] || 0) <= 0) { |
|---|
| .. | .. |
|---|
| 531 | 491 | that.$nextTick(() => { |
|---|
| 532 | 492 | setTimeout(() => { |
|---|
| 533 | 493 | uni.hideLoading(); |
|---|
| 494 | + // 首屏已渲染且loading已关闭,此时才开始后台加载其余分类 |
|---|
| 495 | + that.loadRestCategory(); |
|---|
| 534 | 496 | }, 50) |
|---|
| 535 | 497 | }) |
|---|
| 536 | 498 | } |
|---|
| .. | .. |
|---|
| 600 | 562 | updateDimensions() { |
|---|
| 601 | 563 | const query = uni.createSelectorQuery().in(this); |
|---|
| 602 | 564 | query.select('.goodlistScroll').boundingClientRect(data => { |
|---|
| 603 | | - this.scrollHeight = data.height; |
|---|
| 565 | + // 首屏渲染前该节点可能还不存在,避免报错 |
|---|
| 566 | + if (data) this.scrollHeight = data.height; |
|---|
| 604 | 567 | }).exec(); |
|---|
| 605 | 568 | }, |
|---|
| 606 | 569 | handleScroll(e) { |
|---|
| .. | .. |
|---|
| 631 | 594 | cid: isSub ? that.categoryList[index1].child[index2].id : that.categoryList[index1].id, |
|---|
| 632 | 595 | }).then(res => { |
|---|
| 633 | 596 | let list = res.data.list; |
|---|
| 634 | | - // [P0-优化] 装配阶段为商品节点打 _key,供模板 :key 使用 |
|---|
| 635 | | - that.ensureProductKey(list); |
|---|
| 636 | 597 | // 当前浏览分类即时渲染;其余分类进入缓冲队列批量渲染, |
|---|
| 637 | 598 | // 避免几十次$set各自触发一次全量列表渲染 |
|---|
| 638 | 599 | if (index1 == that.navActive && !that.firstScreenReady) { |
|---|