imetting/frontend/src/hooks/useAdminDashboardPage.js

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