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, }; // 生成所有文章的静态路径