- LandingPage: 全新水墨晕染算法背景,循环墨迹动画 - 登录/注册页: 禅意黑白极简风格 - HomePage: 三栏布局优化,标题颜色语义化 - 纪念日组件: 分类逻辑优化,颜色语义统一 - 提醒组件: 分组标题颜色优化,逾期提示更醒目 - 修复农历日期边界问题(29/30天月份) - 添加 lunar-javascript 类型声明 - 清理未使用的导入和代码
419 lines
12 KiB
TypeScript
419 lines
12 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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 绘制圆相(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>
|
||
);
|
||
}
|