vue3、 ant-design-vue
需求: 实现一个 tag 标签,标签内容随宽度,动态展示,标签内容超出就正常显示,多出的tag,用 +n 表示,hover时,tooltip显示剩余tag。
1、能否纯css能否实现
使用flex + flex-wrap: wrap + overflow:hidden;
如图
显然不满足,找到 ResizeObserver 可实现宽度监听
思路:通过ResizeObserver接口获取到父元素变化及子元素的宽度,结合父元素宽度及子元素累计宽度,动态计算宽度。超出的子元素 设置 隐藏。
想到 ant-design-vue中的 多选下拉框属于类似功能。
找到源码文件 ant-design-vue\components\vc-resize-observer\index.tsx 也是使用了ResizeObserver Api
onresize函数就能返回元素宽高,直接使用该组件。
新建一个vue文件,存放父元素及子元素
套上ResizeObserver组件
ResizeObserver 在组件渲染时会触发一次,所以将子元素的宽度保存起来
当父元素宽度变化时,调用自定义的判断超出
这里已经可以实现响应式设置了。
但在,新增子标签的情况下,不会刷新,因为parent组件宽度没变化,onResizeOut,不会被触发。所以需要监听 tagList变化,同时设置父元素 v-if 触发强制更新。
到这里,就可以实现tag响应式展示。
全部代码
使用
//引用
import overflowTag from './overflow-tag.vue';
const list = ref([{label:'11111'},{label:'22222'}]);
<overflowTag :tag-list="List" />
overflow-tag.vue
<script setup lang="ts"> import { onBeforeMount, reactive, ref, computed, watch, nextTick } from 'vue'; import ResizeObserver from 'ant-design-vue/es/vc-resize-observer'; import Item from './item.vue'; interface Size { width: number; height: number; offsetWidth: number; offsetHeight: number; } const props = defineProps({ tagList: { type: Array, default: () => [], }, }); const orderList = ref<any>([]); onBeforeMount(() => { setOrderList(); }); const setOrderList = () => { orderList.value = props.tagList.map((item: any) => ({ ...item, size: { height: 0, offsetWidth: 0 }, disabled: false, })); }; const more = ref({ count: 0, disabled: true, order: 0, size: { offsetWidth: 0, } as Size, text: '', }); const style = computed(() => { if (more.value.disabled) { return { opacity: 0, height: '0px', 'overflow-y': 'hidden', 'pointer-events': 'none', position: 'absolute', }; } return { opacity: 1, height: '24px', }; }); const content = ref({ size: { width: 0, } as Size, }); const resizeObserverDisabled = computed(() => { return !props.tagList.length; }); const onResize = (size: Size, index: number) => { orderList.value[index].size = size; }; const onResizeMore = (size: Size) => { more.value.size = size; }; const onResizeOut = (size: Size) => { content.value.size = size; const contentWidth = size.offsetWidth; let maxWidth = 0; const len = orderList.value.length; more.value.count = 0; more.value.disabled = true; more.value.order = 0; more.value.text = ''; for (let i = 0; i < len; i++) { const item = orderList.value[i]; let nowWidth = item.size.offsetWidth + 8; maxWidth += nowWidth; if (maxWidth < contentWidth) { item.disabled = false; } else { item.disabled = true; if (!more.value.count) { const moreLen = maxWidth - nowWidth + more.value.size.offsetWidth; if (moreLen > contentWidth - 8) { more.value.count = len - i + 1; orderList.value[i - 1].disabled = true; more.value.order = i - 1; } else { more.value.count = len - i; more.value.order = i; } more.value.text = props.tagList .map((item: any) => item.label) .splice(more.value.order, more.value.count) .join(','); more.value.disabled = false; } } } }; watch( () => props.tagList, (value) => { orderList.value = []; nextTick(() => { orderList.value = value.map((item: any) => ({ ...item, size: { height: 0, offsetWidth: 0 }, disabled: false, })); }); }, ); </script> <template> <ResizeObserver :disabled="resizeObserverDisabled" @resize="onResizeOut"> <div v-if="orderList.length" class="overflow-tag"> <a-tooltip :title="more.text"> <ResizeObserver :disabled="resizeObserverDisabled" @resize="onResizeMore" > <div class="overflow-tag-item overflow-tag-item-more" :style="{ ...style, order: more.order, }" > +{{ more.count }} </div> </ResizeObserver> </a-tooltip> <template v-for="(item, index) in orderList" :key="item.label"> <ResizeObserver :disabled="resizeObserverDisabled" @resize="(size) => onResize(size, index)" > <Item :item="item" :order="index" /> </ResizeObserver> </template> </div> </ResizeObserver> </template> <style scoped lang="less"> .overflow-tag { width: 100%; display: flex; flex-wrap: wrap; overflow: hidden; position: relative; .overflow-tag-item { background: #f5f5f5; border: 1px solid #f0f0f0; color: #111; border-radius: 2px; width: auto; padding: 2px 6px; margin-right: 8px; flex: none; align-self: center; max-width: 100%; &:last-of-type { margin-right: 0px; } } .overflow-tag-item-more { margin-right: 0; } } </style>
item.vue
<script setup lang="ts"> import { computed, onBeforeMount, reactive, ref } from 'vue'; const props = defineProps({ item: { type: Object, default: () => ({ disabled: false, label: '', }), }, order: { type: Number, default: 0, }, }); const style = computed(() => { if (props.item.disabled) { return { opacity: 0, height: '0px', 'overflow-y': 'hidden', 'pointer-events': 'none', position: 'absolute', }; } return { opacity: 1, height: '24px', }; }); </script> <template> <div class="overflow-tag-item" :style="{ order: order, ...style, }" > {{ item.label }} </div> </template> <style scoped lang="less"></style>