如何在Vue/Vuetify的v-data-table中添加两个水平滚动条
给Vuetify v-data-table添加顶部联动水平滚动条
我正在练习使用Vue/Vuetify的v-data-table组件,当前表格默认仅在底部显示一个水平滚动条。现在需要给表格顶部也添加一个水平滚动条,要求两个滚动条能同步控制表格内容的横向滚动。我查了资料只找到原生JS/HTML的实现,没找到适配Vue/Vuetify的方案,以下是我的代码:
<template> <v-container> <v-data-table :headers="headers" :items="desserts" :items-per-page="5" class="elevation-1" ></v-data-table> </v-container> </template> <script> export default { name: 'HelloWorld', data () { return { headers: [ { text: 'Dessert (100g serving)', align: 'start', sortable: false, value: 'name', width: 300 }, { text: 'Calories', value: 'calories', width: 300}, { text: 'Fat (g)', value: 'fat', width: 300 }, { text: 'Carbs (g)', value: 'carbs', width: 300 }, { text: 'Protein (g)', value: 'protein', width: 300 }, { text: 'Iron (%)', value: 'iron', width: 300 }, ], desserts: [ { name: 'Frozen Yogurt', calories: 159, fat: 6.0, carbs: 24, protein: 4.0, iron: '1%', }, { name: 'Ice cream sandwich', calories: 237, fat: 9.0, carbs: 37, protein: 4.3, iron: '1%', }, { name: 'Eclair', calories: 262, fat: 16.0, carbs: 23, protein: 6.0, iron: '7%', }, { name: 'Cupcake', calories: 305, fat: 3.7, carbs: 67, protein: 4.3, iron: '8%', }, { name: 'Gingerbread', calories: 356, fat: 16.0, carbs: 49, protein: 3.9, iron: '16%', }, { name: 'Jelly bean', calories: 375, fat: 0.0, carbs: 94, protein: 0.0, iron: '0%', }, { name: 'Lollipop', calories: 392, fat: 0.2, carbs: 98, protein: 0, iron: '2%', }, { name: 'Honeycomb', calories: 408, fat: 3.2, carbs: 87, protein: 6.5, iron: '45%', }, { name: 'Donut', calories: 452, fat: 25.0, carbs: 51, protein: 4.9, iron: '22%', }, { name: 'KitKat', calories: 518, fat: 26.0, carbs: 65, protein: 7, iron: '6%', }, ], } }, } </script>
解决方案
可以通过Vue生命周期钩子结合原生DOM操作实现,核心思路是创建一个顶部滚动容器,同步它和表格原生滚动容器的滚动位置:
- 给
v-data-table添加ref以便获取DOM元素 - 在
mounted钩子中创建顶部滚动条容器,插入到表格上方 - 监听两个滚动容器的
scroll事件,同步它们的scrollLeft值 - 监听窗口
resize事件,保持顶部滚动条宽度和表格一致
修改后的完整代码:
<template> <v-container> <v-data-table ref="dataTable" :headers="headers" :items="desserts" :items-per-page="5" class="elevation-1" ></v-data-table> </v-container> </template> <script> export default { name: 'HelloWorld', data () { return { headers: [ { text: 'Dessert (100g serving)', align: 'start', sortable: false, value: 'name', width: 300 }, { text: 'Calories', value: 'calories', width: 300}, { text: 'Fat (g)', value: 'fat', width: 300 }, { text: 'Carbs (g)', value: 'carbs', width: 300 }, { text: 'Protein (g)', value: 'protein', width: 300 }, { text: 'Iron (%)', value: 'iron', width: 300 }, ], desserts: [ { name: 'Frozen Yogurt', calories: 159, fat: 6.0, carbs: 24, protein: 4.0, iron: '1%', }, { name: 'Ice cream sandwich', calories: 237, fat: 9.0, carbs: 37, protein: 4.3, iron: '1%', }, { name: 'Eclair', calories: 262, fat: 16.0, carbs: 23, protein: 6.0, iron: '7%', }, { name: 'Cupcake', calories: 305, fat: 3.7, carbs: 67, protein: 4.3, iron: '8%', }, { name: 'Gingerbread', calories: 356, fat: 16.0, carbs: 49, protein: 3.9, iron: '16%', }, { name: 'Jelly bean', calories: 375, fat: 0.0, carbs: 94, protein: 0.0, iron: '0%', }, { name: 'Lollipop', calories: 392, fat: 0.2, carbs: 98, protein: 0, iron: '2%', }, { name: 'Honeycomb', calories: 408, fat: 3.2, carbs: 87, protein: 6.5, iron: '45%', }, { name: 'Donut', calories: 452, fat: 25.0, carbs: 51, protein: 4.9, iron: '22%', }, { name: 'KitKat', calories: 518, fat: 26.0, carbs: 65, protein: 7, iron: '6%', }, ], topScrollbar: null, tableWrapper: null } }, mounted() { this.initTopScrollbar() window.addEventListener('resize', this.syncScrollbarWidth) }, beforeUnmount() { window.removeEventListener('resize', this.syncScrollbarWidth) if (this.topScrollbar) { this.topScrollbar.removeEventListener('scroll', this.syncScrollPosition) this.topScrollbar.remove() } if (this.tableWrapper) { this.tableWrapper.removeEventListener('scroll', this.syncScrollPosition) } }, methods: { initTopScrollbar() { // 获取表格的滚动容器 this.tableWrapper = this.$refs.dataTable.$el.querySelector('.v-data-table__wrapper') if (!this.tableWrapper) return // 创建顶部滚动条容器 this.topScrollbar = document.createElement('div') this.topScrollbar.style.width = '100%' this.topScrollbar.style.overflowX = 'auto' this.topScrollbar.style.overflowY = 'hidden' this.topScrollbar.style.marginBottom = '-8px' // 抵消滚动条自身高度影响 // 创建内部填充元素,宽度和表格内容一致 const scrollContent = document.createElement('div') scrollContent.style.width = this.tableWrapper.scrollWidth + 'px' scrollContent.style.height = '1px' this.topScrollbar.appendChild(scrollContent) // 将顶部滚动条插入到表格容器上方 this.tableWrapper.parentNode.insertBefore(this.topScrollbar, this.tableWrapper) // 添加滚动同步监听 this.topScrollbar.addEventListener('scroll', this.syncScrollPosition) this.tableWrapper.addEventListener('scroll', this.syncScrollPosition) }, syncScrollPosition(e) { // 同步两个滚动容器的scrollLeft if (e.target === this.topScrollbar) { this.tableWrapper.scrollLeft = this.topScrollbar.scrollLeft } else { this.topScrollbar.scrollLeft = this.tableWrapper.scrollLeft } }, syncScrollbarWidth() { // 窗口 resize 时更新顶部滚动条内部元素宽度 if (this.topScrollbar && this.tableWrapper) { this.topScrollbar.firstChild.style.width = this.tableWrapper.scrollWidth + 'px' } } } } </script> <style scoped> /* 可选:调整表格容器样式,避免顶部滚动条遮挡表格 */ .v-data-table__wrapper { margin-top: 8px; } </style>
说明
- 通过
ref获取v-data-table的DOM元素,找到其内部的滚动容器.v-data-table__wrapper - 创建的顶部滚动条容器仅显示水平滚动条,内部的1px高度元素用于撑开滚动条的可滚动宽度
- 监听两个滚动容器的滚动事件,实时同步
scrollLeft值,实现联动效果 - 在组件销毁前移除事件监听和DOM元素,避免内存泄漏
内容的提问来源于stack exchange,提问作者Dev User
相关产品推荐
相关产品推荐

