Browse Source

refactor: new `CountTo` component with demo (#5551)

Netfan 1 month ago
parent
commit
24bad09c74

+ 123 - 0
packages/effects/common-ui/src/components/count-to/count-to.vue

@@ -0,0 +1,123 @@
+<script lang="ts" setup>
+import type { CountToProps } from './types';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import { isString } from '@vben-core/shared/utils';
+
+import { TransitionPresets, useTransition } from '@vueuse/core';
+
+const props = withDefaults(defineProps<CountToProps>(), {
+  startVal: 0,
+  duration: 2000,
+  separator: ',',
+  decimal: '.',
+  decimals: 0,
+  delay: 0,
+  transition: () => TransitionPresets.easeOutExpo,
+});
+
+const emit = defineEmits(['started', 'finished']);
+
+const lastValue = ref(props.startVal);
+
+onMounted(() => {
+  lastValue.value = props.endVal;
+});
+
+watch(
+  () => props.endVal,
+  (val) => {
+    lastValue.value = val;
+  },
+);
+
+const currentValue = useTransition(lastValue, {
+  delay: computed(() => props.delay),
+  duration: computed(() => props.duration),
+  disabled: computed(() => props.disabled),
+  transition: computed(() => {
+    return isString(props.transition)
+      ? TransitionPresets[props.transition]
+      : props.transition;
+  }),
+  onStarted() {
+    emit('started');
+  },
+  onFinished() {
+    emit('finished');
+  },
+});
+
+const numMain = computed(() => {
+  const result = currentValue.value
+    .toFixed(props.decimals)
+    .split('.')[0]
+    ?.replaceAll(/\B(?=(\d{3})+(?!\d))/g, ',');
+  return result;
+});
+
+const numDec = computed(() => {
+  return (
+    props.decimal + currentValue.value.toFixed(props.decimals).split('.')[1]
+  );
+});
+</script>
+<template>
+  <div class="count-to" v-bind="$attrs">
+    <slot name="prefix">
+      <div
+        class="count-to-prefix"
+        :style="prefixStyle"
+        :class="prefixClass"
+        v-if="prefix"
+      >
+        {{ prefix }}
+      </div>
+    </slot>
+    <div class="count-to-main" :class="mainClass" :style="mainStyle">
+      <span>{{ numMain }}</span>
+      <span
+        class="count-to-main-decimal"
+        v-if="decimals > 0"
+        :class="decimalClass"
+        :style="decimalStyle"
+      >
+        {{ numDec }}
+      </span>
+    </div>
+    <slot name="suffix">
+      <div
+        class="count-to-suffix"
+        :style="suffixStyle"
+        :class="suffixClass"
+        v-if="suffix"
+      >
+        {{ suffix }}
+      </div>
+    </slot>
+  </div>
+</template>
+<style lang="scss" scoped>
+.count-to {
+  display: flex;
+  align-items: baseline;
+
+  &-prefix {
+    // font-size: 1rem;
+  }
+
+  &-suffix {
+    // font-size: 1rem;
+  }
+
+  &-main {
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    // font-size: 1.5rem;
+
+    &-decimal {
+      // font-size: 0.8rem;
+    }
+  }
+}
+</style>

+ 2 - 0
packages/effects/common-ui/src/components/count-to/index.ts

@@ -0,0 +1,2 @@
+export { default as CountTo } from './count-to.vue';
+export * from './types';

+ 53 - 0
packages/effects/common-ui/src/components/count-to/types.ts

@@ -0,0 +1,53 @@
+import type { CubicBezierPoints, EasingFunction } from '@vueuse/core';
+
+import type { StyleValue } from 'vue';
+
+import { TransitionPresets as TransitionPresetsData } from '@vueuse/core';
+
+export type TransitionPresets = keyof typeof TransitionPresetsData;
+
+export const TransitionPresetsKeys = Object.keys(
+  TransitionPresetsData,
+) as TransitionPresets[];
+
+export interface CountToProps {
+  /** 初始值 */
+  startVal?: number;
+  /** 当前值 */
+  endVal: number;
+  /** 是否禁用动画 */
+  disabled?: boolean;
+  /** 延迟动画开始的时间 */
+  delay?: number;
+  /** 持续时间  */
+  duration?: number;
+  /** 小数位数  */
+  decimals?: number;
+  /** 小数点  */
+  decimal?: string;
+  /** 分隔符  */
+  separator?: string;
+  /** 前缀  */
+  prefix?: string;
+  /** 后缀  */
+  suffix?: string;
+  /** 过渡效果  */
+  transition?: CubicBezierPoints | EasingFunction | TransitionPresets;
+  /** 整数部分的类名 */
+  mainClass?: string;
+  /** 小数部分的类名 */
+  decimalClass?: string;
+  /** 前缀部分的类名 */
+  prefixClass?: string;
+  /** 后缀部分的类名 */
+  suffixClass?: string;
+
+  /** 整数部分的样式 */
+  mainStyle?: StyleValue;
+  /** 小数部分的样式 */
+  decimalStyle?: StyleValue;
+  /** 前缀部分的样式 */
+  prefixStyle?: StyleValue;
+  /** 后缀部分的样式 */
+  suffixStyle?: StyleValue;
+}

+ 1 - 0
packages/effects/common-ui/src/components/index.ts

@@ -1,6 +1,7 @@
 export * from './api-component';
 export * from './captcha';
 export * from './col-page';
+export * from './count-to';
 export * from './ellipsis-text';
 export * from './icon-picker';
 export * from './json-viewer';

+ 9 - 0
playground/src/router/routes/modules/examples.ts

@@ -273,6 +273,15 @@ const routes: RouteRecordRaw[] = [
           title: 'Motion',
         },
       },
+      {
+        name: 'CountTo',
+        path: '/examples/count-to',
+        component: () => import('#/views/examples/count-to/index.vue'),
+        meta: {
+          icon: 'mdi:animation-play',
+          title: 'CountTo',
+        },
+      },
     ],
   },
 ];

+ 178 - 0
playground/src/views/examples/count-to/index.vue

@@ -0,0 +1,178 @@
+<script lang="ts" setup>
+import type { CountToProps, TransitionPresets } from '@vben/common-ui';
+
+import { reactive } from 'vue';
+
+import { CountTo, Page, TransitionPresetsKeys } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  FormItem,
+  Input,
+  InputNumber,
+  message,
+  Row,
+  Select,
+  Switch,
+} from 'ant-design-vue';
+
+const props = reactive<CountToProps & { transition: TransitionPresets }>({
+  decimal: '.',
+  decimals: 2,
+  decimalStyle: {
+    fontSize: 'small',
+    fontStyle: 'italic',
+  },
+  delay: 0,
+  disabled: false,
+  duration: 2000,
+  endVal: 100_000,
+  mainStyle: {
+    color: 'hsl(var(--primary))',
+    fontSize: 'xx-large',
+    fontWeight: 'bold',
+  },
+  prefix: '¥',
+  prefixStyle: {
+    paddingRight: '0.5rem',
+  },
+  separator: ',',
+  startVal: 0,
+  suffix: '元',
+  suffixStyle: {
+    paddingLeft: '0.5rem',
+  },
+  transition: 'easeOutQuart',
+});
+
+function changeNumber() {
+  props.endVal =
+    Math.floor(Math.random() * 100_000_000) / 10 ** (props.decimals || 0);
+}
+
+function openDocumentation() {
+  window.open('https://vueuse.org/core/useTransition/', '_blank');
+}
+
+function onStarted() {
+  message.loading({
+    content: '动画已开始',
+    duration: 0,
+    key: 'animator-info',
+  });
+}
+
+function onFinished() {
+  message.success({
+    content: '动画已结束',
+    duration: 2,
+    key: 'animator-info',
+  });
+}
+</script>
+<template>
+  <Page title="CountTo" description="数字滚动动画组件。使用">
+    <template #description>
+      <span>
+        使用useTransition封装的数字滚动动画组件,每次改变当前值都会产生过渡动画。
+      </span>
+      <Button type="link" @click="openDocumentation">
+        查看useTransition文档
+      </Button>
+    </template>
+    <Card title="基本用法">
+      <div class="flex w-full items-center justify-center pb-4">
+        <CountTo v-bind="props" @started="onStarted" @finished="onFinished" />
+      </div>
+      <Form :model="props">
+        <Row :gutter="20">
+          <Col :span="8">
+            <FormItem label="初始值" name="startVal">
+              <InputNumber v-model:value="props.startVal" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="当前值" name="endVal">
+              <InputNumber
+                v-model:value="props.endVal"
+                class="w-full"
+                :precision="props.decimals"
+              >
+                <template #addonAfter>
+                  <IconifyIcon
+                    v-tippy="`设置一个随机值`"
+                    class="size-5 cursor-pointer outline-none"
+                    icon="ix:random-filled"
+                    @click="changeNumber"
+                  />
+                </template>
+              </InputNumber>
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="禁用动画" name="disabled">
+              <Switch v-model:checked="props.disabled" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="延迟动画" name="delay">
+              <InputNumber v-model:value="props.delay" :min="0" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="持续时间" name="duration">
+              <InputNumber v-model:value="props.duration" :min="0" />
+            </FormItem>
+          </Col>
+
+          <Col :span="8">
+            <FormItem label="小数位数" name="decimals">
+              <InputNumber
+                v-model:value="props.decimals"
+                :min="0"
+                :precision="0"
+              />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="分隔符" name="separator">
+              <Input v-model:value="props.separator" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="小数点" name="decimal">
+              <Input v-model:value="props.decimal" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="动画" name="transition">
+              <Select v-model:value="props.transition">
+                <Select.Option
+                  v-for="preset in TransitionPresetsKeys"
+                  :key="preset"
+                  :value="preset"
+                >
+                  {{ preset }}
+                </Select.Option>
+              </Select>
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="前缀" name="prefix">
+              <Input v-model:value="props.prefix" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="后缀" name="suffix">
+              <Input v-model:value="props.suffix" />
+            </FormItem>
+          </Col>
+        </Row>
+      </Form>
+    </Card>
+  </Page>
+</template>