!49 v2.0.1 版本发布

Merge pull request !49 from dvadmin/v2.x
pull/50/MERGE v2.0.1
dvadmin 2022-04-19 16:49:59 +00:00 committed by Gitee
commit 89f156cb90
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
55 changed files with 1022 additions and 285 deletions

View File

@ -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
~~~
### 访问项目

View File

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

View File

@ -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详细地址)

View File

@ -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, "菜单表")
@ -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, "菜单按钮表")

View File

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

View File

@ -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/<int:pk>/', UserViewSet.as_view({'put': 'change_password'})),
path('user/reset_password/<int:pk>/', 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/<int:pk>/', 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/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
]
urlpatterns += system_url.urls

View File

@ -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="获取成功")

View File

@ -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 = []

View File

@ -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,7 +68,11 @@ class LoginSerializer(TokenObtainPairSerializer):
'no_active_account': _('账号/密码不正确')
}
def validate_captcha(self, captcha):
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)
@ -80,11 +85,14 @@ class LoginSerializer(TokenObtainPairSerializer):
else:
self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误")
def validate(self, attrs):
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": "请求成功",

View File

@ -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 = []

View File

@ -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="未获取到用户")

View File

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

View File

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

View File

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

View File

@ -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/; # 设置代理服务器的协议和地址
}

View File

@ -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: '' }
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

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

View File

@ -20,6 +20,7 @@
注销
</el-dropdown-item>
</el-dropdown-menu>
<el-image v-if="info.avatar" :src="info.avatar" :preview-src-list="[info.avatar]" style="width: 20px;height: 20px" alt="头像"></el-image>
</el-dropdown>
</template>

View File

@ -20,6 +20,21 @@
:label-position="position"
center
>
<el-form-item prop="avatar" required label="头像">
<el-upload
class="avatar-uploader"
list-type="picture-card"
:file-list="fileList"
:action="action"
:headers="headers"
:limit="1"
:disabled="fileList.length===1"
:on-success="handleAvatarSuccess">
<!-- <el-image v-if="userInfo.avatar" :src="userInfo.avatar" :preview-src-list="[userInfo.avatar]" style="width: 100px;height: 100px" alt="头像"></el-image>-->
<!-- <i v-else class="el-icon-plus avatar-uploader-icon" style="width: 100px;height: 100px"></i>-->
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
<el-form-item prop="name" required label="昵称">
<el-input v-model="userInfo.name" clearable></el-input>
</el-form-item>
@ -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;
}
}
}

View File

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

View File

@ -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')
})
}
/**
* 错误页面
*/

View File

@ -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'
// }
]
},
// 是否默认开启页面切换动画

View File

@ -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 支持的格式
* @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 }, {
username = '',
password = '',
captcha = '',
captchaKey = ''
} = {}) {
let res = await SYS_USER_LOGIN({
username,
password,
captcha,
captchaKey
})
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')
},

View File

@ -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谢谢 ~')
}
}

View File

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

View File

@ -78,8 +78,12 @@ export const crudOptions = (vm) => {
disabled: false
},
width: 160,
type: 'input'
type: 'input',
form: {
component: {
placeholder: '请输入文件名称'
}
}
},
{
title: '文件地址',

View File

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

View File

@ -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'
}
]
}
}

View File

@ -0,0 +1,48 @@
<template>
<d2-container :class="{ 'page-compact': crud.pageOptions.compact }">
<d2-crud-x
ref="d2Crud"
v-bind="_crudProps"
v-on="_crudListeners"
crud.options.tableType="vxe-table"
>
<div slot="header">
<crud-search
ref="search"
:options="crud.searchOptions"
@submit="handleSearch"
/>
<crud-toolbar
:search.sync="crud.searchOptions.show"
:compact.sync="crud.pageOptions.compact"
:columns="crud.columns"
@refresh="doRefresh()"
@columns-filter-changed="handleColumnsFilterChanged"
/>
</div>
</d2-crud-x>
</d2-container>
</template>
<script>
import * as api from './api'
import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus'
export default {
name: 'loginLog',
mixins: [d2CrudPlus.crud],
data () {
return {}
},
methods: {
getCrudOptions () {
return crudOptions(this)
},
pageRequest (query) {
return api.GetList(query)
}
}
}
</script>

View File

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

View File

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

View File

@ -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: '请选择状态'
}

View File

@ -48,7 +48,7 @@
>
<template slot="title">
<div>
当前角色<el-tag>管理员</el-tag>
当前角色<el-tag>{{roleObj?roleObj.name:'无'}}</el-tag>
</div>
</template>
<div>

View File

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

View File

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

View File

@ -1,11 +1,3 @@
<!--
* @创建文件时间: 2021-06-01 22:41:21
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-07-29 19:27:10
* 联系Qq:1638245306
* @文件介绍: 用户管理
-->
<template>
<d2-container :class="{ 'page-compact': crud.pageOptions.compact }">
<d2-crud-x
@ -13,6 +5,7 @@
v-bind="_crudProps"
v-on="_crudListeners"
crud.options.tableType="vxe-table"
@resetPwd="resetPwd"
>
<div slot="header">
<crud-search
@ -26,20 +19,8 @@
v-permission="'Create'"
type="primary"
@click="addRow"
><i class="el-icon-plus"/> 新增
</el-button
><i class="el-icon-plus" /> 新增</el-button
>
<el-button
size="small"
type="danger"
@click="onExport"
v-permission="'Export'"
><i class="el-icon-download"/> 导出
</el-button>
<importExcel
importApi="/api/system/user/import/"
v-permission="'Import'">导入
</importExcel>
</el-button-group>
<crud-toolbar
:search.sync="crud.searchOptions.show"
@ -50,6 +31,20 @@
/>
</div>
</d2-crud-x>
<el-dialog title="密码重置" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="resetPwdForm" ref="resetPwdForm" :rules="passwordRules">
<el-form-item label="密码" prop="pwd">
<el-input v-model="resetPwdForm.pwd" type="password" show-password clearable autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="再次输入密码" prop="pwd2">
<el-input v-model="resetPwdForm.pwd2" type="password" show-password clearable autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> </el-button>
<el-button type="primary" @click="resetPwdSubmit"> </el-button>
</div>
</el-dialog>
</d2-container>
</template>
@ -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('表单校验失败,请检查')
}
})
}
}

View File

@ -45,7 +45,10 @@ export const crudOptions = (vm) => {
disabled: false
},
form: {
disabled: true
disabled: true,
component: {
placeholder: '请输入关键词'
}
},
view: { // 查看对话框组件的单独配置
disabled: true