feat: 现代CSS学习工坊 - 18个核心知识点

覆盖CSS3核心特性、Flex/Grid布局、CSS动画、响应式设计、
Tailwind CSS最佳实践、CSS新特性(容器查询、层叠层、:has选择器、
滚动驱动动画、现代色彩系统等)

功能:
- 渐进式学习页面(learn.html)
- 闪卡复习模式(flashcard.html)
- 知识点索引与搜索(roots.html)
- 知识点详情(root-detail.html)
- 学习进度追踪(progress.html)
- PWA离线支持
- SEO优化
This commit is contained in:
lambert.li
2026-05-15 22:50:53 +08:00
commit b41b4740d3
17 changed files with 2583 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
.DS_Store
*.log
*.zip
+87
View File
@@ -0,0 +1,87 @@
# 现代CSS学习工坊
系统学习现代CSS核心特性与最佳实践的交互式学习站点。
## 📚 知识点覆盖
### CSS3核心特性
- CSS选择器进阶
- 伪类与伪元素
- 盒模型与box-sizing
### 布局系统
- Flexbox弹性布局
- Grid网格布局
- Subgrid子网格
### CSS动画
- CSS过渡动画(Transitions
- CSS关键帧动画(Animations
### 响应式设计
- 响应式设计与媒体查询
- 流体排版与clamp()
### CSS变量与函数
- CSS自定义属性(Variables
- CSS函数(calc/min/max/clamp
### CSS新特性
- 容器查询(Container Queries
- 层叠层(Cascade Layers
- :has() 选择器
- CSS滚动吸附(Scroll Snap
- 现代CSS色彩系统
- 滚动驱动动画(Scroll-Driven Animations
## 🎯 功能特性
- **渐进式学习**:从基础到进阶的18个核心知识点
- **代码示例**:每个知识点包含3个实用代码示例
- **测验题**:每个知识点附带测验题,巩固学习成果
- **闪卡复习**:支持翻转卡片、键盘快捷键的闪卡模式
- **学习进度**:本地存储学习进度,跟踪掌握情况
- **知识点索引**:支持搜索和分类筛选
- **PWA支持**:可离线访问,支持添加到主屏幕
- **SEO优化**:完整的Open Graph和SEO元数据
## 🚀 部署
本项目为纯静态站点,可部署到任何静态托管平台:
### Vercel(推荐)
```bash
vercel deploy
```
### GitHub Pages
```bash
git push origin main
```
## 📁 项目结构
```
modern-css-workshop/
├── index.html # 首页
├── learn.html # 渐进式学习页
├── flashcard.html # 闪卡复习页
├── roots.html # 知识点索引页
├── root-detail.html # 知识点详情页
├── progress.html # 学习进度页
├── css/
│ └── minimal.css # 全局样式
├── js/
│ ├── wordData.js # 知识点数据
│ ├── siteConfig.js # 站点配置
│ └── storage.js # 本地存储管理
├── manifest.json # PWA配置
├── sw.js # Service Worker
├── vercel.json # Verc部署配置
├── robots.txt # SEO爬虫配置
└── sitemap.xml # 站点地图
```
---
用纯CSS实现一切可能 ✨
+649
View File
@@ -0,0 +1,649 @@
/* 全局重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
svg {
max-width: 24px;
max-height: 24px;
width: 16px;
height: 16px;
flex-shrink: 0;
}
.nav-logo svg {
width: 20px !important;
height: 20px !important;
}
/* 基础变量 - CSS紫蓝渐变主题 */
:root {
--primary: #8B5CF6;
--primary-dark: #7C3AED;
--primary-light: #EDE9FE;
--code-bg: #1E1B4B;
--code-border: #312E81;
--code-text: #E0E7FF;
--gray-50: #FAFAFA;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
--success: #10B981;
--error: #EF4444;
--radius: 12px;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--gray-50);
color: var(--gray-800);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* 容器 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* 导航 */
.nav {
padding: 20px 0;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--gray-100);
margin-bottom: 48px;
}
.nav-logo {
font-size: 18px;
font-weight: 700;
color: var(--gray-900);
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
}
.nav-logo .emoji {
font-size: 20px !important;
width: 20px !important;
height: 20px !important;
}
.nav-links {
display: flex;
gap: 24px;
list-style: none;
}
.nav-link {
text-decoration: none;
color: var(--gray-600);
font-weight: 500;
transition: var(--transition);
font-size: 14px;
}
.nav-link:hover {
color: var(--primary);
}
/* Hero区 */
.hero {
text-align: center;
margin-bottom: 64px;
}
.hero-title {
font-size: 48px;
font-weight: 800;
line-height: 1.1;
margin-bottom: 20px;
background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: 18px;
color: var(--gray-600);
max-width: 600px;
margin: 0 auto 32px;
line-height: 1.6;
}
.hero-cta {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
/* 按钮 */
.btn {
padding: 10px 20px;
border-radius: 10px;
font-weight: 600;
text-decoration: none;
transition: var(--transition);
border: none;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.btn-primary {
background-color: var(--primary);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 10px 20px -5px rgba(139, 92, 246, 0.3);
}
.btn-secondary {
background-color: var(--gray-50);
color: var(--gray-700);
border: 1px solid var(--gray-200);
}
.btn-secondary:hover {
background-color: var(--gray-100);
border-color: var(--gray-300);
transform: translateY(-2px);
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* 统计卡片 */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
margin-bottom: 64px;
}
.stat-card {
background-color: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
text-align: center;
transition: var(--transition);
border: 1px solid #F1F5F9;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
}
.stat-value {
font-size: 36px;
font-weight: 800;
background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.stat-label {
color: var(--gray-600);
font-size: 14px;
font-weight: 500;
}
/* 卡片通用样式 */
.card {
background-color: white;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
padding: 32px;
transition: var(--transition);
border: 1px solid #F1F5F9;
}
.card:hover {
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.03);
transform: translateY(-2px);
}
/* 闪卡样式 */
.flashcard {
max-width: 600px;
margin: 0 auto;
perspective: 1500px;
height: 380px;
cursor: pointer;
}
.flashcard-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
transform-style: preserve-3d;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
border-radius: 20px;
}
.flashcard.flipped .flashcard-inner {
transform: rotateY(180deg);
}
.flashcard-front, .flashcard-back {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
border-radius: 20px;
padding: 48px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flashcard-front {
background: linear-gradient(135deg, white 0%, #FAFAFA 100%);
border: 1px solid #F1F5F9;
}
.flashcard-back {
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
transform: rotateY(180deg);
border: 1px solid #C4B5FD;
}
/* 学习卡片 */
.learn-card {
max-width: 800px;
margin: 0 auto;
}
.learn-card-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 16px;
color: var(--gray-900);
}
.learn-card-meta {
color: var(--gray-500);
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--gray-100);
font-size: 14px;
}
.learn-card-description {
font-size: 16px;
line-height: 1.8;
color: var(--gray-700);
margin-bottom: 32px;
}
.learn-card-examples {
background-color: var(--gray-50);
padding: 24px;
border-radius: 14px;
margin-bottom: 32px;
}
.learn-card-example {
margin-bottom: 24px;
}
.learn-card-example:last-child {
margin-bottom: 0;
}
.example-title {
font-weight: 600;
color: var(--gray-900);
margin-bottom: 10px;
font-size: 15px;
}
.example-code {
background-color: var(--code-bg);
color: var(--code-text);
padding: 20px;
border-radius: 12px;
font-family: 'Fira Code', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
overflow-x: auto;
border: 1px solid var(--code-border);
white-space: pre;
word-wrap: normal;
}
.example-code::-webkit-scrollbar {
height: 6px;
}
.example-code::-webkit-scrollbar-track {
background: var(--code-border);
border-radius: 3px;
}
.example-code::-webkit-scrollbar-thumb {
background: #6366F1;
border-radius: 3px;
}
.example-code::-webkit-scrollbar-thumb:hover {
background: #818CF8;
}
.learn-navigation {
display: flex;
justify-content: space-between;
margin-top: 40px;
align-items: center;
}
/* 测试题样式 */
.quiz {
background-color: var(--gray-50);
padding: 24px;
border-radius: 14px;
}
.quiz-question {
font-size: 17px;
font-weight: 600;
margin-bottom: 20px;
color: var(--gray-900);
}
.quiz-options {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.quiz-option {
padding: 14px 18px;
background-color: white;
border: 2px solid var(--gray-200);
border-radius: 12px;
cursor: pointer;
transition: var(--transition);
font-size: 15px;
}
.quiz-option:hover {
border-color: var(--primary);
background-color: var(--primary-light);
}
.quiz-option.selected {
border-color: var(--primary);
background-color: var(--primary-light);
}
.quiz-option.correct {
border-color: var(--success);
background-color: #D1FAE5;
color: #065F46;
}
.quiz-option.incorrect {
border-color: var(--error);
background-color: #FEE2E2;
color: #991B1B;
}
.quiz-feedback {
padding: 16px;
border-radius: 12px;
font-weight: 600;
margin-bottom: 24px;
font-size: 14px;
}
.quiz-feedback.correct {
background-color: #D1FAE5;
color: #065F46;
}
.quiz-feedback.incorrect {
background-color: #FEE2E2;
color: #991B1B;
}
/* 索引网格 */
.roots-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.root-card {
background-color: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
text-decoration: none;
color: inherit;
transition: var(--transition);
border: 1px solid #F1F5F9;
border-left: 4px solid var(--primary);
}
.root-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
border-left-color: var(--primary-dark);
}
/* 搜索框 */
.search-box {
width: 100%;
max-width: 600px;
margin: 0 auto 40px;
}
.search-input {
width: 100%;
padding: 14px 18px;
border: 2px solid var(--gray-200);
border-radius: 12px;
font-size: 15px;
transition: var(--transition);
background-color: white;
}
.search-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
}
/* 分类筛选按钮 */
.filter-btn {
font-size: 13px !important;
}
.filter-btn.bg-primary\/10 {
background-color: var(--primary-light) !important;
color: var(--primary) !important;
border-color: var(--primary) !important;
}
/* 进度页面 */
.progress-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.progress-card {
background-color: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
text-align: center;
border: 1px solid #F1F5F9;
}
.progress-number {
font-size: 36px;
font-weight: 800;
background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.progress-label {
color: var(--gray-600);
font-size: 14px;
font-weight: 500;
}
.progress-list {
background-color: white;
padding: 32px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
border: 1px solid #F1F5F9;
}
.progress-list-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 24px;
color: var(--gray-900);
}
/* 工具类 */
.text-center { text-align: center; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mb-8 { margin-bottom: 32px; }
.text-3xl { font-size: 30px; }
.font-bold { font-weight: 700; }
.text-gray-900 { color: var(--gray-900); }
.text-gray-600 { color: var(--gray-600); }
.text-gray-500 { color: var(--gray-500); }
.text-sm { font-size: 14px; }
.text-xs { font-size: 12px; }
.font-medium { font-weight: 500; }
.px-2 { padding-left: 8px; padding-right: 8px; }
.py-1 { padding-top: 4px; padding-bottom: 4px; }
.px-3 { padding-left: 12px; padding-right: 12px; }
.rounded-full { border-radius: 9999px; }
.bg-blue-100 { background-color: #DBEAFE; }
.text-blue-800 { color: #1E40AF; }
.bg-green-100 { background-color: #D1FAE5; }
.text-green-800 { color: #065F46; }
.bg-yellow-100 { background-color: #FEF3C7; }
.text-yellow-800 { color: #92400E; }
.bg-primary\/10 { background-color: var(--primary-light); }
.text-primary { color: var(--primary); }
.hover\:underline:hover { text-decoration: underline; }
.hover\:bg-gray-200:hover { background-color: var(--gray-200); }
.bg-gray-100 { background-color: var(--gray-100); }
.text-gray-700 { color: var(--gray-700); }
.bg-green-50 { background-color: #ECFDF5; }
.text-green-700 { color: #047857; }
.hover\:bg-green-100:hover { background-color: #D1FAE5; }
.inline-flex { display: inline-flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.flex-wrap { flex-wrap: wrap; }
.justify-center { justify-content: center; }
.gap-4 { gap: 16px; }
.gap-2 { gap: 8px; }
.hidden { display: none; }
.mt-4 { margin-top: 16px; }
.mt-8 { margin-top: 32px; }
.flex { display: flex; }
.h-full { height: 100%; }
.rounded-full { border-radius: 9999px; }
.transition-all { transition: all 0.2s; }
.duration-500 { transition-duration: 500ms; }
/* 页脚 */
.footer {
margin-top: 80px;
padding: 40px 0;
border-top: 1px solid var(--gray-100);
text-align: center;
color: var(--gray-500);
}
.footer-tagline {
font-size: 20px;
font-weight: 700;
color: var(--gray-900);
margin-bottom: 12px;
}
.footer-description {
max-width: 600px;
margin: 0 auto 24px;
font-size: 15px;
line-height: 1.7;
}
/* 响应式适配 */
@media (max-width: 768px) {
.hero-title { font-size: 32px; }
.hero-subtitle { font-size: 16px; }
.nav-links { gap: 12px; font-size: 13px; }
.nav { flex-direction: column; gap: 16px; padding-bottom: 16px; }
.flashcard { height: 300px; }
.flashcard-front, .flashcard-back { padding: 24px; }
.roots-grid { grid-template-columns: 1fr; }
.stats, .progress-stats { grid-template-columns: repeat(2, 1fr); }
.card, .progress-list { padding: 24px; }
.learn-card-title { font-size: 24px; }
}
@media (max-width: 480px) {
.stats, .progress-stats { grid-template-columns: 1fr; }
.hero-cta { flex-direction: column; align-items: center; gap: 12px; }
.btn { width: 100%; max-width: 280px; justify-content: center; }
}
+172
View File
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>闪卡复习 - 现代CSS学习工坊</title>
<meta name="description" content="使用闪卡模式高效复习现代CSS知识点,支持翻牌和键盘快捷键">
<meta name="author" content="小秘书">
<meta name="language" content="zh-CN">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://modern-css-workshop.vercel.app/flashcard.html">
<meta property="og:title" content="闪卡复习 - 现代CSS学习工坊">
<meta property="og:description" content="使用闪卡模式高效复习现代CSS知识点,支持翻牌和键盘快捷键">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modern-css-workshop.vercel.app/flashcard.html">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#8B5CF6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CSS学习工坊">
<link rel="apple-touch-icon" href="./icon-192.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/minimal.css">
</head>
<body>
<div class="container">
<nav class="nav">
<a href="./index.html" class="nav-logo">🎨 现代CSS学习工坊</a>
<ul class="nav-links">
<li><a href="./learn.html" class="nav-link">开始学习</a></li>
<li><a href="./flashcard.html" class="nav-link" style="color: var(--primary);">闪卡复习</a></li>
<li><a href="./roots.html" class="nav-link">知识点索引</a></li>
<li><a href="./progress.html" class="nav-link">学习进度</a></li>
</ul>
</nav>
<div class="text-center mb-8">
<h1 class="text-3xl font-bold mb-2 text-gray-900">闪卡复习模式</h1>
<p class="text-gray-600">点击卡片翻转查看答案,使用键盘 ← → 切换卡片,空格键翻转</p>
<div class="mt-4 text-sm text-gray-500">
<span id="current-index">1</span> / <span id="total-count">18</span>
</div>
</div>
<div class="flashcard" id="flashcard">
<div class="flashcard-inner">
<div class="flashcard-front">
<div class="text-xl font-semibold text-gray-500 mb-4">问题:</div>
<h2 class="text-2xl font-bold text-gray-900 mb-6" id="card-question">加载中...</h2>
<p class="text-gray-500 text-sm">点击卡片查看答案 ↓</p>
</div>
<div class="flashcard-back">
<div class="text-xl font-semibold text-gray-500 mb-4">答案:</div>
<div class="text-lg text-gray-700 leading-relaxed" id="card-answer"></div>
<p class="text-gray-500 text-sm mt-6">点击卡片返回问题 ↑</p>
</div>
</div>
</div>
<div class="flex justify-center gap-4 mt-8">
<button id="prev-btn" class="btn btn-secondary">← 上一张</button>
<button id="flip-btn" class="btn btn-primary">翻转卡片</button>
<button id="next-btn" class="btn btn-secondary">下一张 →</button>
</div>
<div class="flex justify-center gap-4 mt-4">
<button id="mark-known-btn" class="btn bg-green-50 hover:bg-green-100 text-green-700">✅ 我已经掌握</button>
</div>
</div>
<script src="./js/wordData.js"></script>
<script src="./js/siteConfig.js"></script>
<script src="./js/storage.js"></script>
<script>
let currentIndex = 0;
let isFlipped = false;
function init() {
document.getElementById('total-count').textContent = WordRoots.length;
const lastVisited = Storage.getLastVisited();
if (lastVisited) {
const index = WordRoots.findIndex(item => item.id === lastVisited);
if (index !== -1) currentIndex = index;
}
renderCard();
updateIndexDisplay();
}
function renderCard() {
const item = WordRoots[currentIndex];
if (!item) return;
document.getElementById('card-question').textContent = `${item.root}\n${item.meaning}`;
document.getElementById('card-answer').textContent = item.description;
if (isFlipped) {
document.getElementById('flashcard').classList.remove('flipped');
isFlipped = false;
}
document.getElementById('prev-btn').disabled = currentIndex === 0;
document.getElementById('next-btn').disabled = currentIndex === WordRoots.length - 1;
if (Storage.isMastered(item.id)) {
document.getElementById('mark-known-btn').textContent = '✅ 已掌握';
document.getElementById('mark-known-btn').disabled = true;
} else {
document.getElementById('mark-known-btn').textContent = '✅ 我已经掌握';
document.getElementById('mark-known-btn').disabled = false;
}
Storage.markLearned(item.id);
Storage.saveLastVisited(item.id);
}
function flipCard() {
document.getElementById('flashcard').classList.toggle('flipped');
isFlipped = !isFlipped;
}
function prevCard() {
if (currentIndex > 0) {
currentIndex--;
renderCard();
updateIndexDisplay();
}
}
function nextCard() {
if (currentIndex < WordRoots.length - 1) {
currentIndex++;
renderCard();
updateIndexDisplay();
} else {
alert('🎉 已经复习完所有知识点啦!');
}
}
function updateIndexDisplay() {
document.getElementById('current-index').textContent = currentIndex + 1;
}
function markAsKnown() {
const item = WordRoots[currentIndex];
Storage.markMastered(item.id);
document.getElementById('mark-known-btn').textContent = '✅ 已掌握';
document.getElementById('mark-known-btn').disabled = true;
setTimeout(() => { nextCard(); }, 500);
}
document.getElementById('flashcard').addEventListener('click', flipCard);
document.getElementById('flip-btn').addEventListener('click', flipCard);
document.getElementById('prev-btn').addEventListener('click', prevCard);
document.getElementById('next-btn').addEventListener('click', nextCard);
document.getElementById('mark-known-btn').addEventListener('click', markAsKnown);
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') prevCard();
if (e.key === 'ArrowRight') nextCard();
if (e.key === ' ') { e.preventDefault(); flipCard(); }
});
init();
</script>
</body>
</html>
+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>现代CSS学习工坊</title>
<!-- SEO 基础 -->
<meta name="description" content="系统学习现代CSS核心特性,涵盖CSS3选择器、Flexbox/Grid布局、CSS动画、响应式设计、Tailwind CSS最佳实践、容器查询、:has选择器、滚动驱动动画等前沿技术。">
<meta name="keywords" content="CSS,CSS3,Flexbox,Grid,响应式设计,CSS动画,容器查询,Tailwind CSS,前端开发,现代CSS">
<meta name="author" content="小秘书">
<meta name="language" content="zh-CN">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://modern-css-workshop.vercel.app">
<!-- Open Graph -->
<meta property="og:title" content="现代CSS学习工坊">
<meta property="og:description" content="系统学习现代CSS核心特性,涵盖CSS3选择器、Flexbox/Grid布局、CSS动画、响应式设计等18个核心知识点。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modern-css-workshop.vercel.app">
<meta property="og:site_name" content="现代CSS学习工坊">
<meta property="og:locale" content="zh_CN">
<!-- Favicon -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
<!-- PWA -->
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#8B5CF6">
<!-- iOS Safari PWA -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CSS学习工坊">
<link rel="apple-touch-icon" href="./icon-192.png">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- 样式 -->
<link rel="stylesheet" href="./css/minimal.css">
</head>
<body>
<div class="container">
<!-- 导航 -->
<nav class="nav">
<a href="./index.html" class="nav-logo">🎨 现代CSS学习工坊</a>
<ul class="nav-links">
<li><a href="./learn.html" class="nav-link">开始学习</a></li>
<li><a href="./flashcard.html" class="nav-link">闪卡复习</a></li>
<li><a href="./roots.html" class="nav-link">知识点索引</a></li>
<li><a href="./progress.html" class="nav-link">学习进度</a></li>
</ul>
</nav>
<!-- Hero区 -->
<section class="hero">
<h1 class="hero-title">
18个核心知识点<br>
掌握现代CSS<br>
布局动画全搞定
</h1>
<p class="hero-subtitle">
从CSS3核心特性到容器查询、:has选择器、滚动驱动动画,系统学习现代CSS新特性和最佳实践
</p>
<div class="hero-cta">
<a href="./learn.html" class="btn btn-primary">开始第一个知识点 →</a>
<a href="./flashcard.html" class="btn btn-secondary">闪卡复习</a>
</div>
</section>
<!-- 统计卡片 -->
<section class="stats">
<div class="stat-card">
<div class="stat-value">18</div>
<div class="stat-label">核心知识点</div>
</div>
<div class="stat-card">
<div class="stat-value">50+</div>
<div class="stat-label">代码示例</div>
</div>
<div class="stat-card">
<div class="stat-value">15分钟</div>
<div class="stat-label">每日学习</div>
</div>
</section>
<!-- 介绍 -->
<section class="card">
<h2 style="font-size: 24px; font-weight: 700; margin-bottom: 16px; color: var(--gray-900);">什么是现代CSS</h2>
<p style="font-size: 18px; line-height: 1.8; color: var(--gray-700); margin-bottom: 16px;">
CSS已经从简单的样式语言进化为强大的视觉表达工具。现代CSS拥有完整的布局系统(Flexbox/Grid)、强大的选择器(:has()、容器查询)、原生动画(Transitions/Animations),甚至可以实现滚动驱动的交互效果。
</p>
<p style="font-size: 18px; line-height: 1.8; color: var(--gray-700); margin-bottom: 16px;">
同时,Tailwind CSS等原子化框架正在改变我们编写样式的方式,通过实用类直接在HTML中组合样式,大幅提升开发效率。
</p>
<p style="font-size: 18px; line-height: 1.8; color: var(--gray-700);">
本网站系统整理了现代CSS的18个核心知识点,从基础到进阶,帮助你建立完整的CSS知识体系,无论是构建响应式页面、实现复杂动画,还是使用最新CSS特性,都能游刃有余。
</p>
</section>
<!-- 页脚 -->
<footer class="footer">
<div class="footer-tagline">用纯CSS实现一切可能</div>
<p class="footer-description">
CSS已经从简单的样式语言进化为强大的视觉表达工具。本网站系统整理了CSS3核心特性、Flexbox/Grid布局、CSS动画、响应式设计、层叠层、:has选择器、滚动驱动动画等18个核心知识点,帮助你全面掌握现代CSS。
</p>
<p>© 2025 现代CSS学习工坊</p>
</footer>
</div>
<!-- 脚本 -->
<script src="./js/wordData.js"></script>
<script src="./js/siteConfig.js"></script>
<script src="./js/storage.js"></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
const siteConfig = {
// 基础信息
topic: "现代CSS",
siteName: "现代CSS学习工坊",
itemName: "知识点",
itemCount: 18,
// 首页Hero区
hero: {
title: [
"18个核心知识点",
"掌握现代CSS",
"布局动画全搞定"
],
subtitle: "从CSS3核心特性到容器查询、:has选择器、滚动驱动动画,系统学习现代CSS新特性和最佳实践",
animation: {
enabled: true,
demoCount: 5
}
},
// 统计卡片
stats: [
{ value: "18", label: "核心知识点" },
{ value: "50+", label: "代码示例" },
{ value: "15分钟", label: "每日学习" }
],
// 底部介绍
footer: {
tagline: "用纯CSS实现一切可能",
description: "CSS已经从简单的样式语言进化为强大的视觉表达工具。本网站系统整理了CSS3核心特性、Flex/Grid布局、CSS动画、响应式设计、层叠层、:has选择器、滚动驱动动画等18个核心知识点,帮助你全面掌握现代CSS。"
},
// 按钮文案
cta: {
primary: "开始第一个知识点 →",
secondary: "闪卡复习"
}
};
+145
View File
@@ -0,0 +1,145 @@
// 存储工具类,处理学习进度的本地存储
const Storage = {
// 存储键名
KEYS: {
PROGRESS: 'modern_css_workshop_progress',
MASTERED: 'modern_css_workshop_mastered',
LAST_VISITED: 'modern_css_workshop_last_visited'
},
// 初始化存储
init() {
try {
if (!localStorage.getItem(this.KEYS.PROGRESS)) {
localStorage.setItem(this.KEYS.PROGRESS, JSON.stringify({}));
}
if (!localStorage.getItem(this.KEYS.MASTERED)) {
localStorage.setItem(this.KEYS.MASTERED, JSON.stringify([]));
}
} catch (e) {
console.error('存储初始化失败:', e);
}
},
// 标记知识点为已学习
markLearned(id) {
try {
const progress = JSON.parse(localStorage.getItem(this.KEYS.PROGRESS) || '{}');
progress[id] = {
learnedAt: Date.now(),
completed: true
};
localStorage.setItem(this.KEYS.PROGRESS, JSON.stringify(progress));
return true;
} catch (e) {
console.error('标记学习进度失败:', e);
return false;
}
},
// 标记知识点为已掌握
markMastered(id) {
try {
const mastered = JSON.parse(localStorage.getItem(this.KEYS.MASTERED) || '[]');
if (!mastered.includes(id)) {
mastered.push(id);
localStorage.setItem(this.KEYS.MASTERED, JSON.stringify(mastered));
}
return true;
} catch (e) {
console.error('标记已掌握失败:', e);
return false;
}
},
// 获取学习进度
getProgress() {
try {
return JSON.parse(localStorage.getItem(this.KEYS.PROGRESS) || '{}');
} catch (e) {
console.error('获取学习进度失败:', e);
return {};
}
},
// 获取已掌握的知识点
getMastered() {
try {
return JSON.parse(localStorage.getItem(this.KEYS.MASTERED) || '[]');
} catch (e) {
console.error('获取已掌握知识点失败:', e);
return [];
}
},
// 检查是否已学习
isLearned(id) {
const progress = this.getProgress();
return !!progress[id]?.completed;
},
// 检查是否已掌握
isMastered(id) {
const mastered = this.getMastered();
return mastered.includes(id);
},
// 获取学习统计
getStats() {
try {
const progress = this.getProgress();
const mastered = this.getMastered();
const total = WordRoots.length;
const learnedCount = Object.values(progress).filter(item => item.completed).length;
const masteredCount = mastered.length;
return {
total,
learned: learnedCount,
mastered: masteredCount,
progress: total > 0 ? Math.round((learnedCount / total) * 100) : 0
};
} catch (e) {
console.error('获取统计信息失败:', e);
return { total: 0, learned: 0, mastered: 0, progress: 0 };
}
},
// 保存最后访问的知识点
saveLastVisited(id) {
try {
localStorage.setItem(this.KEYS.LAST_VISITED, String(id));
return true;
} catch (e) {
console.error('保存最后访问失败:', e);
return false;
}
},
// 获取最后访问的知识点
getLastVisited() {
try {
const id = localStorage.getItem(this.KEYS.LAST_VISITED);
return id ? parseInt(id, 10) : null;
} catch (e) {
console.error('获取最后访问失败:', e);
return null;
}
},
// 重置所有进度
reset() {
try {
localStorage.removeItem(this.KEYS.PROGRESS);
localStorage.removeItem(this.KEYS.MASTERED);
localStorage.removeItem(this.KEYS.LAST_VISITED);
this.init();
return true;
} catch (e) {
console.error('重置进度失败:', e);
return false;
}
}
};
Storage.init();
+585
View File
@@ -0,0 +1,585 @@
const WordRoots = [
// ========== CSS3 核心特性 ==========
{
id: 1,
root: "CSS选择器进阶 (Selectors)",
origin: "CSS3核心特性",
meaning: "CSS3新增的多种选择器,让样式选择更精准",
description: "CSS3引入了大量强大的选择器,包括属性选择器([attr^=value])、兄弟选择器(+、~)、子代选择器(>)、伪类选择器(:nth-child()、:not())等。这些选择器让你可以在不修改HTML结构的情况下精准定位目标元素,减少对JS的依赖,让样式与结构解耦。",
examples: [
{
word: "属性选择器",
meaning: "根据元素属性精确匹配元素",
breakdown: { root: "CSS选择器进阶" },
explanation: "input[type=\"text\"] { border: 1px solid blue; } → 只选中type为text的input元素;[class^=\"icon-\"] → 匹配class以icon-开头的所有元素;[data-role~=\"admin\"] → 匹配data-role属性值中包含admin的元素。"
},
{
word: "子代与兄弟选择器",
meaning: "精确控制层级和兄弟关系",
breakdown: { root: "CSS选择器进阶" },
explanation: ".parent > .child { color: red; } → 只选择直接子元素(不包含孙辈);h2 + p { margin-top: 0; } → 紧跟h2后的第一个p元素;h2 ~ p { color: gray; } → h2后的所有同级p元素。"
},
{
word: "结构伪类选择器",
meaning: "根据元素在DOM中的位置选择",
breakdown: { root: "CSS选择器进阶" },
explanation: "li:nth-child(2n) { background: #f0f0f0; } → 偶数行斑马纹;li:first-child { font-weight: bold; } → 第一个元素加粗;li:nth-last-child(-n+3) { color: red; } → 最后3个元素变红。"
}
],
quiz: {
question: "以下哪个选择器只选择直接子元素?",
options: ["selector descendant", "selector > child", "selector + sibling", "selector ~ siblings"],
correctAnswer: 1
}
},
{
id: 2,
root: "伪类与伪元素 (Pseudo)",
origin: "CSS3核心特性",
meaning: "使用伪类和伪元素为元素的不同状态或特定部分添加样式",
description: "伪类用于定义元素的特殊状态(如:hover、:focus、:first-child、:not()),伪元素用于创建不在DOM中的虚拟元素(如::before、::after、::first-line、::placeholder)。::before和::after配合content属性是最强大的CSS技巧之一,可以在不修改HTML的情况下添加装饰性内容。",
examples: [
{
word: "::before和::after装饰",
meaning: "无需额外HTML元素实现装饰效果",
breakdown: { root: "伪类与伪元素" },
explanation: ".card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: linear-gradient(to bottom, #3B82F6, #8B5CF6); } → 在卡片左侧添加渐变色条,无需额外HTML元素。"
},
{
word: ":not()排除选择",
meaning: "排除特定元素应用样式",
breakdown: { root: "伪类与伪元素" },
explanation: "input:not([type=\"submit\"]) { width: 100%; margin-bottom: 12px; } → 除了submit类型的input外,所有input都占满宽度并带底部间距,避免按钮被意外拉伸。"
},
{
word: "::placeholder自定义",
meaning: "美化输入框占位符文字",
breakdown: { root: "伪类与伪元素" },
explanation: "input::placeholder { color: #9CA3AF; font-style: italic; font-size: 14px; } → 自定义占位符文字的颜色、字体样式和大小,提升表单用户体验。"
}
],
quiz: {
question: "::before和::after伪元素必须配合哪个属性使用?",
options: ["display", "content", "position", "visibility"],
correctAnswer: 1
}
},
{
id: 3,
root: "盒模型与box-sizing",
origin: "CSS3核心特性",
meaning: "理解CSS盒模型,使用box-sizing控制尺寸计算方式",
description: "CSS盒模型定义了元素如何计算尺寸:content-box(默认)只计算内容区域,padding和border会增加总尺寸;border-box则将padding和border包含在width/height内,让布局计算更直观。现代开发中推荐全局设置* { box-sizing: border-box; },这是所有CSS框架和Reset/Normalize的标配。",
examples: [
{
word: "全局box-sizing重置",
meaning: "让所有元素使用border-box计算",
breakdown: { root: "盒模型与box-sizing" },
explanation: "*, *::before, *::after { box-sizing: border-box; } → 全局重置后,设置width: 300px的元素无论加多少padding和border,总宽度都是300px,布局计算不再意外撑破容器。"
},
{
word: "content-box vs border-box对比",
meaning: "两种盒模型的尺寸计算差异",
breakdown: { root: "盒模型与box-sizing" },
explanation: "content-box: width: 200px + padding: 20px*2 + border: 2px*2 = 实际宽度244pxborder-box: width: 200px(已包含padding和border= 实际宽度200px。border-box让布局更加可预测。"
},
{
word: "等宽两列布局",
meaning: "使用border-box轻松实现等宽布局",
breakdown: { root: "盒模型与box-sizing" },
explanation: ".col { width: 50%; padding: 20px; float: left; border: 1px solid #ddd; } → 在border-box下,两个50%宽度的列可以并排显示,padding和border不会导致换行,这是content-box无法做到的。"
}
],
quiz: {
question: "box-sizing: border-box的含义是?",
options: ["只计算内容区域", "padding和border包含在width/height内", "只计算padding", "忽略margin"],
correctAnswer: 1
}
},
// ========== Flex/Grid 布局 ==========
{
id: 4,
root: "Flexbox弹性布局",
origin: "布局系统",
meaning: "使用Flexbox实现一维布局,元素自动分配空间",
description: "Flexbox是一维布局模型,擅长处理单行或单列的布局。通过display: flex激活后,子元素变成flex items,可以使用flex-direction控制方向、justify-content控制主轴对齐、align-items控制交叉轴对齐、flex-grow/shrink/basis控制伸缩比例,轻松实现垂直居中、等分、自适应等常见布局需求。",
examples: [
{
word: "完美居中",
meaning: "Flexbox实现水平垂直居中",
breakdown: { root: "Flexbox弹性布局" },
explanation: ".container { display: flex; justify-content: center; align-items: center; min-height: 100vh; } → 三行代码实现完美居中,无需计算margin或使用绝对定位,是Flexbox最经典的使用场景。"
},
{
word: "导航栏两端分布",
meaning: "使用justify-content: space-between实现",
breakdown: { root: "Flexbox弹性布局" },
explanation: "nav { display: flex; justify-content: space-between; align-items: center; padding: 0 24px; } → Logo在左,导航菜单在右,自动分配剩余空间,中间不留空白。"
},
{
word: "自适应等分卡片",
meaning: "flex-grow实现自动分配剩余空间",
breakdown: { root: "Flexbox弹性布局" },
explanation: ".card { flex: 1; min-width: 250px; } → flex: 1等于flex-grow: 1; flex-shrink: 1; flex-basis: 0%,所有卡片平分剩余空间,min-width防止卡片过窄。"
}
],
quiz: {
question: "flex: 1 等同于以下哪个设置?",
options: ["flex-grow: 1; flex-shrink: 0; flex-basis: auto", "flex-grow: 1; flex-shrink: 1; flex-basis: 0%", "flex-grow: 0; flex-shrink: 1; flex-basis: 100%", "flex-grow: 1; flex-shrink: 1; flex-basis: 100%"],
correctAnswer: 1
}
},
{
id: 5,
root: "Grid网格布局",
origin: "布局系统",
meaning: "使用CSS Grid实现二维网格布局",
description: "Grid是二维布局系统,可以同时控制行和列。通过display: grid激活,使用grid-template-columns/rows定义网格轨道,gap设置间距,grid-column/row让项目跨越多行多列。配合repeat()、fr单位、minmax()、auto-fill/auto-fit等函数,可以实现极其复杂的响应式网格布局,且无需媒体查询。",
examples: [
{
word: "响应式自动网格",
meaning: "无需媒体查询实现响应式网格",
breakdown: { root: "Grid网格布局" },
explanation: ".grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; } → 自动根据容器宽度决定列数,每个卡片最小280px,最大占满剩余空间(1fr),一行写尽响应式。"
},
{
word: "圣杯布局",
meaning: "使用Grid实现经典三栏布局",
breakdown: { root: "Grid网格布局" },
explanation: ".layout { display: grid; grid-template-columns: 200px 1fr 200px; grid-template-rows: auto 1fr auto; } → header和footer跨3列,sidebar左200pxcontent占满中间,sidebar右200px,轻松实现经典圣杯布局。"
},
{
word: "fr单位与minmax()",
meaning: "使用fr和minmax实现灵活网格",
breakdown: { root: "Grid网格布局" },
explanation: ".grid { grid-template-columns: 1fr 2fr 1fr; } → 三列宽度比为1:2:1.grid { grid-template-columns: minmax(200px, 1fr) 3fr; } → 第一列最小200px最大1fr,第二列固定3fr,灵活又有底线。"
}
],
quiz: {
question: "Grid中repeat(auto-fill, minmax(280px, 1fr))的作用是?",
options: ["固定3列布局", "自动根据宽度决定列数,每列最小280px", "每列固定280px", "只能填充4列"],
correctAnswer: 1
}
},
{
id: 6,
root: "Subgrid子网格",
origin: "布局系统",
meaning: "子元素继承父Grid的轨道,实现精准对齐",
description: "Subgrid是Grid的进阶特性,允许嵌套的Grid继承父容器的网格轨道定义。通过grid-template-columns: subgrid声明,子网格可以沿用父级的列定义,解决嵌套Grid无法对齐的问题。这在卡片列表、表格布局、表单对齐等场景中非常有用,让多层嵌套的元素也能完美对齐。",
examples: [
{
word: "卡片列表对齐",
meaning: "卡片标题和内容列对齐",
breakdown: { root: "Subgrid子网格" },
explanation: ".cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }\n.card { display: grid; grid-template-rows: subgrid; grid-row: span 3; } → 所有卡片的标题区、内容区、底部区分别对齐,即使内容长度不同也能保持整齐。"
},
{
word: "表单标签对齐",
meaning: "多列表单的标签列完美对齐",
breakdown: { root: "Subgrid子网格" },
explanation: ".form-group { display: grid; grid-template-columns: subgrid; grid-column: span 2; }\n.form-group label { grid-column: 1; }\n.form-group input { grid-column: 2; } → 多行表单的标签列宽度一致,输入框列对齐,即使标签文字长度不同。"
},
{
word: "兼容性说明",
meaning: "Subgrid的浏览器支持情况",
breakdown: { root: "Subgrid子网格" },
explanation: "Subgrid在Firefox 71+、Chrome 117+、Safari 16+中已支持,覆盖绝大多数现代浏览器。对于不支持的浏览器,可以降级为普通Grid布局,使用@supports (grid-template-columns: subgrid)进行渐进增强。"
}
],
quiz: {
question: "声明子网格继承父Grid轨道的语法是?",
options: ["grid-template-columns: inherit", "grid-template-columns: subgrid", "grid-template-columns: nested", "grid-template-columns: auto"],
correctAnswer: 1
}
},
// ========== CSS动画 ==========
{
id: 7,
root: "CSS过渡动画 (Transitions)",
origin: "CSS动画",
meaning: "使用transition实现平滑的CSS属性变化",
description: "CSS Transition允许属性值在指定时间内平滑过渡,而非瞬间变化。通过transition-property指定要过渡的属性、transition-duration设置时长、transition-timing-function控制速度曲线(ease、linear、ease-in-out等)、transition-delay设置延迟。最常用于hover效果、菜单展开收起、模态框弹出等交互场景。",
examples: [
{
word: "按钮hover效果",
meaning: "平滑过渡按钮背景色和变换",
breakdown: { root: "CSS过渡动画" },
explanation: ".btn { background: #3B82F6; transform: translateY(0); transition: all 0.3s ease; }\n.btn:hover { background: #2563EB; transform: translateY(-2px); } → 鼠标悬停时按钮颜色变深并上移2px,过渡流畅自然。"
},
{
word: "手风琴展开收起",
meaning: "使用max-height实现平滑展开",
breakdown: { root: "CSS过渡动画" },
explanation: ".accordion-content { max-height: 0; overflow: hidden; transition: max-height 0.4s ease-out; }\n.accordion-content.open { max-height: 500px; } → 点击时max-height从0过渡到500px,实现平滑展开效果,比直接切换display更优雅。"
},
{
word: "多属性过渡",
meaning: "同时过渡多个CSS属性",
breakdown: { root: "CSS过渡动画" },
explanation: ".card { transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.2s linear; } → 变换和阴影用0.3s缓动,边框颜色用0.2s线性变化,不同属性可以设置不同的过渡时长和速度曲线。"
}
],
quiz: {
question: "以下哪个timing-function表示先加速后减速的过渡?",
options: ["linear", "ease-in", "ease-out", "ease-in-out"],
correctAnswer: 3
}
},
{
id: 8,
root: "CSS关键帧动画 (Animations)",
origin: "CSS动画",
meaning: "使用@keyframes和animation属性创建复杂动画",
description: "CSS Animations比Transitions更强大,可以定义多个关键帧(0%到100%),实现多阶段动画。通过@keyframes定义动画序列,使用animation属性控制动画名称、时长、速度曲线、延迟、次数(infinite无限循环)、方向(normal/reverse/alternate)和填充模式(forwards/backwards)。无需JavaScript即可实现旋转、弹跳、闪烁、打字等丰富动画效果。",
examples: [
{
word: "加载旋转动画",
meaning: "无限旋转的加载指示器",
breakdown: { root: "CSS关键帧动画" },
explanation: "@keyframes spin { to { transform: rotate(360deg); } }\n.loader { width: 40px; height: 40px; border: 4px solid #ddd; border-top-color: #3B82F6; border-radius: 50%; animation: spin 1s linear infinite; } → 纯CSS实现加载旋转动画,无需任何JS。"
},
{
word: "弹跳入场动画",
meaning: "元素弹跳进入视口",
breakdown: { root: "CSS关键帧动画" },
explanation: "@keyframes bounceIn { 0% { transform: scale(0.3); opacity: 0; } 50% { transform: scale(1.05); } 70% { transform: scale(0.9); } 100% { transform: scale(1); opacity: 1; } }\n.element { animation: bounceIn 0.8s ease-out; } → 多关键帧实现弹跳效果,比简单淡入更有视觉冲击力。"
},
{
word: "打字机光标效果",
meaning: "模拟打字机光标闪烁",
breakdown: { root: "CSS关键帧动画" },
explanation: "@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }\n.cursor { display: inline-block; width: 2px; height: 1em; background: #333; animation: blink 1s step-end infinite; } → 使用step-end实现瞬时切换,模拟光标闪烁效果。"
}
],
quiz: {
question: "animation: name 2s ease infinite中的infinite表示?",
options: ["动画只播放一次", "动画无限循环播放", "动画反向播放", "动画延迟2秒"],
correctAnswer: 1
}
},
// ========== 响应式设计 ==========
{
id: 9,
root: "响应式设计与媒体查询",
origin: "响应式设计",
meaning: "使用媒体查询实现不同屏幕尺寸的自适应布局",
description: "响应式设计的核心是媒体查询(@media),可以根据视口宽度、设备类型、方向等条件应用不同的样式。推荐采用移动优先策略:先写移动端基础样式,再用min-width媒体查询逐步增强到大屏。现代响应式设计还结合了容器查询、Grid的auto-fill/minmax、clamp()等新技术,让响应式更加智能和精细。",
examples: [
{
word: "移动优先媒体查询",
meaning: "从小屏到大屏逐步增强",
breakdown: { root: "响应式设计与媒体查询" },
explanation: ".container { padding: 16px; }\n@media (min-width: 768px) { .container { padding: 24px; max-width: 720px; margin: 0 auto; } }\n@media (min-width: 1024px) { .container { max-width: 960px; } }\n@media (min-width: 1280px) { .container { max-width: 1200px; } } → 移动优先,逐步增强。"
},
{
word: "响应式排版",
meaning: "字号和行宽随屏幕自适应",
breakdown: { root: "响应式设计与媒体查询" },
explanation: "h1 { font-size: clamp(1.75rem, 4vw + 1rem, 3.5rem); }\n.container { max-width: min(90vw, 75ch); margin-inline: auto; } → clamp()让字号在小屏不小于1.75rem,大屏不超过3.5rem,中间随视口线性变化;75ch是最佳阅读宽度。"
},
{
word: "响应式图片",
meaning: "图片自适应容器宽度",
breakdown: { root: "响应式设计与媒体查询" },
explanation: "img { max-width: 100%; height: auto; display: block; } → 图片最大宽度不超过容器,高度自动保持比例,消除底部间隙(display: block),这是响应式图片的基础三件套。"
}
],
quiz: {
question: "移动优先策略中,媒体查询应该使用?",
options: ["max-width", "min-width", "device-width", "orientation"],
correctAnswer: 1
}
},
{
id: 10,
root: "流体排版与clamp()",
origin: "响应式设计",
meaning: "使用clamp()等CSS函数实现平滑的流体排版",
description: "CSS流体排版让元素尺寸在不同视口下平滑变化,而非在断点处跳跃。clamp(min, preferred, max)函数限制值在最小和最大之间,preferred通常使用视口单位(vw)实现缩放。配合calc()、min()、max()等CSS数学函数,可以创建极其精细的响应式排版和间距系统,无需大量媒体查询。",
examples: [
{
word: "流体标题字号",
meaning: "标题字号随视口平滑变化",
breakdown: { root: "流体排版与clamp()" },
explanation: "h1 { font-size: clamp(2rem, 5vw + 0.5rem, 4rem); } → 最小2rem,最大4rem,中间根据视口宽度在5vw+0.5rem和最大/最小值之间取合适值,无需媒体查询实现平滑缩放。"
},
{
word: "流体间距",
meaning: "间距随视口平滑变化",
breakdown: { root: "流体排版与clamp()" },
explanation: ":root { --spacing-lg: clamp(1.5rem, 3vw + 0.5rem, 3rem); }\n.section { margin-bottom: var(--spacing-lg); } → 使用CSS变量+clamp()定义流体间距,在整个站点统一使用,间距随视口平滑变化。"
},
{
word: "min()与max()函数",
meaning: "设置最大/最小值约束",
breakdown: { root: "流体排版与clamp()" },
explanation: ".container { width: min(90%, 1200px); } → 容器宽度取90%视口宽度和1200px中较小的值,避免在大屏上过宽;.sidebar { width: max(250px, 20%); } → 侧边栏最小250px,但至少要占20%。"
}
],
quiz: {
question: "clamp(1rem, 2vw + 0.5rem, 3rem)中,如果2vw+0.5rem=2rem,最终字号是?",
options: ["1rem", "2rem", "3rem", "1.5rem"],
correctAnswer: 1
}
},
// ========== CSS变量与函数 ==========
{
id: 11,
root: "CSS自定义属性 (Variables)",
origin: "CSS变量与函数",
meaning: "使用CSS变量实现可维护的主题和样式系统",
description: "CSS自定义属性(CSS变量)以--开头声明,通过var()函数引用,具有级联特性。相比预处理器变量(Sass/Less),CSS变量可以在运行时修改、支持JavaScript交互、具有作用域(可在特定选择器内重新定义)。它们是构建主题切换、设计系统、动态样式的基石,现代CSS开发几乎离不开CSS变量。",
examples: [
{
word: "设计令牌系统",
meaning: "用CSS变量定义设计系统的核心值",
breakdown: { root: "CSS自定义属性" },
explanation: ":root {\n --color-primary: #3B82F6;\n --color-primary-hover: #2563EB;\n --spacing-sm: 8px;\n --spacing-md: 16px;\n --spacing-lg: 24px;\n --radius: 8px;\n --shadow: 0 2px 8px rgba(0,0,0,0.1);\n} → 集中管理所有设计值,修改一处全局生效。"
},
{
word: "主题切换",
meaning: "运行时切换CSS变量值实现换肤",
breakdown: { root: "CSS自定义属性" },
explanation: ".dark {\n --bg: #1a1a2e;\n --text: #eee;\n --card-bg: #16213e;\n}\nbody { background: var(--bg); color: var(--text); }\n.card { background: var(--card-bg); } → 只需在根元素切换class,所有使用变量的元素自动更新颜色,无需额外JS逻辑。"
},
{
word: "组件级变量覆盖",
meaning: "在特定组件内覆盖全局变量",
breakdown: { root: "CSS自定义属性" },
explanation: ".alert {\n --alert-bg: #DBEAFE;\n --alert-text: #1E40AF;\n background: var(--alert-bg);\n color: var(--alert-text);\n}\n.alert-danger {\n --alert-bg: #FEE2E2;\n --alert-text: #991B1B;\n} → 子组件通过重新定义变量实现变体,保持HTML结构一致。"
}
],
quiz: {
question: "CSS自定义属性的声明格式是?",
options: ["$variable-name", "@variable-name", "--variable-name", "var(variable-name)"],
correctAnswer: 2
}
},
{
id: 12,
root: "CSS函数 (calc/min/max/clamp)",
origin: "CSS变量与函数",
meaning: "CSS数学函数实现动态计算和约束",
description: "CSS提供了一组数学函数:calc()进行加减乘除运算、min()取最小值、max()取最大值、clamp()在范围内取值。这些函数让CSS具备了计算能力,可以在运行时根据视口、变量、混合单位(px+vw+%)动态计算属性值,是实现流体排版、响应式组件、自适应布局的强大工具。",
examples: [
{
word: "calc()动态计算",
meaning: "混合单位计算实现精确布局",
breakdown: { root: "CSS函数" },
explanation: ".sidebar { width: calc(300px + 2rem); }\n.main { width: calc(100% - 300px - 2rem); gap: calc(var(--spacing) * 2); } → calc()支持混合单位(px+rem+%),还可以引用CSS变量进行数学运算,注意运算符两侧必须有空格。"
},
{
word: "min()限制最大宽度",
meaning: "优雅的响应式宽度控制",
breakdown: { root: "CSS函数" },
explanation: ".hero-img { width: min(100%, 800px); } → 图片宽度最大800px,在小屏下占满容器,大屏下不超过800px,一行代码实现响应式图片宽度。"
},
{
word: "max()设置最小值",
meaning: "确保元素不会过小",
breakdown: { root: "CSS函数" },
explanation: ".btn { padding: max(0.5rem, 2vw) max(1rem, 4vw); } → 内边距最小为0.5rem 1rem,随着视口增大而增大,确保按钮在小屏下不会过于紧凑。"
}
],
quiz: {
question: "calc(100% - 2rem)的正确写法中,运算符两侧必须?",
options: ["不需要空格", "必须有空格", "只能有一个空格", "可以用括号包裹"],
correctAnswer: 1
}
},
// ========== 现代CSS新特性 ==========
{
id: 13,
root: "容器查询 (Container Queries)",
origin: "CSS新特性",
meaning: "根据父容器大小而非视口大小应用样式",
description: "容器查询是CSS的革命性特性,让组件根据自身容器宽度而非视口宽度来调整样式。通过container-type定义查询容器,使用@container编写条件样式。这解决了组件在不同上下文(侧边栏vs主内容区)中需要不同布局的问题,是组件级响应式设计的关键,让真正的可复用组件成为可能。",
examples: [
{
word: "基础容器查询",
meaning: "组件根据容器宽度改变布局",
breakdown: { root: "容器查询" },
explanation: ".card-wrapper { container-type: inline-size; }\n@container (min-width: 500px) {\n .card { display: flex; gap: 20px; }\n .card img { width: 200px; }\n}\n/* 默认垂直布局 */\n.card { display: flex; flex-direction: column; } → 容器超过500px时水平排列,否则垂直排列,与视口无关。"
},
{
word: "命名容器查询",
meaning: "使用命名容器避免冲突",
breakdown: { root: "容器查询" },
explanation: ".sidebar { container-type: inline-size; container-name: sidebar; }\n@container sidebar (min-width: 300px) {\n .widget { grid-template-columns: 1fr 1fr; }\n} → 给容器命名,查询时使用容器名,避免多层嵌套容器时的查询冲突。"
},
{
word: "容器查询单位",
meaning: "使用cqw/cqh/cqi等容器单位",
breakdown: { root: "容器查询" },
explanation: ".card-title { font-size: clamp(1rem, 4cqi + 0.5rem, 1.5rem); } → 使用cqi(容器内联尺寸单位)替代vw,让字号随容器宽度变化而非视口宽度,在侧边栏和主内容区中呈现不同的字号。"
}
],
quiz: {
question: "声明元素为容器查询容器的属性是?",
options: ["container: true", "container-type: inline-size", "query-container: yes", "container-query: on"],
correctAnswer: 1
}
},
{
id: 14,
root: "层叠层 (Cascade Layers)",
origin: "CSS新特性",
meaning: "使用@layer控制CSS规则的优先级",
description: "@layer是CSS层叠层特性,允许开发者显式声明样式层的优先级顺序,解决!important滥用和选择器特异性竞争的问题。通过@layer声明层和定义优先级顺序,不同层中的样式按照层顺序而非特异性来决定胜负。这对于大型项目、设计系统、第三方库集成等场景特别有用,让CSS优先级变得可预测和可控。",
examples: [
{
word: "声明层优先级顺序",
meaning: "定义多个层的优先级关系",
breakdown: { root: "层叠层" },
explanation: "@layer reset, base, components, utilities;\n@layer reset { * { margin: 0; box-sizing: border-box; } }\n@layer base { body { font-family: sans-serif; } }\n@layer components { .btn { padding: 8px 16px; } }\n@layer utilities { .hidden { display: none; } } → 优先级:utilities > components > base > reset,无论选择器特异性如何。"
},
{
word: "第三方库样式覆盖",
meaning: "使用层叠层优雅覆盖第三方样式",
breakdown: { root: "层叠层" },
explanation: "@layer vendor, custom;\n@import 'vendor-lib.css' layer(vendor);\n@layer custom {\n .vendor-component { /* 覆盖第三方库样式 */ }\n} → 将第三方库样式放在vendor层,自定义样式放在custom层,无需!important即可覆盖。"
},
{
word: "嵌套层",
meaning: "层可以嵌套,形成层级结构",
breakdown: { root: "层叠层" },
explanation: "@layer base {\n @layer typography {\n h1 { font-size: 2rem; }\n p { line-height: 1.6; }\n }\n @layer layout {\n .container { max-width: 1200px; }\n }\n} → 层可以嵌套,typography和layout都属于base层,但它们之间的优先级由声明顺序决定。"
}
],
quiz: {
question: "@layer reset, base, components; 中优先级最高的是?",
options: ["reset", "base", "components", "三者相同"],
correctAnswer: 2
}
},
{
id: 15,
root: ":has() 选择器",
origin: "CSS新特性",
meaning: "CSS父选择器,根据子元素状态选择父元素",
description: ":has()被称为\"CSS父选择器\",允许根据子元素或后代元素的状态来选择父元素,这是CSS领域长久以来缺失的能力。例如可以选择包含特定子元素的父元素、选择有焦点子元素的父表单等。它的出现让很多以前需要JavaScript才能实现的功能现在可以纯CSS完成,大大减少了JS的使用场景。",
examples: [
{
word: "条件样式",
meaning: "根据子元素是否存在应用父元素样式",
breakdown: { root: ":has() 选择器" },
explanation: ".card:has(img) { display: grid; grid-template-columns: 1fr 1fr; }\n.card:not(:has(img)) { display: block; text-align: center; } → 有图片的卡片使用网格布局,没有图片的卡片使用块级居中布局,无需JS判断。"
},
{
word: "表单状态反馈",
meaning: "根据输入框状态设置父容器样式",
breakdown: { root: ":has() 选择器" },
explanation: ".form-group:has(input:focus) { border-color: #3B82F6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }\n.form-group:has(input:invalid) { border-color: #EF4444; } → 输入框聚焦时父容器高亮,输入无效时父容器变红,纯CSS实现表单验证反馈。"
},
{
word: "列表条件样式",
meaning: "根据列表项数量应用不同样式",
breakdown: { root: ":has() 选择器" },
explanation: "ul:has(> li:nth-child(4)) { columns: 2; } → 当列表超过3项时自动分两列显示;\nav:has(a:hover) a:not(:hover) { opacity: 0.6; } → 导航栏中hover一个链接时,其他链接变淡,突出当前链接。"
}
],
quiz: {
question: ":has() 选择器常被称为?",
options: ["子选择器", "兄弟选择器", "CSS父选择器", "后代选择器"],
correctAnswer: 2
}
},
{
id: 16,
root: "CSS滚动吸附 (Scroll Snap)",
origin: "CSS新特性",
meaning: "使用Scroll Snap实现滚动对齐效果",
description: "CSS Scroll Snap让滚动自动对齐到特定位置,无需JavaScript即可实现轮播图、全屏滚动、粘性章节等效果。通过scroll-snap-type在容器上定义吸附类型(x/y/both),使用scroll-snap-align在项目上定义吸附位置(start/center/end),配合scroll-snap-stop控制吸附强度。这是实现轮播和全屏滚动最简洁的CSS方案。",
examples: [
{
word: "水平轮播图",
meaning: "纯CSS实现水平滚动轮播",
breakdown: { root: "CSS滚动吸附" },
explanation: ".carousel { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 16px; padding: 16px; }\n.carousel-item { flex: 0 0 100%; scroll-snap-align: center; } → 水平滚动时每个项目自动对齐到中心,支持触摸滑动,无需任何JS库。"
},
{
word: "全屏滚动页面",
meaning: "每次滚动完整屏切换",
breakdown: { root: "CSS滚动吸附" },
explanation: ".fullpage { height: 100vh; overflow-y: scroll; scroll-snap-type: y mandatory; }\n.section { height: 100vh; scroll-snap-align: start; } → 每次滚动自动对齐到下一个全屏section,适合单页展示网站、产品介绍页等场景。"
},
{
word: "粘性章节导航",
meaning: "滚动时章节标题吸附到顶部",
breakdown: { root: "CSS滚动吸附" },
explanation: ".article { scroll-snap-type: y proximity; }\n.article h2 { scroll-snap-align: start; position: sticky; top: 0; background: white; padding: 12px 0; border-bottom: 1px solid #eee; } → 滚动时每个h2章节标题吸附到顶部并sticky固定,形成自然的章节分隔效果。"
}
],
quiz: {
question: "scroll-snap-type: x mandatory中mandatory的含义是?",
options: ["滚动时可以不对齐", "滚动停止时必须对齐到吸附点", "只在x轴吸附", "禁止滚动"],
correctAnswer: 1
}
},
{
id: 17,
root: "现代CSS色彩系统",
origin: "CSS新特性",
meaning: "CSS新的色彩函数和宽色域支持",
description: "CSS新增了多种色彩函数和色彩空间支持,包括oklch()、oklab()、color()、color-mix()、color-contrast()等。oklch基于人眼感知均匀分布,比hsl更自然;color-mix()可以混合两个颜色;新的语法支持P3和rec2020等宽色域,让网页色彩更加丰富和精确。这些是现代CSS色彩设计的未来标准。",
examples: [
{
word: "oklch()色彩函数",
meaning: "使用感知均匀的色彩空间",
breakdown: { root: "现代CSS色彩系统" },
explanation: ":root {\n --primary: oklch(65% 0.25 250);\n --primary-light: oklch(85% 0.12 250);\n --primary-dark: oklch(45% 0.3 250);\n} → oklch(亮度 色度 色相),相同亮度变化在视觉上感知一致,生成的色板比hsl更自然。"
},
{
word: "color-mix()颜色混合",
meaning: "在CSS中直接混合两个颜色",
breakdown: { root: "现代CSS色彩系统" },
explanation: ".btn:hover { background: color-mix(in oklch, var(--btn-bg) 80%, black); }\n.btn:active { background: color-mix(in oklch, var(--btn-bg) 90%, white); } → hover时混合20%黑色变深,active时混合10%白色变亮,无需预设多个颜色变量。"
},
{
word: "相对颜色语法",
meaning: "基于已有颜色创建变体",
breakdown: { root: "现代CSS色彩系统" },
explanation: ".card { background: oklch(from var(--primary) 95% 0.05 h); }\n.border { border-color: oklch(from var(--primary) l 0.3 h); } → 使用from语法基于主色调创建不同亮度和色度的变体,保持色相一致,自动生成配套色板。"
}
],
quiz: {
question: "oklch()的三个参数分别代表?",
options: ["红 绿 蓝", "色相 饱和度 亮度", "亮度 色度 色相", "透明度 色相 饱和度"],
correctAnswer: 2
}
},
// ========== 滚动与视口 ==========
{
id: 18,
root: "滚动驱动动画 (Scroll-Driven)",
origin: "CSS新特性",
meaning: "使用CSS实现滚动触发的动画效果",
description: "滚动驱动动画是CSS的最新特性,允许将动画进度绑定到滚动位置,无需JavaScript监听scroll事件。通过animation-timeline: scroll()或view(),可以创建进度条、视差滚动、滚动触发的淡入淡出等效果。这是CSS动画领域的重大突破,让复杂的滚动交互可以用纯CSS实现,性能远超JS方案。",
examples: [
{
word: "页面阅读进度条",
meaning: "顶部显示页面滚动进度条",
breakdown: { root: "滚动驱动动画" },
explanation: ".progress-bar {\n position: fixed; top: 0; left: 0; height: 4px;\n background: linear-gradient(90deg, #3B82F6, #8B5CF6);\n animation: grow-progress auto linear;\n animation-timeline: scroll(root);\n}\n@keyframes grow-progress { from { width: 0; } to { width: 100%; } } → 页面滚动时进度条从0%到100%,无需任何JS。"
},
{
word: "滚动触发的元素淡入",
meaning: "元素进入视口时淡入显示",
breakdown: { root: "滚动驱动动画" },
explanation: ".fade-in {\n animation: fadeIn linear;\n animation-timeline: view();\n animation-range: entry 0% entry 30%;\n}\n@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } → 元素进入视口时自动淡入,30%的进入范围完成动画。"
},
{
word: "视差滚动效果",
meaning: "背景以不同速度滚动产生视差",
breakdown: { root: "滚动驱动动画" },
explanation: ".parallax-bg {\n animation: parallax linear;\n animation-timeline: scroll(root);\n}\n@keyframes parallax { from { transform: translateY(0); } to { transform: translateY(-100px); } } → 背景元素以较慢速度随滚动移动,产生视差效果,无需JS计算滚动位置。"
}
],
quiz: {
question: "animation-timeline: scroll(root)中的root表示?",
options: ["根元素<html>", "文档滚动容器", "最近的滚动容器", "视口"],
correctAnswer: 1
}
}
];
+208
View File
@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学习 - 现代CSS学习工坊</title>
<meta name="description" content="系统学习现代CSS核心知识点,渐进式学习,逐步掌握CSS3特性、Flex/Grid布局、CSS动画、响应式设计等">
<meta name="keywords" content="CSS,CSS3,学习,教程,布局,动画,响应式">
<meta name="author" content="小秘书">
<meta name="language" content="zh-CN">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://modern-css-workshop.vercel.app/learn.html">
<meta property="og:title" content="学习 - 现代CSS学习工坊">
<meta property="og:description" content="系统学习现代CSS核心知识点,渐进式学习,逐步掌握CSS3特性、Flex/Grid布局等">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modern-css-workshop.vercel.app/learn.html">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#8B5CF6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CSS学习工坊">
<link rel="apple-touch-icon" href="./icon-192.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/minimal.css">
</head>
<body>
<div class="container">
<!-- 导航 -->
<nav class="nav">
<a href="./index.html" class="nav-logo">🎨 现代CSS学习工坊</a>
<ul class="nav-links">
<li><a href="./learn.html" class="nav-link" style="color: var(--primary);">开始学习</a></li>
<li><a href="./flashcard.html" class="nav-link">闪卡复习</a></li>
<li><a href="./roots.html" class="nav-link">知识点索引</a></li>
<li><a href="./progress.html" class="nav-link">学习进度</a></li>
</ul>
</nav>
<!-- 学习卡片 -->
<div class="learn-card card" id="learnCard">
<div class="learn-card-title" id="cardTitle">加载中...</div>
<div class="learn-card-meta" id="cardMeta"></div>
<div class="learn-card-description" id="cardDescription"></div>
<div class="learn-card-examples" id="cardExamples"></div>
<div class="quiz" id="quizSection" style="display: none;">
<div class="quiz-question" id="quizQuestion"></div>
<div class="quiz-options" id="quizOptions"></div>
<div id="quizFeedback" class="quiz-feedback" style="display: none;"></div>
<button id="nextBtn" class="btn btn-primary" style="display: none;">下一个知识点 →</button>
</div>
<div class="learn-navigation">
<button id="prevBtn" class="btn btn-secondary">← 上一个</button>
<button id="markMasteredBtn" class="btn btn-primary" style="display: none;">标记为已掌握 ✓</button>
</div>
</div>
</div>
<script src="./js/wordData.js"></script>
<script src="./js/siteConfig.js"></script>
<script src="./js/storage.js"></script>
<script>
let currentIndex = 0;
let selectedOption = null;
let answered = false;
function init() {
const lastVisited = Storage.getLastVisited();
if (lastVisited) {
const index = WordRoots.findIndex(item => item.id === lastVisited);
if (index !== -1) currentIndex = index;
}
renderCard();
}
function renderCard() {
const item = WordRoots[currentIndex];
if (!item) return;
document.title = `${item.root} - 现代CSS学习工坊`;
document.getElementById('cardTitle').textContent = item.root;
document.getElementById('cardMeta').textContent = `${item.origin} · 知识点 ${currentIndex + 1}/${WordRoots.length}`;
document.getElementById('cardDescription').textContent = item.description;
const examplesHtml = item.examples.map(example => `
<div class="learn-card-example">
<div class="example-title">${example.word}${example.meaning}</div>
<div class="example-code">${escapeHtml(example.explanation)}</div>
</div>
`).join('');
document.getElementById('cardExamples').innerHTML = examplesHtml;
renderQuiz(item.quiz);
selectedOption = null;
answered = false;
document.getElementById('quizFeedback').style.display = 'none';
document.getElementById('nextBtn').style.display = 'none';
if (Storage.isMastered(item.id)) {
document.getElementById('markMasteredBtn').textContent = '✓ 已掌握';
document.getElementById('markMasteredBtn').disabled = true;
} else {
document.getElementById('markMasteredBtn').textContent = '标记为已掌握 ✓';
document.getElementById('markMasteredBtn').disabled = false;
}
document.getElementById('prevBtn').disabled = currentIndex === 0;
Storage.markLearned(item.id);
Storage.saveLastVisited(item.id);
}
function renderQuiz(quiz) {
if (!quiz) {
document.getElementById('quizSection').style.display = 'none';
return;
}
document.getElementById('quizSection').style.display = 'block';
document.getElementById('quizQuestion').textContent = quiz.question;
const optionsHtml = quiz.options.map((option, index) => `
<div class="quiz-option" data-index="${index}">${option}</div>
`).join('');
document.getElementById('quizOptions').innerHTML = optionsHtml;
document.querySelectorAll('.quiz-option').forEach(option => {
option.addEventListener('click', () => {
if (answered) return;
document.querySelectorAll('.quiz-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
selectedOption = parseInt(option.dataset.index, 10);
checkAnswer();
});
});
}
function checkAnswer() {
const quiz = WordRoots[currentIndex].quiz;
if (selectedOption === null) return;
answered = true;
const feedbackEl = document.getElementById('quizFeedback');
if (selectedOption === quiz.correctAnswer) {
feedbackEl.textContent = '✅ 回答正确!';
feedbackEl.className = 'quiz-feedback correct';
document.querySelector(`.quiz-option[data-index="${selectedOption}"]`).classList.add('correct');
Storage.markMastered(WordRoots[currentIndex].id);
document.getElementById('markMasteredBtn').textContent = '✓ 已掌握';
document.getElementById('markMasteredBtn').disabled = true;
} else {
feedbackEl.textContent = '❌ 回答错误,正确答案是:' + quiz.options[quiz.correctAnswer];
feedbackEl.className = 'quiz-feedback incorrect';
document.querySelector(`.quiz-option[data-index="${selectedOption}"]`).classList.add('incorrect');
document.querySelector(`.quiz-option[data-index="${quiz.correctAnswer}"]`).classList.add('correct');
}
feedbackEl.style.display = 'block';
document.getElementById('nextBtn').style.display = 'inline-flex';
}
document.getElementById('prevBtn').addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--;
renderCard();
}
});
document.getElementById('nextBtn').addEventListener('click', () => {
if (currentIndex < WordRoots.length - 1) {
currentIndex++;
renderCard();
} else {
alert('🎉 恭喜你已经完成所有知识点的学习!');
window.location.href = './progress.html';
}
});
document.getElementById('markMasteredBtn').addEventListener('click', () => {
const item = WordRoots[currentIndex];
Storage.markMastered(item.id);
document.getElementById('markMasteredBtn').textContent = '✓ 已掌握';
document.getElementById('markMasteredBtn').disabled = true;
});
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
init();
</script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
{
"name": "现代CSS学习工坊",
"short_name": "CSS学习",
"description": "系统学习现代CSS核心特性与最佳实践",
"start_url": "./",
"display": "standalone",
"background_color": "#FAFAFA",
"theme_color": "#8B5CF6",
"icons": [
{
"src": "./icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+136
View File
@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学习进度 - 现代CSS学习工坊</title>
<meta name="description" content="查看你的现代CSS学习进度,掌握情况和学习统计">
<meta name="author" content="小秘书">
<meta name="language" content="zh-CN">
<meta name="robots" content="noindex, nofollow">
<link rel="canonical" href="https://modern-css-workshop.vercel.app/progress.html">
<meta property="og:title" content="学习进度 - 现代CSS学习工坊">
<meta property="og:description" content="查看你的现代CSS学习进度,掌握情况和学习统计">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modern-css-workshop.vercel.app/progress.html">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#8B5CF6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CSS学习工坊">
<link rel="apple-touch-icon" href="./icon-192.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/minimal.css">
</head>
<body>
<div class="container">
<nav class="nav">
<a href="./index.html" class="nav-logo">🎨 现代CSS学习工坊</a>
<ul class="nav-links">
<li><a href="./learn.html" class="nav-link">开始学习</a></li>
<li><a href="./flashcard.html" class="nav-link">闪卡复习</a></li>
<li><a href="./roots.html" class="nav-link">知识点索引</a></li>
<li><a href="./progress.html" class="nav-link" style="color: var(--primary);">学习进度</a></li>
</ul>
</nav>
<div class="text-center mb-8">
<h1 class="text-3xl font-bold mb-2 text-gray-900">学习进度</h1>
<p class="text-gray-600">跟踪你的现代CSS学习情况和掌握程度</p>
</div>
<div class="progress-stats">
<div class="progress-card">
<div class="progress-number" id="total-count">18</div>
<div class="progress-label">总知识点</div>
</div>
<div class="progress-card">
<div class="progress-number" id="learned-count">0</div>
<div class="progress-label">已学习</div>
</div>
<div class="progress-card">
<div class="progress-number" id="mastered-count">0</div>
<div class="progress-label">已掌握</div>
</div>
<div class="progress-card">
<div class="progress-number" id="progress-percent">0%</div>
<div class="progress-label">完成进度</div>
</div>
</div>
<div class="bg-gray-200 rounded-full h-4 mb-8 overflow-hidden">
<div id="progress-bar" class="bg-primary h-full rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
<div class="flex justify-center gap-4 mb-8">
<a href="./learn.html" class="btn btn-primary">继续学习 →</a>
<button id="reset-btn" class="btn btn-secondary">重置学习进度</button>
</div>
<div class="progress-list">
<h2 class="progress-list-title">✅ 已掌握的知识点</h2>
<div id="mastered-list" class="space-y-1">
<p class="text-gray-500 text-center py-6">还没有掌握任何知识点,加油学习哦~</p>
</div>
</div>
</div>
<script src="./js/wordData.js"></script>
<script src="./js/siteConfig.js"></script>
<script src="./js/storage.js"></script>
<script>
function init() {
updateStats();
renderMasteredList();
}
function updateStats() {
const stats = Storage.getStats();
document.getElementById('total-count').textContent = stats.total;
document.getElementById('learned-count').textContent = stats.learned;
document.getElementById('mastered-count').textContent = stats.mastered;
document.getElementById('progress-percent').textContent = `${stats.progress}%`;
document.getElementById('progress-bar').style.width = `${stats.progress}%`;
}
function renderMasteredList() {
const masteredIds = Storage.getMastered();
const listContainer = document.getElementById('mastered-list');
if (masteredIds.length === 0) {
listContainer.innerHTML = '<p class="text-gray-500 text-center py-6">还没有掌握任何知识点,加油学习哦~</p>';
return;
}
const masteredItems = WordRoots.filter(item => masteredIds.includes(item.id));
listContainer.innerHTML = masteredItems.map(item => `
<div class="flex items-center justify-between py-3 px-4 bg-green-50 rounded-lg">
<div>
<div class="font-medium text-gray-900">${item.root}</div>
<div class="text-sm text-gray-500">${item.origin}</div>
</div>
<a href="./root-detail.html?id=${item.id}" class="text-primary hover:underline text-sm">查看详情 →</a>
</div>
`).join('');
}
document.getElementById('reset-btn').addEventListener('click', () => {
if (confirm('确定要重置所有学习进度吗?此操作不可恢复!')) {
Storage.reset();
updateStats();
renderMasteredList();
alert('学习进度已重置');
}
});
init();
</script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://modern-css-workshop.vercel.app/sitemap.xml
+194
View File
@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>知识点详情 - 现代CSS学习工坊</title>
<meta name="description" content="现代CSS知识点详细内容,包含示例代码和测试题">
<meta name="author" content="小秘书">
<meta name="language" content="zh-CN">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://modern-css-workshop.vercel.app/root-detail.html">
<meta property="og:title" content="知识点详情 - 现代CSS学习工坊">
<meta property="og:description" content="现代CSS知识点详细内容,包含示例代码和测试题">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modern-css-workshop.vercel.app/root-detail.html">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#8B5CF6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CSS学习工坊">
<link rel="apple-touch-icon" href="./icon-192.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/minimal.css">
</head>
<body>
<div class="container">
<nav class="nav">
<a href="./index.html" class="nav-logo">🎨 现代CSS学习工坊</a>
<ul class="nav-links">
<li><a href="./learn.html" class="nav-link">开始学习</a></li>
<li><a href="./flashcard.html" class="nav-link">闪卡复习</a></li>
<li><a href="./roots.html" class="nav-link">知识点索引</a></li>
<li><a href="./progress.html" class="nav-link">学习进度</a></li>
</ul>
</nav>
<div class="learn-card card" id="learn-card">
<div class="flex items-center justify-between mb-4">
<a href="./roots.html" class="text-primary hover:underline flex items-center gap-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
返回列表
</a>
<span id="category-tag" class="text-xs font-medium px-2 py-1 rounded-full bg-blue-100 text-blue-800"></span>
</div>
<div class="learn-card-title" id="card-title">加载中...</div>
<div class="learn-card-meta" id="card-meta"></div>
<div class="learn-card-description" id="card-description"></div>
<div class="learn-card-examples" id="card-examples"></div>
<div class="quiz" id="quiz-section" style="display: none;">
<div class="quiz-question" id="quiz-question"></div>
<div class="quiz-options" id="quiz-options"></div>
<div id="quiz-feedback" class="quiz-feedback" style="display: none;"></div>
</div>
<div class="flex justify-between items-center mt-8">
<button id="mark-mastered-btn" class="btn btn-primary">标记为已掌握 ✓</button>
<a href="./learn.html" class="btn btn-secondary">去连续学习 →</a>
</div>
</div>
</div>
<script src="./js/wordData.js"></script>
<script src="./js/siteConfig.js"></script>
<script src="./js/storage.js"></script>
<script>
let selectedOption = null;
let answered = false;
let currentItem = null;
function getQueryParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
function init() {
const id = parseInt(getQueryParam('id'), 10);
if (!id) {
alert('参数错误,返回知识点列表');
window.location.href = './roots.html';
return;
}
currentItem = WordRoots.find(item => item.id === id);
if (!currentItem) {
alert('知识点不存在,返回知识点列表');
window.location.href = './roots.html';
return;
}
renderItem();
}
function renderItem() {
document.title = `${currentItem.root} - 现代CSS学习工坊`;
document.getElementById('category-tag').textContent = currentItem.origin;
document.getElementById('card-title').textContent = currentItem.root;
document.getElementById('card-meta').textContent = currentItem.meaning;
document.getElementById('card-description').textContent = currentItem.description;
const examplesHtml = currentItem.examples.map(example => `
<div class="learn-card-example">
<div class="example-title">${example.word}${example.meaning}</div>
<div class="example-code">${escapeHtml(example.explanation)}</div>
</div>
`).join('');
document.getElementById('card-examples').innerHTML = examplesHtml;
if (currentItem.quiz) {
renderQuiz(currentItem.quiz);
} else {
document.getElementById('quiz-section').style.display = 'none';
}
if (Storage.isMastered(currentItem.id)) {
document.getElementById('mark-mastered-btn').textContent = '✓ 已掌握';
document.getElementById('mark-mastered-btn').disabled = true;
}
Storage.markLearned(currentItem.id);
Storage.saveLastVisited(currentItem.id);
}
function renderQuiz(quiz) {
document.getElementById('quiz-section').style.display = 'block';
document.getElementById('quiz-question').textContent = quiz.question;
const optionsHtml = quiz.options.map((option, index) => `
<div class="quiz-option" data-index="${index}">${option}</div>
`).join('');
document.getElementById('quiz-options').innerHTML = optionsHtml;
document.querySelectorAll('.quiz-option').forEach(option => {
option.addEventListener('click', () => {
if (answered) return;
document.querySelectorAll('.quiz-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
selectedOption = parseInt(option.dataset.index, 10);
checkAnswer();
});
});
}
function checkAnswer() {
const quiz = currentItem.quiz;
if (selectedOption === null) return;
answered = true;
const feedbackEl = document.getElementById('quiz-feedback');
if (selectedOption === quiz.correctAnswer) {
feedbackEl.textContent = '✅ 回答正确!';
feedbackEl.className = 'quiz-feedback correct';
document.querySelector(`.quiz-option[data-index="${selectedOption}"]`).classList.add('correct');
Storage.markMastered(currentItem.id);
document.getElementById('mark-mastered-btn').textContent = '✓ 已掌握';
document.getElementById('mark-mastered-btn').disabled = true;
} else {
feedbackEl.textContent = '❌ 回答错误,正确答案是:' + quiz.options[quiz.correctAnswer];
feedbackEl.className = 'quiz-feedback incorrect';
document.querySelector(`.quiz-option[data-index="${selectedOption}"]`).classList.add('incorrect');
document.querySelector(`.quiz-option[data-index="${quiz.correctAnswer}"]`).classList.add('correct');
}
feedbackEl.style.display = 'block';
}
document.getElementById('mark-mastered-btn').addEventListener('click', () => {
Storage.markMastered(currentItem.id);
document.getElementById('mark-mastered-btn').textContent = '✓ 已掌握';
document.getElementById('mark-mastered-btn').disabled = true;
});
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
init();
</script>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>知识点索引 - 现代CSS学习工坊</title>
<meta name="description" content="现代CSS所有知识点索引,支持搜索和分类筛选,快速查找你需要的内容">
<meta name="author" content="小秘书">
<meta name="language" content="zh-CN">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://modern-css-workshop.vercel.app/roots.html">
<meta property="og:title" content="知识点索引 - 现代CSS学习工坊">
<meta property="og:description" content="现代CSS所有知识点索引,支持搜索和分类筛选">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modern-css-workshop.vercel.app/roots.html">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#8B5CF6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CSS学习工坊">
<link rel="apple-touch-icon" href="./icon-192.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/minimal.css">
</head>
<body>
<div class="container">
<nav class="nav">
<a href="./index.html" class="nav-logo">🎨 现代CSS学习工坊</a>
<ul class="nav-links">
<li><a href="./learn.html" class="nav-link">开始学习</a></li>
<li><a href="./flashcard.html" class="nav-link">闪卡复习</a></li>
<li><a href="./roots.html" class="nav-link" style="color: var(--primary);">知识点索引</a></li>
<li><a href="./progress.html" class="nav-link">学习进度</a></li>
</ul>
</nav>
<div class="text-center mb-8">
<h1 class="text-3xl font-bold mb-2 text-gray-900">知识点索引</h1>
<p class="text-gray-600"><span id="total-count">18</span> 个知识点,搜索关键词快速查找</p>
</div>
<div class="search-box">
<input type="text" id="search-input" placeholder="搜索知识点名称、描述关键词..." class="search-input">
</div>
<div class="flex flex-wrap gap-2 justify-center mb-8">
<button class="filter-btn btn btn-sm bg-primary/10 text-primary px-3 py-1 rounded-full" data-category="all">全部</button>
<button class="filter-btn btn btn-sm bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1 rounded-full" data-category="CSS3核心特性">CSS3核心特性</button>
<button class="filter-btn btn btn-sm bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1 rounded-full" data-category="布局系统">布局系统</button>
<button class="filter-btn btn btn-sm bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1 rounded-full" data-category="CSS动画">CSS动画</button>
<button class="filter-btn btn btn-sm bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1 rounded-full" data-category="响应式设计">响应式设计</button>
<button class="filter-btn btn btn-sm bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1 rounded-full" data-category="CSS变量与函数">CSS变量与函数</button>
<button class="filter-btn btn btn-sm bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1 rounded-full" data-category="CSS新特性">CSS新特性</button>
</div>
<div class="roots-grid" id="roots-grid"></div>
<div id="no-result" class="text-center py-12 text-gray-500 hidden">
<svg class="h-16 w-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h1m4 0h1m-7 4h.01M12 3h.01M3 21h18a2 2 0 002-2V5a2 2 0 00-2-2H3a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<p>没有找到匹配的知识点,请尝试其他关键词</p>
</div>
</div>
<script src="./js/wordData.js"></script>
<script src="./js/siteConfig.js"></script>
<script src="./js/storage.js"></script>
<script>
let currentCategory = 'all';
let searchKeyword = '';
function init() {
document.getElementById('total-count').textContent = WordRoots.length;
renderCards();
}
function renderCards() {
const filteredItems = WordRoots.filter(item => {
if (currentCategory !== 'all' && item.origin !== currentCategory) return false;
if (searchKeyword) {
const keyword = searchKeyword.toLowerCase();
return (
item.root.toLowerCase().includes(keyword) ||
item.meaning.toLowerCase().includes(keyword) ||
item.description.toLowerCase().includes(keyword) ||
item.origin.toLowerCase().includes(keyword)
);
}
return true;
});
const grid = document.getElementById('roots-grid');
const noResult = document.getElementById('no-result');
if (filteredItems.length === 0) {
grid.innerHTML = '';
noResult.classList.remove('hidden');
return;
}
noResult.classList.add('hidden');
grid.innerHTML = filteredItems.map(item => {
const isMastered = Storage.isMastered(item.id);
const isNew = item.origin.includes('新特性');
return `
<a href="./root-detail.html?id=${item.id}" class="root-card group">
<div class="flex justify-between items-start mb-2">
<span class="text-xs font-medium px-2 py-1 rounded-full ${
isNew ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'
}">${item.origin}</span>
${isMastered ? '<span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">✅ 已掌握</span>' : ''}
</div>
<h3 class="text-xl font-bold text-gray-900 group-hover:text-primary transition-colors mb-2">${item.root}</h3>
<p class="text-gray-600 text-sm line-clamp-2">${item.meaning}</p>
</a>
`;
}).join('');
}
document.getElementById('search-input').addEventListener('input', (e) => {
searchKeyword = e.target.value.trim();
renderCards();
});
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
currentCategory = btn.dataset.category;
document.querySelectorAll('.filter-btn').forEach(b => {
b.classList.remove('bg-primary/10', 'text-primary');
b.classList.add('bg-gray-100', 'text-gray-700', 'hover:bg-gray-200');
});
btn.classList.remove('bg-gray-100', 'text-gray-700', 'hover:bg-gray-200');
btn.classList.add('bg-primary/10', 'text-primary');
renderCards();
});
});
init();
</script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://modern-css-workshop.vercel.app/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://modern-css-workshop.vercel.app/learn.html</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://modern-css-workshop.vercel.app/flashcard.html</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://modern-css-workshop.vercel.app/roots.html</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://modern-css-workshop.vercel.app/progress.html</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
+35
View File
@@ -0,0 +1,35 @@
// Service Worker - 提供基本的离线缓存
const CACHE_NAME = 'modern-css-v1';
const ASSETS = [
'./',
'./index.html',
'./learn.html',
'./flashcard.html',
'./roots.html',
'./root-detail.html',
'./progress.html',
'./css/minimal.css',
'./js/wordData.js',
'./js/siteConfig.js',
'./js/storage.js'
];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
);
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((res) => res || fetch(e.request))
);
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
});
+8
View File
@@ -0,0 +1,8 @@
{
"buildCommand": "echo 'Build completed'",
"outputDirectory": ".",
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/$1" }
]
}