Commit 44000079 authored by 金瑞's avatar 金瑞 😿

fate: 弹幕,倒计时,中奖弹窗

parent 3753bb8b
# 垂直弹幕参数
| 参数 | 说明 | 类型 | 默认值 |
| :------------------------- | :------------- | :------ | :----- |
| dataList | 弹幕列表 | String[] | - |
| barrageNum | 弹幕循环数量 | Number | 30 |
| quantity | 弹幕同时显示数量 | Number | 2 |
## 垂直弹幕使用步骤
1. 引入VerticalBarrage
2. 直接进入文件的<View className={styles.text}>{item}</View>处,定制自己需要的样式
# 横向弹幕参数
| 参数 | 说明 | 类型 | 默认值 |
| :------------------------- | :------------- | :------ | :----- |
| dataList | 弹幕列表 | String[] | - |
| trackCount | 弹幕轨道数量 | Number | 1 |
## 横向弹幕使用步骤
1. 引入Barrage
2. 直接进入文件的<View className={styles.text}>{item}</View>处,定制自己需要的样式
\ No newline at end of file
import React from 'react'
import { View, Text } from '@tarojs/components'
import styles from './Barrage.module.less'
class Barrage extends React.Component {
constructor(props) {
super(props)
// 页面数据存储
this.state = {
barrageWay: [],
barrageList: []
}
this.timer = null
}
componentDidMount() {
this.createData()
const self = this
setTimeout(() => {
self.addBarrageListObj()
}, 200)
}
componentWillUnmount() {
clearInterval(this.timer)
}
createData() {
const { dataList, trackCount = 1 } = this.props
const Tracks = Array.from(
{ length: trackCount },
(v, k) => (k + 1) * 40 - 40
)
this.setState({
barrageList: dataList.map((item, index) => ({
width: item.length * 17 + 5,
top: Tracks[index < Tracks.length ? index : index % trackCount],
time: 5,
context: item
}))
})
}
/* *
* 因为从后台获取到的是全部的数据,所以要把数据分开,让每条数据有先后之分
* 每隔一秒往barrageList 数组插入一条数据
* */
addBarrageListObj() {
const self = this
const { barrageList, barrageWay } = this.state
let i = 0
this.timer = setInterval(function () {
barrageWay.push(barrageList[i])
self.setState({
barrageWay: barrageWay
})
i++
if (i === barrageList.length - 1) {
if (self.props.Infinite) {
i = 0
} else {
clearInterval(self.timer)
}
}
}, 1500)
}
render() {
const { barrageWay } = this.state
return (
<View className={styles['barrage-fly']}>
{barrageWay.map((item, i) => {
if (!item?.context) return null
return (
<View
key={i}
className={styles['barrage-textFly']}
style={`animation: ${styles.first} ${item.time}s linear forwards; top: ${item.top}px`}
>
{/* 弹幕内容 */}
<Text className={styles['barrage-text']}>{item.context}</Text>
</View>
)
})}
</View>
)
}
}
export default Barrage
.barrage-fly {
position: relative;
height: 100%;
width: 700px;
z-index: 3;
.barrage-textFly {
position: absolute;
height: 64px;
line-height: 64px;
color: #f9c797;
font-size: 32px;
padding-right: 30px;
}
.barrage-text {
white-space: nowrap;
}
}
@keyframes first {
from {
left: 100%;
}
to {
left: -100%;
}
}
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'
import { View } from '@tarojs/components'
import Taro, { useReady } from '@tarojs/taro'
import styles from './VerticalBarrage.module.less'
const animation = Taro.createAnimation({
transformOrigin: 'top right',
duration: 850,
timeFunction: 'ease-in-out',
delay: 150
})
const VerticalBarrage = (props) => {
const { dataList, barrageNum = 30, quantity = 2 } = props
const [ height, setHeight ] = useState(0)
const [ animationData, setAnimationData ] = useState(null)
const nowIndex = useRef(0)
const timer = useRef(null)
useReady(() => {
const query = Taro.createSelectorQuery()
query.select('#barrage_0').boundingClientRect().exec((res) => {
console.log(res)
const h = res[0].height
setHeight(h)
})
})
const barrageList = useMemo(() => {
if (!dataList.length) return []
if (barrageNum && dataList.length < barrageNum) {
let reslut = [ ...dataList ]
while (reslut.length < barrageNum) {
reslut = [ ...reslut, ...dataList ]
}
return [ ...reslut.slice(0, barrageNum), ...reslut.slice(0, quantity) ]
} else {
return [ ...dataList.slice(0, barrageNum), ...dataList.slice(0, quantity) ]
}
}, [ dataList ])
const callback = useCallback(() => {
timer.current = setInterval(() => {
const data = animation.translateY(-nowIndex.current * height).step()
setAnimationData(data.export())
if (nowIndex.current < barrageList.length - quantity) {
nowIndex.current = nowIndex.current + 1
} else {
nowIndex.current = 0
}
}, 1000)
}, [ barrageList, height ])
useEffect(() => {
callback()
return () => clearInterval(timer.current)
}, [ callback ])
// 控制淡入淡出
const barrageStyle = (index) => {
if (quantity === 1) {
return `${(nowIndex.current === index && nowIndex.current !== 0) ? styles.in : ''} ${(nowIndex.current - 1 === index) ? styles.out : ''}`
} else {
return `${(nowIndex.current + quantity - 1 === index && nowIndex.current !== 0) ? styles.in : ''} ${nowIndex.current - 1 === index ? styles.out : ''}`
}
}
return (
<View className={styles['barrage-container']} style={{ height: height * quantity }}>
<View
className={styles['barrage-box']}
animation={animationData}
style={nowIndex.current === 0 && { transform: 'translateY(0)' }}
>
{
barrageList.map((item, i) => (
<View key={`barrage-${i}`} id={`barrage_${i}`} className={barrageStyle(i)}>
<View className={styles.text}>{item}</View>
</View>
))
}
</View>
</View>
)
}
export default VerticalBarrage
.barrage-container {
border: 1px solid red;
overflow: hidden;
}
.barrage-box {
z-index: 1;
.text {
padding: 10px 0;
}
}
.in {
animation: fadeIn 1.5s;
animation-fill-mode: forwards;
}
.out {
animation: fadeOut 1s;
animation-fill-mode: forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
\ No newline at end of file
import VerticalBarrage from './VerticalBarrage/VerticalBarrage'
import Barrage from './HorizontalBarrage/Barrage'
export {
VerticalBarrage,
Barrage
}
import React, { useState, useEffect, useRef } from 'react' import React from 'react'
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import { useDidShow, useDidHide } from '@tarojs/taro'
import tbcc from 'tbcc-sdk-ts' import tbcc from 'tbcc-sdk-ts'
import './CountDown.less' import './Countdown.less'
const { getServerTime } = tbcc.tb const { getServerTime } = tbcc.tb
export default function CountDown(props) { export default class Countdown extends React.Component {
const { endTime, startTime, onUpdate, type = 1, color = '#000', bgColor = 'transparent', fontSize = '26rpx', padding = '0' } = props static defaultProps = {
const [countDown, setCountDown] = useState(type === 1 ? ['00', ':', '00', ':', '00'] : ['00', '天', '00', '时', '00', '分', '00', '秒']) targetTime: '', // 以时间来进行倒计时
const [nowTime, setNowTime] = useState(startTime || Date.now()) startTime: '', // 开始时间
const countTimer = useRef(null) count: '', // 以秒数来进行倒计时
const isAccessRender = useRef(false)
// targetTime 模式下生效
useEffect(() => { showText: false, // 显示时分秒
countTimeFn() showDay: false, // 显示天数
return () => clearInterval(countTimer.current) symbol: ':', // 间隔符号
}, [endTime, nowTime]) isClose: false, // 手动关闭倒计时
useDidShow(() => { // 共用事件
if(isAccessRender.current) { onTick: () => { }, // 倒计时过程事件
setNowTime(startTime || Date.now()) onEnd: () => { } // 倒计时结束事件
} };
isAccessRender.current = true
}) constructor() {
super(...arguments)
const countTimeFn = async () => { this.state = {
const _nowTime = await getServerTime() day: '0',
hour: '00',
let diffTime = endTime - (startTime || _nowTime) minute: '00',
countTimer.current = setInterval(() => { second: '00',
if (diffTime > 1000) { countText: '0'
let day = Math.floor(diffTime / (3600 * 1000) / 24) }
let hour = Math.floor((diffTime / 1000 / 3600) % 24) this.targetTimestamp = null
let minute = Math.floor((diffTime / 1000 / 60) % 60) this.startTimestamp = null
let second = Math.floor(diffTime / 1000 % 60) this.timer = null
day = day < 10 ? '0' + day : day }
hour = hour < 10 ? '0' + hour : hour
minute = minute < 10 ? '0' + minute : minute componentDidMount() {
second = second < 10 ? '0' + second : second const { targetTime, count } = this.props
setCountDown(type === 1 ? [hour, ':', minute, ':', second] : [day, '天', hour, '时', minute, '分', second, '秒']) if (targetTime) {
diffTime -= 1000 this.formatTargetTime(targetTime)
} else { } else if (count) {
setCountDown(type === 1 ? ['00', ':', '00', ':', '00'] : ['00', '天', '00', '时', '00', '分', '00', '秒']) this.setCountdown(count)
clearInterval(countTimer.current) }
onUpdate && onUpdate() }
UNSAFE_componentWillReceiveProps(nextProps) {
const { targetTime, count } = nextProps
if (targetTime && targetTime !== this.props.targetTime) {
this.formatTargetTime(targetTime)
} else if (count && count !== this.props.count) {
clearInterval(this.timer)
this.setCountdown(count)
}
}
componentWillUnmount() {
this.targetTimestamp = null
clearInterval(this.timer)
}
componentDidHide() {
this.targetTimestamp = null
clearInterval(this.timer)
}
componentDidShow() {
const { targetTime, count } = this.props
if (targetTime) {
this.formatTargetTime(targetTime)
} else if (count) {
this.setCountdown(count)
}
}
setCountdown(count) {
if (count <= 0) {
return
}
let second = count
this.setState({
countText: second
})
this.timer = setInterval(() => {
second--
this.setState({
countText: second
})
this.props.onTick && this.props.onTick(second)
if (second <= 0) {
clearInterval(this.timer)
this.onTimeEnd()
} }
}, 1000) }, 1000)
} }
return ( // 处理日期格式
<View className="count-down"> formatTargetTime(targetTime) {
{ if (!targetTime) {
countDown.map((item, i) => { return
return ( }
<Text className="count-down-time" key={i} style={{ backgroundColor: item !== ':' ? bgColor : 'transparent', fontSize, color, padding }}>{item}</Text> // 避免iOS端上的日期格式有问题
) const time = targetTime.replace(/-/g, '/')
}) this.targetTimestamp = new Date(time).getTime()
} this.getRemainingSecond()
</View> }
)
} // 计算剩余时间秒数
\ No newline at end of file async getRemainingSecond() {
if (!this.targetTimestamp) return
// 当前时间
const currentTimestamp = await getServerTime()
// 剩余时间
const remainingSecond = Math.floor((this.targetTimestamp - currentTimestamp) / 1000)
// 天 时 分 秒
let day = ''
let hour = ''
let minute = ''
let second = ''
if (remainingSecond > 0 && !this.props.isClose) {
day = this.formatNum(parseInt(remainingSecond / 86400))
hour = this.props.showDay
? this.formatNum(parseInt((remainingSecond % 86400) / 3600))
: this.formatNum(parseInt(remainingSecond / 3600))
minute = this.formatNum(parseInt((remainingSecond % 3600) / 60))
second = this.formatNum(parseInt((remainingSecond % 3600) % 60))
this.timeTick(remainingSecond)
} else {
day = '0'
hour = minute = second = '00'
this.onTimeEnd()
}
this.setTimeState(day, hour, minute, second)
}
// 设置时间
setTimeState(day, hour, minute, second) {
// 是否显示天时分秒文字
if (this.props.showText) {
hour += '时'
minute += '分'
second += '秒'
}
this.setState({
day,
hour,
minute,
second
})
}
// 格式数字
formatNum(num) {
return num > 9 ? `${num}` : `0${num}`
}
// 倒计时过程事件
timeTick(remainingSecond) {
this.props.onTick && this.props.onTick(remainingSecond)
setTimeout(() => {
this.getRemainingSecond()
}, 1000)
}
// 倒计时结束触发事件
onTimeEnd() {
this.targetTimestamp = null
this.props.onEnd && this.props.onEnd()
}
render() {
const { symbol, showDay, showText, className, targetTime } = this.props
const { day, hour, minute, second, countText } = this.state
// 末尾有空格
let countdownClass = 'countdown '
if (className) countdownClass += className
if (targetTime) {
return (
<View className={countdownClass}>
{showDay && day !== '00' && (
<Text>
<Text className='day'>{day}</Text>
<Text className='day-text'></Text>
</Text>
)}
<Text>
<Text className='hour'>{hour}</Text>
{!showText && <Text className='symbol'>{symbol}</Text>}
</Text>
<Text className='minute'>{minute}</Text>
{!showText && <Text className='symbol'>{symbol}</Text>}
<Text className='second'>{second}</Text>
</View>
)
}
return (
<View className={countdownClass}>
<Text className='second'>{countText}</Text>
</View>
)
}
}
.count-down { .countdown {
display: flex; display: inline-block;
align-items: center; box-sizing: border-box;
}
.count-down-time {
border-radius: 4rpx;
} }
\ No newline at end of file
## 参数
| 参数 | 说明 | 类型 | 默认值 | 注意 |
| :------------------------- | :------------- | :------ | :----- | :------ |
| targetTime | 目标时间 | String | - | 优先级比count高 |
| count | 倒计秒数 | Number | - | - |
| 以下为使用targetTime 生效 |
| symbol | 间隔符号 | String | - | - |
| showDay | 是否显示天 | Boolean | false | 配合showText同时搭配 |
| showText | 是否显示时分秒 | Boolean | false | 打开自动无视symbol |
| isClose | 是否关闭倒计时 | Boolean | false | - |
## 事件
| 事件名称 | 说明 | 返回参数 |
| :------- | :--------------- | :--------- |
| onTick | 倒计时过程事件 | 剩余的秒数 |
| onEnd | 倒计时结束时触发 | - |
## 实例
```jsx
<CounDown targetTime="2021-10-01 09:00:00">
```
import Countdown from './CountDown'
export default Countdown
import React, { useState, useEffect, useRef } from 'react'
import { View, Text } from '@tarojs/components'
import { useDidShow, useDidHide } from '@tarojs/taro'
import tbcc from 'tbcc-sdk-ts'
import './CountDown.less'
const { getServerTime } = tbcc.tb
export default function CountDown(props) {
const { endTime, startTime, onUpdate, type = 1, color = '#000', bgColor = 'transparent', fontSize = '26rpx', padding = '0' } = props
const [countDown, setCountDown] = useState(type === 1 ? ['00', ':', '00', ':', '00'] : ['00', '天', '00', '时', '00', '分', '00', '秒'])
const [nowTime, setNowTime] = useState(startTime || Date.now())
const countTimer = useRef(null)
const isAccessRender = useRef(false)
useEffect(() => {
countTimeFn()
return () => clearInterval(countTimer.current)
}, [endTime, nowTime])
useDidShow(() => {
if(isAccessRender.current) {
setNowTime(startTime || Date.now())
}
isAccessRender.current = true
})
const countTimeFn = async () => {
const _nowTime = await getServerTime()
let diffTime = endTime - (startTime || _nowTime)
countTimer.current = setInterval(() => {
if (diffTime > 1000) {
let day = Math.floor(diffTime / (3600 * 1000) / 24)
let hour = Math.floor((diffTime / 1000 / 3600) % 24)
let minute = Math.floor((diffTime / 1000 / 60) % 60)
let second = Math.floor(diffTime / 1000 % 60)
day = day < 10 ? '0' + day : day
hour = hour < 10 ? '0' + hour : hour
minute = minute < 10 ? '0' + minute : minute
second = second < 10 ? '0' + second : second
setCountDown(type === 1 ? [hour, ':', minute, ':', second] : [day, '天', hour, '时', minute, '分', second, '秒'])
diffTime -= 1000
} else {
setCountDown(type === 1 ? ['00', ':', '00', ':', '00'] : ['00', '天', '00', '时', '00', '分', '00', '秒'])
clearInterval(countTimer.current)
onUpdate && onUpdate()
}
}, 1000)
}
return (
<View className="count-down">
{
countDown.map((item, i) => {
return (
<Text className="count-down-time" key={i} style={{ backgroundColor: item !== ':' ? bgColor : 'transparent', fontSize, color, padding }}>{item}</Text>
)
})
}
</View>
)
}
\ No newline at end of file
.count-down {
display: flex;
align-items: center;
}
.count-down-time {
border-radius: 4rpx;
}
\ No newline at end of file
import React from 'react'
import { View, Text, Image } from '@tarojs/components'
import Modal from '@/components/_base/Modal/Modal'
import API from '@/api'
import tbcc from 'tbcc-sdk-ts'
import { useThrottle } from '@/hooks/useThrottle'
import './JackpotModal.less'
import useReceive from '@/hooks/useReceive'
const { receiveEnamePrize, receiveObjectPrize } = API
const { commonToast } = tbcc.tb
const JackpotModal = (props) => {
const { onClose, top = '50%', bg = '', width = 300, height = 300, prizesData = {}, receive = false } = props
const [ getReceive ] = useReceive({ receiveEnamePrize, receiveObjectPrize })
const handleClick = useThrottle(async() => {
if (receive) {
const prizeId = prizesData.id || prizesData._id
const type = prizesData.type
const result = await getReceive({ prizeId, type })
if (result) {
commonToast(result.message)
}
} else {
onClose()
}
})
return (
<Modal onClose={onClose} top={top}>
<View className='jackpot_content' style={{ width: `${width / 100}rem`, height: `${height / 100}rem`, backgroundImage: `url(${bg})` }}>
<View className='title_box'>
<Text className='title'>恭喜您</Text>
</View>
<Text className='notify'>抽到了以下奖品</Text>
<View className='gift_box'>
<View className='gift'>
<Image src={prizesData?.image} />
</View>
<Text className='gift_name'>{prizesData?.name}</Text>
</View>
<View className='btn' onTap={handleClick}>领取奖励</View>
<Text className='hint'>奖品可在<Text className='hint_hot'>“我的奖品”</Text>中查看</Text>
</View>
</Modal>
)
}
export default JackpotModal
.jackpot_content {
background-color: #FFF;
.image-property();
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: inset 0.469px 0.883px 1px 0px rgba(249, 223, 203, 0.6);
.title_box {
display: flex;
justify-content: center;
align-items: center;
width: 322px;
height: 70px;
.title {
font-size: 36.364px;
color: #000
}
}
.notify {
margin-top: 45px;
}
.gift_box {
margin-top: 25px;
text-align: center;
.gift {
border-radius: 12px;
background-color: rgb(255, 244, 235);
width: 224px;
height: 225px;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.gift_name {
margin-top: 10px;
}
}
.btn {
margin-top: 40px;
width: 361px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
}
}
\ No newline at end of file
## 参数
| 参数 | 说明 | 类型 | 默认值 |
| :--------- | :----------------------------- | :------- | :----- |
| width | 宽度 | Number | - |
| height | 高度 | Number | - |
| top | 定位 | String | - |
| bg | 背景 | String | false |
| prizesData | 奖品数据 | Object | false |
| receive | 领取按钮是回到页面还是直接领取 | Boolean | false |
| onClose | 关闭按钮的回调 | Function | false |
### 注
```javascript
prizesData = {
_id: "奖品的id",
type: "奖品的类型",
image: "奖品图片url",
name: "奖品名字"
}
```
## useReceive 用法
```javascript
const [ getReceive ] = useReceive({ receiveEnamePrize, receiveObjectPrize })
// 引入useReceive Hooks,调用时传入receiveEnamePrize(优惠券和积分接口), receiveObjectPrize(实物接口),返回一个处理函数。
const result = await getReceive({ prizeId, type })
// 在用户领取时调用它,传入奖品id以及type(符合PRIZE_TYPE枚举格式),返回一个promise对象,处理完毕后成功或者用户身份验证失败等会返回对象(message, stest),如果是上阶段传入的领取接口失败则不返回任何信息。
```
\ No newline at end of file
import JackpotModal from './JackpotModal'
import useReceive from '@/hooks/useReceive'
export { useReceive }
export default JackpotModal
import { useCallback } from 'react'
import { checkIsMember } from 'tbcc-sdk-ts/lib/utils'
import tbccTs from 'tbcc-sdk-ts'
import { PRIZE_TYPE } from '@/const'
const { getUserAddress } = tbccTs.tb
const useReceive = (config) => {
const { receiveEnamePrize = () => { }, receiveObjectPrize = () => { } } = config
// 积分
const handleGetCredits = useCallback(async (id) => {
// 判断是否为会员
const isVip = await checkIsMember()
console.log(isVip)
if (!isVip) {
return {
message: '需加入会员才能领取成功哦',
state: 'error'
}
}
const { success, data } = await receiveEnamePrize({ id })
if (success && data) {
return {
message: '领取成功',
state: 'success'
}
}
}, [checkIsMember, receiveEnamePrize])
// 优惠券
const handleGetEquity = useCallback(async (id) => {
const { success, data } = await receiveEnamePrize({ id })
if (success && data) {
return {
message: '领取成功',
state: '领取成功'
}
}
}, [receiveEnamePrize])
// 实物
const handleReceiveObjectPrize = useCallback(async (params) => {
let errorMessage = ''
const { success, data } = await receiveObjectPrize(params).catch(res => {
errorMessage = res && res.message
}) || {}
if (success && data) {
return {
message: '领取成功',
state: 'success'
}
} else {
return {
message: errorMessage,
state: 'error'
}
}
}, [receiveObjectPrize])
// 确认地址
const handleChooseAddress = useCallback(async (id) => {
let errorMessage = ''
const userAddress = await getUserAddress().catch(err => {
errorMessage = err && err.errorMessage
})
if (!userAddress) {
return {
message: errorMessage,
state: 'error'
}
}
const { name, telNumber, provinceName, cityName, cityCode, countyName, detailInfo, streetName } = userAddress || {}
const params = {
name,
phone: telNumber,
addressDetail: detailInfo,
cityCode,
city: cityName,
province: provinceName,
area: countyName,
streetName,
id
}
const result = await my.confirm({
title: '提示',
content: '确认使用该收货地址:' + name + telNumber + userAddress.duibaAddress.address,
confirmButtonText: '确定',
cancelButtonText: '取消'
})
if (result.confirm) {
return await handleReceiveObjectPrize(params)
}
}, [getUserAddress])
// 导出的方法
const exportFn = useCallback(async ({ type, prizeId }) => {
// 领取优惠券
if (type === PRIZE_TYPE.ENAME) return await handleGetEquity(prizeId)
// 领取实物
if (type === PRIZE_TYPE.OBJECT) return await handleChooseAddress(prizeId)
// 领取积分
if (type === PRIZE_TYPE.CREDITS) return await handleGetCredits(prizeId)
}, [handleGetEquity, handleChooseAddress, handleGetCredits])
return [exportFn]
}
export default useReceive
...@@ -9,7 +9,7 @@ import API from '@/api' ...@@ -9,7 +9,7 @@ import API from '@/api'
import RuleModal from '@/components/_tb_modal/RuleModal/RuleModal' import RuleModal from '@/components/_tb_modal/RuleModal/RuleModal'
import DoHelpModal from '@/components/_tb_modal/DoHelpModal/DoHelpModal' import DoHelpModal from '@/components/_tb_modal/DoHelpModal/DoHelpModal'
import TasksModal from '@/components/_tb_modal/TasksModal/TasksModal' import TasksModal from '@/components/_tb_modal/TasksModal/TasksModal'
import CountDown from '@/components/_tb_comps/CountDown/CountDown' import CountDown from '@/components/_tb_comps/CountDown'
import styles from './index.module.less' import styles from './index.module.less'
import tbcc from 'tbcc-sdk-ts' import tbcc from 'tbcc-sdk-ts'
import { useEffect } from 'react' import { useEffect } from 'react'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment