This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, isBoolean, makeNumberProp, makeStringProp, truthProp } from '../_utils'
export const countupProps = {
...commonProps,
/**
* @description 初始数字
*/
initNum: makeNumberProp(0),
/**
* @description 结束数字
*/
endNum: makeNumberProp(0),
/**
* @description 间隔数字,目前仅支持基础用法
*/
speed: makeNumberProp(1),
/**
* @description 保留小数点后几位
*/
toFixed: makeNumberProp(0),
/**
* @description 触发数字滚动的标识
*/
startFlag: truthProp,
/**
* @description 滚动一次运行时间
*/
during: makeNumberProp(1000),
/**
* @description 数字宽度,常用于自定义无缝滚动
*/
numWidth: makeNumberProp(20),
/**
* @description 数字高度,常用于自定义无缝滚动(抽奖功能必传)
*/
numHeight: makeNumberProp(20),
scrolling: Boolean,
/**
* @description 要变化的数字(用于自定义图片,`initNum`、`endNum` 在此无效)
*/
customChangeNum: makeNumberProp(1),
/**
* @description 自定义图片(建议使用雪碧图实现,使用抽奖功能必须使用雪碧图)
*/
customBgImg: makeStringProp(''),
/**
* @description 图片中数字之间可能会存在间距
*/
customSpacNum: makeNumberProp(0),
/**
* @description 抽奖功能传 machine可选值为 `machine` `' '`
*/
type: makeStringProp(''),
/**
* @description 奖品个数,一共多少个奖品,必传
*/
machinePrizeNum: makeNumberProp(0),
/**
* @description 抽奖位,即滚动几个,必传
*/
machineNum: makeNumberProp(3),
/**
* @description 中奖图标,图标在雪碧图中的位置
*/
machinePrizeLevel: makeNumberProp(0),
/**
* @description 滚动圈数
*/
machineTurnMore: makeNumberProp(0),
}
export type CountUpProps = ExtractPropTypes<typeof countupProps>
export const countupEmits = {
scrollEnd: (val?: boolean) => isBoolean(val) || val === undefined,
}
export type CountUpEmits = typeof countupEmits
export interface IData {
valFlag: boolean
current: number
sortFlag: string
initDigit1: number
initDigit2: number
to0_10: number[]
to10_0: Array<number>
timer: null | any
totalCount: number // 正整数
pointNum: number // 小数位
numberVal: number // 数字
num_total_len: number // 数字长度
relNum: number // 去除小数点
customNumber: number
prizeLevelTrun: number
prizeY: Array<any>
prizeYPrev: Array<any>
// machineTransition: 'none',
finshMachine: number
notPrize: Array<any>
typeMachine: string
}
export interface CountUpInst {
/**
* @description 抽奖
*/
machineLuck: () => void
}

View File

@@ -0,0 +1,575 @@
<script setup lang="ts">
import { computed, defineComponent, nextTick, onMounted, onUnmounted, reactive, watch } from 'vue'
import { PREFIX } from '../_constants'
import { useExtend } from '../_hooks'
import { cloneDeep, getMainClass } from '../_utils'
import type { IData } from './countup'
import { countupEmits, countupProps } from './countup'
const props = defineProps(countupProps)
const emit = defineEmits(countupEmits)
defineExpose({ machineLuck })
const classes = computed(() => {
return getMainClass(props, componentName)
})
const data = reactive<IData>({
valFlag: false,
current: 0,
sortFlag: 'add',
initDigit1: 0,
initDigit2: 0,
to0_10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
to10_0: [0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1],
timer: null,
totalCount: 0, // 正整数
pointNum: 0, // 小数位
numberVal: 0, // 数字
num_total_len: 0, // 数字长度
relNum: 0, // 去除小数点
customNumber: 1,
prizeLevelTrun: 0,
prizeY: [],
prizeYPrev: [],
// machineTransition: 'none',
finshMachine: 0,
notPrize: [],
typeMachine: '',
})
const { startFlag, scrolling, customBgImg, type } = reactive(props)
watch(
() => props.customChangeNum,
(count, prevCount) => {
clearIntervalTime()
// data.customNumber = count;
countGo(0)
},
)
watch(
() => props.machinePrizeLevel,
(count, prevCount) => {
data.prizeLevelTrun = count
},
)
watch(
() => props.initNum,
(count, prevCount) => {
data.current = count
data.valFlag = false
valChange()
},
)
watch(
() => props.endNum,
(count, prevCount) => {
data.current = props.initNum
data.valFlag = false
valChange()
},
)
function valChange() {
if (data.valFlag)
return false
if (startFlag) {
if (scrolling || customBgImg) {
if (type !== 'machine')
countGo()
}
else {
countChange()
setTimeout(() => {
data.valFlag = true
}, 300)
}
}
}
// 清空定时器
function clearIntervalTime() {
clearInterval(Number(data.timer))
data.timer = null
}
// 精确计算
function calculation(num1: number, num2: number, type: string) {
const num1Digits = (num1.toString().split('.')[1] || '').length
const num2Digits = (num2.toString().split('.')[1] || '').length
const baseNum = 10 ** Math.max(num1Digits, num2Digits)
if (type === '-') {
const n = Number((num1 * baseNum - num2 * baseNum).toFixed(0))
return n / baseNum
}
else {
const m = Number((num1 * baseNum + num2 * baseNum).toFixed(0))
return m / baseNum
}
}
// 数字滚动-top值
function topNumber(index: number) {
const { num_total_len, pointNum, initDigit1, initDigit2, sortFlag } = data
const idx1
= sortFlag === 'add' || sortFlag === 'equal'
? String(initDigit2)[index - (num_total_len - pointNum)]
: 10 - Number(String(initDigit2)[index - (num_total_len - pointNum)])
const idx2
= sortFlag === 'add' || sortFlag === 'equal' ? String(initDigit1)[index] : 10 - Number(String(initDigit1)[index])
let num
= index > num_total_len - pointNum - 1
? `${-idx1 * 100}%`
: index <= String(initDigit1).length - 1
? `${-idx2 * 100}%`
: 0
if (num === '-1000%')
num = 0
return num
}
// 数字滚动-到哪里了
function turnNumber(index: number) {
const { num_total_len, pointNum, initDigit1, initDigit2, sortFlag } = data
const idx1 = String(initDigit2)[index - (num_total_len - pointNum)]
const num
= index > num_total_len - pointNum - 1
? idx1 || 0
: index <= String(initDigit1).length - 1
? String(initDigit1)[index]
: 0
return num
}
// 基础用法
function countChange() {
const { endNum, initNum, speed, toFixed } = props
const countTimer = setInterval(() => {
if (initNum > endNum) {
// 减少
if (data.current <= endNum || data.current <= speed) {
// 数字减小有可能导致current小于speed
data.current = Number(endNum.toFixed(toFixed))
clearInterval(countTimer)
emit('scrollEnd')
data.valFlag = false
}
else {
data.current = Number((Number.parseFloat(String(data.current)) - Number.parseFloat(String(speed))).toFixed(toFixed))
}
}
else {
// 增加
if (data.current >= endNum) {
data.current = Number(endNum.toFixed(toFixed))
clearInterval(countTimer)
emit('scrollEnd')
data.valFlag = false
}
else {
data.current = Number((Number.parseFloat(String(data.current)) + Number.parseFloat(String(speed))).toFixed(toFixed))
}
}
}, props.during)
}
function countGo(flag?: number): void {
let { initNum, endNum, toFixed, customBgImg } = props
if (customBgImg)
initNum = props.customChangeNum
// --------------
let startNumber1, startNumber2, endNumber1, endNumber2
if (initNum !== 0) {
if (toFixed !== 0)
initNum = Number(initNum.toFixed(toFixed))
if (String(initNum).includes('.')) {
startNumber1 = String(initNum).split('.')[0].length
startNumber2 = String(initNum).split('.')[1].length
}
else {
startNumber1 = String(initNum).length
startNumber2 = 0
}
}
else {
startNumber1 = 1
startNumber2 = 0
}
if (endNum !== 0) {
if (toFixed !== 0)
endNum = Number(endNum.toFixed(toFixed))
if (String(endNum).includes('.')) {
endNumber1 = String(endNum).split('.')[0].length
endNumber2 = String(endNum).split('.')[1].length
}
else {
endNumber1 = String(endNum).length
endNumber2 = 0
}
}
else {
endNumber1 = 1
endNumber2 = 0
}
const len1 = startNumber1 >= endNumber1 ? startNumber1 : endNumber1
const len2 = startNumber2 >= endNumber2 ? startNumber2 : endNumber2
data.num_total_len = len1 + len2
data.pointNum = len2
// --------------
if (initNum > endNum) {
// 减少
data.sortFlag = 'reduce'
data.to0_10 = [0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
data.totalCount = calculation(initNum, endNum, '-')
data.numberVal = Number(String(initNum))
}
else if (initNum < endNum) {
// 增加
data.sortFlag = 'add'
data.to0_10 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
data.totalCount = calculation(endNum, initNum, '-')
data.numberVal = Number(String(endNum))
}
else {
data.sortFlag = 'equal'
}
// 将小数位数计算后补0
let unit = 1
for (let i = 0; i < data.pointNum; i++)
unit *= 10
const rel_big = data.numberVal * unit // 去除小数点后的数unit几个零表示有几个小数
data.relNum = rel_big
// this.totalCount = rel_big;
if (toFixed !== 0) {
// 计算小数点后的位数,小数位
data.pointNum = String(data.numberVal).split('.')[1] ? String(data.numberVal).split('.')[1].length : 0
// 数字长度
data.num_total_len = String(rel_big).length
}
if (String(initNum).includes('.')) {
const n = String(initNum).split('.')
data.initDigit1 = Number(n[0])
data.initDigit2 = Number(n[1])
}
else {
data.initDigit1 = initNum
data.initDigit2 = 0
}
if (scrolling && !customBgImg) {
// #ifdef H5
// 数字都是从小加到大的所以我们循环转动最后一个数字传入最后一个数字的DOM
nextTick(() => {
if (data.sortFlag === 'equal')
return false
const refsDom: HTMLCollectionOf<Element> = document.getElementsByClassName('nut-countup__number-item')
const element = refsDom[data.num_total_len - 1]
runTurn(element)
})
// #endif
}
else {
if (flag !== 0)
imgNumberScroll()
}
}
function runTurn(el: Element) {
clearIntervalTime()
let m = 1
if (data.pointNum !== 0)
m = 1 / 10 ** data.pointNum;
// 设置定时器
(data.timer as any) = setInterval(() => {
runStep(el)
data.totalCount = calculation(data.totalCount, m, '-')
// that.totalCount--;
if (data.totalCount <= 0) {
clearIntervalTime()
emit('scrollEnd')
data.valFlag = false
}
}, props.during)
}
function runStep(el: any) {
// let currentTurn = el.getAttribute('turn-number');
const currentTurn = el.style.all
let turningNum: number
if (data.sortFlag === 'add')
turningNum = Number.parseInt(String(currentTurn)) + 1
else
turningNum = Number.parseInt(String(currentTurn)) - 1 >= 0 ? Number.parseInt(String(currentTurn)) - 1 : 9
// el.setAttribute('turn-number', String(turningNum));
el.style.all = String(turningNum)
if (el.style.transition === 'none' || turningNum === 1 || !el.style.transition)
el.style.transition = `all linear ${props.during}ms`
if (turningNum === 10 || (data.sortFlag === 'reduce' && turningNum === 0)) {
let timeOut: any = null
// el.style.top = `-${turningNum * 100}%`;
el.style.top = `-${data.sortFlag === 'add' ? turningNum * 100 : (10 - turningNum) * 100}%`
// el.setAttribute('turn-number', '0');
el.style.all = '0'
timeOut = setTimeout(() => {
timeOut && clearTimeout(timeOut)
el.style.transition = 'none'
el.style.top = '0'
// 前面数字的滚动,用于递增
if (turningNum === 10) {
if (el.previousSibling)
runStep(el.previousSibling as HTMLElement)
}
}, 0.975 * props.during)
}
else {
// el.style.top = `-${(10-turningNum)*100}%`;
el.style.top = `-${data.sortFlag === 'add' ? turningNum * 100 : (10 - turningNum) * 100}%`
}
// 用于递减的时候
if (el.style.top === '-100%' && data.sortFlag === 'reduce')
runStep(el.previousSibling as HTMLElement)
}
// 自定义图片
function imgNumberScroll() {
let m = 1
if (data.pointNum !== 0)
m = 10 ** data.pointNum
// #ifdef H5
nextTick(() => {
const f = document.getElementsByClassName('nut-countup__numberimg')[0]
// setTimeout(() => {
// data.relNum = calculation(data.relNum, m * props.speed, '+');
// }, props.during);
f.addEventListener('webkitTransitionEnd', () => {
emit('scrollEnd')
data.valFlag = false
// setTimeout(() => {
// data.relNum = calculation(data.relNum, m * props.speed, '+');
// }, props.during);
})
})
// #endif
}
// 不中奖设置随机数
function generateRandom() {
data.notPrize = []
while (data.notPrize.length < 3) {
const rand: number = Math.floor(Math.random() * props.machinePrizeNum + 1)
if (!data.notPrize.includes(rand))
data.notPrize.push(rand)
}
}
// 抽奖
function machineLuck() {
const machineTurnMoreNum = props.machineTurnMore < 0 ? 0 : props.machineTurnMore
const distance = props.numHeight * props.machinePrizeNum // 所有奖品的高度,雪碧图的高度
if (data.prizeLevelTrun < 0)
generateRandom()
for (let i = 0; i < props.machineNum; i++) {
setTimeout(() => {
const turn = distance * (i + 1 + Number.parseFloat(String(machineTurnMoreNum)))
if (data.prizeYPrev.length !== 0) {
// this.machineTransition = 'none';
// console.log(this.prizeYPrev[i]-(this.numHeight * this.machinePrizeNum));
// this.$set(data.prizeY, i, data.prizeYPrev[i]);
data.prizeY[i] = data.prizeYPrev[i]
}
const local = data.prizeYPrev[i] ? data.prizeYPrev[i] : 0
let newLocation
= turn + local + (props.machinePrizeNum - data.prizeLevelTrun + 1) * props.numHeight + (distance - local)
if (data.prizeLevelTrun < 0)
newLocation += props.numHeight * data.notPrize[i]
scrollTime(
i,
// parseFloat((this.machinePrizeNum-(this.prizeLevelTrun-1))*this.numHeight + turn + local),
newLocation,
local,
)
}, 500 * i)
}
}
useExtend({ machineLuck })
function scrollTime(index: number, total: number, num: number) {
// this.machineTransition = `all linear ${this.during/this.machinePrizeNum}ms`;
let t: any = setInterval(() => {
if (num <= total) {
num += 10
data.prizeY[index] = Number.parseFloat(String(num))
}
else {
clearInterval(t)
t = null
data.finshMachine += 1
data.prizeY[index] = total
if (data.finshMachine === props.machineNum) {
const distance = props.numHeight * props.machinePrizeNum
data.prizeYPrev = []
const prevAry = cloneDeep(data.prizeY)
prevAry.forEach((item: any) => {
let n = item
while (n > distance)
n -= distance
data.prizeYPrev.push(n)
})
setTimeout(() => {
data.finshMachine = 0
if (data.prizeLevelTrun < 0) {
emit('scrollEnd', false)
data.valFlag = false
}
else {
emit('scrollEnd', true)
data.valFlag = false
}
}, 130)
}
}
}, 30)
}
onMounted(() => {
data.current = props.initNum
nextTick(() => {
valChange()
})
})
onUnmounted(() => {
clearIntervalTime()
data.timer = null
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-countup`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="customStyle">
<template v-if="customBgImg !== ''">
<template v-if="type === 'machine'">
<view class="nut-countup__machine" :style="{ height: `${numHeight}px` }">
<view
v-for="(val, index) of machineNum"
:key="`mImg${index}`"
class="nut-countup__machine-item"
:style="{
width: `${numWidth}px`,
height: `${numHeight}px`,
background: `url(${customBgImg}) `,
backgroundPosition: `0 ${data.prizeY[index]}px`,
}"
/>
</view>
</template>
<template v-else>
<view class="nut-countup__numberimg" :style="{ height: `${numHeight}px` }">
<view
v-for="(val, index) of data.num_total_len"
:key="`cImg${index}`"
class="nut-countup__numberimg__item"
:style="{
width: `${numWidth}px`,
height: `${numHeight}px`,
left:
`${numWidth
* (index > data.num_total_len - data.pointNum - 1
? index === data.num_total_len - data.pointNum
? index * 1.5
: index * 1.3
: index)
}px`,
backgroundImage: `url(${customBgImg})`,
backgroundPosition:
`0 ${-(+String(data.relNum)[index] * numHeight + customSpacNum * +String(data.relNum)[index])}px`,
transition: `all linear ${during / 10}ms`,
}"
/>
<view
v-if="data.pointNum > 0"
class="nut-countup-pointstyl"
:style="{
width: `${numWidth / 2}px`,
bottom: 0,
left: `${numWidth * (data.num_total_len - data.pointNum) * 1.1}px`,
fontSize: '30px',
}"
>
.
</view>
</view>
</template>
</template>
<template v-else>
<view
v-if="scrolling"
class="nut-countup__number"
:style="{
width: `${numWidth * data.num_total_len + numWidth / 3}px`,
height: `${numHeight}px`,
lineHeight: `${numHeight}px`,
}"
>
<view
v-for="(val, index) of data.num_total_len"
:key="val"
class="nut-countup__number-item"
:style="{
all: turnNumber(index) as any,
top: topNumber(index),
left: `${numWidth * (index > data.num_total_len - data.pointNum - 1 ? index * 1.1 : index)}px`,
}"
:turn-number="turnNumber(index)"
>
<view
v-for="(item, idx) of data.to0_10"
:key="`dote${idx}`"
class="nut-countup__number-item__span"
:style="{
width: `${numWidth}px`,
height: `${numHeight}px`,
lineHeight: `${numHeight}px`,
}"
>
{{ item }}
</view>
</view>
<view
v-if="data.pointNum > 0"
class="nut-countup-pointstyl"
:style="{
width: `${numWidth / 3}px`,
height: `${numHeight}px`,
lineHeight: `${numHeight}px`,
top: 0,
left: `${numWidth * (data.num_total_len - data.pointNum)}px`,
}"
>
.
</view>
</view>
<template v-else>
{{ data.current }}
</template>
</template>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,64 @@
.nut-theme-dark {
.nut-countup {
color: $dark-color;
background: $dark-background;
box-shadow: none;
}
}
.nut-countup {
display: block;
padding: 5px 20px;
font-weight: bold;
color: #000;
.nut-countup__number {
position: relative;
display: inline-block;
width: 100%;
padding: 0;
overflow: hidden;
font-weight: bold;
text-align: center;
.nut-countup__number-item {
position: absolute;
list-style: none;
transition: none;
.nut-countup__number-item__span {
display: block;
}
}
}
.nut-countup-pointstyl {
position: absolute;
display: block;
}
.nut-countup__machine {
display: block;
overflow: hidden;
.nut-countup__machine-item {
float: left;
background-repeat: repeat-y;
background-attachment: scroll;
background-position: center 0;
}
}
.nut-countup__numberimg {
position: relative;
display: inline-block;
.nut-countup__numberimg__item {
position: absolute;
display: inline-block;
background-repeat: no-repeat;
background-position: 0 0;
transition: none;
}
}
}

View File

@@ -0,0 +1 @@
export type * from './countup'