🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

加班到凌晨,我用 Vue3 + ElementUI 寫了個可編輯的表格元件

20250919_110511.gif

前言

大家好,我是大華!

在我們日常的後台管理開發中,表格可以說是最常用的資料展示和操作元件之一了。

很多用戶還希望能夠直接在線編輯表格資料、插入新行、刪除不需要的行,甚至還需要支援各種類型的資料輸入。

這時候,一個通用的可編輯表格元件就顯得尤為重要。

所以我加班加點整出了這麼一個表格元件。

功能預覽

先來看下效果圖:

20250919_110511.gif

我們看看上面的元件效果圖具備了哪些功能:

  • 支援單元格雙擊編輯
  • 支援右鍵選單操作(插入行、刪除行)
  • 支援多種輸入類型(文本、數字、下拉選擇、日期選擇)
  • 支援彙總行計算
  • 響應式數據更新
  • 靈活的列配置

核心程式碼實現

我們一步步來實現這可編輯的表格元件。

1. 元件基礎結構

首先,我們定義元件的基本結構和 Props

<!-- EditableTable.vue -->
<template>
  <!-- 
    el-table 是 Element Plus 的表格元件
    :data 绑定表格資料
    @cell-dblclick 監聽單元格雙擊事件
    @row-contextmenu 監聽行右鍵點擊事件
    :summary-method 指定彙總行計算方法
    :row-class-name 指定行類名生成方法
    v-bind="$attrs" 繼承所有未聲明的屬性
  -->
  <el-table
    :data="tableData"
    @cell-dblclick="handleCellDblClick"
    @row-contextmenu="handleRowRightClick"
    :summary-method="getSummaries"
    :row-class-name="tableRowClassName"
    :border="border"
    :show-summary="showSummary"
    v-bind="$attrs"
  >
    <!-- 序號列 -->
    <el-table-column
      v-if="showIndex"
      type="index"
      label="序號"
      width="60"
    />

    <!-- 表格列渲染 -->
    <el-table-column
      v-for="column in columns"
      :key="column.prop"
      :prop="column.prop"
      :label="column.label"
      :width="column.width"
    >
      <!-- 使用作用域插槽自訂單元格內容 -->
      <template #default="scope">
        <!-- 根據列類型渲染不同的輸入元件 -->
        <!-- 數字輸入框 -->
        <el-input-number
          v-if="column.type === 'number' && scope.row[`${column.prop}_editing`]"
          v-model.number="scope.row[column.prop]"
          :min="column.min || 0"
          :max="column.max || 100"
          :step="column.step || 1"
          @blur="scope.row[`${column.prop}_editing`] = false"
          size="small"
        />

        <!-- 下拉選擇框 -->
        <el-select
          v-else-if="column.type === 'select' && scope.row[`${column.prop}_editing`]"
          v-model="scope.row[column.prop]"
          :multiple="column.multiple || false"
          @blur="scope.row[`${column.prop}_editing`] = false"
          size="small"
        >
          <el-option
            v-for="item in column.options || []"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>

        <!-- 日期選擇器 -->
        <el-date-picker
          v-else-if="column.type === 'date' && scope.row[`${column.prop}_editing`]"
          v-model="scope.row[column.prop]"
          type="date"
          placeholder="選擇日期"
          @blur="scope.row[`${column.prop}_editing`] = false"
          size="small"
        />

        <!-- 文本編輯框 -->
        <el-input
          v-else-if="scope.row[`${column.prop}_editing`]"
          v-model="scope.row[column.prop]"
          @blur="scope.row[`${column.prop}_editing`] = false"
          size="small"
          autofocus
        />

        <!-- 文本顯示(非編輯狀態) -->
        <div
          v-else
          class="cell-text"
          @dblclick.stop="handleCellDblClick(scope.row, { property: column.prop })"
        >
          {{ formatCellValue(scope.row[column.prop], column) }}
        </div>
      </template>
    </el-table-column>

    <!-- 右鍵選單 -->
    <div
      v-show="showContextMenu"
      class="context-menu"
      :style="{ top: contextMenuTop + 'px', left: contextMenuLeft + 'px' }"
      @mouseleave="hideContextMenu"
    >
      <el-button @click="insertRowAbove" size="small">上方插入一行</el-button>
      <el-button @click="insertRowBelow" size="small">下方插入一行</el-button>
      <el-button @click="openInsertMultipleDialog(false)" size="small">上方插入多行</el-button>
      <el-button @click="openInsertMultipleDialog(true)" size="small">下方插入多行</el-button>
      <el-button type="danger" @click="deleteCurrentRow" size="small">刪除當前行</el-button>
    </div>
  </el-table>

  <!-- 插入多行對話框 -->
  <el-dialog
    v-model="showInsertMultipleDialog"
    :title="insertMultipleBelow ? '在下方插入多行' : '在上方插入多行'"
    width="400px"
  >
    <el-input-number
      v-model="insertRowCount"
      :min="1"
      :max="10"
      label="插入行數"
    />
    <template #footer>
      <el-button @click="showInsertMultipleDialog = false">取消</el-button>
      <el-button type="primary" @click="insertMultipleRows">確定</el-button>
    </template>
  </el-dialog>
</template>

<script lang="ts" setup>
import { ref, computed, nextTick, watch } from 'vue'
import { ElMessageBox, ElMessage } from 'element-plus'

// 定義表格列的介面
interface TableColumn {
  prop: string           // 欄位名
  label: string          // 列標題
  type?: 'text' | 'number' | 'select' | 'date'  // 輸入類型
  width?: string         // 列寬度
  min?: number           // 最小值(數字類型)
  max?: number           // 最大值(數字類型)
  step?: number          // 步長(數字類型)
  options?: Array<{      // 選項(選擇類型)
    value: string | number
    label: string
  }>
  multiple?: boolean     // 是否多選(選擇類型)
  formatter?: (value: any) => string // 自訂格式化函數
}

// 定義元件接收的屬性
const props = defineProps({
  data: {                // 表格資料
    type: Array,
    default: () => []
  },
  columns: {             // 列配置
    type: Array as () => TableColumn[],
    required: true
  },
  showIndex: {           // 是否顯示序號列
    type: Boolean,
    default: false
  },
  border: {              // 是否顯示邊框
    type: Boolean,
    default: true
  },
  showSummary: {         // 是否顯示彙總行
    type: Boolean,
    default: false
  },
  summaryMethod: {       // 自訂彙總方法
    type: Function,
    default: null
  },
  disabledColumns: {     // 禁止編輯的列
    type: Array as () => string[],
    default: () => []
  }
})

// 定義元件可觸發的事件
const emit = defineEmits(['update:data', 'row-added', 'row-deleted'])

// 使用計算屬性處理表格資料,確保響應式
const tableData = computed({
  get: () => props.data,
  set: (value) => emit('update:data', value)
})

// 右鍵選單相關狀態
const showContextMenu = ref(false)
const contextMenuTop = ref(0)
const contextMenuLeft = ref(0)
const currentContextRow = ref<any>(null)

// 插入多行相關狀態
const showInsertMultipleDialog = ref(false)
const insertRowCount = ref(1)
const insertMultipleBelow = ref(false)

// 檢查列是否被禁用編輯
const isDisabledColumn = (prop: string) => {
  return props.disabledColumns.includes(prop)
}

// 格式化單元格顯示值
const formatCellValue = (value: any, column: TableColumn) => {
  if (column.formatter) {
    return column.formatter(value)
  }
  if (column.type === 'select' && column.options) {
    const option = column.options.find(opt => opt.value === value)
    return option ? option.label : value
  }
  return value
}
</script>

<style scoped>
.context-menu {
  position: fixed;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 10px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  z-index: 2000;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.cell-text {
  width: 100%;
  height: 100%;
  padding: 8px 0;
  cursor: default;
}

.cell-text:hover {
  background-color: #f5f7fa;
}
</style>

### 2. 實現單元格編輯功能

單元格編輯是核心功能之一,我們支援多種輸入類型:

```typescript
// 在 script setup 中添加以下程式碼

// 單元格雙擊事件處理
const handleCellDblClick = (row: any, column: any) => {
  const prop = column.property
  if (!prop || isDisabledColumn(prop)) return

  // 關閉其他單元格的編輯狀態
  props.columns.forEach(col => {
    if (col.prop !== prop) {
      row[`${col.prop}_editing`] = false
    }
  })

  // 開啟當前單元格編輯
  row[`${prop}_editing`] = true
}

// 行右鍵點擊事件處理
const handleRowRightClick = (row: any, column: any, event: MouseEvent) => {
  // 阻止瀏覽器默認右鍵選單
  event.preventDefault()

  // 設定當前右鍵點擊的行
  currentContextRow.value = row

  // 設定選單位置
  contextMenuTop.value = event.clientY
  contextMenuLeft.value = event.clientX

  // 顯示選單
  showContextMenu.value = true
}

// 隱藏右鍵選單
const hideContextMenu = () => {
  showContextMenu.value = false
}

3. 實現行操作功能

右鍵選單提供了豐富的行操作功能:

// 創建新行
const createNewRow = () => {
  const newRow: any = {}

  // 根據列配置初始化新行的值
  props.columns.forEach(column => {
    // 設定預設值
    if (column.type === 'number') {
      newRow[column.prop] = 0
    } else if (column.type === 'select' && column.options && column.options.length > 0) {
      newRow[column.prop] = column.multiple ? [] : column.options[0].value
    } else {
      newRow[column.prop] = ''
    }

    // 初始化編輯狀態
    newRow[`${column.prop}_editing`] = false
  })

  return newRow
}

// 在上方插入一行
const insertRowAbove = () => {
  if (!currentContextRow.value) return

  const index = tableData.value.indexOf(currentContextRow.value)
  if (index === -1) return

  const newRow = createNewRow()
  tableData.value.splice(index, 0, newRow)
  emit('row-added', { index, row: newRow })
  hideContextMenu()
}

// 在下方插入一行
const insertRowBelow = () => {
  if (!currentContextRow.value) return

  const index = tableData.value.indexOf(currentContextRow.value)
  if (index === -1) return

  const newRow = createNewRow()
  tableData.value.splice(index + 1, 0, newRow)
  emit('row-added', { index: index + 1, row: newRow })
  hideContextMenu()
}

// 打開插入多行對話框
const openInsertMultipleDialog = (below: boolean) => {
  insertMultipleBelow.value = below
  insertRowCount.value = 1
  showInsertMultipleDialog.value = true
  hideContextMenu()
}

// 插入多行
const insertMultipleRows = () => {
  if (!currentContextRow.value) return

  const index = tableData.value.indexOf(currentContextRow.value)
  if (index === -1) return

  const startIndex = insertMultipleBelow.value ? index + 1 : index
  const newRows = Array.from({ length: insertRowCount.value }, createNewRow)

  tableData.value.splice(startIndex, 0, ...newRows)

  newRows.forEach((row, i) => {
    emit('row-added', { index: startIndex + i, row })
  })

  showInsertMultipleDialog.value = false
}

// 刪除當前行
const deleteCurrentRow = () => {
  if (!currentContextRow.value) return

  const index = tableData.value.indexOf(currentContextRow.value)
  if (index === -1) return

  ElMessageBox.confirm('確定要刪除這一行嗎?', '提示', {
    confirmButtonText: '確定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const deletedRow = tableData.value.splice(index, 1)
    emit('row-deleted', { index, row: deletedRow[0] })
    ElMessage.success('刪除成功')
    hideContextMenu()
  }).catch(() => {
    // 使用者取消刪除
    hideContextMenu()
  })
}

4. 實現彙總行功能

彙總行可以自動計算數值列的總和:

// 彙總行計算方法
const getSummaries = (param: any) => {
  // 如果提供了自訂彙總方法,使用自訂方法
  if (props.summaryMethod) {
    return props.summaryMethod(param)
  }

  const { columns, data } = param
  const sums: string[] = []

  columns.forEach((column: any, index: number) => {
    if (index === 0) {
      sums[index] = '合計'
      return
    }

    // 只對數字類型的列進行彙總
    const colConfig = props.columns.find(col => col.prop === column.property)
    if (!colConfig || colConfig.type !== 'number') {
      sums[index] = ''
      return
    }

    // 計算總和
    const values = data.map((item: any) => Number(item[column.property]))
    if (values.every((value: any) => isNaN(value))) {
      sums[index] = ''
    } else {
      const sum = values.reduce((prev: number, curr: number) => {
        const value = Number(curr)
        return isNaN(value) ? prev : prev + value
      }, 0)
      sums[index] = `${sum}`
    }
  })

  return sums
}

// 行類名生成方法,用於設定彙總行樣式
const tableRowClassName = ({ rowIndex }: { rowIndex: number }) => {
  if (props.showSummary && rowIndex === tableData.value.length) {
    return 'summary-row'
  }
  return ''
}

使用示例

現在讓我們看看如何使用這個元件:

<!-- Example.vue -->
<template>
  <div class="container">
    <h1>可編輯表格示例</h1>

    <EditableTable
      :data="tableData"
      :columns="columns"
      :show-index="true"
      :show-summary="true"
      @update:data="handleDataUpdate"
      @row-added="handleRowAdded"
      @row-deleted="handleRowDeleted"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import EditableTable from './EditableTable.vue'

// 表格資料
const tableData = ref<any>([
  { id: 1, name: '張三', age: 25, gender: 'male', score: 85, birthdate: '1998-05-12' },
  { id: 2, name: '李四', age: 30, gender: 'female', score: 92, birthdate: '1993-08-24' },
  { id: 3, name: '王五', age: 28, gender: 'male', score: 78, birthdate: '1995-11-03' }
])

// 列配置
const columns = ref<any>([
  { prop: 'name', label: '姓名', width: '120px' },
  { 
    prop: 'age', 
    label: '年齡', 
    type: 'number',
    min: 0,
    max: 150
  },
  { 
    prop: 'gender', 
    label: '性別', 
    type: 'select',
    options: [
      { value: 'male', label: '男' },
      { value: 'female', label: '女' }
    ]
  },
  { 
    prop: 'score', 
    label: '分數', 
    type: 'number',
    min: 0,
    max: 100
  },
  { 
    prop: 'birthdate', 
    label: '出生日期', 
    type: 'date' 
  }
])

// 處理資料更新
const handleDataUpdate = (newData: any[]) => {
  tableData.value = newData
  console.log('資料已更新:', newData)
}

// 處理行添加事件
const handleRowAdded = ({ index, row }: { index: number; row: any }) => {
  console.log(`在第 ${index} 行添加了新行:`, row)
}

// 處理行刪除事件
const handleRowDeleted = ({ index, row }: { index: number; row: any }) => {
  console.log(`刪除了第 ${index} 行:`, row)
}
</script>

<style scoped>
.container {
  padding: 20px;
}
</style>

功能擴展建議

這個元件基本的操作是夠用的,你也可以根據實際需求進一步擴展:

  1. 資料驗證 - 添加單元格資料驗證功能,確保輸入資料的正確性
  2. 撤銷重做 - 實現操作歷史記錄,支援撤銷和重做操作
  3. 批量操作 - 支援批量編輯和刪除,提高操作效率
  4. 列配置 - 允許用戶自訂顯示哪些列,以及列的顯示順序
  5. 匯入匯出 - 支援 Excel 匯入匯出功能,方便資料交換
  6. 分頁功能 - 集成分頁支持大資料量,提高性能
  7. 行拖拽排序 - 支援通過拖拽調整行順序
  8. 列寬調整 - 支援通過拖拽調整列寬度

EditableTable 元件完整程式碼

<template>
  <el-table
    :data="tableData"
    @cell-dblclick="handleCellDblClick"
    @row-contextmenu="handleRowRightClick"
    :summary-method="getSummaries"
    :row-class-name="tableRowClassName"
    :border="border"
    :show-summary="showSummary"
    v-bind="$attrs"
  >
    <el-table-column
      v-if="showIndex"
      type="index"
      label="序號"
      align="center"
      :resizable="false"
      width="70"
    />

    <el-table-column
      v-for="column in columns"
      :key="column.prop"
      :prop="column.prop"
      :label="column.label"
      :align="column.align || 'left'"
      :width="column.width"
      :resizable="column.resizable !== false"
    >
      <template #default="scope">
        <el-input-number
          v-if="column.type === 'number'"
          v-model.number="scope.row[column.prop]"
          :min="column.min"
          :max="column.max"
          :step="column.step"
          :precision="column.precision"
          :controls="column.controls !== false"
          :disabled="isDisabled(column, scope.row)"
          style="width: 100%"
        />

        <el-select
          v-else-if="column.type === 'select'"
          v-model="scope.row[column.prop]"
          :multiple="column.multiple || false"
          :multiple-limit="column.multipleLimit || 1"
          :filterable="column.filterable !== false"
          :clearable="column.clearable !== false"
          :disabled="isDisabled(column, scope.row)"
          :placeholder="column.placeholder || '請選擇'"
          style="width: 100%"
        >
          <el-option
            v-for="item in column.options || []"
            :key="item[column.valueKey || 'value']"
            :label="item[column.labelKey || 'label']"
            :value="item[column.valueKey || 'value']"
          />
        </el-select>

        <el-date-picker
          v-else-if="column.type === 'date'"
          v-model="scope.row[column.prop]"
          :type="column.dateType || 'date'"
          :format="column.format || 'YYYY-MM-DD'"
          :value-format="column.valueFormat || 'YYYY-MM-DD'"
          :disabled="isDisabled(column, scope.row)"
          :placeholder="column.placeholder || '選擇日期'"
          style="width: 100%"
        />

        <div
          v-else-if="!scope.row[`${column.prop}_editing`]"
          class="cell-text"
          v-html="formatCellValue(scope.row[column.prop], column.formatter)"
        />

        <el-input
          v-else
          :ref="setInputRef(scope.$index, column.prop)"
          v-model="scope.row[column.prop]"
          :type="column.inputType || 'text'"
          :autosize="{ minRows: 1, maxRows: 4 }"
          :disabled="isDisabled(column, scope.row)"
          @blur="scope.row[`${column.prop}_editing`] = false"
          @keyup.enter="scope.row[`${column.prop}_editing`] = false"
        />
      </template>
    </el-table-column>

    <div
      v-show="showContextMenu"
      id="context-menu"
      class="context-menu"
      @mouseleave="hideContextMenu"
    >
      <el-button type="primary" @click="insertRowAbove">上方插入一行</el-button>
      <el-button @click="openInsertMultipleDialog(false)">上方插入多行</el-button>
      <el-button type="primary" @click="insertRowBelow">下方插入一行</el-button>
      <el-button @click="openInsertMultipleDialog(true)">下方插入多行</el-button>
      <el-button type="danger" @click="deleteCurrentRow">刪除當前行</el-button>
    </div>

    <el-dialog
      v-model="showInsertDialog"
      title="插入多行"
      width="300px"
      append-to-body
    >
      <el-input-number
        v-model="insertRowCount"
        :min="1"
        :max="20"
        style="width: 100%"
      />
      <template #footer>
        <el-button @click="showInsertDialog = false">取消</el-button>
        <el-button type="primary" @click="insertMultipleRows">確定</el-button>
      </template>
    </el-dialog>
  </el-table>
</template>

<script lang="ts" setup>
import { ref, reactive, watch, nextTick, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'

interface TableColumn {
  prop: string
  label: string
  type?: 'text' | 'number' | 'select' | 'date'
  width?: string | number
  align?: 'left' | 'center' | 'right'
  resizable?: boolean
  min?: number
  max?: number
  step?: number
  precision?: number
  options?: any[]
  valueKey?: string
  labelKey?: string
  multiple?: boolean
  multipleLimit?: number
  filterable?: boolean
  clearable?: boolean
  placeholder?: string
  dateType?: string
  format?: string
  valueFormat?: string
  inputType?: string
  disabled?: boolean | ((row: any) => boolean)
  formatter?: (value: any) => string
  controls?: boolean
}

const props = defineProps({
  data: {
    type: Array,
    default: () => []
  },
  columns: {
    type: Array as () => TableColumn[],
    required: true
  },
  showIndex: {
    type: Boolean,
    default: true
  },
  border: {
    type: Boolean,
    default: true
  },
  showSummary: {
    type: Boolean,
    default: false
  },
  summaryMethod: {
    type: Function,
    default: null
  },
  disabledColumns: {
    type: Array as () => string[],
    default: () => []
  }
})

const emit = defineEmits(['update:data', 'row-added', 'row-deleted'])

const tableData = ref<any[]>([])

const showContextMenu = ref(false)
const currentContextRow = reactive({
  index: null as number | null,
  column: null as string | null,
  isHeader: false
})
const showInsertDialog = ref(false)
const insertRowCount = ref(1)
const insertBelow = ref(false)
const inputRefs = ref<Record<string, any>>({})

watch(() => props.data, (newData) => {
  if (newData && newData.length > 0) {
    tableData.value = newData.map(row => {
      const safeRow = (typeof row === 'object' && row !== null) ? { ...row } : {}
      props.columns.forEach(col => {
        safeRow[`${col.prop}_editing`] = false
      })
      return safeRow
    })
  } else {
    tableData.value = []
  }
}, { immediate: true, deep: true })

const createNewRow = () => {
  const newRow: any = {}
  props.columns.forEach(col => {
    newRow[col.prop] = col.type === 'number' ? 0 : ''
    newRow[`${col.prop}_editing`] = false
  })
  return newRow
}

const setInputRef = (rowIndex: number, prop: string) => (el: any) => {
  inputRefs.value[`${rowIndex}-${prop}`] = el
}

const handleCellDblClick = (row: any, column: any) => {
  const prop = column.property
  if (!prop || isDisabledColumn(prop) || isDisabled(column, row)) return

  props.columns.forEach(col => {
    row[`${col.prop}_editing`] = false
  })

  row[`${prop}_editing`] = true

  nextTick(() => {
    const inputKey = `${row.row_index}-${prop}`
    const input = inputRefs.value[inputKey]
    if (input) {
      input.focus()
    }
  })
}

const handleRowRightClick = (row: any, column: any, event: MouseEvent) => {
  event.preventDefault()
  showContextMenu.value = false

  const menu = document.getElementById('context-menu')
  if (menu) {
    menu.style.left = `${event.clientX}px`
    menu.style.top = `${event.clientY}px`
  }

  showContextMenu.value = true
  currentContextRow.index = row.row_index
  currentContextRow.column = column.property
  currentContextRow.isHeader = false
}

const hideContextMenu = () => {
  showContextMenu.value = false
}

const insertRowAbove = () => {
  if (currentContextRow.index === null) return
  const newRow = createNewRow()
  tableData.value.splice(currentContextRow.index, 0, newRow)
  emit('row-added', { index: currentContextRow.index, row: newRow })
  hideContextMenu()
}

const insertRowBelow = () => {
  if (currentContextRow.index === null) return
  const newRow = createNewRow()
  tableData.value.splice(currentContextRow.index + 1, 0, newRow)
  emit('row-added', { index: currentContextRow.index + 1, row: newRow })
  hideContextMenu()
}

const openInsertMultipleDialog = (below: boolean) => {
  insertBelow.value = below
  insertRowCount.value = 1
  showInsertDialog.value = true
}

const insertMultipleRows = () => {
  if (currentContextRow.index === null) return
  const newRows = Array.from({ length: insertRowCount.value }, createNewRow)
  const insertIndex = insertBelow.value ? currentContextRow.index + 1 : currentContextRow.index
  tableData.value.splice(insertIndex, 0, ...newRows)

  emit('row-added', { index: insertIndex, rows: newRows, count: insertRowCount.value })
  showInsertDialog.value = false
  hideContextMenu()
}

const deleteCurrentRow = () => {
  if (currentContextRow.index === null) return

  ElMessageBox.confirm('確定要刪除這一行嗎?', '提示', {
    confirmButtonText: '確定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const deletedRow = tableData.value.splice(currentContextRow.index!, 1)
    emit('row-deleted', { index: currentContextRow.index, row: deletedRow[0] })
    hideContextMenu()
    ElMessage.success('刪除成功')
  }).catch(() => {
    hideContextMenu()
  })
}

const tableRowClassName = ({ row, rowIndex }: { row: any, rowIndex: number }) => {
  row.row_index = rowIndex
}

const formatCellValue = (value: any, formatter?: (value: any) => string) => {
  if (formatter) {
    return formatter(value)
  }
  if (value === null || value === undefined) {
    return ''
  }
  return String(value).replace(/(\r\n|\n)/g, '<br/>')
}

const isDisabledColumn = (prop: string) => {
  return props.disabledColumns.includes(prop)
}

const isDisabled = (column: TableColumn | string, row?: any) => {
  if (typeof column === 'string') {
    return isDisabledColumn(column)
  }

  if (column.disabled === undefined) {
    return isDisabledColumn(column.prop)
  }

  if (typeof column.disabled === 'function') {
    return column.disabled(row)
  }

  return column.disabled
}

const getSummaries = (param: any) => {
  if (props.summaryMethod) {
    return props.summaryMethod(param)
  }

  const { columns, data } = param
  const sums: string[] = []

  columns.forEach((column: any, index: number) => {
    if (index === 0) {
      sums[index] = '合計'
      return
    }

    const colConfig = props.columns.find(col => col.prop === column.property)
    if (!colConfig || colConfig.type !== 'number') {
      sums[index] = ''
      return
    }

    const values = data.map((item: any) => Number(item[column.property]))
    if (values.every((value: any) => isNaN(value))) {
      sums[index] = ''
    } else {
      sums[index] = `${values.reduce((prev: number, curr: number) => {
        const value = Number(curr)
        return isNaN(value) ? prev : prev + value
      }, 0)}`
    }
  })

  return sums
}
</script>

<style scoped>
.cell-text {
  width: 100%;
  min-height: 100%;
  word-break: break-word;
}

.context-menu {
  position: fixed;
  z-index: 9999;
  background: white;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.context-menu button {
  width: 100%;
  text-align: left;
}

::v-deep(.el-button+.el-button) {
  margin-left: 0;
}
</style>

總結

這個元件不僅提供了基本的資料展示功能,還支援多種編輯方式、右鍵操作選單、自動彙總等功能。

關鍵實現要點:

  • 使用動態渲染支援多種輸入類型
  • 利用雙擊和右鍵事件提供直觀的操作方式
  • 通過統一的 API 設計保證元件易用性
  • 提供豐富的自訂配置選項

希望這個元件能夠幫助你在實際專案中提高開發效率!如果你有任何問題或建議,歡迎在評論區留言討論。

記得給文章點個贊,收藏起來,下次需要時可快速查找哦!

本文首發於公眾號:程式員劉大華,專注分享前後端開發的實戰筆記。關注我,少走彎路,一起進步!

📌往期精彩

《SpringBoot 中的 7 種耗時統計方式,你用過幾種?》

《Java8 都出這麼多年了,Optional 還是沒人用?到底卡在哪了?》

《加班到凌晨,我用 Vue3 + ElementUI 寫了個可編輯的表格元件》

《vue3 登入頁還能這麼絲滑?這個 hover 效果太驚艷了》


原文出處:https://juejin.cn/post/7556852841468788746


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝22   💬9   ❤️4
712
🥈
我愛JS
📝4   💬13   ❤️7
255
🥉
御魂
💬1  
3
#5
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付