Appearance
章节5 — 模块综合实战
一、章节概述
本章是模块二的综合实战环节,学员将运用前四章所学知识完成两个完整项目:一个企业官网响应式页面(HTML5 + CSS3 Flex/Grid + 媒体查询),和一个Todo 待办事项 Vue 应用(Vue3 + Pinia + Router)。通过从零到一的完整开发流程,巩固前端工程化思维。
二、学习目标
| 目标分类 | 具体目标 |
|---|---|
| 知识目标 | 理解从设计稿到代码的前端工作流;掌握 SPA 应用的项目结构与路由设计 |
| 技能目标 | 能独立完成响应式企业官网页面开发;能使用 Vue3 + Pinia + Router 构建完整 SPA |
| 素质目标 | 培养项目组织、代码规范和自测习惯,建立前端职业自信 |
三、实战项目一:企业官网响应式页面
3.1 项目需求
项目名称:星河科技 — 企业品牌官网
设计需求:
- 共 5 个区块:顶部导航(Nav)、首屏 Banner(Hero)、业务介绍(Services)、团队展示(Team)、底部信息(Footer)
- 桌面端 ≥ 1024px 显示完整布局
- 平板端 768~1023px 调整为双栏
- 手机端 < 768px 改为单列堆叠,导航折叠为汉堡菜单
3.2 项目结构
corporate-website/
├── index.html
├── css/
│ └── style.css
└── images/
├── hero-bg.jpg
├── service-1.svg
├── service-2.svg
├── service-3.svg
└── team-1.jpg, team-2.jpg, team-3.jpg3.3 代码实现
index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>星河科技 — 数字化解决方案专家</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<!-- 顶部导航 -->
<header class="header">
<div class="container header__inner">
<a href="#" class="logo">✨ 星河科技</a>
<button class="hamburger" id="hamburger" aria-label="菜单">☰</button>
<nav class="nav" id="nav">
<ul class="nav__list">
<li><a href="#hero" class="nav__link active">首页</a></li>
<li><a href="#services" class="nav__link">服务</a></li>
<li><a href="#team" class="nav__link">团队</a></li>
<li><a href="#contact" class="nav__link">联系</a></li>
</ul>
</nav>
</div>
</header>
<!-- 首屏 Banner -->
<section id="hero" class="hero">
<div class="container hero__content">
<h1 class="hero__title">用技术驱动商业未来</h1>
<p class="hero__desc">我们为企业提供一站式数字化解决方案,助力业务增长</p>
<a href="#services" class="btn btn--primary">了解服务</a>
</div>
</section>
<!-- 业务介绍 -->
<section id="services" class="services section">
<div class="container">
<h2 class="section__title">核心业务</h2>
<div class="services__grid">
<div class="service-card">
<div class="service-card__icon">🌐</div>
<h3>Web 开发</h3>
<p>响应式网站、企业门户、电商平台全栈开发</p>
</div>
<div class="service-card">
<div class="service-card__icon">📱</div>
<h3>移动应用</h3>
<p>跨平台 App 开发,iOS / Android 全覆盖</p>
</div>
<div class="service-card">
<div class="service-card__icon">☁️</div>
<h3>云服务</h3>
<p>架构上云、容器化部署、性能优化</p>
</div>
</div>
</div>
</section>
<!-- 团队展示 -->
<section id="team" class="team section">
<div class="container">
<h2 class="section__title">核心团队</h2>
<div class="team__grid">
<article class="team-card">
<img src="images/team-1.jpg" alt="张三" class="team-card__photo" />
<h3>张三</h3>
<p class="team-card__role">CEO & 创始人</p>
</article>
<article class="team-card">
<img src="images/team-2.jpg" alt="李四" class="team-card__photo" />
<h3>李四</h3>
<p class="team-card__role">技术总监</p>
</article>
<article class="team-card">
<img src="images/team-3.jpg" alt="王五" class="team-card__photo" />
<h3>王五</h3>
<p class="team-card__role">产品经理</p>
</article>
</div>
</div>
</section>
<!-- 底部 -->
<footer id="contact" class="footer">
<div class="container footer__inner">
<div class="footer__info">
<p>© 2025 星河科技. All rights reserved.</p>
<p>联系方式:contact@star-tech.com</p>
</div>
<div class="footer__social">
<a href="#" aria-label="微信">💬 微信</a>
<a href="#" aria-label="微博">📢 微博</a>
</div>
</div>
</footer>
<script>
// 汉堡菜单交互
document.getElementById("hamburger").addEventListener("click", () => {
document.getElementById("nav").classList.toggle("nav--open");
});
// 滚动高亮当前导航
const sections = document.querySelectorAll("section[id]");
window.addEventListener("scroll", () => {
const scrollY = window.scrollY;
sections.forEach((sec) => {
const top = sec.offsetTop - 100;
const id = sec.getAttribute("id");
if (scrollY >= top) {
document.querySelectorAll(".nav__link").forEach((link) => {
link.classList.toggle("active", link.getAttribute("href") === `#${id}`);
});
}
});
});
</script>
</body>
</html>css/style.css
css
/* ==================== Reset & Base ==================== */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
color: #333;
line-height: 1.6;
}
a {
text-decoration: none;
color: inherit;
}
ul {
list-style: none;
}
img {
max-width: 100%;
height: auto;
display: block;
}
/* ==================== Utility ==================== */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.section {
padding: 80px 0;
}
.section__title {
text-align: center;
font-size: 2rem;
margin-bottom: 48px;
position: relative;
}
.section__title::after {
content: "";
display: block;
width: 60px;
height: 4px;
background: #42b883;
margin: 12px auto 0;
border-radius: 2px;
}
.btn {
display: inline-block;
padding: 12px 32px;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn--primary {
background: #42b883;
color: #fff;
}
.btn--primary:hover {
background: #38a376;
transform: translateY(-2px);
}
/* ==================== Header ==================== */
.header {
position: fixed;
top: 0;
width: 100%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
z-index: 1000;
}
.header__inner {
display: flex;
justify-content: space-between;
align-items: center;
height: 64px;
}
.logo {
font-size: 1.4rem;
font-weight: 700;
color: #42b883;
}
.nav__list {
display: flex;
gap: 24px;
}
.nav__link {
padding: 8px 0;
border-bottom: 2px solid transparent;
transition: border-color 0.3s;
}
.nav__link:hover,
.nav__link.active {
border-bottom-color: #42b883;
color: #42b883;
}
.hamburger {
display: none;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
/* ==================== Hero ==================== */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding-top: 64px;
}
.hero__title {
font-size: clamp(2rem, 5vw, 3.5rem);
margin-bottom: 16px;
}
.hero__desc {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 32px;
}
/* ==================== Services (Grid) ==================== */
.services__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
.service-card {
text-align: center;
padding: 32px 24px;
border-radius: 12px;
background: #f8f9fa;
transition: transform 0.3s, box-shadow 0.3s;
}
.service-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.service-card__icon {
font-size: 3rem;
margin-bottom: 16px;
}
.service-card h3 {
margin-bottom: 8px;
}
/* ==================== Team (Flex) ==================== */
.team__grid {
display: flex;
gap: 32px;
justify-content: center;
flex-wrap: wrap;
}
.team-card {
flex: 0 0 280px;
text-align: center;
padding: 24px;
border-radius: 12px;
background: #f8f9fa;
}
.team-card__photo {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto 16px;
}
.team-card__role {
color: #666;
font-size: 0.9rem;
}
/* ==================== Footer ==================== */
.footer {
background: #1a1a2e;
color: #ccc;
padding: 40px 0;
}
.footer__inner {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer__social {
display: flex;
gap: 16px;
}
.footer__social a {
transition: color 0.3s;
}
.footer__social a:hover {
color: #42b883;
}
/* ==================== 响应式设计 ==================== */
/* 平板端 768~1023px */
@media (max-width: 1023px) {
.services__grid {
grid-template-columns: repeat(2, 1fr);
}
.team-card {
flex: 0 0 calc(50% - 16px);
}
}
/* 手机端 < 768px */
@media (max-width: 767px) {
.section {
padding: 48px 0;
}
.hamburger {
display: block;
}
.nav {
display: none;
position: absolute;
top: 64px;
left: 0;
width: 100%;
background: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.nav--open {
display: block;
}
.nav__list {
flex-direction: column;
gap: 0;
}
.nav__link {
display: block;
padding: 16px 24px;
border-bottom: 1px solid #eee;
}
.services__grid {
grid-template-columns: 1fr;
}
.team-card {
flex: 0 0 100%;
}
.footer__inner {
flex-direction: column;
gap: 16px;
text-align: center;
}
}
/* 桌面端 ≥ 1024px 已包含在基础样式中(移动优先的备用方案) */四、实战项目二:Todo 待办事项 Vue 应用
4.1 项目需求
项目名称:TodoMaster — 待办事项管理应用
功能需求:
- 用户可添加、删除、切换完成状态的待办事项
- 支持按「全部 / 进行中 / 已完成」筛选
- 每个待办事项可编辑标题
- 数据存储在 Pinia 中持久化到 localStorage
- 使用 Vue Router 实现页面切换(主页 + 关于页)
- 带有统计功能(总数、已完成数、完成率)
4.2 项目结构
todo-master/
├── index.html
├── src/
│ ├── main.js
│ ├── App.vue
│ ├── router/
│ │ └── index.js
│ ├── stores/
│ │ └── todo.js
│ ├── views/
│ │ ├── HomeView.vue
│ │ └── AboutView.vue
│ ├── components/
│ │ ├── TodoHeader.vue
│ │ ├── TodoInput.vue
│ │ ├── TodoList.vue
│ │ ├── TodoItem.vue
│ │ └── TodoFilter.vue
│ └── assets/
│ └── main.css
├── package.json
└── vite.config.js4.3 代码实现
src/main.js
javascript
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";
import "./assets/main.css";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");src/router/index.js
javascript
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/",
name: "Home",
component: () => import("@/views/HomeView.vue"),
meta: { title: "TodoMaster — 待办事项" },
},
{
path: "/about",
name: "About",
component: () => import("@/views/AboutView.vue"),
meta: { title: "关于 TodoMaster" },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 全局前置守卫 — 设置页面标题
router.beforeEach((to) => {
document.title = to.meta?.title || "TodoMaster";
});
export default router;src/stores/todo.js
javascript
import { defineStore } from "pinia";
import { ref, computed, watch } from "vue";
// 从 localStorage 读取数据
function loadTodos() {
try {
const saved = localStorage.getItem("todo-master");
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
}
export const useTodoStore = defineStore("todo", () => {
// ===== State =====
const todos = ref(loadTodos());
const filter = ref("all"); // "all" | "active" | "completed"
// ===== Getter =====
const filteredTodos = computed(() => {
switch (filter.value) {
case "active":
return todos.value.filter((t) => !t.done);
case "completed":
return todos.value.filter((t) => t.done);
default:
return todos.value;
}
});
const stats = computed(() => {
const total = todos.value.length;
const done = todos.value.filter((t) => t.done).length;
return {
total,
done,
active: total - done,
rate: total > 0 ? Math.round((done / total) * 100) : 0,
};
});
// ===== Actions =====
function addTodo(text) {
const trimmed = text.trim();
if (!trimmed) return;
todos.value.push({
id: Date.now(),
text: trimmed,
done: false,
createdAt: new Date().toISOString(),
});
}
function toggleTodo(id) {
const todo = todos.value.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}
function removeTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id);
}
function editTodo(id, newText) {
const todo = todos.value.find((t) => t.id === id);
if (todo) todo.text = newText.trim() || todo.text;
}
function clearCompleted() {
todos.value = todos.value.filter((t) => !t.done);
}
// ===== 持久化 =====
watch(
todos,
(val) => {
localStorage.setItem("todo-master", JSON.stringify(val));
},
{ deep: true }
);
return {
todos,
filter,
filteredTodos,
stats,
addTodo,
toggleTodo,
removeTodo,
editTodo,
clearCompleted,
};
});src/App.vue
vue
<script setup>
import TodoHeader from "./components/TodoHeader.vue";
</script>
<template>
<div class="app">
<TodoHeader />
<main class="container">
<router-view />
</main>
</div>
</template>src/components/TodoHeader.vue
vue
<script setup>
</script>
<template>
<header class="app-header">
<div class="container header-content">
<router-link to="/" class="logo">✅ TodoMaster</router-link>
<nav class="header-nav">
<router-link to="/" class="nav-item" active-class="active">
首页
</router-link>
<router-link to="/about" class="nav-item" active-class="active">
关于
</router-link>
</nav>
</div>
</header>
</template>
<style scoped>
.app-header {
background: #42b883;
color: #fff;
padding: 16px 0;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.4rem;
font-weight: 700;
color: #fff;
}
.header-nav {
display: flex;
gap: 20px;
}
.nav-item {
color: rgba(255, 255, 255, 0.85);
padding: 4px 0;
border-bottom: 2px solid transparent;
transition: 0.3s;
}
.nav-item:hover,
.nav-item.active {
color: #fff;
border-bottom-color: #fff;
}
</style>src/views/HomeView.vue
vue
<script setup>
import { useTodoStore } from "@/stores/todo";
import { storeToRefs } from "pinia";
import TodoInput from "@/components/TodoInput.vue";
import TodoFilter from "@/components/TodoFilter.vue";
import TodoList from "@/components/TodoList.vue";
const store = useTodoStore();
const { stats, filteredTodos } = storeToRefs(store);
</script>
<template>
<div class="home">
<div class="stats-bar">
<span>总计:<strong>{{ stats.total }}</strong></span>
<span>已完成:<strong>{{ stats.done }}</strong></span>
<span>完成率:<strong>{{ stats.rate }}%</strong></span>
</div>
<TodoInput />
<TodoFilter />
<TodoList :todos="filteredTodos" />
</div>
</template>
<style scoped>
.home {
max-width: 600px;
margin: 0 auto;
padding: 32px 0;
}
.stats-bar {
display: flex;
gap: 24px;
justify-content: center;
margin-bottom: 24px;
padding: 12px 24px;
background: #f8f9fa;
border-radius: 8px;
font-size: 0.95rem;
}
.stats-bar strong {
color: #42b883;
}
</style>src/views/AboutView.vue
vue
<script setup>
</script>
<template>
<div class="about">
<h1>关于 TodoMaster</h1>
<p>TodoMaster 是一个基于 Vue 3 + Pinia + Vue Router 构建的待办事项管理应用。</p>
<section class="tech-stack">
<h2>技术栈</h2>
<ul>
<li>⚡ Vue 3 组合式 API + Script Setup</li>
<li>📦 Pinia 状态管理</li>
<li>🚦 Vue Router 路由管理</li>
<li>💾 localStorage 数据持久化</li>
<li>📱 响应式设计</li>
</ul>
</section>
</div>
</template>
<style scoped>
.about {
max-width: 600px;
margin: 0 auto;
padding: 48px 0;
text-align: center;
}
.about h1 {
margin-bottom: 16px;
color: #42b883;
}
.tech-stack {
margin-top: 32px;
text-align: left;
background: #f8f9fa;
padding: 24px 32px;
border-radius: 8px;
}
.tech-stack h2 {
margin-bottom: 12px;
}
.tech-stack li {
padding: 6px 0;
}
</style>src/components/TodoInput.vue
vue
<script setup>
import { ref } from "vue";
import { useTodoStore } from "@/stores/todo";
const store = useTodoStore();
const text = ref("");
function submit() {
store.addTodo(text.value);
text.value = "";
}
</script>
<template>
<form class="todo-input" @submit.prevent="submit">
<input
v-model="text"
type="text"
placeholder="输入新的待办事项..."
class="input-field"
/>
<button type="submit" class="btn-add">添加</button>
</form>
</template>
<style scoped>
.todo-input {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.input-field {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
}
.input-field:focus {
outline: none;
border-color: #42b883;
}
.btn-add {
padding: 12px 24px;
background: #42b883;
color: #fff;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.btn-add:hover {
background: #38a376;
}
</style>src/components/TodoFilter.vue
vue
<script setup>
import { useTodoStore } from "@/stores/todo";
import { storeToRefs } from "pinia";
const store = useTodoStore();
const { filter } = storeToRefs(store);
const filters = [
{ key: "all", label: "全部" },
{ key: "active", label: "进行中" },
{ key: "completed", label: "已完成" },
];
</script>
<template>
<div class="todo-filter">
<button
v-for="f in filters"
:key="f.key"
:class="['filter-btn', { active: filter === f.key }]"
@click="filter = f.key"
>
{{ f.label }}
</button>
<button class="clear-btn" @click="store.clearCompleted()">
清除已完成
</button>
</div>
</template>
<style scoped>
.todo-filter {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.filter-btn {
padding: 6px 16px;
border: 2px solid #e0e0e0;
border-radius: 20px;
background: #fff;
cursor: pointer;
transition: 0.3s;
}
.filter-btn.active {
border-color: #42b883;
background: #42b883;
color: #fff;
}
.clear-btn {
margin-left: auto;
padding: 6px 16px;
border: none;
border-radius: 20px;
background: #ff6b6b;
color: #fff;
cursor: pointer;
font-size: 0.85rem;
}
</style>src/components/TodoList.vue
vue
<script setup>
import TodoItem from "./TodoItem.vue";
defineProps({
todos: { type: Array, required: true },
});
</script>
<template>
<div class="todo-list">
<p v-if="todos.length === 0" class="empty-hint">
✨ 暂无待办事项,添加一条吧!
</p>
<TransitionGroup name="list" tag="ul" class="todo-ul">
<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" />
</TransitionGroup>
</div>
</template>
<style scoped>
.todo-ul {
list-style: none;
}
.empty-hint {
text-align: center;
color: #999;
padding: 48px 0;
}
/* 列表动画 */
.list-enter-active,
.list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.4s ease;
}
</style>src/components/TodoItem.vue
vue
<script setup>
import { ref } from "vue";
import { useTodoStore } from "@/stores/todo";
const props = defineProps({
todo: { type: Object, required: true },
});
const store = useTodoStore();
const editing = ref(false);
const editText = ref("");
function startEdit() {
editing.value = true;
editText.value = props.todo.text;
}
function saveEdit() {
store.editTodo(props.todo.id, editText.value);
editing.value = false;
}
function cancelEdit() {
editing.value = false;
}
</script>
<template>
<li :class="['todo-item', { done: todo.done }]">
<input
type="checkbox"
:checked="todo.done"
@change="store.toggleTodo(todo.id)"
class="todo-checkbox"
/>
<!-- 编辑模式 -->
<template v-if="editing">
<input
v-model="editText"
type="text"
class="edit-input"
@keyup.enter="saveEdit"
@keyup.escape="cancelEdit"
ref="editInput"
/>
<button class="btn-save" @click="saveEdit">保存</button>
<button class="btn-cancel" @click="cancelEdit">取消</button>
</template>
<!-- 展示模式 -->
<template v-else>
<span class="todo-text" @dblclick="startEdit">{{ todo.text }}</span>
<div class="todo-actions">
<button class="btn-edit" @click="startEdit">✏️</button>
<button class="btn-delete" @click="store.removeTodo(todo.id)">🗑️</button>
</div>
</template>
</li>
</template>
<style scoped>
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
margin-bottom: 8px;
transition: box-shadow 0.3s;
}
.todo-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.todo-item.done .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #42b883;
}
.todo-text {
flex: 1;
cursor: pointer;
}
.todo-actions {
display: flex;
gap: 4px;
}
.btn-edit,
.btn-delete,
.btn-save,
.btn-cancel {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
}
.btn-save {
background: #42b883;
color: #fff;
border-radius: 4px;
}
.btn-cancel {
background: #eee;
border-radius: 4px;
}
.edit-input {
flex: 1;
padding: 6px 12px;
border: 2px solid #42b883;
border-radius: 4px;
font-size: 1rem;
}
</style>src/assets/main.css(全局样式)
css
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: #f0f2f5;
color: #333;
line-height: 1.6;
}
a {
text-decoration: none;
color: inherit;
}
ul {
list-style: none;
}
.container {
max-width: 768px;
margin: 0 auto;
padding: 0 20px;
}可选:vite.config.js
javascript
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "url";
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});package.json(依赖)
json
{
"name": "todo-master",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"pinia": "^2.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}4.4 启动项目
bash
# 1. 进入项目目录
cd todo-master
# 2. 安装依赖
npm install
# 3. 启动开发服务器
npm run dev
# → 浏览器访问 http://localhost:5173五、实战要点总结
5.1 企业官网要点
| 技术点 | 应用位置 |
|---|---|
| HTML5 语义化标签 | <header>、<nav>、<section>、<article>、<footer> |
| Flex 布局 | 导航栏、团队卡片行、页脚 |
| Grid 布局 | 业务服务卡片网格 |
| 媒体查询 | 三断点(1024px / 768px)适配 |
| 交互 | 汉堡菜单、滚动高亮导航 |
| CSS 技巧 | clamp() 响应式字号、backdrop-filter 毛玻璃、.section__title::after 装饰线 |
5.2 TodoMaster 要点
| 技术点 | 应用位置 |
|---|---|
| Vue3 组合式 API | 所有组件均使用 <script setup> |
| Pinia | todoStore 管理全部状态,storeToRefs 保持响应式 |
| Vue Router | 主页 + 关于页,全局守卫设置标题 |
| 组件通信 | 父传 props(TodoList → TodoItem),子调 store action |
| 本地持久化 | watch + localStorage 自动保存 |
| 过渡动画 | TransitionGroup 实现列表增删动画 |
| 键盘交互 | 回车提交、Esc 取消编辑 |
六、课后拓展与练习
6.1 企业官网拓展需求
- 为官网添加「联系我们」表单,包含姓名、邮箱、留言,用 Fetch POST 到
https://jsonplaceholder.typicode.com/posts并显示提交成功提示 - 添加「回到顶部」按钮,页面滚动超过 300px 时显示
- 使用 CSS
@keyframes为 Hero 区域的标题和按钮添加淡入上升动画
6.2 TodoMaster 拓展需求
- 添加「截止日期」功能:每个待办可设置到期日,过期时自动标红
- 按创建时间或到期日排序(正序/倒序切换)
- 添加「分类标签」:每个待办可选 Work / Personal / Study,并按标签筛选
- 使用
localforage库替代 localStorage 实现更可靠的存储
6.3 综合思考题
- 如果将官网中的服务卡片从 Grid 改为 Flex,代码需要如何调整?哪种方案更适合?
- 如果 TodoMaster 需要支持多用户登录和云端同步,Pinia 的代码该如何改造?
- 分析企业官网和 TodoMaster 两个项目中,有哪些代码可以抽取为可复用的工具函数或组合式函数?
恭喜你完成模块二 — Web 前端开发的全部学习! 🎉
从 HTML5 语义化标签到 CSS3 布局,从 JavaScript 核心语法到 DOM 事件,从 Vue3 组件化到完整的实战项目,你已经掌握了现代 Web 前端开发的必备技能。接下来进入模块三 — Python 后端开发,继续你的全栈之旅!