添加了会议数据下载
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:
|
with get_db_connection() as connection:
|
||||||
cursor = connection.cursor(dictionary=True)
|
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,))
|
cursor.execute(query, (request_body.username,))
|
||||||
user = cursor.fetchone()
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
|
@ -70,6 +70,7 @@ def login(request_body: LoginRequest, request: Request):
|
||||||
user_id=user['user_id'],
|
user_id=user['user_id'],
|
||||||
username=user['username'],
|
username=user['username'],
|
||||||
caption=user['caption'],
|
caption=user['caption'],
|
||||||
|
avatar_url=user['avatar_url'],
|
||||||
email=user['email'],
|
email=user['email'],
|
||||||
token=token,
|
token=token,
|
||||||
role_id=user['role_id']
|
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)
|
cursor = connection.cursor(dictionary=True)
|
||||||
query = '''
|
query = '''
|
||||||
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags,
|
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.user_id as creator_id, u.caption as creator_username,
|
||||||
m.access_password
|
af.file_path as audio_file_path, af.duration as audio_duration,
|
||||||
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
|
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
|
WHERE m.meeting_id = %s
|
||||||
'''
|
'''
|
||||||
cursor.execute(query, (meeting_id,))
|
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'],
|
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
||||||
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
||||||
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
||||||
|
prompt_name=meeting.get('prompt_name'),
|
||||||
access_password=meeting.get('access_password')
|
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_file_path = meeting['audio_file_path']
|
||||||
|
meeting_data.audio_duration = meeting['audio_duration']
|
||||||
try:
|
try:
|
||||||
transcription_status_data = transcription_service.get_meeting_transcription_status(meeting_id)
|
transcription_status_data = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||||
if transcription_status_data:
|
if transcription_status_data:
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ class Meeting(BaseModel):
|
||||||
creator_id: int
|
creator_id: int
|
||||||
creator_username: str
|
creator_username: str
|
||||||
audio_file_path: Optional[str] = None
|
audio_file_path: Optional[str] = None
|
||||||
|
audio_duration: Optional[float] = None
|
||||||
|
prompt_name: Optional[str] = None
|
||||||
transcription_status: Optional[TranscriptionTaskStatus] = None
|
transcription_status: Optional[TranscriptionTaskStatus] = None
|
||||||
tags: Optional[List[Tag]] = []
|
tags: Optional[List[Tag]] = []
|
||||||
access_password: Optional[str] = None
|
access_password: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -521,3 +521,61 @@
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 2rem;
|
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 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 apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import Dropdown from '../components/Dropdown';
|
import Dropdown from '../components/Dropdown';
|
||||||
|
|
@ -7,6 +7,7 @@ import menuService from '../services/menuService';
|
||||||
import ConfirmDialog from '../components/ConfirmDialog';
|
import ConfirmDialog from '../components/ConfirmDialog';
|
||||||
import Toast from '../components/Toast';
|
import Toast from '../components/Toast';
|
||||||
import PageLoading from '../components/PageLoading';
|
import PageLoading from '../components/PageLoading';
|
||||||
|
import FormModal from '../components/FormModal';
|
||||||
import './AdminDashboard.css';
|
import './AdminDashboard.css';
|
||||||
|
|
||||||
// 常量定义
|
// 常量定义
|
||||||
|
|
@ -67,6 +68,11 @@ const AdminDashboard = ({ user, onLogout }) => {
|
||||||
const [toasts, setToasts] = useState([]);
|
const [toasts, setToasts] = useState([]);
|
||||||
const [kickConfirmInfo, setKickConfirmInfo] = useState(null);
|
const [kickConfirmInfo, setKickConfirmInfo] = useState(null);
|
||||||
|
|
||||||
|
// 会议详情模态框
|
||||||
|
const [showMeetingModal, setShowMeetingModal] = useState(false);
|
||||||
|
const [meetingDetails, setMeetingDetails] = useState(null);
|
||||||
|
const [meetingLoading, setMeetingLoading] = useState(false);
|
||||||
|
|
||||||
// Toast辅助函数
|
// Toast辅助函数
|
||||||
const showToast = (message, type = 'info') => {
|
const showToast = (message, type = 'info') => {
|
||||||
const id = Date.now();
|
const id = Date.now();
|
||||||
|
|
@ -122,7 +128,7 @@ const AdminDashboard = ({ user, onLogout }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoRefresh) {
|
if (autoRefresh && !showMeetingModal) {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCountdown(prev => {
|
setCountdown(prev => {
|
||||||
if (prev <= 1) {
|
if (prev <= 1) {
|
||||||
|
|
@ -134,7 +140,7 @@ const AdminDashboard = ({ user, onLogout }) => {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}
|
}
|
||||||
}, [autoRefresh]);
|
}, [autoRefresh, showMeetingModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTasks();
|
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) {
|
if (loading && !stats) {
|
||||||
return <PageLoading message="加载中..." />;
|
return <PageLoading message="加载中..." />;
|
||||||
}
|
}
|
||||||
|
|
@ -561,7 +588,25 @@ const AdminDashboard = ({ user, onLogout }) => {
|
||||||
<tr key={`${task.task_type}-${task.task_id}`}>
|
<tr key={`${task.task_type}-${task.task_id}`}>
|
||||||
<td>{task.task_id}</td>
|
<td>{task.task_id}</td>
|
||||||
<td>{getTaskTypeText(task.task_type)}</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>{task.creator_name || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={getStatusBadgeClass(task.status)}>
|
<span className={getStatusBadgeClass(task.status)}>
|
||||||
|
|
@ -594,6 +639,63 @@ const AdminDashboard = ({ user, onLogout }) => {
|
||||||
type="warning"
|
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 */}
|
{/* Toast notifications */}
|
||||||
{toasts.map(toast => (
|
{toasts.map(toast => (
|
||||||
<Toast
|
<Toast
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue