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
正在获取会议数据...
+