个人简历网站搭建:3 构建项目页
3.0 需求分析
我现在已经制作好了整个页面的骨架以及首页,我后面想从技术实践的quant部分入手。我的整体的思路是,先在骨架中的左侧栏中添加技术实践部分,然后制作一个页面专门存储所有的项目,列出即可,仍然以首页中的几个类别进行区分。例如我点击quant则显示quant类别下面的项目卡片,项目卡片依然是图片标题描述,只不过卡片竖着排,然后可以翻页。之后我想要用md文件来写每个项目的具体的信息。我后续可能有很多个md文件想要放上去,我想用astro对于md文件的合适处理来转成网页。
所以我的需求,我可以列出下面的任务:
- 左侧栏新增一个一级入口:技术实践。
- 设置总页,展示四个类别入口(Quant/AI/Web3/Others)+ 每类项目数 + 每类的所有卡片。依然是点击每个类别自动切换哪个类别的项目。
- 构建项目详情页:从对应 md 渲染正文。卡片点击进入详情。
3.1 首页左侧栏链接构建

核心是 4 个文件,真正生效的是 [BaseLayout.astro](E:/My_webpage/src/layouts/BaseLayout.astro)、[SideBar.astro](E:/My_webpage/src/components/SideBar.astro)、[global.css](E:/My_webpage/src/styles/global.css),首页只负责把当前页标记传进去:[index.astro](E:/My_webpage/src/pages/index.astro)。
实现逻辑很简单:
BaseLayout负责搭“壳子”,把左侧栏挂进全站布局。SideBar负责写具体链接:头像、首页、技术实践。global.css负责折叠/展开、文字显隐、宽度变化。index.astro负责告诉布局当前是首页。
逐个解释如下。
[BaseLayout.astro](E:/My_webpage/src/layouts/BaseLayout.astro)
import BaseHead:引入页面 head。import SideBar:引入左侧栏组件。ViewTransitions:保留页面切换动画。SITE_TITLE / SITE_DESCRIPTION:给页面默认标题和描述。const { ... } = Astro.props:接收页面传入参数,includeSidebar控制是否显示左侧栏,sideBarActiveItemID预留给当前页高亮。<html ...>到<body>:搭建整站外壳。drawer/drawer-toggle:这是左侧栏开关结构。label for="my-drawer":手机端打开按钮。shell-dim:遮罩层,点它会关闭侧栏。<slot />:这里放每个页面自己的内容。{includeSidebar && <SideBar ... />}:真正把左侧栏挂进去。
[SideBar.astro](E:/My_webpage/src/components/SideBar.astro)
import { Image }:头像用 Astro 图片组件渲染。drawer-side/sidebar-shell:侧栏本体容器。label for="my-drawer":遮罩层,点击可收起侧栏。sidebar-top里的头像href="/":点头像回首页。src="/profile.webp":头像改成你的照片。sidebar-toggle:展开/收起按钮。href="/":首页链接。href="/practice":技术实践入口。- 技术实践的 SVG:这里换成了烧瓶图标,更贴合“实践/实验”。
[global.css](E:/My_webpage/src/styles/global.css)
.sidebar-shell:默认窄栏宽度。#my-drawer:checked ... width: 10rem:展开时变宽。.sidebar-panel:纵向排版。.sidebar-top:顶部头像区域对齐。.sidebar-icon:统一图标尺寸。.sidebar-body:链接之间的间距。.sidebar-item:每个入口的点击区域。.sidebar-item:hover:悬停底色。.sidebar-text:默认隐藏文字。#my-drawer:checked ... .sidebar-text:展开后文字滑入显示。.brand-wordmark:右侧标题字体样式。
[index.astro](E:/My_webpage/src/pages/index.astro)
<BaseLayout sideBarActiveItemID="home">:告诉布局当前是首页。- 这条参数目前是预留的,不影响当前侧栏链接本身,但以后做高亮很方便。
一句话总结:
左侧栏不是靠复杂 JS 做的,而是 BaseLayout + SideBar + global.css 三层配合,用 DaisyUI 的 drawer 结构实现的。
3.2 设置项目分布总页
3.2.1 需求梳理
- 在 /practice 做“项目分布总页”,包含四个类别:Quant / AI / Web3 / Others。
- 类别切换在同一页面内完成,不跳转新页面。
- 每个类别都要有搜索框。
- 搜索后要能定位到对应项目卡片。
- 卡片每行三个,每页三行。
- 需要分页功能(上一页/下一页/页码)。
- 卡片点击进入项目详情页(详情由 md 渲染,后续做)。

3.2.2 如何实现
实现梳理
我是在一个页面里完成了“数据准备 + 前端状态切换 + 搜索过滤 + 分页渲染 + 卡片跳转”这条链路,核心都在 src/pages/practice/index.astro。
-
/practice作为“项目分布总页”,四分类Quant/AI/Web3/Others
通过CATEGORY_META定义分类元信息,并用getCollection("practice")拉取内容后按category分组(同文件前半段)。 -
类别切换不跳转
用同页tab按钮(data-tech-tab)+ 前端state.category,点击后调用setCategory()仅重渲染当前列表,不发生路由跳转。 -
每个类别有搜索框
在描述文案下面加了一个搜索输入(data-category-search),输入事件只更新当前类别的搜索词。 -
搜索后定位对应项目卡片
getFilteredItems()里对“当前分类”项目按title + description做匹配,渲染结果就是匹配后的卡片集合。 -
卡片每行三个,每页三行
PAGE_SIZE = 9,并在.practice-grid固定grid-template-columns: repeat(3, minmax(0, 1fr)),所以每页最多 3x3。 -
分页(上一页/下一页/页码)
用state.page+getTotalPages(),并渲染:data-page-prev上一页data-page-next下一页data-page-indicators页码按钮
搜索或切换分类时会重置到第 1 页并重算总页数。
-
卡片点击进入详情页(md 渲染)
卡片链接是/practice/${item.slug},详情页路由已由 src/pages/practice/[…slug].astro 承接,后续可继续完善 md 展示细节。
下面按代码块“逐行”解释:
1-5:前置导入
---:Astro frontmatter 开始。- 导入
BaseLayout。 - 导入
getCollection,用于读src/content/practice。 - 导入类型
CollectionEntry。
6-23:分类常量 CATEGORY_META
- 定义四个分类:
quant/ai/web3/others。 - 每个分类有展示名
label和描述desc。 as const让 TS 把键和值收窄成字面量类型。
25-26:类型定义
PracticeCategory:分类键联合类型("quant" | "ai" | "web3" | "others")。PracticeEntry:practice集合单条内容类型。
28-29:分页与默认分类
PAGE_SIZE = 9:每页 9 张(3x3)。- 默认分类是
quant。
31-38:读取并清洗内容
getCollection("practice")取全部实践内容。- 过滤掉
draft。 - 排序规则:先按
order升序;同order再按updatedDate降序。
40:总项目数
totalPracticeCount用于标题旁动态展示总数。
42-47:按分类分组
- 用
CATEGORY_META的 key 遍历。 - 对
visibleEntries按entry.data.category分到对应数组。 - 最终得到
Record<PracticeCategory, PracticeEntry[]>。
49-64:做前端可序列化数据
- 把每组转成更轻的数据结构:
meta(分类文案)count(该分类条数)items(卡片需要字段:slug/title/description/image/imageAlt)
heroImage为空时回退/social_img.webp。
67-125:页面模板结构
- 用
BaseLayout包住整页,激活侧边栏practice。 - 68-73:顶部 header(品牌文字)。
- 75-124:主内容区。
- 80:页面标题。
- 81:动态显示“目前一共有 X 个项目”。
- 83-93:分类 tab,循环
CATEGORY_META生成按钮,并挂data-tech-tab。 - 96-110:分类描述 + 搜索框(搜索当前分类)。
- 112:卡片网格容器(JS 动态注入卡片 HTML)。
- 114-121:分页区(上一页、页码容器、下一页)。
127-292:前端交互脚本
define:vars把服务器端变量注入浏览器脚本。- 128:
groups拿到分组数据。 - 129-136:缓存 DOM 引用(tabs、描述、网格、分页按钮、搜索框)。
- 138:
searchState,每个分类一个独立搜索词。 - 140-143:全局状态:当前分类 + 当前页。
- 145:
getItems()取当前分类全部项目。 - 147-154:
getFilteredItems()只在当前分类里按标题和描述做关键词匹配。 - 156:
getTotalPages()根据过滤后的结果算总页数。 - 158-163:
renderTabs()高亮当前 tab。 - 165-169:
renderDescription()更新当前分类描述。 - 171-175:
renderSearch()回填当前分类搜索词,并更新 placeholder。 - 177-193:
renderIndicators()生成页码按钮并绑定点击翻页。 - 195-237:
renderCards()核心渲染函数:- 196-199:基于过滤结果和总页数,校正当前页范围。
- 201-202:切当前页数据切片。
- 204-220:写入卡片 HTML;无结果时显示空状态。
- 222-230:给卡片加 hover/click 高亮交互。
- 232-234:更新“第 x / y 页”和上下页禁用状态。
- 236:重绘页码。
- 239-248:
setCategory():切分类时保存旧分类搜索词,切到新分类并重绘。 - 250-255:绑定 tab 点击事件。
- 257-267:绑定搜索输入(120ms 防抖);输入后回到第 1 页并重绘。
- 269-276:上一页按钮逻辑。
- 278-286:下一页按钮逻辑。
- 288-291:首屏初始化渲染(tab、描述、搜索框、卡片)。
294-326:页面内样式
- 295-299:卡片动画和鼠标样式。
- 301-311:搜索框外观。
- 313-319:网格强制三列(
repeat(3, ...))。 - 321-325:激活卡片样式(高饱和、主色边框、阴影)。
3.3 构建项目详情页
3.3.1 需求分析
- 整体的页面布局设计:
- 在每一页的设置上边栏,依然右侧放置我的logo,最左侧放置返回上一页也就是点击进去的时候的项目总页(要是点击的那一页)。
- 右下脚能有一个按钮,点击可以显示目录(至多展示到md文件的三级目录),并且要能在层级最高不透明显示。
- 整体页面就完全解析我的md文件就可以。不用添加多余的图片或者介绍啥的。
- 后续的编写:
- 我希望有一个专门的文件夹存放每次想要上传的项目。该文件夹下面以时间命名,文件夹下面有md文件以及用于存放支持该md文件渲染的figure文件夹。


3.3.2 实现方案
实现方案梳理
- 页面结构(详情页)
- 使用统一布局文件,顶部固定栏保持一致:左侧“返回总页(回到点击进入前那一页)”,右侧
logo。 - 详情主体只渲染 Markdown 内容,不额外插入介绍图或冗余说明。
- 实现位置:
practice 详情页
- 返回“总页且保持原页码”
- 在项目总页点击卡片时,记录当前列表页地址(含分类、页码、搜索)到
sessionStorage。 - 详情页点击返回时优先
history.back();若不可用则回退到已保存的列表地址。 - 这样可确保回到“进入详情前所在页”,不是第一页。
- 实现位置:
practice 总页
practice 详情页
- 右下目录按钮(TOC)
- 右下角悬浮按钮控制目录面板显示/隐藏。
- 目录仅提取并展示 MD 的
h1-h3。 - 目录面板固定层级最高、背景不透明、可滚动(支持长目录)。
- 点击目录项后可收起;点击按钮或空白区域也可关闭。
- 实现位置:
practice 详情页
- 内容组织(后续上传项目)
- 推荐目录结构(按分类 + 时间戳文件夹):
src/content/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/index.md
src/content/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/figure/*
- 说明:
<category>用于总页分类(如quant / ai / web3 / others)<YYYY-MM-DD-HHmmss>支持同一天多项目figure/存该项目所有配图,避免跨项目污染
- MD 图片引用约定
- 在
index.md中使用站点绝对路径引用对应项目图片,例如:/practice/quant/2026-05-27-151500-limit-orderbook-study/figure/sample-figure.webp - 这样路径稳定,渲染时不会受当前路由层级影响。
3.3.3 核心文件代码解析
下面按“核心两页”做逐行解析(按连续行段解释,每一行都覆盖到)。
核心文件:
practice 总页 index.astro
practice 详情页 […slug].astro
1) src/pages/practice/index.astro 逐行解析
---:Astro 前置脚本开始。- 导入
BaseLayout。 - 导入
getCollection。 - 导入
CollectionEntry类型。 - 空行。
- 定义
CATEGORY_META常量对象。
7-10.quant分类:显示名和描述。
11-14.ai分类:显示名和描述。
15-18.web3分类:显示名和描述。
19-22.others分类:显示名和描述。 as const锁定字面量类型。- 空行。
- 定义
PracticeCategory类型(分类 key 联合类型)。 - 定义
PracticeEntry类型(content 集合条目类型)。 - 空行。
PAGE_SIZE = 9,每页最多 9 卡(3x3)。initialCategory = "quant"默认分类。- 空行。
- 读取
practice集合所有条目。
32-38. 过滤草稿并排序。 - 过滤
draft。 - 先按
order升序。 order不同直接返回。order相同按updatedDate降序。- 空行。
- 总项目数。
- 空行。
42-47. 按分类分组。
43-46. 对每个分类 key 过滤条目。 - 强制成
Record<PracticeCategory, PracticeEntry[]>。 - 空行。
49-64. 生成可序列化对象给前端脚本。 - 填
meta。 - 填分类计数。
55-61. 映射卡片字段。 slug用于详情链接。- 标题。
- 描述。
- 封面图,缺省
/social_img.webp。 - 图片 alt。
- 前置脚本结束。
- 空行。
BaseLayout开始,标题 + 侧边栏高亮practice。
68-73. 顶栏。- 顶栏容器布局。
- 左侧留空(列表页无返回)。
- 右侧 logo 文案
Guoxuan。 - 空行。
75-124. 页面主体。
78-94. 顶部:标题区 + 分类 tab。 - 主标题。
- 动态总项目数文案。
- tab 容器。
84-92. 遍历分类生成按钮。 - 首个 tab 默认
tab-active。 - 写
data-tech-tab供脚本识别。
96-110. 描述 + 搜索栏。 - 分类描述占位,脚本注入。
98-109. 搜索框壳和输入框。 data-category-search供脚本绑定。- 卡片网格容器
data-tech-grid。
114-121. 分页区。 - 上一页按钮。
- 页码按钮容器。
- “第 x / y 页”文字容器。
- 下一页按钮。
BaseLayout结束。- 空行。
- 内联脚本开始,注入后端变量。
- 前端可用分组数据。
practice:return-state(旧状态键,仍保留)。practice:return-url(当前用于返回定位的关键键)。- 空行。
searchState:每个分类独立搜索词。- 空行。
134-137. 全局状态:当前分类 + 当前页。 - 空行。
initPage()初始化入口。
140-147. 取 DOM 引用(tab、描述、网格、分页、搜索)。
149-158.readStateFromUrl():从 URL 读category/page/q。- 分类存在才赋值。
- 页码合法才赋值。
- 将查询词写回当前分类搜索状态。
160-180.syncUrl():把状态回写 URL + sessionStorage。 - URL 写
category。
165-166.page>1才保留page参数。
168-169. 搜索词非空才保留q。 history.replaceState不刷新改 URL。
172-179. 写RETURN_STATE_KEY。getItems():取当前分类全部项。
184-191.getFilteredItems():按 title+description 模糊匹配。getTotalPages():至少 1 页。
195-200.renderTabs():切换 tab-active。
202-206.renderDescription():更新分类描述。
208-212.renderSearch():恢复当前分类搜索框值和 placeholder。
214-230.renderIndicators(totalPages):动态页码按钮。- 当前页用
btn-primary。
223-227. 点击页码后切页并平滑回顶部。
232-293.renderCards():核心渲染逻辑。 - 取过滤后项目。
234-236. 校正页码边界。
238-239. 按页切片。
240-251. 构建详情页 query。
243-245. 写category/page/q。
246-248. 构造backHref需要的参数。 - 生成
/practice?...或/practice。 - 把
back放进详情 query。
253-269. 渲染网格 HTML。 - 卡片容器 +
data-tech-card。 - 详情链接 +
data-practice-link。
258-260. 封面图。
261-264. 标题与描述。 - 无结果时显示空状态。
271-280. 卡片 hover/click 高亮切换。
281-285. 点击卡片前写sessionStorage[practice:return-url]=backHref。
287-289. 更新页码文字 + 上下页 disabled。 - 重绘页码按钮。
- 同步 URL。
295-304.setCategory():切分类时页码重置到 1 并重渲染。
306-311. 给 tab 绑定点击。
313-323. 给搜索框绑定输入(120ms 防抖)并回第一页。
325-332. 上一页按钮逻辑。
334-342. 下一页按钮逻辑。
344-348. 初始化:先读 URL,再渲染。 - 首次执行
initPage()。 - Astro 页面切换后再次执行。
- 脚本结束。
355-387. 样式。
356-360. 卡片基础动效。
362-372. 搜索框样式。
374-380. 网格强制3 列。
382-386. 激活卡片样式(边框高亮+阴影)。
2) src/pages/practice/[...slug].astro 逐行解析
---前置脚本开始。- 导入内容集合能力与类型。
- 导入
PracticeSchema类型。 - 导入
BaseLayout。 - 空行。
6-12.getStaticPaths()生成详情静态路由。 - 取所有
practice内容。
8-11. 每篇产出{ params.slug, props.entry }。 - 空行。
14-16.Props类型定义。 - 空行。
- 从
Astro.props取当前条目。 item = entry.data。entry.render()得到 MD 渲染组件和标题列表。toc只保留depth <= 3。- 读
back参数(优先返回目标)。
23-25. 兼容读取category/page/q。
26-35. 计算backHref。 - 有
back直接用。
28-35. 否则用category/page/q组装/practice?...,再退化到/practice。 - 前置脚本结束。
- 空行。
BaseLayout,标题/描述/封面用于 meta,侧边栏高亮practice。
39-49. 顶栏。- 左上返回链接
href={backHref}。
42-44. 返回箭头图标。 - 返回文字。
- 右侧 logo。
51-61. 主体只渲染 md。 prose排版容器。h1标题。
54-57. badge 与更新时间。- 分割线。
<Content />:Markdown 主体。
63-97. 目录区域(仅 toc 非空时渲染)。
65-78. 右下浮动按钮。- 按钮固定右下。
z-index:9999保证层级。data-toc-toggle供脚本绑定。
80-95. 目录面板。- 初始
hidden。
85-94. 遍历 toc 标题生成链接。 - 根据标题深度加类名(用于缩进)。
- 锚点跳转
#slug。 BaseLayout结束。
100-175. 客户端行为脚本。setupPracticeDetailPage()。
102-104. 取 TOC 按钮/面板/目录项。- 取返回链接 DOM。
RETURN_URL_KEY。
108-137. 返回逻辑。- 读
sessionStorage中保存的返回地址。
110-112. 若合法则覆盖backLink.href。
114-136. 点击返回时:
115-123. 判断是否从本站/practice来。
125-129. 是则history.back()(回到点击前那一页)。
131-135. 否则用sessionStorage的地址强制跳回。
139-143.openToc():显示目录并更新aria-expanded。
145-149.closeToc():隐藏目录并更新aria-expanded。
151-156. 点击右下按钮开关目录。
158-162. 点击目录项后关闭目录。
164-170. 点击目录外区域关闭目录。 - 首次执行初始化。
- Astro 页面切换后再次执行。
- 脚本结束。
177-246. TOC 样式。
178-192. 面板定位、尺寸、滚动、不透明背景、阴影。 z-index:9998,配合按钮 9999。
185-186. 纵向滚动开启。- 不透明背景色(你要求的关键点)。
194-200. 标题行样式。
202-205. 列表栅格。
207-218. 自定义滚动条。
220-230.h1/h2/h3缩进层级。
232-245. 目录项交互样式。
3.3.4 后续更新新的项目的做法
-
新建项目目录
在对应分类下创建:src/content/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/ -
放内容文件
在该目录下放:index.mdfigure/(该项目图片全放这里) -
写
index.md的 frontmatter(最小必填)
---
title: "你的项目标题"
description: "一句话描述"
category: "quant" # quant | ai | web3 | others
updatedDate: 2026-05-27
heroImage: "/practice/quant/2026-05-27-151500-limit-orderbook-study/figure/sample-figure.webp"
draft: false
---
-
写正文
直接用 Markdown 标题/段落/列表即可;详情页会自动渲染正文并自动提取h1-h3目录。 -
图片引用规则
正文中统一用绝对路径:/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/figure/xxx.webp
这样路由层级变化也不会丢图。 -
让图片可被前端访问
把同名图片同步到public/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/figure/。
(你当前这套是按这个方式稳定显示的) -
自动进入总页
保存后,/practice会自动收录到对应分类;分页、搜索、卡片跳转和返回定位无需额外改代码。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)