From 9dd80f5ecb9467fd26af5f625d37fe4ca25f82ed Mon Sep 17 00:00:00 2001 From: CGW406 <13565294+cgw406@user.noreply.gitee.com> Date: Sun, 26 Apr 2026 18:55:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(notice):=20=E5=AE=9E=E7=8E=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端添加Notice模型、API路由和管理界面 - 前端实现消息列表、未读标记和全部已读功能 - 完善消息类型分类和显示样式 - 添加消息点击处理和未读计数逻辑 --- backend/system/admin.py | 23 +- backend/system/apis/notice.py | 123 +++++++++ backend/system/models.py | 25 ++ backend/system/router.py | 2 + web/src/api/sys/model/noticeModel.ts | 46 ++++ web/src/api/sys/notice.ts | 70 +++++ .../header/components/notify/NoticeList.vue | 161 +++++------- .../header/components/notify/index.vue | 239 +++++++++++++++--- 8 files changed, 555 insertions(+), 134 deletions(-) create mode 100644 backend/system/apis/notice.py create mode 100644 web/src/api/sys/model/noticeModel.ts create mode 100644 web/src/api/sys/notice.ts diff --git a/backend/system/admin.py b/backend/system/admin.py index 8c38f3f3..a8f4bdaf 100644 --- a/backend/system/admin.py +++ b/backend/system/admin.py @@ -1,3 +1,24 @@ from django.contrib import admin +from system.models import Notice -# Register your models here. +@admin.register(Notice) +class NoticeAdmin(admin.ModelAdmin): + list_display = ['title', 'notice_type', 'is_read', 'user', 'create_datetime'] + list_filter = ['notice_type', 'is_read', 'create_datetime'] + search_fields = ['title', 'content'] + ordering = ['-create_datetime'] + readonly_fields = ['create_datetime', 'update_datetime'] + + fieldsets = ( + (None, { + 'fields': ('title', 'content', 'notice_type', 'is_read', 'user') + }), + ('额外信息', { + 'fields': ('extra', 'color', 'remark'), + 'classes': ('collapse',) + }), + ('时间信息', { + 'fields': ('create_datetime', 'update_datetime'), + 'classes': ('collapse',) + }), + ) diff --git a/backend/system/apis/notice.py b/backend/system/apis/notice.py new file mode 100644 index 00000000..b912f751 --- /dev/null +++ b/backend/system/apis/notice.py @@ -0,0 +1,123 @@ +from typing import List + +from django.shortcuts import get_object_or_404 +from ninja import Field, ModelSchema, Query, Router, Schema +from ninja.pagination import paginate +from system.models import Notice +from utils.fu_crud import create, delete, retrieve, update +from utils.fu_ninja import FuFilters, MyPagination +from utils.fu_response import FuResponse +from utils.usual import get_user_info_from_token + +router = Router() + + +class Filters(FuFilters): + title: str = Field(None, alias="title") + notice_type: int = Field(None, alias="notice_type") + is_read: bool = Field(None, alias="is_read") + user_id: int = Field(None, alias="user_id") + + +class SchemaIn(ModelSchema): + user_id: int = Field(None, alias="user") + + class Config: + model = Notice + model_exclude = ['id', 'user', 'create_datetime', 'update_datetime'] + + +class SchemaOut(ModelSchema): + class Config: + model = Notice + model_fields = '__all__' + + +class MarkReadSchema(Schema): + notice_ids: List[int] = Field(..., alias="notice_ids") + + +@router.post("/notice", response=SchemaOut) +def create_notice(request, data: SchemaIn): + notice = create(request, data.dict(), Notice) + return notice + + +@router.delete("/notice/{notice_id}") +def delete_notice(request, notice_id: int): + delete(notice_id, Notice) + return {"success": True} + + +@router.put("/notice/{notice_id}", response=SchemaOut) +def update_notice(request, notice_id: int, payload: SchemaIn): + notice = update(request, notice_id, payload, Notice) + return notice + + +@router.get("/notice", response=List[SchemaOut]) +@paginate(MyPagination) +def list_notice(request, filters: Filters = Query(...)): + qs = retrieve(request, Notice, filters) + return qs + + +@router.get("/notice/all/list", response=List[SchemaOut]) +def all_list_notice(request): + qs = retrieve(request, Notice) + return qs + + +@router.get("/notice/{notice_id}", response=SchemaOut) +def get_notice(request, notice_id: int): + notice = get_object_or_404(Notice, id=notice_id) + return notice + + +@router.get("/notice/my/list", response=List[SchemaOut]) +def list_my_notice(request, notice_type: int = None, is_read: bool = None): + user_info = get_user_info_from_token(request) + user_id = user_info['id'] + filters = Filters(user_id=user_id) + if notice_type is not None: + filters.notice_type = notice_type + if is_read is not None: + filters.is_read = is_read + qs = retrieve(request, Notice, filters) + return qs + + +@router.get("/notice/my/unread/count") +def get_my_unread_count(request): + user_info = get_user_info_from_token(request) + user_id = user_info['id'] + count = Notice.objects.filter(user_id=user_id, is_read=False).count() + return {"count": count} + + +@router.put("/notice/mark/read/{notice_id}") +def mark_read(request, notice_id: int): + notice = get_object_or_404(Notice, id=notice_id) + notice.is_read = True + notice.save() + return {"success": True} + + +@router.put("/notice/mark/read/all") +def mark_all_read(request): + user_info = get_user_info_from_token(request) + user_id = user_info['id'] + Notice.objects.filter(user_id=user_id, is_read=False).update(is_read=True) + return {"success": True} + + +@router.put("/notice/mark/read/batch") +def mark_batch_read(request, data: MarkReadSchema): + user_info = get_user_info_from_token(request) + user_id = user_info['id'] + Notice.objects.filter( + user_id=user_id, + id__in=data.notice_ids, + is_read=False + ).update(is_read=True) + return {"success": True} diff --git a/backend/system/models.py b/backend/system/models.py index f7dbbbeb..12a510e8 100644 --- a/backend/system/models.py +++ b/backend/system/models.py @@ -1,6 +1,7 @@ import hashlib import os +from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models from utils.models import CoreModel @@ -410,3 +411,27 @@ class Meta: verbose_name = '代码生成器模板' verbose_name_plural = verbose_name ordering = ('-create_datetime',) + + +class Notice(CoreModel): + NOTICE_TYPE_CHOICES = ( + (0, "通知"), + (1, "消息"), + (2, "待办"), + ) + title = models.CharField(max_length=255, verbose_name="标题", help_text="标题") + content = models.TextField(verbose_name="内容", help_text="内容", null=True, blank=True) + notice_type = models.IntegerField(choices=NOTICE_TYPE_CHOICES, default=0, verbose_name="消息类型", help_text="消息类型(0:通知 1:消息 2:待办)") + is_read = models.BooleanField(default=False, verbose_name="是否已读", help_text="是否已读") + user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name="接收用户", help_text="接收用户", on_delete=models.CASCADE, db_constraint=False, related_name='notices') + extra = models.CharField(max_length=64, verbose_name="额外信息", help_text="额外信息(如状态标签)", null=True, blank=True) + color = models.CharField(max_length=32, verbose_name="标签颜色", help_text="标签颜色", null=True, blank=True) + + class Meta: + db_table = "system_notice" + verbose_name = '消息通知' + verbose_name_plural = verbose_name + ordering = ('-create_datetime',) + + def __str__(self): + return self.title diff --git a/backend/system/router.py b/backend/system/router.py index 89df0841..22ed3f06 100644 --- a/backend/system/router.py +++ b/backend/system/router.py @@ -26,6 +26,7 @@ from system.apis.monitor import router as monitor_router from system.apis.menu_column import router as menu_column_field_router from system.apis.code_generator import router as generator_template_router +from system.apis.notice import router as notice_router system_router = Router() system_router.add_router('/', dept_router, tags=["Dept"]) @@ -49,3 +50,4 @@ system_router.add_router('/', monitor_router, tags=["Monitor"]) system_router.add_router('/', menu_column_field_router, tags=["MenuColumnField"]) system_router.add_router('/', generator_template_router, tags=["GeneratorTemplate"]) +system_router.add_router('/', notice_router, tags=["Notice"]) diff --git a/web/src/api/sys/model/noticeModel.ts b/web/src/api/sys/model/noticeModel.ts new file mode 100644 index 00000000..4939a0ef --- /dev/null +++ b/web/src/api/sys/model/noticeModel.ts @@ -0,0 +1,46 @@ +export interface NoticeModel { + id: number; + title: string; + content: string; + notice_type: number; + is_read: boolean; + user: number; + extra: string; + color: string; + remark: string; + create_datetime: string; + update_datetime: string; + creator: number; + modifier: string; + belong_dept: number; + sort: number; +} + +export interface NoticeListItem { + id: string; + avatar: string; + title: string; + titleDelete?: boolean; + datetime: string; + type: string; + read?: boolean; + description: string; + clickClose?: boolean; + extra?: string; + color?: string; +} + +export interface NoticeTabItem { + key: string; + name: string; + list: NoticeListItem[]; + unreadlist?: NoticeListItem[]; +} + +export interface UnreadCountModel { + count: number; +} + +export interface MarkReadParams { + notice_ids: number[]; +} diff --git a/web/src/api/sys/notice.ts b/web/src/api/sys/notice.ts new file mode 100644 index 00000000..24c06cf0 --- /dev/null +++ b/web/src/api/sys/notice.ts @@ -0,0 +1,70 @@ +import { defHttp } from '/@/utils/http/axios'; +import { NoticeModel, UnreadCountModel, MarkReadParams } from './model/noticeModel'; + +enum Api { + NoticeList = '/api/system/notice', + NoticeMyList = '/api/system/notice/my/list', + NoticeUnreadCount = '/api/system/notice/my/unread/count', + NoticeMarkRead = '/api/system/notice/mark/read', + NoticeMarkAllRead = '/api/system/notice/mark/read/all', + NoticeMarkBatchRead = '/api/system/notice/mark/read/batch', +} + +export function getNoticeList(params?: Recordable) { + return defHttp.get({ + url: Api.NoticeList, + params, + }); +} + +export function getMyNoticeList(params?: { notice_type?: number; is_read?: boolean }) { + return defHttp.get({ + url: Api.NoticeMyList, + params, + }); +} + +export function getUnreadCount() { + return defHttp.get({ + url: Api.NoticeUnreadCount, + }); +} + +export function markNoticeRead(noticeId: number) { + return defHttp.put<{ success: boolean }>({ + url: `${Api.NoticeMarkRead}/${noticeId}`, + }); +} + +export function markAllNoticeRead() { + return defHttp.put<{ success: boolean }>({ + url: Api.NoticeMarkAllRead, + }); +} + +export function markBatchNoticeRead(params: MarkReadParams) { + return defHttp.put<{ success: boolean }>({ + url: Api.NoticeMarkBatchRead, + params, + }); +} + +export function createNotice(data: Partial) { + return defHttp.post({ + url: Api.NoticeList, + params: data, + }); +} + +export function updateNotice(noticeId: number, data: Partial) { + return defHttp.put({ + url: `${Api.NoticeList}/${noticeId}`, + params: data, + }); +} + +export function deleteNotice(noticeId: number) { + return defHttp.delete<{ success: boolean }>({ + url: `${Api.NoticeList}/${noticeId}`, + }); +} diff --git a/web/src/layouts/default/header/components/notify/NoticeList.vue b/web/src/layouts/default/header/components/notify/NoticeList.vue index 28645fce..ca64d2c2 100644 --- a/web/src/layouts/default/header/components/notify/NoticeList.vue +++ b/web/src/layouts/default/header/components/notify/NoticeList.vue @@ -11,8 +11,8 @@ :style="{ cursor: isTitleClickable ? 'pointer' : '' }" :delete="!!item.titleDelete" :ellipsis=" - $props.titleRows && $props.titleRows > 0 - ? { rows: $props.titleRows, tooltip: !!item.title } + titleRows && titleRows > 0 + ? { rows: titleRows, tooltip: !!item.title } : false " :content="item.title" @@ -36,8 +36,8 @@ -