Skip to content

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

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

学习目标 ​

  • 能根据当前角色 ID 调用成员列表接口,将返回数据渲染到表格,并在切换角色时更新成员列表。
  • 能新增“添加角色”路由并绑定按钮点击跳转,准确识别步骤条、三块表单内容区及操作按钮区域的结构。
  • 能使用当前步骤状态驱动步骤条激活项、内容区显示和操作按钮显隐,并说明该场景使用 v-show 保留表单状态的原因。
  • 能通过上一步/下一步事件修改步骤状态,将步骤限制在 1–3,并按状态控制“上一步”“下一步”“确认添加”的显示。
  • 能在角色信息步骤点击下一步时执行表单校验,校验通过才进入下一状态,校验失败保持当前状态。
  • 能封装 Button 组件:使用插槽自定义文案、通过 size 参数适配样式类、在组件内部触发 click 自定义事件,并通过插件 install 与 app.use 实现全局注册使用。

知识点 ​

一、先明确:弹框开关就是一个布尔状态 ​

为什么需要:弹框的打开、关闭不能靠手动改 DOM,否则状态和视图容易脱节。本节用 dialogVisible 表示弹框是否显示,开关逻辑集中、可预测。

怎么想:把弹框看成“由数据驱动的视图”。dialogVisible 为 true 时显示,为 false 时关闭;所有打开/关闭动作都只改这个布尔值。转写稿里反复强调“数据驱动视图”,步骤条是用 currentActive 控制激活项,弹框同理,只是状态换成了 dialogVisible。

怎么做:在 data() 的 return 中声明 dialogVisible: true,表示初始打开;模板中给 el-dialog 绑定 :visible="dialogVisible"。

易错点:dialogVisible 必须写在 data 的 return 里,否则模板访问不到;不要写成字符串 'false',字符串在条件判断中是真值;visible 前不要漏冒号,否则传过去的是字符串 "dialogVisible",不是布尔状态。

二、模板绑定::visible 接收的是开关值 ​

为什么需要:el-dialog 是组件,不是普通 div。它通过 visible 属性决定显示隐藏,因此要把组件的属性接到本组件的状态上。

怎么想:本节笔记写的是 :visible="dialogVisible",属于单向绑定:数据变化会驱动弹框显示隐藏。关闭时不是直接操作弹框 DOM,而是把 dialogVisible 改成 false。

怎么做:保留笔记中的写法:

vue
<el-dialog :visible="dialogVisible" width="680px" title="新增规则">

取消按钮处写 @click="dialogVisible = false",点击后状态变 false,弹框关闭。

易错点:不要把 :visible 和 v-if/v-show 混为一谈。转写稿中“we if/we show”按 Vue 写法应为 v-if/v-show,它们控制普通元素的创建移除或 CSS 显隐;这里的 visible 是 el-dialog 组件自己的属性。弹框内部是否保留表单状态,取决于组件行为,不能简单等同于自己写 v-show。

三、取消关闭与确定按钮的边界 ​

为什么需要:弹框里通常有“取消”和“确定”两个动作。取消只关闭,确定往往要校验、提交,再决定是否关闭。两者不能混在一起。

怎么想:本节代码只实现了取消关闭:@click="dialogVisible = false"。确定按钮目前只有 type="primary",没有绑定点击事件,所以点击不会提交,也不会关闭。这是笔记当前状态,不能误以为已经完成。

怎么做:取消按钮保持直接赋 false;确定按钮后续如需校验,应围绕 addForm、addFormRules 和 ref="addForm" 补逻辑,但本节不展开。打开入口则是在列表页/父级按钮上把 dialogVisible 置为 true,笔记未给出该按钮,实际项目按同一状态思路补齐。

易错点:确定按钮没写 @click 时点击无反应;关闭弹框只改状态,不要手动操作 DOM;如果以后把弹框拆成子组件,子组件不能直接改父组件传入的 prop,应改用事件通知父组件改状态,避免 prop 被直接修改的警告。

四、调试过程与易错点归纳 ​

转写稿在调步骤条时,先手动把激活状态从 1 改到 2、3,观察视图是否跟着变,再绑定按钮事件。弹框也可以这样调:先手动把 dialogVisible 改成 false,看弹框是否关闭;再改回 true,看是否打开;最后点“取消”验证事件是否把状态改掉。这样能把“状态”“绑定”“事件”三段分开排查。

常见错误:忘记声明 dialogVisible;visible 漏冒号;把 false 写成字符串;取消按钮写成 dialogVisible = 'false';确定按钮没写逻辑却以为能提交;表单 ref、model、rules 名称不一致。转写稿中“刀包隐秘”按 Vue 写法应为 $emit,虽然本节弹框未用到,但拆组件时要记住自定义事件要通过 $emit 触发。

五、完整代码(保留笔记原代码) ​

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>

            
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'
        }
      ]
    } // 计费规则校验规则对象
  }
}

本节说明:转写稿与笔记主题不匹配 ​

本节笔记的主题是“根据计费方式适配计费规则”,核心代码只涉及 chargeType 与三类计费规则表单的切换。你提供的转写稿实际讨论的是角色管理、新增角色步骤条、按钮组件封装等内容,与本节计费规则没有直接对应关系。为避免编造,我不能把角色管理、步骤条、v-show 等推导硬套到本节;以下教程仅依据笔记中的模板代码扩写,不虚构接口、校验规则和调试过程。笔记中未提供图片,因此无图片可保留。

需求拆解:为什么需要按计费方式切换规则 ​

为什么需要:同一个“计费规则”表单项下,可能支持按时长、按次、按分段三种计费方式。不同方式需要的输入字段不同,如果全部平铺展示,用户会看到无关输入框,既增加理解成本,也容易填错。
怎么想:先由 addForm.chargeType 表示当前选择的计费方式,再用条件渲染只展示对应规则区域。这样数据结构不变,视图随计费方式变化。
怎么做:在 el-form-item label="计费规则" 内部放置三个区域,分别用 v-if 判断 chargeType 是否为 duration、turn、partition。
易错点:不要把所有区域同时显示;chargeType 的取值必须与模板判断值完全一致,注意大小写和拼写。切换计费方式后,未展示字段是否清空要按业务要求处理,不能想当然。

html
<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>

条件渲染:用 chargeType 控制三个区域 ​

为什么需要:三种计费方式互斥,同一时间只应出现一种规则。用 v-if 可以让不匹配的 DOM 不渲染,视图更干净。
怎么想:把 chargeType 当作唯一开关。duration 对应时长区域,turn 对应按次区域,partition 对应分段区域。三个判断彼此独立,但因为 chargeType 同一时刻只有一个值,所以只会命中一个区域。
怎么做:每个区域外层都写 v-if,条件直接比较 addForm.chargeType。例如时长区域写 v-if="addForm.chargeType === 'duration'"。
易错点:不要写成赋值 =;不要漏掉引号;如果后续新增计费方式,要同步增加分支和字段初始化。这里使用 v-if 是笔记原写法,不要擅自改成 v-show 后声称是本节要求。

字段绑定:三类规则各自对应表单字段 ​

为什么需要:不同计费方式采集的数据不同,必须把输入框绑定到 addForm 中对应字段,提交时才能拿到完整数据。
怎么想:按时长需要“多少小时、每小时多少钱”,因此绑定 durationTime、durationPrice;按次只需要每次价格,绑定 turnPrice;按分段需要首段小时数、首段每小时价格、每增加多少小时、增加多少钱,因此绑定四个字段。
怎么做:统一用 v-model 绑定到 addForm.xxx,输入框加 class="input-box" 便于样式控制。
易错点:字段名要和 addForm 初始化字段一致;数字输入建议确认是否需要 el-input-number 或校验,但笔记未给出,不能擅自编造。还要注意“小时内、每小时收费、每增加小时”这些文案与字段顺序要对应,避免用户误解。

结构组织与后续校验:把规则区域收在表单项内 ​

为什么需要:计费规则属于表单的一部分,放在同一个 el-form-item 内,标签、布局和后续校验都更集中。
怎么想:外层只保留一个“计费规则”标签,内部再按方式分块。这样不会出现三个重复标签,也方便统一控制样式。
怎么做:保留 el-form-item 包裹,内部用 div.duration、div.turn、div.partition 区分区域;分段区域再用 div.item 分行。
易错点:如果后续要加必填校验,需要按当前 chargeType 动态校验对应字段,不能把未展示字段也一律要求必填;切换类型时要注意旧值残留。调试时可以先在页面临时输出 addForm.chargeType,确认分支是否命中,再检查字段绑定是否写错。

确认提交的整体目标 ​

为什么需要:新增规则弹窗里,用户点“确认”不能直接把数据发出去。要先保证表单填写合法,再调用创建规则接口;接口成功后还要让列表看到新数据,并把弹窗关掉。否则会出现空表单提交、提交后列表不更新、弹窗一直留着等问题。

怎么想:把这件事拆成两层:一层是“创建规则接口”的封装,另一层是“确认提交”的业务流程。接口层只负责发请求,业务层负责校验、提交、刷新、关闭。转写稿视频4里做“下一步”时也是这个思路:先判断当前处在角色信息状态,再做表单校验,校验通过才执行进入下一个状态的逻辑;本节 confirmAdd 同样是“先校验,valid 为真才提交”。

怎么做:先保留笔记中的接口封装:

javascript
/**
 * 创建规则
 * @returns
 */
export function createRuleAPI(data) {
  return request({
    url: '/parking/rule',
    method: 'POST',
    data
  })
}

易错点:method 是 POST;提交数据放在 data,不是 params;url 不要多写或少写斜杠。转写稿里有些口音词如“肉ID”实际应写 roleId,“微刚show”实际是 v-show,写代码时要以正确 API 名为准。

在 confirmAdd 中先校验再提交 ​

为什么需要:如果用户没填必填项就点确认,直接发请求会把无效数据交给后端。表单校验的作用就是先把不合格的提交拦下来。

怎么想:笔记代码用 this.$refs.addForm.validate 拿到表单实例的校验能力。回调参数 valid 为 true 才进入提交分支;为 false 就停在当前弹窗。这个推导和转写稿视频4一致:不输入内容点“下一步”会卡住,输入内容后再点才能通过。

怎么做:保留笔记中的确认逻辑:

vue
confirmAdd() {
  this.$refs.addForm.validate(async valid => {
    if (valid) {
      await createRuleAPI(this.addForm)
      this.getRuleList()
      this.dialogVisible = false
    }
  })
}

这里要注意:this.$refs.addForm 是表单组件实例,用来调用 validate;真正传给接口的是表单数据 this.addForm,不要把 $refs.addForm 直接传给 createRuleAPI。

易错点:validate 的回调要写成 async,因为里面用了 await;valid 为假时不能继续执行;this 指向要正确,方法不要写成普通函数导致 this 丢失;接口参数名要和表单 model 对应。转写稿视频7提到“组件标签上的 @click 默认是自定义事件,普通元素上的 click 才是原生事件”,本节按钮触发的是组件方法,调试时先确认点击确实进入了 confirmAdd,再看校验分支有没有进。

提交成功后的刷新与关闭 ​

为什么需要:接口成功只代表后端保存了数据,页面上的表格不会自动变化;弹窗也不会自动消失。所以还要刷新规则列表并关闭弹窗。

怎么想:按笔记代码,await createRuleAPI(this.addForm) 完成后再执行 this.getRuleList() 和 this.dialogVisible = false。顺序很重要:先等提交完成,再刷新和关闭。转写稿视频1调成员列表接口时也是先拿到返回值,再给列表数据赋值、渲染表格;这里同理,提交成功后要让列表重新取数。

怎么做:保持笔记中的两行收尾:

javascript
this.getRuleList()
this.dialogVisible = false

易错点:如果 createRuleAPI 前不加 await,可能在请求还没完成时就刷新列表或关闭弹窗;getRuleList 的方法名要和组件里实际定义一致;dialogVisible 要和弹窗绑定的变量一致。笔记没有写 try/catch 和失败提示,实际项目可按团队规范补充,但不要擅自改动本节已给的主流程。

调试与验证过程 ​

为什么需要:确认提交涉及校验、接口、列表、弹窗四个点,必须逐段验证,不能只看代码“像是对的”。

怎么想:先验证校验拦截,再验证接口请求,最后验证页面收尾。转写稿视频4的调试方式是:不输入内容点下一步,发现“卡住了,没有跳过去”;输入内容后再点,就能过去。迁移到本节:打开新增弹窗,不填必填项点确认,不应该发创建请求;填完后点确认,才应该进入提交分支。

怎么做:打开浏览器 Network 面板,点确认后看是否发出 POST /parking/rule,请求体是不是 this.addForm;请求成功后看规则列表有没有重新请求、弹窗有没有关闭。转写稿视频1里也强调过看接口调用和返回数据,再赋值渲染;本节就是看创建接口返回后,getRuleList() 是否执行、dialogVisible 是否变为 false。

易错点:只看弹窗关闭不代表提交成功,可能是没走 valid 分支;只看接口 200 也不代表列表已刷新。调试时按“点击 → 校验 → 请求 → 刷新 → 关闭”的顺序排查,哪一步没发生就查对应变量和方法名。

代码示例 ​

示例 1:控制弹框打开关闭 + 表单结构与校验(完整可运行) ​

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">
  <style>
    #app { padding: 24px; }
    .form-container { padding: 0 12px; }
    .flex-container { display: flex; gap: 24px; }
    .input-box { width: 90px; margin: 0 6px; }
  </style>
</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-input>
          </el-form-item>
          <el-form-item label="计费规则名称" prop="ruleName">
            <el-input v-model="addForm.ruleName"></el-input>
          </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-input>
            </el-form-item>
            <el-form-item label="收费上限">
              <el-input v-model="addForm.chargeCeiling"></el-input>
            </el-form-item>
          </div>
        </el-form>
      </div>
      <template #footer>
        <el-button size="mini" @click="dialogVisible = false">取 消</el-button>
        <el-button size="mini" type="primary" @click="confirmAdd">确 定</el-button>
      </template>
    </el-dialog>
  </div>

  <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  <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' }
            ]
          }
        }
      },
      methods: {
        confirmAdd() {
          this.$refs.addForm.validate(valid => {
            console.log('表单校验结果:', valid)
          })
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • :visible.sync="dialogVisible":笔记原文写的是 :visible,在 Vue2 + Element UI 中必须加 .sync(或自己监听 @update:visible),否则点弹框右上角的 × 关不掉——因为子组件只改自己的内部状态,改不动父组件的数据。
  • ref="addForm" + :model="addForm" + :rules="addFormRules" + el-form-item 上的 prop:四件套缺一不可,prop 的名字必须与 addForm 中的字段名、rules 中的键名三者一致,否则校验不生效、小红星也不显示。
  • data() 必须返回对象(组件写法),根实例写成对象也可以,但保持函数写法更统一。
  • 笔记里 dialogVisible: true 是为了直接看到弹框,实际项目初始值应为 false,由按钮 @click="dialogVisible = true" 打开。
  • 浏览器内联模板里 <el-input /> 自闭合会被 HTML 解析器吞掉后面的内容,必须写成 <el-input></el-input>;只有在 .vue 单文件组件里才能自闭合。
  • chargeType 的校验 trigger: 'blur' 其实对单选组不太灵,radio 不触发 blur,实际要改成 trigger: 'change'。

示例 2:根据计费方式适配计费规则(完整可运行) ​

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">
  <style>
    #app { padding: 24px; }
    .form-container { padding: 0 12px; }
    .flex-container { display: flex; gap: 24px; }
    .input-box { width: 90px; margin: 0 6px; }
  </style>
</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-input>
          </el-form-item>
          <el-form-item label="计费规则名称" prop="ruleName">
            <el-input v-model="addForm.ruleName"></el-input>
          </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-input>
            </el-form-item>
            <el-form-item label="收费上限">
              <el-input v-model="addForm.chargeCeiling"></el-input>
            </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> 小时
              <el-input v-model="addForm.durationPrice" class="input-box"></el-input> 元
            </div>
            <!-- 按次收费区域 -->
            <div v-if="addForm.chargeType === 'turn'" class="turn">
              每次 <el-input v-model="addForm.turnPrice" class="input-box"></el-input> 元
            </div>
            <!-- 按分段收费区域 -->
            <div v-if="addForm.chargeType === 'partition'" class="partition">
              <div class="item">
                <el-input v-model="addForm.partitionFrameTime" class="input-box"></el-input>小时内,每小时收费
                <el-input v-model="addForm.partitionFramePrice" class="input-box"></el-input> 元
              </div>
              <div class="item">
                每增加<el-input v-model="addForm.partitionIncreaseTime" class="input-box"></el-input>小时,增加
                <el-input v-model="addForm.partitionIncreasePrice" class="input-box"></el-input> 元
              </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" @click="confirmAdd">确 定</el-button>
      </template>
    </el-dialog>
  </div>

  <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  <script>
    new Vue({
      el: '#app',
      data() {
        return {
          dialogVisible: false,
          addForm: {
            ruleNumber: '',
            ruleName: '',
            chargeType: 'duration',
            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' }]
          }
        }
      },
      methods: {
        confirmAdd() {
          this.$refs.addForm.validate(valid => {
            console.log('表单校验结果:', valid)
          })
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • 三块区域用的是 三个独立的 v-if(笔记原文即如此),不是 v-if / v-else-if。因为 chargeType 同一时刻只有一个值,三块天然互斥,用三个 v-if 也能正确显示,不必强行改成 v-else-if。
  • 绑定条件统一是 addForm.chargeType === 'duration' / 'turn' / 'partition',必须与 el-radio 的 label 值严格一致(区分大小写、不能有多余空格),否则切到某个类型时下方一片空白。
  • 三种计费方式的字段全部定义在同一个 addForm 里,切换类型时数据不会被销毁,切回来值还在——这正是把字段放在数据层而不是子组件里的好处。
  • v-if 移除的是真实 DOM:如果你希望切回来还能保留输入,靠的是 addForm 里的数据,而不是 DOM 状态;若把输入框自身状态写死在 DOM 上(如 value 写死),切回来就丢了。
  • 这几个嵌套的 el-input 没有配 prop,所以不参与表单校验;若后续要给"计费规则"做必填校验,需要给外层 el-form-item 加 prop 并自定义校验规则。
  • class="input-box" 统一控制小输入框宽度,保证三段文字排版不错乱。

示例 3:确认提交(完整可运行,含接口函数) ​

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">
  <style>
    #app { padding: 24px; }
    .form-container { padding: 0 12px; }
    .flex-container { display: flex; gap: 24px; }
    .input-box { width: 90px; margin: 0 6px; }
  </style>
</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-input>
          </el-form-item>
          <el-form-item label="计费规则名称" prop="ruleName">
            <el-input v-model="addForm.ruleName"></el-input>
          </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-input>
            </el-form-item>
            <el-form-item label="收费上限">
              <el-input v-model="addForm.chargeCeiling"></el-input>
            </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> 小时
              <el-input v-model="addForm.durationPrice" class="input-box"></el-input> 元
            </div>
            <div v-if="addForm.chargeType === 'turn'" class="turn">
              每次 <el-input v-model="addForm.turnPrice" class="input-box"></el-input> 元
            </div>
            <div v-if="addForm.chargeType === 'partition'" class="partition">
              <div class="item">
                <el-input v-model="addForm.partitionFrameTime" class="input-box"></el-input>小时内,每小时收费
                <el-input v-model="addForm.partitionFramePrice" class="input-box"></el-input> 元
              </div>
              <div class="item">
                每增加<el-input v-model="addForm.partitionIncreaseTime" class="input-box"></el-input

常见错误 ​

  • 现象:弹框关闭后再次打开,表单内容和校验红字还在。→ 原因:只改了 dialogVisible,未重置表单与校验状态。→ 修法:关闭/重新打开时调用 resetFields() 或按需 clearValidate(),并重置 addForm。
  • 现象:点击“确定”后接口已请求,但表单没校验或校验未通过仍提交。→ 原因:提交逻辑未包在 validate 回调中,或未判断 valid。→ 修法:统一走 this.$refs.addForm.validate(async valid => { if (!valid) return; ... })。
  • 现象:切换计费方式后,提交数据里仍带着上一种方式的旧字段。→ 原因:只控制显示隐藏,未清理非当前类型字段。→ 修法:切换 chargeType 时清理无关字段,或提交前按类型组装 payload。
  • 现象:步骤条回退后,上一步填过的内容或选中状态丢失。→ 原因:内容区用 v-if 导致 DOM 销毁重建。→ 修法:需要保留状态时用 v-show;只有确实要销毁重建时才用 v-if。
  • 现象:步骤条“下一步”可无限点击或回退越界。→ 原因:只做自增自减,未限制 currentActive 范围。→ 修法:下一步前判断 < 3,上一步前判断 > 1,并在第一步点下一步时先做表单校验拦截。
  • 现象:封装 button 组件后,父组件写 @click 不触发。→ 原因:组件标签上的事件默认是自定义事件,未在子组件 $emit。→ 修法:子组件内部触发 this.$emit('click');不建议依赖 .native 作为常规方案。
  • 现象:全局组件注册后仍提示未注册。→ 原因:只定义了插件或只调用了 Vue.component,未在入口执行 Vue.use(plugin)。→ 修法:插件暴露 install(Vue),入口导入并 Vue.use(),确保注册代码被执行。

练习 ​

  1. 给新增规则弹框补全“打开即重置、关闭即清理”的逻辑,要求取消和提交成功后表现一致。
  2. 改造 confirmAdd:校验通过才请求;请求成功后关闭弹框、刷新列表、重置表单;请求失败保留弹框。
  3. 实现计费方式切换:duration / turn / partition 只提交当前类型字段,切换时清理其他类型字段。
  4. 给步骤条“下一步”加拦截:第一步校验角色信息表单,校验不通过不进入第二步;同时限制上一步/下一步边界。
  5. 封装一个 base-button:支持默认插槽文案、size 控制类名、点击时 $emit('click'),并改为通过插件全局注册。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1角色管理-获取成员列表08:23
2新增角色-绑定路由跳转04:23
3新增角色-根据状态适配视图的变16:44
4新增角色-根据表单校验判断进入下一个状态06:04
5button组件封装-通过插槽实现文案自定义04:31
6button组件封装-添加size参数06:13
7button组件封装-绑定点击事件07:53
8button组件封装-全局可用11:45

小结 ​

  • 弹框类表单要同时管理“显隐、表单数据、校验状态”三件事,关闭清理比只改 visible 更可靠。
  • 提交入口必须唯一:先校验、再请求、成功后刷新并关闭,失败时保留现场。
  • 动态表单区域不能只做显示切换,还要处理字段清理和提交 payload 组装。
  • 步骤条的核心是单一状态驱动三块视图:步骤条、内容区、操作按钮。
  • 需要保留用户操作状态时优先 v-show;需要彻底重建时才用 v-if。
  • 组件封装要同时考虑插槽、props、事件三端约定;组件标签事件默认按自定义事件处理。
  • 全局组件推荐走插件:定义 install,入口 Vue.use,避免注册逻辑散落。