element-whale-ui 0.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # ShimmerUI
2
+
3
+ This template should help get you started developing with Vue 3 in Vite.
4
+
5
+ ## Recommended IDE Setup
6
+
7
+ [VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
8
+
9
+ ## Recommended Browser Setup
10
+
11
+ - Chromium-based browsers (Chrome, Edge, Brave, etc.):
12
+ - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
13
+ - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
14
+ - Firefox:
15
+ - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
16
+ - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
17
+
18
+ ## Type Support for `.vue` Imports in TS
19
+
20
+ TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
21
+
22
+ ## Customize configuration
23
+
24
+ See [Vite Configuration Reference](https://vite.dev/config/).
25
+
26
+ ## Project Setup
27
+
28
+ ```sh
29
+ pnpm install
30
+ ```
31
+
32
+ ### Compile and Hot-Reload for Development
33
+
34
+ ```sh
35
+ pnpm dev
36
+ ```
37
+
38
+ ### Type-Check, Compile and Minify for Production
39
+
40
+ ```sh
41
+ pnpm build
42
+ ```
43
+
44
+ ### Run Unit Tests with [Vitest](https://vitest.dev/)
45
+
46
+ ```sh
47
+ pnpm test:unit
48
+ ```
49
+
50
+ ### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
51
+
52
+ ```sh
53
+ pnpm test:e2e:dev
54
+ ```
55
+
56
+ This runs the end-to-end tests against the Vite development server.
57
+ It is much faster than the production build.
58
+
59
+ But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
60
+
61
+ ```sh
62
+ pnpm build
63
+ pnpm test:e2e
64
+ ```
65
+
66
+ ### Lint with [ESLint](https://eslint.org/)
67
+
68
+ ```sh
69
+ pnpm lint
70
+ ```
@@ -0,0 +1,2 @@
1
+ .counter[data-v-c63c3e32]{font-weight:800}.number-tabular[data-v-c63c3e32]{font-family:Courier New,SF Mono,Consolas,monospace}.number-modern[data-v-c63c3e32]{font-family:Inter,Roboto,Helvetica Neue,Arial,sans-serif}
2
+ /*$vite$:1*/
@@ -0,0 +1,65 @@
1
+ import { computed as e, createElementBlock as t, defineComponent as n, onMounted as r, onUnmounted as i, openBlock as a, ref as o, toDisplayString as s, watch as c } from "vue";
2
+ //#region src/packages/roll-number/src/index.vue?vue&type=script&setup=true&lang.ts
3
+ var l = { class: "counter number-tabular number-modern" }, u = /* @__PURE__ */ ((e, t) => {
4
+ let n = e.__vccOpts || e;
5
+ for (let [e, r] of t) n[e] = r;
6
+ return n;
7
+ })(/* @__PURE__ */ n({
8
+ __name: "index",
9
+ props: {
10
+ value: { default: 0 },
11
+ duration: { default: 1200 },
12
+ easing: { default: "easeOutQuad" },
13
+ thousandsSeparator: {
14
+ type: Boolean,
15
+ default: !1
16
+ }
17
+ },
18
+ setup(n) {
19
+ let u = n, d = o(0), f = null, p = null, m = 0, h = {
20
+ linear: (e) => e,
21
+ easeOutQuad: (e) => e * (2 - e),
22
+ easeOutCubic: (e) => --e * e * e + 1
23
+ };
24
+ function g(e) {
25
+ p ||= e;
26
+ let t = Math.min((e - p) / u.duration, 1), n = h[u.easing], r = n(t);
27
+ d.value = m + (u.value - m) * r, t < 1 ? f = requestAnimationFrame(g) : (d.value = u.value, f = null);
28
+ }
29
+ function _() {
30
+ f && cancelAnimationFrame(f), p = null, m = d.value, f = requestAnimationFrame(g);
31
+ }
32
+ function v(e) {
33
+ let t = e.toString(), n = t.indexOf(".");
34
+ return n === -1 ? 0 : t.length - n - 1;
35
+ }
36
+ function y(e) {
37
+ let t = e.indexOf(".");
38
+ if (t === -1) return e;
39
+ let n = e.length;
40
+ for (; n > t + 1 && e.charAt(n - 1) === "0";) n--;
41
+ return e.slice(0, n);
42
+ }
43
+ let b = e(() => {
44
+ let e = v(u.value), t = y(d.value.toFixed(e));
45
+ if (u.thousandsSeparator) {
46
+ let e = t.split(".");
47
+ return e[0] = e[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","), e.join(".");
48
+ }
49
+ return t;
50
+ });
51
+ return c([
52
+ () => u.value,
53
+ () => u.duration,
54
+ () => u.easing
55
+ ], () => {
56
+ _();
57
+ }), r(() => {
58
+ _();
59
+ }), i(() => {
60
+ f && cancelAnimationFrame(f);
61
+ }), (e, n) => (a(), t("div", l, s(b.value), 1));
62
+ }
63
+ }), [["__scopeId", "data-v-c63c3e32"]]);
64
+ //#endregion
65
+ export { u as RollNumber };
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require(`vue`)):typeof define==`function`&&define.amd?define([`exports`,`vue`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ElementWhaleUI={},e.Vue))})(this,function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n={class:`counter number-tabular number-modern`};e.RollNumber=((e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n})((0,t.defineComponent)({__name:`index`,props:{value:{default:0},duration:{default:1200},easing:{default:`easeOutQuad`},thousandsSeparator:{type:Boolean,default:!1}},setup(e){let r=e,i=(0,t.ref)(0),a=null,o=null,s=0,c={linear:e=>e,easeOutQuad:e=>e*(2-e),easeOutCubic:e=>--e*e*e+1};function l(e){o||=e;let t=Math.min((e-o)/r.duration,1),n=c[r.easing],u=n(t);i.value=s+(r.value-s)*u,t<1?a=requestAnimationFrame(l):(i.value=r.value,a=null)}function u(){a&&cancelAnimationFrame(a),o=null,s=i.value,a=requestAnimationFrame(l)}function d(e){let t=e.toString(),n=t.indexOf(`.`);return n===-1?0:t.length-n-1}function f(e){let t=e.indexOf(`.`);if(t===-1)return e;let n=e.length;for(;n>t+1&&e.charAt(n-1)===`0`;)n--;return e.slice(0,n)}let p=(0,t.computed)(()=>{let e=d(r.value),t=f(i.value.toFixed(e));if(r.thousandsSeparator){let e=t.split(`.`);return e[0]=e[0].replace(/\B(?=(\d{3})+(?!\d))/g,`,`),e.join(`.`)}return t});return(0,t.watch)([()=>r.value,()=>r.duration,()=>r.easing],()=>{u()}),(0,t.onMounted)(()=>{u()}),(0,t.onUnmounted)(()=>{a&&cancelAnimationFrame(a)}),(e,r)=>((0,t.openBlock)(),(0,t.createElementBlock)(`div`,n,(0,t.toDisplayString)(p.value),1))}}),[[`__scopeId`,`data-v-c63c3e32`]])});
Binary file
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "element-whale-ui",
3
+ "type": "module",
4
+ "version": "0.0.0-beta.0",
5
+ "private": false,
6
+ "description": "A Vue 3 UI component library",
7
+ "main": "./element-whale-ui/element-whale-ui.umd.js",
8
+ "module": "./element-whale-ui/element-whale-ui.es.js",
9
+ "types": "./element-whale-ui/packages/index.d.ts",
10
+ "files": [
11
+ "element-whale-ui",
12
+ "src/packages"
13
+ ],
14
+ "engines": {
15
+ "node": "^20.19.0 || >=22.12.0"
16
+ },
17
+ "scripts": {
18
+ "dev": "vite",
19
+ "build": "run-p type-check \"build-only {@}\" --",
20
+ "build:lib": "vite build",
21
+ "preview": "vite preview",
22
+ "test:unit": "vitest",
23
+ "prepare": "husky",
24
+ "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
25
+ "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
26
+ "build-only": "vite build",
27
+ "type-check": "vue-tsc --build",
28
+ "lint": "run-s lint:*",
29
+ "lint:eslint": "eslint . --fix --cache",
30
+ "lint:style": "stylelint \"**/*.{vue,css,scss}\" --fix",
31
+ "format": "prettier --write --experimental-cli src/"
32
+ },
33
+ "dependencies": {
34
+ "@element-plus/icons-vue": "^2.3.2",
35
+ "dompurify": "^3.4.7",
36
+ "element-plus": "^2.14.0",
37
+ "highlight.js": "^11.11.1",
38
+ "marked": "^18.0.4",
39
+ "marked-highlight": "^2.2.4",
40
+ "pinia": "^3.0.4",
41
+ "vue": "^3.5.32",
42
+ "vue-router": "^5.0.4",
43
+ "vuejs-code-block": "^1.0.1"
44
+ },
45
+ "devDependencies": {
46
+ "@antfu/eslint-config": "^9.0.0",
47
+ "@stylelint/postcss-css-in-js": "^0.38.0",
48
+ "@tsconfig/node24": "^24.0.4",
49
+ "@types/jsdom": "^28.0.1",
50
+ "@types/node": "^24.12.2",
51
+ "@vitejs/plugin-vue": "^6.0.6",
52
+ "@vitejs/plugin-vue-jsx": "^5.1.5",
53
+ "@vitest/eslint-plugin": "^1.6.16",
54
+ "@vue/eslint-config-typescript": "^14.7.0",
55
+ "@vue/test-utils": "^2.4.6",
56
+ "@vue/tsconfig": "^0.9.1",
57
+ "cypress": "^15.14.0",
58
+ "eslint": "^10.2.1",
59
+ "eslint-config-prettier": "^10.1.8",
60
+ "eslint-plugin-cypress": "^6.3.1",
61
+ "eslint-plugin-oxlint": "~1.60.0",
62
+ "eslint-plugin-prettier": "^5.5.5",
63
+ "eslint-plugin-vue": "~10.8.0",
64
+ "husky": "^9.1.7",
65
+ "jiti": "^2.6.1",
66
+ "jsdom": "^29.0.2",
67
+ "lint-staged": "^17.0.4",
68
+ "npm-run-all2": "^8.0.4",
69
+ "oxlint": "~1.60.0",
70
+ "pinia-plugin-persistedstate": "^4.7.1",
71
+ "prettier": "3.8.3",
72
+ "sass": "^1.99.0",
73
+ "start-server-and-test": "^2.1.5",
74
+ "stylelint": "^17.11.0",
75
+ "stylelint-config-recommended-vue": "^1.6.1",
76
+ "stylelint-prettier": "^5.0.3",
77
+ "typescript": "~6.0.0",
78
+ "unplugin-auto-import": "^21.0.0",
79
+ "vite": "^8.0.8",
80
+ "vite-plugin-vue-devtools": "^8.1.1",
81
+ "vitepress": "^1.6.4",
82
+ "vitest": "^4.1.4",
83
+ "vue-tsc": "^3.2.6"
84
+ }
85
+ }
@@ -0,0 +1,6 @@
1
+ import DkTooltipCollapse from './src/index.vue'
2
+
3
+ DkTooltipCollapse.install = function (Vue: any) {
4
+ Vue.component('DkTooltipCollapse', DkTooltipCollapse)
5
+ }
6
+ export default DkTooltipCollapse
@@ -0,0 +1,408 @@
1
+ <script setup lang="ts">
2
+ import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
3
+ import { computed, nextTick, onMounted, onUnmounted, ref, useSlots } from 'vue'
4
+
5
+ const props = withDefaults(defineProps<Props>(), {
6
+ text: '',
7
+ width: '',
8
+ placement: 'top',
9
+ tooltipContent: '',
10
+ effect: 'dark',
11
+ disabled: undefined,
12
+ tooltip: true,
13
+ lineClamp: 1,
14
+ lineHeight: '20px',
15
+ tooltipMaxWidth: '90%',
16
+ toggleBg: '',
17
+ toggleHoverBg: '',
18
+ tooltipLineClamp: 0,
19
+ })
20
+ /** 折叠状态,true为收起状态 */
21
+ const isCollapsed = defineModel('isCollapsed', { type: Boolean, default: true })
22
+ /** 组件属性接口 */
23
+ interface Props {
24
+ /** 显示的文本内容 */
25
+ text?: string
26
+ /** 容器最小宽度 */
27
+ width?: string
28
+ /** tooltip弹出位置 */
29
+ placement?:
30
+ | 'top'
31
+ | 'top-start'
32
+ | 'top-end'
33
+ | 'bottom'
34
+ | 'bottom-start'
35
+ | 'bottom-end'
36
+ | 'left'
37
+ | 'left-start'
38
+ | 'left-end'
39
+ | 'right'
40
+ | 'right-start'
41
+ | 'right-end'
42
+ /** tooltip提示内容,不设置则使用text */
43
+ tooltipContent?: string
44
+ /** tooltip最大宽度 */
45
+ tooltipMaxWidth?: string
46
+ /** tooltip主题色 */
47
+ effect?: 'dark' | 'light'
48
+ /** 是否禁用 */
49
+ disabled?: boolean
50
+ /** 是否使用tooltip模式,false则使用折叠模式 */
51
+ tooltip?: boolean
52
+ /** 折叠模式显示的最大行数 */
53
+ lineClamp?: number
54
+ /** 每行的高度,用于计算展开/收起按钮的位置 */
55
+ lineHeight?: string
56
+ /** 展开/收起按钮的背景色,不设置则使用父元素背景色 */
57
+ toggleBg?: string
58
+ /** 展开/收起按钮的悬停背景色,不设置则使用父元素背景色 */
59
+ toggleHoverBg?: string
60
+ /** tooltip模式下显示的最大行数,默认1(单行省略),设为0或不设置则不限制 */
61
+ tooltipLineClamp?: number
62
+ }
63
+ const slots = useSlots()
64
+
65
+ /**
66
+ * tooltip弹窗样式,计算最大宽度
67
+ */
68
+ const tooltipStyle = computed(() => {
69
+ return props.tooltipMaxWidth ? { maxWidth: props.tooltipMaxWidth } : {}
70
+ })
71
+
72
+ /**
73
+ * 获取文本容器的样式
74
+ * 支持多行省略(tooltipLineClamp > 0)
75
+ */
76
+ function getTextStyle() {
77
+ const style: Record<string, string> = {
78
+ minWidth: props.width || '100%',
79
+ lineHeight: props.lineHeight,
80
+ }
81
+
82
+ if (props.tooltipLineClamp > 0) {
83
+ style.display = '-webkit-box'
84
+ style.WebkitLineClamp = String(props.tooltipLineClamp)
85
+ style.WebkitBoxOrient = 'vertical'
86
+ style.overflow = 'hidden'
87
+ style.textOverflow = 'ellipsis'
88
+ }
89
+
90
+ return style
91
+ }
92
+
93
+ /** 文本容器的DOM引用 */
94
+ const containerRef = ref<HTMLElement>()
95
+ /** 文本是否溢出 */
96
+ const isOverflow = ref(false)
97
+
98
+ /**
99
+ * 判断tooltip是否禁用
100
+ * - 如果手动设置了disabled,使用手动设置的值
101
+ * - 如果是非tooltip模式且有content,则启用
102
+ * - 否则根据文本是否溢出来决定
103
+ */
104
+ const isDisabled = computed(() => {
105
+ if (props.tooltipContent || slots.tooltipContent) {
106
+ return false
107
+ }
108
+
109
+ if (props.disabled !== undefined) {
110
+ return props.disabled
111
+ }
112
+
113
+ return !isOverflow.value
114
+ })
115
+
116
+ /**
117
+ * 检测文本是否溢出
118
+ * - 单行模式:比较文本宽度和容器宽度
119
+ * - 多行模式:比较scrollHeight和clientHeight(垂直溢出)
120
+ */
121
+ function checkOverflow() {
122
+ if (!containerRef.value) {
123
+ return
124
+ }
125
+
126
+ // 多行模式:检测垂直溢出
127
+ if (props.lineClamp && props.lineClamp > 1) {
128
+ if (!isCollapsed.value) {
129
+ isOverflow.value = true
130
+ return
131
+ }
132
+ const textContent: any = containerRef.value.querySelector('.dk-tooltip-text-content')
133
+ if (!textContent) {
134
+ return
135
+ }
136
+ const originalDisplay = textContent.style.display
137
+ const originalLineClamp = textContent.style.webkitLineClamp
138
+ textContent.style.display = 'block'
139
+ textContent.style.webkitLineClamp = 'none'
140
+ const naturalHeight = textContent.scrollHeight
141
+ textContent.style.display = originalDisplay
142
+ textContent.style.webkitLineClamp = originalLineClamp
143
+ const limitedHeight = containerRef.value.clientHeight
144
+ isOverflow.value = naturalHeight > limitedHeight + 1
145
+ return
146
+ }
147
+
148
+ // 单行模式:检测水平溢出
149
+ let parentWidth: number
150
+
151
+ // 如果设置了固定宽度,使用该宽度
152
+ if (props.width) {
153
+ parentWidth = Number.parseFloat(props.width)
154
+ } else {
155
+ // 否则获取父元素的宽度
156
+ const parent = containerRef.value.parentElement
157
+ if (!parent) {
158
+ isOverflow.value = containerRef.value.scrollWidth > containerRef.value.clientWidth
159
+ return
160
+ }
161
+ parentWidth = parent.clientWidth
162
+ }
163
+
164
+ // 计算文本宽度并比较
165
+ const textWidth = getTextWidth(props.text)
166
+ isOverflow.value = textWidth > parentWidth
167
+ }
168
+
169
+ /**
170
+ * 使用canvas测量文本宽度
171
+ * @param text 要测量的文本
172
+ * @returns 文本宽度(像素)
173
+ */
174
+ function getTextWidth(text: string): number {
175
+ const canvas = document.createElement('canvas')
176
+ const context = canvas.getContext('2d')
177
+ if (!context) {
178
+ return 0
179
+ }
180
+ context.font = getComputedStyle(containerRef.value!).font || '14px system-ui'
181
+ const metrics = context.measureText(text)
182
+ return metrics.width
183
+ }
184
+
185
+ /**
186
+ * 获取父元素的背景色
187
+ * 用于折叠按钮的背景色,使其与父元素融合
188
+ */
189
+ const fatherBg = computed(() => {
190
+ if (props.toggleBg) {
191
+ return props.toggleBg
192
+ }
193
+ const parent = containerRef.value?.parentElement
194
+ if (!parent) {
195
+ return ''
196
+ }
197
+ return window.getComputedStyle(parent).backgroundColor
198
+ })
199
+
200
+ /** 监听容器尺寸变化的Observer */
201
+ let resizeObserver: ResizeObserver | null = null
202
+ /** 监听父元素尺寸变化的Observer */
203
+ let parentObserver: ResizeObserver | null = null
204
+
205
+ /**
206
+ * 切换折叠/展开状态
207
+ */
208
+ function toggleCollapse() {
209
+ isCollapsed.value = !isCollapsed.value
210
+ }
211
+
212
+ /**
213
+ * 组件挂载后初始化
214
+ * - 检测文本溢出
215
+ * - 创建ResizeObserver监听容器和父元素尺寸变化
216
+ */
217
+ onMounted(() => {
218
+ nextTick(() => {
219
+ checkOverflow()
220
+ if (containerRef.value) {
221
+ // 监听容器尺寸变化
222
+ resizeObserver = new ResizeObserver(() => {
223
+ checkOverflow()
224
+ })
225
+ resizeObserver.observe(containerRef.value)
226
+
227
+ // 监听父元素尺寸变化
228
+ const parent = containerRef.value.parentElement
229
+ if (parent) {
230
+ parentObserver = new ResizeObserver(() => {
231
+ checkOverflow()
232
+ })
233
+ parentObserver.observe(parent)
234
+ }
235
+ }
236
+ })
237
+ })
238
+
239
+ /**
240
+ * 组件卸载时断开所有Observer连接
241
+ */
242
+ onUnmounted(() => {
243
+ resizeObserver?.disconnect()
244
+ parentObserver?.disconnect()
245
+ })
246
+
247
+ defineExpose({
248
+ toggleCollapse,
249
+ })
250
+ </script>
251
+
252
+ <template>
253
+ <!-- tooltip模式:当设置了tooltip且文本溢出时显示el-tooltip提示 -->
254
+ <el-tooltip
255
+ v-if="tooltip"
256
+ :effect="effect"
257
+ :placement="placement"
258
+ :disabled="isDisabled"
259
+ :popper-style="tooltipStyle"
260
+ >
261
+ <template #content>
262
+ <slot name="tooltipContent">
263
+ {{ tooltipContent || text }}
264
+ </slot>
265
+ </template>
266
+ <div
267
+ ref="containerRef"
268
+ class="dk-tooltip-text"
269
+ :class="{ 'is-multiline': props.tooltipLineClamp && props.tooltipLineClamp > 1 }"
270
+ :style="getTextStyle()"
271
+ >
272
+ <slot>
273
+ {{ text }}
274
+ </slot>
275
+ </div>
276
+ </el-tooltip>
277
+
278
+ <!-- 折叠模式:当tooltip为false时启用,支持多行显示和展开/收起功能 -->
279
+ <div
280
+ v-else
281
+ ref="containerRef"
282
+ class="dk-tooltip-collapse"
283
+ :class="{ 'is-expanded': !isCollapsed }"
284
+ :style="{ minWidth: props.width || '100%', lineHeight: props.lineHeight }"
285
+ >
286
+ <!-- 文本内容区域 -->
287
+ <div
288
+ class="dk-tooltip-text-content"
289
+ :style="
290
+ isCollapsed
291
+ ? {
292
+ display: '-webkit-box',
293
+ WebkitLineClamp: props.lineClamp,
294
+ WebkitBoxOrient: 'vertical',
295
+ }
296
+ : {}
297
+ "
298
+ >
299
+ {{ text }}
300
+ </div>
301
+
302
+ <!-- 展开/收起触发器:当文本溢出时显示 -->
303
+ <div
304
+ v-if="isOverflow"
305
+ class="dk-tooltip-toggle"
306
+ :style="{
307
+ 'height': props.lineHeight,
308
+ 'background': fatherBg,
309
+ 'bottom': !isCollapsed ? `-${props.lineHeight}` : '0',
310
+ '--toggle-hover-bg': props.toggleHoverBg || 'inherit',
311
+ }"
312
+ @click.stop="toggleCollapse"
313
+ >
314
+ <!-- 自定义折叠触发器的插槽,默认显示展开/收起按钮和图标 -->
315
+ <slot name="collapse-trigger" :is-collapsed="isCollapsed">
316
+ <span>{{ isCollapsed ? '展开' : '收起' }}</span>
317
+ <el-icon v-if="isCollapsed">
318
+ <ArrowDown />
319
+ </el-icon>
320
+ <el-icon v-else>
321
+ <ArrowUp />
322
+ </el-icon>
323
+ </slot>
324
+ </div>
325
+ </div>
326
+ </template>
327
+
328
+ <style scoped lang="scss">
329
+ /** tooltip模式的文本样式,单行省略 */
330
+ .dk-tooltip-text {
331
+ overflow: hidden;
332
+ text-overflow: ellipsis;
333
+ white-space: nowrap;
334
+ display: inline-block;
335
+ width: 0;
336
+ min-width: 100%;
337
+ // font-size: 12px;
338
+
339
+ /** 多行省略模式 */
340
+ &.is-multiline {
341
+ white-space: normal;
342
+ word-break: break-word;
343
+ }
344
+ }
345
+
346
+ /** 折叠模式的容器样式 */
347
+ .dk-tooltip-collapse {
348
+ position: relative;
349
+ width: 0;
350
+ min-width: 100%;
351
+ box-sizing: border-box;
352
+
353
+ /** 文本内容区域 */
354
+ .dk-tooltip-text-content {
355
+ display: block;
356
+ overflow: hidden;
357
+ text-overflow: ellipsis;
358
+ word-break: break-all;
359
+ font-size: 12px;
360
+ }
361
+
362
+ /** 展开/收起触发按钮 */
363
+ .dk-tooltip-toggle {
364
+ position: absolute;
365
+ right: 0;
366
+ bottom: 0;
367
+ color: inherit;
368
+ cursor: pointer;
369
+ background: #fff;
370
+ padding-left: 16px;
371
+ display: flex;
372
+ align-items: center;
373
+ font-size: 12px;
374
+
375
+ /** 省略号装饰,折叠时显示 */
376
+ &::before {
377
+ content: '...';
378
+ position: absolute;
379
+ left: 0;
380
+ top: 0;
381
+ width: 100%;
382
+ height: 100%;
383
+ color: inherit;
384
+ }
385
+ }
386
+
387
+ &:hover {
388
+ .dk-tooltip-toggle {
389
+ background: var(--toggle-hover-bg, inherit) !important;
390
+ }
391
+ }
392
+
393
+ /** 展开状态样式 */
394
+ &.is-expanded {
395
+ .dk-tooltip-text-content {
396
+ white-space: normal;
397
+ }
398
+
399
+ /** 展开时隐藏省略号 */
400
+ .dk-tooltip-toggle {
401
+ padding-left: 0;
402
+ &::before {
403
+ display: none;
404
+ }
405
+ }
406
+ }
407
+ }
408
+ </style>
@@ -0,0 +1,421 @@
1
+ <script setup lang="ts">
2
+ import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
3
+ import { ref } from 'vue'
4
+ import ComCollapse from '@/components/CodeCollapse.vue'
5
+ import MarkdownRenderer from '@/components/MarkdownRenderer.vue'
6
+ import DkTooltipCollapse from '../src/index.vue'
7
+
8
+ const collapsedVal = ref(false)
9
+ const aaRef = ref<any>(null)
10
+ function changeVal() {
11
+ aaRef.value?.toggleCollapse()
12
+ }
13
+ const wVal = ref(
14
+ '春江潮水连海月共潮生月共潮生滟波滟随波千万里,何处春江无月明!江流宛转绕芳甸,月照花林皆似霰。白云一片去悠悠,何处相思明月楼',
15
+ )
16
+ const wVal2 = ref(
17
+ '春江潮水连海月共潮生月共潮生滟波滟随波千万里,何处春江无月明!江流宛转绕芳甸,月照花林皆似霰。白云一片去悠悠,何处相',
18
+ )
19
+
20
+ const textVal = ref(`春江潮水连海月共潮生月共潮生。
21
+ 滟波滟随波千万里,何处春江无月明!
22
+ 江流宛转绕芳甸,月照花林皆似霰。
23
+ 空里流霜不觉飞,汀上白沙看不见。
24
+ 江天一色无纤尘,皎皎空中孤月轮。
25
+ 江畔何人初见月?江月何年初照人?
26
+ 人生代代无穷已,江月年年望相似。
27
+ 不知江月待何人,但见长江送流水。
28
+ 白云一片去悠悠,青枫浦上不胜愁。
29
+ 谁家今夜扁舟子?何处相思明月楼?
30
+ 可怜楼上月徘徊,应照离人妆镜台。
31
+ 玉户帘中卷不去,捣衣砧上拂还来。
32
+ 此时相望不相闻,愿逐月华流照君。
33
+ 鸿雁长飞光不度,鱼龙潜跃水成文。
34
+ 昨夜闲潭梦落花,可怜春半不还家。
35
+ 江水流春去欲尽,江潭落月复西斜。
36
+ 斜月沉沉藏海雾,碣石潇湘无限路。
37
+ 不知乘月几人归,落月摇情满江树。`)
38
+ const textVal2 = ref(`坐上那朵离家的云霞
39
+ 飘去无人知晓的天涯
40
+ 背着妈妈说的那句话
41
+ 孩子人生其实不复杂
42
+
43
+ 喔~眼泪轻轻地擦
44
+ 别管那多嘴乌鸦
45
+ 咽下那些风沙
46
+ 你才能慢慢长大
47
+ 要错过几个她
48
+ 用你最好的年华
49
+ 这是青春的代价
50
+
51
+ 【副歌】
52
+ 当离别开出花 伸出新长的枝桠
53
+ 像冬去春又来 等待心雪融化
54
+ 你每次离开家 带着远方的牵挂
55
+ 那城市的繁华 盖住了月牙
56
+
57
+ 当离别开出花 它生长在悬崖
58
+ 在最高的山顶 才听得见回答
59
+ 没什么好害怕 孩子放心去飞吧
60
+ 在你的身后 有个等你的家
61
+
62
+ 坐上那朵离家的云霞
63
+ 飘去无人知晓的天涯
64
+ 背着妈妈说的那句话
65
+ 孩子人生其实不复杂
66
+
67
+ 喔~眼泪轻轻地擦
68
+ 别忘那童年梦话
69
+ 散在远方的花 也随风慢慢长大
70
+ 要错过几个她
71
+ 用你最真的年华
72
+ 这是青春的回答
73
+
74
+ 【副歌重复】
75
+
76
+ 当离别开出花 伸出新长的枝桠
77
+ 像冬去春又来 等待心雪融化
78
+ 你每次离开家 带着远方的牵挂
79
+ 那城市的繁华 盖住了月牙
80
+
81
+ 当离别开出花 它生长在悬崖
82
+ 在最高的山顶 才听得见回答
83
+ 没什么好害怕 孩子放心去飞吧
84
+ 在你的身后 有个等你的家`)
85
+ const tooltipContent = ref(
86
+ '这是唐代诗人张若虚的作品。这首诗共三十六句,描绘了春江月夜的壮丽景色,并由此引发对宇宙、人生的哲理思考以及游子思妇的离愁别绪,素有“孤篇盖全唐”的美誉',
87
+ )
88
+
89
+ const demoCode01 = ref(
90
+ `<DkTooltipCollapse :text="textVal" />
91
+
92
+ const textVal = ref('xxxxxxxxxxxxxxxxxxx')
93
+ import { DkTooltipCollapse } from '@dk-component/comp';`,
94
+ )
95
+
96
+ const demoCode02 = ref(
97
+ `<div style="width: 500px;">
98
+ <DkTooltipCollapse :text="'弹框宽度默认为100%。'+textVal" />
99
+ <DkTooltipCollapse :text="'弹框宽度为350px。'+textVal" tooltip-max-width="350px" />
100
+ <br>
101
+ <DkTooltipCollapse :text="'tooltipLineClamp为2行。'+textVal" tooltip-max-width="400px" :tooltip-line-clamp="2" />
102
+
103
+ <div style="width: 100%; display: flex; text-indent: 24px;">
104
+ <div style="flex: 1;">
105
+ <DkTooltipCollapse :text="textVal2" tooltip-max-width="400px" :tooltip-line-clamp="2" />
106
+ </div>
107
+ </div>
108
+ <br>
109
+
110
+ <h3>自定义 slot</h3>
111
+ <h4>1.slot 显示内容 完全自定义 :</h4>
112
+ <DkTooltipCollapse :text="'弹框宽度默认为100%'+textVal">
113
+ <div>
114
+ <span style="color: red; font-weight: 800;">我是额外的内容, </span>
115
+ <span>{{ textVal }}</span>
116
+ </div>
117
+ </DkTooltipCollapse>
118
+ <h4>2.slot 显示内容 通过 text 传递 :</h4>
119
+ <DkTooltipCollapse text="弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容" width="135px" :tooltip-content="tooltipContent" />
120
+
121
+ <h4>3.slot 弹框内容 自定义 :</h4>
122
+ <DkTooltipCollapse text="弹框-中是自定义内容" width="150px">
123
+ <template #tooltipContent>
124
+ <p>33333333333</p>
125
+ multiple lines<br>second line
126
+ multiple lines<br>second line
127
+ multiple lines<br>second line
128
+ multiple lines<br>second line
129
+ multiple lines<br>second line
130
+ </template>
131
+ </DkTooltipCollapse>
132
+ </div>
133
+
134
+ const textVal = ref('xxxxxxxxxxxxxxxxxxx');
135
+ const tooltipContent = ref("这是唐代诗人张若虚的作品。这首诗共三十六句,描绘了春江月夜的壮丽景色,并由此引发对宇宙、人生的哲理思考以及游子思妇的离愁别绪,素有“孤篇盖全唐”的美誉");
136
+
137
+ import { DkTooltipCollapse } from '@dk-component/comp';`,
138
+ )
139
+
140
+ const demoCode03 = ref(
141
+ `<h4>设置 展开 还是收起</h4>
142
+ <div>
143
+ <h4>设置 展开 还是收起</h4>
144
+ <button @click="changeVal">
145
+ 通过 ref 控制 展开/收起
146
+ </button>
147
+ <DkTooltipCollapse ref="aaRef" :text="textVal" :tooltip="false" toggle-bg="cyan" toggle-hover-bg="red" :is-collapsed="collapsedVal" />
148
+ </div>
149
+ <br>
150
+
151
+ <h4>设置展开行数</h4>
152
+ <div style="display: flex;">
153
+ <div style="flex: 1;">
154
+ <DkTooltipCollapse :text="textVal" :tooltip="false" toggle-bg="cyan" toggle-hover-bg="red" />
155
+ </div>
156
+ <div style="flex: 1; background: yellow; color: red;">
157
+ <DkTooltipCollapse :text="textVal" :tooltip="false" :line-clamp="3" />
158
+ </div>
159
+ <div style="flex: 1; background: #999;">
160
+ <DkTooltipCollapse :text="textVal" :tooltip="false" :line-clamp="2" line-height="40px" />
161
+ </div>
162
+ </div>
163
+
164
+ const textVal = ref('xxxxxxxxxxxxxxxxxxx')
165
+ import { DkTooltipCollapse } from '@dk-component/comp';
166
+
167
+ const collapsedVal = ref(false);
168
+ const aaRef = ref<any>(null);
169
+ function changeVal() {
170
+ aaRef.value?.toggleCollapse();
171
+ }
172
+
173
+ `,
174
+ )
175
+
176
+ const demoCode04 = ref(
177
+ `<DkTooltipCollapse :text="textVal" toggle-bg="cyan" class="my-box">
178
+ <template #collapse-trigger="{ isCollapsed }">
179
+ <el-icon>
180
+ <ArrowDown v-if="isCollapsed" />
181
+ <ArrowUp v-else />
182
+ </el-icon>
183
+ </template>
184
+ </DkTooltipCollapse>
185
+
186
+ const textVal = ref('xxxxxxxxxxxxxxxxxxx')
187
+ import { DkTooltipCollapse } from '@dk-component/comp';
188
+
189
+ <style lang="scss" scoped>
190
+ :deep(.my-box) {
191
+ &.is-expanded {
192
+ .dk-tooltip-toggle {
193
+ bottom: 0 !important;
194
+ }
195
+ }
196
+ }
197
+ </style>
198
+ `,
199
+ )
200
+
201
+ // Markdown表格内容
202
+ const propsTable = ref(`
203
+ | 参数名 | 类型 | 默认值 | 说明 |
204
+ |--------|------|--------|------|
205
+ | text | string | "" | 显示的文本内容 |
206
+ | width | string | "" | 容器最小宽度,例如 "200px" |
207
+ | placement | string | "top" | tooltip 弹框 弹出位置,可选值:top /bottom /left /right /top-start /top-end /bottom-start /bottom-end /left-start /left-end /right-start /right-end |
208
+ | tooltip-content | string | "" | tooltip 弹框 提示内容,不设置则使用 text |
209
+ | tooltip-max-width | string | "90%" | tooltip 弹框 最大宽度 |
210
+ | effect | string | "dark" | tooltip 弹框 主题色,可选值:dark /light |
211
+ | disabled | boolean | undefined | 是否禁用 |
212
+ | tooltip | boolean | true | true-弹框模式; false-折叠模式 |
213
+ | isCollapsed | boolean | true | 起始状态: true-收起; false-展开 |
214
+ | line-clamp | number | 1 | 折叠模式-'展开'时显示的最大行数 |
215
+ | line-height | string | "20px" | 每行的高度,用于计算展开/收起 的位置 |
216
+ | toggleBg | string | "" | 展开/收起 的背景色,默认使用 父元素 的背景色 |
217
+ | toggleHoverBg | string | "" | 展开/收起 的悬停背景色,默认使用 父元素 的背景色 |
218
+ | 事件: toggleCollapse | Function | 折叠模式-点击展开/收起 通过 ref 触发 |
219
+ `)
220
+
221
+ const slotsTable = ref(`
222
+ | 插槽名 | 说明 | 参数 |
223
+ |--------|------|------|
224
+ | default | 自定义 text 显示内容 | html |
225
+ | tooltipContent | 自定义 tooltip 弹框 提示内容 | string/html |
226
+ | collapse-trigger | 展开+收起时:展开+收起的自定义插槽 | html |
227
+ `)
228
+ </script>
229
+
230
+ <template>
231
+ <div class="demo-container height-stretch overflow-auto-y custom-scrollbar-gradient">
232
+ <h1>文本溢出显示省略号并支持展开/收起的组件,同时支持 tooltip 模式显示完整文本</h1>
233
+ <div>
234
+ <h2>1.基础用法</h2>
235
+ <code>不设置宽度,就随父级宽度变化, 默认 100%</code>
236
+ <DkTooltipCollapse :text="textVal" />
237
+ <ComCollapse class="com-collapse" :code="demoCode01" />
238
+ </div>
239
+
240
+ <h2>2.父级宽度为500px时</h2>
241
+ <div style="width: 500px">
242
+ <DkTooltipCollapse :text="`弹框宽度默认为100%。${textVal}`" />
243
+ <DkTooltipCollapse :text="`弹框宽度为350px。${textVal}`" tooltip-max-width="350px" />
244
+ <br>
245
+ <DkTooltipCollapse
246
+ :text="`tooltipLineClamp为2行。${textVal}`"
247
+ tooltip-max-width="400px"
248
+ :tooltip-line-clamp="2"
249
+ />
250
+
251
+ <div style="width: 100%; display: flex; text-indent: 24px">
252
+ <div style="flex: 1">
253
+ <DkTooltipCollapse :text="textVal2" tooltip-max-width="400px" :tooltip-line-clamp="2" />
254
+ </div>
255
+ </div>
256
+ <br>
257
+
258
+ <h3>自定义 slot</h3>
259
+ <h4>1.slot 显示内容 完全自定义 :</h4>
260
+ <DkTooltipCollapse :text="`弹框宽度默认为100%。${textVal}`">
261
+ <div>
262
+ <span style="color: red; font-weight: 800">我是额外的内容, </span>
263
+ <span>{{ textVal }}</span>
264
+ </div>
265
+ </DkTooltipCollapse>
266
+ <h4>2.slot 显示内容 通过 text 传递 :</h4>
267
+ <DkTooltipCollapse
268
+ text="弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容弹框-中是其他内容"
269
+ width="135px"
270
+ :tooltip-content="tooltipContent"
271
+ />
272
+
273
+ <h4>3.slot 弹框内容 自定义 :</h4>
274
+ <DkTooltipCollapse text="弹框-中是自定义内容" width="150px">
275
+ <template #tooltipContent>
276
+ <p>33333333333</p>
277
+ multiple lines<br>second line multiple lines<br>second line multiple lines<br>second
278
+ line multiple lines<br>second line multiple lines<br>second line
279
+ </template>
280
+ </DkTooltipCollapse>
281
+ </div>
282
+ <ComCollapse class="com-collapse" :code="demoCode02" />
283
+
284
+ <h2>3.折叠模式: 展开+收起</h2>
285
+ <code>设置 `tooltip` 为 `false` 启用折叠模式,支持多行省略和展开/收起功能。</code>
286
+
287
+ <div>
288
+ <h4>设置 展开 还是收起</h4>
289
+ <button @click="changeVal">
290
+ 通过 ref 控制 展开/收起
291
+ </button>
292
+ <DkTooltipCollapse
293
+ ref="aaRef"
294
+ :text="textVal"
295
+ :tooltip="false"
296
+ toggle-bg="cyan"
297
+ toggle-hover-bg="red"
298
+ :is-collapsed="collapsedVal"
299
+ />
300
+ </div>
301
+ <br>
302
+
303
+ <h4>设置展开行数</h4>
304
+ <div style="display: flex">
305
+ <div style="flex: 1">
306
+ <DkTooltipCollapse
307
+ :text="`默认1行:${textVal}`"
308
+ :tooltip="false"
309
+ toggle-bg="cyan"
310
+ toggle-hover-bg="red"
311
+ />
312
+ </div>
313
+ <div style="flex: 1; background: yellow; color: red">
314
+ <DkTooltipCollapse :text="`显示3行:${textVal}`" :tooltip="false" :line-clamp="3" />
315
+ </div>
316
+ <div style="flex: 1; background: #999">
317
+ <DkTooltipCollapse
318
+ :text="`行高48px,显示2行:${textVal}`"
319
+ :tooltip="false"
320
+ :line-clamp="2"
321
+ line-height="40px"
322
+ />
323
+ </div>
324
+ </div>
325
+ <ComCollapse class="com-collapse" :code="demoCode03" />
326
+
327
+ <div style="margin-top: 20px; width: 350px; border: 1px solid #ccc">
328
+ <p>设置默认最多显示2行,如果文字不够2行,就不显示展开/收起</p>
329
+ <DkTooltipCollapse
330
+ :text="wVal"
331
+ :tooltip="false"
332
+ toggle-bg="cyan"
333
+ toggle-hover-bg="red"
334
+ :line-clamp="2"
335
+ />
336
+ <p>文字不够2行</p>
337
+ <DkTooltipCollapse
338
+ :text="wVal2"
339
+ :tooltip="false"
340
+ toggle-bg="cyan"
341
+ toggle-hover-bg="red"
342
+ :line-clamp="2"
343
+ />
344
+ </div>
345
+
346
+ <div style="width: 500px">
347
+ <br>
348
+ <h6>注意:默认展开/收起按钮的背景色为父级背景色;如果父级不设置背景色,默认为透明:</h6>
349
+ <DkTooltipCollapse :text="textVal" :tooltip="false" />
350
+ <br>
351
+ <h6>设置 `toggleBg` 为自定义颜色,即可改变展开/收起按钮的背景色:</h6>
352
+ <DkTooltipCollapse :text="textVal" :tooltip="false" toggle-bg="#f5f5f5" />
353
+ <code>
354
+ 此时会出现一个问题:"...展开"会把部分文字遮住部分,是因为设置父级的宽度不合理造成的,
355
+ 可以自己设置 "...展开"的 css 样式,例如:
356
+ <pre>
357
+ :deep(.dk-tooltip-collapse){
358
+ .dk-tooltip-toggle{
359
+ padding-left: 25px;
360
+ }
361
+ }
362
+ </pre>
363
+ </code>
364
+ </div>
365
+
366
+ <h3>自定义:展开/收起按钮</h3>
367
+ <div style="width: 472px" class="list-box">
368
+ <DkTooltipCollapse :tooltip="false" :text="textVal" class="my-box" toggle-hover-bg="red">
369
+ <template #collapse-trigger="{ isCollapsed }">
370
+ <el-icon>
371
+ <ArrowDown v-if="isCollapsed" />
372
+ <ArrowUp v-else />
373
+ </el-icon>
374
+ </template>
375
+ </DkTooltipCollapse>
376
+ </div>
377
+ <ComCollapse class="com-collapse" :code="demoCode04" />
378
+
379
+ <!-- 参数表格 -->
380
+ <section class="markdown-section">
381
+ <h2>DkTooltipCollapse 参数</h2>
382
+ <MarkdownRenderer :content="propsTable" />
383
+ <h2>Slots 插槽</h2>
384
+ <MarkdownRenderer :content="slotsTable" />
385
+ </section>
386
+ </div>
387
+ </template>
388
+
389
+ <style lang="scss" scoped>
390
+ // @import url('@/assets/styles/demo.scss');
391
+ .box-item {
392
+ :deep(.el-tooltip__popper) {
393
+ transform: translate(-60%, -100%) !important;
394
+ margin: 0 !important;
395
+ }
396
+ :deep(.el-tooltip__popper.is-dark) {
397
+ transform: translate(-60%, -100%) !important;
398
+ margin: 0 !important;
399
+ }
400
+ }
401
+
402
+ :deep(.my-box) {
403
+ &.is-expanded {
404
+ .dk-tooltip-toggle {
405
+ bottom: 0 !important;
406
+ }
407
+ }
408
+ }
409
+
410
+ .list-box {
411
+ background: yellowgreen;
412
+ &:hover {
413
+ background: red;
414
+ }
415
+ }
416
+ :deep(.com-collapse) {
417
+ .el-collapse-item__header {
418
+ padding-left: 20px;
419
+ }
420
+ }
421
+ </style>
@@ -0,0 +1,3 @@
1
+ import RollNumber from './roll-number/src/index.vue'
2
+
3
+ export { RollNumber }
@@ -0,0 +1,6 @@
1
+ import RollNumber from './src/index.vue'
2
+
3
+ RollNumber.install = function (Vue: any) {
4
+ Vue.component('RollNumber', RollNumber)
5
+ }
6
+ export default RollNumber
@@ -0,0 +1,183 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
3
+
4
+ /**
5
+ * 组件属性接口
6
+ */
7
+ interface Props {
8
+ /** 目标数字 */
9
+ value?: number
10
+ /** 动画持续时间(毫秒),默认 1200ms */
11
+ duration?: number
12
+ /** 缓动函数类型,默认 'easeOutQuad' */
13
+ easing?: 'easeOutQuad' | 'easeOutCubic' | 'linear'
14
+ /** 是否显示千分号,默认 false */
15
+ thousandsSeparator?: boolean
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ /** 目标数字 */
20
+ value: 0,
21
+ /** 动画持续时间(毫秒),默认 1200ms */
22
+ duration: 1200,
23
+ /** 缓动函数类型,默认 'easeOutQuad' */
24
+ easing: 'easeOutQuad',
25
+ /** 是否显示千分号,默认 false */
26
+ thousandsSeparator: false,
27
+ })
28
+
29
+ /** 当前显示的数字 */
30
+ const count = ref(0)
31
+ /** 动画请求 ID,用于取消动画 */
32
+ let animationId: number | null = null
33
+ /** 动画开始时间戳 */
34
+ let startTime: number | null = null
35
+ /** 动画起始数值 */
36
+ let startValue = 0
37
+
38
+ /**
39
+ * 缓动函数集合
40
+ */
41
+ const easingFunctions = {
42
+ /** 线性缓动 */
43
+ linear: (t: number) => t,
44
+ /** 二次缓动(先快后慢) */
45
+ easeOutQuad: (t: number) => t * (2 - t),
46
+ /** 三次缓动(更柔和的先快后慢) */
47
+ easeOutCubic: (t: number) => --t * t * t + 1,
48
+ }
49
+
50
+ /**
51
+ * 动画帧回调函数
52
+ * @param timestamp - 时间戳
53
+ */
54
+ function animate(timestamp: number) {
55
+ // 记录开始时间
56
+ if (!startTime) {
57
+ startTime = timestamp
58
+ }
59
+ // 计算动画进度 (0-1)
60
+ const progress = Math.min((timestamp - startTime) / props.duration, 1)
61
+ // 获取缓动函数
62
+ const easingFn = easingFunctions[props.easing]
63
+ // 应用缓动函数
64
+ const easedProgress = easingFn(progress)
65
+ // 计算当前数字
66
+ const currentNumber = startValue + (props.value - startValue) * easedProgress
67
+ count.value = currentNumber
68
+
69
+ // 继续下一帧或结束动画
70
+ if (progress < 1) {
71
+ animationId = requestAnimationFrame(animate)
72
+ } else {
73
+ count.value = props.value
74
+ animationId = null
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 开始数字滚动动画
80
+ */
81
+ function startAnimation() {
82
+ // 取消之前的动画
83
+ if (animationId) {
84
+ cancelAnimationFrame(animationId)
85
+ }
86
+ // 重置开始时间
87
+ startTime = null
88
+ // 记录当前值作为起始值
89
+ startValue = count.value
90
+ // 开始新动画
91
+ animationId = requestAnimationFrame(animate)
92
+ }
93
+
94
+ /**
95
+ * 获取数字的小数位数
96
+ * @param num - 数字
97
+ * @returns 小数位数
98
+ */
99
+ function getDecimalPlaces(num: number): number {
100
+ const str = num.toString()
101
+ const dotIndex = str.indexOf('.')
102
+ return dotIndex === -1 ? 0 : str.length - dotIndex - 1
103
+ }
104
+
105
+ /**
106
+ * 去除小数尾部多余的零
107
+ * @param str - 数字字符串
108
+ * @returns 处理后的字符串
109
+ */
110
+ function stripTrailingZeros(str: string): string {
111
+ // 更安全的正则,避免超线性回溯
112
+ const dotIndex = str.indexOf('.')
113
+ if (dotIndex === -1) {
114
+ return str
115
+ }
116
+ let end = str.length
117
+ while (end > dotIndex + 1 && str.charAt(end - 1) === '0') {
118
+ end--
119
+ }
120
+ return str.slice(0, end)
121
+ }
122
+
123
+ /**
124
+ * 格式化显示的数字
125
+ */
126
+ const formattedCount = computed(() => {
127
+ const decimalPlaces = getDecimalPlaces(props.value)
128
+ const fixedNumber = count.value.toFixed(decimalPlaces)
129
+ const stripped = stripTrailingZeros(fixedNumber)
130
+ if (props.thousandsSeparator) {
131
+ const parts = stripped.split('.')
132
+ parts[0]! = parts[0]!.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
133
+ return parts.join('.')
134
+ }
135
+ return stripped
136
+ })
137
+
138
+ /**
139
+ * 监听关键属性变化,重新触发动画
140
+ */
141
+ watch([() => props.value, () => props.duration, () => props.easing], () => {
142
+ startAnimation()
143
+ })
144
+
145
+ /**
146
+ * 组件挂载时启动动画
147
+ */
148
+ onMounted(() => {
149
+ startAnimation()
150
+ })
151
+
152
+ /**
153
+ * 组件卸载时清理动画,防止内存泄漏
154
+ */
155
+ onUnmounted(() => {
156
+ if (animationId) {
157
+ cancelAnimationFrame(animationId)
158
+ }
159
+ })
160
+ </script>
161
+
162
+ <template>
163
+ <!-- 数字滚动组件 -->
164
+ <div class="counter number-tabular number-modern">
165
+ {{ formattedCount }}
166
+ </div>
167
+ </template>
168
+
169
+ <style scoped lang="scss">
170
+ .counter {
171
+ font-weight: 800;
172
+ }
173
+ /* 示例:为数字指定一个等宽字体,让它们占位宽度相同,利于对齐 */
174
+ .number-tabular {
175
+ /* 'Courier New', 'Consolas' 等是常见的等宽字体,数字默认等宽 */
176
+ font-family: 'Courier New', 'SF Mono', 'Consolas', monospace;
177
+ }
178
+
179
+ /* 示例:使用系统默认的 sans-serif 字体,让数字显示更现代 */
180
+ .number-modern {
181
+ font-family: 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
182
+ }
183
+ </style>
@@ -0,0 +1,78 @@
1
+ <script setup lang="ts">
2
+ import ComCollapse from '@/components/CodeCollapse.vue'
3
+ import MarkdownRenderer from '@/components/MarkdownRenderer.vue'
4
+ import RollNumber from '../src/index.vue'
5
+
6
+ const code = ref(`
7
+ <p>基础用法</p>
8
+ <RollNumber :value="50000" :duration="3000" />
9
+ <p>小数位</p>
10
+ <RollNumber :value="50000.123456789" :duration="3000" />
11
+ <p>千分位</p>
12
+ <RollNumber :value="1234567" :duration="3000" :thousands-separator="true" />
13
+ <p class="color-jb">千分位+小数位</p>
14
+ <RollNumber
15
+ :value="1234567.123456789"
16
+ :duration="3000"
17
+ :thousands-separator="true"
18
+ />
19
+
20
+ import { RollNumber } from '@shimmerUI/comp';
21
+ `)
22
+
23
+ // Markdown表格内容
24
+ const propsTable = ref(`
25
+ | 参数名 | 说明 | 类型 | 默认值 |
26
+ |--------|------|------|-------|
27
+ | value | 值 | Number | |
28
+ | duration | 动画持续时间(毫秒) | Number | 1200 |
29
+ | easing | 缓动函数类型 | String | 'easeOutQuad' |
30
+ | thousandsSeparator | 是否显示千分号 | Boolean | false |
31
+ `)
32
+ </script>
33
+
34
+ <template>
35
+ <div class="demo-container height-stretch overflow-auto-y custom-scrollbar-gradient">
36
+ <!-- 标题 -->
37
+ <h2>数字滚动组件-RollNumber</h2>
38
+
39
+ <!-- 组件示例展示 -->
40
+ <section class="demo-section">
41
+ <div class="demo-block">
42
+ <p>基础用法</p>
43
+ <RollNumber :value="50000" :duration="3000" />
44
+ <p>小数位</p>
45
+ <RollNumber :value="50000.123456789" :duration="3000" />
46
+ <p>千分位</p>
47
+ <RollNumber :value="1234567" :duration="3000" :thousands-separator="true" />
48
+ <p class="color-jb">
49
+ 千分位+小数位
50
+ </p>
51
+ <RollNumber :value="1234567.123456789" :duration="3000" :thousands-separator="true" />
52
+ </div>
53
+ <ComCollapse class="com-collapse" :code="code" />
54
+ </section>
55
+
56
+ <!-- 参数表格 -->
57
+ <section>
58
+ <h2>RollNumber API</h2>
59
+ <MarkdownRenderer :content="propsTable" />
60
+ </section>
61
+ </div>
62
+ </template>
63
+
64
+ <style scoped lang="scss">
65
+ .demo-container {
66
+ padding: 20px;
67
+ background-color: #fff;
68
+ }
69
+ .demo-block {
70
+ font-size: 30px;
71
+ }
72
+
73
+ :deep(.com-collapse) {
74
+ .el-collapse-item__header {
75
+ padding-left: 20px;
76
+ }
77
+ }
78
+ </style>