Explorar el Código

文字提示 Tooltip(未完成)

lifanagju_citu hace 3 meses
padre
commit
423e67eed8

+ 2 - 0
components.d.ts

@@ -33,6 +33,7 @@ declare module 'vue' {
     Echarts: typeof import('./src/components/Echarts/index.vue')['default']
     ElCascader: typeof import('element-plus/es')['ElCascader']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
+    Ellipsis: typeof import('./src/components/Ellipsis/index.vue')['default']
     ElTree: typeof import('element-plus/es')['ElTree']
     Empty: typeof import('./src/components/Empty/index.vue')['default']
     File: typeof import('./src/components/Upload/file.vue')['default']
@@ -71,6 +72,7 @@ declare module 'vue' {
     TextInput: typeof import('./src/components/FormUI/TextInput/index.vue')['default']
     TipDialog: typeof import('./src/components/CtDialog/tipDialog.vue')['default']
     ToolBar: typeof import('./src/components/PreviewImg/toolBar.vue')['default']
+    Tooltip: typeof import('./src/components/Tooltip/index.vue')['default']
     ToolTip: typeof import('./src/components/CtTooltip/ToolTip.vue')['default']
     VerificationCode: typeof import('./src/components/VerificationCode/index.vue')['default']
     Verifition: typeof import('./src/components/Verifition/index.vue')['default']

+ 10 - 13
src/components/CtTooltip/index.js

@@ -9,8 +9,6 @@ import MyToolTip from './ToolTip.vue'
 // 位置定位
 function calculationLocation(el, target, placements) {
   if (!el || !target) return;
-  console.log('el:', el)
-  console.log('target:', target)
 
   el.tooltipPosition.y = 0;
   el.tooltipPosition.x = 0;
@@ -40,10 +38,10 @@ function calculationLocation(el, target, placements) {
 const allPlacements = ['left', 'bottom', 'right', 'top']
 
 
-function getElStyleAttr(element, attr) {
-  const styles = window.getComputedStyle(element)
-  return styles[attr]
-}
+// function getElStyleAttr(element, attr) {
+//   const styles = window.getComputedStyle(element)
+//   return styles[attr]
+// }
 
 // const positionXY = getPosition(el)
 // const getPosition = (target) => {
@@ -58,14 +56,16 @@ const isOverflow = (target) => {
   const range = document.createRange()
   range.setStart(target, 0)
   range.setEnd(target, target.childNodes.length)
-  const rangeWidth = range.getBoundingClientRect().width
-  const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
-  return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
-  // return scrollWidth > offsetWidth
+  // const rangeWidth = range.getBoundingClientRect().width
+  // const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
+  // return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
+  return scrollWidth > offsetWidth
 }
 
 export const ellipsisTooltip = localStorage.getItem('useEllipseTooltip') ? {
   mounted(el, binding) {
+    // console.log('mounted-> el:', el)
+    // console.log('mounted-> binding:', binding)
     //获取指令的参数
     const {
         value: {
@@ -90,7 +90,6 @@ export const ellipsisTooltip = localStorage.getItem('useEllipseTooltip') ? {
       if (isOverflow(el)) {
         const directiveList = allPlacements.filter(placement => binding.modifiers[placement])
         const placements = directiveList.length ? directiveList : allPlacements
-        console.log('w_tooltip1:', el.w_tooltip)
         // if (!el.w_tooltip) {}
         // 创建tooltip实例
         const vm = createApp(MyToolTip)
@@ -100,8 +99,6 @@ export const ellipsisTooltip = localStorage.getItem('useEllipseTooltip') ? {
         document.body.appendChild(el.w_tooltip)
         el.w_tooltip.id = `tooltip_${Math.floor(Math.random() * 10000)}`
         el.w_tipInstance = vm.mount(el.w_tooltip)
-        console.log('w_tooltip2:', el.w_tooltip)
-        console.log('w_tipInstance:', el.w_tipInstance)
         // 设置 tooltip 显示方向
         el.w_tipInstance.placements = placement || placements[0] || 'top'
         // 设置显示内容

+ 182 - 0
src/components/Ellipsis/index.vue

@@ -0,0 +1,182 @@
+<!-- vue-amazing-ui组件源码地址 https://themusecatcher.github.io/vue-amazing-ui/guide/components/ellipsis.html -->
+
+<script setup>
+import { ref, computed, watch, onMounted } from 'vue'
+import Tooltip from '@/components/Tooltip'
+import { useResizeObserver } from '@/utils/ellipsis.js'
+
+const props = defineProps({
+  maxWidth: { // 文本最大宽度,单位 px
+    type: [String, Number],
+    default: '100%'
+  },
+  tooltipMaxWidth: { // 弹出提示最大宽度,单位 px,默认为 文本宽度 + 24
+    type: [String, Number],
+    default: undefined
+  },
+  line: { // 最大行数
+    type: Number,
+    default: undefined
+  },
+  expand: { // 是否启用点击文本展开全部
+    type: Boolean,
+    default: false
+  },
+  tooltip: { // 是否启用文本提示框,可自定义设置弹出提示内容 boolean | slot
+    type: Boolean,
+    default: true
+  },
+})
+const tooltipRef = ref() // tooltip 组件引用
+const observeScroll = ref() // tooltip 组件暴露的 observeScroll 函数
+const showTooltip = ref(false) // 是否显示提示框
+const showExpand = ref(false) // 是否可以启用点击展开
+const expanded = ref(false) // 启用点击展开时,是否展开
+const ellipsisRef = ref() // 文本 DOM 引用
+const computedTooltipMaxWidth = ref() // 计算后的弹出提示最大宽度
+const ellipsisLine = ref() // 行数
+const stopObservation = ref(false)
+const emit = defineEmits(['expandChange'])
+const textMaxWidth = computed(() => {
+  if (typeof props.maxWidth === 'number') {
+    return `${props.maxWidth}px`
+  }
+  return props.maxWidth
+})
+watch(
+  () => props.line,
+  (to) => {
+    if (to !== undefined) {
+      ellipsisLine.value = to
+    } else {
+      ellipsisLine.value = 'none'
+    }
+  },
+  {
+    immediate: true
+  }
+)
+watch(
+  () => [props.maxWidth, props.line, props.tooltip],
+  () => {
+    updateTooltipShow()
+  },
+  {
+    deep: true,
+    flush: 'post'
+  }
+)
+useResizeObserver(ellipsisRef, () => {
+  if (stopObservation.value) {
+    setTimeout(() => {
+      stopObservation.value = false
+    })
+  } else {
+    updateTooltipShow()
+  }
+})
+onMounted(() => {
+  updateTooltipShow()
+  observeScroll.value = tooltipRef.value.observeScroll
+})
+function updateTooltipShow() {
+  const scrollWidth = ellipsisRef.value.scrollWidth
+  const scrollHeight = ellipsisRef.value.scrollHeight
+  const clientWidth = ellipsisRef.value.clientWidth
+  const clientHeight = ellipsisRef.value.clientHeight
+  const offsetWidth = ellipsisRef.value.offsetWidth
+  // computedTooltipMaxWidth.value = `${offsetWidth + 24}px`
+  computedTooltipMaxWidth.value = `100vw`
+  if (scrollWidth > clientWidth || scrollHeight > clientHeight) {
+    if (props.expand) {
+      showExpand.value = true
+    }
+    if (props.tooltip) {
+      showTooltip.value = true
+    }
+  } else {
+    if (props.expand) {
+      showExpand.value = false
+    }
+    if (props.tooltip) {
+      showTooltip.value = false
+    }
+  }
+}
+function onExpand() {
+  stopObservation.value = true
+  if (ellipsisLine.value !== 'none') {
+    ellipsisLine.value = 'none'
+    if (props.tooltip && showTooltip.value) {
+      expanded.value = true
+      tooltipRef.value.hide()
+    }
+    emit('expandChange', true)
+  } else {
+    ellipsisLine.value = props.line ?? 'none'
+    if (props.tooltip && !showTooltip.value) {
+      expanded.value = false
+      showTooltip.value = true
+      tooltipRef.value.show()
+    }
+    emit('expandChange', false)
+  }
+}
+function onAnimationEnd() {
+  if (expanded.value) {
+    showTooltip.value = false
+  }
+}
+defineExpose({
+  observeScroll
+})
+</script>
+<template>
+  <Tooltip
+    ref="tooltipRef"
+    style="cursor: pointer;"
+    :style="`max-width: ${textMaxWidth}`"
+    :max-width="computedTooltipMaxWidth"
+    :content-style="{ maxWidth: textMaxWidth }"
+    :tooltip-style="{ padding: '8px 12px' }"
+    :transition-duration="200"
+    @animationend="onAnimationEnd"
+    v-bind="$attrs"
+  >
+    <template #tooltip>
+      <slot v-if="showTooltip" name="tooltip">
+        <slot></slot>
+      </slot>
+    </template>
+    <div
+      ref="ellipsisRef"
+      class="m-ellipsis"
+      :class="[line ? 'ellipsis-line' : 'not-ellipsis-line', { 'ellipsis-cursor-pointer': showExpand }]"
+      :style="`--ellipsis-max-width: ${textMaxWidth}; --ellipsis-line: ${ellipsisLine};`"
+      @click="showExpand ? onExpand() : () => false"
+    >
+      <slot></slot>
+    </div>
+  </Tooltip>
+</template>
+<style lang="scss" scoped>
+.m-ellipsis {
+  overflow: hidden;
+  cursor: text;
+  max-width: var(--ellipsis-max-width);
+}
+.ellipsis-line {
+  display: -webkit-inline-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: var(--ellipsis-line);
+}
+.not-ellipsis-line {
+  display: inline-block;
+  vertical-align: bottom;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.ellipsis-cursor-pointer {
+  cursor: pointer;
+}
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
src/components/Enterprise/hotPromoted.vue


+ 685 - 0
src/components/Tooltip/index.vue

@@ -0,0 +1,685 @@
+<!-- vue-amazing-ui组件源码地址 https://github.com/themusecatcher/vue-amazing-ui/blob/main/components/tooltip/Tooltip.vue -->
+
+<script setup>
+import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
+// import type { CSSProperties } from 'vue'
+import {
+  useSlotsExist,
+  useMutationObserver,
+  useEventListener,
+  useResizeObserver,
+  rafTimeout,
+  cancelRaf
+} from '@/utils/ellipsis.js'
+
+const props = defineProps({
+  maxWidth: { // 文本最大宽度,单位 px
+    type: [String, Number],
+    default: 100
+  },
+  content: { // 展示的内容 string | slot
+    type: String,
+    default: undefined
+  },
+  contentClass: { // 设置展示内容的类名
+    type: String,
+    default: undefined
+  },
+  contentStyle: { // 设置展示内容的样式
+    type: Object,
+    default: () => {}
+  },
+  tooltip: { // 文字提示内容
+    type: String,
+    default: undefined
+  },
+  tooltipClass: { // 设置文字提示的类名
+    type: String,
+    default: undefined
+  },
+  tooltipStyle: { // 设置文字提示的样式
+    type: Object,
+    default: () => {}
+  },
+  bgColor: { // 文字提示框背景颜色
+    type: String,
+    default: 'rgba(0, 0, 0, 0.85)'
+  },
+  arrow: { // 是否显示箭头
+    type: Boolean,
+    default: false
+  },
+  placement: { // 文字提示位置
+    type: String,
+    default: 'top'
+  },
+  flip: { // 文字提示被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置
+    type: Boolean,
+    default: false
+  },
+  trigger: { // 文字提示触发方式 hover/click
+    type: String,
+    default: 'click'
+  },
+  keyboard: { // 是否支持按键操作 (enter 显示;esc 关闭),仅当 trigger: 'click' 时生效
+    type: Boolean,
+    default: false
+  },
+  transitionDuration: { // 文字提示动画的过渡持续时间,单位 ms
+    type: Number,
+    default: 0
+  },
+  showDelay: { // 文字提示显示的延迟时间,单位 ms
+    type: Number,
+    default: 100
+  },
+  hideDelay: { // 文字提示隐藏的延迟时间,单位 ms
+    type: Number,
+    default: 100
+  },
+  show: { // (v-model) 文字提示是否显示
+    type: Boolean,
+    default: false
+  },
+  showControl: { // 只使用 show 属性控制显示隐藏,仅当 trigger: hover 时生效,此时移入移出将不会触发显示隐藏,全部由 show 属性控制
+    type: Boolean,
+    default: false
+  },
+})
+// const tooltip  = ref('')
+const tooltipShow = ref(false) // tooltip 显示隐藏标识
+const tooltipTimer = ref() // tooltip 延迟显示隐藏的定时器标识符
+const scrollTarget = ref(null) // 最近的可滚动父元素
+const top = ref(0) // 提示框 top 定位
+const left = ref(0) // 提示框 left 定位
+const tooltipPlace = ref('top') // 文字提示位置
+const contentRef = ref() // 声明一个同名的模板引用
+const contentWidth = ref(0) // 展示内容宽度
+const contentHeight = ref(0) // 展示内容高度
+const tooltipRef = ref() // tooltip 模板引用
+const tooltipCardRef = ref() // tooltip-card 模板引用
+const tooltipCardWidth = ref(0) // 文字提示内容 tooltip-card 宽度
+const tooltipCardHeight = ref(0) // 文字提示内容 tooltip-card 高度
+const viewportWidth = ref(document.documentElement.clientWidth) // 视口宽度(不包括滚动条)
+const viewportHeight = ref(document.documentElement.clientHeight) // 视口高度(不包括滚动条)
+const emits = defineEmits(['update:show', 'openChange', 'animationend'])
+const slotsExist = useSlotsExist(['tooltip'])
+const tooltipMaxWidth = computed(() => {
+  if (typeof props.maxWidth === 'number') {
+    return `${props.maxWidth}px`
+  }
+  return props.maxWidth
+})
+const showTooltip = computed(() => {
+  return slotsExist.tooltip || props.tooltip
+})
+const tooltipPlacement = computed(() => {
+  switch (tooltipPlace.value) {
+    case 'top':
+      return {
+        transformOrigin: `50% ${top.value}px`,
+        top: `${-top.value}px`,
+        left: `${-left.value}px`
+      }
+    case 'bottom':
+      return {
+        transformOrigin: `50% ${props.arrow ? -4 : -6}px`,
+        bottom: `${-top.value}px`,
+        left: `${-left.value}px`
+      }
+    case 'left':
+      return {
+        transformOrigin: `${left.value}px 50%`,
+        top: `${-top.value}px`,
+        left: `${-left.value}px`
+      }
+    case 'right':
+      return {
+        transformOrigin: `${props.arrow ? -4 : -6}px 50%`,
+        top: `${-top.value}px`,
+        right: `${-left.value}px`
+      }
+    default:
+      return {
+        transformOrigin: `50% ${top.value}px`,
+        top: `${-top.value}px`,
+        left: `${-left.value}px`
+      }
+  }
+})
+watch(
+  () => [props.placement, props.arrow, props.flip],
+  () => {
+    updatePosition()
+  },
+  {
+    deep: true
+  }
+)
+watch(
+  () => props.show,
+  (to) => {
+    if (to && !tooltipShow.value) {
+      onShow()
+    }
+    if (!to && tooltipShow.value) {
+      onHide()
+    }
+  },
+  {
+    immediate: true
+  }
+)
+onMounted(() => {
+  observeScroll()
+})
+onBeforeUnmount(() => {
+  cleanup()
+})
+const mutationObserver = useMutationObserver(
+  scrollTarget,
+  () => {
+    updatePosition()
+  },
+  { subtree: true, childList: true, attributes: true, characterData: true }
+)
+useEventListener(window, 'resize', getViewportSize)
+// 监听 tooltip-card 和 content 的尺寸变化,更新文字提示位置
+useResizeObserver([tooltipCardRef, contentRef], (entries) => {
+  // 排除 tooltip-card 显示过渡动画时的尺寸变化
+  if (entries.length === 1 && entries[0].target.className === 'tooltip-card') {
+    const { blockSize, inlineSize } = entries[0].borderBoxSize[0]
+    if (blockSize === tooltipCardHeight.value && inlineSize === tooltipCardWidth.value) {
+      return
+    }
+  }
+  updatePosition()
+})
+function getViewportSize() {
+  viewportWidth.value = document.documentElement.clientWidth
+  viewportHeight.value = document.documentElement.clientHeight
+  observeScroll() // 窗口尺寸变化时,重新查询并监听最近可滚动父元素
+  updatePosition()
+}
+// 查询并监听最近可滚动父元素
+function observeScroll() {
+  cleanup()
+  scrollTarget.value = getScrollParent(contentRef.value?.parentElement ?? null)
+  scrollTarget.value && scrollTarget.value.addEventListener('scroll', updatePosition)
+  if (scrollTarget.value === document.documentElement) {
+    mutationObserver.start()
+  }
+}
+function cleanup() {
+  scrollTarget.value && scrollTarget.value.removeEventListener('scroll', updatePosition)
+  scrollTarget.value = null
+  mutationObserver.stop()
+}
+// 查询最近的可滚动父元素
+function getScrollParent(el) {
+  const isScrollable = (el) => {
+    const style = window.getComputedStyle(el)
+    if (
+      (el.scrollWidth > el.clientWidth && ['scroll', 'auto'].includes(style.overflowX)) ||
+      (el.scrollHeight > el.clientHeight && ['scroll', 'auto'].includes(style.overflowY)) ||
+      ((el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) && el === document.documentElement)
+    ) {
+      return true
+    }
+    return false
+  }
+  if (el) {
+    return isScrollable(el) ? el : getScrollParent(el.parentElement ?? null)
+  }
+  return null
+}
+// 更新文字提示位置
+function updatePosition() {
+  tooltipShow.value && getPosition()
+}
+
+// 计算文字提示位置
+async function getPosition() {
+  await nextTick()
+  contentWidth.value = contentRef.value.offsetWidth
+  contentHeight.value = contentRef.value.offsetHeight
+  tooltipCardWidth.value = tooltipCardRef.value.offsetWidth
+  tooltipCardHeight.value = tooltipCardRef.value.offsetHeight
+  // debugger
+  // offsetTop
+  if (props.flip) {
+    tooltipPlace.value = getPlacement()
+  }
+  if (['top', 'bottom'].includes(tooltipPlace.value)) {
+    top.value = tooltipCardHeight.value + (props.arrow ? 4 + 12 : 6)
+    left.value = ((tooltipCardWidth.value - contentWidth.value) / 2) + contentWidth.value.offsetLeft
+  } else {
+    top.value = (tooltipCardHeight.value - contentHeight.value) / 2
+    left.value = tooltipCardWidth.value + (props.arrow ? 4 + 12 : 6)
+  }
+}
+// 获取可滚动父元素或视口的矩形信息
+function getShelterRect() {
+  if (scrollTarget.value && scrollTarget.value !== document.documentElement) {
+    return scrollTarget.value.getBoundingClientRect()
+  }
+  return {
+    top: 0,
+    left: 0,
+    bottom: viewportHeight.value,
+    right: viewportWidth.value
+  }
+}
+// 文字提示被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置
+function getPlacement() {
+  const { top, bottom, left, right } = contentRef.value.getBoundingClientRect() // 内容元素各边缘相对于浏览器视口的位置(不包括滚动条)
+  const { top: targetTop, bottom: targetBottom, left: targetLeft, right: targetRight } = getShelterRect() // 滚动元素或视口各边缘相对于浏览器视口的位置(不包括滚动条)
+  const topDistance = top - targetTop - (props.arrow ? 12 : 0) // 内容元素上边缘距离滚动元素上边缘的距离
+  const bottomDistance = targetBottom - bottom - (props.arrow ? 12 : 0) // 内容元素下边缘距离动元素下边缘的距离
+  const leftDistance = left - targetLeft - (props.arrow ? 12 : 0) // 内容元素左边缘距离滚动元素左边缘的距离
+  const rightDistance = targetRight - right - (props.arrow ? 12 : 0) // 内容元素右边缘距离滚动元素右边缘的距离
+  const horizontalDistance = (tooltipCardWidth.value - contentWidth.value) / 2 // 水平方向容纳文字提示需要的最小宽度
+  const verticalDistance = (tooltipCardHeight.value - contentHeight.value) / 2 // 垂直方向容纳文字提示需要的最小高度
+  return findPlace(props.placement, [])
+  // 查询满足条件的 place,如果没有,则返回默认值
+  function findPlace(place, disabledPlaces) {
+    if (place === 'top') {
+      if (!disabledPlaces.includes('top')) {
+        if (topDistance < tooltipCardHeight.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
+          return findPlace('bottom', [...disabledPlaces, 'top'])
+        } else {
+          if (leftDistance >= horizontalDistance && rightDistance >= horizontalDistance) {
+            return 'top'
+          } else {
+            if (disabledPlaces.length !== 3) {
+              if (leftDistance >= horizontalDistance) {
+                return findPlace('left', ['top', 'bottom', 'right'])
+              } else if (rightDistance >= horizontalDistance) {
+                return findPlace('right', ['top', 'bottom', 'left'])
+              }
+            }
+          }
+        }
+      } else {
+        if (!disabledPlaces.includes('bottom')) {
+          return findPlace('bottom', disabledPlaces)
+        } else if (!disabledPlaces.includes('left')) {
+          return findPlace('left', disabledPlaces)
+        } else {
+          return findPlace('right', disabledPlaces)
+        }
+      }
+    } else if (place === 'bottom') {
+      if (!disabledPlaces.includes('bottom')) {
+        if (bottomDistance < tooltipCardHeight.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
+          return findPlace('top', [...disabledPlaces, 'bottom'])
+        } else {
+          if (leftDistance >= horizontalDistance && rightDistance >= horizontalDistance) {
+            return 'bottom'
+          } else {
+            if (disabledPlaces.length !== 3) {
+              if (leftDistance >= horizontalDistance) {
+                return findPlace('left', ['top', 'bottom', 'right'])
+              } else if (rightDistance >= horizontalDistance) {
+                return findPlace('right', ['top', 'bottom', 'left'])
+              }
+            }
+          }
+        }
+      } else {
+        if (!disabledPlaces.includes('top')) {
+          return findPlace('top', disabledPlaces)
+        } else if (!disabledPlaces.includes('left')) {
+          return findPlace('left', disabledPlaces)
+        } else {
+          return findPlace('right', disabledPlaces)
+        }
+      }
+    } else if (place === 'left') {
+      if (!disabledPlaces.includes('left')) {
+        if (leftDistance < tooltipCardWidth.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
+          return findPlace('right', [...disabledPlaces, 'left'])
+        } else {
+          if (topDistance >= verticalDistance && bottomDistance >= verticalDistance) {
+            return 'left'
+          } else {
+            if (disabledPlaces.length !== 3) {
+              if (topDistance >= verticalDistance) {
+                return findPlace('top', ['left', 'right', 'bottom'])
+              } else if (bottomDistance >= verticalDistance) {
+                return findPlace('bottom', ['left', 'right', 'top'])
+              }
+            }
+          }
+        }
+      } else {
+        if (!disabledPlaces.includes('right')) {
+          return findPlace('right', disabledPlaces)
+        } else if (!disabledPlaces.includes('top')) {
+          return findPlace('top', disabledPlaces)
+        } else {
+          return findPlace('bottom', disabledPlaces)
+        }
+      }
+    } else if (place === 'right') {
+      if (!disabledPlaces.includes('right')) {
+        if (rightDistance < tooltipCardWidth.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
+          return findPlace('left', [...disabledPlaces, 'right'])
+        } else {
+          if (topDistance >= verticalDistance && bottomDistance >= verticalDistance) {
+            return 'right'
+          } else {
+            if (disabledPlaces.length !== 3) {
+              if (topDistance >= verticalDistance) {
+                return findPlace('top', ['left', 'right', 'bottom'])
+              } else if (bottomDistance >= verticalDistance) {
+                return findPlace('bottom', ['left', 'right', 'top'])
+              }
+            }
+          }
+        }
+      } else {
+        if (!disabledPlaces.includes('left')) {
+          return findPlace('left', disabledPlaces)
+        } else if (!disabledPlaces.includes('top')) {
+          return findPlace('top', disabledPlaces)
+        } else {
+          return findPlace('bottom', disabledPlaces)
+        }
+      }
+    }
+    return props.placement
+  }
+}
+function onShow() {
+  tooltipTimer.value && cancelRaf(tooltipTimer.value)
+  if (!tooltipShow.value) {
+    tooltipTimer.value = rafTimeout(() => {
+      tooltipShow.value = true
+      getPosition()
+      emits('update:show', true)
+      emits('openChange', true)
+      if (showTooltip.value && props.trigger === 'click') {
+        document.addEventListener('click', handleClick)
+      }
+    }, props.showDelay)
+  }
+}
+function onHide() {
+  tooltipTimer.value && cancelRaf(tooltipTimer.value)
+  if (tooltipShow.value) {
+    tooltipTimer.value = rafTimeout(() => {
+      tooltipShow.value = false
+      emits('update:show', false)
+      emits('openChange', false)
+      if (showTooltip.value && props.trigger === 'click') {
+        document.removeEventListener('click', handleClick)
+      }
+    }, props.hideDelay)
+  }
+}
+function toggleVisible() {
+  if (!tooltipShow.value) {
+    onShow()
+  } else {
+    onHide()
+  }
+}
+function handleClick(e) {
+  if (!tooltipRef.value.contains(e.target)) {
+    onHide()
+  }
+}
+function onEnterWrap() {
+  if (showTooltip.value && props.trigger === 'hover' && !props.showControl) {
+    onShow()
+  }
+}
+function onLeaveWrap() {
+  if (showTooltip.value && props.trigger === 'hover' && !props.showControl) {
+    onHide()
+  }
+}
+function onAnimationEnd() {
+  emits('animationend', tooltipShow.value)
+}
+function onEnterTooltip() {
+  if (props.trigger === 'hover' && !props.showControl) {
+    onShow()
+  }
+}
+function onLeaveTooltip() {
+  if (props.trigger === 'hover' && !props.showControl) {
+    onHide()
+  }
+}
+defineExpose({
+  show: onShow,
+  hide: onHide,
+  observeScroll
+})
+</script>
+<template>
+  <div
+    class="m-tooltip-wrap"
+    :style="`--tooltip-max-width: ${tooltipMaxWidth}; --tooltip-background-color: ${props.bgColor}; --transition-duration: ${props.transitionDuration}ms;`"
+    @mouseenter="onEnterWrap"
+    @mouseleave="onLeaveWrap"
+  >
+    <Transition
+      name="zoom"
+      enter-from-class="zoom-enter"
+      enter-active-class="zoom-enter"
+      enter-to-class="zoom-enter zoom-enter-active"
+      leave-from-class="zoom-leave"
+      leave-active-class="zoom-leave zoom-leave-active"
+      leave-to-class="zoom-leave zoom-leave-active"
+      @animationend="onAnimationEnd"
+    >
+      <div
+        v-show="showTooltip && tooltipShow"
+        ref="tooltipRef"
+        class="m-tooltip-card"
+        :class="{ [`tooltip-${tooltipPlace}-padding`]: props.arrow }"
+        :style="tooltipPlacement"
+        @mouseenter="onEnterTooltip"
+        @mouseleave="onLeaveTooltip"
+        @keydown.esc="props.trigger === 'click' && props.keyboard && tooltipShow ? onHide() : () => false"
+      >
+        <div ref="tooltipCardRef" class="tooltip-card" :class="props.tooltipClass" :style="props.tooltipStyle">
+          <slot name="tooltip">{{ props.tooltip }}</slot>
+        </div>
+        <div v-if="props.arrow" class="tooltip-arrow" :class="`arrow-${tooltipPlace || 'top'}`"></div>
+      </div>
+    </Transition>
+    <span
+      ref="contentRef"
+      class="tooltip-content"
+      :class="props.contentClass"
+      :style="props.contentStyle"
+      @click="showTooltip && props.trigger === 'click' && !tooltipShow ? onShow() : () => false"
+      @keydown.enter="showTooltip && props.trigger === 'click' && props.keyboard ? toggleVisible() : () => false"
+      @keydown.esc="showTooltip && props.trigger === 'click' && props.keyboard && tooltipShow ? onHide() : () => false"
+    >
+      <slot>{{ props.content }}</slot>
+    </span>
+  </div>
+</template>
+<style lang="scss" scoped>
+.zoom-enter {
+  transform: none;
+  opacity: 0;
+  animation-duration: var(--transition-duration);
+  animation-fill-mode: both;
+  animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
+  animation-play-state: paused;
+}
+.zoom-enter-active {
+  animation-name: zoomIn;
+  animation-play-state: running;
+  @keyframes zoomIn {
+    0% {
+      transform: scale(0.8);
+      opacity: 0;
+    }
+    100% {
+      transform: scale(1);
+      opacity: 1;
+    }
+  }
+}
+.zoom-leave {
+  animation-duration: var(--transition-duration);
+  animation-fill-mode: both;
+  animation-play-state: paused;
+  animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86);
+}
+.zoom-leave-active {
+  animation-name: zoomOut;
+  animation-play-state: running;
+  pointer-events: none;
+  @keyframes zoomOut {
+    0% {
+      transform: scale(1);
+      opacity: 1;
+    }
+    100% {
+      transform: scale(0.8);
+      opacity: 0;
+    }
+  }
+}
+.m-tooltip-wrap {
+  position: relative;
+  display: inline-block;
+  .m-tooltip-card {
+    position: absolute;
+    z-index: 999;
+    width: max-content;
+    outline: none;
+    .tooltip-card {
+      min-width: 32px;
+      max-width: var(--tooltip-max-width);
+      min-height: 32px;
+      padding: 6px 8px;
+      font-size: 14px;
+      color: #fff;
+      line-height: 1.5714285714285714;
+      text-align: justify;
+      text-decoration: none;
+      word-break: break-all;
+      background-color: var(--tooltip-background-color);
+      border-radius: 6px;
+      box-shadow:
+        0 6px 16px 0 rgba(0, 0, 0, 0.08),
+        0 3px 6px -4px rgba(0, 0, 0, 0.12),
+        0 9px 28px 8px rgba(0, 0, 0, 0.05);
+      :deep(svg) {
+        fill: currentColor;
+      }
+    }
+    .tooltip-arrow {
+      position: absolute;
+      z-index: 9;
+      display: block;
+      pointer-events: none;
+      width: 16px;
+      height: 16px;
+      overflow: hidden;
+      &::before {
+        position: absolute;
+        width: 16px;
+        height: 8px;
+        background-color: var(--tooltip-background-color);
+        clip-path: path(
+          'M 0 8 A 4 4 0 0 0 2.82842712474619 6.82842712474619 L 6.585786437626905 3.0710678118654755 A 2 2 0 0 1 9.414213562373096 3.0710678118654755 L 13.17157287525381 6.82842712474619 A 4 4 0 0 0 16 8 Z'
+        );
+        content: '';
+      }
+      &::after {
+        position: absolute;
+        width: 8.970562748477143px;
+        height: 8.970562748477143px;
+        margin: auto;
+        border-radius: 0 0 2px 0;
+        transform: translateY(50%) rotate(-135deg);
+        box-shadow: 3px 3px 7px rgba(0, 0, 0, 0.1);
+        z-index: 0;
+        background: transparent;
+        content: '';
+      }
+    }
+    .arrow-top {
+      left: 50%;
+      bottom: 12px;
+      transform: translateX(-50%) translateY(100%) rotate(180deg);
+      &::before {
+        bottom: 0;
+        left: 0;
+      }
+      &::after {
+        bottom: 0;
+        left: 0;
+        right: 0;
+      }
+    }
+    .arrow-bottom {
+      left: 50%;
+      top: 12px;
+      transform: translateX(-50%) translateY(-100%) rotate(0deg);
+      &::before {
+        bottom: 0;
+        left: 0;
+      }
+      &::after {
+        bottom: 0;
+        left: 0;
+        right: 0;
+      }
+    }
+    .arrow-left {
+      top: 50%;
+      right: 12px;
+      transform: translateX(100%) translateY(-50%) rotate(90deg);
+      &::before {
+        bottom: 0;
+        left: 0;
+      }
+      &::after {
+        bottom: 0;
+        left: 0;
+        right: 0;
+      }
+    }
+    .arrow-right {
+      top: 50%;
+      left: 12px;
+      transform: translateX(-100%) translateY(-50%) rotate(-90deg);
+      &::before {
+        bottom: 0;
+        left: 0;
+      }
+      &::after {
+        bottom: 0;
+        left: 0;
+        right: 0;
+      }
+    }
+  }
+  .tooltip-top-padding {
+    padding-bottom: 12px;
+  }
+  .tooltip-bottom-padding {
+    padding-top: 12px;
+  }
+  .tooltip-left-padding {
+    padding-right: 12px;
+  }
+  .tooltip-right-padding {
+    padding-left: 12px;
+  }
+  .tooltip-content {
+    display: inline-block;
+  }
+}
+</style>

+ 249 - 0
src/utils/ellipsis.js

@@ -0,0 +1,249 @@
+import {
+  ref,
+  getCurrentInstance,
+  onMounted,
+  toValue,
+  computed,
+  watch,
+  onBeforeUnmount,
+  onUnmounted,
+  useSlots,
+  reactive,
+  Text,
+  Comment
+} from 'vue'
+
+export function useMounted() {
+  const isMounted = ref(false)
+  // 获取当前组件的实例
+  const instance = getCurrentInstance()
+  if (instance) {
+    onMounted(() => {
+      isMounted.value = true
+    }, instance)
+  }
+  return isMounted
+}
+
+export function useSupported(callback) {
+  const isMounted = useMounted()
+  return computed(() => {
+    // to trigger the ref
+    isMounted.value
+    return Boolean(callback())
+  })
+}
+
+export function useSlotsExist(slotsName = 'default') {
+  const slots = useSlots() // 获取当前组件的所有插槽
+  // 检查特定名称的插槽是否存在且不为空
+  const checkSlotsExist = (slotName) => {
+    const slotsContent = slots[slotName]?.()
+    const checkExist = (slotContent) => {
+      if (slotContent.type === Comment) {
+        return false
+      }
+      if (Array.isArray(slotContent.children) && !slotContent.children.length) {
+        return false
+      }
+      if (slotContent.type !== Text) {
+        return true
+      }
+      if (typeof slotContent.children === 'string') {
+        return slotContent.children.trim() !== ''
+      }
+    }
+    if (slotsContent && slotsContent?.length) {
+      const result = slotsContent.some((slotContent) => {
+        return checkExist(slotContent)
+      })
+      return result
+    }
+    return false
+  }
+  if (Array.isArray(slotsName)) {
+    const slotsExist = reactive({})
+    slotsName.forEach((slotName) => {
+      const exist = computed(() => checkSlotsExist(slotName))
+      slotsExist[slotName] = exist // 将一个 ref 赋值给一个 reactive 属性时,该 ref 会自动解包
+    })
+    return slotsExist
+  } else {
+    return computed(() => checkSlotsExist(slotsName))
+  }
+}
+
+export function useMutationObserver(
+  target,
+  callback,
+  options = {}
+) {
+  const isSupported = useSupported(() => window && 'MutationObserver' in window)
+  const stopObservation = ref(false)
+  let observer
+  const targets = computed(() => {
+    const targetsValue = toValue(target)
+    if (targetsValue) {
+      if (Array.isArray(targetsValue)) {
+        return targetsValue.map((el) => toValue(el)).filter((el) => el)
+      } else {
+        return [targetsValue]
+      }
+    }
+    return []
+  })
+  // 定义清理函数,用于断开 MutationObserver 的连接
+  const cleanup = () => {
+    if (observer) {
+      observer.disconnect()
+      observer = undefined
+    }
+  }
+  // 初始化 MutationObserver,开始观察目标元素
+  const observeElements = () => {
+    if (isSupported.value && targets.value.length && !stopObservation.value) {
+      observer = new MutationObserver(callback)
+      if (observer?.observe) {
+        targets.value.forEach((element) => observer.observe(element, options))
+      }
+    }
+  }
+  // 监听 targets 的变化,当 targets 变化时,重新建立 MutationObserver 观察
+  watch(
+    () => targets.value,
+    () => {
+      cleanup()
+      observeElements()
+    },
+    {
+      immediate: true, // 立即触发回调,以便初始状态也被观察
+      flush: 'post'
+    }
+  )
+  const stop = () => {
+    stopObservation.value = true
+    cleanup()
+  }
+  const start = () => {
+    stopObservation.value = false
+    observeElements()
+  }
+  // 在组件卸载前清理 MutationObserver
+  onBeforeUnmount(() => cleanup())
+  return {
+    stop,
+    start
+  }
+}
+
+export function useEventListener(target, event, callback) {
+  // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
+  onMounted(() => target.addEventListener(event, callback))
+  onUnmounted(() => target.removeEventListener(event, callback))
+}
+
+export function useResizeObserver(
+  target,
+  callback,
+  options = {}
+) {
+  const isSupported = useSupported(() => window && 'ResizeObserver' in window)
+  let observer
+  const stopObservation = ref(false)
+  const targets = computed(() => {
+    const targetsValue = toValue(target)
+    if (targetsValue) {
+      if (Array.isArray(targetsValue)) {
+        return targetsValue.map((el) => toValue(el)).filter((el) => el)
+      } else {
+        return [targetsValue]
+      }
+    }
+    return []
+  })
+  // 定义清理函数,用于断开 ResizeObserver 的连接
+  const cleanup = () => {
+    if (observer) {
+      observer.disconnect()
+      observer = undefined
+    }
+  }
+  // 初始化 ResizeObserver,开始观察目标元素
+  const observeElements = () => {
+    if (isSupported.value && targets.value.length && !stopObservation.value) {
+      observer = new ResizeObserver(callback)
+      if (observer?.observe) {
+        targets.value.forEach((element) => observer.observe(element, options))
+      }
+    }
+  }
+  // 监听 targets 的变化,当 targets 变化时,重新建立 ResizeObserver 观察
+  watch(
+    () => targets.value,
+    () => {
+      cleanup()
+      observeElements()
+    },
+    {
+      immediate: true, // 立即触发回调,以便初始状态也被观察
+      flush: 'post'
+    }
+  )
+  const stop = () => {
+    stopObservation.value = true
+    cleanup()
+  }
+  const start = () => {
+    stopObservation.value = false
+    observeElements()
+  }
+  // 在组件卸载前清理 ResizeObserver
+  onBeforeUnmount(() => cleanup())
+  return {
+    stop,
+    start
+  }
+}
+
+export function rafTimeout(fn, delay, interval = false) {
+  let start = null // 记录动画开始的时间戳
+  function timeElapse(timestamp) {
+    // 定义动画帧回调函数
+    /*
+      timestamp参数:与 performance.now() 的返回值相同,它表示 requestAnimationFrame() 开始去执行回调函数的时刻
+    */
+    if (!start) {
+      // 如果还没有开始时间,则以当前时间为开始时间
+      start = timestamp
+    }
+    const elapsed = timestamp - start
+    if (elapsed >= delay) {
+      try {
+        fn() // 执行目标函数
+      } catch (error) {
+        console.error('Error executing rafTimeout function:', error)
+      }
+      if (interval) {
+        // 如果需要间隔执行,则重置开始时间并继续安排下一次动画帧
+        start = timestamp
+        raf.id = requestAnimationFrame(timeElapse)
+      }
+    } else {
+      raf.id = requestAnimationFrame(timeElapse)
+    }
+  }
+
+  // 创建一个对象用于存储动画帧的 ID,并初始化动画帧
+  const raf = {
+    id: requestAnimationFrame(timeElapse)
+  }
+  return raf
+}
+
+export function cancelRaf(raf = { id }) {
+  if (raf && raf.id && typeof raf.id === 'number') {
+    cancelAnimationFrame(raf.id)
+  } else {
+    console.warn('cancelRaf received an invalid id:', raf)
+  }
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio