@luanlu/mk-motion 1.1.0 → 1.2.1
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/package.json +14 -2
- package/src/a11y/focus-trap.ts +64 -0
- package/src/a11y/keyboard.ts +43 -0
- package/src/components/alert/alert.css +111 -0
- package/src/components/alert/alert.ts +107 -0
- package/src/components/avatar/avatar.css +112 -0
- package/src/components/avatar/avatar.ts +175 -0
- package/src/components/breadcrumb/breadcrumb.css +31 -0
- package/src/components/breadcrumb/breadcrumb.ts +71 -0
- package/src/components/button/button.css +108 -0
- package/src/components/button/button.ts +140 -0
- package/src/components/card/card.css +52 -0
- package/src/components/card/card.ts +87 -0
- package/src/components/collapse/collapse.css +76 -0
- package/src/components/collapse/collapse.ts +168 -0
- package/src/components/dialog/dialog.css +78 -0
- package/src/components/dialog/dialog.ts +164 -0
- package/src/components/drawer/drawer.css +73 -0
- package/src/components/drawer/drawer.ts +131 -0
- package/src/components/empty/empty.css +36 -0
- package/src/components/empty/empty.ts +85 -0
- package/src/components/form/checkbox.css +56 -0
- package/src/components/form/checkbox.ts +119 -0
- package/src/components/form/radio.css +57 -0
- package/src/components/form/radio.ts +153 -0
- package/src/components/form/select.css +91 -0
- package/src/components/form/select.ts +174 -0
- package/src/components/form/slider.css +56 -0
- package/src/components/form/slider.ts +148 -0
- package/src/components/input/input.css +92 -0
- package/src/components/input/input.ts +162 -0
- package/src/components/layout/divider.css +32 -0
- package/src/components/layout/divider.ts +42 -0
- package/src/components/layout/row.css +64 -0
- package/src/components/layout/row.ts +57 -0
- package/src/components/layout/space.css +14 -0
- package/src/components/layout/space.ts +48 -0
- package/src/components/loading/loading.css +37 -0
- package/src/components/loading/loading.ts +46 -0
- package/src/components/menu/menu.css +121 -0
- package/src/components/menu/menu.ts +187 -0
- package/src/components/message/message.css +64 -0
- package/src/components/message/message.ts +96 -0
- package/src/components/popover/popover.css +73 -0
- package/src/components/popover/popover.ts +279 -0
- package/src/components/progress/progress.css +112 -0
- package/src/components/progress/progress.ts +171 -0
- package/src/components/steps/steps.css +127 -0
- package/src/components/steps/steps.ts +102 -0
- package/src/components/styles/components.css +28 -0
- package/src/components/styles/reset.css +24 -0
- package/src/components/styles/tokens.css +248 -0
- package/src/components/styles/variables.css +24 -0
- package/src/components/switch/switch.css +53 -0
- package/src/components/switch/switch.ts +103 -0
- package/src/components/table/table.css +192 -0
- package/src/components/table/table.ts +370 -0
- package/src/components/tabs/tabs.css +138 -0
- package/src/components/tabs/tabs.ts +211 -0
- package/src/components/tag/tag.css +123 -0
- package/src/components/tag/tag.ts +112 -0
- package/src/components/tooltip/tooltip.css +66 -0
- package/src/components/tooltip/tooltip.ts +185 -0
- package/src/core/animator.ts +124 -0
- package/src/core/timeline.ts +128 -0
- package/src/core/utils.ts +47 -0
- package/src/effects/glitch.ts +99 -0
- package/src/effects/particle.ts +134 -0
- package/src/effects/text-split.ts +95 -0
- package/src/effects/wave-text.ts +88 -0
- package/src/gesture/draggable.ts +130 -0
- package/src/gesture/spring.ts +152 -0
- package/src/index.ts +162 -0
- package/src/interactive/coverflow.ts +100 -0
- package/src/interactive/cursor-trail.ts +113 -0
- package/src/interactive/flip-card.ts +114 -0
- package/src/interactive/magnetic.ts +121 -0
- package/src/micro/hover-lift.ts +94 -0
- package/src/micro/ripple.ts +130 -0
- package/src/motion/component-motion.ts +177 -0
- package/src/nuxt/module.ts +46 -0
- package/src/presets/index.ts +69 -0
- package/src/scroll/scroll-trigger.ts +104 -0
- package/src/styles/animations.css +135 -0
- package/src/styles/element-plus.css +174 -0
- package/src/text/count-up.ts +108 -0
- package/src/text/typewriter.ts +109 -0
- package/src/theme/dark.css +19 -0
- package/src/theme/light.css +19 -0
- package/src/theme/theme.ts +65 -0
- package/src/transitions/blur-reveal.ts +92 -0
- package/src/transitions/collapse.ts +112 -0
- package/src/transitions/lazy-image.ts +87 -0
- package/src/transitions/list.ts +75 -0
- package/src/transitions/loading.ts +95 -0
- package/src/transitions/parallax.ts +60 -0
- package/src/transitions/shimmer.ts +105 -0
- package/src/transitions/toast.ts +151 -0
- package/src/types.d.ts +4 -0
- package/src/vite/plugin.ts +45 -0
- package/src/vue/button.ts +28 -9
- package/src/vue/card.ts +28 -8
- package/src/vue/composables/index.ts +4 -0
- package/src/vue/composables/useLoading.ts +12 -0
- package/src/vue/composables/useMessage.ts +16 -0
- package/src/vue/composables/useMotion.ts +19 -0
- package/src/vue/composables/useTheme.ts +12 -0
- package/src/vue/dialog.ts +69 -17
- package/src/vue/index.ts +4 -21
- package/src/vue/input.ts +35 -11
- package/src/vue/slider.ts +22 -4
- package/src/vue/switch.ts +16 -9
- package/src/vue/alert.ts +0 -32
- package/src/vue/avatar.ts +0 -34
- package/src/vue/breadcrumb.ts +0 -32
- package/src/vue/checkbox.ts +0 -32
- package/src/vue/collapse.ts +0 -33
- package/src/vue/divider.ts +0 -32
- package/src/vue/drawer.ts +0 -33
- package/src/vue/empty.ts +0 -33
- package/src/vue/menu.ts +0 -33
- package/src/vue/popover.ts +0 -34
- package/src/vue/progress.ts +0 -33
- package/src/vue/row.ts +0 -32
- package/src/vue/select.ts +0 -33
- package/src/vue/space.ts +0 -32
- package/src/vue/steps.ts +0 -33
- package/src/vue/table.ts +0 -33
- package/src/vue/tabs.ts +0 -33
- package/src/vue/tag.ts +0 -33
- package/src/vue/tooltip.ts +0 -34
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export interface CountUpOptions {
|
|
2
|
+
duration?: number // 动画总时长毫秒
|
|
3
|
+
easing?: 'linear' | 'easeOut' | 'easeInOut'
|
|
4
|
+
decimals?: number // 小数位
|
|
5
|
+
prefix?: string // 前缀
|
|
6
|
+
suffix?: string // 后缀
|
|
7
|
+
separator?: string // 千分位分隔符
|
|
8
|
+
onUpdate?: (value: number) => void
|
|
9
|
+
onComplete?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_COUNT_OPTS: Required<Omit<CountUpOptions, 'onUpdate' | 'onComplete'>> = {
|
|
13
|
+
duration: 2000,
|
|
14
|
+
easing: 'easeOut',
|
|
15
|
+
decimals: 0,
|
|
16
|
+
prefix: '',
|
|
17
|
+
suffix: '',
|
|
18
|
+
separator: ',',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CountUp {
|
|
22
|
+
private element: HTMLElement
|
|
23
|
+
private rafId: number | null = null
|
|
24
|
+
|
|
25
|
+
constructor(element: HTMLElement | string) {
|
|
26
|
+
this.element =
|
|
27
|
+
typeof element === 'string'
|
|
28
|
+
? document.querySelector(element)!
|
|
29
|
+
: element
|
|
30
|
+
|
|
31
|
+
if (!this.element) {
|
|
32
|
+
throw new Error('CountUp: element not found')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
animateTo(
|
|
37
|
+
endValue: number,
|
|
38
|
+
startValue: number = 0,
|
|
39
|
+
options: CountUpOptions = {}
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const opts = { ...DEFAULT_COUNT_OPTS, ...options }
|
|
42
|
+
const startTime = performance.now()
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const tick = (now: number) => {
|
|
46
|
+
const elapsed = now - startTime
|
|
47
|
+
const progress = Math.min(elapsed / opts.duration, 1)
|
|
48
|
+
const eased = this.applyEasing(progress, opts.easing)
|
|
49
|
+
const current = startValue + (endValue - startValue) * eased
|
|
50
|
+
|
|
51
|
+
this.render(current, opts)
|
|
52
|
+
opts.onUpdate?.(current)
|
|
53
|
+
|
|
54
|
+
if (progress < 1) {
|
|
55
|
+
this.rafId = requestAnimationFrame(tick)
|
|
56
|
+
} else {
|
|
57
|
+
this.render(endValue, opts)
|
|
58
|
+
opts.onComplete?.()
|
|
59
|
+
resolve()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.rafId = requestAnimationFrame(tick)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
stop(): void {
|
|
68
|
+
if (this.rafId) {
|
|
69
|
+
cancelAnimationFrame(this.rafId)
|
|
70
|
+
this.rafId = null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private render(value: number, opts: CountUpOptions): void {
|
|
75
|
+
let formatted = value.toFixed(opts.decimals!)
|
|
76
|
+
|
|
77
|
+
// 千分位
|
|
78
|
+
if (opts.separator) {
|
|
79
|
+
const parts = formatted.split('.')
|
|
80
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, opts.separator!)
|
|
81
|
+
formatted = parts.join('.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.element.textContent = `${opts.prefix}${formatted}${opts.suffix}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private applyEasing(t: number, easing: string): number {
|
|
88
|
+
switch (easing) {
|
|
89
|
+
case 'easeOut':
|
|
90
|
+
return 1 - Math.pow(1 - t, 3)
|
|
91
|
+
case 'easeInOut':
|
|
92
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
|
|
93
|
+
default:
|
|
94
|
+
return t
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 快捷方法
|
|
101
|
+
*/
|
|
102
|
+
export function countUp(
|
|
103
|
+
element: HTMLElement | string,
|
|
104
|
+
endValue: number,
|
|
105
|
+
options?: CountUpOptions
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
return new CountUp(element).animateTo(endValue, 0, options)
|
|
108
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface TypewriterOptions {
|
|
2
|
+
speed?: number // 每个字间隔毫秒
|
|
3
|
+
cursor?: boolean // 是否显示闪烁光标
|
|
4
|
+
cursorChar?: string // 光标字符
|
|
5
|
+
onComplete?: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TYPE_OPTS: Required<Omit<TypewriterOptions, 'onComplete'>> = {
|
|
9
|
+
speed: 80,
|
|
10
|
+
cursor: true,
|
|
11
|
+
cursorChar: '|',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Typewriter {
|
|
15
|
+
private element: HTMLElement
|
|
16
|
+
private originalText: string = ''
|
|
17
|
+
private timer: ReturnType<typeof setTimeout> | null = null
|
|
18
|
+
private running = false
|
|
19
|
+
|
|
20
|
+
constructor(element: HTMLElement | string) {
|
|
21
|
+
this.element =
|
|
22
|
+
typeof element === 'string'
|
|
23
|
+
? document.querySelector(element)!
|
|
24
|
+
: element
|
|
25
|
+
|
|
26
|
+
if (!this.element) {
|
|
27
|
+
throw new Error('Typewriter: element not found')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.originalText = this.element.textContent ?? ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(options: TypewriterOptions = {}): Promise<void> {
|
|
34
|
+
if (this.running) return
|
|
35
|
+
this.running = true
|
|
36
|
+
|
|
37
|
+
const opts = { ...DEFAULT_TYPE_OPTS, ...options }
|
|
38
|
+
const text = this.originalText || this.element.textContent || ''
|
|
39
|
+
this.element.textContent = ''
|
|
40
|
+
|
|
41
|
+
if (opts.cursor) {
|
|
42
|
+
this.element.style.borderRight = `2px solid currentColor`
|
|
43
|
+
this.element.style.paddingRight = '2px'
|
|
44
|
+
this.startCursorBlink()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let i = 0
|
|
49
|
+
const typeNext = () => {
|
|
50
|
+
if (!this.running) {
|
|
51
|
+
resolve()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (i < text.length) {
|
|
56
|
+
this.element.textContent += text.charAt(i)
|
|
57
|
+
i++
|
|
58
|
+
this.timer = setTimeout(typeNext, opts.speed)
|
|
59
|
+
} else {
|
|
60
|
+
this.running = false
|
|
61
|
+
opts.onComplete?.()
|
|
62
|
+
resolve()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
typeNext()
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stop(): void {
|
|
70
|
+
this.running = false
|
|
71
|
+
if (this.timer) {
|
|
72
|
+
clearTimeout(this.timer)
|
|
73
|
+
this.timer = null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
reset(): void {
|
|
78
|
+
this.stop()
|
|
79
|
+
this.element.textContent = this.originalText
|
|
80
|
+
this.element.style.borderRight = 'none'
|
|
81
|
+
this.element.style.paddingRight = '0'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private startCursorBlink(): void {
|
|
85
|
+
let visible = true
|
|
86
|
+
const blink = () => {
|
|
87
|
+
if (!this.running) {
|
|
88
|
+
this.element.style.borderRightColor = 'transparent'
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
this.element.style.borderRightColor = visible ? 'currentColor' : 'transparent'
|
|
92
|
+
visible = !visible
|
|
93
|
+
setTimeout(blink, 530)
|
|
94
|
+
}
|
|
95
|
+
blink()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 快捷方法
|
|
101
|
+
*/
|
|
102
|
+
export function typewrite(
|
|
103
|
+
element: HTMLElement | string,
|
|
104
|
+
options?: TypewriterOptions
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const tw = new Typewriter(element)
|
|
107
|
+
tw.reset()
|
|
108
|
+
return tw.start(options)
|
|
109
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[data-mk-theme="dark"] {
|
|
2
|
+
--mk-bg: #09090b;
|
|
3
|
+
--mk-surface: #12121a;
|
|
4
|
+
--mk-surface-hover: #1a1a25;
|
|
5
|
+
--mk-border: #1f1f2e;
|
|
6
|
+
--mk-border-hover: #2e2e42;
|
|
7
|
+
|
|
8
|
+
--mk-primary: #6366f1;
|
|
9
|
+
--mk-primary-hover: #818cf8;
|
|
10
|
+
--mk-primary-active: #4f46e5;
|
|
11
|
+
|
|
12
|
+
--mk-success: #22c55e;
|
|
13
|
+
--mk-warning: #f59e0b;
|
|
14
|
+
--mk-danger: #ef4444;
|
|
15
|
+
|
|
16
|
+
--mk-text: #f8fafc;
|
|
17
|
+
--mk-text-secondary: #94a3b8;
|
|
18
|
+
--mk-text-tertiary: #64748b;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[data-mk-theme="light"] {
|
|
2
|
+
--mk-bg: #ffffff;
|
|
3
|
+
--mk-surface: #f8fafc;
|
|
4
|
+
--mk-surface-hover: #f1f5f9;
|
|
5
|
+
--mk-border: #e2e8f0;
|
|
6
|
+
--mk-border-hover: #cbd5e1;
|
|
7
|
+
|
|
8
|
+
--mk-primary: #6366f1;
|
|
9
|
+
--mk-primary-hover: #4f46e5;
|
|
10
|
+
--mk-primary-active: #4338ca;
|
|
11
|
+
|
|
12
|
+
--mk-success: #22c55e;
|
|
13
|
+
--mk-warning: #f59e0b;
|
|
14
|
+
--mk-danger: #ef4444;
|
|
15
|
+
|
|
16
|
+
--mk-text: #0f172a;
|
|
17
|
+
--mk-text-secondary: #475569;
|
|
18
|
+
--mk-text-tertiary: #94a3b8;
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface ThemeConfig {
|
|
2
|
+
bg?: string
|
|
3
|
+
surface?: string
|
|
4
|
+
'surface-hover'?: string
|
|
5
|
+
border?: string
|
|
6
|
+
'border-hover'?: string
|
|
7
|
+
primary?: string
|
|
8
|
+
'primary-hover'?: string
|
|
9
|
+
'primary-active'?: string
|
|
10
|
+
success?: string
|
|
11
|
+
warning?: string
|
|
12
|
+
danger?: string
|
|
13
|
+
text?: string
|
|
14
|
+
'text-secondary'?: string
|
|
15
|
+
'text-tertiary'?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ThemeManager {
|
|
19
|
+
private currentTheme: 'light' | 'dark' = 'dark'
|
|
20
|
+
private listeners = new Set<(theme: 'light' | 'dark') => void>()
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
// 默认暗色
|
|
24
|
+
document.documentElement.setAttribute('data-mk-theme', 'dark')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setTheme(theme: 'light' | 'dark'): void {
|
|
28
|
+
this.currentTheme = theme
|
|
29
|
+
document.documentElement.setAttribute('data-mk-theme', theme)
|
|
30
|
+
this.listeners.forEach((fn) => fn(theme))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toggle(): void {
|
|
34
|
+
this.setTheme(this.currentTheme === 'dark' ? 'light' : 'dark')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getTheme(): 'light' | 'dark' {
|
|
38
|
+
return this.currentTheme
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
applyCustom(config: ThemeConfig): void {
|
|
42
|
+
const root = document.documentElement
|
|
43
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
44
|
+
root.style.setProperty(`--mk-${key}`, value)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resetCustom(): void {
|
|
49
|
+
const root = document.documentElement
|
|
50
|
+
const props = [
|
|
51
|
+
'bg', 'surface', 'surface-hover', 'border', 'border-hover',
|
|
52
|
+
'primary', 'primary-hover', 'primary-active',
|
|
53
|
+
'success', 'warning', 'danger',
|
|
54
|
+
'text', 'text-secondary', 'text-tertiary',
|
|
55
|
+
]
|
|
56
|
+
props.forEach((p) => root.style.removeProperty(`--mk-${p}`))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onChange(fn: (theme: 'light' | 'dark') => void): () => void {
|
|
60
|
+
this.listeners.add(fn)
|
|
61
|
+
return () => this.listeners.delete(fn)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const theme = new ThemeManager()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface BlurRevealOptions {
|
|
2
|
+
duration?: number
|
|
3
|
+
blurStart?: number // 初始模糊像素
|
|
4
|
+
stagger?: number // 子元素间隔
|
|
5
|
+
childSelector?: string // 子元素选择器
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BLUR: Required<Omit<BlurRevealOptions, 'childSelector'>> = {
|
|
9
|
+
duration: 800,
|
|
10
|
+
blurStart: 10,
|
|
11
|
+
stagger: 100,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 元素从模糊到清晰的渐入效果
|
|
16
|
+
*/
|
|
17
|
+
export function blurReveal(
|
|
18
|
+
element: HTMLElement | string,
|
|
19
|
+
options: BlurRevealOptions = {}
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const el =
|
|
22
|
+
typeof element === 'string'
|
|
23
|
+
? document.querySelector<HTMLElement>(element)!
|
|
24
|
+
: element
|
|
25
|
+
|
|
26
|
+
if (!el) throw new Error('blurReveal: element not found')
|
|
27
|
+
|
|
28
|
+
const opts = { ...DEFAULT_BLUR, ...options }
|
|
29
|
+
|
|
30
|
+
el.style.filter = `blur(${opts.blurStart}px)`
|
|
31
|
+
el.style.opacity = '0'
|
|
32
|
+
el.style.transition = `filter ${opts.duration}ms ease, opacity ${opts.duration}ms ease`
|
|
33
|
+
|
|
34
|
+
// 强制重排
|
|
35
|
+
el.offsetHeight
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
el.style.filter = 'blur(0px)'
|
|
39
|
+
el.style.opacity = '1'
|
|
40
|
+
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
el.style.transition = ''
|
|
43
|
+
resolve()
|
|
44
|
+
}, opts.duration)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 子元素依次模糊渐入
|
|
50
|
+
*/
|
|
51
|
+
export function blurRevealChildren(
|
|
52
|
+
element: HTMLElement | string,
|
|
53
|
+
options: BlurRevealOptions = {}
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const el =
|
|
56
|
+
typeof element === 'string'
|
|
57
|
+
? document.querySelector<HTMLElement>(element)!
|
|
58
|
+
: element
|
|
59
|
+
|
|
60
|
+
if (!el) throw new Error('blurRevealChildren: element not found')
|
|
61
|
+
|
|
62
|
+
const opts = { ...DEFAULT_BLUR, ...options }
|
|
63
|
+
const children = opts.childSelector
|
|
64
|
+
? Array.from(el.querySelectorAll(opts.childSelector))
|
|
65
|
+
: Array.from(el.children)
|
|
66
|
+
|
|
67
|
+
children.forEach((child) => {
|
|
68
|
+
const c = child as HTMLElement
|
|
69
|
+
c.style.filter = `blur(${opts.blurStart}px)`
|
|
70
|
+
c.style.opacity = '0'
|
|
71
|
+
c.style.transition = `filter ${opts.duration}ms ease, opacity ${opts.duration}ms ease`
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
el.offsetHeight
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
let done = 0
|
|
78
|
+
children.forEach((child, i) => {
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
const c = child as HTMLElement
|
|
81
|
+
c.style.filter = 'blur(0px)'
|
|
82
|
+
c.style.opacity = '1'
|
|
83
|
+
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
c.style.transition = ''
|
|
86
|
+
done++
|
|
87
|
+
if (done >= children.length) resolve()
|
|
88
|
+
}, opts.duration)
|
|
89
|
+
}, i * opts.stagger)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export interface CollapseOptions {
|
|
2
|
+
duration?: number
|
|
3
|
+
easing?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const DEFAULT_COLLAPSE: Required<CollapseOptions> = {
|
|
7
|
+
duration: 300,
|
|
8
|
+
easing: 'ease-in-out',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 高度展开动画(模拟 Vue 的 collapse-transition)
|
|
13
|
+
*/
|
|
14
|
+
export function expand(
|
|
15
|
+
element: HTMLElement | string,
|
|
16
|
+
options: CollapseOptions = {}
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const el =
|
|
19
|
+
typeof element === 'string'
|
|
20
|
+
? document.querySelector<HTMLElement>(element)!
|
|
21
|
+
: element
|
|
22
|
+
|
|
23
|
+
if (!el) throw new Error('expand: element not found')
|
|
24
|
+
|
|
25
|
+
const opts = { ...DEFAULT_COLLAPSE, ...options }
|
|
26
|
+
|
|
27
|
+
el.style.overflow = 'hidden'
|
|
28
|
+
el.style.display = ''
|
|
29
|
+
const fullHeight = el.scrollHeight
|
|
30
|
+
|
|
31
|
+
el.style.height = '0px'
|
|
32
|
+
el.style.opacity = '0'
|
|
33
|
+
el.style.transition = `height ${opts.duration}ms ${opts.easing}, opacity ${opts.duration}ms ${opts.easing}`
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
el.style.height = `${fullHeight}px`
|
|
38
|
+
el.style.opacity = '1'
|
|
39
|
+
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
el.style.height = ''
|
|
42
|
+
el.style.overflow = ''
|
|
43
|
+
el.style.transition = ''
|
|
44
|
+
el.style.opacity = ''
|
|
45
|
+
resolve()
|
|
46
|
+
}, opts.duration)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 高度折叠动画
|
|
53
|
+
*/
|
|
54
|
+
export function collapse(
|
|
55
|
+
element: HTMLElement | string,
|
|
56
|
+
options: CollapseOptions = {}
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const el =
|
|
59
|
+
typeof element === 'string'
|
|
60
|
+
? document.querySelector<HTMLElement>(element)!
|
|
61
|
+
: element
|
|
62
|
+
|
|
63
|
+
if (!el) throw new Error('collapse: element not found')
|
|
64
|
+
|
|
65
|
+
const opts = { ...DEFAULT_COLLAPSE, ...options }
|
|
66
|
+
const fullHeight = el.scrollHeight
|
|
67
|
+
|
|
68
|
+
el.style.overflow = 'hidden'
|
|
69
|
+
el.style.height = `${fullHeight}px`
|
|
70
|
+
el.style.transition = `height ${opts.duration}ms ${opts.easing}, opacity ${opts.duration}ms ${opts.easing}`
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
requestAnimationFrame(() => {
|
|
74
|
+
el.style.height = '0px'
|
|
75
|
+
el.style.opacity = '0'
|
|
76
|
+
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
el.style.display = 'none'
|
|
79
|
+
el.style.height = ''
|
|
80
|
+
el.style.overflow = ''
|
|
81
|
+
el.style.transition = ''
|
|
82
|
+
el.style.opacity = ''
|
|
83
|
+
resolve()
|
|
84
|
+
}, opts.duration)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 切换展开/折叠
|
|
91
|
+
*/
|
|
92
|
+
export async function toggleCollapse(
|
|
93
|
+
element: HTMLElement | string,
|
|
94
|
+
options?: CollapseOptions
|
|
95
|
+
): Promise<boolean> {
|
|
96
|
+
const el =
|
|
97
|
+
typeof element === 'string'
|
|
98
|
+
? document.querySelector<HTMLElement>(element)!
|
|
99
|
+
: element
|
|
100
|
+
|
|
101
|
+
if (!el) throw new Error('toggleCollapse: element not found')
|
|
102
|
+
|
|
103
|
+
const isHidden = getComputedStyle(el).display === 'none' || el.offsetHeight === 0
|
|
104
|
+
|
|
105
|
+
if (isHidden) {
|
|
106
|
+
await expand(el, options)
|
|
107
|
+
return true
|
|
108
|
+
} else {
|
|
109
|
+
await collapse(el, options)
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export interface LazyImageOptions {
|
|
2
|
+
threshold?: number
|
|
3
|
+
rootMargin?: string
|
|
4
|
+
blur?: boolean // 加载前是否模糊
|
|
5
|
+
placeholder?: string // 占位背景色
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LAZY: Required<LazyImageOptions> = {
|
|
9
|
+
threshold: 0.1,
|
|
10
|
+
rootMargin: '50px',
|
|
11
|
+
blur: true,
|
|
12
|
+
placeholder: '#1e293b',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 图片懒加载:进入视口时加载,并带渐入动画
|
|
17
|
+
*/
|
|
18
|
+
export function lazyImage(
|
|
19
|
+
imgElement: HTMLImageElement | string,
|
|
20
|
+
options: LazyImageOptions = {}
|
|
21
|
+
): () => void {
|
|
22
|
+
const img =
|
|
23
|
+
typeof imgElement === 'string'
|
|
24
|
+
? document.querySelector<HTMLImageElement>(imgElement)!
|
|
25
|
+
: imgElement
|
|
26
|
+
|
|
27
|
+
if (!img) throw new Error('lazyImage: image not found')
|
|
28
|
+
|
|
29
|
+
const opts = { ...DEFAULT_LAZY, ...options }
|
|
30
|
+
const realSrc = img.dataset.src
|
|
31
|
+
|
|
32
|
+
if (!realSrc) {
|
|
33
|
+
console.warn('lazyImage: img 缺少 data-src 属性')
|
|
34
|
+
return () => {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
img.style.background = opts.placeholder
|
|
38
|
+
img.style.transition = 'opacity 0.6s ease, filter 0.6s ease'
|
|
39
|
+
|
|
40
|
+
if (opts.blur) {
|
|
41
|
+
img.style.filter = 'blur(8px)'
|
|
42
|
+
img.style.opacity = '0.5'
|
|
43
|
+
} else {
|
|
44
|
+
img.style.opacity = '0'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const observer = new IntersectionObserver(
|
|
48
|
+
(entries) => {
|
|
49
|
+
entries.forEach((entry) => {
|
|
50
|
+
if (entry.isIntersecting) {
|
|
51
|
+
const image = entry.target as HTMLImageElement
|
|
52
|
+
const src = image.dataset.src!
|
|
53
|
+
|
|
54
|
+
const temp = new Image()
|
|
55
|
+
temp.onload = () => {
|
|
56
|
+
image.src = src
|
|
57
|
+
image.style.opacity = '1'
|
|
58
|
+
image.style.filter = 'blur(0px)'
|
|
59
|
+
image.style.background = ''
|
|
60
|
+
}
|
|
61
|
+
temp.onerror = () => {
|
|
62
|
+
image.style.opacity = '1'
|
|
63
|
+
image.style.filter = 'blur(0px)'
|
|
64
|
+
}
|
|
65
|
+
temp.src = src
|
|
66
|
+
|
|
67
|
+
observer.unobserve(image)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
{ threshold: opts.threshold, rootMargin: opts.rootMargin }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
observer.observe(img)
|
|
75
|
+
|
|
76
|
+
return () => observer.disconnect()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 批量处理页面内所有懒加载图片
|
|
81
|
+
*/
|
|
82
|
+
export function lazyImages(selector: string = 'img[data-src]'): () => void {
|
|
83
|
+
const images = document.querySelectorAll<HTMLImageElement>(selector)
|
|
84
|
+
const cleaners = Array.from(images).map((img) => lazyImage(img))
|
|
85
|
+
|
|
86
|
+
return () => cleaners.forEach((fn) => fn())
|
|
87
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface ListStaggerOptions {
|
|
2
|
+
stagger?: number // 每项间隔毫秒
|
|
3
|
+
duration?: number // 单个动画时长
|
|
4
|
+
animation?: 'fadeUp' | 'fadeDown' | 'fadeLeft' | 'fadeRight' | 'zoom'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LIST: Required<ListStaggerOptions> = {
|
|
8
|
+
stagger: 60,
|
|
9
|
+
duration: 400,
|
|
10
|
+
animation: 'fadeUp',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PRESETS: Record<string, { from: string; to: string }> = {
|
|
14
|
+
fadeUp: {
|
|
15
|
+
from: 'opacity:0;transform:translate3d(0,16px,0)',
|
|
16
|
+
to: 'opacity:1;transform:translate3d(0,0,0)',
|
|
17
|
+
},
|
|
18
|
+
fadeDown: {
|
|
19
|
+
from: 'opacity:0;transform:translate3d(0,-16px,0)',
|
|
20
|
+
to: 'opacity:1;transform:translate3d(0,0,0)',
|
|
21
|
+
},
|
|
22
|
+
fadeLeft: {
|
|
23
|
+
from: 'opacity:0;transform:translate3d(16px,0,0)',
|
|
24
|
+
to: 'opacity:1;transform:translate3d(0,0,0)',
|
|
25
|
+
},
|
|
26
|
+
fadeRight: {
|
|
27
|
+
from: 'opacity:0;transform:translate3d(-16px,0,0)',
|
|
28
|
+
to: 'opacity:1;transform:translate3d(0,0,0)',
|
|
29
|
+
},
|
|
30
|
+
zoom: {
|
|
31
|
+
from: 'opacity:0;transform:scale3d(0.9,0.9,0.9)',
|
|
32
|
+
to: 'opacity:1;transform:scale3d(1,1,1)',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 列表项依次入场动画(类似 Element Plus Table 行入场)
|
|
38
|
+
*/
|
|
39
|
+
export function listStagger(
|
|
40
|
+
container: HTMLElement | string,
|
|
41
|
+
options: ListStaggerOptions = {}
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const el =
|
|
44
|
+
typeof container === 'string'
|
|
45
|
+
? document.querySelector<HTMLElement>(container)!
|
|
46
|
+
: container
|
|
47
|
+
|
|
48
|
+
if (!el) throw new Error('listStagger: container not found')
|
|
49
|
+
|
|
50
|
+
const opts = { ...DEFAULT_LIST, ...options }
|
|
51
|
+
const preset = PRESETS[opts.animation]
|
|
52
|
+
const items = Array.from(el.children) as HTMLElement[]
|
|
53
|
+
|
|
54
|
+
items.forEach((item) => {
|
|
55
|
+
item.style.cssText += preset.from
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
el.offsetHeight // 强制重排
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let done = 0
|
|
62
|
+
items.forEach((item, i) => {
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
item.style.transition = `all ${opts.duration}ms cubic-bezier(0.25, 0.1, 0.25, 1)`
|
|
65
|
+
item.style.cssText += preset.to
|
|
66
|
+
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
item.style.transition = ''
|
|
69
|
+
done++
|
|
70
|
+
if (done >= items.length) resolve()
|
|
71
|
+
}, opts.duration)
|
|
72
|
+
}, i * opts.stagger)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
}
|