vue3表格显示隐藏列全屏拖动功能

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>

效果如图所示:

相关推荐
冰暮流星4 小时前
css之线性渐变
前端·css
徐同保4 小时前
tailwindcss暗色主题切换
开发语言·前端·javascript
mapbar_front4 小时前
大厂精英为何在中小公司水土不服?
前端
生莫甲鲁浪戴4 小时前
Android Studio新手开发第二十七天
前端·javascript·android studio
细节控菜鸡6 小时前
【2025最新】ArcGIS for JS 实现随着时间变化而变化的热力图
开发语言·javascript·arcgis
2501_916008897 小时前
Web 前端开发常用工具推荐与团队实践分享
android·前端·ios·小程序·uni-app·iphone·webview
SkylerHu7 小时前
前端代码规范:husky+ lint-staged+pre-commit
前端·代码规范
菜鸟una7 小时前
【微信小程序 + 消息订阅 + 授权】 微信小程序实现消息订阅流程介绍,代码示例(仅前端)
前端·vue.js·微信小程序·小程序·typescript·taro·1024程序员节
Yeats_Liao7 小时前
Go Web 编程快速入门 05 - 表单处理:urlencoded 与 multipart
前端·golang·iphone