添加了会议数据下载
parent
ffa4c80438
commit
7cd6ad144a
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -521,3 +521,61 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
// 常量定义
|
||||
|
|
@ -67,6 +68,11 @@ const AdminDashboard = ({ user, onLogout }) => {
|
|||
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') => {
|
||||
const id = Date.now();
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue