Files
wordpress-theme-aksrt/assets/admin/src/pages/users/UsersMessages.vue
T
2026-08-25 17:57:20 +08:00

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>