Vue3实战案例

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.vueTabUser.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计算属性》等文档查阅不会的语法,会掌握得更牢。祝你学习顺利。

发表评论