diff --git a/.gemini-clipboard/clipboard-1768974204648.png b/.gemini-clipboard/clipboard-1768974204648.png deleted file mode 100644 index 6838957..0000000 Binary files a/.gemini-clipboard/clipboard-1768974204648.png and /dev/null differ diff --git a/.gemini-clipboard/clipboard-1769064595946.png b/.gemini-clipboard/clipboard-1769064595946.png new file mode 100644 index 0000000..c3a448b Binary files /dev/null and b/.gemini-clipboard/clipboard-1769064595946.png differ diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py index 82355db..10c812d 100644 --- a/backend/app/api/endpoints/auth.py +++ b/backend/app/api/endpoints/auth.py @@ -23,7 +23,7 @@ def login(request_body: LoginRequest, request: Request): with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) - query = "SELECT user_id, username, caption, email, password_hash, role_id FROM users WHERE username = %s" + query = "SELECT user_id, username, caption, avatar_url, email, password_hash, role_id FROM users WHERE username = %s" cursor.execute(query, (request_body.username,)) user = cursor.fetchone() @@ -70,6 +70,7 @@ def login(request_body: LoginRequest, request: Request): user_id=user['user_id'], username=user['username'], caption=user['caption'], + avatar_url=user['avatar_url'], email=user['email'], token=token, role_id=user['role_id'] diff --git a/backend/app/api/endpoints/meetings.py b/backend/app/api/endpoints/meetings.py index 670238f..0164018 100644 --- a/backend/app/api/endpoints/meetings.py +++ b/backend/app/api/endpoints/meetings.py @@ -314,9 +314,13 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren cursor = connection.cursor(dictionary=True) query = ''' SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, - m.user_id as creator_id, u.caption as creator_username, af.file_path as audio_file_path, - m.access_password - FROM meetings m JOIN users u ON m.user_id = u.user_id LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id + m.user_id as creator_id, u.caption as creator_username, + af.file_path as audio_file_path, af.duration as audio_duration, + p.name as prompt_name, m.access_password + FROM meetings m + JOIN users u ON m.user_id = u.user_id + LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id + LEFT JOIN prompts p ON m.prompt_id = p.id WHERE m.meeting_id = %s ''' cursor.execute(query, (meeting_id,)) @@ -333,10 +337,13 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'], summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags, + prompt_name=meeting.get('prompt_name'), access_password=meeting.get('access_password') ) - if meeting['audio_file_path']: + # 只有路径长度大于5(排除空串或占位符)才认为有录音 + if meeting.get('audio_file_path') and len(meeting['audio_file_path']) > 5: meeting_data.audio_file_path = meeting['audio_file_path'] + meeting_data.audio_duration = meeting['audio_duration'] try: transcription_status_data = transcription_service.get_meeting_transcription_status(meeting_id) if transcription_status_data: diff --git a/backend/app/models/models.py b/backend/app/models/models.py index f2f4259..cba8c56 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -88,6 +88,8 @@ class Meeting(BaseModel): creator_id: int creator_username: str audio_file_path: Optional[str] = None + audio_duration: Optional[float] = None + prompt_name: Optional[str] = None transcription_status: Optional[TranscriptionTaskStatus] = None tags: Optional[List[Tag]] = [] access_password: Optional[str] = None diff --git a/frontend/src/pages/AdminDashboard.css b/frontend/src/pages/AdminDashboard.css index 57ca550..1e3784c 100644 --- a/frontend/src/pages/AdminDashboard.css +++ b/frontend/src/pages/AdminDashboard.css @@ -520,4 +520,62 @@ justify-content: flex-end; gap: 1rem; margin-top: 2rem; +} + +/* 会议详情模态框样式 */ +.meeting-details-info { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.detail-item { + display: flex; + align-items: flex-start; + line-height: 1.5; +} + +.detail-label { + font-weight: 600; + color: #64748b; + width: 80px; + flex-shrink: 0; +} + +.detail-value { + color: #1e293b; + flex: 1; +} + +.audio-item { + display: flex; + align-items: center; + margin-bottom: 0.5rem; +} + +.audio-item:last-child { + margin-bottom: 0; +} + +.download-link { + font-size: 0.875rem; + text-decoration: none; +} + +.download-link:hover { + text-decoration: underline; +} + +/* 弹窗内部加载样式 */ +.modal-body-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 0; + color: #64748b; +} + +.modal-body-loading .loading-spinner { + margin-bottom: 1rem; } \ No newline at end of file diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 2d0d245..a41b3da 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { LogOut, User, Users, Activity, Server, HardDrive, Cpu, MemoryStick, RefreshCw, UserX, ChevronDown, KeyRound, Shield, BookText, Waves, UserCog } from 'lucide-react'; +import { LogOut, User, Users, Activity, Server, HardDrive, Cpu, MemoryStick, RefreshCw, UserX, ChevronDown, KeyRound, Shield, BookText, Waves, UserCog, Search } from 'lucide-react'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import Dropdown from '../components/Dropdown'; @@ -7,6 +7,7 @@ import menuService from '../services/menuService'; import ConfirmDialog from '../components/ConfirmDialog'; import Toast from '../components/Toast'; import PageLoading from '../components/PageLoading'; +import FormModal from '../components/FormModal'; import './AdminDashboard.css'; // 常量定义 @@ -66,6 +67,11 @@ const AdminDashboard = ({ user, onLogout }) => { // Toast和确认对话框 const [toasts, setToasts] = useState([]); const [kickConfirmInfo, setKickConfirmInfo] = useState(null); + + // 会议详情模态框 + const [showMeetingModal, setShowMeetingModal] = useState(false); + const [meetingDetails, setMeetingDetails] = useState(null); + const [meetingLoading, setMeetingLoading] = useState(false); // Toast辅助函数 const showToast = (message, type = 'info') => { @@ -122,7 +128,7 @@ const AdminDashboard = ({ user, onLogout }) => { }, []); useEffect(() => { - if (autoRefresh) { + if (autoRefresh && !showMeetingModal) { const timer = setInterval(() => { setCountdown(prev => { if (prev <= 1) { @@ -134,7 +140,7 @@ const AdminDashboard = ({ user, onLogout }) => { }, 1000); return () => clearInterval(timer); } - }, [autoRefresh]); + }, [autoRefresh, showMeetingModal]); useEffect(() => { fetchTasks(); @@ -237,6 +243,27 @@ const AdminDashboard = ({ user, onLogout }) => { } }; + const handleViewMeeting = async (meetingId) => { + if (!meetingId) return; + setMeetingLoading(true); + setShowMeetingModal(true); + setMeetingDetails(null); // Clear previous + + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId))); + if (response.code === '200') { + setMeetingDetails(response.data); + } else { + showToast('获取会议详情失败', 'error'); + } + } catch (err) { + console.error('Fetch meeting details error:', err); + showToast('获取会议详情失败', 'error'); + } finally { + setMeetingLoading(false); + } + }; + if (loading && !stats) { return ; } @@ -561,7 +588,25 @@ const AdminDashboard = ({ user, onLogout }) => { {task.task_id} {getTaskTypeText(task.task_type)} - {task.meeting_title || '-'} + +
+ {task.meeting_id && task.task_type === 'transcription' && ( + + )} + {task.meeting_title || '-'} +
+ {task.creator_name || '-'} @@ -594,6 +639,63 @@ const AdminDashboard = ({ user, onLogout }) => { type="warning" /> + {/* 会议数据模态框 (使用标准 FormModal) */} + setShowMeetingModal(false)} + title="会议数据" + size="medium" + actions={ + + } + > + {meetingLoading ? ( +
+
+

正在获取会议数据...

+
+ ) : meetingDetails ? ( +
+
+ 会议名称: + {meetingDetails.title} +
+
+ 开始时间: + + {meetingDetails.meeting_time ? new Date(meetingDetails.meeting_time).toLocaleString() : '-'} + +
+
+ 使用模版: + {meetingDetails.prompt_name || '默认模版'} +
+
+ 音频信息: +
+ {meetingDetails.audio_file_path && meetingDetails.audio_file_path.length > 5 ? ( + <> + {meetingDetails.audio_duration ? `${Math.floor(meetingDetails.audio_duration / 60)}分${Math.floor(meetingDetails.audio_duration % 60)}秒` : '未知时长'} + + 下载 + + + ) : ( + 无音频 + )} +
+
+
+ ) : ( +
无法加载数据
+ )} +
+ {/* Toast notifications */} {toasts.map(toast => (