278 lines
8.8 KiB
Vue
278 lines
8.8 KiB
Vue
<template>
|
|
<ArtTableFullScreen>
|
|
<div class="notification-center-page" id="table-full-screen">
|
|
<ElCard shadow="never" class="art-table-card">
|
|
<template #header>
|
|
<div class="page-header">
|
|
<div>
|
|
<h2>通知中心</h2>
|
|
<p>查看余额预警、临期提醒、审批结果和系统告警。</p>
|
|
</div>
|
|
<ElButton
|
|
v-if="hasAuth(NOTIFICATION_PERMISSIONS.readAll)"
|
|
:loading="markingAllRead"
|
|
@click="handleMarkAllRead"
|
|
>
|
|
全部已读
|
|
</ElButton>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="filters">
|
|
<ElSelect v-model="filters.category" clearable placeholder="通知分类" @change="reload">
|
|
<ElOption label="临期提醒" value="expiry" />
|
|
<ElOption label="审批" value="approval" />
|
|
<ElOption label="同步" value="sync" />
|
|
<ElOption label="系统" value="system" />
|
|
</ElSelect>
|
|
<ElSelect v-model="filters.type" clearable placeholder="通知类型" @change="reload">
|
|
<ElOption v-for="type in typeOptions" :key="type" :label="type" :value="type" />
|
|
</ElSelect>
|
|
<ElSelect v-model="filters.severity" clearable placeholder="严重级别" @change="reload">
|
|
<ElOption label="提示" value="info" />
|
|
<ElOption label="警告" value="warning" />
|
|
<ElOption label="严重" value="error" />
|
|
<ElOption label="严重" value="critical" />
|
|
</ElSelect>
|
|
<ElSelect v-model="filters.is_read" clearable placeholder="已读状态" @change="reload">
|
|
<ElOption label="未读" :value="false" />
|
|
<ElOption label="已读" :value="true" />
|
|
</ElSelect>
|
|
<ElButton @click="loadNotifications">刷新</ElButton>
|
|
</div>
|
|
|
|
<ElTable v-loading="loading" :data="notifications" row-key="id" class="notification-table">
|
|
<ElTableColumn label="状态" width="90">
|
|
<template #default="scope">
|
|
<span :class="['read-dot', { unread: !isRead(scope.row) }]" />
|
|
{{ isRead(scope.row) ? '已读' : '未读' }}
|
|
</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn prop="title" label="通知标题" min-width="200" show-overflow-tooltip />
|
|
<ElTableColumn label="分类" width="120">
|
|
<template #default="scope">{{ getCategoryLabel(scope.row.category) }}</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn label="严重级别" width="110">
|
|
<template #default="scope">
|
|
<ElTag :type="getSeverityType(scope.row.severity)">
|
|
{{ getSeverityLabel(scope.row.severity) }}
|
|
</ElTag>
|
|
</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn prop="body" label="通知内容" min-width="300" show-overflow-tooltip />
|
|
<ElTableColumn prop="read_at" label="已读时间" width="180" />
|
|
<ElTableColumn prop="created_at" label="时间" width="180" />
|
|
<ElTableColumn label="操作" width="100" fixed="right">
|
|
<template #default="scope">
|
|
<ElButton
|
|
v-if="hasAuth(NOTIFICATION_PERMISSIONS.read)"
|
|
link
|
|
type="primary"
|
|
@click="handleNotificationClick(scope.row)"
|
|
>
|
|
查看
|
|
</ElButton>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</ElTableColumn>
|
|
</ElTable>
|
|
|
|
<div class="pagination-wrapper">
|
|
<ElPagination
|
|
v-model:current-page="page"
|
|
v-model:page-size="pageSize"
|
|
layout="total, sizes, prev, pager, next"
|
|
:total="total"
|
|
@current-change="loadNotifications"
|
|
@size-change="reload"
|
|
/>
|
|
</div>
|
|
</ElCard>
|
|
</div>
|
|
</ArtTableFullScreen>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { ElMessage } from 'element-plus'
|
|
import { NotificationService } from '@/api/modules'
|
|
import type { NotificationItem, NotificationQueryParams } 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'
|
|
|
|
defineOptions({ name: 'NotificationCenter' })
|
|
|
|
const router = useRouter()
|
|
const notificationStore = useNotificationStore()
|
|
const { hasAuth } = useAuth()
|
|
const notifications = ref<NotificationItem[]>([])
|
|
const loading = ref(false)
|
|
const markingAllRead = ref(false)
|
|
const total = ref(0)
|
|
const page = ref(1)
|
|
const pageSize = ref(20)
|
|
const typeOptions = ref<string[]>([])
|
|
const filters = reactive<NotificationQueryParams>({
|
|
category: undefined,
|
|
type: undefined,
|
|
severity: undefined,
|
|
is_read: undefined
|
|
})
|
|
|
|
const isRead = (item: NotificationItem) => item.is_read
|
|
|
|
const getCategoryLabel = (category?: string | null) =>
|
|
({
|
|
balance: '余额预警',
|
|
expiry: '临期提醒',
|
|
expiring: '临期提醒',
|
|
approval: '审批',
|
|
sync: '同步/系统',
|
|
system: '同步/系统'
|
|
})[category || ''] ||
|
|
category ||
|
|
'-'
|
|
|
|
const getSeverityLabel = (severity?: string | null) =>
|
|
({ info: '提示', warning: '警告', error: '严重', critical: '严重' })[severity || ''] ||
|
|
severity ||
|
|
'-'
|
|
|
|
const getSeverityType = (severity?: string | null) => {
|
|
if (severity === 'error') return 'danger'
|
|
if (severity === 'warning') return 'warning'
|
|
if (severity === 'critical') return 'danger'
|
|
return 'info'
|
|
}
|
|
|
|
const loadNotifications = async () => {
|
|
loading.value = true
|
|
try {
|
|
const response = await NotificationService.getNotifications({
|
|
page: page.value,
|
|
page_size: pageSize.value,
|
|
...filters
|
|
})
|
|
if (response.code !== 0 || !response.data) {
|
|
ElMessage.error(response.msg || '获取通知列表失败')
|
|
return
|
|
}
|
|
notifications.value = response.data.items || []
|
|
total.value = response.data.total || 0
|
|
typeOptions.value = Array.from(
|
|
new Set(
|
|
notifications.value
|
|
.map((item) => item.type)
|
|
.filter((type): type is string => Boolean(type))
|
|
)
|
|
)
|
|
await notificationStore.refreshUnreadCount()
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || '获取通知列表失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const reload = () => {
|
|
page.value = 1
|
|
void loadNotifications()
|
|
}
|
|
|
|
const handleMarkAllRead = async () => {
|
|
markingAllRead.value = true
|
|
try {
|
|
const response = await notificationStore.markAllRead()
|
|
if (response.code !== 0) ElMessage.error(response.msg || '全部已读失败')
|
|
else await loadNotifications()
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || '全部已读失败')
|
|
} finally {
|
|
markingAllRead.value = false
|
|
}
|
|
}
|
|
|
|
const handleNotificationClick = async (item: NotificationItem) => {
|
|
try {
|
|
const targetResponse = await NotificationService.getTarget(item.id)
|
|
const readResponse = await notificationStore.markRead(item.id)
|
|
if (readResponse.code !== 0) {
|
|
ElMessage.error(readResponse.msg || '标记已读失败')
|
|
return
|
|
}
|
|
if (targetResponse.code !== 0 || !targetResponse.data) {
|
|
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
|
await loadNotifications()
|
|
return
|
|
}
|
|
if (!navigateNotificationTarget(router, targetResponse.data)) {
|
|
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
|
}
|
|
await loadNotifications()
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || '处理通知失败')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadNotifications()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.notification-center-page {
|
|
.page-header,
|
|
.filters {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.page-header {
|
|
justify-content: space-between;
|
|
|
|
h2 {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
}
|
|
|
|
p {
|
|
margin: 8px 0 0;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
}
|
|
|
|
.filters {
|
|
flex-wrap: wrap;
|
|
margin-bottom: 16px;
|
|
|
|
.el-select {
|
|
width: 160px;
|
|
}
|
|
}
|
|
|
|
.read-dot {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
margin-right: 6px;
|
|
vertical-align: middle;
|
|
background: var(--el-border-color);
|
|
border-radius: 50%;
|
|
|
|
&.unread {
|
|
background: var(--el-color-danger);
|
|
}
|
|
}
|
|
|
|
.pagination-wrapper {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 16px;
|
|
}
|
|
}
|
|
</style>
|