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)