260 lines
8.2 KiB
Vue
260 lines
8.2 KiB
Vue
<template>
|
|
<PageContainer title="站内通知中心">
|
|
<template #header>
|
|
<div class="header-row">
|
|
<el-space>
|
|
<el-tag type="info">总数 {{ total }}</el-tag>
|
|
<el-tag type="danger">未读 {{ unreadCount }}</el-tag>
|
|
<el-switch
|
|
v-if="canRead"
|
|
v-model="onlyUnread"
|
|
inline-prompt
|
|
active-text="仅看未读"
|
|
inactive-text="全部"
|
|
/>
|
|
<el-button
|
|
v-if="canRead && canMarkRead"
|
|
type="primary"
|
|
plain
|
|
class="notif-mark-all-read-btn"
|
|
:disabled="unreadCount <= 0"
|
|
@click="handleMarkAllRead"
|
|
>
|
|
全部标记已读
|
|
</el-button>
|
|
<el-button v-if="canRead" @click="load">刷新</el-button>
|
|
</el-space>
|
|
</div>
|
|
</template>
|
|
|
|
<el-alert
|
|
v-if="!canRead"
|
|
title="当前账号无查看站内通知权限"
|
|
type="warning"
|
|
show-icon
|
|
:closable="false"
|
|
class="mb-md"
|
|
/>
|
|
|
|
<el-table v-else :data="rows" class="mt-md" empty-text="暂无站内通知">
|
|
<el-table-column prop="title" label="标题" min-width="220" />
|
|
<el-table-column prop="content" label="内容" min-width="360" show-overflow-tooltip />
|
|
<el-table-column prop="status" label="状态" width="100" :formatter="statusFormatter" />
|
|
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
|
<el-table-column prop="readAt" label="已读时间" width="180" />
|
|
<el-table-column label="操作" width="190">
|
|
<template #default="{ row }">
|
|
<el-space wrap>
|
|
<el-button size="small" @click="openDetail(row)">查看详情</el-button>
|
|
<el-button
|
|
v-if="canMarkRead && row.status !== 'READ'"
|
|
type="primary"
|
|
size="small"
|
|
class="notif-mark-read-btn"
|
|
@click="handleMarkRead(row.id)"
|
|
>
|
|
标记已读
|
|
</el-button>
|
|
</el-space>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div v-if="canRead" class="flex-end mt-md">
|
|
<el-pagination
|
|
:current-page="pageNo"
|
|
:page-size="pageSize"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="total"
|
|
@current-change="handlePageChange"
|
|
@size-change="handlePageSizeChange"
|
|
/>
|
|
</div>
|
|
|
|
<InAppNotificationDetailDialog
|
|
v-model="detailVisible"
|
|
:notification="currentNotification"
|
|
:can-mark-read="canMarkRead"
|
|
@mark-read="handleMarkRead"
|
|
/>
|
|
</PageContainer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
import { ElMessage } from "element-plus";
|
|
import { fetchInAppNotifications, markAllInAppNotificationsRead, markInAppNotificationRead } from "../../api/modules";
|
|
import InAppNotificationDetailDialog from "../../components/InAppNotificationDetailDialog.vue";
|
|
import PageContainer from "../../components/PageContainer.vue";
|
|
import { PERMS } from "../../constants/permissions";
|
|
import { useAuthStore } from "../../stores/auth";
|
|
import { useNotificationStore } from "../../stores/notification";
|
|
import { toZhStatus } from "../../utils/status";
|
|
|
|
const authStore = useAuthStore();
|
|
const notificationStore = useNotificationStore();
|
|
|
|
const canRead = computed(() => authStore.scope === "TENANT" && authStore.hasPermission(PERMS.notification.inAppRead));
|
|
const canMarkRead = computed(
|
|
() => authStore.scope === "TENANT" && authStore.hasPermission(PERMS.notification.inAppMarkRead),
|
|
);
|
|
|
|
const onlyUnread = ref(false);
|
|
const pageNo = ref(1);
|
|
const pageSize = ref(20);
|
|
const total = ref(0);
|
|
const unreadCount = ref(0);
|
|
const rows = ref<Record<string, any>[]>([]);
|
|
const detailVisible = ref(false);
|
|
const currentNotification = ref<Record<string, any> | null>(null);
|
|
|
|
const statusFormatter = (_row: unknown, _column: unknown, value: unknown) => toZhStatus(value);
|
|
|
|
const syncCurrentNotification = (targetId?: number) => {
|
|
const id = Number(targetId || currentNotification.value?.id || 0);
|
|
if (!Number.isFinite(id) || id <= 0) {
|
|
return;
|
|
}
|
|
const nextRow = rows.value.find((item) => Number(item?.id || 0) === id) || null;
|
|
if (nextRow) {
|
|
currentNotification.value = nextRow;
|
|
}
|
|
};
|
|
|
|
const load = async () => {
|
|
if (!canRead.value) {
|
|
rows.value = [];
|
|
total.value = 0;
|
|
unreadCount.value = 0;
|
|
return;
|
|
}
|
|
let resp = await fetchInAppNotifications({
|
|
ts: Date.now(),
|
|
pageNo: pageNo.value,
|
|
pageSize: pageSize.value,
|
|
onlyUnread: onlyUnread.value,
|
|
});
|
|
rows.value = Array.isArray(resp?.data?.list) ? resp.data.list : [];
|
|
total.value = Number(resp?.data?.total || 0);
|
|
pageNo.value = Number(resp?.data?.pageNo || pageNo.value || 1);
|
|
pageSize.value = Number(resp?.data?.pageSize || pageSize.value || 20);
|
|
const maxPage = total.value > 0 ? Math.max(1, Math.ceil(total.value / pageSize.value)) : 1;
|
|
if (pageNo.value > maxPage) {
|
|
pageNo.value = maxPage;
|
|
resp = await fetchInAppNotifications({
|
|
ts: Date.now(),
|
|
pageNo: pageNo.value,
|
|
pageSize: pageSize.value,
|
|
onlyUnread: onlyUnread.value,
|
|
});
|
|
rows.value = Array.isArray(resp?.data?.list) ? resp.data.list : [];
|
|
total.value = Number(resp?.data?.total || 0);
|
|
pageNo.value = Number(resp?.data?.pageNo || pageNo.value || 1);
|
|
pageSize.value = Number(resp?.data?.pageSize || pageSize.value || 20);
|
|
}
|
|
unreadCount.value = Number(notificationStore.unreadCount || 0);
|
|
};
|
|
|
|
const handleMarkRead = async (id: number) => {
|
|
await markInAppNotificationRead(id);
|
|
await notificationStore.loadUnreadCount();
|
|
unreadCount.value = Number(notificationStore.unreadCount || 0);
|
|
await load();
|
|
syncCurrentNotification(id);
|
|
ElMessage.success("已标记为已读");
|
|
};
|
|
|
|
const handleMarkAllRead = async () => {
|
|
const resp = await markAllInAppNotificationsRead();
|
|
const affected = Number(resp?.data?.affected || 0);
|
|
await notificationStore.loadUnreadCount();
|
|
unreadCount.value = Number(notificationStore.unreadCount || 0);
|
|
if (onlyUnread.value && affected > 0) {
|
|
pageNo.value = 1;
|
|
}
|
|
await load();
|
|
syncCurrentNotification();
|
|
ElMessage.success(affected > 0 ? `已标记 ${affected} 条通知为已读` : "没有未读通知");
|
|
};
|
|
|
|
const openDetail = (row: Record<string, any>) => {
|
|
currentNotification.value = row;
|
|
detailVisible.value = true;
|
|
};
|
|
|
|
const handlePageChange = async (nextPage: number) => {
|
|
pageNo.value = Number(nextPage || 1);
|
|
await load();
|
|
};
|
|
|
|
const handlePageSizeChange = async (nextPageSize: number) => {
|
|
pageSize.value = Number(nextPageSize || 20);
|
|
pageNo.value = 1;
|
|
await load();
|
|
};
|
|
|
|
watch(rows, () => {
|
|
if (!detailVisible.value || !currentNotification.value) {
|
|
return;
|
|
}
|
|
syncCurrentNotification();
|
|
});
|
|
|
|
watch(onlyUnread, async () => {
|
|
if (!canRead.value) {
|
|
return;
|
|
}
|
|
pageNo.value = 1;
|
|
await load();
|
|
});
|
|
|
|
onMounted(async () => {
|
|
await notificationStore.loadUnreadCount();
|
|
unreadCount.value = Number(notificationStore.unreadCount || 0);
|
|
await load();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.header-row {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
:deep(.notif-mark-all-read-btn.el-button--primary.is-plain) {
|
|
background: rgba(var(--wo-theme-rgb), 0.14) !important;
|
|
border: 1px solid rgba(var(--wo-theme-rgb), 0.3) !important;
|
|
color: var(--wo-brand-primary-dark-2) !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
:deep(.notif-mark-all-read-btn.el-button--primary.is-plain:hover:not(.is-disabled)) {
|
|
background: rgba(var(--wo-theme-rgb), 0.22) !important;
|
|
border-color: var(--wo-brand-primary-dark-2) !important;
|
|
color: var(--wo-brand-primary-dark-2) !important;
|
|
box-shadow: 0 2px 10px rgba(var(--wo-theme-rgb), 0.18);
|
|
}
|
|
|
|
:deep(.notif-mark-read-btn.el-button--primary) {
|
|
background: var(--wo-brand-primary-dark-2) !important;
|
|
border: 1px solid var(--wo-brand-primary-dark-2) !important;
|
|
color: #fff !important;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 8px rgba(var(--wo-theme-rgb), 0.2);
|
|
}
|
|
|
|
:deep(.notif-mark-read-btn.el-button--primary:hover:not(.is-disabled)) {
|
|
background: var(--wo-brand-gradient) !important;
|
|
border-color: transparent !important;
|
|
color: #fff !important;
|
|
}
|
|
|
|
:deep(.notif-mark-all-read-btn.is-disabled),
|
|
:deep(.notif-mark-read-btn.is-disabled) {
|
|
box-shadow: none !important;
|
|
}
|
|
</style>
|