这是一个游戏化的学习进度追踪器,名字叫 StudyHub Pro。它专门为"想转行成为软件测试工程师(SDET)"的人设计,但同时覆盖了三条并行的学习路线:编程测试技能、英语语法写作、以及技术英语词汇。
用户每天打开这个页面,就能看到"今天该学什么"——每周有 5 个具体任务,任务旁边有一个小方框,完成了就点一下打勾,文字会被划掉变灰,就像手机备忘录里的待办清单。每天完成至少一个任务,页面顶部的"连续打卡天数"就会增加,这个机制和你玩手机游戏里的"连续签到"一模一样,专门设计来让你不想断掉。
数据方面,它有两层保存机制:第一层是浏览器本地存储(就算不登录、不联网,你的进度也不会丢);第二层是 Google Firebase 云端数据库(登录 Google 账号后,你在家里电脑打的勾,在公司电脑也能看到)。
如果要用一句话形容它,它就像一本会自动记分的电子学习日记本,同时还带了游戏里的"成就系统"——进度环、连续打卡火焰、周完成动画,每一个细节都在告诉你:你在进步。
位置: 页面最顶部,横跨全宽。
包含什么:
- 左侧:网站 Logo("StudyHub")、三个切换按钮(SDET / English / Tech English)、火焰🔥打卡天数徽章
- 右侧:同步状态小标签(绿色"✅ Synced" 或 "⚫ Offline")、手动同步按钮、Google 登录/退出按钮
HTML 标签: 用的是 <header>,这是 HTML 里专门表示"页头"的标签,就像一栋楼的门牌——它告诉浏览器"这里是整个页面的入口信息区"。
CSS 布局: 用了 display: flex(弹性布局,就像把元素放进一排格子里,自动排好间距),加了 backdrop-filter: blur(18px)(磨砂玻璃效果),还有圆角和半透明白色背景,看起来有一种现代 App 的质感。
JavaScript 交互:
- 点击三个 Tab 按钮 → 隐藏其他面板、只显示选中的那个
- 登录/退出 Google → 触发 Firebase 身份验证流程
- 同步状态会实时更新颜色(绿/黄/红/灰)
位置: 切换面板后,左侧占据大部分宽度的主卡片,跨两行高度。
包含什么:
- 本周标题和目标(比如"Week 1 — Java OOP")
- 阶段标签(Stage 1 / Foundation)和状态标签(未开始 / 进行中 / 完成)
- 周次选择标签栏(W1 W2 W3 … W20,可左右滑动)
- 进度条("3/5 完成")
- 任务清单(每条任务 = 勾选框 + 任务文字 + 折叠的"怎么做"提示)
- 重置按钮(今天 / 本周 / 全部)
HTML 标签: 用 <div class="card"> 包裹,<ul> 和 <li> 组成任务列表。<ul> 是"无序列表",就像一张购物清单,每个 <li> 是清单上的一行。
CSS 布局: 卡片用了 backdrop-filter(磨砂背景)、box-shadow(阴影,让卡片"浮"起来的感觉)、border-radius(圆角)。进度条是一个细长 <div> 套了一个会动的内层 <div>,宽度由 JavaScript 实时计算填入。
JavaScript 交互:
- 点任务 → 切换勾选状态 → 更新进度条 → 检查是否全部完成(完成则触发绿光动画)
- 点周次标签 → 切换显示不同周的任务数据
- 所有变化自动保存到 localStorage
位置: 右侧上方卡片。
包含什么: 一个 7 列的月历格子,今天用紫色圆圈标记,有学习记录的日期用绿色标记并加一个小勾。下方还有进度圆环和"还剩 X 周"的里程碑信息。
HTML 标签: 日历格子用 <div class="calendar-grid"> 配合 CSS Grid(网格布局)实现,就像 Excel 表格,每个格子是一个小 <div>。
CSS 布局: display: grid; grid-template-columns: repeat(7, 1fr) — 意思是"把容器等分成 7 列",刚好对应一周七天。
JavaScript 交互: 每次打开页面,代码会读取 st.dates(记录哪些天完成了学习),然后给对应的日期格子加上绿色 CSS 类,自动渲染出你的打卡历史。
位置: 右侧下方卡片。
包含什么: 两个输入框(填学习时间和地点)、保存按钮、一个大的"🚀 开始学习"倒计时按钮。
HTML 标签: 输入框是 <input type="text">,按钮是 <button>,外层用 <div class="commit-form"> 包裹。
CSS 布局: 用 display: flex; flex-wrap: wrap 让输入框和按钮在宽屏并排显示、窄屏自动换行堆叠。
JavaScript 交互: 点"🚀 开始学习"后,按钮进入 5 秒倒计时,圆环动画从底部往上消失,倒计时结束后随机弹出一条励志提示语(比如"每天一点点,终将变厉害")。
位置: 整个面板最底部。
包含什么: 一个折叠按钮(点击展开/收起),展开后显示本周常见错误提示、每日学习 SOP(标准流程)、周复盘问题。
HTML 标签: 折叠按钮是 <button class="collapsible-toggle">,内容区是 <div class="collapsible-content">。
CSS 交互: 折叠动画用的是 max-height: 0(隐藏时高度为0)→ max-height: 3000px(展开时),配合 transition 让它平滑滑出,不是"啪"地突然出现。
JavaScript 交互: 点击按钮 → 切换 open 这个 CSS 类 → 触发展开/收起动画 → 同时旋转箭头图标(▸ 变成 ▾)。
:root {
--purple: #7C5CFC;
--green: #10B981;
--bg: #F4F0FA;
--shadow-md: 0 4px 16px rgba(30,27,46,0.07);
}逐行大白话:
:root— 代表整个网页的"根",可以理解为"全局设置间"--purple: #7C5CFC— 给这个紫色起了一个名字叫--purple,#7C5CFC是紫色的十六进制颜色代码- 以后任何地方想用这个紫色,直接写
var(--purple)就行,不用记颜色代码
为什么要这样写?
假如你不用变量,把紫色 #7C5CFC 直接写在 200 个地方。有一天你想换成蓝色,你需要找到这 200 个地方一一修改。用了变量,只改一行,全部自动更新。
生活比喻: 这就像奶茶店定了"招牌紫色"这个品牌色,存在设计手册里。所有海报、菜单、杯子都引用手册里的那个色号,不用每次重新调色——改一次手册,全店同步。
.task-checkbox.checked {
background: var(--green); /* 背景变绿 */
border-color: var(--green); /* 边框也变绿 */
box-shadow: 0 0 10px rgba(16,185,129,0.3); /* 绿色光晕 */
}
.task-text.done {
text-decoration: line-through; /* 文字中间画横线 */
color: var(--text-muted); /* 颜色变灰 */
}逐行大白话:
.task-checkbox.checked— "当勾选框同时拥有checked这个标记时,应用以下样式"text-decoration: line-through— 给文字加删除线,就像你手写清单上的划掉效果
为什么要这样写?
CSS 本身不知道你"点了"还是"没点",它只负责"当某个状态存在时显示某种样子"。是 JavaScript 在你点击的瞬间,偷偷给元素贴上 checked 这张标签,CSS 看到标签就自动换装。
生活比喻: 就像超市收银台的价格标签机制——商品本身没有价格,但当收银员扫了条形码(JavaScript 的点击事件),价格牌就自动从"待处理"变成"已结算"(CSS 换了样式)。
function toggleTask(pref, tid, li, cb, ts) {
const st = cur(); // 拿出当前模块的数据
st.tasks[tid] = !st.tasks[tid]; // 取反:true变false,false变true
if (st.tasks[tid]) {
cb.classList.add('checked'); // 给勾选框贴上"已完成"标签
ts.classList.add('done'); // 给文字贴上"已完成"标签
} else {
cb.classList.remove('checked'); // 撕掉"已完成"标签
ts.classList.remove('done');
}
updateStreak(); // 重新计算连续打卡天数
updateUI(pref); // 刷新进度条数字
save(active); // 存进本地抽屉
}逐行大白话:
!st.tasks[tid]— 感叹号是"取反",意思是"原来是开的就关掉,原来是关的就开",就像电灯开关classList.add('checked')— 给 HTML 元素贴上一个 CSS 标签(class),CSS 看到标签就换装- 最后三步是一套固定动作:算天数 → 刷界面 → 存数据
为什么要这样写? 如果不存数据,你刷新页面所有打勾就消失了。如果不刷新 UI,进度条不会动。这三步缺一不可,顺序也很重要——先改数据,再用新数据更新界面。
生活比喻: 就像你在健身房打卡。扫一下手机(点击) → 机器把你记录成"今天来了"(取反存储) → 电子屏幕显示"✅ 已打卡"(CSS 换装) → 月度报告自动更新(刷新进度条)。
function save(k) {
const p = PREF[k]; // 拿到存储的"抽屉标签",比如 "sdet"
const st = S[k]; // 拿到这个模块的数据
localStorage.setItem(p + '_tasks', JSON.stringify(st.tasks));
localStorage.setItem(p + '_streak', String(st.streak));
localStorage.setItem(p + '_week', String(st.week));
}逐行大白话:
localStorage— 浏览器自带的"抽屉",存的是文字格式的数据,关掉浏览器也不会消失localStorage.setItem('sdet_tasks', ...)— 往贴着"sdet_tasks"标签的格子里放东西JSON.stringify(st.tasks)— 把 JavaScript 的数据对象"压扁"成一串文字,因为抽屉只能存文字(就像把立体地图折叠成平面地图再放进信封)
为什么要这样写? 浏览器的内存(就是代码运行时的数据)是临时的,关掉标签页就清空了。localStorage 是硬盘上的持久存储,才能让数据活过"关闭浏览器"这个事件。
生活比喻: 数据就像你在便签纸上写的学习计划。内存是你手里拿着这张便签(关掉手就掉了),localStorage 是把便签贴在冰箱上(明天还在)。JSON.stringify 就是把立体便利贴折叠成能放进抽屉的平面纸。
async function syncNow() {
setSyncState('syncing'); // 顶部标签变成黄色"🔄 同步中"
try {
const dr = db.collection('learningPanels').doc(currentUser.uid);
await dr.set(ld, { merge: true }); // 上传数据到云端(等待完成)
setSyncState('synced'); // 成功后变成绿色"✅ Synced"
} catch(e) {
setSyncState('failed'); // 失败则变成红色"❌ Failed"
}
}逐行大白话:
async— 告诉 JavaScript"这个函数里有需要等待的操作,请耐心"await— "在这里暂停,等网络请求完成再继续"(但页面不会卡住,用户还能操作)try...catch— 试着做某件事,如果出错了不要崩溃,改为执行"出错处理"
为什么要这样写?
联网操作需要时间(可能 0.5 秒,可能 3 秒),如果不用 async/await,代码会在数据还没传完的时候就继续执行下去,导致保存失败但页面显示"成功"的假象。
生活比喻: 就像你用手机给家人发照片。async 是你告诉自己"发照片需要时间,发送中先去做别的事"。await 是"等到对方收到回执再关掉发送页面"。try/catch 是"如果网络断了,弹出'发送失败'而不是直接闪退"。
🎬 想象一个叫小明的人,今晚打开了电脑,准备开始他第 7 天的学习打卡。
第一幕:浏览器收到指令,开始"建房子"
小明在浏览器地址栏输入网址,按下回车。这一刻,浏览器开始工作——它把 HTML 文件请求过来,开始从上到下逐行阅读,就像工头拿到了一份建筑蓝图。
浏览器边读边在内存里搭起一棵"家庭树",叫做 DOM 树(文档对象模型,Document Object Model — 就是把 HTML 的每个标签、每段文字都变成树上的一个"节点",节点之间有父子关系,就像族谱)。<header> 是爷爷,它底下的 <button> 是孙子,所有节点各就各位。
与此同时,CSS 文件也被浏览器读取,生成另一棵树叫 CSSOM 树(CSS 对象模型 — 就是把所有样式规则整理成一个化妆师名单,谁负责给哪个元素化妆、用什么颜色什么字号都列得清清楚楚)。
这两棵树合并在一起,形成渲染树(Render Tree — 演员 + 服装的最终配对清单)。然后浏览器计算每个元素应该出现在屏幕的哪个位置,最终把像素画到屏幕上。
这一切在不到 0.1 秒内完成。
第二幕:JavaScript 登场,去翻"抽屉"
页面框架画出来了,但还没有真实数据——任务清单是空的,打卡天数是 0,日历是空白的。
这时候 JavaScript 的 init() 函数自动启动,它做的第一件事就是去翻浏览器的"本地抽屉"(localStorage)。它一个一个找:
- 抽屉里有没有贴着
sdet_tasks标签的格子?有!里面存着小明上次的打勾记录。 - 有没有
sdet_streak?有!上面写着6,小明已经连续学了 6 天。 - 有没有
sdet_week?有!数字是1,他正在第 2 周(数组从 0 开始,所以 1 = 第 2 周)。
所有数据被取出来,装进叫做 S 的大口袋里,随时准备被使用。
第三幕:Firebase 门卫查身份
几乎同时,Firebase 的身份验证模块像酒吧门口的保安一样默默检查:这个设备上有没有登录的 Google 账号?
auth.onAuthStateChanged() 这个函数一直在后台等待。如果发现小明上次登录过,立刻触发:去云端数据库拉取最新数据,和本地数据比较时间戳——谁最新用谁,然后更新页面顶部同步状态为绿色"✅ Synced"。
这个步骤是异步的(async — 就是"同时在后台跑,不影响前台页面的显示"),所以页面不会因为等待网络而卡住,用户已经可以开始操作了。
第四幕:页面被"填满",小明终于看到内容
renderAll() 函数像一个装修总监,按顺序指挥各部门填内容:
renderPlanTabs()— 检查小明上次选的是哪个 Tab,让那个按钮高亮renderWeekTabs()— 在任务卡片顶部画出 W1 W2 … W20 的周次标签renderTasks()— 把第 2 周的 5 个任务逐条画出来,并根据st.tasks里的记录自动恢复打勾状态renderCalendar()— 画出当月日历,并把st.dates里有记录的日期标绿updateRing()— 计算总进度百分比,更新圆环的弧度
整个填充过程在几毫秒内完成。小明看到的"活的"页面,其实是 JavaScript 把数据一条条"贴"进 DOM 骨架里的结果。
第五幕:所有按钮长上"耳朵"
setupEvents() 函数负责给每个按钮装上"事件监听器"(Event Listener — 就是让按钮长上耳朵,随时等你操作)。这是一次性的绑定,之后就持续生效:
- 三个 Tab 按钮:耳朵在听"被点击"事件,点了就切换面板
- 每个任务条目:耳朵在听"被点击",点了就触发打勾逻辑
- 保存承诺按钮:耳朵在听"被点击",点了存数据并更新显示
- 🚀 倒计时按钮:耳朵在等,一旦点击开始 5 秒倒计时动画
第六幕:小明打了一个勾,发生了五件事
小明完成了"安装 IntelliJ IDEA",点了任务前面的小方框。在接下来的不到 50 毫秒里,发生了这些事:
- 数据取反:
st.tasks['s1t1'] = true(从 false 变 true) - CSS 换装:勾选框被贴上
checked标签变绿,文字被贴上done标签划线变灰 - 进度更新:重新计算
1/5,进度条宽度从 0% 变成 20% - 存进本地:
localStorage.setItem('sdet_tasks', '...')把新状态写进本地抽屉 - 云端同步(2.5 秒后):一个计时器被启动,2.5 秒内如果没有其他操作,就悄悄把数据同步到 Firebase
第 1-4 步是瞬间完成的,小明感觉不到任何延迟。第 5 步在后台默默进行,页面不会卡住,顶部状态标签会短暂变黄"🔄 同步中",完成后变回绿色。
如果是今天第一次打勾,连续打卡天数还会加一,顶部徽章触发弹跳动画,然后日历上今天的格子变绿。
这一个小小的打勾动作,背后是一条完整的"数据流水线"。
| 概念名称 | 一句话解释(大白话) | 在代码中的体现 | 生活比喻 |
|---|---|---|---|
| HTML | 页面的骨架,决定有什么元素 | <header> <button> <ul> <li> 等所有标签 |
房子的砖墙和框架 |
| CSS | 骨架的外衣,决定好不好看 | .card { border-radius: 16px; box-shadow: ... } |
房子的油漆和装修 |
| CSS 变量 | 给颜色/数值起名字,改一处全更新 | :root { --purple: #7C5CFC } |
品牌色手册 |
| Flexbox | 让元素乖乖排成一行或一列 | .top-bar { display: flex } |
把货物整齐摆上流水线传送带 |
| CSS Grid | 把空间切成格子,元素各占一格 | .calendar-grid { grid-template-columns: repeat(7,1fr) } |
Excel 表格 |
| JavaScript | 页面的大脑,让页面能动起来 | toggleTask() save() renderAll() 等所有函数 |
房子里的电路和智能系统 |
| DOM 操作 | 用代码找到并修改页面上的元素 | element.classList.add('checked') |
装修工人按图纸修改墙面 |
| 事件监听 | 给元素装"耳朵",等用户操作 | button.addEventListener('click', ...) |
给按钮派了一个全天候服务员 |
| localStorage | 浏览器的本地抽屉,关掉不丢 | localStorage.setItem('sdet_streak', '7') |
冰箱上的便利贴 |
| JSON | 把复杂数据压扁成文字方便存取 | JSON.stringify(st.tasks) |
把立体地图折叠成可放进信封的平面图 |
| async/await | 处理"需要等待"的操作,不卡页面 | async function syncNow() { await dr.set(...) } |
发快递时继续做别的事,等到货通知再去取 |
| Firebase | Google 的云数据库,跨设备同步 | db.collection('learningPanels').doc(uid) |
把便利贴拍照传到家庭群,所有人都能看到 |
| CSS 动画 | 让变化平滑过渡,不突兀 | transition: width 0.5s @keyframes streakPop |
电梯门缓缓关上,而不是猛地关上 |
| 响应式设计 | 让页面在手机和电脑上都好看 | @media(max-width:720px) clamp(...) |
折叠桌:平时展开用,空间小了折叠收起 |
目标: 把整个网站的主色调从紫色换成你喜欢的颜色(比如蓝色、粉色)。
打开: 你的 HTML 文件,找到 <style> 标签内部。
找到这段代码:
:root {
--purple: #7C5CFC;
--purple-dark: #5E3FD9;
--purple-light: #A78BFA;
}怎么改: 把 #7C5CFC 改成 #E91E8C(粉色)或 #2196F3(蓝色),三行都改。
效果: 刷新页面后,所有按钮、进度条、高亮文字都变成新颜色。
改错了怎么办: 按 Ctrl + Z(Mac 用 Cmd + Z)撤销,或者把颜色改回 #7C5CFC。
目标: 把点击"🚀 开始学习"后弹出的励志语换成你自己写的。
打开: HTML 文件,搜索关键词 hints:(用 Ctrl + F 搜索)。
找到这段代码:
hints: [
'Small progress counts. Keep the streak!',
'One task at a time.',
'Focus on execution.',
...
]怎么改: 把引号里的英文换成你自己想看到的话,中文也行,比如:
'今天比昨天进步一点点就够了!',
'坚持才是最牛的技能。',效果: 点击"🚀 开始学习"按钮,弹出的提示就变成你写的话了。
目标: 把顶部那个🔥打卡天数徽章从黄色改成你喜欢的颜色。
打开: HTML 文件,搜索 .streak-badge。
找到这段代码:
.streak-badge {
background: linear-gradient(135deg, #FEF3C7, #FDE68A);
color: #92400E;
}怎么改: 把 #FEF3C7 改成 #D1FAE5(浅绿),#FDE68A 改成 #6EE7B7(深绿),#92400E 改成 #065F46(深绿文字)。
效果: 刷新后,打卡徽章从黄色变成绿色系,像一个"健康打卡"的感觉。
小提示: 不知道颜色代码?直接去 coolors.co 点你喜欢的颜色,复制 # 开头的代码粘贴进来就行。
🎉 最后想对你说: 你用 vibe coding 做出来的这个项目,包含了很多前端工程师工作好几个月才会碰到的技术——云同步、动画系统、多模块数据管理。真正厉害的不是"你能不能从零写出它",而是"你能不能读懂它、改动它、让它变成真正属于你的东西"。你已经在做这件事了。继续改它、玩它,每改一行代码就是一次真实的学习。🚀