qia-client/src/pages/LandingPage.tsx
ddshi 250c05e85e feat: 禅意设计风格重构与体验优化
- LandingPage: 全新水墨晕染算法背景,循环墨迹动画
- 登录/注册页: 禅意黑白极简风格
- HomePage: 三栏布局优化,标题颜色语义化
- 纪念日组件: 分类逻辑优化,颜色语义统一
- 提醒组件: 分组标题颜色优化,逾期提示更醒目
- 修复农历日期边界问题(29/30天月份)
- 添加 lunar-javascript 类型声明
- 清理未使用的导入和代码
2026-02-02 15:26:47 +08:00

419 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}
}
}
// 绘制圆相Ensō- 缓慢呼吸
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, serif',
}}
>
</Title>
<Text
size="sm"
style={{
letterSpacing: '0.35em',
color: '#888',
fontWeight: 300,
}}
>
AI ·
</Text>
<Text
size="xs"
style={{
color: '#999',
maxWidth: 300,
lineHeight: 1.9,
fontWeight: 300,
}}
>
便
<br />
</Text>
<Group gap="md" mt="lg">
<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={50} style={{ opacity: 0.7 }}>
<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>
);
}