diff --git a/README.md b/README.md index a11b659..4640cc7 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ 👩‍👧‍👦演示地址:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com) -​ 账号:superadmin +- 账号:superadmin -​ 密码:superadmin123456 +- 密码:admin123456 👩‍👦‍👦文档地址:[https://django-vue-admin.com](https://django-vue-admin.com) @@ -130,7 +130,7 @@ npm run dev 8. 启动项目 python3 manage.py runserver 0.0.0.0:8000 或使用 daphne : - daphne -b 0.0.0.0 -8000 application.asgi:application + daphne -b 0.0.0.0 -p 8000 application.asgi:application ~~~ ### 访问项目 diff --git a/backend/application/settings.py b/backend/application/settings.py index f668410..8c60b21 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -28,7 +28,10 @@ from conf.env import * # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm' -sys.path.insert(0, os.path.join(BASE_DIR, 'plugins')) +# 初始化plugins插件路径到环境变量中 +PLUGINS_PATH = os.path.join(BASE_DIR, 'plugins') +[sys.path.insert(0, os.path.join(PLUGINS_PATH, ele)) for ele in os.listdir(PLUGINS_PATH) if + os.path.isdir(os.path.join(PLUGINS_PATH, ele)) and not ele.startswith('__')] # SECURITY WARNING: don't run with debug turned on in production! DEBUG = locals().get('DEBUG', True) diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py index c19f567..9c8def9 100644 --- a/backend/conf/env.example.py +++ b/backend/conf/env.example.py @@ -5,19 +5,24 @@ from application.settings import BASE_DIR # ================================================= # # ************** 数据库 配置 ************** # # ================================================= # -# + # 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库 +# sqlite3 设置 DATABASE_ENGINE = "django.db.backends.sqlite3" -# 数据库名 DATABASE_NAME = os.path.join(BASE_DIR, 'db.sqlite3') + +# 使用mysql时,改为此配置 +# DATABASE_ENGINE = "django.db.backends.mysql" +# DATABASE_NAME = 'django-vue-admin' # mysql 时使用 # 数据库地址 改为自己数据库地址 DATABASE_HOST = "127.0.0.1" -# 数据库端口 +# # 数据库端口 DATABASE_PORT = 3306 -# 数据库用户名 +# # 数据库用户名 DATABASE_USER = "root" -# 数据库密码 +# # 数据库密码 DATABASE_PASSWORD = "123456" + # ================================================= # # ************** redis配置,无redis 可不进行配置 ************** # # ================================================= # @@ -30,3 +35,4 @@ DATABASE_PASSWORD = "123456" DEBUG = True # 线上环境请设置为True ALLOWED_HOSTS = ["*"] LOGIN_NO_CAPTCHA_AUTH = True # 登录接口 /api/token/ 是否需要验证码认证,用于测试,正式环境建议取消 +ENABLE_LOGIN_ANALYSIS_LOG = True # 启动登录详细概略获取(通过调用api获取ip详细地址) diff --git a/backend/dvadmin/system/initialize.py b/backend/dvadmin/system/initialize.py index 20fa3fa..a6d2198 100644 --- a/backend/dvadmin/system/initialize.py +++ b/backend/dvadmin/system/initialize.py @@ -163,6 +163,17 @@ class Initialize(CoreInitialize): "name": "导出", "value": "Export", "creator_id": 1 + }, + { + "id": 9, + "description": None, + "modifier": "1", + "dept_belong_id": 1, + "update_datetime": datetime.datetime.now(), + "create_datetime": datetime.datetime.now(), + "name": "重置密码", + "value": "ResetPwd", + "creator_id": 1 } ] self.save(Button, self.button_data, "权限表标识") @@ -507,6 +518,27 @@ class Initialize(CoreInitialize): "creator_id": 1, "parent_id": 15, "is_catalog": 0 + }, + { + "id": 20, + "description": "", + "modifier": "1", + "dept_belong_id": 1, + "update_datetime": datetime.datetime.now(), + "create_datetime": datetime.datetime.now(), + "icon": "file-text", + "name": "登录日志", + "sort": 1, + "is_link": 0, + "web_path": "/loginLog", + "component": "system/log/loginLog/index", + "component_name": "loginLog", + "status": 1, + "cache": 0, + "visible": 1, + "creator_id": 1, + "parent_id": 15, + "is_catalog": 0 } ] self.save(Menu, self.menu_data, "菜单表") @@ -517,19 +549,19 @@ class Initialize(CoreInitialize): """ self.menu_button_data = [ { - "id": 1, - "description": None, - "modifier": "1", - "dept_belong_id": 1, - "update_datetime": datetime.datetime.now(), - "create_datetime": datetime.datetime.now(), - "name": "查询", - "value": "Search", - "api": "/api/system/menu/", - "method": 0, - "creator_id": 1, - "menu_id": 1 - }, + "id": 1, + "description": None, + "modifier": "1", + "dept_belong_id": 1, + "update_datetime": datetime.datetime.now(), + "create_datetime": datetime.datetime.now(), + "name": "查询", + "value": "Search", + "api": "/api/system/menu/", + "method": 0, + "creator_id": 1, + "menu_id": 1 + }, { "id": 2, "description": None, @@ -1243,6 +1275,47 @@ class Initialize(CoreInitialize): "method": 1, "creator_id": 1, "menu_id": 13 + }, + { + "id": 53, + "description": None, + "modifier": "1", + "dept_belong_id": 1, + "update_datetime": datetime.datetime.now(), + "create_datetime": datetime.datetime.now(), + "name": "重置密码", + "value": "ResetPwd", + "api": "/api/system/user/reset_password/{id}/", + "method": 2, + "creator_id": 1, + "menu_id": 3 + },{ + "id": 54, + "description": None, + "modifier": "1", + "dept_belong_id": "1", + "update_datetime": datetime.datetime.now(), + "create_datetime": datetime.datetime.now(), + "name": "查询", + "value": "Search", + "api": "/api/system/login_log/", + "method": 0, + "creator_id": 1, + "menu_id": 20 + }, + { + "id": 55, + "description": None, + "modifier": "1", + "dept_belong_id": "1", + "update_datetime": datetime.datetime.now(), + "create_datetime": datetime.datetime.now(), + "name": "详情", + "value": "Retrieve", + "api": "/api/system/login_log/{id}/", + "method": 0, + "creator_id": 1, + "menu_id": 20 } ] self.save(MenuButton, self.menu_button_data, "菜单按钮表") diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index 6d0fcda..798612e 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -29,7 +29,7 @@ class Users(AbstractUser, CoreModel): (1, "前台用户"), ) user_type = models.IntegerField(choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, - help_text="用户类型") + help_text="用户类型") post = models.ManyToManyField(to='Post', verbose_name='关联岗位', db_constraint=False, help_text="关联岗位") role = models.ManyToManyField(to='Role', verbose_name='关联角色', db_constraint=False, help_text="关联角色") dept = models.ForeignKey(to='Dept', verbose_name='所属部门', on_delete=models.PROTECT, db_constraint=False, null=True, @@ -206,7 +206,7 @@ class OperationLog(CoreModel): def media_file_name(instance, filename): h = instance.md5sum basename, ext = os.path.splitext(filename) - return os.path.join('media/files', h[0:1], h[1:2], h + ext.lower()) + return os.path.join('files', h[0:1], h[1:2], h + ext.lower()) class FileList(CoreModel): @@ -306,3 +306,32 @@ class SystemConfig(CoreModel): def __str__(self): return f"{self.title}" + + +class LoginLog(CoreModel): + LOGIN_TYPE_CHOICES = ( + (1, '普通登录'), + ) + username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录用户名") + ip = models.CharField(max_length=32, verbose_name="登录ip", null=True, blank=True, help_text="登录ip") + agent = models.TextField(verbose_name="agent信息", null=True, blank=True, help_text="agent信息") + browser = models.CharField(max_length=200, verbose_name="浏览器名", null=True, blank=True, help_text="浏览器名") + os = models.CharField(max_length=200, verbose_name="操作系统", null=True, blank=True, help_text="操作系统") + continent = models.CharField(max_length=50, verbose_name="州", null=True, blank=True, help_text="州") + country = models.CharField(max_length=50, verbose_name="国家", null=True, blank=True, help_text="国家") + province = models.CharField(max_length=50, verbose_name="省份", null=True, blank=True, help_text="省份") + city = models.CharField(max_length=50, verbose_name="城市", null=True, blank=True, help_text="城市") + district = models.CharField(max_length=50, verbose_name="县区", null=True, blank=True, help_text="县区") + isp = models.CharField(max_length=50, verbose_name="运营商", null=True, blank=True, help_text="运营商") + area_code = models.CharField(max_length=50, verbose_name="区域代码", null=True, blank=True, help_text="区域代码") + country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True, help_text="英文全称") + country_code = models.CharField(max_length=50, verbose_name="简称", null=True, blank=True, help_text="简称") + longitude = models.CharField(max_length=50, verbose_name="经度", null=True, blank=True, help_text="经度") + latitude = models.CharField(max_length=50, verbose_name="纬度", null=True, blank=True, help_text="纬度") + login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型", help_text="登录类型") + + class Meta: + db_table = table_prefix + 'system_login_log' + verbose_name = '登录日志' + verbose_name_plural = verbose_name + ordering = ('-create_datetime',) diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py index 2cacc87..5ed359d 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -15,6 +15,7 @@ from dvadmin.system.views.button import ButtonViewSet from dvadmin.system.views.dept import DeptViewSet from dvadmin.system.views.dictionary import DictionaryViewSet from dvadmin.system.views.file_list import FileViewSet +from dvadmin.system.views.login_log import LoginLogViewSet from dvadmin.system.views.menu import MenuViewSet from dvadmin.system.views.menu_button import MenuButtonViewSet from dvadmin.system.views.operation_log import OperationLogViewSet @@ -41,11 +42,14 @@ urlpatterns = [ path('menu/web_router/', MenuViewSet.as_view({'get': 'web_router'})), path('user/user_info/', UserViewSet.as_view({'get': 'user_info', 'put': 'update_user_info'})), path('user/change_password//', UserViewSet.as_view({'put': 'change_password'})), + path('user/reset_password//', UserViewSet.as_view({'put': 'reset_password'})), path('user/export/', UserViewSet.as_view({'post': 'export_data', })), - path('user/import/',UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})), + path('user/import/', UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})), path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})), path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})), path('system_config/get_table_data//', SystemConfigViewSet.as_view({'get': 'get_table_data'})), path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), + path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), + path('login_log//', LoginLogViewSet.as_view({'get': 'retrieve'})), ] urlpatterns += system_url.urls diff --git a/backend/dvadmin/system/views/dept.py b/backend/dvadmin/system/views/dept.py index 050d478..63d83a3 100644 --- a/backend/dvadmin/system/views/dept.py +++ b/backend/dvadmin/system/views/dept.py @@ -6,6 +6,7 @@ @Created on: 2021/6/3 003 0:30 @Remark: 角色管理 """ +from rest_framework import serializers from dvadmin.system.models import Dept from dvadmin.utils.json_response import SuccessResponse @@ -17,7 +18,7 @@ class DeptSerializer(CustomModelSerializer): """ 部门-序列化器 """ - + parent_name = serializers.CharField(read_only=True,source='parent.name') class Meta: model = Dept fields = "__all__" @@ -55,11 +56,11 @@ class DeptViewSet(CustomModelViewSet): update_serializer_class = DeptCreateUpdateSerializer # extra_filter_backends = [] - def list(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True, request=request) - return self.get_paginated_response(serializer.data) - serializer = self.get_serializer(queryset, many=True, request=request) - return SuccessResponse(data=serializer.data, msg="获取成功") + # def list(self, request, *args, **kwargs): + # queryset = self.filter_queryset(self.get_queryset()) + # page = self.paginate_queryset(queryset) + # if page is not None: + # serializer = self.get_serializer(page, many=True, request=request) + # return self.get_paginated_response(serializer.data) + # serializer = self.get_serializer(queryset, many=True, request=request) + # return SuccessResponse(data=serializer.data, msg="获取成功") diff --git a/backend/dvadmin/system/views/file_list.py b/backend/dvadmin/system/views/file_list.py index e9f8b07..87c4355 100644 --- a/backend/dvadmin/system/views/file_list.py +++ b/backend/dvadmin/system/views/file_list.py @@ -17,7 +17,7 @@ class FileSerializer(CustomModelSerializer): url = serializers.SerializerMethodField(read_only=True) def get_url(self, instance): - return str(instance.url) + return 'media/'+str(instance.url) class Meta: model = FileList @@ -41,3 +41,4 @@ class FileViewSet(CustomModelViewSet): queryset = FileList.objects.all() serializer_class = FileSerializer filter_fields = ['name', ] + permission_classes = [] diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 41f0a88..90bf163 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -25,6 +25,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView from application import settings from dvadmin.system.models import Users from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse +from dvadmin.utils.request_util import save_login_log from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.validator import CustomValidationError @@ -56,7 +57,7 @@ class LoginSerializer(TokenObtainPairSerializer): 登录的序列化器: 重写djangorestframework-simplejwt的序列化器 """ - captcha = serializers.CharField(max_length=6) + captcha = serializers.CharField(max_length=6, required=False, allow_null=True) class Meta: model = Users @@ -67,24 +68,31 @@ class LoginSerializer(TokenObtainPairSerializer): 'no_active_account': _('账号/密码不正确') } - def validate_captcha(self, captcha): - self.image_code = CaptchaStore.objects.filter( - id=self.initial_data['captchaKey']).first() - five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) - if self.image_code and five_minute_ago > self.image_code.expiration: - self.image_code and self.image_code.delete() - raise CustomValidationError('验证码过期') - else: - if self.image_code and (self.image_code.response == captcha or self.image_code.challenge == captcha): - self.image_code and self.image_code.delete() - else: - self.image_code and self.image_code.delete() - raise CustomValidationError("图片验证码错误") - def validate(self, attrs): + captcha = self.initial_data.get('captcha', None) + if settings.CAPTCHA_STATE: + if captcha is None: + raise CustomValidationError("验证码不能为空") + self.image_code = CaptchaStore.objects.filter( + id=self.initial_data['captchaKey']).first() + five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) + if self.image_code and five_minute_ago > self.image_code.expiration: + self.image_code and self.image_code.delete() + raise CustomValidationError('验证码过期') + else: + if self.image_code and (self.image_code.response == captcha or self.image_code.challenge == captcha): + self.image_code and self.image_code.delete() + else: + self.image_code and self.image_code.delete() + raise CustomValidationError("图片验证码错误") data = super().validate(attrs) data['name'] = self.user.name data['userId'] = self.user.id + data['avatar'] = self.user.avatar + request = self.context.get('request') + request.user = self.user + # 记录登录日志 + save_login_log(request=request) return { "code": 2000, "msg": "请求成功", diff --git a/backend/dvadmin/system/views/login_log.py b/backend/dvadmin/system/views/login_log.py new file mode 100644 index 0000000..4dc3617 --- /dev/null +++ b/backend/dvadmin/system/views/login_log.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +""" +@author: 猿小天 +@contact: QQ:1638245306 +@Created on: 2021/6/3 003 0:30 +@Remark: 按钮权限管理 +""" +from dvadmin.system.models import LoginLog +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.utils.viewset import CustomModelViewSet + + +class LoginLogSerializer(CustomModelSerializer): + """ + 登录日志权限-序列化器 + """ + + class Meta: + model = LoginLog + fields = "__all__" + read_only_fields = ["id"] + + +class LoginLogViewSet(CustomModelViewSet): + """ + 登录日志接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + """ + queryset = LoginLog.objects.all() + serializer_class = LoginLogSerializer + extra_filter_backends = [] diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index ae13ea0..36cb938 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -11,6 +11,7 @@ import hashlib from django.contrib.auth.hashers import make_password from rest_framework import serializers from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated from dvadmin.system.models import Users from dvadmin.utils.json_response import ErrorResponse, DetailResponse @@ -147,7 +148,7 @@ class UserViewSet(CustomModelViewSet): 'gender': '用户性别(男/女/未知)', 'is_active': '帐号状态(启用/禁用)', 'password': '登录密码', 'dept': '部门ID', 'role': '角色ID'} - @action(methods=['GET'], detail=True, permission_classes=[]) + @action(methods=['GET'], detail=True, permission_classes=[IsAuthenticated]) def user_info(self, request): """获取当前用户信息""" user = request.user @@ -155,18 +156,19 @@ class UserViewSet(CustomModelViewSet): "name": user.name, "mobile": user.mobile, "gender": user.gender, - "email": user.email + "email": user.email, + 'avatar':user.avatar } return DetailResponse(data=result, msg="获取成功") - @action(methods=['PUT'], detail=True, permission_classes=[]) + @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) def update_user_info(self, request): """修改当前用户信息""" user = request.user Users.objects.filter(id=user.id).update(**request.data) return DetailResponse(data=None, msg="修改成功") - @action(methods=['PUT'], detail=True, permission_classes=[]) + @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) def change_password(self, request, *args, **kwargs): """密码修改""" instance = Users.objects.filter(id=kwargs.get('pk')).first() @@ -185,3 +187,22 @@ class UserViewSet(CustomModelViewSet): return ErrorResponse(msg="旧密码不正确") else: return ErrorResponse(msg="未获取到用户") + + @action(methods=['PUT'], detail=True) + def reset_password(self, request, pk): + """ + 密码重置 + """ + instance = Users.objects.filter(id=pk).first() + data = request.data + new_pwd = data.get('newPassword') + new_pwd2 = data.get('newPassword2') + if instance: + if new_pwd != new_pwd2: + return ErrorResponse(msg="两次密码不匹配") + else: + instance.password = make_password(new_pwd) + instance.save() + return DetailResponse(data=None, msg="修改成功") + else: + return ErrorResponse(msg="未获取到用户") diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index 1f5a43b..67c179b 100644 --- a/backend/dvadmin/utils/filters.py +++ b/backend/dvadmin/utils/filters.py @@ -36,7 +36,7 @@ def get_dept(dept_id: int, dept_all_list=None, dept_list=None): if dept_list is None: dept_list = [dept_id] for ele in dept_all_list: - if ele.get('parentId') == dept_id: + if ele.get('parent') == dept_id: dept_list.append(ele.get('id')) get_dept(ele.get('id'), dept_all_list, dept_list) return list(set(dept_list)) @@ -94,10 +94,15 @@ class DataLevelPermissionsFilter(BaseFilterBackend): return queryset.filter(dept_belong_id=user_dept_id) # 3. 根据所有角色 获取所有权限范围 + # (0, "仅本人数据权限"), + # (1, "本部门及以下数据权限"), + # (2, "本部门数据权限"), + # (3, "全部数据权限"), + # (4, "自定数据权限") role_list = request.user.role.filter(status=1).values('admin', 'data_range') - dataScope_list = [] + dataScope_list = [] # 权限范围列表 for ele in role_list: - # 3.1 判断用户是否为超级管理员角色/如果有1(所有数据) 则返回所有数据 + # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据 if 3 == ele.get('data_range') or ele.get('admin') == True: return queryset dataScope_list.append(ele.get('data_range')) diff --git a/backend/dvadmin/utils/request_util.py b/backend/dvadmin/utils/request_util.py index 87fa3ff..14414fb 100644 --- a/backend/dvadmin/utils/request_util.py +++ b/backend/dvadmin/utils/request_util.py @@ -3,12 +3,16 @@ Request工具类 """ import json +import requests +from django.conf import settings from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AnonymousUser from django.urls.resolvers import ResolverMatch from rest_framework_simplejwt.authentication import JWTAuthentication from user_agents import parse +from dvadmin.system.models import LoginLog + def get_request_user(request): """ @@ -163,3 +167,50 @@ def get_verbose_name(queryset=None, view=None, model=None): except Exception as e: pass return model if model else "" + + +def get_ip_analysis(ip): + """ + 获取ip详细概略 + :param ip: ip地址 + :return: + """ + data = { + "continent": "", + "country": "", + "province": "", + "city": "", + "district": "", + "isp": "", + "area_code": "", + "country_english": "", + "country_code": "", + "longitude": "", + "latitude": "" + } + if ip != 'unknown' and ip: + if getattr(settings, 'ENABLE_LOGIN_ANALYSIS_LOG', True): + res = requests.post(url='https://ip.django-vue-admin.com/ip/analysis', data=json.dumps({"ip": ip})) + if res.status_code == 200: + res_data = res.json() + if res_data.get('code') == 0: + data = res_data.get('data') + return data + return data + + +def save_login_log(request): + """ + 保存登录日志 + :return: + """ + ip = get_request_ip(request=request) + analysis_data = get_ip_analysis(ip) + analysis_data['username'] = request.user.username + analysis_data['ip'] = ip + analysis_data['agent'] = str(parse(request.META['HTTP_USER_AGENT'])) + analysis_data['browser'] = get_browser(request) + analysis_data['os'] = get_os(request) + analysis_data['creator_id'] = request.user.id + analysis_data['dept_belong_id'] = getattr(request.user, 'dept_id', '') + LoginLog.objects.create(**analysis_data) diff --git a/backend/dvadmin/utils/serializers.py b/backend/dvadmin/utils/serializers.py index 8ab54ff..c6af51c 100644 --- a/backend/dvadmin/utils/serializers.py +++ b/backend/dvadmin/utils/serializers.py @@ -68,7 +68,7 @@ class CustomModelSerializer(DynamicFieldsMixin,ModelSerializer): def update(self, instance, validated_data): if self.request: if hasattr(self.instance, self.modifier_field_id): - self.instance.modifier = self.get_request_username() + setattr(self.instance, self.modifier_field_id, self.get_request_user_id()) return super().update(instance, validated_data) def get_request_username(self): diff --git a/docker_env/nginx/my.conf b/docker_env/nginx/my.conf index c6a4fb4..8ab724f 100644 --- a/docker_env/nginx/my.conf +++ b/docker_env/nginx/my.conf @@ -7,6 +7,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; + set_real_ip_from 177.7.0.0/16; + real_ip_header X-Forwarded-For; root /usr/share/nginx/html; index index.html index.php index.htm; } @@ -16,6 +18,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + set_real_ip_from 177.7.0.0/16; + real_ip_header X-Forwarded-For; rewrite ^/api/(.*)$ /$1 break; #重写 proxy_pass http://177.7.0.12:8000/; # 设置代理服务器的协议和地址 } diff --git a/web/dependencies-cdn.js b/web/dependencies-cdn.js index 1fc5eb1..427c252 100644 --- a/web/dependencies-cdn.js +++ b/web/dependencies-cdn.js @@ -1,22 +1,22 @@ module.exports = [ - { name: 'vue', library: 'Vue', js: 'https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js', css: '' }, - { name: 'vue-i18n', library: 'VueI18n', js: 'https://cdn.jsdelivr.net/npm/vue-i18n@8.15.1/dist/vue-i18n.min.js', css: '' }, - { name: 'vue-router', library: 'VueRouter', js: 'https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js', css: '' }, - { name: 'vuex', library: 'Vuex', js: 'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js', css: '' }, - { name: 'axios', library: 'axios', js: 'https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js', css: '' }, - { name: 'better-scroll', library: 'BScroll', js: 'https://cdn.jsdelivr.net/npm/better-scroll@1.15.2/dist/bscroll.min.js', css: '' }, - { name: 'axios-mock-adapter', library: 'AxiosMockAdapter', js: 'https://cdn.jsdelivr.net/npm/axios-mock-adapter@1.18.1/dist/axios-mock-adapter.min.js', css: '' }, - { name: 'element-ui', library: 'ELEMENT', js: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/index.js', css: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/theme-chalk/index.css' }, - { name: 'lodash', library: '_', js: 'https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js', css: '' }, - { name: 'ua-parser-js', library: 'UAParser', js: 'https://cdn.jsdelivr.net/npm/ua-parser-js@0.7.20/dist/ua-parser.min.js', css: '' }, - { name: 'js-cookie', library: 'Cookies', js: 'https://cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js', css: '' }, - { name: 'nprogress', library: 'NProgress', js: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', css: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css' }, - { name: 'dayjs', library: 'dayjs', js: 'https://cdn.jsdelivr.net/npm/dayjs@1.8.17/dayjs.min.js', css: '' }, - { name: 'fuse.js', library: 'Fuse', js: 'https://cdn.jsdelivr.net/npm/fuse.js@5.2.3/dist/fuse.min.js', css: '' }, - { name: 'hotkeys-js', library: 'hotkeys', js: 'https://cdn.jsdelivr.net/npm/hotkeys-js@3.7.3/dist/hotkeys.min.js', css: '' }, - { name: 'qs', library: 'Qs', js: 'https://cdn.jsdelivr.net/npm/qs@6.9.1/dist/qs.js', css: '' }, - { name: 'lowdb', library: 'low', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/low.min.js', css: '' }, - { name: 'lowdb/adapters/LocalStorage', library: 'LocalStorage', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/LocalStorage.min.js', css: '' }, - { name: 'screenfull', library: 'screenfull', js: 'https://cdn.jsdelivr.net/npm/screenfull@5.0.2/dist/screenfull.min.js', css: '' }, - { name: 'sortablejs', library: 'Sortable', js: 'https://cdn.jsdelivr.net/npm/sortablejs@1.10.1/Sortable.min.js', css: '' } + // { name: 'vue', library: 'Vue', js: 'https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js', css: '' }, + // { name: 'vue-i18n', library: 'VueI18n', js: 'https://cdn.jsdelivr.net/npm/vue-i18n@8.15.1/dist/vue-i18n.min.js', css: '' }, + // { name: 'vue-router', library: 'VueRouter', js: 'https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js', css: '' }, + // { name: 'vuex', library: 'Vuex', js: 'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js', css: '' }, + // { name: 'axios', library: 'axios', js: 'https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js', css: '' }, + // { name: 'better-scroll', library: 'BScroll', js: 'https://cdn.jsdelivr.net/npm/better-scroll@1.15.2/dist/bscroll.min.js', css: '' }, + // { name: 'axios-mock-adapter', library: 'AxiosMockAdapter', js: 'https://cdn.jsdelivr.net/npm/axios-mock-adapter@1.18.1/dist/axios-mock-adapter.min.js', css: '' }, + // { name: 'element-ui', library: 'ELEMENT', js: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/index.js', css: 'https://cdn.jsdelivr.net/npm/element-ui@2.15.5/lib/theme-chalk/index.css' }, + // { name: 'lodash', library: '_', js: 'https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js', css: '' }, + // { name: 'ua-parser-js', library: 'UAParser', js: 'https://cdn.jsdelivr.net/npm/ua-parser-js@0.7.20/dist/ua-parser.min.js', css: '' }, + // { name: 'js-cookie', library: 'Cookies', js: 'https://cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js', css: '' }, + // { name: 'nprogress', library: 'NProgress', js: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', css: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css' }, + // { name: 'dayjs', library: 'dayjs', js: 'https://cdn.jsdelivr.net/npm/dayjs@1.8.17/dayjs.min.js', css: '' }, + // { name: 'fuse.js', library: 'Fuse', js: 'https://cdn.jsdelivr.net/npm/fuse.js@5.2.3/dist/fuse.min.js', css: '' }, + // { name: 'hotkeys-js', library: 'hotkeys', js: 'https://cdn.jsdelivr.net/npm/hotkeys-js@3.7.3/dist/hotkeys.min.js', css: '' }, + // { name: 'qs', library: 'Qs', js: 'https://cdn.jsdelivr.net/npm/qs@6.9.1/dist/qs.js', css: '' }, + // { name: 'lowdb', library: 'low', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/low.min.js', css: '' }, + // { name: 'lowdb/adapters/LocalStorage', library: 'LocalStorage', js: 'https://cdn.jsdelivr.net/npm/lowdb@1.0.0/dist/LocalStorage.min.js', css: '' }, + // { name: 'screenfull', library: 'screenfull', js: 'https://cdn.jsdelivr.net/npm/screenfull@5.0.2/dist/screenfull.min.js', css: '' }, + // { name: 'sortablejs', library: 'Sortable', js: 'https://cdn.jsdelivr.net/npm/sortablejs@1.10.1/Sortable.min.js', css: '' } ] diff --git a/web/public/image/theme/chester/logo/all.png b/web/public/image/theme/chester/logo/all.png index bb858c3..4d67b0c 100644 Binary files a/web/public/image/theme/chester/logo/all.png and b/web/public/image/theme/chester/logo/all.png differ diff --git a/web/public/image/theme/chester/logo/icon-only.png b/web/public/image/theme/chester/logo/icon-only.png index 13ec4e9..ccc7bd6 100644 Binary files a/web/public/image/theme/chester/logo/icon-only.png and b/web/public/image/theme/chester/logo/icon-only.png differ diff --git a/web/public/image/theme/chester/preview@2x.png b/web/public/image/theme/chester/preview@2x.png deleted file mode 100644 index 7841526..0000000 Binary files a/web/public/image/theme/chester/preview@2x.png and /dev/null differ diff --git a/web/public/image/theme/element/logo/all.png b/web/public/image/theme/element/logo/all.png index d4c4800..e046e04 100644 Binary files a/web/public/image/theme/element/logo/all.png and b/web/public/image/theme/element/logo/all.png differ diff --git a/web/public/image/theme/element/logo/icon-only.png b/web/public/image/theme/element/logo/icon-only.png index f7cb7e4..fdec20b 100644 Binary files a/web/public/image/theme/element/logo/icon-only.png and b/web/public/image/theme/element/logo/icon-only.png differ diff --git a/web/public/image/theme/element/preview@2x.png b/web/public/image/theme/element/preview@2x.png deleted file mode 100644 index 301d606..0000000 Binary files a/web/public/image/theme/element/preview@2x.png and /dev/null differ diff --git a/web/public/image/theme/line/logo/all.png b/web/public/image/theme/line/logo/all.png index 7e16f70..33a2134 100644 Binary files a/web/public/image/theme/line/logo/all.png and b/web/public/image/theme/line/logo/all.png differ diff --git a/web/public/image/theme/line/logo/icon-only.png b/web/public/image/theme/line/logo/icon-only.png index 76af9ce..8098f80 100644 Binary files a/web/public/image/theme/line/logo/icon-only.png and b/web/public/image/theme/line/logo/icon-only.png differ diff --git a/web/public/image/theme/line/preview@2x.png b/web/public/image/theme/line/preview@2x.png deleted file mode 100644 index 502af90..0000000 Binary files a/web/public/image/theme/line/preview@2x.png and /dev/null differ diff --git a/web/public/image/theme/star/logo/all.png b/web/public/image/theme/star/logo/all.png index 453548c..2134b36 100644 Binary files a/web/public/image/theme/star/logo/all.png and b/web/public/image/theme/star/logo/all.png differ diff --git a/web/public/image/theme/star/logo/icon-only.png b/web/public/image/theme/star/logo/icon-only.png index 54a45a4..2bf15c7 100644 Binary files a/web/public/image/theme/star/logo/icon-only.png and b/web/public/image/theme/star/logo/icon-only.png differ diff --git a/web/public/image/theme/star/preview@2x.png b/web/public/image/theme/star/preview@2x.png deleted file mode 100644 index d58a4c8..0000000 Binary files a/web/public/image/theme/star/preview@2x.png and /dev/null differ diff --git a/web/public/image/theme/tomorrow-night-blue/logo/all.png b/web/public/image/theme/tomorrow-night-blue/logo/all.png index bb6d266..051b471 100644 Binary files a/web/public/image/theme/tomorrow-night-blue/logo/all.png and b/web/public/image/theme/tomorrow-night-blue/logo/all.png differ diff --git a/web/public/image/theme/tomorrow-night-blue/logo/icon-only.png b/web/public/image/theme/tomorrow-night-blue/logo/icon-only.png index 94ef44d..7a0db87 100644 Binary files a/web/public/image/theme/tomorrow-night-blue/logo/icon-only.png and b/web/public/image/theme/tomorrow-night-blue/logo/icon-only.png differ diff --git a/web/public/image/theme/tomorrow-night-blue/preview@2x.png b/web/public/image/theme/tomorrow-night-blue/preview@2x.png deleted file mode 100644 index 95cddb4..0000000 Binary files a/web/public/image/theme/tomorrow-night-blue/preview@2x.png and /dev/null differ diff --git a/web/public/image/theme/violet/logo/all.png b/web/public/image/theme/violet/logo/all.png index 453548c..d9511cf 100644 Binary files a/web/public/image/theme/violet/logo/all.png and b/web/public/image/theme/violet/logo/all.png differ diff --git a/web/public/image/theme/violet/logo/icon-only.png b/web/public/image/theme/violet/logo/icon-only.png index 54a45a4..d7806e8 100644 Binary files a/web/public/image/theme/violet/logo/icon-only.png and b/web/public/image/theme/violet/logo/icon-only.png differ diff --git a/web/public/image/theme/violet/preview@2x.png b/web/public/image/theme/violet/preview@2x.png deleted file mode 100644 index f63f20f..0000000 Binary files a/web/public/image/theme/violet/preview@2x.png and /dev/null differ diff --git a/web/src/install.js b/web/src/install.js index 47642e3..b3e7c80 100644 --- a/web/src/install.js +++ b/web/src/install.js @@ -254,7 +254,7 @@ Vue.prototype.commonEndColumns = function (param = {}) { search: { disabled: true }, - type: 'cascader', + type: 'table-selector', dict: { cache: true, url: '/api/system/dept/?limit=999&status=1', @@ -262,9 +262,14 @@ Vue.prototype.commonEndColumns = function (param = {}) { value: 'id', // 数据字典中value字段的属性名 label: 'name', // 数据字典中label字段的属性名 children: 'children', // 数据字典中children字段的属性名 - getData: (url, dict) => { // 配置此参数会覆盖全局的getRemoteDictFunc - return request({ url: url }).then(ret => { - return [{ id: null, name: '根节点', children: XEUtils.toArrayTree(ret.data.data, { parentKey: 'parent', strict: true }) }] + getData: (url, dict, { + _, + component + }) => { + return request({ + url: url, + }).then(ret => { + return ret.data.data }) } }, @@ -273,13 +278,27 @@ Vue.prototype.commonEndColumns = function (param = {}) { component: { props: { elProps: { - clearable: true, - showAllLevels: false, // 仅显示最后一级 - props: { - checkStrictly: true, // 可以不需要选到最后一级 - emitPath: false, - clearable: true - } + treeConfig: { + transform: true, + rowField: 'id', + parentField: 'parent', + expandAll: true + }, + columns: [ + { + field: 'name', + title: '部门名称', + treeNode: true + }, + { + field: 'status', + title: '状态' + }, + { + field: 'parent_name', + title: '父级部门' + } + ] } } }, @@ -289,21 +308,6 @@ Vue.prototype.commonEndColumns = function (param = {}) { ) } } - }, - component: { - dict: { - cache: true, - url: deptPrefix + '?limit=999&status=1', - isTree: true, - value: 'id', // 数据字典中value字段的属性名 - label: 'name', // 数据字典中label字段的属性名 - children: 'children', // 数据字典中children字段的属性名 - getData: (url, dict) => { // 配置此参数会覆盖全局的getRemoteDictFunc - return request({ url: url }).then(ret => { - return [{ id: null, name: '根节点', children: XEUtils.toArrayTree(ret.data.data, { parentKey: 'parent', strict: true }) }] - }) - } - } } }, { diff --git a/web/src/layout/header-aside/components/header-user/index.vue b/web/src/layout/header-aside/components/header-user/index.vue index fd7a566..d60058d 100644 --- a/web/src/layout/header-aside/components/header-user/index.vue +++ b/web/src/layout/header-aside/components/header-user/index.vue @@ -20,6 +20,7 @@ 注销 + diff --git a/web/src/layout/header-aside/components/header-user/userinfo.vue b/web/src/layout/header-aside/components/header-user/userinfo.vue index e1cf799..67c8dff 100644 --- a/web/src/layout/header-aside/components/header-user/userinfo.vue +++ b/web/src/layout/header-aside/components/header-user/userinfo.vue @@ -20,6 +20,21 @@ :label-position="position" center > + + + + + + + @@ -130,6 +145,11 @@ export default { return { position: 'left', activeName: 'userInfo', + action: util.baseURL() + 'api/system/file/', + headers: { + Authorization: 'JWT ' + util.cookies.get('token') + }, + fileList:[], userInfo: { name: '', gender: '', @@ -177,6 +197,7 @@ export default { params: {} }).then((res) => { _self.userInfo = res.data + _self.fileList = [{name:'avatar.png',url:res.data.avatar}] }) }, /** @@ -251,6 +272,16 @@ export default { this.$message.error('表单校验失败,请检查') } }) + }, + /** + * 头像上传 + * @param res + * @param file + */ + handleAvatarSuccess(res, file) { + console.log(11,res) + this.fileList =[{ url: util.baseURL() + res.data.url, name:file.name }] + this.userInfo.avatar = util.baseURL() + res.data.url; } } } diff --git a/web/src/router/index.js b/web/src/router/index.js index ff3c84c..ba1761a 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -41,7 +41,7 @@ const router = new VueRouter({ */ router.beforeEach(async (to, from, next) => { // 白名单 - const whiteList = ['/login', '/auth-redirect', '/bind', '/register'] + const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/oauth2'] // 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201 await store.dispatch('d2admin/page/isLoaded') // 确认已经加载组件尺寸设置 https://github.com/d2-projects/d2-admin/issues/198 diff --git a/web/src/router/routes.js b/web/src/router/routes.js index 78dfcb2..7d9b192 100644 --- a/web/src/router/routes.js +++ b/web/src/router/routes.js @@ -1,8 +1,8 @@ import layoutHeaderAside from '@/layout/header-aside' - +import { checkPlugins } from '@/views/plugins/index.js' // 由于懒加载页面太多的话会造成webpack热更新太慢,所以开发环境不使用懒加载,只有生产环境使用懒加载 const _import = require('@/libs/util.import.' + process.env.NODE_ENV) - +const pluginImport = require('@/libs/util.import.plugin') /** * 在主框架内显示 */ @@ -88,25 +88,25 @@ const frameIn = [{ // component: _import('system/user') // }, // // 系统 按钮配置 - { - path: 'button', - name: 'button', - meta: { - title: '按钮', - auth: true - }, - component: _import('system/button') - }, - // // 系统 菜单权限 - { - path: 'menuButton/:id', - name: 'menuButton', - meta: { - title: '菜单按钮', - auth: true - }, - component: _import('system/menuButton') - }, + // { + // path: 'button', + // name: 'button', + // meta: { + // title: '按钮', + // auth: true + // }, + // component: _import('system/button') + // }, + // // // 系统 菜单权限 + // { + // path: 'menuButton/:id', + // name: 'menuButton', + // meta: { + // title: '菜单按钮', + // auth: true + // }, + // component: _import('system/menuButton') + // }, // // 系统 角色管理 // { // path: 'role', @@ -149,15 +149,15 @@ const frameIn = [{ // component: _import('system/log/operationLog') // }, // 系统 前端日志 - { - path: 'frontendLog', - name: 'frontendLog', - meta: { - title: '前端日志', - auth: true - }, - component: _import('system/log/frontendLog') - }, + // { + // path: 'frontendLog', + // name: 'frontendLog', + // meta: { + // title: '前端日志', + // auth: true + // }, + // component: _import('system/log/frontendLog') + // }, // 刷新页面 必须保留 { path: 'refresh', @@ -186,7 +186,17 @@ const frameOut = [ component: _import('system/login') } ] - +/** + * 第三方登录 + */ +const pluginsType = checkPlugins('dvadmin-oauth2-web') +if (pluginsType) { + frameOut.push({ + path: '/oauth2', + name: 'login', + component: pluginsType === 'local' ? _import('plugins/dvadmin-oauth2-web/src/login/index') : pluginImport('dvadmin-oauth2-web/src/login/index') + }) +} /** * 错误页面 */ diff --git a/web/src/setting.js b/web/src/setting.js index 0da2759..7c9296f 100644 --- a/web/src/setting.js +++ b/web/src/setting.js @@ -36,39 +36,39 @@ export default { title: 'd2admin 经典', name: 'd2', preview: 'image/theme/d2/preview@2x.png' + }, + { + title: 'Chester', + name: 'chester', + preview: 'image/theme/chester/preview@2x.jpg' + }, + { + title: 'Element', + name: 'element', + preview: 'image/theme/element/preview@2x.jpg' + }, + { + title: '紫罗兰', + name: 'violet', + preview: 'image/theme/violet/preview@2x.jpg' + }, + { + title: '简约线条', + name: 'line', + backgroundImage: 'image/theme/line/bg.jpg', + preview: 'image/theme/line/preview@2x.jpg' + }, + { + title: '流星', + name: 'star', + backgroundImage: 'image/theme/star/bg.jpg', + preview: 'image/theme/star/preview@2x.jpg' + }, + { + title: 'Tomorrow Night Blue (vsCode)', + name: 'tomorrow-night-blue', + preview: 'image/theme/tomorrow-night-blue/preview@2x.jpg' } - // { - // title: 'Chester', - // name: 'chester', - // preview: 'image/theme/chester/preview@2x.png' - // }, - // { - // title: 'Element', - // name: 'element', - // preview: 'image/theme/element/preview@2x.png' - // }, - // { - // title: '紫罗兰', - // name: 'violet', - // preview: 'image/theme/violet/preview@2x.png' - // }, - // { - // title: '简约线条', - // name: 'line', - // backgroundImage: 'image/theme/line/bg.jpg', - // preview: 'image/theme/line/preview@2x.png' - // }, - // { - // title: '流星', - // name: 'star', - // backgroundImage: 'image/theme/star/bg.jpg', - // preview: 'image/theme/star/preview@2x.png' - // }, - // { - // title: 'Tomorrow Night Blue (vsCode)', - // name: 'tomorrow-night-blue', - // preview: 'image/theme/tomorrow-night-blue/preview@2x.png' - // } ] }, // 是否默认开启页面切换动画 diff --git a/web/src/store/modules/d2admin/modules/account.js b/web/src/store/modules/d2admin/modules/account.js index 2a24872..ce612c4 100644 --- a/web/src/store/modules/d2admin/modules/account.js +++ b/web/src/store/modules/d2admin/modules/account.js @@ -18,22 +18,20 @@ export default { /** * @description 登录 * @param {Object} context - * @param {Object} payload username {String} 用户账号 - * @param {Object} payload password {String} 密码 - * @param {Object} payload route {Object} 登录成功后定向的路由对象 任何 vue-router 支持的格式 - */ - async login ({ dispatch }, { - username = '', - password = '', - captcha = '', - captchaKey = '' - } = {}) { - let res = await SYS_USER_LOGIN({ - username, - password, - captcha, - captchaKey - }) + * @param {Object} data + * @param {Object} data username {String} 用户账号 + * @param {Object} data password {String} 密码 + * @param {Object} data route {Object} 登录成功后定向的路由对象 任何 vue-router 支持的格式 + * @param {Object} data request function 请求方法 + */ + async login ({ dispatch }, data) { + let request = data.request + if (request) { + delete data.request + } else { + request = SYS_USER_LOGIN + } + let res = await request(data) // 设置 cookie 一定要存 uuid 和 token 两个 cookie // 整个系统依赖这两个数据进行校验和存储 // uuid 是用户身份唯一标识 用户注册的时候确定 并且不可改变 不可重复 @@ -44,7 +42,7 @@ 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 }, { root: true }) + await dispatch('d2admin/user/set', { name: res.name, user_id: res.userId,avatar:res.avatar }, { root: true }) // 用户登录后从持久化数据加载一系列的设置 await dispatch('load') }, diff --git a/web/src/store/modules/d2admin/modules/releases.js b/web/src/store/modules/d2admin/modules/releases.js index d4eb17d..6013da8 100644 --- a/web/src/store/modules/d2admin/modules/releases.js +++ b/web/src/store/modules/d2admin/modules/releases.js @@ -21,6 +21,7 @@ export default { console.log('演示地址:https://demo.django-vue-admin.com') console.log('社区地址:https://bbs.django-vue-admin.com') console.log('文档地址:https://www.django-vue-admin.com') + console.log('前端配置文档地址:https://d2.pub/zh/doc/d2-crud-v2') console.log('请不要吝啬您的 star,谢谢 ~') } } diff --git a/web/src/views/plugins/index.js b/web/src/views/plugins/index.js index ee67b16..48e5b28 100644 --- a/web/src/views/plugins/index.js +++ b/web/src/views/plugins/index.js @@ -1,3 +1,5 @@ +import Vue from 'vue' + function importAll (r) { const __modules = [] r.keys().forEach(key => { @@ -8,10 +10,42 @@ function importAll (r) { return __modules } +export const checkPlugins = function install (pluginName) { + let pluginsList + pluginsList = importAll(require.context('./', true, /index\.js$/)) + if (pluginsList && pluginsList.indexOf(pluginName) !== -1) { + try { + const Module = import('@/views/plugins/' + pluginName + '/src/index') + // 注册组件 + if (Module.default) { + Vue.use(Module.default) + } + // 本地插件 + return 'local' + } catch (exception) {} + } + pluginsList = importAll(require.context('@great-dream/', true, /index\.js$/)) + if (pluginsList && pluginsList.indexOf(pluginName) !== -1) { + // node_modules 封装插件 + try { + const Module = import('@great-dream/' + pluginName + '/src/index') + // 注册组件 + if (Module.default) { + Vue.use(Module.default) + } + // 本地插件 + return 'plugins' + } catch (exception) {} + } + // 未找到插件 + return undefined +} + export const plugins = async function install (Vue, options) { // 查找 src/views/plugins 目录所有插件,插件目录下需有 index.js 文件 // 再查找 node_modules/@great-dream/ 目录下所有插件 // 进行去重并vue注册导入 + if (window.pluginsAll) return let components = [] components = components.concat(importAll(require.context('./', true, /index\.js$/))) components = components.concat(importAll(require.context('@great-dream/', true, /index\.js$/))) diff --git a/web/src/views/system/fileList/crud.js b/web/src/views/system/fileList/crud.js index 6060ce1..d987be9 100644 --- a/web/src/views/system/fileList/crud.js +++ b/web/src/views/system/fileList/crud.js @@ -78,8 +78,12 @@ export const crudOptions = (vm) => { disabled: false }, width: 160, - type: 'input' - + type: 'input', + form: { + component: { + placeholder: '请输入文件名称' + } + } }, { title: '文件地址', diff --git a/web/src/views/system/log/loginLog/api.js b/web/src/views/system/log/loginLog/api.js new file mode 100644 index 0000000..337a05f --- /dev/null +++ b/web/src/views/system/log/loginLog/api.js @@ -0,0 +1,19 @@ +/* + * @创建文件时间: 2021-06-08 10:40:32 + * @Auther: 猿小天 + * @最后修改人: 猿小天 + * @最后修改时间: 2021-06-09 10:36:20 + * 联系Qq:1638245306 + * @文件介绍: 操作日志 + */ +import { request } from '@/api/service' + +export const urlPrefix = '/api/system/login_log/' + +export function GetList (query) { + return request({ + url: urlPrefix, + method: 'get', + params: query + }) +} diff --git a/web/src/views/system/log/loginLog/crud.js b/web/src/views/system/log/loginLog/crud.js new file mode 100644 index 0000000..d7352c8 --- /dev/null +++ b/web/src/views/system/log/loginLog/crud.js @@ -0,0 +1,293 @@ +import { BUTTON_WHETHER_BOOL } from '@/config/button' + +export const crudOptions = (vm) => { + return { + pageOptions: { + compact: true + }, + options: { + tableType: 'vxe-table', + rowKey: true, // 必须设置,true or false + rowId: 'id', + height: '100%', // 表格高度100%, 使用toolbar必须设置 + highlightCurrentRow: false + + }, + rowHandle: { + fixed: 'right', + view: { + thin: true, + text: '', + disabled () { + return !vm.hasPermissions('Retrieve') + } + }, + width: 70, + edit: { + thin: true, + text: '', + show: false, + disabled () { + return !vm.hasPermissions('Update') + } + }, + remove: { + thin: true, + text: '删除', + show: false, + disabled () { + return !vm.hasPermissions('Delete') + } + } + }, + viewOptions: { + componentType: 'form' + }, + formOptions: { + disabled: true, + defaultSpan: 12 // 默认的表单 span + }, + indexRow: { // 或者直接传true,不显示title,不居中 + title: '序号', + align: 'center', + width: 70 + }, + columns: [ + { + title: '关键词', + key: 'search', + show: false, + disabled: true, + search: { + disabled: false + }, + form: { + show: false, + component: { + placeholder: '请输入关键词' + } + } + }, + { + title: 'ID', + key: 'id', + width: 90, + disabled: true, + form: { + disabled: true + } + }, + { + title: '登录用户名', + key: 'username', + search: { + disabled: false + }, + width: 140, + type: 'input', + form: { + disabled: true, + component: { + placeholder: '请输入登录用户名' + } + } + }, + { + title: '登录ip', + key: 'ip', + search: { + disabled: false + }, + width: 130, + type: 'input', + form: { + disabled: true, + component: { + placeholder: '请输入登录ip' + } + } + }, { + title: '运营商', + key: 'isp', + search: { + disabled: true + }, + disabled: true, + width: 180, + type: 'input', + form: { + component: { + placeholder: '请输入操作系统' + } + } + }, { + title: '大州', + key: 'continent', + width: 80, + type: 'input', + form: { + disabled: true, + component: { + placeholder: '请输入州' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '国家', + key: 'country', + width: 80, + type: 'input', + form: { + component: { + placeholder: '请输入国家' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '省份', + key: 'province', + width: 80, + type: 'input', + form: { + component: { + placeholder: '请输入省份' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '城市', + key: 'city', + width: 80, + type: 'input', + form: { + component: { + placeholder: '请输入城市' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '县区', + key: 'district', + width: 80, + type: 'input', + form: { + component: { + placeholder: '请输入县区' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '区域代码', + key: 'area_code', + width: 100, + type: 'input', + form: { + component: { + placeholder: '请输入区域代码' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '英文全称', + key: 'country_english', + width: 120, + type: 'input', + form: { + component: { + placeholder: '请输入英文全称' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '简称', + key: 'country_code', + width: 100, + type: 'input', + form: { + component: { + placeholder: '请输入简称' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '经度', + key: 'longitude', + width: 80, + type: 'input', + disabled: true, + form: { + component: { + placeholder: '请输入经度' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '纬度', + key: 'latitude', + width: 80, + type: 'input', + disabled: true, + form: { + component: { + placeholder: '请输入纬度' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '登录类型', + key: 'login_type', + width: 100, + type: 'select', + search: { + disabled: false + }, + dict: { + data: [{ label: '普通登录', value: 1 }] + }, + form: { + component: { + placeholder: '请选择登录类型' + } + }, + component: { props: { color: 'auto' } } // 自动染色 + }, { + title: '操作系统', + key: 'os', + width: 180, + type: 'input', + form: { + component: { + placeholder: '请输入操作系统' + } + } + }, { + title: '浏览器名', + key: 'browser', + width: 180, + type: 'input', + form: { + component: { + placeholder: '请输入操作系统' + } + } + }, { + title: 'agent信息', + key: 'agent', + disabled: true, + width: 180, + type: 'input', + form: { + component: { + placeholder: '请输入操作系统' + } + } + }, { + fixed: 'right', + title: '登录时间', + key: 'create_datetime', + width: 160, + type: 'datetime' + } + ] + } +} diff --git a/web/src/views/system/log/loginLog/index.vue b/web/src/views/system/log/loginLog/index.vue new file mode 100644 index 0000000..6610bd2 --- /dev/null +++ b/web/src/views/system/log/loginLog/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/web/src/views/system/log/operationLog/crud.js b/web/src/views/system/log/operationLog/crud.js index 5969cff..9ed7946 100644 --- a/web/src/views/system/log/operationLog/crud.js +++ b/web/src/views/system/log/operationLog/crud.js @@ -12,14 +12,15 @@ export const crudOptions = (vm) => { }, rowHandle: { + fixed: 'right', view: { thin: true, - text: '详情', + text: '', disabled () { return !vm.hasPermissions('Retrieve') } }, - width: 160, + width: 70, edit: { thin: true, text: '', @@ -42,7 +43,12 @@ export const crudOptions = (vm) => { }, formOptions: { disabled: true, - defaultSpan: 24 // 默认的表单 span + defaultSpan: 12 // 默认的表单 span + }, + indexRow: { // 或者直接传true,不显示title,不居中 + title: '序号', + align: 'center', + width: 70 }, columns: [ { @@ -106,11 +112,16 @@ export const crudOptions = (vm) => { disabled: true }, disabled: true, - width: 180, - type: 'input', + type: 'textarea', form: { disabled: true, component: { + props: { + type: 'textarea' + }, + autosize: { + minRows: 2, maxRows: 8 + }, placeholder: '请输入关键词' } } @@ -147,7 +158,7 @@ export const crudOptions = (vm) => { search: { disabled: false }, - width: 100, + width: 130, type: 'input', form: { disabled: true, @@ -199,6 +210,7 @@ export const crudOptions = (vm) => { search: { disabled: true }, + minWidth: 240, type: 'input', form: { disabled: true @@ -223,7 +235,8 @@ export const crudOptions = (vm) => { } }, { - title: '创建时间', + fixed: 'right', + title: '操作时间', key: 'create_datetime', width: 160, type: 'datetime', diff --git a/web/src/views/system/log/operationLog/index.vue b/web/src/views/system/log/operationLog/index.vue index 6d88eab..dac12e5 100644 --- a/web/src/views/system/log/operationLog/index.vue +++ b/web/src/views/system/log/operationLog/index.vue @@ -57,11 +57,9 @@ export default { return api.GetList(query) }, addRequest (row) { - console.log('api', api) return api.AddObj(row) }, updateRequest (row) { - console.log('----', row) return api.UpdateObj(row) }, delRequest (row) { diff --git a/web/src/views/system/role/crud.js b/web/src/views/system/role/crud.js index a025a51..bbf1912 100644 --- a/web/src/views/system/role/crud.js +++ b/web/src/views/system/role/crud.js @@ -164,7 +164,7 @@ export const crudOptions = (vm) => { data: BUTTON_WHETHER_BOOL }, form: { - value: 0, + value: false, component: { placeholder: '请选择是否管理员' } @@ -183,7 +183,7 @@ export const crudOptions = (vm) => { data: BUTTON_STATUS_BOOL }, form: { - value: 1, + value: true, component: { placeholder: '请选择状态' } diff --git a/web/src/views/system/role/index.vue b/web/src/views/system/role/index.vue index 56f6e6a..8ed34d7 100644 --- a/web/src/views/system/role/index.vue +++ b/web/src/views/system/role/index.vue @@ -48,7 +48,7 @@ >
diff --git a/web/src/views/system/user/api.js b/web/src/views/system/user/api.js index 4034b53..9b89613 100644 --- a/web/src/views/system/user/api.js +++ b/web/src/views/system/user/api.js @@ -6,7 +6,7 @@ * 联系Qq:1638245306 * @文件介绍: 用户接口 */ -import { request, downloadFile } from '@/api/service' +import { request } from '@/api/service' export const urlPrefix = '/api/system/user/' @@ -43,13 +43,15 @@ export function DelObj (id) { } /** - * 导出 - * @param params + * 重置密码 + * @param id + * @returns {*} + * @constructor */ -export function exportData (params) { - return downloadFile({ - url: urlPrefix + 'export/', - params: params, - method: 'post' +export function ResetPwd (obj) { + return request({ + url: urlPrefix + 'reset_password/' + obj.id + '/', + method: 'put', + data: obj }) } diff --git a/web/src/views/system/user/crud.js b/web/src/views/system/user/crud.js index 84a9a16..ec101c7 100644 --- a/web/src/views/system/user/crud.js +++ b/web/src/views/system/user/crud.js @@ -2,7 +2,8 @@ import { request } from '@/api/service' import { BUTTON_STATUS_BOOL } from '@/config/button' import { urlPrefix as deptPrefix } from '../dept/api' import util from '@/libs/util' -const uploadUrl = util.baseURL() + 'api/system/img/' + +const uploadUrl = util.baseURL() + 'api/system/file/' export const crudOptions = (vm) => { return { pageOptions: { @@ -12,8 +13,8 @@ export const crudOptions = (vm) => { height: '100%' }, rowHandle: { - width: 140, fixed: 'right', + width: 180, view: { thin: true, text: '', @@ -34,7 +35,21 @@ export const crudOptions = (vm) => { disabled () { return !vm.hasPermissions('Delete') } - } + }, + custom: [ + { + thin: true, + text: '', + size: 'small', + type: 'warning', + icon: 'el-icon-refresh-left', + show () { + return vm.hasPermissions('ResetPwd') + }, + emit: 'resetPwd' + } + ] + }, viewOptions: { componentType: 'form' @@ -45,7 +60,7 @@ export const crudOptions = (vm) => { indexRow: { // 或者直接传true,不显示title,不居中 title: '序号', align: 'center', - width: 80 + width: 70 }, columns: [ { @@ -81,7 +96,7 @@ export const crudOptions = (vm) => { search: { disabled: false }, - width: 160, + width: 140, type: 'input', form: { rules: [ // 表单校验规则 @@ -95,7 +110,7 @@ export const crudOptions = (vm) => { }, helper: { render (h) { - return (< el-alert title="密码默认为:admin123456" type="warning" /> + return (< el-alert title="密码默认为:admin123456" type="warning"/> ) } } @@ -105,7 +120,6 @@ export const crudOptions = (vm) => { title: '姓名', key: 'name', search: { - key: 'name__icontains', disabled: false }, type: 'input', @@ -124,18 +138,18 @@ export const crudOptions = (vm) => { }, { title: '部门', - width: 160, key: 'dept', search: { disabled: true }, + minWidth: 140, type: 'table-selector', dict: { cache: false, url: deptPrefix, value: 'id', // 数据字典中value字段的属性名 label: 'name', // 数据字典中label字段的属性名 - getData: (url, dict, { _, component }) => { + getData: (url, dict, { form, component }) => { return request({ url: url, params: { page: 1, limit: 10, status: 1 } }).then(ret => { component._elProps.page = ret.data.page component._elProps.limit = ret.data.limit @@ -173,13 +187,14 @@ export const crudOptions = (vm) => { } } } - }, { + }, + { title: '手机号码', key: 'mobile', - width: 120, search: { disabled: true }, + minWidth: 110, type: 'input', form: { rules: [ @@ -196,7 +211,7 @@ export const crudOptions = (vm) => { }, { title: '邮箱', key: 'email', - width: 120, + minWidth: 160, form: { rules: [ { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] } @@ -209,49 +224,26 @@ export const crudOptions = (vm) => { { title: '性别', key: 'gender', - type: 'select', + type: 'radio', + width: 70, dict: { data: [{ label: '男', value: 1 }, { label: '女', value: 0 }] }, form: { value: 1, - rules: [ - { required: true, message: '性别必填项' } - ], component: { span: 12 - }, - itemProps: { - class: { yxtInput: true } } }, component: { props: { color: 'auto' } } // 自动染色 }, - { - title: '用户类型', - key: 'user_type', - type: 'select', - width: 120, - search: { - key: 'user_type', - value: 0, - disabled: false - }, - dict: { - data: [{ label: '前台用户', value: 1 }, { label: '后台用户', value: 0 }] - }, - form: { - disabled: true - }, - component: { props: { color: 'auto' } } // 自动染色 - }, { title: '状态', key: 'is_active', search: { disabled: false }, - width: 90, + width: 70, type: 'radio', dict: { data: BUTTON_STATUS_BOOL @@ -267,23 +259,22 @@ export const crudOptions = (vm) => { title: '头像', key: 'avatar', type: 'avatar-uploader', - width: 80, + width: 100, align: 'left', form: { component: { props: { uploader: { action: uploadUrl, - name: 'url', headers: { Authorization: 'JWT ' + util.cookies.get('token') }, type: 'form', successHandle (ret, option) { - if (ret.data == null || ret.data === '') { + if (ret.data === null || ret.data === '') { throw new Error('上传失败') } - return { url: ret.data.data.url, key: option.data.key } + return { url: util.baseURL() + ret.data.url, key: option.data.key } } }, elProps: { // 与el-uploader 配置一致 @@ -309,9 +300,8 @@ export const crudOptions = (vm) => { component: { props: { buildUrl (value, item) { - console.log(11, value) if (value && value.indexOf('http') !== 0) { - return '/api/upload/form/download?key=' + value + return util.baseURL() + value } return value } @@ -321,10 +311,10 @@ export const crudOptions = (vm) => { { title: '角色', key: 'role', - width: 160, search: { disabled: true }, + minWidth: 130, type: 'table-selector', dict: { cache: false, @@ -370,6 +360,6 @@ export const crudOptions = (vm) => { } } } - ].concat(vm.commonEndColumns({ update_datetime: { showTable: false } })) + ].concat(vm.commonEndColumns({ update_datetime: { showForm: false, showTable: false }, create_datetime: { showForm: false, showTable: true } })) } } diff --git a/web/src/views/system/user/index.vue b/web/src/views/system/user/index.vue index ffc7d04..b8d787f 100644 --- a/web/src/views/system/user/index.vue +++ b/web/src/views/system/user/index.vue @@ -1,11 +1,3 @@ - @@ -60,10 +55,42 @@ import { d2CrudPlus } from 'd2-crud-plus' export default { name: 'user', - mixins: [d2CrudPlus.crud], data () { - return {} + var validatePass = (rule, value, callback) => { + const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}') + if (value === '') { + callback(new Error('请输入密码')) + } else if (!pwdRegex.test(value)) { + callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)')) + } else { + if (this.resetPwdForm.pwd2 !== '') { + this.$refs.resetPwdForm.validateField('pwd2') + } + callback() + } + } + var validatePass2 = (rule, value, callback) => { + if (value === '') { + callback(new Error('请再次输入密码')) + } else if (value !== this.resetPwdForm.pwd) { + callback(new Error('两次输入密码不一致!')) + } else { + callback() + } + } + return { + dialogFormVisible: false, + resetPwdForm: { + id: null, + pwd: null, + pwd2: null + }, + passwordRules: { + pwd: [{ required: true, message: '必填项' }, { validator: validatePass, trigger: 'blur' }], + pwd2: [{ required: true, message: '必填项' }, { validator: validatePass2, trigger: 'blur' }] + } + } }, methods: { getCrudOptions () { @@ -76,19 +103,38 @@ export default { return api.AddObj(row) }, updateRequest (row) { - console.log('----', row) return api.UpdateObj(row) }, delRequest (row) { return api.DelObj(row.id) }, - onExport () { - this.$confirm('是否确认导出所有数据项?', '警告', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning' - }).then(function () { - return api.exportData() + // 重置密码弹框 + resetPwd ({ row }) { + this.dialogFormVisible = true + this.resetPwdForm.id = row.id + }, + // 重置密码确认 + resetPwdSubmit () { + const that = this + that.$refs.resetPwdForm.validate((valid) => { + if (valid) { + const params = { + id: that.resetPwdForm.id, + newPassword: that.$md5(that.resetPwdForm.pwd), + newPassword2: that.$md5(that.resetPwdForm.pwd2) + } + api.ResetPwd(params).then(res => { + that.dialogFormVisible = false + that.resetPwdForm = { + id: null, + pwd: null, + pwd2: null + } + that.$message.success('修改成功') + }) + } else { + that.$message.error('表单校验失败,请检查') + } }) } } diff --git a/web/src/views/system/whiteList/crud.js b/web/src/views/system/whiteList/crud.js index 302f932..c3abac7 100644 --- a/web/src/views/system/whiteList/crud.js +++ b/web/src/views/system/whiteList/crud.js @@ -45,7 +45,10 @@ export const crudOptions = (vm) => { disabled: false }, form: { - disabled: true + disabled: true, + component: { + placeholder: '请输入关键词' + } }, view: { // 查看对话框组件的单独配置 disabled: true