新增web访问密码查看
parent
7c63ec1ebe
commit
346b5ffb06
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue