diff --git a/examples/sites/demos/apis/number-animation.js b/examples/sites/demos/apis/number-animation.js
new file mode 100644
index 000000000..11f9069b5
--- /dev/null
+++ b/examples/sites/demos/apis/number-animation.js
@@ -0,0 +1,104 @@
+export default {
+ mode: ['pc'],
+ apis: [
+ {
+ name: 'number-animation',
+ type: 'component',
+ props: [
+ {
+ name: 'active',
+ type: 'boolean',
+ defaultValue: 'true',
+ desc: {
+ 'zh-CN': '是否开始动画',
+ 'en-US': 'Whether or not start animation'
+ },
+ mode: ['pc'],
+ pcDemo: 'basic-usage'
+ },
+ {
+ name: 'duration',
+ type: 'number',
+ defaultValue: '3000',
+ desc: {
+ 'zh-CN': '动画持续时间',
+ 'en-US': 'Animation duration'
+ },
+ mode: ['pc'],
+ pcDemo: 'basic-usage'
+ },
+ {
+ name: 'from',
+ type: 'number',
+ defaultValue: '0',
+ desc: {
+ 'zh-CN': '数值动画起始值',
+ 'en-US': 'Starting value of numerical animation'
+ },
+ mode: ['pc'],
+ pcDemo: 'basic-usage'
+ },
+ {
+ name: 'to',
+ type: 'number',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '目标值',
+ 'en-US': 'Target value'
+ },
+ mode: ['pc'],
+ pcDemo: 'basic-usage'
+ },
+ {
+ name: 'precision',
+ type: 'number',
+ defaultValue: '0',
+ desc: {
+ 'zh-CN': '精度,保留小数点后几位',
+ 'en-US': 'Precision, rounded to a few decimal places.'
+ },
+ mode: ['pc'],
+ pcDemo: 'precision'
+ },
+ {
+ name: 'separator',
+ type: 'string',
+ defaultValue: ',',
+ desc: {
+ 'zh-CN': '千分位分隔符',
+ 'en-US': 'Thousandth separator'
+ },
+ mode: ['pc'],
+ pcDemo: 'separator'
+ }
+ ],
+ events: [
+ {
+ name: 'finish',
+ type: '() => void',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '动画结束后的回调',
+ 'en-US': 'The callback after the animation ends.'
+ },
+ mode: ['pc'],
+ pcDemo: 'finish-events'
+ }
+ ],
+ methods: [
+ {
+ name: 'play',
+ type: '() => void',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '播放动画',
+ 'en-US': 'Play Animation'
+ },
+ mode: ['pc'],
+ pcDemo: 'finish-events'
+ }
+ ],
+ slots: []
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/app/number-animation/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/number-animation/basic-usage-composition-api.vue
new file mode 100644
index 000000000..e9d7e4017
--- /dev/null
+++ b/examples/sites/demos/pc/app/number-animation/basic-usage-composition-api.vue
@@ -0,0 +1,16 @@
+
+
from
设置数值动画起始值;to
设置目标值。',
+ 'en-US': 'Set the starting value of numerical animation throughfrom
, to
设置目标值.'
+ },
+ codeFiles: ['basic-usage.vue']
+ },
+ {
+ demoId: 'precision',
+ name: {
+ 'zh-CN': '精度',
+ 'en-US': 'Precision Mode'
+ },
+ desc: {
+ 'zh-CN': '通过 precision
设置 设定精度。',
+ 'en-US': 'Set precision throughprecision
.'
+ },
+ codeFiles: ['precision.vue']
+ },
+ {
+ demoId: 'separator',
+ name: {
+ 'zh-CN': '分隔符',
+ 'en-US': 'Separator Mode'
+ },
+ desc: {
+ 'zh-CN': '通过 separator
设置分隔符。',
+ 'en-US': 'Set delimiter throughseparator
.'
+ },
+ codeFiles: ['separator.vue']
+ },
+
+ {
+ demoId: 'finish-events',
+ name: {
+ 'zh-CN': '动画结束事件',
+ 'en-US': 'Finish Event'
+ },
+ desc: {
+ 'zh-CN': '通过 finish
自定义动画结束后的事件',
+ 'en-US': 'Customize the events after the animation ends throughfinish
.'
+ },
+ codeFiles: ['finish-events.vue']
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js
index 9e015c0ad..3b0158016 100644
--- a/examples/sites/demos/pc/menus.js
+++ b/examples/sites/demos/pc/menus.js
@@ -249,7 +249,8 @@ export const cmpMenus = [
}
},
{ 'nameCn': '用户头像', 'name': 'UserHead', 'key': 'user-head' },
- { 'nameCn': '流程图', 'name': 'Wizard', 'key': 'wizard' }
+ { 'nameCn': '流程图', 'name': 'Wizard', 'key': 'wizard' },
+ { 'nameCn': '数值动画', 'name': 'NumberAnimation', key: 'number-animation' }
]
},
{
diff --git a/packages/modules.json b/packages/modules.json
index 32e14e0e2..8077ac1b2 100644
--- a/packages/modules.json
+++ b/packages/modules.json
@@ -1708,6 +1708,19 @@
"type": "template",
"exclude": false
},
+ "NumberAnimation": {
+ "path": "vue/src/number-animation/index.ts",
+ "type": "component",
+ "exclude": false,
+ "mode": [
+ "pc"
+ ]
+ },
+ "NumberAnimationPc": {
+ "path": "vue/src/number-animation/src/pc.vue",
+ "type": "template",
+ "exclude": false
+ },
"Option": {
"path": "vue/src/option/index.ts",
"type": "component",
diff --git a/packages/renderless/src/number-animation/index.ts b/packages/renderless/src/number-animation/index.ts
new file mode 100644
index 000000000..8f808ece3
--- /dev/null
+++ b/packages/renderless/src/number-animation/index.ts
@@ -0,0 +1,55 @@
+export const onFinish =
+ ({ emit, props, state }) =>
+ () => {
+ state.value = props.to
+ state.animating = false
+ emit('finish')
+ }
+
+const easeOut = (t: number): number => 1 - (1 - t) ** 5
+
+export const play =
+ ({ props, state, api }) =>
+ () => {
+ animate(state, props, api)
+ }
+export const animate = (state, props, api) => {
+ state.animating = true
+ state.value = props.from
+ if (props.from !== props.to) {
+ const startTime = performance.now()
+
+ const tick = () => {
+ const current = performance.now()
+ const elapsedTime = Math.min(current - startTime, props.duration)
+ const currentValue = props.from + (props.to - props.from) * easeOut(elapsedTime / props.duration)
+ if (elapsedTime === props.duration) {
+ api.onFinish()
+ return
+ }
+ state.value = currentValue
+ requestAnimationFrame(tick)
+ }
+ tick()
+ }
+}
+
+export const formattedValue =
+ ({ state, props }) =>
+ () => {
+ // 类型检查
+ if (typeof state.value !== 'number' && typeof state.value !== 'string') return
+ if (typeof props.precision !== 'number') return
+ const numValue = Number(state.value)
+ if (isNaN(numValue) || !isFinite(numValue)) return
+ if (numValue === 0) {
+ return numValue.toFixed(props.precision)
+ }
+ let formatValue = numValue.toFixed(props.precision)
+ if (typeof props.separator === 'string' && props.separator !== '') {
+ const [integerPart, decimalPart] = formatValue.split('.')
+ formatValue =
+ integerPart.replace(/(\d)(?=(\d{3})+$)/g, '$1' + props.separator) + (decimalPart ? '.' + decimalPart : '')
+ }
+ return formatValue
+ }
diff --git a/packages/renderless/src/number-animation/vue.ts b/packages/renderless/src/number-animation/vue.ts
new file mode 100644
index 000000000..9e0f990b9
--- /dev/null
+++ b/packages/renderless/src/number-animation/vue.ts
@@ -0,0 +1,26 @@
+import { play, formattedValue, onFinish } from './index'
+
+export const api = ['state', 'play', 'formattedValue', 'onFinish']
+export const renderless = (props, { onMounted, computed, reactive }, { emit }) => {
+ const api = {}
+
+ const state = reactive({
+ animating: true,
+ value: props.from,
+ showValue: computed(() => api.formattedValue(state, props))
+ })
+
+ onMounted(() => {
+ if (props.active) {
+ api.play(props, state)
+ }
+ })
+ Object.assign(api, {
+ state,
+ play: play({ props, state, api }),
+ formattedValue: formattedValue({ state, props }),
+ onFinish: onFinish({ emit, props, state })
+ })
+
+ return api
+}
diff --git a/packages/theme/src/number-animation/index.less b/packages/theme/src/number-animation/index.less
new file mode 100644
index 000000000..1ab06975c
--- /dev/null
+++ b/packages/theme/src/number-animation/index.less
@@ -0,0 +1,11 @@
+@import '../custom.less';
+@import './vars.less';
+
+@number-animation-item-prefix-cls: ~'@{css-prefix}number-animation';
+
+.@{number-animation-item-prefix-cls} {
+ .inject-NumberAnimation-vars();
+ font-size: var(--tv-NumberAnimation-font-size);
+ font-weight: var(--tv-NumberAnimation-font-weight);
+ margin-bottom: var(--tv-NumberAnimation-margin-bottom);
+}
diff --git a/packages/theme/src/number-animation/vars.less b/packages/theme/src/number-animation/vars.less
new file mode 100644
index 000000000..8d459188f
--- /dev/null
+++ b/packages/theme/src/number-animation/vars.less
@@ -0,0 +1,8 @@
+.inject-NumberAnimation-vars() {
+ // 数字内容下间距
+ --tv-NumberAnimation-margin-bottom: 20px;
+ // 数字内容字体粗细
+ --tv-NumberAnimation-font-weight: var(--tv-font-weight-regular);
+ // 数字内容字体
+ --tv-NumberAnimation-font-size: var(--tv-font-size-heading-lg);
+ }
\ No newline at end of file
diff --git a/packages/theme/src/table/index.less b/packages/theme/src/table/index.less
index bc97332f5..c6dd1d3e1 100644
--- a/packages/theme/src/table/index.less
+++ b/packages/theme/src/table/index.less
@@ -118,7 +118,7 @@
font-size: var(--tv-Table-icon-font-size);
border-radius: var(--tv-Table-check-icon-border-radius);
& path:last-child{
- fill: var(--tv-Table-border-color);
+ fill: var(--tv-Table-icon-border-color);
}
& path:first-child {
diff --git a/packages/theme/src/table/vars.less b/packages/theme/src/table/vars.less
index fd1c64693..866a291d5 100644
--- a/packages/theme/src/table/vars.less
+++ b/packages/theme/src/table/vars.less
@@ -28,7 +28,9 @@
// 表格单元格字体大小
--tv-Table-td-font-size: var(--tv-font-size-default, 14px);
// 表格边框颜色
- --tv-Table-border-color: var(--tv-color-border-divider, #f0f0f0);
+ --tv-Table-border-color: var(--tv-color-border-divider);
+ // 表格复选框边框颜色
+ --tv-Table-icon-border-color: var(--tv-color-border);
// 表头背景颜色
--tv-Table-thead-bg-color: var(--tv-color-bg-header, #f5f5f5);
// 表格图标字体大小
diff --git a/packages/vue/package.json b/packages/vue/package.json
index 1f43158bc..f1f2f7577 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -137,6 +137,7 @@
"@opentiny/vue-month-table": "workspace:~",
"@opentiny/vue-nav-menu": "workspace:~",
"@opentiny/vue-notify": "workspace:~",
+ "@opentiny/vue-number-animation": "workspace:~",
"@opentiny/vue-numeric": "workspace:~",
"@opentiny/vue-option": "workspace:~",
"@opentiny/vue-option-group": "workspace:~",
diff --git a/packages/vue/src/number-animation/__tests__/number-animation.test.tsx b/packages/vue/src/number-animation/__tests__/number-animation.test.tsx
new file mode 100644
index 000000000..b526946df
--- /dev/null
+++ b/packages/vue/src/number-animation/__tests__/number-animation.test.tsx
@@ -0,0 +1,3 @@
+import { describe } from 'vitest'
+
+describe('PC Mode', () => {})
diff --git a/packages/vue/src/number-animation/index.ts b/packages/vue/src/number-animation/index.ts
new file mode 100644
index 000000000..5a00936e7
--- /dev/null
+++ b/packages/vue/src/number-animation/index.ts
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
+ *
+ * Use of this source code is governed by an MIT-style license.
+ *
+ * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+ * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
+ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+ *
+ */
+import NumberAnimation from './src/index'
+import '@opentiny/vue-theme/number-animation/index.less'
+import { version } from './package.json'
+
+/* istanbul ignore next */
+NumberAnimation.install = function (Vue) {
+ Vue.component(NumberAnimation.name, NumberAnimation)
+}
+
+NumberAnimation.version = version
+
+/* istanbul ignore next */
+if (process.env.BUILD_TARGET === 'runtime') {
+ if (typeof window !== 'undefined' && window.Vue) {
+ NumberAnimation.install(window.Vue)
+ }
+}
+
+export default NumberAnimation
diff --git a/packages/vue/src/number-animation/package.json b/packages/vue/src/number-animation/package.json
new file mode 100644
index 000000000..a32ed5b4c
--- /dev/null
+++ b/packages/vue/src/number-animation/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@opentiny/vue-number-animation",
+ "type": "module",
+ "version": "3.20.0",
+ "description": "",
+ "license": "MIT",
+ "sideEffects": false,
+ "main": "lib/index.js",
+ "module": "index.ts",
+ "scripts": {
+ "build": "pnpm -w build:ui $npm_package_name",
+ "//postversion": "pnpm build"
+ },
+ "dependencies": {
+ "@opentiny/vue-button": "workspace:~",
+ "@opentiny/vue-common": "workspace:~",
+ "@opentiny/vue-modal": "workspace:~",
+ "@opentiny/vue-renderless": "workspace:~",
+ "@opentiny/vue-statistic": "workspace:~",
+ "@opentiny/vue-theme": "workspace:~"
+ },
+ "devDependencies": {
+ "@opentiny-internal/vue-test-utils": "workspace:*",
+ "vitest": "catalog:"
+ }
+}
\ No newline at end of file
diff --git a/packages/vue/src/number-animation/src/index.ts b/packages/vue/src/number-animation/src/index.ts
new file mode 100644
index 000000000..eb1a20181
--- /dev/null
+++ b/packages/vue/src/number-animation/src/index.ts
@@ -0,0 +1,46 @@
+import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
+import template from 'virtual-template?pc'
+
+export const $constants = {
+ PREFIX: 'tiny-number-animation'
+}
+
+export const numberAnimationProps = {
+ ...$props,
+ _constants: {
+ type: Object,
+ default: () => $constants
+ },
+ to: {
+ type: Number,
+ default: 0
+ },
+ precision: {
+ type: Number,
+ default: 0
+ },
+ separator: {
+ type: String,
+ default: ','
+ },
+ from: {
+ type: Number,
+ default: 0
+ },
+ active: {
+ type: Boolean,
+ default: true
+ },
+ duration: {
+ type: Number,
+ default: 2000
+ }
+}
+
+export default defineComponent({
+ name: $prefix + 'NumberAnimation',
+ props: numberAnimationProps,
+ setup(props, context) {
+ return $setup({ props, context, template })
+ }
+})
diff --git a/packages/vue/src/number-animation/src/pc.vue b/packages/vue/src/number-animation/src/pc.vue
new file mode 100644
index 000000000..79518877b
--- /dev/null
+++ b/packages/vue/src/number-animation/src/pc.vue
@@ -0,0 +1,29 @@
+
+
+