diff --git a/backend/application/dispatch.py b/backend/application/dispatch.py index 3f19225..6843c75 100644 --- a/backend/application/dispatch.py +++ b/backend/application/dispatch.py @@ -40,7 +40,7 @@ def _get_all_system_config(): from dvadmin.system.models import SystemConfig system_config_obj = ( - SystemConfig.objects.filter(status=True, parent_id__isnull=False) + SystemConfig.objects.filter(parent_id__isnull=False) .values("parent__key", "key", "value", "form_item_type") .order_by("sort") ) @@ -131,6 +131,8 @@ def get_dictionary_config(schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if not settings.DICTIONARY_CONFIG: + refresh_dictionary() if is_tenants_mode(): dictionary_config = settings.DICTIONARY_CONFIG[schema_name or connection.tenant.schema_name] else: @@ -175,6 +177,8 @@ def get_system_config(schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if not settings.SYSTEM_CONFIG: + refresh_system_config() if is_tenants_mode(): dictionary_config = settings.SYSTEM_CONFIG[schema_name or connection.tenant.schema_name] else: diff --git a/backend/application/routing.py b/backend/application/routing.py new file mode 100644 index 0000000..166857b --- /dev/null +++ b/backend/application/routing.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from dvadmin.system import routing as dvadminRouting + + +application = ProtocolTypeRouter({ + 'websocket': AuthMiddlewareStack( + URLRouter( + dvadminRouting.websocket_urlpatterns# 指明路由文件是devops/routing.py + ) + ), +}) \ No newline at end of file diff --git a/backend/application/settings.py b/backend/application/settings.py index c8ce5e9..6112247 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -57,6 +57,7 @@ INSTALLED_APPS = [ "dvadmin.system", "drf_yasg", "captcha", + 'channels', ] MIDDLEWARE = [ @@ -164,6 +165,19 @@ CORS_ORIGIN_ALLOW_ALL = True # 允许cookie CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作 +# ================================================= # +# ********************* channels配置 ******************* # +# ================================================= # +ASGI_APPLICATION = 'application.routing.application' +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [('127.0.0.1', 6379)], #需修改 + }, + }, +} + # ================================================= # # ********************* 日志配置 ******************* # # ================================================= # diff --git a/backend/dvadmin/system/consumers.py b/backend/dvadmin/system/consumers.py new file mode 100644 index 0000000..d7290fc --- /dev/null +++ b/backend/dvadmin/system/consumers.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +import urllib + +from asgiref.sync import sync_to_async +from channels.db import database_sync_to_async +from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer +import json + +from jwt import InvalidSignatureError + +from application import settings +from dvadmin.system.models import MessageCenter + +send_dict = {} + +# 发送消息结构体 +def message(sender, msg_type, msg): + text = { + 'sender': sender, + 'contentType': msg_type, + 'content': msg, + } + return text + +#异步获取消息中心的目标用户 +@database_sync_to_async +def _get_message_center_instance(message_id): + _MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user',flat=True) + if _MessageCenter: + return _MessageCenter + else: + return [] + + +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"] + params = request_data(self.scope) + room = params.get('room')[0] + 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 = room + #收到连接时候处理, + await self.channel_layer.group_add( + self.chat_group_name, + self.channel_name + ) + # 将该客户端的信息发送函数与客户端的唯一身份标识绑定,保存至自定义的字典中 + if len(send_dict)==0: + send_dict.setdefault(self.chat_group_name, {}) + for room in send_dict.keys(): + if room == self.chat_group_name: + send_dict[self.chat_group_name][self.user_id] = self.send + else: + send_dict.setdefault(self.chat_group_name,{}) + await self.accept() + await self.send_json(message('system', 'INFO', '连接成功')) + except InvalidSignatureError: + await self.disconnect(None) + + + async def disconnect(self, close_code): + # 删除 send_dict 中对应的信息 + del send_dict[self.chat_group_name][self.user_id] + # Leave room group + await self.channel_layer.group_discard(self.chat_group_name, self.channel_name) + print("连接关闭") + await self.close(close_code) + + async def receive(self, text_data=None, byte_text_data=None): + print(text_data) + try: + text_data_json = json.loads(text_data) + except Exception as e: + print('数据无法被json格式化', e) + await self.disconnect(400) + else: + print(123,text_data_json) + # 获取将要推送信息的目标身份标识,调用保存在 send_dict中的信息发送函数 + 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 send_dict[self.chat_group_name][send_user](text_data=json.dumps(text_data_json)) \ No newline at end of file diff --git a/backend/dvadmin/system/fixtures/init_dept.json b/backend/dvadmin/system/fixtures/init_dept.json index 43af999..3603364 100644 --- a/backend/dvadmin/system/fixtures/init_dept.json +++ b/backend/dvadmin/system/fixtures/init_dept.json @@ -1,6 +1,7 @@ [ { "name": "DVAdmin团队", + "key": "dvadmin", "sort": 1, "owner": "", "phone": "", @@ -10,6 +11,7 @@ "children": [ { "name": "运营部", + "key": "", "sort": 2, "owner": "", "phone": "", @@ -20,6 +22,7 @@ }, { "name": "技术部", + "key": "technology", "sort": 1, "owner": "", "phone": "", diff --git a/backend/dvadmin/system/fixtures/init_role.json b/backend/dvadmin/system/fixtures/init_role.json index f98462d..911f7ca 100644 --- a/backend/dvadmin/system/fixtures/init_role.json +++ b/backend/dvadmin/system/fixtures/init_role.json @@ -7,5 +7,14 @@ "admin": true, "data_range": 3, "remark": null + }, + { + "name": "用户", + "key": "public", + "sort": 2, + "status": true, + "admin": true, + "data_range": 3, + "remark": null } ] diff --git a/backend/dvadmin/system/fixtures/init_users.json b/backend/dvadmin/system/fixtures/init_users.json index 781cc83..e5856d2 100644 --- a/backend/dvadmin/system/fixtures/init_users.json +++ b/backend/dvadmin/system/fixtures/init_users.json @@ -1,38 +1,59 @@ [ - { - "username": "admin", - "email": "dvadmin@django-vue-admin.com", - "mobile": "18888888888", - "avatar": "", - "name": "管理员", - "gender": 1, - "user_type": 0, - "dept": 1, - "role": [], - "first_name": "", - "last_name": "", - "is_staff": true, - "is_active": true, - "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", - "last_login": null, - "is_superuser": false - }, - { - "username": "superadmin", - "email": "dvadmin@django-vue-admin.com", - "mobile": "13333333333", - "avatar": null, - "name": "超级管理员", - "gender": 1, - "user_type": 0, - "dept": 1, - "role": [], - "first_name": "", - "last_name": "", - "is_staff": true, - "is_active": true, - "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", - "last_login": null, - "is_superuser": true - } + { + "username": "superadmin", + "email": "dvadmin@django-vue-admin.com", + "mobile": "13333333333", + "avatar": null, + "name": "超级管理员", + "gender": 1, + "user_type": 0, + "role": [], + "role_key": [ + "admin" + ], + "dept_key": "dvadmin", + "first_name": "", + "last_name": "", + "is_staff": true, + "is_active": true, + "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", + "last_login": null, + "is_superuser": true + }, + { + "username": "admin", + "email": "dvadmin@django-vue-admin.com", + "mobile": "18888888888", + "avatar": "", + "name": "管理员", + "gender": 1, + "user_type": 0, + "role": [], + "first_name": "", + "last_name": "", + "is_staff": true, + "is_active": true, + "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", + "last_login": null, + "is_superuser": false + }, + { + "username": "test", + "email": "dvadmin@django-vue-admin.com", + "mobile": "18888888888", + "avatar": "", + "name": "测试人员", + "gender": 1, + "user_type": 0, + "role": [], + "role_key": ["public"], + "dept_key": "technology", + "first_name": "", + "last_name": "", + "is_staff": true, + "is_active": true, + "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", + "last_login": null, + "is_superuser": false + } ] diff --git a/backend/dvadmin/system/fixtures/initialize.py b/backend/dvadmin/system/fixtures/initialize.py index 14f260a..e8dc122 100644 --- a/backend/dvadmin/system/fixtures/initialize.py +++ b/backend/dvadmin/system/fixtures/initialize.py @@ -22,7 +22,7 @@ class Initialize(CoreInitialize): """ 初始化部门信息 """ - self.init_base(DeptInitSerializer, unique_fields=['name', 'parent']) + self.init_base(DeptInitSerializer, unique_fields=['name', 'parent','key']) def init_role(self): """ diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index 6aee47b..4b08bf2 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -103,6 +103,7 @@ class Role(CoreModel): class Dept(CoreModel): name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称") + key = models.CharField(max_length=64, unique=True,null=True,blank=True, verbose_name="关联字符", help_text="关联字符") sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序") owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人") phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话") @@ -259,7 +260,7 @@ def media_file_name(instance, filename): class FileList(CoreModel): - name = models.CharField(max_length=50, null=True, blank=True, verbose_name="名称", help_text="名称") + name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称") url = models.FileField(upload_to=media_file_name) md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5") @@ -427,4 +428,4 @@ class MessageCenter(CoreModel): db_table = table_prefix + "message_center" verbose_name = "消息中心" verbose_name_plural = verbose_name - ordering = ("-create_datetime",) \ No newline at end of file + ordering = ("-create_datetime",) diff --git a/backend/dvadmin/system/routing.py b/backend/dvadmin/system/routing.py new file mode 100644 index 0000000..79372d1 --- /dev/null +++ b/backend/dvadmin/system/routing.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from django.urls import path +from . import consumers + +websocket_urlpatterns = [ + path('ws//', consumers.DvadminWebSocket.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者 +] \ No newline at end of file diff --git a/backend/dvadmin/system/views/dept.py b/backend/dvadmin/system/views/dept.py index 41e5cd2..31982f5 100644 --- a/backend/dvadmin/system/views/dept.py +++ b/backend/dvadmin/system/views/dept.py @@ -6,9 +6,11 @@ @Remark: 部门管理 """ from rest_framework import serializers +from rest_framework.decorators import action from dvadmin.system.models import Dept from dvadmin.utils.json_response import DetailResponse, SuccessResponse +from dvadmin.utils.permission import AnonymousUserPermission from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -18,18 +20,17 @@ class DeptSerializer(CustomModelSerializer): 部门-序列化器 """ parent_name = serializers.CharField(read_only=True, source='parent.name') - has_children = serializers.SerializerMethodField() status_label = serializers.SerializerMethodField() + has_children = serializers.SerializerMethodField() + + def get_status_label(self, obj: Dept): + if obj.status: + return "启用" + return "禁用" def get_has_children(self, obj: Dept): return Dept.objects.filter(parent_id=obj.id).count() - def get_status_label(self, instance): - status = instance.status - if status: - return "启用" - return "禁用" - class Meta: model = Dept fields = '__all__' @@ -58,7 +59,8 @@ class DeptInitSerializer(CustomModelSerializer): menu_data['parent'] = instance.id filter_data = { "name": menu_data['name'], - "parent": menu_data['parent'] + "parent": menu_data['parent'], + "key": menu_data['key'] } instance_obj = Dept.objects.filter(**filter_data).first() if instance_obj and not self.initial_data.get('reset'): @@ -72,7 +74,7 @@ class DeptInitSerializer(CustomModelSerializer): class Meta: model = Dept fields = ['name', 'sort', 'owner', 'phone', 'email', 'status', 'parent', 'creator', 'dept_belong_id', - 'children'] + 'children', 'key'] extra_kwargs = { 'creator': {'write_only': True}, 'dept_belong_id': {'write_only': True} @@ -122,7 +124,13 @@ class DeptViewSet(CustomModelViewSet): if lazy: # 如果懒加载模式,返回全部 if not parent: - if self.request.user.is_superuser: + role_list = request.user.role.filter(status=1).values("admin", "data_range") + is_admin = False + for ele in role_list: + if 3 == ele.get("data_range") or ele.get("admin") == True: + is_admin = True + break + if self.request.user.is_superuser or is_admin: queryset = queryset.filter(parent__isnull=True) else: queryset = queryset.filter(id=self.request.user.dept_id) @@ -146,3 +154,10 @@ class DeptViewSet(CustomModelViewSet): queryset = queryset.filter(id=self.request.user.dept_id) data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent') return DetailResponse(data=data, msg="获取成功") + + @action(methods=["GET"], detail=False, permission_classes=[AnonymousUserPermission]) + def all_dept(self, request, *args, **kwargs): + self.extra_filter_backends = [] + queryset = self.filter_queryset(self.get_queryset()) + data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent') + return DetailResponse(data=data, msg="获取成功") diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 9df5669..faaa2ea 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -91,6 +91,16 @@ class LoginSerializer(TokenObtainPairSerializer): data["name"] = self.user.name data["userId"] = self.user.id data["avatar"] = self.user.avatar + dept = getattr(self.user, 'dept', None) + if dept: + data['dept_info'] = { + 'dept_id': dept.id, + 'dept_name': dept.name, + 'dept_key': dept.key + } + role = getattr(self.user, 'role', None) + if role: + data['role_info'] = role.values('id', 'name', 'key') request = self.context.get("request") request.user = self.user # 记录登录日志 diff --git a/backend/dvadmin/system/views/system_config.py b/backend/dvadmin/system/views/system_config.py index cfe7202..aa97091 100644 --- a/backend/dvadmin/system/views/system_config.py +++ b/backend/dvadmin/system/views/system_config.py @@ -250,9 +250,29 @@ class InitSettingsViewSet(APIView): authentication_classes = [] permission_classes = [] + def filter_system_config_values(self, data: dict): + """ + 过滤系统初始化配置 + :param data: + :return: + """ + if not self.request.query_params.get('key', ''): + return data + new_data = {} + for key in self.request.query_params.get('key', '').split('|'): + if key: + new_data.update(**dict(filter(lambda x: x[0].startswith(key), data.items()))) + return new_data + def get(self, request): data = dispatch.get_system_config() if not data: dispatch.refresh_system_config() data = dispatch.get_system_config() + # 不返回后端专用配置 + backend_config = [f"{ele.get('parent__key')}.{ele.get('key')}" for ele in + SystemConfig.objects.filter(status=False, parent_id__isnull=False).values('parent__key', + 'key')] + data = dict(filter(lambda x: x[0] not in backend_config, data.items())) + data = self.filter_system_config_values(data=data) return DetailResponse(data=data) diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index 5ee5fcc..b036370 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -3,7 +3,7 @@ import hashlib from django.contrib.auth.hashers import make_password from django_restql.fields import DynamicSerializerMethodField from rest_framework import serializers -from rest_framework.decorators import action +from rest_framework.decorators import action, permission_classes from rest_framework.permissions import IsAuthenticated from application import dispatch @@ -15,12 +15,24 @@ from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.viewset import CustomModelViewSet +def recursion(instance,parent,result): + new_instance = getattr(instance,parent,None) + res = [] + data = getattr(instance, result, None) + if data: + res.append(data) + if new_instance: + array = recursion(new_instance,parent,result) + res+=(array) + return res + class UserSerializer(CustomModelSerializer): """ 用户管理-序列化器 """ dept_name = serializers.CharField(source='dept.name', read_only=True) role_info = DynamicSerializerMethodField() + dept_name_all = serializers.SerializerMethodField() class Meta: model = Users @@ -30,6 +42,11 @@ class UserSerializer(CustomModelSerializer): "post": {"required": False}, } + def get_dept_name_all(self, instance): + dept_name_all = recursion(instance.dept, "parent", "name") + dept_name_all.reverse() + return "/".join(dept_name_all) + def get_role_info(self, instance, parsed_query): roles = instance.role.all() # You can do what ever you want in here @@ -46,6 +63,16 @@ class UsersInitSerializer(CustomModelSerializer): """ 初始化获取数信息(用于生成初始化json文件) """ + def save(self, **kwargs): + instance = super().save(**kwargs) + role_key = self.initial_data.get('role_key',[]) + role_ids = Role.objects.filter(key__in=role_key).values_list('id',flat=True) + instance.role.set(role_ids) + dept_key = self.initial_data.get('dept_key',None) + dept_id = Dept.objects.filter(key=dept_key).first() + instance.dept = dept_id + instance.save() + return instance class Meta: model = Users @@ -213,17 +240,17 @@ class UserViewSet(CustomModelViewSet): } search_fields = ["username", "name", "gender", "dept__name", "role__name"] # 导出 - export_field_label = [ - "用户账号", - "用户名称", - "用户邮箱", - "手机号码", - "用户性别", - "帐号状态", - "最后登录时间", - "部门名称", - "部门负责人", - ] + export_field_label = { + "username":"用户账号", + "name":"用户名称", + "email":"用户邮箱", + "mobile":"手机号码", + "gender":"用户性别", + "is_active":"帐号状态", + "last_login":"最后登录时间", + "dept_name":"部门名称", + "dept_owner":"部门负责人", + } export_serializer_class = ExportUserProfileSerializer # 导入 import_serializer_class = UserProfileImportSerializer @@ -254,12 +281,26 @@ class UserViewSet(CustomModelViewSet): """获取当前用户信息""" user = request.user result = { + "id": user.id, "name": user.name, "mobile": user.mobile, + "user_type": user.user_type, "gender": user.gender, "email": user.email, "avatar": user.avatar, + "dept": user.dept.id, + "is_superuser": user.is_superuser, + "role": user.role.values_list('id', flat=True), } + dept = getattr(user, 'dept', None) + if dept: + result['dept_info'] = { + 'dept_id': dept.id, + 'dept_name': dept.name + } + role = getattr(user, 'role', None) + if role: + result['role_info'] = role.values('id', 'name', 'key') return DetailResponse(data=result, msg="获取成功") @action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated]) @@ -272,22 +313,23 @@ class UserViewSet(CustomModelViewSet): @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) def change_password(self, request, *args, **kwargs): """密码修改""" - instance = Users.objects.filter(id=kwargs.get("pk")).first() data = request.data old_pwd = data.get("oldPassword") new_pwd = data.get("newPassword") new_pwd2 = data.get("newPassword2") - if instance: - if new_pwd != new_pwd2: - return ErrorResponse(msg="两次密码不匹配") - elif instance.check_password(old_pwd): - instance.password = make_password(new_pwd) - instance.save() - return DetailResponse(data=None, msg="修改成功") - else: - return ErrorResponse(msg="旧密码不正确") + if old_pwd is None or new_pwd is None or new_pwd2 is None: + return ErrorResponse(msg="参数不能为空") + if new_pwd != new_pwd2: + return ErrorResponse(msg="两次密码不匹配") + check_password = request.user.check_password(old_pwd) + if not check_password: + check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest()) + if check_password: + request.user.password = make_password(new_pwd) + request.user.save() + return DetailResponse(data=None, msg="修改成功") else: - return ErrorResponse(msg="未获取到用户") + return ErrorResponse(msg="旧密码不正确") @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) def reset_to_default_password(self, request, *args, **kwargs): diff --git a/backend/dvadmin/utils/import_export_mixin.py b/backend/dvadmin/utils/import_export_mixin.py index 1a35e34..ea64ba6 100644 --- a/backend/dvadmin/utils/import_export_mixin.py +++ b/backend/dvadmin/utils/import_export_mixin.py @@ -26,6 +26,21 @@ class ImportSerializerMixin: # 表格表头最大宽度,默认50个字符 export_column_width = 50 + def is_number(self,num): + try: + float(num) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(num) + return True + except (TypeError, ValueError): + pass + return False + def get_string_len(self, string): """ 获取字符串最大长度 @@ -35,6 +50,8 @@ class ImportSerializerMixin: length = 4 if string is None: return length + if self.is_number(string): + return length for char in string: length += 2.1 if ord(char) > 256 else 1 return round(length, 1) if length <= self.export_column_width else self.export_column_width @@ -155,6 +172,21 @@ class ExportSerializerMixin: # 表格表头最大宽度,默认50个字符 export_column_width = 50 + def is_number(self,num): + try: + float(num) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(num) + return True + except (TypeError, ValueError): + pass + return False + def get_string_len(self, string): """ 获取字符串最大长度 @@ -164,6 +196,8 @@ class ExportSerializerMixin: length = 4 if string is None: return length + if self.is_number(string): + return length for char in string: length += 2.1 if ord(char) > 256 else 1 return round(length, 1) if length <= self.export_column_width else self.export_column_width @@ -176,36 +210,35 @@ class ExportSerializerMixin: :param kwargs: :return: """ - assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ queryset = self.filter_queryset(self.get_queryset()) + assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ + assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ data = self.export_serializer_class(queryset, many=True).data # 导出excel 表 response = HttpResponse(content_type="application/msexcel") response["Access-Control-Expose-Headers"] = f"Content-Disposition" - response["Content-Disposition"] = f'attachment;filename={quote(str(f"导出{get_verbose_name(queryset)}.xlsx"))}' + response["content-disposition"] = f'attachment;filename={quote(str(f"导出{get_verbose_name(queryset)}.xlsx"))}' wb = Workbook() ws = wb.active - header_data = ["序号", *self.export_field_label] + header_data = ["序号", *self.export_field_label.values()] + hidden_header = ["#", *self.export_field_label.keys()] df_len_max = [self.get_string_len(ele) for ele in header_data] row = get_column_letter(len(self.export_field_label) + 1) column = 1 ws.append(header_data) for index, results in enumerate(data): results_list = [] - for inx, result in enumerate(results.values()): - # 布尔值进行更新 - if result is True: - result = "是" - elif result is False: - result = "否" - if isinstance(result, int): - result = str(result) - # 计算最大列宽度 - result_column_width = self.get_string_len(result) - if result_column_width > df_len_max[inx + 1]: - df_len_max[inx + 1] = result_column_width - - results_list.append(result) + for h_index, h_item in enumerate(hidden_header): + for key,val in results.items(): + if key == h_item: + if val is None or val=="": + results_list.append("") + else: + results_list.append(val) + # 计算最大列宽度 + result_column_width = self.get_string_len(val) + if h_index !=0 and result_column_width > df_len_max[h_index]: + df_len_max[h_index] = result_column_width ws.append([index + 1, *results_list]) column += 1 #  更新列宽 diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index ecac1a1..273aeaf 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -76,6 +76,16 @@ class CoreModel(models.Model): verbose_name = '核心模型' verbose_name_plural = verbose_name + def delete(self, using=None, soft_delete=True, *args, **kwargs): + """ + Soft delete object (set its ``is_deleted`` field to True). + Actually delete object if setting ``soft`` to False. + """ + if soft_delete: + self.is_deleted = True + self.save(using=using) + else: + return super(CoreModel, self).delete(using=using, *args, **kwargs) diff --git a/backend/dvadmin/utils/viewset.py b/backend/dvadmin/utils/viewset.py index 5e2008f..30ecc09 100644 --- a/backend/dvadmin/utils/viewset.py +++ b/backend/dvadmin/utils/viewset.py @@ -38,7 +38,7 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi extra_filter_backends = [DataLevelPermissionsFilter] permission_classes = [CustomPermission] import_field_dict = {} - export_field_label = [] + export_field_label = {} def filter_queryset(self, queryset): for backend in set(set(self.filter_backends) | set(self.extra_filter_backends or [])): diff --git a/backend/requirements.txt b/backend/requirements.txt index 497fa6c..1aa39d9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -asgiref==3.3.4 +asgiref==3.5.2 certifi==2021.5.30 chardet==4.0.0 coreapi==2.3.3 @@ -41,3 +41,5 @@ urllib3==1.26.6 user-agents==2.2.0 whitenoise==5.3.0 openpyxl==3.0.9 +channels==3.0.5 +channels-redis==3.4.1 diff --git a/web/.env b/web/.env index af33066..4425b6d 100644 --- a/web/.env +++ b/web/.env @@ -6,6 +6,9 @@ VUE_APP_TITLE=D2Admin # 网络请求公用地址 VUE_APP_API=/api/ +# websocket地址 +VUE_APP_WEBSOCKET="" + # 仓库地址 VUE_APP_REPO=https://github.com/d2-projects/d2-admin-start-kit diff --git a/web/.env.development b/web/.env.development index 2b35a9a..eab5c45 100644 --- a/web/.env.development +++ b/web/.env.development @@ -6,4 +6,5 @@ VUE_APP_TITLE=企业级后台管理系统 VUE_APP_PM_ENABLED = true # 后端接口地址及端口(域名) VUE_APP_API = "http://127.0.0.1:8000" +VUE_APP_WEBSOCKET = "ws://127.0.0.1:8000" diff --git a/web/package.json b/web/package.json index 41966e1..1aa01a4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "django-vue-admin", - "version": "2.0.4", + "version": "2.0.6", "scripts": { "serve": "vue-cli-service serve --open", "start": "npm run serve", @@ -37,6 +37,7 @@ "lowdb": "^1.0.0", "nprogress": "^0.2.0", "qiankun": "^2.7.2", + "qrcodejs2": "^0.0.2", "screenfull": "^5.0.2", "sortablejs": "^1.10.1", "ua-parser-js": "^0.7.20", diff --git a/web/src/api/service.js b/web/src/api/service.js index 7470667..e4c9480 100644 --- a/web/src/api/service.js +++ b/web/src/api/service.js @@ -224,7 +224,7 @@ export const downloadFile = function ({ url, params, method, filename }) { responseType: 'blob' // headers: {Accept: 'application/vnd.openxmlformats-officedocument'} }).then(res => { - const fileName = window.decodeURI(res.headers['content-disposition'].split('=')[1]) || filename + '.xls' || '文件导出.xls' + const fileName = window.decodeURI(filename + '.xls' || res.headers['content-disposition'].split('=')[1]) || '文件导出.xls' if (res) { const blob = new Blob([res.data], { type: 'charset=utf-8' }) const elink = document.createElement('a') diff --git a/web/src/api/websocket.js b/web/src/api/websocket.js index a06bdfc..2e7ad67 100644 --- a/web/src/api/websocket.js +++ b/web/src/api/websocket.js @@ -3,7 +3,7 @@ import util from '@/libs/util' function initWebSocket (e) { const token = util.cookies.get('token') if (token) { - const wsUri = 'ws://127.0.0.1:8000/?auth=' + token + const wsUri = process.env.VUE_APP_WEBSOCKET + '/ws/' + token + '/?room=message_center' this.socket = new WebSocket(wsUri)// 这里面的this都指向vue this.socket.onerror = webSocketOnError this.socket.onmessage = webSocketOnMessage @@ -53,18 +53,13 @@ function webSocketOnMessage (e) { // 关闭websiocket function closeWebsocket () { console.log('连接已关闭...') - close() -} -function close () { - this.socket.close() // 关闭 websocket - this.socket.onclose = function (e) { - console.log(e)// 监听关闭事件 - console.log('关闭') - } + // close() + this.socket.close() } + function webSocketSend (message) { this.socket.send(JSON.stringify(message)) } export default { - initWebSocket, close, webSocketSend + initWebSocket, closeWebsocket, webSocketSend } diff --git a/web/src/assets/svg-icons/icons/dingding.svg b/web/src/assets/svg-icons/icons/dingding.svg new file mode 100644 index 0000000..7db1949 --- /dev/null +++ b/web/src/assets/svg-icons/icons/dingding.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/dept-format/index.js b/web/src/components/dept-format/index.js new file mode 100644 index 0000000..3af55fc --- /dev/null +++ b/web/src/components/dept-format/index.js @@ -0,0 +1,7 @@ +function install (Vue) { + Vue.component('dept-format', () => import('./lib/dept-format')) +} + +export default { + install +} diff --git a/web/src/components/dept-format/lib/dept-format.vue b/web/src/components/dept-format/lib/dept-format.vue new file mode 100644 index 0000000..1d176be --- /dev/null +++ b/web/src/components/dept-format/lib/dept-format.vue @@ -0,0 +1,47 @@ + + +Footer diff --git a/web/src/components/index.js b/web/src/components/index.js index 943e6c8..6f42469 100644 --- a/web/src/components/index.js +++ b/web/src/components/index.js @@ -9,3 +9,4 @@ Vue.component('d2-icon-svg', () => import('./d2-icon-svg/index.vue')) Vue.component('importExcel', () => import('./importExcel/index.vue')) Vue.component('foreignKey', () => import('./foreign-key/index.vue')) Vue.component('manyToMany', () => import('./many-to-many/index.vue')) +Vue.component('dept-format', () => import('./dept-format/lib/dept-format.vue')) diff --git a/web/src/components/table-selector/table-selector.vue b/web/src/components/table-selector/table-selector.vue index 45833ad..de2f998 100644 --- a/web/src/components/table-selector/table-selector.vue +++ b/web/src/components/table-selector/table-selector.vue @@ -201,7 +201,7 @@ export default { // this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true) // } // this.initData() - console.log(this) + this.searchTableData() }, computed: { _elProps () { diff --git a/web/src/install.js b/web/src/install.js index fa38537..2fab2ec 100644 --- a/web/src/install.js +++ b/web/src/install.js @@ -16,7 +16,6 @@ import { request } from '@/api/service' import util from '@/libs/util' import XEUtils from 'xe-utils' import store from '@/store/index' -import { urlPrefix as deptPrefix } from '@/views/system/dept/api' import types from '@/config/d2p-extends/types' import { checkPlugins, plugins } from '@/views/plugins' @@ -243,7 +242,8 @@ Vue.prototype.commonEndColumns = function (param = {}) { }, dept_belong_id: { showForm: (param.dept_belong_id && param.dept_belong_id.showForm) !== undefined ? param.dept_belong_id.showForm : false, - showTable: (param.dept_belong_id && param.dept_belong_id.showTable) !== undefined ? param.dept_belong_id.showTable : false + showTable: (param.dept_belong_id && param.dept_belong_id.showTable) !== undefined ? param.dept_belong_id.showTable : false, + showSearch: (param.dept_belong_id && param.dept_belong_id.showSearch) !== undefined ? param.dept_belong_id.showSearch : false }, modifier_name: { showForm: (param.modifier_name && param.modifier_name.showForm) !== undefined ? param.modifier_name.showForm : false, @@ -293,61 +293,41 @@ Vue.prototype.commonEndColumns = function (param = {}) { } }, { - title: '数据归属部门', + title: '所属部门', key: 'dept_belong_id', show: showData.dept_belong_id.showTable, width: 150, search: { - disabled: true + disabled: !showData.dept_belong_id.showSearch }, - type: 'table-selector', + type: 'tree-selector', dict: { - cache: true, - url: deptPrefix, - isTree: true, + cache: false, + url: '/api/system/dept/all_dept/', + // isTree: true, + // dept: true, value: 'id', // 数据字典中value字段的属性名 label: 'name', // 数据字典中label字段的属性名 - children: 'children', // 数据字典中children字段的属性名 - getData: (url, dict, { - _, - component - }) => { - return request({ - url: url, - params: { limit: 999, status: 1 } - }).then(ret => { - return ret.data.data - }) - } + children: 'children' // 数据字典中children字段的属性名 + // getData: (url, dict, { + // _, + // component + // }) => { + // return request({ + // url: url + // }).then(ret => { + // return XEUtils.toArrayTree(ret.data, { parentKey: 'parent', strict: false }) + // }) + // } + }, + component: { + name: 'dept-format', + props: { multiple: false, clearable: true } }, form: { disabled: !showData.dept_belong_id.showForm, component: { - props: { - elProps: { - treeConfig: { - transform: true, - rowField: 'id', - parentField: 'parent', - expandAll: true - }, - columns: [ - { - field: 'name', - title: '部门名称', - treeNode: true - }, - { - field: 'status', - title: '状态' - }, - { - field: 'parent_name', - title: '父级部门' - } - ] - } - } + props: { multiple: false, clearable: true } }, helper: { render (h) { @@ -355,6 +335,12 @@ Vue.prototype.commonEndColumns = function (param = {}) { ) } } + }, + // 接收时,处理数据 + valueBuilder (row, col) { + if (row[col.key]) { + row[col.key] = Number(row[col.key]) + } } }, { diff --git a/web/src/layout/header-aside/layout.vue b/web/src/layout/header-aside/layout.vue index 2415ada..601d890 100644 --- a/web/src/layout/header-aside/layout.vue +++ b/web/src/layout/header-aside/layout.vue @@ -197,7 +197,7 @@ export default { }, destroyed () { // 离开路由之后断开websocket连接 - this.$websocket.close() + this.$websocket.closeWebsocket() } } diff --git a/web/src/libs/util.import.plugin.js b/web/src/libs/util.import.plugin.js index aa3794d..1f9d17c 100644 --- a/web/src/libs/util.import.plugin.js +++ b/web/src/libs/util.import.plugin.js @@ -1 +1,9 @@ -module.exports = file => () => import('@great-dream/' + file) +module.exports = file => { + var result + try { + result = require('@great-dream/' + file).default + } catch (error) { + result = require('@/views/plugins/' + file).default + } + return result +} diff --git a/web/src/libs/util.js b/web/src/libs/util.js index 9d0abd1..266436f 100644 --- a/web/src/libs/util.js +++ b/web/src/libs/util.js @@ -38,12 +38,24 @@ util.open = function (url) { */ util.baseURL = function () { var baseURL = process.env.VUE_APP_API - if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenant-web') !== -1) { + var param = baseURL.split('/')[3] || '' + if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenant-web') !== -1 && (!param || baseURL.startsWith('/'))) { + // 1.把127.0.0.1 替换成和前端一样域名 + // 2.把 ip 地址替换成和前端一样域名 + // 3.把 /api 或其他类似的替换成和前端一样域名 // document.domain var host = baseURL.split('/')[2] - var prot = host.split(':')[1] || 80 - host = document.domain + ':' + prot - baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + (baseURL.split('/')[3] || '') + if (host) { + var prot = baseURL.split(':')[2] || 80 + if (prot === 80 || prot === 443) { + host = document.domain + } else { + host = document.domain + ':' + prot + } + baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param + } else { + baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL + } } if (!baseURL.endsWith('/')) { baseURL += '/' @@ -79,4 +91,30 @@ util.randomString = function (e) { return n } +util.ArrayToTree = function (rootList, parentValue, parentName, list) { + for (const item of rootList) { + if (item.parent === parentValue) { + if (parentName) { + item.name = parentName + '/' + item.name + } + list.push(item) + } + } + + for (const i of list) { + // 如果子元素里面存在children就直接递归,不存在就生成一个children + if (i.children) { + util.ArrayToTree(rootList, i.id, i.name, i.children) + } else { + i.children = [] + util.ArrayToTree(rootList, i.id, i.name, i.children) + } + + if (i.children.length === 0) { + delete i.children + } + } + return list +} + export default util diff --git a/web/src/router/index.js b/web/src/router/index.js index 6d19422..83771a9 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -17,6 +17,7 @@ import util from '@/libs/util.js' // 路由数据 import routes from './routes' import { getMenu, handleAsideMenu, handleRouter, checkRouter } from '@/menu' +import { request } from '@/api/service' // fix vue-router NavigationDuplicated const VueRouterPush = VueRouter.prototype.push @@ -55,6 +56,24 @@ router.beforeEach(async (to, from, next) => { // 请根据自身业务需要修改 const token = util.cookies.get('token') if (token && token !== 'undefined') { + if (!store.state.d2admin.user.info.name) { + var res = await request({ + url: '/api/system/user/user_info/', + method: 'get', + params: {} + }) + await store.dispatch('d2admin/user/set', { + name: res.data.name, + user_id: res.data.id, + avatar: res.data.avatar, + role_info: res.data.role_info, + dept_info: res.data.dept_info, + is_superuser: res.data.is_superuser + }, { root: true }) + await store.dispatch('d2admin/account/load') + store.dispatch('d2admin/dept/load') + store.dispatch('d2admin/settings/init') + } if (!store.state.d2admin.menu || store.state.d2admin.menu.aside.length === 0) { // 动态添加路由 getMenu().then(ret => { diff --git a/web/src/store/modules/d2admin/modules/account.js b/web/src/store/modules/d2admin/modules/account.js index e3f0d79..b934005 100644 --- a/web/src/store/modules/d2admin/modules/account.js +++ b/web/src/store/modules/d2admin/modules/account.js @@ -44,7 +44,13 @@ export default { util.cookies.set('token', res.access) util.cookies.set('refresh', res.refresh) // 设置 vuex 用户信息 - await dispatch('d2admin/user/set', { name: res.name, user_id: res.userId, avatar: res.avatar }, { root: true }) + await dispatch('d2admin/user/set', { + name: res.name, + user_id: res.userId, + avatar: res.avatar, + role_info: res.role_info, + dept_info: res.dept_info + }, { root: true }) // 用户登录后从持久化数据加载一系列的设置 await dispatch('load') }, diff --git a/web/src/store/modules/d2admin/modules/dept.js b/web/src/store/modules/d2admin/modules/dept.js new file mode 100644 index 0000000..0b964ee --- /dev/null +++ b/web/src/store/modules/d2admin/modules/dept.js @@ -0,0 +1,38 @@ +import { request } from '@/api/service' +import util from '@/libs/util' + +export default { + namespaced: true, + state: { + // 用户信息 + data: undefined + }, + actions: { + /** + * @description 初始化部门数据 + * @param {Object} context + * @param {*} info info + */ + async getDeptName ({ state, dispatch }, { data }) { + const nameDict = {} + for (const items of data) { + if (items.children) { + const filterData = await dispatch('getDeptName', { data: items.children }) + for (var key in filterData) { + nameDict[key] = filterData[key] + } + } + nameDict[items.id] = items.name + } + return nameDict + }, + async load ({ state, dispatch }, info) { + // 持久化 + const ret = await request({ + url: '/api/system/dept/all_dept/' + }) + const data = util.ArrayToTree(ret.data.data || ret.data, null, null, []) + state.data = await dispatch('getDeptName', { data: data }) + } + } +} diff --git a/web/src/store/modules/d2admin/modules/user.js b/web/src/store/modules/d2admin/modules/user.js index 13c9fc3..5dad7b4 100644 --- a/web/src/store/modules/d2admin/modules/user.js +++ b/web/src/store/modules/d2admin/modules/user.js @@ -14,12 +14,12 @@ export default { // store 赋值 state.info = info // 持久化 - await dispatch('d2admin/db/set', { - dbName: 'sys', - path: 'user.info', - value: info, - user: true - }, { root: true }) + // await dispatch('d2admin/db/set', { + // dbName: 'sys', + // path: 'user.info', + // value: info, + // user: true + // }, { root: true }) }, /** * @description 从数据库取用户数据 @@ -27,12 +27,12 @@ export default { */ async load ({ state, dispatch }) { // store 赋值 - state.info = await dispatch('d2admin/db/get', { - dbName: 'sys', - path: 'user.info', - defaultValue: {}, - user: true - }, { root: true }) + // state.info = await dispatch('d2admin/db/get', { + // dbName: 'sys', + // path: 'user.info', + // defaultValue: {}, + // user: true + // }, { root: true }) } } } diff --git a/web/src/views/system/config/components/formContent.vue b/web/src/views/system/config/components/formContent.vue index 970cfdb..191ec85 100644 --- a/web/src/views/system/config/components/formContent.vue +++ b/web/src/views/system/config/components/formContent.vue @@ -2,16 +2,22 @@
变量标题 - 变量值 - 变量名 + 变量值 + 变量名 + 是否前端配置 + 操作 - + - + + {{ item.label }} @@ -183,7 +189,7 @@

删除后无法恢复,确定删除吗?

取消 - 确定 + 确定
删除 @@ -195,7 +201,32 @@
- {{ editableTabsItem.key }}.{{ item.key }} + + + + + {{ editableTabsItem.key }}.{{ item.key }} + + + + + + + + + + + + + 确定 @@ -272,7 +303,7 @@ export default { // 获取数据 getInit () { const that = this - api.GetList({ parent: this.options.id }).then(res => { + api.GetList({ parent: this.options.id, limit: 999 }).then(res => { const { data } = res.data this.formList = data const form = {} @@ -382,11 +413,17 @@ export default { } }, // 子表删除 - onRemoveChild (row, index) { + onRemoveChild (row, index, refName) { + console.log(row, index) if (row.id) { - console.log(1, 'ok') + api.DelObj(row.id).then(res => { + this.refreshView() + }) } else { this.childTableData.splice(index, 1) + const tableName = 'xTable_' + refName + const tableData = this.$refs[tableName][0].remove(row) + console.log(tableData) } }, // 图片预览 @@ -445,6 +482,25 @@ export default { if (value.uid === file.uid) index = inx }) this.form[key].splice(index, 1) + }, + // 配置的行删除 + onDelRow (obj) { + api.DelObj(obj.id).then(res => { + this.refreshView() + }) + }, + // 行编辑 + onEdit (index) { + const that = this + that.$set(that.formList[index], 'new_key', that.formList[index].key) + that.$set(that.formList[index], 'edit', true) + }, + // 行编辑保存 + onEditSave (obj) { + obj.key = JSON.parse(JSON.stringify(obj.new_key)) + api.UpdateObj(obj).then(res => { + this.refreshView() + }) } }, mounted () { diff --git a/web/src/views/system/dept/crud.js b/web/src/views/system/dept/crud.js index 7357049..b3ff85c 100644 --- a/web/src/views/system/dept/crud.js +++ b/web/src/views/system/dept/crud.js @@ -135,46 +135,6 @@ export const crudOptions = (vm) => { } } }, - // { - // title: '上级部门', - // key: 'parent', - // show: false, - // search: { - // disabled: true - // }, - // type: 'cascader', - // dict: { - // cache: false, - // url: deptPrefix, - // isTree: true, - // value: 'id', // 数据字典中value字段的属性名 - // label: 'name', // 数据字典中label字段的属性名 - // children: 'children', // 数据字典中children字段的属性名 - // getData: (url, dict) => { // 配置此参数会覆盖全局的getRemoteDictFunc - // return request({ url: url, params: { limit: 999, status: 1 } }).then(ret => { - // const data = XEUtils.toArrayTree(ret.data.data, { parentKey: 'parent', strict: true }) - // return [{ id: null, name: '根节点', children: data }] - // }) - // } - // }, - // form: { - // component: { - // span: 12, - - // props: { - // elProps: { - // clearable: true, - // showAllLevels: false, // 仅显示最后一级 - // props: { - // checkStrictly: true, // 可以不需要选到最后一级 - // emitPath: false, - // clearable: true - // } - // } - // } - // } - // } - // }, { title: '部门名称', key: 'name', @@ -207,6 +167,22 @@ export const crudOptions = (vm) => { } } }, + { + title: '部门标识', + key: 'key', + sortable: true, + form: { + component: { + props: { + clearable: true + }, + placeholder: '请输入标识字符' + }, + itemProps: { + class: { yxtInput: true } + } + } + }, { title: '负责人', key: 'owner', diff --git a/web/src/views/system/dept/index.vue b/web/src/views/system/dept/index.vue index a362e70..1e935e8 100644 --- a/web/src/views/system/dept/index.vue +++ b/web/src/views/system/dept/index.vue @@ -49,13 +49,17 @@ export default { }, addRequest (row) { d2CrudPlus.util.dict.clear() + this.$store.dispatch('d2admin/dept/load') return api.createObj(row) }, updateRequest (row) { d2CrudPlus.util.dict.clear() + this.$store.dispatch('d2admin/dept/load') return api.UpdateObj(row) }, delRequest (row) { + d2CrudPlus.util.dict.clear() + this.$store.dispatch('d2admin/dept/load') return api.DelObj(row.id) }, // 授权 diff --git a/web/src/views/system/login/css/style.css b/web/src/views/system/login/css/style.css index 05aac86..4428c16 100644 --- a/web/src/views/system/login/css/style.css +++ b/web/src/views/system/login/css/style.css @@ -121,7 +121,7 @@ img { /*-- form styling --*/ .w3l-form-info { - padding-top: 6em; + padding-top: 2em; } .w3l-signinform{ padding: 40px 40px; @@ -241,8 +241,10 @@ img { .w3_info { padding: 1em 1em; background: transparent; - max-width: 450px; + width: 450px; display: grid; + position: fixed; + right: 12vw; margin-left: auto; } @@ -317,7 +319,7 @@ h5 { color: #000; } .footer { - padding-top: 3em; + padding-top: 1em; } .footer p { text-align: center; diff --git a/web/src/views/system/login/page.vue b/web/src/views/system/login/page.vue index c4bc6d5..aef8bd7 100644 --- a/web/src/views/system/login/page.vue +++ b/web/src/views/system/login/page.vue @@ -68,9 +68,10 @@ - + @@ -125,16 +126,25 @@