前端不规则卡片布局实现

关键代码


// html
  <page-table
          :api="ajaxApi"
          :defaultData="defaultData"
          ref="pageRef"
          class="table-body-contain"
          :pageSize="8"
          @passData="onPassDataHandle"
        >
          <template #="{ datas, loading }">
            <div class="img-text-box" ref="imgTextBoxRef" v-loading="loading">
              <template v-if="datas.length !== 0">
                <div
                  v-for="item in datas"
                  :key="item.id"
                  class="image-text-card"
                  :class="{ selected: selectedId === item.id }"
                  @click="setSelect(item)"
                >
                  <div class="image-text-card-content">
                    <div class="mask">
                      <Icon class="check-icon" icon="Check"></Icon>
                    </div>
                    <div class="title">{{ item.imgTextList[0].title }}</div>
                    <ImageTextPreview
                      :list="item.imgTextList"
                      size="mini"
                      :maxHeight="200"
                      :dynamicHeight="true"
                    />
                  </div>
                </div>
              </template>
              <div v-else style="width: 100%">
                <el-empty description="暂无数据" />
              </div>
            </div>
          </template>
        </page-table>

// 逻辑
const heightArray = ref<number[]>([])

const getWaterfall = () => {
  let columns = 4 //定义布局的列数为5
  let totalWidth = 0
  const items: NodeListOf<HTMLElement> =
    document.querySelectorAll('.image-text-card')
  for (let i = 0; i < items.length; i++) {
    //遍历整个子元素的DOM集合
    if (i < columns) {
      //小于columns的子元素作为第一行
      items[i].style.top = 20 + 'px'
      items[i].style.left = (items[0].clientWidth + 20) * i + 'px'
      totalWidth += items[0].clientWidth + 20
      heightArray.value.push(items[i].clientHeight) //遍历结束时,数组this.array保存的是第一行子元素的元素高度
    } else {
      //大于等于columns的子元素将作其他行
      let minHeight = Math.min(...heightArray.value) //  找到第一列的最小高度
      let index = heightArray.value.findIndex((item) => item === minHeight) // 找到最小高度的索引
      //设置当前子元素项的位置
      items[i].style.top =
        heightArray.value[index] + 20 + 20 * Math.floor(i / 4) + 'px'
      items[i].style.left = items[index].offsetLeft + 'px'
      //重新定义数组最小项的高度 进行累加
      heightArray.value[index] += items[i].clientHeight
    }
  }

  if (imgTextBoxRef.value) {
    imgTextBoxRef.value.style.height =
      Math.max(...heightArray.value) + 40 + 'px'
  }
}

完整代码

<template>
  <div class="picImgText-dialog">
    <dialog-com
      :visible="visible"
      :width="900"
      title="选择图文"
      @confrimMethod="confrimMethod"
      @changeVisible="$emit('changeVisible', $event)"
    >
      <div class="search-box">
        <el-input
          v-model="title"
          class="title-input"
          placeholder="标题"
          clearable
        >
          <template #append>
            <el-button :icon="Search" @click="onSearchHandle" />
          </template>
        </el-input>
      </div>
      <div class="content-box" ref="contentBoxRef">
        <page-table
          :api="ajaxApi"
          :defaultData="defaultData"
          ref="pageRef"
          class="table-body-contain"
          :pageSize="8"
          @passData="onPassDataHandle"
        >
          <template #="{ datas, loading }">
            <div class="img-text-box" ref="imgTextBoxRef" v-loading="loading">
              <template v-if="datas.length !== 0">
                <div
                  v-for="item in datas"
                  :key="item.id"
                  class="image-text-card"
                  :class="{ selected: selectedId === item.id }"
                  @click="setSelect(item)"
                >
                  <div class="image-text-card-content">
                    <div class="mask">
                      <Icon class="check-icon" icon="Check"></Icon>
                    </div>
                    <div class="title">{{ item.imgTextList[0].title }}</div>
                    <ImageTextPreview
                      :list="item.imgTextList"
                      size="mini"
                      :maxHeight="200"
                      :dynamicHeight="true"
                    />
                  </div>
                </div>
              </template>
              <div v-else style="width: 100%">
                <el-empty description="暂无数据" />
              </div>
            </div>
          </template>
        </page-table>
      </div>
    </dialog-com>
  </div>
</template>

<script setup="props, { emit }" lang="ts">
import { reactive, toRefs, onMounted, ref, nextTick } from 'vue'
import { Search, Plus, Check } from '@element-plus/icons-vue'
import ImageTextPreview from './common/imageTextPreview.vue'
import { getMessData, addResource } from '@/api/material'
import { getImageTextList } from '@/api/material/imageText'
import { genFileId, UploadRequestOptions } from 'element-plus'
import { tipMsg } from '@/utils/message'

interface PageRef extends HTMLElement {
  loadData(): void
}
interface ContentBoxRef extends HTMLElement {}
interface ImgTextBoxRef extends HTMLElement {}

interface Props {
  visible: Boolean
  withMediaId: Boolean
  isSingle: Boolean
}

const props = withDefaults(defineProps<Props>(), {
  visible: () => false,
  withMediaId: () => false,
  isSingle: () => false,
})

const emits = defineEmits(['changeVisible', 'confirm'])

const pageRef = ref<PageRef | null>(null)
const contentBoxRef = ref<ContentBoxRef | null>(null)
const imgTextBoxRef = ref<ImgTextBoxRef | null>(null)

const state = reactive({
  title: '',
  selectedId: 0,
  selectRecource: {},
  defaultData: [],
})

const confrimMethod = (e: string) => {
  if (e === 'confirm') {
    submit()
  }
}

const heightArray = ref<number[]>([])

const getWaterfall = () => {
  let columns = 4 //定义布局的列数为5
  let totalWidth = 0
  const items: NodeListOf<HTMLElement> =
    document.querySelectorAll('.image-text-card')
  for (let i = 0; i < items.length; i++) {
    //遍历整个子元素的DOM集合
    if (i < columns) {
      //小于columns的子元素作为第一行
      items[i].style.top = 20 + 'px'
      items[i].style.left = (items[0].clientWidth + 20) * i + 'px'
      totalWidth += items[0].clientWidth + 20
      heightArray.value.push(items[i].clientHeight) //遍历结束时,数组this.array保存的是第一行子元素的元素高度
    } else {
      //大于等于columns的子元素将作其他行
      let minHeight = Math.min(...heightArray.value) //  找到第一列的最小高度
      let index = heightArray.value.findIndex((item) => item === minHeight) // 找到最小高度的索引
      //设置当前子元素项的位置
      items[i].style.top =
        heightArray.value[index] + 20 + 20 * Math.floor(i / 4) + 'px'
      items[i].style.left = items[index].offsetLeft + 'px'
      //重新定义数组最小项的高度 进行累加
      heightArray.value[index] += items[i].clientHeight
    }
  }

  if (imgTextBoxRef.value) {
    imgTextBoxRef.value.style.height =
      Math.max(...heightArray.value) + 40 + 'px'
  }
}

onMounted(() => {
  nextTick(() => {
    if (contentBoxRef.value) {
      const { width } = contentBoxRef.value.getBoundingClientRect()
      if (imgTextBoxRef.value) {
        imgTextBoxRef.value.style.width = width - 40 + 'px'
      }
    }
    if (pageRef.value) {
      pageRef.value.loadData()
    }
  })
})

const onPassDataHandle = () => {
  nextTick(() => {
    heightArray.value = []
    getWaterfall()
  })
}

const submit = () => {
  if (!state.selectedId) {
    tipMsg('请选择图文素材', 'error')
    return
  }
  emits('confirm', {
    resourceId: state.selectedId,
    resource: state.selectRecource,
  })
}

const setSelect = (item: any) => {
  if (state.selectedId === item.id) {
    state.selectedId = 0
    state.selectRecource = {}
  } else {
    state.selectedId = item.id
    state.selectRecource = item
  }
}

const onSearchHandle = () => {
  if (pageRef.value) {
    pageRef.value.loadData()
  }
}

const ajaxApi = {
  getList(param: MaterialAPI.PageParams) {
    return getImageTextList({
      resourceTitle: state.title,
      subResourceType: props.isSingle ? 1 : undefined, //只查询单图文
      withMediaId: props.withMediaId ? 1 : undefined,
      ...param,
    })
  },
}

const { title, defaultData, selectedId } = toRefs(state)
</script>

<style scoped lang="scss">
.picImgText-dialog {
  .search-box {
    display: flex;
    justify-content: space-between;
    margin-bottom: 20px;
    .title-input {
      width: 250px;
    }
    .btns {
      display: flex;
      align-items: center;
      .tips {
        font-size: 14px;
        color: #9a9a9a;
        padding-right: 5px;
      }
    }
  }
  .content-box {
    display: flex;
    width: 100%;

    .left {
      width: 200px;
      ul li {
        list-style: none;
        padding: 10px 2px;
        cursor: pointer;
        &:hover {
          background-color: #eaf5fb;
        }
      }
    }
    .img-text-box {
      display: flex;
      flex-wrap: wrap;
      margin-left: 20px;
      position: relative;
      width: 100%;
      min-height: 200px;

      .image-text-card {
        cursor: pointer;
        position: absolute;

        .image-text-card-content {
          position: relative;
        }

        &.selected {
          .mask {
            display: flex;
          }
        }

        .mask {
          width: 100%;
          height: 100%;
          background-color: rgba(0, 0, 0, 0.4);
          position: absolute;
          top: 0;
          left: 0;
          z-index: 2;
          display: none;
          justify-content: center;
          align-items: center;

          .check-icon {
            width: 80px;
            height: 80px;
            color: #fff;
          }
        }
        .title {
          width: 190px;
          background-color: #f5f5f5;
          border: 1px solid #e4e4e6;
          border-bottom: none;
          padding: 8px;
          text-align: center;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          word-wrap: normal;
          font-size: 14px;
        }
      }
    }
  }
}
</style>

效果

不规则卡片布局

posted @ 2022-07-15 11:00  Michelyuan  阅读(428)  评论(0编辑  收藏  举报