Files
admin-web/src/modules/notifications/pages/PlatformNotificationsView.vue
T
2026-10-21 13:22:42 +08:00

128 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import '@/styles/adminWorkbench.css'
import '@/styles/adminDensity.css'
import { computed, onMounted, ref, watch } from 'vue'
import { CheckCheck } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getMyNotification, getMyUnreadNotificationCount, listMyNotifications, markAllMyNotificationsRead, markMyNotificationRead } from '../api/notificationCenter'
import { notificationCategoryLabel } from '../presentation'
import type { NotificationInboxDetail, NotificationInboxItem, NotificationInboxStatus, PageResult } from '../types'
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const result = ref<PageResult<NotificationInboxItem>>({ records: [], page: 1, size: 20, total: 0, pages: 0 })
const filters = ref<QueryModel>({ status: '', category: '' })
const unreadCount = ref<number | null>(null)
const detailOpen = ref(false)
const detailLoading = ref(false)
const detailError = ref<ApiErrorDetails | null>(null)
const detail = ref<NotificationInboxDetail | null>(null)
const detailRow = ref<NotificationInboxItem | null>(null)
let detailRequest = 0
watch(detailOpen, value => { if (!value) { detailRequest++; detailRow.value = null } })
const access = useAccessPreviewStore()
const canRead = computed(() => access.can('platform:notification:read'))
const canReadAll = computed(() => access.can('platform:notification:read-all'))
const queryFields: QueryField[] = [
{ key: 'status', type: 'select', label: '消息状态', placeholder: '全部状态', options: [{ label: '未读', value: 'UNREAD' }, { label: '已读', value: 'READ' }] },
{ key: 'category', type: 'input', label: '消息分类', placeholder: '请输入消息分类' },
]
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
{ key: 'title', prop: 'title', label: '消息标题', minWidth: 220 },
{ key: 'summary', prop: 'summary', label: '内容摘要', minWidth: 300, showOverflowTooltip: true },
{ key: 'category', label: '分类', width: 130, slotName: 'category' },
{ key: 'sentAt', label: '发送时间', width: 180, slotName: 'sentAt' },
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
]
function statusLabel(status: NotificationInboxStatus) { return status === 'UNREAD' ? '未读' : '已读' }
function formatDate(value: string | null) { return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—' }
function search() { page.value === 1 ? void load() : page.value = 1 }
async function load() {
loading.value = true
error.value = null
try {
result.value = await listMyNotifications({ page: page.value, size: pageSize.value, status: filters.value.status as NotificationInboxStatus || undefined, category: String(filters.value.category || '').trim() || undefined })
unreadCount.value = await getMyUnreadNotificationCount()
} catch (reason) {
error.value = getApiErrorDetails(reason, '我的消息加载失败')
result.value = { records: [], page: page.value, size: pageSize.value, total: 0, pages: 0 }
} finally { loading.value = false }
}
async function openDetail(row: NotificationInboxItem) {
const request = ++detailRequest
detailRow.value = row
detailOpen.value = true; detailLoading.value = true; detailError.value = null; detail.value = null
try {
const result = await getMyNotification(row.id)
if (request !== detailRequest || !detailOpen.value) return
detail.value = result
if (result.status === 'UNREAD' && canRead.value) {
const read = await markMyNotificationRead(row.id)
if (request !== detailRequest || !detailOpen.value) return
detail.value = read
await load()
}
} catch (reason) { if (request === detailRequest && detailOpen.value) detailError.value = getApiErrorDetails(reason, '消息详情加载失败') } finally { if (request === detailRequest) detailLoading.value = false }
}
async function markRead(row: NotificationInboxItem) {
if (!canRead.value || row.status !== 'UNREAD') return
try { await markMyNotificationRead(row.id); ElMessage.success('消息已标记为已读'); await load() } catch (reason) { ElMessage.error(getApiErrorMessage(reason, '标记已读失败')) }
}
async function markAllRead() {
try { await markAllMyNotificationsRead(); ElMessage.success('全部消息已标记为已读'); await load() } catch (reason) { ElMessage.error(getApiErrorMessage(reason, '标记已读失败')) }
}
watch([page, pageSize], () => void load())
onMounted(() => void load())
</script>
<template>
<AdminPage class="admin-density admin-workbench">
<ListPageLayout aria-label="我的消息列表">
<section class="workbench-table is-page-surface"><DataTable preference-key="notifications-platformnotificationsview" appearance="integrated" column-settings-mode="show" :results-visible="!error" v-model:page="page" v-model:page-size="pageSize" :data="result.records" :columns="columns" :loading="loading" :total="result.total" row-key="id" empty-text="暂无消息">
<template #toolbar-start><h1>我的消息</h1></template>
<template #filters><QueryToolbar v-model="filters" ariaLabel="我的消息筛选" :fields="queryFields" preference-key="notifications-inbox-query" @search="search" /></template>
<template #notices><template v-if="error"><LoadErrorState title="我的消息加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template></template>
<template #toolbar-actions><el-button v-if="canReadAll && unreadCount" type="primary" plain :disabled="loading" @click="markAllRead"><CheckCheck :size="16" />全部标为已读</el-button></template>
<template #pagination-left><span>共 {{ result.total }} 条<span v-if="unreadCount">,未读 {{ unreadCount }} 条</span></span></template>
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" @click="openDetail(row as NotificationInboxItem)">查看</el-button><el-button v-if="canRead && (row as NotificationInboxItem).status === 'UNREAD'" class="density-action" data-tone="neutral" @click="markRead(row as NotificationInboxItem)">标为已读</el-button></div></template>
<template #category="{ row }">{{ notificationCategoryLabel((row as NotificationInboxItem).category) }}</template>
<template #sentAt="{ row }">{{ formatDate((row as NotificationInboxItem).sentAt) }}</template>
<template #status="{ row }"><el-tag :type="(row as NotificationInboxItem).status === 'UNREAD' ? 'danger' : 'info'">{{ statusLabel((row as NotificationInboxItem).status) }}</el-tag></template>
</DataTable></section>
</ListPageLayout>
<AppDrawer class="admin-density admin-density-drawer admin-density-drawer--detail admin-workbench is-drawer" v-model="detailOpen" title="消息详情" size="min(680px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close>
<LoadErrorState v-if="detailError" title="消息详情加载失败" :message="detailError.message" :loading="detailLoading" @retry="detailRow && openDetail(detailRow)"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
<article v-else-if="detail" class="density-dialog-sections notification-detail">
<section class="density-dialog-section" data-tone="summary" aria-label="消息资料">
<h3 class="density-dialog-section__title message-heading"><span>{{ detail.title || '消息详情' }}</span><el-tag :type="detail.status === 'UNREAD' ? 'danger' : 'info'">{{ statusLabel(detail.status) }}</el-tag></h3>
<p class="density-dialog-note">{{ notificationCategoryLabel(detail.category) }} · {{ formatDate(detail.sentAt) }}</p>
</section>
<section class="density-dialog-section" data-tone="source" aria-label="消息正文"><h3 class="density-dialog-section__title">消息正文</h3><div class="message-body">{{ detail.body || '暂无消息内容' }}</div></section>
</article>
</AppDrawer>
</AdminPage>
</template>
<style scoped>
.message-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--admin-space-5); }
.message-heading > span { min-width: 0; overflow-wrap: anywhere; }
.message-body { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>