2026-03-26 06:55:12 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { useParams, Link } from 'react-router-dom';
|
|
|
|
|
import {
|
|
|
|
|
Layout, Card, Typography, Space, Button,
|
|
|
|
|
Result, Spin, App, Tag, Divider, Empty, Input
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
LockOutlined, EyeOutlined, EyeInvisibleOutlined,
|
|
|
|
|
CopyOutlined, CheckCircleOutlined, ShareAltOutlined,
|
|
|
|
|
PlayCircleFilled, PauseCircleFilled,
|
|
|
|
|
HomeOutlined, CalendarOutlined, LoginOutlined
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import apiClient from '../utils/apiClient';
|
|
|
|
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
2026-01-19 11:03:08 +00:00
|
|
|
import MarkdownRenderer from '../components/MarkdownRenderer';
|
2026-03-26 06:55:12 +00:00
|
|
|
import BrandLogo from '../components/BrandLogo';
|
|
|
|
|
import tools from '../utils/tools';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const { Header, Content } = Layout;
|
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
const MeetingPreview = () => {
|
|
|
|
|
const { meeting_id } = useParams();
|
2026-03-26 06:55:12 +00:00
|
|
|
const { message } = App.useApp();
|
|
|
|
|
const [meeting, setMeeting] = useState(null);
|
2026-01-19 11:03:08 +00:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState(null);
|
2026-03-26 06:55:12 +00:00
|
|
|
const [password, setPassword] = useState('');
|
|
|
|
|
const [isAuthorized, setIsAuthorized] = useState(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-26 06:55:12 +00:00
|
|
|
fetchPreview();
|
2026-01-19 11:03:08 +00:00
|
|
|
}, [meeting_id]);
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const fetchPreview = async (pwd = null) => {
|
|
|
|
|
setLoading(true);
|
2026-01-19 11:03:08 +00:00
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
const url = buildApiUrl(`/api/meetings/preview/${meeting_id}${pwd ? `?password=${pwd}` : ''}`);
|
|
|
|
|
const res = await apiClient.get(url);
|
|
|
|
|
setMeeting(res.data);
|
|
|
|
|
setIsAuthorized(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.response?.status === 401) {
|
|
|
|
|
setIsAuthorized(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
} else {
|
2026-03-26 06:55:12 +00:00
|
|
|
setError('无法加载会议预览');
|
2026-01-19 11:03:08 +00:00
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const handleVerify = () => {
|
|
|
|
|
if (!password) {
|
|
|
|
|
message.warning('请输入访问密码');
|
2026-01-19 11:03:08 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
fetchPreview(password);
|
2026-01-19 11:03:08 +00:00
|
|
|
};
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
if (loading && !meeting) return <div style={{ textAlign: 'center', padding: '100px' }}><Spin size="large" /></div>;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
if (!isAuthorized) {
|
2026-01-19 11:03:08 +00:00
|
|
|
return (
|
2026-03-26 06:55:12 +00:00
|
|
|
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5' }}>
|
|
|
|
|
<Card style={{ width: 400, borderRadius: 16, textAlign: 'center', boxShadow: '0 8px 24px rgba(0,0,0,0.05)' }}>
|
|
|
|
|
<LockOutlined style={{ fontSize: 48, color: '#1677ff', marginBottom: 24 }} />
|
|
|
|
|
<Title level={3}>此会议受密码保护</Title>
|
|
|
|
|
<Paragraph type="secondary">请输入访问密码以查看会议纪要</Paragraph>
|
|
|
|
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
|
|
|
|
<Input.Password
|
|
|
|
|
size="large"
|
|
|
|
|
placeholder="访问密码"
|
|
|
|
|
value={password}
|
|
|
|
|
onChange={e => setPassword(e.target.value)}
|
|
|
|
|
onPressEnter={handleVerify}
|
|
|
|
|
/>
|
|
|
|
|
<Button type="primary" size="large" block icon={<EyeOutlined />} onClick={handleVerify}>立即查看</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
<div style={{ marginTop: 24 }}>
|
|
|
|
|
<Link to="/"><Button type="link" icon={<HomeOutlined />}>返回首页</Button></Link>
|
2026-01-19 11:03:08 +00:00
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
</Card>
|
2026-01-19 11:03:08 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
if (error) return <Result status="error" title={error} extra={<Link to="/"><Button type="primary" icon={<HomeOutlined />}>返回首页</Button></Link>} />;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-03-26 06:55:12 +00:00
|
|
|
<Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
|
|
|
|
|
<Header style={{ background: '#fff', padding: '0 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}>
|
|
|
|
|
<Space>
|
|
|
|
|
<BrandLogo title="iMeeting 会议预览" size={28} titleSize={18} gap={10} />
|
|
|
|
|
</Space>
|
|
|
|
|
<Link to="/"><Button icon={<LoginOutlined />}>登录系统</Button></Link>
|
|
|
|
|
</Header>
|
|
|
|
|
|
|
|
|
|
<Content style={{ padding: '40px 20px', maxWidth: 900, margin: '0 auto', width: '100%' }}>
|
|
|
|
|
<Card bordered={false} style={{ borderRadius: 16 }}>
|
|
|
|
|
<Title level={2}>{meeting.title}</Title>
|
|
|
|
|
<Space wrap style={{ marginBottom: 20 }}>
|
|
|
|
|
{meeting.tags?.map(t => <Tag key={t.id} color="blue">{t.name}</Tag>)}
|
|
|
|
|
<Text type="secondary"><CalendarOutlined /> {tools.formatDateTime(meeting.meeting_time)}</Text>
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
|
|
|
|
<div className="preview-content">
|
|
|
|
|
{meeting.summary ? (
|
|
|
|
|
<MarkdownRenderer content={meeting.summary} />
|
|
|
|
|
) : (
|
|
|
|
|
<Empty description="暂无会议总结内容" />
|
|
|
|
|
)}
|
2026-01-19 11:03:08 +00:00
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
</Card>
|
|
|
|
|
</Content>
|
|
|
|
|
</Layout>
|
2026-01-19 11:03:08 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default MeetingPreview;
|