Initial commit: OmniAI Web Frontend

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:38:01 +08:00
commit bedee3ba8d
183 changed files with 94805 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
import {
ArrowRightOutlined,
DashboardOutlined,
FileSearchOutlined,
PlayCircleOutlined,
PlusOutlined,
ShoppingOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import WelcomeSplash from "./WelcomeSplash";
import featureEcommerceImage from "../../assets/home-features/feature-ecommerce.jpg";
import featureScriptImage from "../../assets/home-features/feature-script.jpg";
import featureTokenImage from "../../assets/home-features/feature-token.jpg";
import heroImage1 from "../../../projects/public/hero-1.png";
import heroImage2 from "../../../projects/public/hero-2.png";
import heroImage3 from "../../../projects/public/hero-3.png";
interface HomePageProps {
onOpenGenerate: () => void;
onOpenCanvas?: () => void;
onOpenEcommerce: () => void;
onOpenScriptReview?: () => void;
onOpenTokenMonitor?: () => void;
}
const HOME_BACKGROUND_VIDEO = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/%E6%A0%B7%E7%89%87.mp4";
const HOME_CAROUSEL_IMAGES = [
{ imageUrl: heroImage1, title: "灵感生成" },
{ imageUrl: heroImage2, title: "画布创作" },
{ imageUrl: heroImage3, title: "商业素材" },
];
const HOME_FEATURES = [
{
key: "script",
eyebrow: "Script Review",
title: "剧本智能测评",
description: "用六维雷达评分拆解剧本质量,从结构、节奏、人物到商业潜力给出可执行的优化路径。",
imageUrl: featureScriptImage,
actionLabel: "开始测评",
icon: <FileSearchOutlined />,
stats: ["六维评分", "质量量化", "逐项优化"],
},
{
key: "token",
eyebrow: "Team Tokens",
title: "团队 Token 监控",
description: "实时追踪团队 Token 消耗、项目分布和成员使用情况,让预算、配额和成本都能被清楚管理。",
imageUrl: featureTokenImage,
actionLabel: "查看面板",
icon: <DashboardOutlined />,
stats: ["实时概览", "成员明细", "成本分析"],
},
{
key: "ecommerce",
eyebrow: "AI Commerce",
title: "AI 电商生成",
description: "上传产品图后自动生成主图、场景图、详情素材和短视频方案,快速覆盖多平台商品视觉。",
imageUrl: featureEcommerceImage,
actionLabel: "开始生成",
icon: <ShoppingOutlined />,
stats: ["多场景", "多角度", "批量输出"],
},
];
const HOME_EXPERIENCE_POINTS = [
{ label: "生成", meta: "图像 / 视频", tone: "green" },
{ label: "测评", meta: "剧本质量", tone: "cyan" },
{ label: "成本", meta: "Token 用量", tone: "violet" },
{ label: "电商", meta: "商品视觉", tone: "amber" },
];
const HOME_CAROUSEL_SLOTS = [-4, -3, -2, -1, 0, 1, 2, 3, 4];
const HOME_CAROUSEL_TRANSITION_MS = 860;
interface HomeCarouselMotion {
direction: number;
progress: 0 | 1;
}
function getPositiveModulo(value: number, length: number) {
return ((value % length) + length) % length;
}
function getHomeCarouselCardStyle(offset: number): CSSProperties {
const depth = Math.abs(offset);
const direction = Math.sign(offset);
const isActive = depth === 0;
const xByDepth = [0, 286, 456, 610, 735, 840];
const yByDepth = [8, -2, -8, -13, -18, -24];
const scaleByDepth = [1, 0.98, 0.94, 0.91, 0.88, 0.84];
const x = direction * (xByDepth[depth] ?? xByDepth[xByDepth.length - 1]!);
const y = yByDepth[depth] ?? yByDepth[yByDepth.length - 1]!;
const z = isActive ? 90 : 28 - depth;
const scale = scaleByDepth[depth] ?? scaleByDepth[scaleByDepth.length - 1]!;
return {
"--apple-card-offset": offset,
"--apple-card-depth": depth,
"--apple-card-z": 80 - depth,
"--apple-card-x": `${x}px`,
"--apple-card-y": `${y}px`,
"--apple-card-z-offset": `${z}px`,
"--apple-card-rotate-y": "0deg",
"--apple-card-rotate-z": "0deg",
"--apple-card-scale": String(scale),
"--apple-card-opacity": String(depth > 4 ? 0 : 1),
} as CSSProperties;
}
function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor }: HomePageProps) {
const [splashDismissed, setSplashDismissed] = useState(() => sessionStorage.getItem("omniai:splash-seen") === "1");
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
const [carouselMotion, setCarouselMotion] = useState<HomeCarouselMotion | null>(null);
const [carouselIsResetting, setCarouselIsResetting] = useState(false);
const carouselFrameRef = useRef<number | null>(null);
const carouselResetFrameRef = useRef<number | null>(null);
const carouselTimerRef = useRef<number | null>(null);
const carouselSlotOffsets = useMemo(() => {
const direction = carouselMotion?.direction ?? 0;
const minSlot = HOME_CAROUSEL_SLOTS[0]! + Math.min(direction, 0);
const maxSlot = HOME_CAROUSEL_SLOTS[HOME_CAROUSEL_SLOTS.length - 1]! + Math.max(direction, 0);
return Array.from({ length: maxSlot - minSlot + 1 }, (_, index) => minSlot + index);
}, [carouselMotion?.direction]);
const startCarouselShift = useCallback(
(rawDirection: number) => {
const direction = Math.sign(rawDirection);
if (!direction || HOME_CAROUSEL_IMAGES.length <= 1 || carouselMotion) return;
if (carouselFrameRef.current !== null) {
window.cancelAnimationFrame(carouselFrameRef.current);
}
if (carouselTimerRef.current !== null) {
window.clearTimeout(carouselTimerRef.current);
}
if (carouselResetFrameRef.current !== null) {
window.cancelAnimationFrame(carouselResetFrameRef.current);
}
setCarouselIsResetting(false);
setCarouselMotion({ direction, progress: 0 });
carouselFrameRef.current = window.requestAnimationFrame(() => {
carouselFrameRef.current = window.requestAnimationFrame(() => {
setCarouselMotion((current) => (current?.direction === direction ? { direction, progress: 1 } : current));
});
});
carouselTimerRef.current = window.setTimeout(() => {
setCarouselIsResetting(true);
setActiveSlideIndex((current) => getPositiveModulo(current + direction, HOME_CAROUSEL_IMAGES.length));
setCarouselMotion(null);
carouselResetFrameRef.current = window.requestAnimationFrame(() => {
carouselResetFrameRef.current = window.requestAnimationFrame(() => {
setCarouselIsResetting(false);
});
});
}, HOME_CAROUSEL_TRANSITION_MS);
},
[carouselMotion],
);
useEffect(() => {
const timerId = window.setInterval(() => {
startCarouselShift(-1);
}, 2600);
return () => window.clearInterval(timerId);
}, [startCarouselShift]);
useEffect(
() => () => {
if (carouselFrameRef.current !== null) {
window.cancelAnimationFrame(carouselFrameRef.current);
}
if (carouselTimerRef.current !== null) {
window.clearTimeout(carouselTimerRef.current);
}
if (carouselResetFrameRef.current !== null) {
window.cancelAnimationFrame(carouselResetFrameRef.current);
}
},
[],
);
const handleFeatureOpen = (featureKey: string) => {
if (featureKey === "script") {
(onOpenScriptReview ?? onOpenGenerate)();
return;
}
if (featureKey === "token") {
(onOpenTokenMonitor ?? onOpenGenerate)();
return;
}
if (featureKey === "ecommerce") {
onOpenEcommerce();
return;
}
onOpenGenerate();
};
return (
<>
{!splashDismissed && (
<WelcomeSplash
onEnter={() => {
sessionStorage.setItem("omniai:splash-seen", "1");
setSplashDismissed(true);
}}
/>
)}
<section className="omni-home page-motion">
{splashDismissed && (
<video
className="omni-home__bg-video"
src={HOME_BACKGROUND_VIDEO}
autoPlay
muted
loop
playsInline
preload="metadata"
aria-hidden="true"
/>
)}
<div className="omni-home__scrim" aria-hidden="true" />
<div className="omni-home__shell">
<section className="omni-home__hero" aria-label="OmniAI 首页">
<div className="omni-home__copy">
<h1>OmniAI </h1>
<p>绿</p>
</div>
<div className={`omni-home__carousel${carouselIsResetting ? " is-resetting" : ""}`} aria-label="创作案例轮播">
<div className="omni-home__carousel-stage">
<div className="omni-home__carousel-deck">
{carouselSlotOffsets.map((slotOffset) => {
const itemIndex = getPositiveModulo(activeSlideIndex + slotOffset, HOME_CAROUSEL_IMAGES.length);
const slide = HOME_CAROUSEL_IMAGES[itemIndex];
const visualOffset = slotOffset - (carouselMotion?.direction ?? 0) * (carouselMotion?.progress ?? 0);
const isActive = visualOffset === 0;
if (!slide) return null;
return (
<button
key={slotOffset}
type="button"
className={`omni-home__carousel-card${isActive ? " is-active" : ""}`}
style={getHomeCarouselCardStyle(visualOffset)}
aria-label={`切换到${slide.title}`}
aria-pressed={isActive}
onClick={() => {
if (!isActive) startCarouselShift(slotOffset);
}}
>
<img src={slide.imageUrl} alt={slide.title} />
</button>
);
})}
</div>
</div>
</div>
<div className="omni-home__actions" aria-label="首页入口">
<button type="button" className="omni-home__entry" onClick={onOpenGenerate}>
<PlusOutlined />
<span></span>
</button>
<button type="button" className="omni-home__entry omni-home__entry--primary" onClick={onOpenGenerate}>
<PlayCircleOutlined />
<span></span>
</button>
<button type="button" className="omni-home__entry" onClick={onOpenEcommerce}>
<ShoppingOutlined />
<span></span>
</button>
</div>
</section>
</div>
<main className="omni-home__feature-pages" aria-label="OmniAI 功能介绍">
{HOME_FEATURES.map((feature, index) => (
<section key={feature.key} className={`omni-home__feature-page is-${feature.key}${index % 2 ? " is-alt" : ""}`}>
<div className="omni-home__feature-copy">
<span>
{feature.icon}
{feature.eyebrow}
</span>
<h2>{feature.title}</h2>
<p>{feature.description}</p>
<button type="button" onClick={() => handleFeatureOpen(feature.key)}>
{feature.actionLabel}
<ArrowRightOutlined />
</button>
</div>
<div className="omni-home__feature-visual" aria-hidden="true">
<img src={feature.imageUrl} alt="" />
</div>
<div className="omni-home__feature-stats" aria-hidden="true">
{feature.stats.map((item) => (
<span key={item}>{item}</span>
))}
</div>
</section>
))}
<section className="omni-home__experience" aria-label="点击体验">
<div className="omni-home__experience-copy">
<span>
<ThunderboltOutlined />
Click To Experience
</span>
<h2> OmniAI</h2>
<p></p>
</div>
<div className="omni-home__experience-visual" aria-hidden="true">
<div className="omni-home__experience-line is-top" />
<div className="omni-home__experience-line is-bottom" />
<div className="omni-home__experience-routes">
{HOME_EXPERIENCE_POINTS.map((point) => (
<span key={point.label} className={`omni-home__experience-route is-${point.tone}`}>
<b>{point.label}</b>
<small>{point.meta}</small>
</span>
))}
</div>
</div>
<div className="omni-home__experience-actions">
<button type="button" className="is-primary" onClick={onOpenGenerate}>
<PlayCircleOutlined />
</button>
<button type="button" onClick={onOpenEcommerce}>
<ShoppingOutlined />
</button>
</div>
</section>
</main>
</section>
</>
);
}
export default HomePage;
+106
View File
@@ -0,0 +1,106 @@
import { useCallback, useEffect, useRef, useState } from "react";
interface WelcomeSplashProps {
onEnter: () => void;
}
const MATRIX_CHARS =
"01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+[]{};:?/\\|~`";
export default function WelcomeSplash({ onEnter }: WelcomeSplashProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef(0);
const [showWelcome, setShowWelcome] = useState(false);
const [exiting, setExiting] = useState(false);
const handleEnter = useCallback(() => {
setExiting(true);
setTimeout(onEnter, 700);
}, [onEnter]);
useEffect(() => {
const timer = setTimeout(() => setShowWelcome(true), 6000);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
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>
);
}