feat: 首页课程预览改为卡片书架(WIP),新增设计参考图库

- 新增 CourseShelf 组件:title/desc/done/relations 卡片,hover 轮播预期成果 GIF
- static/gifs 占位动图 6 张(同名替换即成片)
- design-refs/pinterest 设计稿参考 13 张,含出处与版权说明
- .gitignore 忽略 .zcodeignore 工具本地配置
This commit is contained in:
cqh963852 committed 2026-10-02 07:30:09 +08:00
1 parent 54990b3dfc
commit 3637129fd5
27 files changed
+523 -25

No files matched your search

@@ -0,0 +1,75 @@
import React, { useState } from "react";
import clsx from "clsx";
import Link from "@docusaurus/Link";
import useBaseUrl from "@docusaurus/useBaseUrl";
import type { CourseSection } from "./courses";
import styles from "./styles.module.css";
/**
* 单张课程卡:静态展示 title/desc/done/relations,
* hover(或键盘 focus)时挂载 GIF——"学完能得到什么"的动态预告。
* GIF 只在首次 hover 时加载,避免首屏拉全部动图。
*/
export default function CourseCard({ course }: { course: CourseSection }) {
const [previewing, setPreviewing] = useState(false);
const gifUrl = useBaseUrl(`gifs/${course.id}.gif`);
return (
<Link
to={course.href}
className={clsx("card", styles.card)}
style={{ ["--course-color" as string]: course.color }}
onMouseEnter={() => setPreviewing(true)}
onMouseLeave={() => setPreviewing(false)}
onFocus={() => setPreviewing(true)}
onBlur={() => setPreviewing(false)}
>
<div className={styles.spine} aria-hidden="true" />
<div className={styles.media}>
{previewing ? (
<img
className={styles.gif}
src={gifUrl}
alt={`${course.title} 学完后的预期成果演示`}
loading="eager"
/>
) : (
<div className={styles.mediaHint}>
<span className={styles.mediaHintIcon}>{"\u25B6"}</span>
悬停预览预期成果
</div>
)}
</div>
<div className={styles.body}>
<div className={styles.titleRow}>
<h3 className={styles.title}>{course.title}</h3>
<span
className={clsx(
styles.doneFlag,
course.done ? styles.done : styles.wip,
)}
title={course.done ? "已完成" : "写作中"}
>
{course.done ? "✓ 已完成" : "写作中"}
</span>
</div>
<p className={styles.description}>{course.description}</p>
<div className={styles.relations}>
{course.relations.length > 0 ? (
course.relations.map((r) => (
<span key={r} className={styles.relationChip}>
{r}
</span>
))
) : (
<span className={styles.relationEmpty}>关联章节整理中</span>
)}
</div>
</div>
</Link>
);
}
@@ -0,0 +1,81 @@
/**
* 全栈工程师教程 · 课程数据源
*
* 卡片书架的唯一数据入口:改卡片内容、完成度、关系,只动这个文件。
* GIF 换片:替换 static/gifs/<id>.gif 即可,无需改代码。
*/
export type CourseSection = {
/** 章节短 id,同时是占位 GIF 的文件名(static/gifs/<id>.gif) */
id: string;
/** 卡片标题(对应 docs 里 _category_.json 的 label) */
title: string;
/** 一句话预期结果:学完这一部分,读者能做出什么 */
description: string;
/** 写作完成度 */
done: boolean;
/** 与其他部分的关系(前置/依赖等)。TODO: 关系列出后填充,元素为对方 id */
relations: string[];
/** 章节首页路由(generated-index) */
href: string;
/** 卡片主题色(书脊/角标),来自设计稿 00 的多彩课程卡 */
color: string;
};
// "垃圾桶"(docs/web/archive)是旧文归档,不进书架。
export const courseSections: CourseSection[] = [
{
id: "hello-world",
title: "快速开始",
description: "写出第一个网页并在浏览器打开,跑起你的第一个 HTTP 服务器。",
done: true,
relations: [], // TODO: 列出与其他部分的关系
href: "/docs/category/快速开始",
color: "#6366f1",
},
{
id: "hello-react",
title: "快速开始 React",
description: "用脚手架创建 React 应用,亲手拼出第一个 Todo List。",
done: true,
relations: [], // TODO
href: "/docs/category/快速开始-react",
color: "#ec4899",
},
{
id: "more-example",
title: "更多示例",
description: "浮动布局等补充示例,巩固前面攒下的积木。",
done: true,
relations: [], // TODO
href: "/docs/category/更多示例",
color: "#f59e0b",
},
{
id: "more",
title: "更多",
description: "把示例工程构建为可发布产物,看清 build 到底做了什么。",
done: true,
relations: [], // TODO
href: "/docs/category/更多",
color: "#10b981",
},
{
id: "full-stack",
title: "全栈基础",
description: "从网页到服务与数据库,串起完整的最小全栈闭环。",
done: false,
relations: [], // TODO
href: "/docs", // TODO: 全栈基础暂无页面,落笔后改为其分类路由
color: "#0ea5e9",
},
{
id: "advance",
title: "高级教程",
description: "数据流、纯函数、CAP——工程上的心智模型。",
done: true,
relations: [], // TODO
href: "/docs/category/高级教程",
color: "#8b5cf6",
},
];
@@ -0,0 +1,27 @@
import React from "react";
import Heading from "@theme/Heading";
import { courseSections } from "./courses";
import CourseCard from "./CourseCard";
import styles from "./styles.module.css";
/**
* 课程预览书架:全栈工程师教程的卡片总览(取代目录树式预览)。
* 数据见 courses.ts;卡片交互见 CourseCard.tsx。
*/
export default function CourseShelf() {
return (
<section className={styles.shelf}>
<Heading as="h2" className={styles.heading}>
全栈工程师教程 · 课程书架
</Heading>
<p className={styles.subheading}>
共 {courseSections.length} 个部分。悬停卡片,预览学完这一部分能得到什么。
</p>
<div className={styles.grid}>
{courseSections.map((course) => (
<CourseCard key={course.id} course={course} />
))}
</div>
</section>
);
}
@@ -0,0 +1,179 @@
/* 课程书架:卡片网格,风格取自设计稿 00(E-learning Learning Plan Dashboard)
多彩课程卡 + 顶部 accent 条 + 完成角标。 */
.shelf {
padding: 2rem 0 3rem;
}
.heading {
text-align: center;
}
.subheading {
text-align: center;
color: var(--ifm-color-emphasis-700);
margin-bottom: 2rem;
}
.grid {
display: grid;
gap: 1.25rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
/* ---- 单张卡片 ---- */
.card {
--course-color: #6366f1;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--ifm-color-emphasis-300);
background-color: var(--ifm-card-background-color, var(--ifm-background-color));
box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
transition:
transform 0.15s ease-out,
box-shadow 0.15s ease-out,
border-color 0.15s ease-out;
}
.card:hover,
.card:focus-visible {
transform: translateY(-3px);
border-color: var(--course-color);
box-shadow: 0 6px 20px rgb(0 0 0 / 18%);
text-decoration: none;
}
/* 书脊:卡片左侧的主题色竖条,hover 常亮 */
.spine {
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: var(--course-color);
opacity: 0.45;
transition: opacity 0.15s ease-out;
}
.card:hover .spine,
.card:focus-visible .spine {
opacity: 1;
}
/* ---- 媒体区:hover 轮播预期成果 GIF ---- */
.media {
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
background:
linear-gradient(135deg, rgb(15 23 42 / 92%), rgb(30 41 59 / 92%));
overflow: hidden;
}
.gif {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.mediaHint {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--ifm-color-emphasis-600);
font-size: 0.85rem;
}
.mediaHintIcon {
color: var(--course-color);
}
/* ---- 内容区 ---- */
.body {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.1rem 1.1rem 1.25rem;
flex: 1;
}
.titleRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.title {
margin: 0;
font-size: 1.05rem;
}
.card:hover .title {
color: var(--course-color);
}
.doneFlag {
flex-shrink: 0;
font-size: 0.72rem;
padding: 0.15rem 0.55rem;
border-radius: 999px;
white-space: nowrap;
}
.done {
color: #047857;
background: #d1fae5;
}
.wip {
color: #92400e;
background: #fef3c7;
}
[data-theme="dark"] .done {
color: #6ee7b7;
background: rgb(4 120 87 / 25%);
}
[data-theme="dark"] .wip {
color: #fcd34d;
background: rgb(146 64 14 / 25%);
}
.description {
margin: 0;
flex: 1;
color: var(--ifm-color-emphasis-800);
font-size: 0.9rem;
line-height: 1.5;
}
/* ---- 关系区:后续列出章节依赖后渲染 chips ---- */
.relations {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-height: 1.4rem;
align-items: center;
}
.relationChip {
font-size: 0.72rem;
padding: 0.1rem 0.5rem;
border-radius: 999px;
color: var(--course-color);
border: 1px solid var(--course-color);
}
.relationEmpty {
font-size: 0.72rem;
color: var(--ifm-color-emphasis-500);
}
+21 -25
View File
@@ -1,38 +1,31 @@
import React, { Fragment } from "react";
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import CourseShelf from "@site/src/components/CourseShelf";
import styles from "./index.module.css";
const HomePageHeader = () => {
const { siteConfig } = useDocusaurusContext();
return (
<Fragment>
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs">
快速开始 - 5min ⏱️
</Link>
</div>
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs">
快速开始 - 5min ⏱️
</Link>
<Link
className="button button--secondary button--lg"
to="https://github.com/idealjs/stack/discussions"
>
参与讨论
</Link>
</div>
</header>
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="https://github.com/idealjs/stack/discussions"
>
参与讨论
</Link>
</div>
</div>
</header>
</Fragment>
</div>
</header>
);
};
@@ -41,6 +34,9 @@ const Home = () => {
return (
<Layout title={`Hello from ${siteConfig.title}`} description="全栈教程">
<HomePageHeader />
<main>
<CourseShelf />
</main>
</Layout>
);
};