topfans/frontend/store/modules/mailbox.js
2026-07-14 21:59:15 +08:00

121 lines
4.3 KiB
JavaScript

import {
getNotificationsApi, getUnreadCountApi,
markAsReadApi, markAllAsReadApi,
deleteNotificationApi, clearNotificationsApi,
} from '@/utils/api.js'
// 站内邮箱 4 类分组 (V1.2.10 排除 like/system)
export const GROUPS = [
{ type: 'activity', label: '活动通知' },
{ type: 'feedback_replied', label: '反馈回复' },
{ type: 'report_resolved', label: '举报结果' },
{ type: 'target_reported', label: '被举报' },
// ⚠️ like / system 不进 V1.2.10 (用户已确认保留旧 push 体验)
]
const state = () => ({
itemsByType: GROUPS.reduce((acc, g) => { acc[g.type] = []; return acc }, {}),
unreadByType: GROUPS.reduce((acc, g) => { acc[g.type] = 0; return acc },
{ like: 0, system: 0 }), // 仍接收所有 type 用于聚合,只不进 INBOX
totalUnread: 0,
detailCache: null,
isInMailboxPage: false,
loading: false,
noMoreByType: GROUPS.reduce((acc, g) => { acc[g.type] = true; return acc }, {}),
})
const getters = {
// profile.vue 📬 badge 显示值 (V1.2.10: 排除 like/system)
inboxUnreadOnly: (state) => {
return GROUPS.reduce((sum, g) => sum + (state.unreadByType[g.type] || 0), 0)
},
}
const mutations = {
SET_ITEMS(state, { type, items, sortUnreadFirst = true }) {
const sorted = sortUnreadFirst
? [...items].sort((a, b) =>
(a.is_read === b.is_read) ? (b.created_at - a.created_at) : (a.is_read ? 1 : -1)
)
: items
state.itemsByType[type] = sorted
},
APPEND_ITEMS(state, { type, items }) {
state.itemsByType[type].push(...items)
},
UPDATE_ITEM(state, { type, id, patch }) {
const arr = state.itemsByType[type]
const i = arr.findIndex(n => n.id === id)
if (i >= 0) arr[i] = { ...arr[i], ...patch }
},
REMOVE_ITEM(state, { type, id }) {
state.itemsByType[type] = state.itemsByType[type].filter(n => n.id !== id)
},
RESORT_ITEMS(state, type) {
state.itemsByType[type].sort((a, b) =>
(a.is_read === b.is_read) ? (b.created_at - a.created_at) : (a.is_read ? 1 : -1)
)
},
SET_NO_MORE(state, { type, value }) {
state.noMoreByType[type] = value
},
DECREMENT_TYPE(state, type) {
state.unreadByType[type] = Math.max(0, (state.unreadByType[type] || 0) - 1)
},
REPLACE_UNREAD_BY_TYPE(state, byType) {
state.unreadByType = { ...state.unreadByType, ...byType }
},
SET_TOTAL_UNREAD(state, n) { state.totalUnread = n },
MARK_ALL_READ(state) {
state.unreadByType = GROUPS.reduce((acc, g) => { acc[g.type] = 0; return acc },
{ ...state.unreadByType })
Object.keys(state.itemsByType).forEach(t => {
state.itemsByType[t] = state.itemsByType[t].map(n => ({ ...n, is_read: true }))
})
},
CLEAR_ALL(state) {
GROUPS.forEach(g => { state.itemsByType[g.type] = [] })
GROUPS.forEach(g => { state.unreadByType[g.type] = 0 })
},
SET_DETAIL_CACHE(state, item) { state.detailCache = item },
SET_LOADING(state, v) { state.loading = v },
SET_IS_IN_PAGE(state, v) { state.isInMailboxPage = v },
PREPEND_ITEM(state, { type, notification }) {
// V1.2.6 幂等:防重复
const list = state.itemsByType[type] || []
if (list.some(n => n.id === notification.id)) return
list.unshift(notification)
list.sort((a, b) =>
(a.is_read === b.is_read) ? (b.created_at - a.created_at) : (a.is_read ? 1 : -1)
)
state.itemsByType[type] = list
},
}
const actions = {
// App.vue receive 事件后调用,只更新未读 + 可选 PREPEND_ITEM
applyPushPayload({ commit, rootState }, { data, title = '', content = '' }) {
if (!data || !data.unreads_by_type) return
commit('REPLACE_UNREAD_BY_TYPE', data.unreads_by_type)
commit('SET_TOTAL_UNREAD', data.total_unread || 0)
if (rootState.mailbox.isInMailboxPage && data.notification_id && data.type) {
commit('PREPEND_ITEM', {
type: data.type,
notification: {
id: data.notification_id,
title, content,
data: JSON.stringify(data),
is_read: false,
created_at: Date.now(),
},
})
}
},
async alignFromServerUnread({ commit }) {
const resp = await getUnreadCountApi()
commit('REPLACE_UNREAD_BY_TYPE', resp.data)
},
}
export default { namespaced: true, state, getters, mutations, actions }