feat(消息通知): 消息通知

顶部消息通知完成
This commit is contained in:
猿小天
2023-03-07 22:13:41 +08:00
parent c63ad90821
commit c0c627b8df
10 changed files with 351 additions and 30 deletions

View File

@@ -8,21 +8,25 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
http_application = get_asgi_application()
from application.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http":http_application,
'websocket': AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns #指明路由文件是devops/routing.py
"http": http_application,
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns # 指明路由文件是devops/routing.py
)
)
),
})

View File

@@ -4,10 +4,10 @@ Django settings for application project.
Generated by 'django-admin startproject' using Django 3.2.3.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
import os
@@ -274,9 +274,6 @@ REST_FRAMEWORK = {
),
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated", # 只有经过身份认证确定用户身份才能访问
# 'rest_framework.permissions.IsAdminUser', # is_staff=True才能访问 —— 管理员(员工)权限
# 'rest_framework.permissions.AllowAny', # 允许所有
# 'rest_framework.permissions.IsAuthenticatedOrReadOnly', # 有身份 或者 只读访问(self.list,self.retrieve)
],
"EXCEPTION_HANDLER": "dvadmin.utils.exception.CustomExceptionHandler", # 自定义的异常处理
}

View File

@@ -9,7 +9,7 @@
</template>
<script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch,onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
@@ -33,7 +33,8 @@ const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from "/@/utils/websocket";
import {ElNotification} from "element-plus";
// 获取版本号
const getVersion = computed(() => {
let isVersion = false;
@@ -57,6 +58,8 @@ onBeforeMount(() => {
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
//websockt 模块
websocket.init(wsReceive)
});
// 页面加载时
onMounted(() => {
@@ -90,4 +93,28 @@ watch(
deep: true,
}
);
// websocket相关代码
import {messageCenterStore} from "/@/stores/messageCenter";
const wsReceive = (message: any) => {
const data = JSON.parse(message.data)
const { unread } = data
const messageCenter = messageCenterStore()
messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') {
ElNotification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 5000
})
}
}
onBeforeUnmount(() => {
// 关闭连接
websocket.close()
})
</script>

View File

@@ -39,7 +39,7 @@
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :is-dot="true">
<el-badge :value="messageCenter.unread">
<el-icon :title="$t('message.user.title4')">
<ele-Bell />
</el-icon>
@@ -205,6 +205,10 @@ onMounted(() => {
initI18nOrSize('globalI18n', 'disabledI18n');
}
});
//消息中心的未读数量
import {messageCenterStore} from "/@/stores/messageCenter";
const messageCenter = messageCenterStore()
</script>
<style scoped lang="scss">

View File

@@ -2,16 +2,16 @@
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">{{ $t('message.user.newTitle') }}</div>
<div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div>
<!-- <div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div>-->
</div>
<div class="content-box">
<template v-if="state.newsList.length > 0">
<div class="content-box-item" v-for="(v, k) in state.newsList" :key="k">
<div>{{ v.label }}</div>
<div>{{ v.title }}</div>
<div class="content-box-msg">
{{ v.value }}
<div v-html="v.content"></div>
</div>
<div class="content-box-time">{{ v.time }}</div>
<div class="content-box-time">{{ v.create_datetime }}</div>
</div>
</template>
<el-empty :description="$t('message.user.newDesc')" v-else></el-empty>
@@ -21,17 +21,11 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { reactive } from 'vue';
import { reactive,onBeforeMount,ref,onMounted } from 'vue';
// 定义变量内容
const state = reactive({
newsList: [
{
label: '关于版本发布的通知',
value: 'django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!',
time: '2023-02-14',
},
],
newsList: [] as any,
});
// 全部已读点击
@@ -39,9 +33,27 @@ const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
import {useRouter } from "vue-router";
const route = useRouter()
const onGoToGiteeClick = () => {
window.open('https://gitee.com/huge-dream/django-vue3-admin');
route.push('/messageCenter')
};
//获取最新消息
import { request } from "/@/utils/service";
const getLastMsg= ()=>{
request({
url: '/api/system/message_center/get_newest_msg/',
method: 'get',
params: {}
}).then((res:any) => {
const { data } = res
state.newsList= [data]
})
}
onMounted(()=>{
getLastMsg()
})
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,15 @@
import {defineStore} from "pinia";
/**
* 消息中心
*/
export const messageCenterStore = defineStore('messageCenter', {
state: () => ({
// 未读消息
unread: 0
}),
actions: {
async setUnread (number:any) {
this.unread = number
}
},
});

61
web/src/utils/baseUrl.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* @description 校验是否为租户模式。租户模式把域名替换成 域名 加端口
*/
export const getBaseURL = function () {
var baseURL = import.meta.env.VITE_API_URL as any
var param = baseURL.split('/')[3] || ''
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2.把 ip 地址替换成和前端一样域名
// 3.把 /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2]
if (host) {
var prot = baseURL.split(':')[2] || 80
if (prot === 80 || prot === 443) {
host = document.domain
} else {
host = document.domain + ':' + prot
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
}
if (!baseURL.endsWith('/')) {
baseURL += '/'
}
return baseURL
}
export const getWsBaseURL = function () {
let baseURL = import.meta.env.VITE_API_URL as any
let param = baseURL.split('/')[3] || ''
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2.把 ip 地址替换成和前端一样域名
// 3.把 /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2]
if (host) {
var prot = baseURL.split(':')[2] || 80
if (prot === 80 || prot === 443) {
host = document.domain
} else {
host = document.domain + ':' + prot
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
} else if (param !== '' || baseURL.startsWith('/')) {
baseURL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
if (!baseURL.endsWith('/')) {
baseURL += '/'
}
if (baseURL.startsWith('http')) { // https 也默认会被替换成 wss
baseURL = baseURL.replace('http', 'ws')
}
return baseURL
}

109
web/src/utils/websocket.ts Normal file
View File

@@ -0,0 +1,109 @@
import {ElNotification as message} from 'element-plus'
import store from '@/store'
import {Session} from "/@/utils/storage";
import {getWsBaseURL} from "/@/utils/baseUrl";
import socket from '@/types/api/socket'
const websocket: socket = {
websocket: null,
connectURL: getWsBaseURL(),
// 开启标识
socket_open: false,
// 心跳timer
hearbeat_timer: null,
// 心跳发送频率
hearbeat_interval: 2 * 1000,
// 是否自动重连
is_reonnect: true,
// 重连次数
reconnect_count: 3,
// 已发起重连次数
reconnect_current: 1,
// 重连timer
reconnect_timer: null,
// 重连频率
reconnect_interval: 5 * 1000,
init: (receiveMessage: Function | null) => {
if (!('WebSocket' in window)) {
message.warning('浏览器不支持WebSocket')
return null
}
const token = Session.get('token')
if(!token){
message.warning('websocket认证失败')
return null
}
const wsUrl = `${getWsBaseURL()}ws/${token}/`
websocket.websocket = new WebSocket(wsUrl)
websocket.websocket.onmessage = (e: any) => {
if (receiveMessage) {
receiveMessage(e)
}
}
websocket.websocket.onclose = (e: any) => {
websocket.socket_open = false
// 需要重新连接
if (websocket.is_reonnect) {
websocket.reconnect_timer = setTimeout(() => {
// 超过重连次数
if (websocket.reconnect_current > websocket.reconnect_count) {
clearTimeout(websocket.reconnect_timer)
websocket.is_reonnect = false
return
}
// 记录重连次数
websocket.reconnect_current++
websocket.reconnect()
}, websocket.reconnect_interval)
}
}
// 连接成功
websocket.websocket.onopen = function () {
websocket.socket_open = true
websocket.is_reonnect = true
// 开启心跳
websocket.heartbeat()
}
// 连接发生错误
websocket.websocket.onerror = function () { }
},
heartbeat: () => {
websocket.hearbeat_timer && clearInterval(websocket.hearbeat_timer)
websocket.hearbeat_timer = setInterval(() => {
let data = {
token: Session.get('token')
}
websocket.send(data)
}, websocket.hearbeat_interval)
},
send: (data, callback = null) => {
// 开启状态直接发送
if (websocket.websocket.readyState === websocket.websocket.OPEN) {
websocket.websocket.send(JSON.stringify(data))
callback && callback()
} else {
clearInterval(websocket.hearbeat_timer)
message({
type: 'warning',
message: 'socket链接已断开',
duration: 1000,
})
}
},
close: () => {
websocket.is_reonnect = false
websocket.websocket.close()
websocket.websocket = null;
},
/**
* 重新连接
*/
reconnect: () => {
if (websocket.websocket && !websocket.is_reonnect) {
websocket.close()
}
websocket.init(null)
},
}
export default websocket;

View File

@@ -0,0 +1,92 @@
import {Notification} from 'element-plus'
import store from '@/store'
import {Session} from "/@/utils/storage";
import {getWsBaseURL} from "/@/utils/baseUrl";
let socket=null;
function initWebSocket () {
const token = Session.get('token');
if (token) {
const wsUri = getWsBaseURL() + 'ws/' + token + '/'
this.socket = new WebSocket(wsUri)// 这里面的this都指向vue
this.socket.onerror = webSocketOnError
this.socket.onmessage = webSocketOnMessage
this.socket.onclose = closeWebsocket
}
}
function webSocketOnError (e) {
Notification({
title: '',
message: 'WebSocket连接发生错误' + JSON.stringify(e),
type: 'error',
position: 'bottom-right',
duration: 3000
})
}
/**
* 接收消息
* @param e
* @returns {any}
*/
function webSocketOnMessage (e) {
const data = JSON.parse(e.data)
const { unread } = data
store.dispatch('d2admin/messagecenter/setUnread', unread || 0)
if (data.contentType === 'SYSTEM') {
Notification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 3000
})
} else if (data.contentType === 'ERROR') {
Notification({
title: '',
message: data.content,
type: 'error',
position: 'bottom-right',
duration: 0
})
} else if (data.contentType === 'INFO') {
Notification({
title: '温馨提示',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 0
})
} else {
Notification({
title: '温馨提示',
message: data.content,
type: 'info',
position: 'bottom-right',
duration: 3000
})
}
}
// 关闭websiocket
function closeWebsocket () {
console.log('连接已关闭...')
Notification({
title: 'websocket',
message: '连接已关闭...',
type: 'danger',
position: 'bottom-right',
duration: 3000
})
}
/**
* 发送消息
* @param message
*/
function webSocketSend (message) {
this.socket.send(JSON.stringify(message))
}
export default {
initWebSocket, closeWebsocket, webSocketSend
}

View File

@@ -7,7 +7,7 @@ export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: {},
params: query,
})
}
export function GetObj(id: InfoReq) {