imetting/frontend/src/pages/AdminDashboard.jsx

611 lines
22 KiB
React
Raw Normal View History

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 apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import Dropdown from '../components/Dropdown';
import menuService from '../services/menuService';
import ConfirmDialog from '../components/ConfirmDialog';
import Toast from '../components/Toast';
import PageLoading from '../components/PageLoading';
import './AdminDashboard.css';
// 常量定义
const AUTO_REFRESH_INTERVAL = 30; // 自动刷新间隔(秒)
const STATUS_BADGE_MAP = {
'pending': 'status-badge status-pending',
'processing': 'status-badge status-processing',
'completed': 'status-badge status-completed',
'failed': 'status-badge status-failed'
};
const STATUS_TEXT_MAP = {
'pending': '待处理',
'processing': '处理中',
'completed': '已完成',
'failed': '失败'
};
const TASK_TYPE_TEXT_MAP = {
'transcription': '转录',
'summary': '总结',
'knowledge_base': '知识库'
};
// 辅助函数
const getStatusBadgeClass = (status) => STATUS_BADGE_MAP[status] || 'status-badge';
const getStatusText = (status) => STATUS_TEXT_MAP[status] || status;
const getTaskTypeText = (type) => TASK_TYPE_TEXT_MAP[type] || type;
// 默认管理员菜单
const getDefaultMenus = () => [
{ menu_code: 'account_settings', menu_name: '账户设置', menu_type: 'link', menu_url: '/account-settings' },
{ menu_code: 'prompt_management', menu_name: '提示词仓库', menu_type: 'link', menu_url: '/prompts' },
{ menu_code: 'platform_admin', menu_name: '平台管理', menu_type: 'link', menu_url: '/admin' },
{ menu_code: 'logout', menu_name: '退出登录', menu_type: 'action' }
];
const AdminDashboard = ({ user, onLogout }) => {
// 统计数据
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 [error, setError] = useState('');
// 菜单权限相关状态
const [userMenus, setUserMenus] = useState([]);
// 任务筛选
const [taskType, setTaskType] = useState('all');
const [taskStatus, setTaskStatus] = useState('all');
// 自动刷新定时器
const [autoRefresh, setAutoRefresh] = useState(true);
const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL);
// Toast和确认对话框
const [toasts, setToasts] = useState([]);
const [kickConfirmInfo, setKickConfirmInfo] = useState(null);
// Toast辅助函数
const showToast = (message, type = 'info') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
};
const removeToast = (id) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
// 获取菜单配置
const getMenuItemConfig = (menu) => {
const iconMap = {
'change_password': <KeyRound size={16} />,
'account_settings': <UserCog size={16} />,
'prompt_management': <BookText size={16} />,
'platform_admin': <Shield size={16} />,
'logout': <LogOut size={16} />
};
const actionMap = {
'logout': onLogout
};
return {
icon: iconMap[menu.menu_code],
label: menu.menu_name,
onClick: menu.menu_type === 'link' && menu.menu_url
? () => window.location.href = menu.menu_url
: actionMap[menu.menu_code]
};
};
// 获取用户菜单权限
const fetchUserMenus = async () => {
try {
const response = await menuService.getUserMenus();
if (response.code === '200') {
setUserMenus(response.data.menus || []);
} else {
setUserMenus(getDefaultMenus());
}
} catch (error) {
console.error('Error fetching user menus:', error);
setUserMenus(getDefaultMenus());
}
};
// 初始化和自动刷新
useEffect(() => {
fetchAllData();
fetchUserMenus();
}, []);
useEffect(() => {
if (autoRefresh) {
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
fetchAllData();
return AUTO_REFRESH_INTERVAL;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [autoRefresh]);
useEffect(() => {
fetchTasks();
}, [taskType, taskStatus]);
const fetchAllData = async () => {
try {
setLoading(true);
await Promise.all([
fetchStats(),
fetchOnlineUsers(),
fetchUsersList(),
fetchTasks(),
fetchResources()
]);
setError('');
} catch (err) {
console.error('获取数据失败:', err);
setError('加载数据失败,请稍后重试');
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS));
if (response.code === '200') {
setStats(response.data);
}
} catch (err) {
console.error('获取统计数据失败:', err);
}
};
const fetchOnlineUsers = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS));
if (response.code === '200') {
setOnlineUsers(response.data.users || []);
}
} catch (err) {
console.error('获取在线用户失败:', err);
}
};
const fetchUsersList = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.USER_STATS));
if (response.code === '200') {
setUsersList(response.data.users || []);
}
} catch (err) {
console.error('获取用户列表失败:', err);
}
};
const fetchTasks = async () => {
try {
const params = new URLSearchParams();
if (taskType !== 'all') params.append('task_type', taskType);
if (taskStatus !== 'all') params.append('status', taskStatus);
params.append('limit', '20');
const url = `${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`;
const response = await apiClient.get(buildApiUrl(url));
if (response.code === '200') {
setTasks(response.data.tasks || []);
}
} catch (err) {
console.error('获取任务列表失败:', err);
}
};
const fetchResources = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES));
if (response.code === '200') {
setResources(response.data);
}
} catch (err) {
console.error('获取系统资源失败:', err);
}
};
const handleKickUser = async () => {
try {
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.KICK_USER(kickConfirmInfo.user_id)));
if (response.code === '200') {
showToast('用户已被踢出', 'success');
fetchOnlineUsers();
} else {
showToast(`踢出失败: ${response.message}`, 'error');
}
} catch (err) {
console.error('踢出用户失败:', err);
showToast('踢出用户失败', 'error');
} finally {
setKickConfirmInfo(null);
}
};
if (loading && !stats) {
return <PageLoading message="加载中..." />;
}
return (
<div className="admin-dashboard">
{/* 顶部导航栏 - 使用与普通Dashboard一致的样式 */}
<div className="dashboard-header">
<div className="header-content">
<div className="logo">
<Waves size={32} className="logo-icon" />
<span>管理面板</span>
</div>
<div className="user-actions">
<button
className={`auto-refresh-toggle ${autoRefresh ? 'active' : ''}`}
onClick={() => setAutoRefresh(!autoRefresh)}
title={autoRefresh ? '关闭自动刷新' : '开启自动刷新'}
>
<RefreshCw size={16} />
{autoRefresh ? '自动刷新' : '手动刷新'}
</button>
<Dropdown
trigger={
<div className="user-menu-trigger">
{user.avatar_url ? (
<img
src={user.avatar_url.startsWith('http') ? user.avatar_url : `${apiClient.defaults.baseURL || ''}${user.avatar_url}`}
alt={user.caption}
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
objectFit: 'cover',
border: '1px solid #e2e8f0'
}}
/>
) : (
<User size={20} />
)}
<span>{user.caption}</span>
<ChevronDown size={16} />
</div>
}
items={userMenus.map(menu => getMenuItemConfig(menu))}
/>
</div>
</div>
</div>
{/* Dashboard Content */}
<div className="dashboard-content">
{error && <div className="error-message">{error}</div>}
{/* 统计卡片 */}
{stats && (
<div className="stats-grid">
{/* 用户统计 */}
<div className="stat-card">
<div className="stat-icon" style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<Users size={24} />
</div>
<div className="stat-content">
<h3>用户统计</h3>
<div className="stat-number">{stats.users.total}</div>
<div className="stat-details">
<span>今日新增: {stats.users.today_new}</span>
<span>在线: {stats.users.online}</span>
</div>
</div>
</div>
{/* 会议统计 */}
<div className="stat-card">
<div className="stat-icon" style={{ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }}>
<Activity size={24} />
</div>
<div className="stat-content">
<h3>会议统计</h3>
<div className="stat-number">{stats.meetings.total}</div>
<div className="stat-details">
<span>今日新增: {stats.meetings.today_new}</span>
</div>
</div>
</div>
{/* 存储统计 */}
<div className="stat-card">
<div className="stat-icon" style={{ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }}>
<HardDrive size={24} />
</div>
<div className="stat-content">
<h3>存储统计</h3>
<div className="stat-number">{stats.storage.audio_total_size_gb} GB</div>
<div className="stat-details">
<span>音频文件: {stats.storage.audio_files_count} </span>
</div>
</div>
</div>
{/* 服务器资源 */}
{resources && (
<div className="stat-card">
<div className="stat-icon" style={{ background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }}>
<Server size={24} />
</div>
<div className="stat-content">
<h3>服务器资源</h3>
<div className="resource-bars">
<div className="resource-item">
<Cpu size={14} />
<div className="resource-bar">
<div className="resource-fill" style={{ width: `${resources.cpu.percent}%` }}></div>
</div>
<span>{resources.cpu.percent}%</span>
</div>
<div className="resource-item">
<MemoryStick size={14} />
<div className="resource-bar">
<div className="resource-fill" style={{ width: `${resources.memory.percent}%` }}></div>
</div>
<span>{resources.memory.percent}%</span>
</div>
<div className="resource-item">
<HardDrive size={14} />
<div className="resource-bar">
<div className="resource-fill" style={{ width: `${resources.disk.percent}%` }}></div>
</div>
<span>{resources.disk.percent}%</span>
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* 任务统计 */}
{stats && (
<div className="tasks-overview">
<h2>任务概览</h2>
<div className="task-stats-grid">
<div className="task-stat-card">
<h3>转录任务</h3>
<div className="task-stat-numbers">
<div className="task-stat-item">
<span className="task-stat-label">进行中</span>
<span className="task-stat-value running">{stats.tasks.transcription.running}</span>
</div>
<div className="task-stat-item">
<span className="task-stat-label">已完成</span>
<span className="task-stat-value completed">{stats.tasks.transcription.completed}</span>
</div>
<div className="task-stat-item">
<span className="task-stat-label">失败</span>
<span className="task-stat-value failed">{stats.tasks.transcription.failed}</span>
</div>
</div>
</div>
<div className="task-stat-card">
<h3>总结任务</h3>
<div className="task-stat-numbers">
<div className="task-stat-item">
<span className="task-stat-label">进行中</span>
<span className="task-stat-value running">{stats.tasks.summary.running}</span>
</div>
<div className="task-stat-item">
<span className="task-stat-label">已完成</span>
<span className="task-stat-value completed">{stats.tasks.summary.completed}</span>
</div>
<div className="task-stat-item">
<span className="task-stat-label">失败</span>
<span className="task-stat-value failed">{stats.tasks.summary.failed}</span>
</div>
</div>
</div>
<div className="task-stat-card">
<h3>知识库任务</h3>
<div className="task-stat-numbers">
<div className="task-stat-item">
<span className="task-stat-label">进行中</span>
<span className="task-stat-value running">{stats.tasks.knowledge_base.running}</span>
</div>
<div className="task-stat-item">
<span className="task-stat-label">已完成</span>
<span className="task-stat-value completed">{stats.tasks.knowledge_base.completed}</span>
</div>
<div className="task-stat-item">
<span className="task-stat-label">失败</span>
<span className="task-stat-value failed">{stats.tasks.knowledge_base.failed}</span>
</div>
</div>
</div>
</div>
</div>
)}
{/* 用户列表 */}
<div className="admin-panel full-width">
<div className="panel-header">
<h2>用户列表 ({usersList.length})</h2>
</div>
<div className="panel-content">
{usersList.length === 0 ? (
<div className="empty-state">暂无用户数据</div>
) : (
<table className="users-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>姓名</th>
<th>注册时间</th>
<th>最新登录时间</th>
<th>会议数量</th>
<th>会议时长</th>
</tr>
</thead>
<tbody>
{usersList.map(u => (
<tr key={u.user_id}>
<td>{u.user_id}</td>
<td>{u.username}</td>
<td>{u.caption}</td>
<td>{u.created_at ? new Date(u.created_at).toLocaleString('zh-CN') : '-'}</td>
<td>{u.last_login_time ? new Date(u.last_login_time).toLocaleString('zh-CN') : '-'}</td>
<td>{u.meeting_count || 0}</td>
<td>{u.total_duration_formatted || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
<div className="admin-content-grid">
{/* 在线用户列表 */}
<div className="admin-panel">
<div className="panel-header">
<h2>在线用户 ({onlineUsers.length})</h2>
</div>
<div className="panel-content">
{onlineUsers.length === 0 ? (
<div className="empty-state">暂无在线用户</div>
) : (
<table className="online-users-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>姓名</th>
<th>会话数</th>
<th>剩余时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{onlineUsers.map(u => (
<tr key={u.user_id}>
<td>{u.user_id}</td>
<td>{u.username}</td>
<td>{u.caption}</td>
<td>{u.token_count}</td>
<td>{u.ttl_hours}h</td>
<td>
<button
className="kick-btn"
onClick={() => setKickConfirmInfo(u)}
title="踢出用户"
>
<UserX size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{/* 任务监控 */}
<div className="admin-panel">
<div className="panel-header">
<h2>任务监控</h2>
<div className="panel-filters">
<select value={taskType} onChange={(e) => setTaskType(e.target.value)}>
<option value="all">全部类型</option>
<option value="transcription">转录</option>
<option value="summary">总结</option>
<option value="knowledge_base">知识库</option>
</select>
<select value={taskStatus} onChange={(e) => setTaskStatus(e.target.value)}>
<option value="all">全部状态</option>
<option value="running">进行中</option>
<option value="completed">已完成</option>
<option value="failed">失败</option>
</select>
</div>
</div>
<div className="panel-content">
{tasks.length === 0 ? (
<div className="empty-state">暂无任务</div>
) : (
<table className="tasks-table">
<thead>
<tr>
<th>任务ID</th>
<th>类型</th>
<th>关联对象</th>
<th>创建者</th>
<th>状态</th>
<th>进度</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{tasks.map(task => (
<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>{task.creator_name || '-'}</td>
<td>
<span className={getStatusBadgeClass(task.status)}>
{getStatusText(task.status)}
</span>
</td>
<td>
{task.progress !== null ? `${task.progress}%` : '-'}
</td>
<td>{new Date(task.created_at).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
</div>
{/* 踢出用户确认对话框 */}
<ConfirmDialog
isOpen={!!kickConfirmInfo}
onClose={() => setKickConfirmInfo(null)}
onConfirm={handleKickUser}
title="踢出用户"
message={`确定要踢出用户"${kickConfirmInfo?.caption}"吗?该用户将被强制下线。`}
confirmText="确定踢出"
cancelText="取消"
type="warning"
/>
{/* Toast notifications */}
{toasts.map(toast => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)}
/>
))}
</div>
);
};
export default AdminDashboard;