可自定义设置以下属性:
-
滚动文字数组(scrollText),类型:Array<{title: string, link?: string}>|{title: string, link?: string},必传,默认[],滚动文字数组,single 为 true 时,类型为 Text
-
是否启用单条文字滚动效果(single),只支持水平文字滚动,为 true 时,amount 自动设为 1,默认 false
-
滚动区域宽度(width),类型:number | string,默认 '100%'
-
滚动区域高度(height),类型:number,单位px,默认 50
-
滚动区域样式(boardStyle),优先级低于 width、height,类型:CSSProperties,默认 {}
-
滚动文字样式(textStyle),类型:CSSProperties,默认 {}
-
滚动区域展示条数,水平滚动时生效(amount),类型:number,默认 4
-
水平滚动文字各列间距或垂直滚动文字两边的边距(gap),类型:number,单位px,默认 20
-
水平滚动动画执行时间间隔(interval),类型:number,单位ms,水平滚动时生效,默认 10
-
水平滚动动画每次执行时移动距离(step),类型:number,单位px,水平滚动时生效,默认 1,与 interval 配合控制滚动速度
-
是否垂直滚动(vertical),类型:boolean,默认 false
-
垂直文字滚动时间间隔(verticalInterval),类型:number,单位ms,垂直滚动时生效,默认 3000
效果如下图: 在线预览
注:组件引用方法 import { rafTimeout, cancelRaf } from '../utils' 请参考以下博客:
其中引入使用了以下工具函数:
①创建文字滚动组件TextScroll.vue:
<script setup lang="ts">
import { ref, computed, watch, onMounted, type CSSProperties } from 'vue'
import { rafTimeout, cancelRaf, useResizeObserver } from '../utils'
interface Text {
title: string // 文字标题
link?: string // 跳转链接
}
interface Props {
scrollText: Text[] | Text // 滚动文字数组,single 为 true 时,类型为 Text
single?: boolean // 是否启用单条文字滚动效果,只支持水平文字滚动,为 true 时,amount 自动设为 1
width?: number | string // 滚动区域宽度,单位px
height?: number // 滚动区域高度,单位px
boardStyle?: CSSProperties // 滚动区域样式,优先级低于 width、height
textStyle?: CSSProperties // 滚动文字样式
amount?: number // 滚动区域展示条数,水平滚动时生效
gap?: number // 水平滚动文字各列间距或垂直滚动文字两边的边距,单位px
interval?: number // 水平滚动动画执行时间间隔,单位ms,水平滚动时生效
step?: number // 水平滚动动画每次执行时移动距离,单位px,水平滚动时生效,与 interval 配合控制滚动速度
vertical?: boolean // 是否垂直滚动
verticalInterval?: number // 垂直文字滚动时间间隔,单位ms,垂直滚动时生效
}
const props = withDefaults(defineProps<Props>(), {
scrollText: () => [],
single: false,
width: '100%',
height: 50,
boardStyle: () => ({}),
textStyle: () => ({}),
amount: 4,
gap: 20,
interval: 10,
step: 1,
vertical: false,
verticalInterval: 3000
})
const textData = computed(() => {
if (props.single) {
return [props.scrollText, props.scrollText]
} else {
return [...(props.scrollText as Text[])]
}
})
const textAmount = computed(() => {
return textData.value.length || 0
})
const totalWidth = computed(() => {
// 文字滚动区域总宽度
if (typeof props.width === 'number') {
return props.width + 'px'
} else {
return props.width
}
})
const displayAmount = computed(() => {
if (props.single) {
return 1
} else {
return props.amount
}
})
const left = ref(0)
const horizontalMoveRaf = ref() // 水平滚动引用
const verticalMoveRaf = ref() // 垂直滚动引用
const origin = ref(true) // 垂直滚动初始状态
const horizonRef = ref()
const distance = ref(0) // 每条滚动文字移动距离
watch(
() => [
textData,
props.width,
props.amount,
props.gap,
props.step,
props.interval,
props.vertical,
props.verticalInterval
],
() => {
initScroll()
},
{
deep: true, // 强制转成深层侦听器
flush: 'post'
}
)
useResizeObserver(horizonRef, () => {
initScroll()
})
onMounted(() => {
initScroll()
})
function initScroll() {
if (!props.vertical) {
distance.value = getDistance() // 获取每列文字宽度
} else {
origin.value = true
}
horizontalMoveRaf.value && cancelRaf(horizontalMoveRaf.value)
verticalMoveRaf.value && cancelRaf(verticalMoveRaf.value)
startMove() // 开始滚动
}
function getDistance(): number {
return parseFloat((horizonRef.value.offsetWidth / displayAmount.value).toFixed(2))
}
function startMove() {
if (props.vertical) {
if (textAmount.value > 1) {
verticalMoveRaf.value && cancelRaf(verticalMoveRaf.value)
verticalMove() // 垂直滚动
}
} else {
if (textAmount.value > displayAmount.value) {
// 超过 amount 条开始滚动
horizontalMoveRaf.value && cancelRaf(horizontalMoveRaf.value)
horizonMove() // 水平滚动
}
}
}
function horizonMove() {
horizontalMoveRaf.value = rafTimeout(
() => {
if (left.value >= distance.value) {
textData.value.push(textData.value.shift() as Text) // 将第一条数据放到最后
left.value = 0
} else {
left.value += props.step // 每次移动step(px)
}
},
props.interval,
true
)
}
function stopMove() {
// 暂停动画
if (props.vertical) {
verticalMoveRaf.value && cancelRaf(verticalMoveRaf.value)
} else {
horizontalMoveRaf.value && cancelRaf(horizontalMoveRaf.value)
}
}
const emit = defineEmits(['click'])
function onClick(text: Text) {
// 通知父组件点击的标题
emit('click', text)
}
const actIndex = ref(0)
function verticalMove() {
verticalMoveRaf.value = rafTimeout(
() => {
if (origin.value) {
origin.value = false
}
actIndex.value = (actIndex.value + 1) % textAmount.value
verticalMove()
},
origin.value ? props.verticalInterval : props.verticalInterval + 1000
)
}
</script>
<template>
<div
v-if="!vertical"
ref="horizonRef"
class="m-slider-horizon"
:style="[boardStyle, `height: ${height}px; width: ${totalWidth}; --gap: ${gap}px;`]"
>
<div class="m-scroll-view" :style="`will-change: transform; transform: translateX(${-left}px);`">
<a
class="u-slide-title"
:style="[textStyle, `width: ${distance - gap}px;`]"
v-for="(text, index) in <Text[]>textData"
:key="index"
:title="text.title"
:href="text.link ? text.link : 'javascript:;'"
:target="text.link ? '_blank' : '_self'"
@mouseenter="stopMove"
@mouseleave="startMove"
@click="onClick(text)"
>
{{ text.title || '--' }}
</a>
</div>
</div>
<div
v-else
class="m-slider-vertical"
:style="[
boardStyle,
`height: ${height}px; width: ${totalWidth}; --enter-move: ${height}px; --leave-move: ${-height}px; --gap: ${gap}px;`
]"
>
<TransitionGroup name="slide">
<div class="m-scroll-view" v-for="(text, index) in <Text[]>textData" :key="index" v-show="actIndex === index">
<a
class="u-slider"
:style="textStyle"
:title="text.title"
:href="text.link ? text.link : 'javascript:;'"
:target="text.link ? '_blank' : '_self'"
@mouseenter="stopMove"
@mouseleave="startMove"
@click="onClick(text)"
>
{{ text.title || '--' }}
</a>
</div>
</TransitionGroup>
</div>
</template>
<style lang="less" scoped>
// 水平滚动
.m-slider-horizon {
overflow: hidden;
box-shadow: 0px 0px 5px #d3d3d3;
border-radius: 6px;
background-color: #fff;
.m-scroll-view {
height: 100%;
display: inline-flex;
align-items: center;
gap: var(--gap);
padding-left: var(--gap);
.u-slide-title {
font-size: 16px;
font-weight: 400;
color: rgba(0, 0, 0, 0.88);
line-height: 1.57;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @themeColor;
}
}
}
}
// 垂直滚动
.slide-enter-active,
.slide-leave-active {
transition: all 1s ease;
}
.slide-enter-from {
transform: translateY(var(--enter-move)) scale(0.5);
opacity: 0;
}
.slide-leave-to {
transform: translateY(var(--leave-move)) scale(0.5);
opacity: 0;
}
.m-slider-vertical {
overflow: hidden;
box-shadow: 0px 0px 5px #d3d3d3;
border-radius: 6px;
background-color: #fff;
position: relative;
.m-scroll-view {
position: absolute;
left: 0;
right: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--gap);
.u-slider {
font-size: 16px;
font-weight: 400;
color: rgba(0, 0, 0, 0.88);
line-height: 1.57;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @themeColor;
}
}
}
}
</style>
其中引入使用了
- Vue3栅格(Grid)
- Vue3滑动输入条(Slider)
- Vue3输入框(Input)
- Vue3数字输入框(InputNumber)
- Vue3弹性布局(Flex)
- Vue3间距(Space)
- Vue3开关(Switch)
②在要使用的页面引入:
<script setup lang="ts">
import TextScroll from './TextScroll.vue'
import { ref, reactive } from 'vue'
const scrollText = ref<any[]>([
{
title: '美国作家杰罗姆·大卫·塞林格创作的唯一一部长篇小说',
link: 'https://blog.csdn.net/Dandrose?type=blog'
},
{
title: '首次出版于1951年'
},
{
title:
'塞林格将故事的起止局限于16岁的中学生霍尔顿·考尔菲德从离开学校到纽约游荡的三天时间内,塞林格将故事的起止局限于16岁的中学生霍尔顿·考尔菲德从离开学校到纽约游荡的三天时间内'
},
{
title: '并借鉴了意识流天马行空的写作方法,充分探索了一个十几岁少年的内心世界'
},
{
title: '愤怒与焦虑是此书的两大主题,主人公的经历和思想在青少年中引起强烈共鸣'
}
])
const singleText = {
title: '请用一只玫瑰纪念我...',
link: 'https://blog.csdn.net/Dandrose?type=blog'
}
function onClick(text: any) {
// 获取点击的标题
console.log('text:', text)
}
const state = reactive({
single: false,
height: 60,
fontSize: 16,
fontWeight: 400,
color: 'rgba(0, 0, 0, 0.88)',
backgroundColor: '#FFF',
amount: 4,
gap: 20,
interval: 10,
step: 1,
vertical: false,
verticalInterval: 3000
})
</script>
<template>
<div>
<h1>{{ $route.name }} {{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">水平文字滚动</h2>
<TextScroll :scrollText="scrollText" @click="onClick" />
<h2 class="mt30 mb10">自定义滚动速度</h2>
<TextScroll :scrollText="scrollText" :step="2" @click="onClick" />
<h2 class="mt30 mb10">单条文字滚动</h2>
<TextScroll
:scrollText="singleText"
single
:width="360"
:text-style="{ fontSize: '24px', fontWeight: 600, color: '#FF5B29' }"
@click="onClick"
/>
<h2 class="mt30 mb10">垂直文字滚动</h2>
<TextScroll
:scrollText="scrollText"
:board-style="{ backgroundColor: '#e6f4ff' }"
:text-style="{ fontSize: '20px' }"
vertical
@click="onClick"
/>
<h2 class="mt30 mb10">自定义样式</h2>
<TextScroll
:scrollText="scrollText"
:board-style="{ backgroundColor: '#e6f4ff', borderRadius: '12px' }"
:text-style="{ fontSize: '28px', color: '#FF9800' }"
:gap="30"
:height="80"
@click="onClick"
/>
<h2 class="mt30 mb10">文字滚动配置器</h2>
<Row :gutter="[24, 8]">
<Col :span="6">
<Flex vertical gap="middle">
height:
<Slider v-model:value="state.height" :min="6" :max="180" />
</Flex>
</Col>
<Col :span="6">
<Flex vertical gap="middle">
fontSize:
<Slider v-model:value="state.fontSize" :min="6" :max="180" />
</Flex>
</Col>
<Col :span="6">
<Flex vertical>
fontWeight:
<InputNumber v-model:value="state.fontWeight" :step="100" :min="100" :max="1000" />
</Flex>
</Col>
<Col :span="6">
<Space vertical>
color:
<Input v-model:value="state.color" placeholder="color" />
</Space>
</Col>
<Col :span="6">
<Flex vertical>
backgroundColor:
<Input v-model:value="state.backgroundColor" placeholder="backgroundColor" />
</Flex>
</Col>
<Col :span="6">
<Flex vertical gap="middle">
amount:
<Slider v-model:value="state.amount" :min="1" :max="scrollText.length" />
</Flex>
</Col>
<Col :span="6">
<Flex vertical gap="middle">
gap:
<Slider v-model:value="state.gap" :min="10" :max="100" />
</Flex>
</Col>
<Col :span="6">
<Flex vertical gap="middle">
interval:
<Slider v-model:value="state.interval" :min="5" :max="100" />
</Flex>
</Col>
<Col :span="6">
<Flex vertical gap="middle">
step:
<Slider v-model:value="state.step" :min="0.1" :step="0.1" :max="20" />
</Flex>
</Col>
<Col :span="6">
<Space vertical>
vertical:
<Switch v-model:checked="state.vertical" />
</Space>
</Col>
<Col :span="6">
<Flex vertical gap="middle">
verticalInterval:
<Slider v-model:value="state.verticalInterval" :min="1000" :step="100" :max="10000" />
</Flex>
</Col>
</Row>
<TextScroll
class="mt30"
:scrollText="scrollText"
:single="state.single"
:height="state.height"
:board-style="{
backgroundColor: state.backgroundColor
}"
:text-style="{
fontSize: state.fontSize + 'px',
fontWeight: state.fontWeight,
color: state.color
}"
:amount="state.amount"
:gap="state.gap"
:interval="state.interval"
:step="state.step"
:vertical="state.vertical"
:vertical-interval="state.verticalInterval"
@click="onClick"
/>
</div>
</template>