Vue 后台管理系统:封装通用el-table导出方法(附完整源码)

一、为什么要封装自定义导出方法?

在基于若依框架二次开发的后台系统中,框架本身提供了一个 download() 方法用于导出文件。但随着业务增长,会遇到若干限制:

js 复制代码
// 若依框架内置 download 方法(有局限)
export function download(url, params, filename) {
  return service.post(url, params, {
    transformRequest: [(params) => { return tansParams(params) }], // 写死 form 格式
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, // 写死 Content-Type
    responseType: 'blob',
  })
}

实际业务中经常碰到以下问题:

痛点 原因
后端接口要求传 JSON body 内置方法写死了 form-urlencoded 格式
接口是 GET 请求 内置方法只能 POST
需要携带额外请求头 无法自定义 headers
使用了独立鉴权方式 无法覆盖默认 token 格式

为此,我们封装一个更灵活的 commonDownload 方法来解决这些问题。


二、完整实现代码

在 src/utils/request.js 中添加以下方法:

js 复制代码
import axios from 'axios'
import { Loading, Message } from 'element-ui'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
import { blobValidate, tansParams } from '@/utils/ruoyi'
import errorCode from '@/utils/errorCode'

let downloadLoadingInstance

/**
 * 自定义通用导出方法
 * 相比框架内置的 download(),支持 GET/POST、JSON/form、自定义 headers
 *
 * @param {Object}  options
 * @param {string}  options.url                   - 请求完整地址
 * @param {string}  options.requsetMethod         - 请求方法:'get' | 'post'
 * @param {Object} [options.params]               - URL query 参数(GET 场景)
 * @param {Object} [options.data]                 - 请求 body(POST JSON 场景)
 * @param {boolean}[options.needTransformRequest] - true 时将 data 转 form 格式,默认 false
 * @param {Object} [options.headers]              - 自定义请求头,默认带 Bearer Token
 * @param {string}  options.filename              - 保存的文件名,如 '订单列表.xlsx'
 */
export function commonDownload({
  url,
  requsetMethod,
  params,
  needTransformRequest,
  data,
  headers,
  filename,
}) {
  // 第一步:展示全屏 Loading,防止用户重复点击
  downloadLoadingInstance = Loading.service({
    text: '正在下载数据,请稍候',
    spinner: 'el-icon-loading',
    background: 'rgba(0, 0, 0, 0.7)',
  })

  // 第二步:直接用原始 axios 发请求,绕开响应拦截器
  return axios({
    url,
    method: requsetMethod,
    params,
    data,
    // 按需将 data 转成 form-urlencoded 格式
    transformRequest: needTransformRequest
      ? [(data) => tansParams(data)]
      : undefined,
    // 默认以 JSON + Bearer Token 发送,可通过 headers 参数完全覆盖
    headers: headers || {
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + getToken(),
    },
    responseType: 'blob',  // 接收二进制流
    timeout: 120000,       // 超时时间 2 分钟
  })
    .then(async (res) => {
      const blobData = res.data

      // 第三步:校验是否为合法文件(防止把 JSON 错误响应当文件下载)
      const isValidFile = await blobValidate(blobData)

      if (isValidFile) {
        // 合法文件:触发浏览器下载
        const blob = new Blob([blobData])
        saveAs(blob, filename)
      } else {
        // 非合法文件:解析错误信息并提示用户
        const resText = await blobData.text()
        const rspObj = JSON.parse(resText)
        const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
        Message.error(errMsg)
      }

      downloadLoadingInstance.close()
    })
    .catch((err) => {
      console.error(err)
      Message.error('下载文件出现错误,请联系管理员!')
      downloadLoadingInstance.close()
    })
}

三、全局挂载

在 src/main.js 中注册到 Vue 原型,所有组件无需单独引入:

js 复制代码
// src/main.js
import { download, commonDownload } from '@/utils/request'

Vue.prototype.download = download             // 保留框架原生方法
Vue.prototype.commonDownload = commonDownload // 新增自定义方法

四、使用示例:订单管理列表

下面以一个订单管理页面为例,演示完整用法。

4.1 页面结构

vue 复制代码
<template>
  <div class="order-page">
    <!-- 筛选栏 -->
    <div class="filter-bar">
      <el-input
        v-model="queryParams.orderNo"
        placeholder="订单号"
        clearable
        style="width: 200px"
      />
      <el-select v-model="queryParams.status" placeholder="订单状态" clearable>
        <el-option label="待支付" :value="0" />
        <el-option label="已支付" :value="1" />
        <el-option label="已取消" :value="2" />
      </el-select>
      <el-date-picker
        v-model="queryParams.dateRange"
        type="daterange"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        value-format="yyyy-MM-dd"
      />
      <el-button type="primary" @click="handleQuery">查询</el-button>
      <el-button icon="el-icon-download" @click="handleExport">导出</el-button>
    </div>

    <!-- 列表 -->
    <el-table v-loading="loading" :data="orderList">
      <el-table-column label="订单号"   prop="orderNo"     />
      <el-table-column label="商品名称" prop="productName" />
      <el-table-column label="金额(元)" prop="amount"      />
      <el-table-column label="下单时间" prop="createTime"  />
      <el-table-column label="状态"     prop="statusLabel" />
    </el-table>

    <el-pagination
      :current-page.sync="queryParams.pageNum"
      :page-size.sync="queryParams.pageSize"
      :total="total"
      layout="total, sizes, prev, pager, next"
      @size-change="loadList"
      @current-change="loadList"
    />
  </div>
</template>

4.2 逻辑部分

js 复制代码
<script>
import { getOrderList } from '@/api/order'

export default {
  name: 'OrderList',
  data() {
    return {
      loading: false,
      total: 0,
      orderList: [],
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        orderNo: '',
        status: '',     // 空字符串表示"全部状态"
        dateRange: [],
      },
    }
  },
  created() {
    this.loadList()
  },
  methods: {
    async loadList() {
      this.loading = true
      try {
        const { orderNo, status, dateRange, pageNum, pageSize } = this.queryParams
        const res = await getOrderList({
          orderNo:   orderNo || undefined,
          status:    status !== '' ? status : undefined,
          startDate: dateRange?.[0] || undefined,
          endDate:   dateRange?.[1] || undefined,
          pageNum,
          pageSize,
        })
        this.orderList = res.rows || []
        this.total     = res.total || 0
      } finally {
        this.loading = false
      }
    },

    handleQuery() {
      this.queryParams.pageNum = 1
      this.loadList()
    },

    /** 导出当前筛选条件下的全量数据 */
    handleExport() {
      const { orderNo, status, dateRange } = this.queryParams

      this.commonDownload({
        // ① 后端导出接口地址(含 base URL)
        url: process.env.VUE_APP_BASE_API + '/order/export',

        // ② POST 请求,参数放在 body 里
        requsetMethod: 'post',

        // ③ 请求 body(只传有值的字段,undefined 会被 axios 自动忽略)
        data: {
          orderNo:   orderNo || undefined,
          // 状态值 0 是合法值,不能用 || undefined,需要严格判断
          status:    status !== '' ? status : undefined,
          startDate: dateRange?.[0] || undefined,
          endDate:   dateRange?.[1] || undefined,
        },

        // ④ 文件名
        filename: '订单列表.xlsx',
      })
    },
  },
}
</script>

4.3 空值过滤的注意事项

data 里用了两种写法,原因如下:

js 复制代码
// ✅ 字符串类型:空字符串用 || undefined 过滤即可
orderNo: orderNo || undefined

// ❌ 数字类型(含 0):不能用 || undefined,0 会被错误过滤掉
status: status || undefined  // 当 status = 0(待支付)时,会变成 undefined!

// ✅ 数字类型:必须用严格判断
status: status !== '' ? status : undefined

五、参数速查表

参数 类型 必填 说明
url string ✅ 请求完整地址(含 base URL)
requsetMethod string ✅ 'get' 或 'post'
filename string ✅ 下载文件名,如 '订单列表.xlsx'
data object ❌ POST 请求 body(JSON 格式传参)
params object ❌ URL 查询参数(GET 请求或附加参数)
needTransformRequest boolean ❌ true 时把 data 转 form 格式,默认 false
headers object ❌ 完全自定义请求头,默认附带 Bearer Token

六、其他场景示例

场景一:GET 请求导出

js 复制代码
this.commonDownload({
  url: process.env.VUE_APP_BASE_API + '/report/monthly/export',
  requsetMethod: 'get',
  params: {
    year:  this.queryParams.year,
    month: this.queryParams.month,
  },
  filename: '月度报表.xlsx',
})

场景二:兼容老接口(form-urlencoded 格式)

js 复制代码
this.commonDownload({
  url: process.env.VUE_APP_BASE_API + '/legacy/staff/export',
  requsetMethod: 'post',
  data: { deptId: this.deptId, status: 1 },
  needTransformRequest: true,  // 转成 key=val&key2=val2 格式
  filename: '员工列表.xlsx',
})

场景三:带额外请求头(多租户系统)

js 复制代码
this.commonDownload({
  url: process.env.VUE_APP_BASE_API + '/tenant/invoice/export',
  requsetMethod: 'post',
  data: { startDate: '2024-01-01', endDate: '2024-12-31' },
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer ' + getToken(),
    'X-Tenant-Id': this.currentTenantId,  // 额外的租户标识
  },
  filename: '发票记录.xlsx',
})

七、总结

commonDownload 方法的核心设计要点:

  1. 绕开响应拦截器 :直接用原始 axios,避免 blob 流被 JSON 解析器破坏
  2. 防御性校验 :blobValidate 识别"伪装成文件的错误响应",给用户正确提示
  3. 灵活参数设计:同时支持 GET/POST、JSON/form、自定义 headers,一个方法覆盖绝大多数导出场景
  4. 全局挂载 :注册到 Vue.prototype,业务组件直接 this.commonDownload() 调用
  5. 空值过滤:传参时区分字符串和数字类型,用正确的方式过滤空值
相关推荐
TheITSea38 分钟前
JavaScript Web APIs
开发语言·前端·javascript
专业程序开发源1 小时前
springboot生命故事书制作小程序65290-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·mysql·小程序·课程设计
鬓戈1 小时前
Claude Code frontend-design 插件调研 与 Vue 旧系统风格一致性方案
前端·vue.js·人工智能
开开心心就好1 小时前
以图搜图找重复图片,本地工具离线就能用
javascript·智能手机·ffmpeg·c#·ocr·word·音视频
vx_Biye_Design1 小时前
springboot角色扮演服务平台65161-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·spring·课程设计
企业数字化笔记1 小时前
视频局部合成怎么做得自然?掩膜修复、边缘羽化、时序稳定与背景替换
java·javascript·音视频
vx_Biye_Design1 小时前
springboot咖啡厅顾客点单管理系统12080-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
vx_Biye_Design2 小时前
springbootNBA数据分析系统平台42434-计算机课程设计、毕业设计
java·vue.js·spring boot·python·django·课程设计·express
liangshanbo12154 小时前
JavaScript 面试题:Object.keys() 的返回顺序是无序的吗?
开发语言·javascript·ecmascript
默_笙16 小时前
🚦 请三个 AI 考官给 RAG 打分:量化评估实战(下)
前端·javascript