主题
角色管理与合同详情(三)
本页对应 角色管理与合同详情,共 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(),确保注册代码被执行。
练习
- 给新增规则弹框补全“打开即重置、关闭即清理”的逻辑,要求取消和提交成功后表现一致。
- 改造
confirmAdd:校验通过才请求;请求成功后关闭弹框、刷新列表、重置表单;请求失败保留弹框。 - 实现计费方式切换:
duration / turn / partition只提交当前类型字段,切换时清理其他类型字段。 - 给步骤条“下一步”加拦截:第一步校验角色信息表单,校验不通过不进入第二步;同时限制上一步/下一步边界。
- 封装一个
base-button:支持默认插槽文案、size控制类名、点击时$emit('click'),并改为通过插件全局注册。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 角色管理-获取成员列表 | 08:23 |
| 2 | 新增角色-绑定路由跳转 | 04:23 |
| 3 | 新增角色-根据状态适配视图的变 | 16:44 |
| 4 | 新增角色-根据表单校验判断进入下一个状态 | 06:04 |
| 5 | button组件封装-通过插槽实现文案自定义 | 04:31 |
| 6 | button组件封装-添加size参数 | 06:13 |
| 7 | button组件封装-绑定点击事件 | 07:53 |
| 8 | button组件封装-全局可用 | 11:45 |
小结
- 弹框类表单要同时管理“显隐、表单数据、校验状态”三件事,关闭清理比只改
visible更可靠。 - 提交入口必须唯一:先校验、再请求、成功后刷新并关闭,失败时保留现场。
- 动态表单区域不能只做显示切换,还要处理字段清理和提交 payload 组装。
- 步骤条的核心是单一状态驱动三块视图:步骤条、内容区、操作按钮。
- 需要保留用户操作状态时优先
v-show;需要彻底重建时才用v-if。 - 组件封装要同时考虑插槽、props、事件三端约定;组件标签事件默认按自定义事件处理。
- 全局组件推荐走插件:定义
install,入口Vue.use,避免注册逻辑散落。