304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
|
|
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";
|
|||
|
|
|
|||
|
|
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("/meeting-create")
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
[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" />
|
|||
|
|
|
|||
|
|
<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">
|
|||
|
|
<div className="home-landing__visual-frame">
|
|||
|
|
<div className="home-landing__visual-glow home-landing__visual-glow--primary" />
|
|||
|
|
<div className="home-landing__visual-glow home-landing__visual-glow--secondary" />
|
|||
|
|
<div className="home-landing__visual-grid" />
|
|||
|
|
<div className="home-landing__visual-beam" />
|
|||
|
|
<div className="home-landing__visual-radar" />
|
|||
|
|
<div className="home-landing__visual-pulse home-landing__visual-pulse--one" />
|
|||
|
|
<div className="home-landing__visual-pulse home-landing__visual-pulse--two" />
|
|||
|
|
<div className="home-landing__visual-chip home-landing__visual-chip--top">Live capture</div>
|
|||
|
|
<div className="home-landing__visual-chip home-landing__visual-chip--bottom">Speaker focus</div>
|
|||
|
|
<div className="home-landing__visual-waveform">
|
|||
|
|
{Array.from({ length: 10 }).map((_, index) => (
|
|||
|
|
<span key={`visual-wave-${index}`} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</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__board-glow" />
|
|||
|
|
<div className="home-landing__board-grid" />
|
|||
|
|
<div className="home-landing__board-panel home-landing__board-panel--summary">
|
|||
|
|
<span className="home-landing__board-pill">Meeting Summary</span>
|
|||
|
|
<div className="home-landing__board-lines">
|
|||
|
|
<span className="home-landing__board-line home-landing__board-line--lg" />
|
|||
|
|
<span className="home-landing__board-line home-landing__board-line--md" />
|
|||
|
|
<span className="home-landing__board-line home-landing__board-line--sm" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="home-landing__board-panel home-landing__board-panel--activity">
|
|||
|
|
<div className="home-landing__board-bars">
|
|||
|
|
<span />
|
|||
|
|
<span />
|
|||
|
|
<span />
|
|||
|
|
<span />
|
|||
|
|
<span />
|
|||
|
|
<span />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="home-landing__board-panel home-landing__board-panel--timeline">
|
|||
|
|
<div className="home-landing__board-node home-landing__board-node--active" />
|
|||
|
|
<div className="home-landing__board-node" />
|
|||
|
|
<div className="home-landing__board-node" />
|
|||
|
|
<div className="home-landing__board-rail" />
|
|||
|
|
</div>
|
|||
|
|
<div className="home-landing__board-stats">
|
|||
|
|
<div className="home-landing__board-stat" />
|
|||
|
|
<div className="home-landing__board-stat" />
|
|||
|
|
<div className="home-landing__board-stat" />
|
|||
|
|
</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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|