完成提醒模块

This commit is contained in:
柏码の讲师 2023-10-28 19:05:15 +08:00
parent b52f17c95a
commit 2e2929c749
8 changed files with 465 additions and 203 deletions

View File

@ -0,0 +1,36 @@
package com.example.controller;
import com.example.entity.RestBean;
import com.example.entity.vo.response.NotificationVO;
import com.example.service.NotificationService;
import com.example.utils.Const;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.Min;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/notification")
public class NotificationController {
@Resource
NotificationService service;
@GetMapping("/list")
public RestBean<List<NotificationVO>> listNotification(@RequestAttribute(Const.ATTR_USER_ID) int id) {
return RestBean.success(service.findUserNotification(id));
}
@GetMapping("/delete")
public RestBean<List<NotificationVO>> deleteNotification(@RequestParam @Min(0) int id,
@RequestAttribute(Const.ATTR_USER_ID) int uid) {
service.deleteUserNotification(id, uid);
return RestBean.success();
}
@GetMapping("/delete-all")
public RestBean<List<NotificationVO>> deleteAllNotification(@RequestAttribute(Const.ATTR_USER_ID) int uid) {
service.deleteUserAllNotification(uid);
return RestBean.success();
}
}

View File

@ -0,0 +1,20 @@
package com.example.entity.dto;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.entity.BaseData;
import lombok.Data;
@Data
@TableName("db_notification")
public class Notification implements BaseData {
@TableId(type = IdType.AUTO)
Integer id;
Integer uid;
String title;
String content;
String type;
String url;
String time;
}

View File

@ -0,0 +1,15 @@
package com.example.entity.vo.response;
import lombok.Data;
import java.util.Date;
@Data
public class NotificationVO {
Integer id;
String title;
String content;
String type;
String url;
Date time;
}

View File

@ -0,0 +1,9 @@
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.Notification;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NotificationMapper extends BaseMapper<Notification> {
}

View File

@ -0,0 +1,14 @@
package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.dto.Notification;
import com.example.entity.vo.response.NotificationVO;
import java.util.List;
public interface NotificationService extends IService<Notification> {
List<NotificationVO> findUserNotification(int uid);
void deleteUserNotification(int id, int uid);
void deleteUserAllNotification(int uid);
void addNotification(int uid, String title, String content, String type, String url);
}

View File

@ -0,0 +1,41 @@
package com.example.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.dto.Notification;
import com.example.entity.vo.response.NotificationVO;
import com.example.mapper.NotificationMapper;
import com.example.service.NotificationService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class NotificationServiceImpl extends ServiceImpl<NotificationMapper, Notification> implements NotificationService {
@Override
public List<NotificationVO> findUserNotification(int uid) {
return this.list(Wrappers.<Notification>query().eq("uid", uid))
.stream()
.map(notification -> notification.asViewObject(NotificationVO.class))
.toList();
}
public void deleteUserNotification(int id, int uid){
this.remove(Wrappers.<Notification>query().eq("id", id).eq("uid", uid));
}
public void deleteUserAllNotification(int uid){
this.remove(Wrappers.<Notification>query().eq("uid", uid));
}
@Override
public void addNotification(int uid, String title, String content, String type, String url) {
Notification notification = new Notification();
notification.setUid(uid);
notification.setTitle(title);
notification.setContent(content);
notification.setType(type);
notification.setUrl(url);
this.save(notification);
}
}

View File

@ -14,6 +14,7 @@ import com.example.entity.vo.response.TopicDetailVO;
import com.example.entity.vo.response.TopicPreviewVO;
import com.example.entity.vo.response.TopicTopVO;
import com.example.mapper.*;
import com.example.service.NotificationService;
import com.example.service.TopicService;
import com.example.utils.CacheUtils;
import com.example.utils.Const;
@ -58,6 +59,9 @@ public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements
@Resource
StringRedisTemplate template;
@Resource
NotificationService notificationService;
private Set<Integer> types = null;
@PostConstruct
private void initTypes() {
@ -122,6 +126,26 @@ public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements
BeanUtils.copyProperties(vo, comment);
comment.setTime(new Date());
commentMapper.insert(comment);
Topic topic = baseMapper.selectById(vo.getTid());
Account account = accountMapper.selectById(uid);
if(vo.getQuote() > 0) {
TopicComment com = commentMapper.selectById(vo.getQuote());
if(!Objects.equals(account.getId(), com.getUid())) {
notificationService.addNotification(
com.getUid(),
"您有新的帖子评论回复",
account.getUsername()+" 回复了你发表的评论,快去看看吧!",
"success", "/index/topic-detail/"+com.getTid()
);
}
} else if (!Objects.equals(account.getId(), topic.getUid())) {
notificationService.addNotification(
topic.getUid(),
"您有新的帖子回复",
account.getUsername()+" 回复了你发表主题: "+topic.getTitle()+",快去看看吧!",
"success", "/index/topic-detail/"+topic.getId()
);
}
return null;
}

View File

@ -4,245 +4,348 @@ import router from "@/router";
import {useStore} from "@/store";
import {reactive, ref} from "vue";
import {
Back,
Bell,
ChatDotSquare, Collection, DataLine,
Document, Files,
Location, Lock, Message, Monitor,
Notification, Operation,
Position,
School, Search,
Umbrella, User
Back,
Bell,
ChatDotSquare, Check, Collection, DataLine,
Document, Files,
Location, Lock, Message, Monitor,
Notification, Operation,
Position,
School, Search,
Umbrella, User
} from "@element-plus/icons-vue";
import LightCard from "@/components/LightCard.vue";
const store = useStore()
const loading = ref(true)
const searchInput = reactive({
type: '1',
text: ''
type: '1',
text: ''
})
const notification = ref([])
get('/api/user/info', (data) => {
store.user = data
loading.value = false
store.user = data
loading.value = false
})
const loadNotification =
() => get('/api/notification/list', data => notification.value = data)
loadNotification()
function userLogout() {
logout(() => router.push("/"))
logout(() => router.push("/"))
}
function confirmNotification(id, url) {
get(`/api/notification/delete?id=${id}`, () => {
loadNotification()
window.open(url)
})
}
function deleteAllNotification() {
get(`/api/notification/delete-all`, loadNotification)
}
</script>
<template>
<div class="main-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
<el-container style="height: 100%" v-if="!loading">
<el-header class="main-content-header">
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
<div style="flex: 1;padding: 0 20px;text-align: center">
<el-input v-model="searchInput.text" style="width: 100%;max-width: 500px" placeholder="搜索论坛相关内容...">
<template #prefix>
<el-icon><Search/></el-icon>
</template>
<template #append>
<el-select style="width: 120px" v-model="searchInput.type">
<el-option value="1" label="帖子广场"/>
<el-option value="2" label="校园活动"/>
<el-option value="3" label="表白墙"/>
<el-option value="4" label="教务通知"/>
</el-select>
</template>
</el-input>
</div>
<div class="user-info">
<div class="profile">
<div>{{store.user.username}}</div>
<div>{{store.user.email}}</div>
</div>
<el-dropdown>
<el-avatar :src="store.avatarUrl"/>
<template #dropdown>
<el-dropdown-item>
<el-icon><Operation/></el-icon>
个人设置
</el-dropdown-item>
<el-dropdown-item>
<el-icon><Message/></el-icon>
消息列表
</el-dropdown-item>
<el-dropdown-item @click="userLogout" divided>
<el-icon><Back/></el-icon>
退出登录
</el-dropdown-item>
</template>
</el-dropdown>
</div>
</el-header>
<el-container>
<el-aside width="230px">
<el-scrollbar style="height: calc(100vh - 55px)">
<el-menu
router
:default-active="$route.path"
:default-openeds="['1', '2', '3']"
style="min-height: calc(100vh - 55px)">
<el-sub-menu index="1">
<template #title>
<el-icon><Location/></el-icon>
<span><b>校园论坛</b></span>
</template>
<el-menu-item index="/index">
<template #title>
<el-icon><ChatDotSquare/></el-icon>
帖子广场
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><Bell/></el-icon>
失物招领
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><Notification/></el-icon>
校园活动
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><Umbrella/></el-icon>
表白墙
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><School/></el-icon>
海文考研
<el-tag style="margin-left: 10px" size="small">合作机构</el-tag>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="2">
<template #title>
<el-icon><Position/></el-icon>
<span><b>探索与发现</b></span>
</template>
<el-menu-item>
<template #title>
<el-icon><Document/></el-icon>
成绩查询
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><Files/></el-icon>
班级课程表
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><Monitor/></el-icon>
教务通知
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><Collection/></el-icon>
在线图书馆
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon><DataLine/></el-icon>
预约教室
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="3">
<template #title>
<el-icon><Operation/></el-icon>
<span><b>个人设置</b></span>
</template>
<el-menu-item index="/index/user-setting">
<template #title>
<el-icon><User/></el-icon>
个人信息设置
</template>
</el-menu-item>
<el-menu-item index="/index/privacy-setting">
<template #title>
<el-icon><Lock/></el-icon>
账号安全设置
</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<el-main class="main-content-page">
<el-scrollbar style="height: calc(100vh - 55px)">
<router-view v-slot="{ Component }">
<transition name="el-fade-in-linear" mode="out-in">
<component :is="Component" style="height: 100%"/>
</transition>
</router-view>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</div>
<div class="main-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
<el-container style="height: 100%" v-if="!loading">
<el-header class="main-content-header">
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
<div style="flex: 1;padding: 0 20px;text-align: center">
<el-input v-model="searchInput.text" style="width: 100%;max-width: 500px"
placeholder="搜索论坛相关内容...">
<template #prefix>
<el-icon>
<Search/>
</el-icon>
</template>
<template #append>
<el-select style="width: 120px" v-model="searchInput.type">
<el-option value="1" label="帖子广场"/>
<el-option value="2" label="校园活动"/>
<el-option value="3" label="表白墙"/>
<el-option value="4" label="教务通知"/>
</el-select>
</template>
</el-input>
</div>
<div class="user-info">
<el-popover placement="bottom" :width="350" trigger="click">
<template #reference>
<el-badge style="margin-right: 15px" is-dot :hidden="!notification.length">
<div class="notification">
<el-icon><Bell/></el-icon>
<div style="font-size: 10px">消息</div>
</div>
</el-badge>
</template>
<el-empty :image-size="80" description="暂时没有未读消息哦~" v-if="!notification.length"/>
<el-scrollbar :max-height="500" v-else>
<light-card v-for="item in notification" class="notification-item"
@click="confirmNotification(item.id, item.url)">
<div>
<el-tag size="small" :type="item.type">消息</el-tag>&nbsp;
<span style="font-weight: bold">{{item.title}}</span>
</div>
<el-divider style="margin: 7px 0 3px 0"/>
<div style="font-size: 13px;color: grey">
{{item.content}}
</div>
</light-card>
</el-scrollbar>
<div style="margin-top: 10px">
<el-button size="small" type="info" :icon="Check" @click="deleteAllNotification"
style="width: 100%" plain>清除全部未读消息</el-button>
</div>
</el-popover>
<div class="profile">
<div>{{ store.user.username }}</div>
<div>{{ store.user.email }}</div>
</div>
<el-dropdown>
<el-avatar :src="store.avatarUrl"/>
<template #dropdown>
<el-dropdown-item>
<el-icon>
<Operation/>
</el-icon>
个人设置
</el-dropdown-item>
<el-dropdown-item>
<el-icon>
<Message/>
</el-icon>
消息列表
</el-dropdown-item>
<el-dropdown-item @click="userLogout" divided>
<el-icon>
<Back/>
</el-icon>
退出登录
</el-dropdown-item>
</template>
</el-dropdown>
</div>
</el-header>
<el-container>
<el-aside width="230px">
<el-scrollbar style="height: calc(100vh - 55px)">
<el-menu
router
:default-active="$route.path"
:default-openeds="['1', '2', '3']"
style="min-height: calc(100vh - 55px)">
<el-sub-menu index="1">
<template #title>
<el-icon>
<Location/>
</el-icon>
<span><b>校园论坛</b></span>
</template>
<el-menu-item index="/index">
<template #title>
<el-icon>
<ChatDotSquare/>
</el-icon>
帖子广场
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Bell/>
</el-icon>
失物招领
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Notification/>
</el-icon>
校园活动
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Umbrella/>
</el-icon>
表白墙
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<School/>
</el-icon>
海文考研
<el-tag style="margin-left: 10px" size="small">合作机构</el-tag>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="2">
<template #title>
<el-icon>
<Position/>
</el-icon>
<span><b>探索与发现</b></span>
</template>
<el-menu-item>
<template #title>
<el-icon>
<Document/>
</el-icon>
成绩查询
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Files/>
</el-icon>
班级课程表
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Monitor/>
</el-icon>
教务通知
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Collection/>
</el-icon>
在线图书馆
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<DataLine/>
</el-icon>
预约教室
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="3">
<template #title>
<el-icon>
<Operation/>
</el-icon>
<span><b>个人设置</b></span>
</template>
<el-menu-item index="/index/user-setting">
<template #title>
<el-icon>
<User/>
</el-icon>
个人信息设置
</template>
</el-menu-item>
<el-menu-item index="/index/privacy-setting">
<template #title>
<el-icon>
<Lock/>
</el-icon>
账号安全设置
</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<el-main class="main-content-page">
<el-scrollbar style="height: calc(100vh - 55px)">
<router-view v-slot="{ Component }">
<transition name="el-fade-in-linear" mode="out-in">
<component :is="Component" style="height: 100%"/>
</transition>
</router-view>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</div>
</template>
<style lang="less" scoped>
.notification-item {
transition: .3s;
&:hover {
cursor: pointer;
opacity: 0.7;
}
}
.notification {
font-size: 22px;
line-height: 14px;
text-align: center;
transition: color .3s;
&:hover {
color: grey;
cursor: pointer;
}
}
.main-content-page {
padding: 0;
background-color: #f7f8fa;
padding: 0;
background-color: #f7f8fa;
}
.dark .main-content-page {
background-color: #212225;
background-color: #212225;
}
.main-content {
height: 100vh;
width: 100vw;
height: 100vh;
width: 100vw;
}
.main-content-header {
border-bottom: solid 1px var(--el-border-color);
height: 55px;
display: flex;
align-items: center;
box-sizing: border-box;
.logo{
height: 32px;
}
.user-info {
border-bottom: solid 1px var(--el-border-color);
height: 55px;
display: flex;
justify-content: flex-end;
align-items: center;
box-sizing: border-box;
.el-avatar:hover {
cursor: pointer;
.logo {
height: 32px;
}
.profile {
text-align: right;
margin-right: 20px;
.user-info {
display: flex;
justify-content: flex-end;
align-items: center;
:first-child {
font-size: 18px;
font-weight: bold;
line-height: 20px;
}
.el-avatar:hover {
cursor: pointer;
}
:last-child {
font-size: 10px;
color: grey;
}
.profile {
text-align: right;
margin-right: 20px;
:first-child {
font-size: 18px;
font-weight: bold;
line-height: 20px;
}
:last-child {
font-size: 10px;
color: grey;
}
}
}
}
}
</style>