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

278 lines
9.0 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,
CustomerServiceOutlined,
PlayCircleOutlined,
RadarChartOutlined,
SoundOutlined,
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;
badge: string;
icon: React.ReactNode;
description: string[];
accent: string;
onClick: () => void;
};
type RecentCard = {
id: number | string;
title: string;
duration: string;
time: string;
tags: string[];
};
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);
useEffect(() => {
const fetchRecentTasks = async () => {
try {
const response = await getRecentTasks();
setRecentTasks(response.data.data || []);
} catch (error) {
console.error("Home recent tasks load failed", error);
} finally {
setLoading(false);
}
};
void fetchRecentTasks();
}, []);
const quickEntries = useMemo<QuickEntry[]>(
() => [
{
title: "开启实时会议",
badge: "实时协作",
icon: <AudioOutlined />,
description: ["边开会边转写,自动沉淀结构化纪要", "适合讨论会、评审会、客户沟通"],
accent: "violet",
onClick: () => navigate("/meeting-live-create")
},
{
title: "上传音频",
badge: "离线整理",
icon: <VideoCameraAddOutlined />,
description: ["上传录音文件,区分发言人并整理内容", "适合访谈录音、培训音频、课程复盘"],
accent: "cyan",
onClick: () => navigate("/meetings?create=true")
}
],
[navigate]
);
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 */}
<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>
</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-recent-card__pin" aria-hidden="true" />
<div className="home-recent-card__head">
<Title level={4}>{card.title}</Title>
<PlayCircleOutlined />
</div>
<div className="home-recent-card__tags">
{card.tags.map((tag) => (
<Tag key={`${card.id}-${tag}-${index}`}>{tag}</Tag>
))}
</div>
<div className="home-recent-card__foot">
<span>{card.duration}</span>
<span>{card.time}</span>
</div>
</article>
))}
</div>
) : (
<div className="home-landing__empty">
<Empty description="暂无最近记录" />
</div>
)}
</section>
</div>
);
}