以下给出了几种实现展开和收起效果的方案,在日常开发的工作中这种效果应该也是很常见的,这里就不去过多的阐述了。最后的大致效果如下所示:
方案一 利用 MutationObserver API 实现,但是该API在一些浏览器上是存在兼容性问题的,比如360浏览器、QQ浏览器等,具体的可以查看其官网的解析:developer.mozilla.org/zh-CN/docs/… 比如在QQ浏览器上的效果如下:
代码如下:
<template>
<div class="mt30">
<input type="checkbox" class="exp" id="exp" v-show="false" />
<div
class="text"
ref="textContent"
:style="{ '--bottom': bottom, '-webkit-line-clamp': lineNum }"
>
<label
class="btn"
ref="optBtn"
for="exp"
v-show="showExport && showExpBtn"
></label>
<slot></slot>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
const props = defineProps({
lineNum: {
type: Number,
default: 2,
},
showExport: {
type: Boolean,
default: true,
},
// showExpBtn: {
// type: Boolean,
// default: true,
// },
});
const optBtn = ref(null);
const textContent = ref(null);
const bottom = ref();
const showExpBtn = ref(false);
onMounted(() => {
getBtnHeight();
const mutation = new MutationObserver(handleShowExp);
const config = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
};
mutation.observe(textContent.value, config);
});
//处理计算是否展示展开和收起按钮
const handleShowExp = () => {
if (textContent.value.scrollHeight > textContent.value.clientHeight) {
showExpBtn.value = true;
getBtnHeight();
}
};
const getBtnHeight = () => {
//计算展开/收起按钮的高度,动态改变
const offsetHeight = optBtn.value.offsetHeight - 2;
bottom.value = `-${offsetHeight}px`;
// handelTooltip();
};
</script>
<style scoped lang="scss">
.text {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
transition: 0.3s max-height;
}
.exp:checked + .text {
max-height: 2000px;
/*超出最大行高度就可以了*/
}
.exp:checked + .text {
-webkit-line-clamp: 999 !important;
/*设置一个足够大的行数就可以了*/
max-height: none;
}
.exp:checked + .text .btn::after {
content: "收起";
}
.exp:checked + .text .btn::before {
visibility: hidden;
/*在展开状态下隐藏省略号*/
}
.btn::after {
content: "展开";
}
.text::before {
content: "";
float: right;
width: 0;
height: 100%;
margin-bottom: var(--bottom);
}
.btn {
margin-right: 12px;
float: right;
clear: both;
cursor: pointer;
color: #377ef9;
}
</style>
使用方式:
<Textellipsis :lineNum="2" :showExport="true" :showExpBtn="true">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac justo
vitae nulla maximus scelerisque. Nulla facilisi. Vestibulum id viverra
risus. Sed id metus vel elit lacinia tincidunt vel eu leo. Curabitur
elementum lacus id dui vehicula eleifend.Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Sed ac justo vitae nulla maximus
scelerisque. Nulla facilisi. Vestibulum id viverra risus. Sed id metus
vel elit lacinia tincidunt vel eu leo. Curabitur elementum lacus id
dui vehicula eleifend.Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Sed ac justo vitae nulla maximus scelerisque. Nulla
facilisi. Vestibulum id viverra risus. Sed id metus vel elit lacinia
tincidunt vel eu leo. Curabitur elementum lacus id dui vehicula
eleifend.
</Textellipsis>
方案二 这里其实就是利用vue3的一些常规手段去实现了,比如computed,watch等知识 代码如下:
<template>
<div ref="textOverflow" class="text-overflow" :style="boxStyle">
<div>
<span ref="overEllipsis" class="color-3c fs24">
<span v-html="realText"></span>
</span>
</div>
<div class="flex-box" ref="slotRef" v-if="showSlotNode">
<div class="slot-box">
<slot :click-toggle="toggle" :expanded="expanded"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, ref } from "vue";
//vue3实现多行文本展开收起组件
const props = defineProps({
text: {
type: String,
default: "",
},
maxLines: {
type: Number,
default: 4,
// slot 图标占据一行,实际为3行
},
width: {
type: Number,
default: 0,
},
});
let offset = ref(props.text.length);
let expanded = ref(false);
let slotBoxWidth = ref(0);
let textBoxWidth = ref(props.width);
let showSlotNode = ref(false);
const boxStyle = computed(() => {
if (props.width) {
return {
width: props.width + "px",
};
}
});
const realText = computed(() => {
// 是否被截取
const isCutOut = offset.value !== props.text.length;
let realText = props.text;
if (isCutOut && !expanded.value) {
realText = props.text.slice(0, offset.value) + "...";
}
return realText;
});
const calculateOffset = (from, to) => {
nextTick(() => {
if (Math.abs(from - to) <= 1) return;
if (isOverflow()) {
to = offset.value;
} else {
from = offset.value;
}
offset.value = Math.floor((from + to) / 2);
calculateOffset(from, to);
});
};
const isOverflow = () => {
const { len, lastWidth } = getLines();
if (len < props.maxLines) {
return false;
}
if (props.maxLines) {
// 超出部分 行数 > 最大行数 或则 已经是最大行数但最后一行宽度 + 后面内容超出正常宽度
const lastLineOver = !!(
len === props.maxLines &&
lastWidth + slotBoxWidth.value > textBoxWidth.value
);
if (len > props.maxLines || lastLineOver) {
return true;
}
}
return false;
};
const getLines = () => {
const clientRects = overEllipsis.value.getClientRects();
return {
len: clientRects.length,
lastWidth: clientRects[clientRects.length - 1].width,
};
};
const toggle = () => {
expanded.value = !expanded.value;
};
let slotRef = ref(null);
let textOverflow = ref(null);
let overEllipsis = ref(null);
onMounted(() => {
const { len } = getLines();
if (len > props.maxLines) {
showSlotNode.value = true;
nextTick(() => {
slotBoxWidth.value = slotRef.value.clientWidth;
textBoxWidth.value = textOverflow.value.clientWidth;
calculateOffset(0, props.text.length);
});
}
});
</script>
<style scoped lang="scss">
.slot-box {
display: inline-block;
}
</style>
使用方式:
<Textellipsis :text="desc_text" :width="1200" :maxLines="4">
<template #default="{ clickToggle, expanded }">
<div
@click="clickToggle"
style="
padding-top: 10px;
color: #4088ed;
font-size: 16px;
font-weight: 500;
cursor: pointer;
"
>
{{ expanded ? "收起" : "展开" }}
</div>
</template>
</Textellipsis>
该方案来自:juejin.cn/post/718064… 但是该方案存在一个很大的问题就是,假设我后端返回的格式里包含标签元素,比如以下形式的:
<p class="MsoNormal">
脱贫人口小额信贷是金融帮扶的重要政策,是解决农村脱贫群众发展产业融资难、融资贵的有效途径,帮助脱贫户(含监测对象)发展到户产业稳定脱贫。
</p>
那么上面的方案就实现不了了。
方案三 以下方案也是直接用vue3的相关知识实现的,该方案就可以实现后台如果返回的数据包含元素标签也可以正常渲染和展示,代码如下:
<template>
<section class="room_content">
<!-- 内容区域(这里的id是为了区分多个该组件共存问题,否则会冲突) -->
<div
:id="`id_${id}`"
:class="state.textOver && !state.foldBtn ? 'showEllipsis' : ''"
>
<slot></slot>
</div>
<!-- 展开折叠按钮 -->
<span v-if="state.textOver" class="btnWrap" @click="uclick()">
<div>{{ state.foldBtn ? "折叠" : "展开" }}</div>
</span>
</section>
</template>
<script setup>
import { reactive, nextTick, onBeforeMount } from "vue";
const props = defineProps({
// 区分id(必填,否则报错)
id: { type: String, required: true },
});
const state = reactive({
textOver: false, //是否超过了2行
foldBtn: false, //当前是否展开或收起
});
// 处理操作
onBeforeMount(async () => {
nextTick(() => {
// 获取内容元素
const domRef = document.getElementById(`id_${props.id}`);
if (domRef) {
// 计算并改变是否展开和折叠
const height = window.getComputedStyle(domRef).height.replace("px", "");
if (+height > 40) {
//40 -- 2行的高度
state.textOver = true;
} else {
state.textOver = false;
}
}
});
});
// 点击展开折叠按钮
const uclick = () => {
// 直接取反
state.foldBtn = !state.foldBtn;
};
</script>
<style scoped>
/* 根节点 */
.room_content {
position: relative;
}
/* 文本溢出 */
.showEllipsis {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 展开/收起按钮位置及样式 */
.btnWrap {
position: absolute;
cursor: pointer;
color: #4b9ae9;
font-size: 16px;
font-weight: 500;
right: 0;
top: 20px;
z-index: 9999;
right: 40px;
}
</style>
使用
<Textellipsis id="2">
<div v-html="desc_text"></div>
</Textellipsis>
更多使用方式:
<template>
<div>
<!-- 文字形式 -->
<Textellipsis id="1">
<div class="text">
这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式这是文字的形式
</div>
</Textellipsis>
<!-- 容器形式 -->
<Textellipsis id="2">
<div v-for="(item, index) in 20" class="div">
容器{{ index }}
</div>
</Textellipsis>
<!-- 标签形式 -->
<Textellipsis id="3">
<span v-for="(item, index) in 40" class="tag">
标签{{ index }}
</span>
</Textellipsis>
</div>
</template>
<script setup>
// 引入组件,注意路径!
import Textellipsis from '@/components/Textellipsis.vue'
</script>
<style scoped>
/* 文字形式 */
.text {
font-size: 15px;
letter-spacing: 1px;
color: #666;
}
/* 容器形式 */
.div {
width: 100px;
height: 30px;
background: pink;
display: inline-block;
margin-right: 20px;
margin-bottom: 20px;
}
/* 标签形式 */
.tag {
display: inline-block;
margin-right: 20px;
margin-bottom: 20px;
}
</style>
常见问题:
- 如果您的内容是异步(比如接口请求过来的),就必须先用v-if隐藏本组件,当拿到数据的时候再渲染,否则无法计算高度。示例:
<Textellipsis id="2" v-if="desc_text">数据</Textellipsis>
最终效果: