Vue 3 实战案例完全指南
本文档通过多个完整实战案例,把 Vue 3 的常用知识点串起来:数据绑定、列表渲染、条件显示、事件处理、表单、组件、简单状态等。每个案例都给出可运行的代码和说明,适合新手照着做、改一改练手。
建议先学完《Vue3模板语法》《Vue3组件》《Vue3表单》等基础文档,再来做这些案例。
一、案例一:待办事项(Todo List)
1.1 需求
- 输入框输入内容,回车或点按钮添加一条待办。
- 每条可勾选完成、删除。
- 可筛选:全部 / 未完成 / 已完成。
- 显示未完成数量。
1.2 技术点
- v-model 绑定输入框。
- v-for 渲染列表,:key 用唯一 id。
- v-if / v-show 或计算属性做筛选。
- @click、@keyup.enter 事件。
- computed 得到“筛选后的列表”和“未完成数量”。
1.3 完整示例
<template>
<div class="todo-app">
<h2>待办事项</h2>
<input
v-model.trim="newTodo"
placeholder="输入后回车添加"
@keyup.enter="add"
/>
<button @click="add">添加</button>
<div class="filters">
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">全部</button>
<button :class="{ active: filter === 'active' }" @click="filter = 'active'">未完成</button>
<button :class="{ active: filter === 'done' }" @click="filter = 'done'">已完成</button>
</div>
<p>未完成:{{ leftCount }} 条</p>
<ul>
<li v-for="item in filteredList" :key="item.id" class="todo-item">
<input type="checkbox" v-model="item.done" />
<span :class="{ done: item.done }">{{ item.text }}</span>
<button @click="remove(item.id)">删除</button>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const newTodo = ref('')
const filter = ref('all')
const list = ref([
{ id: 1, text: '学习 Vue 3', done: false },
{ id: 2, text: '写一个 Todo', done: true }
])
function add() {
if (!newTodo.value) return
list.value.push({
id: Date.now(),
text: newTodo.value,
done: false
})
newTodo.value = ''
}
function remove(id) {
list.value = list.value.filter((item) => item.id !== id)
}
const filteredList = computed(() => {
if (filter.value === 'active') return list.value.filter((item) => !item.done)
if (filter.value === 'done') return list.value.filter((item) => item.done)
return list.value
})
const leftCount = computed(() => list.value.filter((item) => !item.done).length)
</script>
<style scoped>
.todo-app { max-width: 400px; margin: 20px auto; }
.filters { margin: 10px 0; }
.filters button { margin-right: 8px; }
.filters .active { font-weight: bold; }
.todo-item { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.done { text-decoration: line-through; color: #999; }
</style>
1.4 可扩展
- 用 localStorage 持久化(watch list 存、onMounted 读)。
- 拆成 TodoItem 子组件,用 props + emit 通信。
二、案例二:计数器(多计数器 + 总和)
2.1 需求
- 多个计数器,每个有 +1、-1、重置。
- 显示所有计数器的总和。
- 可增加/删除一个计数器。
2.2 技术点
- v-for 渲染多个计数器,每个一项 ref 或统一用数组存数字。
- computed 算总和。
- 数组增删(push、splice)触发视图更新。
2.3 完整示例
<template>
<div class="counter-app">
<h2>多计数器</h2>
<button @click="addCounter">新增计数器</button>
<p>总和:{{ total }}</p>
<div v-for="(c, i) in counters" :key="c.id" class="counter-row">
<span>计数器 {{ i + 1 }}</span>
<button @click="c.count--">-</button>
<span>{{ c.count }}</span>
<button @click="c.count++">+</button>
<button @click="reset(i)">重置</button>
<button @click="removeCounter(i)">删除</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const counters = ref([
{ id: 1, count: 0 },
{ id: 2, count: 0 }
])
const total = computed(() => counters.value.reduce((sum, c) => sum + c.count, 0))
function addCounter() {
counters.value.push({ id: Date.now(), count: 0 })
}
function reset(i) {
counters.value[i].count = 0
}
function removeCounter(i) {
counters.value.splice(i, 1)
}
</script>
<style scoped>
.counter-row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
</style>
三、案例三:登录/注册表单 + 简单校验
3.1 需求
- 登录:用户名、密码;提交时校验非空,并模拟请求。
- 可切换到“注册”:多一个“确认密码”,校验两次密码一致。
3.2 技术点
- v-model 绑定表单;v-model.trim。
- v-if 切换登录/注册表单。
- @submit.prevent 阻止默认提交;在方法里做校验、设 error 文案。
- ref 存表单数据和错误信息。
3.3 完整示例
<template>
<div class="auth-form">
<h2>{{ isLogin ? '登录' : '注册' }}</h2>
<form @submit.prevent="submit">
<input v-model.trim="form.username" placeholder="用户名" />
<p v-if="errors.username" class="error">{{ errors.username }}</p>
<input v-model="form.password" type="password" placeholder="密码" />
<p v-if="errors.password" class="error">{{ errors.password }}</p>
<template v-if="!isLogin">
<input v-model="form.password2" type="password" placeholder="确认密码" />
<p v-if="errors.password2" class="error">{{ errors.password2 }}</p>
</template>
<button type="submit">{{ isLogin ? '登录' : '注册' }}</button>
</form>
<button type="button" @click="isLogin = !isLogin">
{{ isLogin ? '去注册' : '去登录' }}
</button>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const isLogin = ref(true)
const form = reactive({
username: '',
password: '',
password2: ''
})
const errors = ref({})
function submit() {
errors.value = {}
if (!form.username) errors.value.username = '请输入用户名'
if (!form.password) errors.value.password = '请输入密码'
if (!isLogin.value) {
if (form.password !== form.password2) errors.value.password2 = '两次密码不一致'
}
if (Object.keys(errors.value).length > 0) return
if (isLogin.value) {
console.log('登录', form.username)
} else {
console.log('注册', form.username)
}
}
</script>
<style scoped>
.auth-form { max-width: 320px; margin: 20px auto; }
.error { color: red; font-size: 12px; margin: 4px 0; }
</style>
四、案例四:商品列表 + 搜索 + 加入购物车
4.1 需求
- 展示商品列表(名称、价格);上方搜索框按名称过滤。
- 每项有“加入购物车”按钮;购物车显示已加数量和总价。
4.2 技术点
- v-for 渲染商品;computed 做搜索过滤。
- 购物车用 ref 存对象 { 商品id: 数量 } 或数组 { id, name, price, qty }。
- computed 算总价、总件数。
4.3 完整示例
<template>
<div class="shop">
<h2>商品列表</h2>
<input v-model.trim="keyword" placeholder="搜索商品" />
<ul class="product-list">
<li v-for="p in filteredProducts" :key="p.id" class="product">
<span>{{ p.name }}</span>
<span>¥{{ p.price }}</span>
<button @click="addCart(p)">加入购物车</button>
</li>
</ul>
<h3>购物车({{ cartCount }} 件,共 ¥{{ cartTotal }})</h3>
<ul v-if="cartCount > 0">
<li v-for="item in cartList" :key="item.id">
{{ item.name }} x {{ item.qty }} = ¥{{ (item.price * item.qty).toFixed(2) }}
<button @click="removeCart(item.id)">移除</button>
</li>
</ul>
<p v-else>购物车为空</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const keyword = ref('')
const products = ref([
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 },
{ id: 3, name: '橙子', price: 4 }
])
const cart = ref([])
const filteredProducts = computed(() => {
const k = keyword.value.toLowerCase()
if (!k) return products.value
return products.value.filter((p) => p.name.toLowerCase().includes(k))
})
function addCart(p) {
const item = cart.value.find((x) => x.id === p.id)
if (item) item.qty++
else cart.value.push({ ...p, qty: 1 })
}
function removeCart(id) {
cart.value = cart.value.filter((x) => x.id !== id)
}
const cartList = computed(() => cart.value)
const cartCount = computed(() => cart.value.reduce((sum, x) => sum + x.qty, 0))
const cartTotal = computed(() =>
cart.value.reduce((sum, x) => sum + x.price * x.qty, 0).toFixed(2)
)
</script>
<style scoped>
.shop { max-width: 500px; margin: 20px auto; }
.product-list { list-style: none; padding: 0; }
.product { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; }
</style>
五、案例五:Tab 切换 + KeepAlive 缓存
5.1 需求
- 多个 Tab(如“首页”“列表”“我的”),点击切换内容。
- 切换走的 Tab 内容不销毁,再切回来时保留状态(用 KeepAlive)。
5.2 技术点
- v-for 或写死几个 Tab;currentTab 存当前 tab 的 id 或组件名。
- 动态组件;KeepAlive 包住以缓存。
5.3 完整示例
<!-- App.vue -->
<template>
<div class="tab-app">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: currentTab === tab.id }"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</div>
</template>
<script setup>
import { ref } from 'vue'
import TabHome from './TabHome.vue'
import TabList from './TabList.vue'
import TabUser from './TabUser.vue'
const currentTab = ref('TabHome')
const tabs = [
{ id: 'TabHome', name: '首页' },
{ id: 'TabList', name: '列表' },
{ id: 'TabUser', name: '我的' }
]
</script>
<style scoped>
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tabs .active { font-weight: bold; }
</style>
TabHome.vue(示例):内部有一个输入框,切换走再切回来时内容仍在。
<template>
<div>
<h3>首页</h3>
<input v-model="text" placeholder="输入会被 KeepAlive 保留" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
TabList.vue、TabUser.vue 可类似写一个 + ,并确保在 App 里用 component :is 时能解析到组件(上面用字符串 ‘TabHome’ 需在 components 里注册或改用组件引用)。
更稳妥的写法是用组件引用作为 currentTab:
const currentTab = ref(TabHome)
const tabs = [
{ id: 'home', name: '首页', comp: TabHome },
{ id: 'list', name: '列表', comp: TabList },
{ id: 'user', name: '我的', comp: TabUser }
]
模板里 :is=”tabs.find(t => t.id === currentTab)?.comp” 或先算出当前组件再 :is=”currentComp”。
上面简化成 :is=”currentTab” 时,需在 components 里注册 TabHome 等,并让 currentTab 为组件名字符串(与 name 一致)或组件对象。
六、案例六:主题切换(深色/浅色)
6.1 需求
- 一个按钮切换深色/浅色主题;整页背景和文字颜色随之变化。
6.2 技术点
- ref 存 ‘light’ / ‘dark’;用 computed 或 class 绑定根元素的 class。
- 根元素 :class=”theme” 或 class=”app” :class=”theme”,CSS 里写 .dark、.light 的样式。
6.3 完整示例
<template>
<div class="app" :class="theme">
<h2>主题切换</h2>
<button @click="toggleTheme">当前:{{ theme }},点击切换</button>
<p>这段文字会随主题变色。</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<style scoped>
.app { padding: 20px; min-height: 100vh; transition: background 0.3s, color 0.3s; }
.app.light { background: #fff; color: #333; }
.app.dark { background: #222; color: #eee; }
</style>
七、案例七:弹窗组件(Teleport + v-model)
7.1 需求
- 点击按钮打开弹窗;弹窗内容用 Teleport 挂到 body,避免被父级裁剪。
- 支持关闭(按钮或点击遮罩);父组件用 v-model 控制显示与否。
7.2 技术点
- Teleport to=”body” 包住弹窗 DOM。
- 子组件 defineProps([‘modelValue’])、defineEmits([‘update:modelValue’]),内部关闭时 emit(‘update:modelValue’, false)。
- 父组件 v-model=”show” 即 :modelValue=”show” + @update:modelValue=”show = $event”。
7.3 完整示例
Modal.vue:
<template>
<Teleport to="body">
<div v-if="modelValue" class="modal-mask" @click.self="close">
<div class="modal-box">
<slot></slot>
<button @click="close">关闭</button>
</div>
</div>
</Teleport>
</template>
<script setup>
defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue'])
function close() {
emit('update:modelValue', false)
}
</script>
<style scoped>
.modal-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-box {
background: #fff;
padding: 24px;
border-radius: 8px;
min-width: 300px;
}
</style>
使用:
<template>
<button @click="show = true">打开弹窗</button>
<Modal v-model="show">
<p>弹窗内容</p>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const show = ref(false)
</script>
八、案例小结与学习顺序建议
| 案例 | 主要知识点 |
|---|---|
| Todo List | v-model、v-for、computed、@click、筛选 |
| 多计数器 | v-for、computed、数组增删 |
| 登录/注册 | 表单、v-if 切换、校验、errors |
| 商品+购物车 | v-for、computed 过滤与汇总、数组/对象状态 |
| Tab+KeepAlive | 动态组件、KeepAlive |
| 主题切换 | :class、ref、简单状态 |
| 弹窗 | Teleport、v-model、插槽、emit |
建议顺序: 先做 Todo 和 计数器,再做 表单 和 商品+购物车,最后做 Tab、主题、弹窗。
每个案例都可以先照抄跑通,再自己改:加字段、加校验、拆成子组件、用 Pinia 存购物车等。
结合《Vue3指令》《Vue3组件》《Vue3表单》《Vue3计算属性》等文档查阅不会的语法,会掌握得更牢。祝你学习顺利。