feat(widgets): add phase 4 validation and accessibility
This commit is contained in:
1 parent
8cad3c2d3d
commit
e1723a8f08
117 files changed
+6
-17338
No files matched your search
@@ -1,308 +0,0 @@
|
||||
<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',
|
||||
credits_added: 'warning',
|
||||
credits_deducted: 'warning',
|
||||
download_ready: 'primary',
|
||||
verify_email: 'info',
|
||||
pending_comments: 'warning',
|
||||
}
|
||||
return map[type] || 'default'
|
||||
}
|
||||
|
||||
function getTypeLabel(type) {
|
||||
const map = {
|
||||
notice: '系统通知',
|
||||
system: '系统通知',
|
||||
comment_reply: '评论回复',
|
||||
comment_approved: '评论审核',
|
||||
credits_added: '积分增加',
|
||||
credits_deducted: '积分扣减',
|
||||
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>
|
||||
Reference in new issue
Block a user