最近接收到一个大屏需求,需求是酱紫的,直接上图
需求拆解
- 液晶数字滚动效果和特定的字体。
- 有从旧值过渡到新值的效果,所以需要知道oldValue和newValue两个值。
- 如果新旧值相差不大的话,产生的随机数组的个数
首先实现液晶数字组件
template部分
<template>
<div class="flipper-container">
<div class="flipper-item" v-for="item in state.flipperData">
<div class="flipper-item-list" :data-number="item">
<div
class="flipper-item__inner"
v-for="number in numbers"
:key="number"
>
{{ number }}
</div>
</div>
</div>
<div class="animation-list">
<div
class="animation-item"
:class="isIncreasing ? 'green' : 'red'"
v-for="number in state.differenceData"
>
<span class="mark">
{{ isIncreasing ? '+' : '-' }}
</span>
<span class="number">{{ number }}</span>
</div>
</div>
</div>
</template>
- 导入液晶字体文件
- 首先每一个数字都是由一组0-9的数字排列组成,然后根据当前数字的值,使用translateY滚动到相应的位置,以达到显示当前数字的效果。
- animation-list中包裹的则是随机生成的数字,用于右侧相加动效
核心函数
首先我们需要一个方法把number转为Array,类似 1234 -> [1, 2, 3, 4],我们最终渲染页面的时候,是要for循环一个数组
/**
* @description number -> Array<number>
* @param {Number} data
* @return {Array<number>}
*/
const getFlipperData = (data: number = 0) => {
return data
.toString()
.padStart(state.flipperLength, '0') // 左侧不够长度的 填充0
.split('') // 转数组
.map(item => stringToNumber(item)) // 数组内转number
}
我们还需要一个辅助函数,用于生成oldValue到newValue之间的随机数组
/**
* @description 获取差值的随机数数组
* @param {Number} difference 差值
* @returns {Array<Number>}
*/
const getDifferenceData = (difference: number) => {
if (difference === 0) return []
let result: number[] = []
let sum = 0
for (let i = 0; i < state.differenceGroupNumber - 1; i++) {
let randomNum = Math.floor(
Math.random() * (difference - sum - (state.differenceGroupNumber - i)) + 1
)
result.push(randomNum)
sum += randomNum
}
result.push(stringToNumber((difference - sum).toFixed(1)))
return result
}
script代码块
<script setup lang="ts">
import { computed, onMounted, reactive } from 'vue'
interface Data {
oldValue: number
newValue: number
}
interface Props {
data: Data
}
interface State {
flipperData: number[]
flipperLength: number
difference: number
differenceData: number[]
differenceGroupNumber: number
}
const props = withDefaults(defineProps<Props>(), {})
const numbers = Object.freeze([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
// 默认的分组数
const DEFAULT_GROUP = 5
// 默认的动画时间
const ANIMATE_TIME = 1000
const state = reactive<State>({
// 当前的flipperData
flipperData: [],
// 数字长度
flipperLength: 0,
// 差值
difference: 0,
// 差值数组
differenceData: [],
// 分组数量
differenceGroupNumber: 0,
})
// 默认样式配置,灵活的话,也可由props传入
const styleInfo = reactive({
width: 65,
height: 90,
fontSize: 80,
})
// 是否是递增
const isIncreasing = computed(() => state.difference > 0)
/**
* @description 延迟函数
* @param {Number} timer 毫秒
* @returns {Promise<any>}
*/
const sleep = (time: number) => {
return new Promise(resolve => setTimeout(resolve, time))
}
/**
* @description 字符串转数字
* @param {String} str
* @returns {Number}
*/
const stringToNumber = (str: string) => +str
/**
* @description number -> Array<number>
* @param {Number} data
* @return {Array<number>}
*/
const getFlipperData = (data: number = 0) => {
return data
.toString()
.padStart(state.flipperLength, '0') // 左侧不够长度的 填充0
.split('') // 转数组
.map(item => stringToNumber(item)) // 数组内转number
}
/**
* @description 获取分组数量
* @param {Number} difference 差值
* @returns {Array<number>}
*/
const getDifferenceGroupNumber = (difference: number) => {
difference = Math.abs(difference)
if (difference <= 1) {
return 1
} else if (difference <= 5) {
return 2
} else if (difference <= 10) {
return 3
} else {
return DEFAULT_GROUP
}
}
/**
* @description 获取差值的随机数数组
* @param {Number} difference 差值
* @returns {Array<Number>}
*/
const getDifferenceData = (difference: number) => {
if (difference === 0) return []
let result: number[] = []
let sum = 0
for (let i = 0; i < state.differenceGroupNumber - 1; i++) {
let randomNum = Math.floor(
Math.random() * (difference - sum - (state.differenceGroupNumber - i)) + 1
)
result.push(randomNum)
sum += randomNum
}
result.push(stringToNumber((difference - sum).toFixed(1)))
return result
}
// 更新动画
const updateStyle = () => {
const flipperListEle = document.querySelectorAll('.flipper-item-list')
flipperListEle.forEach((ele: HTMLElement) => {
const number = ele.dataset.number ?? 0
ele.style.transform = `translate3d(0, -${styleInfo.height * +number}px, 0)`
})
}
/**
* @description 循环 生成随机数组,用oldValue累加更新 flipperData
*/
const updateFlipperData = async () => {
let value = props.data.oldValue
for (const number of state.differenceData) {
value += number
value.toString().padStart(state.flipperLength, '0')
state.flipperData = getFlipperData(value)
console.log('updateFlipperData', state.flipperData)
await sleep(ANIMATE_TIME).then(updateStyle)
}
}
const initData = async () => {
state.flipperLength = props.data.newValue.toString().length
state.flipperData = getFlipperData(props.data.oldValue)
// 获取差值
state.difference = props.data.newValue - props.data.oldValue
// 根据差值的大小,生成分组个数
state.differenceGroupNumber = getDifferenceGroupNumber(state.difference)
// 更具分组个数,生成随机数组
state.differenceData = getDifferenceData(state.difference)
// TODO: 首次更新动画 等待dom更新完毕
await sleep(ANIMATE_TIME).then(updateStyle)
}
const init = () => {
initData()
updateFlipperData()
}
onMounted(init)
</script>
style的话就随便写写,不是很擅长,主要的就是 @keyframes 的动画,可以根据需求来改写动画
<style scoped lang="less">
// 随机数
@random: `Math.floor(Math.random() * 6) - 3`;
// 随机缩放
@scale: `Math.random() * 1.8`;
// 角度随机
@degrees: unit(@random, deg);
// 分组个数
@differenceGroupNumber: v-bind('state.differenceGroupNumber');
.flipper-container {
position: relative;
display: flex;
width: 100%;
height: 100%;
align-items: flex-end;
.flipper-item {
position: relative;
width: v-bind('`${styleInfo.width}px`');
height: v-bind('`${styleInfo.height}px`');
line-height: v-bind('`${styleInfo.height}px`');
padding-top: 10px;
margin-right: 3%;
font-family: 'Segment7';
font-size: v-bind('`${styleInfo.fontSize}px`');
text-align: center;
color: #ffffff;
border-width: 2px;
border-style: solid;
border-color: #fff;
border-radius: 4px;
background-image: linear-gradient(45deg, #30374d, #485573);
overflow: hidden;
&-list {
height: v-bind('`${styleInfo.height * 10}px`');
transition: all 1s ease;
background-color: transparent;
}
&__inner {
width: v-bind('`${styleInfo.width}px`');
height: v-bind('`${styleInfo.height}px`');
background-color: transparent;
}
&:before {
content: '';
position: absolute;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(
-70deg,
transparent 45%,
rgba(0, 0, 0, 0.17) 46%
);
left: 50%;
transform: translate(-50%);
}
&:after {
content: '8';
position: absolute;
top: 10px;
left: 0;
right: 0;
bottom: 0;
color: rgba(106, 139, 255, 0.2);
font-family: 'Segment7';
}
&:last-child {
margin-right: 0;
}
}
@keyframes fadeIn {
0% {
transform: translateY(-50px) rotate(@degrees) scale(0.5);
opacity: 0;
}
25% {
transform: translateY(-60px) rotate(@degrees) scale(1.3);
opacity: 0.8;
}
50% {
transform: translateY(-70px) rotate(@degrees) scale(1);
opacity: 1;
}
75% {
transform: translateY(-80px) rotate(@degrees) scale(0.8);
opacity: 0.6;
}
100% {
transform: translateY(-100px) rotate(0) scale(0.6);
opacity: 0;
}
}
.animation-loops(@i) when (@i <= 5) {
&:nth-of-type(@{i}) {
transform: rotate(@degrees) scale(@scale);
animation: fadeIn 1s @i * 1s forwards;
}
.animation-loops(@i + 1);
}
.animation-list {
font-size: 40px;
.animation-item {
position: absolute;
bottom: 20px;
right: 0;
opacity: 0;
min-width: 55px !important;
text-align: center;
.number {
font-family: 'Segment7';
}
&.green {
color: #52c41a;
}
&.red {
color: #ff4d4f;
}
.animation-loops(1);
}
}
}
</style>
虽然混迹掘金好多年了,还是第一次想起写文章,排版好🌶🐔,不会不会写,仅仅记录一下!!