最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Vue3+ElementPlus+xlsx的通用表格導(dǎo)出方案

 更新時(shí)間:2026年02月26日 08:56:09   作者:D_C_tyu  
在前端開發(fā)中,將復(fù)雜的 el-table 表格導(dǎo)出為 Excel 是一個(gè)高頻需求,特別是當(dāng)表格包含多級(jí)表頭、合并單元格以及自定義計(jì)算列時(shí),傳統(tǒng)的導(dǎo)出方案往往不理想,本文將分享一套基于 Vue3 + Element Plus + xlsx 的通用解決方案,需要的朋友可以參考下

摘要:在前端開發(fā)中,將復(fù)雜的 el-table 表格導(dǎo)出為 Excel 是一個(gè)高頻需求。特別是當(dāng)表格包含多級(jí)表頭、合并單元格以及自定義計(jì)算列時(shí),傳統(tǒng)的導(dǎo)出方案往往不理想。本文將分享一套基于 Vue3 + Element Plus + xlsx 的通用解決方案,完美還原表格結(jié)構(gòu),支持單元格樣式調(diào)整與動(dòng)態(tài)數(shù)據(jù)格式化。

一. 前言

在日常開發(fā)中,經(jīng)常會(huì)提出這樣的需求:“把這個(gè)表格導(dǎo)出來,格式要和頁(yè)面上一樣,表頭要合并,數(shù)據(jù)要居中。” 如果是扁平的表格,使用 json2excel 或簡(jiǎn)單的 xlsx 寫入即可。但一旦遇到如下場(chǎng)景:

  1. 多級(jí)表頭(例如:分類 1 > 水果 > 蘋果)
  2. 單元格合并(表頭需要自動(dòng)計(jì)算 rowspan/colspan)
  3. 非數(shù)據(jù)源字段(例如:表格中顯示的“合計(jì)”是前端計(jì)算的,不在原始數(shù)據(jù)里)
  4. 樣式保留(導(dǎo)出的 Excel 需要居中、邊框等)

這就需要我們深入解析 el-table 的列配置,并手動(dòng)構(gòu)建 Excel 的合并區(qū)域。今天,我就把這套經(jīng)過生產(chǎn)環(huán)境驗(yàn)證的代碼分享給大家。

二. 效果預(yù)覽

  • 頁(yè)面展示:包含多級(jí)表頭(分類 1/2,水果/蔬菜等),最后一列為前端計(jì)算的“合計(jì)”。

  • 導(dǎo)出效果:Excel 文件完美復(fù)刻表頭層級(jí),合并單元格正確,所有單元格內(nèi)容水平垂直居中,且“合計(jì)”列數(shù)據(jù)準(zhǔn)確。

三. 核心依賴

我們需要以下庫(kù)來處理 Excel 文件和樣式:

npm install xlsx xlsx-style-vite file-saver
# 或者
yarn add xlsx xlsx-style-vite file-saver

注意:標(biāo)準(zhǔn)的 xlsx 庫(kù)對(duì)樣式支持有限,這里使用 xlsx-style-vite 來支持單元格樣式(如居中)。

四. 核心工具函數(shù) (exportExcelFun.js)

這是本方案的靈魂所在。我們需要遞歸解析 el-tablecolumns 配置,計(jì)算表頭的深度、跨行跨列數(shù),并生成對(duì)應(yīng)的合并規(guī)則。

請(qǐng)將以下代碼保存為 ./utils/exportExcelFun.js

import * as XLSX from 'xlsx'
import * as XLSXStyleVite from 'xlsx-style-vite'
import FileSaver from 'file-saver'
import { ElNotification } from 'element-plus'

/**
 * 計(jì)算列配置的最大嵌套深度(跳過 selection 列)
 * @param columns 列配置數(shù)組
 * @param currentDepth 當(dāng)前深度(從1開始)
 * @returns 最大深度
 */
export function getMaxDepth (columns, currentDepth = 1) {
  return columns.reduce((max, col) => {
    if (col.type === 'selection') return max
    return col.children?.length
      ? Math.max(max, getMaxDepth(col.children, currentDepth + 1))
      : max
  }, currentDepth)
}

/**
 * 為列配置計(jì)算 rowSpan 和 colSpan(深拷貝避免污染原始數(shù)據(jù))
 * @param columns 列配置
 * @param maxDepth 表頭總行數(shù)
 * @param currentDepth 當(dāng)前深度(從0開始)
 * @returns 處理后的列配置(含 rowSpan/colSpan)
 */
export function calculateSpan (columns, maxDepth, currentDepth = 0) {
  // 安全深拷貝
  const clone = (obj) => {
    if (obj === null || typeof obj !== 'object') return obj
    if (Array.isArray(obj)) return obj.map(clone)
    const copy = {}
    Object.keys(obj).forEach(key => {
      if (typeof obj[key] !== 'function') {
        copy[key] = clone(obj[key])
      }
    })
    return copy
  }
  const processed = clone(columns)
  const traverse = (cols, depth) => {
    let totalColSpan = 0
    cols.forEach(col => {
      if (col.type === 'selection') return
      if (col.children?.length) {
        // 非葉子節(jié)點(diǎn):跨列 = 子節(jié)點(diǎn)跨列之和,跨行 = 1
        const childColSpan = traverse(col.children, depth + 1)
        col.colSpan = childColSpan
        col.rowSpan = 1
        totalColSpan += childColSpan
      } else {
        // 葉子節(jié)點(diǎn):跨列 = 1,跨行 = 剩余行數(shù)
        col.colSpan = 1
        col.rowSpan = maxDepth - depth
        totalColSpan += 1
      }
    })
    return totalColSpan
  }
  traverse(processed, currentDepth)
  return processed
}

/**
 * 生成表頭行數(shù)據(jù)及字段映射
 * @param columns 處理后的列配置(含 rowSpan/colSpan)
 * @returns { headerRows: string[][], keyArr: string[] }
 */
export function calcTableHeaderArray (columns) {
  const depth = getMaxDepth(columns)
  const headerRows = Array.from({ length: depth }, () => [])
  const keyArr = []
  const generateRows = (cols, rowIndex, colIndex) => {
    let currentCol = colIndex
    cols.forEach(col => {
      if (col.type === 'selection') return
      // 記錄字段名(僅葉子節(jié)點(diǎn))
      if (!col.children?.length && col.property) {
        keyArr.push(col.property)
      }
      // 填充當(dāng)前行
      headerRows[rowIndex][currentCol] = col.label || ''
      // 遞歸處理子列
      if (col.children?.length) {
        currentCol = generateRows(col.children, rowIndex + 1, currentCol)
      } else {
        // 葉子節(jié)點(diǎn):向下填充空單元格
        for (let r = rowIndex + 1; r < depth; r++) {
          headerRows[r][currentCol] = ''
        }
        currentCol += 1
      }
    })
    return currentCol
  }
  generateRows(columns, 0, 0)
  return { headerRows, keyArr }
}

/**
 * 生成合并區(qū)域配置
 * @param columns 處理后的列配置(含 rowSpan/colSpan)
 * @returns 合并區(qū)域數(shù)組
 */
export function generateMergeRanges (columns) {
  const ranges = []
  const traverse = (cols, startRow, startCol) => {
    let currentCol = startCol
    cols.forEach(col => {
      if (col.type === 'selection') return
      const endRow = startRow + col.rowSpan - 1
      const endCol = currentCol + col.colSpan - 1
      // 僅合并非1x1區(qū)域
      if (col.rowSpan > 1 || col.colSpan > 1) {
        ranges.push({
          s: { r: startRow, c: currentCol },
          e: { r: endRow, c: endCol }
        })
      }
      // 遞歸處理子列
      if (col.children?.length) {
        traverse(col.children, startRow + col.rowSpan, currentCol)
      }
      currentCol += col.colSpan
    })
    return currentCol
  }
  traverse(columns, 0, 0)
  // 按起始行/列排序(Excel要求)
  return ranges.sort((a, b) =>
    a.s.r !== b.s.r ? a.s.r - b.s.r : a.s.c - b.s.c
  )
}

/**
 * 字符串轉(zhuǎn) ArrayBuffer
 */
export function s2ab (s) {
  const buf = new ArrayBuffer(s.length)
  const view = new Uint8Array(buf)
  for (let i = 0; i < s.length; i++) {
    view[i] = s.charCodeAt(i) & 0xFF
  }
  return buf
}

/**
 * 導(dǎo)出 Excel 文件
 * @param options 導(dǎo)出配置
 */
export function prepareForExport ({
  tableRef, // 表格ref
  exportLoading, // loading
  tableData = [], // 表格數(shù)據(jù)
  fieldFormatters = {}, // 定義字段處理函數(shù)映射,沒有可不傳
  sheetName = 'Sheet1', // 標(biāo)簽頁(yè)名稱
  fileName = 'export.xlsx' // 文件名
}) {
  // 健壯性校驗(yàn)
  if (!tableRef?.value) {
    ElNotification.error({ title: '錯(cuò)誤', message: '表格引用無效' })
    return
  }
  if (!tableData.value?.length) {
    ElNotification.warning({ title: '提示', message: '無數(shù)據(jù)可導(dǎo)出' })
    return
  }
  exportLoading.value = true
  try {
    const rawColumns = tableRef.value.columns || []
    if (!rawColumns.length) throw new Error('表格列配置為空')
    // 計(jì)算 rowSpan/colSpan
    const maxDepth = getMaxDepth(rawColumns)
    const processedColumns = calculateSpan(rawColumns, maxDepth, 0)
    const { headerRows, keyArr } = calcTableHeaderArray(processedColumns)
    // 構(gòu)建數(shù)據(jù)行
    const dataRows = tableData.value.map((row, index) => {
      const rowData = [index + 1] // 序號(hào)列
      keyArr.forEach(prop => {
        const formatter = fieldFormatters[prop]
        rowData.push(
          formatter ? formatter(row) : row[prop] ?? ''
        )
      })
      return rowData
    })
    // 生成工作表
    const ws = XLSX.utils.aoa_to_sheet([...headerRows, ...dataRows])
    // 設(shè)置全局樣式
    if (ws['!ref']) {
      const range = XLSX.utils.decode_range(ws['!ref'])
      for (let R = range.s.r; R <= range.e.r; R++) {
        for (let C = range.s.c; C <= range.e.c; C++) {
          const cellRef = XLSX.utils.encode_cell({ r: R, c: C })
          if (!ws[cellRef]) continue
          // 僅設(shè)置缺失樣式的單元格
          ws[cellRef].s = {
            ...ws[cellRef].s || {},
            alignment: {
              horizontal: 'center',
              vertical: 'center',
              wrapText: true
            }
          }
        }
      }
    }
    // 應(yīng)用合并區(qū)域
    ws['!merges'] = generateMergeRanges(processedColumns)
    // 生成并下載文件
    const wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, sheetName)
    const wbout = XLSXStyleVite.write(wb, {
      bookType: 'xlsx',
      type: 'binary',
      cellStyles: true
    })
    setTimeout(() => {
      FileSaver.saveAs(
        new Blob([s2ab(wbout)], { type: 'application/octet-stream' }),
          `${fileName.replace(/\.xlsx$/, '')}.xlsx`
      )
      exportLoading.value = false
      ElNotification.success({ title: '成功', message: '導(dǎo)出成功' })
    }, 1000)
  } catch (error) {
    exportLoading.value = false
    ElNotification.error({
      title: '導(dǎo)出失敗',
      message: error instanceof Error ? error.message : '未知錯(cuò)誤'
    })
  }
}

五. 頁(yè)面組件實(shí)現(xiàn) (Vue3)

在業(yè)務(wù)頁(yè)面中,使用非常簡(jiǎn)單。重點(diǎn)在于:

  1. el-table 綁定 ref。
  2. 通過 fieldFormatters 處理非原始數(shù)據(jù)的列(如合計(jì)列)。
<script setup>
import { onMounted, ref } from 'vue'
import { prepareForExport } from '@/utils/exportExcelFun'

onMounted(() => {
  getTableData()
})

const year0 = ref(new Date().getFullYear()) // 今年
const tableData = ref([])
const loading = ref(false)
const getTableData = () => {
  loading.value = true
  // 生成測(cè)試數(shù)據(jù)
  const data = Array.from({ length: 10 }, (_, i) => ({
    name: '測(cè)試名稱' + (i + 1),
    appleNum: Math.floor(Math.random() * 50) + 10, // 10–59
    bananaNum: Math.floor(Math.random() * 40) + 5, // 5–44
    eggplantNum: Math.floor(Math.random() * 30) + 8, // 8–37
    celeryNum: Math.floor(Math.random() * 35) + 12, // 12–46
    spinachNum: Math.floor(Math.random() * 25) + 15, // 15–39
    chipsNum: Math.floor(Math.random() * 60) + 20, // 20–79
    sausageNum: Math.floor(Math.random() * 45) + 10, // 10–54
    nutNum: Math.floor(Math.random() * 20) + 5, // 5–24
    beveragesNum: Math.floor(Math.random() * 80) + 30 // 30–109
  }))
  tableData.value = data
  loading.value = false
}

// 表格選中
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
  multipleSelection.value = val
}

const calcTotalNum = (row) => {
  return row.appleNum + row.bananaNum + row.eggplantNum + row.celeryNum + row.spinachNum + row.chipsNum + row.sausageNum + row.nutNum + row.beveragesNum
}

const tableRef = ref(null)
const exportLoading = ref(false)
const exportToExcel = () => {
  // 定義字段處理函數(shù)映射
  const fieldFormatters = {
    totalNum: (row) => {
      return calcTotalNum(row)
    }
  }
  const currentYear = year0.value.toString()
  prepareForExport({
    tableRef,
    exportLoading,
    tableData,
    fieldFormatters,
    sheetName: '標(biāo)簽頁(yè)1',
    fileName: `清單_${currentYear}年.xlsx`
  })
}
</script>

<template>
  <div class="serviceBudgetSummaryIndex">
    <div class="topWrap">
      <div class="formTit">
        多級(jí)表頭表格導(dǎo)出excel示例
      </div>
      <div class="opBtn">
        <el-button type="primary" @click="exportToExcel" :loading="exportLoading">導(dǎo)出</el-button>
      </div>
    </div>
    <div class="tableBox">
      <el-table
        v-loading="loading"
        :data="tableData"
        border
        stripe
        height="100%"
        style="width: 100%"
        @selection-change="handleSelectionChange"
        ref="tableRef"
      >
        <el-table-column fixed="left" type="selection" width="80" align="center" />
        <el-table-column fixed="left" type="index" label="序號(hào)" align="center" min-width="80"></el-table-column>
        <el-table-column fixed="left" prop="name" label="名稱" align="center" min-width="100"></el-table-column>
        <el-table-column label="分類1" align="center">
          <el-table-column label="水果" align="center">
            <el-table-column prop="appleNum" label="蘋果" align="center" min-width="100"></el-table-column>
            <el-table-column prop="bananaNum" label="香蕉" align="center" min-width="100"></el-table-column>
          </el-table-column>
          <el-table-column label="蔬菜" align="center">
            <el-table-column prop="eggplantNum" label="茄子" align="center" min-width="100"></el-table-column>
            <el-table-column prop="celeryNum" label="芹菜" align="center" min-width="100"></el-table-column>
            <el-table-column prop="spinachNum" label="菠菜" align="center" min-width="100"></el-table-column>
          </el-table-column>
        </el-table-column>
        <el-table-column label="分類2" align="center">
          <el-table-column label="零食" align="center">
            <el-table-column prop="chipsNum" label="薯片" align="center" min-width="100"></el-table-column>
            <el-table-column prop="sausageNum" label="香腸" align="center" min-width="100"></el-table-column>
          </el-table-column>
          <el-table-column prop="nutNum" label="堅(jiān)果" align="center" min-width="100"></el-table-column>
          <el-table-column prop="beveragesNum" label="飲料" align="center" min-width="100"></el-table-column>
        </el-table-column>
        <el-table-column prop="totalNum" label="合計(jì)" align="center" min-width="100">
          <template #default="{ row }">
            {{ calcTotalNum(row) }}
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.serviceBudgetSummaryIndex {
  width: 100%;
  height: 100%;
  .topWrap {
    display: flex;
    justify-content: space-between;
    .el-button--primary {
      background: #356af9;
      border-radius: 4px;
      border-color: #356af9;
    }
  }
  .tableBox {
    height: calc(100% - 105px);
    margin-top: 5px;
  }
}
</style>

六. 關(guān)鍵技術(shù)點(diǎn)解析

1. 自動(dòng)解析表頭層級(jí)

我們并沒有硬編碼 Excel 的表頭,而是直接讀取 tableRef.value.columns。通過 getMaxDepth 遞歸計(jì)算表頭有多少行,再通過 calculateSpan 計(jì)算每個(gè)表頭單元格應(yīng)該占據(jù)的行數(shù)(rowSpan)和列數(shù)(colSpan)。這使得代碼與模板解耦,你修改 el-table 的結(jié)構(gòu),導(dǎo)出邏輯無需變動(dòng)。

2. 處理非數(shù)據(jù)源字段

注意代碼中的 fieldFormatters。 在表格中,“合計(jì)”列是通過 template #default 前端計(jì)算的,原始 tableData 中并沒有 totalNum 字段。

我們?cè)试S傳入一個(gè)映射對(duì)象,如果某個(gè)字段有對(duì)應(yīng)的 formatter 函數(shù),則執(zhí)行函數(shù)計(jì)算值,否則直接取數(shù)據(jù)。這完美解決了計(jì)算列導(dǎo)出的問題。

3. 樣式與合并

  • 合并:利用 ws['!merges'] 屬性,傳入之前計(jì)算好的 ranges 數(shù)組,Excel 會(huì)自動(dòng)合并單元格。
  • 居中:遍歷生成的工作表所有單元格,設(shè)置 alignment: { horizontal: 'center', vertical: 'center' }。這比在 HTML 中寫 CSS 要繁瑣,但在 xlsx-style-vite 的幫助下變得可行。

注意事項(xiàng)

  1. Selection 列處理:代碼中自動(dòng)跳過了 type='selection' 的列,因?yàn)?Excel 中不需要復(fù)選框。
  2. 大數(shù)據(jù)量:當(dāng)前方案會(huì)遍歷所有單元格設(shè)置樣式。如果表格行數(shù)超過 1000 行,生成速度可能會(huì)變慢。對(duì)于超大數(shù)據(jù)量,建議移除全量樣式設(shè)置,或采用后端導(dǎo)出。
  3. 依賴版本xlsx-style-vite 是專門為 Vite 構(gòu)建工具優(yōu)化的帶樣式版本,如果你使用的是 Webpack,可能需要尋找其他支持樣式的 xlsx 分支或插件。

七. 總結(jié)

通過解析 Element Plus 表格的列配置,結(jié)合 xlsx 庫(kù)的底層能力,我們可以實(shí)現(xiàn)高度還原的 Excel 導(dǎo)出功能。這套方案不僅支持多級(jí)表頭合并,還靈活支持前端計(jì)算列,能夠覆蓋絕大部分el-table表格的導(dǎo)出需求。

本文代碼已在 Vue3 + Element Plus 環(huán)境下實(shí)測(cè),可直接復(fù)制使用。

以上就是基于Vue3+ElementPlus+xlsx的通用表格導(dǎo)出方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3 ElementPlus通用表格導(dǎo)出的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 同步cookie插件原理及實(shí)現(xiàn)示例

    同步cookie插件原理及實(shí)現(xiàn)示例

    這篇文章主要為大家介紹了同步cookie插件原理及實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-10-10
  • Vue頁(yè)面切換空白與刷新404問題的深度解析

    Vue頁(yè)面切換空白與刷新404問題的深度解析

    在使用?Vue.js?開發(fā)單頁(yè)應(yīng)用?(SPA)?的過程中,開發(fā)者經(jīng)常會(huì)遇到兩個(gè)常見問題,頁(yè)面切換時(shí)出現(xiàn)短暫的空白屏幕,以及刷新頁(yè)面時(shí)返回?404?錯(cuò)誤,下面我們就來講講這兩個(gè)問題該如何將解決吧
    2025-05-05
  • vue分頁(yè)組件table-pagebar使用實(shí)例解析

    vue分頁(yè)組件table-pagebar使用實(shí)例解析

    這篇文章主要為大家詳細(xì)解析了vue分頁(yè)組件table-pagebar使用實(shí)例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-08-08
  • axios的interceptors多次執(zhí)行問題解決

    axios的interceptors多次執(zhí)行問題解決

    這篇文章主要為大家介紹了axios中interceptors多次執(zhí)行問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • Vue代碼分割懶加載的實(shí)現(xiàn)方法

    Vue代碼分割懶加載的實(shí)現(xiàn)方法

    這篇文章主要給大家介紹了關(guān)于Vue代碼分割懶加載的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-11-11
  • 手把手教你使用vue-cli腳手架(圖文解析)

    手把手教你使用vue-cli腳手架(圖文解析)

    本篇文章主要介紹了手把手教你使用vue-cli腳手架(圖文解析),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-11-11
  • vue-cli3.0 環(huán)境變量與模式配置方法

    vue-cli3.0 環(huán)境變量與模式配置方法

    vue-cli3.0移除了配置文件目錄: config和build文件夾。可以說是非常的精簡(jiǎn)了,那移除了配置文件目錄后如何自定義配置環(huán)境變量和模式呢?這篇文章主要介紹了vue-cli3.0 環(huán)境變量與模式 ,需要的朋友可以參考下
    2018-11-11
  • vue2.0使用md-edit編輯器的過程

    vue2.0使用md-edit編輯器的過程

    這篇文章主要介紹了vue2.0+使用md-edit編輯器的解決方案,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2024-02-02
  • Vue 中axios配置實(shí)例詳解

    Vue 中axios配置實(shí)例詳解

    本文通過實(shí)例代碼給大家介紹了Vue axios配置,非常不錯(cuò),代碼簡(jiǎn)單易懂,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2018-07-07
  • Vue3?中的??shallowReactive?詳解

    Vue3?中的??shallowReactive?詳解

    本文深入探討了Vue3中新特性shallowReactive的使用和原理,shallowReactive是創(chuàng)建淺響應(yīng)式對(duì)象的API,只對(duì)對(duì)象的第一層屬性進(jìn)行響應(yīng)式轉(zhuǎn)換,不對(duì)嵌套對(duì)象處理,有助于性能優(yōu)化和狀態(tài)管理,同時(shí)也指出了shallowReactive在使用中的注意事項(xiàng)
    2024-10-10

最新評(píng)論

同德县| 琼海市| 沂南县| 英超| 许昌县| 徐汇区| 余姚市| 瑞安市| 神农架林区| 万荣县| 新绛县| 蒲城县| 谢通门县| 望城县| 阳东县| 开封县| 佛教| 江川县| 大埔区| 常宁市| 乐平市| 承德县| 柘城县| 北京市| 安仁县| 鄯善县| 太康县| 子长县| 邹平县| 偃师市| 台江县| 阳高县| 洛阳市| 东兴市| 马关县| 阿城市| 衡山县| 天等县| 盐津县| 正宁县| 多伦县|