Merge remote-tracking branch 'origin/dev' into dev

pull/102/MERGE
猿小天 2023-07-11 11:58:59 +08:00
commit 42372165ac
22 changed files with 496 additions and 201 deletions

View File

@ -3,6 +3,7 @@ import logging
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from application import settings from application import settings
from dvadmin.system import signals
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,9 +30,9 @@ class Command(BaseCommand):
reset = True reset = True
if isinstance(options.get("n"), list) or isinstance(options.get("N"), list): if isinstance(options.get("n"), list) or isinstance(options.get("N"), list):
reset = False reset = False
signals.pre_init_complete.send(sender=None, msg='开始初始化', data={"reset": reset})
for app in settings.INSTALLED_APPS: for app in settings.INSTALLED_APPS:
signals.detail_init_complete.send(sender=None, msg='初始化中', data={"app": app, "reset": reset})
try: try:
exec( exec(
f""" f"""
@ -50,4 +51,5 @@ main(reset={reset})
) )
except ModuleNotFoundError: except ModuleNotFoundError:
pass pass
signals.post_init_complete.send(sender=None, msg='初始化完成', data={"reset": reset})
print("初始化数据完成!") print("初始化数据完成!")

View File

@ -131,6 +131,7 @@ class Dept(CoreModel):
null=True, null=True,
blank=True, blank=True,
help_text="上级部门", help_text="上级部门",
db_index=True
) )
@classmethod @classmethod

View File

@ -0,0 +1,12 @@
from django.dispatch import Signal
# 初始化信号
pre_init_complete = Signal(providing_args=['msg', 'data'])
detail_init_complete = Signal(providing_args=['msg', 'data'])
post_init_complete = Signal(providing_args=['msg', 'data'])
# 租户初始化信号
pre_tenants_init_complete = Signal(providing_args=['msg', 'data'])
detail_tenants_init_complete = Signal(providing_args=['msg', 'data'])
post_tenants_init_complete = Signal(providing_args=['msg', 'data'])
post_tenants_all_init_complete = Signal(providing_args=['msg', 'data'])
# 租户创建完成信号
tenants_create_complete = Signal(providing_args=['msg', 'data'])

View File

@ -13,6 +13,7 @@ from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.permission import AnonymousUserPermission from dvadmin.utils.permission import AnonymousUserPermission
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.filters import LazyLoadFilter
class DeptSerializer(CustomModelSerializer): class DeptSerializer(CustomModelSerializer):
@ -120,6 +121,12 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class DeptLazyFilter(LazyLoadFilter):
class Meta:
model = Dept
fields = ['name', 'parent', 'status']
class DeptViewSet(CustomModelViewSet): class DeptViewSet(CustomModelViewSet):
""" """
部门管理接口 部门管理接口
@ -129,11 +136,13 @@ class DeptViewSet(CustomModelViewSet):
retrieve:单例 retrieve:单例
destroy:删除 destroy:删除
""" """
queryset = Dept.objects.all() queryset = Dept.objects.all()
serializer_class = DeptSerializer serializer_class = DeptSerializer
create_serializer_class = DeptCreateUpdateSerializer create_serializer_class = DeptCreateUpdateSerializer
update_serializer_class = DeptCreateUpdateSerializer update_serializer_class = DeptCreateUpdateSerializer
filter_fields = ['name', 'id', 'parent'] # filter_fields = ["name", "id", "parent"]
filter_class = DeptLazyFilter
search_fields = [] search_fields = []
# extra_filter_backends = [] # extra_filter_backends = []
import_serializer_class = DeptImportSerializer import_serializer_class = DeptImportSerializer

View File

@ -114,7 +114,7 @@ class MenuInitSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Menu model = Menu
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status', fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id'] 'cache', 'visible', 'parent', 'children', 'menu_button', 'frame_out', 'creator', 'dept_belong_id']
extra_kwargs = { extra_kwargs = {
'creator': {'write_only': True}, 'creator': {'write_only': True},
'dept_belong_id': {'write_only': True} 'dept_belong_id': {'write_only': True}
@ -137,7 +137,8 @@ class WebRouterSerializer(CustomModelSerializer):
else: else:
# 根据当前角色获取权限按钮id集合 # 根据当前角色获取权限按钮id集合
permissionIds = self.request.user.role.values_list('permission', flat=True) permissionIds = self.request.user.role.values_list('permission', flat=True)
queryset = instance.menuPermission.filter(id__in=permissionIds, menu=instance.id).values_list('value', flat=True) queryset = instance.menuPermission.filter(id__in=permissionIds, menu=instance.id).values_list('value',
flat=True)
if queryset: if queryset:
return queryset return queryset
else: else:
@ -145,7 +146,8 @@ class WebRouterSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Menu model = Menu
fields = ('id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component', fields = (
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible', 'menuPermission', 'frame_out') 'component_name', 'cache', 'visible', 'menuPermission', 'frame_out')
read_only_fields = ["id"] read_only_fields = ["id"]
@ -165,6 +167,7 @@ class MenuViewSet(CustomModelViewSet):
update_serializer_class = MenuCreateSerializer update_serializer_class = MenuCreateSerializer
search_fields = ['name', 'status'] search_fields = ['name', 'status']
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog'] filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
# extra_filter_backends = [] # extra_filter_backends = []
@action(methods=['GET'], detail=False, permission_classes=[]) @action(methods=['GET'], detail=False, permission_classes=[])

View File

@ -23,7 +23,7 @@ class MenuButtonSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MenuButton model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu'] fields = ["id", "name", "value", "api", "method", "menu"]
read_only_fields = ["id"] read_only_fields = ["id"]
@ -34,7 +34,7 @@ class MenuButtonInitSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MenuButton model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu'] fields = ["id", "name", "value", "api", "method", "menu"]
read_only_fields = ["id"] read_only_fields = ["id"]
@ -58,21 +58,26 @@ class MenuButtonViewSet(CustomModelViewSet):
retrieve:单例 retrieve:单例
destroy:删除 destroy:删除
""" """
queryset = MenuButton.objects.all() queryset = MenuButton.objects.all()
serializer_class = MenuButtonSerializer serializer_class = MenuButtonSerializer
create_serializer_class = MenuButtonCreateUpdateSerializer create_serializer_class = MenuButtonCreateUpdateSerializer
update_serializer_class = MenuButtonCreateUpdateSerializer update_serializer_class = MenuButtonCreateUpdateSerializer
extra_filter_backends = [] extra_filter_backends = []
@action(methods=['get'], detail=False) @action(methods=["GET"], detail=False, permission_classes=[])
def get_btn_permission(self, request): def get_btn_permission(self, request):
""" """
获取当前用户的按钮权限 获取当前用户的按钮权限
""" """
user = request.user user = request.user
if not user.is_superuser: if not user.is_superuser:
menuIds = user.role.values_list('menu__id', flat=True) menuIds = user.role.values_list("menu__id", flat=True)
else: else:
menuIds = Menu.objects.filter(status=1) menuIds = Menu.objects.filter(status=1)
queryset = MenuButton.objects.filter(menu__in=menuIds).annotate(permission=Concat('menu__web_path',Value(':'),'value',output_field=CharField())).values_list('permission',flat=True) queryset = (
MenuButton.objects.filter(menu__in=menuIds)
.annotate(permission=Concat("menu__web_path", Value(":"), "value", output_field=CharField()))
.values_list("permission", flat=True)
)
return DetailResponse(data=queryset) return DetailResponse(data=queryset)

View File

@ -7,6 +7,7 @@
@Remark: 系统配置 @Remark: 系统配置
""" """
import django_filters import django_filters
from django.db import connection
from django.db.models import Q from django.db.models import Q
from django_filters.rest_framework import BooleanFilter from django_filters.rest_framework import BooleanFilter
from rest_framework import serializers from rest_framework import serializers
@ -276,5 +277,6 @@ class InitSettingsViewSet(APIView):
SystemConfig.objects.filter(status=False, parent_id__isnull=False).values('parent__key', SystemConfig.objects.filter(status=False, parent_id__isnull=False).values('parent__key',
'key')] 'key')]
data = dict(filter(lambda x: x[0] not in backend_config, data.items())) data = dict(filter(lambda x: x[0] not in backend_config, data.items()))
data = self.filter_system_config_values(data=data) if hasattr(connection, 'tenant'):
data['schema_name'] = connection.tenant.schema_name
return DetailResponse(data=data) return DetailResponse(data=data)

View File

@ -11,7 +11,7 @@ import traceback
from django.db.models import ProtectedError, RestrictedError from django.db.models import ProtectedError, RestrictedError
from django.http import Http404 from django.http import Http404
from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, PermissionDenied
from rest_framework.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED from rest_framework.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED
from rest_framework.views import set_rollback, exception_handler from rest_framework.views import set_rollback, exception_handler
@ -50,6 +50,8 @@ def CustomExceptionHandler(ex, context):
elif isinstance(ex, DRFAPIException): elif isinstance(ex, DRFAPIException):
set_rollback() set_rollback()
msg = ex.detail msg = ex.detail
if isinstance(ex, PermissionDenied):
msg = f'{msg} ({context["request"].method}: {context["request"].path})'
if isinstance(msg, dict): if isinstance(msg, dict):
for k, v in msg.items(): for k, v in msg.items():
for i in v: for i in v:

View File

@ -12,13 +12,15 @@ from collections import OrderedDict
from functools import reduce from functools import reduce
import six import six
from django import forms
from django.db import models from django.db import models
from django.db.models import Q, F from django.db.models import Q, F
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django_filters import utils from django_filters import utils
from django_filters.conf import settings from django_filters.conf import settings
from django_filters.constants import ALL_FIELDS from django_filters.constants import ALL_FIELDS
from django_filters.filters import CharFilter from django_filters.filters import CharFilter, BooleanFilter
from django_filters.filterset import FilterSet, FilterSetMetaclass
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
@ -71,9 +73,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
permission__api=F("url"), permission__method=F("method") permission__api=F("url"), permission__method=F("method")
) )
api_white_list = [ api_white_list = [
str(item.get("permission__api").replace("{id}", ".*?")) str(item.get("permission__api").replace("{id}", ".*?")) + ":" + str(item.get("permission__method"))
+ ":"
+ str(item.get("permission__method"))
for item in api_white_list for item in api_white_list
if item.get("permission__api") if item.get("permission__api")
] ]
@ -119,19 +119,13 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据) # 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
if 0 in dataScope_list: if 0 in dataScope_list:
return queryset.filter( return queryset.filter(creator=request.user, dept_belong_id=user_dept_id)
creator=request.user, dept_belong_id=user_dept_id
)
# 5. 自定数据权限 获取部门,根据部门过滤 # 5. 自定数据权限 获取部门,根据部门过滤
dept_list = [] dept_list = []
for ele in dataScope_list: for ele in dataScope_list:
if ele == 4: if ele == 4:
dept_list.extend( dept_list.extend(request.user.role.filter(status=1).values_list("dept__id", flat=True))
request.user.role.filter(status=1).values_list(
"dept__id", flat=True
)
)
elif ele == 2: elif ele == 2:
dept_list.append(user_dept_id) dept_list.append(user_dept_id)
elif ele == 1: elif ele == 1:
@ -141,7 +135,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
user_dept_id, user_dept_id,
) )
) )
if queryset.model._meta.model_name == 'dept': if queryset.model._meta.model_name == "dept":
return queryset.filter(id__in=list(set(dept_list))) return queryset.filter(id__in=list(set(dept_list)))
return queryset.filter(dept_belong_id__in=list(set(dept_list))) return queryset.filter(dept_belong_id__in=list(set(dept_list)))
else: else:
@ -186,16 +180,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# TODO: remove assertion in 2.1 # TODO: remove assertion in 2.1
if filterset_class is None and hasattr(view, "filter_class"): if filterset_class is None and hasattr(view, "filter_class"):
utils.deprecate( utils.deprecate(
"`%s.filter_class` attribute should be renamed `filterset_class`." "`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__
% view.__class__.__name__
) )
filterset_class = getattr(view, "filter_class", None) filterset_class = getattr(view, "filter_class", None)
# TODO: remove assertion in 2.1 # TODO: remove assertion in 2.1
if filterset_fields is None and hasattr(view, "filter_fields"): if filterset_fields is None and hasattr(view, "filter_fields"):
utils.deprecate( utils.deprecate(
"`%s.filter_fields` attribute should be renamed `filterset_fields`." "`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
% view.__class__.__name__
) )
filterset_fields = getattr(view, "filter_fields", None) filterset_fields = getattr(view, "filter_fields", None)
@ -224,7 +216,8 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
return [ return [
f.name f.name
for f in sorted(opts.fields + opts.many_to_many) for f in sorted(opts.fields + opts.many_to_many)
if (f.name == 'id') or not isinstance(f, models.AutoField) if (f.name == "id")
or not isinstance(f, models.AutoField)
and not (getattr(f.remote_field, "parent_link", False)) and not (getattr(f.remote_field, "parent_link", False))
] ]
@ -255,9 +248,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# Remove excluded fields # Remove excluded fields
exclude = exclude or [] exclude = exclude or []
if not isinstance(fields, dict): if not isinstance(fields, dict):
fields = [ fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude]
(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude
]
else: else:
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
@ -291,9 +282,12 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
if field is None: if field is None:
undefined.append(field_name) undefined.append(field_name)
# 更新默认字符串搜索为模糊搜索 # 更新默认字符串搜索为模糊搜索
if isinstance(field, (models.CharField)) and filterset_fields == '__all__' and lookups == [ if (
'exact']: isinstance(field, (models.CharField))
lookups = ['icontains'] and filterset_fields == "__all__"
and lookups == ["exact"]
):
lookups = ["icontains"]
for lookup_expr in lookups: for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr) filter_name = cls.get_filter_name(field_name, lookup_expr)
@ -303,20 +297,15 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
continue continue
if field is not None: if field is not None:
filters[filter_name] = cls.filter_for_field( filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
field, field_name, lookup_expr
)
# Allow Meta.fields to contain declared filters *only* when a list/tuple # Allow Meta.fields to contain declared filters *only* when a list/tuple
if isinstance(cls._meta.fields, (list, tuple)): if isinstance(cls._meta.fields, (list, tuple)):
undefined = [ undefined = [f for f in undefined if f not in cls.declared_filters]
f for f in undefined if f not in cls.declared_filters
]
if undefined: if undefined:
raise TypeError( raise TypeError(
"'Meta.fields' must not contain non-model field names: %s" "'Meta.fields' must not contain non-model field names: %s" % ", ".join(undefined)
% ", ".join(undefined)
) )
# Add in declared filters. This is necessary since we don't enforce adding # Add in declared filters. This is necessary since we don't enforce adding
@ -364,3 +353,146 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
if not filterset.is_valid() and self.raise_exception: if not filterset.is_valid() and self.raise_exception:
raise utils.translate_validation(filterset.errors) raise utils.translate_validation(filterset.errors)
return filterset.qs return filterset.qs
# ####################### 懒加载FilterSet ####################### #
import time
def calculate_execution_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
execution_time = end_time - start_time
print(f"Function {func.__name__} took {execution_time:.6f} seconds to execute.", flush=True)
return result
return wrapper
# def get_children(model: models, obj_id: int, all_qs=None, rec_list=None):
# if not all_qs:
# all_qs = model.objects.all().values("id", "parent")
# if rec_list is None:
# rec_list = [obj_id]
# for ele in all_qs:
# if ele.get("parent") == obj_id:
# rec_list.append(ele.get("id"))
# get_dept(ele.get("id"), all_qs, rec_list)
# return list(set(rec_list))
# @calculate_execution_time
# def get_qs_children(model, qs):
# dept_ids = []
# for d in qs:
# r = get_children(model, d.id)
# dept_ids.extend(r)
# return list(set(dept_ids))
def next_layer_data(qs_filter, qs_node):
parent_nodes = set(qs_node.values_list("id", flat=True))
if set(qs_filter) == set(qs_node):
return parent_nodes
# qs_filter内所有父级id 去重
parent_ids = set()
for node in qs_filter:
while node.parent:
if node.id in parent_nodes:
parent_ids.add(node.id)
break
if node.parent.id in parent_nodes:
parent_ids.add(node.parent.id)
break
node = node.parent
# print(f"过滤查询集 ==> {qs_filter}", flush=True)
# print(f"待渲染节点的id ==> {parent_nodes=}", flush=True)
# print(f"过滤查询集的父节点id ==> {parent_ids=}", flush=True)
return parent_ids
def construct_data(qs_filter, qs_node, is_parent):
filter_node_ids = set(qs_filter.values_list("id", flat=True))
render_node_ids = set(qs_node.values_list("id", flat=True))
hidden_node_ids = set()
for node in qs_filter:
while node.parent:
if node.parent in qs_filter:
hidden_node_ids.add(node.id)
node = node.parent
on_show = filter_node_ids.difference(hidden_node_ids)
on_expand = hidden_node_ids & render_node_ids
# print(f"完整查询结果 {filter_node_ids}")
# print(f"待展示的节点(未过滤) {render_node_ids}")
# print(f"查询结果中的子节点 {hidden_node_ids}")
# print(f"查询后首先渲染的父节点 {on_show}")
# print(f"展开父节点时要渲染的节点 {on_expand}")
return on_expand if is_parent else on_show
class FilterSetOptions:
def __init__(self, options=None):
self.model = getattr(options, "model", None)
self.fields = getattr(options, "fields", None)
self.exclude = getattr(options, "exclude", None)
# CharField默认模糊查询
self.filter_overrides = getattr(
options,
"filter_overrides",
{
models.CharField: {
"filter_class": CharFilter,
"extra": lambda f: {
"lookup_expr": "icontains",
},
},
models.BooleanField: {
"filter_class": BooleanFilter,
"extra": lambda f: {
"widget": forms.RadioSelect,
},
},
},
)
self.form = getattr(options, "form", forms.Form)
class LazyLoadFilterSetMetaclass(FilterSetMetaclass):
def __new__(cls, name, bases, attrs):
attrs["declared_filters"] = cls.get_declared_filters(bases, attrs)
new_class = super().__new__(cls, name, bases, attrs)
new_class._meta = FilterSetOptions(getattr(new_class, "Meta", None))
new_class.base_filters = new_class.get_filters()
return new_class
class LazyLoadFilter(FilterSet, metaclass=LazyLoadFilterSetMetaclass):
@property
# @calculate_execution_time
def qs(self):
queryset = self.queryset
# print(self.form.cleaned_data, flush=True)
filter_params = [k for k, v in self.form.cleaned_data.items() if v in [None, ""]]
for field in filter_params:
self.form.cleaned_data.pop(field)
is_parent = self.form.cleaned_data.pop("parent", None) is not None
# print(queryset, flush=True)
if self.form.cleaned_data:
self.queryset = queryset.model.objects.all()
# 从根节点开始
# node_ids = next_layer_data(super().qs, queryset)
# 按匹配结果显示
node_ids = construct_data(super().qs, queryset, is_parent)
return queryset.model.objects.filter(id__in=node_ids)
return super().qs

View File

@ -19,6 +19,7 @@
"china-division": "^2.4.0", "china-division": "^2.4.0",
"core-js": "^3.4.3", "core-js": "^3.4.3",
"cropperjs": "^1.5.6", "cropperjs": "^1.5.6",
"crypto-js": "^4.1.1",
"d2-crud-plus": "^2.17.9", "d2-crud-plus": "^2.17.9",
"d2-crud-x": "^2.17.9", "d2-crud-x": "^2.17.9",
"d2p-extends": "^2.17.9", "d2p-extends": "^2.17.9",
@ -43,7 +44,9 @@
"ua-parser-js": "^0.7.20", "ua-parser-js": "^0.7.20",
"viser-vue": "^2.4.8", "viser-vue": "^2.4.8",
"vue": "2.7.14", "vue": "2.7.14",
"vue-clipboard2": "^0.3.3",
"vue-core-video-player": "^0.2.0", "vue-core-video-player": "^0.2.0",
"vue-cropper": "^0.6.2",
"vue-echarts": "^6.5.4", "vue-echarts": "^6.5.4",
"vue-grid-layout": "^2.4.0", "vue-grid-layout": "^2.4.0",
"vue-html2pdf": "^1.8.0", "vue-html2pdf": "^1.8.0",
@ -51,6 +54,7 @@
"vue-infinite-scroll": "^2.0.2", "vue-infinite-scroll": "^2.0.2",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vue-splitpane": "^1.0.6", "vue-splitpane": "^1.0.6",
"vue-swiper-component": "^2.1.3",
"vuex": "^3.1.2", "vuex": "^3.1.2",
"vxe-table": "^3.3.2", "vxe-table": "^3.3.2",
"xe-utils": "^3.2.1" "xe-utils": "^3.2.1"

View File

@ -180,10 +180,10 @@ export default {
if (this.fileList.length > 0) { if (this.fileList.length > 0) {
const file = this.fileList[0] const file = this.fileList[0]
log.debug('file,', file, file.status) log.debug('file,', file, file.status)
if (file.response != null && file.response.url != null) { if (file.url != null) {
return file.response.url
} else if (file.url != null) {
return file.url return file.url
} else if (file.response != null && file.response.url != null) {
return file.response.url
} }
} }
return null return null

View File

@ -24,6 +24,7 @@
:rules="[ :rules="[
{ required: elProps.fields[key].required, message: '不能为空', trigger: 'blur' }, { required: elProps.fields[key].required, message: '不能为空', trigger: 'blur' },
]" ]"
style="text-align: center"
> >
<el-select v-model="field[key]" v-if="elProps.fields[key].type === 'select'" placeholder="请选择"> <el-select v-model="field[key]" v-if="elProps.fields[key].type === 'select'" placeholder="请选择">
<el-option <el-option
@ -34,13 +35,22 @@
</el-option> </el-option>
</el-select> </el-select>
<el-input-number style="width: 100%" v-else-if="elProps.fields[key].type === 'number'" controls-position="right" v-model="field[key]"></el-input-number> <el-input-number style="width: 100%" v-else-if="elProps.fields[key].type === 'number'" controls-position="right" v-model="field[key]"></el-input-number>
<div v-else-if="elProps.fields[key].type === 'image'" style="height: 50px;width: 50px;"> <div v-else-if="elProps.fields[key].type === 'image'" style="height: 30px;width: 30px;">
<d2p-file-uploader v-model="field[key]" :elProps="elProps.fields[key].elProps || { listType: 'picture-card', accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', limit: 1 }"></d2p-file-uploader> <d2p-file-uploader v-model="field[key]" :elProps="elProps.fields[key].elProps || { listType: 'picture-card', accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', limit: 1 }"></d2p-file-uploader>
</div> </div>
<!-- 富文本 --> <!-- 富文本 -->
<span v-else-if="elProps.fields[key].type === 'ueditor'"> <span v-else-if="elProps.fields[key].type === 'ueditor'">
<values-popover v-model="field[key]" :elProps="{ type: 'ueditor' }" @previewClick="previewClick(index,key)"></values-popover> <values-popover v-model="field[key]" :elProps="{ type: 'ueditor' }" @previewClick="previewClick(index,key)"></values-popover>
</span> </span>
<!-- 多对多 -->
<span v-else-if="elProps.fields[key].type === 'many_to_many'">
<values-popover
v-model="field[key]"
:dict="elProps.fields[key].dict"
:elProps="{ type: elProps.fields[key].value?.type || 'strList', rowKey: elProps.fields[key].value?.rowKey || 'title', label: elProps.value?.title || '答复选项内容' }"
@listClick="manyToManyClick(index,key)">
</values-popover>
</span>
<el-input v-model="field[key]" v-else placeholder="请输入"></el-input> <el-input v-model="field[key]" v-else placeholder="请输入"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -63,11 +73,31 @@
:visible.sync="previewVisible" :visible.sync="previewVisible"
append-to-body append-to-body
width="900"> width="900">
<d2p-ueditor v-model="currentForm.data[ueditorIndex][ueditorKey]" :config="ueditorConfig"></d2p-ueditor> <d2p-ueditor
v-if="currentForm.data && currentForm.data[ueditorIndex] && ueditorKey"
v-model="currentForm.data[ueditorIndex][ueditorKey]"
:config="ueditorConfig">
</d2p-ueditor>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button type="primary" @click="previewVisible = false">完成</el-button> <el-button type="primary" @click="previewVisible = false">完成</el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog
title="编辑"
:visible.sync="manyToManyVisible"
append-to-body
v-if="currentForm.data && currentForm.data[manyToManyIndex] && manyToManyKey"
:width="elProps.fields[manyToManyKey].dialogWidth">
<foreign-key-crud-form
v-model="currentForm.data[manyToManyIndex][manyToManyKey]"
:isInitRows="elProps.fields[manyToManyKey].isInitRows"
:elProps="elProps.fields[manyToManyKey].elProps"
@change="foreignChange"
></foreign-key-crud-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="manyToManyVisible = false">保存</el-button>
</span>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
@ -124,6 +154,50 @@ export default {
required: true, required: true,
min: 0, min: 0,
max: null max: null
},
option_data: {
name: '选项题目',
type: 'many_to_many',
span: 2,
default: [],
required: false,
unit: '个',
value: {
type: 'strList',
rowKey: 'name',
title: '选项内容'
},
//
isInitRows: true,
dialogWidth: '700',
dict: {
value: 'id', // value
label: 'name' // label
},
elProps: {
index: {
name: '序号',
span: 2
},
fields: {
name: {
name: '题目选项内容',
type: 'input',
span: 10,
default: null,
required: true
},
sort: {
name: '排序',
type: 'number',
span: 8,
default: 0,
required: true,
min: 0,
max: null
}
}
}
} }
} }
} }
@ -160,7 +234,11 @@ export default {
// //
previewVisible: false, previewVisible: false,
ueditorIndex: 0, ueditorIndex: 0,
ueditorKey: null ueditorKey: null,
//
manyToManyIndex: 0,
manyToManyKey: null,
manyToManyVisible: false
} }
}, },
computed: { computed: {
@ -250,6 +328,20 @@ export default {
this.ueditorKey = key this.ueditorKey = key
this.previewVisible = true this.previewVisible = true
console.log('previewClick', index, key) console.log('previewClick', index, key)
},
//
manyToManyClick (index, key) {
this.manyToManyIndex = index
this.manyToManyKey = key
this.manyToManyVisible = true
if (!this.currentForm.data[this.manyToManyIndex][this.manyToManyKey]) {
this.currentForm.data[this.manyToManyIndex][this.manyToManyKey] = []
}
},
foreignChange (res) {
if (this.manyToManyKey) {
this.currentForm.data[this.manyToManyIndex][this.manyToManyKey] = res
}
} }
} }
} }

View File

@ -54,7 +54,7 @@
</div> </div>
<div slot="reference" ref="divRef" :style="{'pointerEvents': disabled?'none':''}"> <div slot="reference" ref="divRef" :style="{'pointerEvents': disabled?'none':''}">
<div v-if="currentValue" class="div-input el-input__inner" :class="disabled?'div-disabled':''"> <div v-if="currentValue" class="div-input el-input__inner" :class="disabled?'div-disabled':''">
<div> <div v-if="currentValue instanceof Array">
<el-tag <el-tag
style="margin-right: 5px" style="margin-right: 5px"
v-for="(item,index) in currentValue" v-for="(item,index) in currentValue"
@ -70,7 +70,7 @@
</el-tag> </el-tag>
</div> </div>
</div> </div>
<el-input v-else placeholder="请选择" slot:reference clearable :disabled="disabled"></el-input> <el-input v-else placeholder="请选择" slot:reference clearable :disabled="disabled" :size="size"></el-input>
</div> </div>
</el-popover> </el-popover>
</div> </div>
@ -95,6 +95,11 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
size: {
type: String,
required: false,
default: ''
},
// //
dict: { dict: {
type: Object, type: Object,

View File

@ -18,10 +18,10 @@
{{ item[dict.label] }} {{ item[dict.label] }}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<el-button type="primary" plain size="mini" slot="reference"><span> {{ value.length }} {{ elProps.unit }}</span> <el-button type="primary" plain size="mini" slot="reference" @click="listClick"><span> {{ value.length }} {{ elProps.unit }}</span>
</el-button> </el-button>
</el-popover> </el-popover>
<el-button v-else type="primary" plain size="mini" slot="reference"><span> {{ <el-button v-else type="primary" plain size="mini" slot="reference" @click="listClick"><span> {{
value.length value.length
}} {{ elProps.unit }}</span> }} {{ elProps.unit }}</span>
</el-button> </el-button>
@ -46,10 +46,10 @@
@show="showEvents" @show="showEvents"
@hide="show=false"> @hide="show=false">
<div v-html="value" v-if="show"></div> <div v-html="value" v-if="show"></div>
<el-button type="primary" plain size="mini" slot="reference"><span>预览</span> <el-button type="primary" plain size="mini" slot="reference" @click="previewClick"><span>预览</span>
</el-button> </el-button>
</el-popover> </el-popover>
<el-button v-else type="primary" plain size="mini" slot="reference"><span>预览</span> <el-button v-else type="primary" plain size="mini" slot="reference" @click="previewClick"><span>预览</span>
</el-button> </el-button>
</div> </div>
</div> </div>
@ -125,7 +125,9 @@ export default {
if (this.value.constructor === Array) { if (this.value.constructor === Array) {
const ids = [] const ids = []
this.value.map(res => { this.value.map(res => {
if (res) {
ids.push(res[this.dict.value]) ids.push(res[this.dict.value])
}
}) })
params[this.dict.value] = ids params[this.dict.value] = ids
} else { } else {
@ -135,6 +137,12 @@ export default {
request({ url: this.dict.url, params: params }).then(ret => { request({ url: this.dict.url, params: params }).then(ret => {
this.data = ret.data.data || ret.data this.data = ret.data.data || ret.data
}) })
},
previewClick () {
this.$emit('previewClick')
},
listClick () {
this.$emit('listClick')
} }
} }
} }

View File

@ -1,11 +1,3 @@
<!--
* @创建文件时间: 2021-06-01 22:41:20
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-07-27 00:18:52
* 联系Qq:1638245306
* @文件介绍:
-->
<template> <template>
<el-dropdown size="small" class="d2-mr"> <el-dropdown size="small" class="d2-mr">
<el-link <el-link
@ -28,6 +20,9 @@
<el-dropdown-item @click.native="userInfo"> <el-dropdown-item @click.native="userInfo">
<d2-icon name="cog" class="d2-mr-5" />个人信息 <d2-icon name="cog" class="d2-mr-5" />个人信息
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="clientInfo" v-if="info.tenant_id === 100000">
<d2-icon name="cog" class="d2-mr-5" />租户信息
</el-dropdown-item>
<el-dropdown-item @click.native="logOff" divided> <el-dropdown-item @click.native="logOff" divided>
<d2-icon name="power-off" class="d2-mr-5" /> <d2-icon name="power-off" class="d2-mr-5" />
注销 注销
@ -61,6 +56,10 @@ export default {
/** 个人信息 */ /** 个人信息 */
userInfo () { userInfo () {
this.$router.push({ path: 'userInfo' }) this.$router.push({ path: 'userInfo' })
},
/** 租户信息 */
clientInfo () {
this.$router.push({ path: 'myClientInfo' })
} }
} }
} }

View File

@ -38,7 +38,9 @@ import util from '@/libs/util'
import VueCoreVideoPlayer from 'vue-core-video-player' import VueCoreVideoPlayer from 'vue-core-video-player'
// 引入echarts // 引入echarts
import * as echarts from 'echarts' // 注册echarts组件 import * as echarts from 'echarts' // 注册echarts组件
// 第三方组件
import VueClipboard from 'vue-clipboard2'
Vue.use(VueClipboard)
Vue.use(VueCoreVideoPlayer) Vue.use(VueCoreVideoPlayer)
// 核心插件 // 核心插件
Vue.use(d2Admin) Vue.use(d2Admin)

View File

@ -1,11 +1,3 @@
/*
* @创建文件时间: 2021-06-01 22:41:21
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-11-19 21:35:56
* 联系Qq:1638245306
* @文件介绍: 菜单获取
*/
import { uniqueId } from 'lodash' import { uniqueId } from 'lodash'
import { request } from '@/api/service' import { request } from '@/api/service'
import store from '@/store' import store from '@/store'
@ -78,7 +70,8 @@ export const handleRouter = function (menuData) {
meta: { meta: {
title: item.name, title: item.name,
auth: true, auth: true,
cache: item.cache cache: item.cache,
openInNewWindow: item.frame_out
} }
} }
if (item.frame_out) { if (item.frame_out) {

View File

@ -1,11 +1,3 @@
/*
* @创建文件时间: 2021-06-01 22:41:21
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-11-19 21:49:43
* 联系Qq:1638245306
* @文件介绍:
*/
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
// 进度条 // 进度条
@ -42,7 +34,7 @@ const router = new VueRouter({
*/ */
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
// 白名单 // 白名单
const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/oauth2'] const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/clientRenew', '/oauth2']
// 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201 // 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
await store.dispatch('d2admin/page/isLoaded') await store.dispatch('d2admin/page/isLoaded')
// 确认已经加载组件尺寸设置 https://github.com/d2-projects/d2-admin/issues/198 // 确认已经加载组件尺寸设置 https://github.com/d2-projects/d2-admin/issues/198
@ -92,11 +84,21 @@ router.beforeEach(async (to, from, next) => {
next({ path: to.fullPath, replace: true, params: to.params }) next({ path: to.fullPath, replace: true, params: to.params })
}) })
} else { } else {
next()
const childrenPath = window.qiankunActiveRule || [] const childrenPath = window.qiankunActiveRule || []
if (to.name) { if (to.name) {
// name 属性说明是主应用的路由 // name 属性说明是主应用的路由
if (to.meta.openInNewWindow && !to.query.newWindow) {
// 在新窗口中打开路由
const { href } = router.resolve({
path: to.path + '?newWindow=1'
})
window.open(href, '_blank')
// 取消当前导航
NProgress.done()
next(false)
} else {
next() next()
}
} else if (childrenPath.some((item) => to.path.includes(item))) { } else if (childrenPath.some((item) => to.path.includes(item))) {
next() next()
} else { } else {

View File

@ -13,6 +13,7 @@ export const crudOptions = (vm) => {
height: '100%', // 表格高度100%, 使用toolbar必须设置 height: '100%', // 表格高度100%, 使用toolbar必须设置
highlightCurrentRow: false, highlightCurrentRow: false,
defaultExpandAll: true, defaultExpandAll: true,
resizable: true,
treeConfig: { treeConfig: {
transform: true, transform: true,
rowField: 'id', rowField: 'id',
@ -20,7 +21,13 @@ export const crudOptions = (vm) => {
hasChild: 'hasChild', hasChild: 'hasChild',
lazy: true, lazy: true,
loadMethod: ({ row }) => { loadMethod: ({ row }) => {
return api.GetList({ parent: row.id }).then(ret => { let query = JSON.parse(JSON.stringify(vm.getSearch().getForm()))
query = Object.fromEntries(
Object.entries(query).filter(([_, value]) => ![undefined, null, [], '[]', ''].includes(value))
)
query.parent = row.id
// console.log(query)
return api.GetList({ ...query }).then(ret => {
return ret.data.data return ret.data.data
}) })
}, },
@ -28,6 +35,7 @@ export const crudOptions = (vm) => {
} }
}, },
rowHandle: { rowHandle: {
fixed: 'right',
width: 140, width: 140,
view: { view: {
thin: true, thin: true,
@ -55,7 +63,7 @@ export const crudOptions = (vm) => {
// 或者直接传true,不显示title不居中 // 或者直接传true,不显示title不居中
title: '序号', title: '序号',
align: 'center', align: 'center',
width: 100 width: 70
}, },
viewOptions: { viewOptions: {
@ -71,7 +79,7 @@ export const crudOptions = (vm) => {
show: false, show: false,
disabled: true, disabled: true,
search: { search: {
disabled: false disabled: true
}, },
form: { form: {
disabled: true, disabled: true,
@ -136,8 +144,8 @@ export const crudOptions = (vm) => {
} }
} }
}, },
width: 180,
type: 'input', type: 'input',
showOverflow: 'tooltip',
form: { form: {
rules: [ rules: [
// 表单校验规则 // 表单校验规则

View File

@ -6,6 +6,7 @@
import { mapActions, mapState } from 'vuex' import { mapActions, mapState } from 'vuex'
import localeMixin from '@/locales/mixin.js' import localeMixin from '@/locales/mixin.js'
import * as api from '@/views/system/login/api' import * as api from '@/views/system/login/api'
import { checkPlugins } from '@/views/plugins'
export default { export default {
mixins: [localeMixin], mixins: [localeMixin],
@ -55,7 +56,8 @@ export default {
username: 'admin', username: 'admin',
password: 'admin123456' password: 'admin123456'
} }
] ],
isTenant: checkPlugins('dvadmin-tenants-web')
} }
}, },
computed: { computed: {
@ -68,7 +70,8 @@ export default {
helpUrl: state => state.settings.data['login.help_url'], // helpUrl: state => state.settings.data['login.help_url'], //
privacyUrl: state => state.settings.data['login.privacy_url'], // privacyUrl: state => state.settings.data['login.privacy_url'], //
clauseUrl: state => state.settings.data['login.clause_url'], // clauseUrl: state => state.settings.data['login.clause_url'], //
captchaState: state => state.settings.data['base.captcha_state'] !== undefined ? state.settings.data['base.captcha_state'] : true // captchaState: state => state.settings.data['base.captcha_state'] !== undefined ? state.settings.data['base.captcha_state'] : true, //
isPublic: state => state.settings.data.schema_name === 'public' //
}) })
}, },
mounted () { mounted () {

View File

@ -69,9 +69,20 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<button class="btn btn-primary btn-block" style="padding: 10px 10px;" @click="submit"> <el-row v-if="isTenant && isPublic">
登录 <el-col :span="11">
<button class="btn btn-primary btn-block" style="padding: 10px 10px;" @click="submit"></button>
</el-col>
<el-col :span="11" :offset="2">
<button
class="btn btn-primary btn-block"
style="padding: 10px 10px;background-color: #409eff;color: #fff;"
@click="$router.push('/register')">
免费试用
</button> </button>
</el-col>
</el-row>
<button v-else class="btn btn-primary btn-block" style="padding: 10px 10px;" @click="submit"></button>
<component v-if="componentTag" :is="componentTag"></component> <component v-if="componentTag" :is="componentTag"></component>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>

View File

@ -275,7 +275,7 @@ export const crudOptions = (vm) => {
return request({ return request({
url: url url: url
}).then(ret => { }).then(ret => {
return ret.data.data return ret.data
}) })
} }
}, },