feat: show unread message count

This commit is contained in:
Tim
2025-07-07 19:26:38 +08:00
parent 60b789759a
commit 87d0441ef6
7 changed files with 94 additions and 0 deletions

View File

@@ -9,6 +9,7 @@
<router-link class="menu-item" exact-active-class="selected" to="/message">
<i class="menu-item-icon fas fa-envelope"></i>
<span class="menu-item-text">我的消息</span>
<span v-if="unreadCount > 0" class="unread-dot"></span>
</router-link>
<router-link class="menu-item" exact-active-class="selected" to="/about">
<i class="menu-item-icon fas fa-info-circle"></i>
@@ -31,6 +32,9 @@
<script>
import { themeState, cycleTheme, ThemeMode } from '../utils/theme'
import { authState } from '../utils/auth'
import { fetchUnreadCount } from '../utils/notification'
import { watch } from 'vue'
export default {
name: 'MenuComponent',
props: {
@@ -39,6 +43,9 @@ export default {
default: true
}
},
data() {
return { unreadCount: 0 }
},
computed: {
iconClass() {
switch (themeState.mode) {
@@ -51,6 +58,19 @@ export default {
}
}
},
async mounted() {
const updateCount = async () => {
if (authState.loggedIn) {
this.unreadCount = await fetchUnreadCount()
} else {
this.unreadCount = 0
}
}
await updateCount()
watch(() => authState.loggedIn, async () => {
await updateCount()
})
},
methods: { cycleTheme }
}
</script>
@@ -94,6 +114,15 @@ export default {
opacity: 0.5;
}
.unread-dot {
display: inline-block;
width: 8px;
height: 8px;
margin-left: 4px;
border-radius: 50%;
background-color: red;
}
.menu-footer {
height: 30px;
display: flex;

View File

@@ -0,0 +1,17 @@
import { API_BASE_URL } from '../main'
import { getToken } from './auth'
export async function fetchUnreadCount() {
try {
const token = getToken()
if (!token) return 0
const res = await fetch(`${API_BASE_URL}/api/notifications/unread-count`, {
headers: { Authorization: `Bearer ${token}` }
})
if (!res.ok) return 0
const data = await res.json()
return data.count
} catch (e) {
return 0
}
}

View File

@@ -29,6 +29,14 @@ public class NotificationController {
.collect(Collectors.toList());
}
@GetMapping("/unread-count")
public UnreadCount unreadCount(Authentication auth) {
long count = notificationService.countUnread(auth.getName());
UnreadCount uc = new UnreadCount();
uc.setCount(count);
return uc;
}
@PostMapping("/read")
public void markRead(@RequestBody MarkReadRequest req, Authentication auth) {
notificationService.markRead(auth.getName(), req.getIds());
@@ -115,4 +123,9 @@ public class NotificationController {
private String username;
private String avatar;
}
@Data
private static class UnreadCount {
private long count;
}
}

View File

@@ -10,4 +10,5 @@ import java.util.List;
public interface NotificationRepository extends JpaRepository<Notification, Long> {
List<Notification> findByUserOrderByCreatedAtDesc(User user);
List<Notification> findByUserAndReadOrderByCreatedAtDesc(User user, boolean read);
long countByUserAndRead(User user, boolean read);
}

View File

@@ -45,4 +45,10 @@ public class NotificationService {
}
notificationRepository.saveAll(notifs);
}
public long countUnread(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
return notificationRepository.countByUserAndRead(user, false);
}
}

View File

@@ -60,4 +60,14 @@ class NotificationControllerTest {
verify(notificationService).markRead("alice", List.of(1L,2L));
}
@Test
void unreadCountEndpoint() throws Exception {
Mockito.when(notificationService.countUnread("alice")).thenReturn(3L);
mockMvc.perform(get("/api/notifications/unread-count")
.principal(new UsernamePasswordAuthenticationToken("alice","p")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.count").value(3));
}
}

View File

@@ -60,4 +60,22 @@ class NotificationServiceTest {
assertEquals(1, list.size());
verify(nRepo).findByUserOrderByCreatedAtDesc(user);
}
@Test
void countUnreadReturnsRepositoryValue() {
NotificationRepository nRepo = mock(NotificationRepository.class);
UserRepository uRepo = mock(UserRepository.class);
NotificationService service = new NotificationService(nRepo, uRepo);
User user = new User();
user.setId(3L);
user.setUsername("carl");
when(uRepo.findByUsername("carl")).thenReturn(Optional.of(user));
when(nRepo.countByUserAndRead(user, false)).thenReturn(5L);
long count = service.countUnread("carl");
assertEquals(5L, count);
verify(nRepo).countByUserAndRead(user, false);
}
}