瀏覽代碼

Merge branch 'dev'

zhangchong 1 年之前
父節點
當前提交
b8a48e7c3b
共有 3 個文件被更改,包括 159 次插入321 次删除
  1. 0 319
      src/components/ExpandText/index.vue
  2. 3 2
      src/components/ProcessTimeLine/index.vue
  3. 156 0
      src/components/TextEllipsis/index.vue

+ 0 - 319
src/components/ExpandText/index.vue

@@ -1,319 +0,0 @@
-<!-- <template>
-    <div ref="textOverflow" class="text-overflow" :style="boxStyle">
-        <span ref="overEllipsis" class="content">{{ realText }}</span>
-        <span class="slot-box" ref="slotRef" v-if="showSlotNode">
-            <slot :click-toggle="toggle" :expanded="expanded">
-                <el-button @click="toggle" link type="primary" class="ml5 tan">
-                    {{ expanded ? stowText : expandText }}
-                    <SvgIcon name="ele-ArrowUp" class="ml2" v-show="expanded" />
-                    <SvgIcon name="ele-ArrowRight" class="ml2" v-show="!expanded" />
-                </el-button>
-            </slot>
-        </span>
-    </div>
-    
-</template>
-
-<script lang="ts" setup name="CommonContent">
-import { computed, onMounted, ref, nextTick } from 'vue'
- //用法
- //<ExpandText :text="content" :maxLines="3">
- //</ExpandText>
-const props = defineProps({
-    text: {
-        type: String,
-        required: true
-    },
-    maxLines: {
-        type: Number,
-        default: 3,
-    },
-    width: {
-        type: Number,
-        default: 0,
-    },
-    expandText: {
-        type: String,
-        default: "展开",
-    },
-    stowText: {
-        type: String,
-        default: "收起",
-    }
-})
-const expanded = ref(false)
-const slotBoxWidth = ref(0)
-const showSlotNode = ref(false)
-const textOverflow = ref(null as any)
-const overEllipsis = ref(null as any)
-const slotRef = ref(null as any)
-const offset = ref(props.text.length)
-const textBoxWidth = ref(props.width)
-
-const boxStyle = computed(() => {
-    if (props.width) {
-        return {
-            width: props.width + "%",
-        };
-    } else {
-        return ''
-    }
-})
-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: any, to: any) => {
-    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;
-}
-
-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">
-.text-overflow {
-    .slot-box {
-        display: inline-block;
-    }
-}
-</style> -->
-、
-<!--展开/收起 组件-->
-<template>
-	<div
-		class="collapse w100 pd15"
-		ref="collapse"
-		:style="{
-			height: state.isShow ? state.containerH + 'px' : state.containerH + 'px',
-			backgroundColor: backgroundColor,
-		}"
-	>
-		<div
-			class="collapse-content"
-			ref="content"
-			:style="{ position: state.isShow ? 'unset' : 'absolute', top: 0, lineHeight: state.isShow ? '' : props.defaultHeight + 'px' }"
-		>
-			<slot></slot>
-		</div>
-		<div
-			v-if="!hideControl"
-			v-show="state.controlVisible"
-			class="collapse-control"
-			:style="{
-				backgroundColor: backgroundColor,
-				position: 'absolute',
-				bottom: '0',
-				right: '0',
-				height: props.defaultHeight + 'px',
-				lineHeight: props.defaultHeight + 'px',
-			}"
-		>
-			<el-button link type="primary" @click="handleControl">
-				{{ state.isShow ? foldText : unfoldText }}
-				<template v-if="!hideControlIcon">
-					<SvgIcon name="ele-ArrowUp" class="ml4" :style="{ transform: state.isShow ? 'none' : 'rotateZ(90deg)' }" />
-				</template>
-			</el-button>
-		</div>
-	</div>
-</template>
-
-<script lang="ts" setup name="collapse">
-import { onMounted, reactive, watch, ref, onUnmounted, nextTick } from 'vue';
-// 定义父组件传过来的值
-const props = defineProps({
-	visible: Boolean,
-
-	// 默认显示的内容高度
-	defaultHeight: {
-		type: Number,
-		default: 30,
-	},
-
-	// 是否隐藏内置控制按钮
-	hideControl: Boolean,
-
-	// 是否隐藏内置控制按钮icon
-	hideControlIcon: Boolean,
-
-	foldText: {
-		type: String,
-		default: '收起',
-	},
-
-	unfoldText: {
-		type: String,
-		default: '详情',
-	},
-	backgroundColor: {
-		type: String,
-		default: 'var(--hotline-bg-main-color)',
-	},
-});
-
-// 定义子组件向父组件传值/事件
-const emit = defineEmits(['change']);
-const content = ref<RefType>();
-const state = reactive({
-	isShow: false,
-	controlVisible: true,
-
-	containerH: props.defaultHeight,
-	contentH: 0,
-	DOMWatcher: null as any,
-	padding: 14,
-});
-
-// 展开收起
-const handleControl = () => {
-	if (state.isShow) handleFold();
-	else unfold();
-};
-
-const init = () => {
-	state.contentH = content.value.offsetHeight + 30 + 20;
-	/**
-	 * 当内容变化的同时也是展开状态的话,那么需要更新容器高度
-	 * 1. 当最新的内容高度大于默认高度时,则把容器高度更新最新内容高度一致即可
-	 * 2. 反之,把容器高度设到最小(即默认高度),同时设为收起状态
-	 */
-	if (state.isShow) {
-		if (state.contentH > props.defaultHeight) {
-			state.containerH = state.contentH;
-		} else {
-			state.containerH = props.defaultHeight;
-			state.isShow = false;
-		}
-	}
-	state.controlVisible = content.value.offsetHeight > props.defaultHeight; // 控制按钮的显示隐藏
-};
-
-const watcher = () => {
-	state.DOMWatcher = new MutationObserver(() => {
-		// 监测到DOM变化,重新计算高度
-		refresh();
-	});
-	state.DOMWatcher.observe(content.value, {
-		childList: true,
-		attributes: true,
-		characterData: true,
-		subtree: true,
-	});
-};
-const refresh = () => {
-	nextTick(() => {
-		init();
-	});
-};
-// 收起
-const handleFold = () => {
-	state.containerH = props.defaultHeight;
-	nextTick(() => {
-		state.isShow = false;
-	});
-};
-// 展开
-const unfold = () => {
-	const contentH = content.value.offsetHeight;
-	if (contentH > props.defaultHeight) {
-		state.containerH = contentH;
-	}
-	nextTick(() => {
-		state.isShow = true;
-	});
-};
-watch(
-	() => props.visible,
-	(v: any) => {
-		if (v) {
-			unfold();
-		} else {
-			handleFold();
-		}
-	}
-);
-watch(
-	() => state.isShow,
-	(v: any) => {
-		emit('change', v);
-	}
-);
-
-onMounted(() => {
-	init();
-	watcher();
-	if (props.visible) {
-		unfold();
-	}
-});
-onUnmounted(() => {
-	state.DOMWatcher.disconnect();
-});
-</script>
-
-<style scoped lang="scss">
-.collapse {
-	position: relative;
-	overflow: hidden;
-	border-radius: 3px;
-	transition: height 0.28s ease-in-out;
-	.collapse-control {
-		z-index: 2;
-		padding: 0 10px;
-	}
-}
-</style>

+ 3 - 2
src/components/ProcessTimeLine/index.vue

@@ -72,7 +72,8 @@
 					</div>
 					<div class="opinion" v-if="item.opinion">
 						<span class="opinion-title">意见</span>
-						<ExpandText> {{ item.opinion }} </ExpandText>
+						<text-ellipsis :content="item.opinion" :rows="1">
+            </text-ellipsis>
 					</div>
 				</el-card>
 				<!-- 展开收起 -->
@@ -134,7 +135,7 @@ const state = reactive<any>({
 	expandedKeys: [], // 当前列表需要展开的节点id组成的数组
 	curNameId: 0, //保存current的值
 });
-const ExpandText = defineAsyncComponent(() => import('/@/components/ExpandText/index.vue'));
+const TextEllipsis = defineAsyncComponent(() => import('/src/components/TextEllipsis/index.vue'));
 
 const isOpen = computed(() => {
 	return (id: string) => {

+ 156 - 0
src/components/TextEllipsis/index.vue

@@ -0,0 +1,156 @@
+<template>
+	<div class="text-ellipsis w100" :class="[!isExpand && 'un-expand']">
+		<div ref="contentEl" class="text-ellipsis-content" :style="{ maxHeight: isExpand ? 'none' : `${maxHeight}px` }">
+			<!-- 占位符 -->
+			<span class="text-ellipsis-placeholder" :style="{ height: placeholderHeight + 'px' }"></span>
+			<!-- 内容+操作按钮,不留空格 -->
+			{{ isExpand ? content : ''
+			}}<span v-if="isEll" ref="tailEl" class="text-ellipsis-tail">
+				<span class="text-ellipsis-dot" v-if="!isExpand">{{ dot }}</span
+				><span v-if="!single" @click="onActionClick">
+					<el-button link type="primary"
+						>{{ actionText }} <SvgIcon :class="{ 'is-reverse': !isExpand }" name="ele-ArrowUp" class="ml3 arrow" size="16px" /></el-button
+				></span> </span
+			>{{ isExpand ? '' : content }}
+		</div>
+		<span v-if="single && isEll" @click="onActionClick">
+			<el-button link type="primary"
+				>{{ actionText }}<SvgIcon :class="{ 'is-reverse': !isExpand }" name="ele-ArrowUp" class="ml3 arrow" size="16px" /></el-button
+		></span>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineExpose, ref, onBeforeMount, watch, computed, nextTick } from 'vue';
+
+const props = defineProps({
+	// 文本内容
+	content: {
+		type: String,
+		default: '',
+		required: true,
+	},
+	// 省略行数
+	rows: {
+		type: Number,
+		default: 5,
+	},
+	// 展开文案
+	expandText: {
+		type: String,
+		default: '展开',
+	},
+	// 收起文案
+	collapseText: {
+		type: String,
+		default: '收起',
+	},
+	// 省略点
+	dot: {
+		type: String,
+		default: '...',
+	},
+	single: {
+		type: Boolean,
+		default: false,
+	},
+});
+
+const isEll = ref(false); // 是否省略
+const isExpand = ref(false); // 是否展开
+const contentEl = ref<null | HTMLElement>(null); // 容器dom
+const tailEl = ref<null | HTMLElement>(null); // 操作按钮dom
+const placeholderHeight = ref(0); // 占位符高度
+const maxHeight = ref(0); // 最大高度
+
+const actionText = computed(() => {
+	return isExpand.value ? props.collapseText : props.expandText;
+});
+
+function toNum(val: any): number {
+	if (!val) return 0;
+
+	return parseFloat(val);
+}
+
+let lazyToCalc = false; // 延迟执行
+
+// 计算内容省略
+async function calcEll() {
+	await nextTick();
+	if (!contentEl.value) return;
+	// 计算最大高度
+	const { lineHeight } = window.getComputedStyle(contentEl.value);
+	if (Number.isNaN(lineHeight)) {
+		console.warn(`text-ellipsis 组件不能设置line-height为${lineHeight}`);
+	}
+	maxHeight.value = toNum(lineHeight) * props.rows;
+	// 判断是否省略内容
+	isEll.value = contentEl.value.scrollHeight > maxHeight.value;
+	// 计算占位符高度: 容器高度 - 操作按钮高度
+	if (isEll.value) {
+		// 延迟执行,解决内容已经展开时,触发计算,tailEl容器展开时的高度与收起时高度不一致,导致错位
+		if (isExpand.value) {
+			lazyToCalc = true;
+			return;
+		}
+		await nextTick();
+		if (tailEl.value) {
+			placeholderHeight.value = maxHeight.value - tailEl.value.offsetHeight;
+		}
+	}
+}
+
+// 展开/收起点击
+function onActionClick() {
+	isExpand.value = !isExpand.value;
+	if (lazyToCalc) {
+		lazyToCalc = false;
+		calcEll();
+	}
+}
+
+onBeforeMount(() => {
+	calcEll();
+});
+
+watch(() => [props.content, props.rows], calcEll);
+
+defineExpose({
+	update: calcEll,
+});
+</script>
+
+<style lang="scss" scoped>
+.text-ellipsis {
+	line-height: 30px;
+	background-color: var(--hotline-bg-main-color);
+	padding: 0 10px;
+	&.un-expand {
+		.text-ellipsis-placeholder {
+			float: right;
+		}
+		.text-ellipsis-tail {
+			float: right;
+			clear: both;
+		}
+	}
+	&.is-single {
+		.text-ellipsis-action {
+			display: block;
+		}
+	}
+}
+.text-ellipsis-content {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	word-wrap: break-word;
+	word-break: break-word;
+	text-justify: inter-character;
+	text-align: justify;
+	white-space: pre-line;
+}
+.arrow.is-reverse {
+	transform: rotateZ(-180deg);
+}
+</style>