From b5a25b2f4015009889411e091bfc003a2b09772c Mon Sep 17 00:00:00 2001 From: erkelost <1256029807@qq.com> Date: Tue, 30 Nov 2021 16:07:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(statistic):=20'=E6=96=B0=E5=A2=9Estati?= =?UTF-8?q?stic=E7=BB=9F=E8=AE=A1=E6=95=B0=E5=80=BC=E7=BB=84=E4=BB=B6'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistic/__tests__/statistic.spec.ts | 8 + packages/devui-vue/devui/statistic/index.ts | 17 ++ .../devui/statistic/src/statistic-types.ts | 63 ++++++ .../devui/statistic/src/statistic.scss | 34 ++++ .../devui/statistic/src/statistic.tsx | 85 ++++++++ .../devui/statistic/src/utils/animation.ts | 129 ++++++++++++ .../devui/statistic/src/utils/easing.ts | 27 +++ .../devui/statistic/src/utils/separator.ts | 54 +++++ .../docs/components/statistic/index.md | 191 ++++++++++++++++++ .../docs/en-US/components/statistic/index.md | 191 ++++++++++++++++++ 10 files changed, 799 insertions(+) create mode 100644 packages/devui-vue/devui/statistic/__tests__/statistic.spec.ts create mode 100644 packages/devui-vue/devui/statistic/index.ts create mode 100644 packages/devui-vue/devui/statistic/src/statistic-types.ts create mode 100644 packages/devui-vue/devui/statistic/src/statistic.scss create mode 100644 packages/devui-vue/devui/statistic/src/statistic.tsx create mode 100644 packages/devui-vue/devui/statistic/src/utils/animation.ts create mode 100644 packages/devui-vue/devui/statistic/src/utils/easing.ts create mode 100644 packages/devui-vue/devui/statistic/src/utils/separator.ts create mode 100644 packages/devui-vue/docs/components/statistic/index.md create mode 100644 packages/devui-vue/docs/en-US/components/statistic/index.md 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 00000000..fb9c9e89 --- /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 00000000..116831ee --- /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 00000000..db27965c --- /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 00000000..a619e6ce --- /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 00000000..e57ebf11 --- /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 00000000..8b1ef695 --- /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 || 'linear' + this.onStart = onStart + this.onUpdate = onUpdate || function () {} + 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 00000000..6dfa4ca2 --- /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 00000000..91178bcc --- /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 00000000..2b8809b4 --- /dev/null +++ b/packages/devui-vue/docs/components/statistic/index.md @@ -0,0 +1,191 @@ +# 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 | 延迟进行动画时间 | +| valueFrom | `number` | 0 | 动画初始值 | +| animation | `boolean` | false | 是否开启动画 | +| easing | `string` | quartOut | 数字动画效果 | +| start | `boolean` | false | 是否开始动画 | + +d-statistic 事件 + +| 事件 | 类型 | 说明 | 跳转 Demo | +| ---- | ---- | ---- | --------- | +| | | | | +| | | | | +| | | | | 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 00000000..5bc17b46 --- /dev/null +++ b/packages/devui-vue/docs/en-US/components/statistic/index.md @@ -0,0 +1,191 @@ +# 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 + +| 参数 | 类型 | 默认 | 说明 | +| ------------------ | ------------------ | -------- | ---------------------------- | +| 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 | +| valueFrom | `number` | 0 | Animation initial value | +| animation | `boolean` | false | Turn on animation | +| easing | `string` | quartOut | Digital animation effect | +| start | `boolean` | false | Start animation | + +d-statistic 事件 + +| 事件 | 类型 | 说明 | 跳转 Demo | +| ---- | ---- | ---- | --------- | +| | | | | +| | | | | +| | | | | -- Gitee From 8bfc1a2896cc1d01a2c7daf834858e90f47881b5 Mon Sep 17 00:00:00 2001 From: erkelost <1256029807@qq.com> Date: Tue, 30 Nov 2021 16:16:37 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(ripple):=20'=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E9=97=AE=E9=A2=98'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui/ripple/__tests__/ripple.spec.ts | 47 ---- .../devui-vue/devui/ripple/src/options.ts | 12 +- .../devui/ripple/src/ripple-directive.ts | 2 - .../devui-vue/docs/components/ripple/index.md | 4 +- .../docs/en-US/components/ripple/index.md | 221 ------------------ 5 files changed, 3 insertions(+), 283 deletions(-) delete mode 100644 packages/devui-vue/devui/ripple/__tests__/ripple.spec.ts delete mode 100644 packages/devui-vue/docs/en-US/components/ripple/index.md diff --git a/packages/devui-vue/devui/ripple/__tests__/ripple.spec.ts b/packages/devui-vue/devui/ripple/__tests__/ripple.spec.ts deleted file mode 100644 index e4e3fddc..00000000 --- a/packages/devui-vue/devui/ripple/__tests__/ripple.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { nextTick, createApp } from 'vue' -import { mount } from '@vue/test-utils' -import Ripple from '../index' -import { DEFAULT_PLUGIN_OPTIONS } from '../src/options' -// 全局属性 -const global = { - directives: { - ripple: Ripple - } -} -describe('ripple', () => { - it('ripple should render correctly', async () => { - const wrapper = mount( - { - template: ` -
- ` - }, - { - global - } - ) - await nextTick() - const rippleElement = wrapper.find('.ripple-container') as any - await rippleElement.trigger('click') - console.log(rippleElement.element.childElementCount) - - expect(wrapper.find('div').exists()).toBeTruthy() - }) - it('test ripple plugin', () => { - const app = createApp({}).use(Ripple) - expect(app.directive('ripple', Ripple)).toBeTruthy() - }) - - it('ripple default options', () => { - expect(DEFAULT_PLUGIN_OPTIONS).toEqual({ - directive: 'ripple', - color: 'currentColor', - initialOpacity: 0.2, - finalOpacity: 0.1, - duration: 0.8, - easing: 'ease-out', - delayTime: 75, - disabled: false - }) - }) -}) diff --git a/packages/devui-vue/devui/ripple/src/options.ts b/packages/devui-vue/devui/ripple/src/options.ts index 18f04ca9..c8d6273c 100644 --- a/packages/devui-vue/devui/ripple/src/options.ts +++ b/packages/devui-vue/devui/ripple/src/options.ts @@ -45,15 +45,6 @@ interface IRippleDirectiveOptions { * 75 */ delayTime: number - /** - * 禁止 水波 - * - * @note - * 类似于 debounceTime - * @default - * 75 - */ - disabled: boolean } interface IRipplePluginOptions extends IRippleDirectiveOptions { @@ -83,8 +74,7 @@ const DEFAULT_PLUGIN_OPTIONS: IRipplePluginOptions = { finalOpacity: 0.1, duration: 0.8, easing: 'ease-out', - delayTime: 75, - disabled: false + delayTime: 75 } export { diff --git a/packages/devui-vue/devui/ripple/src/ripple-directive.ts b/packages/devui-vue/devui/ripple/src/ripple-directive.ts index e4ad61eb..a87fc224 100644 --- a/packages/devui-vue/devui/ripple/src/ripple-directive.ts +++ b/packages/devui-vue/devui/ripple/src/ripple-directive.ts @@ -13,8 +13,6 @@ export default { el.addEventListener('pointerdown', (event) => { const options = optionMap.get(el) - // 必须确保disabled 属性存在 否则指令终止报错 - if (binding.value && binding.value.disabled) return if (options === false) return diff --git a/packages/devui-vue/docs/components/ripple/index.md b/packages/devui-vue/docs/components/ripple/index.md index 3212227f..47b87fd6 100644 --- a/packages/devui-vue/docs/components/ripple/index.md +++ b/packages/devui-vue/docs/components/ripple/index.md @@ -95,10 +95,10 @@ Button 组件