diff --git a/packages/devui-vue/devui/statistic/__tests__/statistic.spec.ts b/packages/devui-vue/devui/statistic/__tests__/statistic.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb9c9e8903d8935a48e5f2b5946f2dd62edeac24 --- /dev/null +++ b/packages/devui-vue/devui/statistic/__tests__/statistic.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils' +import { Statistic } from '../index' + +describe('statistic test', () => { + it('statistic init render', async () => { + // todo + }) +}) diff --git a/packages/devui-vue/devui/statistic/index.ts b/packages/devui-vue/devui/statistic/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..116831ee7fa9e81dfad9691db365ec9d382c0145 --- /dev/null +++ b/packages/devui-vue/devui/statistic/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import Statistic from './src/statistic' + +Statistic.install = function(app: App): void { + app.component(Statistic.name, Statistic) +} + +export { Statistic } + +export default { + title: 'Statistic 统计数值', + category: '数据展示', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Statistic as any) + } +} diff --git a/packages/devui-vue/devui/statistic/src/statistic-types.ts b/packages/devui-vue/devui/statistic/src/statistic-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..db27965c2a682798630622fedb5a1bf1ddc0d0b7 --- /dev/null +++ b/packages/devui-vue/devui/statistic/src/statistic-types.ts @@ -0,0 +1,63 @@ +import type { PropType, ExtractPropTypes, CSSProperties } from 'vue' +import type { easingType } from './utils/animation' +export const statisticProps = { + title: { + type: String, + default: '' + }, + value: { + type: [Number, String] + }, + prefix: { + type: String + }, + suffix: { + type: String + }, + precision: { + type: Number + }, + groupSeparator: { + type: String, + default: ',' + }, + showGroupSeparator: { + type: Boolean, + default: false + }, + titleStyle: { + type: Object as PropType + }, + contentStyle: { + type: Object as PropType + }, + animationDuration: { + type: Number, + default: 2000 + }, + valueFrom: { + type: Number + }, + animation: { + type: Boolean, + default: false + }, + start: { + type: Boolean, + default: false + }, + extra: { + type: String, + default: '' + }, + easing: { + type: String as PropType, + default: 'easeOutCubic' + }, + delay: { + type: Number, + default: 0 + } +} as const + +export type StatisticProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/statistic/src/statistic.scss b/packages/devui-vue/devui/statistic/src/statistic.scss new file mode 100644 index 0000000000000000000000000000000000000000..a619e6ce5c2c119d0cc0160ff82ad1796237c4b7 --- /dev/null +++ b/packages/devui-vue/devui/statistic/src/statistic.scss @@ -0,0 +1,34 @@ +.devui-statistic { + box-sizing: border-box; + margin: 0; + padding: 0; + font-size: 14px; + font-variant: tabular-nums; + line-height: 1.5715; + list-style: none; + + &-title { + margin-bottom: 4 px; + opacity: 0.7; + font-size: 14px; + } + + &-content { + font-size: 24px; + display: flex; + align-items: center; + vertical-align: center; + } + + &-prefix { + margin-right: 6px; + } + + &-suffix { + margin-left: 6px; + } + + &--value { + display: inline-block; + } +} diff --git a/packages/devui-vue/devui/statistic/src/statistic.tsx b/packages/devui-vue/devui/statistic/src/statistic.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e57ebf1127858cfabba21b0e4b83618caba7cdbd --- /dev/null +++ b/packages/devui-vue/devui/statistic/src/statistic.tsx @@ -0,0 +1,85 @@ +import { defineComponent, computed, ref, onMounted, watch } from 'vue' +import { statisticProps, StatisticProps } from './statistic-types' +import { analysisValueType } from './utils/separator' +import { Tween } from './utils/animation' +import './statistic.scss' + +export default defineComponent({ + name: 'DStatistic', + props: statisticProps, + inheritAttrs: false, + setup(props: StatisticProps, ctx) { + const innerValue = ref(props.valueFrom ?? props.value) + const tween = ref(null) + + const animation = ( + from: number = props.valueFrom ?? 0, + to: number = typeof props.value === 'number' ? props.value : Number(props.value) + ) => { + if (from !== to) { + tween.value = new Tween({ + from: { + value: from + }, + to: { + value: to + }, + delay: props.delay, + duration: props.animationDuration, + easing: props.easing, + onUpdate: (keys: any) => { + innerValue.value = keys.value + }, + onFinish: () => { + innerValue.value = to + } + }) + tween.value.start() + } + } + + const statisticValue = computed(() => { + return analysisValueType( + innerValue.value, + props.value, + props.groupSeparator, + props.precision, + props.showGroupSeparator, + props.animation + ) + }) + onMounted(() => { + if (props.animation && props.start) { + animation() + } + }) + + watch( + () => props.start, + (value) => { + if (value && !tween.value) { + animation() + } + } + ) + return () => { + return ( +
+
+ {ctx.slots.title?.() || props.title} +
+
+ {props.prefix || ctx.slots.prefix?.() ? ( + {ctx.slots.prefix?.() || props.prefix} + ) : null} + {statisticValue.value} + {props.suffix || ctx.slots.suffix?.() ? ( + {ctx.slots.suffix?.() || props.suffix} + ) : null} +
+ {ctx.slots.extra?.() || props.extra} +
+ ) + } + } +}) diff --git a/packages/devui-vue/devui/statistic/src/utils/animation.ts b/packages/devui-vue/devui/statistic/src/utils/animation.ts new file mode 100644 index 0000000000000000000000000000000000000000..778f51d51b7e08f0f8146ecd147fd841c2806305 --- /dev/null +++ b/packages/devui-vue/devui/statistic/src/utils/animation.ts @@ -0,0 +1,129 @@ +import * as easing from './easing' + +export type easingType = 'easeOutCubic' | 'linear' | 'easeOutExpo' | 'easeInOutExpo' +export interface startFunc { + (key: number): number +} +export interface updateFunc { + (key: any): any +} +export interface finishFunc { + (key: any): any +} +export interface fromType { + value: number +} +export interface toType { + value: number +} +export interface AnimationOptions { + from: fromType + to: toType + duration?: number + delay?: number + easing?: easingType + onStart?: startFunc + onUpdate?: updateFunc + onFinish?: finishFunc +} + +export class Tween { + from: fromType + to: toType + duration?: number + delay?: number + easing?: easingType + onStart?: startFunc + onUpdate?: updateFunc + onFinish?: finishFunc + startTime?: number + started?: boolean + finished?: boolean + timer?: null | number + time?: number + elapsed?: number + keys?: any + constructor(options: AnimationOptions) { + const { from, to, duration, delay, easing, onStart, onUpdate, onFinish } = options + for (const key in from) { + if (to[key] === undefined) { + to[key] = from[key] + } + } + + for (const key in to) { + if (from[key] === undefined) { + from[key] = to[key] + } + } + + this.from = from + this.to = to + this.duration = duration + this.delay = delay + this.easing = easing + this.onStart = onStart + this.onUpdate = onUpdate + this.onFinish = onFinish + this.startTime = Date.now() + this.delay + this.started = false + this.finished = false + this.timer = null + this.keys = {} + } + + update() { + this.time = Date.now() + // delay some time + if (this.time < this.startTime) { + return + } + if (this.finished) { + return + } + // finish animation + if (this.elapsed === this.duration) { + if (!this.finished) { + this.finished = true + this.onFinish && this.onFinish(this.keys) + } + return + } + // elapsed 时间 和 duration 时间比较 逝去光阴 + this.elapsed = this.time - this.startTime + // 防止 时间 一直 流逝 ~ + this.elapsed = this.elapsed > this.duration ? this.duration : this.elapsed + // 从0 到 1 elapsed time + for (const key in this.to) { + this.keys[key] = + this.from[key] + + (this.to[key] - this.from[key]) * easing[this.easing](this.elapsed / this.duration) + } + if (!this.started) { + this.onStart && this.onStart(this.keys) + this.started = true + } + this.onUpdate(this.keys) + } + + // 递归 重绘 + start() { + this.startTime = Date.now() + this.delay + const tick = () => { + this.update() + this.timer = requestAnimationFrame(tick) + + if (this.finished) { + // 在判断 update中 结束后 停止 重绘 + cancelAnimationFrame(this.timer) + this.timer = null + } + } + tick() + } + + stop() { + cancelAnimationFrame(this.timer) + this.timer = null + } +} diff --git a/packages/devui-vue/devui/statistic/src/utils/easing.ts b/packages/devui-vue/devui/statistic/src/utils/easing.ts new file mode 100644 index 0000000000000000000000000000000000000000..6dfa4ca2f810be95f0928ea2360fae73bec1fdf0 --- /dev/null +++ b/packages/devui-vue/devui/statistic/src/utils/easing.ts @@ -0,0 +1,27 @@ +// pow 返回 基数的指数次幂 t ** power +const pow = Math.pow +const sqrt = Math.sqrt + +export const easeOutCubic = function (x: number) { + return 1 - pow(1 - x, 3) +} +export const linear = (x) => x +export const easeOutExpo = function (x: number) { + return x === 1 ? 1 : 1 - pow(2, -10 * x) +} + +export const easeInOutExpo = function (x: number) { + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? pow(2, 20 * x - 10) / 2 + : (2 - pow(2, -20 * x + 10)) / 2 +} +export const easeInExpo = function (x: number) { + return x === 0 ? 0 : pow(2, 10 * x - 10) +} +export const easeInOutCirc = function (x: number) { + return x < 0.5 ? (1 - sqrt(1 - pow(2 * x, 2))) / 2 : (sqrt(1 - pow(-2 * x + 2, 2)) + 1) / 2 +} diff --git a/packages/devui-vue/devui/statistic/src/utils/separator.ts b/packages/devui-vue/devui/statistic/src/utils/separator.ts new file mode 100644 index 0000000000000000000000000000000000000000..91178bccbc0862d6ce8ff48467dff169505f75ec --- /dev/null +++ b/packages/devui-vue/devui/statistic/src/utils/separator.ts @@ -0,0 +1,54 @@ +export type valueType = string | number + +export const separator = ( + SeparatorString: string, // value + groupSeparator: string, // 千分位分隔符 + showGroupSeparator: boolean // 是否展示千分位分隔符 +): string => { + const res = SeparatorString.replace(/\d+/, function (n) { + // 先提取整数部分 + return n.replace(/(\d)(?=(\d{3})+$)/g, function ($1) { + return $1 + `${showGroupSeparator ? groupSeparator : ''}` + }) + }) + return res +} + +export const isHasDot = (value: number): boolean => { + if (!isNaN(value)) { + return (value + '').indexOf('.') !== -1 + } +} +export const analysisValueType = ( + value: valueType, // 动态value 值 + propsValue: valueType, // 用户传入value + groupSeparator: string, // 千位分隔符 + splitPrecisionNumber: number, // 分割精度, 小数点 + showGroupSeparator: boolean // 是否展示千分位分隔符 +): string => { + const fixedNumber = + propsValue.toString().indexOf('.') !== -1 + ? propsValue.toString().length - propsValue.toString().indexOf('.') - 1 + : 0 + if (typeof value === 'number') { + if (isHasDot(value)) { + return splitPrecisionNumber + ? separator( + value.toFixed(splitPrecisionNumber).toString(), + groupSeparator, + showGroupSeparator + ) + : separator(value.toFixed(fixedNumber).toString(), groupSeparator, showGroupSeparator) + } else { + return splitPrecisionNumber + ? separator( + value.toFixed(splitPrecisionNumber).toString(), + groupSeparator, + showGroupSeparator + ) + : separator(value.toString(), groupSeparator, showGroupSeparator) + } + } else { + return value + } +} diff --git a/packages/devui-vue/docs/components/statistic/index.md b/packages/devui-vue/docs/components/statistic/index.md new file mode 100644 index 0000000000000000000000000000000000000000..f3f020d62e16d2ceada82f7a979bc3e5e8d45392 --- /dev/null +++ b/packages/devui-vue/docs/components/statistic/index.md @@ -0,0 +1,185 @@ +# Statistic 统计数值 + +### 何时使用 + +当需要展示带描述的统计类数据时使用 + +### 基本用法 + +:::demo + +```vue + +``` + +::: + +### 在卡片中使用 + +在卡片中展示统计数值。 +:::demo + +```vue + +``` + +::: + +### 数值动画 + +我们可以通过设置 animation 属性 开启数值动画。可以在页面加载时开始动画,也可以手动控制 +:::demo + +```vue + + +``` + +::: + +### 插槽的使用 + +前缀和后缀插槽 +:::demo + +```vue + +``` + +::: + +### d-statistic + +| 参数 | 类型 | 默认 | 说明 | +| ------------------ | ------------------ | -------- | ---------------- | +| title | `string \| v-slot` | - | 数值的标题 | +| extra | `string \| v-slot` | - | 额外内容 | +| value | `number \| string` | - | 数值内容 | +| group-separator | `string` | , | 设置千分位标识符 | +| precision | `number` | - | 设置数值精度 | +| suffix | `string \| v-slot` | - | 设置数值的后缀 | +| prefix | `string \| v-slot` | - | 设置数值的前缀 | +| title-style | `style` | - | 标题样式 | +| content-style | `style` | - | 内容样式 | +| animation-duration | `number` | 2000 | 动画持续时间 | +| delay | `number` | 0 | 延迟进行动画时间 | +| value-from | `number` | 0 | 动画初始值 | +| animation | `boolean` | false | 是否开启动画 | +| easing | `string` | quartOut | 数字动画效果 | +| start | `boolean` | false | 是否开始动画 | + + diff --git a/packages/devui-vue/docs/en-US/components/statistic/index.md b/packages/devui-vue/docs/en-US/components/statistic/index.md new file mode 100644 index 0000000000000000000000000000000000000000..e15adb35a3800a653ef142efd41d7f31fdccec42 --- /dev/null +++ b/packages/devui-vue/docs/en-US/components/statistic/index.md @@ -0,0 +1,183 @@ +# Statistic + +### When to use + +Used when it is necessary to display statistical data with description + +### Basic Usage + +:::demo + +```vue + +``` + +::: + +### Use in card + +Display statistics in cards. +:::demo + +```vue + +``` + +::: + +### Numerical animation + +We can start numerical animation by setting the animation attribute. You can start the animation when the page loads, or you can control it manually +:::demo + +```vue + + +``` + +::: + +### Use of slots + +Prefix and suffix slots +:::demo + +```vue + +``` + +::: + +### d-statistic + +| parameter | type | default | introduce | +| ------------------ | ------------------ | -------- | ---------------------------- | +| title | `string \| v-slot` | - | Title of value | +| extra | `string \| v-slot` | - | Extra content | +| value | `number \| string` | - | Value content | +| group-separator | `string` | , | Set group-separator | +| precision | `number` | - | Set numeric precision | +| suffix | `string \| v-slot` | - | Sets the suffix of the value | +| prefix | `string \| v-slot` | - | Sets the prefix of the value | +| title-style | `style` | - | Title Style | +| content-style | `style` | - | Content style | +| animation-duration | `number` | 2000 | Animation duration | +| delay | `number` | 0 | Delay animation time | +| value-from | `number` | 0 | Animation initial value | +| animation | `boolean` | false | Turn on animation | +| easing | `string` | quartOut | Digital animation effect | +| start | `boolean` | false | Start animation |