4343 </el-button >
4444 </el-menu-item >
4545
46+ <el-menu-item index =" notification" class =" notification-item" >
47+ <el-popover
48+ placement =" bottom-end"
49+ :width =" 300"
50+ trigger =" click"
51+ popper-class =" notification-popover"
52+ >
53+ <template #reference >
54+ <el-badge :value =" unreadCount" :hidden =" !hasUnread" class =" notification-badge" >
55+ <Bell class="notification-icon" />
56+ </el-badge >
57+ </template >
58+
59+ <div class =" notification-container" >
60+ <div class =" notification-header" >
61+ <span >通知</span >
62+ <el-button type =" text" @click =" markAllAsRead" >全部标为已读</el-button >
63+ </div >
64+ <div class =" notification-list" >
65+ <template v-if =" notifications .length " >
66+ <div v-for =" notification in notifications"
67+ :key =" notification.id"
68+ class =" notification-item"
69+ :class =" { 'unread': !notification.isRead }"
70+ @click =" handleNotificationClick(notification)" >
71+ <img :src =" notification.sender?.avatar || defaultAvatar" class =" sender-avatar" />
72+ <div class =" notification-content" >
73+ <div class =" notification-message" >{{ notification.message }}</div >
74+ <div class =" notification-time" >{{ formatTime(notification.createTime) }}</div >
75+ </div >
76+ </div >
77+ </template >
78+ <div v-else class =" empty-notifications" >
79+ 暂无通知
80+ </div >
81+ </div >
82+ </div >
83+ </el-popover >
84+ </el-menu-item >
85+
4686 <el-sub-menu index =" 3" class =" user-menu" >
4787 <template #title >
4888 <div class =" user-info" >
73113</template >
74114
75115<script setup>
76- import { ref , inject ,computed ,onMounted } from ' vue' ;
116+ import { ref , inject ,computed ,onMounted , onBeforeUnmount } from ' vue' ;
77117import { useRouter } from ' vue-router' ;
78118import { useStore } from ' vuex' ;
119+ import { Bell } from ' lucide-vue-next' ;
79120
80121const router = useRouter ();
81122const store = useStore ();
@@ -87,21 +128,109 @@ const user = computed(() => store.state.user);
87128const userName = computed (() => user .value ? .username || ' User' );
88129const userAvatar = computed (() => user .value ? .avatar || defaultAvatar);
89130
131+ const notifications = ref ([]);
132+ const unreadCount = computed (() => notifications .value .filter (n => ! n .isRead ).length );
133+ const hasUnread = computed (() => unreadCount .value > 0 );
134+
135+ // WebSocket 连接
136+ let ws = null ;
137+
138+ // 初始化 WebSocket 连接
139+ const initWebSocket = () => {
140+ if (isLoggedIn .value && user .value ? .id ) {
141+ ws = new WebSocket (` ws://localhost:8088/api/ws/notifications/${ user .value .id } ` );
142+
143+ ws .onopen = () => {
144+ console .log (' WebSocket connected' );
145+ };
146+
147+ ws .onmessage = (event ) => {
148+ const notification = JSON .parse (event .data );
149+ notifications .value .unshift (notification);
150+ };
151+
152+ ws .onerror = (error ) => {
153+ console .error (' WebSocket error:' , error);
154+ };
155+ }
156+ };
157+
158+ // 获取通知列表
159+ const fetchNotifications = async () => {
160+ try {
161+ const response = await fetch (` http://localhost:8088/api/notifications/${ user .value ? .id }` );
162+ if (!response.ok) throw new Error('Failed to fetch notifications');
163+ const data = await response.json();
164+ notifications.value = data.data || [];
165+ } catch (error) {
166+ console.error('Error fetching notifications:', error);
167+ }
168+ };
169+
170+ // 标记所有通知为已读
171+ const markAllAsRead = async () => {
172+ try {
173+ const response = await fetch(
174+ ` http: // localhost:8088/api/notifications/${user.value?.id}/mark-all-read`,
175+ { method: ' POST' }
176+ );
177+ if (! response .ok ) throw new Error (' Failed to mark notifications as read' );
178+ notifications .value = notifications .value .map (n => ({ ... n, isRead: true }));
179+ } catch (error) {
180+ console.error('Error marking notifications as read:', error);
181+ }
182+ };
183+
184+ // 处理通知点击
185+ const handleNotificationClick = async (notification) => {
186+ if (!notification.isRead) {
187+ try {
188+ await fetch(
189+ ` http: // localhost:8088/api/notifications/${user.value?.id}/${notification.id}/mark-read`,
190+ { method: ' POST' }
191+ );
192+ notification .isRead = true ;
193+ } catch (error) {
194+ console .error (' Error marking notification as read:' , error);
195+ }
196+ }
197+
198+ // 根据通知类型导航到相应页面
199+ if (notification .type === ' comment' ) {
200+ router .push (` /article/${ notification .articleId } ` );
201+ } else if (notification .type === ' like' ) {
202+ router .push (` /article/${ notification .articleId } ` );
203+ }
204+ };
205+
90206const handleAvatarError = (e ) => {
91207 e .target .src = defaultAvatar;
92208};
93209
210+ // 格式化时间
211+ const formatTime = (time ) => {
212+ return dayjs (time).format (' YYYY-MM-DD HH:mm' );
213+ };
214+
94215onMounted (async () => {
95216 if (isLoggedIn .value ) {
96217 try {
97218 console .log (' 头像:' ,userAvatar .value );
219+ await fetchNotifications ();
220+ initWebSocket ();
98221// await store.dispatch('getUserInfo'); // 确保在 Vuex 中实现此 action
99222 } catch (error) {
100223 console .error (' Failed to fetch user info:' , error);
101224 }
102225 }
103226});
104227
228+ onBeforeUnmount (() => {
229+ if (ws) {
230+ ws .close ();
231+ }
232+ });
233+
105234const menuItems = [
106235 {index: ' 0' , label: ' 首页' , route: ' Home' },
107236 { index: ' 5' , label: ' 健康中心' , route: ' HealthCenter' },
@@ -147,6 +276,7 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
147276 background: #ffffff;
148277}
149278
279+ /* Logo styles */
150280.logo - container {
151281 padding: 0 20px ;
152282 transition: all 0 .3s ease;
@@ -162,12 +292,20 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
162292 transform: scale (1.05 );
163293}
164294
295+ /* Menu items styles */
165296.menu - items- left {
166297 display: flex;
167298 margin- right: auto;
168299 gap: 10px ;
169300}
170301
302+ .menu - items- right {
303+ display: flex;
304+ align- items: center;
305+ margin- right: 20px ;
306+ gap: 8px ; /* Add gap between items */
307+ }
308+
171309.menu - item- animated {
172310 font- size: 16px ;
173311 font- weight: 500 ;
@@ -192,12 +330,7 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
192330 width: 100 % ;
193331}
194332
195- .menu - items- right {
196- display: flex;
197- align- items: center;
198- margin- right: 20px ;
199- }
200-
333+ /* Auth items styles */
201334.auth - item {
202335 font- size: 16px ;
203336 font- weight: 500 ;
@@ -209,6 +342,7 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
209342 color: var (-- el- color- primary);
210343}
211344
345+ /* Record button styles */
212346.record - button {
213347 display: flex;
214348 align- items: center;
@@ -229,6 +363,101 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
229363 transition: transform 0 .3s ease;
230364}
231365
366+ /* Notification styles */
367+ .notification - item {
368+ display: flex;
369+ align- items: center;
370+ height: 64px ;
371+ padding: 0 8px ;
372+ }
373+
374+ .notification - badge {
375+ display: flex;
376+ align- items: center;
377+ justify- content: center;
378+ height: 100 % ;
379+ }
380+
381+ .notification - badge : deep (.el - badge__content ) {
382+ background- color: #f56c6c;
383+ z- index: 10 ;
384+ }
385+
386+ .notification - icon {
387+ width: 24px ;
388+ height: 24px ;
389+ color: #606266 ;
390+ cursor: pointer;
391+ transition: all 0 .3s ease;
392+ }
393+
394+ .notification - icon: hover {
395+ color: var (-- el- color- primary);
396+ }
397+
398+ /* Notification popover styles */
399+ .notification - container {
400+ max- height: 400px ;
401+ overflow- y: auto;
402+ }
403+
404+ .notification - header {
405+ display: flex;
406+ justify- content: space- between;
407+ align- items: center;
408+ padding: 12px 16px ;
409+ border- bottom: 1px solid #ebeef5;
410+ }
411+
412+ .notification - list {
413+ padding: 8px 0 ;
414+ }
415+
416+ .notification - list .notification - item {
417+ display: flex;
418+ padding: 12px 16px ;
419+ cursor: pointer;
420+ transition: all 0 .3s ease;
421+ height: auto;
422+ }
423+
424+ .notification - list .notification - item: hover {
425+ background- color: #f5f7fa;
426+ }
427+
428+ .notification - item .unread {
429+ background- color: #f0f9ff;
430+ }
431+
432+ .sender - avatar {
433+ width: 40px ;
434+ height: 40px ;
435+ border- radius: 50 % ;
436+ margin- right: 12px ;
437+ }
438+
439+ .notification - content {
440+ flex: 1 ;
441+ }
442+
443+ .notification - message {
444+ font- size: 14px ;
445+ color: #303133 ;
446+ margin- bottom: 4px ;
447+ }
448+
449+ .notification - time {
450+ font- size: 12px ;
451+ color: #909399 ;
452+ }
453+
454+ .empty - notifications {
455+ padding: 24px ;
456+ text- align: center;
457+ color: #909399 ;
458+ }
459+
460+ /* User menu styles */
232461.user - info {
233462 display: flex;
234463 align- items: center;
@@ -249,6 +478,7 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
249478 border: 2px solid #fff;
250479 box- shadow: 0 2px 8px rgba (0 , 0 , 0 , 0.1 );
251480 transition: all 0 .3s ease;
481+ object- fit: cover;
252482}
253483
254484.avatar : hover {
@@ -278,15 +508,4 @@ const handleSelect = (key, keyPath) => console.log(key, keyPath);
278508 height: 18px ;
279509 opacity: 0.8 ;
280510}
281-
282- .avatar {
283- width: 32px ;
284- height: 32px ;
285- border- radius: 50 % ;
286- border: 2px solid #fff;
287- box- shadow: 0 2px 8px rgba (0 , 0 , 0 , 0.1 );
288- transition: all 0 .3s ease;
289- object- fit: cover; /* 确保图片适当裁剪以填充圆形区域 */
290- }
291-
292511< / style>
0 commit comments