Vue3文字滚动(TextScroll)

简介: 这是一个可定制的文字滚动组件,支持水平和垂直滚动。主要属性包括滚动文字数组 `scrollText`、是否启用单条文字滚动 `single`、滚动区域宽高 `width` 和 `height`、滚动区域和文字样式 `boardStyle` 和 `textStyle`、滚动条数 `amount`、间距 `gap`、动画间隔 `interval` 和 `step`、以及垂直滚动时间间隔 `verticalInterval`。组件内置多种样式调整功能,并提供在线预览示例。

可自定义设置以下属性:

  • 滚动文字数组(scrollText),类型:Array<{title: string, link?: string}> | {title: string, link?: string},默认 [],滚动文字数组,single 为 true 时,类型为 Text;多条文字滚动时,数组长度必须大于等于 amount 才能滚动

  • 是否启用单条文字滚动效果(single),只支持水平文字滚动,为 true 时,amount 自动设为 1,默认 false

  • 滚动区域宽度(width),类型:number | string,单位 px,默认 '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

效果如下图: 在线预览

其中引入使用了以下工具函数:

①创建文字滚动组件TextScroll.vue:

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { CSSProperties } from 'vue'
import { rafTimeout, cancelRaf, useResizeObserver } from '../utils'
interface Text {
  title: string // 文字标题
  link?: string // 跳转链接
}
interface Props {
  scrollText?: Text[] | Text // 滚动文字数组,single 为 true 时,类型为 Text;多条文字滚动时,数组长度必须大于等于 amount 才能滚动
  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 {
    const text = props.scrollText as Text[]
    if (text.length === props.amount) {
      return [...text, ...text]
    } else {
      return [...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 horizontalRef = ref() // 水平滚动 DOM 引用
const verticalRef = ref() // 垂直滚动 DOM 引用
const left = ref(0)
const distance = ref(0) // 每条滚动文字移动距离
const horizontalMoveRaf = ref()
const verticalMoveRaf = ref()
const origin = ref(true) // 垂直滚动初始状态
watch(
  () => [
    textData,
    props.width,
    props.amount,
    props.gap,
    props.step,
    props.interval,
    props.vertical,
    props.verticalInterval
  ],
  () => {
    initScroll()
  },
  {
    deep: true, // 强制转成深层侦听器
    flush: 'post'
  }
)
useResizeObserver([horizontalRef, verticalRef], () => {
  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((horizontalRef.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 activeIndex = ref(0)
function verticalMove() {
  verticalMoveRaf.value = rafTimeout(
    () => {
      if (origin.value) {
        origin.value = false
      }
      activeIndex.value = (activeIndex.value + 1) % textAmount.value
      verticalMove()
    },
    origin.value ? props.verticalInterval : props.verticalInterval + 1000
  )
}
</script>
<template>
  <div
    v-if="!vertical"
    ref="horizontalRef"
    class="m-slider-horizontal"
    :style="[boardStyle, `--text-gap: ${gap}px; height: ${height}px; width: ${totalWidth};`]"
  >
    <div class="m-scroll-view" :style="`will-change: transform; transform: translateX(${-left}px);`">
      <a
        class="slide-text"
        :style="[textStyle, `width: ${distance}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
    ref="verticalRef"
    class="m-slider-vertical"
    :style="[
      boardStyle,
      ` --enter-move: ${height}px; --leave-move: ${-height}px; --tex-gap: ${gap}px; height: ${height}px; width: ${totalWidth};`
    ]"
  >
    <TransitionGroup name="slide">
      <div class="m-scroll-view" v-for="(text, index) in <Text[]>textData" :key="index" v-show="activeIndex === index">
        <a
          class="slide-text"
          :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-horizontal {
  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;
    .slide-text {
      padding-left: var(--text-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;
      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(--tex-gap);
    .slide-text {
      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>

②在要使用的页面引入:

<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, 12]">
      <Col :span="6">
        <Flex vertical>
          height:
          <Slider v-model:value="state.height" :min="6" :max="180" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex 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">
        <Space gap="small" vertical>
          color:
          <Input v-model:value="state.color" placeholder="color" />
        </Space>
      </Col>
      <Col :span="6">
        <Flex gap="small" vertical>
          backgroundColor:
          <Input v-model:value="state.backgroundColor" placeholder="backgroundColor" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical>
          amount:
          <Slider v-model:value="state.amount" :min="1" :max="scrollText.length" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical>
          gap:
          <Slider v-model:value="state.gap" :min="10" :max="100" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical>
          interval:
          <Slider v-model:value="state.interval" :min="5" :max="100" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical>
          step:
          <Slider v-model:value="state.step" :min="0.1" :step="0.1" :max="20" />
        </Flex>
      </Col>
      <Col :span="6">
        <Space gap="small" vertical>
          vertical:
          <Switch v-model="state.vertical" />
        </Space>
      </Col>
      <Col :span="6">
        <Flex vertical>
          verticalInterval:
          <Slider v-model:value="state.verticalInterval" :min="1000" :step="100" :max="10000" />
        </Flex>
      </Col>
    </Row>
    <TextScroll
      class="mt30"
      style="margin-bottom: 100px"
      :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>
相关文章
|
1月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
130 64
|
4天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
23 3
|
1月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
34 8
|
29天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
31 1
|
29天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
39 1
|
9天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
1月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
43 1
vue学习第一章
|
1月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
30 1
|
1月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
38 1
vue学习第四章
|
1月前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
28 1
vue学习第7章(循环)

热门文章

最新文章

相关实验场景

更多