qia-client/src/pages/LandingPage.tsx
ddshi 69953c43cf refactor: 简化 landing page 优化字体样式
- 回退复杂的功能介绍区域,保留简洁设计
- 优化副标题可读性:添加柔和背景 + serif 字体
- 保持整体禅意风格不变

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 17:22:27 +08:00

410 lines
11 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Button, Container, Title, Text, Group, Stack } from '@mantine/core';
import { useNavigate } from 'react-router-dom';
import iconUrl from '../assets/icon.png';
// 禅意算法背景 - 手印静寂(循环墨晕效果)
function ZenBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
window.addEventListener('resize', resize);
const random = (min = 0, max = 1) => {
return min + Math.random() * (max - min);
};
const noise = (x: number, y: number, t: number) => {
return (Math.sin(x * 0.01 + t) * Math.cos(y * 0.01 + t * 0.7) + 1) * 0.5;
};
interface InkStroke {
points: { x: number; y: number; weight: number }[];
x: number;
y: number;
angle: number;
speed: number;
inkAlpha: number;
baseWeight: number;
maxLength: number;
currentLength: number;
complete: boolean;
}
let strokes: InkStroke[] = [];
let time = 0;
let lastStrokeTime = 0;
const strokeInterval = 30;
const createStroke = (): InkStroke | null => {
const angle = random(0, Math.PI * 2);
const radius = random(canvas.width * 0.1, canvas.width * 0.4);
const x = canvas.width / 2 + Math.cos(angle) * radius;
const y = canvas.height / 2 + Math.sin(angle) * radius;
return {
points: [{ x, y, weight: random(0.3, 1.5) }],
x,
y,
angle: random(0, Math.PI * 2),
speed: random(0.4, 1.0),
inkAlpha: random(10, 30),
baseWeight: random(0.3, 1.5),
maxLength: random(30, 90),
currentLength: 0,
complete: false,
};
};
const initStrokes = () => {
strokes = [];
for (let i = 0; i < 8; i++) {
const stroke = createStroke();
if (stroke) {
stroke.currentLength = random(0, stroke.maxLength * 0.5);
strokes.push(stroke);
}
}
};
initStrokes();
ctx.fillStyle = '#faf9f7';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const animate = () => {
time += 0.008;
if (Math.floor(time * 125) % 3 === 0) {
ctx.fillStyle = 'rgba(250, 249, 247, 0.12)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
if (time - lastStrokeTime > strokeInterval * 0.01) {
lastStrokeTime = time;
strokes = strokes.filter(s => !s.complete || s.currentLength < s.maxLength);
const newCount = Math.floor(random(0, 2));
for (let i = 0; i < newCount; i++) {
const stroke = createStroke();
if (stroke) strokes.push(stroke!);
}
}
for (const stroke of strokes) {
if (stroke.complete) continue;
const n = noise(stroke.x, stroke.y, time);
stroke.angle += (n - 0.5) * 0.12;
const breath = Math.sin(time * 1.5 + stroke.x * 0.01) * 0.25;
const currentSpeed = stroke.speed * (1 + breath * 0.2);
stroke.x += Math.cos(stroke.angle) * currentSpeed;
stroke.y += Math.sin(stroke.angle) * currentSpeed;
const progress = stroke.currentLength / stroke.maxLength;
const weightVar = Math.sin(progress * Math.PI) * 1.0;
const weight = Math.max(0.2, stroke.baseWeight + weightVar);
stroke.points.push({ x: stroke.x, y: stroke.y, weight });
stroke.currentLength++;
if (
stroke.currentLength >= stroke.maxLength ||
stroke.x < -50 || stroke.x > canvas.width + 50 ||
stroke.y < -50 || stroke.y > canvas.height + 50
) {
stroke.complete = true;
}
if (stroke.points.length > 1) {
for (let i = 1; i < stroke.points.length; i++) {
const p1 = stroke.points[i - 1];
const p2 = stroke.points[i];
const alpha = stroke.inkAlpha * (1 - i / stroke.points.length * 0.4) / 100;
const size = p2.weight * random(0.8, 1.2);
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = `rgba(25, 25, 25, ${alpha})`;
ctx.lineWidth = size;
ctx.lineCap = 'round';
ctx.stroke();
if (random(0, 1) < 0.3) {
ctx.beginPath();
ctx.arc(p2.x, p2.y, size * random(0.5, 1.5), 0, Math.PI * 2);
ctx.fillStyle = `rgba(25, 25, 25, ${alpha * 0.3})`;
ctx.fill();
}
}
}
}
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const breathScale = 1 + Math.sin(time * 0.3) * 0.015;
const radius = Math.min(canvas.width, canvas.height) * 0.18 * breathScale;
const gap = Math.PI * 0.18;
const startAngle = -Math.PI / 2 + time * 0.05;
ctx.beginPath();
for (let a = startAngle; a < startAngle + Math.PI * 2 - gap; a += 0.04) {
const noiseOffset = noise(Math.cos(a) * 2, Math.sin(a) * 2, time * 0.2) * 5 - 2.5;
const r = radius + noiseOffset;
const x = centerX + Math.cos(a) * r;
const y = centerY + Math.sin(a) * r;
if (a === startAngle) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.strokeStyle = 'rgba(25, 25, 25, 0.06)';
ctx.lineWidth = 0.8;
ctx.stroke();
animationRef.current = requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resize);
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 0,
}}
/>
);
}
export function LandingPage() {
const navigate = useNavigate();
return (
<div
style={{
minHeight: '100vh',
background: '#faf9f7',
position: 'relative',
overflow: 'hidden',
}}
>
<ZenBackground />
{/* 装饰性圆相 */}
<svg
style={{
position: 'fixed',
top: '8%',
right: '12%',
width: 200,
height: 200,
opacity: 0.05,
pointerEvents: 'none',
}}
viewBox="0 0 100 100"
>
<circle
cx="50"
cy="50"
r="45"
fill="none"
stroke="#1a1a1a"
strokeWidth="1"
strokeDasharray="250 30"
/>
</svg>
<svg
style={{
position: 'fixed',
bottom: '12%',
left: '10%',
width: 160,
height: 160,
opacity: 0.04,
pointerEvents: 'none',
}}
viewBox="0 0 100 100"
>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="#1a1a1a"
strokeWidth="0.8"
strokeDasharray="200 50"
/>
</svg>
<Container
size="sm"
style={{
position: 'relative',
zIndex: 1,
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
padding: '2rem',
}}
>
<Stack align="center" gap="lg">
{/* 产品图标 */}
<div
style={{
width: 90,
height: 90,
borderRadius: '50%',
background: 'rgba(26, 26, 26, 0.03)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '0.5rem',
}}
>
<img
src={iconUrl}
alt="掐日子"
style={{
width: 60,
height: 60,
borderRadius: '50%',
}}
/>
</div>
{/* 主标题 - 优化字体 */}
<Title
order={1}
style={{
fontWeight: 300,
fontSize: 'clamp(2.2rem, 7vw, 3.2rem)',
letterSpacing: '0.25em',
color: '#1a1a1a',
fontFamily: 'Noto Serif SC, Georgia, serif',
lineHeight: 1.3,
}}
>
</Title>
{/* 副标题 - 增强可读性 */}
<Text
size="lg"
style={{
letterSpacing: '0.35em',
color: '#444',
fontWeight: 400,
fontSize: '1rem',
fontFamily: 'Noto Serif SC, serif',
padding: '0.4rem 1rem',
background: 'rgba(250, 250, 250, 0.8)',
borderRadius: 2,
}}
>
AI ·
</Text>
{/* 描述文案 */}
<Text
size="xs"
style={{
color: '#777',
maxWidth: 320,
lineHeight: 2,
fontWeight: 300,
fontSize: '0.85rem',
}}
>
便
<br />
</Text>
<Group gap="md" mt="md">
<Button
size="sm"
onClick={() => navigate('/login')}
style={{
background: '#1a1a1a',
border: '1px solid #1a1a1a',
padding: '0 2rem',
fontWeight: 400,
letterSpacing: '0.15em',
borderRadius: 2,
}}
>
</Button>
<Button
size="sm"
variant="outline"
onClick={() => navigate('/register')}
style={{
borderColor: '#ccc',
color: '#1a1a1a',
padding: '0 2rem',
fontWeight: 400,
letterSpacing: '0.15em',
borderRadius: 2,
}}
>
</Button>
</Group>
<Group gap={40} mt={40} style={{ opacity: 0.6 }}>
<Stack gap={3} align="center">
<Text size="xs" fw={300} c="#444"></Text>
<Text size="xs" c="#666" style={{ letterSpacing: '0.1em' }}></Text>
</Stack>
<Stack gap={3} align="center">
<Text size="xs" fw={300} c="#444"></Text>
<Text size="xs" c="#666" style={{ letterSpacing: '0.1em' }}></Text>
</Stack>
<Stack gap={3} align="center">
<Text size="xs" fw={300} c="#444"></Text>
<Text size="xs" c="#666" style={{ letterSpacing: '0.1em' }}>AI</Text>
</Stack>
<Stack gap={3} align="center">
<Text size="xs" fw={300} c="#444"></Text>
<Text size="xs" c="#666" style={{ letterSpacing: '0.1em' }}>便</Text>
</Stack>
</Group>
</Stack>
</Container>
</div>
);
}