@@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.routing import ProtocolTypeRouter
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
@@ -18,15 +16,6 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
|
||||
http_application = get_asgi_application()
|
||||
|
||||
from application.routing import websocket_urlpatterns
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"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 是该路由的消费者
|
||||
]
|
||||
@@ -399,8 +399,12 @@ DICTIONARY_CONFIG = {}
|
||||
# ================================================= #
|
||||
# 租户共享app
|
||||
TENANT_SHARED_APPS = []
|
||||
# 普通租户独有app
|
||||
TENANT_EXCLUSIVE_APPS = []
|
||||
# 插件 urlpatterns
|
||||
PLUGINS_URL_PATTERNS = []
|
||||
# 所有模式有的
|
||||
SHARED_APPS = []
|
||||
# ********** 一键导入插件配置开始 **********
|
||||
# 例如:
|
||||
# from dvadmin_upgrade_center.settings import * # 升级中心
|
||||
|
||||
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 settings
|
||||
from application.sse_views import sse_view
|
||||
from dvadmin.system.views.dictionary import InitDictionaryViewSet
|
||||
from dvadmin.system.views.login import (
|
||||
LoginView,
|
||||
@@ -40,6 +41,7 @@ dispatch.init_system_config()
|
||||
dispatch.init_dictionary()
|
||||
# =========== 初始化系统配置 =================
|
||||
|
||||
permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ]
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="Snippets API",
|
||||
@@ -50,7 +52,7 @@ schema_view = get_schema_view(
|
||||
license=openapi.License(name="BSD License"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=(permissions.IsAuthenticated,),
|
||||
permission_classes=permission_classes,
|
||||
generator_class=CustomOpenAPISchemaGenerator,
|
||||
)
|
||||
# 前端页面映射
|
||||
@@ -115,6 +117,8 @@ urlpatterns = (
|
||||
# 前端页面映射
|
||||
path('web/', web_view, name='web_view'),
|
||||
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.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}
|
||||
}
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
exclude = ["venv"] # 需要排除的文件目录
|
||||
exclude = ["venv", ".venv"] # 需要排除的文件目录
|
||||
for root, dirs, files in os.walk('.'):
|
||||
dirs[:] = list(set(dirs) - set(exclude))
|
||||
if 'migrations' in dirs:
|
||||
|
||||
@@ -4,3 +4,7 @@ from django.apps import AppConfig
|
||||
class SystemConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'dvadmin.system'
|
||||
|
||||
def ready(self):
|
||||
# 注册信号
|
||||
import dvadmin.system.signals # 确保路径正确
|
||||
|
||||
@@ -546,5 +546,50 @@
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "文件存储引擎",
|
||||
"value": "file_engine",
|
||||
"type": 0,
|
||||
"color": null,
|
||||
"is_value": false,
|
||||
"status": true,
|
||||
"sort": 9,
|
||||
"remark": null,
|
||||
"children": [
|
||||
{
|
||||
"label": "本地",
|
||||
"value": "local",
|
||||
"type": 0,
|
||||
"color": "primary",
|
||||
"is_value": true,
|
||||
"status": true,
|
||||
"sort": 1,
|
||||
"remark": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"label": "阿里云oss",
|
||||
"value": "oss",
|
||||
"type": 0,
|
||||
"color": "success",
|
||||
"is_value": true,
|
||||
"status": true,
|
||||
"sort": 2,
|
||||
"remark": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"label": "腾讯cos",
|
||||
"value": "cos",
|
||||
"type": 0,
|
||||
"color": "warning",
|
||||
"is_value": true,
|
||||
"status": true,
|
||||
"sort": 3,
|
||||
"remark": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -235,5 +235,252 @@
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "文件存储配置",
|
||||
"key": "file_storage",
|
||||
"value": null,
|
||||
"sort": 0,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": null,
|
||||
"placeholder": null,
|
||||
"setting": null,
|
||||
"children": [
|
||||
{
|
||||
"title": "存储引擎",
|
||||
"key": "file_engine",
|
||||
"value": "local",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 4,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请选择存储引擎",
|
||||
"setting": "file_engine",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "文件是否备份",
|
||||
"key": "file_backup",
|
||||
"value": false,
|
||||
"sort": 2,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 9,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "启用云存储时,文件是否备份到本地",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-AccessKey",
|
||||
"key": "aliyun_access_key",
|
||||
"value": null,
|
||||
"sort": 3,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入AccessKey",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-Secret",
|
||||
"key": "aliyun_access_secret",
|
||||
"value": null,
|
||||
"sort": 4,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Secret",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-Endpoint",
|
||||
"key": "aliyun_endpoint",
|
||||
"value": null,
|
||||
"sort": 5,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Endpoint",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-上传路径",
|
||||
"key": "aliyun_path",
|
||||
"value": "/media/",
|
||||
"sort": 5,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入上传路径",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "阿里云-Bucket",
|
||||
"key": "aliyun_bucket",
|
||||
"value": null,
|
||||
"sort": 7,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Bucket",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},{
|
||||
"title": "阿里云-cdn地址",
|
||||
"key": "aliyun_cdn_url",
|
||||
"value": null,
|
||||
"sort": 7,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入cdn地址",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-SecretId",
|
||||
"key": "tencent_secret_id",
|
||||
"value": null,
|
||||
"sort": 8,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入SecretId",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-SecretKey",
|
||||
"key": "tencent_secret_key",
|
||||
"value": null,
|
||||
"sort": 9,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入SecretKey",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-Region",
|
||||
"key": "tencent_region",
|
||||
"value": null,
|
||||
"sort": 10,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Region",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-Bucket",
|
||||
"key": "tencent_bucket",
|
||||
"value": null,
|
||||
"sort": 11,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入Bucket",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"title": "腾讯云-上传路径",
|
||||
"key": "tencent_path",
|
||||
"value": "/media/",
|
||||
"sort": 12,
|
||||
"status": false,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [
|
||||
{
|
||||
"required": false,
|
||||
"message": "必填项不能为空"
|
||||
}
|
||||
],
|
||||
"placeholder": "请输入上传路径",
|
||||
"setting": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -29,7 +29,7 @@ class Command(BaseCommand):
|
||||
def serializer_data(self, serializer, query_set: QuerySet):
|
||||
serializer = serializer(query_set, many=True)
|
||||
data = json.loads(json.dumps(serializer.data, ensure_ascii=False))
|
||||
with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w') as f:
|
||||
with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w',encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
return
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from application import dispatch
|
||||
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
|
||||
|
||||
from dvadmin3_flow.base_model import FlowBaseModel
|
||||
class Role(CoreModel,FlowBaseModel):
|
||||
|
||||
class Role(CoreModel):
|
||||
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
|
||||
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
|
||||
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
|
||||
@@ -77,8 +77,14 @@ class Users(CoreModel, AbstractUser):
|
||||
objects = CustomUserManager()
|
||||
|
||||
def set_password(self, raw_password):
|
||||
if raw_password:
|
||||
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.name == "":
|
||||
self.name = self.username
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_users"
|
||||
verbose_name = "用户表"
|
||||
|
||||
@@ -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()
|
||||
detail_init_complete = Signal()
|
||||
@@ -10,3 +16,12 @@ post_tenants_init_complete = Signal()
|
||||
post_tenants_all_init_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 = []
|
||||
|
||||
def get_queryset(self):
|
||||
# 判断是否是 Swagger 文档生成阶段,防止报错
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return self.queryset.model.objects.none()
|
||||
|
||||
# 正常请求下的逻辑
|
||||
if self.request.user.is_superuser:
|
||||
return super().get_queryset()
|
||||
return super().get_queryset().filter(creator=self.request.user)
|
||||
|
||||
@@ -35,8 +35,8 @@ class FileSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def create(self, validated_data):
|
||||
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
|
||||
file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("file_storage.file_backup")
|
||||
file = self.initial_data.get('file')
|
||||
file_size = file.size
|
||||
validated_data['name'] = str(file)
|
||||
@@ -52,15 +52,15 @@ class FileSerializer(CustomModelSerializer):
|
||||
if file_backup:
|
||||
validated_data['url'] = file
|
||||
if file_engine == 'oss':
|
||||
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
|
||||
file_path = ali_oss_upload(file)
|
||||
from dvadmin.utils.aliyunoss import ali_oss_upload
|
||||
file_path = ali_oss_upload(file, file_name=validated_data['name'])
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
elif file_engine == 'cos':
|
||||
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload
|
||||
file_path = tencent_cos_upload(file)
|
||||
from dvadmin.utils.tencentcos import tencent_cos_upload
|
||||
file_path = tencent_cos_upload(file, file_name=validated_data['name'])
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
|
||||
@@ -139,21 +139,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
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):
|
||||
"""
|
||||
消息中心-新增-序列化器
|
||||
@@ -182,10 +167,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
|
||||
targetuser_instance.is_valid(raise_exception=True)
|
||||
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
|
||||
|
||||
class Meta:
|
||||
@@ -225,10 +206,6 @@ class MessageCenterViewSet(CustomModelViewSet):
|
||||
queryset.save()
|
||||
instance = self.get_object()
|
||||
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="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
|
||||
62
backend/dvadmin/utils/aliyunoss.py
Normal file
62
backend/dvadmin/utils/aliyunoss.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import oss2
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from application import dispatch
|
||||
|
||||
|
||||
# 进度条
|
||||
# 当无法确定待上传的数据长度时,total_bytes的值为None。
|
||||
def percentage(consumed_bytes, total_bytes):
|
||||
if total_bytes:
|
||||
rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
|
||||
print('\r{0}% '.format(rate), end='')
|
||||
|
||||
|
||||
def ali_oss_upload(file, file_name):
|
||||
"""
|
||||
阿里云OSS上传
|
||||
"""
|
||||
try:
|
||||
file.seek(0)
|
||||
file_read = file.read()
|
||||
except Exception as e:
|
||||
file_read = file
|
||||
if not file:
|
||||
raise ValidationError('请上传文件')
|
||||
# 转存到oss
|
||||
path_prefix = dispatch.get_system_config_values("file_storage.aliyun_path")
|
||||
if not path_prefix.endswith('/'):
|
||||
path_prefix = path_prefix + '/'
|
||||
if path_prefix.startswith('/'):
|
||||
path_prefix = path_prefix[1:]
|
||||
base_fil_name = f'{path_prefix}{file_name}'
|
||||
# 获取OSS配置
|
||||
# 获取的AccessKey
|
||||
access_key_id = dispatch.get_system_config_values("file_storage.aliyun_access_key")
|
||||
access_key_secret = dispatch.get_system_config_values("file_storage.aliyun_access_secret")
|
||||
auth = oss2.Auth(access_key_id, access_key_secret)
|
||||
# 这个是需要用特定的地址,不同地域的服务器地址不同,不要弄错了
|
||||
# 参考官网给的地址配置https://www.alibabacloud.com/help/zh/object-storage-service/latest/regions-and-endpoints#concept-zt4-cvy-5db
|
||||
endpoint = dispatch.get_system_config_values("file_storage.aliyun_endpoint")
|
||||
bucket_name = dispatch.get_system_config_values("file_storage.aliyun_bucket")
|
||||
if bucket_name.endswith(endpoint):
|
||||
bucket_name = bucket_name.replace(f'.{endpoint}', '')
|
||||
# 你的项目名称,类似于不同的项目上传的图片前缀url不同
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name) # 项目名称
|
||||
# 生成外网访问的文件路径
|
||||
aliyun_cdn_url = dispatch.get_system_config_values("file_storage.aliyun_cdn_url")
|
||||
if aliyun_cdn_url:
|
||||
if aliyun_cdn_url.endswith('/'):
|
||||
aliyun_cdn_url = aliyun_cdn_url[1:]
|
||||
file_path = f"{aliyun_cdn_url}/{base_fil_name}"
|
||||
else:
|
||||
file_path = f"https://{bucket_name}.{endpoint}/{base_fil_name}"
|
||||
# 这个是阿里提供的SDK方法
|
||||
res = bucket.put_object(base_fil_name, file_read, progress_callback=percentage)
|
||||
# 如果上传状态是200 代表成功 返回文件外网访问路径
|
||||
if res.status == 200:
|
||||
return file_path
|
||||
else:
|
||||
return None
|
||||
@@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model):
|
||||
super().delete(using=using, *args, **kwargs)
|
||||
|
||||
|
||||
class CoreModelManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
is_deleted = getattr(self.model, 'is_soft_delete', False)
|
||||
flow_work_status = getattr(self.model, 'flow_work_status', False)
|
||||
queryset = super().get_queryset()
|
||||
if flow_work_status:
|
||||
queryset = queryset.filter(flow_work_status=1)
|
||||
if is_deleted:
|
||||
queryset = queryset.filter(is_deleted=False)
|
||||
return queryset
|
||||
def create(self,request: Request=None, **kwargs):
|
||||
data = {**kwargs}
|
||||
if request:
|
||||
request_user = request.user
|
||||
data["creator"] = request_user
|
||||
data["modifier"] = request_user.id
|
||||
data["dept_belong_id"] = request_user.dept_id
|
||||
# 调用父类的create方法执行实际的创建操作
|
||||
return super().create(**data)
|
||||
|
||||
class CoreModel(models.Model):
|
||||
"""
|
||||
核心标准抽象模型模型,可直接继承使用
|
||||
@@ -98,7 +118,8 @@ class CoreModel(models.Model):
|
||||
verbose_name="修改时间")
|
||||
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
|
||||
objects = CoreModelManager()
|
||||
all_objects = models.Manager()
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = '核心模型'
|
||||
|
||||
56
backend/dvadmin/utils/tencentcos.py
Normal file
56
backend/dvadmin/utils/tencentcos.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from application import dispatch
|
||||
from qcloud_cos import CosConfig
|
||||
from qcloud_cos import CosS3Client
|
||||
|
||||
|
||||
# 进度条
|
||||
# 当无法确定待上传的数据长度时,total_bytes的值为None。
|
||||
def percentage(consumed_bytes, total_bytes):
|
||||
if total_bytes:
|
||||
rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
|
||||
print('\r{0}% '.format(rate), end='')
|
||||
|
||||
def tencent_cos_upload(file, file_name):
|
||||
try:
|
||||
file.seek(0)
|
||||
file_read = file.read()
|
||||
except Exception as e:
|
||||
file_read = file
|
||||
if not file:
|
||||
raise ValidationError('请上传文件')
|
||||
# 生成文件名
|
||||
path_prefix = dispatch.get_system_config_values("file_storage.tencent_path")
|
||||
if not path_prefix.endswith('/'):
|
||||
path_prefix = path_prefix + '/'
|
||||
if path_prefix.startswith('/'):
|
||||
path_prefix = path_prefix[1:]
|
||||
base_fil_name = f'{path_prefix}{file_name}'
|
||||
# 获取cos配置
|
||||
# 1. 设置用户属性, 包括 secret_id, secret_key, region等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成
|
||||
secret_id = dispatch.get_system_config_values("file_storage.tencent_secret_id") # 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
|
||||
secret_key = dispatch.get_system_config_values("file_storage.tencent_secret_key") # 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
|
||||
region = dispatch.get_system_config_values("file_storage.tencent_region") # 替换为用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket # COS 支持的所有 region 列表参见https://cloud.tencent.com/document/product/436/6224
|
||||
bucket = dispatch.get_system_config_values("file_storage.tencent_bucket") # 要访问的桶名称
|
||||
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
|
||||
client = CosS3Client(config)
|
||||
# 访问地址
|
||||
base_file_url = f'https://{bucket}.cos.{region}.myqcloud.com'
|
||||
# 生成外网访问的文件路径
|
||||
if base_file_url.endswith('/'):
|
||||
file_path = base_file_url + base_fil_name
|
||||
else:
|
||||
file_path = f'{base_file_url}/{base_fil_name}'
|
||||
# 这个是阿里提供的SDK方法 bucket是调用的4.1中配置的变量名
|
||||
try:
|
||||
response = client.put_object(
|
||||
Bucket=bucket,
|
||||
Body=file_read,
|
||||
Key=base_fil_name,
|
||||
EnableMD5=False
|
||||
)
|
||||
return file_path
|
||||
except:
|
||||
return None
|
||||
@@ -7,26 +7,27 @@ djangorestframework==3.15.2
|
||||
django-restql==0.15.4
|
||||
django-simple-captcha==0.6.0
|
||||
django-timezone-field==7.0
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
djangorestframework_simplejwt==5.4.0
|
||||
drf-yasg==1.21.7
|
||||
mysqlclient==2.2.0
|
||||
pypinyin==0.51.0
|
||||
ua-parser==0.18.0
|
||||
pyparsing==3.1.2
|
||||
openpyxl==3.1.5
|
||||
requests==2.32.3
|
||||
requests==2.32.4
|
||||
typing-extensions==4.12.2
|
||||
tzlocal==5.2
|
||||
channels==4.1.0
|
||||
channels-redis==4.2.0
|
||||
websockets==11.0.3
|
||||
user-agents==2.2.0
|
||||
six==1.16.0
|
||||
whitenoise==6.7.0
|
||||
psycopg2==2.9.9
|
||||
uvicorn==0.30.3
|
||||
gunicorn==22.0.0
|
||||
gunicorn==23.0.0
|
||||
gevent==24.2.1
|
||||
Pillow==10.4.0
|
||||
pyinstaller==6.9.0
|
||||
dvadmin3-celery==3.1.6
|
||||
oss2==2.19.1
|
||||
cos-python-sdk-v5==1.9.37
|
||||
@@ -2,7 +2,7 @@
|
||||
ENV = 'development'
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = 'http://127.0.0.1:8001'
|
||||
VITE_API_URL = 'http://127.0.0.1:8000'
|
||||
|
||||
# 是否启用按钮权限
|
||||
VITE_PM_ENABLED = true
|
||||
|
||||
@@ -35,7 +35,6 @@ const route = useRoute();
|
||||
const stores = useTagsViewRoutes();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
import websocket from '/@/utils/websocket';
|
||||
const core = useCore();
|
||||
const router = useRouter();
|
||||
// 获取版本号
|
||||
@@ -92,63 +91,5 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
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>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
class="tableSelector"
|
||||
multiple
|
||||
:collapseTags="props.tableConfig.collapseTags"
|
||||
@remove-tag="removeTag"
|
||||
v-model="data"
|
||||
placeholder="请选择"
|
||||
@visible-change="visibleChange"
|
||||
@@ -66,7 +65,7 @@ import { request } from '/@/utils/service';
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array || String || Number,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
tableConfig: {
|
||||
type: Object,
|
||||
@@ -87,6 +86,8 @@ const props = defineProps({
|
||||
},
|
||||
displayLabel: {},
|
||||
} as any);
|
||||
console.log(props.tableConfig);
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
// tableRef
|
||||
const tableRef = ref();
|
||||
@@ -137,6 +138,8 @@ const handleCurrentChange = (val: any) => {
|
||||
*/
|
||||
const getDict = async () => {
|
||||
const url = props.tableConfig.url;
|
||||
console.log(url);
|
||||
|
||||
const params = {
|
||||
page: pageConfig.page,
|
||||
limit: pageConfig.limit,
|
||||
@@ -162,29 +165,25 @@ const getDict = async () => {
|
||||
|
||||
// 获取节点值
|
||||
const getNodeValues = () => {
|
||||
console.log(props.tableConfig.url);
|
||||
|
||||
request({
|
||||
url:props.tableConfig.valueUrl,
|
||||
url: props.tableConfig.url,
|
||||
method: 'post',
|
||||
data:{ids:props.modelValue}
|
||||
}).then(res=>{
|
||||
data: { ids: props.modelValue },
|
||||
}).then((res) => {
|
||||
if (res.data.length > 0) {
|
||||
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) => {
|
||||
tableRef.value!.toggleRowSelection(
|
||||
row,
|
||||
true,
|
||||
false
|
||||
)
|
||||
})
|
||||
tableRef.value!.toggleRowSelection(row, true, false);
|
||||
});
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下拉框展开/关闭
|
||||
@@ -206,11 +205,10 @@ const handlePageChange = (page: any) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(()=>{
|
||||
getNodeValues()
|
||||
},1000)
|
||||
})
|
||||
|
||||
// setTimeout(() => {
|
||||
// getNodeValues();
|
||||
// }, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -57,26 +57,7 @@
|
||||
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
|
||||
></i>
|
||||
</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>
|
||||
<div></div>
|
||||
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
||||
<span class="layout-navbars-breadcrumb-user-link">
|
||||
<span v-if="isSocketOpen">
|
||||
@@ -115,8 +96,7 @@ import other from '/@/utils/other';
|
||||
import mittBus from '/@/utils/mitt';
|
||||
import { Session, Local } from '/@/utils/storage';
|
||||
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 Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
|
||||
@@ -148,17 +128,6 @@ const layoutUserFlexNum = computed(() => {
|
||||
// 定义变量内容
|
||||
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 = () => {
|
||||
if (!screenfull.isEnabled) {
|
||||
@@ -237,8 +206,10 @@ const onLanguageChange = (lang: string) => {
|
||||
initI18nOrSize('globalI18n', 'disabledI18n');
|
||||
};
|
||||
// 初始化组件大小/i18n
|
||||
const initI18nOrSize = (value: string, attr: string) => {
|
||||
state[attr] = Local.get('themeConfig')[value];
|
||||
const initI18nOrSize = (value: string, attr: keyof typeof state) => {
|
||||
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(() => {
|
||||
@@ -246,12 +217,32 @@ onMounted(() => {
|
||||
initI18nOrSize('globalComponentSize', 'disabledSize');
|
||||
initI18nOrSize('globalI18n', 'disabledI18n');
|
||||
}
|
||||
getMessageCenterCount();
|
||||
});
|
||||
|
||||
//消息中心的未读数量
|
||||
import { messageCenterStore } from '/@/stores/messageCenter';
|
||||
import {getBaseURL} from "/@/utils/baseUrl";
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<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>
|
||||
<div class="content-box">
|
||||
@@ -33,27 +34,28 @@ const onAllReadClick = () => {
|
||||
state.newsList = [];
|
||||
};
|
||||
// 前往通知中心点击
|
||||
import {useRouter } from "vue-router";
|
||||
const route = useRouter()
|
||||
import { useRouter } from 'vue-router';
|
||||
const route = useRouter();
|
||||
const onGoToGiteeClick = () => {
|
||||
route.push('/messageCenter')
|
||||
route.push('/messageCenter');
|
||||
};
|
||||
//获取最新消息
|
||||
import { request } from "/@/utils/service";
|
||||
import { request } from '/@/utils/service';
|
||||
const getLastMsg = () => {
|
||||
request({
|
||||
url: '/api/system/message_center/get_newest_msg/',
|
||||
method: 'get',
|
||||
params: {}
|
||||
params: {},
|
||||
}).then((res: any) => {
|
||||
const { data } = res
|
||||
state.newsList= [data]
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
getLastMsg()
|
||||
})
|
||||
const { data } = res;
|
||||
console.log(data);
|
||||
|
||||
state.newsList = [data];
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
getLastMsg();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -93,7 +93,7 @@ const initElMenuOffsetLeft = () => {
|
||||
nextTick(() => {
|
||||
let els = <HTMLElement>document.querySelector('.el-menu.el-menu--horizontal li.is-active');
|
||||
if (!els) return false;
|
||||
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft;
|
||||
// elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft;
|
||||
});
|
||||
};
|
||||
// 路由过滤递归函数
|
||||
|
||||
@@ -98,10 +98,22 @@ export function formatTwoStageRoutes(arr: any) {
|
||||
|
||||
const frameOutRoutes = staticRoutes.map(item => item.path)
|
||||
|
||||
const checkToken = ()=>{
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const _oauth2_token = urlParams.get('_oauth2_token');
|
||||
if (_oauth2_token) {
|
||||
Session.set('token', _oauth2_token);
|
||||
const cleanUrl = window.location.href.split('?')[0];
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
useUserInfo(pinia).setUserInfos();
|
||||
|
||||
}
|
||||
}
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
|
||||
await checkVersion()
|
||||
checkToken()
|
||||
NProgress.configure({showSpinner: false});
|
||||
if (to.meta.title) NProgress.start();
|
||||
const token = Session.get('token');
|
||||
|
||||
@@ -6,7 +6,9 @@ import {useFrontendMenuStore} from "/@/stores/frontendMenu";
|
||||
|
||||
// 用户信息
|
||||
export interface UserInfosState {
|
||||
id: '',
|
||||
avatar: string;
|
||||
is_superuser: boolean,
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ import headerImage from '/@/assets/img/headerImage.png';
|
||||
export const useUserInfo = defineStore('userInfo', {
|
||||
state: (): UserInfosStates => ({
|
||||
userInfos: {
|
||||
id:'',
|
||||
avatar: '',
|
||||
username: '',
|
||||
name: '',
|
||||
@@ -19,6 +20,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
mobile: '',
|
||||
gender: '',
|
||||
pwd_change_count:null,
|
||||
is_superuser: false,
|
||||
dept_info: {
|
||||
dept_id: 0,
|
||||
dept_name: '',
|
||||
@@ -37,6 +39,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
this.userInfos.pwd_change_count = count;
|
||||
},
|
||||
async updateUserInfos(userInfos:any) {
|
||||
this.userInfos.id = userInfos.id;
|
||||
this.userInfos.username = userInfos.name;
|
||||
this.userInfos.avatar = userInfos.avatar;
|
||||
this.userInfos.name = userInfos.name;
|
||||
@@ -46,6 +49,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
this.userInfos.dept_info = userInfos.dept_info;
|
||||
this.userInfos.role_info = userInfos.role_info;
|
||||
this.userInfos.pwd_change_count = userInfos.pwd_change_count;
|
||||
this.userInfos.is_superuser = userInfos.is_superuser;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
},
|
||||
async setUserInfos() {
|
||||
@@ -54,6 +58,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
this.userInfos = Session.get('userInfo');
|
||||
} else {
|
||||
let userInfos: any = await this.getApiUserInfo();
|
||||
this.userInfos.id = userInfos.id;
|
||||
this.userInfos.username = userInfos.data.name;
|
||||
this.userInfos.avatar = userInfos.data.avatar;
|
||||
this.userInfos.name = userInfos.data.name;
|
||||
@@ -63,17 +68,16 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
||||
this.userInfos.role_info = userInfos.data.role_info;
|
||||
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
|
||||
this.userInfos.is_superuser = userInfos.data.is_superuser;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
}
|
||||
},
|
||||
async setWebSocketState(socketState: boolean) {
|
||||
this.isSocketOpen = socketState;
|
||||
},
|
||||
async getApiUserInfo() {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
method: 'get',
|
||||
}).then((res:any)=>{
|
||||
this.userInfos.id = res.data.id;
|
||||
this.userInfos.username = res.data.name;
|
||||
this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage;
|
||||
this.userInfos.name = res.data.name;
|
||||
@@ -83,6 +87,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
this.userInfos.dept_info = res.data.dept_info;
|
||||
this.userInfos.role_info = res.data.role_info;
|
||||
this.userInfos.pwd_change_count = res.data.pwd_change_count;
|
||||
this.userInfos.is_superuser = res.data.is_superuser;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
})
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.icon-selector-warp-title {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
left: 15px;
|
||||
|
||||
@@ -28,3 +28,10 @@ export function getUserInfo() {
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function getBackends() {
|
||||
return request({
|
||||
url: '/api/dvadmin3_social_oauth2/backend/get_login_backend/',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
139
web/src/views/system/login/component/oauth2.vue
Normal file
139
web/src/views/system/login/component/oauth2.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="other-fast-way" v-if="backends.length">
|
||||
<div class="fast-title"><span>其他快速方式登录</span></div>
|
||||
<ul class="fast-list">
|
||||
<li v-for="(v, k) in backends" :key="v">
|
||||
<a @click.once="handleOAuth2LoginClick(v)" style="width: 50px;color: #18bc9c">
|
||||
<img :src="v.icon" :alt="v.app_name" />
|
||||
<p>{{ v.app_name }}</p>
|
||||
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, reactive, toRefs } from 'vue';
|
||||
import * as loginApi from '../api';
|
||||
import { OAuth2Backend } from '/@/views/system/login/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'loginOAuth2',
|
||||
setup() {
|
||||
const handleOAuth2LoginClick = (backend: OAuth2Backend) => {
|
||||
history.replaceState(null, '', location.pathname + location.search);
|
||||
window.location.href = backend.authentication_url + '?next=' + window.location.href;
|
||||
};
|
||||
const state = reactive({
|
||||
handleOAuth2LoginClick: handleOAuth2LoginClick,
|
||||
backends: [],
|
||||
});
|
||||
const getBackends = async () => {
|
||||
loginApi.getBackends().then((ret: any) => {
|
||||
state.backends = ret.data;
|
||||
});
|
||||
};
|
||||
// const handleTreeClick = (record: MenuTreeItemType) => {
|
||||
// menuButtonRef.value?.handleRefreshTable(record);
|
||||
// menuFieldRef.value?.handleRefreshTable(record)
|
||||
// };
|
||||
|
||||
onMounted(() => {
|
||||
getBackends();
|
||||
});
|
||||
return {
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-content-form {
|
||||
margin-top: 20px;
|
||||
@for $i from 1 through 4 {
|
||||
.login-animation#{$i} {
|
||||
opacity: 0;
|
||||
animation-name: error-num;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: calc($i/10) + s;
|
||||
}
|
||||
}
|
||||
|
||||
.login-content-code {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-content-submit {
|
||||
width: 100%;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 300;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.login-msg {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
.other-fast-way {
|
||||
//height: 240px;
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
//display: flex;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
.fast-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fast-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
li {
|
||||
margin-left: 20px;
|
||||
opacity: 0;
|
||||
animation-name: error-num;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.1s;
|
||||
a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 35px;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -34,6 +34,8 @@
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
<OAuth2 />
|
||||
|
||||
<!-- <Scan v-if="state.isScan" />-->
|
||||
<!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">-->
|
||||
<!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
|
||||
@@ -81,6 +83,8 @@ const Account = defineAsyncComponent(() => import('/@/views/system/login/compone
|
||||
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
|
||||
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
|
||||
const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.vue'));
|
||||
const OAuth2 = defineAsyncComponent(() => import('/@/views/system/login/component/oauth2.vue'));
|
||||
|
||||
import _ from "lodash-es";
|
||||
import {useUserInfo} from "/@/stores/userInfo";
|
||||
const { userInfos } = storeToRefs(useUserInfo());
|
||||
|
||||
8
web/src/views/system/login/types.ts
Normal file
8
web/src/views/system/login/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export interface OAuth2Backend {
|
||||
app_name: string;
|
||||
backend_name: string;
|
||||
icon: string;
|
||||
authentication_url: string;
|
||||
}
|
||||
|
||||
@@ -254,6 +254,9 @@ const handleSubmit = () => {
|
||||
let res;
|
||||
menuBtnLoading.value = true;
|
||||
if (menuFormData.id) {
|
||||
if (menuFormData.parent == undefined) {
|
||||
menuFormData.parent = null
|
||||
}
|
||||
res = await UpdateObj(menuFormData);
|
||||
} else {
|
||||
res = await AddObj(menuFormData);
|
||||
|
||||
@@ -131,10 +131,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
data: [
|
||||
{ value: 0, label: '按用户' },
|
||||
{ value: 1, label: '按角色' },
|
||||
{
|
||||
value: 2,
|
||||
label: '按部门',
|
||||
},
|
||||
{ value: 2, label: '按部门' },
|
||||
{ value: 3, label: '通知公告' },
|
||||
],
|
||||
}),
|
||||
@@ -142,14 +139,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
component: {
|
||||
optionName: 'el-radio-button',
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '必选项',
|
||||
// @ts-ignore
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
rules: [{ required: true, message: '必选项', trigger: ['blur', 'change'] }],
|
||||
},
|
||||
},
|
||||
target_user: {
|
||||
@@ -191,10 +181,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
}),
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{
|
||||
required: true,
|
||||
message: '必填项',
|
||||
},
|
||||
{ required: true, message: '必填项' },
|
||||
],
|
||||
},
|
||||
column: {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
highlight-current
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
:check-on-click-leaf="false"
|
||||
>
|
||||
</el-tree>
|
||||
</template>
|
||||
|
||||
@@ -181,7 +181,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
},
|
||||
},
|
||||
dict: dict({
|
||||
value: dictionary('button_status_bool'),
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ import { computed } from "vue";
|
||||
import { Md5 } from 'ts-md5';
|
||||
import { commonCrudConfig } from "/@/utils/commonCrud";
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import {exportData} from "./api";
|
||||
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
@@ -81,9 +82,9 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
title: "导出",//鼠标停留显示的信息
|
||||
show: auth('user:Export'),
|
||||
click: (ctx: any) => ElMessageBox.confirm(
|
||||
'确定重设密码吗?', '提示',
|
||||
'确定导出数据吗?', '提示',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
).then(() => resetToDefaultPasswordRequest(ctx.row))
|
||||
).then(() => exportData(ctx.row))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user