2026-04-02 18:37:45 +08:00

493 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="zh-readMore">
<view class="zh-readMore-wrapper" :style="[textStyle]">
<view
class="zh-text"
:class="textClass"
:style="[textStyleObject, { '--unfold_packup_line': `${unfold_packup_line}px` }, { '--duration': `${duration}s` }]"
>
<view class="zh-open" :style="[unfoldTextStyle]" v-if="!showall && is_open" @click.stop="open">
<text class="zh-ellipsis">...</text>
<text class="zh-open-text">{{ unfoldText }}</text>
</view>
<text class="" :style="[lineBreakStyles]">
<text v-if="title" :style="[titleStyle]">{{ title }}</text>
{{ text }}
</text>
<text class="zh-fewer" v-if="is_open && showall" @click.stop="open">
<text class="zh-fewer-text" :style="[packupTextStyle]">{{ packupText }}</text>
</text>
</view>
</view>
<view class="zh-overflowcont" id="zh-overflowcont" :style="[textStyle, lineStyle]">
<text class="zh-overflowcont-text" :style="[lineBreakStyle]">
<text v-if="title" :style="[titleStyle]">{{ title }}</text>
{{ text }}
</text>
</view>
<view class="zh-overflowcont" id="zh-overflowcontline" :style="[textStyle]">
<text class="" :style="[lineBreakStyle]">{{ text }}</text>
</view>
<view class="zh-overflowcont">
<view class="" :style="[unfoldTextStyle]" id="zh-open">
<text class="zh-ellipsis">...</text>
<text class="zh-overflowcont-open">{{ unfoldText }}</text>
</view>
</view>
</view>
</template>
<!-- #ifdef VUE3 -->
<!-- vue3 -->
<script lang="ts" setup>
/*
* @param {text,String} 需要展示的文本
* @param {textStyle,Object} 需要展示的文本样式
* @param {rows,Number} 文本容器中超出多行后显示省略号
* @param {lineBreak,Boolean} 保留空白字符序列,同时允许正常换行
* @param {packupLineBreak,Boolean} 只在收起时,忽略空白字符;展开时,保留空白字符序列,同时允许正常换行
* @param {unfoldText,String} 展开操作的文案
* @param {unfoldTextStyle,Object} 展开操作的样式
* @param {packupText,String} 收起操作的文案
* @param {packupTextStyle,Object} 收起操作的样式
* @param {duration,Number || String} 展示动画过度时间(单位s)
*/
import { computed, nextTick, ref, watch, getCurrentInstance } from 'vue'
const props = defineProps({
title: {
type: String,
default: () => '',
},
titleStyle: {
type: Object,
default: () => ({
'font-weight': '600',
'margin-right': '10rpx',
}),
},
text: {
type: String,
default: () => '',
},
textStyle: {
type: Object,
default: () => ({
color: '#000',
'font-size': '28rpx',
'font-weight': '400',
}),
},
rows: {
type: Number,
default: 3,
},
lineBreak: {
type: Boolean,
default: true,
},
packupLineBreak: {
type: Boolean,
default: false,
},
unfoldText: {
type: String,
default: () => '展开',
},
unfoldTextStyle: {
type: Object,
default: () => ({
color: '#206ef7',
'font-size': '28rpx',
'font-weight': '400',
}),
},
packupText: {
type: String,
default: () => '收起',
},
packupTextStyle: {
type: Object,
default: () => ({
color: '#206ef7',
'font-size': '28rpx',
'font-weight': '400',
}),
},
duration: {
type: [Number, String],
default: () => 0,
},
})
const box_h = ref(0) //展开后的高度
const showall = ref(false) //是否展开
const is_open = ref(false) //是否显示展开/收起按钮
const unfold_packup_line = ref(22) //展开收起宽度
const settime = ref(null) //定时器标识
const instance = getCurrentInstance()
const selectQuery = uni.createSelectorQuery().in(instance.proxy)
//导出自定义事件
const emit = defineEmits(['click'])
//事件
//展开收起事件
const open = () => {
showall.value = !showall.value
emit('click', showall.value) //展开true 关闭false
}
//计算属性
const textStyleObject = computed(() => {
const { rows } = props
return {
'max-height': showall.value ? `${1.5 * box_h.value}em` : `${1.5 * rows}em`,
}
})
const textClass = computed(() => {
return showall.value ? 'showall' : ''
})
const lineStyle = computed(() => {
const { rows } = props
return {
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': rows,
overflow: 'hidden',
'word-break': 'break-all',
}
})
const lineBreakStyles = computed(() => {
const { lineBreak, packupLineBreak } = props
if (!lineBreak) {
return {
'white-space': 'normal',
}
} else {
if (packupLineBreak) {
return {
'white-space': showall.value ? 'pre-wrap' : 'normal',
}
} else {
return {
'white-space': 'pre-wrap',
}
}
}
})
const lineBreakStyle = computed(() => {
const { lineBreak } = props
return {
'white-space': lineBreak ? 'pre-wrap' : 'normal',
}
})
// 监听
watch(
() => props.text,
(newValue, oldValue) => {
const { rows } = props
nextTick(() => {
clearTimeout(settime.value)
settime.value = setTimeout(() => {
selectQuery.select(`#zh-overflowcont`).boundingClientRect()
selectQuery.select('#zh-overflowcontline').boundingClientRect()
selectQuery.select('#zh-open').boundingClientRect()
selectQuery.exec(res => {
if (res && res[0] && res[1]) {
const h1 = res[0].height // 收起时高度
const h2 = res[1].height // 展开时高度
const lineCount = rows
const lineHeight = h1 / lineCount
// 计算实际行数(考虑空行)
const actualLines = Math.ceil(h2 / lineHeight)
// 如果实际行数超过限制行数或者高度差超过0.1行,才显示展开/收起按钮
if (actualLines > lineCount || h2 > h1 + lineHeight * 0.1) {
is_open.value = true
} else {
is_open.value = false
}
box_h.value = Math.ceil(h2 / lineHeight) + 1
showall.value = false
}
if (res && res[2]) {
let unfold_packup_width = res[2].height - 1.5 || 22
unfold_packup_line.value = unfold_packup_width
}
})
}, 100)
})
},
{
deep: true,
immediate: true,
}
)
</script>
<!-- #endif -->
<!-- #ifdef VUE2 -->
<!-- vue2 -->
<script>
/*
* @param {text,String} 需要展示的文本
* @param {textStyle,Object} 需要展示的文本样式
* @param {rows,Number} 文本容器中超出多行后显示省略号
* @param {lineBreak,Boolean} 保留空白字符序列,同时允许正常换行
* @param {packupLineBreak,Boolean} 只在收起时,忽略空白字符;展开时,保留空白字符序列,同时允许正常换行
* @param {unfoldText,String} 展开操作的文案
* @param {unfoldTextStyle,Object} 展开操作的样式
* @param {packupText,String} 收起操作的文案
* @param {packupTextStyle,Object} 收起操作的样式
* @param {duration,Number || String} 展示动画过度时间(单位s)
*/
export default {
name: 'zh-readMore',
props: {
title: {
type: String,
default: () => '',
},
titleStyle: {
type: Object,
default: () => ({
'font-weight': '600',
'margin-right': '10rpx',
}),
},
text: {
type: String,
default: () => '',
},
textStyle: {
type: Object,
default: () => ({
color: '#000',
'font-size': '28rpx',
'font-weight': '400',
}),
},
rows: {
type: Number,
default: 3,
},
lineBreak: {
type: Boolean,
default: true,
},
packupLineBreak: {
type: Boolean,
default: false,
},
unfoldText: {
type: String,
default: () => '展开',
},
unfoldTextStyle: {
type: Object,
default: () => ({
color: '#206ef7',
'font-size': '28rpx',
'font-weight': '400',
}),
},
packupText: {
type: String,
default: () => '收起',
},
packupTextStyle: {
type: Object,
default: () => ({
color: '#206ef7',
'font-size': '28rpx',
'font-weight': '400',
}),
},
duration: {
type: Number || String,
default: () => 0,
},
},
data() {
return {
box_h: 0, //展开后的高度
showall: false, //是否展开
is_open: false, //是否显示展开/收起按钮
unfold_packup_line: 22, //展开收起宽度
settime: null, //定时器标识
}
},
methods: {
//展开收起事件
open() {
this.showall = !this.showall
this.$emit('click', this.showall) //展开true 关闭false
},
},
mounted() {},
computed: {
textStyleObject() {
return {
'max-height': this.showall ? `${1.5 * this.box_h}em` : `${1.5 * this.rows}em`,
}
},
textClass() {
return this.showall ? 'showall' : ''
},
lineStyle() {
return {
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': this.rows,
overflow: 'hidden',
'word-break': 'break-all',
}
},
lineBreakStyles() {
if (!this.lineBreak) {
return {
'white-space': 'normal',
}
} else {
if (this.packupLineBreak) {
return {
'white-space': this.showall ? 'pre-wrap' : 'normal',
}
} else {
return {
'white-space': 'pre-wrap',
}
}
}
},
lineBreakStyle() {
return {
'white-space': this.lineBreak ? 'pre-wrap' : 'normal',
}
},
},
watch: {
text: {
handler(newValue, oldValue) {
this.$nextTick(() => {
clearTimeout(this.settime)
this.settime = setTimeout(() => {
const selectQuery = uni.createSelectorQuery().in(this)
selectQuery.select(`#zh-overflowcont`).boundingClientRect()
selectQuery.select('#zh-overflowcontline').boundingClientRect()
selectQuery.select('#zh-open').boundingClientRect()
selectQuery.exec(res => {
if (res && res[0] && res[1]) {
const h1 = res[0].height // 收起时高度
const h2 = res[1].height // 展开时高度
const lineCount = this.rows
const lineHeight = h1 / lineCount
// 计算实际行数(考虑空行)
const actualLines = Math.ceil(h2 / lineHeight)
// 如果实际行数超过限制行数或者高度差超过0.1行,才显示展开/收起按钮
if (actualLines > lineCount || h2 > h1 + lineHeight * 0.1) {
this.is_open = true
} else {
this.is_open = false
}
this.box_h = Math.ceil(h2 / lineHeight) + 1
this.showall = false
}
if (res && res[2]) {
let unfold_packup_width = res[2].height - 1.5 || 22
this.unfold_packup_line = unfold_packup_width
}
})
}, 100)
})
},
deep: true,
immediate: true,
},
},
}
</script>
<!-- #endif -->
<style lang="scss">
.zh-readMore {
position: relative;
.zh-readMore-wrapper {
overflow: hidden;
display: flex;
.zh-text {
position: relative;
line-height: 1.5;
text-align: justify;
text-overflow: ellipsis;
word-break: break-all;
transition: var(--duration) max-height linear;
}
.zh-text::before {
float: right;
height: calc(100% - var(--unfold_packup_line));
content: '';
}
.zh-open {
position: relative;
float: right;
clear: both;
line-height: 1.5;
z-index: 2;
.zh-open-text {
margin-left: 6rpx;
}
}
.zh-fewer {
display: inline-block;
.zh-fewer-text {
margin-left: 6rpx;
}
}
}
}
.zh-ellipsis {
font-size: 30rpx;
color: #434343;
}
.zh-overflowcont {
position: absolute;
top: -10000px;
left: -10000px;
width: 100%;
line-height: 1.5;
text-align: justify;
text-overflow: ellipsis;
word-break: break-all;
.zh-overflowcont-text {
display: block;
}
.zh-overflowcont-open {
margin-left: 6rpx;
}
}
</style>