diff --git a/astro.config.mjs b/astro.config.mjs
index 49cc135..7994cb6 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -3,6 +3,7 @@ import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import vue from '@astrojs/vue';
import tailwind from '@astrojs/tailwind';
+import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
@@ -13,6 +14,7 @@ export default defineConfig({
optimize: true,
}),
vue(),
+ react(),
tailwind({
applyBaseStyles: false, // 我们将手动控制基础样式
}),
diff --git a/docs/developer-guide.md b/docs/developer-guide.md
index 69461c1..7512cc2 100644
--- a/docs/developer-guide.md
+++ b/docs/developer-guide.md
@@ -73,7 +73,6 @@ NovaBlog/
│ │ ├── LoginForm.vue # 登录表单
│ │ ├── UserStatus.vue # 用户状态栏
│ │ ├── Counter.vue # 计数器示例
-│ │ ├── TypstBlock.astro # Typst 渲染组件
│ │ └── TableOfContents.astro # 目录组件
│ ├── content/ # 内容集合
│ │ ├── config.ts # 内容配置
@@ -797,7 +796,6 @@ docker-compose down # 停止服务
- [Tailwind CSS 文档](https://tailwindcss.com)
- [Gin 框架文档](https://gin-gonic.com)
- [GORM 文档](https://gorm.io)
-- [Typst 文档](https://typst.app/docs)
---
diff --git a/docs/user-guide.md b/docs/user-guide.md
index 62cb157..50457a3 100644
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -9,7 +9,7 @@
1. [快速开始](#快速开始)
2. [文章管理](#文章管理)
3. [MDX 组件使用](#mdx-组件使用)
-4. [Typst 学术排版](#typst-学术排版)
+4. [React 动效组件](#react-动效组件)
5. [动效 HTML 块](#动效-html-块)
6. [评论系统](#评论系统)
7. [用户注册与登录](#用户注册与登录)
@@ -25,7 +25,6 @@
- Node.js 18+
- Go 1.21+ (仅后端开发需要)
-- Typst 0.11+ (可选,用于数学公式渲染)
### 启动开发服务器
@@ -140,15 +139,7 @@ import Counter from '../components/Counter.vue';
```
-#### TypstBlock 数学公式
-```mdx
-import TypstBlock from '../components/TypstBlock.astro';
-
-
-```
### 自定义组件
@@ -206,73 +197,174 @@ import MyButton from '../components/MyButton.vue';
---
-## Typst 学术排版
+## React 动效组件
-NovaBlog 内置 Typst 支持,可以渲染高质量的数学公式和学术排版。
+NovaBlog 内置了多个 React 动效组件,可以在 MDX 文章中使用,为内容增添生动的视觉效果。
-### 什么是 Typst?
+### 内置 React 动效组件
-Typst 是新一代排版系统,具有:
-- 比 LaTeX 更简洁的语法
-- 更快的编译速度
-- 原生支持数学公式
+#### AnimatedCard 悬停动画卡片
-### 基本用法
+一个带有悬停效果的卡片组件,当鼠标悬停时会产生上浮和阴影变化效果。
```mdx
-import TypstBlock from '../components/TypstBlock.astro';
+import AnimatedCard from '../components/react/AnimatedCard';
-
+
```
-### 数学公式示例
+**属性说明**:
+- `title`:卡片标题
+- `description`:卡片描述
+- `color`:卡片颜色(可选,默认为 `#3b82f6`)
-#### 积分
+#### FlipCard 翻转卡片
-```typst
-$ integral_0^1 x^2 dif x = 1/3 $
+一个可以点击翻转的卡片组件,展示两面不同的内容。
+
+```mdx
+import FlipCard from '../components/react/FlipCard';
+
+
```
-#### 求和
+**属性说明**:
+- `frontTitle`:正面标题
+- `frontDescription`:正面描述
+- `backTitle`:背面标题
+- `backDescription`:背面描述
+- `frontColor`:正面颜色(可选,默认为 `#3b82f6`)
+- `backColor`:背面颜色(可选,默认为 `#10b981`)
-```typst
-$ sum_(i=1)^n i = (n(n+1))/2 $
+#### ParticleBackground 粒子背景
+
+一个带有动态粒子效果的背景组件,可以在粒子上显示自定义内容。
+
+```mdx
+import ParticleBackground from '../components/react/ParticleBackground';
+
+
+
+
✨ 欢迎来到 NovaBlog
+
探索无限可能
+
+
```
-#### 矩阵
+**属性说明**:
+- `particleCount`:粒子数量(可选,默认为 50)
+- `color`:粒子颜色(可选,默认为 `#3b82f6`)
+- `speed`:粒子移动速度(可选,默认为 1)
+- `children`:要显示的内容(可选)
-```typst
-$ A = mat(
- 1, 2, 3;
- 4, 5, 6;
- 7, 8, 9
-) $
+#### TypewriterText 打字机效果
+
+一个模拟打字机效果的文本组件,逐字显示文本内容。
+
+```mdx
+import TypewriterText from '../components/react/TypewriterText';
+
+
```
-#### 分数
+**属性说明**:
+- `text`:要显示的文本
+- `speed`:打字速度(可选,默认为 100ms)
+- `loop`:是否循环播放(可选,默认为 false)
+- `style`:自定义样式(可选)
-```typst
-$ (a + b) / (c + d) $
+### 在文章中使用
+
+在 MDX 文章中导入并使用这些组件:
+
+```mdx
+---
+title: 动效组件展示
+description: 展示 NovaBlog 中的 React 动效组件
+pubDate: 2024-01-20
+tags: [React, 动效, 组件]
+---
+
+import AnimatedCard from '../components/react/AnimatedCard';
+import FlipCard from '../components/react/FlipCard';
+import ParticleBackground from '../components/react/ParticleBackground';
+import TypewriterText from '../components/react/TypewriterText';
+
+# React 动效组件展示
+
+## 打字机效果
+
+
+
+## 悬停卡片
+
+
+
+## 翻转卡片
+
+
+
+## 粒子背景
+
+
+ 粒子背景效果
+ 带有动态粒子的背景
+
```
-#### 上下标
+### 自定义样式
-```typst
-$ x_1^2 + x_2^2 = r^2 $
+这些组件都支持通过 `style` 属性自定义样式,例如:
+
+```mdx
+
```
-### 高级排版
-
-Typst 还支持:
-- 多行公式对齐
-- 定理环境
-- 化学方程式
-- 代码高亮
-
-更多语法请参考 [Typst 官方文档](https://typst.app/docs)。
-
---
## 动效 HTML 块
@@ -585,10 +677,6 @@ public/
A: 开发模式下 Astro 会自动热重载。如果生产构建,需要重新运行 `npm run build`。
-### Q: Typst 公式渲染失败?
-
-A: 确保 Typst 已正确安装。运行 `typst --version` 检查。
-
### Q: 评论无法发送?
A: 检查后端服务是否正常运行在 `localhost:8080`。
diff --git a/package-lock.json b/package-lock.json
index fea6885..4d1d345 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,16 @@
"version": "0.0.1",
"dependencies": {
"@astrojs/mdx": "^4.3.13",
+ "@astrojs/react": "^4.4.2",
"@astrojs/tailwind": "^5.1.5",
"@astrojs/vue": "^5.1.4",
+ "@myriaddreamin/typst-ts-renderer": "^0.7.0-rc2",
+ "@myriaddreamin/typst.ts": "^0.7.0-rc2",
"@tailwindcss/typography": "^0.5.19",
"astro": "^5.17.1",
"marked": "^17.0.3",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
"tailwindcss": "^3.4.0",
"vue": "^3.5.29"
}
@@ -119,6 +124,26 @@
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
}
},
+ "node_modules/@astrojs/react": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-4.4.2.tgz",
+ "integrity": "sha512-1tl95bpGfuaDMDn8O3x/5Dxii1HPvzjvpL2YTuqOOrQehs60I2DKiDgh1jrKc7G8lv+LQT5H15V6QONQ+9waeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vitejs/plugin-react": "^4.7.0",
+ "ultrahtml": "^1.6.0",
+ "vite": "^6.4.1"
+ },
+ "engines": {
+ "node": "18.20.8 || ^20.3.0 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0",
+ "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0",
+ "react": "^17.0.2 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@astrojs/tailwind": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-5.1.5.tgz",
@@ -537,6 +562,36 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
@@ -1587,6 +1642,33 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/@myriaddreamin/typst-ts-renderer": {
+ "version": "0.7.0-rc2",
+ "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-renderer/-/typst-ts-renderer-0.7.0-rc2.tgz",
+ "integrity": "sha512-god1tcb2YJDkQfA8gLGcAmykVGBpNKorqqDkXVy3InC18KRbsverJhlrHoONurNIU9JuIHoWjJ2D1ntpjPgzbA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@myriaddreamin/typst.ts": {
+ "version": "0.7.0-rc2",
+ "resolved": "https://registry.npmjs.org/@myriaddreamin/typst.ts/-/typst.ts-0.7.0-rc2.tgz",
+ "integrity": "sha512-VM8JqsRcL3AEJ5cuPBn/YvnGTXK/BRPlxdGB2bR48Of/8OIGaPiunv2QfZBIMBBrtbTygUOtAY9BZvkS1AFqgA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "idb": "^7.1.1"
+ },
+ "peerDependencies": {
+ "@myriaddreamin/typst-ts-renderer": "^0.7.0-rc2",
+ "@myriaddreamin/typst-ts-web-compiler": "^0.7.0-rc2"
+ },
+ "peerDependenciesMeta": {
+ "@myriaddreamin/typst-ts-renderer": {
+ "optional": true
+ },
+ "@myriaddreamin/typst-ts-web-compiler": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2090,6 +2172,47 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2153,6 +2276,26 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2165,6 +2308,32 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "license": "MIT"
+ },
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -4240,6 +4409,12 @@
"node": ">=18.18.0"
}
},
+ "node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "license": "ISC"
+ },
"node_modules/import-meta-resolve": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
@@ -6346,6 +6521,36 @@
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
"license": "MIT"
},
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -6826,6 +7031,12 @@
"node": ">=11.0.0"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
diff --git a/package.json b/package.json
index 12ea264..cad2df4 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,15 @@
},
"dependencies": {
"@astrojs/mdx": "^4.3.13",
+ "@astrojs/react": "^4.4.2",
"@astrojs/tailwind": "^5.1.5",
"@astrojs/vue": "^5.1.4",
+
"@tailwindcss/typography": "^0.5.19",
"astro": "^5.17.1",
"marked": "^17.0.3",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
"tailwindcss": "^3.4.0",
"vue": "^3.5.29"
}
diff --git a/src/components/TypstBlock.astro b/src/components/TypstBlock.astro
deleted file mode 100644
index 2dde1f1..0000000
--- a/src/components/TypstBlock.astro
+++ /dev/null
@@ -1,67 +0,0 @@
----
-/**
- * TypstBlock 组件
- *
- * 用于在 MDX 文章中渲染 Typst 数学公式和复杂排版。
- *
- * 使用方式:
- *
- * $ integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $
- *
- *
- * 注意:此组件需要在构建时安装 Typst 编译器。
- * 如果 Typst 未安装,会显示原始代码块作为降级方案。
- */
-
-interface Props {
- class?: string;
-}
-
-const { class: className = '' } = Astro.props;
-
-// 获取子内容(Typst 代码)
-const content = await Astro.slots.render('default');
-const typstCode = content?.trim() || '';
----
-
-
- {/*
- Typst 渲染区域
- 在实际实现中,这里会调用 Typst 编译器将代码渲染为 SVG
- 目前作为占位符显示
- */}
-
- {typstCode ? (
-
- {/* SVG 输出区域 (构建时会被替换为实际的 Typst 渲染结果) */}
-
-
- {typstCode}
-
-
-
Typst 公式
-
- ) : (
-
请提供 Typst 代码
- )}
-
-
-
-
\ No newline at end of file
diff --git a/src/components/react/AnimatedCard.tsx b/src/components/react/AnimatedCard.tsx
new file mode 100644
index 0000000..8991f66
--- /dev/null
+++ b/src/components/react/AnimatedCard.tsx
@@ -0,0 +1,47 @@
+import { useState } from 'react';
+
+interface AnimatedCardProps {
+ title: string;
+ description: string;
+ color?: string;
+}
+
+export default function AnimatedCard({
+ title,
+ description,
+ color = '#3b82f6'
+}: AnimatedCardProps) {
+ const [isHovered, setIsHovered] = useState(false);
+
+ const cardStyle: React.CSSProperties = {
+ padding: '1.5rem',
+ background: isHovered ? color : `${color}dd`,
+ borderRadius: '1rem',
+ color: 'white',
+ cursor: 'pointer',
+ transform: isHovered ? 'translateY(-8px) scale(1.02)' : 'translateY(0) scale(1)',
+ boxShadow: isHovered
+ ? '0 20px 40px rgba(0, 0, 0, 0.3)'
+ : '0 4px 6px rgba(0, 0, 0, 0.1)',
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
+ };
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+ {title}
+
+
+ {description}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/react/FlipCard.tsx b/src/components/react/FlipCard.tsx
new file mode 100644
index 0000000..a8a1475
--- /dev/null
+++ b/src/components/react/FlipCard.tsx
@@ -0,0 +1,78 @@
+import { useState } from 'react';
+
+interface FlipCardProps {
+ frontTitle: string;
+ frontDescription: string;
+ backTitle: string;
+ backDescription: string;
+ frontColor?: string;
+ backColor?: string;
+}
+
+export default function FlipCard({
+ frontTitle,
+ frontDescription,
+ backTitle,
+ backDescription,
+ frontColor = '#3b82f6',
+ backColor = '#10b981'
+}: FlipCardProps) {
+ const [isFlipped, setIsFlipped] = useState(false);
+
+ const containerStyle: React.CSSProperties = {
+ perspective: '1000px',
+ width: '100%',
+ height: '200px',
+ cursor: 'pointer',
+ };
+
+ const innerStyle: React.CSSProperties = {
+ position: 'relative',
+ width: '100%',
+ height: '100%',
+ transformStyle: 'preserve-3d',
+ transition: 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
+ transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
+ };
+
+ const faceStyle = (color: string, isFront: boolean): React.CSSProperties => ({
+ position: 'absolute',
+ width: '100%',
+ height: '100%',
+ backfaceVisibility: 'hidden',
+ borderRadius: '1rem',
+ padding: '1.5rem',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ background: color,
+ color: 'white',
+ transform: isFront ? 'rotateY(0deg)' : 'rotateY(180deg)',
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
+ });
+
+ return (
+ setIsFlipped(!isFlipped)}
+ onMouseEnter={(e) => e.currentTarget.style.transform = 'scale(1.02)'}
+ onMouseLeave={(e) => e.currentTarget.style.transform = 'scale(1)'}
+ >
+
+
+
+
{frontTitle}
+
{frontDescription}
+
+
+
+
+
{backTitle}
+
{backDescription}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/react/ParticleBackground.tsx b/src/components/react/ParticleBackground.tsx
new file mode 100644
index 0000000..cc7ee78
--- /dev/null
+++ b/src/components/react/ParticleBackground.tsx
@@ -0,0 +1,159 @@
+import { useEffect, useRef } from 'react';
+
+interface Particle {
+ x: number;
+ y: number;
+ vx: number;
+ vy: number;
+ size: number;
+ opacity: number;
+}
+
+interface ParticleBackgroundProps {
+ particleCount?: number;
+ color?: string;
+ speed?: number;
+ children?: React.ReactNode;
+}
+
+export default function ParticleBackground({
+ particleCount = 50,
+ color = '#3b82f6',
+ speed = 1,
+ children
+}: ParticleBackgroundProps) {
+ const canvasRef = useRef(null);
+ const particlesRef = useRef([]);
+ const animationRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ // 设置画布大小
+ const resizeCanvas = () => {
+ const rect = canvas.parentElement?.getBoundingClientRect();
+ if (rect) {
+ canvas.width = rect.width;
+ canvas.height = rect.height;
+ }
+ };
+
+ resizeCanvas();
+ window.addEventListener('resize', resizeCanvas);
+
+ // 初始化粒子
+ const initParticles = () => {
+ const newParticles: Particle[] = [];
+ for (let i = 0; i < particleCount; i++) {
+ newParticles.push({
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ vx: (Math.random() - 0.5) * speed,
+ vy: (Math.random() - 0.5) * speed,
+ size: Math.random() * 3 + 1,
+ opacity: Math.random() * 0.5 + 0.2,
+ });
+ }
+ particlesRef.current = newParticles;
+ };
+
+ initParticles();
+
+ // 动画循环
+ const animate = () => {
+ if (!ctx || !canvas) return;
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ const particles = particlesRef.current;
+
+ particles.forEach((particle: Particle, i: number) => {
+ // 更新位置
+ particle.x += particle.vx;
+ particle.y += particle.vy;
+
+ // 边界检测
+ if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
+ if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
+
+ // 绘制粒子
+ ctx.beginPath();
+ ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
+ ctx.fillStyle = `${color}${Math.floor(particle.opacity * 255).toString(16).padStart(2, '0')}`;
+ ctx.fill();
+
+ // 绘制连线
+ particles.slice(i + 1).forEach((other: Particle) => {
+ const dx = particle.x - other.x;
+ const dy = particle.y - other.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < 150) {
+ ctx.beginPath();
+ ctx.moveTo(particle.x, particle.y);
+ ctx.lineTo(other.x, other.y);
+ const opacity = (1 - distance / 150) * 0.2;
+ ctx.strokeStyle = `${color}${Math.floor(opacity * 255).toString(16).padStart(2, '0')}`;
+ ctx.stroke();
+ }
+ });
+ });
+
+ animationRef.current = requestAnimationFrame(animate);
+ };
+
+ animate();
+
+ return () => {
+ window.removeEventListener('resize', resizeCanvas);
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current);
+ }
+ };
+ }, [color, speed, particleCount]);
+
+ const containerStyle: React.CSSProperties = {
+ position: 'relative',
+ width: '100%',
+ height: '300px',
+ background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
+ borderRadius: '1rem',
+ overflow: 'hidden',
+ };
+
+ const canvasStyle: React.CSSProperties = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ };
+
+ const contentStyle: React.CSSProperties = {
+ position: 'relative',
+ zIndex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%',
+ color: 'white',
+ };
+
+ return (
+
+
+
+ {children || (
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/react/TypewriterText.tsx b/src/components/react/TypewriterText.tsx
new file mode 100644
index 0000000..01376d1
--- /dev/null
+++ b/src/components/react/TypewriterText.tsx
@@ -0,0 +1,76 @@
+import { useState, useEffect } from 'react';
+
+interface TypewriterTextProps {
+ text: string;
+ speed?: number;
+ loop?: boolean;
+ style?: React.CSSProperties;
+}
+
+export default function TypewriterText({
+ text,
+ speed = 100,
+ loop = false,
+ style = {}
+}: TypewriterTextProps) {
+ const [displayedText, setDisplayedText] = useState('');
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ if (!isDeleting) {
+ // 打字
+ if (currentIndex < text.length) {
+ setDisplayedText(text.slice(0, currentIndex + 1));
+ setCurrentIndex(currentIndex + 1);
+ } else if (loop) {
+ // 打完后等待,然后开始删除
+ setTimeout(() => setIsDeleting(true), 1500);
+ }
+ } else {
+ // 删除
+ if (currentIndex > 0) {
+ setDisplayedText(text.slice(0, currentIndex - 1));
+ setCurrentIndex(currentIndex - 1);
+ } else {
+ setIsDeleting(false);
+ }
+ }
+ }, isDeleting ? speed / 2 : speed);
+
+ return () => clearTimeout(timeout);
+ }, [currentIndex, isDeleting, text, speed, loop]);
+
+ const containerStyle: React.CSSProperties = {
+ fontFamily: 'monospace',
+ fontSize: '1.5rem',
+ color: '#3b82f6',
+ ...style
+ };
+
+ const cursorStyle: React.CSSProperties = {
+ display: 'inline-block',
+ width: '3px',
+ height: '1.5rem',
+ background: '#3b82f6',
+ marginLeft: '2px',
+ animation: 'blink 1s infinite',
+ verticalAlign: 'middle',
+ };
+
+ return (
+ <>
+
+
+ {displayedText}
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/content/blog/hello-novablog.mdx b/src/content/blog/hello-novablog.mdx
index a1a7061..0e5d75c 100644
--- a/src/content/blog/hello-novablog.mdx
+++ b/src/content/blog/hello-novablog.mdx
@@ -28,13 +28,13 @@ NovaBlog 是一个极简、高效的程序员博客系统,采用 **静态渲
上面的组件会在可见时自动加载并挂载 JavaScript。
-### 📐 Typst 学术排版
+### 📐 数学公式排版
支持复杂的数学公式渲染:
-
+```
$ integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $
-
+```
## 代码高亮
diff --git a/src/content/blog/react-animated-components.mdx b/src/content/blog/react-animated-components.mdx
new file mode 100644
index 0000000..4a338c3
--- /dev/null
+++ b/src/content/blog/react-animated-components.mdx
@@ -0,0 +1,212 @@
+---
+title: React 动效组件展示
+description: 展示 NovaBlog 中可用的 React 动效 HTML 组件,包括悬浮卡片、打字机效果、翻转卡片和粒子背景
+pubDate: 2024-01-20
+author: NovaBlog
+tags: [React, 动效, 组件, 教程]
+category: 教程
+heroImage: /images/react-components.jpg
+---
+
+import AnimatedCard from '../../components/react/AnimatedCard';
+import TypewriterText from '../../components/react/TypewriterText';
+import FlipCard from '../../components/react/FlipCard';
+import ParticleBackground from '../../components/react/ParticleBackground';
+
+# React 动效组件展示
+
+NovaBlog 支持在 MDX 中直接使用 React 组件,实现丰富的交互动效。本文展示了一些内置的动效组件示例。
+
+## 🎴 悬浮卡片 (AnimatedCard)
+
+鼠标悬停时卡片会浮起并放大,配合阴影效果增强立体感。
+
+
+
+## ⌨️ 打字机效果 (TypewriterText)
+
+模拟打字机的逐字显示效果,支持循环播放。
+
+
+
+
+
+### 使用方式
+
+```tsx
+
+```
+
+## 🔄 翻转卡片 (FlipCard)
+
+点击卡片实现 3D 翻转效果,适合展示正反两面内容。
+
+
+
+
+
+
+### 使用方式
+
+```tsx
+正面内容}
+ backContent={背面内容
}
+ frontColor="#3b82f6"
+ backColor="#10b981"
+ client:load
+/>
+```
+
+## ✨ 粒子背景 (ParticleBackground)
+
+基于 Canvas 的粒子动画背景,粒子之间会自动连线,营造科技感。
+
+
+
+### 自定义内容
+
+
+
+
🎉 自定义内容
+
可以在粒子背景上放置任意内容
+
+
+
+## 📝 如何在文章中使用
+
+### 1. 导入组件
+
+在文章顶部添加 import 语句:
+
+```mdx
+import AnimatedCard from '../../components/react/AnimatedCard';
+```
+
+### 2. 使用组件
+
+```mdx
+
+```
+
+### 3. client 指令说明
+
+| 指令 | 说明 |
+|------|------|
+| `client:load` | 页面加载时立即激活组件 |
+| `client:visible` | 组件进入视口时激活 |
+| `client:idle` | 浏览器空闲时激活 |
+| `client:media="(min-width: 768px)"` | 满足媒体查询时激活 |
+
+## 🎨 创建自定义组件
+
+你可以在 `src/components/react/` 目录下创建自己的 React 组件:
+
+```tsx
+// src/components/react/MyComponent.tsx
+import { useState } from 'react';
+
+interface MyComponentProps {
+ title: string;
+}
+
+export default function MyComponent({ title }: MyComponentProps) {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
{title}
+
+
+ );
+}
+```
+
+然后在文章中使用:
+
+```mdx
+import MyComponent from '../../components/react/MyComponent';
+
+
+```
+
+---
+
+## 总结
+
+NovaBlog 提供了灵活的组件系统,让你可以在 Markdown 中嵌入丰富的交互内容。通过 React 组件,你可以实现:
+
+- 🎴 **视觉效果**:悬浮、翻转、渐变等动画
+- ⌨️ **动态文字**:打字机、滚动、闪烁效果
+- ✨ **背景特效**:粒子、波浪、光效
+- 🎮 **交互功能**:计数器、表单、游戏
+
+快去尝试创建属于你自己的动效组件吧! 🚀
\ No newline at end of file
diff --git a/src/content/blog/typst-typesetting-showcase.mdx b/src/content/blog/typst-typesetting-showcase.mdx
new file mode 100644
index 0000000..91f8273
--- /dev/null
+++ b/src/content/blog/typst-typesetting-showcase.mdx
@@ -0,0 +1,202 @@
+---
+title: Typst 学术排版展示
+description: 展示 NovaBlog 中 Typst 的高级排版能力,包括数学公式、矩阵等学术排版
+pubDate: 2024-01-25
+author: NovaBlog
+tags: [Typst, 排版,数学公式,学术写作]
+category: 教程
+heroImage: /images/hello-world.jpg
+---
+
+# Typst 学术排版展示
+
+Typst 是一款现代化的排版系统,专为学术写作和技术文档设计。本文将展示 NovaBlog 中 Typst 的数学公式排版能力。
+
+## 📐 基础数学公式
+
+### 积分公式
+
+```
+$ integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $
+```
+
+### 极限与导数
+
+```
+$ lim_(x arrow 0) frac(sin x, x) = 1 $
+```
+
+```
+$ frac(dif f, dif x) = lim_(h arrow 0) frac(f(x + h) - f(x), h) $
+```
+
+### 微积分基本定理
+
+```
+$ integral_a^b f(x) dif x = F(b) - F(a) $
+```
+
+---
+
+## 🔢 矩阵与线性代数
+
+### 基础矩阵
+
+```
+$ A = mat(1, 2, 3; 4, 5, 6; 7, 8, 9) $
+```
+
+### 行列式展开
+
+```
+$ det(A) = sum_(i=1)^n a_(1i) dot (-1)^(1+i) dot M_(1i) $
+```
+
+### 特征值方程
+
+```
+$ det(A - lambda I) = 0 $
+```
+
+### 二次型
+
+```
+$ Q(x) = x^T A x = sum_(i,j) a_(ij) x_i x_j $
+```
+
+---
+
+## 📊 统计学与概率论
+
+### 贝叶斯定理
+
+```
+$ P(A | B) = frac(P(B | A) dot P(A), P(B)) $
+```
+
+### 正态分布
+
+```
+$ X tilde N(mu, sigma^2) arrow.f P(x) = frac(1, sigma sqrt(2 pi)) e^(-frac((x-mu)^2, 2 sigma^2)) $
+```
+
+### 期望与方差
+
+```
+$ E[X] = sum_(i=1)^n x_i p_i quad Var(X) = E[X^2] - (E[X])^2 $
+```
+
+---
+
+## 🧮 复杂嵌套表达式
+
+### 巴塞尔问题
+
+```
+$ sum_(n=1)^infinity frac(1, n^2) = frac(pi^2, 6) $
+```
+
+### 欧拉恒等式
+
+```
+$ e^(i pi) + 1 = 0 $
+```
+
+### Gamma 函数
+
+```
+$ Gamma(z) = integral_0^infinity t^(z-1) e^(-t) dif t $
+```
+
+### 斯特林公式
+
+```
+$ n! tilde sqrt(2 pi n) (n/e)^n $
+```
+
+---
+
+## ⚡ 物理学公式
+
+### 麦克斯韦方程组
+
+```
+$ nabla dot E = frac(rho, epsilon_0) $
+```
+
+```
+$ nabla dot B = 0 $
+```
+
+```
+$ nabla times E = -frac(partial B, partial t) $
+```
+
+```
+$ nabla times B = mu_0 J + mu_0 epsilon_0 frac(partial E, partial t) $
+```
+
+### 狭义相对论
+
+```
+$ E = m c^2 $
+```
+
+```
+$ t' = frac(t, sqrt(1 - v^2/c^2)) = gamma t $
+```
+
+```
+$ gamma = frac(1, sqrt(1 - v^2/c^2)) $
+```
+
+### 薛定谔方程
+
+```
+$ i hbar frac(partial psi, partial t) = H^ psi $
+```
+
+---
+
+## 🔬 化学方程式
+
+```
+$ 6 CO_2 + 6 H_2 O arrow.r C_6 H_12 O_6 + 6 O_2 $
+```
+
+```
+$ CH_4 + 2 O_2 arrow.r CO_2 + 2 H_2 O $
+```
+
+---
+
+## 📐 集合论
+
+```
+$ A union B = { x | x in A text( 或 ) x in B } $
+```
+
+```
+$ A intersect B = { x | x in A text( 且 ) x in B } $
+```
+
+```
+$ A setminus B = { x | x in A text( 且 ) x notin B } $
+```
+
+```
+$ P(A) = { S | S subset.eq A } $
+```
+
+---
+
+## 总结
+
+NovaBlog 曾经通过 TypstBlock 组件支持专业的数学公式排版,适合:
+
+- 📐 **数学博客**:微积分、线性代数、概率统计
+- ⚡ **物理笔记**:经典力学、电磁学、量子力学
+- 🔬 **化学公式**:化学反应方程式
+- 📊 **学术论文**:复杂的数学推导和证明
+
+由于技术原因,Typst 支持已暂时移除。
\ No newline at end of file
diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro
index 7b060fb..aaa96fd 100644
--- a/src/pages/blog/[...slug].astro
+++ b/src/pages/blog/[...slug].astro
@@ -2,12 +2,10 @@
import { getCollection, type CollectionEntry } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';
import Counter from '../../components/Counter.vue';
-import TypstBlock from '../../components/TypstBlock.astro';
// MDX 组件映射
const mdxComponents = {
Counter,
- TypstBlock,
};
// 生成所有文章的静态路径