Files
omniai-web/src/features/home/WelcomeSplash.tsx
T
stringadmin 4ed02aaad5 feat: 错误监控面板、生成通知、社区搜索、任务队列优化
- AdminMonitor: admin用户可见的客户端错误实时监控面板,右下角浮窗
- generationNotifier: 生成完成浏览器通知 + 站内Toast
- CommunityPage: 新增搜索框,标题/描述/标签模糊匹配,防抖300ms
- App.tsx: 全局unhandled error/rejection监听上报
- WorkbenchPage: 任务并发提示改为显示当前任务数
- serverConnection: 后端client-errors路由注册
- WelcomeSplash: 欢迎按钮全程显示

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-06-03 02:01:21 +08:00

123 lines
3.8 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
interface WelcomeSplashProps {
onEnter: () => void;
}
const MATRIX_CHARS =
"01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+[]{};:?/\\|~`";
const prefersReducedMotion = typeof window !== "undefined"
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
: false;
export default function WelcomeSplash({ onEnter }: WelcomeSplashProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef(0);
const [showWelcome, setShowWelcome] = useState(true);
const [exiting, setExiting] = useState(false);
const handleEnter = useCallback(() => {
setExiting(true);
setTimeout(onEnter, prefersReducedMotion ? 0 : 700);
}, [onEnter]);
useEffect(() => {
const timer = setTimeout(() => setShowWelcome(true), prefersReducedMotion ? 0 : 6000);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (prefersReducedMotion) {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = "rgba(0, 0, 0, 0.85)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
return;
}
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const fontSize = width > 1200 ? 20 : width > 768 ? 18 : 14;
const columns = Math.floor(width / fontSize);
const drops: number[] = [];
for (let i = 0; i < columns; i++) {
drops[i] = Math.random() * -(height / fontSize);
}
function draw() {
ctx!.fillStyle = "rgba(0, 0, 0, 0.07)";
ctx!.fillRect(0, 0, width, height);
ctx!.font = `${fontSize}px "Courier New", monospace`;
ctx!.textAlign = "center";
for (let i = 0; i < drops.length; i++) {
const char = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
const intensity = Math.min(0.6 + (drops[i] / (height / fontSize)) * 0.4, 1);
const g = Math.floor(100 + 155 * intensity);
ctx!.fillStyle = `rgba(40, ${g}, 60, 0.9)`;
ctx!.shadowBlur = 6;
ctx!.shadowColor = "#0f0";
const x = i * fontSize + fontSize / 2;
const y = drops[i] * fontSize;
ctx!.fillText(char, x, y);
ctx!.shadowBlur = 0;
drops[i] += 0.7 + Math.random() * 1.4;
if (drops[i] * fontSize > height && Math.random() > 0.96) {
drops[i] = 0;
}
}
}
function animate() {
draw();
rafRef.current = requestAnimationFrame(animate);
}
animate();
function onResize() {
width = window.innerWidth;
height = window.innerHeight;
canvas!.width = width;
canvas!.height = height;
}
window.addEventListener("resize", onResize);
return () => {
cancelAnimationFrame(rafRef.current);
window.removeEventListener("resize", onResize);
};
}, []);
return (
<div className={`welcome-splash${exiting ? " is-exiting" : ""}`}>
<canvas ref={canvasRef} className="welcome-splash__canvas" />
<div className="welcome-splash__ambient" />
<div className="welcome-splash__hero">
<h1 className="welcome-splash__title">OmniAI</h1>
<p className="welcome-splash__subtitle">The future with OmniAI</p>
{showWelcome && (
<button
type="button"
className="welcome-splash__enter"
onClick={handleEnter}
>
</button>
)}
</div>
</div>
);
}