2026-06-05 07:07:45 +00:00
|
|
|
|
import { App, Button, Card, Input, Space, Typography } from "antd";
|
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2026-06-25 02:08:32 +00:00
|
|
|
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
2026-06-05 07:07:45 +00:00
|
|
|
|
|
2026-09-14 08:15:19 +00:00
|
|
|
|
import {getMeetingPreviewAccess, getPublicMeetingPreview, getPublicRealtimeMeetingSessionStatus} from "@/api/meeting";
|
2026-06-05 07:07:45 +00:00
|
|
|
|
import LoadingScreen from "@/components/LoadingScreen";
|
2026-06-25 02:08:32 +00:00
|
|
|
|
import PageHeader from "@/components/PageHeader";
|
2026-06-05 07:07:45 +00:00
|
|
|
|
import MeetingPreviewView from "@/components/preview/MeetingPreviewView";
|
|
|
|
|
|
import usePageTitle from "@/hooks/usePageTitle";
|
|
|
|
|
|
import type { MeetingChapterVO, MeetingTranscriptVO, MeetingVO } from "@/types";
|
2026-06-25 02:08:32 +00:00
|
|
|
|
import { hasAccessToken } from "@/utils/auth";
|
2026-06-05 07:07:45 +00:00
|
|
|
|
import { buildMeetingPreviewUrl } from "@/utils/meeting";
|
|
|
|
|
|
|
|
|
|
|
|
const { Paragraph, Title } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
export default function MeetingPreviewPage() {
|
|
|
|
|
|
const { message } = App.useApp();
|
2026-06-25 02:08:32 +00:00
|
|
|
|
const navigate = useNavigate();
|
2026-06-05 07:07:45 +00:00
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
|
const [searchParams] = useSearchParams();
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [meeting, setMeeting] = useState<MeetingVO | null>(null);
|
|
|
|
|
|
const [transcripts, setTranscripts] = useState<MeetingTranscriptVO[]>([]);
|
|
|
|
|
|
const [chapters, setChapters] = useState<MeetingChapterVO[]>([]);
|
|
|
|
|
|
const [passwordRequired, setPasswordRequired] = useState(false);
|
|
|
|
|
|
const [passwordVerified, setPasswordVerified] = useState(false);
|
|
|
|
|
|
const [accessPassword, setAccessPassword] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
const meetingId = useMemo(() => Number(id), [id]);
|
|
|
|
|
|
const presetAccessPassword = useMemo(() => (searchParams.get("accessPassword") || "").trim(), [searchParams]);
|
|
|
|
|
|
usePageTitle(meeting?.title || "会议预览");
|
|
|
|
|
|
|
2026-06-25 02:08:32 +00:00
|
|
|
|
const handleBack = () => {
|
|
|
|
|
|
navigate(hasAccessToken() ? "/meetings" : "/login", { replace: true });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-05 07:07:45 +00:00
|
|
|
|
const loadPreview = async (password?: string) => {
|
|
|
|
|
|
const previewResp = await getPublicMeetingPreview(meetingId, password);
|
2026-09-14 08:15:19 +00:00
|
|
|
|
const preview = previewResp.data.data;
|
|
|
|
|
|
if (preview.meeting.meetingType === "REALTIME") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const statusResp = await getPublicRealtimeMeetingSessionStatus(meetingId, password);
|
|
|
|
|
|
const status = statusResp.data.data.status;
|
|
|
|
|
|
if (status !== "COMPLETING" && status !== "COMPLETED") {
|
|
|
|
|
|
const query = password ? `?accessPassword=${encodeURIComponent(password)}` : "";
|
|
|
|
|
|
navigate(`/meetings/${meetingId}/realtime-preview${query}`, {replace: true});
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 状态查询异常时继续进入原预览页。
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setMeeting(preview.meeting);
|
|
|
|
|
|
setTranscripts(preview.transcripts || []);
|
|
|
|
|
|
setChapters(preview.chapters || []);
|
2026-06-05 07:07:45 +00:00
|
|
|
|
setPasswordVerified(true);
|
2026-09-14 08:15:19 +00:00
|
|
|
|
return false;
|
2026-06-05 07:07:45 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const run = async () => {
|
|
|
|
|
|
if (!meetingId || Number.isNaN(meetingId)) {
|
|
|
|
|
|
message.error("会议编号无效");
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const accessResp = await getMeetingPreviewAccess(meetingId);
|
|
|
|
|
|
const required = !!accessResp.data.data.passwordRequired;
|
|
|
|
|
|
setPasswordRequired(required);
|
|
|
|
|
|
|
|
|
|
|
|
if (!required) {
|
|
|
|
|
|
await loadPreview();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (presetAccessPassword) {
|
|
|
|
|
|
try {
|
2026-09-14 08:15:19 +00:00
|
|
|
|
const redirected = await loadPreview(presetAccessPassword);
|
|
|
|
|
|
if (redirected) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-05 07:07:45 +00:00
|
|
|
|
setAccessPassword(presetAccessPassword);
|
|
|
|
|
|
return;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setPasswordVerified(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void run();
|
2026-06-25 02:08:32 +00:00
|
|
|
|
}, [meetingId, presetAccessPassword, message]);
|
2026-06-05 07:07:45 +00:00
|
|
|
|
|
|
|
|
|
|
const handleSubmitPassword = async () => {
|
|
|
|
|
|
if (!accessPassword.trim()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-09-14 08:15:19 +00:00
|
|
|
|
const redirected = await loadPreview(accessPassword.trim());
|
|
|
|
|
|
if (redirected) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-05 07:07:45 +00:00
|
|
|
|
message.success("访问校验通过");
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error("访问密码错误");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (loading && !meeting && !passwordRequired) {
|
|
|
|
|
|
return <LoadingScreen text="正在加载会议预览..." />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (passwordRequired && !passwordVerified) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="preview-page">
|
2026-06-25 02:08:32 +00:00
|
|
|
|
<div className="preview-page__inner">
|
|
|
|
|
|
<PageHeader title="分享预览" back onBack={handleBack} />
|
|
|
|
|
|
<Card className="surface-card preview-password-card">
|
|
|
|
|
|
<Title level={3}>会议预览</Title>
|
|
|
|
|
|
<Paragraph type="secondary">该会议已设置访问密码,请输入密码后继续访问分享内容。</Paragraph>
|
|
|
|
|
|
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
|
|
|
|
|
<Input.Password
|
|
|
|
|
|
value={accessPassword}
|
|
|
|
|
|
placeholder="请输入访问密码"
|
|
|
|
|
|
onChange={(event) => setAccessPassword(event.target.value)}
|
|
|
|
|
|
onPressEnter={() => void handleSubmitPassword()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button type="primary" block loading={loading} onClick={() => void handleSubmitPassword()}>
|
|
|
|
|
|
进入预览
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
2026-06-05 07:07:45 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!meeting) {
|
|
|
|
|
|
return <LoadingScreen text="未找到会议内容" />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="preview-page">
|
|
|
|
|
|
<div className="preview-page__inner">
|
2026-06-25 02:08:32 +00:00
|
|
|
|
<PageHeader title="分享预览" back onBack={handleBack} />
|
2026-06-05 07:07:45 +00:00
|
|
|
|
<MeetingPreviewView
|
|
|
|
|
|
meeting={meeting}
|
|
|
|
|
|
transcripts={transcripts}
|
|
|
|
|
|
meetingChapters={chapters}
|
|
|
|
|
|
shareUrl={buildMeetingPreviewUrl(meeting.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|