)
可自定義設置以下屬性滾動文字數組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 | false默認 4為 false 時所有文字平鋪展示水平滾動時生效水平滾動文字各列間距或垂直滾動文字兩邊的間距gap類型number單位 px默認 20水平滾動時移動的速度speed類型number單位是像素每秒默認 48水平滾動時生效是否垂直滾動vertical類型boolean默認 false垂直滾動過渡持續時間duration類型number單位 ms默認 1000垂直滾動時生效垂直文字滾動時間間隔interval類型number單位 ms默認 3000垂直滾動時生效是否啟用文本省略組件ellipsis類型boolean默認 falseEllipsis 組件屬性配置ellipsisProps類型object參考 Ellipsis Props用于配置文本省略彈出提示鼠標移入是否暫停滾動pauseOnMouseEnter類型boolean默認 false效果如下圖在線預覽①創建文字滾動組件TextScroll.vue其中引入使用了以下組件和工具函數Vue3文本省略Ellipsis定時器 rafTimeout cancelRaf監聽DOM尺寸 useResizeObserver獲取依賴注入 useInject??script setup langts import { ref, computed, watch, nextTick } from vue import type { CSSProperties } from vue import Ellipsis from components/ellipsis 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 | false // 滾動區域展示條數為 false 時所有文字平鋪展示水平滾動時生效 gap?: number // 水平滾動文字各列間距或垂直滾動文字兩邊的間距單位 px speed?: number // 水平滾動時移動的速度單位是像素每秒水平滾動時生效 vertical?: boolean // 是否垂直滾動 duration?: number // 垂直滾動過渡持續時間單位 ms垂直滾動時生效 interval?: number // 垂直文字滾動時間間隔單位 ms垂直滾動時生效 ellipsis?: boolean // 是否啟用文本省略組件 ellipsisProps?: object // Ellipsis 組件屬性配置參考 Ellipsis Props用于配置文本省略彈出提示 pauseOnMouseEnter?: boolean // 鼠標移入是否暫停滾動 } const props withDefaults(definePropsProps(), { items: () [], single: false, width: 100%, height: 50, itemStyle: () ({}), hrefHoverColor: undefined, amount: 4, gap: 20, speed: 48, vertical: false, duration: 1000, interval: 3000, ellipsis: false, ellipsisProps: () ({}), pauseOnMouseEnter: false }) const horizontalRef ref() // 水平滾動 DOM 引用 const horizontalWrapWidth refnumber(0) // 水平滾動容器寬度 const verticalRef ref() // 垂直滾動 DOM 引用 const groupRef ref() // 水平滾動內容 DOM 引用 const groupWidth refnumber(0) // 水平滾動內容寬度 const playState refpaused | running(paused) // 水平滾動動畫執行狀態 const reset refboolean(true) // 重置水平滾動動畫狀態 const activeIndex refnumber(0) // 垂直滾動當前索引 const verticalMoveRaf ref() // 垂直滾動定時器引用標識 const originVertical refboolean(true) // 垂直滾動初始狀態 const scrollItems refItem[]([]) // 滾動目標文字數組 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 } if (props.amount false) { return 0 } return props.amount }) const itemWidth computed(() { // 水平滾動單條文字寬度 if (props.amount false) { return auto } return ${parseFloat((horizontalWrapWidth.value / displayAmount.value).toFixed(2)) - props.gap}px }) 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() }, { 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 refhorizontalRef classtext-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; ] mouseenterpauseOnMouseEnter ? stopMove() : () false mouseleavepauseOnMouseEnter ? startMove() : () false div refgroupRef classscroll-items-group :class{ scroll-items-reset: reset } animationiterationonAnimationIteration component :isitem.href ? a : div classscroll-item :class{ href-item: !ellipsis item.href } :style[ellipsis ? null : itemStyle, width: ${itemWidth};] v-for(item, index) in scrollItems :keyindex :titleellipsis ? null : item.title :hrefitem.href :targetitem.target clickonClick(item) Ellipsis v-ifellipsis :max-widthitemWidth :content-class${item.href ? href-item : null} :content-styleitemStyle v-bindellipsisProps {{ item.title }} /Ellipsis template v-else{{ item.title }}/template /component /div div classscroll-items-group :class{ scroll-items-reset: reset } component :isitem.href ? a : div classscroll-item :class{ href-item: !ellipsis item.href } :style[ellipsis ? null : itemStyle, width: ${itemWidth};] v-for(item, index) in scrollItems :keyindex :titleellipsis ? null : item.title :hrefitem.href :targetitem.target clickonClick(item) Ellipsis v-ifellipsis :max-widthitemWidth :content-class${item.href ? href-item : null} :content-styleitemStyle v-bindellipsisProps {{ item.title }} /Ellipsis template v-else{{ item.title }}/template /component /div /div div v-else refverticalRef classtext-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; ] mouseenterpauseOnMouseEnter ? stopMove() : () false mouseleavepauseOnMouseEnter ? startMove() : () false TransitionGroup nameslide div classscroll-item-wrap v-for(item, index) in scrollItems :keyindex v-showactiveIndex index component :isitem.href ? a : div classscroll-item :class{ href-item: !ellipsis item.href } :style!ellipsis ? itemStyle : {} :titleellipsis ? null : item.title :hrefitem.href :targetitem.target clickonClick(item) Ellipsis v-ifellipsis :content-class${item.href ? href-item : null} :content-style{ ...itemStyle, maxWidth: 100% } v-bindellipsisProps {{ item.title }} /Ellipsis template v-else{{ item.title }}/template /component /div /TransitionGroup /div /template style langless scoped // 水平滾動 .text-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 { margin-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; } :deep(.href-item) { cursor: pointer; transition: color 0.3s; :hover { color: var(--text-scroll-href-hover-color) !important; } .ellipsis-container { cursor: inherit; } } } .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; } .text-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; } :deep(.href-item) { cursor: pointer; transition: color 0.3s; :hover { color: var(--text-scroll-href-hover-color) !important; } .ellipsis-container { cursor: inherit; } } } } /style其中引入使用了以下組件Vue3柵格GridVue3滑動輸入條SliderVue3輸入框InputVue3數字輸入框InputNumberVue3彈性布局FlexVue3間距SpaceVue3按鈕ButtonVue3開關SwitchVue3顏色選擇器ColorPicker②在要使用的頁面引入script setup langts import TextScroll from ./TextScroll.vue import { ref, reactive } from vue import type { TextScrollItem } from vue-amazing-ui const scrollItems refTextScrollItem[]([ { title: 美國作家杰羅姆·大衛·塞林格創作的唯一一部長篇小說, href: https://themusecatcher.blog.csdn.net, target: _blank }, { title: 《麥田里的守望者》首次出版于1951年, href: https://themusecatcher.blog.csdn.net, target: _blank }, { title: 塞林格將故事的起止局限于16歲的中學生霍爾頓·考爾菲德從離開學校到紐約游蕩的三天時間內 }, { title: 并借鑒了意識流天馬行空的寫作方法充分探索了一個十幾歲少年的內心世界, href: https://themusecatcher.blog.csdn.net, target: _blank }, { title: 憤怒與焦慮是此書的兩大主題主人公的經歷和思想在青少年中引起強烈共鳴, href: https://themusecatcher.blog.csdn.net, target: _blank } ]) const singleItem: TextScrollItem { title: 請用一只玫瑰紀念我 } const textScrollRef ref() const disabled refboolean(true) const vertical refboolean(false) const ellipsis refboolean(true) function onClick(item: TextScrollItem) { // 獲取點擊的 item console.log(item, item) } function handleStart() { textScrollRef.value.start() disabled.value true } function handleStop() { textScrollRef.value.stop() disabled.value false } function handleReset() { textScrollRef.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, ellipsis: true, pauseOnMouseEnter: true }) /script template div h1{{ $route.name }} {{ $route.meta.title }}/h1 h2 classmt30 mb10水平文字滾動/h2 TextScroll :itemsscrollItems clickonClick / h2 classmt30 mb10垂直文字滾動/h2 TextScroll stylebackground-color: #e6f4ff :itemsscrollItems :item-style{ fontSize: 20px } vertical clickonClick / h2 classmt30 mb10單條文字滾動/h2 Flex vertical TextScroll :itemssingleItem single :width300 :item-style{ fontSize: 24px, fontWeight: 600, color: darkred } clickonClick / TextScroll :items[singleItem] vertical :width300 :item-style{ fontSize: 24px, fontWeight: 600, color: darkred } clickonClick / /Flex h2 classmt30 mb10自定義樣式/h2 TextScroll stylebackground-color: #e6f4ff; border-radius: 12px :itemsscrollItems :item-style{ fontSize: 20px, fontWeight: 500, color: #FF9800 } :height60 clickonClick / h2 classmt30 mb10鏈接懸浮色/h2 TextScroll :itemsscrollItems href-hover-color#ff6900 clickonClick / h2 classmt30 mb10展示條數和間距/h2 Flex vertical TextScroll :itemsscrollItems :amount3 :gap30 clickonClick / TextScroll :itemsscrollItems :amountfalse :gap30 clickonClick / /Flex h2 classmt30 mb10滾動速度/h2 Flex vertical TextScroll :itemsscrollItems :speed72 clickonClick / TextScroll :itemsscrollItems vertical :duration800 :interval2000 clickonClick / /Flex h2 classmt30 mb10文本省略彈出提示/h2 Flex vertical TextScroll :itemsscrollItems ellipsis pause-on-mouse-enter clickonClick / TextScroll :itemsscrollItems ellipsis pause-on-mouse-enter vertical clickonClick / /Flex h2 classmt30 mb10鼠標移入暫停/h2 Flex vertical TextScroll :itemsscrollItems pause-on-mouse-enter clickonClick / TextScroll :itemsscrollItems vertical pause-on-mouse-enter clickonClick / /Flex h2 classmt30 mb10使用 Methods/h2 Flex vertical Space vertical Space aligncenter vertical: Switch v-modelvertical / ellipsis: Switch v-modelellipsis / /Space Space Button typeprimary :disableddisabled clickhandleStart開始/Button Button clickhandleStop暫停/Button Button typeprimary ghost clickhandleReset重置/Button /Space /Space TextScroll reftextScrollRef :verticalvertical :ellipsisellipsis :itemsscrollItems clickonClick / /Flex h2 classmt30 mb10文字滾動配置器/h2 Flex vertical Row :gutter[24, 12] Col :span6 Flex gapsmall vertical height: Slider v-model:valuestate.height :min6 :max180 / /Flex /Col Col :span6 Flex gapsmall vertical fontSize: Slider v-model:valuestate.fontSize :min6 :max180 / /Flex /Col Col :span6 Flex gapsmall vertical fontWeight: InputNumber v-model:valuestate.fontWeight :step100 :min100 :max1000 / /Flex /Col Col :span6 Flex gapsmall vertical color: ColorPicker v-model:valuestate.color / /Flex /Col Col :span6 Flex gapsmall vertical backgroundColor: ColorPicker v-model:valuestate.backgroundColor / /Flex /Col Col :span6 Flex gapsmall vertical hrefHoverColor: ColorPicker v-model:valuestate.hrefHoverColor / /Flex /Col Col :span6 Flex gapsmall vertical amount: Slider v-model:valuestate.amount :min1 :maxscrollItems.length / /Flex /Col Col :span6 Flex gapsmall vertical gap: Slider v-model:valuestate.gap :min10 :max100 / /Flex /Col Col :span6 Flex gapsmall vertical speed: Slider v-model:valuestate.speed :min10 :max100 / /Flex /Col Col :span6 Space gapsmall vertical vertical: Switch v-modelstate.vertical / /Space /Col Col :span6 Flex gapsmall vertical duration: Slider v-model:valuestate.duration :min100 :step100 :max3000 / /Flex /Col Col :span6 Flex gapsmall vertical interval: Slider v-model:valuestate.interval :min1000 :step100 :max10000 / /Flex /Col Col :span6 Space gapsmall vertical ellipsis: Switch v-modelstate.ellipsis / /Space /Col Col :span6 Space gapsmall vertical pauseOnMouseEnter: Switch v-modelstate.pauseOnMouseEnter / /Space /Col /Row TextScroll :stylebackground-color: ${state.backgroundColor} :itemsscrollItems :singlestate.single :heightstate.height :item-style{ fontSize: state.fontSize px, fontWeight: state.fontWeight, color: state.color } :href-hover-colorstate.hrefHoverColor :amountstate.amount :gapstate.gap :speedstate.speed :verticalstate.vertical :durationstate.duration :intervalstate.interval :ellipsisstate.ellipsis :pause-on-mouse-enterstate.pauseOnMouseEnter clickonClick / /Flex /div /template