Selaa lähdekoodia

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

* feat(ReText): 新增ReText组件 - 基于El-Text, 增加自动省略显示Tooltip功能, 高可复用性
苗大 1 vuosi sitten
vanhempi
commit
f6eaa8d6d8

+ 1 - 0
locales/en.yaml

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

+ 1 - 0
locales/zh-CN.yaml

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

+ 0 - 1
src/components/ReCropper/src/circled.css

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

+ 7 - 0
src/components/ReText/index.ts

@@ -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 - 0
src/components/ReText/src/index.vue

@@ -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>

+ 46 - 157
src/layout/components/sidebar/sidebarItem.vue

@@ -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

+ 4 - 6
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);

+ 9 - 0
src/router/modules/components.ts

@@ -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 - 0
src/views/components/text.vue

@@ -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>