This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
import { computed, defineComponent, getCurrentInstance, onMounted, reactive, ref } from 'vue'
import { CLICK_EVENT, PREFIX } from '../_constants'
import { useRect, useSelectorQuery } from '../_hooks'
import { getRandomId } from '../_utils'
import type { EllipsisedValue } from './ellipsis'
import { ellipsisEmits, ellipsisProps } from './ellipsis'
const props = defineProps(ellipsisProps)
const emit = defineEmits(ellipsisEmits)
const instance = getCurrentInstance() as ComponentInternalInstance
const { query } = useSelectorQuery(instance)
const refRandomId = getRandomId()
const rootId = `root${refRandomId}`
const symbolContainId = `symbolContain${refRandomId}`
const rootContainId = `rootContain${refRandomId}`
const contantCopy = ref(props.content)
let maxHeight = 0 // 超出的最大高度
let lineHeight = 0 // 当行的最大高度
let originHeight = 0 // 原始高度
const ellipsis = reactive<EllipsisedValue>({})
const widthRef = ref('auto')
const state = reactive({
exceeded: false, // 是否超出
expanded: false, // 是否折叠
})
let widthBase = [14, 10, 7, 8.4, 10] // 中、英(大)、英(小)、数字、其他字符的基础宽度
let symbolTextWidth = widthBase[0] * 0.7921
const chineseReg = /^[\u4E00-\u9FA5]+$/ // 汉字
const digitReg = /^\d+$/ // 数字
const letterUpperReg = /^[A-Z]+$/ // 字母
const letterLowerReg = /^[a-z]+$/ // 字母
const classes = computed(() => {
const prefixCls = componentName
return {
ell: true,
[prefixCls]: true,
}
})
const symbolText = computed(() => {
if (props.direction === 'end' || props.direction === 'middle')
return `${props.symbol}${props.expandText}`
return `${props.symbol}${props.expandText}${props.symbol}`
})
onMounted(() => {
setTimeout(() => {
getSymbolInfo()
getReference()
}, 100)
})
// 获取省略号宽度
async function getSymbolInfo() {
const refe = await useRect(symbolContainId, instance)
symbolTextWidth = refe.width ? Math.ceil(refe.width) : Math.ceil(widthBase[0] * 0.7921)
}
async function getReference() {
query.select(rootId)
&& query
.select(`#${rootId}`)
.fields(
{
computedStyle: ['width', 'height', 'lineHeight', 'paddingTop', 'paddingBottom', 'fontSize'],
},
(res: any) => {
lineHeight = pxToNumber(res.lineHeight === 'normal' ? props.lineHeight : res.lineHeight)
maxHeight = Math.floor(
lineHeight * (Number(props.rows) + 0.5) + pxToNumber(res.paddingTop) + pxToNumber(res.paddingBottom),
)
originHeight = pxToNumber(res.height)
widthRef.value = res.width
// 设置基础字符
const bsize = pxToNumber(res.fontSize)
widthBase = [bsize, bsize * 0.72, bsize * 0.53, bsize * 0.4, bsize * 0.75]
calcEllipse()
},
)
.exec()
}
// 计算省略号的位置
async function calcEllipse() {
const refe = await useRect(rootContainId, instance)
if (refe.height! <= maxHeight) {
state.exceeded = false
}
else {
const rowNum = Math.floor(props.content.length / (originHeight / lineHeight - 1)) // 每行的字数
if (props.direction === 'middle') {
const end = props.content.length
ellipsis.leading = tailorContent(0, rowNum * (Number(props.rows) + 0.5), 'end')
ellipsis.tailing = tailorContent(props.content.length - rowNum * (Number(props.rows) + 0.5), end, 'start')
}
else if (props.direction === 'end') {
const end = rowNum * (Number(props.rows) + 0.5)
ellipsis.leading = tailorContent(0, end)
}
else {
const start = props.content.length - rowNum * (Number(props.rows) + 0.5) - 5
ellipsis.tailing = tailorContent(start, props.content.length)
}
// 进行兜底判断,是否符合要求
assignContent()
setTimeout(() => {
verifyEllipsis()
}, 100)
}
}
// 验证省略号
async function verifyEllipsis() {
const refe = await useRect(rootContainId, instance)
if (refe && refe.height && refe.height > maxHeight) {
if (props.direction === 'end')
ellipsis.leading = ellipsis.leading?.slice(0, ellipsis.leading.length - 1)
else
ellipsis.tailing = ellipsis.tailing?.slice(1, ellipsis.tailing.length)
assignContent()
setTimeout(() => {
verifyEllipsis()
}, 100)
}
}
function assignContent() {
contantCopy.value = `${ellipsis.leading || ''}${ellipsis.leading ? props.symbol : ''}${props.expandText || ''}${ellipsis.tailing ? props.symbol : ''}${ellipsis.tailing || ''}`
}
// 计算省略号
function tailorContent(left: number, right: number, type = '') {
const threeDotWidth = symbolTextWidth
const direc = props.direction === 'middle' && type ? type : props.direction
state.exceeded = true
let widthPart = -1
const start = left
const end = right
let cutoff = 0
let marking = 0
if (direc === 'end') {
marking = start
cutoff = end
}
else {
marking = end
cutoff = start
}
const contentWidth = pxToNumber(widthRef.value) * Number(props.rows) - threeDotWidth
const contentPartWidth = props.direction === 'middle' ? contentWidth / 2 : contentWidth
while (widthPart < contentPartWidth) {
const zi = props.content[marking]
if (chineseReg.test(zi))
widthPart = Number(widthPart + widthBase[0])
else if (letterUpperReg.test(zi))
widthPart = Number(widthPart + widthBase[1])
else if (letterLowerReg.test(zi))
widthPart = Number(widthPart + widthBase[2])
else if (digitReg.test(zi))
widthPart = Number(widthPart + widthBase[3])
else
widthPart = Number(widthPart + widthBase[4])
cutoff = marking
direc === 'end' ? marking++ : marking--
}
if (direc === 'end')
return props.content.slice(0, cutoff)
else
return props.content.slice(cutoff, end)
}
function pxToNumber(value: string | null) {
if (!value)
return 0
const match = value.match(/^\d*(\.\d*)?/)
return match ? Number(match[0]) : 0
}
// 展开收起
function clickHandle(type: number) {
if (type === 1) {
state.expanded = true
emit('change', 'expand')
}
else {
state.expanded = false
emit('change', 'collapse')
}
}
// 文本点击
function handleClick() {
emit(CLICK_EVENT)
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-ellipsis`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="customClass" :style="customStyle">
<view :id="rootId" :class="classes" @click="handleClick">
<view v-if="!state.exceeded" class="nut-ellipsis__wordbreak">
{{ content }}
</view>
<view v-if="state.exceeded && !state.expanded" class="nut-ellipsis__wordbreak">
{{ ellipsis.leading }}{{ ellipsis.leading && symbol
}}
<view v-if="expandText" class="nut-ellipsis__text" @click.stop="clickHandle(1)">
{{ expandText }}
</view>{{ ellipsis.tailing && symbol }}{{ ellipsis.tailing }}
</view>
<view v-if="state.exceeded && state.expanded">
{{ content }}
<view v-if="expandText" class="nut-ellipsis__text" @click.stop="clickHandle(2)">
{{ collapseText }}
</view>
</view>
</view>
<view :id="rootContainId" class="nut-ellipsis__copy" :style="{ width: widthRef }">
<view>{{ contantCopy }}</view>
</view>
<!-- 省略号 symbol -->
<view :id="symbolContainId" class="nut-ellipsis__copy" style="display: inline">
{{
symbolText
}}
</view>
<!-- 数字 9 英文 W -->
</view>
</template>
<style lang="scss">
@import './index';
</style>