add more react function and remove typst support
This commit is contained in:
@@ -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>
|
||||
47
src/components/react/AnimatedCard.tsx
Normal file
47
src/components/react/AnimatedCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
78
src/components/react/FlipCard.tsx
Normal file
78
src/components/react/FlipCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
159
src/components/react/ParticleBackground.tsx
Normal file
159
src/components/react/ParticleBackground.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
src/components/react/TypewriterText.tsx
Normal file
76
src/components/react/TypewriterText.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user