305 lines
9.0 KiB
Vue
305 lines
9.0 KiB
Vue
<template>
|
|
<div class="aksrt-page-container">
|
|
<PageHeader
|
|
title="消息中心"
|
|
desc="管理站内通知、系统消息和用户消息数据。"
|
|
/>
|
|
|
|
<n-grid cols="1 s:2 m:4" :x-gap="16" :y-gap="16" responsive="screen">
|
|
<n-grid-item>
|
|
<n-statistic label="全部消息" :value="stats.total" />
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-statistic label="未读消息" :value="stats.unread">
|
|
<template #suffix>
|
|
<n-tag v-if="stats.unread > 0" type="warning" size="small">{{ stats.unread }}</n-tag>
|
|
</template>
|
|
</n-statistic>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-statistic label="今日新增" :value="stats.today" />
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-statistic label="系统通知" :value="stats.system" />
|
|
</n-grid-item>
|
|
</n-grid>
|
|
|
|
<n-card size="small" title="消息列表" style="margin-top:16px;">
|
|
<template #header-extra>
|
|
<n-space>
|
|
<n-select
|
|
v-model:value="filters.type"
|
|
placeholder="消息类型"
|
|
:options="typeOptions"
|
|
clearable
|
|
style="width:160px;"
|
|
@update:value="handleFilterChange"
|
|
/>
|
|
<n-select
|
|
v-model:value="filters.read"
|
|
placeholder="已读状态"
|
|
:options="readOptions"
|
|
clearable
|
|
style="width:120px;"
|
|
@update:value="handleFilterChange"
|
|
/>
|
|
<n-button @click="loadMessages" :loading="loading">刷新</n-button>
|
|
<n-button type="primary" @click="markAllRead" :loading="markingAll">全部已读</n-button>
|
|
</n-space>
|
|
</template>
|
|
|
|
<n-data-table
|
|
:columns="columns"
|
|
:data="messages"
|
|
:loading="loading"
|
|
:pagination="pagination"
|
|
:row-class-name="rowClassName"
|
|
remote
|
|
@update:page="handlePageChange"
|
|
@update:page-size="handlePageSizeChange"
|
|
/>
|
|
</n-card>
|
|
|
|
<n-drawer v-model:show="showDetail" :width="480">
|
|
<n-drawer-content :title="detailMessage?.title || '消息详情'">
|
|
<template v-if="detailMessage">
|
|
<n-descriptions bordered :column="1" label-placement="left">
|
|
<n-descriptions-item label="类型">
|
|
<n-tag :type="getTypeTag(detailMessage.type)" size="small">{{ getTypeLabel(detailMessage.type) }}</n-tag>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="状态">
|
|
<n-tag :type="detailMessage.is_read ? 'success' : 'warning'" size="small">
|
|
{{ detailMessage.is_read ? '已读' : '未读' }}
|
|
</n-tag>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="用户">{{ detailMessage.user_name || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="时间">{{ detailMessage.created_at || '-' }}</n-descriptions-item>
|
|
</n-descriptions>
|
|
|
|
<div class="aksrt-message-detail">
|
|
<h4>{{ detailMessage.title }}</h4>
|
|
<p>{{ detailMessage.description || detailMessage.content || '暂无详细内容' }}</p>
|
|
<a
|
|
v-if="detailMessage.target_url"
|
|
:href="detailMessage.target_url"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
查看目标
|
|
</a>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<n-space>
|
|
<n-button @click="showDetail = false">关闭</n-button>
|
|
<n-button v-if="detailMessage && !detailMessage.is_read" type="primary" @click="markCurrentRead">
|
|
标记已读
|
|
</n-button>
|
|
</n-space>
|
|
</template>
|
|
</n-drawer-content>
|
|
</n-drawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
|
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
|
|
import PageHeader from '@/components/PageHeader.vue'
|
|
import api from '@/api'
|
|
|
|
const message = useMessage()
|
|
|
|
const loading = ref(false)
|
|
const markingAll = ref(false)
|
|
const showDetail = ref(false)
|
|
const detailMessage = ref(null)
|
|
const messages = ref([])
|
|
const typeStats = ref([])
|
|
const stats = reactive({ total: 0, unread: 0, today: 0, system: 0 })
|
|
const filters = reactive({ type: null, read: null })
|
|
const pagination = reactive({ page: 1, pageSize: 20, itemCount: 0, pageCount: 1, showSizePicker: true, pageSizes: [20, 50, 100] })
|
|
|
|
const readOptions = [
|
|
{ label: '未读', value: '0' },
|
|
{ label: '已读', value: '1' },
|
|
]
|
|
|
|
const typeOptions = computed(() => typeStats.value.map((item) => ({
|
|
label: getTypeLabel(item.type),
|
|
value: item.type,
|
|
})))
|
|
|
|
const columns = [
|
|
{
|
|
title: '类型',
|
|
key: 'type',
|
|
width: 120,
|
|
render: (row) => h(NTag, { type: getTypeTag(row.type), size: 'small' }, () => getTypeLabel(row.type)),
|
|
},
|
|
{ title: '标题', key: 'title', ellipsis: { tooltip: true } },
|
|
{ title: '用户', key: 'user_name', width: 140, ellipsis: { tooltip: true } },
|
|
{ title: '时间', key: 'created_at', width: 180 },
|
|
{
|
|
title: '状态',
|
|
key: 'is_read',
|
|
width: 90,
|
|
render: (row) => h(NTag, { type: row.is_read ? 'success' : 'warning', size: 'small' }, () => (row.is_read ? '已读' : '未读')),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 140,
|
|
render: (row) => h(NSpace, { size: 'small' }, () => [
|
|
h(NButton, { size: 'tiny', onClick: () => openDetail(row) }, () => '查看'),
|
|
!row.is_read ? h(NButton, { size: 'tiny', type: 'primary', onClick: () => markOneRead(row) }, () => '已读') : null,
|
|
]),
|
|
},
|
|
]
|
|
|
|
function getTypeTag(type) {
|
|
const map = {
|
|
notice: 'info',
|
|
system: 'info',
|
|
comment_reply: 'success',
|
|
comment_approved: 'success',
|
|
download_ready: 'primary',
|
|
verify_email: 'info',
|
|
pending_comments: 'warning',
|
|
}
|
|
return map[type] || 'default'
|
|
}
|
|
|
|
function getTypeLabel(type) {
|
|
const map = {
|
|
notice: '系统通知',
|
|
system: '系统通知',
|
|
comment_reply: '评论回复',
|
|
comment_approved: '评论审核',
|
|
download_ready: '下载就绪',
|
|
verify_email: '邮箱验证',
|
|
pending_comments: '待审评论',
|
|
}
|
|
return map[type] || type || '通知'
|
|
}
|
|
|
|
function rowClassName(row) {
|
|
return row.is_read ? '' : 'aksrt-row--unread'
|
|
}
|
|
|
|
async function loadMessages() {
|
|
loading.value = true
|
|
try {
|
|
const response = await api.getNotificationOverview({
|
|
page: pagination.page,
|
|
per_page: pagination.pageSize,
|
|
...(filters.type ? { type: filters.type } : {}),
|
|
...(filters.read !== null ? { is_read: filters.read } : {}),
|
|
})
|
|
|
|
const payload = response.data || {}
|
|
const summary = payload.summary || {}
|
|
const pager = payload.pagination || {}
|
|
|
|
messages.value = Array.isArray(payload.recent) ? payload.recent : []
|
|
typeStats.value = Array.isArray(payload.types) ? payload.types : []
|
|
|
|
stats.total = Number(summary.total || payload.total || 0)
|
|
stats.unread = Number(summary.unread || payload.unread || 0)
|
|
stats.today = Number(summary.today || payload.today || 0)
|
|
stats.system = Number(summary.system || payload.system || 0)
|
|
|
|
pagination.itemCount = Number(pager.total_items || messages.value.length)
|
|
pagination.pageCount = Number(pager.total_pages || 1)
|
|
pagination.page = Number(pager.page || pagination.page)
|
|
pagination.pageSize = Number(pager.per_page || pagination.pageSize)
|
|
} catch (error) {
|
|
message.error('加载消息失败: ' + (error.message || '未知错误'))
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openDetail(row) {
|
|
detailMessage.value = row
|
|
showDetail.value = true
|
|
}
|
|
|
|
async function markOneRead(row) {
|
|
try {
|
|
await api.markNotificationRead(row.id)
|
|
row.is_read = true
|
|
stats.unread = Math.max(0, stats.unread - 1)
|
|
if (detailMessage.value && detailMessage.value.id === row.id) {
|
|
detailMessage.value.is_read = true
|
|
}
|
|
message.success('已标记为已读')
|
|
} catch (error) {
|
|
message.error(error.message || '操作失败')
|
|
}
|
|
}
|
|
|
|
async function markCurrentRead() {
|
|
if (detailMessage.value) {
|
|
await markOneRead(detailMessage.value)
|
|
}
|
|
}
|
|
|
|
async function markAllRead() {
|
|
markingAll.value = true
|
|
try {
|
|
await api.markAllNotificationsRead()
|
|
messages.value = messages.value.map((item) => ({ ...item, is_read: true }))
|
|
if (detailMessage.value) {
|
|
detailMessage.value = { ...detailMessage.value, is_read: true }
|
|
}
|
|
stats.unread = 0
|
|
message.success('全部已读')
|
|
} catch (error) {
|
|
message.error(error.message || '操作失败')
|
|
} finally {
|
|
markingAll.value = false
|
|
}
|
|
}
|
|
|
|
function handleFilterChange() {
|
|
pagination.page = 1
|
|
loadMessages()
|
|
}
|
|
|
|
function handlePageChange(page) {
|
|
pagination.page = page
|
|
loadMessages()
|
|
}
|
|
|
|
function handlePageSizeChange(pageSize) {
|
|
pagination.pageSize = pageSize
|
|
pagination.page = 1
|
|
loadMessages()
|
|
}
|
|
|
|
onMounted(loadMessages)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aksrt-row--unread {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.aksrt-message-detail {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.aksrt-message-detail h4 {
|
|
margin: 0 0 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.aksrt-message-detail p {
|
|
margin: 0 0 12px;
|
|
color: #666;
|
|
line-height: 1.7;
|
|
white-space: pre-wrap;
|
|
}
|
|
</style>
|