蛇形布局

32 阅读2分钟

image.png

模板

     <ul
      class="flex-1 flex flex-wrap gap-16 mt-5 overflow-y-scroll overflow-x-hidden ul-scrollbar"
      id="snakeLayout"
    >
      <li
        v-for="(item, zhangIndex) in [2, 3, 2, 4, 3, 9]"
        class="group/li h-[244px] item relative rounded-xl flex-none flex flex-col bg-[#303a4ccc] border border-[#3e4b62]"
      >
        <!-- 标题 -->
        <div class="flex-none bg-[#5e6a80] p-2 px-5 rounded-lg flex items-center justify-between">
          <div class="text-lg line-clamp-1">第{{ zhangIndex + 1 }}章 语法分析器的构建</div>
          <div>7个资源</div>
        </div>
        <!-- 子项 -->
        <ul class="flex-1 overflow-hidden flex gap-16 justify-center p-5 relative z-10">
          <li
            v-for="(i, index) in item"
            :key="index"
            class="flex flex-col items-center odd:self-end even:self-start even:flex-col-reverse relative"
          >
            <div
              class="w-8 h-8 rounded-full bg-[#72819C] relative cursor-pointer"
              title="使用flex和Box布局中的"
            >
              <img
                v-if="index % 2 === 0 && index !== item - 1"
                src="@/assets/course_images/k-lj-1.png"
                class="w-24 max-w-none absolute bottom-2 left-1"
                alt=""
              />
              <img
                v-if="index % 2 === 1 && index !== item - 1"
                src="@/assets/course_images/k-lj-2.png"
                class="w-24 max-w-none absolute top-2 left-1"
                alt=""
              />
              <div class="w-8 h-8 rounded-full bg-[#72819C] absolute top-0 left-0"></div>
            </div>
            <div class="flex items-center gap-1 relative h-9">
              <div class="flex-1 w-36 gap-2 absolute left-1/2 -translate-x-1/2 flex items-center">
                <img
                  src="@/assets/course_images/k-dp-sp.svg"
                  v-if="index % 2 === 0 && index !== 8"
                  class="flex-none w-5"
                  alt=""
                />
                <img
                  src="@/assets/course_images/k-dp-task.svg"
                  v-if="index % 2 === 1 && index !== 8"
                  class="flex-none w-5"
                  alt=""
                />
                <span class="flex-1 line-clamp-1">使用flex和Box布局中的</span>
              </div>
            </div>
          </li>
        </ul>
        <!-- 箭头 -->

        <div class="absolute top-0 right-0 bottom-0 left-0 z-0">
          <!-- left -->
          <img
            src="@/assets/course_images/k-arrow.png"
            class="w-14 h-20 absolute rotate-180 -left-[3.56rem] top-1/2 -translate-y-1/2 hidden group-[.arrow-left]/li:block"
            alt=""
          />
          <!-- right -->
          <img
            src="@/assets/course_images/k-arrow.png"
            class="w-14 h-20 absolute -right-[3.56rem] top-1/2 -translate-y-1/2 hidden group-[.arrow-right]/li:block"
            alt=""
          />

          <!-- bottom left -->
          <img
            src="@/assets/course_images/k-arrow.png"
            class="w-14 h-20 absolute rotate-90 -bottom-[4.34rem] left-14 hidden group-[.arrow-bottom-left]/li:block"
            alt=""
          />
          <!-- bottom right -->
          <img
            src="@/assets/course_images/k-arrow.png"
            class="w-14 h-20 absolute rotate-90 -bottom-[4.34rem] right-14 hidden group-[.arrow-bottom-right]/li:block"
            alt=""
          />
        </div>
      </li>
    </ul>

js

methods:{
     applySnakeLayout() {
      const items = Array.from(document.querySelectorAll('#snakeLayout .item'))
      if (items.length === 0) return

      // 获取容器宽度和gap
      const container = document.getElementById('snakeLayout')
      const containerWidth = container.offsetWidth
      const gap = parseInt(getComputedStyle(container).gap) || 0

      // 重置所有项目的样式
      items.forEach((item) => {
        item.style.order = ''
        item.style.flex = ''
        item.style.width = ''

        // 移除之前添加的箭头类
        item.classList.remove('arrow-right', 'arrow-left', 'arrow-down-right', 'arrow-down-left')
      })

      // 按照自然顺序分组行
      let rows = []
      let currentRow = []
      let currentRowWidth = 0

      // 按照DOM顺序处理项目
      for (let i = 0; i < items.length; i++) {
        const item = items[i]
        const itemWidth = item.offsetWidth

        // 计算添加此项目后的总宽度(包括gap)
        const totalWidth = currentRow.length === 0 ? itemWidth : currentRowWidth + gap + itemWidth

        // 如果能放下就添加到当前行,否则开始新行
        if (totalWidth <= containerWidth || currentRow.length === 0) {
          currentRow.push(item)
          currentRowWidth = totalWidth
        } else {
          rows.push([...currentRow])
          currentRow = [item]
          currentRowWidth = itemWidth
        }
      }

      // 添加最后一行
      if (currentRow.length > 0) {
        rows.push(currentRow)
      }

      // 为每行设置样式实现蛇形布局和撑满效果
      let globalOrder = 1

      rows.forEach((row, rowIndex) => {
        // 计算当前行的总间隙
        const totalGap = (row.length - 1) * gap
        // 计算可用于项目的总宽度
        const availableWidth = containerWidth - totalGap

        // 计算当前行所有项目的自然宽度总和
        const naturalWidthSum = row.reduce((sum, item) => sum + item.offsetWidth, 0)

        if (rowIndex % 2 === 0) {
          // 偶数行:从左到右 1 3 5 7 9...
          row.forEach((item, colIndex) => {
            const ratio = item.offsetWidth / naturalWidthSum
            const flexWidth = availableWidth * ratio
            item.style.width = `${flexWidth}px`
            item.style.order = globalOrder++
            // 添加.arrow-down-right类

            if (rowIndex != rows.length - 1 || colIndex != row.length - 1) {
              if (colIndex === row.length - 1) {
                item.classList.add('arrow-bottom-right')
              } else {
                // 添加.arrow-right类
                item.classList.add('arrow-right')
              }
            }
          })
        } else {
          // 奇数行:从右到左 2 4 6 8 10...
          for (let i = row.length - 1; i >= 0; i--) {
            const item = row[i]
            const colIndex = row.length - 1 - i
            // 按比例分配宽度
            const ratio = item.offsetWidth / naturalWidthSum
            const flexWidth = availableWidth * ratio
            item.style.width = `${flexWidth}px`
            item.style.order = globalOrder++

            if (rowIndex != rows.length - 1 || colIndex != row.length - 1) {
              // 添加.arrow-left类
              if (colIndex > 0) {
                item.classList.add('arrow-left')
              } else {
                // 添加.arrow-down-left类
                item.classList.add('arrow-bottom-left')
              }
            }
          }
        }
      })
    }
}