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

@@ -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>
</>
);
}