修改网站markdown解析格式
This commit is contained in:
129
README.md
129
README.md
@@ -1,16 +1,127 @@
|
||||
# React + Vite
|
||||
# Markdown To Web
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
此仓库将一组本地 Markdown 笔记(位于 `public/mengyanote`)转换成一个静态的 React 网站,便于浏览、检索和发布到网络上。
|
||||
|
||||
Currently, two official plugins are available:
|
||||
本 README 为中文说明,包含项目简介、运行/构建步骤、目录结构、如何编辑笔记以及部署和贡献指南,方便直接发布到 GitHub。
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
## 主要特性
|
||||
|
||||
## React Compiler
|
||||
- 从本地目录(`public/mengyanote`)递归读取 Markdown 文件并生成静态数据(`src/data`)。
|
||||
- 使用 `react-markdown` 渲染 Markdown,支持数学公式(remark/rehype 插件)与代码高亮。
|
||||
- 目录树侧边栏、内容渲染器等基础浏览功能。
|
||||
- 通过简单脚本将 Markdown 文件转为项目需要的 JSON 数据,便于在静态站点中直接使用。
|
||||
|
||||
The React Compiler is not enabled on this template. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
## 技术栈
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
- React 19
|
||||
- Vite
|
||||
- react-markdown + remark/rehype 插件(remark-gfm、remark-math、rehype-katex、rehype-highlight 等)
|
||||
|
||||
## 快速开始(Windows / cmd.exe)
|
||||
|
||||
1. 安装依赖
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
2. 本地开发(会先生成静态数据)
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
这会先执行 `node scripts/generateData.js` 从 `public/mengyanote` 读取 Markdown 并生成 `src/data` 下的 `directoryTree.json` / `fileContents.json` / `stats.json`,然后启动 Vite 开发服务器。
|
||||
|
||||
3. 生成生产构建
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. 预览构建产物
|
||||
|
||||
```
|
||||
npm run preview
|
||||
```
|
||||
|
||||
5. 单独生成数据(不启动服务器)
|
||||
|
||||
```
|
||||
npm run generate-data
|
||||
```
|
||||
|
||||
6. 代码检查
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 项目结构(重要文件/目录)
|
||||
|
||||
- `public/mengyanote/` - 源 Markdown 笔记目录(请把你的 .md 文件放在这里以纳入站点)。
|
||||
- `scripts/generateData.js` - 将 Markdown 文件读取并生成 `src/data` 的脚本。
|
||||
- `src/data/` - 由脚本生成的 JSON 数据(目录树、文件内容、统计信息)。
|
||||
- `src/components/MarkdownRenderer.jsx` - Markdown 渲染组件(样式与功能定制点)。
|
||||
- `src/components/MarkdownRenderer.css` - Markdown 渲染相关样式(可在此处简化或替换为你喜欢的样式)。
|
||||
- `src/components/Sidebar.jsx` - 侧边栏目录树组件。
|
||||
- `src/context/AppContext.jsx` - 全局上下文与路由状态。
|
||||
- `scripts/` - 工具脚本(目前包含 `generateData.js`)。
|
||||
|
||||
示例:如果你想让 Markdown 渲染更简洁(“换成我图片那种”样式),可以编辑 `src/components/MarkdownRenderer.css` 或直接修改 `MarkdownRenderer.jsx` 中的渲染类名/元素结构。
|
||||
|
||||
## 如何编辑/添加笔记
|
||||
|
||||
1. 在 `public/mengyanote` 下增加或修改 `.md` 文件,保持目录组织即可。
|
||||
2. 运行 `npm run generate-data`(或 `npm run dev`)以重新生成 `src/data`。
|
||||
3. 刷新浏览器查看最新内容。
|
||||
|
||||
注意:脚本会忽略以 `.` 开头的文件/目录(例如 `.obsidian`)和一些预设项(`node_modules`、`.git` 等)。
|
||||
|
||||
## 部署建议
|
||||
|
||||
- 静态站点:`npm run build` 会在 `dist/` 生成静态文件,适合部署到 GitHub Pages、Netlify、Vercel、或任意静态文件托管服务。
|
||||
- GitHub Pages:构建后将 `dist/` 的内容发布到 gh-pages 分支或通过 GitHub Actions 自动发布。
|
||||
|
||||
示例(手工):
|
||||
|
||||
1. 构建
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. 将 `dist/` 内容上传到你的静态托管服务或 gh-pages 分支。
|
||||
|
||||
如果需要,我可以为你添加一个自动部署到 GitHub Pages 的 GitHub Actions 工作流。
|
||||
|
||||
## 定制渲染样式
|
||||
|
||||
如果你觉得当前 Markdown 美化太重并想改回更简洁的“图片优先”或原始样式:
|
||||
|
||||
- 编辑 `src/components/MarkdownRenderer.css`:移除或覆盖过度的样式(字体、背景、代码块高亮等)。
|
||||
- 编辑 `src/components/MarkdownRenderer.jsx`:调整渲染器的 className、元素包装或忽略某些 remark/rehype 插件。
|
||||
|
||||
常见修改点:
|
||||
- 移除 KaTeX 或代码高亮:在 `src/components/MarkdownRenderer.jsx` 中删除相应的 rehype 插件 import 与使用。
|
||||
- 简化图片样式:在 CSS 中将 img 的 max-width、margin 等属性调整为你想要的样式。
|
||||
|
||||
## 贡献与许可
|
||||
|
||||
欢迎提交 Issue 或 Pull Request。仓库包含 `LICENSE`(请查看该文件以确认许可证类型)。
|
||||
|
||||
为了保持项目干净:
|
||||
- 新功能请先创建 issue 讨论。
|
||||
- 提交 PR 时附带简短说明和相关截图(如果是 UI 变更)。
|
||||
|
||||
## 联系方式
|
||||
|
||||
如需帮助或定制:在 Issue 中描述你想要的样式和示例图片/链接,我可以帮你修改 `MarkdownRenderer.css` / `MarkdownRenderer.jsx` 实现视觉风格。
|
||||
|
||||
---
|
||||
|
||||
祝发布顺利!如果你想,我可以:
|
||||
|
||||
- 帮你把 README 翻译成英文版本并放在 `README.en.md`;
|
||||
- 或直接替你修改 Markdown 渲染样式(把渲染改成更像你“图片那种”的风格)。
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
|
||||
8
public/mengyanote/.obsidian/workspace.json
vendored
8
public/mengyanote/.obsidian/workspace.json
vendored
@@ -13,12 +13,12 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "树萌芽的小本本/目前已部署网站.md",
|
||||
"file": "编程语言/Android/Linux配置安卓Gradle构建环境.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "目前已部署网站"
|
||||
"title": "Linux配置安卓Gradle构建环境"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -186,8 +186,9 @@
|
||||
},
|
||||
"active": "8304b0e105b08ed0",
|
||||
"lastOpenFiles": [
|
||||
"树萌芽的小本本/网站小技巧.md",
|
||||
"编程语言/Android/安卓Gradle构建常用命令总结.md",
|
||||
"树萌芽的小本本/目前已部署网站.md",
|
||||
"树萌芽的小本本/网站小技巧.md",
|
||||
"Docker/优秀好用的Docker镜像/FileCodeBox-文件快递柜.md",
|
||||
"Docker/优秀好用的Docker镜像/Postgres数据库.md",
|
||||
"Docker/优秀好用的Docker镜像/未命名.md",
|
||||
@@ -207,7 +208,6 @@
|
||||
"Docker/Docker镜像快速迁移.md",
|
||||
"无线-HCIA 02.md",
|
||||
"Linux相关/把Ubuntu镜像源切换到阿里云.md",
|
||||
"编程语言/Android/安卓Gradle构建常用命令总结.md",
|
||||
"临时解决方案/修改hosts方式来直连Github.md",
|
||||
"临时解决方案/萌芽云剪切板.md",
|
||||
"2025年9月紧急规划.md",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Android Gradle 常用命令速查(基于 `./gradlew`)
|
||||
|
||||
|
||||
**基础**
|
||||
##### **基础**
|
||||
|
||||
- `./gradlew tasks`
|
||||
列出可用的 Gradle 任务(查看当前项目能跑什么任务)。
|
||||
@@ -13,7 +12,7 @@
|
||||
清理构建产物(删除 `build/` 目录)。
|
||||
|
||||
|
||||
**构建 APK / AAB**
|
||||
##### **构建 APK / AAB**
|
||||
|
||||
- `./gradlew assembleDebug`
|
||||
构建 debug APK(输出:`app/build/outputs/apk/debug/*.apk`)。
|
||||
@@ -28,7 +27,7 @@
|
||||
生成 debug bundle(少用,通常用于测试)。
|
||||
|
||||
|
||||
**按 module / productFlavor / buildType 构建**
|
||||
##### **按 module / productFlavor / buildType 构建**
|
||||
|
||||
- `./gradlew :moduleName:assembleRelease`
|
||||
构建指定 module(多模块项目时用)。
|
||||
@@ -37,7 +36,7 @@
|
||||
构建指定 flavor + buildType(例如 `assemblePaidRelease`)。
|
||||
|
||||
|
||||
**安装与卸载**
|
||||
##### **安装与卸载**
|
||||
|
||||
- `./gradlew installDebug`
|
||||
将 debug APK 安装到连接的设备/模拟器(需要 adb 可用)。
|
||||
@@ -48,14 +47,14 @@
|
||||
- 如果用生成的 APK 手动安装:`adb install -r app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
|
||||
**测试**
|
||||
##### **测试**
|
||||
|
||||
- 单元测试(JVM):`./gradlew test` 或 `./gradlew testDebugUnitTest`
|
||||
|
||||
- 仪器/设备测试(connected devices):`./gradlew connectedAndroidTest` 或 `./gradlew connectedCheck`
|
||||
|
||||
|
||||
**静态检查 / 报表**
|
||||
##### **静态检查 / 报表**
|
||||
|
||||
- `./gradlew lint` 或 `./gradlew lintDebug`
|
||||
运行 Android Lint。
|
||||
@@ -64,7 +63,7 @@
|
||||
输出签名信息(SHA1/SHA256),常用于配置 API key(Google/Firebase)。
|
||||
|
||||
|
||||
**调试构建问题的常用参数**
|
||||
##### **调试构建问题的常用参数**
|
||||
|
||||
- `--stacktrace` / `--full-stacktrace`:打印堆栈跟踪(排错用)
|
||||
|
||||
@@ -83,7 +82,7 @@
|
||||
- `--refresh-dependencies`:刷新依赖缓存
|
||||
|
||||
|
||||
**性能 / CI 常用组合示例**
|
||||
##### **性能 / CI 常用组合示例**
|
||||
|
||||
- 本地快速一把:`./gradlew clean assembleDebug --parallel --info`
|
||||
|
||||
@@ -92,7 +91,7 @@
|
||||
- 只构建 moduleA 的 release:`./gradlew :moduleA:assembleRelease`
|
||||
|
||||
|
||||
**常见路径**
|
||||
##### **常见路径**
|
||||
|
||||
- APK:`app/build/outputs/apk/<buildType|flavor>/...`
|
||||
|
||||
@@ -101,13 +100,10 @@
|
||||
- 临时构建缓存:`~/.gradle/caches/`
|
||||
|
||||
|
||||
**小贴士**
|
||||
##### **小贴士**
|
||||
|
||||
- 始终用项目里的 Gradle Wrapper(`./gradlew`),保证 Gradle 版本一致。
|
||||
|
||||
- Release 构建需要正确的 `signingConfig`(通常放在 `gradle.properties` + `build.gradle`),也可以在 CI 用 `-P` 传参数(注意不要把敏感信息放在日志里)。
|
||||
|
||||
- 出问题先加 `--stacktrace --info` 看详情,再定位是依赖、ProGuard/R8、签名还是资源冲突。
|
||||
|
||||
|
||||
如果你想,我可以把你常用的几条命令做成一个可复用的脚本/CI job 模板(GitHub Actions/GitLab CI),或者按你项目的 flavor 给出精确的 assemble 命令。要哪个直接说。
|
||||
59
src/App.css
59
src/App.css
@@ -1,19 +1,17 @@
|
||||
:root {
|
||||
--color-bg: #eafbf2;
|
||||
--color-bg-secondary: #d6f3e3;
|
||||
--color-surface: rgba(255, 255, 255, 0.88);
|
||||
--color-surface-strong: #ffffff;
|
||||
--color-border: rgba(47, 177, 112, 0.2);
|
||||
--color-text: #1f3a2c;
|
||||
--color-muted: #4a6c5a;
|
||||
--color-link: #269c66;
|
||||
--color-link-hover: #1e7a51;
|
||||
--color-accent: #33c57a;
|
||||
--color-accent-contrast: #0f3322;
|
||||
--shadow-soft: 0 18px 38px rgba(36, 118, 74, 0.18);
|
||||
--radius-lg: 24px;
|
||||
--sidebar-width: clamp(240px, 21vw, 320px);
|
||||
--gradient-bg: linear-gradient(135deg, #f2fff8 0%, #c9f4df 40%, #a6e7d0 100%);
|
||||
--color-bg: #f5f7fb;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f1f4fa;
|
||||
--color-border: #d8deed;
|
||||
--color-text: #1f2a44;
|
||||
--color-muted: #6f7b92;
|
||||
--color-accent: #3a7afe;
|
||||
--color-accent-soft: rgba(58, 122, 254, 0.1);
|
||||
--color-danger: #d6455d;
|
||||
--sidebar-width: clamp(250px, 20vw, 320px);
|
||||
--shadow-soft: 0 20px 45px rgba(86, 105, 141, 0.15);
|
||||
--radius-lg: 22px;
|
||||
--font-family-base: 'Inter', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -25,32 +23,32 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--gradient-bg);
|
||||
background: linear-gradient(135deg, #f6f9ff 0%, #edf2fb 45%, #fbfdff 100%);
|
||||
color: var(--color-text);
|
||||
font-family: 'Inter', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
font-family: var(--font-family-base);
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
padding: clamp(0.75rem, 2vw, 2rem);
|
||||
padding: clamp(1rem, 4vw, 2.5rem);
|
||||
}
|
||||
|
||||
.app {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: calc(100vh - clamp(1.5rem, 4vw, 4rem));
|
||||
min-height: calc(100vh - clamp(2rem, 6vw, 5rem));
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body ::selection {
|
||||
background-color: rgba(51, 197, 122, 0.35);
|
||||
color: var(--color-accent-contrast);
|
||||
background-color: rgba(58, 122, 254, 0.18);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -58,38 +56,39 @@ body ::selection {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: rgba(216, 222, 237, 0.4);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(43, 156, 105, 0.25);
|
||||
border-radius: 9999px;
|
||||
background: rgba(111, 123, 146, 0.35);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(43, 156, 105, 0.4);
|
||||
background: rgba(111, 123, 146, 0.55);
|
||||
}
|
||||
|
||||
button:focus,
|
||||
a:focus {
|
||||
outline: 3px solid rgba(51, 197, 122, 0.45);
|
||||
outline: 2px solid rgba(58, 122, 254, 0.45);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
#root {
|
||||
padding: clamp(0.5rem, 3vw, 1.25rem);
|
||||
padding: clamp(0.75rem, 4vw, 1.75rem);
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: calc(100vh - clamp(1rem, 6vw, 3rem));
|
||||
min-height: calc(100vh - clamp(1.5rem, 7vw, 3.5rem));
|
||||
border-radius: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#root {
|
||||
padding: clamp(0.5rem, 4vw, 1rem);
|
||||
padding: clamp(0.5rem, 5vw, 1.2rem);
|
||||
}
|
||||
|
||||
.app {
|
||||
|
||||
@@ -2,172 +2,68 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(233, 250, 240, 0.92) 100%);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-area.with-sidebar {
|
||||
border-left: 1px solid rgba(47, 177, 112, 0.12);
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.content-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding: clamp(1.25rem, 3vw, 2rem) clamp(1.25rem, 4vw, 2.5rem) clamp(0.75rem, 2vw, 1.25rem);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(47, 177, 112, 0.12);
|
||||
z-index: 10;
|
||||
padding: 1.75rem clamp(2rem, 5vw, 3rem) 1.25rem;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: rgba(47, 177, 112, 0.35);
|
||||
color: rgba(31, 42, 68, 0.3);
|
||||
}
|
||||
|
||||
.breadcrumb-text {
|
||||
padding: 0.25rem 0.55rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(51, 197, 122, 0.12);
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-text:hover {
|
||||
background: rgba(51, 197, 122, 0.22);
|
||||
color: var(--color-text);
|
||||
background: rgba(58, 122, 254, 0.08);
|
||||
}
|
||||
|
||||
.content-title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 3vw, 2.4rem);
|
||||
font-weight: 700;
|
||||
font-size: clamp(2rem, 3.5vw, 2.6rem);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: clamp(1.5rem, 4vw, 3rem) clamp(1.25rem, 6vw, 3.5rem) clamp(2.5rem, 6vw, 4rem);
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: clamp(2rem, 4vw, 3.5rem);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.content-body.with-toc {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 260px);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: clamp(2rem, 6vw, 3.5rem);
|
||||
}
|
||||
|
||||
.markdown-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-toc {
|
||||
position: sticky;
|
||||
top: clamp(6rem, 12vw, 7rem);
|
||||
align-self: start;
|
||||
padding: 1.25rem 1.1rem;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
border: 1px solid rgba(47, 177, 112, 0.14);
|
||||
box-shadow: 0 16px 32px rgba(31, 58, 44, 0.12);
|
||||
max-height: calc(100vh - clamp(8rem, 14vw, 10rem));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-toc h3 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
display: block;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 10px;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toc-link.level-2 {
|
||||
padding-left: 1.15rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.toc-link.level-3 {
|
||||
padding-left: 1.85rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.toc-link:hover,
|
||||
.toc-link.active {
|
||||
background: rgba(51, 197, 122, 0.18);
|
||||
color: var(--color-text);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.65rem;
|
||||
padding: 4rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid rgba(51, 197, 122, 0.2);
|
||||
border-top: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
width: min(860px, 100%);
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
color: var(--color-text);
|
||||
line-height: 1.75;
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -182,31 +78,31 @@
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin: 2.2rem 0 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin: 2rem 0 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: clamp(2rem, 3vw, 2.6rem);
|
||||
border-bottom: 2px solid rgba(47, 177, 112, 0.14);
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: clamp(2.1rem, 3vw, 2.8rem);
|
||||
border-bottom: 2px solid rgba(31, 42, 68, 0.08);
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: clamp(1.6rem, 2.4vw, 2.1rem);
|
||||
border-bottom: 1px solid rgba(47, 177, 112, 0.16);
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: clamp(1.7rem, 2.5vw, 2.2rem);
|
||||
border-bottom: 1px solid rgba(31, 42, 68, 0.08);
|
||||
padding-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: clamp(1.35rem, 2vw, 1.6rem);
|
||||
font-size: clamp(1.4rem, 2vw, 1.7rem);
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.heading-anchor {
|
||||
@@ -214,16 +110,16 @@
|
||||
left: -1.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(47, 177, 112, 0.8);
|
||||
color: rgba(31, 42, 68, 0.35);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.markdown-content h1:hover .heading-anchor,
|
||||
.markdown-content h2:hover .heading-anchor,
|
||||
.markdown-content h3:hover .heading-anchor,
|
||||
.markdown-content h4:hover .heading-anchor,
|
||||
.markdown-content h1:hover .heading-anchor {
|
||||
.markdown-content h4:hover .heading-anchor {
|
||||
opacity: 1;
|
||||
transform: translate(-4px, -50%);
|
||||
}
|
||||
@@ -232,10 +128,9 @@
|
||||
.markdown-content ul,
|
||||
.markdown-content ol,
|
||||
.markdown-content blockquote,
|
||||
.markdown-content table,
|
||||
.markdown-content pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.35rem;
|
||||
.markdown-content pre,
|
||||
.markdown-content table {
|
||||
margin: 0 0 1.35rem;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
@@ -248,32 +143,27 @@
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: var(--color-link);
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
color: var(--color-link-hover);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.inline-code {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.35rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(33, 169, 102, 0.12);
|
||||
border: 1px solid rgba(33, 169, 102, 0.18);
|
||||
background: rgba(58, 122, 254, 0.08);
|
||||
border: 1px solid rgba(58, 122, 254, 0.16);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
position: relative;
|
||||
background: #0f2c1f;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 18px 36px rgba(12, 52, 34, 0.45);
|
||||
background: #f6f8ff;
|
||||
border: 1px solid rgba(58, 122, 254, 0.15);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -282,75 +172,99 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: #bcf8d9;
|
||||
text-transform: uppercase;
|
||||
background: rgba(58, 122, 254, 0.08);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.code-language {
|
||||
letter-spacing: 0.1em;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.code-copy-button {
|
||||
background: rgba(188, 248, 217, 0.14);
|
||||
color: #bcf8d9;
|
||||
border: 1px solid rgba(188, 248, 217, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid rgba(58, 122, 254, 0.25);
|
||||
background: #fff;
|
||||
color: var(--color-muted);
|
||||
border-radius: 8px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.code-copy-button:hover {
|
||||
background: rgba(188, 248, 217, 0.24);
|
||||
transform: translateY(-1px);
|
||||
color: var(--color-accent);
|
||||
border-color: rgba(58, 122, 254, 0.5);
|
||||
}
|
||||
|
||||
.code-copy-button.copied {
|
||||
background: rgba(188, 248, 217, 0.4);
|
||||
color: #0f2c1f;
|
||||
color: var(--color-accent);
|
||||
border-color: rgba(58, 122, 254, 0.7);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 1.25rem 1.5rem;
|
||||
padding: 1.1rem 1.35rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent !important;
|
||||
color: #e9fff2;
|
||||
font-size: 0.95rem;
|
||||
color: #1f2a44;
|
||||
}
|
||||
|
||||
.markdown-image {
|
||||
margin: 1.6rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-image img {
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(31, 42, 68, 0.1);
|
||||
box-shadow: 0 18px 38px rgba(31, 42, 68, 0.12);
|
||||
}
|
||||
|
||||
.markdown-image figcaption {
|
||||
margin-top: 0.55rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
margin: 1.6rem 0;
|
||||
padding: 1rem 1.25rem;
|
||||
border-left: 4px solid rgba(58, 122, 254, 0.45);
|
||||
background: rgba(58, 122, 254, 0.1);
|
||||
border-radius: 0 14px 14px 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px dashed rgba(31, 42, 68, 0.12);
|
||||
margin: 2.25rem 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(47, 177, 112, 0.14);
|
||||
box-shadow: 0 14px 28px rgba(31, 58, 44, 0.12);
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(31, 42, 68, 0.12);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.table-wrapper table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 520px;
|
||||
}
|
||||
|
||||
.table-wrapper th,
|
||||
.table-wrapper td {
|
||||
padding: 0.85rem 1rem;
|
||||
border-bottom: 1px solid rgba(47, 177, 112, 0.12);
|
||||
border-bottom: 1px solid rgba(31, 42, 68, 0.1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-wrapper th {
|
||||
background: rgba(51, 197, 122, 0.12);
|
||||
color: var(--color-text);
|
||||
color: var(--color-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -358,205 +272,117 @@ pre code {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-wrapper tr:hover td {
|
||||
background: rgba(51, 197, 122, 0.08);
|
||||
}
|
||||
|
||||
.custom-blockquote {
|
||||
margin: 1.75rem 0;
|
||||
padding: 1.1rem 1.4rem 1.1rem 1.2rem;
|
||||
border-left: 5px solid rgba(47, 177, 112, 0.6);
|
||||
border-radius: 0 16px 16px 0;
|
||||
background: rgba(51, 197, 122, 0.12);
|
||||
color: var(--color-text);
|
||||
box-shadow: 0 12px 24px rgba(31, 58, 44, 0.1);
|
||||
}
|
||||
|
||||
.custom-blockquote strong:first-child {
|
||||
display: block;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px dashed rgba(47, 177, 112, 0.35);
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 18px 36px rgba(31, 58, 44, 0.18);
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
.markdown-image {
|
||||
margin: 1.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-image figcaption {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.custom-blockquote.callout-info {
|
||||
border-left-color: rgba(70, 199, 150, 0.7);
|
||||
background: rgba(70, 199, 150, 0.18);
|
||||
}
|
||||
|
||||
.custom-blockquote.callout-warning {
|
||||
border-left-color: rgba(255, 187, 92, 0.85);
|
||||
background: rgba(255, 187, 92, 0.2);
|
||||
}
|
||||
|
||||
.custom-blockquote.callout-danger {
|
||||
border-left-color: rgba(255, 107, 107, 0.85);
|
||||
background: rgba(255, 107, 107, 0.18);
|
||||
}
|
||||
|
||||
.custom-blockquote.callout-success {
|
||||
border-left-color: rgba(62, 201, 133, 0.85);
|
||||
background: rgba(62, 201, 133, 0.2);
|
||||
}
|
||||
|
||||
.external-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.external-link-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 3rem 0;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(111, 123, 146, 0.25);
|
||||
border-top: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: clamp(2rem, 6vw, 4rem);
|
||||
min-height: calc(100vh - clamp(10rem, 16vw, 12rem));
|
||||
min-height: calc(100vh - 7rem);
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
text-align: center;
|
||||
max-width: 640px;
|
||||
padding: clamp(2rem, 6vw, 3rem);
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 28px 48px rgba(31, 58, 44, 0.16);
|
||||
max-width: 520px;
|
||||
padding: 2.4rem;
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: 0 24px 48px rgba(86, 105, 141, 0.18);
|
||||
}
|
||||
|
||||
.welcome-content h1 {
|
||||
font-size: clamp(2.4rem, 4vw, 3rem);
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.9rem;
|
||||
font-size: clamp(2.1rem, 3.2vw, 2.6rem);
|
||||
}
|
||||
|
||||
.welcome-content p {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 auto 1.6rem;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1.1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: rgba(51, 197, 122, 0.12);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(47, 177, 112, 0.14);
|
||||
color: var(--color-text);
|
||||
gap: 0.6rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.katex-display {
|
||||
padding: 1.25rem;
|
||||
border-radius: 14px;
|
||||
background: rgba(51, 197, 122, 0.1);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-content .task-list-item {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.markdown-content .task-list-item input[type='checkbox'] {
|
||||
margin-right: 0.6rem;
|
||||
transform: scale(1.1);
|
||||
accent-color: var(--color-accent);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.content-body.with-toc {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.content-toc {
|
||||
position: relative;
|
||||
top: auto;
|
||||
max-height: none;
|
||||
order: -1;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-header {
|
||||
padding: 1rem 1.25rem;
|
||||
padding: 1.2rem 1.5rem 0.9rem;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: clamp(1.6rem, 6vw, 2rem);
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
font-size: 0.98rem;
|
||||
.markdown-pane {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heading-anchor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.welcome-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-header {
|
||||
padding: 0.85rem 1rem;
|
||||
padding: 1rem 1.1rem 0.75rem;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
@@ -15,6 +15,7 @@ import 'highlight.js/styles/github.css';
|
||||
function Breadcrumbs({ filePath }) {
|
||||
const breadcrumbs = generateBreadcrumbs(filePath);
|
||||
if (breadcrumbs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="breadcrumbs" aria-label="当前位置">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
@@ -28,8 +29,7 @@ function Breadcrumbs({ filePath }) {
|
||||
}
|
||||
|
||||
function CodeBlock({ inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const language = match ? match[1] : '';
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
if (inline) {
|
||||
return (
|
||||
@@ -39,13 +39,34 @@ function CodeBlock({ inline, className, children, ...props }) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeText = React.Children.toArray(children).join('').replace(/\n$/, '');
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const language = match ? match[1] : 'text';
|
||||
const buttonClass = 'code-copy-button' + (copied ? ' copied' : '');
|
||||
const buttonLabel = copied ? '已复制' : '复制代码';
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeText);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2200);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy code block', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="code-block-wrapper">
|
||||
{language && (
|
||||
<div className="code-block-header">
|
||||
<span className="code-language">{language}</span>
|
||||
<button type="button" className={buttonClass} onClick={handleCopy} aria-live="polite">
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<pre className={className} {...props}>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
@@ -57,19 +78,21 @@ function CustomLink({ href, children, ...props }) {
|
||||
if (href && href.startsWith('[[') && href.endsWith(']]')) {
|
||||
const linkText = href.slice(2, -2);
|
||||
return (
|
||||
<span className="internal-link" title={内部链接: }>
|
||||
<span className="internal-link" title={`内部链接: ${linkText}`}>
|
||||
{children || linkText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const isExternal = href && /^(https?:)?\/\//.test(href);
|
||||
const linkClass = isExternal ? 'external-link' : 'internal-link';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={isExternal ? '_blank' : '_self'}
|
||||
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||
className={isExternal ? 'external-link' : 'internal-link'}
|
||||
className={linkClass}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -86,97 +109,8 @@ function CustomTable({ children, ...props }) {
|
||||
);
|
||||
}
|
||||
|
||||
function useHeadings(content) {
|
||||
return useMemo(() => {
|
||||
const headingRegex = /^(#{1,4})\s+(.+)$/gm;
|
||||
const headings = [];
|
||||
let match;
|
||||
|
||||
while ((match = headingRegex.exec(content)) !== null) {
|
||||
const [, hashes, text] = match;
|
||||
const level = hashes.length;
|
||||
if (level > 4) continue;
|
||||
|
||||
const plainText = text.replace(/[*_~]/g, '').trim();
|
||||
const id = plainText
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
|
||||
headings.push({ id, text: plainText, level });
|
||||
}
|
||||
|
||||
return headings;
|
||||
}, [content]);
|
||||
}
|
||||
|
||||
function TableOfContents({ headings }) {
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<aside className="content-toc" aria-label="目录">
|
||||
<h3>目录</h3>
|
||||
<ul className="toc-list">
|
||||
{headings.map((heading) => (
|
||||
<li key={heading.id} className="toc-item">
|
||||
<a
|
||||
className={ oc-link level-}
|
||||
href={#}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MarkdownRenderer() {
|
||||
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
|
||||
const [activeHeading, setActiveHeading] = useState(null);
|
||||
const headings = useHeadings(currentContent);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || headings.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||
|
||||
if (visible[0]) {
|
||||
setActiveHeading(visible[0].target.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '-40% 0px -50% 0px',
|
||||
threshold: [0, 1],
|
||||
}
|
||||
);
|
||||
|
||||
headings.forEach((heading) => {
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) {
|
||||
observer.observe(element);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
headings.forEach((heading) => {
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) {
|
||||
observer.unobserve(element);
|
||||
}
|
||||
});
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [headings]);
|
||||
|
||||
const renderHeading = useCallback((Tag) => ({ children, ...props }) => {
|
||||
function createHeadingRenderer(tag) {
|
||||
return function HeadingRenderer({ children, ...props }) {
|
||||
const text = React.Children.toArray(children)
|
||||
.map((child) => {
|
||||
if (typeof child === 'string') return child;
|
||||
@@ -190,111 +124,74 @@ export default function MarkdownRenderer() {
|
||||
|
||||
const id = text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
|
||||
.replace(/[^a-z0-9\u00c0-\u024f\u4e00-\u9fa5\s-]/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
|
||||
const HeadingTag = tag;
|
||||
|
||||
return (
|
||||
<Tag id={id} {...props}>
|
||||
<a href={#} aria-hidden className="heading-anchor">
|
||||
<HeadingTag id={id} {...props}>
|
||||
<a href={`#${id}`} aria-hidden className="heading-anchor">
|
||||
#
|
||||
</a>
|
||||
{children}
|
||||
</Tag>
|
||||
</HeadingTag>
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
}
|
||||
|
||||
const components = useMemo(() => ({
|
||||
const headingComponents = {
|
||||
h1: createHeadingRenderer('h1'),
|
||||
h2: createHeadingRenderer('h2'),
|
||||
h3: createHeadingRenderer('h3'),
|
||||
h4: createHeadingRenderer('h4'),
|
||||
};
|
||||
|
||||
export default function MarkdownRenderer() {
|
||||
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
|
||||
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
code: CodeBlock,
|
||||
a: CustomLink,
|
||||
table: CustomTable,
|
||||
h1: renderHeading('h1'),
|
||||
h2: renderHeading('h2'),
|
||||
h3: renderHeading('h3'),
|
||||
h4: renderHeading('h4'),
|
||||
blockquote: ({ children, ...props }) => {
|
||||
const childText = React.Children.toArray(children)
|
||||
.map((child) => {
|
||||
if (typeof child === 'string') return child.trim();
|
||||
if (
|
||||
React.isValidElement(child) &&
|
||||
typeof child.props.children === 'string'
|
||||
) {
|
||||
return child.props.children.trim();
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const calloutMatch = childText.match(/^\s*\[(info|warning|danger|success)\]\s*/i);
|
||||
const calloutType = calloutMatch ? calloutMatch[1].toLowerCase() : null;
|
||||
|
||||
return (
|
||||
<blockquote
|
||||
className={custom-blockquote}
|
||||
{...props}
|
||||
>
|
||||
...headingComponents,
|
||||
blockquote: ({ children, ...props }) => (
|
||||
<blockquote className="custom-blockquote" {...props}>
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
},
|
||||
),
|
||||
img: ({ alt, ...props }) => (
|
||||
<figure className="markdown-image">
|
||||
<img alt={alt} {...props} />
|
||||
{alt && <figcaption>{alt}</figcaption>}
|
||||
</figure>
|
||||
),
|
||||
ol: ({ ordered, ...props }) => <ol className="ordered-list" {...props} />,
|
||||
ul: ({ ordered, ...props }) => <ul className="unordered-list" {...props} />,
|
||||
li: ({ checked, children, ...props }) => (
|
||||
<li
|
||||
className={list-item}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
}), [renderHeading]);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeHeading) return undefined;
|
||||
|
||||
const tocLinks = document.querySelectorAll('.toc-link');
|
||||
tocLinks.forEach((link) => {
|
||||
if (link.getAttribute('href') === #) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
tocLinks.forEach((link) => link.classList.remove('active'));
|
||||
};
|
||||
}, [activeHeading, headings]);
|
||||
const contentAreaClass = 'content-area' + (sidebarOpen ? ' with-sidebar' : '');
|
||||
|
||||
if (!currentFile) {
|
||||
return (
|
||||
<div className={content-area }>
|
||||
<div className={contentAreaClass}>
|
||||
<div className="welcome-message">
|
||||
<div className="welcome-content">
|
||||
<h1>🌱 欢迎来到萌芽笔记</h1>
|
||||
<p>请从左侧导航栏选择一个笔记文件开始阅读</p>
|
||||
<h1>🌙 欢迎回来</h1>
|
||||
<p>从左侧目录选择任意 Markdown 笔记即可开始阅读。</p>
|
||||
<div className="welcome-features">
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">📝</span>
|
||||
<span>支持完整的Markdown语法</span>
|
||||
<span>原汁原味的 Markdown 样式</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">🎨</span>
|
||||
<span>代码语法高亮</span>
|
||||
<span className="feature-icon">💡</span>
|
||||
<span>深色界面,夜间更护眼</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">📊</span>
|
||||
<span>数学公式与图表</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">📱</span>
|
||||
<span>移动端友好布局</span>
|
||||
<span className="feature-icon">⚡</span>
|
||||
<span>代码高亮与复制一键搞定</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,16 +201,15 @@ export default function MarkdownRenderer() {
|
||||
}
|
||||
|
||||
const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent);
|
||||
const hasHeadings = headings.length > 0;
|
||||
|
||||
return (
|
||||
<div className={content-area }>
|
||||
<div className={contentAreaClass}>
|
||||
<div className="content-header">
|
||||
<Breadcrumbs filePath={currentFile} />
|
||||
<h1 className="content-title">{fileTitle}</h1>
|
||||
</div>
|
||||
|
||||
<div className={content-body }>
|
||||
<div className="content-body">
|
||||
<div className="markdown-pane">
|
||||
{isLoading ? (
|
||||
<div className="loading-content">
|
||||
@@ -332,8 +228,6 @@ export default function MarkdownRenderer() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasHeadings && <TableOfContents headings={headings} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(236, 251, 242, 0.92) 100%);
|
||||
background: var(--color-surface-alt);
|
||||
border-right: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.6rem 1.2rem 1.75rem;
|
||||
position: relative;
|
||||
z-index: 1002;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease, width 0.3s ease, padding 0.3s ease;
|
||||
padding: 1.5rem 1.25rem 1.75rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.sidebar.closed {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -24,35 +21,32 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.02em;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background: rgba(51, 197, 122, 0.16);
|
||||
color: var(--color-text);
|
||||
border: 1px solid rgba(47, 177, 112, 0.18);
|
||||
border-radius: 999px;
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 12px 24px rgba(86, 105, 141, 0.12);
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
background: rgba(51, 197, 122, 0.28);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.toggle-button:active {
|
||||
transform: translateY(0);
|
||||
color: var(--color-accent);
|
||||
border-color: rgba(58, 122, 254, 0.6);
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
@@ -61,40 +55,10 @@
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 2.5rem 1rem;
|
||||
color: var(--color-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(51, 197, 122, 0.18);
|
||||
border-top: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.directory-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
@@ -106,45 +70,73 @@
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.tree-node-content:hover {
|
||||
background: rgba(47, 177, 112, 0.12);
|
||||
color: var(--color-text);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.tree-node-content.selected {
|
||||
background: rgba(51, 197, 122, 0.25);
|
||||
color: var(--color-text);
|
||||
background: rgba(58, 122, 254, 0.16);
|
||||
color: var(--color-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
font-size: 1rem;
|
||||
transition: transform 0.3s ease;
|
||||
opacity: 0.9;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tree-node-content.selected .tree-node-icon {
|
||||
transform: scale(1.05);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.tree-node-children {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.7rem;
|
||||
padding: 2.5rem 1rem;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(111, 123, 146, 0.25);
|
||||
border-top: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-backdrop {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(18, 43, 31, 0.18);
|
||||
backdrop-filter: blur(2px);
|
||||
background: rgba(16, 23, 35, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1001;
|
||||
transition: opacity 0.25s ease;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sidebar-backdrop.active {
|
||||
@@ -154,49 +146,38 @@
|
||||
|
||||
.sidebar-toggle {
|
||||
position: fixed;
|
||||
top: clamp(1rem, 5vw, 1.5rem);
|
||||
left: clamp(1rem, 5vw, 1.5rem);
|
||||
z-index: 1200;
|
||||
top: 1.25rem;
|
||||
left: 1.25rem;
|
||||
z-index: 120;
|
||||
}
|
||||
|
||||
.sidebar-toggle .toggle-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: 0 16px 32px rgba(31, 58, 44, 0.18);
|
||||
padding: 0.45rem 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle .toggle-button.open {
|
||||
background: rgba(47, 177, 112, 0.22);
|
||||
}
|
||||
|
||||
.sidebar.closed ~ .sidebar-toggle .toggle-button {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
.toggle-button.open {
|
||||
color: var(--color-accent);
|
||||
border-color: rgba(58, 122, 254, 0.55);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
top: clamp(0.75rem, 3vw, 1.5rem);
|
||||
left: clamp(0.75rem, 3vw, 1.5rem);
|
||||
height: calc(100% - clamp(1.5rem, 6vw, 3rem));
|
||||
top: clamp(1rem, 5vw, 2rem);
|
||||
left: clamp(1rem, 5vw, 2rem);
|
||||
height: calc(100% - clamp(2rem, 10vw, 4rem));
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
transform: translateX(calc(-100% - 2.5rem));
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,13 +192,5 @@
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar.closed {
|
||||
width: var(--sidebar-width);
|
||||
padding: 1.6rem 1.2rem 1.75rem;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,15 @@ function TreeNode({ node, level = 0 }) {
|
||||
|
||||
const isSelected = currentFile === node.path;
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const paddingLeft = `${level * 18 + 14}px`;
|
||||
const contentClass = isSelected ? 'tree-node-content selected' : 'tree-node-content';
|
||||
const folderIconClass = `tree-node-icon${node.isExpanded ? ' expanded' : ''}`;
|
||||
|
||||
return (
|
||||
<div className="tree-node">
|
||||
<div
|
||||
className={ ree-node-content }
|
||||
style={{ paddingLeft: ${level * 18 + 14}px }}
|
||||
className={contentClass}
|
||||
style={{ paddingLeft }}
|
||||
onClick={handleClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -33,7 +36,7 @@ function TreeNode({ node, level = 0 }) {
|
||||
}}
|
||||
>
|
||||
{node.type === NODE_TYPES.FOLDER && (
|
||||
<span className={ ree-node-icon } aria-hidden>
|
||||
<span className={folderIconClass} aria-hidden>
|
||||
{hasChildren ? (node.isExpanded ? '📂' : '📁') : '📁'}
|
||||
</span>
|
||||
)}
|
||||
@@ -58,10 +61,13 @@ export default function Sidebar() {
|
||||
const { directoryTree, isLoading, error, sidebarOpen, toggleSidebar } = useApp();
|
||||
|
||||
const toggleLabel = sidebarOpen ? '收起目录' : '展开目录';
|
||||
const sidebarClass = sidebarOpen ? 'sidebar open' : 'sidebar closed';
|
||||
const toggleButtonClass = sidebarOpen ? 'toggle-button open' : 'toggle-button';
|
||||
const backdropClass = sidebarOpen ? 'sidebar-backdrop active' : 'sidebar-backdrop';
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleScrollLock = () => {
|
||||
@@ -80,7 +86,7 @@ export default function Sidebar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className={sidebar } aria-hidden={!sidebarOpen}>
|
||||
<aside className={sidebarClass} aria-hidden={!sidebarOpen}>
|
||||
<div className="sidebar-header">
|
||||
<h2>📚 萌芽笔记</h2>
|
||||
<button type="button" onClick={toggleSidebar} className="toggle-button" aria-label={toggleLabel}>
|
||||
@@ -113,7 +119,7 @@ export default function Sidebar() {
|
||||
</aside>
|
||||
|
||||
<div
|
||||
className={sidebar-backdrop }
|
||||
className={backdropClass}
|
||||
onClick={sidebarOpen ? toggleSidebar : undefined}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -122,7 +128,7 @@ export default function Sidebar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSidebar}
|
||||
className={ oggle-button }
|
||||
className={toggleButtonClass}
|
||||
aria-label={toggleLabel}
|
||||
>
|
||||
{sidebarOpen ? '✕' : '☰'}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"totalFiles": 125,
|
||||
"totalFolders": 50,
|
||||
"generatedAt": "2025-09-28T13:25:29.475Z",
|
||||
"sourceDirectory": "C:\\Users\\BigTang\\Desktop\\Markdown转网页\\markdown-to-web\\public\\mengyanote"
|
||||
"generatedAt": "2025-09-29T05:49:17.078Z",
|
||||
"sourceDirectory": "E:\\React\\markdown-to-web\\public\\mengyanote"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
:root {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -8,13 +8,13 @@
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-link-hover);
|
||||
color: #1f5ce6;
|
||||
}
|
||||
|
||||
img,
|
||||
|
||||
Reference in New Issue
Block a user