實戰小行星 — To-Do List 就是把前 14 天學到的核心招式整合起來,做一個可用的小作品:
新增任務、完成/取消完成、刪除、搜尋/過濾、統計數量,還能把資料存到瀏覽器(重整不會不見)。
經過第12、13天實作練習後,肯定會覺得今天的很簡單哈哈哈,那我們正式開始吧!
下面給是這次的目標、學到的重點、讓我們一步一步完成版本(也可直接貼進 Vite + Vue3 專案的 src/App.vue 跑起來)。
v-model
(表單雙向綁定)v-for
(任務列表)v-if
/ v-show
(空狀態、提示)v-on
:class
動態樣式(完成劃線…)computed
(過濾/統計) watchEffect
或 watch
(同步 localStorage)onMounted
讀取初始值src/App.vue
<template>
<main class="todo">
<h1>🪨 小行星任務清單</h1>
<!-- 新增任務 -->
<form class="add" @submit.prevent="addTask">
<input
v-model.trim="draft"
placeholder="輸入任務,例如:校對艦隊日誌"
maxlength="80"
/>
<button class="primary">加入</button>
</form>
<p v-if="error" class="error">{{ error }}</p>
<!-- 搜尋與過濾 -->
<section class="filters">
<input v-model.trim="keyword" placeholder="搜尋關鍵字…" />
<div class="tabs">
<button
v-for="opt in filters"
:key="opt.value"
:class="{ active: status === opt.value }"
@click="status = opt.value"
>
{{ opt.label }}
</button>
</div>
</section>
<!-- 統計 -->
<p class="stats">
總數:{{ total }} 未完成:{{ remaining }} 已完成:{{ done }}
</p>
<!-- 清單 -->
<ul class="list" v-if="filtered.length">
<li v-for="t in filtered" :key="t.id" class="item">
<label>
<input type="checkbox" v-model="t.done" />
<span :class="{ done: t.done }">{{ t.text }}</span>
</label>
<button class="ghost" @click="removeTask(t.id)">刪除</button>
</li>
</ul>
<p v-else class="empty">目前沒有符合條件的任務 ✨</p>
</main>
</template>
<script setup>
import { ref, computed, watchEffect, onMounted } from 'vue'
const STORAGE_KEY = 'asteroid-todos'
// 狀態
const todos = ref([])
const draft = ref('')
const error = ref('')
const keyword = ref('')
const status = ref('all') // all | active | completed
// 讀取 localStorage
onMounted(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) todos.value = JSON.parse(raw)
} catch {}
})
// 自動同步到 localStorage
watchEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos.value))
})
// 新增
function addTask() {
error.value = ''
if (!draft.value) {
error.value = '請輸入任務內容'
return
}
todos.value.unshift({
id: Date.now(),
text: draft.value,
done: false
})
draft.value = ''
}
// 刪除
function removeTask(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
// 過濾與搜尋
const filters = [
{ label: '全部', value: 'all' },
{ label: '待辦', value: 'active' },
{ label: '已完成', value: 'completed' }
]
const filtered = computed(() => {
const kw = keyword.value.toLowerCase()
return todos.value
.filter(t => {
if (status.value === 'active') return !t.done
if (status.value === 'completed') return t.done
return true
})
.filter(t => (kw ? t.text.toLowerCase().includes(kw) : true))
})
// 統計
const total = computed(() => todos.value.length)
const done = computed(() => todos.value.filter(t => t.done).length)
const remaining = computed(() => total.value - done.value)
</script>
<style scoped>
:root { color-scheme: dark; }
.todo { max-width: 720px; margin: 48px auto; font: 16px/1.6 ui-sans-serif, system-ui; color: #ffffff; }
h1 { margin: 0 0 16px; color: #4279b0ff; }
.add { display: flex; gap: 8px; }
.add input { flex: 1; padding: 10px 12px; border-radius: 10px; border: 1px solid #334155; background:#0b1020; color:#e2e8f0; }
button { padding: 8px 12px; border-radius: 10px; border: 1px solid #334155; background:#1f2a44; color:#e2e8f0; cursor:pointer; }
button.primary { background:#2563eb; border-color:#1d4ed8; }
.error { color:#fca5a5; margin:6px 0 0; }
.filters { display:flex; gap:12px; align-items:center; margin:14px 0; }
.filters input { flex: 1; padding: 8px 10px; border-radius: 8px; border: 1px solid #334155; background:#0b1020; color:#e2e8f0; }
.tabs { display:flex; gap:8px; }
.tabs .active { outline:2px solid #dcc891ff; }
.stats { color:#94a3b8; margin-top:-2px; }
.list { list-style:none; padding:0; margin:12px 0 0; }
.item { display:flex; justify-content:space-between; align-items:center; padding:10px 12px; border:1px solid #24324d; border-radius:12px; background:#0f172a; margin-bottom:8px; }
.item .done { text-decoration: line-through; opacity:.6; }
.empty { color:#94a3b8; }
</style>
@vueuse/motion
或 SortableJS
)<TodoItem>
元件(把操作列做成 #actions
插槽)經過今天,我們更明白前面幾天學會的概念與如何運用!明天我們將了解Vue Router的概念~
參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3