refactor: 优化首页布局和样式
- 简化首页布局,移除不必要的视觉元素 - 更新 `RightVisual.less` 和 `index.less`,调整样式和动画 - 在 `index.html` 中添加 Google Fonts 链接 - 更新 `index.tsx`,简化组件结构并优化内容展示dev_na
parent
d780278da4
commit
3cd1c48bce
|
|
@ -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.
|
|
@ -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%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue