可自定义设置以下属性:
-
滚动文字数组(items),类型:Item[] | Item,默认 [];single 为 true 时,类型为 Item;多条文字水平滚动时,数组长度必须大于等于 amount 才能滚动
-
是否启用单条文字滚动效果(single),类型:boolean,默认 false;水平滚动时生效;为 true 时,amount 自动设为 1
-
滚动区域宽度(width),类型:string | number,单位 px,默认 '100%'
-
滚动区域高度(height),类型:number,单位 px,默认 50
-
滚动文字样式(itemStyle),类型:CSSProperties,默认 {}
-
链接文字鼠标悬浮颜色(hrefHoverColor),类型:string,默认 undefined;仅当 href 存在时生效
-
滚动区域展示条数(amount),类型:number,默认 4;水平滚动时生效
-
水平滚动文字各列间距或垂直滚动文字两边的间距(gap),类型:number,单位 px,默认 20
-
水平滚动时移动的速度(speed),类型:number,单位是像素每秒,默认 48;水平滚动时生效
-
是否垂直滚动(vertical),类型:boolean,默认 false
-
垂直滚动过渡持续时间(duration),类型:number,单位 ms,默认 1000;垂直滚动时生效
-
垂直文字滚动时间间隔(interval),类型:number,单位 ms,默认 3000;垂直滚动时生效
-
鼠标移入是否暂停滚动(pauseOnMouseEnter),类型:boolean,默认 false
效果如下图:
在线预览
①创建文字滚动组件TextScroll.vue:
其中引入使用了以下工具函数:
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import type { CSSProperties } from 'vue'
import { rafTimeout, cancelRaf, useResizeObserver, useInject } from 'components/utils'
export interface Item {
title: string // 文字标题
href?: string // 跳转链接
target?: '_self' | '_blank' // 跳转链接打开方式,href 存在时生效
}
export interface Props {
items?: Item[] | Item // 滚动文字数组,single 为 true 时,类型为 Item;多条文字水平滚动时,数组长度必须大于等于 amount 才能滚动
single?: boolean // 是否启用单条文字滚动效果,水平滚动时生效;为 true 时,amount 自动设为 1
width?: number | string // 滚动区域宽度,单位 px
height?: number // 滚动区域高度,单位 px
itemStyle?: CSSProperties // 滚动文字样式
hrefHoverColor?: string // 链接文字鼠标悬浮颜色;仅当 href 存在时生效
amount?: number // 滚动区域展示条数,水平滚动时生效
gap?: number // 水平滚动文字各列间距或垂直滚动文字两边的间距,单位 px
speed?: number // 水平滚动时移动的速度,单位是像素每秒,水平滚动时生效
vertical?: boolean // 是否垂直滚动
duration?: number // 垂直滚动过渡持续时间,单位 ms,垂直滚动时生效
interval?: number // 垂直文字滚动时间间隔,单位 ms,垂直滚动时生效
pauseOnMouseEnter?: boolean // 鼠标移入是否暂停滚动
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
single: false,
width: '100%',
height: 50,
itemStyle: () => ({}),
hrefHoverColor: undefined,
amount: 4,
gap: 20,
speed: 48,
vertical: false,
duration: 1000,
interval: 3000,
pauseOnMouseEnter: false
})
const horizontalRef = ref() // 水平滚动 DOM 引用
const horizontalWrapWidth = ref<number>(0) // 水平滚动容器宽度
const verticalRef = ref() // 垂直滚动 DOM 引用
const groupRef = ref() // 水平滚动内容 DOM 引用
const groupWidth = ref<number>(0) // 水平滚动内容宽度
const playState = ref<'paused' | 'running'>('paused') // 水平滚动动画执行状态
const reset = ref<boolean>(true) // 重置水平滚动动画状态
const activeIndex = ref<number>(0) // 垂直滚动当前索引
const verticalMoveRaf = ref() // 垂直滚动定时器引用标识
const originVertical = ref<boolean>(true) // 垂直滚动初始状态
const scrollItems = ref<Item[]>([])
const { colorPalettes } = useInject('TextScroll') // 主题色注入
const emit = defineEmits(['click'])
const itemsAmount = computed(() => {
return scrollItems.value.length
})
// 文字滚动区域尺寸样式
const scrollBoardStyle = computed(() => {
return {
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: `${props.height}px`
}
})
// 水平滚动时展示条数
const displayAmount = computed(() => {
if (props.single) {
return 1
} else {
return props.amount
}
})
const itemWidth = computed(() => {
// 水平滚动单条文字宽度
return parseFloat((horizontalWrapWidth.value / displayAmount.value).toFixed(2))
})
const animationDuration = computed(() => {
// 水平滚动动画持续时间
return groupWidth.value / props.speed
})
watch(
() => props.items,
() => {
if (props.single) {
scrollItems.value = [props.items] as Item[]
} else {
if (props.vertical && (props.items as Item[]).length === 1) {
scrollItems.value = [...(props.items as Item[]), ...(props.items as Item[])]
} else {
scrollItems.value = [...(props.items as Item[])]
}
}
},
{
immediate: true,
deep: true
}
)
watch(scrollItems, () => {
resetMove()
})
watch(
() => [props.vertical, props.duration, props.interval],
() => {
initScroll()
},
{
deep: true,
flush: 'post'
}
)
useResizeObserver([horizontalRef, groupRef, verticalRef], () => {
initScroll()
})
function initScroll(): void {
verticalMoveRaf.value && cancelRaf(verticalMoveRaf.value)
if (!originVertical.value) {
originVertical.value = true
}
if (!props.vertical) {
getScrollSize()
}
startMove() // 开始滚动
}
// 获取水平滚动容器宽度;水平滚动内容宽度
function getScrollSize(): void {
horizontalWrapWidth.value = horizontalRef.value.offsetWidth
groupWidth.value = groupRef.value.offsetWidth
}
// 重置水平滚动状态
function resetScrollState(): void {
playState.value = 'paused'
nextTick(() => {
void horizontalRef.value?.offsetTop // 强制浏览器触发重排
playState.value = 'running'
})
}
// 当 CSS Animation 运动到最后一帧时触发
function onAnimationIteration(): void {
resetScrollState()
}
function verticalMove(): void {
verticalMoveRaf.value = rafTimeout(
() => {
if (originVertical.value) {
originVertical.value = false
}
activeIndex.value = (activeIndex.value + 1) % itemsAmount.value
},
originVertical.value ? props.interval : props.interval + props.duration,
true
)
}
function onClick(item: Item): void {
emit('click', item)
}
// 滚动开始
function startMove(): void {
if (props.vertical) {
if (itemsAmount.value >= 1) {
verticalMove() // 垂直滚动
}
} else {
if (itemsAmount.value >= displayAmount.value) {
// 超过 amount 条开始滚动
reset.value = false
playState.value = 'running' // 水平滚动
}
}
}
// 滚动暂停
function stopMove(): void {
if (props.vertical) {
originVertical.value = true
verticalMoveRaf.value && cancelRaf(verticalMoveRaf.value)
} else {
playState.value = 'paused'
}
}
// 滚动重置
function resetMove(): void {
if (props.vertical) {
verticalMoveRaf.value && cancelRaf(verticalMoveRaf.value)
if (activeIndex.value !== 0) {
activeIndex.value = 0
originVertical.value = false
} else {
originVertical.value = true
}
startMove()
} else {
playState.value = 'paused'
reset.value = true
nextTick(() => {
void horizontalRef.value?.offsetTop
startMove()
})
}
}
defineExpose({
start: startMove,
stop: stopMove,
reset: resetMove
})
</script>
<template>
<div
v-if="!vertical"
ref="horizontalRef"
class="m-scroll-horizontal"
:style="[
scrollBoardStyle,
`
--text-scroll-shadow-color: #d3d3d3;
--text-scroll-bg-color: #fff;
--text-scroll-href-hover-color: ${hrefHoverColor || colorPalettes[5]};
--text-scroll-item-gap: ${gap}px;
--text-scroll-play-state: ${playState};
--text-scroll-duration: ${animationDuration}s;
--text-scroll-delay: 0s;
--text-scroll-iteration-count: infinite;
`
]"
@mouseenter="pauseOnMouseEnter ? stopMove() : () => false"
@mouseleave="pauseOnMouseEnter ? startMove() : () => false"
>
<div
ref="groupRef"
class="scroll-items-group"
:class="{ 'scroll-items-reset': reset }"
@animationiteration="onAnimationIteration"
>
<component
:is="item.href ? 'a' : 'div'"
class="scroll-item"
:class="{ 'href-item': item.href }"
:style="[itemStyle, `width: ${itemWidth}px;`]"
v-for="(item, index) in <Item[]>scrollItems"
:key="index"
:title="item.title"
:href="item.href"
:target="item.target"
@click="onClick(item)"
>
{{ item.title }}
</component>
</div>
<div class="scroll-items-group" :class="{ 'scroll-items-reset': reset }">
<component
:is="item.href ? 'a' : 'div'"
class="scroll-item"
:class="{ 'href-item': item.href }"
:style="[itemStyle, `width: ${itemWidth}px;`]"
v-for="(item, index) in scrollItems"
:key="index"
:title="item.title"
:href="item.href"
:target="item.target"
@click="onClick(item)"
>
{{ item.title }}
</component>
</div>
</div>
<div
v-else
ref="verticalRef"
class="m-scroll-vertical"
:style="[
scrollBoardStyle,
`
--text-scroll-shadow-color: #d3d3d3;
--text-scroll-bg-color: #fff;
--text-scroll-href-hover-color: ${hrefHoverColor || colorPalettes[5]};
--text-scroll-duration: ${duration}ms;
--text-scroll-timing-function: ease;
--text-scroll-scale: 0.5;
--text-scroll-item-padding: ${gap}px;
`
]"
@mouseenter="pauseOnMouseEnter ? stopMove() : () => false"
@mouseleave="pauseOnMouseEnter ? startMove() : () => false"
>
<TransitionGroup name="slide">
<div class="scroll-item-wrap" v-for="(item, index) in scrollItems" :key="index" v-show="activeIndex === index">
<component
:is="item.href ? 'a' : 'div'"
class="scroll-item"
:class="{ 'href-item': item.href }"
:style="itemStyle"
:title="item.title"
:href="item.href"
:target="item.target"
@click="onClick(item)"
>
{{ item.title }}
</component>
</div>
</TransitionGroup>
</div>
</template>
<style lang="less" scoped>
// 水平滚动
.m-scroll-horizontal {
overflow: hidden;
display: flex;
box-shadow: 0px 0px 5px var(--text-scroll-shadow-color);
border-radius: 6px;
background-color: var(--text-scroll-bg-color);
.scroll-items-group {
z-index: 1;
display: flex;
align-items: center;
will-change: transform;
animation: horizontalScroll var(--text-scroll-duration) linear var(--text-scroll-delay)
var(--text-scroll-iteration-count);
animation-play-state: var(--text-scroll-play-state);
@keyframes horizontalScroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
.scroll-item {
padding-left: var(--text-scroll-item-gap);
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;
}
.href-item {
cursor: pointer;
transition: color 0.3s;
&:hover {
color: var(--text-scroll-href-hover-color) !important;
}
}
}
.scroll-items-reset {
animation: none;
}
}
// 垂直滚动
.slide-enter-active,
.slide-leave-active {
transition: all var(--text-scroll-duration) var(--text-scroll-timing-function);
}
.slide-enter-from {
transform: translateY(100%) scale(var(--text-scroll-scale));
opacity: 0;
}
.slide-leave-to {
transform: translateY(-100%) scale(var(--text-scroll-scale));
opacity: 0;
}
.m-scroll-vertical {
overflow: hidden;
position: relative;
box-shadow: 0px 0px 5px var(--text-scroll-shadow-color);
border-radius: 6px;
background-color: var(--text-scroll-bg-color);
.scroll-item-wrap {
position: absolute;
left: 0;
right: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--text-scroll-item-padding);
.scroll-item {
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;
}
.href-item {
cursor: pointer;
transition: color 0.3s;
&:hover {
color: var(--text-scroll-href-hover-color) !important;
}
}
}
}
</style>
其中引入使用了以下组件:
- Vue3栅格(Grid)
- Vue3滑动输入条(Slider)
- Vue3输入框(Input)
- Vue3数字输入框(InputNumber)
- Vue3弹性布局(Flex)
- Vue3间距(Space)
- Vue3按钮(Button)
- Vue3开关(Switch)
- Vue3颜色选择器(ColorPicker)
②在要使用的页面引入:
<script setup lang="ts">
import TextScroll from './TextScroll.vue'
import { ref, reactive } from 'vue'
import type { TextScrollItem } from 'vue-amazing-ui'
const scrollItems = ref<TextScrollItem[]>([
{
title: '美国作家杰罗姆·大卫·塞林格创作的唯一一部长篇小说',
href: 'https://blog.youkuaiyun.com/Dandrose?type=blog',
target: '_blank'
},
{
title: '《麦田里的守望者》首次出版于1951年',
href: 'https://blog.youkuaiyun.com/Dandrose?type=blog',
target: '_blank'
},
{
title: '塞林格将故事的起止局限于16岁的中学生霍尔顿·考尔菲德从离开学校到纽约游荡的三天时间内'
},
{
title: '并借鉴了意识流天马行空的写作方法,充分探索了一个十几岁少年的内心世界',
href: 'https://blog.youkuaiyun.com/Dandrose?type=blog',
target: '_blank'
},
{
title: '愤怒与焦虑是此书的两大主题,主人公的经历和思想在青少年中引起强烈共鸣',
href: 'https://blog.youkuaiyun.com/Dandrose?type=blog',
target: '_blank'
}
])
const singleItem: TextScrollItem = {
title: '请用一只玫瑰纪念我 🌹'
}
const textScroll = ref()
const disabled = ref<boolean>(true)
const vertical = ref<boolean>(false)
function onClick(item: TextScrollItem) {
// 获取点击的 item
console.log('item', item)
}
function handleStart() {
textScroll.value.start()
disabled.value = true
}
function handleStop() {
textScroll.value.stop()
disabled.value = false
}
function handleReset() {
textScroll.value.reset()
disabled.value = true
}
const state = reactive({
single: false,
height: 50,
fontSize: 16,
fontWeight: 400,
color: 'rgba(0, 0, 0, 0.88)',
backgroundColor: '#fff',
hrefHoverColor: '#1677ff',
amount: 4,
gap: 20,
speed: 48,
vertical: false,
duration: 1000,
interval: 3000,
pauseOnMouseEnter: false
})
</script>
<template>
<div>
<h1>{
{ $route.name }} {
{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">水平文字滚动</h2>
<TextScroll :items="scrollItems" @click="onClick" />
<h2 class="mt30 mb10">垂直文字滚动</h2>
<TextScroll
style="background-color: #e6f4ff"
:items="scrollItems"
:item-style="{ fontSize: '20px' }"
vertical
@click="onClick"
/>
<h2 class="mt30 mb10">单条文字滚动</h2>
<Flex vertical>
<TextScroll
:items="singleItem"
single
:width="280"
:item-style="{ fontSize: '24px', fontWeight: 600, color: 'darkred' }"
@click="onClick"
/>
<TextScroll
:items="[singleItem]"
vertical
:width="300"
:item-style="{ fontSize: '24px', fontWeight: 600, color: 'darkred' }"
@click="onClick"
/>
</Flex>
<h2 class="mt30 mb10">自定义样式</h2>
<TextScroll
style="background-color: #e6f4ff; border-radius: 12px"
:items="scrollItems"
:item-style="{ fontSize: '20px', fontWeight: 500, color: '#FF9800' }"
:height="60"
@click="onClick"
/>
<h2 class="mt30 mb10">自定义链接悬浮色</h2>
<TextScroll :items="scrollItems" href-hover-color="#ff6900" @click="onClick" />
<h2 class="mt30 mb10">自定义展示条数和间距</h2>
<TextScroll :items="scrollItems" :amount="3" :gap="30" @click="onClick" />
<h2 class="mt30 mb10">自定义滚动速度</h2>
<Flex vertical>
<TextScroll :items="scrollItems" :speed="72" @click="onClick" />
<TextScroll :items="scrollItems" vertical :duration="800" :interval="2000" @click="onClick" />
</Flex>
<h2 class="mt30 mb10">鼠标移入暂停</h2>
<Flex vertical>
<TextScroll :items="scrollItems" pause-on-mouse-enter @click="onClick" />
<TextScroll :items="scrollItems" vertical pause-on-mouse-enter @click="onClick" />
</Flex>
<h2 class="mt30 mb10">使用 Methods</h2>
<Flex vertical>
<Space vertical>
<Space align="center">
vertical:
<Switch v-model="vertical" />
</Space>
<Space>
<Button type="primary" :disabled="disabled" @click="handleStart">开始</Button>
<Button @click="handleStop">暂停</Button>
<Button type="primary" ghost @click="handleReset">重置</Button>
</Space>
</Space>
<TextScroll ref="textScroll" :vertical="vertical" :items="scrollItems" @click="onClick" />
</Flex>
<h2 class="mt30 mb10">文字滚动配置器</h2>
<Flex vertical>
<Row :gutter="[24, 12]">
<Col :span="6">
<Flex gap="small" vertical>
height:
<Slider v-model:value="state.height" :min="6" :max="180" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
fontSize:
<Slider v-model:value="state.fontSize" :min="6" :max="180" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
fontWeight:
<InputNumber v-model:value="state.fontWeight" :step="100" :min="100" :max="1000" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
color:
<ColorPicker v-model:value="state.color" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
backgroundColor:
<ColorPicker v-model:value="state.backgroundColor" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
hrefHoverColor:
<ColorPicker v-model:value="state.hrefHoverColor" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
amount:
<Slider v-model:value="state.amount" :min="1" :max="scrollItems.length" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
gap:
<Slider v-model:value="state.gap" :min="10" :max="100" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
speed:
<Slider v-model:value="state.speed" :min="10" :max="100" />
</Flex>
</Col>
<Col :span="6">
<Space gap="small" vertical>
vertical:
<Switch v-model="state.vertical" />
</Space>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
duration:
<Slider v-model:value="state.duration" :min="100" :step="100" :max="3000" />
</Flex>
</Col>
<Col :span="6">
<Flex gap="small" vertical>
interval:
<Slider v-model:value="state.interval" :min="1000" :step="100" :max="10000" />
</Flex>
</Col>
<Col :span="6">
<Space gap="small" vertical>
pauseOnMouseEnter:
<Switch v-model="state.pauseOnMouseEnter" />
</Space>
</Col>
</Row>
<TextScroll
:style="`background-color: ${state.backgroundColor}`"
:items="scrollItems"
:single="state.single"
:height="state.height"
:item-style="{
fontSize: state.fontSize + 'px',
fontWeight: state.fontWeight,
color: state.color
}"
:href-hover-color="state.hrefHoverColor"
:amount="state.amount"
:gap="state.gap"
:speed="state.speed"
:vertical="state.vertical"
:duration="state.duration"
:interval="state.interval"
:pause-on-mouse-enter="state.pauseOnMouseEnter"
@click="onClick"
/>
</Flex>
</div>
</template>