171 lines
5.5 KiB
Vue
171 lines
5.5 KiB
Vue
<template>
|
|
<div v-if="visible" class="notice-overlay" @click="close">
|
|
<div class="notice" @click.stop>
|
|
<div class="header">
|
|
<span class="text">通知</span>
|
|
<button
|
|
v-if="hasAuth(NOTIFICATION_PERMISSIONS.readAll)"
|
|
class="read-all"
|
|
type="button"
|
|
@click="handleMarkAllRead"
|
|
>
|
|
全部已读
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bar">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.value"
|
|
type="button"
|
|
:class="{ active: activeCategory === tab.value }"
|
|
@click="activeCategory = tab.value"
|
|
>
|
|
{{ tab.label }}<span v-if="tab.count"> ({{ tab.count }})</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<div v-loading="notificationStore.loading" class="scroll">
|
|
<button
|
|
v-for="item in filteredItems"
|
|
:key="item.id"
|
|
type="button"
|
|
:class="['notification-item', { unread: !isRead(item) }]"
|
|
@click="handleNotificationClick(item)"
|
|
>
|
|
<span :class="['icon', `severity-${item.severity || 'info'}`]">
|
|
<i class="iconfont-sys"></i>
|
|
</span>
|
|
<span class="text">
|
|
<strong>{{ item.title }}</strong>
|
|
<small>{{ item.body }}</small>
|
|
<time>{{ item.created_at || '-' }}</time>
|
|
</span>
|
|
<span v-if="!isRead(item)" class="unread-dot" />
|
|
</button>
|
|
|
|
<div v-if="!filteredItems.length" class="empty-tips">
|
|
<i class="iconfont-sys"></i>
|
|
<p>暂无通知</p>
|
|
</div>
|
|
</div>
|
|
<div class="btn-wrapper">
|
|
<ElButton class="view-all" @click="handleViewAll">进入通知中心</ElButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { ElMessage } from 'element-plus'
|
|
import { NotificationService } from '@/api/modules'
|
|
import type { NotificationCategory, NotificationItem } from '@/types/api'
|
|
import { useNotificationStore } from '@/store/modules/notification'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
|
import { navigateNotificationTarget } from '@/utils/business/notificationNavigation'
|
|
|
|
const props = defineProps<{ modelValue: boolean }>()
|
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
|
|
|
const router = useRouter()
|
|
const notificationStore = useNotificationStore()
|
|
const { hasAuth } = useAuth()
|
|
const activeCategory = ref<'all' | NotificationCategory>('all')
|
|
const visible = computed(() => props.modelValue)
|
|
|
|
const categoryMatches = (item: NotificationItem, category: string) => {
|
|
if (category === 'all') return true
|
|
if (category === 'system') return item.category === 'sync' || item.category === 'system'
|
|
if (category === 'expiry') return item.category === 'expiry' || item.category === 'expiring'
|
|
return item.category === category
|
|
}
|
|
|
|
const isRead = (item: NotificationItem) => item.is_read
|
|
|
|
const filteredItems = computed(() =>
|
|
notificationStore.recentNotifications.filter((item) =>
|
|
categoryMatches(item, activeCategory.value)
|
|
)
|
|
)
|
|
|
|
const tabs = computed(() => {
|
|
return [
|
|
{ value: 'all', label: '全部', count: notificationStore.summary.total },
|
|
{ value: 'approval', label: '审批', count: notificationStore.summary.approval },
|
|
{ value: 'expiry', label: '临期', count: notificationStore.summary.expiry },
|
|
{
|
|
value: 'system',
|
|
label: '同步/系统',
|
|
count: notificationStore.summary.sync + notificationStore.summary.system
|
|
}
|
|
]
|
|
})
|
|
|
|
const close = () => emit('update:modelValue', false)
|
|
|
|
const handleViewAll = () => {
|
|
close()
|
|
void router.push('/notifications')
|
|
}
|
|
|
|
const handleMarkAllRead = async () => {
|
|
try {
|
|
const response = await notificationStore.markAllRead()
|
|
if (response.code !== 0) ElMessage.error(response.msg || '全部已读失败')
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || '全部已读失败')
|
|
}
|
|
}
|
|
|
|
const handleNotificationClick = async (item: NotificationItem) => {
|
|
if (!hasAuth(NOTIFICATION_PERMISSIONS.read)) {
|
|
ElMessage.warning('您没有标记通知已读的权限')
|
|
return
|
|
}
|
|
try {
|
|
const readResponse = await notificationStore.markRead(item.id)
|
|
if (readResponse.code !== 0) {
|
|
ElMessage.error(readResponse.msg || '标记已读失败')
|
|
return
|
|
}
|
|
const targetResponse = await NotificationService.getTarget(item.id)
|
|
if (targetResponse.code !== 0 || !targetResponse.data) {
|
|
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
|
return
|
|
}
|
|
if (!navigateNotificationTarget(router, targetResponse.data)) {
|
|
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
|
} else {
|
|
close()
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || '处理通知失败')
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(open) => {
|
|
if (open) {
|
|
activeCategory.value = 'all'
|
|
void notificationStore.refreshSummary().catch((error) => {
|
|
ElMessage.error(error?.message || '获取通知失败')
|
|
})
|
|
}
|
|
}
|
|
)
|
|
|
|
onMounted(() => {
|
|
void notificationStore.refreshUnreadCount().catch(() => undefined)
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@use './style';
|
|
</style>
|