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. 空值过滤:传参时区分字符串和数字类型,用正确的方式过滤空值
相关推荐
mONESY43 分钟前
JavaScript 栈、队列、数组与链表核心知识点总结
javascript·面试
ZengLiangYi1 小时前
TypeScript 项目配置:tsconfig、ESM、路径别名
javascript·typescript·aigc
晓13131 小时前
【Cocos Creator 3.x】篇——第二章 入门
前端·javascript·游戏引擎
想要成为糕糕手1 小时前
前端必修课:JavaScript 数组与数据结构底层逻辑全解析
javascript·数据结构·面试
xiaofeichaichai2 小时前
React Hooks
前端·javascript·react.js
数据知道2 小时前
C++ 层拦截:修改 Blink 引擎与 V8 绑定的底层逻辑
javascript·数据采集·指纹浏览器·风控
拉拉肥_King2 小时前
Vue 3 主题切换深度解析:从炫酷动画到零闪烁方案
前端·vue.js
2301_773643622 小时前
ceph镜像
前端·javascript·ceph
To_OC3 小时前
万字解析《JS语言精粹》之第四章:函数15大核心精髓(JS灵魂核心)
前端·javascript·代码规范
宋拾壹3 小时前
同时添加多个类目
android·开发语言·javascript