imeeting/frontend/src/pages/home/index.tsx

278 lines
8.9 KiB
TypeScript
Raw Normal View History

import { useEffect, useMemo, useState } from "react";
import {
AudioOutlined,
ArrowRightOutlined,
VideoCameraOutlined,
VideoCameraAddOutlined
} from "@ant-design/icons";
import { Button, Empty, Skeleton, Tag, Typography } from "antd";
import { useNavigate } from "react-router-dom";
import dayjs from "dayjs";
import { getRecentTasks } from "@/api/business/dashboard";
import type { MeetingVO } from "@/api/business/meeting";
import "./index.less";
import RightVisual from "./RightVisual";
const { Text, Title } = Typography;
type QuickEntry = {
title: string;
icon: React.ReactNode;
description: string[];
accent: string;
badge: string;
onClick: () => void;
};
type RecentCard = {
id: number | string;
title: string;
duration: string;
time: string;
tags: string[];
};
const RECENT_CARD_READ_STORAGE_KEY = "home_recent_card_read_ids";
const fallbackRecentCards: RecentCard[] = [
{
id: "sample-1",
title: "2026-03-25 16:05 记录",
duration: "01:10",
time: "今天 16:05",
tags: ["发言人", "降噪", "速度", "模仿", "暂停"]
},
{
id: "sample-2",
title: "【示例】开会用通义听悟,高效又省心",
duration: "02:14",
time: "2026-03-24 11:04",
tags: ["会议日程", "笔记", "发言人", "协同", "纪要"]
},
{
id: "sample-3",
title: "【示例】上课用通义听悟,学习效率 UPUP",
duration: "02:01",
time: "2026-03-23 11:04",
tags: ["转写", "笔记", "学习", "教学音频", "课程音频"]
}
];
function buildRecentCards(tasks: MeetingVO[]): RecentCard[] {
if (!tasks.length) {
return fallbackRecentCards;
}
return tasks.slice(0, 3).map((task, index) => ({
id: task.id,
title: task.title,
duration: `0${index + 1}:${10 + index * 12}`,
time: dayjs(task.meetingTime || task.createdAt).format("YYYY-MM-DD HH:mm"),
tags: task.tags?.split(",").filter(Boolean).slice(0, 5) || []
}));
}
export default function HomePage() {
const navigate = useNavigate();
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
const [loading, setLoading] = useState(true);
const [readCardIds, setReadCardIds] = useState<string[]>(() => {
if (typeof window === "undefined") {
return [];
}
try {
const rawValue = window.localStorage.getItem(RECENT_CARD_READ_STORAGE_KEY);
if (!rawValue) {
return [];
}
const parsed = JSON.parse(rawValue);
return Array.isArray(parsed) ? parsed.map(String) : [];
} catch {
return [];
}
});
const [wordIndex, setWordIndex] = useState(0);
const ROTATING_WORDS = useMemo(() => ["都有迹可循", "都能被听见", "都值得记录", "都清晰可见"], []);
useEffect(() => {
const interval = setInterval(() => {
setWordIndex((prev) => (prev + 1) % ROTATING_WORDS.length);
}, 3000);
return () => clearInterval(interval);
}, [ROTATING_WORDS.length]);
useEffect(() => {
const fetchRecentTasks = async () => {
try {
const response = await getRecentTasks();
const payload: any = (response as any).data || response;
setRecentTasks(payload?.data || payload || []);
} catch (error) {
console.error("Home recent tasks load failed", error);
} finally {
setLoading(false);
}
};
void fetchRecentTasks();
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(RECENT_CARD_READ_STORAGE_KEY, JSON.stringify(readCardIds));
}, [readCardIds]);
const quickEntries = useMemo<QuickEntry[]>(
() => [
{
title: "开启实时记录",
icon: <AudioOutlined />,
description: ["实时语音转文字", "同步翻译,智能总结要点"],
accent: "violet",
badge: "会议神器",
onClick: () => navigate("/meetings?action=create&type=realtime")
},
{
title: "上传音视频",
icon: <VideoCameraAddOutlined />,
description: ["音视频转文字", "区分发言人,一键导出"],
accent: "cyan",
badge: "iMeeting",
onClick: () => navigate("/meetings?create=true")
}
],
[navigate]
);
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
const handleRecentCardClick = (card: RecentCard) => {
const cardId = String(card.id);
setReadCardIds((prev) => (prev.includes(cardId) ? prev : [...prev, cardId]));
if (typeof card.id === "number") {
navigate(`/meetings/${card.id}`);
}
};
return (
<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>
<div className="home-content-wrapper">
<header className="home-hero">
<Title level={1} className="home-title">
<span className="home-title-accent-wrapper">
<span
className="home-title-accent-scroller"
style={{ transform: `translateY(-${wordIndex * 1.2}em)` }}
>
{ROTATING_WORDS.map((word) => (
<span key={word} className="home-title-accent">
{word}
</span>
))}
</span>
</span>
</Title>
<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-action-badge">{entry.badge}</div>
<div className="home-action-icon-wrapper">
<div className="home-action-icon">{entry.icon}</div>
<div className="home-action-icon-glow" />
<div className="home-action-icon-circle" />
</div>
<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>
))}
</div>
</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>
{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={() => handleRecentCardClick(card)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleRecentCardClick(card);
}
}}
role="button"
tabIndex={0}
>
{!readCardIds.includes(String(card.id)) && <span className="home-recent-card-dot" />}
<div className="home-recent-card-head">
<Title level={4} className="home-recent-card-title">{card.title}</Title>
<div className="home-recent-card-icon">
<VideoCameraOutlined className="home-recent-card-play-icon" />
</div>
</div>
<div className="home-recent-card-tags">
{card.tags.slice(0, 4).map((tag) => (
<Tag key={`${card.id}-${tag}`} className="home-recent-card-tag" bordered={false}>
{tag}
</Tag>
))}
</div>
<div className="home-recent-card-footer">
<span className="home-recent-card-duration">{card.duration}</span>
<span className="home-recent-card-time">{card.time}</span>
</div>
</article>
))}
</div>
) : (
<div className="home-empty-state">
<Empty description="暂无最近记录" />
</div>
)}
</section>
</div>
</main>
);
}