孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第14次 · 表单控件:输入框、下拉、单选、多选
模块总览

第14次 · 表单控件:输入框、下拉、单选、多选

2026-09-20 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 14-表单控件输入框下拉单选多选.zip
下载 ZIP 压缩包

第14次 · 表单控件:输入框、下拉、单选、多选

第1/7页 · 先知道今天要做什么

第 6 课你学过 v-model:它让数据和界面双向同步。今天要做的,是把 v-model 接到 Element Plus 的各类输入控件上,并且在选控件时问自己一个问题:这个数据最合适的输入方式是什么?

同样是“选择”这件事,就可能用到六种控件:下拉框、单选、多选、开关、滑块、评分。选错了,用户就会难受;选对了,用户会觉得“这个系统挺顺手”。控件选型是前端最容易体现专业度的地方。

今天的任务是把八种常用输入控件全部过一遍,并记住一句话:数据是什么类型,就选什么控件。

今天的场景

做一个“学员信息录入卡”。八个字段,八种控件,右侧实时显示录入结果。你要能说出每个字段为什么用这个控件。

下课时拿出这三样结果

做到什么 在哪里看 完成的证据
会用八种输入控件 录入卡 八种控件都能改到数据,右侧快照同步变化。
会做多选与可搜索下拉 报名方向、班级 多选返回数组;filterable 时能输入关键字过滤;multiple 时能折叠标签。
能说清控件选型依据 讲解与自查 每个字段都能说出“为什么不用另一种控件”。

你只需要带着这些基础来

第 6 课的 v-model 与绑定类型(字符串 / 数组 / 布尔 / 数字),第 13 课的按钮。

本课时 45 分钟怎么上

时间 老师带着做 学生留下什么
0—6 分钟 给出六个字段的数据类型,让学生先自己猜该用什么控件。 一张“字段 → 控件”的猜答案。
6—24 分钟 讲输入框家族:普通、数字、多行、密码、带前缀后缀、字数限制。 会按数据类型选输入框。
24—38 分钟 讲选择家族:下拉(单选/多选/可搜索/分组)、单选、多选、开关、滑块。 会写四种选择控件。
38—43 分钟 粘贴本课 App.vue,逐项改值看快照。 八种控件全部跑通。
43—45 分钟 抽查:问“报名方向为什么不能用单选”。 全员提交。

从哪里开始

复制第 13 课的工程,改名 lesson-vue-14。不需要新增依赖。

今天先掌握到这里

第 15 课讲表格(展示数据),第 16 课讲反馈组件(消息提示)。表单校验从第 17 课才开始——本课只做录入,不做校验。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档


第2/7页 · 把新词说清楚

控件选型对照表(本课最重要的一张表)

数据特征 该用的控件 绑定的类型
任意短文本(姓名、学号) el-input 字符串
数字,且有范围 el-input-number 数字
长文本(备注、说明) el-input type="textarea" 字符串
密码 el-input type="password" show-password 字符串
从固定几项里选一个(少于 5 项) el-radio-group 字符串/数字
从固定几项里选一个(多于 5 项) el-select 字符串/数字
从固定几项里选多个 el-checkbox-group 或 el-select multiple 数组
是 / 否 el-switch 或 el-radio-group 布尔
一个范围内的数值(比例、评分) el-slider 数字
星级评价(1—5) el-rate 数字

一条实操建议:选项少于 5 个用单选按钮,多于 5 个用下拉框。 单选按钮把所有选项都摊开,用户一眼看到全部;下拉框节省空间,但要点击才能看到。

输入框家族的常用属性

属性 作用 什么时候用
placeholder 空值时显示的提示文字 基本都要写,说明该填什么
clearable 显示一键清空按钮 可选的输入
maxlength + show-word-limit 限制长度并显示计数 备注、简介
prefix-icon / suffix-icon 前后缀图标 搜索框加放大镜
disabled / readonly 禁用 / 只读 有条件不可编辑时
type="textarea" + :rows 多行文本 长文本
show-password 密码可见开关 密码输入

disabled 与 readonly 的区别:disabled 是“灰掉、不可点、不提交”;readonly 是“可以选中复制,但不能改”。需要用户复制内容时用 readonly。

下拉框的四个常用能力

写法 效果
filterable 可输入关键字过滤(选项多时必备)
multiple 多选(值变成数组)
collapse-tags 多选时把标签折叠成 +N,避免撑开
clearable 允许清空
el-option-group 给选项分组显示
<el-select v-model="dirs" multiple filterable collapse-tags placeholder="可多选" style="width: 100%">
  <el-option-group label="前端方向">
    <el-option label="Vue" value="vue" />
    <el-option label="React" value="react" />
  </el-option-group>
  <el-option-group label="后端方向">
    <el-option label="Java" value="java" />
    <el-option label="Go" value="go" />
  </el-option-group>
</el-select>

单选 / 多选的两个细节

  1. el-radio 的 value 是新写法(老版本叫 label,容易和显示文字混在一起,现在已经分开)。推荐一律写 value;
  2. el-checkbox-group 的 v-model 必须是数组,这是最常见的错误来源。

只看这几行:类型匹配

const form = reactive({
  name: '',        // 字符串 → el-input
  age: 18,         // 数字  → el-input-number
  tags: [],        // 数组  → el-checkbox-group / el-select multiple
  enabled: true    // 布尔  → el-switch
})

先口头预测:如果把 tags 从 [] 改成 ''(空字符串),会发生什么?

多选框会报错或行为异常。 el-checkbox-group 要求 v-model 是一个数组,它需要往里面 push / 移除元素。给字符串它就没法工作。这就是“绑定类型必须匹配控件”的最直接例子。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档


第3/7页 · 先看一个小例子

完整代码:App.vue(学员信息录入卡)

<script setup>
import { ref, reactive, computed } from 'vue'
import {
  ElConfigProvider, ElCard, ElRow, ElCol, ElForm, ElFormItem, ElInput,
  ElInputNumber, ElSelect, ElOption, ElOptionGroup, ElRadioGroup, ElRadio,
  ElCheckboxGroup, ElCheckbox, ElSwitch, ElSlider, ElRate, ElButton,
  ElDivider, ElDescriptions, ElDescriptionsItem, ElAlert, ElSpace, ElTag,
  ElMessage
} from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'

const form = reactive({
  name: '张三',            // 字符串 → el-input
  sno: '2026001',          // 字符串 → el-input
  age: 18,                 // 数字   → el-input-number
  gender: '男',            // 单选   → el-radio-group
  cls: '',                 // 单选   → el-select
  directions: [],          // 多选   → el-select multiple
  skills: ['vue'],         // 多选   → el-checkbox-group
  enabled: true,           // 布尔   → el-switch
  progress: 60,            // 数字   → el-slider
  rating: 4,               // 数字   → el-rate
  password: '',            // 字符串 → el-input show-password
  remark: ''               // 长文本 → textarea
})

const skillOptions = ['vue', 'react', 'java', 'python', 'sql']
const dirOptions = [
  { label: '前端方向', items: [{ label: 'Vue 开发', value: 'vue' }, { label: 'React 开发', value: 'react' }] },
  { label: '后端方向', items: [{ label: 'Java 后端', value: 'java' }, { label: 'Go 后端', value: 'go' }] },
  { label: '数据方向', items: [{ label: '数据分析', value: 'data' }, { label: '数据库运维', value: 'dba' }] }
]

// 实时快照:检验每个控件到底写了什么类型进去
const snapshot = computed(() => ({
  name: form.name,
  类型_name: typeof form.name,
  age: form.age,
  类型_age: typeof form.age,
  directions: form.directions,
  类型_directions: Array.isArray(form.directions) ? 'array' : typeof form.directions,
  skills: form.skills,
  enabled: form.enabled,
  类型_enabled: typeof form.enabled,
  progress: form.progress,
  rating: form.rating
}))

const remarkLen = computed(() => form.remark.length)

function submit() {
  ElMessage.success('已收集:「' + form.name + '」,方向 ' + form.directions.length + ' 个')
  console.log('提交快照:', JSON.parse(JSON.stringify(form)))
}

function reset() {
  Object.assign(form, {
    name: '', sno: '', age: 18, gender: '男', cls: '', directions: [],
    skills: [], enabled: true, progress: 50, rating: 0, password: '', remark: ''
  })
  ElMessage.info('已重置')
}
</script>

<template>
  <el-config-provider :locale="zhCn">
    <div class="page">
      <h1 class="title">学员信息录入</h1>

      <el-row :gutter="16">
        <el-col :xs="24" :md="14">
          <el-card>
            <template #header>八种控件(每个都改一下试试)</template>

            <el-form label-width="96px" @submit.prevent>
              <el-divider content-position="left">文本类</el-divider>

              <el-form-item label="姓名">
                <el-input v-model.trim="form.name" placeholder="请输入姓名" clearable />
              </el-form-item>

              <el-form-item label="学号">
                <el-input v-model.trim="form.sno" placeholder="只读示例:可以选中复制" readonly />
              </el-form-item>

              <el-form-item label="密码">
                <el-input v-model="form.password" type="password" show-password placeholder="试试右侧的小眼睛" />
              </el-form-item>

              <el-form-item label="年龄">
                <el-input-number v-model="form.age" :min="10" :max="60" @change="v => ElMessage.info('年龄变成数字:' + v + '(' + typeof v + ')')" />
              </el-form-item>

              <el-form-item label="备注">
                <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="100" show-word-limit placeholder="最多 100 字" />
              </el-form-item>

              <el-divider content-position="left">选择类</el-divider>

              <el-form-item label="性别">
                <!-- 选项少于 5 个:用单选按钮,一眼看全 -->
                <el-radio-group v-model="form.gender">
                  <el-radio value="男">男</el-radio>
                  <el-radio value="女">女</el-radio>
                  <el-radio value="保密">保密</el-radio>
                </el-radio-group>
              </el-form-item>

              <el-form-item label="班级">
                <!-- 单选 + 可搜索 -->
                <el-select v-model="form.cls" filterable clearable placeholder="可输入关键字过滤" style="width: 100%">
                  <el-option v-for="i in 6" :key="i" :label="'计算机' + i + '班'" :value="'计算机' + i + '班'" />
                </el-select>
              </el-form-item>

              <el-form-item label="报名方向">
                <!-- 多选 + 分组 + 标签折叠:值一定是数组 -->
                <el-select v-model="form.directions" multiple filterable collapse-tags placeholder="可多选" style="width: 100%">
                  <el-option-group v-for="g in dirOptions" :key="g.label" :label="g.label">
                    <el-option v-for="it in g.items" :key="it.value" :label="it.label" :value="it.value" />
                  </el-option-group>
                </el-select>
              </el-form-item>

              <el-form-item label="已掌握技能">
                <!-- 多选框组:值必须是数组 -->
                <el-checkbox-group v-model="form.skills">
                  <el-checkbox v-for="s in skillOptions" :key="s" :value="s">{{ s }}</el-checkbox>
                </el-checkbox-group>
              </el-form-item>

              <el-form-item label="是否启用">
                <el-switch v-model="form.enabled" active-text="启用" inactive-text="停用" />
              </el-form-item>

              <el-form-item label="学习进度">
                <el-slider v-model="form.progress" show-input style="width: 100%" />
              </el-form-item>

              <el-form-item label="课程评分">
                <el-rate v-model="form.rating" show-score allow-half />
              </el-form-item>

              <el-form-item>
                <el-button type="primary" @click="submit">提交</el-button>
                <el-button @click="reset">重置</el-button>
              </el-form-item>
            </el-form>
          </el-card>
        </el-col>

        <el-col :xs="24" :md="10">
          <el-card class="mb">
            <template #header>实时类型快照(这是本课的关键)</template>
            <pre class="json">{{ JSON.stringify(snapshot, null, 2) }}</pre>
            <el-alert
              title="重点看:name / age / directions / skills / enabled 分别是什么类型。控件不同,写进数据的类型就不同。"
              type="info" :closable="false" show-icon
            />
          </el-card>

          <el-card>
            <template #header>字段速览</template>
            <el-descriptions :column="1" border size="small">
              <el-descriptions-item label="姓名">{{ form.name || '—' }}</el-descriptions-item>
              <el-descriptions-item label="年龄">{{ form.age }}({{ typeof form.age }})</el-descriptions-item>
              <el-descriptions-item label="班级">{{ form.cls || '未选' }}</el-descriptions-item>
              <el-descriptions-item label="报名方向">
                <el-tag v-for="d in form.directions" :key="d" size="small" style="margin-right: 4px">{{ d }}</el-tag>
                <span v-if="!form.directions.length">未选</span>
              </el-descriptions-item>
              <el-descriptions-item label="技能">{{ form.skills.join('、') || '未选' }}</el-descriptions-item>
              <el-descriptions-item label="启用">{{ form.enabled ? '是' : '否' }}</el-descriptions-item>
              <el-descriptions-item label="备注字数">{{ remarkLen }} / 100</el-descriptions-item>
            </el-descriptions>
          </el-card>
        </el-col>
      </el-row>
    </div>
  </el-config-provider>
</template>

<style>
html, body, #app {
  margin: 0; padding: 0; max-width: none; width: auto; height: auto;
  display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>

<style scoped>
.page { max-width: 1150px; margin: 28px auto; padding: 0 16px; }
.title { font-size: 22px; color: #303133; margin: 0 0 18px; }
.mb { margin-bottom: 16px; }
.json {
  background: #1f2430; color: #a9dc76; padding: 12px; border-radius: 6px;
  font-size: 12px; line-height: 1.7; margin: 0 0 12px; max-height: 420px; overflow: auto;
}
</style>

预期看见什么

  1. 左边一张长表单,用两条分割线分成“文本类”和“选择类”。
  2. 右边一张卡实时显示各字段的类型快照,另一张卡是字段速览。
  3. 在姓名框输入,快照里 name 是字符串;改年龄,age 是 数字;勾选两个技能,skills 是 数组;切换开关,enabled 是 布尔。
  4. “报名方向”下拉:输入“vue”能过滤;选三项后显示成 +2 折叠标签。
  5. 备注框输入时右下角字数实时增加。
  6. 点“提交”,Console 里打印完整快照。

先猜一猜,读完代码再回答

我把“学号”设成了 readonly 而不是 disabled,为什么?

<el-input v-model.trim="form.sno" readonly />

因为学号是用户需要看到、需要复制的东西。 disabled 会让输入框灰掉且无法选中文字,用户想复制作业号都做不到;readonly 允许选中与复制,只是不让改。这就是“要不要让用户复制”这个问题的正确答案。 反过来,如果这个字段是“系统自动生成、完全不该被人碰”(比如数据库主键),那就用 disabled。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档


第4/7页 · 一步一步做出来

第1步:从最简单的输入框开始

<el-input v-model="name" placeholder="请输入姓名" clearable />

预期:能输入、能一键清空。

第2步:加字数限制与计数

<el-input v-model="remark" type="textarea" :rows="3" maxlength="100" show-word-limit />

预期:输入到 100 字无法再输入,右下角显示 100/100。

第3步:数字输入与类型验证

<el-input-number v-model="age" :min="10" :max="60" @change="v => console.log(typeof v)" />

预期:Console 打印 number。再换成普通 el-input 不加 .number,预期打印 string——这就是两种控件的本质区别。

第4步:三种选择控件

单选(选项少)、下拉(选项多)、多选(要选多个) 各写一个。预期:多选控件的值在快照里是数组;把 v-model 的初始值从 [] 改成 '',预期:报错或异常,验证“类型必须匹配”。

第5步:多选下拉的四个能力

依次加上 multiple、filterable、collapse-tags、clearable,每加一个观察一次变化。预期:能多选、能搜索、标签会折叠、能清空。

第6步:滑块与评分

<el-slider v-model="progress" show-input />
<el-rate v-model="rating" show-score allow-half />

预期:滑块带数字输入框且双向联动;评分可以打半星。

第7步:整段替换为本课完整版

把第 3 页的完整 App.vue 覆盖 src/App.vue,逐项验证八种控件。

本课文件速查

位置 本次作用 绑定类型
form.name 普通文本输入 字符串
form.age 数字输入框 数字
form.directions 多选下拉(分组 + 折叠) 数组
form.skills 多选框组 数组
form.enabled 开关 布尔
form.progress / form.rating 滑块 / 评分 数字
snapshot 实时显示每个字段的类型 —

完成修改后检查

操作或输入 预期观察
改年龄 快照里 类型_age 是 number。
勾两个技能 skills 是长度为 2 的数组。
下拉选三项 显示成折叠标签 +2。
输入超过 100 字 输入被截住,计数显示 100/100。
点“重置” 所有控件回到初始状态。
在“学号”框里选中文字 可以选中复制(因为是 readonly)。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档


第5/7页 · 换成自己的代码

你的任务

  1. 做一张自己的录入表单,至少 10 个字段,必须覆盖:字符串、数字、数组、布尔四种类型。
  2. 刻意做一次错误实验:把某个多选控件的初值改成非数组,记录现象。
  3. 加一个“控件选型说明”:在表单每个字段旁边加一行小字,写“为什么用这个控件”(例如“选项只有 3 个,用单选按钮一眼看全”)。
  4. 加一个联动禁用:当“是否启用”关闭时,把“学习进度”和“课程评分”设为禁用(:disabled="!form.enabled"),并说明理由。
  5. 做一个字段速览表:用 el-descriptions 把当前数据完整展示出来,包括数组用 join 拼接。

按这张操作单完成

步骤 改哪里 运行后应该看到什么
1 十字段表单 模板 四种类型的字段都有。
2 错误实验 多选初值 记录报错或异常现象。
3 选型说明 字段旁小字 每个字段一句理由。
4 联动禁用 :disabled 关闭开关后两个控件变灰。
5 速览表 el-descriptions 当前数据完整可读。

最低完成标准:先把这三项做实

检查项 通过条件
类型正确 多选是数组、数字控件给数字、开关给布尔,能在快照里验证。
控件选型合理 选项 ≤5 个用单选按钮,>5 个用下拉,多选一律用数组。
都改得动 每个控件都能真正改到数据(不是只显示)。

在本地写练习记录

  1. 画出“字段 → 控件 → 数据类型”的对照表。
  2. 记录多选初值写错时的报错原文。
  3. 写下 readonly 与 disabled 的选择理由(你的表单里哪几个用哪个)。

最后交什么

提交 src/App.vue 与 练习记录.md。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档


第6/7页 · 出错时从哪里查

把现象和检查动作对上

看到什么现象 / 报错 先检查哪里 修好后怎样确认
多选控件选不动 / 报错 v-model 绑的不是数组。 改成 [] 后正常。
数字框拿到的是字符串 用了 el-input 而没加 .number。 换 el-input-number 或在 v-model 后加 .number。
el-radio 不显示文字 把显示文字写到了 label 上又同时用了 value。 显示文字放标签中间,值放 value。
el-select 选项不显示 el-option 忘了写 label,或 value 与数据对不上。 补上后正常。
下拉选项很多但没搜索 忘了 filterable。 加上后可搜索。
多选标签把布局撑开 忘了 collapse-tags。 加上后折叠成 +N。
readonly 输入框还能被改 写成了 :readonly="'false'"(字符串恒为真)或写错属性名。 用 readonly 或 :readonly="bool值"。
输入框里汉字输入时计数不对 用了 .lazy 导致更新滞后。 去掉 .lazy 后计数实时。

排错口诀

多选必是数组,开关必是布尔,数字用 number 控件; 选项五个以内用单选,多了用下拉; 要给用户复制就用 readonly,彻底不让碰才用 disabled; 下拉难用先看三件套:filterable、clearable、collapse-tags。

不要用这些办法“修复”

  1. 不要为了让多选控件“能用”而把数组改成字符串。数据一旦类型错了,后面做统计、提交接口都会陆续出问题。
  2. 不要给整个表单的每个控件都加 clearable。像“姓名”这种必填项,清空按钮只是给用户添乱。
  3. 不要用 el-input 加一堆正则去模拟数字框。el-input-number 已经处理好了上下限、步进和输入校验。

需要老师帮忙时交这些

出问题那个控件的完整标签、对应数据的定义(初值是什么)、控制台报错原文、你期望的数据类型与实际得到的类型。

老师巡视时只问三句话

  1. 这个字段为什么用这种控件?(能说出选型依据)
  2. 它绑的数据是什么类型?怎么验证的?(能指出快照里的类型)
  3. 如果要选 20 个方向,你还会用现在这个控件吗?(能说出改用多选下拉 + 搜索)

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档


第7/7页 · 用自己的话讲一遍

回到第 3 页:你刚才猜对了吗?

为什么学号用 readonly 而不是 disabled?

参考解释:学号是需要用户“看到并复制”的信息,disabled 会让文本无法选中;readonly 保持可选中、可复制,只是不可编辑。是否禁用要看用户对这个字段还有没有别的需求(复制、查看),而不只是“能不能改”。

回答本课的两个问题

问题 参考解释
单选按钮与下拉框怎么选? 看选项数量与使用频率。选项少于 5 个、或需要用户一眼看全时用单选按钮;选项多、需要搜索时用下拉框。
为什么多选一定是数组? 因为“多选”的结果天然可能是零个、一个或多个;只有数组能同时表示这三种情况。

五个必须能答上来的问题

考考自己 可以这样通俗回答
el-input-number 和 el-input 的区别? 前者给的是数字,自带上下限与步进;后者给的是字符串。
下拉框三个必备属性是什么? filterable(可搜索)、clearable(可清空)、多选时加 collapse-tags。
是 / 否该用什么? 用 el-switch;如果两者不是“开关式”的语义(如“男/女/保密”),就用 el-radio-group。
长文本用什么? el-input type="textarea",按需加 maxlength 与 show-word-limit。
如何一眼看出控件写进去的是什么类型? 页面上放一个快照区,用 typeof / Array.isArray 打印出来。

一分钟现场演示指南

  1. 演示类型:改年龄、勾多选、切开关,指着快照说“三处类型完全不同”。
  2. 演示选型:把班级下拉临时改成单选按钮,指出“6 个选项摊开后太占地方,所以用下拉”。
  3. 演示 readonly / disabled:现场选中文字复制,说明差别。
  4. 收尾一句:“控件选型不是为了好看,是为了让用户少犯错。”

交作业前最后检查

  • src/App.vue 与 练习记录.md 已保存。
  • 四种数据类型(字符串 / 数字 / 数组 / 布尔)都有对应控件。
  • 多选控件的初值是数组。
  • 每个控件都能真正改到数据。
  • Console 无红色报错。

课后再做一个小对照

把“报名方向”从多选下拉改成多选框组(el-checkbox-group),再改回下拉。写下两种方式的优劣:什么时候摊开更好(选项少、需要强调),什么时候收起更好(选项多、节省空间)。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档