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 (
{/* Header */}
iMeeting
欢迎,{userInfo?.caption}
} items={userMenus.map(menu => getMenuItemConfig(menu))} align="right" className="user-menu-dropdown" />
{/* 用户信息、统计和标签云一行布局 */}
{/* 左侧列:用户卡片和知识库入口 */}

{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 && (

修改密码

{passwordChangeError &&

{passwordChangeError}

} {passwordChangeSuccess &&

{passwordChangeSuccess}

}
setOldPassword(e.target.value)} required />
setNewPassword(e.target.value)} required />
setConfirmPassword(e.target.value)} required />
)} {/* 声纹采集模态框 */} setShowVoiceprintModal(false)} onSuccess={handleVoiceprintUpload} templateConfig={voiceprintTemplate} /> {/* 删除声纹确认对话框 */} setShowDeleteVoiceprintDialog(false)} onConfirm={handleDeleteVoiceprint} title="删除声纹" message="确定要删除声纹数据吗?删除后可以重新采集。" confirmText="删除" cancelText="取消" type="danger" /> {/* 回到顶部按钮 */} ); }; export default Dashboard;