From 64bbb0a987a1f66373b6470b7c2001a582c3394f Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 22:25:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(notice):=20=E9=A1=B6=E6=A0=8F=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E9=93=83=E9=93=9B=20+=20=E9=80=9A=E7=9F=A5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ty-notice 铃铛(未读角标 + 列表 + 已读)+ notice store 轮询 - 通知管理页(ty-crud) Co-Authored-By: Claude Opus 4.8 --- components.d.ts | 6 ++ src/api/notice.ts | 89 +++++++++++++++++++++ src/components/ty-header.vue | 1 + src/components/ty-notice.vue | 128 ++++++++++++++++++++++++++++++ src/pages/system/notice/index.vue | 63 +++++++++++++++ src/stores/modules/notice.ts | 45 +++++++++++ typed-router.d.ts | 13 +++ 7 files changed, 345 insertions(+) create mode 100644 src/api/notice.ts create mode 100644 src/components/ty-notice.vue create mode 100644 src/pages/system/notice/index.vue create mode 100644 src/stores/modules/notice.ts diff --git a/components.d.ts b/components.d.ts index 316f41e..7c60292 100644 --- a/components.d.ts +++ b/components.d.ts @@ -13,6 +13,7 @@ export {} declare module 'vue' { export interface GlobalComponents { ChangePasswordForm: typeof import('./src/components/change-password-form.vue')['default'] + ElBadge: typeof import('element-plus/es')['ElBadge'] ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElButton: typeof import('element-plus/es')['ElButton'] @@ -36,6 +37,7 @@ declare module 'vue' { ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] + ElPopover: typeof import('element-plus/es')['ElPopover'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElResult: typeof import('element-plus/es')['ElResult'] @@ -59,6 +61,7 @@ declare module 'vue' { TyForm: typeof import('./src/components/ty-form.vue')['default'] TyHeader: typeof import('./src/components/ty-header.vue')['default'] TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default'] + TyNotice: typeof import('./src/components/ty-notice.vue')['default'] TySearchForm: typeof import('./src/components/ty-search-form.vue')['default'] TySetting: typeof import('./src/components/ty-setting.vue')['default'] TySettingPanel: typeof import('./src/components/ty-setting-panel.vue')['default'] @@ -74,6 +77,7 @@ declare module 'vue' { // For TSX support declare global { const ChangePasswordForm: typeof import('./src/components/change-password-form.vue')['default'] + const ElBadge: typeof import('element-plus/es')['ElBadge'] const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] const ElButton: typeof import('element-plus/es')['ElButton'] @@ -97,6 +101,7 @@ declare global { const ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] const ElOption: typeof import('element-plus/es')['ElOption'] const ElPagination: typeof import('element-plus/es')['ElPagination'] + const ElPopover: typeof import('element-plus/es')['ElPopover'] const ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] const ElResult: typeof import('element-plus/es')['ElResult'] @@ -120,6 +125,7 @@ declare global { const TyForm: typeof import('./src/components/ty-form.vue')['default'] const TyHeader: typeof import('./src/components/ty-header.vue')['default'] const TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default'] + const TyNotice: typeof import('./src/components/ty-notice.vue')['default'] const TySearchForm: typeof import('./src/components/ty-search-form.vue')['default'] const TySetting: typeof import('./src/components/ty-setting.vue')['default'] const TySettingPanel: typeof import('./src/components/ty-setting-panel.vue')['default'] diff --git a/src/api/notice.ts b/src/api/notice.ts new file mode 100644 index 0000000..88fe0e4 --- /dev/null +++ b/src/api/notice.ts @@ -0,0 +1,89 @@ +import { http } from '@/api/http' + +export type NoticeStatus = 'enabled' | 'disabled' + +export interface Notice { + id: string + tenantCode: string + title: string + content: string + status: NoticeStatus + createdAt: number + updatedAt: number +} + +export interface MyNotice { + id: string + title: string + content: string + status: string + createdAt: number + readAt: number | null +} + +export interface NoticeListQuery { + page: number + pageSize: number + title?: string + status?: string +} + +export interface NoticeListResult { + list: Notice[] + total: number + page: number + pageSize: number +} + +export interface MyNoticeListResult { + list: MyNotice[] + total: number + page: number + pageSize: number +} + +export interface NoticeUpsertData { + title: string + content: string + status?: string +} + +/** 通知分页(管理侧)。 */ +export function listNotices(query: NoticeListQuery, signal?: AbortSignal): Promise { + const params: Record = { page: query.page, pageSize: query.pageSize } + if (query.title) params.title = query.title + if (query.status) params.status = query.status + return http.get('/notices', { params, signal }) +} + +export function createNotice(data: NoticeUpsertData): Promise { + return http.post('/notices', data) +} + +export function updateNotice(id: string, data: NoticeUpsertData): Promise { + return http.patch(`/notices/${id}`, data) +} + +export function deleteNotice(id: string): Promise { + return http.delete(`/notices/${id}`) +} + +/** 当前用户可见通知(含已读状态)。 */ +export function listMyNotices(query: { page: number; pageSize: number }, signal?: AbortSignal): Promise { + return http.get('/notices/mine', { params: query, signal }) +} + +/** 未读通知数。 */ +export function getUnreadCount(signal?: AbortSignal): Promise { + return http.get('/notices/unread-count', { signal }) +} + +/** 标记单条已读。 */ +export function markRead(id: string): Promise { + return http.post(`/notices/${id}/read`) +} + +/** 标记全部已读。 */ +export function markAllRead(): Promise { + return http.post('/notices/read-all') +} diff --git a/src/components/ty-header.vue b/src/components/ty-header.vue index 904b63d..244f52d 100644 --- a/src/components/ty-header.vue +++ b/src/components/ty-header.vue @@ -49,6 +49,7 @@ + diff --git a/src/components/ty-notice.vue b/src/components/ty-notice.vue new file mode 100644 index 0000000..8a33304 --- /dev/null +++ b/src/components/ty-notice.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/pages/system/notice/index.vue b/src/pages/system/notice/index.vue new file mode 100644 index 0000000..191c10a --- /dev/null +++ b/src/pages/system/notice/index.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/stores/modules/notice.ts b/src/stores/modules/notice.ts new file mode 100644 index 0000000..e9350d9 --- /dev/null +++ b/src/stores/modules/notice.ts @@ -0,0 +1,45 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import { getUnreadCount, listMyNotices, markAllRead, markRead, type MyNotice } from '@/api/notice' + +/** 站内通知:未读数 + 最近列表 + 轮询。 */ +export const useNoticeStore = defineStore('noticeStore', () => { + const unreadCount = ref(0) + const list = ref([]) + let timer: number | null = null + + async function fetchUnread(): Promise { + unreadCount.value = await getUnreadCount() + } + + async function fetchList(): Promise { + const r = await listMyNotices({ page: 1, pageSize: 10 }) + list.value = r.list + } + + async function readOne(id: string): Promise { + await markRead(id) + await Promise.all([fetchUnread(), fetchList()]) + } + + async function readAll(): Promise { + await markAllRead() + await Promise.all([fetchUnread(), fetchList()]) + } + + function startPolling(interval = 30000): void { + stopPolling() + timer = window.setInterval(() => { + void fetchUnread() + }, interval) + } + + function stopPolling(): void { + if (timer != null) { + clearInterval(timer) + timer = null + } + } + + return { unreadCount, list, fetchUnread, fetchList, readOne, readAll, startPolling, stopPolling } +}) diff --git a/typed-router.d.ts b/typed-router.d.ts index 2f3c9dc..227d33f 100644 --- a/typed-router.d.ts +++ b/typed-router.d.ts @@ -114,6 +114,13 @@ declare module 'vue-router/auto-routes' { Record, | never >, + '/system/notice/': RouteRecordInfo< + '/system/notice/', + '/system/notice', + Record, + Record, + | never + >, '/system/org/': RouteRecordInfo< '/system/org/', '/system/org', @@ -268,6 +275,12 @@ declare module 'vue-router/auto-routes' { views: | never } + 'src/pages/system/notice/index.vue': { + routes: + | '/system/notice/' + views: + | never + } 'src/pages/system/org/index.vue': { routes: | '/system/org/'