add more react function and remove typst support

This commit is contained in:
Jiao77
2026-03-01 19:15:43 +08:00
parent 4ba51e1755
commit 4760dbafe0
12 changed files with 994 additions and 72 deletions

View File

@@ -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, // 我们将手动控制基础样式
}),

211
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}

View File

@@ -1,67 +0,0 @@
---
/**
* TypstBlock 组件
*
* 用于在 MDX 文章中渲染 Typst 数学公式和复杂排版。
*
* 使用方式:
* <TypstBlock>
* $ integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $
* </TypstBlock>
*
* 注意:此组件需要在构建时安装 Typst 编译器。
* 如果 Typst 未安装,会显示原始代码块作为降级方案。
*/
interface Props {
class?: string;
}
const { class: className = '' } = Astro.props;
// 获取子内容Typst 代码)
const content = await Astro.slots.render('default');
const typstCode = content?.trim() || '';
---
<div class:list={['typst-block', 'my-6', className]}>
{/*
Typst 渲染区域
在实际实现中,这里会调用 Typst 编译器将代码渲染为 SVG
目前作为占位符显示
*/}
<div class="p-4 bg-muted rounded-lg border border-border overflow-x-auto">
{typstCode ? (
<div class="flex flex-col items-center">
{/* SVG 输出区域 (构建时会被替换为实际的 Typst 渲染结果) */}
<div class="typst-output text-lg" data-typst-code={typstCode}>
<code class="font-mono text-primary-600 dark:text-primary-400">
{typstCode}
</code>
</div>
<span class="text-xs text-foreground/40 mt-2">Typst 公式</span>
</div>
) : (
<p class="text-foreground/40 text-center">请提供 Typst 代码</p>
)}
</div>
</div>
<style>
.typst-block {
/* 确保 Typst 内容居中显示 */
text-align: center;
}
.typst-output {
/* 为数学公式提供合适的样式 */
font-family: 'Latin Modern Math', 'STIX Two Math', 'Noto Serif SC', serif;
line-height: 1.8;
}
.typst-output svg {
/* SVG 输出样式 */
max-width: 100%;
height: auto;
}
</style>

View File

@@ -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 (
<div
style={cardStyle}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<h3 style={{
margin: '0 0 0.5rem 0',
fontSize: '1.25rem',
fontWeight: 'bold'
}}>
{title}
</h3>
<p style={{ margin: 0, opacity: 0.9 }}>
{description}
</p>
</div>
);
}

View File

@@ -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 (
<div
style={containerStyle}
onClick={() => setIsFlipped(!isFlipped)}
onMouseEnter={(e) => e.currentTarget.style.transform = 'scale(1.02)'}
onMouseLeave={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
<div style={innerStyle}>
<div style={faceStyle(frontColor, true)}>
<div style={{ textAlign: 'center' }}>
<h3 style={{ margin: '0 0 0.5rem 0', fontSize: '1.25rem', fontWeight: 'bold' }}>{frontTitle}</h3>
<p style={{ margin: 0, opacity: 0.9 }}>{frontDescription}</p>
</div>
</div>
<div style={faceStyle(backColor, false)}>
<div style={{ textAlign: 'center' }}>
<h3 style={{ margin: '0 0 0.5rem 0', fontSize: '1.25rem', fontWeight: 'bold' }}>{backTitle}</h3>
<p style={{ margin: 0, opacity: 0.9 }}>{backDescription}</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -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<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]);
const animationRef = useRef<number | null>(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 (
<div style={containerStyle}>
<canvas ref={canvasRef} style={canvasStyle} />
<div style={contentStyle}>
{children || (
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '2rem', marginBottom: '0.5rem' }}> </h3>
<p style={{ opacity: 0.8 }}></p>
</div>
)}
</div>
</div>
);
}

View File

@@ -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 (
<>
<style>{`
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
`}</style>
<span style={containerStyle}>
{displayedText}
<span style={cursorStyle} />
</span>
</>
);
}

View File

@@ -28,13 +28,13 @@ NovaBlog 是一个极简、高效的程序员博客系统,采用 **静态渲
上面的组件会在可见时自动加载并挂载 JavaScript。
### 📐 Typst 学术排版
### 📐 数学公式排版
支持复杂的数学公式渲染:
<TypstBlock>
```
$ integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $
</TypstBlock>
```
## 代码高亮

View File

@@ -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)
鼠标悬停时卡片会浮起并放大,配合阴影效果增强立体感。
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '1.5rem', margin: '2rem 0' }}>
<AnimatedCard
title="🚀 快速开发"
description="使用 Astro + React 实现极速开发体验"
color="#3b82f6"
client:load
/>
<AnimatedCard
title="🎨 精美设计"
description="内置多种动效组件,轻松创建炫酷页面"
color="#10b981"
client:load
/>
<AnimatedCard
title="⚡ 高性能"
description="Islands 架构,按需加载,极致性能"
color="#f59e0b"
client:load
/>
</div>
## ⌨️ 打字机效果 (TypewriterText)
模拟打字机的逐字显示效果,支持循环播放。
<div style={{
background: '#1a1a2e',
padding: '2rem',
borderRadius: '1rem',
textAlign: 'center',
margin: '2rem 0'
}}>
<TypewriterText
text="Hello, NovaBlog! 欢迎来到你的新博客..."
speed={80}
loop={true}
client:load
/>
</div>
### 使用方式
```tsx
<TypewriterText
text="你要显示的文字"
speed={100} // 打字速度(毫秒)
loop={true} // 是否循环播放
client:load
/>
```
## 🔄 翻转卡片 (FlipCard)
点击卡片实现 3D 翻转效果,适合展示正反两面内容。
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1.5rem', margin: '2rem 0' }}>
<FlipCard
frontTitle="💡 小提示"
frontDescription="点击翻转查看更多"
backTitle="✅ 详细说明"
backDescription="这是一个翻转卡片的背面内容"
frontColor="#8b5cf6"
backColor="#06b6d4"
client:load
/>
<FlipCard
frontTitle="🎯 技术栈"
frontDescription="React + TypeScript"
backTitle="📦 组件库"
backDescription="支持自定义样式和动画"
frontColor="#ec4899"
backColor="#14b8a6"
client:load
/>
</div>
### 使用方式
```tsx
<FlipCard
frontContent={<div>正面内容</div>}
backContent={<div>背面内容</div>}
frontColor="#3b82f6"
backColor="#10b981"
client:load
/>
```
## ✨ 粒子背景 (ParticleBackground)
基于 Canvas 的粒子动画背景,粒子之间会自动连线,营造科技感。
<ParticleBackground
particleCount={60}
color="#60a5fa"
speed={0.8}
client:load
/>
### 自定义内容
<ParticleBackground
particleCount={30}
color="#f472b6"
speed={1.5}
client:load
>
<div style={{ textAlign: 'center' }}>
<h2 style={{ fontSize: '2.5rem', marginBottom: '1rem' }}>🎉 自定义内容</h2>
<p>可以在粒子背景上放置任意内容</p>
</div>
</ParticleBackground>
## 📝 如何在文章中使用
### 1. 导入组件
在文章顶部添加 import 语句:
```mdx
import AnimatedCard from '../../components/react/AnimatedCard';
```
### 2. 使用组件
```mdx
<AnimatedCard
title="标题"
description="描述"
color="#3b82f6"
client:load
/>
```
### 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 (
<div style={{ padding: '1rem', background: '#f3f4f6' }}>
<h3>{title}</h3>
<button onClick={() => setCount(count + 1)}>
点击次数: {count}
</button>
</div>
);
}
```
然后在文章中使用:
```mdx
import MyComponent from '../../components/react/MyComponent';
<MyComponent title="我的组件" client:load />
```
---
## 总结
NovaBlog 提供了灵活的组件系统,让你可以在 Markdown 中嵌入丰富的交互内容。通过 React 组件,你可以实现:
- 🎴 **视觉效果**:悬浮、翻转、渐变等动画
- ⌨️ **动态文字**:打字机、滚动、闪烁效果
- ✨ **背景特效**:粒子、波浪、光效
- 🎮 **交互功能**:计数器、表单、游戏
快去尝试创建属于你自己的动效组件吧! 🚀

View File

@@ -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 支持已暂时移除。

View File

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