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:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
*.log
|
||||
*.zip
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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 = 实际宽度244px;border-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左200px,content占满中间,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
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://modern-css-workshop.vercel.app/sitemap.xml
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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)))
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"buildCommand": "echo 'Build completed'",
|
||||
"outputDirectory": ".",
|
||||
"routes": [
|
||||
{ "handle": "filesystem" },
|
||||
{ "src": "/(.*)", "dest": "/$1" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user