Skip to content

Commit e76c097

Browse files
committed
update my_article_page
1 parent 008cd81 commit e76c097

6 files changed

Lines changed: 409 additions & 78 deletions

File tree

src/components/ArticleInteractionBar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const handleShare = () => emit('share')
5454
:size="20"
5555
:class="{ 'fill-current': isFavorited }"
5656
/>
57-
<span>{{ favoriteCount || 4 }}</span>
57+
<span>{{ favoriteCount || 0 }}</span>
5858
</button>
5959

6060
<!-- 评论按钮 -->

src/components/header.vue

Lines changed: 237 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,46 @@
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">
@@ -73,9 +113,10 @@
73113
</template>
74114

75115
<script setup>
76-
import { ref, inject,computed,onMounted } from 'vue';
116+
import { ref, inject,computed,onMounted, onBeforeUnmount } from 'vue';
77117
import { useRouter } from 'vue-router';
78118
import { useStore } from 'vuex';
119+
import { Bell } from 'lucide-vue-next';
79120
80121
const router = useRouter();
81122
const store = useStore();
@@ -87,21 +128,109 @@ const user = computed(() => store.state.user);
87128
const userName = computed(() => user.value?.username || 'User');
88129
const 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+
90206
const 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+
94215
onMounted(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+
105234
const 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>

src/router/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ const routes = [
3232
name: 'Forum',
3333
component: () => import('../views/forum.vue')
3434
},
35+
{
36+
path: '/editor/:id?',
37+
name: 'EditArticle',
38+
component: () => import('../views/forum/ArticleEditor.vue'),
39+
props: true
40+
},
3541
{
3642
path: '/article/editor',
3743
name: 'ArticleEditor',

0 commit comments

Comments
 (0)