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..924ff1d9 --- /dev/null +++ b/backend/system/apis/notice.py @@ -0,0 +1,156 @@ +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, Users, Role +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") + + +class SchemaIn(ModelSchema): + class Config: + model = Notice + model_exclude = ['id', 'user', 'create_datetime', 'update_datetime', 'creator', 'modifier', 'belong_dept'] + + +class SchemaOut(ModelSchema): + class Config: + model = Notice + model_fields = '__all__' + + +class MarkReadSchema(Schema): + notice_ids: List[int] = Field(..., alias="notice_ids") + + +def is_admin_user(request) -> bool: + user_info = get_user_info_from_token(request) + user_id = user_info['id'] + user = get_object_or_404(Users, id=user_id) + if user_info.get('is_superuser', False): + return True + admin_roles = user.role.filter(admin=True) + return admin_roles.exists() + + +def get_current_user_id(request) -> int: + user_info = get_user_info_from_token(request) + return user_info['id'] + + +@router.post("/notice", response=SchemaOut) +def create_notice(request, data: SchemaIn): + user_id = get_current_user_id(request) + data_dic = data.dict() + data_dic['user_id'] = user_id + notice = create(request, data_dic, Notice) + return notice + + +@router.delete("/notice/{notice_id}") +def delete_notice(request, notice_id: int): + user_id = get_current_user_id(request) + notice = get_object_or_404(Notice, id=notice_id) + if notice.user_id != user_id and not is_admin_user(request): + return FuResponse(code=403, msg="无权限删除此消息") + delete(notice_id, Notice) + return {"success": True} + + +@router.put("/notice/{notice_id}", response=SchemaOut) +def update_notice(request, notice_id: int, payload: SchemaIn): + user_id = get_current_user_id(request) + notice = get_object_or_404(Notice, id=notice_id) + if notice.user_id != user_id and not is_admin_user(request): + return FuResponse(code=403, msg="无权限修改此消息") + notice = update(request, notice_id, payload, Notice) + return notice + + +@router.get("/notice", response=List[SchemaOut]) +@paginate(MyPagination) +def list_notice(request, filters: Filters = Query(...)): + if is_admin_user(request): + qs = retrieve(request, Notice, filters) + else: + user_id = get_current_user_id(request) + filters.user_id = user_id + qs = retrieve(request, Notice, filters) + return qs + + +@router.get("/notice/all/list", response=List[SchemaOut]) +def all_list_notice(request): + if not is_admin_user(request): + return FuResponse(code=403, msg="仅管理员可访问此接口") + qs = retrieve(request, Notice) + return qs + + +@router.get("/notice/{notice_id}", response=SchemaOut) +def get_notice(request, notice_id: int): + user_id = get_current_user_id(request) + notice = get_object_or_404(Notice, id=notice_id) + if notice.user_id != user_id and not is_admin_user(request): + return FuResponse(code=403, msg="无权限查看此消息") + return notice + + +@router.get("/notice/my/list", response=List[SchemaOut]) +def list_my_notice(request, notice_type: int = None, is_read: bool = None): + user_id = get_current_user_id(request) + filters = 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_id = get_current_user_id(request) + 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): + user_id = get_current_user_id(request) + notice = get_object_or_404(Notice, id=notice_id) + if notice.user_id != user_id: + return FuResponse(code=403, msg="无权限操作此消息") + notice.is_read = True + notice.save() + return {"success": True} + + +@router.put("/notice/mark/read/all") +def mark_all_read(request): + user_id = get_current_user_id(request) + 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_id = get_current_user_id(request) + 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..9b46e888 --- /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(data: MarkReadParams) { + return defHttp.put<{ success: boolean }>({ + url: Api.NoticeMarkBatchRead, + data, + }); +} + +export function createNotice(data: Partial) { + return defHttp.post({ + url: Api.NoticeList, + data, + }); +} + +export function updateNotice(noticeId: number, data: Partial) { + return defHttp.put({ + url: `${Api.NoticeList}/${noticeId}`, + 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 @@ -