记录--vue双重循环

这篇博客探讨了如何在Vue中通过`v-for`指令处理和展示两层嵌套的数据结构。示例展示了如何遍历`items`数组,并分别显示`Name`和其对应的`Items`列表,为每个子项渲染`itemname`属性。

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

 items: [
        {
          Name: "进度",
          Items: [
            { itemname: "状态" },
            { itemname: "状态" },
            { itemname: "状态" },
            { itemname: "信息" },
          ],
        },
        {
          Name: "2",
          Items: [
            { itemname: "1" },
            { itemname: "2" },
            { itemname: "2" },
            { itemname: "4" },
          ],
        },
        {
          Name: "3",
          Items: [
            { itemname: "1" },
            { itemname: "2" },
            { itemname: "3" },
            { itemname: "4" },
          ],
        },
      ],
      传递两层数据使用时
             <div v-for="(ite, index) in items" v-bind:key="index" > <span > {{ ite.Name }}</span>
            <ul>
              <li
                v-for="(item, index) in ite.Items" v-bind:key="index" >  
                {{ item.itemname }}
           
<think>我们正在实现一个Vue中的无缝横向滚动效果,用于循环展示超宽文本。根据引用内容,我们注意到以下几点关键信息:1.需要实现无缝滚动,即一个文本滚动完后紧接着下一个文本,没有间隔。2.引用的代码中使用了两个div块(#viewBox和#marquee)来放置内容,并通过隐藏块(#hide)来获取宽度。3.通过动画实现横向滚动。但是,用户要求的是循环数据,即有多条数据(数组)需要滚动展示。我们需要实现的是:-多条文本连续横向滚动,当第一条文本滚动出视野后,紧接着第二条文本出现,且是无缝衔接(即没有间隙)。-由于文本长度不一,我们可能需要动态计算每条文本的宽度。考虑到无缝滚动,我们通常的做法是:1.将数据复制一份,这样当滚动到复制数据的末尾时,可以立即跳转到原始数据的开头,实现循环的假象。2.使用CSS动画或requestAnimationFrame来实现滚动。然而,由于用户要求横向滚动,并且每条文本可能很长,我们需要保证:-每条文本不换行(white-space:nowrap)-容器宽度固定,超出隐藏。根据引用[2]中的思路,我们可以这样实现:1.使用一个父容器(.PackagingShell),设置overflow:hidden,固定高度和宽度。2.内部一个滚动容器(.viewBox),不换行,包含两个部分:a.实际要显示的数据列表(.marquee-list)b.一个隐藏的块(.hide-block),用于获取每条文本的宽度(但这里我们可能需要每条文本的宽度,所以可能需要分别获取)但是,为了实现无缝衔接,我们通常需要将数据列表复制一份(即两份相同的数据),然后滚动到第一份数据末尾时瞬间重置位置,实现无缝循环。然而,由于每条文本长度不同,我们需要动态计算每条文本的宽度,从而设置滚动动画的时间。但这样比较麻烦。另一种简单做法:使用CSS动画,通过设置每一条数据的滚动时间相同,但滚动距离不同(根据文本宽度)来实现?这样也不可行。我们可以这样设计:1.将每条数据都放在一个独立的div中,这些div水平排列(display:inline-block;white-space:nowrap;)2.用一个滚动容器包裹所有这些div,然后让这个容器水平向左滚动。3.但是,为了实现无缝,我们需要在数据列表后面再复制一份相同的数据,当滚动到第一份数据的末尾时(即滚动到复制数据的开头),我们立即将滚动位置重置回初始位置(但此时用户不可见),从而实现循环。具体步骤:1.组件挂载后,复制一份数据(比如原本数据是list,我们设置totalList=list.concat(list)),这样我们就有了双份数据。2.计算每条数据的宽度(使用隐藏的块或者通过ref来获取),然后计算出第一份数据的总宽度(即原始数据的总宽度)。3.设置滚动容器的宽度为第一份数据总宽度的两倍(因为有两份数据)。4.使用CSS动画或requestAnimationFrame来移动滚动容器(向左移动)。5.当滚动距离达到第一份数据的总宽度时,立即将滚动容器的位置重置到0(无动画),这样就开始滚动第二份数据(看起来和第一份一样),如此循环。但注意:使用CSS动画的话,重置位置会打断动画,所以我们可以用JS动态控制。或者,我们可以通过CSS动画滚动整个双份数据,然后当动画结束时,瞬间重置位置并再次开始相同的动画。但这样会有停顿感。为了更加平滑,我们使用requestAnimationFrame动态改变transform,并在达到第一份数据宽度后重置到0。然而,用户可能更希望用纯CSS实现,我们可以利用CSS动画的关键帧,但需要动态生成动画关键帧(因为数据总宽度不确定)。考虑到简单性,这里我们使用JS控制滚动(使用requestAnimationFrame)。但根据引用[2]中的代码,他们使用了隐藏块来获取宽度,我们也需要获取每条文本的宽度。具体实现步骤:1.模板结构:<divclass="scroll-container"><divclass="PackagingShell"><!--固定宽度,超出隐藏--><divclass="viewBox"ref="viewBox":style="{transform:`translateX(${offset}px)`}"><divv-for="(item,index)intotalList":key="index"class="marquee-item"ref="marqueeItems">{{item.content}}</div></div></div></div>2.data中:offset:0,//当前滚动的距离itemWidths:[],//每条文本的宽度totalWidth:0,//第一份数据的总宽度3.mounted:-获取每个marquee-item的宽度,并计算第一份数据的总宽度(原数据长度)-设置滚动容器的宽度为第一份数据总宽度的两倍(这样双份数据不会换行)-启动动画循环(requestAnimationFrame)4.动画循环函数:每次将offset减少一定值(比如1px),当offset小于负的totalWidth时,将offset重置为0(此时因为第二份数据和第一份一样,所以用户看不出跳变)5.注意:为了无缝,跳转瞬间应该没有任何视觉变化,所以跳转时正好是第一份数据完全离开视野时,第二份数据刚好出现在最右侧,我们跳回0,显示第一份数据,但此时第二份数据的位置和第一份数据跳转前的位置内容一样。但是,这样实现会有一个问题:滚动速度固定(每帧1px),但帧率可能波动,导致速度不一致。我们可以控制速度(例如每秒滚动50px)来调整。改进:在动画循环中,我们计算时间差,根据速度(px/s)来计算应该滚动的距离。另外,为了避免重复计算,我们可以在resize时重新计算宽度,并更新totalWidth。考虑到性能,我们使用transform:translateX来移动,因为会触发硬件加速。代码实现:但是,用户可能有多条数据,并且需要循环展示。我们假设数据通过props传入,这里使用示例数据。下面我们一个组件来实现:注意:由于初始时我们无法获取元素宽度(因为还未渲染),所以需要在mounted或updated中获取,但是updated可能会多次触发,所以我们可以用nextTick确保DOM更新。但是,数据可能变化,我们需要监听数据变化重新计算。由于要使用ref获取DOM,我们需要注意v-for的项和ref数组的顺序。另外,当数据为空时,不需要滚动。具体代码:由于用户要求无缝衔接,我们在滚动到第一份数据末尾后重置到0,由于是瞬时重置,所以视觉上是无缝的。我们使用requestAnimationFrame来实现动画循环。由于用户可能要求暂停(如hover时暂停),我们可以添加事件控制。我们使用速度控制,例如每秒滚动80px。步骤:1.在mounted中初始化:获取宽度,设置总宽度,启动动画。2.在beforeDestroy中停止动画。3.动画循环函数:-记录当前时间(或使用时间差)-根据速度计算本次帧应该滚动的距离-更新offset-如果offset小于负的totalWidth,则offset增加totalWidth(相当于跳回0,但为了平滑,我们直接设置offset为0,同时因为有两份数据,跳回0后显示的是第一份数据,而此时第二份数据已经显示完,所以可以继续滚动第一份数据,实现循环-更新translateX-继续下一帧但是,当总宽度(totalWidth)为0时(比如数据为空),则不进行滚动。我们还要注意,如果数据长度改变,需要重新计算。由于该组件可能用在响应式环境中,当窗口大小改变时,文本宽度也会变,所以需要监听窗口resize事件,重新计算宽度。代码实现:由于代码较长,我们分步骤。另外,为了性能,我们可以在组件销毁时移除事件监听。下面是完整代码:</think> # Vue横向无缝滚动展示超宽文本的实现 基于您的需求,我将实现一个Vue组件,用于循环数据文本超宽时横向自动滚动,并且实现无缝衔接效果。 ## 实现原理与设计思路 1. **数据结构**:使用双重数据循环解决滚动跳转问题,实现视觉无缝效果 2. **滚动控制**:利用CSS3动画实现高性能滚动效果,保证移动端兼容性 3. **响应式处理**:自动适应不同尺寸的屏幕和文本长度 4. **无缝原理**:在视觉滚动到中间位置时重置位置,用户无感知 5. **性能优化**:使用`will-change`和`transform`开启GPU加速 ## 完整实现代码 ```vue <template> <div class="seamless-scroll-container"> <!-- 容器限制最大宽度 --> <div class="scroll-viewport" ref="scrollViewport"> <!-- 滚动轨道 --> <div class="scroll-track" ref="scrollTrack" :style="{ width: trackWidth + 'px' }" > <!-- 双重数据渲染实现无缝效果 --> <div class="scroll-item" v-for="(item, index) in duplicatedItems" :key="index" :style="{ width: itemWidths[item.id] ? `${itemWidths[item.id]}px` : 'auto' }" @mouseenter="pauseScroll" @mouseleave="resumeScroll" > <span class="text-content">{{ item.content }}</span> <span class="item-id" v-if="showDebug">ID: {{ item.id }}</span> </div> </div> </div> </div> </template> <script> export default { name: 'SeamlessHorizontalScroll', props: { items: { type: Array, default: () => [ { id: 1, content: '第一条超宽文本:Vue实现无缝横向滚动展示方案' }, { id: 2, content: '第二条:响应式设计自动适应不同屏幕尺寸和文本长度' }, { id: 3, content: '第三条:鼠标悬停暂停,移出恢复,提升用户体验' } ] }, scrollSpeed: { type: Number, default: 50 // 滚动速度,单位px/s }, showDebug: { type: Boolean, default: false } }, data() { return { duplicatedItems: [], // 双倍数据用于无缝滚动 itemWidths: {}, // 记录每个项目的实际宽度 trackWidth: 0, // 滚动轨道总宽度 trackInstance: null, // 滚动动画实例 animationSpeed: '30s', isPaused: false }; }, computed: { itemsTotalWidth() { return Object.values(this.itemWidths).reduce((sum, width) => sum + width, 0); } }, watch: { items: { immediate: true, handler(newVal) { // 创建双倍数据,实现无缝循环 this.duplicatedItems = [...newVal, ...newVal]; this.$nextTick(() => { this.measureItems(); this.setupAnimation(); }); } }, scrollSpeed(newSpeed) { this.updateAnimationSpeed(); } }, mounted() { window.addEventListener('resize', this.handleResize); this.$nextTick(this.setupAnimation); }, beforeDestroy() { window.removeEventListener('resize', this.handleResize); if (this.trackInstance) { this.trackInstance.cancel(); } }, methods: { // 测量所有项目宽度 measureItems() { this.$nextTick(() => { const items = this.$el.querySelectorAll('.scroll-item'); const originalItems = this.items; items.forEach((el, index) => { const itemId = index % originalItems.length; this.itemWidths[originalItems[itemId].id] = el.getBoundingClientRect().width; }); this.trackWidth = this.itemsTotalWidth * 2; }); }, // 设置动画效果 setupAnimation() { if (this.trackInstance) { this.trackInstance.cancel(); } this.updateAnimationSpeed(); const trackElement = this.$refs.scrollTrack; const viewportWidth = this.$refs.scrollViewport.clientWidth; if (this.itemsTotalWidth <= viewportWidth) { // 内容宽度不足时禁用滚动 trackElement.style.animation = 'none'; return; } trackElement.style.animation = `${this.animationSpeed} linear infinite scrollAnimation`; }, // 根据滚动速度更新动画速度 updateAnimationSpeed() { // 确保至少有原始数据 if (this.itemsTotalWidth <= 0) return; // 计算动画时长(滚动一个完整循环所需时间) // 公式:时长 = 距离 / 速度 const duration = this.itemsTotalWidth / this.scrollSpeed; this.animationSpeed = `${duration}s`; }, // 窗口大小变化处理 handleResize() { this.measureItems(); this.setupAnimation(); }, // 暂停滚动 pauseScroll() { if (this.trackInstance) return; const trackElement = this.$refs.scrollTrack; if (!trackElement) return; // 记录当前动画状态 const style = getComputedStyle(trackElement); const matrix = new DOMMatrix(style.transform); // 创建手动控制实例 this.trackInstance = { transform: matrix.translateX, cancel: () => trackElement.style.transform = '' }; this.isPaused = true; }, // 恢复滚动 resumeScroll() { if (!this.trackInstance) return; const trackElement = this.$refs.scrollTrack; trackElement.style.animation = `${this.animationSpeed} linear infinite scrollAnimation`; trackElement.style.transform = ''; this.trackInstance = null; this.isPaused = false; } } }; </script> <style scoped> .seamless-scroll-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .scroll-viewport { overflow: hidden; position: relative; height: 60px; background: #f7f9fc; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); padding: 0 15px; } .scroll-track { display: flex; position: absolute; height: 100%; will-change: transform; /* 优化性能 */ backface-visibility: hidden; /* 防止iOS闪动 */ } .scroll-item { display: flex; align-items: center; justify-content: center; padding: 0 25px; height: 100%; flex-shrink: 0; border-right: 1px solid #eaeff5; background: linear-gradient(to right, #f0f4fa, #ffffff); box-sizing: border-box; transition: transform 0.2s ease; } .text-content { font-size: 16px; font-weight: 500; color: #2c3e50; white-space: nowrap; } .item-id { display: inline-block; margin-left: 12px; font-size: 12px; color: #7f8c8d; background: #e0e7ff; padding: 2px 8px; border-radius: 12px; } /* 滚动动画 */ @keyframes scrollAnimation { 0% { transform: translateX(0); } 100% { transform: translateX(calc(-50% - 1px)); /* -50%配合双倍数据实现无缝 */ } } .scroll-track { animation: scrollAnimation 30s linear infinite; } @media (max-width: 768px) { .scroll-viewport { height: 50px; } .text-content { font-size: 14px; } .scroll-item { padding: 0 15px; } } </style> ``` ## 核心功能说明 ### 1. 无缝滚动实现 - 使用双倍数据(`duplicatedItems = [...items, ...items]`)渲染 - 当滚动到数据中间点时,CSS动画设置`transform: translateX(-50%)`实现视觉重置 - 使用`will-change`和`transform`开启GPU
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值