新增web访问密码查看

codex/dev
AlanPaine 2026-03-27 08:01:52 +00:00
parent 7c63ec1ebe
commit 346b5ffb06
4 changed files with 91 additions and 3 deletions

View File

@ -87,6 +87,7 @@ class Meeting(BaseModel):
overall_status: Optional[str] = None overall_status: Optional[str] = None
overall_progress: Optional[int] = None overall_progress: Optional[int] = None
current_stage: Optional[str] = None current_stage: Optional[str] = None
access_password: Optional[str] = None
class TranscriptSegment(BaseModel): class TranscriptSegment(BaseModel):
segment_id: int segment_id: int

View File

@ -5,7 +5,7 @@ import { QRCodeSVG } from 'qrcode.react';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享" }) => { const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享", children }) => {
const { message } = App.useApp(); const { message } = App.useApp();
const handleCopy = () => { const handleCopy = () => {
@ -43,6 +43,11 @@ const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享" }) =>
<div style={{ marginTop: 12, background: '#f5f5f5', padding: '8px 12px', borderRadius: 6, textAlign: 'left' }}> <div style={{ marginTop: 12, background: '#f5f5f5', padding: '8px 12px', borderRadius: 6, textAlign: 'left' }}>
<Text code ellipsis style={{ width: '100%', display: 'block' }}>{url}</Text> <Text code ellipsis style={{ width: '100%', display: 'block' }}>{url}</Text>
</div> </div>
{children ? (
<div style={{ marginTop: 20, textAlign: 'left' }}>
{children}
</div>
) : null}
</div> </div>
</Modal> </Modal>
); );

View File

@ -36,6 +36,7 @@ const API_CONFIG = {
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`, UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`, REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`,
NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation`, NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation`,
ACCESS_PASSWORD: (meetingId) => `/api/meetings/${meetingId}/access-password`,
PREVIEW_DATA: (meetingId) => `/api/meetings/${meetingId}/preview-data`, PREVIEW_DATA: (meetingId) => `/api/meetings/${meetingId}/preview-data`,
LLM_MODELS: '/api/llm-models/active' LLM_MODELS: '/api/llm-models/active'
}, },

View File

@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { import {
Card, Row, Col, Button, Space, Typography, Tag, Avatar, Card, Row, Col, Button, Space, Typography, Tag, Avatar,
Tooltip, Progress, Spin, App, Dropdown, 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'; } from 'antd';
import { import {
ClockCircleOutlined, UserOutlined, ClockCircleOutlined, UserOutlined,
@ -75,6 +75,9 @@ const MeetingDetails = ({ user }) => {
const [showQRModal, setShowQRModal] = useState(false); const [showQRModal, setShowQRModal] = useState(false);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1); const [playbackRate, setPlaybackRate] = useState(1);
const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false);
const [accessPasswordDraft, setAccessPasswordDraft] = useState('');
const [savingAccessPassword, setSavingAccessPassword] = useState(false);
// Drawer // Drawer
const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false); const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false);
@ -87,6 +90,7 @@ const MeetingDetails = ({ user }) => {
const audioRef = useRef(null); const audioRef = useRef(null);
const transcriptRefs = useRef([]); const transcriptRefs = useRef([]);
const isMeetingOwner = user?.user_id === meeting?.creator_id;
/* ══════════════════ 数据获取 ══════════════════ */ /* ══════════════════ 数据获取 ══════════════════ */
@ -105,6 +109,8 @@ const MeetingDetails = ({ user }) => {
setLoading(true); setLoading(true);
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id))); const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
setMeeting(response.data); setMeeting(response.data);
setAccessPasswordEnabled(Boolean(response.data.access_password));
setAccessPasswordDraft(response.data.access_password || '');
if (response.data.transcription_status) { if (response.data.transcription_status) {
const ts = response.data.transcription_status; const ts = response.data.transcription_status;
@ -220,6 +226,40 @@ const MeetingDetails = ({ user }) => {
finally { setIsUploading(false); } 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 = () => { const openAudioUploadPicker = () => {
document.getElementById('audio-upload-input')?.click(); document.getElementById('audio-upload-input')?.click();
}; };
@ -864,7 +904,48 @@ const MeetingDetails = ({ user }) => {
)} )}
</Drawer> </Drawer>
<QRCodeModal open={showQRModal} onClose={() => setShowQRModal(false)} url={`${window.location.origin}/meetings/preview/${meeting_id}`} /> <QRCodeModal open={showQRModal} onClose={() => setShowQRModal(false)} url={`${window.location.origin}/meetings/preview/${meeting_id}`}>
{isMeetingOwner ? (
<>
<Divider style={{ margin: '0 0 16px' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Text strong>访问密码保护</Text>
<Switch
checked={accessPasswordEnabled}
checkedChildren="已开启"
unCheckedChildren="已关闭"
onChange={setAccessPasswordEnabled}
/>
</div>
{accessPasswordEnabled ? (
<Space direction="vertical" style={{ width: '100%' }} size={12}>
<Input.Password
value={accessPasswordDraft}
onChange={(e) => setAccessPasswordDraft(e.target.value)}
placeholder="请输入访问密码"
/>
<Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
<Text type="secondary">开启后访客打开分享链接时需要输入这个密码</Text>
<Space>
<Button onClick={copyAccessPassword} disabled={!accessPasswordDraft}>复制密码</Button>
<Button type="primary" loading={savingAccessPassword} onClick={saveAccessPassword}>保存密码</Button>
</Space>
</Space>
</Space>
) : (
<Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
<Text type="secondary">关闭后任何拿到链接的人都可以直接查看预览页</Text>
<Button type="primary" loading={savingAccessPassword} onClick={saveAccessPassword}>关闭密码保护</Button>
</Space>
)}
</>
) : meeting?.access_password ? (
<>
<Divider style={{ margin: '0 0 16px' }} />
<Text type="secondary">该分享链接已启用访问密码密码由会议创建人管理</Text>
</>
) : null}
</QRCodeModal>
<MeetingFormDrawer <MeetingFormDrawer
open={editDrawerOpen} open={editDrawerOpen}
onClose={() => setEditDrawerOpen(false)} onClose={() => setEditDrawerOpen(false)}