Vue3文字滚动(TextScroll)

文章详细介绍了如何在Vue.js中创建一个可自定义设置的滚动文字组件,包括滚动文字数组、单条滚动效果、区域尺寸、字体样式、动画参数等属性。同时,组件支持水平和垂直滚动,提供了鼠标悬停暂停功能,并通过rafTimeout进行性能优化。示例代码展示了组件的实现和使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

可自定义设置以下属性: 

  • 滚动文字数组(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>

 其中引入使用了以下组件:

②在要使用的页面引入:

<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>
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theMuseCatcher

您的支持是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值