2026-03-26 09:42:29 +00:00
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2026-03-26 03:18:44 +00:00
|
|
|
|
import {
|
|
|
|
|
|
AudioOutlined,
|
|
|
|
|
|
ArrowRightOutlined,
|
2026-04-08 06:34:59 +00:00
|
|
|
|
VideoCameraOutlined,
|
2026-03-26 03:18:44 +00:00
|
|
|
|
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";
|
2026-03-26 09:42:29 +00:00
|
|
|
|
import RightVisual from "./RightVisual";
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
|
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
type QuickEntry = {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
icon: React.ReactNode;
|
|
|
|
|
|
description: string[];
|
|
|
|
|
|
accent: string;
|
2026-04-08 06:34:59 +00:00
|
|
|
|
badge: string;
|
2026-03-26 03:18:44 +00:00
|
|
|
|
onClick: () => void;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type RecentCard = {
|
|
|
|
|
|
id: number | string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
duration: string;
|
|
|
|
|
|
time: string;
|
|
|
|
|
|
tags: string[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const RECENT_CARD_READ_STORAGE_KEY = "home_recent_card_read_ids";
|
|
|
|
|
|
|
2026-03-26 03:18:44 +00:00
|
|
|
|
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"),
|
2026-04-08 06:34:59 +00:00
|
|
|
|
tags: task.tags?.split(",").filter(Boolean).slice(0, 5) || []
|
2026-03-26 03:18:44 +00:00
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function HomePage() {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
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]);
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchRecentTasks = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await getRecentTasks();
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const payload: any = (response as any).data || response;
|
|
|
|
|
|
setRecentTasks(payload?.data || payload || []);
|
2026-03-26 03:18:44 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Home recent tasks load failed", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void fetchRecentTasks();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (typeof window === "undefined") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.localStorage.setItem(RECENT_CARD_READ_STORAGE_KEY, JSON.stringify(readCardIds));
|
|
|
|
|
|
}, [readCardIds]);
|
|
|
|
|
|
|
2026-03-26 03:18:44 +00:00
|
|
|
|
const quickEntries = useMemo<QuickEntry[]>(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{
|
2026-04-03 02:40:34 +00:00
|
|
|
|
title: "开启实时记录",
|
2026-03-26 03:18:44 +00:00
|
|
|
|
icon: <AudioOutlined />,
|
2026-04-03 02:40:34 +00:00
|
|
|
|
description: ["实时语音转文字", "同步翻译,智能总结要点"],
|
2026-03-26 03:18:44 +00:00
|
|
|
|
accent: "violet",
|
2026-04-08 06:34:59 +00:00
|
|
|
|
badge: "会议神器",
|
|
|
|
|
|
onClick: () => navigate("/meetings?action=create&type=realtime")
|
2026-03-26 03:18:44 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-04-03 02:40:34 +00:00
|
|
|
|
title: "上传音视频",
|
2026-03-26 03:18:44 +00:00
|
|
|
|
icon: <VideoCameraAddOutlined />,
|
2026-04-03 02:40:34 +00:00
|
|
|
|
description: ["音视频转文字", "区分发言人,一键导出"],
|
2026-03-26 03:18:44 +00:00
|
|
|
|
accent: "cyan",
|
2026-04-08 06:34:59 +00:00
|
|
|
|
badge: "iMeeting",
|
2026-03-26 09:42:29 +00:00
|
|
|
|
onClick: () => navigate("/meetings?create=true")
|
2026-03-26 03:18:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
[navigate]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
|
|
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 03:18:44 +00:00
|
|
|
|
return (
|
2026-04-03 02:40:34 +00:00
|
|
|
|
<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">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
每一次交流,
|
|
|
|
|
|
<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>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</Title>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
|
|
|
|
|
|
<div className="home-quick-actions">
|
|
|
|
|
|
{quickEntries.map((entry) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`home-action-item home-action-item--${entry.accent}`}
|
|
|
|
|
|
onClick={entry.onClick}
|
|
|
|
|
|
key={entry.title}
|
2026-03-26 03:18:44 +00:00
|
|
|
|
>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<div className="home-action-badge">{entry.badge}</div>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
<div className="home-action-icon-wrapper">
|
|
|
|
|
|
<div className="home-action-icon">{entry.icon}</div>
|
|
|
|
|
|
<div className="home-action-icon-glow" />
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<div className="home-action-icon-circle" />
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</div>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
<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>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
</div>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="home-recent-section">
|
|
|
|
|
|
<div className="home-section-header">
|
2026-04-03 06:38:36 +00:00
|
|
|
|
<Title level={3}>最近</Title>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
<Button type="link" onClick={() => navigate("/meetings")} className="home-view-all">
|
|
|
|
|
|
查看全部 <ArrowRightOutlined />
|
|
|
|
|
|
</Button>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</div>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
|
|
|
|
|
|
{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"
|
2026-04-08 06:34:59 +00:00
|
|
|
|
onClick={() => handleRecentCardClick(card)}
|
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleRecentCardClick(card);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabIndex={0}
|
2026-04-03 02:40:34 +00:00
|
|
|
|
>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
{!readCardIds.includes(String(card.id)) && <span className="home-recent-card-dot" />}
|
|
|
|
|
|
<div className="home-recent-card-head">
|
2026-04-03 02:40:34 +00:00
|
|
|
|
<Title level={4} className="home-recent-card-title">{card.title}</Title>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<div className="home-recent-card-icon">
|
|
|
|
|
|
<VideoCameraOutlined className="home-recent-card-play-icon" />
|
2026-04-03 02:40:34 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<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>
|
2026-04-03 02:40:34 +00:00
|
|
|
|
</article>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="home-empty-state">
|
|
|
|
|
|
<Empty description="暂无最近记录" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
);
|
2026-04-03 02:40:34 +00:00
|
|
|
|
}
|