Vue3+Element Plus 实现动态行列表格

1,259 阅读1分钟

效果

列标签操作:支持增(可批量)、删、恢复,支持右键编辑、拖拽排序 动画1.gif 表格行操作:支持增、删、恢复 动画2.gif

代码

Vue函数等已通过unplugin-auto-import自动导入,Element Plus已通过unplugin-vue-components自动导入

// src/assets/styles/_mixin.scss
@mixin flex($direction: null, $justify: null, $items: null, $gap: null) {
  display: flex;
  @if $direction {
    flex-direction: $direction;
  }
  @if $justify {
    justify-content: $justify;
  }
  @if $items {
    align-items: $items;
  }
  @if $gap {
    gap: $gap;
  }
}

以上为封装flexbox样式

<template>
  <div>
    <div class="hstack" style="margin-bottom: 12px">
      <VueDraggable v-model="tableCols" class="tags">
        <el-tag
          v-for="col in tableCols"
          :key="col"
          size="large"
          disable-transitions
          closable
          @close="deleteCol(col)"
          @contextmenu.prevent="openColEditor(col)"
        >
          {{ col }}
        </el-tag>
      </VueDraggable>
      <el-input
        v-if="inputVisible"
        ref="inputRef"
        v-model="newCol"
        clearable
        @keyup.enter="createCol"
        @blur="createCol"
        style="width: 100px"
      />
      <el-button v-else :icon="Plus" plain @click="showInput" />
      <el-button
        :icon="RefreshLeft"
        plain
        @click="recoverCol"
        :disabled="deletedCols.length === 0"
      />
    </div>
    <el-table :data="tableData">
      <el-table-column v-for="col in tableCols" :key="col" :prop="col" :label="col">
        <template #default="{ row }">
          <el-input v-model="row[col]" />
        </template>
      </el-table-column>
      <el-table-column v-if="tableCols.length > 0" align="center" width="160">
        <template #header>
          <el-button type="primary" link @click="createRow">添加行</el-button>
          <el-button type="primary" link @click="recoverRow" :disabled="deletedRows.length === 0">
            恢复行
          </el-button>
        </template>
        <template #default="{ $index }">
          <el-button type="danger" link @click="deleteRow($index)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
  <el-dialog v-model="colEditParams.dialogVisible" :show-close="false" @closed="closeColEditor">
    <el-form label-width="auto">
      <el-form-item label="原列名">
        <el-input v-model="colEditParams.oldName" disabled />
      </el-form-item>
      <el-form-item label="新列名">
        <el-input v-model="colEditParams.newName" @keyup.enter="updateCol" />
      </el-form-item>
    </el-form>
    <template #footer>
      <div>
        <el-button @click="closeColEditor">取消</el-button>
        <el-button type="primary" @click="updateCol" :disabled="colEditParams.newName === ''">
          确定
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup>
import { VueDraggable } from 'vue-draggable-plus'
import { Plus, RefreshLeft } from '@element-plus/icons-vue'

const tableCols = ref(['列1', '列2'])
const tableData = ref([
  { 列1: '1-1', 列2: '1-2' },
  { 列1: '2-1', 列2: '2-2' }
])

const deletedCols = ref([])
const deleteCol = (col) => {
  tableCols.value.splice(tableCols.value.indexOf(col), 1)
  deletedCols.value.push(col)
}

const inputVisible = ref(false)
const inputRef = ref(null)
const newCol = ref('')
const showInput = () => {
  inputVisible.value = true
  nextTick(() => {
    inputRef.value.input.focus()
  })
}

const createCol = () => {
  if (!newCol.value) return
  const newCols = newCol.value.split(';')
  tableCols.value.push(...newCols)
  inputVisible.value = false
  newCol.value = ''
}

const recoverCol = () => {
  if (deletedCols.value.length === 0) return
  const col = deletedCols.value.pop()
  tableCols.value.push(col)
}

const colEditParams = reactive({
  dialogVisible: false,
  oldName: '',
  newName: ''
})

const openColEditor = (oldName) => {
  Object.assign(colEditParams, { dialogVisible: true, oldName })
}
const closeColEditor = () => {
  Object.assign(colEditParams, {
    dialogVisible: false,
    oldName: '',
    newName: ''
  })
}
const updateCol = () => {
  const { oldName, newName } = toRaw(colEditParams)
  if (newName === '') return
  const index = tableCols.value.findIndex((col) => col === oldName)
  deleteCol(oldName)
  tableCols.value.splice(index, 0, newName)
  tableData.value.forEach((item) => {
    item[newName] = item[oldName]
  })
  closeColEditor()
}

const createRow = () => {
  const newRow = {}
  tableCols.value.forEach((col) => {
    newRow[col] = null
  })
  tableData.value.push(newRow)
}

const deletedRows = ref([])
const deleteRow = (index) => {
  const row = tableData.value.splice(index, 1)[0]
  deletedRows.value.push(row)
}

const recoverRow = () => {
  if (deletedRows.value.length === 0) return
  const row = deletedRows.value.pop()
  tableData.value.push(row)
}
</script>

<style scoped lang="scss">
@use '@/assets/styles/mixin';

.hstack {
  @include mixin.flex($items: center, $gap: 8px);

  .el-button + .el-button {
    margin-left: 0;
  }
}

.tags {
  flex-wrap: wrap;
  @include mixin.flex($items: center, $gap: 8px);
}

.el-tag {
  cursor: grab;

  &:active {
    cursor: grabbing;
  }
}
</style>