refactor: decouple notification logic and align my notice endpoint

This commit is contained in:
Ray.Hao
2025-11-27 22:07:07 +08:00
parent 793dca66b1
commit f8f194c13c
10 changed files with 160 additions and 99 deletions

View File

@@ -211,7 +211,7 @@ export default defineMock([
// 我的通知分页列表
{
url: "notices/my-page",
url: "notices/my",
method: ["GET"],
body: {
code: "00000",

View File

@@ -46,7 +46,7 @@ const NoticeAPI = {
/** 获取我的通知分页列表 */
getMyNoticePage(queryParams?: NoticePageQuery) {
return request<any, PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/my-page`,
url: `${NOTICE_BASE_URL}/my`,
method: "get",
params: queryParams,
});

View File

@@ -79,84 +79,32 @@
</template>
<script setup lang="ts">
import NoticeAPI, { NoticePageVO, NoticeDetailVO } from "@/api/system/notice-api";
import router from "@/router";
import { useNotificationCenter } from "@/composables/notice/useNotificationCenter";
const noticeList = ref<NoticePageVO[]>([]);
const noticeDialogVisible = ref(false);
const noticeDetail = ref<NoticeDetailVO | null>(null);
const {
noticeList,
noticeDialogVisible,
noticeDetail,
fetchMyNotices,
readNotice,
markAllAsRead,
viewMore,
} = useNotificationCenter();
import { useStomp } from "@/composables/websocket/useStomp";
const { subscribe, unsubscribe, isConnected } = useStomp();
watch(
() => isConnected.value,
(connected) => {
if (connected) {
subscribe("/user/queue/message", (message: any) => {
console.log("收到通知消息:", message);
const data = JSON.parse(message.body);
const id = data.id;
if (!noticeList.value.some((notice) => notice.id == id)) {
noticeList.value.unshift({
id,
title: data.title,
type: data.type,
publishTime: data.publishTime,
});
ElNotification({
title: "您收到一条新的通知消息!",
message: data.title,
type: "success",
position: "bottom-right",
});
}
});
}
}
);
/**
* 获取我的通知公告
*/
function featchMyNotice() {
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then((data) => {
noticeList.value = data.list;
});
}
// 阅读通知公告
function handleReadNotice(id: string) {
NoticeAPI.getDetail(id).then((data) => {
noticeDialogVisible.value = true;
noticeDetail.value = data;
// 标记为已读
const index = noticeList.value.findIndex((notice) => notice.id === id);
if (index >= 0) {
noticeList.value.splice(index, 1);
}
});
readNotice(id);
}
// 查看更多
function handleViewMoreNotice() {
router.push({ name: "MyNotice" });
viewMore();
}
// 全部已读
function handleMarkAllAsRead() {
NoticeAPI.readAll().then(() => {
noticeList.value = [];
});
markAllAsRead();
}
onMounted(() => {
featchMyNotice();
});
onBeforeUnmount(() => {
unsubscribe("/user/queue/message");
fetchMyNotices();
});
</script>

View File

@@ -265,6 +265,5 @@ export function useAiAction(options: UseAiActionOptions = {}) {
executeAiAction,
executeCommand,
handleAutoSearch,
init,
};
}

View File

@@ -8,7 +8,7 @@ export { useLayout } from "./layout/useLayout";
export { useLayoutMenu } from "./layout/useLayoutMenu";
export { useDeviceDetection } from "./layout/useDeviceDetection";
export { useAiAction } from "./useAiAction";
export type { UseAiActionOptions, AiActionHandler } from "./useAiAction";
export { useAiAction } from "./ai/useAiAction";
export type { UseAiActionOptions, AiActionHandler } from "./ai/useAiAction";
export { useTableSelection } from "./useTableSelection";
export { useTableSelection } from "./table/useTableSelection";

View File

@@ -0,0 +1,106 @@
import { ref, onMounted, onBeforeUnmount } from "vue";
import NoticeAPI, {
type NoticePageVO,
type NoticeDetailVO,
type NoticePageQuery,
} from "@/api/system/notice-api";
import { useStomp } from "@/composables/websocket/useStomp";
import router from "@/router";
const DEFAULT_PAGE_SIZE = 5;
const noticeList = ref<NoticePageVO[]>([]);
const noticeDialogVisible = ref(false);
const noticeDetail = ref<NoticeDetailVO | null>(null);
const { subscribe, unsubscribe, isConnected } = useStomp();
let subscribed = false;
function normalizeQuery(params?: Partial<NoticePageQuery>): NoticePageQuery {
return {
pageNum: 1,
pageSize: DEFAULT_PAGE_SIZE,
isRead: 0,
...(params || {}),
} as NoticePageQuery;
}
async function fetchMyNotices(params?: Partial<NoticePageQuery>) {
const query = normalizeQuery(params);
const page = await NoticeAPI.getMyNoticePage(query);
noticeList.value = page.list || [];
}
async function readNotice(id: string) {
const data = await NoticeAPI.getDetail(id);
noticeDetail.value = data;
noticeDialogVisible.value = true;
const index = noticeList.value.findIndex((item) => item.id === id);
if (index >= 0) {
noticeList.value.splice(index, 1);
}
}
async function markAllAsRead() {
await NoticeAPI.readAll();
noticeList.value = [];
}
function viewMore() {
router.push({ name: "MyNotice" });
}
function setupStompSubscription() {
if (subscribed || !isConnected.value) return;
subscribe("/user/queue/message", (message: any) => {
try {
const data = JSON.parse(message.body || "{}");
const id = data.id;
if (!id) return;
if (!noticeList.value.some((item) => item.id === id)) {
noticeList.value.unshift({
id,
title: data.title,
type: data.type,
publishTime: data.publishTime,
} as NoticePageVO);
ElNotification({
title: "您收到一条新的通知消息!",
message: data.title,
type: "success",
position: "bottom-right",
});
}
} catch (e) {
console.error("解析通知消息失败", e);
}
});
subscribed = true;
}
export function useNotificationCenter() {
onMounted(() => {
fetchMyNotices();
setupStompSubscription();
});
onBeforeUnmount(() => {
unsubscribe("/user/queue/message");
subscribed = false;
});
return {
noticeList,
noticeDialogVisible,
noticeDetail,
fetchMyNotices,
readNotice,
markAllAsRead,
viewMore,
};
}

View File

@@ -61,7 +61,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "my-notice",
name: "MyNotice",
component: () => import("@/views/system/notice/components/MyNotice.vue"),
component: () => import("@/views/profile/notice/index.vue"),
meta: { title: "我的通知", icon: "user", hidden: true },
},
{

View File

@@ -323,7 +323,8 @@ onBeforeUnmount(() => {
flex-direction: column;
gap: 1.5rem;
justify-content: flex-start;
width: min(560px, 100%);
justify-self: end;
width: min(520px, 100%);
padding: clamp(2rem, 3vw, 2.75rem);
margin-inline: auto;
background: rgba(255, 255, 255, 0.95);

View File

@@ -8,18 +8,18 @@
v-model="queryParams.title"
placeholder="关键字"
clearable
@keyup.enter="handleQuery()"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" @click="handleQuery()">
<el-button type="primary" @click="handleQuery">
<template #icon>
<Search />
</template>
搜索
</el-button>
<el-button @click="handleResetQuery()">
<el-button @click="handleResetQuery">
<template #icon>
<Refresh />
</template>
@@ -44,7 +44,6 @@
<DictLabel v-model="scope.row.type" code="notice_type" />
</template>
</el-table-column>
<el-table-column align="center" label="发布人" prop="publisherName" width="100" />
<el-table-column align="center" label="通知等级" width="100">
<template #default="scope">
<DictLabel v-model="scope.row.level" code="notice_level" />
@@ -57,7 +56,6 @@
prop="publishTime"
width="150"
/>
<el-table-column align="center" label="发布人" prop="publisherName" width="150" />
<el-table-column align="center" label="状态" width="100">
<template #default="scope">
@@ -79,7 +77,7 @@
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="handleQuery()"
@pagination="handleQuery"
/>
</el-card>
@@ -115,11 +113,16 @@ defineOptions({
inheritAttrs: false,
});
import NoticeAPI, { NoticePageVO, NoticePageQuery, NoticeDetailVO } from "@/api/system/notice-api";
import { onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import NoticeAPI, {
type NoticePageVO,
type NoticePageQuery,
type NoticeDetailVO,
} from "@/api/system/notice-api";
const queryFormRef = ref();
const pageData = ref<NoticePageVO[]>([]);
const loading = ref(false);
const total = ref(0);
@@ -131,32 +134,35 @@ const queryParams = reactive<NoticePageQuery>({
const noticeDialogVisible = ref(false);
const noticeDetail = ref<NoticeDetailVO | null>(null);
//
function handleQuery() {
async function handleQuery() {
loading.value = true;
NoticeAPI.getMyNoticePage(queryParams)
.then((data) => {
pageData.value = data.list;
total.value = data.total;
})
.finally(() => {
loading.value = false;
});
try {
const data = await NoticeAPI.getMyNoticePage(queryParams);
pageData.value = data.list;
total.value = data.total;
} catch (error) {
ElMessage.error("获取通知列表失败");
console.error("获取我的通知失败", error);
} finally {
loading.value = false;
}
}
//
function handleResetQuery() {
queryFormRef.value!.resetFields();
queryFormRef.value?.resetFields();
queryParams.pageNum = 1;
handleQuery();
}
//
function handleReadNotice(id: string) {
NoticeAPI.getDetail(id).then((data) => {
noticeDialogVisible.value = true;
async function handleReadNotice(id: string) {
try {
const data = await NoticeAPI.getDetail(id);
noticeDetail.value = data;
});
noticeDialogVisible.value = true;
} catch (error) {
ElMessage.error("获取通知详情失败");
console.error("获取通知详情失败", error);
}
}
onMounted(() => {
@@ -168,6 +174,7 @@ onMounted(() => {
:deep(.el-dialog__header) {
text-align: center;
}
.notice-detail {
&__wrapper {
padding: 0 20px;