diff --git a/backend/application/dispatch.py b/backend/application/dispatch.py index ae77479..3f19225 100644 --- a/backend/application/dispatch.py +++ b/backend/application/dispatch.py @@ -9,7 +9,7 @@ def is_tenants_mode(): 判断是否为租户模式 :return: """ - return hasattr(connection, 'tenant') and connection.tenant.schema_name + return hasattr(connection, "tenant") and connection.tenant.schema_name # ================================================= # @@ -17,27 +17,37 @@ def is_tenants_mode(): # ================================================= # def _get_all_dictionary(): from dvadmin.system.models import Dictionary + queryset = Dictionary.objects.filter(status=True, is_value=False) data = [] for instance in queryset: - data.append({ - "id": instance.id, - "value": instance.value, - "children": list(Dictionary.objects.filter(parent=instance.id).filter(status=1). - values('label', 'value', 'type', 'color')) - }) + data.append( + { + "id": instance.id, + "value": instance.value, + "children": list( + Dictionary.objects.filter(parent=instance.id) + .filter(status=1) + .values("label", "value", "type", "color") + ), + } + ) return {ele.get("value"): ele for ele in data} def _get_all_system_config(): data = {} from dvadmin.system.models import SystemConfig - system_config_obj = SystemConfig.objects.filter(status=True, parent_id__isnull=False).values( - 'parent__key', 'key', 'value', 'form_item_type').order_by('sort') + + system_config_obj = ( + SystemConfig.objects.filter(status=True, parent_id__isnull=False) + .values("parent__key", "key", "value", "form_item_type") + .order_by("sort") + ) for system_config in system_config_obj: - value = system_config.get('value', '') - if value and system_config.get('form_item_type') == 7: - value = value[0].get('url') + value = system_config.get("value", "") + if value and system_config.get("form_item_type") == 7: + value = value[0].get("url") data[f"{system_config.get('parent__key')}.{system_config.get('key')}"] = value return data @@ -50,12 +60,12 @@ def init_dictionary(): try: if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model + for tenant in get_tenant_model().objects.filter(): with tenant_context(tenant): settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary() else: settings.DICTIONARY_CONFIG = _get_all_dictionary() - print("初始化字典配置完成") except Exception as e: print("请先进行数据库迁移!") return @@ -71,12 +81,12 @@ def init_system_config(): if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model + for tenant in get_tenant_model().objects.filter(): with tenant_context(tenant): settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config() else: settings.SYSTEM_CONFIG = _get_all_system_config() - print("初始化系统配置完成") except Exception as e: print("请先进行数据库迁移!") return @@ -89,6 +99,7 @@ def refresh_dictionary(): """ if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model + for tenant in get_tenant_model().objects.filter(): with tenant_context(tenant): settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary() @@ -103,6 +114,7 @@ def refresh_system_config(): """ if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model + for tenant in get_tenant_model().objects.filter(): with tenant_context(tenant): settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config() diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index 6dec1b1..77f530d 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -13,7 +13,7 @@ STATUS_CHOICES = ( ) -class Users(AbstractUser, CoreModel): +class Users(CoreModel,AbstractUser): username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号", help_text="用户账号") email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱") mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话") @@ -410,3 +410,22 @@ class LoginLog(CoreModel): verbose_name = "登录日志" verbose_name_plural = verbose_name ordering = ("-create_datetime",) + + +class MessageCenter(CoreModel): + title = models.CharField(max_length=100,verbose_name="标题",help_text="标题") + content = models.TextField(verbose_name="内容",help_text="内容") + target_type=models.IntegerField(default=0,verbose_name="目标类型",help_text="目标类型") + target_user = models.ForeignKey(to=Users,related_name="target_user",null=True,blank=True,db_constraint=False,on_delete=models.CASCADE,verbose_name="目标用户",help_text="目标用户") + target_dept = models.ForeignKey(to=Dept, null=True, blank=True, db_constraint=False, on_delete=models.CASCADE, + verbose_name="目标部门", help_text="目标部门") + target_role = models.ForeignKey(to=Role, null=True, blank=True, db_constraint=False, on_delete=models.CASCADE, + verbose_name="目标角色", help_text="目标角色") + is_read=models.BooleanField(default=False,blank=True,verbose_name="是否已读",help_text="是否已读") + + class Meta: + db_table = table_prefix + "message_center" + verbose_name = "消息中心" + verbose_name_plural = verbose_name + ordering = ("-create_datetime",) + diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py index 06fcd9e..6b8c95f 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -9,6 +9,7 @@ from dvadmin.system.views.file_list import FileViewSet from dvadmin.system.views.login_log import LoginLogViewSet from dvadmin.system.views.menu import MenuViewSet from dvadmin.system.views.menu_button import MenuButtonViewSet +from dvadmin.system.views.message_center import MessageCenterViewSet from dvadmin.system.views.operation_log import OperationLogViewSet from dvadmin.system.views.role import RoleViewSet from dvadmin.system.views.system_config import SystemConfigViewSet @@ -26,6 +27,7 @@ system_url.register(r'area', AreaViewSet) system_url.register(r'file', FileViewSet) system_url.register(r'api_white_list', ApiWhiteListViewSet) system_url.register(r'system_config', SystemConfigViewSet) +system_url.register(r'message_center',MessageCenterViewSet) urlpatterns = [ path('user/export/', UserViewSet.as_view({'post': 'export_data', })), diff --git a/backend/dvadmin/system/views/dept.py b/backend/dvadmin/system/views/dept.py index 62b397b..bb88ea3 100644 --- a/backend/dvadmin/system/views/dept.py +++ b/backend/dvadmin/system/views/dept.py @@ -19,10 +19,17 @@ class DeptSerializer(CustomModelSerializer): """ parent_name = serializers.CharField(read_only=True, source='parent.name') has_children = serializers.SerializerMethodField() + status_label = serializers.SerializerMethodField() def get_has_children(self, obj: Dept): return Dept.objects.filter(parent_id=obj.id).count() + def get_status_label(self, instance): + status = instance.status + if status: + return "启用" + return "禁用" + class Meta: model = Dept fields = '__all__' diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index ef9f40b..5ee5fcc 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -1,12 +1,14 @@ import hashlib from django.contrib.auth.hashers import make_password +from django_restql.fields import DynamicSerializerMethodField from rest_framework import serializers from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from application import dispatch -from dvadmin.system.models import Users +from dvadmin.system.models import Users, Role, Dept +from dvadmin.system.views.role import RoleSerializer from dvadmin.utils.json_response import ErrorResponse, DetailResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.validator import CustomUniqueValidator @@ -17,6 +19,8 @@ class UserSerializer(CustomModelSerializer): """ 用户管理-序列化器 """ + dept_name = serializers.CharField(source='dept.name', read_only=True) + role_info = DynamicSerializerMethodField() class Meta: model = Users @@ -26,6 +30,17 @@ class UserSerializer(CustomModelSerializer): "post": {"required": False}, } + def get_role_info(self, instance, parsed_query): + roles = instance.role.all() + # You can do what ever you want in here + # `parsed_query` param is passed to BookSerializer to allow further querying + serializer = RoleSerializer( + roles, + many=True, + parsed_query=parsed_query + ) + return serializer.data + class UsersInitSerializer(CustomModelSerializer): """ @@ -128,10 +143,14 @@ class ExportUserProfileSerializer(CustomModelSerializer): last_login = serializers.DateTimeField( format="%Y-%m-%d %H:%M:%S", required=False, read_only=True ) - dept__deptName = serializers.CharField(source="dept.deptName", default="") - dept__owner = serializers.CharField(source="dept.owner", default="") + is_active = serializers.SerializerMethodField(read_only=True) + dept_name = serializers.CharField(source="dept.name", default="") + dept_owner = serializers.CharField(source="dept.owner", default="") gender = serializers.CharField(source="get_gender_display", read_only=True) + def get_is_active(self, instance): + return "启用" if instance.is_active else "停用" + class Meta: model = Users fields = ( @@ -142,8 +161,8 @@ class ExportUserProfileSerializer(CustomModelSerializer): "gender", "is_active", "last_login", - "dept__deptName", - "dept__owner", + "dept_name", + "dept_owner", ) @@ -157,15 +176,6 @@ class UserProfileImportSerializer(CustomModelSerializer): data.save() return data - def run_validation(self, data={}): - # 把excel 数据进行格式转换 - if type(data) is dict: - data["role"] = str(data["role"]).split(",") - data["dept_id"] = str(data["dept"]).split(",") - data["gender"] = {"男": "1", "女": "0", "未知": "2"}.get(data["gender"]) - data["is_active"] = {"启用": True, "禁用": False}.get(data["is_active"]) - return super().run_validation(data) - class Meta: model = Users exclude = ( @@ -222,11 +232,21 @@ class UserViewSet(CustomModelViewSet): "name": "用户名称", "email": "用户邮箱", "mobile": "手机号码", - "gender": "用户性别(男/女/未知)", - "is_active": "帐号状态(启用/禁用)", + "gender": { + "title": "用户性别", + "choices": { + "data": {"未知": 2, "男": 1, "女": 0}, + } + }, + "is_active": { + "title": "帐号状态", + "choices": { + "data": {"启用": True, "禁用": False}, + } + }, "password": "登录密码", - "dept": "部门ID", - "role": "角色ID", + "dept": {"title": "部门", "choices": {"queryset": Dept.objects.filter(status=True), "values_name": "name"}}, + "role": {"title": "角色", "choices": {"queryset": Role.objects.filter(status=True), "values_name": "name"}}, } @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) diff --git a/backend/dvadmin/utils/import_export.py b/backend/dvadmin/utils/import_export.py index 5a01968..882ff4c 100644 --- a/backend/dvadmin/utils/import_export.py +++ b/backend/dvadmin/utils/import_export.py @@ -1,37 +1,67 @@ # -*- coding: utf-8 -*- import os +import re import openpyxl from django.conf import settings -def import_to_data(file_url, field_data): +def import_to_data(file_url, field_data, m2m_fields=None): """ 读取导入的excel文件 - :param request: + :param file_url: :param field_data: 首行数据源 - :param data: 数据源 - :param FilName: 文件名 + :param m2m_fields: 多对多字段 :return: """ # 读取excel 文件 file_path_dir = os.path.join(settings.BASE_DIR, file_url) workbook = openpyxl.load_workbook(file_path_dir) table = workbook[workbook.sheetnames[0]] + # 获取参数映射 + validation_data_dict = {} + for key, value in field_data.items(): + if isinstance(value, dict): + choices = value.get("choices", {}) + data_dict = {} + if choices.get("data"): + for k, v in choices.get("data").items(): + data_dict[k] = v + elif choices.get("queryset") and choices.get("values_name"): + data_list = choices.get("queryset").values(choices.get("values_name"), "id") + for ele in data_list: + data_dict[ele.get(choices.get("values_name"))] = ele.get("id") + else: + continue + validation_data_dict[key] = data_dict # 创建一个空列表,存储Excel的数据 tables = [] for i, row in enumerate(range(table.max_row)): if i == 0: continue array = {} - for index, ele in enumerate(field_data.keys()): + for index, key in enumerate(field_data.keys()): cell_value = table.cell(row=row + 1, column=index + 2).value - print(cell_value) # 由于excel导入数字类型后,会出现数字加 .0 的,进行处理 if type(cell_value) is float and str(cell_value).split(".")[1] == "0": cell_value = int(str(cell_value).split(".")[0]) if type(cell_value) is str: cell_value = cell_value.strip(" \t\n\r") - array[ele] = cell_value + if cell_value is None: + continue + if key in validation_data_dict: + array[key] = validation_data_dict.get(key, {}).get(cell_value, None) + if key in m2m_fields: + array[key] = list( + filter( + lambda x: x, + [ + validation_data_dict.get(key, {}).get(value, None) + for value in re.split(r"[,;:|.,;:\s]\s*", cell_value) + ], + ) + ) + else: + array[key] = cell_value tables.append(array) return tables diff --git a/backend/dvadmin/utils/import_export_mixin.py b/backend/dvadmin/utils/import_export_mixin.py index 5a5c9f2..1a35e34 100644 --- a/backend/dvadmin/utils/import_export_mixin.py +++ b/backend/dvadmin/utils/import_export_mixin.py @@ -4,7 +4,8 @@ from urllib.parse import quote from django.db import transaction from django.http import HttpResponse from openpyxl import Workbook -from openpyxl.utils import get_column_letter +from openpyxl.worksheet.datavalidation import DataValidation +from openpyxl.utils import get_column_letter, quote_sheetname from openpyxl.worksheet.table import Table, TableStyleInfo from rest_framework.request import Request @@ -15,13 +16,28 @@ from dvadmin.utils.request_util import get_verbose_name class ImportSerializerMixin: """ - 自定义导出模板、导入功能 + 自定义导入模板、导入功能 """ # 导入字段 import_field_dict = {} # 导入序列化器 import_serializer_class = None + # 表格表头最大宽度,默认50个字符 + export_column_width = 50 + + def get_string_len(self, string): + """ + 获取字符串最大长度 + :param string: + :return: + """ + length = 4 + if string is None: + return length + for char in string: + length += 2.1 if ord(char) > 256 else 1 + return round(length, 1) if length <= self.export_column_width else self.export_column_width @transaction.atomic # Django 事务,防止出错 def import_data(self, request: Request, *args, **kwargs): @@ -44,10 +60,49 @@ class ImportSerializerMixin: "Content-Disposition" ] = f'attachment;filename={quote(str(f"导入{get_verbose_name(queryset)}模板.xlsx"))}' wb = Workbook() + ws1 = wb.create_sheet("data", 1) + ws1.sheet_state = "hidden" ws = wb.active row = get_column_letter(len(self.import_field_dict) + 1) column = 10 - ws.append(["序号", *self.import_field_dict.values()]) + header_data = [ + "序号", + ] + validation_data_dict = {} + for index, ele in enumerate(self.import_field_dict.values()): + if isinstance(ele, dict): + header_data.append(ele.get("title")) + choices = ele.get("choices", {}) + if choices.get("data"): + data_list = [] + data_list.extend(choices.get("data").keys()) + validation_data_dict[ele.get("title")] = data_list + elif choices.get("queryset") and choices.get("values_name"): + data_list = choices.get("queryset").values_list(choices.get("values_name"), flat=True) + validation_data_dict[ele.get("title")] = list(data_list) + else: + continue + column_letter = get_column_letter(len(validation_data_dict)) + dv = DataValidation( + type="list", + formula1=f"{quote_sheetname('data')}!${column_letter}$2:${column_letter}${len(validation_data_dict[ele.get('title')]) + 1}", + allow_blank=True, + ) + ws.add_data_validation(dv) + dv.add(f"{get_column_letter(index + 2)}2:{get_column_letter(index + 2)}1048576") + else: + header_data.append(ele) + # 添加数据列 + ws1.append(list(validation_data_dict.keys())) + for index, validation_data in enumerate(validation_data_dict.values()): + for inx, ele in enumerate(validation_data): + ws1[f"{get_column_letter(index + 1)}{inx + 2}"] = ele + # 插入导出模板正式数据 + df_len_max = [self.get_string_len(ele) for ele in header_data] + ws.append(header_data) + #  更新列宽 + for index, width in enumerate(df_len_max): + ws.column_dimensions[get_column_letter(index + 1)].width = width tab = Table(displayName="Table1", ref=f"A1:{row}{column}") # 名称管理器 style = TableStyleInfo( name="TableStyleLight11", @@ -63,8 +118,14 @@ class ImportSerializerMixin: updateSupport = request.data.get("updateSupport") # 从excel中组织对应的数据结构,然后使用序列化器保存 - data = import_to_data(request.data.get("url"), self.import_field_dict) queryset = self.filter_queryset(self.get_queryset()) + # 获取多对多字段 + m2m_fields = [ + ele.attname + for ele in queryset.model._meta.get_fields() + if hasattr(ele, "many_to_many") and ele.many_to_many == True + ] + data = import_to_data(request.data.get("url"), self.import_field_dict, m2m_fields) unique_list = [ ele.attname for ele in queryset.model._meta.get_fields() if hasattr(ele, "unique") and ele.unique == True ] @@ -91,6 +152,21 @@ class ExportSerializerMixin: export_field_label = [] # 导出序列化器 export_serializer_class = None + # 表格表头最大宽度,默认50个字符 + export_column_width = 50 + + def get_string_len(self, string): + """ + 获取字符串最大长度 + :param string: + :return: + """ + length = 4 + if string is None: + return length + for char in string: + length += 2.1 if ord(char) > 256 else 1 + return round(length, 1) if length <= self.export_column_width else self.export_column_width def export_data(self, request: Request, *args, **kwargs): """ @@ -109,13 +185,33 @@ class ExportSerializerMixin: response["Content-Disposition"] = f'attachment;filename={quote(str(f"导出{get_verbose_name(queryset)}.xlsx"))}' wb = Workbook() ws = wb.active + header_data = ["序号", *self.export_field_label] + df_len_max = [self.get_string_len(ele) for ele in header_data] row = get_column_letter(len(self.export_field_label) + 1) column = 1 - ws.append(["序号", *self.export_field_label]) + ws.append(header_data) for index, results in enumerate(data): - ws.append([index + 1, *list(results.values())]) + results_list = [] + for inx, result in enumerate(results.values()): + # 布尔值进行更新 + if result is True: + result = "是" + elif result is False: + result = "否" + if isinstance(result, int): + result = str(result) + # 计算最大列宽度 + result_column_width = self.get_string_len(result) + if result_column_width > df_len_max[inx + 1]: + df_len_max[inx + 1] = result_column_width + + results_list.append(result) + ws.append([index + 1, *results_list]) column += 1 - tab = Table(displayName="Table2", ref=f"A1:{row}{column}") # 名称管理器 + #  更新列宽 + for index, width in enumerate(df_len_max): + ws.column_dimensions[get_column_letter(index + 1)].width = width + tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器 style = TableStyleInfo( name="TableStyleLight11", showFirstColumn=True, diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index 99cf1e5..ecac1a1 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -10,12 +10,49 @@ import uuid from django.apps import apps from django.db import models +from django.db.models import QuerySet from application import settings - table_prefix = settings.TABLE_PREFIX # 数据库表名前缀 +class SoftDeleteQuerySet(QuerySet): + def delete(self,soft_delete=True): + """ + 重写删除方法 + 当soft_delete为True时表示软删除,则修改删除时间为当前时间,否则直接删除 + :param soft: Boolean 是否软删除,默认是 + :return: Tuple eg.(3, {'lqModel.Test': 3}) + """ + if soft_delete: + return self.update(is_deleted=True) + else: + return super(SoftDeleteQuerySet, self).delete() + + + +class SoftDeleteManager(models.Manager): + """支持软删除""" + + def __init__(self, *args, **kwargs): + self.__add_is_del_filter = False + super(SoftDeleteManager, self).__init__(*args, **kwargs) + + def filter(self, *args, **kwargs): + # 考虑是否主动传入is_deleted + if not kwargs.get('is_deleted') is None: + self.__add_is_del_filter = True + return super(SoftDeleteManager, self).filter(*args, **kwargs) + + def get_queryset(self): + if self.__add_is_del_filter: + return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=False) + return SoftDeleteQuerySet(self.model).exclude(is_deleted=True) + + def get_by_natural_key(self,name): + return SoftDeleteQuerySet(self.model).get(username=name) + + class CoreModel(models.Model): """ 核心标准抽象模型模型,可直接继承使用 @@ -30,6 +67,9 @@ class CoreModel(models.Model): update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间") create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", verbose_name="创建时间") + is_deleted = models.BooleanField(verbose_name="是否软删除",help_text='是否软删除', default=False, db_index=True) + objects = SoftDeleteManager() + class Meta: abstract = True @@ -37,6 +77,8 @@ class CoreModel(models.Model): verbose_name_plural = verbose_name + + def get_all_models_objects(model_name=None): """ 获取所有 models 对象 diff --git a/backend/dvadmin/utils/serializers.py b/backend/dvadmin/utils/serializers.py index 09cf50c..90a685e 100644 --- a/backend/dvadmin/utils/serializers.py +++ b/backend/dvadmin/utils/serializers.py @@ -80,6 +80,9 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer): def update(self, instance, validated_data): if self.request: + if str(self.request.user) != "AnonymousUser": + if self.modifier_field_id in self.fields.fields: + validated_data[self.modifier_field_id] = self.get_request_user_id() if hasattr(self.instance, self.modifier_field_id): setattr( self.instance, self.modifier_field_id, self.get_request_user_id() diff --git a/backend/dvadmin/utils/viewset.py b/backend/dvadmin/utils/viewset.py index 1340338..5e2008f 100644 --- a/backend/dvadmin/utils/viewset.py +++ b/backend/dvadmin/utils/viewset.py @@ -92,7 +92,13 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi def destroy(self, request, *args, **kwargs): instance = self.get_object() - self.perform_destroy(instance) + request_data = request.data + soft_delete = request_data.get('soft_delete',True) + if soft_delete: + instance.is_deleted = True + instance.save() + else: + self.perform_destroy(instance) return DetailResponse(data=[], msg="删除成功") diff --git a/web/.env.preview b/web/.env.preview index 7bbf2aa..1855d20 100644 --- a/web/.env.preview +++ b/web/.env.preview @@ -1,5 +1,4 @@ -# 构建预览页面 - +# 预览环境 # 指定构建模式 NODE_ENV=production diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 0000000..8b3fa56 --- /dev/null +++ b/web/.env.production @@ -0,0 +1,15 @@ +# 生产环境 +# 指定构建模式 +NODE_ENV=production + +# 标记当前构建方式 +VUE_APP_BUILD_MODE=PREVIEW +# 页面 title 前缀 +VUE_APP_TITLE=企业级后台管理系统 +# 显示源码按钮 +VUE_APP_SCOURCE_LINK=FALSE + +# 部署路径 +VUE_APP_PUBLIC_PATH=/ +# 启用权限管理 +VUE_APP_PM_ENABLED = true diff --git a/web/src/api/service.js b/web/src/api/service.js index 4262a4c..7470667 100644 --- a/web/src/api/service.js +++ b/web/src/api/service.js @@ -213,13 +213,14 @@ const refreshTken = function () { * 下载文件 * @param url * @param params + * @param method * @param filename */ -export const downloadFile = function ({ url, data, method, filename }) { +export const downloadFile = function ({ url, params, method, filename }) { request({ url: url, method: method, - data: data, + params: params, responseType: 'blob' // headers: {Accept: 'application/vnd.openxmlformats-officedocument'} }).then(res => { diff --git a/web/src/components/foreign-key/README.md b/web/src/components/foreign-key/README.md new file mode 100644 index 0000000..4f7c5c1 --- /dev/null +++ b/web/src/components/foreign-key/README.md @@ -0,0 +1,28 @@ +# 一对多表格显示配置说明 +本组件用于多对多返回数据使用,例如:角色信息 +```angular2html +dept_name = "dvadmin团队" + +#crud的配置 +component: { +name: 'foreignKey', +valueBinding: 'dept_name' +} +``` +## crud.js +``` + { + component: { + name: 'foreignKey', + valueBinding: 'dept_name', + } + } +``` + +## 配置说明 + + +| Name | Description | Type | Required | Default | +| ---------- | ---------------- | ------- | -------- | -------------- | +| name | 字段所使用的组件 | String | true | foreignKey | +| valueBinding | row中的key | String | true | - | diff --git a/web/src/components/foreign-key/index.js b/web/src/components/foreign-key/index.js new file mode 100644 index 0000000..6190ec2 --- /dev/null +++ b/web/src/components/foreign-key/index.js @@ -0,0 +1,15 @@ +import { d2CrudPlus } from 'd2-crud-plus' +import group from './group' + +function install (Vue, options) { + Vue.component('foreign-key', () => import('./index')) + if (d2CrudPlus != null) { + // 注册字段类型`demo-extend` + d2CrudPlus.util.columnResolve.addTypes(group) + } +} + +// 导出install, 通过`vue.use(D2pDemoExtend)`安装后 ,`demo-extend` 就可以在`crud.js`中使用了 +export default { + install +} diff --git a/web/src/components/foreign-key/index.vue b/web/src/components/foreign-key/index.vue new file mode 100644 index 0000000..ef68d27 --- /dev/null +++ b/web/src/components/foreign-key/index.vue @@ -0,0 +1,42 @@ + + diff --git a/web/src/components/index.js b/web/src/components/index.js index 73ffe1f..943e6c8 100644 --- a/web/src/components/index.js +++ b/web/src/components/index.js @@ -7,3 +7,5 @@ Vue.component('d2-container', d2Container) Vue.component('d2-icon', () => import('./d2-icon')) Vue.component('d2-icon-svg', () => import('./d2-icon-svg/index.vue')) Vue.component('importExcel', () => import('./importExcel/index.vue')) +Vue.component('foreignKey', () => import('./foreign-key/index.vue')) +Vue.component('manyToMany', () => import('./many-to-many/index.vue')) diff --git a/web/src/components/many-to-many/README.md b/web/src/components/many-to-many/README.md new file mode 100644 index 0000000..516302e --- /dev/null +++ b/web/src/components/many-to-many/README.md @@ -0,0 +1,34 @@ +# 多对多表格显示配置说明 +本组件用于多对多返回数据使用,例如:角色信息 +```angular2html +role_info = [ + {"id":1,"name":"普通用户"}, + {"id":2,"name":"管理员"} +] + +#crud的配置 +component: { +name: 'manyToMany', +valueBinding: 'role_info', +children: 'name' +} +``` +## crud.js +``` + { + component: { + name: 'manyToMany', + valueBinding: 'role_name', + children: 'name' + } + } +``` + +## 配置说明 + + +| Name | Description | Type | Required | Default | +| ---------- | ---------------- | ------- | -------- | -------------- | +| name | 字段所使用的组件 | String | true | manyToMany | +| valueBinding | row中的key | String | true | - | +| children | 数组中的key | String | true | name | diff --git a/web/src/components/many-to-many/index.js b/web/src/components/many-to-many/index.js new file mode 100644 index 0000000..e8ba588 --- /dev/null +++ b/web/src/components/many-to-many/index.js @@ -0,0 +1,15 @@ +import { d2CrudPlus } from 'd2-crud-plus' +import group from './group' + +function install (Vue, options) { + Vue.component('many-to-many', () => import('./index')) + if (d2CrudPlus != null) { + // 注册字段类型`demo-extend` + d2CrudPlus.util.columnResolve.addTypes(group) + } +} + +// 导出install, 通过`vue.use(D2pDemoExtend)`安装后 ,`demo-extend` 就可以在`crud.js`中使用了 +export default { + install +} diff --git a/web/src/components/many-to-many/index.vue b/web/src/components/many-to-many/index.vue new file mode 100644 index 0000000..e3c6f4e --- /dev/null +++ b/web/src/components/many-to-many/index.vue @@ -0,0 +1,60 @@ + + diff --git a/web/src/components/table-selector/table-selector.vue b/web/src/components/table-selector/table-selector.vue index 0aeda52..45833ad 100644 --- a/web/src/components/table-selector/table-selector.vue +++ b/web/src/components/table-selector/table-selector.vue @@ -201,6 +201,7 @@ export default { // this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true) // } // this.initData() + console.log(this) }, computed: { _elProps () { @@ -327,32 +328,23 @@ export default { this.$emit('current-change', event) }, openDialog () { - if (this.disabled) { + const that = this + if (that.disabled) { return } - this.dialogVisible = true - setTimeout(() => { - if (this.selected != null) { - const ids = this.selected.map( - (item) => item[this._elProps.props.value] - ) - ids.forEach((id) => { - const current = this.$refs.elTree.store.nodesMap[id] - if (current != null) { - this.doExpandParent(current) - } - }) - this.$nextTick(() => { - if (this.multiple) { - // this.$refs.elTree.setCheckedKeys(ids, this.leafOnly); - this.$refs.elTree.setCheckboxRow(ids) - } else if (ids.length > 0) { - // this.$refs.elTree.setCurrentKey(ids[0]); - this.$refs.elTree.setRadioRow(ids[0]) - } - }) - } - }, 1) + that.dialogVisible = true + if (that.value != null) { + that.$nextTick(() => { + const refs = Object.assign({}, that.$refs) + const { elTree } = refs + console.log(elTree) + if (that.multiple) { + elTree.setCheckboxRow(that.selected, true) + } else { + elTree.setRadioRow(that.selected[0]) + } + }) + } }, doExpandParent (node) { if (node.parent != null) { @@ -412,20 +404,6 @@ export default { // 获取选中的行数据 refreshSelected () { let nodes = null - // if (this.multiple) { - // nodes = this.$refs.elTree.getCheckedNodes( - // this.leafOnly, - // this.includeHalfChecked - // ); - // } else { - // const node = this.$refs.elTree.getCurrentNode(); - // if (node == null) { - // nodes = []; - // } else { - // nodes = [node]; - // } - // } - if (this.multiple) { nodes = this.$refs.elTree.getCheckboxRecords() } else { diff --git a/web/src/install.js b/web/src/install.js index 4219c02..e6e2c96 100644 --- a/web/src/install.js +++ b/web/src/install.js @@ -256,6 +256,10 @@ Vue.prototype.commonEndColumns = function (param = {}) { create_datetime: { showForm: (param.create_datetime && param.create_datetime.showForm) !== undefined ? param.create_datetime.showForm : false, showTable: (param.create_datetime && param.create_datetime.showTable) !== undefined ? param.create_datetime.showTable : true + }, + is_deleted: { + showForm: (param.is_deleted && param.is_deleted.showForm) !== undefined ? param.is_deleted.showForm : false, + showTable: (param.is_deleted && param.is_deleted.showTable) !== undefined ? param.is_deleted.showTable : false } } return [ @@ -388,6 +392,23 @@ Vue.prototype.commonEndColumns = function (param = {}) { form: { disabled: !showData.create_datetime.showForm } + }, + { + title: '是否软删除', + key: 'is_deleted', + width: 160, + search: { + disabled: !showData.is_deleted.showForm, + }, + show: showData.is_deleted.showTable, + type: 'radio', + dict:{ + data:[{label:"是",value:true},{label:"否",value:false}] + }, + form: { + disabled: !showData.is_deleted.showForm, + + } } ] } diff --git a/web/src/layout/header-aside/layout.vue b/web/src/layout/header-aside/layout.vue index 5370c16..68e114f 100644 --- a/web/src/layout/header-aside/layout.vue +++ b/web/src/layout/header-aside/layout.vue @@ -160,8 +160,8 @@ export default { routerViewKey () { // 默认情况下 key 类似 __transition-n-/foo // 这里的字符串操作是为了最终 key 的格式和原来相同 类似 __transition-n-__stamp-time-/foo - const stamp = this.$route.meta[`__stamp-${this.$route.path}`] || '' - return `${stamp ? `__stamp-${stamp}-` : ''}${this.$route.path}` + const stamp = this.$route.meta[`__stamp-${this.$route.fullpath}`] || '' + return `${stamp ? `__stamp-${stamp}-` : ''}${this.$route.fullpath}` }, /** * @description 最外层容器的背景图片样式 diff --git a/web/src/libs/util.js b/web/src/libs/util.js index 0b00d8b..9d0abd1 100644 --- a/web/src/libs/util.js +++ b/web/src/libs/util.js @@ -2,11 +2,13 @@ import cookies from './util.cookies' import db from './util.db' import log from './util.log' import dayjs from 'dayjs' +import filterParams from './util.params' const util = { cookies, db, - log + log, + filterParams } /** diff --git a/web/src/libs/util.params.js b/web/src/libs/util.params.js new file mode 100644 index 0000000..d4ad979 --- /dev/null +++ b/web/src/libs/util.params.js @@ -0,0 +1,19 @@ +import util from '@/libs/util' + +/** + * 对请求参数进行过滤 + *@param that=>this + *@param array:其他字段数组 + */ +const filterParams = function (that, array) { + that.$nextTick(()=>{ + const arr = that.crud.columns + const columnKeys = arr.map(item => { + return item.key + }) + let newArray = [...columnKeys, array, 'id'] + newArray = [...new Set(newArray)] + that.crud.searchOptions.form.query = '{' + newArray.toString() + '}' + }) +} +export default filterParams diff --git a/web/src/main.js b/web/src/main.js index e60e997..36b5dfc 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -32,10 +32,14 @@ import 'vxe-table/lib/style.css' // md5加密 import md5 from 'js-md5' +//websocket +import websocket from '@/api/websocket' + // 核心插件 Vue.use(d2Admin) Vue.use(VXETable) Vue.prototype.$md5 = md5 +Vue.prototype.$websocket = websocket new Vue({ router, diff --git a/web/src/views/system/menuButton/crud.js b/web/src/views/system/menuButton/crud.js index e102e0b..4bff9d4 100644 --- a/web/src/views/system/menuButton/crud.js +++ b/web/src/views/system/menuButton/crud.js @@ -104,12 +104,13 @@ export const crudOptions = (vm) => { }, valueChange (key, value, form, { getColumn, mode, component, immediate, getComponent }) { if (value != null) { - console.log('component.dictOptions', component.dictOptions) + // console.log('component.dictOptions', component.dictOptions) const obj = component.dictOptions.find(item => { - console.log(item.label, value) + // console.log(item.label, value) return item.value === value }) if (obj && obj.value) { + form.name = obj.label form.value = obj.value } } diff --git a/web/src/views/system/role/api.js b/web/src/views/system/role/api.js index 5633a73..f215c52 100644 --- a/web/src/views/system/role/api.js +++ b/web/src/views/system/role/api.js @@ -14,7 +14,7 @@ export function GetList (query) { return request({ url: urlPrefix, method: 'get', - params: query + params: {...query} }) } diff --git a/web/src/views/system/user/api.js b/web/src/views/system/user/api.js index 5743af2..8f88c54 100644 --- a/web/src/views/system/user/api.js +++ b/web/src/views/system/user/api.js @@ -5,7 +5,7 @@ export function GetList (query) { return request({ url: urlPrefix, method: 'get', - params: query + params: { ...query } }) } @@ -29,7 +29,15 @@ export function DelObj (id) { return request({ url: urlPrefix + id + '/', method: 'delete', - data: { id } + data: { soft_delete: true } + }) +} + +export function BatchDel (keys) { + return request({ + url: urlPrefix + 'multiple_delete/', + method: 'delete', + data: { keys } }) } diff --git a/web/src/views/system/user/crud.js b/web/src/views/system/user/crud.js index 9838106..caaa4d0 100644 --- a/web/src/views/system/user/crud.js +++ b/web/src/views/system/user/crud.js @@ -1,7 +1,9 @@ import { request } from '@/api/service' import { urlPrefix as deptPrefix } from '../dept/api' +import util from '@/libs/util' export const crudOptions = (vm) => { + util.filterParams(vm, ['dept_name','role_info{name}']) return { pageOptions: { compact: true @@ -9,7 +11,12 @@ export const crudOptions = (vm) => { options: { height: '100%', tableType: 'vxe-table', - rowKey: true // 必须设置,true or false + rowKey: true, + rowId: 'id' + }, + selectionRow: { + align: 'center', + width: 46 }, rowHandle: { width: 240, @@ -128,6 +135,7 @@ export const crudOptions = (vm) => { placeholder: '请输入密码' }, value: vm.systemConfig('base.default_password'), + editDisabled: true, itemProps: { class: { yxtInput: true } } @@ -193,6 +201,10 @@ export const crudOptions = (vm) => { pagination: true, props: { multiple: false } } + }, + component: { + name: 'foreignKey', + valueBinding: 'dept_name' } }, { @@ -368,6 +380,11 @@ export const crudOptions = (vm) => { ] } } + }, + component: { + name: 'manyToMany', + valueBinding: 'role_info', + children: 'name' } } ].concat(vm.commonEndColumns({ diff --git a/web/src/views/system/user/index.vue b/web/src/views/system/user/index.vue index 6be1535..21293a0 100644 --- a/web/src/views/system/user/index.vue +++ b/web/src/views/system/user/index.vue @@ -21,9 +21,12 @@ > 新增 + + 批量删除 + 导出 @@ -42,6 +45,16 @@ @columns-filter-changed="handleColumnsFilterChanged" /> + + +