添加了会议数据下载

main
mula.liu 2026-01-22 15:23:28 +08:00
parent ffa4c80438
commit 7cd6ad144a
7 changed files with 179 additions and 9 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -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']

View File

@ -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:

View File

@ -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

View File

@ -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;
}

View File

@ -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 <PageLoading message="加载中..." />;
}
@ -561,7 +588,25 @@ const AdminDashboard = ({ user, onLogout }) => {
<tr key={`${task.task_type}-${task.task_id}`}>
<td>{task.task_id}</td>
<td>{getTaskTypeText(task.task_type)}</td>
<td>{task.meeting_title || '-'}</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{task.meeting_id && task.task_type === 'transcription' && (
<button
type="button"
className="btn-icon"
onClick={(e) => {
e.stopPropagation();
handleViewMeeting(task.meeting_id);
}}
title="查看详情"
style={{ padding: '4px', height: 'auto', width: 'auto', border: 'none', background: 'transparent', cursor: 'pointer', color: '#667eea' }}
>
<Search size={16} />
</button>
)}
<span>{task.meeting_title || '-'}</span>
</div>
</td>
<td>{task.creator_name || '-'}</td>
<td>
<span className={getStatusBadgeClass(task.status)}>
@ -594,6 +639,63 @@ const AdminDashboard = ({ user, onLogout }) => {
type="warning"
/>
{/* 会议数据模态框 (使用标准 FormModal) */}
<FormModal
isOpen={showMeetingModal}
onClose={() => setShowMeetingModal(false)}
title="会议数据"
size="medium"
actions={
<button className="btn btn-secondary" onClick={() => setShowMeetingModal(false)}>关闭</button>
}
>
{meetingLoading ? (
<div className="modal-body-loading">
<div className="loading-spinner"></div>
<p>正在获取会议数据...</p>
</div>
) : meetingDetails ? (
<div className="meeting-details-info">
<div className="detail-item">
<span className="detail-label">会议名称:</span>
<span className="detail-value">{meetingDetails.title}</span>
</div>
<div className="detail-item">
<span className="detail-label">开始时间:</span>
<span className="detail-value">
{meetingDetails.meeting_time ? new Date(meetingDetails.meeting_time).toLocaleString() : '-'}
</span>
</div>
<div className="detail-item">
<span className="detail-label">使用模版:</span>
<span className="detail-value">{meetingDetails.prompt_name || '默认模版'}</span>
</div>
<div className="detail-item">
<span className="detail-label">音频信息:</span>
<div className="detail-value" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
{meetingDetails.audio_file_path && meetingDetails.audio_file_path.length > 5 ? (
<>
<span>{meetingDetails.audio_duration ? `${Math.floor(meetingDetails.audio_duration / 60)}${Math.floor(meetingDetails.audio_duration % 60)}` : '未知时长'}</span>
<a
href={meetingDetails.audio_file_path.startsWith('http') ? meetingDetails.audio_file_path : `${apiClient.defaults.baseURL || ''}/${meetingDetails.audio_file_path.replace(/^\//, '')}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#3b82f6', textDecoration: 'underline', cursor: 'pointer' }}
>
下载
</a>
</>
) : (
<span style={{ color: '#94a3b8' }}>无音频</span>
)}
</div>
</div>
</div>
) : (
<div className="empty-state">无法加载数据</div>
)}
</FormModal>
{/* Toast notifications */}
{toasts.map(toast => (
<Toast