imetting/frontend/src/hooks/useAdminDashboardPage.js

259 lines
7.7 KiB
JavaScript
Raw Normal View History

2026-04-08 11:19:33 +00:00
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);
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) => {
setMeetingLoading(true);
setShowMeetingModal(true);
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 {
2026-04-09 09:51:34 +00:00
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meetingId)));
2026-04-08 11:19:33 +00:00
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('下载失败');
}
};
2026-04-09 09:51:34 +00:00
const handleDownloadAudio = async (meetingId, audioFilePath) => {
try {
const response = await fetch(buildApiUrl(`${API_ENDPOINTS.MEETINGS.AUDIO(meetingId)}/stream`), {
credentials: 'include',
});
if (!response.ok) {
throw new Error(`audio download failed: ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const fileNameFromPath = audioFilePath?.split('/').pop();
const fallbackExtension = fileNameFromPath?.includes('.') ? '' : '.mp3';
link.href = url;
link.download = fileNameFromPath || `meeting_audio_${meetingId}${fallbackExtension}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('下载音频失败:', error);
message.error('下载音频失败');
}
};
2026-04-08 11:19:33 +00:00
const closeMeetingModal = () => {
setShowMeetingModal(false);
setMeetingDetails(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,
fetchAllData,
fetchOnlineUsers,
handleKickUser,
handleViewMeeting,
handleDownloadTranscript,
2026-04-09 09:51:34 +00:00
handleDownloadAudio,
2026-04-08 11:19:33 +00:00
closeMeetingModal,
taskCompletionRate,
};
}