响应式布局中解决使用space-between导致的最后一行布局问题(uniapp)

40 阅读4分钟

Span 占位符布局修复笔记

思路参考:Flex 弹性布局从入门到实践 | arry老师的博客-艾编程

一、问题描述(本项目是横屏app)

在使用 justify-content: space-between 的 flexbox 布局中,如果最后一行只有 1-2 个元素,会导致布局不整齐。需要添加占位符来填充最后一行,使布局保持整齐。如图所示

image.png 想要的效果: image.png

二、解决方案

2.1 核心思路

  1. 计算一行能放多少个 item

    • 测量实际 item 的宽度
    • 计算可用宽度(屏幕宽度 - paddingLeft - 右边距)
    • 根据 item 宽度和 gap 计算一行能放多少个
  2. 添加 span 占位符

    • span 数量 = 一行能放的个数 - 2
    • span 宽度 = item 宽度(不包含 gap,因为 flexbox 会自动处理)
  3. 关键点

    • 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;
  }
}

七、总结

核心要点

  1. span 宽度 = item 宽度(不包含 gap)
  2. span 数量 = itemsPerRow - 2
  3. gap 由 flexbox 自动处理,不需要手动计算
  4. 计算时机:数据加载完成后,确保 DOM 已渲染

易错点

  1. span 宽度 = item 宽度 + gap(会导致总宽度过大)

  2. span 宽度 = item 宽度(gap 由 flexbox 自动处理)

  3. 使用 uni.nextTick(H5 环境不支持)

  4. 使用 Vue 的 nextTick

  5. 在数据加载前计算(找不到元素)

  6. 在数据加载完成后计算