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)

实现逻辑很简单:

  1. BaseLayout 负责搭“壳子”,把左侧栏挂进全站布局。
  2. SideBar 负责写具体链接:头像、首页、技术实践。
  3. global.css 负责折叠/展开、文字显隐、宽度变化。
  4. index.astro 负责告诉布局当前是首页。

逐个解释如下。

  1. [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 ... />}:真正把左侧栏挂进去。
  1. [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:这里换成了烧瓶图标,更贴合“实践/实验”。
  1. [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:右侧标题字体样式。
  1. [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")。
  • PracticeEntrypractice 集合单条内容类型。

28-29:分页与默认分类

  • PAGE_SIZE = 9:每页 9 张(3x3)。
  • 默认分类是 quant

31-38:读取并清洗内容

  • getCollection("practice") 取全部实践内容。
  • 过滤掉 draft
  • 排序规则:先按 order 升序;同 order 再按 updatedDate 降序。

40:总项目数

  • totalPracticeCount 用于标题旁动态展示总数。

42-47:按分类分组

  • CATEGORY_META 的 key 遍历。
  • visibleEntriesentry.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 实现方案

实现方案梳理

  1. 页面结构(详情页)
  • 使用统一布局文件,顶部固定栏保持一致:左侧“返回总页(回到点击进入前那一页)”,右侧 logo
  • 详情主体只渲染 Markdown 内容,不额外插入介绍图或冗余说明。
  • 实现位置:
    practice 详情页
  1. 返回“总页且保持原页码”
  • 在项目总页点击卡片时,记录当前列表页地址(含分类、页码、搜索)到 sessionStorage
  • 详情页点击返回时优先 history.back();若不可用则回退到已保存的列表地址。
  • 这样可确保回到“进入详情前所在页”,不是第一页。
  • 实现位置:
    practice 总页
    practice 详情页
  1. 右下目录按钮(TOC)
  • 右下角悬浮按钮控制目录面板显示/隐藏。
  • 目录仅提取并展示 MD 的 h1-h3
  • 目录面板固定层级最高、背景不透明、可滚动(支持长目录)。
  • 点击目录项后可收起;点击按钮或空白区域也可关闭。
  • 实现位置:
    practice 详情页
  1. 内容组织(后续上传项目)
  • 推荐目录结构(按分类 + 时间戳文件夹):
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/ 存该项目所有配图,避免跨项目污染
  1. 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 逐行解析

  1. ---:Astro 前置脚本开始。
  2. 导入 BaseLayout
  3. 导入 getCollection
  4. 导入 CollectionEntry 类型。
  5. 空行。
  6. 定义 CATEGORY_META 常量对象。
    7-10. quant 分类:显示名和描述。
    11-14. ai 分类:显示名和描述。
    15-18. web3 分类:显示名和描述。
    19-22. others 分类:显示名和描述。
  7. as const 锁定字面量类型。
  8. 空行。
  9. 定义 PracticeCategory 类型(分类 key 联合类型)。
  10. 定义 PracticeEntry 类型(content 集合条目类型)。
  11. 空行。
  12. PAGE_SIZE = 9,每页最多 9 卡(3x3)。
  13. initialCategory = "quant" 默认分类。
  14. 空行。
  15. 读取 practice 集合所有条目。
    32-38. 过滤草稿并排序。
  16. 过滤 draft
  17. 先按 order 升序。
  18. order 不同直接返回。
  19. order 相同按 updatedDate 降序。
  20. 空行。
  21. 总项目数。
  22. 空行。
    42-47. 按分类分组。
    43-46. 对每个分类 key 过滤条目。
  23. 强制成 Record<PracticeCategory, PracticeEntry[]>
  24. 空行。
    49-64. 生成可序列化对象给前端脚本。
  25. meta
  26. 填分类计数。
    55-61. 映射卡片字段。
  27. slug 用于详情链接。
  28. 标题。
  29. 描述。
  30. 封面图,缺省 /social_img.webp
  31. 图片 alt。
  32. 前置脚本结束。
  33. 空行。
  34. BaseLayout 开始,标题 + 侧边栏高亮 practice
    68-73. 顶栏。
  35. 顶栏容器布局。
  36. 左侧留空(列表页无返回)。
  37. 右侧 logo 文案 Guoxuan
  38. 空行。
    75-124. 页面主体。
    78-94. 顶部:标题区 + 分类 tab。
  39. 主标题。
  40. 动态总项目数文案。
  41. tab 容器。
    84-92. 遍历分类生成按钮。
  42. 首个 tab 默认 tab-active
  43. data-tech-tab 供脚本识别。
    96-110. 描述 + 搜索栏。
  44. 分类描述占位,脚本注入。
    98-109. 搜索框壳和输入框。
  45. data-category-search 供脚本绑定。
  46. 卡片网格容器 data-tech-grid
    114-121. 分页区。
  47. 上一页按钮。
  48. 页码按钮容器。
  49. “第 x / y 页”文字容器。
  50. 下一页按钮。
  51. BaseLayout 结束。
  52. 空行。
  53. 内联脚本开始,注入后端变量。
  54. 前端可用分组数据。
  55. practice:return-state(旧状态键,仍保留)。
  56. practice:return-url(当前用于返回定位的关键键)。
  57. 空行。
  58. searchState:每个分类独立搜索词。
  59. 空行。
    134-137. 全局状态:当前分类 + 当前页。
  60. 空行。
  61. initPage() 初始化入口。
    140-147. 取 DOM 引用(tab、描述、网格、分页、搜索)。
    149-158. readStateFromUrl():从 URL 读 category/page/q
  62. 分类存在才赋值。
  63. 页码合法才赋值。
  64. 将查询词写回当前分类搜索状态。
    160-180. syncUrl():把状态回写 URL + sessionStorage。
  65. URL 写 category
    165-166. page>1 才保留 page 参数。
    168-169. 搜索词非空才保留 q
  66. history.replaceState 不刷新改 URL。
    172-179. 写 RETURN_STATE_KEY
  67. getItems():取当前分类全部项。
    184-191. getFilteredItems():按 title+description 模糊匹配。
  68. getTotalPages():至少 1 页。
    195-200. renderTabs():切换 tab-active。
    202-206. renderDescription():更新分类描述。
    208-212. renderSearch():恢复当前分类搜索框值和 placeholder。
    214-230. renderIndicators(totalPages):动态页码按钮。
  69. 当前页用 btn-primary
    223-227. 点击页码后切页并平滑回顶部。
    232-293. renderCards():核心渲染逻辑。
  70. 取过滤后项目。
    234-236. 校正页码边界。
    238-239. 按页切片。
    240-251. 构建详情页 query。
    243-245. 写 category/page/q
    246-248. 构造 backHref 需要的参数。
  71. 生成 /practice?.../practice
  72. back 放进详情 query。
    253-269. 渲染网格 HTML。
  73. 卡片容器 + data-tech-card
  74. 详情链接 + data-practice-link
    258-260. 封面图。
    261-264. 标题与描述。
  75. 无结果时显示空状态。
    271-280. 卡片 hover/click 高亮切换。
    281-285. 点击卡片前写 sessionStorage[practice:return-url]=backHref
    287-289. 更新页码文字 + 上下页 disabled。
  76. 重绘页码按钮。
  77. 同步 URL。
    295-304. setCategory():切分类时页码重置到 1 并重渲染。
    306-311. 给 tab 绑定点击。
    313-323. 给搜索框绑定输入(120ms 防抖)并回第一页。
    325-332. 上一页按钮逻辑。
    334-342. 下一页按钮逻辑。
    344-348. 初始化:先读 URL,再渲染。
  78. 首次执行 initPage()
  79. Astro 页面切换后再次执行。
  80. 脚本结束。
    355-387. 样式。
    356-360. 卡片基础动效。
    362-372. 搜索框样式。
    374-380. 网格强制 3 列
    382-386. 激活卡片样式(边框高亮+阴影)。

2) src/pages/practice/[...slug].astro 逐行解析

  1. --- 前置脚本开始。
  2. 导入内容集合能力与类型。
  3. 导入 PracticeSchema 类型。
  4. 导入 BaseLayout
  5. 空行。
    6-12. getStaticPaths() 生成详情静态路由。
  6. 取所有 practice 内容。
    8-11. 每篇产出 { params.slug, props.entry }
  7. 空行。
    14-16. Props 类型定义。
  8. 空行。
  9. Astro.props 取当前条目。
  10. item = entry.data
  11. entry.render() 得到 MD 渲染组件和标题列表。
  12. toc 只保留 depth <= 3
  13. back 参数(优先返回目标)。
    23-25. 兼容读取 category/page/q
    26-35. 计算 backHref
  14. back 直接用。
    28-35. 否则用 category/page/q 组装 /practice?...,再退化到 /practice
  15. 前置脚本结束。
  16. 空行。
  17. BaseLayout,标题/描述/封面用于 meta,侧边栏高亮 practice
    39-49. 顶栏。
  18. 左上返回链接 href={backHref}
    42-44. 返回箭头图标。
  19. 返回文字。
  20. 右侧 logo。
    51-61. 主体只渲染 md。
  21. prose 排版容器。
  22. h1 标题。
    54-57. badge 与更新时间。
  23. 分割线。
  24. <Content />:Markdown 主体。
    63-97. 目录区域(仅 toc 非空时渲染)。
    65-78. 右下浮动按钮。
  25. 按钮固定右下。
  26. z-index:9999 保证层级。
  27. data-toc-toggle 供脚本绑定。
    80-95. 目录面板。
  28. 初始 hidden
    85-94. 遍历 toc 标题生成链接。
  29. 根据标题深度加类名(用于缩进)。
  30. 锚点跳转 #slug
  31. BaseLayout 结束。
    100-175. 客户端行为脚本。
  32. setupPracticeDetailPage()
    102-104. 取 TOC 按钮/面板/目录项。
  33. 取返回链接 DOM。
  34. RETURN_URL_KEY
    108-137. 返回逻辑。
  35. 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. 点击目录外区域关闭目录。
  36. 首次执行初始化。
  37. Astro 页面切换后再次执行。
  38. 脚本结束。
    177-246. TOC 样式。
    178-192. 面板定位、尺寸、滚动、不透明背景、阴影。
  39. z-index:9998,配合按钮 9999。
    185-186. 纵向滚动开启。
  40. 不透明背景色(你要求的关键点)。
    194-200. 标题行样式。
    202-205. 列表栅格。
    207-218. 自定义滚动条。
    220-230. h1/h2/h3 缩进层级。
    232-245. 目录项交互样式。

3.3.4 后续更新新的项目的做法

  1. 新建项目目录
    在对应分类下创建:
    src/content/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/

  2. 放内容文件
    在该目录下放:
    index.md
    figure/(该项目图片全放这里)

  3. 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
---
  1. 写正文
    直接用 Markdown 标题/段落/列表即可;详情页会自动渲染正文并自动提取 h1-h3 目录。

  2. 图片引用规则
    正文中统一用绝对路径:
    /practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/figure/xxx.webp
    这样路由层级变化也不会丢图。

  3. 让图片可被前端访问
    把同名图片同步到 public/practice/<category>/<YYYY-MM-DD-HHmmss>-<slug>/figure/
    (你当前这套是按这个方式稳定显示的)

  4. 自动进入总页
    保存后,/practice 会自动收录到对应分类;分页、搜索、卡片跳转和返回定位无需额外改代码。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐