-
Notifications
You must be signed in to change notification settings - Fork 7.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 增加文本省略组件 * refactor: 重构文本省略组件 * feat: 增加远程懒加载下拉树功能
- Loading branch information
Showing
10 changed files
with
454 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { withInstall } from '@/utils'; | ||
import ellipsisText from './src/EllipsisText.vue'; | ||
|
||
export const EllipsisText = withInstall(ellipsisText); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
<script setup lang="ts"> | ||
import { ref, computed, watchEffect, nextTick } from 'vue'; | ||
import type { CSSProperties } from 'vue'; | ||
import Tooltip from './Tooltip.vue'; | ||
interface Props { | ||
maxWidth?: number | string; // 文本最大宽度 | ||
line?: number; // 最大行数 | ||
expand?: boolean; // 是否启用点击文本展开全部 | ||
tooltip?: boolean; // 是否启用文本提示框 | ||
// 以下均为 tooltip 组件属性 | ||
tooltipMaxWidth?: number; // 提示框内容最大宽度,单位px,默认不设置时,提示文本内容自动与展示文本宽度保持一致 | ||
tooltipFontSize?: number; // 提示文本字体大小,单位px,优先级高于 overlayStyle | ||
tooltipColor?: string; // 提示文本字体颜色,优先级高于 overlayStyle | ||
tooltipBackgroundColor?: string; // 提示框背景颜色,优先级高于 overlayStyle | ||
tooltipOverlayStyle?: CSSProperties; // 提示框内容区域样式 | ||
} | ||
const props = withDefaults(defineProps<Props>(), { | ||
maxWidth: '100%', | ||
line: undefined, | ||
expand: false, | ||
tooltip: true, | ||
tooltipMaxWidth: undefined, | ||
tooltipFontSize: 14, | ||
tooltipColor: '#FFF', | ||
tooltipBackgroundColor: 'rgba(0, 0, 0, .85)', | ||
tooltipOverlayStyle: () => ({ padding: '8px 12px', textAlign: 'justify' }), | ||
}); | ||
const textMaxWidth = computed(() => { | ||
if (typeof props.maxWidth === 'number') { | ||
return props.maxWidth + 'px'; | ||
} | ||
return props.maxWidth; | ||
}); | ||
const showTooltip = ref(); | ||
const ellipsis = ref(); | ||
const defaultTooltipMaxWidth = ref(); | ||
watchEffect(() => { | ||
showTooltip.value = props.tooltip; | ||
}); | ||
watchEffect( | ||
() => { | ||
if (props.tooltip) { | ||
if (props.tooltipMaxWidth) { | ||
defaultTooltipMaxWidth.value = props.tooltipMaxWidth; | ||
} else { | ||
defaultTooltipMaxWidth.value = ellipsis.value.offsetWidth + 24; | ||
} | ||
} | ||
}, | ||
{ flush: 'post' }, | ||
); | ||
const emit = defineEmits(['expandChange']); | ||
function onExpand() { | ||
if (ellipsis.value.style['-webkit-line-clamp']) { | ||
if (props.tooltip) { | ||
showTooltip.value = false; | ||
nextTick(() => { | ||
ellipsis.value.style['-webkit-line-clamp'] = ''; | ||
}); | ||
} else { | ||
ellipsis.value.style['-webkit-line-clamp'] = ''; | ||
} | ||
emit('expandChange', true); | ||
} else { | ||
if (props.tooltip) { | ||
showTooltip.value = true; | ||
} | ||
ellipsis.value.style['-webkit-line-clamp'] = props.line; | ||
emit('expandChange', false); | ||
} | ||
} | ||
</script> | ||
<template> | ||
<Tooltip | ||
v-if="showTooltip" | ||
:max-width="defaultTooltipMaxWidth" | ||
:fontSize="tooltipFontSize" | ||
:color="tooltipColor" | ||
:backgroundColor="tooltipBackgroundColor" | ||
:overlayStyle="tooltipOverlayStyle" | ||
> | ||
<template #tooltip> | ||
<slot name="tooltip"> | ||
<slot></slot> | ||
</slot> | ||
</template> | ||
<div | ||
ref="ellipsis" | ||
class="m-ellipsis" | ||
:class="[line ? 'ellipsis-line' : 'not-ellipsis-line', { 'cursor-pointer': expand }]" | ||
:style="`-webkit-line-clamp: ${line}; max-width: ${textMaxWidth};`" | ||
@click="expand ? onExpand() : () => false" | ||
v-bind="$attrs" | ||
> | ||
<slot></slot> | ||
</div> | ||
</Tooltip> | ||
<div | ||
v-else | ||
ref="ellipsis" | ||
class="m-ellipsis" | ||
:class="[line ? 'ellipsis-line' : 'not-ellipsis-line', { 'cursor-pointer': expand }]" | ||
:style="`-webkit-line-clamp: ${line}; max-width: ${textMaxWidth};`" | ||
@click="expand ? onExpand() : () => false" | ||
v-bind="$attrs" | ||
> | ||
<slot></slot> | ||
</div> | ||
</template> | ||
|
||
<style lang="less" scoped> | ||
.m-ellipsis { | ||
overflow: hidden; | ||
cursor: text; | ||
} | ||
.ellipsis-line { | ||
display: -webkit-inline-box; | ||
-webkit-box-orient: vertical; | ||
} | ||
.not-ellipsis-line { | ||
display: inline-block; | ||
text-overflow: ellipsis; | ||
vertical-align: bottom; | ||
white-space: nowrap; | ||
} | ||
.cursor-pointer { | ||
cursor: pointer; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
<script setup lang="ts"> | ||
import { ref } from 'vue'; | ||
import type { CSSProperties } from 'vue'; | ||
import { rafTimeout, cancelRaf } from './_utils'; | ||
interface Props { | ||
maxWidth?: number; // 提示框内容最大宽度,单位px | ||
content?: string; // 展示的文本 string | slot | ||
tooltip?: string; // 提示的文本 string | slot | ||
fontSize?: number; // 提示文本字体大小,单位px,优先级高于 overlayStyle | ||
color?: string; // 提示文本字体颜色,优先级高于 overlayStyle | ||
backgroundColor?: string; // 提示框背景颜色,优先级高于 overlayStyle | ||
overlayStyle?: CSSProperties; // 提示框内容区域样式 | ||
} | ||
withDefaults(defineProps<Props>(), { | ||
maxWidth: 120, | ||
content: '暂无内容', | ||
tooltip: '暂无提示', | ||
fontSize: 14, | ||
color: '#FFF', | ||
backgroundColor: 'rgba(0, 0, 0, .85)', | ||
overlayStyle: () => ({}), | ||
}); | ||
const visible = ref(false); | ||
const hideTimer = ref(); | ||
const top = ref(0); // 提示框top定位 | ||
const left = ref(0); // 提示框left定位 | ||
const contentRef = ref(); // 声明一个同名的模板引用 | ||
const tooltipRef = ref(); // 声明一个同名的模板引用 | ||
function getPosition() { | ||
const contentWidth = contentRef.value && contentRef.value.offsetWidth; // 展示文本宽度 | ||
const tooltipWidth = tooltipRef.value && tooltipRef.value.offsetWidth; // 提示文本宽度 | ||
const tooltipHeight = tooltipRef.value && tooltipRef.value.offsetHeight; // 提示文本高度 | ||
top.value = tooltipHeight + 4; | ||
left.value = (tooltipWidth - contentWidth) / 2; | ||
} | ||
const emit = defineEmits(['openChange']); | ||
function onShow() { | ||
getPosition(); | ||
cancelRaf(hideTimer.value); | ||
visible.value = true; | ||
emit('openChange', visible.value); | ||
} | ||
function onHide(): void { | ||
hideTimer.value = rafTimeout(() => { | ||
visible.value = false; | ||
emit('openChange', visible.value); | ||
}, 100); | ||
} | ||
</script> | ||
<template> | ||
<div class="m-tooltip" @mouseenter="onShow" @mouseleave="onHide"> | ||
<div | ||
ref="tooltipRef" | ||
class="m-tooltip-content" | ||
:class="{ 'show-tip': visible }" | ||
:style="`--tooltip-font-size: ${fontSize}px; --tooltip-color: ${color}; --tooltip-background-color: ${backgroundColor}; max-width: ${maxWidth}px; top: ${-top}px; left: ${-left}px;`" | ||
@mouseenter="onShow" | ||
@mouseleave="onHide" | ||
> | ||
<div class="u-tooltip" :style="overlayStyle"> | ||
<slot name="tooltip">{{ tooltip }}</slot> | ||
</div> | ||
<div class="m-tooltip-arrow"> | ||
<span class="u-tooltip-arrow"></span> | ||
</div> | ||
</div> | ||
<div ref="contentRef"> | ||
<slot>{{ content }}</slot> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<style lang="less" scoped> | ||
.m-tooltip { | ||
display: inline-block; | ||
position: relative; | ||
.m-tooltip-content { | ||
position: absolute; | ||
z-index: 9999; | ||
width: max-content; | ||
padding-bottom: 12px; | ||
transform: scale(0.8); // 缩放变换 | ||
transform-origin: 50% 75%; | ||
transition: | ||
transform 0.25s, | ||
opacity 0.25s; | ||
opacity: 0; | ||
pointer-events: none; | ||
.u-tooltip { | ||
min-width: 32px; | ||
min-height: 32px; | ||
padding: 6px 8px; | ||
border-radius: 6px; | ||
background-color: var(--tooltip-background-color); | ||
box-shadow: | ||
0 6px 16px 0 rgb(0 0 0 / 8%), | ||
0 3px 6px -4px rgb(0 0 0 / 12%), | ||
0 9px 28px 8px rgb(0 0 0 / 5%); | ||
color: var(--tooltip-color); | ||
font-size: var(--tooltip-font-size); | ||
line-height: 1.5714; | ||
text-align: start; | ||
text-decoration: none; | ||
word-wrap: break-word; | ||
} | ||
.m-tooltip-arrow { | ||
content: ''; | ||
display: block; | ||
position: absolute; | ||
z-index: 9; | ||
bottom: 12px; | ||
left: 50%; | ||
width: 16px; | ||
height: 16px; | ||
overflow: hidden; | ||
transform: translateX(-50%) translateY(100%) rotate(180deg); | ||
pointer-events: none; | ||
&::before { | ||
position: absolute; | ||
bottom: 0; | ||
inset-inline-start: 0; | ||
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' | ||
); | ||
} | ||
&::after { | ||
content: ''; | ||
position: absolute; | ||
z-index: 0; | ||
bottom: 0; | ||
width: 8.9706px; | ||
height: 8.9706px; | ||
inset-inline: 0; | ||
margin: auto; | ||
transform: translateY(50%) rotate(-135deg); | ||
border-radius: 0 0 2px; | ||
background: transparent; | ||
box-shadow: 3px 3px 7px rgb(0 0 0 / 10%); | ||
} | ||
} | ||
} | ||
.show-tip { | ||
transform: scale(1); // 缩放变换 | ||
opacity: 1; | ||
pointer-events: auto; | ||
} | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// cancelAnimationFrame | ||
export const cancelAnimationFrame = window.cancelAnimationFrame; | ||
// 使用 requestAnimationFrame 模拟 setTimeout 和 setInterval | ||
export function rafTimeout(fn: Function, delay = 0, interval = false): object { | ||
const requestAnimationFrame = | ||
typeof window !== 'undefined' ? window.requestAnimationFrame : () => {}; | ||
let start: any = null; | ||
function timeElapse(timestamp: number) { | ||
/* | ||
timestamp参数:与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻 | ||
*/ | ||
if (!start) { | ||
start = timestamp; | ||
} | ||
const elapsed = timestamp - start; | ||
if (elapsed >= delay) { | ||
fn(); // 执行目标函数func | ||
if (interval) { | ||
// 使用间歇调用 | ||
start = null; | ||
raf.id = requestAnimationFrame(timeElapse); | ||
} | ||
} else { | ||
raf.id = requestAnimationFrame(timeElapse); | ||
} | ||
} | ||
const raf = { | ||
// 引用类型保存,方便获取 requestAnimationFrame()方法返回的 ID. | ||
id: requestAnimationFrame(timeElapse), | ||
}; | ||
return raf; | ||
} | ||
// 用于取消 rafTimeout 函数 | ||
export function cancelRaf(raf: { id: number }): void { | ||
const cancelAnimationFrame = | ||
typeof window !== 'undefined' ? window.cancelAnimationFrame : () => {}; | ||
if (raf && raf.id) { | ||
cancelAnimationFrame(raf.id); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.