赞
踩
vue2.0瀑布流+虚拟列表效果预览
目录
瀑布流布局是现代浏览器常见布局之一,是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。
瀑布流布局通常用于电商、视频、图片网站等,例如抖音、花瓣、小红书等。其优点本文不做介绍。
虚拟列表是一种优化长列表性能的手段。
它能够做到节省内存、提升页面流畅性、提升用户体验等,通俗来讲就是只在你能看见的地方渲染元素,你看不见的地方部分渲染或者不渲染。
实现瀑布流布局+虚拟列表,首先我的这个案例有一个大前提,即拿到的图片数据均为已知宽高!
1. 纯css还是js?
纯css实现瀑布流有点麻烦,不管是用column-count也好grid也好flex也好反正都行,但是它无法实现虚拟列表啊,所以毙掉这种方式,另外每一个元素都有自己的位置,那就需要绝对定位,因此肯定是选择js+绝对定位的方式。
还有,用原生js实现不会受到框架的限制,方便进行拓展。
2. translate还是left top?
translate不会引起重排而left top会,还能开启硬件加速,性能肯定强于left top,translate赢麻了。
3.什么时候才渲染真实dom?
当不存在虚拟列表时,dom元素排列如下图所示:
上刻线与下刻线, 是决定元素是否被渲染的重要参照,根据元素与参照的位置,我们得出以下结论:
使用虚拟列表后,上述所列情况中的①⑤不会进行渲染,其余情况均为渲染。
4. 需要对虚拟列表设置startIdx和endIdx?
需要,生成位置表(下文均有介绍)后,比起直接循环整个位置表,当位置表中记录了1000条甚至更多条记录时,startIdx和endIdx的存在明确了循环区间,极大的缩短循环次数,减少页面留白时间,提升性能。
5. 上下滚动时,如何进行添加、删除dom?
不管怎么滚动,都要做两件事情。
第一件事:
根据不同的滚动方向,添加dom元素
第二件事:
循环位置表,从startIdx至endIdx(如图6-1中的5与16),将对应索引元素的位置与上述第6点中提到的情况①⑤进行比较, 此时会出现:
循环结束后更新startIdx、endIdx、已渲染表,下次发生滚动事件时继续重复这套逻辑。
解释下为何不直接循环已渲染表进行元素的删除?如果是这样,那就会出现一个bug:如图8-2所示,虚线框为上次视口位置,实线框为当前视口位置,如果按照直接循环已渲染表的方式,那么就只有删除dom这一种情况,于是图中索引6的dom应被删除,结束后更新startIdx、endIdx、已渲染表,那么如果此刻向上滚动,回到上次视口位置,startIdx将从5开始0结束进行寻找,索引为6的dom明明满足,却没有执行添加dom,造成了该位置缺失dom,从而形成了bug。
流程图:
页面vue文件代码如下:
- <template>
- <div class="container">
- <div class="water-fall-container">
- <div class="box">
- <div class="loading">加载中...</div>
- </div>
- </div>
- <div class="to-top" @click="onclick">↑</div>
- </div>
-
- </template>
- <script>
- import WHList from './data'
- import './debounce'
- import'./throttle'
- export default {
- name:'WaterfallVirtual',
- data(){
- return{
- waterfallContainerDom:'',
- containerDom:'',
- loadingDom:'',
- canvas:'',
- getTextLineHeightCtx:'',
- list:[],
- page:1,
- pageSize:50,
- hasNextPage:true,
- gap:16,
- columnWidth:0,
- containerTop:0,
- domDataList:[],
- positionList:[],
- renderMap:{},
- startIdx:0,
- endIdx:0,
- screenOffset:'',// 偏移量
- isLoadNextPage:false,// 是否加载下一页数据
- testList:[
- '《蜡笔小新》是一部于1992年出品的日本家庭搞笑动画片,该片主要由本乡满、原惠一、武藤裕治导演,日本朝日电视台于1992年4月13日播映了第一集。至今仍在播出。',
- '看过蜡笔小新的人,都知道,他有一个很逗的老爸——野原广志。 这位胡须浓密、面条脸的野原广志先生是一名普通的上班族,在车上享受着和周围女子相互挤攘的感觉(偶尔旁边是大叔也很囧)。',
- '脚臭的广志,小气的美伢,淘气的小新……',
- ],
- imgList:[
- "https://img2.baidu.com/it/u=3600821550,221281285&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500",
- "https://img0.baidu.com/it/u=2506471502,1373494428&fm=253&fmt=auto&app=120&f=JPEG?w=530&h=500",
- "https://img2.baidu.com/it/u=824566914,3863846826&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=500"
- ],
- resizeCallback:null,
- lastOffsetWidth:'',
- lastScrollNumY:0, // 上次滚动距离Y
- lastScrollNumX :0,// 上次滚动距离X
- scrollDirection:1,// 上次滚动方向 向下 为 1,向上为 -1
- }
- },
- methods:{
- getList(){// 获取数据
- return new Promise(resolve => {
- const start = (this.page - 1) * this.pageSize
- const nextList = WHList.slice(start, start + this.pageSize)
- this.hasNextPage = !!nextList.length
- this.list = this.page === 1 ? nextList : this.list.concat(nextList)
- setTimeout(() => {
- resolve(nextList)
- }, this.page === 1 ? 0 : 2000) // 模拟发送请求
- })
- },
- computeDomData(list, startRenderIdx = 0){// 计算数据形成 排序表
- const tempDomDataList = []
- for (let i = 0; i < list.length; i++) {
- const param = {
- idx: startRenderIdx + i,
- img:this.imgList[Math.trunc(Math.random() * 3)],
- columnIdx: 0,
- width: this.columnWidth,
- height: list[i].h * this.columnWidth / list[i].w,
- left: 0,
- top: 0,
- text: this.testList[Math.trunc(Math.random() * 3)],
- lineHeight: 74,// 根据css设置的值计算得到
- }
- // 排序,第一项必定是长度最短的一列
- this.positionList.sort((a, b) => a.columnHeight - b.columnHeight)
- param.columnIdx = this.positionList[0].columnIdx
- param.left = (param.columnIdx - 1) * (this.gap + this.columnWidth)
-
- param.top = this.positionList[0].columnHeight
-
- const canvas = document.createElement('canvas')
- this.getTextLineHeightCtx = canvas.getContext('2d')
- this.getTextLineHeightCtx.font = '16px sans-serif'
- // css 样式表设置了 纵坐标的12px内边距,要加上
- param.lineHeight = this.getTextLineHeightCtx.measureText(param.text).width + 24 > this.columnWidth ? 98 : 78
-
- param.height += param.lineHeight
- this.positionList[0].columnHeight += param.height + this.gap
- tempDomDataList.push(param)
- }
- this.domDataList = this.domDataList.concat(tempDomDataList)
-
- // 设置容器高度
- this.positionList.sort((a, b) => a.columnHeight - b.columnHeight)
- this.containerDom.style.height = this.positionList[this.positionList.length - 1].columnHeight + 32 + 'px'
-
- },
- renderDomByDomDataList(startRenderIdx = 0){// 根据元素列表进行渲染
- if (!this.domDataList.length) return
- const tempRenderMap = {}
- let topIdx = startRenderIdx
- let bottomIdx = startRenderIdx
-
- // 处于这两条线之间的元素将被渲染进容器
- for (let i = startRenderIdx; i < this.domDataList.length; i++) {
- const { idx } = this.domDataList[i]
- const { overTopLine, underBottomLine } = this.checkIsRender(this.domDataList[i])
- const dom = this.containerDom.querySelector(`#item_${idx}`)
- if (overTopLine || underBottomLine) {
- dom?.remove()
- continue
- }
- topIdx = topIdx < idx ? topIdx : idx
- bottomIdx = bottomIdx < idx ? idx : bottomIdx
-
- if (dom) {
- tempRenderMap[idx] = this.createDom(dom, this.domDataList[i])
- } else {
- tempRenderMap[idx] = this.createDom(document.createElement('div'), this.domDataList[i])
- this.containerDom.append(tempRenderMap[idx])
- }
- }
- const keys = Object.keys(Object.assign(this.renderMap, tempRenderMap))
- this.startIdx = +keys[0]
- this.endIdx = +keys[keys.length - 1]
- },
- checkIsRender(params){// 计算元素是否符合渲染条件
- const { top, height } = params
- const y = top + height + this.containerTop
- // 1个视口的数据再快速滚动滚动条时大概率会有加载项,不妨扩大到上下各0.5个视口,共2个视口内的数据,这样就比较丝滑了,这里也是自由发挥
- const topLine = this.waterfallContainerDom.scrollTop - this.screenOffset
- const bottomLine = this.waterfallContainerDom.scrollTop + this.waterfallContainerDom.offsetHeight + this.screenOffset
- // 是否在上线之上
- const overTopLine = topLine > y
- // 是否在下线之下
- const underBottomLine = top > bottomLine
- return{
- overTopLine,
- underBottomLine,
- }
- },
- createDom(dom, param){// 创建瀑布流每一项 dom元素
- dom.classList.add('waterfall-item')
- dom.style.width = param.width + 'px'
- dom.style.height = param.height + 'px'
- dom.style.transform = `translate(${param.left}px, ${param.top}px)`
- dom.id = `item_${param.idx}`
- // <div class="main">${param.idx}</div>
- // <div class="main">${param.idx}</div>
- dom.innerHTML = `
- <image class="main" src="${param.img}" alt=""/>
- <div class="footer" style="height: ${param.lineHeight}px">
- <div class="text">${param.idx}--${param.text}</div>
- <div class="info">@脆脆土豆条 -《蜡笔小新》</div>
- </div>`
- return dom
- },
- getColumnNum(boxWidth){// 根据容器宽度获取显示列数(自由发挥)
- if (boxWidth >= 1600) return 5
- else if (boxWidth >= 1200) return 4
- else if (boxWidth >= 768 && boxWidth < 1200) return 3
- else return 2
- },
- computeColumnWidth(){// 计算瀑布流每一列列宽
- // 首先计算应呈现的列数
- const columnNum = this.getColumnNum(window.innerWidth)
- const allGapLength = this.gap * (columnNum - 1)
- this.columnWidth = (this.containerDom.offsetWidth - allGapLength) / columnNum
- },
- initPositionList(){// 重置瀑布流每一列数据
- this.positionList = []
- // 首先计算应呈现的列数
- for (let i = 0; i < this.getColumnNum(window.innerWidth); i++) {
- this.positionList.push({
- columnIdx: i + 1,
- columnHeight: 0
- })
- }
- },
- updateDomPosition(direction = 1){// 当滚动条滚动时,更新容器内的 每一项 元素是 插入 还是 删除
- const tempRenderMap = {}
- console.log(this,'updateDomPosition',this.endIdx)
- for (let i = this.startIdx; i <= this.endIdx; i++) {// 检查已渲染列表中的元素,不符合条件删除元素,反之插入元素
-
- if(!this.domDataList[i]) return
- const { overTopLine, underBottomLine } = this.checkIsRender(this.domDataList[i])
- if (overTopLine || underBottomLine) {
- this.renderMap[i]?.remove()
- } else if (this.renderMap[i]) {
- tempRenderMap[i] = this.renderMap[i]
- } else {
- tempRenderMap[i] = this.createDom(document.createElement('div'), this.domDataList[i])
- this.containerDom.append(tempRenderMap[i])
- }
- }
- // 向上
- if (direction < 0) {
-
- for (let i = this.startIdx - 1; i >= 0; i--) {
-
- const { overTopLine } = this.checkIsRender(this.domDataList[i])
- if (overTopLine) break
- tempRenderMap[i] = this.createDom(document.createElement('div'), this.domDataList[i])
- this.containerDom.append(tempRenderMap[i])
- }
- } else { // 向下
-
- for(let i = this.endIdx + 1; i < this.domDataList.length; i++) {
-
-
- const { underBottomLine } = this.checkIsRender(this.domDataList[i])
- // 只要找到Bottom在下线之下的立即停止
- if (underBottomLine) break
- tempRenderMap[i] = this.createDom(document.createElement('div'), this.domDataList[i])
- this.containerDom.append(tempRenderMap[i])
- }
- }
- this.renderMap = tempRenderMap
- const keys = Object.keys(this.renderMap)
- this.startIdx = +keys[0]
- this.endIdx = +keys[keys.length - 1]
- },
- resizeFn(){
- this.computeColumnWidth()
- // 如果宽度发生变化时,若列宽是一致的不用处理
- if (this.lastOffsetWidth !== window.innerWidth && this.columnWidth === this.domDataList[0]?.width) return
- this.lastOffsetWidth = window.innerWidth
- this.initPositionList()
- this.domDataList = []
- this.renderMap = {}
- this.computeDomData(this.list, 0)
- this.renderDomByDomDataList(0)
- },
- resize:window.debounce(function(){// 窗口变化事件
- console.log('resize')
- if (this.isLoadNextPage) {// 加载数据时发生了视口变化,保存回调
- this.resizeCallback = this.resizeFn()
- return
- }
- this.resizeFn()
- }, 150),
- handleScroll:window.throttle(async function(){// 窗口滚动事件
- this.waterfallContainerDom.scrollTop >= window.innerHeight ? this.gotoTopDom.classList.add('active') : this.gotoTopDom.classList.remove('active')
- this.scrollDirection = this.waterfallContainerDom.scrollTop - this.lastScrollNumY >= 0 ? 1 : -1
- this.lastScrollNumY = this.waterfallContainerDom.scrollTop
-
- this.updateDomPosition(this.scrollDirection)
-
- if (this.isLoadNextPage || !this.hasNextPage) return false
- if (this.waterfallContainerDom.scrollTop + this.waterfallContainerDom.offsetHeight >= this.waterfallContainerDom.scrollHeight * 0.85) {
- this.isLoadNextPage = true
- this.loadingDom.classList.add('active')
- this.page += 1
- const list = await this.getList()
- this.isLoadNextPage = false
- this.loadingDom.classList.remove('active')
- // 加载数据期间发生了视口变化时,执行一次回调
- if (this.resizeCallback) {
- this.resizeCallback()
- this.resizeCallback = null
- } else {
- // 节点信息排列完毕后进行渲染
- const startIdx = (this.page - 1) * this.pageSize
- this.computeDomData(list, startIdx)
- this.renderDomByDomDataList(startIdx)
- }
- }
- }, 150),
- onclick(){// 渠道顶部
- this.waterfallContainerDom.scrollTo({
- left: 0,
- top: 0,
- behavior: "smooth"
- })
- },
- async getData(){
- this.computeDomData(await this.getList(), 0)
- this.renderDomByDomDataList(0)// 节点信息排列完毕后进行渲染
- }
- },
- mounted() {
- this.waterfallContainerDom = document.querySelector('.water-fall-container')
- this.screenOffset =this.waterfallContainerDom.offsetHeight / 2
- this.containerDom = document.querySelector('.box')
- this.loadingDom = document.querySelector('.loading')
- this.gotoTopDom = document.querySelector('.to-top')
- this.lastOffsetWidth = window.innerWidth
- this.waterfallContainerDom.addEventListener('scroll', ()=>{// 添加滚动事件监听器
- console.log('滚动事件触发');
- this.handleScroll()
- });
- window.addEventListener('resize', ()=>{// 添加滚动事件监听器
- console.log('视窗大小变化');
- this.resize()
- });
- this.computeColumnWidth()
- this.initPositionList()
- this.getData()
- },
- created(){
- this.$bus.emit('title', '虚拟列表+瀑布流');
- },
- }
- </script>
- <style lang="less">
- #app{
- width: 100%;
- height: 100vh;
- display: flex;
- }
- html{
- overflow: hidden;
- }
- .container{
- height: 100%;
- flex-grow: 1;
- flex-shrink: 0;
- padding-top: 0px;
- }
- .to-top{
- position: fixed;
- right: 40px;
- bottom: 40px;
- cursor: pointer;
- transform: scale(0);
- transition: transform .15s;
- width: 60px;
- height: 60px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- background-color: #f8f8f8;
- color: tomato;
- font-size: 32px;
- }
- .to-top.active{
- transform: scale(1);
- }
-
- .loading{
- height: 32px;
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 20px;
- opacity: 0;
- transition: all .15s;
- }
-
- .loading.active{
- opacity: 1;
- }
-
- .header{
- height: 80px;
- background-color: #aaa;
- }
-
- .water-fall-container::-webkit-scrollbar{
- width: 8px;
- background-color: #eee;
- }
- .water-fall-container::-webkit-scrollbar-thumb{
- background-color: #bbb;
- border-radius: 4px;
- }
-
- .water-fall-container::-webkit-scrollbar-thumb:hover{
- background-color: #aaa;
- }
-
- .water-fall-container{
- padding: 20px;
- height: calc(100% - 130px);
- overflow-y: scroll;
- overflow-x: hidden;
- }
-
- .box{
- position: relative;
- width: 100%;
- }
-
- .waterfall-item{
- position: absolute;
- transition: all .12s;
- font-family: sans-serif;
- display: flex;
- flex-direction: column;
- }
-
- .main{
- flex-grow: 1;
- flex-shrink: 0;
- background-color: pink;
- border-top-left-radius: 8px;
- border-top-right-radius: 8px;
- object-fit: contain;
- }
- .footer{
- box-sizing: border-box;
- padding: 12px;
- background-color: darksalmon;
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
- }
- .info{
- font-size: 14px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .text{
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- font-size: 16px;
- line-height: 24px;
- margin-bottom: 10px;
- letter-spacing: 0;
- }
- </style>
debounce文件内容如下
- var FUNC_ERROR_TEXT = 'Expected a function';
-
- var NAN = 0 / 0;
-
- var symbolTag = '[object Symbol]';
-
- var reTrim = /^\s+|\s+$/g;
-
- var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
-
- var reIsBinary = /^0b[01]+$/i;
-
- var reIsOctal = /^0o[0-7]+$/i;
-
- var freeParseInt = parseInt;
-
- var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
-
- var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
-
- var root = freeGlobal || freeSelf || Function('return this')();
-
- var objectProto = Object.prototype;
-
-
- var objectToString = objectProto.toString;
-
- var nativeMax = Math.max,
- nativeMin = Math.min;
-
- var now = function() {
- return root.Date.now();
- };
-
- function debounce(func, wait, options) {
- var lastArgs,
- lastThis,
- maxWait,
- result,
- timerId,
- lastCallTime,
- lastInvokeTime = 0,
- leading = false,
- maxing = false,
- trailing = true;
-
- if (typeof func != 'function') {
- throw new TypeError(FUNC_ERROR_TEXT);
- }
- wait = toNumber(wait) || 0;
- if (isObject(options)) {
- leading = !!options.leading;
- maxing = 'maxWait' in options;
- maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
- trailing = 'trailing' in options ? !!options.trailing : trailing;
- }
-
- function invokeFunc(time) {
- var args = lastArgs,
- thisArg = lastThis;
-
- lastArgs = lastThis = undefined;
- lastInvokeTime = time;
- result = func.apply(thisArg, args);
- return result;
- }
-
- function leadingEdge(time) {
- // Reset any `maxWait` timer.
- lastInvokeTime = time;
- // Start the timer for the trailing edge.
- timerId = setTimeout(timerExpired, wait);
- // Invoke the leading edge.
- return leading ? invokeFunc(time) : result;
- }
-
- function remainingWait(time) {
- var timeSinceLastCall = time - lastCallTime,
- timeSinceLastInvoke = time - lastInvokeTime,
- result = wait - timeSinceLastCall;
-
- return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
- }
-
- function shouldInvoke(time) {
- var timeSinceLastCall = time - lastCallTime,
- timeSinceLastInvoke = time - lastInvokeTime;
-
- // Either this is the first call, activity has stopped and we're at the
- // trailing edge, the system time has gone backwards and we're treating
- // it as the trailing edge, or we've hit the `maxWait` limit.
- return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
- (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
- }
- function timerExpired() {
- var time = now();
- if (shouldInvoke(time)) {
- return trailingEdge(time);
- }
- // Restart the timer.
- timerId = setTimeout(timerExpired, remainingWait(time));
- }
- function trailingEdge(time) {
- timerId = undefined;
- // Only invoke if we have `lastArgs` which means `func` has been
- // debounced at least once.
- if (trailing && lastArgs) {
- return invokeFunc(time);
- }
- lastArgs = lastThis = undefined;
- return result;
- }
- function cancel() {
- if (timerId !== undefined) {
- clearTimeout(timerId);
- }
- lastInvokeTime = 0;
- lastArgs = lastCallTime = lastThis = timerId = undefined;
- }
- function flush() {
- return timerId === undefined ? result : trailingEdge(now());
- }
- function debounced() {
- var time = now(),
- isInvoking = shouldInvoke(time);
- lastArgs = arguments;
- lastThis = this;
- lastCallTime = time;
- if (isInvoking) {
- if (timerId === undefined) {
- return leadingEdge(lastCallTime);
- }
- if (maxing) {
- // Handle invocations in a tight loop.
- timerId = setTimeout(timerExpired, wait);
- return invokeFunc(lastCallTime);
- }
- }
- if (timerId === undefined) {
- timerId = setTimeout(timerExpired, wait);
- }
- return result;
- }
- debounced.cancel = cancel;
- debounced.flush = flush;
- return debounced;
- }
- function isObject(value) {
- var type = typeof value;
- return !!value && (type == 'object' || type == 'function');
- }
- function isObjectLike(value) {
- return !!value && typeof value == 'object';
- }
- function isSymbol(value) {
- return typeof value == 'symbol' ||
- (isObjectLike(value) && objectToString.call(value) == symbolTag);
- }
- function toNumber(value) {
- if (typeof value == 'number') {
- return value;
- }
- if (isSymbol(value)) {
- return NAN;
- }
- if (isObject(value)) {
- var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
- value = isObject(other) ? (other + '') : other;
- }
- if (typeof value != 'string') {
- return value === 0 ? value : +value;
- }
- value = value.replace(reTrim, '');
- var isBinary = reIsBinary.test(value);
- return (isBinary || reIsOctal.test(value))
- ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
- : (reIsBadHex.test(value) ? NAN : +value);
- }
- window.debounce = debounce;
thorttle文件代码如下
-
- var FUNC_ERROR_TEXT = 'Expected a function';
-
- function throttle(func, wait, options) {
- var leading = true,
- trailing = true;
-
- if (typeof func != 'function') {
- throw new TypeError(FUNC_ERROR_TEXT);
- }
-
- if (isObject(options)) {
- leading = 'leading' in options ? !!options.leading : leading;
- trailing = 'trailing' in options ? !!options.trailing : trailing;
- }
-
- return window.debounce(func, wait, {
- 'leading': leading,
- 'maxWait': wait,
- 'trailing': trailing
- });
- }
-
- function isObject(value) {
- return typeof value === 'object' && value!== null;
- }
-
- window.throttle = throttle;
data文件代码如下(这里我的是随机生成的,宽高可自行修改)
- const WHList1 = [
- {
- "w": 600,
- "h": 600
- },
- {
- "w": 600,
- "h": 1067
- },
- {
- "w": 600,
- "h": 600
- },
- {
- "w": 600,
- "h": 1067
- },
- {
- "w": 600,
- "h": 800
- },
- {
- "w": 600,
- "h": 1067
- },
- {
- "w": 600,
- "h": 800
- },
- {
- "w": 600,
- "h": 600
- },
- {
- "w": 600,
- "h": 700
- },
- {
- "w": 600,
- "h": 600
- },
- {
- "w": 600,
- "h": 1067
- },
- {
- "w": 600,
- "h": 700
- },
- {
- "w": 600,
- "h": 700
- },
- ],
- let WHList = WHList1.concat(WHList2)
- export default WHList
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。