300 lines
9.1 KiB
JavaScript
300 lines
9.1 KiB
JavaScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { App } from 'antd';
|
|
import apiClient from '../utils/apiClient';
|
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
|
|
|
const AUTO_REFRESH_INTERVAL = 30;
|
|
|
|
export default function useAdminDashboardPage() {
|
|
const { message, modal } = App.useApp();
|
|
const inFlightRef = useRef(false);
|
|
const mountedRef = useRef(true);
|
|
|
|
const [stats, setStats] = useState(null);
|
|
const [onlineUsers, setOnlineUsers] = useState([]);
|
|
const [usersList, setUsersList] = useState([]);
|
|
const [tasks, setTasks] = useState([]);
|
|
const [resources, setResources] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [taskLoading, setTaskLoading] = useState(false);
|
|
const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
|
|
|
|
const [taskType, setTaskType] = useState('all');
|
|
const [taskStatus, setTaskStatus] = useState('all');
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL);
|
|
|
|
const [showMeetingModal, setShowMeetingModal] = useState(false);
|
|
const [meetingDetails, setMeetingDetails] = useState(null);
|
|
const [meetingLoading, setMeetingLoading] = useState(false);
|
|
const [selectedTaskRecord, setSelectedTaskRecord] = useState(null);
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true;
|
|
return () => {
|
|
mountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
const fetchStats = useCallback(async () => {
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS));
|
|
if (response.code === '200') {
|
|
setStats(response.data);
|
|
}
|
|
}, []);
|
|
|
|
const fetchOnlineUsers = useCallback(async () => {
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS));
|
|
if (response.code === '200') {
|
|
setOnlineUsers(response.data.users || []);
|
|
}
|
|
}, []);
|
|
|
|
const fetchUsersList = useCallback(async () => {
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.USER_STATS));
|
|
if (response.code === '200') {
|
|
setUsersList(response.data.users || []);
|
|
}
|
|
}, []);
|
|
|
|
const fetchTasks = useCallback(async () => {
|
|
try {
|
|
setTaskLoading(true);
|
|
const params = new URLSearchParams();
|
|
if (taskType !== 'all') params.append('task_type', taskType);
|
|
if (taskStatus !== 'all') params.append('status', taskStatus);
|
|
params.append('limit', '20');
|
|
|
|
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`));
|
|
if (response.code === '200') {
|
|
setTasks(response.data.tasks || []);
|
|
}
|
|
} finally {
|
|
setTaskLoading(false);
|
|
}
|
|
}, [taskStatus, taskType]);
|
|
|
|
const fetchResources = useCallback(async () => {
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES));
|
|
if (response.code === '200') {
|
|
setResources(response.data);
|
|
}
|
|
}, []);
|
|
|
|
const fetchAllData = useCallback(async ({ silent = false } = {}) => {
|
|
if (inFlightRef.current) {
|
|
return;
|
|
}
|
|
inFlightRef.current = true;
|
|
|
|
try {
|
|
if (!silent && mountedRef.current) {
|
|
setLoading(true);
|
|
}
|
|
await Promise.all([fetchStats(), fetchOnlineUsers(), fetchUsersList(), fetchTasks(), fetchResources()]);
|
|
if (mountedRef.current) {
|
|
setLastUpdatedAt(new Date());
|
|
setCountdown(AUTO_REFRESH_INTERVAL);
|
|
}
|
|
} catch (error) {
|
|
console.error('获取数据失败:', error);
|
|
if (mountedRef.current && !silent) {
|
|
message.error('加载数据失败,请稍后重试');
|
|
}
|
|
} finally {
|
|
inFlightRef.current = false;
|
|
if (mountedRef.current && !silent) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}, [fetchOnlineUsers, fetchResources, fetchStats, fetchTasks, fetchUsersList, message]);
|
|
|
|
useEffect(() => {
|
|
fetchAllData();
|
|
}, [fetchAllData]);
|
|
|
|
useEffect(() => {
|
|
if (!autoRefresh || showMeetingModal) {
|
|
return undefined;
|
|
}
|
|
|
|
const timer = setInterval(() => {
|
|
setCountdown((prev) => {
|
|
if (prev <= 1) {
|
|
fetchAllData({ silent: true });
|
|
return AUTO_REFRESH_INTERVAL;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
|
|
return () => clearInterval(timer);
|
|
}, [autoRefresh, fetchAllData, showMeetingModal]);
|
|
|
|
useEffect(() => {
|
|
fetchTasks();
|
|
}, [fetchTasks]);
|
|
|
|
const handleKickUser = (user) => {
|
|
modal.confirm({
|
|
title: '踢出用户',
|
|
content: `确定要踢出用户"${user.caption}"吗?该用户将被强制下线。`,
|
|
okText: '确定',
|
|
okType: 'danger',
|
|
cancelText: '取消',
|
|
onOk: async () => {
|
|
try {
|
|
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.KICK_USER(user.user_id)));
|
|
if (response.code === '200') {
|
|
message.success('用户已被踢出');
|
|
fetchOnlineUsers();
|
|
}
|
|
} catch {
|
|
message.error('踢出用户失败');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleViewMeeting = async (meetingId, taskRecord = null) => {
|
|
setMeetingLoading(true);
|
|
setShowMeetingModal(true);
|
|
setSelectedTaskRecord(taskRecord);
|
|
try {
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
|
if (response.code === '200') {
|
|
setMeetingDetails(response.data);
|
|
}
|
|
} catch {
|
|
message.error('获取会议详情失败');
|
|
} finally {
|
|
setMeetingLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDownloadTranscript = async (meetingId) => {
|
|
try {
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meetingId)));
|
|
if (response.code === '200') {
|
|
const dataStr = JSON.stringify(response.data, null, 2);
|
|
const blob = new Blob([dataStr], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `transcript_${meetingId}.json`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
} catch {
|
|
message.error('下载失败');
|
|
}
|
|
};
|
|
|
|
const downloadResourceByPath = useCallback((resourcePath, fallbackFileName) => {
|
|
const normalizedPath = typeof resourcePath === 'string' ? resourcePath.trim() : '';
|
|
if (!normalizedPath) {
|
|
message.error('文件路径不存在');
|
|
return;
|
|
}
|
|
|
|
const href = buildApiUrl(normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`);
|
|
const fileNameFromPath = normalizedPath.split('/').pop();
|
|
const link = document.createElement('a');
|
|
link.href = href;
|
|
link.download = fileNameFromPath || fallbackFileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}, [message]);
|
|
|
|
const handleDownloadAudio = (meetingId, audioFilePath) => {
|
|
try {
|
|
downloadResourceByPath(audioFilePath, `meeting_audio_${meetingId}.mp3`);
|
|
} catch (error) {
|
|
console.error('下载音频失败:', error);
|
|
message.error('下载音频失败');
|
|
}
|
|
};
|
|
|
|
const handleDownloadSummaryResult = (taskId, resultPath) => {
|
|
try {
|
|
downloadResourceByPath(resultPath, `summary_${taskId}.md`);
|
|
} catch (error) {
|
|
console.error('下载总结结果失败:', error);
|
|
message.error('下载总结结果失败');
|
|
}
|
|
};
|
|
|
|
const handleRetryTask = async (taskRecord) => {
|
|
if (!taskRecord?.task_id || !taskRecord?.task_type) {
|
|
message.error('任务信息不完整');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.post(
|
|
buildApiUrl(API_ENDPOINTS.ADMIN.TASK_RETRY(taskRecord.task_type, taskRecord.task_id))
|
|
);
|
|
message.success(response.message || '任务已提交重试');
|
|
await fetchTasks();
|
|
|
|
if (selectedTaskRecord?.task_id === taskRecord.task_id) {
|
|
setSelectedTaskRecord((prev) => (prev ? {
|
|
...prev,
|
|
task_id: response.data?.task_id || prev.task_id,
|
|
status: response.data?.status || prev.status,
|
|
progress: response.data?.progress ?? prev.progress,
|
|
} : prev));
|
|
}
|
|
} catch (error) {
|
|
message.error(error?.response?.data?.message || '任务重试失败');
|
|
}
|
|
};
|
|
|
|
const closeMeetingModal = () => {
|
|
setShowMeetingModal(false);
|
|
setMeetingDetails(null);
|
|
setSelectedTaskRecord(null);
|
|
};
|
|
|
|
const taskCompletionRate = useMemo(() => {
|
|
const all = tasks.length || 1;
|
|
const completed = tasks.filter((item) => item.status === 'completed').length;
|
|
return Math.round((completed / all) * 100);
|
|
}, [tasks]);
|
|
|
|
return {
|
|
stats,
|
|
onlineUsers,
|
|
usersList,
|
|
tasks,
|
|
resources,
|
|
loading,
|
|
taskLoading,
|
|
lastUpdatedAt,
|
|
taskType,
|
|
setTaskType,
|
|
taskStatus,
|
|
setTaskStatus,
|
|
autoRefresh,
|
|
setAutoRefresh,
|
|
countdown,
|
|
showMeetingModal,
|
|
meetingDetails,
|
|
meetingLoading,
|
|
selectedTaskRecord,
|
|
fetchAllData,
|
|
fetchOnlineUsers,
|
|
handleKickUser,
|
|
handleViewMeeting,
|
|
handleDownloadTranscript,
|
|
handleDownloadAudio,
|
|
handleDownloadSummaryResult,
|
|
handleRetryTask,
|
|
closeMeetingModal,
|
|
taskCompletionRate,
|
|
};
|
|
}
|