[toc]
| 单词 | 词性 | 音标 | 含义 |
|---|---|---|---|
| TodoApp | n. | /ˈtuːduː æp/ | 本节综合练习的待办事项组件 |
| computed | adj. | /kəmˈpjuːtɪd/ | 计算属性,由其他响应式数据派生(如未完成计数) |
| ref | n. | /ref/ | 声明响应式数据的 API,数组需重新赋值才触发更新 |
| v-for | - | /viː fɔː(r)/ | Vue 的列表渲染指令,按数组生成多项 |
| filter | v. | /ˈfɪltə(r)/ | 数组过滤方法,常用来生成新数组实现删除 |
| scoped | adj. | /ˈskəʊpt/ | 样式只作用于当前组件,防止污染别处 |
| Todo | n. | /ˈtuːduː/ | 待办项的类型定义(id / title / done) |
Lecture14-中型页面串讲练习
1、本节讲什么
把前面内容串起来,做一个**"待办事项"小页面**:能添加、勾选、删除,数据存内存。这是 Vue 部分的综合练习——重点是顺手把第 11 节的栅格布局、第 13 节的 Flex 用起来。
2、概念速览
本节定位:综合练习,把前面学的模板、响应式、循环、布局拼成一个完整页面,目的是"串起来用",不是学新概念。
布局怎么串:页面骨架用 el-row / el-col 划分(第 11 节),骨架内的细节用 Flex 排(第 13 节),样式写在 <style scoped>(第 13 节)——正是上一节理的"各管一段"分工。
类比:像把几个方法拼成一个完整功能。
正确 vs 误区:别追求一次写对,重点是"能拆成小步骤、每步验证"——这正是 Vibe Coding 的节奏。
3、先看效果

一个"输入框 + 按钮"在顶部一行、下面是列表,每条待办一行:勾选框 + 文字 + 删除按钮。骨架用 el-row/el-col,行内用 Flex。
src/TodoApp.vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
interface Todo { id: number; title: string; done: boolean }
const todos = ref<Todo[]>([
{ id: 1, title: '学习 Vue', done: false },
{ id: 2, title: '学习 TS', done: true },
])
const input = ref('')
const undoneCount = computed(() => todos.value.filter(t => !t.done).length)
function add() {
if (!input.value.trim()) {
ElMessage.warning('先输入内容') // 空输入提醒
return
}
todos.value.push({ id: Date.now(), title: input.value, done: false })
input.value = ''
}
function remove(id: number) {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
<template>
<!-- ① 页面骨架:el-row 一行,各块用 el-col 划分 -->
<el-row :gutter="16">
<el-col :span="24">
<h2>待办事项</h2>
</el-col>
<!-- ② 输入区:el-col 内用 flex 把输入框和按钮排成一行 -->
<el-col :span="24">
<div class="add-bar">
<el-input v-model="input" placeholder="输入待办,回车添加" @keyup.enter="add" />
<el-button type="primary" @click="add">添加</el-button>
</div>
</el-col>
<!-- ③ 状态 + 列表区 -->
<el-col :span="24">
<p>还剩 {{ undoneCount }} 项未完成</p>
<ul>
<li v-for="t in todos" :key="t.id" class="todo-item">
<input type="checkbox" v-model="t.done" />
<span :style="{ textDecoration: t.done ? 'line-through' : 'none' }" class="title">{{ t.title }}</span>
<el-button size="small" type="danger" @click="remove(t.id)">删除</el-button>
</li>
</ul>
</el-col>
</el-row>
</template>
<style scoped>
/* ② 输入区:flex 排一行,输入框撑满、按钮固定右侧(flex: 1 见第 21 节) */
.add-bar { display: flex; gap: 8px; }
.add-bar .el-input { flex: 1; }
/* ③ 每条待办:flex 排一行,勾选框 + 文字 + 删除按钮 */
.todo-item { display: flex; align-items: center; gap: 8px; }
.todo-item .title { flex: 1; }
</style>
4、照着做
- 建
TodoApp.vue,替换 App.vue,跑起来。 - 看布局:
el-row/el-col把页面分成哪几块?add-bar和todo-item里的 Flex 各排了什么? - 把"添加 / 勾选 / 删除 / 计数"四件事都亲手做一遍,确认都能用。
5、常见疑问
- Q:为什么用
ref<Todo[]>这种写法? A:给数组定类型,TS 才能帮你检查push的对象字段对不对——这就是第 6 节类型的实战价值。 - Q:为什么要把输入框和按钮包一层
.add-bar再 flex? A:el-input和el-button是 Element Plus 组件,它们本身是块级组件,直接并排放不下。包一个普通div当 flex 容器,就能用flex+gap控制它们在一行、还留间距——这是"在 el-col 里用 flex 排组件"的标准做法。 - Q:删除用
filter重新赋值,为什么不是splice? A:改ref数组要触发响应式,用filter生成新数组重新赋值最稳。splice直接改原数组有时不触发更新。记住"改 ref 数组就重新赋值"。
6、规范要点
- 本节的
TodoApp是"一个组件干一件事"的好例子:逻辑在脚本里、结构在模板里、样式在 style 里,三者分清楚。 - 骨架用
el-row/el-col,行内细节用 Flex,样式带scoped——这一节你已经把第 20、22 节的布局技术完整用了一遍。 - 数据操作(添加/删除)写成带名字的函数(
add/remove),别在模板里写一堆内联逻辑。 - 到这里你已经能读懂并修改一个"有布局、有交互"的完整中型 Vue 页面——接下来交给 AI 前,先自己能跑通。
7、本节验证
- 能独立跑通 TodoApp 的添加 / 勾选 / 删除 / 计数 ✅
- 会用 el-row / el-col 搭骨架、列内用 flex 排细节 ✅
- 会用 ref<Todo[]> 给数组定类型 ✅
- 知道改 ref 数组要用 filter 重新赋值触发响应式 ✅