mirror of
https://gitee.com/yiming_chang/vue-pure-admin.git
synced 2024-11-29 17:57:37 +08:00
feat(ReText): 新增ReText
组件,支持自动省略显示Tooltip
功能, 支持多行省略, 高可复用性 (#898)
* feat(ReText): 新增ReText组件 - 基于El-Text, 增加自动省略显示Tooltip功能, 高可复用性
This commit is contained in:
parent
c62731df5b
commit
f6eaa8d6d8
@ -44,6 +44,7 @@ menus:
|
||||
hsmap: Map
|
||||
hsdraggable: Draggable
|
||||
hssplitPane: Split Pane
|
||||
hsText: Text Ellipsis
|
||||
hsElButton: Button
|
||||
hsbutton: Button Animation
|
||||
hsCheckButton: Check Button
|
||||
|
@ -44,6 +44,7 @@ menus:
|
||||
hsmap: 地图
|
||||
hsdraggable: 拖拽
|
||||
hssplitPane: 切割面板
|
||||
hsText: 文本省略
|
||||
hsElButton: 按钮
|
||||
hsCheckButton: 可选按钮
|
||||
hsbutton: 按钮动效
|
||||
|
@ -1,4 +1,3 @@
|
||||
@import "tippy.js/themes/light.css";
|
||||
@import "cropperjs/dist/cropper.css";
|
||||
|
||||
.re-circled {
|
||||
|
7
src/components/ReText/index.ts
Normal file
7
src/components/ReText/index.ts
Normal 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;
|
62
src/components/ReText/src/index.vue
Normal file
62
src/components/ReText/src/index.vue
Normal 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>
|
@ -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
|
||||
|
10
src/main.ts
10
src/main.ts
@ -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);
|
||||
|
@ -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",
|
||||
|
164
src/views/components/text.vue
Normal file
164
src/views/components/text.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user