vue3表格显示隐藏列全屏拖动功能
vue3表格显示隐藏列全屏拖动功能
- 表格组件
- 主页面使用 RightToolbar 组件
表格组件
创建
RightToolbar.vue文件
<template><div class="top-right-btn"><el-row><el-tooltip class="item" effect="dark" content="刷新" placement="top"><el-button circle @click="refresh()"><Icon icon="ep:refresh" /></el-button></el-tooltip><el-tooltip class="item" effect="dark" content="全屏" placement="top"><el-button circle @click="toggleTableFullScreen"><Icon :icon="isTableFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" /></el-button></el-tooltip><el-tooltipclass="item"effect="dark"content="显隐列"placement="top"v-if="columns && columns.length > 0"><el-button circle @click="showColumn()" v-if="showColumnsType === 'transfer'"><Icon icon="ep:menu" /></el-button><el-dropdown v-else trigger="click" :hide-on-click="false" style="padding-left: 8px"><el-button circle><Icon icon="ep:menu" /></el-button><template #dropdown><el-dropdown-menu><div class="sticky-header"><el-checkboxv-model="allSelected":indeterminate="isIndeterminate"@change="toggleAllSelection"style="width: 100%;padding: 4px 10px;border-bottom: 1px solid var(--el-border-color-light);">全选</el-checkbox></div><!-- 拖拽区域 --><divclass="scrollable-content"style="max-height: 400px; overflow-y: auto"@dragenter="handleContainerDragEnter"@dragleave="handleContainerDragLeave"@dragover.prevent><divv-for="(item, index) in columns":key="item.key"class="draggable-item":class="{'drag-over': dragOverIndex === index,dragging: dragStartIndex === index,'ghost-item': dragStartIndex === index}"draggable="true"@dragstart="handleDragStart($event, index)"@dragend="handleDragEnd"@dragover="handleDragOver($event, index)"@dragenter="handleDragEnter($event, index)"@dragleave="handleDragLeave($event, index)"@drop="handleDrop($event, index)"><el-dropdown-item class="dropdown-item-wrapper"><div class="column-item"><div class="drag-indicator"><Icon icon="ep:rank" class="drag-handle" /><div class="drag-line"></div></div><el-checkboxv-model="item.visible"@change="updateSelectionState"class="column-checkbox"><span class="column-label">{{ item.label }}</span></el-checkbox></div></el-dropdown-item></div><!-- 拖拽占位符 --><divv-if="showDropPlaceholder"class="drop-placeholder":class="{ 'drag-over': dragOverIndex === -1 }"><div class="placeholder-content"><Icon icon="ep:plus" class="placeholder-icon" /><span>拖拽到此位置</span></div></div></div></el-dropdown-menu></template></el-dropdown></el-tooltip></el-row><el-dialog :title="title" v-model="open" append-to-body><el-transfer :titles="['显示', '隐藏']" v-model="value" :data="columns" @change="dataChange"><template #left-footer><div class="transfer-footer"><span>拖动可排序</span></div></template><template #right-footer><div class="transfer-footer"><span>拖动可排序</span></div></template></el-transfer></el-dialog></div>
</template><script setup lang="ts">
import { ref, defineProps, defineEmits, watch, computed } from 'vue'defineOptions({ name: 'RightToolbar' })
const props = defineProps({columns: { type: Array, required: true },search: { type: Boolean, default: true },showColumnsType: { type: String, default: 'checkbox' },gutter: { type: Number, default: 10 }
})const emit = defineEmits(['update:columns', 'queryTable', 'toggleTableFullScreen'])// 状态管理
const value = ref([])
const title = ref('显示/隐藏')
const open = ref(false)
const allSelected = ref(false)
const isIndeterminate = ref(false)
const isTableFullscreen = ref(false)// 拖拽状态
const dragStartIndex = ref(-1)
const dragOverIndex = ref(-1)
const isDragging = ref(false)
const isContainerDragOver = ref(false)// 计算属性
const showDropPlaceholder = computed(() => {return isContainerDragOver.value && dragOverIndex.value === -1
})// 原生拖拽方法 - 优化版
const handleDragStart = (event: DragEvent, index: number) => {dragStartIndex.value = indexdragOverIndex.value = -1isDragging.value = trueif (event.dataTransfer) {event.dataTransfer.effectAllowed = 'move'// 设置拖拽图像const target = event.target as HTMLElementevent.dataTransfer.setDragImage(target, 20, 20)}// 添加全局拖拽类document.body.classList.add('drag-active')
}const handleDragEnd = () => {isDragging.value = falsedragStartIndex.value = -1dragOverIndex.value = -1isContainerDragOver.value = false// 移除全局拖拽类document.body.classList.remove('drag-active')
}const handleDragOver = (event: DragEvent) => {event.preventDefault()if (event.dataTransfer) {event.dataTransfer.dropEffect = 'move'}
}const handleDragEnter = (event: DragEvent, index: number) => {event.preventDefault()if (dragStartIndex.value !== index) {dragOverIndex.value = index}
}const handleDragLeave = (event: DragEvent) => {// 检查是否真正离开了当前元素const relatedTarget = event.relatedTarget as Nodeconst currentTarget = event.currentTarget as Nodeif (!currentTarget.contains(relatedTarget)) {dragOverIndex.value = -1}
}const handleContainerDragEnter = () => {isContainerDragOver.value = true
}const handleContainerDragLeave = (event: DragEvent) => {const relatedTarget = event.relatedTarget as Nodeconst currentTarget = event.currentTarget as Nodeif (!currentTarget.contains(relatedTarget)) {isContainerDragOver.value = falsedragOverIndex.value = -1}
}const handleDrop = (event: DragEvent, targetIndex: number) => {event.preventDefault()if (dragStartIndex.value === targetIndex || dragStartIndex.value === -1) {resetDragState()return}// 执行拖拽排序const newColumns = [...props.columns]const [movedItem] = newColumns.splice(dragStartIndex.value, 1)newColumns.splice(targetIndex, 0, movedItem)emit('update:columns', newColumns)resetDragState()
}const resetDragState = () => {dragStartIndex.value = -1dragOverIndex.value = -1isDragging.value = falseisContainerDragOver.value = falsedocument.body.classList.remove('drag-active')
}const toggleTableFullScreen = () => {isTableFullscreen.value = !isTableFullscreen.valueemit('toggleTableFullScreen', isTableFullscreen.value)
}const updateSelectionState = () => {const visibleCount = props.columns.filter((col: any) => col.visible).lengthconst totalCount = props.columns.lengthallSelected.value = visibleCount === totalCountisIndeterminate.value = visibleCount > 0 && visibleCount < totalCount
}const toggleAllSelection = (val: boolean) => {const newColumns = props.columns.map((col: any) => ({...col,visible: val}))emit('update:columns', newColumns)allSelected.value = valisIndeterminate.value = false
}function refresh() {emit('queryTable')
}function dataChange(data) {const newColumns = props.columns.map((item: any) => ({...item,visible: !data.includes(item.key)}))emit('update:columns', newColumns)
}function showColumn() {open.value = true
}watch(() => props.columns,() => {updateSelectionState()},{ immediate: true, deep: true }
)
</script><style scoped>
.top-right-btn {display: flex;align-items: center;justify-content: end;
}.sticky-header {position: sticky;top: 0;background: white;z-index: 2;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}.el-dropdown-menu {padding: 0;min-width: 180px;
}.transfer-footer {padding: 4px 8px;font-size: 11px;color: var(--el-text-color-secondary);border-top: 1px solid var(--el-border-color-light);
}.scrollable-content {max-height: 400px;overflow-y: auto;position: relative;
}/* 拖拽项样式 - 间距调小 */
.draggable-item {cursor: grab;user-select: none;transition: all 0.2s ease;border: 1px solid transparent;border-radius: 4px;margin: 1px 2px;
}.draggable-item:hover {background-color: var(--el-fill-color-light);
}.draggable-item:active {cursor: grabbing;
}/* 拖拽状态样式 */
.draggable-item.dragging {opacity: 0.6;transform: scale(0.98);background-color: var(--el-color-primary-light-9);border-color: var(--el-color-primary);
}.draggable-item.drag-over {background-color: var(--el-color-primary-light-8);border-color: var(--el-color-primary);transform: translateX(4px);
}.draggable-item.ghost-item {opacity: 0.4;
}/* 列项布局 - 间距调小 */
.column-item {display: flex;align-items: center;width: 100%;padding: 2px 0;
}.drag-indicator {display: flex;align-items: center;margin-right: 6px;opacity: 0.5;transition: opacity 0.2s ease;
}.draggable-item:hover .drag-indicator {opacity: 1;
}.drag-handle {cursor: inherit;color: var(--el-text-color-secondary);font-size: 12px;transition: color 0.2s ease;
}.drag-line {width: 1px;height: 12px;background: linear-gradient(to bottom,transparent 0%,var(--el-text-color-secondary) 20%,var(--el-text-color-secondary) 80%,transparent 100%);margin-left: 2px;opacity: 0.6;
}.column-checkbox {flex: 1;
}.column-label {font-size: 13px;color: var(--el-text-color-regular);
}/* 下拉菜单项调整 - 间距调小 */
.dropdown-item-wrapper {padding: 0 !important;
}.dropdown-item-wrapper :deep(.el-dropdown-menu__item) {padding: 0 8px;pointer-events: none;
}/* 拖拽占位符 - 间距调小 */
.drop-placeholder {border: 1px dashed var(--el-border-color);border-radius: 4px;margin: 4px 2px;padding: 12px;text-align: center;background-color: var(--el-fill-color-lighter);transition: all 0.3s ease;opacity: 0.7;
}.drop-placeholder.drag-over {border-color: var(--el-color-primary);background-color: var(--el-color-primary-light-9);opacity: 1;
}.placeholder-content {display: flex;flex-direction: column;align-items: center;gap: 4px;color: var(--el-text-color-secondary);font-size: 11px;
}.placeholder-icon {font-size: 14px;color: var(--el-color-primary);
}/* 全局拖拽状态 */
:global(.drag-active) {cursor: grabbing !important;
}:global(.drag-active *) {cursor: inherit !important;
}/* 滚动条优化 */
.scrollable-content::-webkit-scrollbar {width: 4px;
}.scrollable-content::-webkit-scrollbar-track {background: var(--el-fill-color-lighter);border-radius: 2px;
}.scrollable-content::-webkit-scrollbar-thumb {background: var(--el-border-color-dark);border-radius: 2px;
}.scrollable-content::-webkit-scrollbar-thumb:hover {background: var(--el-text-color-placeholder);
}/* 按钮间距调小 */
.el-row .el-tooltip.item {margin-left: 4px;
}.el-row .el-tooltip.item:first-child {margin-left: 0;
}
</style>
主页面使用 RightToolbar 组件
<template>
<div ref="tableContainer" class="table-container"><el-row class="m-2"><div style="position: absolute; right: 0"><RightToolbar@query-table="getList"v-model:columns="columns"@toggleTableFullScreen="handleTableFullscreen"/></div></el-row><el-tablev-loading="loading":data="list"bordersize="small":height="'calc(100vh - 420px)'"show-summary:summary-method="getSummaries"class="custom-table"><el-table-columnalign="center"label="单据号"prop="masterOrder.sheetCode"min-width="120"fixed/><template v-for="column in visibleColumns" :key="column.key"><!-- 仓库代码 --><el-table-columnv-if="column.key === 'storageCode'"align="center"label="仓库代码"prop="masterOrder.storage.storageCode"min-width="100"/><!-- 仓库名称 --><el-table-columnv-else-if="column.key === 'storageName'"align="center"label="仓库名称"prop="masterOrder.storage.storageName"min-width="160"show-overflow-tooltip/><!-- 单据状态 --><el-table-columnv-else-if="column.key === 'workStatus'"align="center"label="单据状态"prop="masterOrder.workStatus"><template #default="scope"><dict-tag:type="DICT_TYPE.GOODS_IS_VALID":value="scope.row.masterOrder.workStatus"min-width="120"/></template></el-table-column><!-- 类别编号 --><el-table-columnv-else-if="column.key === 'cateCode'"align="center"label="类别编号"prop="item.cateCode"/><!-- 类别名称 --><el-table-column v-else-if="column.key === 'cateName'" align="center" label="类别名称"prop="item.cateName" /><!-- 商品货号 --><el-table-columnv-else-if="column.key === 'itemCode'"align="center"label="商品货号"prop="item.code"min-width="120"/><!-- 条形码 --><el-table-columnv-else-if="column.key === 'barcode'"align="center"label="条形码"prop="item.barcode"min-width="180"/><!-- 商品名称 --><el-table-columnv-else-if="column.key === 'itemName'"align="center"label="商品名称"prop="item.itemName"min-width="140"show-overflow-tooltip/></template></el-table><!-- 分页 --><Paginationv-model:limit="queryParams.pageSize"v-model:page="queryParams.pageNo":total="total"@pagination="getList"/></div>
</template><script lang="ts" setup>const columns = ref([{ key: 'storageCode', label: '仓库代码', visible: true },{ key: 'storageName', label: '仓库名称', visible: true },{ key: 'workStatus', label: '单据状态', visible: true },{ key: 'cateCode', label: '类别编号', visible: true },{ key: 'cateName', label: '类别名称', visible: true },{ key: 'itemCode', label: '商品货号', visible: true },{ key: 'barcode', label: '条形码', visible: true },{ key: 'itemName', label: '商品名称', visible: true }
])// 计算可见的列
const visibleColumns = computed(() => {return columns.value.filter((column) => column.visible)
})const tableContainer = ref<HTMLElement | null>(null)
const isFullscreen = ref(false)// 全屏切换
const handleTableFullscreen = (state: boolean) => {isFullscreen.value = stateif (state) {// 进入全屏document.body.style.overflow = 'hidden'tableContainer.value?.classList.add('fullscreen-active')} else {// 退出全屏document.body.style.overflow = ''tableContainer.value?.classList.remove('fullscreen-active')}
}
</script><style lang="scss" scoped>
.table-container {position: relative;height: calc(100% - 150px); transition: all 0.3s;
}/* 全屏状态样式 */
.table-container.fullscreen-active {position: fixed;inset: 0;z-index: 2000;height: auto !important;padding: 20px;overflow: auto;background: #fff;
}/* 全屏时的表格样式 */
.fullscreen-active .el-table {height: calc(100vh - 150px) !important; /* 减去padding */
}
</style>
效果如图所示:

