diff --git a/packages/varlet-shared/package.json b/packages/varlet-shared/package.json
index a513a18a26f..c7380024c32 100644
--- a/packages/varlet-shared/package.json
+++ b/packages/varlet-shared/package.json
@@ -37,7 +37,7 @@
"build": "tsup src/index.ts --format esm,cjs --out-dir=lib --dts --clean"
},
"dependencies": {
- "rattail": "1.0.0"
+ "rattail": "1.0.12"
},
"devDependencies": {
"@types/node": "^18.7.18",
diff --git a/packages/varlet-shared/src/index.ts b/packages/varlet-shared/src/index.ts
index 939ca93a374..a39a5949eed 100644
--- a/packages/varlet-shared/src/index.ts
+++ b/packages/varlet-shared/src/index.ts
@@ -1,2 +1,3 @@
-export * from './is.js'
+export * from './is'
+export * from './motion'
export * from 'rattail'
diff --git a/packages/varlet-shared/src/motion.ts b/packages/varlet-shared/src/motion.ts
new file mode 100644
index 00000000000..c1b7f73d519
--- /dev/null
+++ b/packages/varlet-shared/src/motion.ts
@@ -0,0 +1,110 @@
+import { requestAnimationFrame, clamp, cancelAnimationFrame } from 'rattail'
+
+export interface MotionOptions {
+ from: number
+ to: number
+ duration?: number
+ frame?: ({ value, done }: { value: number; done: boolean }) => void
+ timingFunction?: (v: number) => number
+ onStateChange?: (state: MotionState) => void
+}
+
+export type MotionState = 'running' | 'paused' | 'pending' | 'finished'
+
+export interface Motion {
+ state: MotionState
+ start: () => void
+ pause: () => void
+ reset: () => void
+}
+
+export function motion(options: MotionOptions) {
+ const {
+ from,
+ to,
+ duration = 300,
+ frame = () => {},
+ timingFunction = (value) => value,
+ onStateChange = () => {},
+ } = options
+
+ let state: MotionState = 'pending'
+ let value = from
+ const distance = to - from
+ let ticker: number | undefined = undefined
+ let startTime: number | undefined = undefined
+ let pausedTime: number | undefined = undefined
+ let sleepTime: number = 0
+
+ function start() {
+ if (state === 'running' || state === 'finished') {
+ return
+ }
+
+ setState('running')
+
+ const now = performance.now()
+ startTime = startTime != null ? startTime : now
+ sleepTime += pausedTime != null ? now - pausedTime : 0
+ pausedTime = undefined
+ tick()
+
+ function tick() {
+ ticker = requestAnimationFrame(() => {
+ if (state !== 'running') {
+ return
+ }
+
+ const now = performance.now()
+ const executionTime = now - startTime! - sleepTime
+ const progress = clamp(executionTime / duration, 0, 1)
+ value = distance * timingFunction(progress) + from
+
+ if (progress >= 1) {
+ setState('finished')
+ frame({ value: to, done: true })
+ return
+ }
+
+ frame({ value, done: false })
+ tick()
+ })
+ }
+ }
+
+ function pause() {
+ if (state !== 'running') {
+ return
+ }
+
+ setState('paused')
+ pausedTime = performance.now()
+ cancelAnimationFrame(ticker!)
+ }
+
+ function reset() {
+ cancelAnimationFrame(ticker!)
+ setState('pending')
+ value = from
+ ticker = undefined
+ startTime = undefined
+ pausedTime = undefined
+ sleepTime = 0
+ }
+
+ function getState() {
+ return state
+ }
+
+ function setState(_state: MotionState) {
+ state = _state
+ onStateChange(_state)
+ }
+
+ return {
+ start,
+ pause,
+ reset,
+ getState,
+ }
+}
diff --git a/packages/varlet-ui/src/count-to/CountTo.vue b/packages/varlet-ui/src/count-to/CountTo.vue
new file mode 100644
index 00000000000..0c9252cd063
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/CountTo.vue
@@ -0,0 +1,51 @@
+
+
+
{{ value }}: {{ state }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/varlet-ui/src/count-to/__tests__/index.spec.js b/packages/varlet-ui/src/count-to/__tests__/index.spec.js
new file mode 100644
index 00000000000..6bfa7e199a2
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/__tests__/index.spec.js
@@ -0,0 +1,8 @@
+import CountTo from '..'
+import { createApp } from 'vue'
+import { expect, test } from 'vitest'
+
+test('test count-to plugin', () => {
+ const app = createApp({}).use(CountTo)
+ expect(app.component(CountTo.name)).toBeTruthy()
+})
diff --git a/packages/varlet-ui/src/count-to/countTo.less b/packages/varlet-ui/src/count-to/countTo.less
new file mode 100644
index 00000000000..84273c1554b
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/countTo.less
@@ -0,0 +1,2 @@
+.var-count-to {
+}
diff --git a/packages/varlet-ui/src/count-to/docs/en-US.md b/packages/varlet-ui/src/count-to/docs/en-US.md
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/varlet-ui/src/count-to/docs/zh-CN.md b/packages/varlet-ui/src/count-to/docs/zh-CN.md
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/varlet-ui/src/count-to/example/index.vue b/packages/varlet-ui/src/count-to/example/index.vue
new file mode 100644
index 00000000000..6acdf74149a
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/example/index.vue
@@ -0,0 +1,11 @@
+
+
+
+ Mobile phone example code
+
+
diff --git a/packages/varlet-ui/src/count-to/example/locale/en-US.ts b/packages/varlet-ui/src/count-to/example/locale/en-US.ts
new file mode 100644
index 00000000000..cbec61de2fa
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/example/locale/en-US.ts
@@ -0,0 +1,3 @@
+export default {
+ // Example locale
+}
diff --git a/packages/varlet-ui/src/count-to/example/locale/index.ts b/packages/varlet-ui/src/count-to/example/locale/index.ts
new file mode 100644
index 00000000000..8855457aa5c
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/example/locale/index.ts
@@ -0,0 +1,17 @@
+import { Locale } from '@varlet/ui'
+import zhCN from './zh-CN'
+import enUS from './en-US'
+
+const { add, use: exampleUse, t, merge } = Locale.useLocale()
+
+const use = (lang: string) => {
+ Locale.use(lang)
+ exampleUse(lang)
+}
+
+Locale.add('zh-CN', Locale.zhCN)
+Locale.add('en-US', Locale.enUS)
+add('zh-CN', zhCN)
+add('en-US', enUS)
+
+export { add, t, merge, use }
diff --git a/packages/varlet-ui/src/count-to/example/locale/zh-CN.ts b/packages/varlet-ui/src/count-to/example/locale/zh-CN.ts
new file mode 100644
index 00000000000..cbec61de2fa
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/example/locale/zh-CN.ts
@@ -0,0 +1,3 @@
+export default {
+ // Example locale
+}
diff --git a/packages/varlet-ui/src/count-to/index.ts b/packages/varlet-ui/src/count-to/index.ts
new file mode 100644
index 00000000000..d3b8ea52e48
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/index.ts
@@ -0,0 +1,12 @@
+import CountTo from './CountTo.vue'
+import { withInstall, withPropsDefaultsSetter } from '../utils/components'
+import { props as countToProps } from './props'
+
+withInstall(CountTo)
+withPropsDefaultsSetter(CountTo, countToProps)
+
+export { countToProps }
+
+export const _CountToComponent = CountTo
+
+export default CountTo
diff --git a/packages/varlet-ui/src/count-to/props.ts b/packages/varlet-ui/src/count-to/props.ts
new file mode 100644
index 00000000000..455f4337f46
--- /dev/null
+++ b/packages/varlet-ui/src/count-to/props.ts
@@ -0,0 +1 @@
+export const props = {}
diff --git a/packages/varlet-ui/tsconfig.json b/packages/varlet-ui/tsconfig.json
index 482daeeb6e0..703da671663 100644
--- a/packages/varlet-ui/tsconfig.json
+++ b/packages/varlet-ui/tsconfig.json
@@ -8,5 +8,5 @@
"allowJs": true,
"types": ["vitest/globals"]
},
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "types/countTo.d.ts"]
}
diff --git a/packages/varlet-ui/types/countTo.d.ts b/packages/varlet-ui/types/countTo.d.ts
new file mode 100644
index 00000000000..091866ad7c4
--- /dev/null
+++ b/packages/varlet-ui/types/countTo.d.ts
@@ -0,0 +1,13 @@
+import { VarComponent, BasicAttributes, SetPropsDefaults } from './varComponent'
+
+export declare const countToProps: Record
+
+export interface CountToProps extends BasicAttributes {}
+
+export class CountTo extends VarComponent {
+ static setPropsDefaults: SetPropsDefaults
+
+ $props: CountToProps
+}
+
+export class _CountToComponent extends CountTo {}
diff --git a/packages/varlet-ui/varlet.config.mjs b/packages/varlet-ui/varlet.config.mjs
index 07b6452e1f5..b69a40c80ac 100644
--- a/packages/varlet-ui/varlet.config.mjs
+++ b/packages/varlet-ui/varlet.config.mjs
@@ -263,6 +263,14 @@ export default defineConfig({
},
type: 1,
},
+ {
+ text: {
+ 'zh-CN': 'CountTo 数字动画',
+ 'en-US': 'CountTo',
+ },
+ doc: 'count-to',
+ type: 2,
+ },
{
text: {
'zh-CN': 'Skeleton 骨架屏',
diff --git a/packages/varlet-use/src/index.ts b/packages/varlet-use/src/index.ts
index 32c378af620..1b7134e81a0 100644
--- a/packages/varlet-use/src/index.ts
+++ b/packages/varlet-use/src/index.ts
@@ -1,13 +1,14 @@
-export * from './useEventListener.js'
-export * from './useClickOutside.js'
-export * from './onSmartMounted.js'
-export * from './onSmartUnmounted.js'
-export * from './useParent.js'
-export * from './useChildren.js'
-export * from './onWindowResize.js'
-export * from './useInitialized.js'
-export * from './useTouch.js'
-export * from './useId.js'
-export * from './useClientId.js'
-export * from './useWindowSize.js'
-export * from './useVModel.js'
+export * from './useEventListener'
+export * from './useClickOutside'
+export * from './onSmartMounted'
+export * from './onSmartUnmounted'
+export * from './useParent'
+export * from './useChildren'
+export * from './onWindowResize'
+export * from './useInitialized'
+export * from './useTouch'
+export * from './useId'
+export * from './useClientId'
+export * from './useWindowSize'
+export * from './useVModel'
+export * from './useMotion'
diff --git a/packages/varlet-use/src/useMotion.ts b/packages/varlet-use/src/useMotion.ts
new file mode 100644
index 00000000000..9c2c0d13f4f
--- /dev/null
+++ b/packages/varlet-use/src/useMotion.ts
@@ -0,0 +1,40 @@
+import { MotionState, motion } from '@varlet/shared'
+import { ref } from 'vue'
+
+export interface UseMotionOptions {
+ from: number
+ to: number
+ duration?: number
+ timingFunction?: (v: number) => number
+ onFinished?: (value: number) => void
+}
+
+export function useMotion(options: UseMotionOptions) {
+ const value = ref(options.from)
+ const state = ref('pending')
+ const { start, reset, pause, getState } = motion({
+ ...options,
+ frame: ({ value: newValue, done }) => {
+ value.value = newValue
+
+ if (done) {
+ options.onFinished?.(value.value)
+ }
+ },
+ onStateChange(newState) {
+ state.value = newState
+ },
+ })
+
+ return {
+ value,
+ state,
+ start,
+ pause,
+ getState,
+ reset: () => {
+ value.value = options.from
+ reset()
+ },
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 105db3c7921..a5a625b92b6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -274,8 +274,8 @@ importers:
packages/varlet-shared:
dependencies:
rattail:
- specifier: 1.0.0
- version: 1.0.0
+ specifier: 1.0.12
+ version: 1.0.12
devDependencies:
'@types/node':
specifier: ^18.7.18
@@ -4242,8 +4242,8 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
- rattail@1.0.0:
- resolution: {integrity: sha512-ccYN21TuJWxP4/XI0BfYcc9NFE+wvNk5tT4J4oyYMG3vUryiW218NZdXAbtp6C1L4IZLjdCW/dFz1Ym4WwBGFA==}
+ rattail@1.0.12:
+ resolution: {integrity: sha512-LOWNF0ZXzJ9O7MOfkBZ6lGJYjqk/zrBLSJ9Du+XjNpf6COfyMwbkKnQEIArmpG8iSDX9DK0ww+yE3N35IYIuHw==}
engines: {pnpm: '>=9.0'}
rc@1.2.8:
@@ -9302,7 +9302,7 @@ snapshots:
range-parser@1.2.1: {}
- rattail@1.0.0:
+ rattail@1.0.12:
dependencies:
mitt: 3.0.1