1
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { Bell, CheckCheck, Eye, RefreshCcw } 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 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) {
|
||||
detailOpen.value = true; detailLoading.value = true; detailError.value = null; detail.value = null
|
||||
try {
|
||||
detail.value = await getMyNotification(row.id)
|
||||
if (detail.value.status === 'UNREAD' && canRead.value) {
|
||||
detail.value = await markMyNotificationRead(row.id)
|
||||
await load()
|
||||
}
|
||||
} catch (reason) { detailError.value = getApiErrorDetails(reason, '消息详情加载失败') } finally { 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>
|
||||
<ListPageLayout aria-label="我的消息列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="我的消息筛选" :fields="queryFields" preference-key="notifications-inbox-query" @search="search" /></template>
|
||||
<template v-if="error" #state><LoadErrorState title="我的消息加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template>
|
||||
<section v-if="!error" class="surface-panel table-panel">
|
||||
<DataTable 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-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="table-actions"><el-button link type="primary" @click="openDetail(row as NotificationInboxItem)"><Eye :size="15" />查看</el-button><el-button v-if="canRead && (row as NotificationInboxItem).status === 'UNREAD'" link type="primary" @click="markRead(row as NotificationInboxItem)"><CheckCheck :size="15" />标为已读</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 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"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<article v-else-if="detail" class="notification-detail"><header><h2>{{ detail.title }}</h2><el-tag :type="detail.status === 'UNREAD' ? 'danger' : 'info'">{{ statusLabel(detail.status) }}</el-tag></header><p class="meta">{{ notificationCategoryLabel(detail.category) }} · {{ formatDate(detail.sentAt) }}</p><div class="body">{{ detail.body || '暂无消息内容' }}</div></article>
|
||||
</AppDrawer>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-actions { display: flex; min-height: 32px; align-items: center; gap: 8px; }
|
||||
.table-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.notification-detail header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.notification-detail h2 { margin: 0; color: var(--text-primary, #1f2937); font-size: 20px; line-height: 1.4; }
|
||||
.notification-detail .meta { color: var(--text-secondary, #64748b); font-size: 12px; }
|
||||
.notification-detail .body { padding-top: 16px; border-top: 1px solid var(--border-color, #e5e7eb); color: var(--text-primary, #1f2937); white-space: pre-wrap; line-height: 1.8; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user