@@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from channels.auth import AuthMiddlewareStack
|
from channels.routing import ProtocolTypeRouter
|
||||||
from channels.security.websocket import AllowedHostsOriginValidator
|
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||||
@@ -18,15 +16,6 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
|||||||
|
|
||||||
http_application = get_asgi_application()
|
http_application = get_asgi_application()
|
||||||
|
|
||||||
from application.routing import websocket_urlpatterns
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter({
|
application = ProtocolTypeRouter({
|
||||||
"http": http_application,
|
"http": http_application,
|
||||||
'websocket': AllowedHostsOriginValidator(
|
|
||||||
AuthMiddlewareStack(
|
|
||||||
URLRouter(
|
|
||||||
websocket_urlpatterns # 指明路由文件是devops/routing.py
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from django.urls import path
|
|
||||||
from application.websocketConfig import MegCenter
|
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
|
||||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
|
|
||||||
]
|
|
||||||
33
backend/application/sse_views.py
Normal file
33
backend/application/sse_views.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# views.py
|
||||||
|
import time
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
|
||||||
|
from application import settings
|
||||||
|
from dvadmin.system.models import MessageCenterTargetUser
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
def event_stream(user_id):
|
||||||
|
last_sent_time = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 从 Redis 中获取最后数据库变更时间
|
||||||
|
last_db_change_time = cache.get('last_db_change_time', 0)
|
||||||
|
# 只有当数据库发生变化时才检查总数
|
||||||
|
if last_db_change_time and last_db_change_time > last_sent_time:
|
||||||
|
count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count()
|
||||||
|
yield f"data: {count}\n\n"
|
||||||
|
last_sent_time = time.time()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def sse_view(request):
|
||||||
|
token = request.GET.get('token')
|
||||||
|
decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||||
|
user_id = decoded.get('user_id')
|
||||||
|
response = StreamingHttpResponse(event_stream(user_id), content_type='text/event-stream')
|
||||||
|
response['Cache-Control'] = 'no-cache'
|
||||||
|
return response
|
||||||
@@ -24,6 +24,7 @@ from rest_framework_simplejwt.views import (
|
|||||||
|
|
||||||
from application import dispatch
|
from application import dispatch
|
||||||
from application import settings
|
from application import settings
|
||||||
|
from application.sse_views import sse_view
|
||||||
from dvadmin.system.views.dictionary import InitDictionaryViewSet
|
from dvadmin.system.views.dictionary import InitDictionaryViewSet
|
||||||
from dvadmin.system.views.login import (
|
from dvadmin.system.views.login import (
|
||||||
LoginView,
|
LoginView,
|
||||||
@@ -40,6 +41,7 @@ dispatch.init_system_config()
|
|||||||
dispatch.init_dictionary()
|
dispatch.init_dictionary()
|
||||||
# =========== 初始化系统配置 =================
|
# =========== 初始化系统配置 =================
|
||||||
|
|
||||||
|
permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ]
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
openapi.Info(
|
openapi.Info(
|
||||||
title="Snippets API",
|
title="Snippets API",
|
||||||
@@ -50,7 +52,7 @@ schema_view = get_schema_view(
|
|||||||
license=openapi.License(name="BSD License"),
|
license=openapi.License(name="BSD License"),
|
||||||
),
|
),
|
||||||
public=True,
|
public=True,
|
||||||
permission_classes=(permissions.IsAuthenticated,),
|
permission_classes=permission_classes,
|
||||||
generator_class=CustomOpenAPISchemaGenerator,
|
generator_class=CustomOpenAPISchemaGenerator,
|
||||||
)
|
)
|
||||||
# 前端页面映射
|
# 前端页面映射
|
||||||
@@ -115,6 +117,8 @@ urlpatterns = (
|
|||||||
# 前端页面映射
|
# 前端页面映射
|
||||||
path('web/', web_view, name='web_view'),
|
path('web/', web_view, name='web_view'),
|
||||||
path('web/<path:filename>', serve_web_files, name='serve_web_files'),
|
path('web/<path:filename>', serve_web_files, name='serve_web_files'),
|
||||||
|
# sse
|
||||||
|
path('sse/', sse_view, name='sse'),
|
||||||
]
|
]
|
||||||
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL)
|
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL)
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async, async_to_sync
|
|
||||||
from channels.db import database_sync_to_async
|
|
||||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer
|
|
||||||
import json
|
|
||||||
|
|
||||||
from channels.layers import get_channel_layer
|
|
||||||
from jwt import InvalidSignatureError
|
|
||||||
from rest_framework.request import Request
|
|
||||||
|
|
||||||
from application import settings
|
|
||||||
from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser
|
|
||||||
from dvadmin.system.views.message_center import MessageCenterTargetUserSerializer
|
|
||||||
from dvadmin.utils.serializers import CustomModelSerializer
|
|
||||||
|
|
||||||
send_dict = {}
|
|
||||||
|
|
||||||
|
|
||||||
# 发送消息结构体
|
|
||||||
def set_message(sender, msg_type, msg, unread=0):
|
|
||||||
text = {
|
|
||||||
'sender': sender,
|
|
||||||
'contentType': msg_type,
|
|
||||||
'content': msg,
|
|
||||||
'unread': unread
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
# 异步获取消息中心的目标用户
|
|
||||||
@database_sync_to_async
|
|
||||||
def _get_message_center_instance(message_id):
|
|
||||||
from dvadmin.system.models import MessageCenter
|
|
||||||
_MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user', flat=True)
|
|
||||||
if _MessageCenter:
|
|
||||||
return _MessageCenter
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def _get_message_unread(user_id):
|
|
||||||
"""获取用户的未读消息数量"""
|
|
||||||
from dvadmin.system.models import MessageCenterTargetUser
|
|
||||||
count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count()
|
|
||||||
return count or 0
|
|
||||||
|
|
||||||
|
|
||||||
def request_data(scope):
|
|
||||||
query_string = scope.get('query_string', b'').decode('utf-8')
|
|
||||||
qs = urllib.parse.parse_qs(query_string)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
class DvadminWebSocket(AsyncJsonWebsocketConsumer):
|
|
||||||
async def connect(self):
|
|
||||||
try:
|
|
||||||
import jwt
|
|
||||||
self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"]
|
|
||||||
decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"])
|
|
||||||
if decoded_result:
|
|
||||||
self.user_id = decoded_result.get('user_id')
|
|
||||||
self.chat_group_name = "user_" + str(self.user_id)
|
|
||||||
# 收到连接时候处理,
|
|
||||||
await self.channel_layer.group_add(
|
|
||||||
self.chat_group_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
await self.accept()
|
|
||||||
# 主动推送消息
|
|
||||||
unread_count = await _get_message_unread(self.user_id)
|
|
||||||
if unread_count == 0:
|
|
||||||
# 发送连接成功
|
|
||||||
await self.send_json(set_message('system', 'SYSTEM', '您已上线'))
|
|
||||||
else:
|
|
||||||
await self.send_json(
|
|
||||||
set_message('system', 'SYSTEM', "请查看您的未读消息~",
|
|
||||||
unread=unread_count))
|
|
||||||
except InvalidSignatureError:
|
|
||||||
await self.disconnect(None)
|
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
|
||||||
# Leave room group
|
|
||||||
await self.channel_layer.group_discard(self.chat_group_name, self.channel_name)
|
|
||||||
print("连接关闭")
|
|
||||||
try:
|
|
||||||
await self.close(close_code)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MegCenter(DvadminWebSocket):
|
|
||||||
"""
|
|
||||||
消息中心
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def receive(self, text_data):
|
|
||||||
# 接受客户端的信息,你处理的函数
|
|
||||||
text_data_json = json.loads(text_data)
|
|
||||||
message_id = text_data_json.get('message_id', None)
|
|
||||||
user_list = await _get_message_center_instance(message_id)
|
|
||||||
for send_user in user_list:
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"user_" + str(send_user),
|
|
||||||
{'type': 'push.message', 'json': text_data_json}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def push_message(self, event):
|
|
||||||
"""消息发送"""
|
|
||||||
message = event['json']
|
|
||||||
await self.send(text_data=json.dumps(message))
|
|
||||||
|
|
||||||
|
|
||||||
class MessageCreateSerializer(CustomModelSerializer):
|
|
||||||
"""
|
|
||||||
消息中心-新增-序列化器
|
|
||||||
"""
|
|
||||||
class Meta:
|
|
||||||
model = MessageCenter
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = ["id"]
|
|
||||||
|
|
||||||
|
|
||||||
def websocket_push(user_id, message):
|
|
||||||
username = "user_" + str(user_id)
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
async_to_sync(channel_layer.group_send)(
|
|
||||||
username,
|
|
||||||
{
|
|
||||||
"type": "push.message",
|
|
||||||
"json": message
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None,
|
|
||||||
target_role=None, message: dict = None, request=Request):
|
|
||||||
if message is None:
|
|
||||||
message = {"contentType": "INFO", "content": None}
|
|
||||||
if target_role is None:
|
|
||||||
target_role = []
|
|
||||||
if target_dept is None:
|
|
||||||
target_dept = []
|
|
||||||
data = {
|
|
||||||
"title": title,
|
|
||||||
"content": content,
|
|
||||||
"target_type": target_type,
|
|
||||||
"target_user": target_user,
|
|
||||||
"target_dept": target_dept,
|
|
||||||
"target_role": target_role
|
|
||||||
}
|
|
||||||
message_center_instance = MessageCreateSerializer(data=data, request=request)
|
|
||||||
message_center_instance.is_valid(raise_exception=True)
|
|
||||||
message_center_instance.save()
|
|
||||||
users = target_user or []
|
|
||||||
if target_type in [1]: # 按角色
|
|
||||||
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
|
|
||||||
if target_type in [2]: # 按部门
|
|
||||||
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
|
|
||||||
if target_type in [3]: # 系统通知
|
|
||||||
users = Users.objects.values_list('id', flat=True)
|
|
||||||
targetuser_data = []
|
|
||||||
for user in users:
|
|
||||||
targetuser_data.append({
|
|
||||||
"messagecenter": message_center_instance.instance.id,
|
|
||||||
"users": user
|
|
||||||
})
|
|
||||||
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=request)
|
|
||||||
targetuser_instance.is_valid(raise_exception=True)
|
|
||||||
targetuser_instance.save()
|
|
||||||
for user in users:
|
|
||||||
username = "user_" + str(user)
|
|
||||||
unread_count = async_to_sync(_get_message_unread)(user)
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
async_to_sync(channel_layer.group_send)(
|
|
||||||
username,
|
|
||||||
{
|
|
||||||
"type": "push.message",
|
|
||||||
"json": {**message, 'unread': unread_count}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -4,3 +4,7 @@ from django.apps import AppConfig
|
|||||||
class SystemConfig(AppConfig):
|
class SystemConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'dvadmin.system'
|
name = 'dvadmin.system'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
# 注册信号
|
||||||
|
import dvadmin.system.signals # 确保路径正确
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
from django.dispatch import Signal
|
import time
|
||||||
|
|
||||||
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
from django.dispatch import Signal, receiver
|
||||||
|
from django.core.cache import cache
|
||||||
|
from dvadmin.system.models import MessageCenterTargetUser
|
||||||
|
|
||||||
# 初始化信号
|
# 初始化信号
|
||||||
pre_init_complete = Signal()
|
pre_init_complete = Signal()
|
||||||
detail_init_complete = Signal()
|
detail_init_complete = Signal()
|
||||||
@@ -10,3 +16,12 @@ post_tenants_init_complete = Signal()
|
|||||||
post_tenants_all_init_complete = Signal()
|
post_tenants_all_init_complete = Signal()
|
||||||
# 租户创建完成信号
|
# 租户创建完成信号
|
||||||
tenants_create_complete = Signal()
|
tenants_create_complete = Signal()
|
||||||
|
|
||||||
|
# 全局变量用于标记最后修改时间
|
||||||
|
last_db_change_time = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=MessageCenterTargetUser)
|
||||||
|
@receiver(post_delete, sender=MessageCenterTargetUser)
|
||||||
|
def update_last_change_time(sender, **kwargs):
|
||||||
|
cache.set('last_db_change_time', time.time(), timeout=None) # 设置永不超时的键值对
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ class DownloadCenterViewSet(CustomModelViewSet):
|
|||||||
extra_filter_class = []
|
extra_filter_class = []
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
# 判断是否是 Swagger 文档生成阶段,防止报错
|
||||||
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
|
return self.queryset.model.objects.none()
|
||||||
|
|
||||||
|
# 正常请求下的逻辑
|
||||||
if self.request.user.is_superuser:
|
if self.request.user.is_superuser:
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
return super().get_queryset().filter(creator=self.request.user)
|
return super().get_queryset().filter(creator=self.request.user)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class MessageCenterSerializer(CustomModelSerializer):
|
|||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
def get_user_info(self, instance, parsed_query):
|
def get_user_info(self, instance, parsed_query):
|
||||||
if instance.target_type in (1,2,3):
|
if instance.target_type in (1, 2, 3):
|
||||||
return []
|
return []
|
||||||
users = instance.target_user.all()
|
users = instance.target_user.all()
|
||||||
# You can do what ever you want in here
|
# You can do what ever you want in here
|
||||||
@@ -108,7 +108,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
|||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
def get_user_info(self, instance, parsed_query):
|
def get_user_info(self, instance, parsed_query):
|
||||||
if instance.target_type in (1,2,3):
|
if instance.target_type in (1, 2, 3):
|
||||||
return []
|
return []
|
||||||
users = instance.target_user.all()
|
users = instance.target_user.all()
|
||||||
# You can do what ever you want in here
|
# You can do what ever you want in here
|
||||||
@@ -139,21 +139,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
|||||||
read_only_fields = ["id"]
|
read_only_fields = ["id"]
|
||||||
|
|
||||||
|
|
||||||
def websocket_push(user_id, message):
|
|
||||||
"""
|
|
||||||
主动推送消息
|
|
||||||
"""
|
|
||||||
username = "user_" + str(user_id)
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
async_to_sync(channel_layer.group_send)(
|
|
||||||
username,
|
|
||||||
{
|
|
||||||
"type": "push.message",
|
|
||||||
"json": message
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MessageCenterCreateSerializer(CustomModelSerializer):
|
class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||||
"""
|
"""
|
||||||
消息中心-新增-序列化器
|
消息中心-新增-序列化器
|
||||||
@@ -182,10 +167,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
|
|||||||
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
|
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
|
||||||
targetuser_instance.is_valid(raise_exception=True)
|
targetuser_instance.is_valid(raise_exception=True)
|
||||||
targetuser_instance.save()
|
targetuser_instance.save()
|
||||||
for user in users:
|
|
||||||
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
|
|
||||||
websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM',
|
|
||||||
"content": '您有一条新消息~', "unread": unread_count})
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -225,10 +206,6 @@ class MessageCenterViewSet(CustomModelViewSet):
|
|||||||
queryset.save()
|
queryset.save()
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(instance)
|
serializer = self.get_serializer(instance)
|
||||||
# 主动推送消息
|
|
||||||
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
|
|
||||||
websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT',
|
|
||||||
"content": '您查看了一条消息~', "unread": unread_count})
|
|
||||||
return DetailResponse(data=serializer.data, msg="获取成功")
|
return DetailResponse(data=serializer.data, msg="获取成功")
|
||||||
|
|
||||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||||
|
|||||||
@@ -7,25 +7,24 @@ djangorestframework==3.15.2
|
|||||||
django-restql==0.15.4
|
django-restql==0.15.4
|
||||||
django-simple-captcha==0.6.0
|
django-simple-captcha==0.6.0
|
||||||
django-timezone-field==7.0
|
django-timezone-field==7.0
|
||||||
djangorestframework-simplejwt==5.3.1
|
djangorestframework_simplejwt==5.4.0
|
||||||
drf-yasg==1.21.7
|
drf-yasg==1.21.7
|
||||||
mysqlclient==2.2.0
|
mysqlclient==2.2.0
|
||||||
pypinyin==0.51.0
|
pypinyin==0.51.0
|
||||||
ua-parser==0.18.0
|
ua-parser==0.18.0
|
||||||
pyparsing==3.1.2
|
pyparsing==3.1.2
|
||||||
openpyxl==3.1.5
|
openpyxl==3.1.5
|
||||||
requests==2.32.3
|
requests==2.32.4
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.12.2
|
||||||
tzlocal==5.2
|
tzlocal==5.2
|
||||||
channels==4.1.0
|
channels==4.1.0
|
||||||
channels-redis==4.2.0
|
channels-redis==4.2.0
|
||||||
websockets==11.0.3
|
|
||||||
user-agents==2.2.0
|
user-agents==2.2.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
whitenoise==6.7.0
|
whitenoise==6.7.0
|
||||||
psycopg2==2.9.9
|
psycopg2==2.9.9
|
||||||
uvicorn==0.30.3
|
uvicorn==0.30.3
|
||||||
gunicorn==22.0.0
|
gunicorn==23.0.0
|
||||||
gevent==24.2.1
|
gevent==24.2.1
|
||||||
Pillow==10.4.0
|
Pillow==10.4.0
|
||||||
pyinstaller==6.9.0
|
pyinstaller==6.9.0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
ENV = 'development'
|
ENV = 'development'
|
||||||
|
|
||||||
# 本地环境接口地址
|
# 本地环境接口地址
|
||||||
VITE_API_URL = 'http://127.0.0.1:8001'
|
VITE_API_URL = 'http://127.0.0.1:8000'
|
||||||
|
|
||||||
# 是否启用按钮权限
|
# 是否启用按钮权限
|
||||||
VITE_PM_ENABLED = true
|
VITE_PM_ENABLED = true
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const route = useRoute();
|
|||||||
const stores = useTagsViewRoutes();
|
const stores = useTagsViewRoutes();
|
||||||
const storesThemeConfig = useThemeConfig();
|
const storesThemeConfig = useThemeConfig();
|
||||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||||
import websocket from '/@/utils/websocket';
|
|
||||||
const core = useCore();
|
const core = useCore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 获取版本号
|
// 获取版本号
|
||||||
@@ -92,63 +91,5 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
mittBus.off('openSetingsDrawer', () => {});
|
mittBus.off('openSetingsDrawer', () => {});
|
||||||
});
|
});
|
||||||
// 监听路由的变化,设置网站标题
|
|
||||||
watch(
|
|
||||||
() => route.path,
|
|
||||||
() => {
|
|
||||||
other.useTitle();
|
|
||||||
other.useFavicon();
|
|
||||||
if (!websocket.websocket) {
|
|
||||||
//websockt 模块
|
|
||||||
try {
|
|
||||||
websocket.init(wsReceive)
|
|
||||||
} catch (e) {
|
|
||||||
console.log('websocket错误');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
} else if (data.contentType === 'Content') {
|
|
||||||
ElMessageBox.confirm(data.content, data.notificationTitle, {
|
|
||||||
confirmButtonText: data.notificationButton,
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
cancelButtonText: '关闭',
|
|
||||||
type: 'info',
|
|
||||||
closeOnClickModal: false,
|
|
||||||
}).then(() => {
|
|
||||||
ElMessageBox.close();
|
|
||||||
const path = data.path;
|
|
||||||
if (route.path === path) {
|
|
||||||
core.bus.emit('onNewTask', { name: 'onNewTask' });
|
|
||||||
} else {
|
|
||||||
router.push({ path});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
// 关闭连接
|
|
||||||
websocket.close();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
class="tableSelector"
|
class="tableSelector"
|
||||||
multiple
|
multiple
|
||||||
:collapseTags="props.tableConfig.collapseTags"
|
:collapseTags="props.tableConfig.collapseTags"
|
||||||
@remove-tag="removeTag"
|
|
||||||
v-model="data"
|
v-model="data"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
@visible-change="visibleChange"
|
@visible-change="visibleChange"
|
||||||
@@ -59,34 +58,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, defineProps, onMounted, reactive, ref, watch} from 'vue';
|
import { computed, defineProps, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import XEUtils from 'xe-utils';
|
import XEUtils from 'xe-utils';
|
||||||
import { request } from '/@/utils/service';
|
import { request } from '/@/utils/service';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array || String || Number,
|
type: Array || String || Number,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
tableConfig: {
|
tableConfig: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default:{
|
default: {
|
||||||
url: null,
|
url: null,
|
||||||
label: null, //显示值
|
label: null, //显示值
|
||||||
value: null, //数据值
|
value: null, //数据值
|
||||||
isTree: false,
|
isTree: false,
|
||||||
lazy: true,
|
lazy: true,
|
||||||
size:'default',
|
size: 'default',
|
||||||
load: () => {},
|
load: () => {},
|
||||||
data: [], //默认数据
|
data: [], //默认数据
|
||||||
isMultiple: false, //是否多选
|
isMultiple: false, //是否多选
|
||||||
collapseTags:false,
|
collapseTags: false,
|
||||||
treeProps: { children: 'children', hasChildren: 'hasChildren' },
|
treeProps: { children: 'children', hasChildren: 'hasChildren' },
|
||||||
columns: [], //每一项对应的列表项
|
columns: [], //每一项对应的列表项
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
displayLabel: {},
|
displayLabel: {},
|
||||||
} as any);
|
} as any);
|
||||||
|
console.log(props.tableConfig);
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
// tableRef
|
// tableRef
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
@@ -137,6 +138,8 @@ const handleCurrentChange = (val: any) => {
|
|||||||
*/
|
*/
|
||||||
const getDict = async () => {
|
const getDict = async () => {
|
||||||
const url = props.tableConfig.url;
|
const url = props.tableConfig.url;
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
page: pageConfig.page,
|
page: pageConfig.page,
|
||||||
limit: pageConfig.limit,
|
limit: pageConfig.limit,
|
||||||
@@ -162,29 +165,25 @@ const getDict = async () => {
|
|||||||
|
|
||||||
// 获取节点值
|
// 获取节点值
|
||||||
const getNodeValues = () => {
|
const getNodeValues = () => {
|
||||||
|
console.log(props.tableConfig.url);
|
||||||
|
|
||||||
request({
|
request({
|
||||||
url:props.tableConfig.valueUrl,
|
url: props.tableConfig.url,
|
||||||
method:'post',
|
method: 'post',
|
||||||
data:{ids:props.modelValue}
|
data: { ids: props.modelValue },
|
||||||
}).then(res=>{
|
}).then((res) => {
|
||||||
if(res.data.length>0){
|
if (res.data.length > 0) {
|
||||||
data.value = res.data.map((item:any)=>{
|
data.value = res.data.map((item: any) => {
|
||||||
return item[props.tableConfig.label]
|
return item[props.tableConfig.label];
|
||||||
})
|
});
|
||||||
|
|
||||||
tableRef.value!.clearSelection()
|
tableRef.value!.clearSelection();
|
||||||
res.data.forEach((row) => {
|
res.data.forEach((row) => {
|
||||||
tableRef.value!.toggleRowSelection(
|
tableRef.value!.toggleRowSelection(row, true, false);
|
||||||
row,
|
});
|
||||||
true,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下拉框展开/关闭
|
* 下拉框展开/关闭
|
||||||
@@ -205,12 +204,11 @@ const handlePageChange = (page: any) => {
|
|||||||
getDict();
|
getDict();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(() => {
|
||||||
setTimeout(()=>{
|
// setTimeout(() => {
|
||||||
getNodeValues()
|
// getNodeValues();
|
||||||
},1000)
|
// }, 1000);
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -57,30 +57,11 @@
|
|||||||
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
|
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div></div>
|
||||||
<span v-if="!isSocketOpen" class="online-status-span">
|
|
||||||
<el-popconfirm
|
|
||||||
width="250"
|
|
||||||
ref="onlinePopoverRef"
|
|
||||||
:confirm-button-text="$t('message.user.retry')"
|
|
||||||
:icon="InfoFilled"
|
|
||||||
trigger="hover"
|
|
||||||
icon-color="#626AEF"
|
|
||||||
:title="$t('message.user.onlinePrompt')"
|
|
||||||
@confirm="onlineConfirmEvent"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
|
|
||||||
<img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
|
||||||
</el-badge>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
||||||
<span class="layout-navbars-breadcrumb-user-link">
|
<span class="layout-navbars-breadcrumb-user-link">
|
||||||
<span v-if="isSocketOpen">
|
<span v-if="isSocketOpen">
|
||||||
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
|
<el-badge is-dot class="item" :class="{ 'online-status': isSocketOpen, 'online-down': !isSocketOpen }">
|
||||||
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||||
</el-badge>
|
</el-badge>
|
||||||
</span>
|
</span>
|
||||||
@@ -115,8 +96,7 @@ import other from '/@/utils/other';
|
|||||||
import mittBus from '/@/utils/mitt';
|
import mittBus from '/@/utils/mitt';
|
||||||
import { Session, Local } from '/@/utils/storage';
|
import { Session, Local } from '/@/utils/storage';
|
||||||
import headerImage from '/@/assets/img/headerImage.png';
|
import headerImage from '/@/assets/img/headerImage.png';
|
||||||
import websocket from '/@/utils/websocket';
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
|
||||||
// 引入组件
|
// 引入组件
|
||||||
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
|
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
|
||||||
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
|
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
|
||||||
@@ -148,17 +128,6 @@ const layoutUserFlexNum = computed(() => {
|
|||||||
// 定义变量内容
|
// 定义变量内容
|
||||||
const { isSocketOpen } = storeToRefs(useUserInfo());
|
const { isSocketOpen } = storeToRefs(useUserInfo());
|
||||||
|
|
||||||
// websocket状态
|
|
||||||
const onlinePopoverRef = ref()
|
|
||||||
const onlineConfirmEvent = () => {
|
|
||||||
if (!isSocketOpen.value) {
|
|
||||||
websocket.is_reonnect = true
|
|
||||||
websocket.reconnect_current = 1
|
|
||||||
websocket.reconnect()
|
|
||||||
}
|
|
||||||
// 手动隐藏弹出
|
|
||||||
unref(onlinePopoverRef).popperRef?.delayHide?.()
|
|
||||||
}
|
|
||||||
// 全屏点击时
|
// 全屏点击时
|
||||||
const onScreenfullClick = () => {
|
const onScreenfullClick = () => {
|
||||||
if (!screenfull.isEnabled) {
|
if (!screenfull.isEnabled) {
|
||||||
@@ -237,8 +206,10 @@ const onLanguageChange = (lang: string) => {
|
|||||||
initI18nOrSize('globalI18n', 'disabledI18n');
|
initI18nOrSize('globalI18n', 'disabledI18n');
|
||||||
};
|
};
|
||||||
// 初始化组件大小/i18n
|
// 初始化组件大小/i18n
|
||||||
const initI18nOrSize = (value: string, attr: string) => {
|
const initI18nOrSize = (value: string, attr: keyof typeof state) => {
|
||||||
state[attr] = Local.get('themeConfig')[value];
|
const themeConfig = Local.get('themeConfig') as { [key: string]: any } | null;
|
||||||
|
const configValue = ((themeConfig && themeConfig[value]) as string) || '';
|
||||||
|
state[attr] = configValue as unknown as never;
|
||||||
};
|
};
|
||||||
// 页面加载时
|
// 页面加载时
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -246,12 +217,32 @@ onMounted(() => {
|
|||||||
initI18nOrSize('globalComponentSize', 'disabledSize');
|
initI18nOrSize('globalComponentSize', 'disabledSize');
|
||||||
initI18nOrSize('globalI18n', 'disabledI18n');
|
initI18nOrSize('globalI18n', 'disabledI18n');
|
||||||
}
|
}
|
||||||
|
getMessageCenterCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
//消息中心的未读数量
|
//消息中心的未读数量
|
||||||
import { messageCenterStore } from '/@/stores/messageCenter';
|
import { messageCenterStore } from '/@/stores/messageCenter';
|
||||||
import {getBaseURL} from "/@/utils/baseUrl";
|
import { getBaseURL } from '/@/utils/baseUrl';
|
||||||
const messageCenter = messageCenterStore();
|
const messageCenter = messageCenterStore();
|
||||||
|
let eventSource: EventSource | null = null; // 存储 EventSource 实例
|
||||||
|
const token = Session.get('token');
|
||||||
|
const getMessageCenterCount = () => {
|
||||||
|
// 创建 EventSource 实例并连接到后端 SSE 端点
|
||||||
|
eventSource = new EventSource(`${getBaseURL()}/sse/?token=${token}`); // 替换为你的后端地址
|
||||||
|
|
||||||
|
// 监听消息事件
|
||||||
|
eventSource.onmessage = function (event) {
|
||||||
|
messageCenter.setUnread(+event.data); // 更新总记录数
|
||||||
|
};
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
eventSource.onerror = function (err) {
|
||||||
|
console.error('SSE 错误:', err);
|
||||||
|
if (eventSource !== null && eventSource.readyState === EventSource.CLOSED) {
|
||||||
|
console.log('连接已关闭');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -298,7 +289,7 @@ const messageCenter = messageCenterStore();
|
|||||||
:deep(.el-badge__content.is-fixed) {
|
:deep(.el-badge__content.is-fixed) {
|
||||||
top: 12px;
|
top: 12px;
|
||||||
}
|
}
|
||||||
.online-status{
|
.online-status {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
:deep(.el-badge__content.is-fixed) {
|
:deep(.el-badge__content.is-fixed) {
|
||||||
top: 30px;
|
top: 30px;
|
||||||
@@ -310,7 +301,7 @@ const messageCenter = messageCenterStore();
|
|||||||
background-color: #18bc9c;
|
background-color: #18bc9c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.online-down{
|
.online-down {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
:deep(.el-badge__content.is-fixed) {
|
:deep(.el-badge__content.is-fixed) {
|
||||||
top: 30px;
|
top: 30px;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="layout-navbars-breadcrumb-user-news">
|
<div class="layout-navbars-breadcrumb-user-news">
|
||||||
<div class="head-box">
|
<div class="head-box">
|
||||||
<div class="head-box-title">{{ $t('message.user.newTitle') }}</div>
|
<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>
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<template v-if="state.newsList.length > 0">
|
<template v-if="state.newsList.length > 0">
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
||||||
import { reactive,onBeforeMount,ref,onMounted } from 'vue';
|
import { reactive, onBeforeMount, ref, onMounted } from 'vue';
|
||||||
|
|
||||||
// 定义变量内容
|
// 定义变量内容
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -33,27 +34,28 @@ const onAllReadClick = () => {
|
|||||||
state.newsList = [];
|
state.newsList = [];
|
||||||
};
|
};
|
||||||
// 前往通知中心点击
|
// 前往通知中心点击
|
||||||
import {useRouter } from "vue-router";
|
import { useRouter } from 'vue-router';
|
||||||
const route = useRouter()
|
const route = useRouter();
|
||||||
const onGoToGiteeClick = () => {
|
const onGoToGiteeClick = () => {
|
||||||
route.push('/messageCenter')
|
route.push('/messageCenter');
|
||||||
};
|
};
|
||||||
//获取最新消息
|
//获取最新消息
|
||||||
import { request } from "/@/utils/service";
|
import { request } from '/@/utils/service';
|
||||||
const getLastMsg= ()=>{
|
const getLastMsg = () => {
|
||||||
request({
|
request({
|
||||||
url: '/api/system/message_center/get_newest_msg/',
|
url: '/api/system/message_center/get_newest_msg/',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: {}
|
params: {},
|
||||||
}).then((res:any) => {
|
}).then((res: any) => {
|
||||||
const { data } = res
|
const { data } = res;
|
||||||
state.newsList= [data]
|
console.log(data);
|
||||||
})
|
|
||||||
}
|
|
||||||
onMounted(()=>{
|
|
||||||
getLastMsg()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
state.newsList = [data];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
getLastMsg();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -72,9 +72,6 @@ export const useUserInfo = defineStore('userInfo', {
|
|||||||
Session.set('userInfo', this.userInfos);
|
Session.set('userInfo', this.userInfos);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setWebSocketState(socketState: boolean) {
|
|
||||||
this.isSocketOpen = socketState;
|
|
||||||
},
|
|
||||||
async getApiUserInfo() {
|
async getApiUserInfo() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/system/user/user_info/',
|
url: '/api/system/user/user_info/',
|
||||||
|
|||||||
@@ -131,10 +131,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
data: [
|
data: [
|
||||||
{ value: 0, label: '按用户' },
|
{ value: 0, label: '按用户' },
|
||||||
{ value: 1, label: '按角色' },
|
{ value: 1, label: '按角色' },
|
||||||
{
|
{ value: 2, label: '按部门' },
|
||||||
value: 2,
|
|
||||||
label: '按部门',
|
|
||||||
},
|
|
||||||
{ value: 3, label: '通知公告' },
|
{ value: 3, label: '通知公告' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -142,14 +139,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
component: {
|
component: {
|
||||||
optionName: 'el-radio-button',
|
optionName: 'el-radio-button',
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [{ required: true, message: '必选项', trigger: ['blur', 'change'] }],
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '必选项',
|
|
||||||
// @ts-ignore
|
|
||||||
trigger: ['blur', 'change'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
target_user: {
|
target_user: {
|
||||||
@@ -191,10 +181,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
}),
|
}),
|
||||||
rules: [
|
rules: [
|
||||||
// 表单校验规则
|
// 表单校验规则
|
||||||
{
|
{ required: true, message: '必填项' },
|
||||||
required: true,
|
|
||||||
message: '必填项',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
highlight-current
|
highlight-current
|
||||||
show-checkbox
|
show-checkbox
|
||||||
default-expand-all
|
default-expand-all
|
||||||
|
:check-on-click-leaf="false"
|
||||||
>
|
>
|
||||||
</el-tree>
|
</el-tree>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user