feat(ReText): 新增ReText组件,支持自动省略显示Tooltip功能, 支持多行省略, 高可复用性 (#898)

* feat(ReText): 新增ReText组件 - 基于El-Text, 增加自动省略显示Tooltip功能, 高可复用性
This commit is contained in:
苗大 2024-02-19 13:36:07 +08:00 committed by GitHub
parent c62731df5b
commit f6eaa8d6d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 294 additions and 164 deletions

View File

@ -44,6 +44,7 @@ menus:
hsmap: Map
hsdraggable: Draggable
hssplitPane: Split Pane
hsText: Text Ellipsis
hsElButton: Button
hsbutton: Button Animation
hsCheckButton: Check Button

View File

@ -44,6 +44,7 @@ menus:
hsmap: 地图
hsdraggable: 拖拽
hssplitPane: 切割面板
hsText: 文本省略
hsElButton: 按钮
hsCheckButton: 可选按钮
hsbutton: 按钮动效

View File

@ -1,4 +1,3 @@
@import "tippy.js/themes/light.css";
@import "cropperjs/dist/cropper.css";
.re-circled {

View File

@ -0,0 +1,7 @@
import reText from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 支持`Tooltip`提示的文本省略组件 */
export const ReText = withInstall(reText);
export default ReText;

View File

@ -0,0 +1,62 @@
<script lang="ts" setup>
import { h, onMounted, ref, useSlots } from "vue";
import { useTippy, type TippyOptions } from "vue-tippy";
const props = defineProps({
//
lineClamp: {
type: [String, Number]
},
tippyProps: {
type: Object as PropType<TippyOptions>,
default: () => ({})
}
});
const $slots = useSlots();
const textRef = ref();
const tippyFunc = ref();
const isTextEllipsis = (el: HTMLElement) => {
if (!props.lineClamp) {
//
return el.scrollWidth > el.clientWidth;
} else {
//
return el.scrollHeight > el.clientHeight;
}
};
const getTippyProps = () => ({
content: h($slots.content || $slots.default),
...props.tippyProps
});
function handleHover(event: MouseEvent) {
if (isTextEllipsis(event.target as HTMLElement)) {
tippyFunc.value.setProps(getTippyProps());
tippyFunc.value.enable();
} else {
tippyFunc.value.disable();
}
}
onMounted(() => {
tippyFunc.value = useTippy(textRef.value?.$el, getTippyProps());
});
</script>
<template>
<el-text
v-bind="{
truncated: !lineClamp,
lineClamp,
...$attrs
}"
ref="textRef"
@mouseover.self="handleHover"
>
<slot />
</el-text>
</template>

View File

@ -3,10 +3,12 @@ import path from "path";
import { getConfig } from "@/config";
import { menuType } from "../../types";
import extraIcon from "./extraIcon.vue";
import { useDark } from "@pureadmin/utils";
import { ReText } from "@/components/ReText";
import { useNav } from "@/layout/hooks/useNav";
import { transformI18n } from "@/plugins/i18n";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
import { type CSSProperties, type PropType, computed, ref, toRaw } from "vue";
import ArrowUp from "@iconify-icons/ep/arrow-up-bold";
import EpArrowDown from "@iconify-icons/ep/arrow-down-bold";
@ -14,6 +16,7 @@ import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
const { isDark } = useDark();
const props = defineProps({
item: {
@ -29,13 +32,6 @@ const props = defineProps({
}
});
const getSpanStyle = computed((): CSSProperties => {
return {
width: "100%",
textAlign: "center"
};
});
const getNoDropdownStyle = computed((): CSSProperties => {
return {
display: "flex",
@ -43,15 +39,7 @@ const getNoDropdownStyle = computed((): CSSProperties => {
};
});
const getMenuTextStyle = computed(() => {
return {
overflow: "hidden",
textOverflow: "ellipsis",
outline: "none"
};
});
const getsubMenuIconStyle = computed((): CSSProperties => {
const getSubMenuIconStyle = computed((): CSSProperties => {
return {
display: "flex",
justifyContent: "center",
@ -65,43 +53,6 @@ const getsubMenuIconStyle = computed((): CSSProperties => {
};
});
const getSubTextStyle = computed((): CSSProperties => {
if (!isCollapse.value) {
return {
width: "210px",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis"
};
} else {
return {
width: ""
};
}
});
const getSubMenuDivStyle = computed((): any => {
return item => {
return !isCollapse.value
? {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
}
: {
width: "100%",
textAlign:
item?.parentId === null
? "center"
: layout.value === "mix" && item?.pathList?.length === 2
? "center"
: ""
};
};
});
const expandCloseIcon = computed(() => {
if (!getConfig()?.MenuArrowIconNoTransition) return "";
return {
@ -113,41 +64,6 @@ const expandCloseIcon = computed(() => {
});
const onlyOneChild: menuType = ref(null);
// showTooltip
const hoverMenuMap = new WeakMap();
// dom
const menuTextRef = ref(null);
function hoverMenu(key) {
// showTooltip退
if (hoverMenuMap.get(key)) return;
nextTick(() => {
//
menuTextRef.value?.scrollWidth > menuTextRef.value?.clientWidth
? Object.assign(key, {
showTooltip: true
})
: Object.assign(key, {
showTooltip: false
});
hoverMenuMap.set(key, true);
});
}
//
function overflowSlice(text, item?: any) {
const newText =
(text?.length > 1 ? text.toString().slice(0, 1) : text) + "...";
if (item && !(isCollapse.value && item?.parentId === null)) {
return layout.value === "mix" &&
item?.pathList?.length === 2 &&
isCollapse.value
? newText
: text;
}
return newText;
}
function hasOneShowingChild(children: menuType[] = [], parent: menuType) {
const showingChildren = children.filter((item: any) => {
@ -194,7 +110,7 @@ function resolvePath(routePath) {
<div
v-if="toRaw(props.item.meta.icon)"
class="sub-menu-icon"
:style="getsubMenuIconStyle"
:style="getSubMenuIconStyle"
>
<component
:is="
@ -205,51 +121,34 @@ function resolvePath(routePath) {
"
/>
</div>
<span
<el-text
v-if="
!props.item?.meta.icon &&
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
(!props.item?.meta.icon &&
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1) ||
(!onlyOneChild.meta.icon &&
isCollapse &&
layout === 'mix' &&
props.item?.pathList?.length === 2)
"
:style="getSpanStyle"
truncated
class="!px-4 !text-inherit"
>
{{ overflowSlice(transformI18n(onlyOneChild.meta.title)) }}
</span>
<span
v-if="
!onlyOneChild.meta.icon &&
isCollapse &&
layout === 'mix' &&
props.item?.pathList?.length === 2
"
:style="getSpanStyle"
>
{{ overflowSlice(transformI18n(onlyOneChild.meta.title)) }}
</span>
{{ transformI18n(onlyOneChild.meta.title) }}
</el-text>
<template #title>
<div :style="getDivStyle">
<span v-if="layout === 'horizontal'">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
<el-tooltip
v-else
placement="top"
:effect="tooltipEffect"
:offset="-10"
:disabled="!onlyOneChild.showTooltip"
<ReText
:tippyProps="{
offset: [0, -10],
theme: !isDark ? tooltipEffect : undefined
}"
class="!text-inherit"
>
<template #content>
{{ transformI18n(onlyOneChild.meta.title) }}
</template>
<span
ref="menuTextRef"
:style="getMenuTextStyle"
@mouseover="hoverMenu(onlyOneChild)"
>
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</el-tooltip>
{{ transformI18n(onlyOneChild.meta.title) }}
</ReText>
<extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
</div>
</template>
@ -264,48 +163,38 @@ function resolvePath(routePath) {
<template #title>
<div
v-if="toRaw(props.item.meta.icon)"
:style="getsubMenuIconStyle"
:style="getSubMenuIconStyle"
class="sub-menu-icon"
>
<component
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/>
</div>
<span v-if="layout === 'horizontal'">
{{ transformI18n(props.item.meta.title) }}
</span>
<div
<ReText
v-if="
!(
layout === 'vertical' &&
isCollapse &&
toRaw(props.item.meta.icon) &&
props.item.parentId === null
)
"
:style="getSubMenuDivStyle(props.item)"
:tippyProps="{
offset: [0, -10],
theme: !isDark ? tooltipEffect : undefined
}"
:class="{
'!text-inherit': true,
'!px-4':
layout !== 'horizontal' &&
isCollapse &&
!toRaw(props.item.meta.icon) &&
props.item.parentId === null
}"
>
<el-tooltip
v-if="layout !== 'horizontal'"
placement="top"
:effect="tooltipEffect"
:offset="-10"
:disabled="!props.item.showTooltip"
>
<template #content>
{{ transformI18n(props.item.meta.title) }}
</template>
<span
ref="menuTextRef"
:style="getSubTextStyle"
@mouseover="hoverMenu(props.item)"
>
{{
overflowSlice(transformI18n(props.item.meta.title), props.item)
}}
</span>
</el-tooltip>
<extraIcon v-if="!isCollapse" :extraIcon="props.item.meta.extraIcon" />
</div>
{{ transformI18n(props.item.meta.title) }}
</ReText>
<extraIcon v-if="!isCollapse" :extraIcon="props.item.meta.extraIcon" />
</template>
<sidebar-item

View File

@ -31,7 +31,7 @@ Object.keys(directives).forEach(key => {
app.directive(key, (directives as { [key: string]: Directive })[key]);
});
// 全局注册`@iconify/vue`图标库
// 全局注册@iconify/vue图标库
import {
IconifyIconOffline,
IconifyIconOnline,
@ -45,13 +45,11 @@ app.component("FontIcon", FontIcon);
import { Auth } from "@/components/ReAuth";
app.component("Auth", Auth);
// 全局注册`vue-tippy`
// 全局注册vue-tippy
import "tippy.js/dist/tippy.css";
import "tippy.js/animations/perspective.css";
import "tippy.js/themes/light.css";
import VueTippy from "vue-tippy";
app.use(VueTippy, {
defaultProps: { animation: "perspective" }
});
app.use(VueTippy);
getPlatformConfig(app).then(async config => {
setupStore(app);

View File

@ -91,6 +91,15 @@ export default {
title: $t("menus.hssegmented")
}
},
{
path: "/components/text",
name: "PureText",
component: () => import("@/views/components/text.vue"),
meta: {
title: $t("menus.hsText"),
extraIcon: "IF-pure-iconfont-new svg"
}
},
{
path: "/components/el-button",
name: "PureButton",

View File

@ -0,0 +1,164 @@
<script lang="ts" setup>
import dayjs from "dayjs";
import { ref } from "vue";
import { ReText } from "@/components/ReText";
defineOptions({
name: "PureText"
});
const customContent = ref("自定义tooltip内容");
const changeTooltipContent = () => {
customContent.value =
"现在的时间是: " + dayjs().format("YYYY-MM-DD HH:mm:ss");
};
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="font-medium">
文本省略基于
<el-link
href="https://element-plus.org/zh-CN/component/text.html"
target="_blank"
style="margin: 0 4px 5px; font-size: 16px"
>
el-text
</el-link>
<el-link
href="https://vue-tippy.netlify.app/basic-usage"
target="_blank"
style="margin: 0 4px 5px; font-size: 16px"
>
VueTippy
</el-link>
自动省略后显示 Tooltip 提示 支持多行省略
</span>
</div>
</template>
<p class="mb-2">基础用法</p>
<el-space wrap>
<ul class="content">
<li>
<ReText>
测试文本这是一个稍微有点长的文本过长省略后鼠标悬浮会有tooltip提示
</ReText>
<ReText :lineClamp="2">
测试文本这是一个稍微有点长的文本lineClamp参数为2即两行过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
</ul>
</el-space>
<el-divider />
<p class="mb-2">自定义 Tooltip 内容</p>
<div class="mb-2">
<el-button @click="changeTooltipContent">
点击切换下方 Tooltip 内容
</el-button>
</div>
<el-space wrap>
<ul class="content">
<li>
<ReText :tippyProps="{ content: customContent }">
props写法 -
测试文本这是一个稍微有点长的文本过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
<li>
<ReText>
<template #content>
<div>
<b>这是插槽写法: </b>
<div>{{ customContent }}</div>
</div>
</template>
插槽写法 -
测试文本这是一个稍微有点长的文本过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
</ul>
</el-space>
<el-divider />
<p class="mb-2">自定义 el-text 配置</p>
<el-space wrap>
<ul class="content">
<li>
<ReText type="primary" size="large">
测试文本这是一个稍微有点长的文本过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
<li>
<ReText :lineClamp="4" type="info">
测试文本这是一个非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长的文本lineClamp参数为4即四行过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
</ul>
</el-space>
<el-divider />
<p class="mb-2">自定义 VueTippy 配置</p>
<el-space wrap>
<ul class="content">
<li>
<ReText
:tippyProps="{ offset: [0, -20], theme: 'light', arrow: false }"
>
偏移白色无箭头 -
测试文本这是一个稍微有点长的文本过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
<li>
<ReText :lineClamp="4" :tippyProps="{ followCursor: true }">
鼠标跟随 -
测试文本这是一个非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长非常非常长的文本lineClamp参数为4即四行过长省略后鼠标悬浮会有tooltip提示
</ReText>
</li>
</ul>
</el-space>
<el-divider />
<p class="mb-2">组件嵌套: 不需要省略的需设置 truncated false</p>
<el-space wrap>
<ul class="content">
<li>
<ReText tag="p" :lineClamp="2">
This is a paragraph. Paragraph start
<ReText :truncated="false">
This is ReText
<ReText tag="sup" size="small" :truncated="false">
superscript
</ReText>
</ReText>
<el-text>
This is El-Text
<el-text tag="sub" size="small"> subscript </el-text>
</el-text>
<el-text tag="ins">Inserted</el-text>
<el-text tag="del">Deleted</el-text>
<el-text tag="mark">Marked</el-text>
Paragraph end.
</ReText>
</li>
</ul>
</el-space>
</el-card>
</template>
<style lang="scss" scoped>
.content {
width: 400px;
padding: 15px;
overflow: hidden;
resize: horizontal;
background-color: var(--el-color-info-light-9);
border-radius: 8px;
}
</style>