Skip to content

角色管理与合同详情(二) ​

本页对应 角色管理与合同详情,共 8 段视频,约 1 小时 31 分。

学习目标 ​

  • 能够调用权限列表接口获取树形数据,并使用 Tree 组件完成基础渲染,通过 props 配置正确识别 title、children 字段。
  • 能够为 Tree 组件配置显示复选框、默认展开全部节点,并区分布尔属性绑定中 checked 与 :checked="true" 的写法差异。
  • 能够使用递归函数遍历多层树形数据,为每一层节点添加 disabled: true,实现全部权限点禁用展示。
  • 能够根据角色 ID 获取已选权限数据,调用 Tree 实例方法回填高亮选中项,并处理多棵树实例的遍历与 node-key 配置。
  • 能够利用 Promise.all 或异步等待保证角色列表和权限树数据请求完成后再执行首次高亮渲染,避免空数据取值报错。
  • 能够基于 Tabs 组件改造功能权限与成员列表结构,并通过 v-model 控制默认激活标签页及双向切换。

知识点 ​

先明确目标:把接口数据交给表格 ​

为什么需要:页面一打开就要展示规则列表,不能靠手写静态数据。转写稿里反复强调“先准备静态模板,再调接口把数据拎回来”,因为只有先看清结构,后面才知道数据往哪里放。

怎么想:先确认接口返回结构。转写稿调试时打开 Network,刷新后看到请求返回 data.rows,说明列表数据在 rows 里,不是直接挂在 res 上。

怎么做:本节核心代码就是围绕“获取—保存—渲染”三步:

vue
import { getRuleListAPI } from '@/apis/car'

mounted() {
  this.getRuleList()
}
  
methods: {
  // 获取规则列表
  async getRuleList() {
    const res = await getRuleListAPI(this.params)
    this.ruleList = res.data.rows
  }
}

<el-table :data="ruleList" style="width: 100%">

易错点:不要把 res.data.rows 写成 res.rows;ruleList 要先在 data 里定义成数组,否则模板绑定会报错或无法响应。

在 mounted 里发起首次获取 ​

为什么需要:列表是页面初始内容,需要在组件挂载后尽快请求。转写稿里也是“在 mounted 里先给它做一个调用”,这是最直接的初始化入口。

怎么想:mounted 时模板已经准备就绪,此时发请求,数据回来后赋值即可触发视图更新。不要把请求直接写在模板表达式里。

怎么做:在 mounted() 中调用 this.getRuleList(),方法统一放在 methods 里,便于后续点击刷新、分页时复用。

易错点:注意 mounted 拼写和 this 指向;不要在 mounted 里直接写 getRuleList() 而漏掉 this。转写稿里说的“惊得/盯一下”都是口误,实际是 mounted 和调试打印。

用 async/await 拿 rows 并保存 ​

为什么需要:接口是异步的,直接取返回值会拿到 Promise。转写稿里强调“拿到数据以后要存下来”,否则模板没有数据源。

怎么想:先 await 接口,确认 res.data.rows 是数组,再赋给 this.ruleList。如果后端返回的是树形结构,也要先看清每一层字段;转写稿里“竖形/数形/确形”都是“树形”的同音误写。

怎么做:const res = await getRuleListAPI(this.params),然后 this.ruleList = res.data.rows。调试时可在赋值后打印 this.ruleList,确认是数组且字段正确。

易错点:async 方法里忘记 await,会把 Promise 存进 ruleList;接口无参数时也不要随意编参数。转写稿明确说有些配置数据由后端写死,但依旧要通过接口拿回来。

模板绑定 el-table ​

为什么需要:数据拿到后必须绑定到表格组件,el-table 的 :data 就是数据入口。

怎么想:先让表格能渲染空数据,再让接口数据进来。转写稿里先看“没有数据”的状态,再调接口,这样能区分是请求问题还是渲染问题。

怎么做:保留 :data="ruleList",列上的 prop 要与 rows 里的字段一致。若字段名不匹配,表格列会显示空白;树形组件里则要用 props/defaultProps 指定 label 等字段。转写稿里后端字段叫 title,组件默认认 label,所以需要配置映射。

易错点:不要直接改接口返回对象;空数组时表格显示“暂无数据”是正常的。转写稿里“暂红数据/占用数字”是口误,实际是空数据占位。

调试与易错点:先跑通再定制 ​

为什么需要:转写稿的调试顺序很值得照做:先静态模板,再 Network 看请求,再打印数据,再渲染,最后加默认展开、勾选框、禁用、高亮等配置。

怎么想:遇到“有结构但没字”“能展开但空白”,先查字段名是否匹配;遇到初始化高亮无效,先确认列表和树数据是否都已返回,避免异步竞争。

怎么做:树形组件常用配置包括 show-checkbox、default-expand-all、disabled、node-key、setCheckedKeys;转写稿里的“diable”应纠正为 disabled,“note kit”是 node-key,“setter check case”是 setCheckedKeys。这些都要在数据渲染出来后使用。

易错点:布尔属性写 show-checkbox 与 :show-checkbox="true" 要区分;递归给多层树加禁用状态时要有结束条件,只对存在 children 的项继续递归。初始化若要在数据回来后模拟选中第一项,必须等接口完成,否则会读到空数组并报错。

先明确目标:先跑通一个最小导出 DEMO ​

为什么需要:实际开发里导出 Excel 常见两种方案:后端主导和前端主导。后端主导是前端调导出接口,后端返回文件流,浏览器触发下载,适合大数据量;前端主导是在浏览器里把 JSON 数据转成 Excel,适合少量数据。本节要学的是前端主导的基础实现。

怎么想:使用三方插件做需求时,不要一上来就套业务。笔记里明确给了顺序:先“不考虑具体业务,实现一个能跑起来最简单的 DEMO”,也就是先完成基础环境和基础效果;再在这个 DEMO 上按实际业务数据修改。这样能先确认插件、安装、导出链路都通。

怎么做:选用 SheetJS 社区版,按“创建工作簿 → 创建工作表 → 把工作表加入工作簿 → 调用导出方法”的流程实现。

易错点:前端主导不适合一次性导出超大数量数据;不要一开始就把业务字段、表头、排序全塞进去,否则出问题时分不清是插件链路还是业务处理出错。

基础导出流程示意图

认识工作簿、工作表和导出流程 ​

为什么需要:插件的 API 是围绕 Excel 文件结构设计的。Excel 最外层是工作簿,工作簿里可以有多个工作表,工作表里才是单元格。理解这个从属关系,才知道代码为什么要分四步。

怎么想:先把工作簿想成“父亲”,工作表是里面的一个表,最后再把数据写进工作表并导出。转写稿里反复强调:第一步创建工作簿,第二步创建工作表,第三步把工作表添加到工作簿,第四步调用插件方法导出。

怎么做:安装插件:

bash
npm i --save https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz

然后在业务方法中导入 utils 和 writeFileXLSX,按流程调用 book_new、json_to_sheet、book_append_sheet、writeFileXLSX。

易错点:步骤顺序不要乱;工作表名称是 book_append_sheet 的第三个参数,可以自定义;不要手写文件流,导出交给插件方法。

编写最小可运行导出方法 ​

为什么需要:最小 DEMO 要能证明“数据能变成 Excel 并下载”。笔记里的示例用两个对象模拟数据,先不接接口。

怎么想:json_to_sheet 要求传入对象数组;它默认会把对象字段名当成表头,所以 name、age 会先变成英文表头。若需要中文表头,就用 sheet_add_aoa 从 A1 开始覆盖表头。

怎么做:

vue
import { utils, writeFileXLSX } from 'xlsx'

exportToExcel() {
  // 创建一个新的工作簿
  const workbook = utils.book_new()
  // 创建一个工作表 要求一个对象数组格式
  const worksheet = utils.json_to_sheet(
    [
      { name: '张三', age: 18 },
      { name: '李四', age: 28 }
    ]
  )
 
  // 把工作表添加到工作簿  Data为工作表名称
  utils.book_append_sheet(workbook, worksheet, 'Data')
  // 改写表头
  utils.sheet_add_aoa(worksheet, [['姓名', '年龄']], { origin: 'A1' })
  // 导出方法进行导出
  writeFileXLSX(workbook, 'SheetJSVueAoO.xlsx')
}

易错点:sheet_add_aoa 的第二个参数是二维数组,不能丢掉外层数组;origin: 'A1' 表示从 A1 开始写;writeFileXLSX 的文件名要带 .xlsx。

调试与验证:看保存框和导出的表格内容 ​

为什么需要:导出功能不能只看代码有没有报错,要实际打开文件核对表头、数据、工作表名和文件名。

怎么想:转写稿中的验证过程是:点击“导出 Excel”后,浏览器会唤起本地保存框,允许重命名;保存后打开文件,核对表头是不是“姓名、年龄”,数据是不是张三 18、李四 28,工作表名是不是 Data,初始文件名是不是 SheetJSVueAoO.xlsx。

怎么做:点击导出按钮,保存文件并打开。若表头仍是 name、age,检查 sheet_add_aoa 是否调用、参数是否写对;若没有下载,检查 writeFileXLSX 是否导入并调用。

易错点:不要期望代码里写什么顺序,异步请求就一定按这个顺序返回;真实业务中如果数据来自接口,要等接口返回后再导出。转写稿里也提醒了异步问题:两个数据请求都完成后再执行后续高亮/导出类逻辑,否则可能拿到空数组或 undefined。

从基础 DEMO 改造成业务导出 ​

为什么需要:基础 DEMO 的数据是写死的,真实业务要从接口拿当前要导出的数据,字段顺序和表头也往往与后端返回不一致。

怎么想:先替换数据源,把写死数组换成接口返回的对象数组;再处理字段顺序,按业务要求的字段顺序重组每个对象;最后用中文表头覆盖默认英文表头。转写稿中的思路是:用映射遍历原数组,每一项返回一个新对象;遍历“正确顺序的 key 数组”,动态往新对象里塞 key: item[key],这样新对象字段顺序就按业务顺序排列;表头再用与字段顺序一一对应的中文数组传给 sheet_add_aoa。

怎么做:保持四步导出流程不变,只改 json_to_sheet 的数据、sheet_add_aoa 的表头数组和 writeFileXLSX 的文件名。转写稿调试时先直接塞后端数据,发现“能导出但顺序不对、表头是英文”,再分别调整顺序和表头,最终核对第一列是计费规则编号、第二列是计费规则名称等。

易错点:后端返回字段名不一定叫 label 这类插件默认名,导出表头要自己映射;字段顺序不对不要靠改表头硬凑,要先重组对象;前端主导导出要确认数据量,少量数据才更适合在浏览器侧处理。

先明确:这是“前端主导”的业务数据导出 ​

为什么需要:实际开发里导出 Excel 常见两种方案。后端主导是前端调“导出接口”,后端返回文件流,浏览器触发下载,适合大数据量;前端主导是前端先调列表接口拿到 JSON 数据,再在浏览器里把 JSON 转成 Excel 文件。本节代码属于后者。

怎么想:先把“拿业务数据”和“生成 Excel”拆开。拿数据仍然调用列表接口;生成 Excel 则按“工作簿 → 工作表 → 写入数据 → 导出文件”的顺序做。转写稿里也强调,先跑通一个最简 demo,再把写死数据替换成业务数据,不要一上来就追求完整业务。

怎么做:安装并引入 xlsx 相关能力,业务方法里先 await getRuleListAPI(this.params) 获取当前要导出的数据,再进入表格生成流程。

易错点:前端主导导出只适合少量数据;数据量很大时应走后端文件流方案。不要误以为调了列表接口就自动下载,真正触发下载的是后面的 writeFileXLSX。

用表头 key 数组重排后端数据 ​

为什么需要:后端返回的 res.data.rows 字段顺序不一定符合 Excel 展示顺序,而且可能包含不需要导出的字段。直接丢给 SheetJS,表头顺序和字段内容都不可控。

怎么想:准备两个顺序完全一致的数组:tableHeaderKeys 放英文字段 key,tableHeaderValues 放中文表头。然后对 rows 做 map,每一项都新建一个对象,按 key 数组顺序把值塞进去。转写稿里的调试过程是:先把 rows 直接放进去,能导出,但发现“顺序不对、表头是英文”,于是再调整。

怎么做:

javascript
// 表头英文字段key
const tableHeaderKeys = ['ruleNumber', 'ruleName', 'freeDuration', 'chargeCeiling', 'chargeType', 'ruleNameView']
// 表头中文字段value
const tableHeaderValues = ['计费规则编号', '计费规则名称', '免费时长(分钟)', '收费上线(元)', '计费方式', '计费规则']
// 以excel表格的顺序调整后端数据
const sheetData = res.data.rows.map((item) => {
  const obj = {}
  tableHeaderKeys.forEach(key => {
    obj[key] = item[key]
  })
  return obj
})

易错点:tableHeaderKeys 和 tableHeaderValues 必须一一对应,长度和顺序都不能错;map 里要返回新对象,不能只改原对象却忘记 return;字段名要和接口返回一致,例如 ruleNumber 不能写成 ruleNo。

创建工作簿、工作表并覆盖中文表头 ​

为什么需要:SheetJS 的 json_to_sheet 会把 JSON 的 key 默认当成表头,所以直接生成时表头是英文。业务表格需要中文表头。

怎么想:先由 sheetData 生成工作表,再创建工作簿,把工作表追加进去;最后用 sheet_add_aoa 从 A1 开始写入中文表头,覆盖默认英文表头。转写稿中对应的调试就是:数据顺序调整好后,再把提前准备的中文表头数组替换进去。

怎么做:

javascript
// 创建一个工作表
const worksheet = utils.json_to_sheet(sheetData)
// 创建一个新的工作簿
const workbook = utils.book_new()
// 把工作表添加到工作簿
utils.book_append_sheet(workbook, worksheet, 'Data')
// 改写表头
utils.sheet_add_aoa(worksheet, [tableHeaderValues], { origin: 'A1' })

易错点:sheet_add_aoa 的第二个参数是二维数组,所以 tableHeaderValues 外面还要包一层 [];origin: 'A1' 表示从第一行第一列开始覆盖。工作表名称 'Data' 是显示在 Excel 底部的工作表名,不是文件名。

完整导出方法与调试检查 ​

为什么需要:把前面的步骤串成一个异步方法,绑定到导出按钮上,才能完成“按当前业务数据导出”。

怎么想:先取数据,再重排,再建表,再覆盖表头,最后 writeFileXLSX 下载。调试时重点看三处:接口是否返回当前页数据、导出的列顺序是否与 tableHeaderKeys 一致、第一行是否变成中文表头。

怎么做:本节完整代码如下:

javascript
// 导出excel
async exportToExcel() {
  // 获取要导出的业务数据
  const res = await getRuleListAPI(this.params)
  // 表头英文字段key
  const tableHeaderKeys = ['ruleNumber', 'ruleName', 'freeDuration', 'chargeCeiling', 'chargeType', 'ruleNameView']
  // 表头中文字段value
  const tableHeaderValues = ['计费规则编号', '计费规则名称', '免费时长(分钟)', '收费上线(元)', '计费方式', '计费规则']
  // 以excel表格的顺序调整后端数据
  const sheetData = res.data.rows.map((item) => {
    const obj = {}
    tableHeaderKeys.forEach(key => {
      obj[key] = item[key]
    })
    return obj
  })
  // 创建一个工作表
  const worksheet = utils.json_to_sheet(sheetData)
  // 创建一个新的工作簿
  const workbook = utils.book_new()
  // 把工作表添加到工作簿
  utils.book_append_sheet(workbook, worksheet, 'Data')
  // 改写表头
  utils.sheet_add_aoa(worksheet, [tableHeaderValues], { origin: 'A1' })
  writeFileXLSX(workbook, 'SheetJSVueAoO.xlsx')
}

易错点:exportToExcel 是 async,内部用了 await,调用处不要漏掉异常处理;this.params 要确认是当前分页/筛选条件,否则导出的可能不是用户想导的那批数据;文件名 SheetJSVueAoO.xlsx 可按业务改成更语义化的名称。

先明确本节目标:弹框的显示与隐藏由数据控制 ​

为什么需要: 弹框不是靠直接操作 DOM 显示/隐藏,而是由 Vue 的响应式数据驱动。这样逻辑清晰,也方便后续在提交成功、取消、接口返回后统一控制。
怎么想: el-dialog 的 visible 决定弹框是否显示。我们准备一个布尔值 dialogVisible,把它绑定给 visible。值为 true 就显示,改为 false 就关闭。
怎么做: 模板写 :visible="dialogVisible",data 中初始化 dialogVisible: true。取消按钮通过 @click="dialogVisible = false" 修改这个布尔值。
易错点: 不要写成 visible="dialogVisible",否则传进去的是字符串,不是布尔表达式。转写稿视频3也专门提醒过:布尔属性建议加冒号,明确让 Vue 按表达式解析,不要依赖隐式转换。调试时可以先把 dialogVisible 改成 false 刷新,确认弹框不显示;再点取消,观察它是否变为 false。

取消按钮:关闭动作要直接、可追踪 ​

为什么需要: 取消是弹框最基础的交互,用户点取消后应立刻关闭,不触发表单提交。
怎么想: 当前代码没有复杂流程,取消只需要把 dialogVisible 置为 false。这属于“事件修改数据,数据驱动视图”。
怎么做: 在 template #footer 中写:

vue
<el-button size="mini" @click="dialogVisible = false">取 消</el-button>

易错点: 当前“确定”按钮没有绑定点击事件,也没有写校验/提交逻辑,它只是占位。不要误以为点确定会自动关闭。后续如果要做提交,应先在确定事件里调用 this.$refs.addForm.validate(),校验通过后再提交并关闭。转写稿视频7强调过初始化时要先保证前置数据就绪;如果以后弹框打开依赖接口数据,也要等数据回来再打开,避免表单或列表为空。

表单与弹框配合:数据对象和校验规则要提前准备 ​

为什么需要: 弹框里是新增规则表单,打开弹框时表单要能立即渲染,所以 addForm、addFormRules 必须先在 data 中准备好。
怎么想: el-form 通过 :model="addForm" 绑定数据对象,通过 :rules="addFormRules" 绑定校验规则,通过 ref="addForm" 方便后续取表单实例。
怎么做: 保留笔记中的表单结构和 data 配置。注意 chargeType 默认是 'duration',所以单选组初始会选中“时长收费”。
易错点: 取消关闭后,表单数据不会自动清空;如果业务要求每次新增都重置,需要在打开或关闭时处理。当前代码未写重置逻辑,不能想当然认为它会清空。另外 dialogVisible 不要拼错,布尔值也不要写成字符串 "false"。

本节代码保留 ​

vue
<!-- 弹框 -->
<el-dialog :visible="dialogVisible" width="680px" title="新增规则">
  <!-- 表单接口 -->
  <div class="form-container">
    <el-form ref="addForm" :model="addForm" :rules="addFormRules" label-position="top">
      <el-form-item label="计费规则编号" prop="ruleNumber">
        <el-input v-model="addForm.ruleNumber" />
      </el-form-item>
      <el-form-item label="计费规则名称" prop="ruleName">
        <el-input v-model="addForm.ruleName" />
      </el-form-item>
      <el-form-item label="计费方式" prop="chargeType">
        <el-radio-group v-model="addForm.chargeType" size="small">
          <el-radio label="duration" border>时长收费</el-radio>
          <el-radio label="turn" border>按次收费</el-radio>
          <el-radio label="partition" border>分段收费</el-radio>
        </el-radio-group>
      </el-form-item>
      <div class="flex-container">
        <el-form-item label="免费时长">
          <el-input v-model="addForm.freeDuration" />
        </el-form-item>
        <el-form-item label="收费上限">
          <el-input v-model="addForm.chargeCeiling" />
        </el-form-item>
      </div>
      <el-form-item label="计费规则">
        <!-- 按时长收费区域 -->
        <div class="duration">
          每 <el-input v-model="addForm.durationTime" class="input-box" /> 小时
          <el-input v-model="addForm.durationPrice" class="input-box" /> 元
        </div>
        <!-- 按次收费区域 -->
        <div class="turn">
          每次 <el-input v-model="addForm.turnPrice" class="input-box" /> 元
        </div>
        <!-- 按分段收费区域 -->
        <div class="partition">
          <div class="item">
            <el-input v-model="addForm.partitionFrameTime" class="input-box" />小时内,每小时收费
            <el-input v-model="addForm.partitionFramePrice" class="input-box" /> 元
          </div>
          <div class="item">
            每增加<el-input v-model="addForm.partitionIncreaseTime" class="input-box" />小时,增加
            <el-input v-model="addForm.partitionIncreasePrice" class="input-box" /> 元
          </div>
        </div>
      </el-form-item>
    </el-form>
  </div>
  <template #footer>
    <el-button size="mini" @click="dialogVisible = false">取 消</el-button>
    <el-button size="mini" type="primary">确 定</el-button>
  </template>
</el-dialog>
js
data() {
  return {
    dialogVisible: true, // 控制弹框关闭打开
    addForm: {
      ruleNumber: '', // 计费规则编号
      ruleName: '', // 计费规则名称
      chargeType: 'duration', // 计费规则类型 duration / turn / partition
      chargeCeiling: null,
      freeDuration: null,
      // 时长计费独有字段
      durationTime: null, // 时长计费单位时间
      durationPrice: null, // 时长计费单位价格
      durationType: 'hour',
      // 按次收费独有字段
      turnPrice: null,
      // 分段计费独有字段
      partitionFrameTime: null, // 段内时间
      partitionFramePrice: null, // 段内费用
      partitionIncreaseTime: null, // 超出时间
      partitionIncreasePrice: null // 超出费为收费价钱
    }, // 计费规则表单对象
    addFormRules: {
      ruleNumber: [
        {
          required: true,
          message: '请输入规则编号',
          trigger: 'blur'
        }
      ],
      ruleName: [
        {
          required: true,
          message: '请输入规则名称',
          trigger: 'blur'
        }
      ],
      chargeType: [
        {
          required: true,
          message: '请选择收费类型',
          trigger: 'blur'
        }
      ]
    } // 计费规则校验规则对象
  }
}

代码示例 ​

示例一:SheetJS 基础导出(最小可跑 DEMO) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>SheetJS 基础导出 DEMO</title>
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  <!-- 0.20.0 官方 CDN,与 npm i https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz 同版本 -->
  <script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
</head>
<body>
<div id="app">
  <el-button type="primary" size="mini" @click="exportToExcel">导出 Excel</el-button>
</div>

<script>
  new Vue({
    el: '#app',
    methods: {
      exportToExcel() {
        // 1. 创建一个新的工作簿
        const workbook = XLSX.utils.book_new()

        // 2. 创建一个工作表,要求一个对象数组格式
        const worksheet = XLSX.utils.json_to_sheet([
          { name: '张三', age: 18 },
          { name: '李四', age: 28 }
        ])

        // 3. 把工作表添加到工作簿,'Data' 为工作表名称(对应底部 sheet 标签)
        XLSX.utils.book_append_sheet(workbook, worksheet, 'Data')

        // 4. 改写表头:从 A1 开始覆盖成中文
        XLSX.utils.sheet_add_aoa(worksheet, [['姓名', '年龄']], { origin: 'A1' })

        // 5. 调用插件方法导出
        XLSX.writeFileXLSX(workbook, 'SheetJSVueAoO.xlsx')
      }
    }
  })
</script>
</body>
</html>

要点解释:

  • 导出流程是固定的五步:建工作簿 → 建工作表 → 表加到簿 → 改表头 → writeFileXLSX 导出,顺序不能乱,book_append_sheet 之后 workbook 里才有内容。
  • 项目里用 ESM 写法 import { utils, writeFileXLSX } from 'xlsx',调的是 utils.book_new() / writeFileXLSX(...);CDN 全局变量写法是 XLSX.utils.book_new() / XLSX.writeFileXLSX(...),二者一一对应,别混着写。
  • json_to_sheet 只认对象数组,对象的 key 就是默认表头;本例默认表头是 name/age,所以必须用 sheet_add_aoa 覆盖成中文。
  • sheet_add_aoa 第二个参数外层必须是数组([['姓名','年龄']]),把中括号去掉会直接报错。
  • 易错点:sheet_add_aoa 只是覆盖第一行,不会删掉原来的英文表头行——它写在 A1 正好盖住,所以 origin 千万别写错。
  • 导出前浏览器会弹下载框,文件名由第二个参数决定,可随时修改。

示例二:按业务数据导出(调顺序 + 中文表头) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>按业务数据导出</title>
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  <script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
</head>
<body>
<div id="app">
  <el-button type="primary" size="mini" @click="exportToExcel">导出 Excel</el-button>
</div>

<script>
  // 模拟接口:真实项目里换成 import { getRuleListAPI } from '@/apis/car'
  const getRuleListAPI = (params) => Promise.resolve({
    data: {
      rows: [
        { id: 1, ruleNameView: '每1小时5元', chargeType: 'duration', chargeCeiling: 50, freeDuration: 15, ruleName: '时长规则', ruleNumber: 'R001' },
        { id: 2, ruleNameView: '每次10元', chargeType: 'turn', chargeCeiling: 100, freeDuration: 0, ruleName: '按次规则', ruleNumber: 'R002' }
      ]
    }
  })

  new Vue({
    el: '#app',
    data() {
      return { params: { page: 1, pageSize: 10 } }
    },
    methods: {
      // 导出 excel
      async exportToExcel() {
        // 1. 获取要导出的业务数据
        const res = await getRuleListAPI(this.params)

        // 2. 表头英文字段 key —— 数组顺序即导出列顺序
        const tableHeaderKeys = ['ruleNumber', 'ruleName', 'freeDuration', 'chargeCeiling', 'chargeType', 'ruleNameView']
        // 3. 表头中文字段 value —— 与上面一一对应
        const tableHeaderValues = ['计费规则编号', '计费规则名称', '免费时长(分钟)', '收费上线(元)', '计费方式', '计费规则']

        // 4. 以 excel 表格的顺序调整后端数据(映射出新的对象数组)
        const sheetData = res.data.rows.map((item) => {
          const obj = {}
          tableHeaderKeys.forEach(key => {
            obj[key] = item[key]
          })
          return obj
        })

        // 5. 创建工作表 → 创建工作簿 → 表加入簿
        const worksheet = XLSX.utils.json_to_sheet(sheetData)
        const workbook = XLSX.utils.book_new()
        XLSX.utils.book_append_sheet(workbook, worksheet, 'Data')

        // 6. 改写表头
        XLSX.utils.sheet_add_aoa(worksheet, [tableHeaderValues], { origin: 'A1' })

        XLSX.writeFileXLSX(workbook, 'SheetJSVueAoO.xlsx')
      }
    }
  })
</script>
</body>
</html>

要点解释:

  • 直接传 res.data.rows 也能导出,但列顺序是后端返回对象的 key 顺序(本例 id、ruleNameView 会排到前面),所以必须做第 4 步的顺序调整。
  • 调整顺序的核心是 map + 内层 forEach(tableHeaderKeys):外层 map 保证条数不变,内层按有序的 key 数组动态往新对象里塞属性,对象的 key 就有了顺序。
  • 取值一定写成 obj[key] = item[key],不要写 obj[key] = key(那样每格都是字段名)。
  • 如果后端对象里有 id 等不需要的字段,这套写法天然就过滤掉了,不用额外 delete。
  • 易错点:tableHeaderKeys 与 tableHeaderValues 必须严格一一对应、长度相同,否则中文表头会错位。
  • 易错点:sheet_add_aoa 的第二个参数不能漏掉外层数组,[tableHeaderValues] 不能写成 tableHeaderValues。
  • await 拿到的数据只用于导出,不影响页面表格;如果想让导出数据和当前表格完全一致,注意把 this.params 里的分页参数一起传进去。

示例三:导出入口所在的弹框与表单(打开/关闭控制) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>新增规则弹框</title>
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
  <el-button type="primary" size="mini" @click="dialogVisible = true">新增规则</el-button>

  <!-- 弹框 -->
  <el-dialog :visible.sync="dialogVisible" width="680px" title="新增规则">
    <div class="form-container">
      <el-form ref="addForm" :model="addForm" :rules="addFormRules" label-position="top">
        <el-form-item label="计费规则编号" prop="ruleNumber">
          <el-input v-model="addForm.ruleNumber" />
        </el-form-item>
        <el-form-item label="计费规则名称" prop="ruleName">
          <el-input v-model="addForm.ruleName" />
        </el-form-item>
        <el-form-item label="计费方式" prop="chargeType">
          <el-radio-group v-model="addForm.chargeType" size="small">
            <el-radio label="duration" border>时长收费</el-radio>
            <el-radio label="turn" border>按次收费</el-radio>
            <el-radio label="partition" border>分段收费</el-radio>
          </el-radio-group>
        </el-form-item>
        <div class="flex-container">
          <el-form-item label="免费时长">
            <el-input v-model="addForm.freeDuration" />
          </el-form-item>
          <el-form-item label="收费上限">
            <el-input v-model="addForm.chargeCeiling" />
          </el-form-item>
        </div>
        <el-form-item label="计费规则">
          <!-- 按时长收费区域 -->
          <div v-if="addForm.chargeType === 'duration'" class="duration">
            每 <el-input v-model="addForm.durationTime" class="input-box" /> 小时
            <el-input v-model="addForm.durationPrice" class="input-box" /> 元
          </div>
          <!-- 按次收费区域 -->
          <div v-if="addForm.chargeType === 'turn'" class="turn">
            每次 <el-input v-model="addForm.turnPrice" class="input-box" /> 元
          </div>
          <!-- 按分段收费区域 -->
          <div v-if="addForm.chargeType === 'partition'" class="partition">
            <div class="item">
              <el-input v-model="addForm.partitionFrameTime" class="input-box" />小时内,每小时收费
              <el-input v-model="addForm.partitionFramePrice" class="input-box" /> 元
            </div>
            <div class="item">
              每增加<el-input v-model="addForm.partitionIncreaseTime" class="input-box" />小时,增加
              <el-input v-model="addForm.partitionIncreasePrice" class="input-box" /> 元
            </div>
          </div>
        </el-form-item>
      </el-form>
    </div>
    <template #footer>
      <el-button size="mini" @click="dialogVisible = false">取 消</el-button>
      <el-button size="mini" type="primary">确 定</el-button>
    </template>
  </el-dialog>
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        dialogVisible: false, // 控制弹框关闭打开
        addForm: {
          ruleNumber: '', // 计费规则编号
          ruleName: '', // 计费规则名称
          chargeType: 'duration', // 计费规则类型 duration / turn / partition
          chargeCeiling: null,
          freeDuration: null,
          // 时长计费独有字段
          durationTime: null, // 时长计费单位时间
          durationPrice: null, // 时长计费单位价格
          durationType: 'hour',
          // 按次收费独有字段
          turnPrice: null,
          // 分段计费独有字段
          partitionFrameTime: null, // 段内时间
          partitionFramePrice: null, // 段内费用
          partitionIncreaseTime: null, // 超出时间
          partitionIncreasePrice: null // 超出费用收费价钱
        }, // 计费规则表单对象
        addFormRules: {
          ruleNumber: [{ required: true, message: '请输入规则编号', trigger: 'blur' }],
          ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
          chargeType: [{ required: true, message: '请选择收费类型', trigger: 'blur' }]
        } // 计费规则校验规则对象
      }
    }
  })
</script>
</body>
</html>

要点解释:

  • 笔记里的 :visible="dialogVisible" 只能单向受控,点右上角 × 关不掉;这里补成 :visible.sync 才是双向绑定(对应 Element UI 2.x 的写法),底部「取 消」按钮里的 dialogVisible = false 也必须配合它才生效。
  • el-form-item 上的 prop 必须和 addFormRules 的 key、v-model 的字段名三者完全一致,否则校验不触发。
  • 三种计费方式共用同一份 addForm,靠 chargeType 的 v-if 切换显示区域;切换时不会清空其他分支的字段,提交前需要自己判断只取当前类型对应的字段。
  • 易错点:el-input 绑定的是 null 时显示为空,但提交前要记得做数字转换,否则后端收到的是字符串。
  • 笔记中「确 定」按钮没有绑定事件,实际开发需补 @click="handleSubmit",在 handleSubmit 里先 this.$refs.addForm.validate(valid => { ... }),校验通过再调新增接口并关闭弹框。

常见错误 ​

  • 现象:el-tree 有节点但文字不显示;原因:后端字段是 title,组件默认识别 label;修法:给 el-tree 配 :props="{ label: 'title', children: 'children' }",并核对 children 字段名。
  • 现象:树节点没有复选框或默认不展开;原因:show-checkbox、default-expand-all 写成字符串或错误绑定;修法:布尔属性统一用绑定写法,如 :show-checkbox="true"、:default-expand-all="true",或确认裸写属性的语义。
  • 现象:只有第一层节点被禁用,子节点仍可选;原因:递归只处理了顶层,未对 children 继续调用;修法:递归函数内先处理当前节点,再判断 item.children 并递归,必须有结束条件。
  • 现象:调用 setCheckedKeys 不生效或报缺少 node-key;原因:el-tree 未配置唯一标识字段;修法:加 node-key="id",并确保后端返回的 id 唯一;v-for 中的 tree ref 是数组时要遍历后逐个调用。
  • 现象:页面初始化时高亮报 undefined 或读不到 roleId;原因:mounted 中立即模拟点击,但角色列表和树数据接口尚未返回;修法:先 await 两个数据请求,再调用切换/回填方法,必要时等树渲染完成后再操作。
  • 现象:导出 Excel 表头是英文、列顺序与页面不一致;原因:直接把 res.data.rows 传给 json_to_sheet;修法:先按业务表头 keys 映射成有序对象数组,再用 sheet_add_aoa 改写中文表头。
  • 现象:导出数据为空或只有表头;原因:取错响应层级,如把 res.data.rows 写成 res.data,或导出参数与列表参数不一致;修法:打印响应确认 rows,导出前重新 await 获取数据,并核对分页/筛选参数。
  • 现象:el-dialog 关不掉或打开状态不同步;原因:直接改 prop 或 visible 绑定方式不对;修法:Vue 2 + Element UI 使用 :visible.sync="dialogVisible",关闭时改本地 dialogVisible。
  • 现象:切换计费方式后提交了无关字段;原因:表单对象保留所有类型字段,提交时未按 chargeType 组装;修法:提交前根据当前类型组装 payload,清理无关字段。
  • 现象:el-tabs 默认不激活或切换后内容不匹配;原因:v-model 的值与 el-tab-pane 的 name 不一致;修法:让 activeName 初始值与第一个 name 完全相同,并检查点击后双向绑定值变化。

练习 ​

  1. 给权限树配置 props,使后端 title 显示为节点文字、children 正常嵌套;同时开启复选框和默认全部展开,并在控制台检查每层字段是否被正确识别。
  2. 写一个递归函数,给权限树所有层级节点添加 disabled: true;要求打印每一层处理结果,并验证叶子节点和中间节点都不可选。
  3. 实现角色切换高亮:点击角色拿到 roleId,请求对应权限列表;遍历 v-for 生成的 tree 实例,调用 setCheckedKeys 回填。补上 node-key,并处理页面首次加载时的默认高亮。
  4. 完成计费规则导出:先直接导出 rows 观察问题,再按指定表头 keys 重排数据、替换中文表头,最后导出 xlsx;对比两次导出结果的列顺序和表头。
  5. 改造 tabs:建立“功能权限 / 成员列表”两个标签页,默认激活“功能权限”,把权限树放入第一个标签页,成员列表先放占位内容;验证 activeName 与 name 的对应关系。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1角色管理-基础树形结构渲染14:22
2day06-角色管理-05数据导出18:18
3角色管理-tree默认展开和可选配置03:56
4计费规则导出-按照实际业务数据导出16:12
5角色管理-通过递归函数处理禁用状态09:44
6角色管理-点击切换时高亮已选的权限点16:59
7角色管理-初始化的时候完成首次渲染06:31
8角色管理-成员列表-tabs组件改造05:48

小结 ​

  1. 树组件能否正常显示,优先查数据字段与 props 映射,不要先怀疑接口没数据。
  2. 布尔型组件属性统一用绑定写法,避免字符串、隐式转换和“看起来写了但没生效”的问题。
  3. 处理树形数据必须同时考虑“当前节点处理、子级递归、结束条件”,否则容易只处理一层或递归失控。
  4. 通过 ref 调第三方组件方法前,先确认 ref 是单个实例还是 v-for 下的实例数组,并确保组件已渲染。
  5. 初始化回填属于异步时序问题,必须等依赖数据请求完成后再操作组件,不能依赖代码书写顺序。
  6. 导出功能先跑通最小 demo,再做业务映射:取数、按表头重排、改中文表头、写文件,逐步替换。
  7. 弹框、表单、tabs 的状态都由本地响应式变量驱动;提交前按业务类型组装 payload,避免脏字段。
  8. 第三方组件报错先查文档的必填配置和方法要求,例如 node-key、props、setCheckedKeys 的使用前提。