Vue期末作业-代办事项案例

作者:犯困乐 发布时间: 2026-06-15 阅读量:8 评论数:0

1. 技术栈

分类 技术 版本
框架 Vue 3.5+
构建工具 Vite 8.0
样式 Scoped CSS -
图标 内联 SVG -

2. 核心技术要点

2.1 Composition API

使用 Vue 3 Composition API 进行状态管理:

import { ref, computed } from 'vue'

状态定义

  • newTask - 输入框绑定值
  • filter - 当前筛选模式(all/active/completed
  • nextId - 任务ID自增器
  • todos - 任务列表数据

2.2 计算属性

filteredTodos - 响应式筛选:

const filteredTodos = computed(() => {
  if (filter.value === 'active') return todos.value.filter(t => !t.completed)
  if (filter.value === 'completed') return todos.value.filter(t => t.completed)
  return todos.value
})

remainingCount - 未完成计数:

const remainingCount = computed(() => todos.value.filter(t => !t.completed).length)

2.3 响应式数据操作

添加任务 - unshift 插入头部:

todos.value.unshift({ id: nextId.value++, text: text, completed: false })

删除任务 - filter 创建新数组:

todos.value = todos.value.filter(t => t.id !== id)

切换状态 - 直接修改响应式对象:

const todo = todos.value.find(t => t.id === id)
todo.completed = !todo.completed

3. 模板渲染

3.1 列表渲染

<li
  v-for="todo in filteredTodos"
  :key="todo.id"
  class="todo-item"
  :class="{ completed: todo.completed }"
>

关键点

  • :key="todo.id" 优化 Diff 算法性能
  • :class 动态绑定完成状态样式

3.2 自定义复选框

<label class="todo-checkbox-wrapper">
  <input type="checkbox" class="todo-checkbox" :checked="todo.completed" />
  <span class="todo-checkmark">
    <svg v-if="todo.completed" ...>...</svg>
  </span>
</label>

实现原理

  • 隐藏原生 checkbox(opacity: 0
  • 使用 <span> + SVG 自定义外观
  • 利用 <label> 绑定点击事件

3.3 条件渲染

  • 删除按钮默认隐藏,hover 时显示
  • SVG 勾选图标仅在完成状态渲染

4. 样式系统

4.1 颜色规范

用途 色值
主背景 #ffffff
标题栏 #ffe9cc
输入区 #fff5e6
悬停态 #fffaf0
主强调色 #b83a2b
完成态 #4caf50

4.2 交互状态

元素 默认 Hover Active/Focus
输入框 浅边框 边框加深 红边框+外发光
列表项 白色 浅橙背景 -
复选框 灰色圈 红圈 绿圈+勾
筛选按钮 白底浅边 浅橙底 红底白字

4.3 动画过渡

统一使用 0.2s ease 过渡:

  • 边框颜色
  • 背景颜色
  • 文字颜色
  • 透明度
  • 阴影效果

5. 响应式适配

@media (max-width: 600px) {
  .todo-title { font-size: 22px; }
  .todo-footer { flex-direction: column; }
}

完整案例

<script setup>
import { ref, computed } from 'vue'

const newTask = ref('')
const filter = ref('all')
const nextId = ref(4)

const todos = ref([
  { id: 1, text: '晨练', completed: false },
  { id: 2, text: '练书法', completed: true },
  { id: 3, text: '读书', completed: false }
])

const filteredTodos = computed(() => {
  if (filter.value === 'active') {
    return todos.value.filter(t => !t.completed)
  } else if (filter.value === 'completed') {
    return todos.value.filter(t => t.completed)
  }
  return todos.value
})

const remainingCount = computed(() => {
  return todos.value.filter(t => !t.completed).length
})

const addTodo = () => {
  const text = newTask.value.trim()
  if (text === '') return
  todos.value.unshift({
    id: nextId.value++,
    text: text,
    completed: false
  })
  newTask.value = ''
}

const removeTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

const toggleTodo = (id) => {
  const todo = todos.value.find(t => t.id === id)
  if (todo) {
    todo.completed = !todo.completed
  }
}

const setFilter = (f) => {
  filter.value = f
}
</script>

<template>
  <div class="todo-app">
    <div class="todo-wrapper">
      <header class="todo-header">
        <h1 class="todo-title">待办事项-25计应3班任佳乐</h1>
      </header>

      <section class="todo-input-section">
        <div class="input-group">
          <input
            type="text"
            class="todo-input"
            v-model.trim="newTask"
            @keyup.enter="addTodo"
            placeholder="请填写任务"
          />
        </div>
      </section>

      <section class="todo-list-section">
        <ul class="todo-list">
          <li
            v-for="todo in filteredTodos"
            :key="todo.id"
            class="todo-item"
            :class="{ completed: todo.completed }"
          >
            <label class="todo-checkbox-wrapper">
              <input
                type="checkbox"
                class="todo-checkbox"
                :checked="todo.completed"
                @change="toggleTodo(todo.id)"
              />
              <span class="todo-checkmark">
                <svg
                  v-if="todo.completed"
                  viewBox="0 0 24 24"
                  width="16"
                  height="16"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="3"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                >
                  <polyline points="4 12 10 18 20 6"></polyline>
                </svg>
              </span>
            </label>
            <span class="todo-text">{{ todo.text }}</span>
            <button
              type="button"
              class="todo-delete"
              @click="removeTodo(todo.id)"
              aria-label="删除任务"
            >
              <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                <line x1="5" y1="5" x2="19" y2="19"></line>
                <line x1="19" y1="5" x2="5" y2="19"></line>
              </svg>
            </button>
          </li>
        </ul>
      </section>

      <footer class="todo-footer">
        <span class="todo-count">共{{ remainingCount }}条未完成任务</span>
        <div class="todo-filters">
          <button
            type="button"
            class="filter-btn"
            :class="{ active: filter === 'all' }"
            @click="setFilter('all')"
          >
            All
          </button>
          <button
            type="button"
            class="filter-btn"
            :class="{ active: filter === 'active' }"
            @click="setFilter('active')"
          >
            Active
          </button>
          <button
            type="button"
            class="filter-btn"
            :class="{ active: filter === 'completed' }"
            @click="setFilter('completed')"
          >
            Completed
          </button>
        </div>
      </footer>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  min-height: 100vh;
  background: #ffffff;
  font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
  color: #333333;
  padding: 24px 12px;
}

.todo-wrapper {
  max-width: 880px;
  margin: 0 auto;
  border: 1px solid #e8d6b8;
  border-radius: 4px;
  overflow: hidden;
  background: #ffffff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}

.todo-header {
  background: #ffe9cc;
  padding: 18px 24px;
  text-align: center;
  border-bottom: 1px solid #e8d6b8;
}

.todo-title {
  margin: 0;
  font-size: 28px;
  font-weight: 600;
  color: #b83a2b;
  letter-spacing: 1px;
}

.todo-input-section {
  background: #fff5e6;
  padding: 14px 24px;
  border-bottom: 1px solid #f0e0c8;
}

.todo-input {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 14px;
  font-size: 15px;
  line-height: 1.5;
  color: #333333;
  background: #ffffff;
  border: 1px solid #d9c8a8;
  border-radius: 3px;
  outline: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.todo-input::placeholder {
  color: #aa9980;
}

.todo-input:hover {
  border-color: #c9a876;
}

.todo-input:focus {
  border-color: #b83a2b;
  box-shadow: 0 0 0 2px rgba(184, 58, 43, 0.12);
}

.todo-list-section {
  background: #ffffff;
}

.todo-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 24px;
  border-bottom: 1px solid #f2ead9;
  transition: background-color 0.2s ease;
}

.todo-item:last-child {
  border-bottom: none;
}

.todo-item:hover {
  background: #fffaf0;
}

.todo-checkbox-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  cursor: pointer;
}

.todo-checkbox {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.todo-checkmark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 2px solid #b8b8b8;
  border-radius: 50%;
  color: #4caf50;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

.todo-checkbox-wrapper .todo-checkmark {
  border: 2px solid #b8b8b8;
  background: #ffffff;
  color: transparent;
}

.todo-item.completed .todo-checkmark {
  border-color: #4caf50;
  background: #ffffff;
  color: #4caf50;
}

.todo-checkbox-wrapper:hover .todo-checkmark {
  border-color: #b83a2b;
}

.todo-item.completed .todo-checkbox-wrapper:hover .todo-checkmark {
  border-color: #3d8b40;
}

.todo-text {
  flex: 1;
  font-size: 15px;
  line-height: 1.5;
  color: #333333;
  user-select: none;
  transition: color 0.2s ease, text-decoration 0.2s ease;
}

.todo-item.completed .todo-text {
  color: #999999;
  text-decoration: line-through;
}

.todo-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 3px;
  color: #b83a2b;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
  flex-shrink: 0;
}

.todo-item:hover .todo-delete,
.todo-delete:focus-visible {
  opacity: 1;
}

.todo-delete:hover {
  background: #fdecec;
  color: #9a2a1e;
}

.todo-delete:active {
  background: #f9d9d9;
}

.todo-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: 14px 24px;
  background: #fffaf0;
  border-top: 1px solid #f0e0c8;
  font-size: 13px;
  color: #666666;
}

.todo-count {
  line-height: 1.5;
}

.todo-filters {
  display: flex;
  gap: 8px;
}

.filter-btn {
  padding: 6px 14px;
  font-size: 13px;
  line-height: 1.4;
  color: #555555;
  background: #ffffff;
  border: 1px solid #d9c8a8;
  border-radius: 3px;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.filter-btn:hover {
  background: #fff5e6;
  border-color: #c9a876;
}

.filter-btn.active {
  color: #ffffff;
  background: #b83a2b;
  border-color: #b83a2b;
}

.filter-btn.active:hover {
  background: #9a2a1e;
  border-color: #9a2a1e;
}

@media (max-width: 600px) {
  .todo-app {
    padding: 12px 6px;
  }
  .todo-header {
    padding: 14px 16px;
  }
  .todo-title {
    font-size: 22px;
  }
  .todo-input-section,
  .todo-item,
  .todo-footer {
    padding-left: 16px;
    padding-right: 16px;
  }
  .todo-footer {
    flex-direction: column;
    align-items: flex-start;
  }
}
</style>

评论