From 98bbc36453e99214bf474f8bcc46f9486a2a25db Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:11:25 +0800 Subject: [PATCH] feat: show channel message indicator --- frontend_nuxt/components/HeaderComponent.vue | 22 ++++++++++- frontend_nuxt/composables/useChannelUnread.js | 38 +++++++++++++++++++ frontend_nuxt/pages/message-box/[id].vue | 3 ++ frontend_nuxt/pages/message-box/index.vue | 8 +++- 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 frontend_nuxt/composables/useChannelUnread.js diff --git a/frontend_nuxt/components/HeaderComponent.vue b/frontend_nuxt/components/HeaderComponent.vue index 5963eb5bc..8f3e3573d 100644 --- a/frontend_nuxt/components/HeaderComponent.vue +++ b/frontend_nuxt/components/HeaderComponent.vue @@ -6,7 +6,10 @@ - + {{ unreadMessageCount }} + @@ -85,6 +89,7 @@ import ToolTip from '~/components/ToolTip.vue' import SearchDropdown from '~/components/SearchDropdown.vue' import { authState, clearToken, loadCurrentUser } from '~/utils/auth' import { useUnreadCount } from '~/composables/useUnreadCount' +import { useChannelUnread } from '~/composables/useChannelUnread' import { useIsMobile } from '~/utils/screen' import { themeState, cycleTheme, ThemeMode } from '~/utils/theme' import { toast } from '~/main' @@ -103,6 +108,7 @@ const props = defineProps({ const isLogin = computed(() => authState.loggedIn) const isMobile = useIsMobile() const { count: unreadMessageCount, fetchUnreadCount } = useUnreadCount() +const { hasUnread: hasChannelUnread, fetchChannelUnread } = useChannelUnread() const avatar = ref('') const showSearch = ref(false) const searchDropdown = ref(null) @@ -227,8 +233,10 @@ onMounted(async () => { } const updateUnread = async () => { if (authState.loggedIn) { - // Initialize the unread count composable fetchUnreadCount() + fetchChannelUnread() + } else { + fetchChannelUnread() } } @@ -413,6 +421,16 @@ onMounted(async () => { box-sizing: border-box; } +.unread-dot { + position: absolute; + top: -2px; + right: -4px; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #ff4d4f; +} + .rss-icon { animation: rss-glow 2s 3; } diff --git a/frontend_nuxt/composables/useChannelUnread.js b/frontend_nuxt/composables/useChannelUnread.js new file mode 100644 index 000000000..9ebd02500 --- /dev/null +++ b/frontend_nuxt/composables/useChannelUnread.js @@ -0,0 +1,38 @@ +import { ref } from 'vue' +import { getToken } from '~/utils/auth' + +const hasUnread = ref(false) + +export function useChannelUnread() { + const config = useRuntimeConfig() + const API_BASE_URL = config.public.apiBaseUrl + + const setFromList = (channels) => { + hasUnread.value = Array.isArray(channels) && channels.some((c) => c.unreadCount > 0) + } + + const fetchChannelUnread = async () => { + const token = getToken() + if (!token) { + hasUnread.value = false + return + } + try { + const response = await fetch(`${API_BASE_URL}/api/channels`, { + headers: { Authorization: `Bearer ${token}` }, + }) + if (response.ok) { + const data = await response.json() + setFromList(data) + } + } catch (e) { + console.error('Failed to fetch channel unread status:', e) + } + } + + return { + hasUnread, + fetchChannelUnread, + setFromList, + } +} diff --git a/frontend_nuxt/pages/message-box/[id].vue b/frontend_nuxt/pages/message-box/[id].vue index fd969f9c5..ff2066924 100644 --- a/frontend_nuxt/pages/message-box/[id].vue +++ b/frontend_nuxt/pages/message-box/[id].vue @@ -69,6 +69,7 @@ import { renderMarkdown } from '~/utils/markdown' import MessageEditor from '~/components/MessageEditor.vue' import { useWebSocket } from '~/composables/useWebSocket' import { useUnreadCount } from '~/composables/useUnreadCount' +import { useChannelUnread } from '~/composables/useChannelUnread' import TimeManager from '~/utils/time' import BaseTimeline from '~/components/BaseTimeline.vue' import BasePlaceholder from '~/components/BasePlaceholder.vue' @@ -78,6 +79,7 @@ const route = useRoute() const API_BASE_URL = config.public.apiBaseUrl const { connect, disconnect, subscribe, isConnected } = useWebSocket() const { fetchUnreadCount: refreshGlobalUnreadCount } = useUnreadCount() +const { fetchChannelUnread: refreshChannelUnread } = useChannelUnread() let subscription = null const messages = ref([]) @@ -258,6 +260,7 @@ async function markConversationAsRead() { }) // After marking as read, refresh the global unread count refreshGlobalUnreadCount() + refreshChannelUnread() } catch (e) { console.error('Failed to mark conversation as read', e) } diff --git a/frontend_nuxt/pages/message-box/index.vue b/frontend_nuxt/pages/message-box/index.vue index 8fcdeefeb..a4391e40d 100644 --- a/frontend_nuxt/pages/message-box/index.vue +++ b/frontend_nuxt/pages/message-box/index.vue @@ -117,6 +117,7 @@ import { getToken, fetchCurrentUser } from '~/utils/auth' import { toast } from '~/main' import { useWebSocket } from '~/composables/useWebSocket' import { useUnreadCount } from '~/composables/useUnreadCount' +import { useChannelUnread } from '~/composables/useChannelUnread' import TimeManager from '~/utils/time' import { stripMarkdownLength } from '~/utils/markdown' import SearchPersonDropdown from '~/components/SearchPersonDropdown.vue' @@ -131,6 +132,8 @@ const currentUser = ref(null) const API_BASE_URL = config.public.apiBaseUrl const { connect, disconnect, subscribe, isConnected } = useWebSocket() const { fetchUnreadCount: refreshGlobalUnreadCount } = useUnreadCount() +const { fetchChannelUnread: refreshChannelUnread, setFromList: setChannelUnreadFromList } = + useChannelUnread() let subscription = null const activeTab = ref('messages') @@ -189,7 +192,9 @@ async function fetchChannels() { headers: { Authorization: `Bearer ${token}` }, }) if (!response.ok) throw new Error('无法加载频道') - channels.value = await response.json() + const data = await response.json() + channels.value = data + setChannelUnreadFromList(data) } catch (e) { toast.error(e.message) } finally { @@ -228,6 +233,7 @@ onActivated(async () => { if (currentUser.value) { await fetchConversations() refreshGlobalUnreadCount() // Refresh global count when entering the list + refreshChannelUnread() const token = getToken() if (token && !isConnected.value) { connect(token)