第 6 章:综合实战——打造个人主页¶
📺 本章概览¶
| 项目 | 内容 |
|---|---|
| 学习目标 | 通过阅读一个完整的响应式个人主页项目,综合理解 HTML、CSS、JavaScript 三者的协作方式 |
| 核心比喻 | 精装样板房 —— 将前面学到的所有知识整合成一个完整的、可交付的作品 |
| 预计时长 | 120 分钟 |
| 关键概念 | 项目结构、CSS 变量主题切换、打字机效果、Intersection Observer、表单验证、响应式导航 |
| 实践任务 | 逐段阅读个人主页的完整代码,理解每个模块的实现原理,尝试修改配色和内容 |
恭喜你来到了最后一章!前面五章我们分别学习了 HTML5 结构、CSS3 样式、响应式布局、JavaScript 语法和 DOM 操作。现在,是时候把这些知识**融会贯通**,打造一个真正拿得出手的作品了。
本章我们将从零开始,一步步构建一个**完整的个人主页**,包含以下功能模块:
- 🎨 导航栏(响应式,含移动端汉堡菜单)
- 🦸 英雄区(Hero Section,含打字机效果)
- 📋 关于我(个人简介 + 技能标签)
- 💼 项目展示(卡片网格布局)
- 📬 联系方式(表单 + 表单验证)
- 🌙 深色/浅色主题切换
- 📱 完全响应式(手机、平板、桌面)
6.1 项目概览¶
本章我们将**逐段阅读**一个完整的个人主页项目。这个项目整合了前五章的所有知识,是你理解"HTML + CSS + JS 如何协作"的最佳范例。
你将看到的效果¶
- 深色/浅色主题切换:点击按钮,整个页面配色瞬间改变
- 打字机动画:英雄区的职位描述像打字一样逐字出现
- 响应式布局:手机、平板、桌面三种设备自动适配
- 滚动动画:元素进入视口时优雅地淡入
- 表单验证:实时检查用户输入,给出友好提示
- 平滑导航:点击导航链接平滑滚动到对应区域
6.2 HTML 结构——搭建骨架¶
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<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">
<script src="js/main.js" defer></script>
</head>
<body>
<!-- ========== 导航栏 ========== -->
<header class="header" id="header">
<nav class="nav container">
<a href="#" class="nav-logo">ZhangSan.dev</a>
<ul class="nav-menu" id="nav-menu">
<li><a href="#home" class="nav-link">首页</a></li>
<li><a href="#about" class="nav-link">关于</a></li>
<li><a href="#projects" class="nav-link">项目</a></li>
<li><a href="#contact" class="nav-link">联系</a></li>
</ul>
<div class="nav-actions">
<button class="theme-toggle" id="theme-toggle" aria-label="切换主题">
<span class="theme-icon">🌙</span>
</button>
<button class="hamburger" id="hamburger" aria-label="菜单">
<span></span>
<span></span>
<span></span>
</button>
</div>
</nav>
</header>
<main>
<!-- ========== 英雄区 ========== -->
<section class="hero section" id="home">
<div class="hero-container container">
<div class="hero-content">
<p class="hero-greeting">你好,我是</p>
<h1 class="hero-name">张三</h1>
<p class="hero-title">
<span class="typed-text" id="typed-text"></span>
<span class="typed-cursor">|</span>
</p>
<p class="hero-description">
热爱前端开发,专注于构建优雅、高性能的 Web 应用。
相信代码可以改变世界,一行一行地。
</p>
<div class="hero-buttons">
<a href="#projects" class="btn btn-primary">查看作品</a>
<a href="#contact" class="btn btn-outline">联系我</a>
</div>
</div>
<div class="hero-image">
<div class="hero-avatar">
<img src="images/avatar.jpg" alt="张三的头像" onerror="this.style.display='none'; this.parentElement.innerHTML='<div style=\\'width:200px;height:200px;border-radius:50%;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center;color:white;font-size:48px;\\'>张</div>'">
</div>
</div>
</div>
<a href="#about" class="scroll-down" aria-label="向下滚动">
<span></span>
</a>
</section>
<!-- ========== 关于我 ========== -->
<section class="about section" id="about">
<div class="container">
<h2 class="section-title">关于我</h2>
<p class="section-subtitle">一个热爱技术与生活的开发者</p>
<div class="about-content">
<div class="about-text">
<p>
我是一名拥有 3 年经验的前端开发工程师,目前专注于 React 生态和现代 CSS 技术。
我相信好的用户体验来自于对细节的极致追求。
</p>
<p>
工作之余,我喜欢写技术博客、参与开源项目,也热衷于帮助新人入门前端开发。
这个个人主页就是我用纯 HTML、CSS 和 JavaScript 从零构建的。
</p>
</div>
<div class="skills">
<h3>技术栈</h3>
<div class="skill-tags">
<span class="skill-tag">HTML5</span>
<span class="skill-tag">CSS3</span>
<span class="skill-tag">JavaScript</span>
<span class="skill-tag">React</span>
<span class="skill-tag">Vue.js</span>
<span class="skill-tag">Node.js</span>
<span class="skill-tag">Git</span>
<span class="skill-tag">Webpack</span>
<span class="skill-tag">TypeScript</span>
<span class="skill-tag">Tailwind CSS</span>
</div>
</div>
</div>
</div>
</section>
<!-- ========== 项目展示 ========== -->
<section class="projects section" id="projects">
<div class="container">
<h2 class="section-title">我的项目</h2>
<p class="section-subtitle">一些我最近完成的作品</p>
<div class="project-grid">
<article class="project-card">
<div class="project-image">
<div class="project-placeholder">📊</div>
</div>
<div class="project-info">
<h3>数据可视化仪表盘</h3>
<p>基于 ECharts 的实时数据监控面板,支持多种图表类型和自定义主题。</p>
<div class="project-tags">
<span>React</span>
<span>ECharts</span>
<span>WebSocket</span>
</div>
<div class="project-links">
<a href="#" class="btn btn-small">演示</a>
<a href="#" class="btn btn-small btn-outline">源码</a>
</div>
</div>
</article>
<article class="project-card">
<div class="project-image">
<div class="project-placeholder">🛒</div>
</div>
<div class="project-info">
<h3>电商小程序</h3>
<p>全栈电商应用,包含商品浏览、购物车、订单管理和支付集成。</p>
<div class="project-tags">
<span>Vue.js</span>
<span>Node.js</span>
<span>MongoDB</span>
</div>
<div class="project-links">
<a href="#" class="btn btn-small">演示</a>
<a href="#" class="btn btn-small btn-outline">源码</a>
</div>
</div>
</article>
<article class="project-card">
<div class="project-image">
<div class="project-placeholder">📝</div>
</div>
<div class="project-info">
<h3>Markdown 编辑器</h3>
<p>支持实时预览的 Markdown 编辑器,支持语法高亮和自定义主题。</p>
<div class="project-tags">
<span>TypeScript</span>
<span>CodeMirror</span>
</div>
<div class="project-links">
<a href="#" class="btn btn-small">演示</a>
<a href="#" class="btn btn-small btn-outline">源码</a>
</div>
</div>
</article>
</div>
</div>
</section>
<!-- ========== 联系方式 ========== -->
<section class="contact section" id="contact">
<div class="container">
<h2 class="section-title">联系我</h2>
<p class="section-subtitle">有任何问题或合作意向,欢迎随时联系</p>
<form class="contact-form" id="contact-form">
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" placeholder="请输入你的姓名" required>
<span class="error-message"></span>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" name="email" placeholder="请输入你的邮箱" required>
<span class="error-message"></span>
</div>
<div class="form-group">
<label for="subject">主题</label>
<input type="text" id="subject" name="subject" placeholder="请输入主题">
</div>
<div class="form-group">
<label for="message">留言</label>
<textarea id="message" name="message" rows="5" placeholder="请输入你的留言内容" required></textarea>
<span class="error-message"></span>
</div>
<button type="submit" class="btn btn-primary btn-block">
<span class="btn-text">发送消息</span>
<span class="btn-loading" style="display:none;">发送中...</span>
</button>
</form>
</div>
</section>
</main>
<!-- ========== 页脚 ========== -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<p class="footer-copy">© 2026 张三. All rights reserved.</p>
<div class="footer-social">
<a href="#" aria-label="GitHub" title="GitHub">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
<a href="#" aria-label="Email" title="Email">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 4L12 13 2 4"/></svg>
</a>
</div>
</div>
</div>
</footer>
<!-- 返回顶部按钮 -->
<button class="back-to-top" id="back-to-top" aria-label="返回顶部">↑</button>
</body>
</html>
6.3 CSS 样式——穿上漂亮的衣服¶
/* ============================================
CSS 变量 & 主题系统
============================================ */
:root {
/* 颜色 */
--primary: #667eea;
--primary-dark: #5a6fd6;
--secondary: #764ba2;
--accent: #f093fb;
/* 浅色主题 */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #636e72;
--text-muted: #b2bec3;
--border: #e9ecef;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
--shadow-hover: 0 8px 30px rgba(0, 0, 0, 0.12);
/* 尺寸 */
--header-height: 70px;
--max-width: 1100px;
--radius: 12px;
--radius-sm: 6px;
--transition: 0.3s ease;
}
/* 深色主题 */
[data-theme="dark"] {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-card: #1f2b47;
--text-primary: #e4e6eb;
--text-secondary: #b0b3b8;
--text-muted: #6c757d;
--border: #2d3748;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--shadow-hover: 0 8px 30px rgba(0, 0, 0, 0.4);
}
/* ============================================
全局重置
============================================ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
scroll-padding-top: var(--header-height);
}
body {
font-family: "Microsoft YaHei", "PingFang SC", "Helvetica Neue", sans-serif;
font-size: 16px;
line-height: 1.7;
color: var(--text-primary);
background-color: var(--bg-primary);
transition: background-color var(--transition), color var(--transition);
}
img {
max-width: 100%;
height: auto;
display: block;
}
a {
color: var(--primary);
text-decoration: none;
transition: color var(--transition);
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 24px;
}
.section {
padding: 80px 0;
}
.section-title {
font-size: 32px;
text-align: center;
margin-bottom: 12px;
color: var(--text-primary);
}
.section-subtitle {
text-align: center;
color: var(--text-secondary);
margin-bottom: 48px;
font-size: 17px;
}
/* ============================================
导航栏
============================================ */
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: var(--header-height);
background: var(--bg-primary);
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.05);
z-index: 1000;
transition: background var(--transition), box-shadow var(--transition);
}
.header.scrolled {
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.nav-logo {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.5px;
}
.nav-menu {
display: flex;
list-style: none;
gap: 32px;
}
.nav-link {
color: var(--text-secondary);
font-size: 15px;
font-weight: 500;
position: relative;
padding: 4px 0;
}
.nav-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--primary);
transition: width var(--transition);
}
.nav-link:hover,
.nav-link.active {
color: var(--primary);
}
.nav-link:hover::after,
.nav-link.active::after {
width: 100%;
}
.nav-actions {
display: flex;
align-items: center;
gap: 12px;
}
.theme-toggle {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: transform var(--transition);
}
.theme-toggle:hover {
transform: rotate(30deg);
}
/* 汉堡菜单(移动端) */
.hamburger {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.hamburger span {
display: block;
width: 24px;
height: 2px;
background: var(--text-primary);
border-radius: 2px;
transition: all var(--transition);
}
.hamburger.active span:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.hamburger.active span:nth-child(2) {
opacity: 0;
}
.hamburger.active span:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
/* ============================================
英雄区
============================================ */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
position: relative;
overflow: hidden;
}
.hero-container {
display: flex;
align-items: center;
gap: 60px;
width: 100%;
}
.hero-content {
flex: 1;
}
.hero-greeting {
font-size: 18px;
color: var(--primary);
margin-bottom: 8px;
font-weight: 500;
}
.hero-name {
font-size: 52px;
font-weight: 800;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 12px;
line-height: 1.2;
}
.hero-title {
font-size: 22px;
color: var(--text-secondary);
margin-bottom: 20px;
min-height: 33px;
}
.typed-cursor {
animation: blink 0.8s infinite;
color: var(--primary);
font-weight: 300;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.hero-description {
font-size: 17px;
color: var(--text-secondary);
line-height: 1.8;
margin-bottom: 32px;
max-width: 480px;
}
.hero-buttons {
display: flex;
gap: 16px;
}
.hero-image {
flex-shrink: 0;
}
.hero-avatar {
width: 280px;
height: 280px;
border-radius: 50%;
overflow: hidden;
border: 4px solid var(--primary);
box-shadow: 0 0 60px rgba(102, 126, 234, 0.3);
animation: float 3s ease-in-out infinite;
}
.hero-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15px); }
}
.scroll-down {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 50px;
border: 2px solid var(--text-muted);
border-radius: 15px;
display: flex;
justify-content: center;
padding-top: 8px;
}
.scroll-down span {
width: 4px;
height: 8px;
background: var(--text-muted);
border-radius: 2px;
animation: scrollDown 1.5s infinite;
}
@keyframes scrollDown {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(12px); opacity: 0; }
}
/* ============================================
按钮
============================================ */
.btn {
display: inline-block;
padding: 12px 28px;
border-radius: var(--radius-sm);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
border: 2px solid transparent;
text-align: center;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-outline {
border-color: var(--primary);
color: var(--primary);
background: transparent;
}
.btn-outline:hover {
background: var(--primary);
color: white;
}
.btn-small {
padding: 6px 16px;
font-size: 13px;
}
.btn-block {
width: 100%;
}
/* ============================================
关于我
============================================ */
.about {
background: var(--bg-secondary);
}
.about-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
align-items: start;
}
.about-text p {
margin-bottom: 16px;
color: var(--text-secondary);
font-size: 16px;
}
.skills h3 {
font-size: 18px;
margin-bottom: 16px;
color: var(--text-primary);
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.skill-tag {
padding: 6px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 14px;
color: var(--text-secondary);
transition: all var(--transition);
}
.skill-tag:hover {
border-color: var(--primary);
color: var(--primary);
transform: translateY(-2px);
}
/* ============================================
项目展示
============================================ */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 30px;
}
.project-card {
background: var(--bg-card);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform var(--transition), box-shadow var(--transition);
}
.project-card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-hover);
}
.project-image {
height: 180px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
}
.project-placeholder {
font-size: 56px;
opacity: 0.8;
}
.project-info {
padding: 24px;
}
.project-info h3 {
font-size: 18px;
margin-bottom: 10px;
color: var(--text-primary);
}
.project-info p {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 16px;
line-height: 1.6;
}
.project-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.project-tags span {
padding: 3px 10px;
background: var(--bg-secondary);
border-radius: 4px;
font-size: 12px;
color: var(--text-muted);
}
.project-links {
display: flex;
gap: 10px;
}
/* ============================================
联系表单
============================================ */
.contact {
background: var(--bg-secondary);
}
.contact-form {
max-width: 600px;
margin: 0 auto;
background: var(--bg-card);
padding: 40px;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 600;
font-size: 14px;
color: var(--text-primary);
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: var(--radius-sm);
font-size: 15px;
font-family: inherit;
background: var(--bg-primary);
color: var(--text-primary);
transition: border-color var(--transition), box-shadow var(--transition);
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
}
.form-group input.error,
.form-group textarea.error {
border-color: #e74c3c;
}
.error-message {
display: block;
color: #e74c3c;
font-size: 13px;
margin-top: 4px;
min-height: 18px;
}
/* ============================================
页脚
============================================ */
.footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 30px 0;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-copy {
color: var(--text-muted);
font-size: 14px;
}
.footer-social {
display: flex;
gap: 16px;
}
.footer-social a {
color: var(--text-muted);
transition: color var(--transition), transform var(--transition);
}
.footer-social a:hover {
color: var(--primary);
transform: translateY(-2px);
}
/* ============================================
返回顶部按钮
============================================ */
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--primary);
color: white;
border: none;
font-size: 20px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: all var(--transition);
z-index: 999;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-to-top:hover {
background: var(--primary-dark);
transform: translateY(-3px);
}
/* ============================================
响应式设计
============================================ */
/* 平板端(≤ 1024px) */
@media (max-width: 1024px) {
.hero-container {
flex-direction: column-reverse;
text-align: center;
gap: 40px;
}
.hero-description {
max-width: 100%;
}
.hero-buttons {
justify-content: center;
}
.hero-avatar {
width: 200px;
height: 200px;
}
.about-content {
grid-template-columns: 1fr;
gap: 32px;
}
}
/* 手机端(≤ 768px) */
@media (max-width: 768px) {
.section {
padding: 60px 0;
}
.section-title {
font-size: 26px;
}
.hero-name {
font-size: 36px;
}
.hero-title {
font-size: 18px;
}
.hamburger {
display: flex;
}
.nav-menu {
position: fixed;
top: var(--header-height);
left: 0;
width: 100%;
background: var(--bg-primary);
flex-direction: column;
align-items: center;
gap: 0;
padding: 20px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transform: translateY(-120%);
transition: transform var(--transition);
z-index: 999;
}
.nav-menu.active {
transform: translateY(0);
}
.nav-menu li {
width: 100%;
text-align: center;
}
.nav-link {
display: block;
padding: 14px 0;
font-size: 16px;
}
.project-grid {
grid-template-columns: 1fr;
}
.contact-form {
padding: 24px;
}
.footer-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.back-to-top {
bottom: 20px;
right: 20px;
width: 40px;
height: 40px;
font-size: 18px;
}
}
6.4 JavaScript 交互——让页面活起来¶
// ============================================
// 1. 主题切换(深色/浅色模式)
// ============================================
const themeToggle = document.querySelector('#theme-toggle');
const themeIcon = themeToggle.querySelector('.theme-icon');
const html = document.documentElement;
// 从 localStorage 读取保存的主题
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
themeIcon.textContent = savedTheme === 'dark' ? '☀️' : '🌙';
themeToggle.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeIcon.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});
// ============================================
// 2. 打字机效果
// ============================================
const typedText = document.querySelector('#typed-text');
const titles = [
'前端开发工程师',
'React 爱好者',
'开源贡献者',
'终身学习者'
];
let titleIndex = 0;
let charIndex = 0;
let isDeleting = false;
let typeSpeed = 100;
function typeEffect() {
const currentTitle = titles[titleIndex];
if (isDeleting) {
typedText.textContent = currentTitle.substring(0, charIndex - 1);
charIndex--;
typeSpeed = 50;
} else {
typedText.textContent = currentTitle.substring(0, charIndex + 1);
charIndex++;
typeSpeed = 120;
}
// 一个词打完,停顿后开始删除
if (!isDeleting && charIndex === currentTitle.length) {
typeSpeed = 2000; // 停顿 2 秒
isDeleting = true;
}
// 删除完毕,切换到下一个词
else if (isDeleting && charIndex === 0) {
isDeleting = false;
titleIndex = (titleIndex + 1) % titles.length;
typeSpeed = 500;
}
setTimeout(typeEffect, typeSpeed);
}
// 启动打字机效果
setTimeout(typeEffect, 1000);
// ============================================
// 3. 移动端汉堡菜单
// ============================================
const hamburger = document.querySelector('#hamburger');
const navMenu = document.querySelector('#nav-menu');
const navLinks = document.querySelectorAll('.nav-link');
hamburger.addEventListener('click', () => {
hamburger.classList.toggle('active');
navMenu.classList.toggle('active');
});
// 点击导航链接后关闭菜单
navLinks.forEach(link => {
link.addEventListener('click', () => {
hamburger.classList.remove('active');
navMenu.classList.remove('active');
});
});
// ============================================
// 4. 导航栏滚动效果
// ============================================
const header = document.querySelector('#header');
const backToTop = document.querySelector('#back-to-top');
const sections = document.querySelectorAll('.section');
// 滚动时:导航栏添加阴影 + 显示/隐藏返回顶部按钮
window.addEventListener('scroll', () => {
// 导航栏阴影
if (window.scrollY > 50) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
// 返回顶部按钮
if (window.scrollY > 500) {
backToTop.classList.add('visible');
} else {
backToTop.classList.remove('visible');
}
// 高亮当前区域的导航链接
let currentSection = '';
sections.forEach(section => {
const sectionTop = section.offsetTop - 100;
if (window.scrollY >= sectionTop) {
currentSection = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${currentSection}`) {
link.classList.add('active');
}
});
});
// 返回顶部
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// ============================================
// 5. 表单验证
// ============================================
const contactForm = document.querySelector('#contact-form');
function showError(input, message) {
const formGroup = input.closest('.form-group');
const errorElement = formGroup.querySelector('.error-message');
input.classList.add('error');
errorElement.textContent = message;
}
function clearError(input) {
const formGroup = input.closest('.form-group');
const errorElement = formGroup.querySelector('.error-message');
input.classList.remove('error');
errorElement.textContent = '';
}
function validateEmail(email) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(email);
}
// 实时验证
const nameInput = document.querySelector('#name');
const emailInput = document.querySelector('#email');
const messageInput = document.querySelector('#message');
nameInput.addEventListener('input', () => {
if (nameInput.value.trim().length > 0) {
clearError(nameInput);
}
});
emailInput.addEventListener('input', () => {
if (validateEmail(emailInput.value)) {
clearError(emailInput);
}
});
messageInput.addEventListener('input', () => {
if (messageInput.value.trim().length > 0) {
clearError(messageInput);
}
});
// 表单提交
contactForm.addEventListener('submit', (event) => {
event.preventDefault();
let isValid = true;
// 验证姓名
if (nameInput.value.trim() === '') {
showError(nameInput, '请输入你的姓名');
isValid = false;
} else {
clearError(nameInput);
}
// 验证邮箱
if (emailInput.value.trim() === '') {
showError(emailInput, '请输入你的邮箱');
isValid = false;
} else if (!validateEmail(emailInput.value)) {
showError(emailInput, '请输入有效的邮箱地址');
isValid = false;
} else {
clearError(emailInput);
}
// 验证留言
if (messageInput.value.trim() === '') {
showError(messageInput, '请输入你的留言内容');
isValid = false;
} else {
clearError(messageInput);
}
if (isValid) {
const submitBtn = contactForm.querySelector('button[type="submit"]');
const btnText = submitBtn.querySelector('.btn-text');
const btnLoading = submitBtn.querySelector('.btn-loading');
// 模拟发送
btnText.style.display = 'none';
btnLoading.style.display = 'inline';
submitBtn.disabled = true;
setTimeout(() => {
alert('消息已发送!感谢你的联系。(演示模式)');
contactForm.reset();
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
submitBtn.disabled = false;
}, 1500);
}
});
// ============================================
// 6. 滚动动画(元素进入视口时淡入)
// ============================================
const animateElements = document.querySelectorAll(
'.project-card, .skill-tag, .about-text'
);
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
},
{ threshold: 0.1 }
);
animateElements.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
6.5 代码解读——理解每一行¶
6.5.1 主题切换原理¶
// 核心思路:修改 <html> 的 data-theme 属性
html.setAttribute('data-theme', 'dark');
// CSS 中通过属性选择器切换变量值
[data-theme="dark"] {
--bg-primary: #1a1a2e;
--text-primary: #e4e6eb;
}
代码解读: 主题切换的本质是**切换 CSS 变量的值**。我们只需要修改 <html> 上的 data-theme 属性,所有使用了 CSS 变量的元素都会自动更新颜色。这就是 CSS 变量的强大之处——一处修改,全局生效。
6.5.2 打字机效果原理¶
打字机效果的核心是一个**递归的 setTimeout**:
- 打字阶段:逐字增加
textContent,charIndex++ - 停顿阶段:打完一个词后等待 2 秒
- 删除阶段:逐字减少
textContent,charIndex-- - 切换阶段:删除完毕后切换到下一个词
// 状态机:打字 → 停顿 → 删除 → 切换 → 打字...
if (!isDeleting && charIndex === currentTitle.length) {
isDeleting = true; // 打完 → 开始删除
} else if (isDeleting && charIndex === 0) {
isDeleting = false; // 删完 → 切换下一个词
titleIndex++;
}
6.5.3 Intersection Observer——滚动动画¶
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 元素进入视口 → 播放动画
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
},
{ threshold: 0.1 } // 元素 10% 可见时触发
);
代码解读: IntersectionObserver 是浏览器原生 API,用于监测元素是否进入视口。相比传统的 scroll 事件监听,它的性能好得多——浏览器会在空闲时批量处理回调。
6.6 部署到 GitHub Pages¶
完成代码后,你可以免费部署到 GitHub Pages:
- 在 GitHub 上创建一个名为
username.github.io的仓库。 - 将
index.html、css/、js/、images/推送到仓库。 - 进入仓库 Settings → Pages,选择
main分支,点击 Save。 - 等待几分钟,访问
https://username.github.io即可看到你的个人主页。
验证步骤¶
- 将 HTML、CSS、JS 代码分别保存为
index.html、css/style.css、js/main.js。 - 用浏览器打开
index.html,观察完整页面的渲染效果。 - 点击右上角的主题切换按钮(🌙/☀️),观察整个页面配色的变化——理解 CSS 变量切换的原理。
- 观察英雄区的打字机效果——注意文字逐字出现、停顿、删除、切换的完整循环。
- 按 F12 → Toggle Device Toolbar,切换手机/平板/桌面视图,观察布局变化。
- 滚动页面,观察导航栏阴影变化、返回顶部按钮的出现、以及元素淡入动画。
- 在联系表单中提交空内容,观察验证错误提示;输入有效内容后提交,观察发送状态。
📋 本章要点总结¶
- 掌握完整前端项目的文件结构设计
- 理解 CSS 变量 +
data-theme实现主题切换的原理 - 会用
setTimeout递归实现打字机效果 - 掌握移动端汉堡菜单的 HTML + CSS + JS 完整实现
- 理解
IntersectionObserver实现滚动动画 - 能实现完整的表单验证(实时验证 + 提交验证)
- 会用
localStorage持久化用户偏好(主题选择) - 理解
scroll-behavior: smooth和scroll-padding-top的配合 - 掌握
window.scrollTo()实现平滑滚动 - 了解 GitHub Pages 的部署流程
📚 课后练习¶
基础练习¶
- 将打字机效果的词库改为你自己的职业描述。
- 修改配色方案(CSS 变量中的
--primary和--secondary),打造属于你自己的配色。 - 添加真实的项目截图替换占位符。
进阶挑战¶
- 添加"博客"板块,使用 JavaScript 动态渲染文章列表。
- 实现多语言切换(中/英文),将所有文字提取为配置对象。
- 使用 GitHub API 动态拉取你的真实项目列表并展示。
🎉 结语¶
恭喜你完成了全部 6 章的学习!从 HTML5 的文档结构,到 CSS3 的样式布局,再到 JavaScript 的交互逻辑,你已经掌握了前端开发的三大核心技术。
回顾一下你学到的内容:
| 章节 | 核心技能 |
|---|---|
| 第 1 章 | HTML5 语义化标签、文档结构、表单基础 |
| 第 2 章 | CSS3 选择器、盒模型、颜色字体、过渡动画 |
| 第 3 章 | Flexbox、Grid、媒体查询、响应式设计 |
| 第 4 章 | JavaScript 变量、函数、数组、对象、条件循环 |
| 第 5 章 | DOM 查询与修改、事件监听、事件委托、表单处理 |
| 第 6 章 | 综合实战:主题切换、打字机效果、滚动动画、完整项目 |
前端的世界非常广阔,本教程只是一个起点。接下来你可以:
- 📖 学习一个前端框架(React / Vue / Angular)
- 🎨 深入学习 CSS 动画和 SVG
- ⚡ 学习构建工具(Vite / Webpack)
- 🔧 学习 TypeScript 为代码添加类型安全
- 🚀 学习 Next.js / Nuxt.js 构建全栈应用
记住:最好的学习方式就是动手做项目。 现在就去创建你的 GitHub Pages 个人主页吧!