feat: 首页课程预览改为卡片书架(WIP),新增设计参考图库
- 新增 CourseShelf 组件:title/desc/done/relations 卡片,hover 轮播预期成果 GIF - static/gifs 占位动图 6 张(同名替换即成片) - design-refs/pinterest 设计稿参考 13 张,含出处与版权说明 - .gitignore 忽略 .zcodeignore 工具本地配置
This commit is contained in:
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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Reference in new issue
Block a user