!66 正式发布v2.0.5版本

1. 新增:数据导出,可根据前端Search进行过滤导出
2. 新增:"软删除"字段,修改删除功能,可以使用软删除功能,修改查询,默认排除已软删除的数据
3. 优化:table-seletor选择器
4. 优化:导出数据自动设置列宽
5. 优化:数据导入导出功能
6. 修复:修改人不会在修改后改变问题
pull/67/head v2.0.5
dvadmin 2022-08-13 15:51:27 +00:00 committed by Gitee
commit 891da887a5
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
31 changed files with 618 additions and 103 deletions

View File

@ -9,7 +9,7 @@ def is_tenants_mode():
判断是否为租户模式 判断是否为租户模式
:return: :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(): def _get_all_dictionary():
from dvadmin.system.models import Dictionary from dvadmin.system.models import Dictionary
queryset = Dictionary.objects.filter(status=True, is_value=False) queryset = Dictionary.objects.filter(status=True, is_value=False)
data = [] data = []
for instance in queryset: for instance in queryset:
data.append({ data.append(
{
"id": instance.id, "id": instance.id,
"value": instance.value, "value": instance.value,
"children": list(Dictionary.objects.filter(parent=instance.id).filter(status=1). "children": list(
values('label', 'value', 'type', 'color')) Dictionary.objects.filter(parent=instance.id)
}) .filter(status=1)
.values("label", "value", "type", "color")
),
}
)
return {ele.get("value"): ele for ele in data} return {ele.get("value"): ele for ele in data}
def _get_all_system_config(): def _get_all_system_config():
data = {} data = {}
from dvadmin.system.models import SystemConfig 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: for system_config in system_config_obj:
value = system_config.get('value', '') value = system_config.get("value", "")
if value and system_config.get('form_item_type') == 7: if value and system_config.get("form_item_type") == 7:
value = value[0].get('url') value = value[0].get("url")
data[f"{system_config.get('parent__key')}.{system_config.get('key')}"] = value data[f"{system_config.get('parent__key')}.{system_config.get('key')}"] = value
return data return data
@ -50,12 +60,12 @@ def init_dictionary():
try: try:
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
for tenant in get_tenant_model().objects.filter(): for tenant in get_tenant_model().objects.filter():
with tenant_context(tenant): with tenant_context(tenant):
settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary() settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary()
else: else:
settings.DICTIONARY_CONFIG = _get_all_dictionary() settings.DICTIONARY_CONFIG = _get_all_dictionary()
print("初始化字典配置完成")
except Exception as e: except Exception as e:
print("请先进行数据库迁移!") print("请先进行数据库迁移!")
return return
@ -71,12 +81,12 @@ def init_system_config():
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
for tenant in get_tenant_model().objects.filter(): for tenant in get_tenant_model().objects.filter():
with tenant_context(tenant): with tenant_context(tenant):
settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config() settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config()
else: else:
settings.SYSTEM_CONFIG = _get_all_system_config() settings.SYSTEM_CONFIG = _get_all_system_config()
print("初始化系统配置完成")
except Exception as e: except Exception as e:
print("请先进行数据库迁移!") print("请先进行数据库迁移!")
return return
@ -89,6 +99,7 @@ def refresh_dictionary():
""" """
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
for tenant in get_tenant_model().objects.filter(): for tenant in get_tenant_model().objects.filter():
with tenant_context(tenant): with tenant_context(tenant):
settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary() settings.DICTIONARY_CONFIG[connection.tenant.schema_name] = _get_all_dictionary()
@ -103,6 +114,7 @@ def refresh_system_config():
""" """
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
for tenant in get_tenant_model().objects.filter(): for tenant in get_tenant_model().objects.filter():
with tenant_context(tenant): with tenant_context(tenant):
settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config() settings.SYSTEM_CONFIG[connection.tenant.schema_name] = _get_all_system_config()

View File

@ -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="用户账号") 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="邮箱") 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="电话") 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 = "登录日志"
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
ordering = ("-create_datetime",) 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",)

View File

@ -9,6 +9,7 @@ from dvadmin.system.views.file_list import FileViewSet
from dvadmin.system.views.login_log import LoginLogViewSet from dvadmin.system.views.login_log import LoginLogViewSet
from dvadmin.system.views.menu import MenuViewSet from dvadmin.system.views.menu import MenuViewSet
from dvadmin.system.views.menu_button import MenuButtonViewSet 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.operation_log import OperationLogViewSet
from dvadmin.system.views.role import RoleViewSet from dvadmin.system.views.role import RoleViewSet
from dvadmin.system.views.system_config import SystemConfigViewSet 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'file', FileViewSet)
system_url.register(r'api_white_list', ApiWhiteListViewSet) system_url.register(r'api_white_list', ApiWhiteListViewSet)
system_url.register(r'system_config', SystemConfigViewSet) system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center',MessageCenterViewSet)
urlpatterns = [ urlpatterns = [
path('user/export/', UserViewSet.as_view({'post': 'export_data', })), path('user/export/', UserViewSet.as_view({'post': 'export_data', })),

View File

@ -19,10 +19,17 @@ class DeptSerializer(CustomModelSerializer):
""" """
parent_name = serializers.CharField(read_only=True, source='parent.name') parent_name = serializers.CharField(read_only=True, source='parent.name')
has_children = serializers.SerializerMethodField() has_children = serializers.SerializerMethodField()
status_label = serializers.SerializerMethodField()
def get_has_children(self, obj: Dept): def get_has_children(self, obj: Dept):
return Dept.objects.filter(parent_id=obj.id).count() return Dept.objects.filter(parent_id=obj.id).count()
def get_status_label(self, instance):
status = instance.status
if status:
return "启用"
return "禁用"
class Meta: class Meta:
model = Dept model = Dept
fields = '__all__' fields = '__all__'

View File

@ -1,12 +1,14 @@
import hashlib import hashlib
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django_restql.fields import DynamicSerializerMethodField
from rest_framework import serializers from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from application import dispatch 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.json_response import ErrorResponse, DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator 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: class Meta:
model = Users model = Users
@ -26,6 +30,17 @@ class UserSerializer(CustomModelSerializer):
"post": {"required": False}, "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): class UsersInitSerializer(CustomModelSerializer):
""" """
@ -128,10 +143,14 @@ class ExportUserProfileSerializer(CustomModelSerializer):
last_login = serializers.DateTimeField( last_login = serializers.DateTimeField(
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
) )
dept__deptName = serializers.CharField(source="dept.deptName", default="") is_active = serializers.SerializerMethodField(read_only=True)
dept__owner = serializers.CharField(source="dept.owner", default="") 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) gender = serializers.CharField(source="get_gender_display", read_only=True)
def get_is_active(self, instance):
return "启用" if instance.is_active else "停用"
class Meta: class Meta:
model = Users model = Users
fields = ( fields = (
@ -142,8 +161,8 @@ class ExportUserProfileSerializer(CustomModelSerializer):
"gender", "gender",
"is_active", "is_active",
"last_login", "last_login",
"dept__deptName", "dept_name",
"dept__owner", "dept_owner",
) )
@ -157,15 +176,6 @@ class UserProfileImportSerializer(CustomModelSerializer):
data.save() data.save()
return data 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: class Meta:
model = Users model = Users
exclude = ( exclude = (
@ -222,11 +232,21 @@ class UserViewSet(CustomModelViewSet):
"name": "用户名称", "name": "用户名称",
"email": "用户邮箱", "email": "用户邮箱",
"mobile": "手机号码", "mobile": "手机号码",
"gender": "用户性别(男/女/未知)", "gender": {
"is_active": "帐号状态(启用/禁用)", "title": "用户性别",
"choices": {
"data": {"未知": 2, "": 1, "": 0},
}
},
"is_active": {
"title": "帐号状态",
"choices": {
"data": {"启用": True, "禁用": False},
}
},
"password": "登录密码", "password": "登录密码",
"dept": "部门ID", "dept": {"title": "部门", "choices": {"queryset": Dept.objects.filter(status=True), "values_name": "name"}},
"role": "角色ID", "role": {"title": "角色", "choices": {"queryset": Role.objects.filter(status=True), "values_name": "name"}},
} }
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])

View File

@ -1,37 +1,67 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import re
import openpyxl import openpyxl
from django.conf import settings 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文件 读取导入的excel文件
:param request: :param file_url:
:param field_data: 首行数据源 :param field_data: 首行数据源
:param data: 数据源 :param m2m_fields: 多对多字段
:param FilName: 文件名
:return: :return:
""" """
# 读取excel 文件 # 读取excel 文件
file_path_dir = os.path.join(settings.BASE_DIR, file_url) file_path_dir = os.path.join(settings.BASE_DIR, file_url)
workbook = openpyxl.load_workbook(file_path_dir) workbook = openpyxl.load_workbook(file_path_dir)
table = workbook[workbook.sheetnames[0]] 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的数据 # 创建一个空列表存储Excel的数据
tables = [] tables = []
for i, row in enumerate(range(table.max_row)): for i, row in enumerate(range(table.max_row)):
if i == 0: if i == 0:
continue continue
array = {} 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 cell_value = table.cell(row=row + 1, column=index + 2).value
print(cell_value)
# 由于excel导入数字类型后会出现数字加 .0 的,进行处理 # 由于excel导入数字类型后会出现数字加 .0 的,进行处理
if type(cell_value) is float and str(cell_value).split(".")[1] == "0": if type(cell_value) is float and str(cell_value).split(".")[1] == "0":
cell_value = int(str(cell_value).split(".")[0]) cell_value = int(str(cell_value).split(".")[0])
if type(cell_value) is str: if type(cell_value) is str:
cell_value = cell_value.strip(" \t\n\r") 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) tables.append(array)
return tables return tables

View File

@ -4,7 +4,8 @@ from urllib.parse import quote
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from openpyxl import Workbook 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 openpyxl.worksheet.table import Table, TableStyleInfo
from rest_framework.request import Request from rest_framework.request import Request
@ -15,13 +16,28 @@ from dvadmin.utils.request_util import get_verbose_name
class ImportSerializerMixin: class ImportSerializerMixin:
""" """
自定义导模板导入功能 自定义导模板导入功能
""" """
# 导入字段 # 导入字段
import_field_dict = {} import_field_dict = {}
# 导入序列化器 # 导入序列化器
import_serializer_class = None 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 事务,防止出错 @transaction.atomic # Django 事务,防止出错
def import_data(self, request: Request, *args, **kwargs): def import_data(self, request: Request, *args, **kwargs):
@ -44,10 +60,49 @@ class ImportSerializerMixin:
"Content-Disposition" "Content-Disposition"
] = f'attachment;filename={quote(str(f"导入{get_verbose_name(queryset)}模板.xlsx"))}' ] = f'attachment;filename={quote(str(f"导入{get_verbose_name(queryset)}模板.xlsx"))}'
wb = Workbook() wb = Workbook()
ws1 = wb.create_sheet("data", 1)
ws1.sheet_state = "hidden"
ws = wb.active ws = wb.active
row = get_column_letter(len(self.import_field_dict) + 1) row = get_column_letter(len(self.import_field_dict) + 1)
column = 10 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}") # 名称管理器 tab = Table(displayName="Table1", ref=f"A1:{row}{column}") # 名称管理器
style = TableStyleInfo( style = TableStyleInfo(
name="TableStyleLight11", name="TableStyleLight11",
@ -63,8 +118,14 @@ class ImportSerializerMixin:
updateSupport = request.data.get("updateSupport") updateSupport = request.data.get("updateSupport")
# 从excel中组织对应的数据结构然后使用序列化器保存 # 从excel中组织对应的数据结构然后使用序列化器保存
data = import_to_data(request.data.get("url"), self.import_field_dict)
queryset = self.filter_queryset(self.get_queryset()) 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 = [ unique_list = [
ele.attname for ele in queryset.model._meta.get_fields() if hasattr(ele, "unique") and ele.unique == True 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_field_label = []
# 导出序列化器 # 导出序列化器
export_serializer_class = None 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): 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"))}' response["Content-Disposition"] = f'attachment;filename={quote(str(f"导出{get_verbose_name(queryset)}.xlsx"))}'
wb = Workbook() wb = Workbook()
ws = wb.active 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) row = get_column_letter(len(self.export_field_label) + 1)
column = 1 column = 1
ws.append(["序号", *self.export_field_label]) ws.append(header_data)
for index, results in enumerate(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 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( style = TableStyleInfo(
name="TableStyleLight11", name="TableStyleLight11",
showFirstColumn=True, showFirstColumn=True,

View File

@ -10,12 +10,49 @@ import uuid
from django.apps import apps from django.apps import apps
from django.db import models from django.db import models
from django.db.models import QuerySet
from application import settings from application import settings
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀 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): 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="修改时间") 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="创建时间", create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间") verbose_name="创建时间")
is_deleted = models.BooleanField(verbose_name="是否软删除",help_text='是否软删除', default=False, db_index=True)
objects = SoftDeleteManager()
class Meta: class Meta:
abstract = True abstract = True
@ -37,6 +77,8 @@ class CoreModel(models.Model):
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def get_all_models_objects(model_name=None): def get_all_models_objects(model_name=None):
""" """
获取所有 models 对象 获取所有 models 对象

View File

@ -80,6 +80,9 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
if self.request: 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): if hasattr(self.instance, self.modifier_field_id):
setattr( setattr(
self.instance, self.modifier_field_id, self.get_request_user_id() self.instance, self.modifier_field_id, self.get_request_user_id()

View File

@ -92,6 +92,12 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
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) self.perform_destroy(instance)
return DetailResponse(data=[], msg="删除成功") return DetailResponse(data=[], msg="删除成功")

View File

@ -1,5 +1,4 @@
# 构建预览页面 # 预览环境
# 指定构建模式 # 指定构建模式
NODE_ENV=production NODE_ENV=production

15
web/.env.production Normal file
View File

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

View File

@ -213,13 +213,14 @@ const refreshTken = function () {
* 下载文件 * 下载文件
* @param url * @param url
* @param params * @param params
* @param method
* @param filename * @param filename
*/ */
export const downloadFile = function ({ url, data, method, filename }) { export const downloadFile = function ({ url, params, method, filename }) {
request({ request({
url: url, url: url,
method: method, method: method,
data: data, params: params,
responseType: 'blob' responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'} // headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then(res => { }).then(res => {

View File

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

View File

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

View File

@ -0,0 +1,42 @@
<template>
<el-tag :type="color">{{ currentValue }}</el-tag>
</template>
<script>
//
//
export default {
name: 'foreign-key',
props: {
color: {
require: false
},
value: {
type: [String, Number],
required: false
}
},
data () {
return {
currentValue: ''
}
},
watch:{
value(nv,ov){
const { row } = this.$parent.scope
const valueBinding = this.$parent.valueBinding
this.setValue(row[valueBinding])
}
},
created () {
const { row } = this.$parent.scope
const valueBinding = this.$parent.valueBinding
this.setValue(row[valueBinding])
},
methods: {
setValue (value) {
// value
this.currentValue = value
}
}
}
</script>

View File

@ -7,3 +7,5 @@ Vue.component('d2-container', d2Container)
Vue.component('d2-icon', () => import('./d2-icon')) Vue.component('d2-icon', () => import('./d2-icon'))
Vue.component('d2-icon-svg', () => import('./d2-icon-svg/index.vue')) Vue.component('d2-icon-svg', () => import('./d2-icon-svg/index.vue'))
Vue.component('importExcel', () => import('./importExcel/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'))

View File

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

View File

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

View File

@ -0,0 +1,60 @@
<template>
<div>
<el-tag style="margin-right: 10px" :type="color" v-for="(item,index) in currentValue" :key="index">{{
item[key]
}}
</el-tag>
</div>
</template>
<script>
//
//
export default {
name: 'many-to-many',
props: {
color: {
require: false
},
value: {
type: Array,
required: false
}
},
data () {
return {
currentValue: [],
key: 'name'
}
},
watch: {
value (nv, ov) {
const { row } = this.$parent.scope
const { children } = this.$parent
if (children) {
const valueBinding = this.$parent.valueBinding
this.setValue(row[valueBinding])
this.key = children
} else {
this.setValue([])
}
}
},
created () {
const { row } = this.$parent.scope
const { children } = this.$parent
if (children) {
const valueBinding = this.$parent.valueBinding
this.setValue(row[valueBinding])
this.key = children
} else {
this.setValue([])
}
},
methods: {
setValue (value) {
// value
this.currentValue = value
}
}
}
</script>

View File

@ -201,6 +201,7 @@ export default {
// this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true) // this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true)
// } // }
// this.initData() // this.initData()
console.log(this)
}, },
computed: { computed: {
_elProps () { _elProps () {
@ -327,32 +328,23 @@ export default {
this.$emit('current-change', event) this.$emit('current-change', event)
}, },
openDialog () { openDialog () {
if (this.disabled) { const that = this
if (that.disabled) {
return return
} }
this.dialogVisible = true that.dialogVisible = true
setTimeout(() => { if (that.value != null) {
if (this.selected != null) { that.$nextTick(() => {
const ids = this.selected.map( const refs = Object.assign({}, that.$refs)
(item) => item[this._elProps.props.value] const { elTree } = refs
) console.log(elTree)
ids.forEach((id) => { if (that.multiple) {
const current = this.$refs.elTree.store.nodesMap[id] elTree.setCheckboxRow(that.selected, true)
if (current != null) { } else {
this.doExpandParent(current) elTree.setRadioRow(that.selected[0])
}
})
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)
}, },
doExpandParent (node) { doExpandParent (node) {
if (node.parent != null) { if (node.parent != null) {
@ -412,20 +404,6 @@ export default {
// //
refreshSelected () { refreshSelected () {
let nodes = null 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) { if (this.multiple) {
nodes = this.$refs.elTree.getCheckboxRecords() nodes = this.$refs.elTree.getCheckboxRecords()
} else { } else {

View File

@ -256,6 +256,10 @@ Vue.prototype.commonEndColumns = function (param = {}) {
create_datetime: { create_datetime: {
showForm: (param.create_datetime && param.create_datetime.showForm) !== undefined ? param.create_datetime.showForm : false, 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 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 [ return [
@ -388,6 +392,23 @@ Vue.prototype.commonEndColumns = function (param = {}) {
form: { form: {
disabled: !showData.create_datetime.showForm 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,
}
} }
] ]
} }

View File

@ -160,8 +160,8 @@ export default {
routerViewKey () { routerViewKey () {
// key __transition-n-/foo // key __transition-n-/foo
// key __transition-n-__stamp-time-/foo // key __transition-n-__stamp-time-/foo
const stamp = this.$route.meta[`__stamp-${this.$route.path}`] || '' const stamp = this.$route.meta[`__stamp-${this.$route.fullpath}`] || ''
return `${stamp ? `__stamp-${stamp}-` : ''}${this.$route.path}` return `${stamp ? `__stamp-${stamp}-` : ''}${this.$route.fullpath}`
}, },
/** /**
* @description 最外层容器的背景图片样式 * @description 最外层容器的背景图片样式

View File

@ -2,11 +2,13 @@ import cookies from './util.cookies'
import db from './util.db' import db from './util.db'
import log from './util.log' import log from './util.log'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import filterParams from './util.params'
const util = { const util = {
cookies, cookies,
db, db,
log log,
filterParams
} }
/** /**

View File

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

View File

@ -32,10 +32,14 @@ import 'vxe-table/lib/style.css'
// md5加密 // md5加密
import md5 from 'js-md5' import md5 from 'js-md5'
//websocket
import websocket from '@/api/websocket'
// 核心插件 // 核心插件
Vue.use(d2Admin) Vue.use(d2Admin)
Vue.use(VXETable) Vue.use(VXETable)
Vue.prototype.$md5 = md5 Vue.prototype.$md5 = md5
Vue.prototype.$websocket = websocket
new Vue({ new Vue({
router, router,

View File

@ -104,12 +104,13 @@ export const crudOptions = (vm) => {
}, },
valueChange (key, value, form, { getColumn, mode, component, immediate, getComponent }) { valueChange (key, value, form, { getColumn, mode, component, immediate, getComponent }) {
if (value != null) { if (value != null) {
console.log('component.dictOptions', component.dictOptions) // console.log('component.dictOptions', component.dictOptions)
const obj = component.dictOptions.find(item => { const obj = component.dictOptions.find(item => {
console.log(item.label, value) // console.log(item.label, value)
return item.value === value return item.value === value
}) })
if (obj && obj.value) { if (obj && obj.value) {
form.name = obj.label
form.value = obj.value form.value = obj.value
} }
} }

View File

@ -14,7 +14,7 @@ export function GetList (query) {
return request({ return request({
url: urlPrefix, url: urlPrefix,
method: 'get', method: 'get',
params: query params: {...query}
}) })
} }

View File

@ -5,7 +5,7 @@ export function GetList (query) {
return request({ return request({
url: urlPrefix, url: urlPrefix,
method: 'get', method: 'get',
params: query params: { ...query }
}) })
} }
@ -29,7 +29,15 @@ export function DelObj (id) {
return request({ return request({
url: urlPrefix + id + '/', url: urlPrefix + id + '/',
method: 'delete', method: 'delete',
data: { id } data: { soft_delete: true }
})
}
export function BatchDel (keys) {
return request({
url: urlPrefix + 'multiple_delete/',
method: 'delete',
data: { keys }
}) })
} }

View File

@ -1,7 +1,9 @@
import { request } from '@/api/service' import { request } from '@/api/service'
import { urlPrefix as deptPrefix } from '../dept/api' import { urlPrefix as deptPrefix } from '../dept/api'
import util from '@/libs/util'
export const crudOptions = (vm) => { export const crudOptions = (vm) => {
util.filterParams(vm, ['dept_name','role_info{name}'])
return { return {
pageOptions: { pageOptions: {
compact: true compact: true
@ -9,7 +11,12 @@ export const crudOptions = (vm) => {
options: { options: {
height: '100%', height: '100%',
tableType: 'vxe-table', tableType: 'vxe-table',
rowKey: true // 必须设置true or false rowKey: true,
rowId: 'id'
},
selectionRow: {
align: 'center',
width: 46
}, },
rowHandle: { rowHandle: {
width: 240, width: 240,
@ -128,6 +135,7 @@ export const crudOptions = (vm) => {
placeholder: '请输入密码' placeholder: '请输入密码'
}, },
value: vm.systemConfig('base.default_password'), value: vm.systemConfig('base.default_password'),
editDisabled: true,
itemProps: { itemProps: {
class: { yxtInput: true } class: { yxtInput: true }
} }
@ -193,6 +201,10 @@ export const crudOptions = (vm) => {
pagination: true, pagination: true,
props: { multiple: false } 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({ ].concat(vm.commonEndColumns({

View File

@ -21,9 +21,12 @@
> >
<i class="el-icon-plus" /> 新增 <i class="el-icon-plus" /> 新增
</el-button> </el-button>
<el-button size="small" type="danger" @click="batchDelete">
<i class="el-icon-delete"></i> 批量删除
</el-button>
<el-button <el-button
size="small" size="small"
type="danger" type="warning"
@click="onExport" @click="onExport"
v-permission="'Export'" v-permission="'Export'"
><i class="el-icon-download" /> 导出 ><i class="el-icon-download" /> 导出
@ -42,6 +45,16 @@
@columns-filter-changed="handleColumnsFilterChanged" @columns-filter-changed="handleColumnsFilterChanged"
/> />
</div> </div>
<span slot="PaginationPrefixSlot" class="prefix">
<el-button
class="square"
size="mini"
title="批量删除"
@click="batchDelete"
icon="el-icon-delete"
:disabled="!multipleSelection || multipleSelection.length == 0"
/>
</span>
</d2-crud-x> </d2-crud-x>
<el-dialog <el-dialog
title="密码重置" title="密码重置"
@ -81,7 +94,6 @@
import * as api from './api' import * as api from './api'
import { crudOptions } from './crud' import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus' import { d2CrudPlus } from 'd2-crud-plus'
export default { export default {
name: 'user', name: 'user',
mixins: [d2CrudPlus.crud], mixins: [d2CrudPlus.crud],
@ -144,13 +156,18 @@ export default {
delRequest (row) { delRequest (row) {
return api.DelObj(row.id) return api.DelObj(row.id)
}, },
batchDelRequest (ids) {
return api.BatchDel(ids)
},
onExport () { onExport () {
const that = this
this.$confirm('是否确认导出所有数据项?', '警告', { this.$confirm('是否确认导出所有数据项?', '警告', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(function () { }).then(function () {
return api.exportData() const query = that.getSearch().getForm()
return api.exportData({ ...query })
}) })
}, },
// //