- {tagStats.map((tag, index) => {
- const size = getTagSize(tag.count, maxCount);
+ {allTags.map((tag) => {
const isSelected = selectedTags.includes(tag.name);
return (
);
})}
diff --git a/src/config/api.js b/src/config/api.js
index df8435e..e1be57f 100644
--- a/src/config/api.js
+++ b/src/config/api.js
@@ -21,6 +21,7 @@ const API_CONFIG = {
},
MEETINGS: {
LIST: '/api/meetings',
+ STATS: '/api/meetings/stats',
DETAIL: (meetingId) => `/api/meetings/${meetingId}`,
EDIT: (meetingId) => `/api/meetings/${meetingId}/edit`,
CREATE: '/api/meetings',
diff --git a/src/pages/Dashboard.css b/src/pages/Dashboard.css
index 25fc74f..bcb022b 100644
--- a/src/pages/Dashboard.css
+++ b/src/pages/Dashboard.css
@@ -91,24 +91,53 @@
height: 100%;
}
+/* 统计卡片 */
+.unified-stats-card {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ border: 1px solid #e2e8f0;
+}
+
+/* 筛选卡片整合搜索框和标签云 */
+.filter-card-wrapper {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ border: 1px solid #e2e8f0;
+ display: flex;
+ flex-direction: column;
+}
+
+.filter-card-search {
+ padding: 1.5rem 1.5rem 1rem 1.5rem;
+ border-bottom: 1px solid #f1f5f9;
+}
+
+.filter-card-tags {
+ flex: 1;
+}
+
+.filter-card-tags .tag-cloud-container {
+ background: transparent;
+ box-shadow: none;
+ border: none;
+ padding: 1rem 1.5rem 1.5rem 1.5rem;
+}
+
.user-card {
background: white;
- padding: 1rem 1.5rem;
+ padding: 1.75rem 1.5rem;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
- gap: 1rem;
+ gap: 1.25rem;
border: 1px solid #e2e8f0;
flex: 3;
}
-.tag-cloud-wrapper {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
.filter-result-bar {
background: white;
padding: 1rem 1.5rem;
@@ -147,8 +176,8 @@
}
.user-avatar {
- width: 40px;
- height: 40px;
+ width: 50px;
+ height: 50px;
background: linear-gradient(45deg, #667eea, #764ba2);
border-radius: 50%;
display: flex;
@@ -166,25 +195,25 @@
display: flex;
align-items: center;
gap: 0.75rem;
- margin-bottom: 0.15rem;
+ margin-bottom: 0.35rem;
}
.user-details h2 {
margin: 0;
color: #1e293b;
- font-size: 1.1rem;
+ font-size: 1.25rem;
font-weight: 600;
}
.user-email {
color: #64748b;
- margin: 0 0 0.15rem 0;
- font-size: 0.8rem;
+ margin: 0 0 0.25rem 0;
+ font-size: 0.875rem;
}
.join-date {
color: #94a3b8;
- font-size: 0.7rem;
+ font-size: 0.75rem;
margin: 0;
}
@@ -679,11 +708,11 @@
.kb-entry-card {
display: flex;
align-items: center;
- gap: 1rem;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- padding: 1rem 1.5rem;
+ gap: 0.75rem;
+ background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
+ padding: 0.75rem 1rem;
border-radius: 12px;
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
+ box-shadow: 0 2px 8px rgba(6, 182, 212, 0.25);
transition: all 0.3s ease;
text-decoration: none;
position: relative;
@@ -709,15 +738,15 @@
.kb-entry-card:hover {
transform: translateY(-2px);
- box-shadow: 0 4px 16px rgba(102, 126, 234, 0.35);
+ box-shadow: 0 4px 16px rgba(6, 182, 212, 0.35);
text-decoration: none;
}
.kb-entry-icon {
- width: 48px;
- height: 48px;
+ width: 36px;
+ height: 36px;
background: rgba(255, 255, 255, 0.2);
- border-radius: 12px;
+ border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
@@ -731,16 +760,16 @@
}
.kb-entry-content h2 {
- margin: 0 0 0.25rem 0;
+ margin: 0 0 0.15rem 0;
color: white;
- font-size: 1.1rem;
+ font-size: 0.95rem;
font-weight: 700;
}
.kb-entry-content p {
margin: 0;
color: rgba(255, 255, 255, 0.9);
- font-size: 0.85rem;
+ font-size: 0.75rem;
font-weight: 400;
}
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index cda0f28..a66aef2 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -5,18 +5,23 @@ import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MeetingTimeline from '../components/MeetingTimeline';
import TagCloud from '../components/TagCloud';
+import ExpandSearchBox from '../components/ExpandSearchBox';
import VoiceprintCollectionModal from '../components/VoiceprintCollectionModal';
import ConfirmDialog from '../components/ConfirmDialog';
import PageLoading from '../components/PageLoading';
+import meetingCacheService from '../services/meetingCacheService';
import './Dashboard.css';
const Dashboard = ({ user, onLogout }) => {
const [userInfo, setUserInfo] = useState(null);
- const [meetings, setMeetings] = useState(null);
- const [filteredMeetings, setFilteredMeetings] = useState([]);
+ 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 [dropdownOpen, setDropdownOpen] = useState(false);
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
@@ -36,9 +41,35 @@ const Dashboard = ({ user, onLogout }) => {
useEffect(() => {
fetchUserData();
+ fetchMeetingsStats();
fetchVoiceprintData();
+
+ // 开发环境下,在控制台添加缓存调试命令
+ 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()');
+ }
}, [user.user_id]);
+ // 当筛选条件变化时,重新加载第一页
+ useEffect(() => {
+ fetchMeetings(1, false);
+ }, [selectedTags, filterType, searchQuery]);
+
const fetchVoiceprintData = async () => {
try {
setVoiceprintLoading(true);
@@ -61,30 +92,97 @@ const Dashboard = ({ user, onLogout }) => {
// 过滤会议
useEffect(() => {
- filterMeetings();
- }, [meetings, selectedTags, filterType]);
+ fetchMeetings(1, false);
+ }, [selectedTags, filterType, searchQuery]);
- const filterMeetings = () => {
- if (!meetings) return;
- let filtered = [...meetings];
+ const fetchMeetings = async (page = 1, isLoadMore = false) => {
+ try {
+ // 生成当前过滤器的键
+ const filterKey = meetingCacheService.generateFilterKey(filterType, searchQuery, selectedTags);
- // 根据创建/参与类型过滤
- if (filterType === 'created') {
- filtered = filtered.filter(meeting => String(meeting.creator_id) === String(user.user_id));
- } else if (filterType === 'attended') {
- filtered = filtered.filter(meeting => String(meeting.creator_id) !== String(user.user_id));
+ // 如果不是加载更多,先检查是否有该过滤器的缓存
+ 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);
}
+ };
- // 根据选中的标签过滤
- if (selectedTags.length > 0) {
- filtered = filtered.filter(meeting => {
- if (!meeting.tags || meeting.tags.length === 0) return false;
- const meetingTags = meeting.tags.map(tag => typeof tag === 'string' ? tag : tag.name);
- return selectedTags.some(selectedTag => meetingTags.includes(selectedTag));
+ 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);
}
+ };
- setFilteredMeetings(filtered);
+ const handleLoadMore = () => {
+ if (!loadingMore && pagination.has_more) {
+ fetchMeetings(pagination.page + 1, true);
+ }
};
const handleTagClick = (tagName) => {
@@ -104,6 +202,7 @@ const Dashboard = ({ user, onLogout }) => {
const clearFilters = () => {
setSelectedTags([]);
setFilterType('all');
+ setSearchQuery('');
};
useEffect(() => {
@@ -120,32 +219,27 @@ const Dashboard = ({ user, onLogout }) => {
const fetchUserData = async () => {
try {
- setLoading(true);
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);
-
- const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
- setMeetings(meetingsResponse.data);
+ setUserInfo(userResponse.data);
} catch (err) {
console.error('Error fetching data:', err);
setError('获取数据失败,请刷新重试');
- } finally {
- setLoading(false);
}
};
const handleDeleteMeeting = async (meetingId) => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
- // Refresh meetings list
- const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
- setMeetings(meetingsResponse.data);
+ // 清除所有缓存,因为删除会影响统计和列表
+ meetingCacheService.clearAll();
+ // 刷新会议列表和统计
+ await fetchMeetings(1, false);
+ await fetchMeetingsStats();
} catch (err) {
console.error('Error deleting meeting:', err);
- // You might want to show an error message to the user here
}
};
@@ -231,7 +325,7 @@ const Dashboard = ({ user, onLogout }) => {
});
};
- if (loading || !meetings) {
+ if (loading && meetings.length === 0) {
return
;
}
@@ -240,17 +334,18 @@ const Dashboard = ({ user, onLogout }) => {
{error}
-
+
);
}
- const groupedMeetings = groupMeetingsByDate(filteredMeetings);
+ const groupedMeetings = groupMeetingsByDate(meetings);
- // 计算统计数据
- const createdMeetings = meetings.filter(m => String(m.creator_id) === String(user.user_id));
- const attendedMeetings = meetings.filter(m => String(m.creator_id) !== String(user.user_id));
+ // 使用统计数据
+ const createdMeetings = meetingsStats.created_meetings;
+ const attendedMeetings = meetingsStats.attended_meetings;
+ const allMeetings = meetingsStats.all_meetings;
return (
@@ -290,7 +385,7 @@ const Dashboard = ({ user, onLogout }) => {
-
+
@@ -328,14 +423,14 @@ const Dashboard = ({ user, onLogout }) => {
{/* 知识库入口卡片 */}
-
+
-
+
@@ -353,7 +448,7 @@ const Dashboard = ({ user, onLogout }) => {
我创建的会议
- {createdMeetings.length}
+ {createdMeetings}
@@ -366,7 +461,7 @@ const Dashboard = ({ user, onLogout }) => {
我参加的会议
- {attendedMeetings.length}
+ {attendedMeetings}
@@ -379,19 +474,32 @@ const Dashboard = ({ user, onLogout }) => {