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

278 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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>
);
}