mirror of
https://github.com/pure-admin/pure-admin-thin.git
synced 2025-11-16 23:53:37 +08:00
perf: 首页
This commit is contained in:
2
src/components/ReCountTo/README.md
Normal file
2
src/components/ReCountTo/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
normal 普通数字动画组件
|
||||
rebound 回弹式数字动画组件
|
||||
11
src/components/ReCountTo/index.ts
Normal file
11
src/components/ReCountTo/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import reNormalCountTo from "./src/normal";
|
||||
import reboundCountTo from "./src/rebound";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 普通数字动画组件 */
|
||||
const ReNormalCountTo = withInstall(reNormalCountTo);
|
||||
|
||||
/** 回弹式数字动画组件 */
|
||||
const ReboundCountTo = withInstall(reboundCountTo);
|
||||
|
||||
export { ReNormalCountTo, ReboundCountTo };
|
||||
179
src/components/ReCountTo/src/normal/index.tsx
Normal file
179
src/components/ReCountTo/src/normal/index.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
unref
|
||||
} from "vue";
|
||||
import { countToProps } from "./props";
|
||||
import { isNumber } from "@pureadmin/utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ReNormalCountTo",
|
||||
props: countToProps,
|
||||
emits: ["mounted", "callback"],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive<{
|
||||
localStartVal: number;
|
||||
printVal: number | null;
|
||||
displayValue: string;
|
||||
paused: boolean;
|
||||
localDuration: number | null;
|
||||
startTime: number | null;
|
||||
timestamp: number | null;
|
||||
rAF: any;
|
||||
remaining: number | null;
|
||||
color: string;
|
||||
fontSize: string;
|
||||
}>({
|
||||
localStartVal: props.startVal,
|
||||
displayValue: formatNumber(props.startVal),
|
||||
printVal: null,
|
||||
paused: false,
|
||||
localDuration: props.duration,
|
||||
startTime: null,
|
||||
timestamp: null,
|
||||
remaining: null,
|
||||
rAF: null,
|
||||
color: null,
|
||||
fontSize: "16px"
|
||||
});
|
||||
|
||||
const getCountDown = computed(() => {
|
||||
return props.startVal > props.endVal;
|
||||
});
|
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
const { startVal, duration, color, fontSize } = props;
|
||||
state.localStartVal = startVal;
|
||||
state.startTime = null;
|
||||
state.localDuration = duration;
|
||||
state.paused = false;
|
||||
state.color = color;
|
||||
state.fontSize = fontSize;
|
||||
state.rAF = requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
function pauseResume() {
|
||||
if (state.paused) {
|
||||
resume();
|
||||
state.paused = false;
|
||||
} else {
|
||||
pause();
|
||||
state.paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
cancelAnimationFrame(state.rAF);
|
||||
}
|
||||
|
||||
function resume() {
|
||||
state.startTime = null;
|
||||
state.localDuration = +(state.remaining as number);
|
||||
state.localStartVal = +(state.printVal as number);
|
||||
requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
function reset() {
|
||||
state.startTime = null;
|
||||
cancelAnimationFrame(state.rAF);
|
||||
state.displayValue = formatNumber(props.startVal);
|
||||
}
|
||||
|
||||
function count(timestamp: number) {
|
||||
const { useEasing, easingFn, endVal } = props;
|
||||
if (!state.startTime) state.startTime = timestamp;
|
||||
state.timestamp = timestamp;
|
||||
const progress = timestamp - state.startTime;
|
||||
state.remaining = (state.localDuration as number) - progress;
|
||||
if (useEasing) {
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal =
|
||||
state.localStartVal -
|
||||
easingFn(
|
||||
progress,
|
||||
0,
|
||||
state.localStartVal - endVal,
|
||||
state.localDuration as number
|
||||
);
|
||||
} else {
|
||||
state.printVal = easingFn(
|
||||
progress,
|
||||
state.localStartVal,
|
||||
endVal - state.localStartVal,
|
||||
state.localDuration as number
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal =
|
||||
state.localStartVal -
|
||||
(state.localStartVal - endVal) *
|
||||
(progress / (state.localDuration as number));
|
||||
} else {
|
||||
state.printVal =
|
||||
state.localStartVal +
|
||||
(endVal - state.localStartVal) *
|
||||
(progress / (state.localDuration as number));
|
||||
}
|
||||
}
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal = state.printVal < endVal ? endVal : state.printVal;
|
||||
} else {
|
||||
state.printVal = state.printVal > endVal ? endVal : state.printVal;
|
||||
}
|
||||
state.displayValue = formatNumber(state.printVal);
|
||||
if (progress < (state.localDuration as number)) {
|
||||
state.rAF = requestAnimationFrame(count);
|
||||
} else {
|
||||
emit("callback");
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(num: number | string) {
|
||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||
num = Number(num).toFixed(decimals);
|
||||
num += "";
|
||||
const x = num.split(".");
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimal + x[1] : "";
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (separator && !isNumber(separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, "$1" + separator + "$2");
|
||||
}
|
||||
}
|
||||
return prefix + x1 + x2 + suffix;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
emit("mounted");
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
color: props.color,
|
||||
fontSize: props.fontSize
|
||||
}}
|
||||
>
|
||||
{state.displayValue}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
31
src/components/ReCountTo/src/normal/props.ts
Normal file
31
src/components/ReCountTo/src/normal/props.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { PropType } from "vue";
|
||||
import propTypes from "@/utils/propTypes";
|
||||
export const countToProps = {
|
||||
startVal: propTypes.number.def(0),
|
||||
endVal: propTypes.number.def(2020),
|
||||
duration: propTypes.number.def(1300),
|
||||
autoplay: propTypes.bool.def(true),
|
||||
decimals: {
|
||||
type: Number as PropType<number>,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value >= 0;
|
||||
}
|
||||
},
|
||||
color: propTypes.string.def(),
|
||||
fontSize: propTypes.string.def(),
|
||||
decimal: propTypes.string.def("."),
|
||||
separator: propTypes.string.def(","),
|
||||
prefix: propTypes.string.def(""),
|
||||
suffix: propTypes.string.def(""),
|
||||
useEasing: propTypes.bool.def(true),
|
||||
easingFn: {
|
||||
type: Function as PropType<
|
||||
(t: number, b: number, c: number, d: number) => number
|
||||
>,
|
||||
default(t: number, b: number, c: number, d: number) {
|
||||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
|
||||
}
|
||||
}
|
||||
};
|
||||
72
src/components/ReCountTo/src/rebound/index.tsx
Normal file
72
src/components/ReCountTo/src/rebound/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import "./rebound.css";
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
unref,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount
|
||||
} from "vue";
|
||||
import { reboundProps } from "./props";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ReboundCountTo",
|
||||
props: reboundProps,
|
||||
setup(props) {
|
||||
const ulRef = ref();
|
||||
const timer = ref(null);
|
||||
|
||||
onBeforeMount(() => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const testUA = regexp => regexp.test(ua);
|
||||
const isSafari = testUA(/safari/g) && !testUA(/chrome/g);
|
||||
|
||||
// Safari浏览器的兼容代码
|
||||
isSafari &&
|
||||
(timer.value = setTimeout(() => {
|
||||
ulRef.value.setAttribute(
|
||||
"style",
|
||||
`
|
||||
animation: none;
|
||||
transform: translateY(calc(var(--i) * -9.09%))
|
||||
`
|
||||
);
|
||||
}, props.delay * 1000));
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(unref(timer));
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div
|
||||
class="scroll-num"
|
||||
style={{ "--i": props.i, "--delay": props.delay }}
|
||||
>
|
||||
<ul ref="ulRef" style={{ fontSize: "32px" }}>
|
||||
<li>0</li>
|
||||
<li>1</li>
|
||||
<li>2</li>
|
||||
<li>3</li>
|
||||
<li>4</li>
|
||||
<li>5</li>
|
||||
<li>6</li>
|
||||
<li>7</li>
|
||||
<li>8</li>
|
||||
<li>9</li>
|
||||
<li>0</li>
|
||||
</ul>
|
||||
|
||||
<svg width="0" height="0">
|
||||
<filter id="blur">
|
||||
<feGaussianBlur
|
||||
in="SourceGraphic"
|
||||
stdDeviation={`0 ${props.blur}`}
|
||||
/>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
14
src/components/ReCountTo/src/rebound/props.ts
Normal file
14
src/components/ReCountTo/src/rebound/props.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { PropType } from "vue";
|
||||
import propTypes from "@/utils/propTypes";
|
||||
export const reboundProps = {
|
||||
delay: propTypes.number.def(1),
|
||||
blur: propTypes.number.def(2),
|
||||
i: {
|
||||
type: Number as PropType<number>,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value < 10 && value >= 0 && Number.isInteger(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
77
src/components/ReCountTo/src/rebound/rebound.css
Normal file
77
src/components/ReCountTo/src/rebound/rebound.css
Normal file
@@ -0,0 +1,77 @@
|
||||
.scroll-num {
|
||||
width: var(--width, 20px);
|
||||
height: var(--height, calc(var(--width, 20px) * 1.8));
|
||||
color: var(--color, #333);
|
||||
font-size: var(--height, calc(var(--width, 20px) * 1.1));
|
||||
line-height: var(--height, calc(var(--width, 20px) * 1.8));
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
|
||||
}
|
||||
|
||||
ul {
|
||||
animation:
|
||||
move 0.3s linear infinite,
|
||||
bounce-in-down 1s calc(var(--delay) * 1s) forwards;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from {
|
||||
transform: translateY(-90%);
|
||||
filter: url(#blur);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(1%);
|
||||
filter: url(#blur);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-in-down {
|
||||
from {
|
||||
transform: translateY(calc(var(--i) * -9.09% - 7%));
|
||||
filter: none;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(calc(var(--i) * -9.09% + 3%));
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(calc(var(--i) * -9.09% - 1%));
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translateY(calc(var(--i) * -9.09% + 0.6%));
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translateY(calc(var(--i) * -9.09% - 0.3%));
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(calc(var(--i) * -9.09%));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enhance-bounce-in-down {
|
||||
25% {
|
||||
transform: translateY(8%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-4%);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translateY(2%);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translateY(-1%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
39
src/components/ReFlicker/index.css
Normal file
39
src/components/ReFlicker/index.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.point {
|
||||
width: var(--point-width);
|
||||
height: var(--point-height);
|
||||
background: var(--point-background);
|
||||
position: relative;
|
||||
border-radius: var(--point-border-radius);
|
||||
}
|
||||
|
||||
.point-flicker:after {
|
||||
background: var(--point-background);
|
||||
}
|
||||
|
||||
.point-flicker:before,
|
||||
.point-flicker:after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
border-radius: var(--point-border-radius);
|
||||
animation: flicker 1.2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(var(--point-scale));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
44
src/components/ReFlicker/index.ts
Normal file
44
src/components/ReFlicker/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import "./index.css";
|
||||
import { h, defineComponent, type Component } from "vue";
|
||||
|
||||
export interface attrsType {
|
||||
width?: string;
|
||||
height?: string;
|
||||
borderRadius?: number | string;
|
||||
background?: string;
|
||||
scale?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆点、方形闪烁动画组件
|
||||
* @param width 可选 string 宽
|
||||
* @param height 可选 string 高
|
||||
* @param borderRadius 可选 number | string 传0为方形、传50%或者不传为圆形
|
||||
* @param background 可选 string 闪烁颜色
|
||||
* @param scale 可选 number | string 闪烁范围,默认2,值越大闪烁范围越大
|
||||
* @returns Component
|
||||
*/
|
||||
export function useRenderFlicker(attrs?: attrsType): Component {
|
||||
return defineComponent({
|
||||
name: "ReFlicker",
|
||||
render() {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
class: "point point-flicker",
|
||||
style: {
|
||||
"--point-width": attrs?.width ?? "12px",
|
||||
"--point-height": attrs?.height ?? "12px",
|
||||
"--point-background":
|
||||
attrs?.background ?? "var(--el-color-primary)",
|
||||
"--point-border-radius": attrs?.borderRadius ?? "50%",
|
||||
"--point-scale": attrs?.scale ?? "2"
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => []
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user