去年鐵人賽我做了一個任務管理系統來練後端,今年決定延續這個作品,挑戰把前兩週學到的 Vue 3 知識整合到一個實際的小專案。
這不只是 CRUD,而是希望在一個小範例中一次練熟 Composition API、資料監控、props/emit 等核心觀念。
watch
更適合指定監聽、watchEffect
則適合快速處理副作用,像自動顯示完成率就是最佳案例。以下分享我如何一步步完成這個整合練習,另外也可以觀察到,在 React 需要額外 state 管理工具的東西,Vue 只靠 computed
和 watchEffect
就能達成具體可以實作如下功能:
src/
├─ App.vue # 主應用
├─ components/
│ ├─ TodoInput.vue # 新增待辦輸入框
│ ├─ TodoItem.vue # 單個待辦項目
│ └─ TodoStats.vue # 統計資訊
<script setup lang="ts">
import { ref, computed, watch, watchEffect } from 'vue'
import TodoInput from './components/TodoInput.vue'
import TodoItem from './components/TodoItem.vue'
import TodoStats from './components/TodoStats.vue'
// 響應式資料
const todos = ref<{ id: number; text: string; done: boolean }[]>([])
// 新增待辦
function addTodo(text: string) {
todos.value.push({
id: Date.now(),
text,
done: false
})
}
// 切換完成狀態
function toggleTodo(id: number) {
const t = todos.value.find(t => t.id === id)
if (t) t.done = !t.done
}
// 移除待辦
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id)
}
// computed: 已完成比例
const completedRatio = computed(() => {
if (todos.value.length === 0) return 0
const done = todos.value.filter(t => t.done).length
return Math.round((done / todos.value.length) * 100)
})
// watch:當 todos 有變化時記錄 log
watch(todos, (newVal) => {
console.log('待辦清單更新 =>', newVal)
}, { deep: true })
// watchEffect:副作用 → 自動顯示當前完成率
watchEffect(() => {
console.log(`當前完成率 => ${completedRatio.value}%`)
})
</script>
<template>
<div class="p-6 max-w-md mx-auto space-y-4">
<h1 class="text-2xl font-bold">Todo List</h1>
<!-- 新增輸入框 -->
<TodoInput @add="addTodo" />
<!-- 待辦清單 -->
<div class="space-y-2">
<TodoItem
v-for="t in todos"
:key="t.id"
v-model:done="t.done"
:text="t.text"
@remove="removeTodo(t.id)"
/>
</div>
<!-- 統計資訊 -->
<TodoStats :total="todos.length" :ratio="completedRatio" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const text = ref('')
const emit = defineEmits<{
(e: 'add', text: string): void
}>()
function add() {
if (text.value.trim() === '') return
emit('add', text.value)
text.value = ''
}
</script>
<template>
<div class="flex space-x-2">
<input
v-model="text"
@keyup.enter="add"
class="border rounded px-2 py-1 flex-1"
placeholder="輸入待辦..."
/>
<button @click="add" class="bg-blue-500 text-white px-3 py-1 rounded">
新增
</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
text: string
done: boolean
}>()
const emit = defineEmits<{
(e: 'update:done', value: boolean): void
(e: 'remove'): void
}>()
// 使用 v-model:done 雙向綁定
</script>
<template>
<div class="flex items-center space-x-2 border rounded px-2 py-1">
<input
type="checkbox"
:checked="props.done"
@change="emit('update:done', !props.done)"
/>
<span :class="{ 'line-through text-gray-500': props.done }">
{{ props.text }}
</span>
<button @click="emit('remove')" class="text-red-500 ml-auto">x</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
total: number
ratio: number
}>()
</script>
<template>
<div class="mt-4 text-gray-600">
<p>總待辦數:{{ props.total }}</p>
<p>完成率:{{ props.ratio }}%</p>
</div>
</template>
這次的小專案就像是 Vue 3 的縮影:
資料一動,畫面自動同步,從資料響應到元件拆分、雙向綁定,再到副作用監控,都一次練到。
下一步我要進入 Composable 的世界。
它和 Vue 3.6 的新底層「Alien Signals」沒有直接關係,但因為新版引擎更快更省記憶體,寫 composable 也能無痛獲得效能紅利唷!