import React, { useState, useEffect, useRef } from 'react';
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X, Library, BookText, Waves } from 'lucide-react';
import apiClient from '../utils/apiClient';
import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MeetingTimeline from '../components/MeetingTimeline';
import TagCloud from '../components/TagCloud';
import SimpleSearchInput from '../components/SimpleSearchInput';
import VoiceprintCollectionModal from '../components/VoiceprintCollectionModal';
import ConfirmDialog from '../components/ConfirmDialog';
import PageLoading from '../components/PageLoading';
import ScrollToTop from '../components/ScrollToTop';
import Dropdown from '../components/Dropdown';
import meetingCacheService from '../services/meetingCacheService';
import menuService from '../services/menuService';
import './Dashboard.css';
const Dashboard = ({ user, onLogout }) => {
const [userInfo, setUserInfo] = useState(null);
const [meetings, setMeetings] = useState([]);
const [meetingsStats, setMeetingsStats] = useState({ all_meetings: 0, created_meetings: 0, attended_meetings: 0 });
const [pagination, setPagination] = useState({ page: 1, total: 0, has_more: false });
const [selectedTags, setSelectedTags] = useState([]);
const [filterType, setFilterType] = useState('all'); // 'all', 'created', 'attended'
const [searchQuery, setSearchQuery] = useState(''); // 搜索关键词
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState('');
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordChangeError, setPasswordChangeError] = useState('');
const [passwordChangeSuccess, setPasswordChangeSuccess] = useState('');
// 声纹相关状态
const [voiceprintStatus, setVoiceprintStatus] = useState(null);
const [showVoiceprintModal, setShowVoiceprintModal] = useState(false);
const [voiceprintTemplate, setVoiceprintTemplate] = useState(null);
const [voiceprintLoading, setVoiceprintLoading] = useState(true);
const [showDeleteVoiceprintDialog, setShowDeleteVoiceprintDialog] = useState(false);
// 菜单权限相关状态
const [userMenus, setUserMenus] = useState([]);
useEffect(() => {
fetchUserData();
fetchMeetingsStats();
fetchVoiceprintData();
fetchUserMenus();
// 开发环境下,在控制台添加缓存调试命令
if (process.env.NODE_ENV === 'development') {
window.meetingCache = {
stats: () => meetingCacheService.getStats(),
clear: () => {
meetingCacheService.clearAll();
console.log('Cache cleared!');
},
info: () => {
const stats = meetingCacheService.getStats();
console.log('Meeting Cache Stats:', stats);
console.log(`- Cached filters: ${stats.filterCount}`);
console.log(`- Total pages: ${stats.totalPages}`);
console.log(`- Total meetings: ${stats.totalMeetings}`);
console.log(`- Cache size: ~${stats.cacheSize} KB`);
}
};
console.log('💡 Cache debug commands available: window.meetingCache.stats(), window.meetingCache.clear(), window.meetingCache.info()');
}
// 清理函数: 当组件卸载或用户切换时清空缓存
return () => {
console.log('Dashboard unmounting, clearing meeting cache');
meetingCacheService.clearAll();
};
}, [user.user_id]);
// 当筛选条件变化时,重新加载第一页
useEffect(() => {
fetchMeetings(1, false);
}, [selectedTags, filterType, searchQuery]);
const fetchVoiceprintData = async () => {
try {
setVoiceprintLoading(true);
// 获取声纹状态
const statusResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.STATUS(user.user_id)));
console.log('声纹状态响应:', statusResponse);
setVoiceprintStatus(statusResponse.data);
// 获取朗读模板
const templateResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.TEMPLATE));
console.log('朗读模板响应:', templateResponse);
setVoiceprintTemplate(templateResponse.data);
} catch (err) {
console.error('获取声纹数据失败:', err);
} finally {
setVoiceprintLoading(false);
}
};
const fetchUserMenus = async () => {
try {
console.log('[Dashboard] 开始获取用户菜单...');
const response = await menuService.getUserMenus();
console.log('[Dashboard] 菜单API响应:', response);
if (response.code === '200') {
const menus = response.data.menus || [];
console.log('[Dashboard] 用户菜单获取成功,菜单数量:', menus.length, '菜单内容:', menus);
setUserMenus(menus);
} else {
console.error('[Dashboard] 获取用户菜单失败:', response.message);
// 使用默认菜单作为fallback
setUserMenus(getDefaultMenus());
}
} catch (err) {
console.error('[Dashboard] 获取用户菜单异常:', err);
// 使用默认菜单作为fallback
setUserMenus(getDefaultMenus());
}
};
// 获取默认菜单(fallback)
const getDefaultMenus = () => {
const defaultMenus = [
{ menu_code: 'change_password', menu_name: '修改密码', menu_type: 'action', sort_order: 1 },
{ menu_code: 'prompt_management', menu_name: '提示词仓库', menu_type: 'link', menu_url: '/prompt-management', sort_order: 2 },
{ menu_code: 'logout', menu_name: '退出登录', menu_type: 'action', sort_order: 99 }
];
// 如果是管理员,添加平台管理菜单
if (user.role_id === 1) {
defaultMenus.splice(2, 0, {
menu_code: 'platform_admin',
menu_name: '平台管理',
menu_type: 'link',
menu_url: '/admin/management',
sort_order: 3
});
}
console.log('[Dashboard] 使用默认菜单:', defaultMenus);
return defaultMenus;
};
// 将菜单code映射到图标和行为
const getMenuItemConfig = (menu) => {
const iconMap = {
'change_password': ,
'prompt_management': ,
'platform_admin': ,
'logout':
};
const actionMap = {
'change_password': () => setShowChangePasswordModal(true),
'prompt_management': () => window.location.href = '/prompt-management',
'platform_admin': () => window.location.href = '/admin/management',
'logout': onLogout
};
return {
icon: iconMap[menu.menu_code] || null,
label: menu.menu_name,
onClick: menu.menu_type === 'link' && menu.menu_url
? () => window.location.href = menu.menu_url
: actionMap[menu.menu_code] || (() => {})
};
};
// 过滤会议
useEffect(() => {
fetchMeetings(1, false);
}, [selectedTags, filterType, searchQuery]);
const fetchMeetings = async (page = 1, isLoadMore = false) => {
try {
// 生成当前过滤器的键(包含user_id)
const filterKey = meetingCacheService.generateFilterKey(user.user_id, filterType, searchQuery, selectedTags);
// 如果不是加载更多,先检查是否有该过滤器的缓存
if (!isLoadMore) {
const allCached = meetingCacheService.getAllPages(filterKey);
if (allCached && allCached.pages[1]) {
console.log('Using cached data for filter:', filterKey);
// 恢复第一页数据
const firstPage = allCached.pages[1];
setMeetings(firstPage.meetings);
setPagination(firstPage.pagination);
return;
}
} else {
// 加载更多时,检查该页是否有缓存
const cachedPage = meetingCacheService.getPage(filterKey, page);
if (cachedPage) {
console.log('Using cached page:', page, 'for filter:', filterKey);
setMeetings(prev => [...prev, ...cachedPage.meetings]);
setPagination(cachedPage.pagination);
return;
}
}
// 没有缓存,从服务器获取
if (isLoadMore) {
setLoadingMore(true);
} else {
setLoading(true);
}
const params = {
user_id: user.user_id,
page: page,
filter_type: filterType,
search: searchQuery || undefined,
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined
};
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { params });
const newMeetings = response.data.meetings;
const newPagination = {
page: response.data.page,
total: response.data.total,
has_more: response.data.has_more
};
// 缓存当前页数据
meetingCacheService.setPage(filterKey, page, newMeetings, newPagination);
if (isLoadMore) {
// 加载更多:追加数据
setMeetings(prev => [...prev, ...newMeetings]);
} else {
// 新查询:替换数据
setMeetings(newMeetings);
}
setPagination(newPagination);
} catch (err) {
console.error('Error fetching meetings:', err);
setError('获取会议列表失败,请刷新重试');
} finally {
setLoading(false);
setLoadingMore(false);
}
};
const fetchMeetingsStats = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.STATS), {
params: { user_id: user.user_id }
});
setMeetingsStats(response.data);
} catch (err) {
console.error('Error fetching meetings stats:', err);
}
};
const handleLoadMore = () => {
if (!loadingMore && pagination.has_more) {
fetchMeetings(pagination.page + 1, true);
}
};
const handleTagClick = (tagName) => {
setSelectedTags(prev => {
if (prev.includes(tagName)) {
return prev.filter(tag => tag !== tagName);
} else {
return [...prev, tagName];
}
});
};
const handleFilterTypeChange = (type) => {
setFilterType(type);
};
const clearFilters = () => {
setSelectedTags([]);
setFilterType('all');
setSearchQuery('');
};
const fetchUserData = async () => {
try {
console.log('Fetching user data for user_id:', user.user_id);
const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
console.log('User response:', userResponse.data);
setUserInfo(userResponse.data);
} catch (err) {
console.error('Error fetching data:', err);
setError('获取数据失败,请刷新重试');
}
};
const handleDeleteMeeting = async (meetingId) => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
// 清除所有缓存,因为删除会影响统计和列表
meetingCacheService.clearAll();
// 刷新会议列表和统计
await fetchMeetings(1, false);
await fetchMeetingsStats();
} catch (err) {
console.error('Error deleting meeting:', err);
}
};
const handlePasswordChange = async (e) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
setPasswordChangeError('新密码不匹配');
return;
}
if (newPassword.length < 6) {
setPasswordChangeError('新密码长度不能少于6位');
return;
}
setPasswordChangeError('');
setPasswordChangeSuccess('');
try {
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE_PASSWORD(user.user_id)), {
old_password: oldPassword,
new_password: newPassword,
});
setPasswordChangeSuccess('密码修改成功!');
// 清空输入框并准备关闭模态框
setOldPassword('');
setNewPassword('');
setConfirmPassword('');
setTimeout(() => {
setShowChangePasswordModal(false);
setPasswordChangeSuccess('');
}, 2000);
} catch (err) {
setPasswordChangeError(err.response?.data?.message || '密码修改失败');
}
};
const handleVoiceprintUpload = async (formData) => {
try {
await apiClient.post(
buildApiUrl(API_ENDPOINTS.VOICEPRINT.UPLOAD(user.user_id)),
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
// 上传成功后刷新声纹状态并关闭模态框
await fetchVoiceprintData();
setShowVoiceprintModal(false);
} catch (err) {
throw new Error(err.response?.data?.message || '声纹上传失败');
}
};
const handleDeleteVoiceprint = async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.VOICEPRINT.DELETE(user.user_id)));
await fetchVoiceprintData();
} catch (err) {
console.error('删除声纹失败:', err);
}
};
const groupMeetingsByDate = (meetingsToGroup) => {
return meetingsToGroup.reduce((acc, meeting) => {
const date = new Date(meeting.meeting_time || meeting.created_at).toISOString().split('T')[0];
if (!acc[date]) {
acc[date] = [];
}
acc[date].push(meeting);
return acc;
}, {});
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
if (loading && meetings.length === 0) {
return ;
}
if (error) {
return (
{error}
);
}
const groupedMeetings = groupMeetingsByDate(meetings);
// 使用统计数据
const createdMeetings = meetingsStats.created_meetings;
const attendedMeetings = meetingsStats.attended_meetings;
const allMeetings = meetingsStats.all_meetings;
return (
{/* 用户信息、统计和标签云一行布局 */}
{/* 左侧列:用户卡片和知识库入口 */}
{userInfo?.caption}
{/* 声纹采集按钮 - 放在姓名后 */}
{!voiceprintLoading && (
<>
{voiceprintStatus?.has_voiceprint ? (
setShowDeleteVoiceprintDialog(true)}
title="点击删除声纹"
>
声纹
) : (
)}
>
)}
{userInfo?.email}
加入时间:{formatDate(userInfo?.created_at)}
{/* 知识库入口卡片 */}
{/* 统一的统计卡片 */}
会议统计
handleFilterTypeChange('created')}
>
我创建的会议
{createdMeetings}
handleFilterTypeChange('attended')}
>
我参加的会议
{attendedMeetings}
handleFilterTypeChange('all')}
>
全部会议
{allMeetings}
{/* 搜索和标签过滤卡片 */}
{/* Meetings Timeline Section */}
{showChangePasswordModal && (
)}
{/* 声纹采集模态框 */}
setShowVoiceprintModal(false)}
onSuccess={handleVoiceprintUpload}
templateConfig={voiceprintTemplate}
/>
{/* 删除声纹确认对话框 */}
setShowDeleteVoiceprintDialog(false)}
onConfirm={handleDeleteVoiceprint}
title="删除声纹"
message="确定要删除声纹数据吗?删除后可以重新采集。"
confirmText="删除"
cancelText="取消"
type="danger"
/>
{/* 回到顶部按钮 */}
);
};
export default Dashboard;