瀑布流相关
个人建议(供参考)用flex布局+JS计算两列高度来实现。基本思路是flex-box容器下有左右两列容器(对应组件state中的list1和list2数组),从list1开始创建子项,创建后计算左右两列容器的高度来比较,高度小的列对应的list数组就会push一个子项,这样循环到列表项全部创建完为止。(因为商品流的图片区域高度是固定的,影响列表子项容器高度的只有标签,所以不用考虑图片加载完成后才能得到的高度,即不用考虑Image组件的onLoad事件,因此实现起来还是简单的)
注意点:Taro3的setState是异步的,给数组赋值后,获取列的DOM高度要在setState完成后。
目前纯CSS实现的瀑布流布局从multi-columns、flex-box到grid都有一定的局限性。产品要求商品流的是按行的方式排列的,高度不均的变化影响的是子项的水平位置。multi-columns是按列来排列子项,纯CSS的flex-box的瀑布流实现基本也是用列排列子项的思路。现在的grid布局实现瀑布流要基本确定每一个子项的行空间大小。grid有瀑布流布局的草案,可以完美实现瀑布流,但是还没有成为规范。
flex布局+JS计算两列高度来实现瀑布流
import Taro from '@tarojs/taro'
import React, { Component } from 'react'
import { View, Text, Image, ScrollView } from '@tarojs/components'
import { gotoPresaleGoodDetails } from '@/utils/common/presale/index'
import { jumpSpotGoodsDetail } from '@/utils/common/linkUrl/index'
import { imgCdn, storeSign } from '@/utils/conf'
import { addCartApi } from '@/api/pages/cart/index'
import { tip } from '@/src/utils/util'
import './index.scss'
/**
* @description 公共组件==> 商品列表(预售/NEW/HOT/特供)
*/
export default class GatherListComp extends Component {
static defaultProps = {
list: []
}
state = {
}
componentWillMount() { }
componentDidMount() { }
gotoGoodDetails(item) {
// 预售or现货
if (item.labelType == 1 || item.skuType == 2 || item.type == 2) {// 现货
jumpSpotGoodsDetail(item.skuId)
}
else if (item.labelType == 2 || item.skuType == 1 || item.type == 1) {// 预售
gotoPresaleGoodDetails(item.id || item.skuId)
}
}
getIconFlag(item) {
let flag = ''
if (item.labelType == 2) {
flag = 'icon-pre'
}
if (item.skuType == 1) {
flag = 'icon-new'
}
if (item.skuType == 2) {
flag = 'icon-hot'
}
if (item.skuType == 3) {
flag = 'icon-tg'
}
return flag
}
// 加入购物车
addShopCarNum(item) {
Taro.showLoading({ title: '添加中', mask: true })
addCartApi({ skuId: item.skuId, quantity: 1 }).then(res => {
Taro.hideLoading()
tip('成功加入购物车')
}).catch(res => {
Taro.hideLoading()
let errorMsg = res && (res.message || res.error || '')
tip(errorMsg || '加入购物车失败')
})
}
goodsItemRender (item, index) {
return (
<View className='gather-list__item' key={index}>
<View onClick={this.gotoGoodDetails.bind(this, item)} className={['gather-list__item-icon', this.getIconFlag(item)]}></View>
<Image onClick={this.gotoGoodDetails.bind(this, item)} className='gather-list__item-pic' src={item.cover} lazyLoad={true}></Image>
<View className='gather-list__item-wrap'>
<View>
<View className='gather-list__item-title ell' onClick={this.gotoGoodDetails.bind(this, item)}>{item.skuName}</View>
<View className='gather-list__promotion' onClick={this.gotoGoodDetails.bind(this, item)}>
{Array.isArray(item.tagList) && item.tagList.length ?
item.tagList.map((tag) => {
return (
<Text className='gather-list__promotion-item'>{tag}</Text>
)
}) : ''
}
</View>
<View className='gather-list__item-price'>
{/*促销价或原价*/}
<Text onClick={this.gotoGoodDetails.bind(this, item)}>
<Text className='gather-list__price-txt1'>¥</Text>
<Text className='gather-list__price-txt2'>{item.preSalePrice || item.sellPrice}</Text>
</Text>
{/*划线价*/}
<Text className='gather-list__through-price' onClick={this.gotoGoodDetails.bind(this, item)}>
<Text>¥</Text>
<Text>{item.preSalePrice || item.sellPrice}</Text>
</Text>
</View>
</View>
{/*加购按钮*/}
{
item.labelType != 2 ?
<View className='gather-list__add-icon' onClick={this.addShopCarNum.bind(this, item)}>
<Text className='gather-list__add-horizontal'></Text>
<Text className='gather-list__add-vertical'></Text>
</View>
: ''
}
</View>
</View>
)
}
render() {
const { list } = this.props
let leftData = [] // 左边的数据
let rightData = [] // 右边的数据
let leftTagLine = 0
let rightTagLine = 0
if (Array.isArray(list) && list.length) {
list.filter((row, index) =>{
// todo 暂时写死标签
let tagList = []
if (index % 5 == 0) {
tagList = ['直减20', '满100减20']
} else if (index % 4 == 0) {
tagList = ['直减20', '满100有赠品', '满88减20', '两件5折', '三件4折']
} else if (index % 3 == 0) {
tagList = ['直减20', '满199有赠品', '满99减15', '满199减40']
} else if (index % 2 == 0) {
tagList = ['满100有赠品', '买一送一']
}
row.tagList = tagList
let tagLine = 0 // tag标签占的行数
if (Array.isArray(row.tagList) && row.tagList.length) {
let newTagList = []
// 计算每个标签的宽度
row.tagList.filter((tag) => {
let tagWidth = 0
if (tag) {
var regChinese = /^[\u4e00-\u9fa5]+$/;
for (let i = 0; i < tag.length; i++) {
let oneChar = tag.charAt(i)
if (regChinese.test(oneChar)){ // 字体大小10px, 中文字体宽度10px
tagWidth += 10
} else { // 其他字符宽度5.5px
tagWidth += 5.5
}
}
}
tagWidth += 4 + 4 + 4 // 标签的左右内边距 + 右外边距
newTagList.push({ tag, tagWidth })
})
tagLine = calcTagLine(JSON.parse(JSON.stringify(newTagList)), tagLine)
}
// 如果左侧的高度小于右侧的高度,往左侧添加数据
if ((leftTagLine * 23 + 247 * leftData.length) <= (rightTagLine * 23 + 247 * rightData.length)) {
leftTagLine += tagLine
leftData.push(row)
} else {
rightTagLine += tagLine
rightData.push(row)
}
// console.log(`leftTagLine:${leftTagLine}\t\t\t\t\trightTagLine:${rightTagLine}`)
// console.log(`leftData:${leftData.length}\t\t\t\t\trightData:${rightData.length}`)
})
}
function calcTagLine (newArr, tagLine) {
let oneLineWidth = 160 // 每行的宽度
let isBr = false // 是否换行,true为已换行
// 换行判断,满足条件换行,行数+1
function dealIsBr(tagArr = [], startIndex = 1) {
let widthAdd = 0
for (let i = 0; tagArr.length >= startIndex && i < startIndex; i++) {
widthAdd += tagArr[i].tagWidth
}
if ((tagArr.length >= startIndex && (widthAdd) >= oneLineWidth)) {
tagLine += 1
tagArr.splice(0, startIndex-1)
isBr = true
} else if ((tagArr.length === startIndex && (widthAdd) <= oneLineWidth)) {
tagLine += 1
tagArr.splice(0, startIndex)
isBr = true
}
if (!isBr && tagArr.length && startIndex >= 0 && startIndex <= tagArr.length && widthAdd) {
dealIsBr(tagArr, startIndex + 1)
}
}
dealIsBr(newArr)
if (newArr.length > 0) {
tagLine = calcTagLine(newArr, tagLine)
}
return tagLine
}
return (
<View className='gather-list'>
<View>
{leftData.map((item, index) => {
return (
this.goodsItemRender(item, index)
)
})}
</View>
<View>
{rightData.map((item, index) => {
return (
this.goodsItemRender(item, index)
)
})}
</View>
</View>
)
}
}
$img-cdn: "https://xxx.xxx.com";
.gather-list {
display: flex;
flex-direction: row;
.gather-list__item {
width: 352px;
margin-right: 14px;
margin-bottom: 16px;
box-sizing: border-box;
position: relative;
&:nth-of-type(2n) {
margin-right: 0;
}
.gather-list__item-icon {
position: absolute;
right: 0;
top: 0;
width: 98px;
height: 50px;
&.icon-pre {
background: url($img-cdn + "/toptoy/index/index-icon-pre-v8.png") 0 0 no-repeat;
background-size: 100% 100%;
}
&.icon-hot {
background: url($img-cdn + "/toptoy/index/index-icon-hot-v8.png") 0 0 no-repeat;
background-size: 100% 100%;
}
&.icon-tg {
background: url($img-cdn + "/toptoy/index/index-icon-tg-v8.png") 0 0 no-repeat;
background-size: 100% 100%;
}
&.icon-new {
background: url($img-cdn + "/toptoy/index/index-icon-new-v8.png") 0 0 no-repeat;
background-size: 100% 100%;
}
}
.gather-list__item-pic {
width: 352px;
height: 352px;
}
.gather-list__item-wrap {
background: #ffffff;
box-sizing: border-box;
padding: 10px 16px;
position: relative;
.gather-list__item-title {
font-size: 26px;
font-weight: 400;
color: #333333;
line-height: 42px;
}
.gather-list__promotion {
.gather-list__promotion-item {
display: inline-block;
padding: 6px 8px;
line-height: normal;
background: #FBED00;
border-radius: 6px;
margin: 8px 8px 0 0;
font-size: 20px;
font-weight: 400;
color: #DA7100;
text-align: justify;
text-justify: newspaper;
word-break: break-all;
word-wrap: break-word;
}
}
.gather-list__item-price {
margin-top: 12px;
margin-bottom: 16px;
font-size: 24px;
line-height: 48px;
color: #333333;
font-weight: bold;
.gather-list__price-txt2 {
font-size: 32px;
}
}
.gather-list__through-price {
text-decoration: line-through;
font-size: 18px;
font-weight: 400;
color: #999999;
margin: 0 8px;
}
}
.gather-list__add-icon {
position: absolute;
right: 16px;
bottom: 27px;
width: 46px;
height: 46px;
background: #000000;
border-radius: 50%;
.gather-list__add-horizontal {
display: inline-block;
position: absolute;
top: 22px;
left: 13px;
width: 22px;
height: 3px;
background: #FBED00;
border-radius: 2px;
}
.gather-list__add-vertical {
display: inline-block;
position: absolute;
top: 13px;
left: 22px;
width: 3px;
height: 22px;
background: #FBED00;
border-radius: 2px;
}
}
}
}
实现效果如图