Web 课设:从零搭建响应式个人主页 — 志趣 ZhiQu 返回首页开源项目 节点Web 课设:从零搭建响应式个人主页
Web 课设:从零搭建响应式个人主页
> 大学 Web 前端课程设计完整教程 — 从新建文件夹到 1310 行代码,每一步都可复现。
一、项目背景与评分对照
本学期 Web 开发课程大作业,要求实现一个完整的个人主页。先看清老师怎么打分:
| 评分项 | 要求 | 本文对应 |
|---|
| 布局方案 | DIV+CSS,禁止纯表格 | 第五节:Grid + Flexbox |
| 代码规范 | 语义化 HTML、缩进、注释 | 全文代码含中文注释 |
| 表单验证 | JS 必填项/格式/长度校验 | 第七节:完整表单校验 |
| 代码量 | ≥1000 行 | 最终 1310 行 |
| 交互效果 | 动态效果加分 | 打字机/暗黑模式/滚动动画 |
二、第一步:新建项目
mkdir personal-homepage
cd personal-homepage
mkdir css js images
touch index.html css/style.css js/main.js
最终目录结构:
personal-homepage/
├── index.html ← 主页面
├── css/
│ └── style.css ← 样式表
├── js/
│ └── main.js ← 交互逻辑
└── images/ ← 图片(可选)
三、第二步:HTML 骨架
先写页面结构。5 个 ``,语义化标签:
首页
关于
技能
项目
联系
你好,我是 Lin
查看作品
联系我
关于我
技能专长
项目作品
联系我
© 2026 Lin. All rights reserved.
>
截图 1:浏览器打开 index.html,确认 5 个区块都在页面上。
四、第三步:CSS 变量统一配色
用 CSS 变量管理颜色,方便后续调整和暗黑模式:
/* ===== CSS 变量 ===== */
:root {
--color-primary: #2563eb;
--color-bg: #ffffff;
--color-text: #1e293b;
--color-border: #e2e8f0;
--radius: 12px;
--transition: all 0.3s ease;
}
/* ===== 暗黑模式 ===== */
[data-theme="dark"] {
--color-bg: #0f172a;
--color-text: #e2e8f0;
--color-border: #334155;
}
之后所有颜色都用 引用,一键切换主题。
var(--color-xxx)
> 💡 常见坑:[data-theme] 属性要加在 标签上,不是。
五、第四步:Flexbox + Grid 布局
.header {
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
display: flex; align-items: center; justify-content: space-between;
height: 64px; padding: 0 24px;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
}
.skills-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
@media (max-width: 768px) {
.skills-grid { grid-template-columns: 1fr; }
.nav { /* 改为汉堡菜单 */ }
}
> 💡 常见坑:@media 断点用 768px 覆盖大部分手机,iPad 横屏用 1024px。
六、第五步:打字机效果
var roles = ['AI 应用开发者', '全栈 Web 工程师'];
var roleIndex = 0, charIndex = 0, isDeleting = false;
function typeEffect() {
var current = roles[roleIndex];
var text = isDeleting
? current.substring(0, charIndex - 1)
: current.substring(0, charIndex + 1);
document.getElementById('typingText').textContent = text;
if (isDeleting) charIndex--;
else charIndex++;
var speed = isDeleting ? 50 : 100;
if (!isDeleting && charIndex === current.length) {
speed = 1500; // 打完停顿 1.5 秒
isDeleting = true;
} else if (isDeleting && charIndex === 0) {
isDeleting = false;
roleIndex = (roleIndex + 1) % roles.length;
}
setTimeout(typeEffect, speed);
}
typeEffect();
> 💡 常见坑:定时器在页面切换后如果还在跑,用 clearTimeout 销毁。本页面是单页不需处理。
七、第六步:表单 JS 校验
document.getElementById('contactForm').addEventListener('submit', function(e) {
e.preventDefault();
var name = document.getElementById('name').value.trim();
var email = document.getElementById('email').value.trim();
var msg = document.getElementById('message').value.trim();
var valid = true;
// 清空旧错误
document.querySelectorAll('.error').forEach(function(el) { el.classList.remove('error'); });
document.querySelectorAll('.error-message').forEach(function(el) { el.textContent = ''; });
// 姓名:必填 + 2-20 字
if (!name) { showErr('name', '请输入姓名'); valid = false; }
else if (name.length < 2) { showErr('name', '至少2个字符'); valid = false; }
// 邮箱:正则
if (!email) { showErr('email', '请输入邮箱'); valid = false; }
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showErr('email', '格式不正确'); valid = false;
}
// 留言:10-500字 + 实时计数
if (!msg) { showErr('message', '请输入内容'); valid = false; }
else if (msg.length < 10) { showErr('message', '至少10个字符'); valid = false; }
if (valid) {
alert('发送成功!');
this.reset();
}
});
function showErr(id, msg) {
document.getElementById(id).classList.add('error');
document.getElementById(id + 'Error').textContent = msg;
}
/* 输入框报错样式 */
input.error, textarea.error { border-color: #ef4444; }
.error-message { color: #ef4444; font-size: 12px; min-height: 18px; }
> 💡 常见坑:`` 要加 novalidate 属性,禁用浏览器默认校验,统一走 JS 逻辑。
>
截图 2:不填任何字段点发送,看红色提示。
八、第七步:暗黑模式
var themeToggle = document.getElementById('themeToggle');
var currentTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', currentTheme);
themeToggle.addEventListener('click', function() {
var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
this.querySelector('i').className = next === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
});
> 💡 常见坑:刷新后主题丢失→检查 localStorage.setItem 是否执行,确认 key 名一致。
>
截图 3:点击月亮按钮后的暗黑模式效果。
九、第八步:滚动动画
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.15 });
document.querySelectorAll('.fade-in').forEach(function(el) {
observer.observe(el);
});
.fade-in { opacity: 0; transform: translateY(30px); transition: all 0.6s ease; }
.fade-in.visible { opacity: 1; transform: translateY(0); }
> 💡 常见坑:IntersectionObserver 不兼容 IE,课设只需在现代浏览器演示。
十、开发踩坑记录
| 问题 | 原因 | 解决 |
|---|
| 导航栏遮挡内容 | position:fixed 脱离文档流 | 给 `` 加 padding-top: 64px |
| 暗黑模式刷新丢失 | 没存 localStorage | localStorage.setItem('theme', next) |
| 表单验证不提示 | 忘写 e.preventDefault() | 表单 submit 事件第一行加 |
| Grid 不换行 | 没写 @media 媒体查询 | @media(max-width:768px) 改单列 |
| 定时器页面切换还在跑 | 单页应用不需处理 | 多页用 clearTimeout |
十一、拓展方向(拿更高分)
| 加分项 | 实现思路 |
|---|
| 回到顶部按钮 | 监听 scrollY > 500 显示,window.scrollTo({top:0, behavior:'smooth'}) |
| 技能进度条动画 | IntersectionObserver 触发时 width 从 0 过渡到目标值 |
| 图片懒加载 | `` 或 IntersectionObserver |
| 后端表单提交 | fetch('/api/contact', {method:'POST', body: JSON.stringify(data)}) |
十二、项目统计
| 文件 | 行数 | 大小 |
|---|
index.html | 338 | 14 KB |
css/style.css | 647 | 13 KB |
js/main.js | 325 | 11 KB |
| 总计 | 1,310 | 38 KB |
十三、核心知识点总结
| 知识点 | 具体应用 |
|---|
| HTML5 语义化 | header/nav/main/section/article/footer |
| CSS3 Flexbox | 导航栏居中分布、Hero 垂直居中 |
| CSS3 Grid | 技能 3 列、关于 2 列、项目 3 列 |
| CSS 变量 | var(--color-primary) 统一配色、一键暗黑 |
| 媒体查询 | @media (max-width: 768px) 移动端单列 |
| DOM 操作 | getElementById / querySelectorAll / classList |
| 事件监听 | addEventListener('submit'/'click'/'scroll') |
| 正则表达式 | 邮箱格式 /^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
| localStorage | 暗黑模式偏好持久化 |
| IntersectionObserver | 滚动进入视口触发动画 |
| 定时器 | setTimeout 递归实现打字机 |
> 完整源代码在课堂展示时可查看。任何问题欢迎评论区交流。