增加了会议访问密码
parent
3bb66c8e81
commit
a708031347
|
|
@ -171,7 +171,7 @@
|
|||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
max-width: 650px;
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ const HomePage = ({ onLogin }) => {
|
|||
{/* Hero Section */}
|
||||
<section className="hero">
|
||||
<div className="hero-content">
|
||||
<h1 className="hero-title">iMeeting —— 让会议更智能</h1>
|
||||
<h1 className="hero-title">iMeeting - 灵 枢 (Líng Shū)</h1>
|
||||
<p className="hero-subtitle">
|
||||
通过AI将会议音频转录并自动总结,对齐团队目标,构建企业知识库。
|
||||
让每一次谈话都产生价值
|
||||
</p>
|
||||
<button
|
||||
className="cta-button"
|
||||
|
|
|
|||
|
|
@ -1517,14 +1517,17 @@
|
|||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.summary-modal-content {
|
||||
padding: 0 24px 24px;
|
||||
max-height: calc(90vh - 120px);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0; /* 关键:允许 flex 子元素缩小 */
|
||||
}
|
||||
|
||||
.summary-input-section {
|
||||
|
|
@ -1832,14 +1835,17 @@
|
|||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.transcript-edit-content {
|
||||
padding: 20px;
|
||||
max-height: calc(90vh - 120px);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0; /* 关键:允许 flex 子元素缩小 */
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
|
|
@ -2225,4 +2231,374 @@
|
|||
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* 访问密码管理样式 */
|
||||
.access-password-section {
|
||||
background: #fff;
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.access-password-section h2 {
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.password-management {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* 创建人密码控制 */
|
||||
.creator-password-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.password-toggle-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.password-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.password-toggle-btn.enabled {
|
||||
border-color: #10b981;
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.password-toggle-btn.disabled {
|
||||
border-color: #f59e0b;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.password-toggle-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.password-toggle-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.password-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.password-label {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.password-value {
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
letter-spacing: 0.2em;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.password-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.password-action-btn:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.password-action-btn.copied {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.password-hint {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f8fafc;
|
||||
border-left: 3px solid #f59e0b;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 非创建人查看密码样式 */
|
||||
.viewer-password-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.password-info-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.password-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.password-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.password-content h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.password-display-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.password-display-row .password-label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.password-display-row .password-value {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.no-password-message {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.no-password-message p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 访问密码 - 行内样式 */
|
||||
.meta-item-password {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.password-control-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.password-toggle-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.password-toggle-inline.enabled {
|
||||
border-color: #10b981;
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.password-toggle-inline.disabled {
|
||||
border-color: #cbd5e1;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.password-toggle-inline.enabled:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.password-toggle-inline.disabled:hover:not(:disabled) {
|
||||
background: #f9fafb;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.password-toggle-inline:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.password-text {
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
color: white;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.password-view-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.password-view-inline .password-text {
|
||||
color: #1e293b;
|
||||
padding: 0.3rem 0.6rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.password-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.35rem;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.password-toggle-inline.disabled + .password-icon-btn,
|
||||
.password-view-inline .password-icon-btn {
|
||||
background: white;
|
||||
border-color: #cbd5e1;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.password-icon-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.password-toggle-inline.disabled + .password-icon-btn:hover,
|
||||
.password-view-inline .password-icon-btn:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.no-password-text {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 参会人员 - 行内样式 */
|
||||
.meta-item-attendees {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #334155;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.attendees-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.attendee-name {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.attendee-count {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
|||
import apiClient from '../utils/apiClient';
|
||||
import configService from '../utils/configService';
|
||||
import tools from '../utils/tools';
|
||||
import { ArrowLeft, Clock, Users, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff, Image, QrCode, MoreVertical, Upload, ChevronLeft, ChevronRight, Loader } from 'lucide-react';
|
||||
import { ArrowLeft, Clock, Users, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff, Image, QrCode, MoreVertical, Upload, ChevronLeft, ChevronRight, Loader, Lock, Unlock, Eye, EyeOff, Copy, Check } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
|
@ -69,6 +69,13 @@ const MeetingDetails = ({ user }) => {
|
|||
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 默认100MB
|
||||
const [uploadError, setUploadError] = useState('');
|
||||
|
||||
// 访问密码相关状态
|
||||
const [accessPassword, setAccessPassword] = useState(null);
|
||||
const [passwordEnabled, setPasswordEnabled] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [passwordCopied, setPasswordCopied] = useState(false);
|
||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||
|
||||
// 音频加载状态
|
||||
const [audioLoading, setAudioLoading] = useState(true); // 音频是否正在加载
|
||||
const [audioCanPlay, setAudioCanPlay] = useState(false); // 音频是否可以播放
|
||||
|
|
@ -242,7 +249,16 @@ const MeetingDetails = ({ user }) => {
|
|||
|
||||
const response = await apiClient.get(`${baseUrl}${detailEndpoint}`);
|
||||
setMeeting(response.data);
|
||||
|
||||
|
||||
// 设置访问密码状态
|
||||
if (response.data.access_password) {
|
||||
setAccessPassword(response.data.access_password);
|
||||
setPasswordEnabled(true);
|
||||
} else {
|
||||
setAccessPassword(null);
|
||||
setPasswordEnabled(false);
|
||||
}
|
||||
|
||||
// Handle transcription status from meeting details
|
||||
if (response.data.transcription_status) {
|
||||
const newStatus = response.data.transcription_status;
|
||||
|
|
@ -1081,6 +1097,58 @@ const MeetingDetails = ({ user }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 访问密码管理函数
|
||||
const generatePassword = () => {
|
||||
// 生成4位混合密码(数字+字母)
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
|
||||
let password = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return password;
|
||||
};
|
||||
|
||||
const handleTogglePassword = async () => {
|
||||
try {
|
||||
setPasswordLoading(true);
|
||||
|
||||
if (passwordEnabled) {
|
||||
// 关闭密码
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meeting_id}/access-password`), {
|
||||
password: null
|
||||
});
|
||||
setAccessPassword(null);
|
||||
setPasswordEnabled(false);
|
||||
setShowPassword(false);
|
||||
showToast('访问密码已关闭', 'success');
|
||||
} else {
|
||||
// 生成并开启密码
|
||||
const newPassword = generatePassword();
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meeting_id}/access-password`), {
|
||||
password: newPassword
|
||||
});
|
||||
setAccessPassword(newPassword);
|
||||
setPasswordEnabled(true);
|
||||
setShowPassword(true);
|
||||
showToast('访问密码已生成', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('密码操作失败:', err);
|
||||
showToast(err.response?.data?.message || '密码操作失败,请重试', 'error');
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyPassword = () => {
|
||||
if (accessPassword) {
|
||||
navigator.clipboard.writeText(accessPassword);
|
||||
setPasswordCopied(true);
|
||||
showToast('密码已复制到剪贴板', 'success');
|
||||
setTimeout(() => setPasswordCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id);
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -1181,12 +1249,12 @@ const MeetingDetails = ({ user }) => {
|
|||
<div className="meta-grid">
|
||||
<div className="meta-item">
|
||||
<Calendar size={18} />
|
||||
<strong>会议日期:</strong>
|
||||
<strong>日期:</strong>
|
||||
<span>{tools.formatDateTime(meeting.meeting_time).split(' ')[0].slice(2)}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<Clock size={18} />
|
||||
<strong>会议时间:</strong>
|
||||
<strong>时间:</strong>
|
||||
<span>{tools.formatDateTime(meeting.meeting_time).split(' ')[1]}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
|
|
@ -1194,26 +1262,73 @@ const MeetingDetails = ({ user }) => {
|
|||
<strong>创建人:</strong>
|
||||
<span>{meeting.creator_username}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<div className="meta-item meta-item-password">
|
||||
<Lock size={18} />
|
||||
<strong>访问密码:</strong>
|
||||
{isCreator ? (
|
||||
// 创建人:显示开启/关闭按钮
|
||||
<div className="password-control-inline">
|
||||
<button
|
||||
className={`password-toggle-inline ${passwordEnabled ? 'enabled' : 'disabled'}`}
|
||||
onClick={handleTogglePassword}
|
||||
disabled={passwordLoading}
|
||||
title={passwordEnabled ? '点击关闭密码' : '点击开启密码'}
|
||||
>
|
||||
{passwordLoading ? (
|
||||
<Loader size={14} className="spin" />
|
||||
) : passwordEnabled ? (
|
||||
<>
|
||||
<span className="password-text">{showPassword ? accessPassword : '••••'}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>开启</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{passwordEnabled && accessPassword && (
|
||||
<button
|
||||
className="password-icon-btn"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
title={showPassword ? '隐藏密码' : '显示密码'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 非创建人:显示密码或"未设置"
|
||||
passwordEnabled && accessPassword ? (
|
||||
<div className="password-view-inline">
|
||||
<span className="password-text">{showPassword ? accessPassword : '••••'}</span>
|
||||
<button
|
||||
className="password-icon-btn"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
title={showPassword ? '隐藏密码' : '显示密码'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="no-password-text">未设置</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="meta-item meta-item-attendees">
|
||||
<Users size={18} />
|
||||
<strong>参会人数:</strong>
|
||||
<span>{meeting.attendees.length}</span>
|
||||
<strong>参会人员<span className="attendee-count">({meeting.attendees.length}人)</span>:</strong>
|
||||
<span className="attendees-inline">
|
||||
{meeting.attendees.map((attendee, index) => (
|
||||
<span key={index} className="attendee-name">
|
||||
{typeof attendee === 'string' ? attendee : attendee.caption}
|
||||
{index < meeting.attendees.length - 1 && '、'}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="card-section">
|
||||
<h2><Users size={20} /> 参会人员</h2>
|
||||
<div className="attendees-list">
|
||||
{meeting.attendees.map((attendee, index) => (
|
||||
<div key={index} className="attendee-chip">
|
||||
<User size={16} />
|
||||
<span>{typeof attendee === 'string' ? attendee : attendee.caption}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Audio Player Section */}
|
||||
<section className="card-section audio-section">
|
||||
<div className="section-header-with-menu">
|
||||
|
|
@ -1718,7 +1833,7 @@ const MeetingDetails = ({ user }) => {
|
|||
<div className="summary-modal-overlay" onClick={closeSummaryModal}>
|
||||
<div className="summary-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h3><Brain size={20} /> AI会议总结</h3>
|
||||
<h3><Brain size={20} /> AI总结</h3>
|
||||
<button
|
||||
className="close-btn"
|
||||
onClick={closeSummaryModal}
|
||||
|
|
|
|||
|
|
@ -543,3 +543,180 @@
|
|||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
/* 密码保护界面样式 */
|
||||
.password-protection-modal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.password-modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 48px 40px;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12);
|
||||
text-align: center;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.password-icon-large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.password-modal-content h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.password-modal-content > p {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.password-input-group {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: 100%;
|
||||
padding: 14px 50px 14px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
letter-spacing: 0.3em;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.password-input:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.password-input.error {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.password-input:disabled {
|
||||
background: #f8fafc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.password-toggle-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.password-toggle-btn:hover {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.password-error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
color: #dc2626;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.password-verify-btn {
|
||||
width: 100%;
|
||||
padding: 14px 24px;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.password-verify-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.password-verify-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.password-verify-btn:disabled {
|
||||
background: #cbd5e1;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 移动端密码界面优化 */
|
||||
@media (max-width: 768px) {
|
||||
.password-modal-content {
|
||||
padding: 32px 24px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.password-icon-large {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.password-modal-content h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.password-modal-content > p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
padding: 12px 45px 12px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.password-verify-btn {
|
||||
padding: 12px 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import remarkGfm from 'remark-gfm';
|
|||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import { Tabs } from 'antd';
|
||||
import { Lock, Eye, EyeOff, AlertCircle } from 'lucide-react';
|
||||
import MindMap from '../components/MindMap';
|
||||
import './MeetingPreview.css';
|
||||
|
||||
|
|
@ -17,7 +18,21 @@ const MeetingPreview = () => {
|
|||
const [error, setError] = useState(null);
|
||||
const [errorType, setErrorType] = useState(''); // 'not_found', 'no_summary', 'network'
|
||||
|
||||
// 密码验证相关状态
|
||||
const [isPasswordProtected, setIsPasswordProtected] = useState(false);
|
||||
const [passwordVerified, setPasswordVerified] = useState(false);
|
||||
const [passwordInput, setPasswordInput] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [verifyingPassword, setVerifyingPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 检查是否已经验证过密码(使用 sessionStorage)
|
||||
const verifiedKey = `meeting_${meeting_id}_verified`;
|
||||
const isVerified = sessionStorage.getItem(verifiedKey) === 'true';
|
||||
if (isVerified) {
|
||||
setPasswordVerified(true);
|
||||
}
|
||||
fetchMeetingPreviewData();
|
||||
}, [meeting_id]);
|
||||
|
||||
|
|
@ -29,7 +44,17 @@ const MeetingPreview = () => {
|
|||
const result = await response.json();
|
||||
|
||||
if (result.code === "200") {
|
||||
setMeetingData(result.data);
|
||||
// 检查是否需要密码保护 - 优先检查 sessionStorage
|
||||
const verifiedKey = `meeting_${meeting_id}_verified`;
|
||||
const isVerified = sessionStorage.getItem(verifiedKey) === 'true';
|
||||
|
||||
if (result.data.has_password && !isVerified && !passwordVerified) {
|
||||
setIsPasswordProtected(true);
|
||||
setMeetingData(null);
|
||||
} else {
|
||||
setMeetingData(result.data);
|
||||
setIsPasswordProtected(false);
|
||||
}
|
||||
setError(null);
|
||||
setErrorType('');
|
||||
} else if (result.code === "404") {
|
||||
|
|
@ -51,6 +76,56 @@ const MeetingPreview = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handlePasswordVerify = async () => {
|
||||
if (!passwordInput.trim()) {
|
||||
setPasswordError('请输入访问密码');
|
||||
return;
|
||||
}
|
||||
|
||||
setVerifyingPassword(true);
|
||||
setPasswordError('');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/meetings/${meeting_id}/verify-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ password: passwordInput }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === "200" && result.data.verified) {
|
||||
// 验证成功
|
||||
setPasswordVerified(true);
|
||||
setIsPasswordProtected(false);
|
||||
setPasswordInput('');
|
||||
setPasswordError('');
|
||||
|
||||
// 保存验证状态到 sessionStorage
|
||||
const verifiedKey = `meeting_${meeting_id}_verified`;
|
||||
sessionStorage.setItem(verifiedKey, 'true');
|
||||
|
||||
// 重新获取会议数据
|
||||
fetchMeetingPreviewData();
|
||||
} else {
|
||||
setPasswordError('密码错误,请重试');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('验证密码失败:', err);
|
||||
setPasswordError('验证失败,请重试');
|
||||
} finally {
|
||||
setVerifyingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handlePasswordVerify();
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTime) => {
|
||||
if (!dateTime) return '';
|
||||
const date = new Date(dateTime);
|
||||
|
|
@ -76,6 +151,59 @@ const MeetingPreview = () => {
|
|||
);
|
||||
}
|
||||
|
||||
// 如果需要密码验证,显示密码输入界面
|
||||
if (isPasswordProtected && !passwordVerified) {
|
||||
return (
|
||||
<div className="preview-container">
|
||||
<div className="password-protection-modal">
|
||||
<div className="password-modal-content">
|
||||
<div className="password-icon-large">
|
||||
<Lock size={48} />
|
||||
</div>
|
||||
<h2>此会议需要访问密码</h2>
|
||||
<p>请输入密码以查看会议总结</p>
|
||||
|
||||
<div className="password-input-group">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={passwordInput}
|
||||
onChange={(e) => setPasswordInput(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="请输入4位访问密码"
|
||||
className={`password-input ${passwordError ? 'error' : ''}`}
|
||||
disabled={verifyingPassword}
|
||||
maxLength={4}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className="password-toggle-btn"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
type="button"
|
||||
>
|
||||
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{passwordError && (
|
||||
<div className="password-error-message">
|
||||
<AlertCircle size={16} />
|
||||
<span>{passwordError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="password-verify-btn"
|
||||
onClick={handlePasswordVerify}
|
||||
disabled={verifyingPassword || !passwordInput.trim()}
|
||||
>
|
||||
{verifyingPassword ? '验证中...' : '验证密码'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="preview-container">
|
||||
|
|
|
|||
Loading…
Reference in New Issue