feat: 顶部通知铃铛与站内通知中心
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m7s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m7s
This commit is contained in:
277
src/views/notifications/index.vue
Normal file
277
src/views/notifications/index.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<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="balance" />
|
||||
<ElOption label="临期提醒" value="expiry" />
|
||||
<ElOption label="审批" value="approval" />
|
||||
<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="success" />
|
||||
</ElSelect>
|
||||
<ElSelect v-model="filters.read_status" clearable placeholder="已读状态" @change="reload">
|
||||
<ElOption label="未读" value="unread" />
|
||||
<ElOption label="已读" value="read" />
|
||||
</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="content" label="通知内容" min-width="300" show-overflow-tooltip />
|
||||
<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,
|
||||
read_status: undefined
|
||||
})
|
||||
|
||||
const isRead = (item: NotificationItem) =>
|
||||
item.read === true || item.is_read === true || item.read_status === 'read'
|
||||
|
||||
const getCategoryLabel = (category?: string | null) =>
|
||||
({
|
||||
balance: '余额预警',
|
||||
expiry: '临期提醒',
|
||||
expiring: '临期提醒',
|
||||
approval: '审批',
|
||||
sync: '同步/系统',
|
||||
system: '同步/系统'
|
||||
})[category || ''] ||
|
||||
category ||
|
||||
'-'
|
||||
|
||||
const getSeverityLabel = (severity?: string | null) =>
|
||||
({ info: '提示', warning: '警告', error: '严重', success: '成功' })[severity || ''] ||
|
||||
severity ||
|
||||
'-'
|
||||
|
||||
const getSeverityType = (severity?: string | null) => {
|
||||
if (severity === 'error') return 'danger'
|
||||
if (severity === 'warning') return 'warning'
|
||||
if (severity === 'success') return 'success'
|
||||
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 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.content || '该通知暂无可跳转目标')
|
||||
await loadNotifications()
|
||||
return
|
||||
}
|
||||
if (!navigateNotificationTarget(router, targetResponse.data)) {
|
||||
ElMessage.info(targetResponse.data.message || item.content || '该通知暂无可跳转目标')
|
||||
}
|
||||
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>
|
||||
Reference in New Issue
Block a user