!123 正式发布v3.2.0版本

Merge pull request !123 from dvadmin/develop
This commit is contained in:
dvadmin
2025-06-22 13:45:30 +00:00
committed by Gitee
39 changed files with 852 additions and 472 deletions

View File

@@ -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
)
)
),
}) })

View File

@@ -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 是该路由的消费者
]

View File

@@ -399,8 +399,12 @@ DICTIONARY_CONFIG = {}
# ================================================= # # ================================================= #
# 租户共享app # 租户共享app
TENANT_SHARED_APPS = [] TENANT_SHARED_APPS = []
# 普通租户独有app
TENANT_EXCLUSIVE_APPS = []
# 插件 urlpatterns # 插件 urlpatterns
PLUGINS_URL_PATTERNS = [] PLUGINS_URL_PATTERNS = []
# 所有模式有的
SHARED_APPS = []
# ********** 一键导入插件配置开始 ********** # ********** 一键导入插件配置开始 **********
# 例如: # 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心 # from dvadmin_upgrade_center.settings import * # 升级中心

View 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

View File

@@ -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)

View File

@@ -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}
}
)

View File

@@ -2,7 +2,7 @@
import os import os
exclude = ["venv"] # 需要排除的文件目录 exclude = ["venv", ".venv"] # 需要排除的文件目录
for root, dirs, files in os.walk('.'): for root, dirs, files in os.walk('.'):
dirs[:] = list(set(dirs) - set(exclude)) dirs[:] = list(set(dirs) - set(exclude))
if 'migrations' in dirs: if 'migrations' in dirs:

View File

@@ -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 # 确保路径正确

View File

@@ -546,5 +546,50 @@
"children": [] "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": []
}
]
} }
] ]

View File

@@ -235,5 +235,252 @@
"children": [] "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": []
}
]
}
] ]

View File

@@ -29,7 +29,7 @@ class Command(BaseCommand):
def serializer_data(self, serializer, query_set: QuerySet): def serializer_data(self, serializer, query_set: QuerySet):
serializer = serializer(query_set, many=True) serializer = serializer(query_set, many=True)
data = json.loads(json.dumps(serializer.data, ensure_ascii=False)) 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) json.dump(data, f, indent=4, ensure_ascii=False)
return return

View File

@@ -9,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from application import dispatch from application import dispatch
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models 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="角色名称") name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, 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="角色顺序") sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
@@ -77,7 +77,13 @@ class Users(CoreModel, AbstractUser):
objects = CustomUserManager() objects = CustomUserManager()
def set_password(self, raw_password): def set_password(self, raw_password):
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) 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: class Meta:
db_table = table_prefix + "system_users" db_table = table_prefix + "system_users"

View File

@@ -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) # 设置永不超时的键值对

View File

@@ -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)

View File

@@ -35,8 +35,8 @@ class FileSerializer(CustomModelSerializer):
fields = "__all__" fields = "__all__"
def create(self, validated_data): def create(self, validated_data):
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local' file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup") file_backup = dispatch.get_system_config_values("file_storage.file_backup")
file = self.initial_data.get('file') file = self.initial_data.get('file')
file_size = file.size file_size = file.size
validated_data['name'] = str(file) validated_data['name'] = str(file)
@@ -52,15 +52,15 @@ class FileSerializer(CustomModelSerializer):
if file_backup: if file_backup:
validated_data['url'] = file validated_data['url'] = file
if file_engine == 'oss': if file_engine == 'oss':
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload from dvadmin.utils.aliyunoss import ali_oss_upload
file_path = ali_oss_upload(file) file_path = ali_oss_upload(file, file_name=validated_data['name'])
if file_path: if file_path:
validated_data['file_url'] = file_path validated_data['file_url'] = file_path
else: else:
raise ValueError("上传失败") raise ValueError("上传失败")
elif file_engine == 'cos': elif file_engine == 'cos':
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload from dvadmin.utils.tencentcos import tencent_cos_upload
file_path = tencent_cos_upload(file) file_path = tencent_cos_upload(file, file_name=validated_data['name'])
if file_path: if file_path:
validated_data['file_url'] = file_path validated_data['file_url'] = file_path
else: else:

View File

@@ -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])

View 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

View File

@@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model):
super().delete(using=using, *args, **kwargs) 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): class CoreModel(models.Model):
""" """
核心标准抽象模型模型,可直接继承使用 核心标准抽象模型模型,可直接继承使用
@@ -98,7 +118,8 @@ class CoreModel(models.Model):
verbose_name="修改时间") verbose_name="修改时间")
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间") verbose_name="创建时间")
objects = CoreModelManager()
all_objects = models.Manager()
class Meta: class Meta:
abstract = True abstract = True
verbose_name = '核心模型' verbose_name = '核心模型'

View 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

View File

@@ -7,26 +7,27 @@ 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
dvadmin3-celery==3.1.6 dvadmin3-celery==3.1.6
oss2==2.19.1
cos-python-sdk-v5==1.9.37

View File

@@ -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

View File

@@ -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>

View File

@@ -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"
@@ -29,9 +28,9 @@
max-height="200" max-height="200"
height="200" height="200"
:highlight-current-row="!props.tableConfig.isMultiple" :highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@select="handleSelectionChange" @select="handleSelectionChange"
@selectAll="handleSelectionChange" @selectAll="handleSelectionChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" /> <el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
@@ -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()
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(
row,
true,
false
)
})
}
})
}
tableRef.value!.clearSelection();
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(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>

View File

@@ -57,33 +57,14 @@
: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>
{{ userInfos.username === '' ? 'common' : userInfos.username }} {{ userInfos.username === '' ? 'common' : userInfos.username }}
<el-icon class="el-icon--right"> <el-icon class="el-icon--right">
<ele-ArrowDown /> <ele-ArrowDown />
@@ -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,29 +289,29 @@ 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;
font-size: 14px; font-size: 14px;
left: 5px; left: 5px;
height: 12px; height: 12px;
width: 12px; width: 12px;
padding: 0; padding: 0;
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;
font-size: 14px; font-size: 14px;
left: 5px; left: 5px;
height: 12px; height: 12px;
width: 12px; width: 12px;
padding: 0; padding: 0;
background-color: #979b9c; background-color: #979b9c;
} }
} }
} }
</style> </style>

View File

@@ -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);
})
} state.newsList = [data];
onMounted(()=>{ });
getLastMsg() };
}) onMounted(() => {
getLastMsg();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -93,7 +93,7 @@ const initElMenuOffsetLeft = () => {
nextTick(() => { nextTick(() => {
let els = <HTMLElement>document.querySelector('.el-menu.el-menu--horizontal li.is-active'); let els = <HTMLElement>document.querySelector('.el-menu.el-menu--horizontal li.is-active');
if (!els) return false; if (!els) return false;
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft; // elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft;
}); });
}; };
// 路由过滤递归函数 // 路由过滤递归函数

View File

@@ -98,10 +98,22 @@ export function formatTwoStageRoutes(arr: any) {
const frameOutRoutes = staticRoutes.map(item => item.path) 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) => { router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新 // 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
await checkVersion() await checkVersion()
checkToken()
NProgress.configure({showSpinner: false}); NProgress.configure({showSpinner: false});
if (to.meta.title) NProgress.start(); if (to.meta.title) NProgress.start();
const token = Session.get('token'); const token = Session.get('token');

View File

@@ -2,17 +2,19 @@
* 定义接口来定义对象的类型 * 定义接口来定义对象的类型
* `stores` 全部类型定义在这里 * `stores` 全部类型定义在这里
*/ */
import {useFrontendMenuStore} from "/@/stores/frontendMenu"; import { useFrontendMenuStore } from "/@/stores/frontendMenu";
// 用户信息 // 用户信息
export interface UserInfosState { export interface UserInfosState {
id: '',
avatar: string; avatar: string;
is_superuser: boolean,
username: string; username: string;
name: string; name: string;
email: string; email: string;
mobile: string; mobile: string;
gender: string; gender: string;
pwd_change_count:null|number; pwd_change_count: null | number;
dept_info: { dept_info: {
dept_id: number; dept_id: number;
dept_name: string; dept_name: string;
@@ -107,9 +109,9 @@ export interface ConfigStates {
export interface FrontendMenu { export interface FrontendMenu {
arrayRouter: Array<any>; arrayRouter: Array<any>;
treeRouter:Array<any>; treeRouter: Array<any>;
frameOutRoutes:Array<any>; frameOutRoutes: Array<any>;
frameInRoutes:Array<any>; frameInRoutes: Array<any>;
} }

View File

@@ -12,6 +12,7 @@ import headerImage from '/@/assets/img/headerImage.png';
export const useUserInfo = defineStore('userInfo', { export const useUserInfo = defineStore('userInfo', {
state: (): UserInfosStates => ({ state: (): UserInfosStates => ({
userInfos: { userInfos: {
id:'',
avatar: '', avatar: '',
username: '', username: '',
name: '', name: '',
@@ -19,6 +20,7 @@ export const useUserInfo = defineStore('userInfo', {
mobile: '', mobile: '',
gender: '', gender: '',
pwd_change_count:null, pwd_change_count:null,
is_superuser: false,
dept_info: { dept_info: {
dept_id: 0, dept_id: 0,
dept_name: '', dept_name: '',
@@ -37,6 +39,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.pwd_change_count = count; this.userInfos.pwd_change_count = count;
}, },
async updateUserInfos(userInfos:any) { async updateUserInfos(userInfos:any) {
this.userInfos.id = userInfos.id;
this.userInfos.username = userInfos.name; this.userInfos.username = userInfos.name;
this.userInfos.avatar = userInfos.avatar; this.userInfos.avatar = userInfos.avatar;
this.userInfos.name = userInfos.name; this.userInfos.name = userInfos.name;
@@ -46,6 +49,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.dept_info = userInfos.dept_info; this.userInfos.dept_info = userInfos.dept_info;
this.userInfos.role_info = userInfos.role_info; this.userInfos.role_info = userInfos.role_info;
this.userInfos.pwd_change_count = userInfos.pwd_change_count; this.userInfos.pwd_change_count = userInfos.pwd_change_count;
this.userInfos.is_superuser = userInfos.is_superuser;
Session.set('userInfo', this.userInfos); Session.set('userInfo', this.userInfos);
}, },
async setUserInfos() { async setUserInfos() {
@@ -54,6 +58,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos = Session.get('userInfo'); this.userInfos = Session.get('userInfo');
} else { } else {
let userInfos: any = await this.getApiUserInfo(); let userInfos: any = await this.getApiUserInfo();
this.userInfos.id = userInfos.id;
this.userInfos.username = userInfos.data.name; this.userInfos.username = userInfos.data.name;
this.userInfos.avatar = userInfos.data.avatar; this.userInfos.avatar = userInfos.data.avatar;
this.userInfos.name = userInfos.data.name; 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.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info; this.userInfos.role_info = userInfos.data.role_info;
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count; this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
this.userInfos.is_superuser = userInfos.data.is_superuser;
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/',
method: 'get', method: 'get',
}).then((res:any)=>{ }).then((res:any)=>{
this.userInfos.id = res.data.id;
this.userInfos.username = res.data.name; this.userInfos.username = res.data.name;
this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage; this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage;
this.userInfos.name = res.data.name; 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.dept_info = res.data.dept_info;
this.userInfos.role_info = res.data.role_info; this.userInfos.role_info = res.data.role_info;
this.userInfos.pwd_change_count = res.data.pwd_change_count; this.userInfos.pwd_change_count = res.data.pwd_change_count;
this.userInfos.is_superuser = res.data.is_superuser;
Session.set('userInfo', this.userInfos); Session.set('userInfo', this.userInfos);
}) })
}, },

View File

@@ -7,7 +7,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
.icon-selector-warp-title { .icon-selector-warp-title {
position: absolute; position: relative;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
left: 15px; left: 15px;

View File

@@ -28,3 +28,10 @@ export function getUserInfo() {
method: 'get', method: 'get',
}); });
} }
export function getBackends() {
return request({
url: '/api/dvadmin3_social_oauth2/backend/get_login_backend/',
method: 'get',
});
}

View 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>

View File

@@ -34,7 +34,9 @@
</el-tab-pane> --> </el-tab-pane> -->
</el-tabs> </el-tabs>
</div> </div>
<!-- <Scan v-if="state.isScan" />--> <OAuth2 />
<!-- <Scan v-if="state.isScan" />-->
<!-- <div class="login-content-main-sacn" @click="state.isScan = !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>--> <!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
<!-- <div class="login-content-main-sacn-delta"></div>--> <!-- <div class="login-content-main-sacn-delta"></div>-->
@@ -81,6 +83,8 @@ const Account = defineAsyncComponent(() => import('/@/views/system/login/compone
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue')); const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue')); const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.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 _ from "lodash-es";
import {useUserInfo} from "/@/stores/userInfo"; import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo()); const { userInfos } = storeToRefs(useUserInfo());

View File

@@ -0,0 +1,8 @@
export interface OAuth2Backend {
app_name: string;
backend_name: string;
icon: string;
authentication_url: string;
}

View File

@@ -254,6 +254,9 @@ const handleSubmit = () => {
let res; let res;
menuBtnLoading.value = true; menuBtnLoading.value = true;
if (menuFormData.id) { if (menuFormData.id) {
if (menuFormData.parent == undefined) {
menuFormData.parent = null
}
res = await UpdateObj(menuFormData); res = await UpdateObj(menuFormData);
} else { } else {
res = await AddObj(menuFormData); res = await AddObj(menuFormData);

View File

@@ -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: {

View File

@@ -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>

View File

@@ -181,7 +181,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
}, },
}, },
dict: dict({ dict: dict({
value: dictionary('button_status_bool'), data: dictionary('button_status_bool'),
}), }),
}, },
}, },

View File

@@ -19,6 +19,7 @@ import { computed } from "vue";
import { Md5 } from 'ts-md5'; import { Md5 } from 'ts-md5';
import { commonCrudConfig } from "/@/utils/commonCrud"; import { commonCrudConfig } from "/@/utils/commonCrud";
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import {exportData} from "./api";
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => { const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query); return await api.GetList(query);
@@ -81,9 +82,9 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
title: "导出",//鼠标停留显示的信息 title: "导出",//鼠标停留显示的信息
show: auth('user:Export'), show: auth('user:Export'),
click: (ctx: any) => ElMessageBox.confirm( click: (ctx: any) => ElMessageBox.confirm(
'确定重设密码吗?', '提示', '确定导出数据吗?', '提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(() => resetToDefaultPasswordRequest(ctx.row)) ).then(() => exportData(ctx.row))
} }
} }
}, },