From 346b5ffb06cac3a59a057358810b58cdf1cc0a1d Mon Sep 17 00:00:00 2001 From: AlanPaine Date: Fri, 27 Mar 2026 08:01:52 +0000 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eweb=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/models/models.py | 1 + frontend/src/components/QRCodeModal.jsx | 7 +- frontend/src/config/api.js | 1 + frontend/src/pages/MeetingDetails.jsx | 85 ++++++++++++++++++++++++- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 3007da0..8fb2bc2 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -87,6 +87,7 @@ class Meeting(BaseModel): overall_status: Optional[str] = None overall_progress: Optional[int] = None current_stage: Optional[str] = None + access_password: Optional[str] = None class TranscriptSegment(BaseModel): segment_id: int diff --git a/frontend/src/components/QRCodeModal.jsx b/frontend/src/components/QRCodeModal.jsx index eb747a3..aa9cb55 100644 --- a/frontend/src/components/QRCodeModal.jsx +++ b/frontend/src/components/QRCodeModal.jsx @@ -5,7 +5,7 @@ import { QRCodeSVG } from 'qrcode.react'; const { Text, Paragraph } = Typography; -const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享" }) => { +const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享", children }) => { const { message } = App.useApp(); const handleCopy = () => { @@ -43,6 +43,11 @@ const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享" }) =>
{url}
+ {children ? ( +
+ {children} +
+ ) : null} ); diff --git a/frontend/src/config/api.js b/frontend/src/config/api.js index dfc8abd..190b6a1 100644 --- a/frontend/src/config/api.js +++ b/frontend/src/config/api.js @@ -36,6 +36,7 @@ const API_CONFIG = { UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`, REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`, NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation`, + ACCESS_PASSWORD: (meetingId) => `/api/meetings/${meetingId}/access-password`, PREVIEW_DATA: (meetingId) => `/api/meetings/${meetingId}/preview-data`, LLM_MODELS: '/api/llm-models/active' }, diff --git a/frontend/src/pages/MeetingDetails.jsx b/frontend/src/pages/MeetingDetails.jsx index d72a6ce..4643d10 100644 --- a/frontend/src/pages/MeetingDetails.jsx +++ b/frontend/src/pages/MeetingDetails.jsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { Card, Row, Col, Button, Space, Typography, Tag, Avatar, Tooltip, Progress, Spin, App, Dropdown, - Divider, List, Timeline, Tabs, Input, Upload, Empty, Drawer, Select + Divider, List, Timeline, Tabs, Input, Upload, Empty, Drawer, Select, Switch } from 'antd'; import { ClockCircleOutlined, UserOutlined, @@ -75,6 +75,9 @@ const MeetingDetails = ({ user }) => { const [showQRModal, setShowQRModal] = useState(false); const [isUploading, setIsUploading] = useState(false); const [playbackRate, setPlaybackRate] = useState(1); + const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false); + const [accessPasswordDraft, setAccessPasswordDraft] = useState(''); + const [savingAccessPassword, setSavingAccessPassword] = useState(false); // 转录编辑 Drawer const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false); @@ -87,6 +90,7 @@ const MeetingDetails = ({ user }) => { const audioRef = useRef(null); const transcriptRefs = useRef([]); + const isMeetingOwner = user?.user_id === meeting?.creator_id; /* ══════════════════ 数据获取 ══════════════════ */ @@ -105,6 +109,8 @@ const MeetingDetails = ({ user }) => { setLoading(true); const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id))); setMeeting(response.data); + setAccessPasswordEnabled(Boolean(response.data.access_password)); + setAccessPasswordDraft(response.data.access_password || ''); if (response.data.transcription_status) { const ts = response.data.transcription_status; @@ -220,6 +226,40 @@ const MeetingDetails = ({ user }) => { finally { setIsUploading(false); } }; + const saveAccessPassword = async () => { + const nextPassword = accessPasswordEnabled ? accessPasswordDraft.trim() : null; + if (accessPasswordEnabled && !nextPassword) { + message.warning('开启访问密码后,请先输入密码'); + return; + } + + setSavingAccessPassword(true); + try { + const res = await apiClient.put( + buildApiUrl(API_ENDPOINTS.MEETINGS.ACCESS_PASSWORD(meeting_id)), + { password: nextPassword } + ); + const savedPassword = res.data?.password || null; + setMeeting((prev) => (prev ? { ...prev, access_password: savedPassword } : prev)); + setAccessPasswordEnabled(Boolean(savedPassword)); + setAccessPasswordDraft(savedPassword || ''); + message.success(res.message || '访问密码已更新'); + } catch (error) { + message.error(error?.response?.data?.message || '访问密码更新失败'); + } finally { + setSavingAccessPassword(false); + } + }; + + const copyAccessPassword = async () => { + if (!accessPasswordDraft) { + message.warning('当前没有可复制的访问密码'); + return; + } + await navigator.clipboard.writeText(accessPasswordDraft); + message.success('访问密码已复制'); + }; + const openAudioUploadPicker = () => { document.getElementById('audio-upload-input')?.click(); }; @@ -864,7 +904,48 @@ const MeetingDetails = ({ user }) => { )} - setShowQRModal(false)} url={`${window.location.origin}/meetings/preview/${meeting_id}`} /> + setShowQRModal(false)} url={`${window.location.origin}/meetings/preview/${meeting_id}`}> + {isMeetingOwner ? ( + <> + +
+ 访问密码保护 + +
+ {accessPasswordEnabled ? ( + + setAccessPasswordDraft(e.target.value)} + placeholder="请输入访问密码" + /> + + 开启后,访客打开分享链接时需要输入这个密码 + + + + + + + ) : ( + + 关闭后,任何拿到链接的人都可以直接查看预览页 + + + )} + + ) : meeting?.access_password ? ( + <> + + 该分享链接已启用访问密码,密码由会议创建人管理。 + + ) : null} +
setEditDrawerOpen(false)}