refactor: 优化首页布局和样式

- 简化首页布局,移除不必要的视觉元素
- 更新 `RightVisual.less` 和 `index.less`,调整样式和动画
- 在 `index.html` 中添加 Google Fonts 链接
- 更新 `index.tsx`,简化组件结构并优化内容展示
dev_na
chenhao 2026-04-03 10:40:34 +08:00
parent d780278da4
commit 3cd1c48bce
6 changed files with 405 additions and 2032 deletions

View File

@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Calistoga&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>MeetingAI - 智能会议系统</title>
</head>
<body>

Binary file not shown.

View File

@ -1,154 +1,23 @@
.home-right-visual {
position: relative;
position: absolute;
top: 50%;
left: 35%; // 向左侧偏移(原本是 50%
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
perspective: 1200px;
z-index: 0;
z-index: 10;
pointer-events: none;
&__glow {
position: absolute;
border-radius: 50%;
filter: blur(50px);
z-index: -1;
pointer-events: none;
animation: pulseGlow 6s ease-in-out infinite alternate;
&--main {
width: 280px;
height: 280px;
background: rgba(103, 103, 244, 0.18);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&--secondary {
width: 180px;
height: 180px;
background: rgba(165, 214, 255, 0.25);
top: 15%;
right: 15%;
animation-delay: -2s;
}
}
&__soundwave {
position: relative;
width: 380px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
z-index: 2;
.home-right-visual__bar {
width: 5px;
height: 100%;
background: linear-gradient(180deg, rgba(165, 214, 255, 0.95) 0%, rgba(103, 103, 244, 0.95) 100%);
border-radius: 999px;
transform-origin: center;
/* The base transform uses the envelope to shape the bell curve */
transform: scaleY(calc(0.05 + 0.1 * var(--envelope)));
animation: soundwave-bounce var(--duration) ease-in-out infinite alternate;
/* Sweep delay creates the wave-like motion */
animation-delay: calc(-0.1s * var(--index));
box-shadow: 0 0 12px rgba(103, 103, 244, 0.25);
will-change: transform;
}
}
&__drop {
position: absolute;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 240, 255, 0.3) 100%);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow:
0 12px 40px rgba(103, 103, 244, 0.15),
inset 0 0 20px rgba(255, 255, 255, 0.95),
inset 4px 4px 10px rgba(255, 255, 255, 0.6);
animation: floatDrop 6s ease-in-out infinite;
transform-style: preserve-3d;
overflow: hidden;
&-inner {
position: absolute;
width: 150%;
height: 150%;
top: -25%;
left: -25%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8) 0%, transparent 60%);
pointer-events: none;
transform: rotate(-45deg);
}
&--1 {
width: 80px;
height: 80px;
top: 10%;
right: 12%;
animation-duration: 7s;
animation-name: floatDrop1;
}
&--2 {
width: 120px;
height: 110px;
bottom: -10%;
left: -5%;
animation-duration: 9s;
animation-delay: -4s;
animation-name: floatDrop3;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.6) 0%, rgba(180, 180, 255, 0.25) 100%);
}
&--3 {
width: 50px;
height: 50px;
top: -5%;
left: 35%;
animation-duration: 8s;
animation-delay: -3s;
animation-name: floatDrop5;
}
&__video {
width: 800px;
max-width: none;
height: auto;
object-fit: cover;
// 轻微的阴影可以让视频更好地融入背景
filter: drop-shadow(0 15px 35px rgba(99, 102, 241, 0.15));
// webm 如果带透明通道,正常显示即可,不再需要 mix-blend-mode 干扰
}
}
@keyframes soundwave-bounce {
0% {
transform: scaleY(calc(0.05 + 0.1 * var(--envelope)));
opacity: 0.5;
}
100% {
transform: scaleY(calc(0.1 + 0.85 * var(--envelope)));
opacity: 1;
filter: brightness(1.2);
}
}
@keyframes pulseGlow {
0% { transform: translate(-50%, -50%) scale(0.85); opacity: 0.5; }
100% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
}
@keyframes floatDrop1 {
0%, 100% { transform: translateY(0) rotate(0deg); border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; }
33% { transform: translateY(-22px) rotate(12deg); border-radius: 50% 50% 60% 40% / 50% 40% 70% 40%; }
66% { transform: translateY(12px) rotate(-8deg); border-radius: 30% 70% 50% 50% / 30% 60% 40% 60%; }
}
@keyframes floatDrop3 {
0%, 100% { transform: translateY(0) scale(1); border-radius: 50% 50% 30% 70% / 60% 40% 60% 40%; }
50% { transform: translateY(-28px) scale(1.02); border-radius: 30% 70% 50% 50% / 40% 60% 40% 60%; }
}
@keyframes floatDrop5 {
0%, 100% { transform: translateY(0) scale(1) rotate(0deg); border-radius: 45% 55% 65% 35% / 45% 45% 55% 55%; }
50% { transform: translateY(20px) scale(0.95) rotate(-10deg); border-radius: 55% 45% 35% 65% / 55% 55% 45% 45%; }
}

View File

@ -2,57 +2,16 @@ import React from "react";
import "./RightVisual.less";
export default function RightVisual() {
const BAR_COUNT = 48;
// Calculate a Gaussian envelope for the soundwave so the center is tallest
const getEnvelope = (i: number) => {
const center = BAR_COUNT / 2;
const x = (i - center) / (center * 0.8);
// Gaussian bell curve
const envelope = Math.exp(-Math.pow(x, 2));
return Math.max(0.05, envelope);
};
// Deterministic pseudo-random duration for a more organic, less rigid feel
const getDuration = (i: number) => {
return 0.9 + (Math.sin(i * 76543) * 0.5 + 0.5) * 0.6; // between 0.9s and 1.5s
};
return (
<div className="home-right-visual" aria-hidden="true">
{/* Soundwave Animation */}
<div className="home-right-visual__soundwave">
{Array.from({ length: BAR_COUNT }).map((_, i) => {
const envelope = getEnvelope(i);
const duration = getDuration(i);
return (
<div
key={i}
className="home-right-visual__bar"
style={{
"--index": i,
"--envelope": envelope,
"--duration": `${duration}s`,
} as React.CSSProperties}
/>
);
})}
</div>
{/* Floating Glass Droplets for ambient feel */}
<div className="home-right-visual__drop home-right-visual__drop--1">
<div className="home-right-visual__drop-inner" />
</div>
<div className="home-right-visual__drop home-right-visual__drop--2">
<div className="home-right-visual__drop-inner" />
</div>
<div className="home-right-visual__drop home-right-visual__drop--3">
<div className="home-right-visual__drop-inner" />
</div>
{/* Light Glare / Glows */}
<div className="home-right-visual__glow home-right-visual__glow--main" />
<div className="home-right-visual__glow home-right-visual__glow--secondary" />
<video
className="home-right-visual__video"
src="/bg-small.7a2ab458.webm"
autoPlay
loop
muted
playsInline
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import {
AudioOutlined,
ArrowRightOutlined,
CustomerServiceOutlined,
PlayCircleOutlined,
RadarChartOutlined,
SoundOutlined,
VideoCameraAddOutlined
} from "@ant-design/icons";
import { Button, Empty, Skeleton, Tag, Typography } from "antd";
@ -20,7 +17,6 @@ const { Text, Title } = Typography;
type QuickEntry = {
title: string;
badge: string;
icon: React.ReactNode;
description: string[];
accent: string;
@ -96,18 +92,16 @@ export default function HomePage() {
const quickEntries = useMemo<QuickEntry[]>(
() => [
{
title: "开启实时会议",
badge: "实时协作",
title: "开启实时记录",
icon: <AudioOutlined />,
description: ["边开会边转写,自动沉淀结构化纪要", "适合讨论会、评审会、客户沟通"],
description: ["实时语音转文字", "同步翻译,智能总结要点"],
accent: "violet",
onClick: () => navigate("/meeting-live-create")
},
{
title: "上传音频",
badge: "离线整理",
title: "上传音视频",
icon: <VideoCameraAddOutlined />,
description: ["上传录音文件,区分发言人并整理内容", "适合访谈录音、培训音频、课程复盘"],
description: ["音视频转文字", "区分发言人,一键导出"],
accent: "cyan",
onClick: () => navigate("/meetings?create=true")
}
@ -118,161 +112,87 @@ export default function HomePage() {
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
return (
<div className="home-landing">
<div className="home-landing__halo home-landing__halo--large" />
<div className="home-landing__halo home-landing__halo--small" />
{/* Replaced ambient field with dynamic right visual */}
<main className="home-container">
{/* Massive Abstract Background Sphere matching the design/img.png */}
<div className="home-bg-visual" aria-hidden="true">
<div className="home-bg-sphere" />
<RightVisual />
</div>
<section className="home-landing__hero">
<div className="home-landing__copy">
<div className="home-landing__eyebrow">
<RadarChartOutlined />
<span>iMeeting </span>
</div>
<Title level={1} className="home-landing__title">
<span> </span>
<div className="home-content-wrapper">
<header className="home-hero">
<Title level={1} className="home-title">
<span className="home-title-accent"></span>
</Title>
<div className="home-landing__status">
<div className="home-landing__status-item">
<CustomerServiceOutlined />
<span></span>
</div>
<div className="home-landing__status-item">
<SoundOutlined />
<span>线</span>
</div>
</div>
</div>
<div className="home-landing__visual" aria-hidden="true">
<RightVisual />
</div>
</section>
<section className="home-landing__entry-stage">
<div className="home-landing__entry-grid home-landing__entry-grid--two">
{quickEntries.map((entry) => (
<article
key={entry.title}
className={`home-entry-card home-entry-card--${entry.accent}`}
onClick={entry.onClick}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
entry.onClick();
}
}}
>
<div className="home-entry-card__shine" aria-hidden="true" />
<div className="home-entry-card__topline">
<div className="home-entry-card__icon">{entry.icon}</div>
<div className="home-entry-card__badge">{entry.badge}</div>
</div>
<Title level={3}>{entry.title}</Title>
<div className="home-entry-card__content">
{entry.description.map((line) => (
<Text key={line} className="home-entry-card__line">
{line}
</Text>
))}
</div>
<div className="home-entry-card__media" aria-hidden="true">
<div className="home-entry-card__track">
<span />
<span />
<span />
<span />
<span />
<span />
<span />
<span />
<span />
</div>
<div className="home-entry-card__pulse" />
</div>
<div className="home-entry-card__cta" aria-hidden="true">
<span></span>
<ArrowRightOutlined />
</div>
</article>
))}
</div>
<div className="home-landing__soundstage" aria-hidden="true">
<div className="home-landing__soundstage-head">
<span className="home-landing__soundstage-kicker">Workflow Lens</span>
<div className="home-landing__soundstage-copy">
<span></span>
<span></span>
</div>
</div>
<div className="home-landing__soundstage-pills">
<span></span>
<span></span>
<span></span>
</div>
<div className="home-landing__soundstage-trace">
<span />
<span />
<span />
<span />
<span />
<span />
<span />
</div>
</div>
</section>
<section className="home-landing__recent">
<div className="home-landing__section-head">
<Title level={3}></Title>
<Button type="link" onClick={() => navigate("/meetings")}>
</Button>
</div>
{loading ? (
<div className="home-landing__recent-grid">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="home-recent-card">
<Skeleton active paragraph={{ rows: 3 }} title={{ width: "70%" }} />
</div>
))}
</div>
) : recentCards.length ? (
<div className="home-landing__recent-grid">
{recentCards.map((card, index) => (
<article
key={card.id}
className="home-recent-card"
onClick={() => typeof card.id === "number" && navigate(`/meetings/${card.id}`)}
<div className="home-quick-actions">
{quickEntries.map((entry) => (
<div
className={`home-action-item home-action-item--${entry.accent}`}
onClick={entry.onClick}
key={entry.title}
>
<div className="home-recent-card__pin" aria-hidden="true" />
<div className="home-recent-card__head">
<Title level={4}>{card.title}</Title>
<PlayCircleOutlined />
<div className="home-action-icon-wrapper">
<div className="home-action-icon">{entry.icon}</div>
<div className="home-action-icon-glow" />
</div>
<div className="home-recent-card__tags">
{card.tags.map((tag) => (
<Tag key={`${card.id}-${tag}-${index}`}>{tag}</Tag>
<Title level={3} className="home-action-title">{entry.title}</Title>
<div className="home-action-desc">
{entry.description.map((line) => (
<Text key={line} className="home-action-line">
{line}
</Text>
))}
</div>
<div className="home-recent-card__foot">
<span>{card.duration}</span>
<span>{card.time}</span>
</div>
</article>
</div>
))}
</div>
) : (
<div className="home-landing__empty">
<Empty description="暂无最近记录" />
</header>
<section className="home-recent-section">
<div className="home-section-header">
<Title level={3}> / </Title>
<Button type="link" onClick={() => navigate("/meetings")} className="home-view-all">
<ArrowRightOutlined />
</Button>
</div>
)}
</section>
</div>
{loading ? (
<div className="home-recent-grid">
{[...Array(3)].map((_, i) => (
<div key={i} className="home-recent-skeleton">
<Skeleton active paragraph={{ rows: 2 }} title={{ width: "60%" }} />
</div>
))}
</div>
) : recentCards.length ? (
<div className="home-recent-grid">
{recentCards.map((card) => (
<article
key={card.id}
className="home-recent-card"
onClick={() => typeof card.id === "number" && navigate(`/meetings/${card.id}`)}
>
<div className="home-recent-card-thumbnail">
<PlayCircleOutlined className="home-recent-card-play-icon" />
</div>
<div className="home-recent-card-content">
<Title level={4} className="home-recent-card-title">{card.title}</Title>
<div className="home-recent-card-footer">
<span className="home-recent-card-duration">{card.duration}</span>
<span className="home-recent-card-action"></span>
</div>
</div>
</article>
))}
</div>
) : (
<div className="home-empty-state">
<Empty description="暂无最近记录" />
</div>
)}
</section>
</div>
</main>
);
}
}