Span 占位符布局修复笔记
思路参考:Flex 弹性布局从入门到实践 | arry老师的博客-艾编程
一、问题描述(本项目是横屏app)
在使用 justify-content: space-between 的 flexbox 布局中,如果最后一行只有 1-2 个元素,会导致布局不整齐。需要添加占位符来填充最后一行,使布局保持整齐。如图所示
想要的效果:
二、解决方案
2.1 核心思路
-
计算一行能放多少个 item
- 测量实际 item 的宽度
- 计算可用宽度(屏幕宽度 - paddingLeft - 右边距)
- 根据 item 宽度和 gap 计算一行能放多少个
-
添加 span 占位符
- span 数量 = 一行能放的个数 - 2
- span 宽度 = item 宽度(不包含 gap,因为 flexbox 会自动处理)
-
关键点
- span 宽度只需要等于 item 宽度
- gap 会由 flexbox 的
gap属性自动添加在元素之间 - 如果 span 宽度 = item 宽度 + gap,会导致总宽度过大
三、实现代码
3.1 变量定义
// span 占位符数量
const spacerCount = ref(0)
// span 的宽度(不包含 gap)
const spacerWidth = ref(0)
3.2 计算函数
// 计算一行能放多少个 item 和需要的 span 数量
const calculateSpacerCount = () => {
nextTick(() => {
try {
// 确保有数据
if (coralList.value.length === 0) {
console.log('没有数据,跳过计算')
return
}
const query = uni.createSelectorQuery()
// 同时查询列表容器和第一个 item
query.select('.coral-list').boundingClientRect()
query.select('.coral-item').boundingClientRect()
query.exec((res) => {
const listRect = res[0]
const itemRect = res[1]
if (!listRect) {
console.log('未找到 .coral-list')
return
}
if (!itemRect) {
console.log('未找到 .coral-item')
// 使用估算值
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.windowWidth
const rightMarginVw = 3.2
const rightMarginPx = (rightMarginVw / 100) * screenWidth
const availableWidth = screenWidth - containerMarginLeft.value - rightMarginPx
const gapVw = 2.67
const gapPx = (gapVw / 100) * screenWidth
const estimatedItemWidth = (45 / 100) * screenWidth
const itemsPerRow = Math.floor((availableWidth + gapPx) / (estimatedItemWidth + gapPx))
spacerCount.value = Math.max(0, itemsPerRow - 2)
spacerWidth.value = estimatedItemWidth
return
}
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.windowWidth
// 计算可用宽度:屏幕宽度 - paddingLeft - 右边距 3.2vw
const rightMarginVw = 3.2
const rightMarginPx = (rightMarginVw / 100) * screenWidth
const availableWidth = screenWidth - containerMarginLeft.value - rightMarginPx
// gap 是 2.67vw
const gapVw = 2.67
const gapPx = (gapVw / 100) * screenWidth
console.log('计算参数:', {
screenWidth,
containerMarginLeft: containerMarginLeft.value,
rightMarginPx,
availableWidth,
gapPx,
itemRect: { width: itemRect.width, height: itemRect.height }
})
// 如果 itemRect 存在且宽度大于 0,使用实际测量的宽度
if (itemRect && itemRect.width > 0) {
// 计算一行能放多少个
// 公式推导:
// 可用宽度 = item宽度 * n + gap * (n-1)
// availableWidth = itemRect.width * n + gapPx * (n - 1)
// availableWidth = n * (itemRect.width + gapPx) - gapPx
// n = (availableWidth + gapPx) / (itemRect.width + gapPx)
const itemsPerRow = Math.floor((availableWidth + gapPx) / (itemRect.width + gapPx))
// span 数量 = 一行能放的个数 - 2
spacerCount.value = Math.max(0, itemsPerRow - 2)
// 关键:span 的宽度 = item 宽度(不包含 gap)
// gap 会由 flexbox 的 gap 属性自动处理
spacerWidth.value = itemRect.width
console.log('计算结果:', {
itemsPerRow,
spacerCount: spacerCount.value,
spacerWidth: spacerWidth.value,
itemWidth: itemRect.width,
gapPx
})
}
})
} catch (error) {
console.error('计算占位符数量失败:', error)
spacerCount.value = 0
}
})
}
3.3 模板使用
<view class="coral-list">
<view class="coral-item" v-for="(item, index) in coralList" :key="index" @click="goToDetail(item)">
<!-- item 内容 -->
</view>
<!-- 添加占位符 span -->
<span
class="coral-item-spacer"
v-for="i in spacerCount"
:key="'spacer-' + i"
:style="{ width: spacerWidth + 'px' }"
></span>
</view>
3.4 样式定义
.coral-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 3.33vh 2.67vw; // gap 会自动在元素之间添加间距
.coral-item {
display: flex;
gap: 1.47vh;
overflow: hidden;
// item 没有固定宽度,由内容决定
}
.coral-item-spacer {
// span 的宽度通过动态 style 设置(不包含 gap)
display: block;
flex-shrink: 0;
height: 0; // 不占用垂直空间
visibility: hidden; // 隐藏但保留布局空间
// 宽度通过内联样式动态设置: width: spacerWidth + 'px'
}
}
四、解析
4.1 为什么 span 宽度 = item 宽度(不包含 gap)?
原因:
- flexbox 的
gap属性会自动在所有元素之间添加间距 - 如果 span 宽度 = item 宽度 + gap,会导致:
- span 本身占据 item 宽度 + gap 的空间
- flexbox 还会在 span 前后再添加 gap
- 总宽度 = item 宽度 + gap + gap = 过大
正确做法:
- span 宽度 = item 宽度
- flexbox 的 gap 会自动在 span 和相邻元素之间添加间距
- 总宽度 = item 宽度 + gap(由 flexbox 自动添加)
4.2 计算每行个数
可用宽度 = item宽度 × n + gap × (n-1)
展开:
availableWidth = itemWidth × n + gapPx × (n - 1)
availableWidth = n × itemWidth + n × gapPx - gapPx
availableWidth = n × (itemWidth + gapPx) - gapPx
移项:
availableWidth + gapPx = n × (itemWidth + gapPx)
因此:
n = (availableWidth + gapPx) / (itemWidth + gapPx)
4.3 span 数量为什么是 itemsPerRow - 2?
原因:
- 如果一行能放 3 个 item
- 最后一行有 1 个 item:需要 2 个 span 填满(1 + 2 = 3)
- 最后一行有 2 个 item:需要 1 个 span 填满(2 + 1 = 3)
- 最后一行有 3 个 item:不需要 span(3 = 3)
公式:
- span 数量 = itemsPerRow - 最后一行 item 数量
- 最坏情况是最后一行只有 1 个 item
- 所以 span 数量 = itemsPerRow - 1
- 但为了保险,使用 itemsPerRow - 2,确保即使有 2 个 item 也能填满
六、完整代码
// 1. 定义变量
const spacerCount = ref(0)
const spacerWidth = ref(0)
// 2. 计算函数
const calculateSpacerCount = () => {
nextTick(() => {
if (coralList.value.length === 0) return
const query = uni.createSelectorQuery()
query.select('.coral-list').boundingClientRect()
query.select('.coral-item').boundingClientRect()
query.exec((res) => {
const itemRect = res[1]
if (!itemRect || itemRect.width <= 0) return
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.windowWidth
const rightMarginPx = (3.2 / 100) * screenWidth
const availableWidth = screenWidth - containerMarginLeft.value - rightMarginPx
const gapPx = (2.67 / 100) * screenWidth
const itemsPerRow = Math.floor((availableWidth + gapPx) / (itemRect.width + gapPx))
spacerCount.value = Math.max(0, itemsPerRow - 2)
spacerWidth.value = itemRect.width // 不包含 gap
})
})
}
// 3. 在数据加载后调用
getCoralList().then(() => {
nextTick(() => {
setTimeout(() => {
calculateSpacerCount()
}, 200)
})
})
<template>
<view class="coral-list">
<view class="coral-item" v-for="(item, index) in coralList" :key="index">
<!-- 内容 -->
</view>
<span
class="coral-item-spacer"
v-for="i in spacerCount"
:key="'spacer-' + i"
:style="{ width: spacerWidth + 'px' }"
></span>
</view>
</template>
.coral-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 3.33vh 2.67vw; // gap 自动处理间距
.coral-item-spacer {
display: block;
flex-shrink: 0;
height: 0;
visibility: hidden;
}
}
七、总结
核心要点
- span 宽度 = item 宽度(不包含 gap)
- span 数量 = itemsPerRow - 2
- gap 由 flexbox 自动处理,不需要手动计算
- 计算时机:数据加载完成后,确保 DOM 已渲染
易错点
-
span 宽度 = item 宽度 + gap(会导致总宽度过大)
-
span 宽度 = item 宽度(gap 由 flexbox 自动处理)
-
使用
uni.nextTick(H5 环境不支持) -
使用 Vue 的
nextTick -
在数据加载前计算(找不到元素)
-
在数据加载完成后计算