vue3表格显示隐藏列全屏拖动功能
- 表格组件
- [主页面使用 RightToolbar 组件](#主页面使用 RightToolbar 组件)
表格组件
创建
RightToolbar.vue文件
typescript
<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-tooltip
class="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-checkbox
v-model="allSelected"
:indeterminate="isIndeterminate"
@change="toggleAllSelection"
style="
width: 100%;
padding: 4px 10px;
border-bottom: 1px solid var(--el-border-color-light);
"
>
全选
</el-checkbox>
</div>
<!-- 拖拽区域 -->
<div
class="scrollable-content"
style="max-height: 400px; overflow-y: auto"
@dragenter="handleContainerDragEnter"
@dragleave="handleContainerDragLeave"
@dragover.prevent
>
<div
v-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-checkbox
v-model="item.visible"
@change="updateSelectionState"
class="column-checkbox"
>
<span class="column-label">{{ item.label }}</span>
</el-checkbox>
</div>
</el-dropdown-item>
</div>
<!-- 拖拽占位符 -->
<div
v-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 = index
dragOverIndex.value = -1
isDragging.value = true
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
// 设置拖拽图像
const target = event.target as HTMLElement
event.dataTransfer.setDragImage(target, 20, 20)
}
// 添加全局拖拽类
document.body.classList.add('drag-active')
}
const handleDragEnd = () => {
isDragging.value = false
dragStartIndex.value = -1
dragOverIndex.value = -1
isContainerDragOver.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 Node
const currentTarget = event.currentTarget as Node
if (!currentTarget.contains(relatedTarget)) {
dragOverIndex.value = -1
}
}
const handleContainerDragEnter = () => {
isContainerDragOver.value = true
}
const handleContainerDragLeave = (event: DragEvent) => {
const relatedTarget = event.relatedTarget as Node
const currentTarget = event.currentTarget as Node
if (!currentTarget.contains(relatedTarget)) {
isContainerDragOver.value = false
dragOverIndex.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 = -1
dragOverIndex.value = -1
isDragging.value = false
isContainerDragOver.value = false
document.body.classList.remove('drag-active')
}
const toggleTableFullScreen = () => {
isTableFullscreen.value = !isTableFullscreen.value
emit('toggleTableFullScreen', isTableFullscreen.value)
}
const updateSelectionState = () => {
const visibleCount = props.columns.filter((col: any) => col.visible).length
const totalCount = props.columns.length
allSelected.value = visibleCount === totalCount
isIndeterminate.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 = val
isIndeterminate.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 组件
typescript
<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-table
v-loading="loading"
:data="list"
border
size="small"
:height="'calc(100vh - 420px)'"
show-summary
:summary-method="getSummaries"
class="custom-table"
>
<el-table-column
align="center"
label="单据号"
prop="masterOrder.sheetCode"
min-width="120"
fixed
/>
<template v-for="column in visibleColumns" :key="column.key">
<!-- 仓库代码 -->
<el-table-column
v-if="column.key === 'storageCode'"
align="center"
label="仓库代码"
prop="masterOrder.storage.storageCode"
min-width="100"
/>
<!-- 仓库名称 -->
<el-table-column
v-else-if="column.key === 'storageName'"
align="center"
label="仓库名称"
prop="masterOrder.storage.storageName"
min-width="160"
show-overflow-tooltip
/>
<!-- 单据状态 -->
<el-table-column
v-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-column
v-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-column
v-else-if="column.key === 'itemCode'"
align="center"
label="商品货号"
prop="item.code"
min-width="120"
/>
<!-- 条形码 -->
<el-table-column
v-else-if="column.key === 'barcode'"
align="center"
label="条形码"
prop="item.barcode"
min-width="180"
/>
<!-- 商品名称 -->
<el-table-column
v-else-if="column.key === 'itemName'"
align="center"
label="商品名称"
prop="item.itemName"
min-width="140"
show-overflow-tooltip
/>
</template>
</el-table>
<!-- 分页 -->
<Pagination
v-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 = state
if (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>
效果如图所示:
