Merge remote-tracking branch 'upstream/develop'
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
from django.core.cache import cache
|
||||||
|
from dvadmin.utils.validator import CustomValidationError
|
||||||
|
|
||||||
|
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
|
||||||
|
|
||||||
|
|
||||||
def is_tenants_mode():
|
def is_tenants_mode():
|
||||||
@@ -68,6 +72,9 @@ def init_dictionary():
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
cache.set(f"init_dictionary", _get_all_dictionary())
|
||||||
|
return
|
||||||
if is_tenants_mode():
|
if is_tenants_mode():
|
||||||
from django_tenants.utils import tenant_context, get_tenant_model
|
from django_tenants.utils import tenant_context, get_tenant_model
|
||||||
|
|
||||||
@@ -88,7 +95,9 @@ def init_system_config():
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
cache.set(f"init_system_config", _get_all_system_config())
|
||||||
|
return
|
||||||
if is_tenants_mode():
|
if is_tenants_mode():
|
||||||
from django_tenants.utils import tenant_context, get_tenant_model
|
from django_tenants.utils import tenant_context, get_tenant_model
|
||||||
|
|
||||||
@@ -107,6 +116,9 @@ def refresh_dictionary():
|
|||||||
刷新字典配置
|
刷新字典配置
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
cache.set(f"init_dictionary", _get_all_dictionary())
|
||||||
|
return
|
||||||
if is_tenants_mode():
|
if is_tenants_mode():
|
||||||
from django_tenants.utils import tenant_context, get_tenant_model
|
from django_tenants.utils import tenant_context, get_tenant_model
|
||||||
|
|
||||||
@@ -122,6 +134,9 @@ def refresh_system_config():
|
|||||||
刷新系统配置
|
刷新系统配置
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
cache.set(f"init_system_config", _get_all_system_config())
|
||||||
|
return
|
||||||
if is_tenants_mode():
|
if is_tenants_mode():
|
||||||
from django_tenants.utils import tenant_context, get_tenant_model
|
from django_tenants.utils import tenant_context, get_tenant_model
|
||||||
|
|
||||||
@@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None):
|
|||||||
:param schema_name: 对应字典配置的租户schema_name值
|
:param schema_name: 对应字典配置的租户schema_name值
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
init_dictionary_data = cache.get(f"init_dictionary")
|
||||||
|
if not init_dictionary_data:
|
||||||
|
refresh_dictionary()
|
||||||
|
return cache.get(f"init_dictionary") or {}
|
||||||
if not settings.DICTIONARY_CONFIG:
|
if not settings.DICTIONARY_CONFIG:
|
||||||
refresh_dictionary()
|
refresh_dictionary()
|
||||||
if is_tenants_mode():
|
if is_tenants_mode():
|
||||||
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
|
|||||||
:param schema_name: 对应字典配置的租户schema_name值
|
:param schema_name: 对应字典配置的租户schema_name值
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
dictionary_config = cache.get(f"init_dictionary")
|
||||||
|
if not dictionary_config:
|
||||||
|
refresh_dictionary()
|
||||||
|
dictionary_config = cache.get(f"init_dictionary")
|
||||||
|
return dictionary_config.get(key)
|
||||||
dictionary_config = get_dictionary_config(schema_name)
|
dictionary_config = get_dictionary_config(schema_name)
|
||||||
return dictionary_config.get(key)
|
return dictionary_config.get(key)
|
||||||
|
|
||||||
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
|
|||||||
:param schema_name: 对应字典配置的租户schema_name值
|
:param schema_name: 对应字典配置的租户schema_name值
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
children = get_dictionary_values(key, schema_name) or []
|
res = get_dictionary_values(key, schema_name) or []
|
||||||
for ele in children:
|
for ele in res.get('children'):
|
||||||
if ele.get("value") == str(name):
|
if ele.get("value") == str(name):
|
||||||
return ele.get("label")
|
return ele.get("label")
|
||||||
return ""
|
return ""
|
||||||
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
|
|||||||
:param schema_name: 对应字典配置的租户schema_name值
|
:param schema_name: 对应字典配置的租户schema_name值
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
init_dictionary_data = cache.get(f"init_system_config")
|
||||||
|
if not init_dictionary_data:
|
||||||
|
refresh_system_config()
|
||||||
|
return cache.get(f"init_system_config") or {}
|
||||||
if not settings.SYSTEM_CONFIG:
|
if not settings.SYSTEM_CONFIG:
|
||||||
refresh_system_config()
|
refresh_system_config()
|
||||||
if is_tenants_mode():
|
if is_tenants_mode():
|
||||||
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
|
|||||||
:param schema_name: 对应系统配置的租户schema_name值
|
:param schema_name: 对应系统配置的租户schema_name值
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if dispatch_db_type == 'redis':
|
||||||
|
system_config = cache.get(f"init_system_config")
|
||||||
|
if not system_config:
|
||||||
|
refresh_system_config()
|
||||||
|
system_config = cache.get(f"init_system_config")
|
||||||
|
return system_config.get(key)
|
||||||
system_config = get_system_config(schema_name)
|
system_config = get_system_config(schema_name)
|
||||||
return system_config.get(key)
|
return system_config.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_config_values_to_dict(key, schema_name=None):
|
||||||
|
"""
|
||||||
|
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
|
||||||
|
:param key: 对应系统配置的key值(字典编号)
|
||||||
|
:param schema_name: 对应系统配置的租户schema_name值
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
values_dict = {}
|
||||||
|
config_values = get_system_config_values(key, schema_name)
|
||||||
|
if not isinstance(config_values, list):
|
||||||
|
raise CustomValidationError("该方式仅限于数组类型系统配置")
|
||||||
|
for ele in get_system_config_values(key, schema_name):
|
||||||
|
values_dict[ele.get('key')] = ele.get('value')
|
||||||
|
return values_dict
|
||||||
|
|
||||||
|
|
||||||
def get_system_config_label(key, name, schema_name=None):
|
def get_system_config_label(key, name, schema_name=None):
|
||||||
"""
|
"""
|
||||||
获取获取系统配置label值
|
获取获取系统配置label值
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class Users(CoreModel, AbstractUser):
|
|||||||
help_text="关联部门",
|
help_text="关联部门",
|
||||||
)
|
)
|
||||||
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
|
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
|
||||||
|
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
|
||||||
objects = CustomUserManager()
|
objects = CustomUserManager()
|
||||||
|
|
||||||
def set_password(self, raw_password):
|
def set_password(self, raw_password):
|
||||||
@@ -407,6 +408,18 @@ class FileList(CoreModel):
|
|||||||
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
|
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
|
||||||
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
|
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
|
||||||
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
|
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
|
||||||
|
UPLOAD_METHOD_CHOIDES = (
|
||||||
|
(0, '默认上传'),
|
||||||
|
(1, '文件选择器上传'),
|
||||||
|
)
|
||||||
|
upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式')
|
||||||
|
FILE_TYPE_CHOIDES = (
|
||||||
|
(0, '图片'),
|
||||||
|
(1, '视频'),
|
||||||
|
(2, '音频'),
|
||||||
|
(3, '其他'),
|
||||||
|
)
|
||||||
|
file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型')
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.md5sum: # file is new
|
if not self.md5sum: # file is new
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import connection
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from application import dispatch
|
from application import dispatch
|
||||||
from dvadmin.system.models import FileList
|
from dvadmin.system.models import FileList
|
||||||
from dvadmin.utils.json_response import DetailResponse
|
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||||
from dvadmin.utils.serializers import CustomModelSerializer
|
from dvadmin.utils.serializers import CustomModelSerializer
|
||||||
from dvadmin.utils.viewset import CustomModelViewSet
|
from dvadmin.utils.viewset import CustomModelViewSet
|
||||||
|
|
||||||
@@ -15,8 +18,21 @@ class FileSerializer(CustomModelSerializer):
|
|||||||
url = serializers.SerializerMethodField(read_only=True)
|
url = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
def get_url(self, instance):
|
def get_url(self, instance):
|
||||||
base_url = f"{self.request.scheme}://{self.request.get_host()}/"
|
if self.request.query_params.get('prefix'):
|
||||||
return base_url + (instance.file_url or (f'media/{str(instance.url)}'))
|
if settings.ENVIRONMENT in ['local']:
|
||||||
|
prefix = 'http://127.0.0.1:8000'
|
||||||
|
elif settings.ENVIRONMENT in ['test']:
|
||||||
|
prefix = 'http://{host}/api'.format(host=self.request.get_host())
|
||||||
|
else:
|
||||||
|
prefix = 'https://{host}/api'.format(host=self.request.get_host())
|
||||||
|
if instance.file_url:
|
||||||
|
return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}"
|
||||||
|
return (f'{prefix}/media/{str(instance.url)}')
|
||||||
|
return instance.file_url or (f'media/{str(instance.url)}')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FileList
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FileList
|
model = FileList
|
||||||
@@ -35,6 +51,8 @@ class FileSerializer(CustomModelSerializer):
|
|||||||
validated_data['md5sum'] = md5.hexdigest()
|
validated_data['md5sum'] = md5.hexdigest()
|
||||||
validated_data['engine'] = file_engine
|
validated_data['engine'] = file_engine
|
||||||
validated_data['mime_type'] = file.content_type
|
validated_data['mime_type'] = file.content_type
|
||||||
|
ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None)
|
||||||
|
validated_data['file_type'] = 3 if ft is None else ft
|
||||||
if file_backup:
|
if file_backup:
|
||||||
validated_data['url'] = file
|
validated_data['url'] = file
|
||||||
if file_engine == 'oss':
|
if file_engine == 'oss':
|
||||||
@@ -64,6 +82,22 @@ class FileSerializer(CustomModelSerializer):
|
|||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class FileAllSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FileList
|
||||||
|
fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class FileFilter(django_filters.FilterSet):
|
||||||
|
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名")
|
||||||
|
mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FileList
|
||||||
|
fields = ['name', 'mime_type', 'upload_method', 'file_type']
|
||||||
|
|
||||||
|
|
||||||
class FileViewSet(CustomModelViewSet):
|
class FileViewSet(CustomModelViewSet):
|
||||||
"""
|
"""
|
||||||
文件管理接口
|
文件管理接口
|
||||||
@@ -75,5 +109,22 @@ class FileViewSet(CustomModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = FileList.objects.all()
|
queryset = FileList.objects.all()
|
||||||
serializer_class = FileSerializer
|
serializer_class = FileSerializer
|
||||||
filter_fields = ['name', ]
|
filter_class = FileFilter
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
|
|
||||||
|
@action(methods=['GET'], detail=False)
|
||||||
|
def get_all(self, request):
|
||||||
|
data1 = self.get_serializer(self.get_queryset(), many=True).data
|
||||||
|
data2 = []
|
||||||
|
if dispatch.is_tenants_mode():
|
||||||
|
from django_tenants.utils import schema_context
|
||||||
|
with schema_context('public'):
|
||||||
|
data2 = self.get_serializer(FileList.objects.all(), many=True).data
|
||||||
|
return DetailResponse(data=data2+data1)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode():
|
||||||
|
from django_tenants.utils import schema_context
|
||||||
|
with schema_context('public'):
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ from datetime import datetime, timedelta
|
|||||||
from captcha.views import CaptchaStore, captcha_image
|
from captcha.views import CaptchaStore, captcha_image
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||||
@@ -97,16 +100,17 @@ class LoginSerializer(TokenObtainPairSerializer):
|
|||||||
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
|
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
|
||||||
attrs['username'] = user.username
|
attrs['username'] = user.username
|
||||||
data = super().validate(attrs)
|
data = super().validate(attrs)
|
||||||
|
data["username"] = self.user.username
|
||||||
data["name"] = self.user.name
|
data["name"] = self.user.name
|
||||||
data["userId"] = self.user.id
|
data["userId"] = self.user.id
|
||||||
data["avatar"] = self.user.avatar
|
data["avatar"] = self.user.avatar
|
||||||
data['user_type'] = self.user.user_type
|
data['user_type'] = self.user.user_type
|
||||||
|
data['pwd_change_count'] = self.user.pwd_change_count
|
||||||
dept = getattr(self.user, 'dept', None)
|
dept = getattr(self.user, 'dept', None)
|
||||||
if dept:
|
if dept:
|
||||||
data['dept_info'] = {
|
data['dept_info'] = {
|
||||||
'dept_id': dept.id,
|
'dept_id': dept.id,
|
||||||
'dept_name': dept.name,
|
'dept_name': dept.name,
|
||||||
|
|
||||||
}
|
}
|
||||||
role = getattr(self.user, 'role', None)
|
role = getattr(self.user, 'role', None)
|
||||||
if role:
|
if role:
|
||||||
|
|||||||
@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
|
|||||||
"""用于前端获取当前角色的路由"""
|
"""用于前端获取当前角色的路由"""
|
||||||
user = request.user
|
user = request.user
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
queryset = self.queryset.filter(status=1).order_by("id")
|
queryset = self.queryset.filter(status=1).order_by("sort")
|
||||||
else:
|
else:
|
||||||
role_list = user.role.values_list('id', flat=True)
|
role_list = user.role.values_list('id', flat=True)
|
||||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
|
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
|
||||||
queryset = Menu.objects.filter(id__in=menu_list).order_by("id")
|
queryset = Menu.objects.filter(id__in=menu_list).order_by("sort")
|
||||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ class UserViewSet(CustomModelViewSet):
|
|||||||
"dept": user.dept_id,
|
"dept": user.dept_id,
|
||||||
"is_superuser": user.is_superuser,
|
"is_superuser": user.is_superuser,
|
||||||
"role": user.role.values_list('id', flat=True),
|
"role": user.role.values_list('id', flat=True),
|
||||||
|
"pwd_change_count":user.pwd_change_count
|
||||||
}
|
}
|
||||||
if hasattr(connection, 'tenant'):
|
if hasattr(connection, 'tenant'):
|
||||||
result['tenant_id'] = connection.tenant and connection.tenant.id
|
result['tenant_id'] = connection.tenant and connection.tenant.id
|
||||||
@@ -319,7 +320,6 @@ class UserViewSet(CustomModelViewSet):
|
|||||||
"""密码修改"""
|
"""密码修改"""
|
||||||
data = request.data
|
data = request.data
|
||||||
old_pwd = data.get("oldPassword")
|
old_pwd = data.get("oldPassword")
|
||||||
print(old_pwd)
|
|
||||||
new_pwd = data.get("newPassword")
|
new_pwd = data.get("newPassword")
|
||||||
new_pwd2 = data.get("newPassword2")
|
new_pwd2 = data.get("newPassword2")
|
||||||
if old_pwd is None or new_pwd is None or new_pwd2 is None:
|
if old_pwd is None or new_pwd is None or new_pwd2 is None:
|
||||||
@@ -335,12 +335,28 @@ class UserViewSet(CustomModelViewSet):
|
|||||||
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
|
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
|
||||||
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
||||||
if verify_password:
|
if verify_password:
|
||||||
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||||
|
request.user.password = make_password(new_pwd)
|
||||||
|
request.user.pwd_change_count += 1
|
||||||
request.user.save()
|
request.user.save()
|
||||||
return DetailResponse(data=None, msg="修改成功")
|
return DetailResponse(data=None, msg="修改成功")
|
||||||
else:
|
else:
|
||||||
return ErrorResponse(msg="旧密码不正确")
|
return ErrorResponse(msg="旧密码不正确")
|
||||||
|
|
||||||
|
@action(methods=["post"], detail=False, permission_classes=[IsAuthenticated])
|
||||||
|
def login_change_password(self, request, *args, **kwargs):
|
||||||
|
"""初次登录进行密码修改"""
|
||||||
|
data = request.data
|
||||||
|
new_pwd = data.get("password")
|
||||||
|
new_pwd2 = data.get("password_regain")
|
||||||
|
if new_pwd != new_pwd2:
|
||||||
|
return ErrorResponse(msg="两次密码不匹配")
|
||||||
|
else:
|
||||||
|
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||||
|
request.user.pwd_change_count += 1
|
||||||
|
request.user.save()
|
||||||
|
return DetailResponse(data=None, msg="修改成功")
|
||||||
|
|
||||||
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
|
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
|
||||||
def reset_to_default_password(self, request,pk):
|
def reset_to_default_password(self, request,pk):
|
||||||
"""恢复默认密码"""
|
"""恢复默认密码"""
|
||||||
|
|||||||
BIN
web/src/assets/login-bg.png
Normal file
BIN
web/src/assets/login-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
84
web/src/components/fileSelector/fileItem.vue
Normal file
84
web/src/components/fileSelector/fileItem.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="itemRef" class="file-item" :title="data.name" @mouseenter="isShow = true" @mouseleave="isShow = false">
|
||||||
|
<div v-if="showTitle" class="file-name" :class="{ show: isShow }">{{ data.name }}</div>
|
||||||
|
<component :is="FileTypes[data.file_type].tag" v-bind="FileTypes[data.file_type].attr" />
|
||||||
|
<div v-if="props.showClose" class="file-del" :class="{ show: isShow }">
|
||||||
|
<el-icon :size="24" color="white" @click.stop="delFileHandle" style="cursor: pointer;">
|
||||||
|
<CircleClose style="mix-blend-mode: difference;" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { ref, defineProps, PropType, watch, onMounted, h } from 'vue';
|
||||||
|
import { successNotification } from '/@/utils/message';
|
||||||
|
import { getBaseURL } from '/@/utils/baseUrl';
|
||||||
|
const props = defineProps({
|
||||||
|
fileData: { type: Object as PropType<any>, required: true },
|
||||||
|
api: { type: Object as PropType<any>, required: true },
|
||||||
|
showTitle: { type: Boolean, default: true },
|
||||||
|
showClose: { type: Boolean, default: true },
|
||||||
|
});
|
||||||
|
const _OtherFileComponent = defineComponent({ template: '<el-icon><Files /></el-icon>' });
|
||||||
|
const FileTypes = [
|
||||||
|
{ tag: 'img', attr: { src: getBaseURL(props.fileData.url), draggable: false } },
|
||||||
|
{ tag: 'video', attr: { src: getBaseURL(props.fileData.url), controls: false, autoplay: true, muted: true, loop: true } },
|
||||||
|
{ tag: 'audio', attr: { src: getBaseURL(props.fileData.url), controls: true, autoplay: false, muted: false, loop: false, volume: 0 } },
|
||||||
|
{ tag: _OtherFileComponent, attr: { style: { fontSize: '2rem' } } },
|
||||||
|
];
|
||||||
|
const isShow = ref<boolean>(false);
|
||||||
|
const itemRef = ref<HTMLDivElement>();
|
||||||
|
const data = ref<any>(null);
|
||||||
|
const delFileHandle = () => props.api.DelObj(props.fileData.id).then(() => {
|
||||||
|
successNotification('删除成功');
|
||||||
|
emit('onDelFile');
|
||||||
|
});
|
||||||
|
watch(props.fileData, (nVal) => data.value = nVal, { immediate: true, deep: true });
|
||||||
|
const emit = defineEmits(['onDelFile']);
|
||||||
|
defineExpose({});
|
||||||
|
onMounted(() => { });
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.file-item {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item>* {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 4px 12px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-del {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
476
web/src/components/fileSelector/index.vue
Normal file
476
web/src/components/fileSelector/index.vue
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
<template>
|
||||||
|
<div style="width: 100%;" :class="props.class" :style="props.style">
|
||||||
|
<slot name="input" v-bind="{}">
|
||||||
|
<div v-if="props.showInput" style="width: 100%;" :class="props.inputClass" :style="props.inputStyle">
|
||||||
|
<el-select v-if="props.inputType === 'selector'" v-model="data" suffix-icon="arrow-down" clearable
|
||||||
|
:multiple="props.multiple" placeholder="请选择文件" @click="selectVisiable = true && !props.disabled"
|
||||||
|
:disabled="props.disabled" @clear="selectedInit" @remove-tag="selectedInit">
|
||||||
|
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
|
||||||
|
:label="item.name" />
|
||||||
|
</el-select>
|
||||||
|
<div v-if="props.inputType === 'image'" style="position: relative;" class="form-display"
|
||||||
|
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
|
||||||
|
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
|
||||||
|
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
|
||||||
|
<template #error>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div v-show="!(!!data)"
|
||||||
|
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||||
|
<el-icon :size="24">
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||||
|
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||||
|
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
|
||||||
|
@mouseleave="formDisplayLeave"
|
||||||
|
style="position: relative; display: flex; align-items: center; justify-items: center;"
|
||||||
|
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
|
||||||
|
<video :src="data" :controls="false" :autoplay="true" :muted="true" :loop="true"
|
||||||
|
:style="{ maxWidth: props.inputSize * 2 + 'px', maxHeight: props.inputSize + 'px', margin: '0 auto' }"></video>
|
||||||
|
<div v-show="!(!!data)"
|
||||||
|
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||||
|
<el-icon :size="24">
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||||
|
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||||
|
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
|
||||||
|
@mouseleave="formDisplayLeave"
|
||||||
|
style="position: relative; display: flex; align-items: center; justify-items: center;"
|
||||||
|
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
|
||||||
|
<audio :src="data" :controls="!!data" :autoplay="false" :muted="true" :loop="true"
|
||||||
|
style="width: 100%; z-index: 1;"></audio>
|
||||||
|
<div v-show="!(!!data)"
|
||||||
|
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||||
|
<el-icon :size="24">
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||||
|
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||||
|
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<el-dialog v-model="selectVisiable" :draggable="false" width="50%" :align-center="false" :append-to-body="true"
|
||||||
|
@open="if (listData.length === 0) listRequest();" @close="onClose" @closed="onClosed" modal-class="_overlay">
|
||||||
|
<template #header>
|
||||||
|
<span class="el-dialog__title">文件选择</span>
|
||||||
|
<el-divider style="margin: 0;" />
|
||||||
|
</template>
|
||||||
|
<div style="padding: 4px;">
|
||||||
|
<div style="width: 100%; display: flex; justify-content: space-between; gap: 12px;">
|
||||||
|
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
|
||||||
|
@tab-change="handleTabChange" v-if="!isSuperTenent">
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="0" label="图片" />
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="1" label="视频" />
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="2" label="音频" />
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="3" label="其他" />
|
||||||
|
</el-tabs>
|
||||||
|
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
|
||||||
|
@tab-change="handleTabChange" v-if="isTenentMode">
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="4" label="系统图片" />
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="5" label="系统视频" />
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="6" label="系统音频" />
|
||||||
|
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="7" label="系统其他" />
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<el-row justify="space-between" class="headerBar">
|
||||||
|
<el-col :span="12">
|
||||||
|
<slot name="actionbar-left">
|
||||||
|
<el-input v-model="filterForm.name" :placeholder="`请输入${TypeLabel[tabsActived % 4]}名`"
|
||||||
|
prefix-icon="search" clearable @change="listRequest" />
|
||||||
|
<div>
|
||||||
|
<el-tag v-if="props.multiple" type="primary" effect="light">
|
||||||
|
一共选中 {{ data?.length || 0 }} 个文件
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" style="width: 100%; display: flex; gap: 12px; justify-content: flex-end;">
|
||||||
|
<slot name="actionbar-right" v-bind="{}">
|
||||||
|
<el-button type="default" circle icon="refresh" @click="listRequest" />
|
||||||
|
<template v-if="tabsActived > 3 ? isSuperTenent : true">
|
||||||
|
<el-upload ref="uploadRef" :action="getBaseURL() + 'api/system/file/'" :multiple="false" :drag="false"
|
||||||
|
:data="{ upload_method: 1 }" :show-file-list="true" :accept="AcceptList[tabsActived % 4]"
|
||||||
|
:on-success="() => { listRequest(); listRequestAll(); uploadRef.clearFiles(); }"
|
||||||
|
v-if="props.showUploadButton">
|
||||||
|
<el-button type="primary" icon="plus">上传{{ TypeLabel[tabsActived % 4] }}</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button type="info" icon="link" @click="netVisiable = true" v-if="props.showNetButton">
|
||||||
|
网络{{ TypeLabel[tabsActived % 4] }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</slot>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div v-if="!listData.length">
|
||||||
|
<slot name="empty">
|
||||||
|
<el-empty description="无内容,请上传" style="width: 100%; height: calc(50vh); margin-top: 24px; padding: 4px;" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div ref="listContainerRef" class="listContainer" v-else>
|
||||||
|
<div v-for="item, index in listData" :key="index" @click="onItemClick($event)" :data-id="item[props.valueKey]"
|
||||||
|
:style="{ width: (props.itemSize || 100) + 'px', cursor: props.selectable ? 'pointer' : 'normal' }">
|
||||||
|
<slot name="item" :data="item">
|
||||||
|
<FileItem :fileData="item" :api="fileApi" :showClose="tabsActived < 4 || isSuperTenent"
|
||||||
|
@onDelFile="listRequest(); listRequestAll();" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="listPaginator">
|
||||||
|
<el-pagination background size="small" layout="total, sizes, prev, pager, next" :total="pageForm.total"
|
||||||
|
v-model:page-size="pageForm.limit" :page-sizes="[10, 20, 30, 40, 50]" v-model:current-page="pageForm.page"
|
||||||
|
:hide-on-single-page="false" @change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 只要在获取中,就最大程度阻止关闭dialog -->
|
||||||
|
<el-dialog v-model="netVisiable" :draggable="false" width="50%" :align-center="false" :append-to-body="true"
|
||||||
|
:title="'网络' + TypeLabel[tabsActived % 4] + '上传'" @closed="netUrl = ''" :close-on-click-modal="!netLoading"
|
||||||
|
:close-on-press-escape="!netLoading" :show-close="!netLoading" modal-class="_overlay">
|
||||||
|
<el-form-item :label="TypeLabel[tabsActived % 4] + '链接'">
|
||||||
|
<el-input v-model="netUrl" placeholder="请输入网络连接" clearable @input="netChange">
|
||||||
|
<template #prepend>
|
||||||
|
<el-select v-model="netPrefix" style="width: 110px;">
|
||||||
|
<el-option v-for="item, index in ['HTTP://', 'HTTPS://']" :key="index" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="!netLoading" type="default" @click="netVisiable = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmNetUrl" :loading="netLoading">
|
||||||
|
{{ netLoading ? '网络文件获取中...' : '确定' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<template #footer v-if="props.showInput">
|
||||||
|
<el-button type="default" @click="onClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="onSave">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUi, UserPageQuery, AddReq, EditReq, DelReq } from '@fast-crud/fast-crud';
|
||||||
|
import { ref, reactive, defineProps, PropType, watch, onMounted, nextTick } from 'vue';
|
||||||
|
import { getBaseURL } from '/@/utils/baseUrl';
|
||||||
|
import { request } from '/@/utils/service';
|
||||||
|
import { SHOW } from './types';
|
||||||
|
import FileItem from './fileItem.vue';
|
||||||
|
import { pluginsAll } from '/@/views/plugins/index';
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useUserInfo } from "/@/stores/userInfo";
|
||||||
|
import { errorNotification, successNotification } from '/@/utils/message';
|
||||||
|
|
||||||
|
const userInfos = storeToRefs(useUserInfo()).userInfos;
|
||||||
|
const isTenentMode = !!(pluginsAll && pluginsAll.length && pluginsAll.indexOf('dvadmin3-tenants-web') >= 0);
|
||||||
|
const isSuperTenent = (userInfos.value as any).schema_name === 'public';
|
||||||
|
|
||||||
|
const TypeLabel = ['图片', '视频', '音频', '文件']
|
||||||
|
const AcceptList = ['image/*', 'video/*', 'audio/*', ''];
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {},
|
||||||
|
class: { type: Object as PropType<String | Object>, default: '' },
|
||||||
|
inputClass: { type: Object as PropType<String | Object>, default: '' },
|
||||||
|
style: { type: Object as PropType<Object | string>, default: {} },
|
||||||
|
inputStyle: { type: Object as PropType<Object | string>, default: {} },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
|
||||||
|
tabsType: { type: Object as PropType<'' | 'card' | 'border-card'>, default: '' },
|
||||||
|
itemSize: { type: Number, default: 100 },
|
||||||
|
|
||||||
|
// 1000图片 100视频 10音频 1 其他 控制tabs的显示
|
||||||
|
tabsShow: { type: Number, default: SHOW.ALL },
|
||||||
|
|
||||||
|
// 是否可以多选,默认单选
|
||||||
|
// 该值为true时inputType必须是selector(暂不支持其他type的多选)
|
||||||
|
multiple: { type: Boolean, default: false },
|
||||||
|
|
||||||
|
// 是否可选,该参数用于只上传和展示而不选择和绑定model的情况
|
||||||
|
selectable: { type: Boolean, default: true },
|
||||||
|
|
||||||
|
// 该参数用于控制是否显示表单item。若赋值为false,则不会显示表单item,也不会显示底部按钮
|
||||||
|
// 如果不显示表单item,则无法触发dialog,需要父组件通过修改本组件暴露的 selectVisiable 状态来控制dialog
|
||||||
|
showInput: { type: Boolean, default: true },
|
||||||
|
|
||||||
|
// 表单item类型,不为selector是需要设置valueKey,否则可能获取不到媒体数据
|
||||||
|
inputType: { type: Object as PropType<'selector' | 'image' | 'video' | 'audio'>, default: 'selector' },
|
||||||
|
// inputType不为selector时生效
|
||||||
|
inputSize: { type: Number, default: 100 },
|
||||||
|
|
||||||
|
// v-model绑定的值是file数据的哪个key,默认是url
|
||||||
|
valueKey: { type: String, default: 'url' },
|
||||||
|
|
||||||
|
showUploadButton: { type: Boolean, default: true },
|
||||||
|
showNetButton: { type: Boolean, default: true },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const selectVisiable = ref<boolean>(false);
|
||||||
|
const tabsActived = ref<number>([3, 2, 1, 0][((props.tabsShow & (props.tabsShow - 1)) === 0) ? Math.log2(props.tabsShow) : 3]);
|
||||||
|
const fileApiPrefix = '/api/system/file/';
|
||||||
|
const fileApi = {
|
||||||
|
GetList: (query: UserPageQuery) => request({ url: fileApiPrefix, method: 'get', params: query }),
|
||||||
|
AddObj: (obj: AddReq) => request({ url: fileApiPrefix, method: 'post', data: obj }),
|
||||||
|
DelObj: (id: DelReq) => request({ url: fileApiPrefix + id + '/', method: 'delete', data: { id } }),
|
||||||
|
GetAll: () => request({ url: fileApiPrefix + 'get_all/' }),
|
||||||
|
};
|
||||||
|
// 过滤表单
|
||||||
|
const filterForm = reactive({ name: '' });
|
||||||
|
// 分页表单
|
||||||
|
const pageForm = reactive({ page: 1, limit: 10, total: 0 });
|
||||||
|
// 展示的数据列表
|
||||||
|
const listData = ref<any[]>([]);
|
||||||
|
const listAllData = ref<any[]>([]);
|
||||||
|
const listRequest = async () => {
|
||||||
|
let res = await fileApi.GetList({
|
||||||
|
page: pageForm.page,
|
||||||
|
limit: pageForm.limit,
|
||||||
|
file_type: isTenentMode ? tabsActived.value % 4 : tabsActived.value,
|
||||||
|
system: tabsActived.value > 3,
|
||||||
|
upload_method: 1,
|
||||||
|
...filterForm
|
||||||
|
});
|
||||||
|
listData.value = [];
|
||||||
|
await nextTick();
|
||||||
|
listData.value = res.data;
|
||||||
|
pageForm.total = res.total;
|
||||||
|
pageForm.page = res.page;
|
||||||
|
pageForm.limit = res.limit;
|
||||||
|
selectedInit();
|
||||||
|
};
|
||||||
|
const formDisplayEnter = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'block');
|
||||||
|
const formDisplayLeave = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'none');
|
||||||
|
const listRequestAll = async () => {
|
||||||
|
if (props.inputType !== 'selector') return;
|
||||||
|
let res = await fileApi.GetAll();
|
||||||
|
listAllData.value = res.data;
|
||||||
|
};
|
||||||
|
// tab改变时触发
|
||||||
|
const handleTabChange = (name: string) => { pageForm.page = 1; listRequest(); };
|
||||||
|
// 分页器改变时触发
|
||||||
|
const handlePageChange = (currentPage: number, pageSize: number) => { pageForm.page = currentPage; pageForm.limit = pageSize; listRequest(); };
|
||||||
|
// 选择的行为
|
||||||
|
const listContainerRef = ref<any>();
|
||||||
|
const onItemClick = async (e: MouseEvent) => {
|
||||||
|
if (!props.selectable) return;
|
||||||
|
let target = e.target as HTMLElement;
|
||||||
|
let flat = 0; // -1删除 0不变 1添加
|
||||||
|
while (!target.dataset.id) target = target.parentElement as HTMLElement;
|
||||||
|
let fileId = target.dataset.id;
|
||||||
|
if (props.multiple) {
|
||||||
|
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
|
||||||
|
else { target.classList.add('active'); flat = 1; }
|
||||||
|
if (data.value.length) {
|
||||||
|
if (flat === 1) data.value.push(fileId);
|
||||||
|
else data.value.splice(data.value.indexOf(fileId), 1);
|
||||||
|
} else data.value = [fileId];
|
||||||
|
// 去重排序,<降序,>升序
|
||||||
|
data.value = Array.from(new Set(data.value)).sort();
|
||||||
|
} else {
|
||||||
|
for (let i of listContainerRef.value?.children) (i as HTMLElement).classList.remove('active');
|
||||||
|
target.classList.add('active');
|
||||||
|
data.value = fileId;
|
||||||
|
}
|
||||||
|
// onDataChange(data.value);
|
||||||
|
};
|
||||||
|
// 每次列表刷新都得更新一下选择状态,因为所有标签页共享列表
|
||||||
|
const selectedInit = async () => {
|
||||||
|
if (!props.selectable) return;
|
||||||
|
await nextTick(); // 不等待一次不会刷新
|
||||||
|
for (let i of (listContainerRef.value?.children || [])) {
|
||||||
|
i.classList.remove('active');
|
||||||
|
let fid = (i as HTMLElement).dataset.id;
|
||||||
|
if (props.multiple) { if (data.value?.includes(fid)) i.classList.add('active'); }
|
||||||
|
else { if (fid === data.value) i.classList.add('active'); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const uploadRef = ref<any>();
|
||||||
|
const onSave = () => {
|
||||||
|
onDataChange(data.value);
|
||||||
|
emit('onSave', data.value);
|
||||||
|
selectVisiable.value = false;
|
||||||
|
};
|
||||||
|
const onClose = () => {
|
||||||
|
data.value = props.modelValue;
|
||||||
|
emit('onClose');
|
||||||
|
selectVisiable.value = false;
|
||||||
|
};
|
||||||
|
const onClosed = () => {
|
||||||
|
clearState();
|
||||||
|
emit('onClosed');
|
||||||
|
};
|
||||||
|
// 清空状态
|
||||||
|
const clearState = () => {
|
||||||
|
filterForm.name = '';
|
||||||
|
pageForm.page = 1;
|
||||||
|
pageForm.limit = 10;
|
||||||
|
pageForm.total = 0;
|
||||||
|
listData.value = [];
|
||||||
|
// all数据不能清,因为all只会在挂载的时候赋值一次
|
||||||
|
// listAllData.value = [];
|
||||||
|
};
|
||||||
|
const clear = () => { data.value = null; onDataChange(null); }
|
||||||
|
|
||||||
|
|
||||||
|
// 网络文件部分
|
||||||
|
const netLoading = ref<boolean>(false);
|
||||||
|
const netVisiable = ref<boolean>(false);
|
||||||
|
const netUrl = ref<string>('');
|
||||||
|
const netPrefix = ref<string>('HTTP://');
|
||||||
|
const netChange = () => {
|
||||||
|
let s = netUrl.value.trim();
|
||||||
|
if (s.toUpperCase().startsWith('HTTP://') || s.toUpperCase().startsWith('HTTPS://')) s = s.split('://')[1];
|
||||||
|
if (s.startsWith('/')) s = s.substring(1);
|
||||||
|
netUrl.value = s;
|
||||||
|
};
|
||||||
|
const confirmNetUrl = () => {
|
||||||
|
if (!netUrl.value) return;
|
||||||
|
netLoading.value = true;
|
||||||
|
let controller = new AbortController();
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
controller.abort();
|
||||||
|
}, 10 * 1000);
|
||||||
|
fetch(netPrefix.value + netUrl.value, { signal: controller.signal }).then(async (res: Response) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (!res.ok) errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
|
||||||
|
const _ = res.url.split('?')[0].split('/');
|
||||||
|
let filename = _[_.length - 1];
|
||||||
|
// let filetype = res.headers.get('content-type')?.split('/')[1] || '';
|
||||||
|
let blob = await res.blob();
|
||||||
|
let file = new File([blob], filename, { type: blob.type });
|
||||||
|
let form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
form.append('upload_method', '1');
|
||||||
|
fetch(getBaseURL() + 'api/system/file/', { method: 'post', body: form })
|
||||||
|
.then(() => successNotification('网络文件上传成功!'))
|
||||||
|
.then(() => { netVisiable.value = false; listRequest(); listRequestAll(); })
|
||||||
|
.catch(() => errorNotification('网络文件上传失败!'))
|
||||||
|
.then(() => netLoading.value = false);
|
||||||
|
}).catch((err: any) => {
|
||||||
|
console.log(err);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
|
||||||
|
netLoading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// fs-crud部分
|
||||||
|
const data = ref<any>(null);
|
||||||
|
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => data.value = val,
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
const { ui } = useUi();
|
||||||
|
const formValidator = ui.formItem.injectFormItemContext();
|
||||||
|
const onDataChange = (value: any) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
formValidator.onChange();
|
||||||
|
formValidator.onBlur();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.multiple && props.inputType !== 'selector')
|
||||||
|
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
|
||||||
|
listRequestAll();
|
||||||
|
console.log('fileselector tenentmdoe', isTenentMode);
|
||||||
|
console.log('fileselector supertenent', isSuperTenent);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-display {
|
||||||
|
--fileselector-close-display: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
._overlay {
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerBar>* {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input-group__prepend) {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listContainer {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-auto-rows: min-content;
|
||||||
|
grid-gap: 36px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 8px;
|
||||||
|
height: calc(50vh);
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listContainer>* {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, .2);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
box-shadow: 0 0 8px var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listPaginator {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
justify-items: center;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addControllorHover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addControllorHover:hover {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeHover {
|
||||||
|
display: var(--fileselector-close-display);
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
web/src/components/fileSelector/types.ts
Normal file
7
web/src/components/fileSelector/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const SHOW = {
|
||||||
|
IMAGE: 0b1000, // 图片
|
||||||
|
VIDEO: 0b0100, // 视频
|
||||||
|
AUDIO: 0b0010, // 音频
|
||||||
|
OTHER: 0b0001, // 其他
|
||||||
|
ALL: 0b1111, // 全部
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
one1: 'User name login',
|
one1: 'User name login',
|
||||||
two2: 'Mobile number',
|
two2: 'Mobile number',
|
||||||
|
changePwd: 'Change The Password',
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
one3: 'Third party login',
|
one3: 'Third party login',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
one1: '账号密码登录',
|
one1: '账号密码登录',
|
||||||
two2: '手机号登录',
|
two2: '手机号登录',
|
||||||
|
changePwd: '密码修改',
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
one3: '第三方登录',
|
one3: '第三方登录',
|
||||||
@@ -12,6 +13,8 @@ export default {
|
|||||||
accountPlaceholder1: '请输入登录账号/邮箱/手机号',
|
accountPlaceholder1: '请输入登录账号/邮箱/手机号',
|
||||||
accountPlaceholder2: '请输入登录密码',
|
accountPlaceholder2: '请输入登录密码',
|
||||||
accountPlaceholder3: '请输入验证码',
|
accountPlaceholder3: '请输入验证码',
|
||||||
|
accountPlaceholder4:'请输入新密码',
|
||||||
|
accountPlaceholder5:'请再次输入新密码',
|
||||||
accountBtnText: '登 录',
|
accountBtnText: '登 录',
|
||||||
},
|
},
|
||||||
mobile: {
|
mobile: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
one1: '用戶名登入',
|
one1: '用戶名登入',
|
||||||
two2: '手機號登入',
|
two2: '手機號登入',
|
||||||
|
changePwd: '密码修改',
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
one3: '協力廠商登入',
|
one3: '協力廠商登入',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export async function initBackEndControlRoutes() {
|
|||||||
if (!Session.get('token')) return false;
|
if (!Session.get('token')) return false;
|
||||||
// 触发初始化用户信息 pinia
|
// 触发初始化用户信息 pinia
|
||||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
|
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
|
||||||
await useUserInfo().setUserInfos();
|
await useUserInfo().getApiUserInfo();
|
||||||
// 获取路由菜单数据
|
// 获取路由菜单数据
|
||||||
const res = await getBackEndControlRoutes();
|
const res = await getBackEndControlRoutes();
|
||||||
// 无登录权限时,添加判断
|
// 无登录权限时,添加判断
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import {checkVersion} from "/@/utils/upgrade";
|
|||||||
const storesThemeConfig = useThemeConfig(pinia);
|
const storesThemeConfig = useThemeConfig(pinia);
|
||||||
const {themeConfig} = storeToRefs(storesThemeConfig);
|
const {themeConfig} = storeToRefs(storesThemeConfig);
|
||||||
const {isRequestRoutes} = themeConfig.value;
|
const {isRequestRoutes} = themeConfig.value;
|
||||||
|
import {useUserInfo} from "/@/stores/userInfo";
|
||||||
|
const { userInfos } = storeToRefs(useUserInfo());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建一个可以被 Vue 应用程序使用的路由实例
|
* 创建一个可以被 Vue 应用程序使用的路由实例
|
||||||
@@ -111,7 +113,10 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
|
next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
|
||||||
Session.clear();
|
Session.clear();
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
} else if (token && to.path === '/login') {
|
}else if (token && to.path === '/login' && userInfos.value.pwd_change_count===0 ) {
|
||||||
|
next('/login');
|
||||||
|
NProgress.done();
|
||||||
|
} else if (token && to.path === '/login' && userInfos.value.pwd_change_count>0) {
|
||||||
next('/home');
|
next('/home');
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
}else if(token && frameOutRoutes.includes(to.path) ){
|
}else if(token && frameOutRoutes.includes(to.path) ){
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface UserInfosState {
|
|||||||
email: string;
|
email: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
gender: string;
|
gender: string;
|
||||||
|
pwd_change_count:null|number;
|
||||||
dept_info: {
|
dept_info: {
|
||||||
dept_id: number;
|
dept_id: number;
|
||||||
dept_name: string;
|
dept_name: string;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const useUserInfo = defineStore('userInfo', {
|
|||||||
email: '',
|
email: '',
|
||||||
mobile: '',
|
mobile: '',
|
||||||
gender: '',
|
gender: '',
|
||||||
|
pwd_change_count:null,
|
||||||
dept_info: {
|
dept_info: {
|
||||||
dept_id: 0,
|
dept_id: 0,
|
||||||
dept_name: '',
|
dept_name: '',
|
||||||
@@ -29,16 +30,19 @@ export const useUserInfo = defineStore('userInfo', {
|
|||||||
isSocketOpen: false
|
isSocketOpen: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async updateUserInfos() {
|
async setPwdChangeCount(count: number) {
|
||||||
let userInfos: any = await this.getApiUserInfo();
|
this.userInfos.pwd_change_count = count;
|
||||||
this.userInfos.username = userInfos.data.name;
|
},
|
||||||
this.userInfos.avatar = userInfos.data.avatar;
|
async updateUserInfos(userInfos:any) {
|
||||||
this.userInfos.name = userInfos.data.name;
|
this.userInfos.username = userInfos.name;
|
||||||
this.userInfos.email = userInfos.data.email;
|
this.userInfos.avatar = userInfos.avatar;
|
||||||
this.userInfos.mobile = userInfos.data.mobile;
|
this.userInfos.name = userInfos.name;
|
||||||
this.userInfos.gender = userInfos.data.gender;
|
this.userInfos.email = userInfos.email;
|
||||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
this.userInfos.mobile = userInfos.mobile;
|
||||||
this.userInfos.role_info = userInfos.data.role_info;
|
this.userInfos.gender = userInfos.gender;
|
||||||
|
this.userInfos.dept_info = userInfos.dept_info;
|
||||||
|
this.userInfos.role_info = userInfos.role_info;
|
||||||
|
this.userInfos.pwd_change_count = userInfos.pwd_change_count;
|
||||||
Session.set('userInfo', this.userInfos);
|
Session.set('userInfo', this.userInfos);
|
||||||
},
|
},
|
||||||
async setUserInfos() {
|
async setUserInfos() {
|
||||||
@@ -55,6 +59,7 @@ export const useUserInfo = defineStore('userInfo', {
|
|||||||
this.userInfos.gender = userInfos.data.gender;
|
this.userInfos.gender = userInfos.data.gender;
|
||||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
this.userInfos.dept_info = userInfos.data.dept_info;
|
||||||
this.userInfos.role_info = userInfos.data.role_info;
|
this.userInfos.role_info = userInfos.data.role_info;
|
||||||
|
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
|
||||||
Session.set('userInfo', this.userInfos);
|
Session.set('userInfo', this.userInfos);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -65,7 +70,18 @@ export const useUserInfo = defineStore('userInfo', {
|
|||||||
return request({
|
return request({
|
||||||
url: '/api/system/user/user_info/',
|
url: '/api/system/user/user_info/',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
}).then((res:any)=>{
|
||||||
|
this.userInfos.username = res.data.name;
|
||||||
|
this.userInfos.avatar = res.data.avatar;
|
||||||
|
this.userInfos.name = res.data.name;
|
||||||
|
this.userInfos.email = res.data.email;
|
||||||
|
this.userInfos.mobile = res.data.mobile;
|
||||||
|
this.userInfos.gender = res.data.gender;
|
||||||
|
this.userInfos.dept_info = res.data.dept_info;
|
||||||
|
this.userInfos.role_info = res.data.role_info;
|
||||||
|
this.userInfos.pwd_change_count = res.data.pwd_change_count;
|
||||||
|
Session.set('userInfo', this.userInfos);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export const downloadFile = function ({ url, params, method, filename = '文件
|
|||||||
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
|
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
|
||||||
}).then((res: any) => {
|
}).then((res: any) => {
|
||||||
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载
|
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载
|
||||||
|
// if (res.headers && res.headers['Content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
|
||||||
if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
|
if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
|
||||||
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
|
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
|
||||||
const fileName = xlsxName || `${filename}.xlsx`
|
const fileName = xlsxName || `${filename}.xlsx`
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import * as api from './api';
|
import * as api from './api';
|
||||||
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
import {
|
||||||
|
UserPageQuery,
|
||||||
|
AddReq,
|
||||||
|
DelReq,
|
||||||
|
EditReq,
|
||||||
|
CrudExpose,
|
||||||
|
CrudOptions,
|
||||||
|
CreateCrudOptionsProps,
|
||||||
|
CreateCrudOptionsRet,
|
||||||
|
dict
|
||||||
|
} from '@fast-crud/fast-crud';
|
||||||
|
import fileSelector from '/@/components/fileSelector/index.vue';
|
||||||
|
import { shallowRef } from 'vue';
|
||||||
|
|
||||||
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
const pageRequest = async (query: UserPageQuery) => {
|
const pageRequest = async (query: UserPageQuery) => {
|
||||||
return await api.GetList(query);
|
return await api.GetList(query);
|
||||||
};
|
};
|
||||||
@@ -20,7 +32,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
|||||||
actionbar: {
|
actionbar: {
|
||||||
buttons: {
|
buttons: {
|
||||||
add: {
|
add: {
|
||||||
show: false,
|
show: true,
|
||||||
|
click: () => context.openAddHandle?.()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -30,6 +43,17 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
|||||||
editRequest,
|
editRequest,
|
||||||
delRequest,
|
delRequest,
|
||||||
},
|
},
|
||||||
|
tabs: {
|
||||||
|
show: true,
|
||||||
|
name: 'file_type',
|
||||||
|
type: '',
|
||||||
|
options: [
|
||||||
|
{ value: 0, label: '图片' },
|
||||||
|
{ value: 1, label: '视频' },
|
||||||
|
{ value: 2, label: '音频' },
|
||||||
|
{ value: 3, label: '其他' },
|
||||||
|
]
|
||||||
|
},
|
||||||
rowHandle: {
|
rowHandle: {
|
||||||
//固定右侧
|
//固定右侧
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
@@ -96,14 +120,25 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
|||||||
},
|
},
|
||||||
type: 'input',
|
type: 'input',
|
||||||
column: {
|
column: {
|
||||||
minWidth: 120,
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
component: {
|
component: {
|
||||||
placeholder: '请输入文件名称',
|
placeholder: '请输入文件名称',
|
||||||
|
clearable: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
preview: {
|
||||||
|
title: '预览',
|
||||||
|
column: {
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
url: {
|
url: {
|
||||||
title: '文件地址',
|
title: '文件地址',
|
||||||
type: 'file-uploader',
|
type: 'file-uploader',
|
||||||
@@ -111,7 +146,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
minWidth: 200,
|
minWidth: 360,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
md5sum: {
|
md5sum: {
|
||||||
@@ -120,12 +155,98 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
minWidth: 120,
|
minWidth: 300,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
disabled: false,
|
disabled: false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mime_type: {
|
||||||
|
title: '文件类型',
|
||||||
|
type: 'input',
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
minWidth: 160
|
||||||
|
}
|
||||||
|
},
|
||||||
|
file_type: {
|
||||||
|
title: '文件类型',
|
||||||
|
type: 'dict-select',
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: '图片', value: 0, color: 'success' },
|
||||||
|
{ label: '视频', value: 1, color: 'warning' },
|
||||||
|
{ label: '音频', value: 2, color: 'danger' },
|
||||||
|
{ label: '其他', value: 3, color: 'primary' },
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
column: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
component: {
|
||||||
|
placeholder: '请选择文件类型'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
title: '文件大小',
|
||||||
|
column: {
|
||||||
|
minWidth: 120
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
upload_method: {
|
||||||
|
title: '上传方式',
|
||||||
|
type: 'dict-select',
|
||||||
|
dict: dict({
|
||||||
|
data: [
|
||||||
|
{ label: '默认上传', value: 0, color: 'primary' },
|
||||||
|
{ label: '文件选择器上传', value: 1, color: 'warning' },
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
column: {
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create_datetime: {
|
||||||
|
title: '创建时间',
|
||||||
|
column: {
|
||||||
|
minWidth: 160
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// fileselectortest: {
|
||||||
|
// title: '文件选择器测试',
|
||||||
|
// type: 'file-selector',
|
||||||
|
// width: 200,
|
||||||
|
// form: {
|
||||||
|
// component: {
|
||||||
|
// name: shallowRef(fileSelector),
|
||||||
|
// vModel: 'modelValue',
|
||||||
|
// tabsShow: 0b0100,
|
||||||
|
// itemSize: 100,
|
||||||
|
// multiple: false,
|
||||||
|
// selectable: true,
|
||||||
|
// showInput: true,
|
||||||
|
// inputType: 'video',
|
||||||
|
// valueKey: 'url',
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<fs-page>
|
<fs-page>
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
<FileSelector v-model="selected" :showInput="false" ref="fileSelectorRef" :tabsShow="SHOW.ALL" :itemSize="120"
|
||||||
|
:multiple="true" :selectable="false">
|
||||||
|
<!-- <template #input="scope">
|
||||||
|
input:{{ scope }}
|
||||||
|
</template> -->
|
||||||
|
<!-- <template #actionbar-left="scope">
|
||||||
|
actionbar-left:{{ scope }}
|
||||||
|
</template> -->
|
||||||
|
<!-- <template #actionbar-right="scope">
|
||||||
|
actionbar-right:{{ scope }}
|
||||||
|
</template> -->
|
||||||
|
<!-- <template #empty="scope">
|
||||||
|
empty:{{ scope }}
|
||||||
|
</template> -->
|
||||||
|
<!-- <template #item="{ data }">
|
||||||
|
{{ data }}
|
||||||
|
</template> -->
|
||||||
|
</FileSelector>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
|
<template #actionbar-left="scope">
|
||||||
|
<el-upload :action="getBaseURL() + 'api/system/file/'" :multiple="false"
|
||||||
|
:on-success="() => crudExpose.doRefresh()" :drag="false" :show-file-list="false">
|
||||||
|
<el-button type="primary" icon="plus">上传</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</template>
|
||||||
|
<template #cell_size="scope">
|
||||||
|
<span>{{ scope.row.size ? getSizeDisplay(scope.row.size) : '0b' }}</span>
|
||||||
|
</template>
|
||||||
|
<template #cell_preview="scope">
|
||||||
|
<div v-if="scope.row.file_type === 0">
|
||||||
|
<el-image style="width: 100%; aspect-ratio: 1 /1 ;" :src="getBaseURL(scope.row.url)"
|
||||||
|
:preview-src-list="[getBaseURL(scope.row.url)]" :preview-teleported="true" />
|
||||||
|
</div>
|
||||||
|
<div v-if="scope.row.file_type === 1" class="_preview"
|
||||||
|
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
|
||||||
|
<el-icon :size="60">
|
||||||
|
<VideoCamera />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-if="scope.row.file_type === 2" class="_preview"
|
||||||
|
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
|
||||||
|
<el-icon :size="60">
|
||||||
|
<Headset />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<el-icon v-if="scope.row.file_type === 3" :size="60">
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
<div v-if="scope.row.file_type > 3">未知类型</div>
|
||||||
|
</template>
|
||||||
|
</fs-crud>
|
||||||
|
<div class="preview" :class="{ show: openPreview }">
|
||||||
|
<video v-show="videoPreviewSrc" :src="videoPreviewSrc" class="previewItem" :controls="true" :autoplay="true"
|
||||||
|
:muted="true" :loop="false" ref="videoPreviewRef"></video>
|
||||||
|
<audio v-show="audioPreviewSrc" :src="audioPreviewSrc" class="previewItem" :controls="true" :autoplay="false"
|
||||||
|
:muted="true" :loop="false" ref="audioPreviewRef"></audio>
|
||||||
|
<div class="closePreviewBtn">
|
||||||
|
<el-icon :size="48" color="white" style="cursor: pointer;" @click="closePreview">
|
||||||
|
<CircleClose />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fs-page>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, nextTick } from 'vue';
|
||||||
import { useExpose, useCrud } from '@fast-crud/fast-crud';
|
import { useExpose, useCrud } from '@fast-crud/fast-crud';
|
||||||
import { createCrudOptions } from './crud';
|
import { createCrudOptions } from './crud';
|
||||||
|
import { getBaseURL } from '/@/utils/baseUrl';
|
||||||
|
import FileSelector from '/@/components/fileSelector/index.vue';
|
||||||
|
import { SHOW } from '/@/components/fileSelector/types';
|
||||||
|
|
||||||
|
const fileSelectorRef = ref<any>(null);
|
||||||
|
const getSizeDisplay = (n: number) => n < 1024 ? n + 'b' : (n < 1024 * 1024 ? (n / 1024).toFixed(2) + 'Kb' : (n / (1024 * 1024)).toFixed(2) + 'Mb');
|
||||||
|
|
||||||
|
const openAddHandle = async () => {
|
||||||
|
fileSelectorRef.value.selectVisiable = true;
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
// crud组件的ref
|
// crud组件的ref
|
||||||
const crudRef = ref();
|
const crudRef = ref();
|
||||||
// crud 配置的ref
|
// crud 配置的ref
|
||||||
@@ -15,12 +87,81 @@ const crudBinding = ref();
|
|||||||
// 暴露的方法
|
// 暴露的方法
|
||||||
const { crudExpose } = useExpose({ crudRef, crudBinding });
|
const { crudExpose } = useExpose({ crudRef, crudBinding });
|
||||||
// 你的crud配置
|
// 你的crud配置
|
||||||
const { crudOptions } = createCrudOptions({ crudExpose });
|
const { crudOptions } = createCrudOptions({ crudExpose, context: { openAddHandle } });
|
||||||
// 初始化crud配置
|
// 初始化crud配置
|
||||||
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
|
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
|
||||||
|
|
||||||
|
const selected = ref<any>([]);
|
||||||
|
const openPreview = ref<boolean>(false);
|
||||||
|
const videoPreviewSrc = ref<string>('');
|
||||||
|
const audioPreviewSrc = ref<string>('');
|
||||||
|
const videoPreviewRef = ref<HTMLVideoElement>();
|
||||||
|
const audioPreviewRef = ref<HTMLAudioElement>();
|
||||||
|
const openPreviewHandle = (src: string, type: string) => {
|
||||||
|
openPreview.value = true;
|
||||||
|
(videoPreviewRef.value as HTMLVideoElement).muted = true;
|
||||||
|
(audioPreviewRef.value as HTMLAudioElement).muted = true;
|
||||||
|
if (type === 'video') videoPreviewSrc.value = src;
|
||||||
|
else audioPreviewSrc.value = src;
|
||||||
|
window.addEventListener('keydown', onPreviewKeydown);
|
||||||
|
};
|
||||||
|
const closePreview = () => {
|
||||||
|
openPreview.value = false;
|
||||||
|
videoPreviewSrc.value = '';
|
||||||
|
audioPreviewSrc.value = '';
|
||||||
|
window.removeEventListener('keydown', onPreviewKeydown);
|
||||||
|
};
|
||||||
|
const onPreviewKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
openPreview.value = false;
|
||||||
|
videoPreviewSrc.value = '';
|
||||||
|
audioPreviewSrc.value = '';
|
||||||
|
window.removeEventListener('keydown', onPreviewKeydown);
|
||||||
|
};
|
||||||
|
|
||||||
// 页面打开后获取列表数据
|
// 页面打开后获取列表数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
crudExpose.doRefresh();
|
crudExpose.doRefresh();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.preview {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
height: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewItem {
|
||||||
|
width: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 50%;
|
||||||
|
transform: translate(25%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closePreviewBtn {
|
||||||
|
width: 50%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-75%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
._preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,6 +13,15 @@ export function login(params: object) {
|
|||||||
data: params
|
data: params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loginChangePwd(data: object) {
|
||||||
|
return request({
|
||||||
|
url: '/api/system/user/login_change_password/',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/system/user/user_info/',
|
url: '/api/system/user/user_info/',
|
||||||
|
|||||||
@@ -45,6 +45,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<!-- 申请试用-->
|
||||||
|
<div style="text-align: center" v-if="showApply()">
|
||||||
|
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
|
||||||
|
<span>申请试用</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -67,6 +73,7 @@ import { SystemConfigStore } from '/@/stores/systemConfig';
|
|||||||
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
|
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
|
||||||
import { Md5 } from 'ts-md5';
|
import { Md5 } from 'ts-md5';
|
||||||
import { errorMessage } from '/@/utils/message';
|
import { errorMessage } from '/@/utils/message';
|
||||||
|
import {getBaseURL} from "/@/utils/baseUrl";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'loginAccount',
|
name: 'loginAccount',
|
||||||
@@ -125,6 +132,9 @@ export default defineComponent({
|
|||||||
state.ruleForm.captchaKey = ret.data.key;
|
state.ruleForm.captchaKey = ret.data.key;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const applyBtnClick = async () => {
|
||||||
|
window.open(getBaseURL('/api/system/apply_for_trial/'));
|
||||||
|
};
|
||||||
const refreshCaptcha = async () => {
|
const refreshCaptcha = async () => {
|
||||||
state.ruleForm.captcha=''
|
state.ruleForm.captcha=''
|
||||||
loginApi.getCaptcha().then((ret: any) => {
|
loginApi.getCaptcha().then((ret: any) => {
|
||||||
@@ -138,8 +148,13 @@ export default defineComponent({
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
|
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
|
||||||
if (res.code === 2000) {
|
if (res.code === 2000) {
|
||||||
|
const {data} = res
|
||||||
|
Cookies.set('username', res.data.username);
|
||||||
Session.set('token', res.data.access);
|
Session.set('token', res.data.access);
|
||||||
Cookies.set('username', res.data.name);
|
useUserInfo().setPwdChangeCount(data.pwd_change_count)
|
||||||
|
if(data.pwd_change_count==0){
|
||||||
|
return router.push('/login');
|
||||||
|
}
|
||||||
if (!themeConfig.value.isRequestRoutes) {
|
if (!themeConfig.value.isRequestRoutes) {
|
||||||
// 前端控制路由,2、请注意执行顺序
|
// 前端控制路由,2、请注意执行顺序
|
||||||
initFrontEndControlRoutes();
|
initFrontEndControlRoutes();
|
||||||
@@ -162,21 +177,18 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
};
|
};
|
||||||
const getUserInfo = () => {
|
|
||||||
useUserInfo().setUserInfos();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 登录成功后的跳转
|
// 登录成功后的跳转
|
||||||
const loginSuccess = () => {
|
const loginSuccess = () => {
|
||||||
//登录成功获取用户信息,获取系统字典数据
|
|
||||||
getUserInfo();
|
|
||||||
//获取所有字典
|
//获取所有字典
|
||||||
DictionaryStore().getSystemDictionarys();
|
DictionaryStore().getSystemDictionarys();
|
||||||
|
|
||||||
// 初始化登录成功时间问候语
|
// 初始化登录成功时间问候语
|
||||||
let currentTimeInfo = currentTime.value;
|
let currentTimeInfo = currentTime.value;
|
||||||
// 登录成功,跳到转首页
|
// 登录成功,跳到转首页
|
||||||
|
const pwd_change_count = userInfos.value.pwd_change_count
|
||||||
|
if(pwd_change_count>0){
|
||||||
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
||||||
if (route.query?.redirect) {
|
if (route.query?.redirect) {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -191,6 +203,7 @@ export default defineComponent({
|
|||||||
state.loading.signIn = true;
|
state.loading.signIn = true;
|
||||||
const signInText = t('message.signInText');
|
const signInText = t('message.signInText');
|
||||||
ElMessage.success(`${currentTimeInfo},${signInText}`);
|
ElMessage.success(`${currentTimeInfo},${signInText}`);
|
||||||
|
}
|
||||||
// 添加 loading,防止第一次进入界面时出现短暂空白
|
// 添加 loading,防止第一次进入界面时出现短暂空白
|
||||||
NextLoading.start();
|
NextLoading.start();
|
||||||
};
|
};
|
||||||
@@ -199,7 +212,10 @@ export default defineComponent({
|
|||||||
//获取系统配置
|
//获取系统配置
|
||||||
SystemConfigStore().getSystemConfigs();
|
SystemConfigStore().getSystemConfigs();
|
||||||
});
|
});
|
||||||
|
// 是否显示申请试用按钮
|
||||||
|
const showApply = () => {
|
||||||
|
return window.location.href.indexOf('public') != -1
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refreshCaptcha,
|
refreshCaptcha,
|
||||||
@@ -209,6 +225,8 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
formRef,
|
formRef,
|
||||||
rules,
|
rules,
|
||||||
|
applyBtnClick,
|
||||||
|
showApply,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -249,7 +267,7 @@ export default defineComponent({
|
|||||||
.login-content-submit {
|
.login-content-submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
font-weight: 300;
|
font-weight: 800;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
276
web/src/views/system/login/component/changePwd.vue
Normal file
276
web/src/views/system/login/component/changePwd.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<el-form ref="formRef" size="large" class="login-content-form" :model="state.ruleForm" :rules="rules"
|
||||||
|
@keyup.enter="loginClick">
|
||||||
|
<el-form-item class="login-animation1" prop="username">
|
||||||
|
<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" readonly
|
||||||
|
v-model="ruleForm.username" clearable autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon class="el-input__icon"><ele-User /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="login-animation2" prop="password">
|
||||||
|
<el-input :type="isShowPassword ? 'text' : 'password'"
|
||||||
|
:placeholder="$t('message.account.accountPlaceholder4')" v-model="ruleForm.password">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<i class="iconfont el-input__icon login-content-password"
|
||||||
|
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
|
||||||
|
@click="isShowPassword = !isShowPassword">
|
||||||
|
</i>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="login-animation3" prop="password_regain">
|
||||||
|
<el-input :type="isShowPassword ? 'text' : 'password'"
|
||||||
|
:placeholder="$t('message.account.accountPlaceholder5')" v-model="ruleForm.password_regain">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<i class="iconfont el-input__icon login-content-password"
|
||||||
|
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
|
||||||
|
@click="isShowPassword = !isShowPassword">
|
||||||
|
</i>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="login-animation4">
|
||||||
|
<el-button type="primary" class="login-content-submit" round @click="loginClick" :loading="loading.signIn">
|
||||||
|
<span>{{ $t('message.account.accountBtnText') }}</span>
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<!-- 申请试用-->
|
||||||
|
<div style="text-align: center" v-if="showApply()">
|
||||||
|
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
|
||||||
|
<span>申请试用</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { toRefs, reactive, defineComponent, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { ElMessage, FormInstance, FormRules } from 'element-plus';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||||
|
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
|
||||||
|
import { initBackEndControlRoutes } from '/@/router/backEnd';
|
||||||
|
import { Session } from '/@/utils/storage';
|
||||||
|
import { formatAxis } from '/@/utils/formatTime';
|
||||||
|
import { NextLoading } from '/@/utils/loading';
|
||||||
|
import * as loginApi from '/@/views/system/login/api';
|
||||||
|
import { useUserInfo } from '/@/stores/userInfo';
|
||||||
|
import { DictionaryStore } from '/@/stores/dictionary';
|
||||||
|
import { SystemConfigStore } from '/@/stores/systemConfig';
|
||||||
|
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
|
||||||
|
import { Md5 } from 'ts-md5';
|
||||||
|
import { errorMessage } from '/@/utils/message';
|
||||||
|
import { getBaseURL } from "/@/utils/baseUrl";
|
||||||
|
import { loginChangePwd } from "/@/views/system/login/api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'changePwd',
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const storesThemeConfig = useThemeConfig();
|
||||||
|
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||||
|
const { userInfos } = storeToRefs(useUserInfo());
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const state = reactive({
|
||||||
|
isShowPassword: false,
|
||||||
|
ruleForm: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
password_regain: ''
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
signIn: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const 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 (state.ruleForm.password !== '') {
|
||||||
|
formRef.value.validateField('password');
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const validatePass2 = (rule, value, callback) => {
|
||||||
|
if (value === '') {
|
||||||
|
callback(new Error('请再次输入密码'));
|
||||||
|
} else if (value !== state.ruleForm.password) {
|
||||||
|
callback(new Error('两次输入密码不一致!'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请填写账号', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请填写密码',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: validatePass,
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
password_regain: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请填写密码',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: validatePass2,
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const formRef = ref();
|
||||||
|
// 时间获取
|
||||||
|
const currentTime = computed(() => {
|
||||||
|
return formatAxis(new Date());
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyBtnClick = async () => {
|
||||||
|
window.open(getBaseURL('/api/system/apply_for_trial/'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginClick = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate((valid: any) => {
|
||||||
|
if (valid) {
|
||||||
|
loginApi.loginChangePwd({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password), password_regain: Md5.hashStr(state.ruleForm.password_regain) }).then((res: any) => {
|
||||||
|
if (res.code === 2000) {
|
||||||
|
if (!themeConfig.value.isRequestRoutes) {
|
||||||
|
// 前端控制路由,2、请注意执行顺序
|
||||||
|
initFrontEndControlRoutes();
|
||||||
|
loginSuccess();
|
||||||
|
} else {
|
||||||
|
// 模拟后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
|
||||||
|
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
|
||||||
|
initBackEndControlRoutes();
|
||||||
|
// 执行完 initBackEndControlRoutes,再执行 signInSuccess
|
||||||
|
loginSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch((err: any) => {
|
||||||
|
// 登录错误之后,刷新验证码
|
||||||
|
errorMessage("登录失败")
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
errorMessage("请填写登录信息")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 登录成功后的跳转
|
||||||
|
const loginSuccess = () => {
|
||||||
|
|
||||||
|
//获取所有字典
|
||||||
|
DictionaryStore().getSystemDictionarys();
|
||||||
|
|
||||||
|
// 初始化登录成功时间问候语
|
||||||
|
let currentTimeInfo = currentTime.value;
|
||||||
|
// 登录成功,跳到转首页
|
||||||
|
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
||||||
|
if (route.query?.redirect) {
|
||||||
|
router.push({
|
||||||
|
path: <string>route.query?.redirect,
|
||||||
|
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
// 登录成功提示
|
||||||
|
// 关闭 loading
|
||||||
|
state.loading.signIn = true;
|
||||||
|
const signInText = t('message.signInText');
|
||||||
|
ElMessage.success(`${currentTimeInfo},${signInText}`);
|
||||||
|
// 添加 loading,防止第一次进入界面时出现短暂空白
|
||||||
|
NextLoading.start();
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
state.ruleForm.username = Cookies.get('username')
|
||||||
|
//获取系统配置
|
||||||
|
SystemConfigStore().getSystemConfigs();
|
||||||
|
});
|
||||||
|
// 是否显示申请试用按钮
|
||||||
|
const showApply = () => {
|
||||||
|
return window.location.href.indexOf('public') != -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginClick,
|
||||||
|
loginSuccess,
|
||||||
|
state,
|
||||||
|
formRef,
|
||||||
|
rules,
|
||||||
|
applyBtnClick,
|
||||||
|
showApply,
|
||||||
|
...toRefs(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login-content-form {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
@for $i from 1 through 5 {
|
||||||
|
.login-animation#{$i} {
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: error-num;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-delay: calc($i/10) + s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content-password {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content-captcha {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content-submit {
|
||||||
|
width: 100%;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,51 +5,52 @@
|
|||||||
<img :src="siteLogo" />
|
<img :src="siteLogo" />
|
||||||
<div class="login-left-logo-text">
|
<div class="login-left-logo-text">
|
||||||
<span>{{ getSystemConfig['login.site_title'] || getThemeConfig.globalViceTitle }}</span>
|
<span>{{ getSystemConfig['login.site_title'] || getThemeConfig.globalViceTitle }}</span>
|
||||||
<span class="login-left-logo-text-msg">{{
|
<span class="login-left-logo-text-msg" style="margin-top: 5px;">{{
|
||||||
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
|
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-left-img">
|
|
||||||
<img :src="loginMain" />
|
|
||||||
</div>
|
|
||||||
<img :src="loginBg" class="login-left-waves" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="login-right flex z-10">
|
<div class="login-right flex z-10">
|
||||||
<div class="login-right-warp flex-margin">
|
<div class="login-right-warp flex-margin">
|
||||||
<span class="login-right-warp-one"></span>
|
<!-- <span class="login-right-warp-one"></span>-->
|
||||||
<span class="login-right-warp-two"></span>
|
<!-- <span class="login-right-warp-two"></span>-->
|
||||||
<div class="login-right-warp-mian">
|
<div class="login-right-warp-mian">
|
||||||
<div class="login-right-warp-main-title">{{ getSystemConfig['login.site_title'] ||
|
<div class="login-right-warp-main-title">
|
||||||
getThemeConfig.globalTitle }} 欢迎您!</div>
|
{{userInfos.pwd_change_count===0?'初次登录修改密码':'欢迎登录'}}
|
||||||
|
</div>
|
||||||
<div class="login-right-warp-main-form">
|
<div class="login-right-warp-main-form">
|
||||||
<div v-if="!state.isScan">
|
<div v-if="!state.isScan">
|
||||||
<el-tabs v-model="state.tabsActiveName">
|
<el-tabs v-model="state.tabsActiveName">
|
||||||
<el-tab-pane :label="$t('message.label.one1')" name="account">
|
<el-tab-pane :label="$t('message.label.changePwd')" name="changePwd" v-if="userInfos.pwd_change_count===0">
|
||||||
|
<ChangePwd />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane :label="$t('message.label.one1')" name="account" v-else>
|
||||||
<Account />
|
<Account />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- TODO 手机号码登录未接入,展示隐藏 -->
|
<!-- TODO 手机号码登录未接入,展示隐藏 -->
|
||||||
<!-- <el-tab-pane :label="$t('message.label.two2')" name="mobile">
|
<!-- <el-tab-pane :label="$t('message.label.two2')" name="mobile">
|
||||||
<Mobile />
|
<Mobile />
|
||||||
</el-tab-pane> -->
|
</el-tab-pane> -->
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
<Scan v-if="state.isScan" />
|
<!-- <Scan v-if="state.isScan" />-->
|
||||||
<div class="login-content-main-sacn" @click="state.isScan = !state.isScan">
|
<!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">-->
|
||||||
<i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
|
<!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
|
||||||
<div class="login-content-main-sacn-delta"></div>
|
<!-- <div class="login-content-main-sacn-delta"></div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-authorization z-10">
|
<div class="login-authorization z-10">
|
||||||
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 django-vue-admin.com' }} 版权所有</p>
|
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 北京信码新创科技有限公司' }} 版权所有</p>
|
||||||
<p class="la-other">
|
<p class="la-other" style="margin-top: 5px;">
|
||||||
<a href="https://beian.miit.gov.cn" target="_blank">{{ getSystemConfig['login.keep_record'] ||
|
<a href="https://beian.miit.gov.cn" target="_blank">{{ getSystemConfig['login.keep_record'] ||
|
||||||
'晋ICP备18005113号-3' }}</a>
|
'京ICP备2021031018号' }}</a>
|
||||||
|
|
|
|
||||||
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : 'https://django-vue-admin.com'"
|
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : '#'"
|
||||||
target="_blank">帮助</a>
|
target="_blank">帮助</a>
|
||||||
|
|
|
|
||||||
<a
|
<a
|
||||||
@@ -60,26 +61,29 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="siteBg">
|
<div v-if="loginBg">
|
||||||
<img :src="siteBg" class="fixed inset-0 z-1 w-full h-full" />
|
<img :src="loginBg" class="loginBg fixed inset-0 z-1 w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="loginIndex">
|
<script setup lang="ts" name="loginIndex">
|
||||||
import { defineAsyncComponent, onMounted, reactive, computed } from 'vue';
|
import {defineAsyncComponent, onMounted, reactive, computed, watch} from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||||
import { NextLoading } from '/@/utils/loading';
|
import { NextLoading } from '/@/utils/loading';
|
||||||
import logoMini from '/@/assets/logo-mini.svg';
|
import logoMini from '/@/assets/logo-mini.svg';
|
||||||
import loginMain from '/@/assets/login-main.svg';
|
import loginMain from '/@/assets/login-main.svg';
|
||||||
import loginBg from '/@/assets/login-bg.svg';
|
import loginBg from '/@/assets/login-bg.png';
|
||||||
import { SystemConfigStore } from '/@/stores/systemConfig'
|
import { SystemConfigStore } from '/@/stores/systemConfig'
|
||||||
import { getBaseURL } from "/@/utils/baseUrl";
|
import { getBaseURL } from "/@/utils/baseUrl";
|
||||||
// 引入组件
|
// 引入组件
|
||||||
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
|
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
|
||||||
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
|
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
|
||||||
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
|
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
|
||||||
|
const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.vue'));
|
||||||
import _ from "lodash-es";
|
import _ from "lodash-es";
|
||||||
|
import {useUserInfo} from "/@/stores/userInfo";
|
||||||
|
const { userInfos } = storeToRefs(useUserInfo());
|
||||||
|
|
||||||
// 定义变量内容
|
// 定义变量内容
|
||||||
const storesThemeConfig = useThemeConfig();
|
const storesThemeConfig = useThemeConfig();
|
||||||
@@ -89,6 +93,16 @@ const state = reactive({
|
|||||||
isScan: false,
|
isScan: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
watch(()=>userInfos.value.pwd_change_count,(val)=>{
|
||||||
|
if(val===0){
|
||||||
|
state.tabsActiveName ='changePwd'
|
||||||
|
}else{
|
||||||
|
state.tabsActiveName ='account'
|
||||||
|
}
|
||||||
|
},{deep:true,immediate:true})
|
||||||
|
|
||||||
|
|
||||||
// 获取布局配置信息
|
// 获取布局配置信息
|
||||||
const getThemeConfig = computed(() => {
|
const getThemeConfig = computed(() => {
|
||||||
return themeConfig.value;
|
return themeConfig.value;
|
||||||
@@ -187,13 +201,13 @@ onMounted(() => {
|
|||||||
width: 700px;
|
width: 700px;
|
||||||
|
|
||||||
.login-right-warp {
|
.login-right-warp {
|
||||||
border: 1px solid var(--el-color-primary-light-3);
|
//border: 1px solid var(--el-color-primary-light-3);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--el-color-white);
|
//background-color: var(--el-color-white);
|
||||||
|
|
||||||
.login-right-warp-one,
|
.login-right-warp-one,
|
||||||
.login-right-warp-two {
|
.login-right-warp-two {
|
||||||
@@ -265,7 +279,8 @@ onMounted(() => {
|
|||||||
.login-right-warp-main-title {
|
.login-right-warp-main-title {
|
||||||
height: 130px;
|
height: 130px;
|
||||||
line-height: 130px;
|
line-height: 130px;
|
||||||
font-size: 27px;
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
animation: logoAnimation 0.3s ease;
|
animation: logoAnimation 0.3s ease;
|
||||||
@@ -321,7 +336,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-authorization {
|
.login-authorization {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -354,7 +354,8 @@ const uploadImg = (data: any) => {
|
|||||||
api.uploadAvatar(formdata).then((res: any) => {
|
api.uploadAvatar(formdata).then((res: any) => {
|
||||||
if (res.code === 2000) {
|
if (res.code === 2000) {
|
||||||
selectImgVisible.value = false;
|
selectImgVisible.value = false;
|
||||||
state.personalForm.avatar = getBaseURL() + res.data.url;
|
// state.personalForm.avatar = getBaseURL() + res.data.url;
|
||||||
|
state.personalForm.avatar = res.data.url;
|
||||||
api.updateUserInfo(state.personalForm).then((res: any) => {
|
api.updateUserInfo(state.personalForm).then((res: any) => {
|
||||||
successMessage('更新成功');
|
successMessage('更新成功');
|
||||||
getUserInfo();
|
getUserInfo();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
|
|||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: env.VITE_PORT as unknown as number,
|
port: env.VITE_PORT as unknown as number,
|
||||||
open: true,
|
open: false,
|
||||||
hmr: true,
|
hmr: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/gitee': {
|
'/gitee': {
|
||||||
|
|||||||
Reference in New Issue
Block a user