!76 正式发布v2.0.6版本

1. 优化:用户管理中对部门信息的返回
2. 优化:baseURL
3. 修复:系统配置中,radio保存的值错误
4. 优化:导出功能
5. 新增:部门model中加入key字段,可以用于初始化用户
6. 新增:加入channels做websocket
7. 修复:对象.delete会导致软删除不生效的bug
8. 优化:系统配置,区分前台配置和后台配置
pull/77/head v2.0.6
dvadmin 2022-11-03 16:13:05 +00:00 committed by Gitee
commit 6e8f64bea6
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
43 changed files with 721 additions and 12148 deletions

View File

@ -40,7 +40,7 @@ def _get_all_system_config():
from dvadmin.system.models import SystemConfig
system_config_obj = (
SystemConfig.objects.filter(status=True, parent_id__isnull=False)
SystemConfig.objects.filter(parent_id__isnull=False)
.values("parent__key", "key", "value", "form_item_type")
.order_by("sort")
)
@ -131,6 +131,8 @@ def get_dictionary_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if not settings.DICTIONARY_CONFIG:
refresh_dictionary()
if is_tenants_mode():
dictionary_config = settings.DICTIONARY_CONFIG[schema_name or connection.tenant.schema_name]
else:
@ -175,6 +177,8 @@ def get_system_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if not settings.SYSTEM_CONFIG:
refresh_system_config()
if is_tenants_mode():
dictionary_config = settings.SYSTEM_CONFIG[schema_name or connection.tenant.schema_name]
else:

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from dvadmin.system import routing as dvadminRouting
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter(
dvadminRouting.websocket_urlpatterns# 指明路由文件是devops/routing.py
)
),
})

View File

@ -57,6 +57,7 @@ INSTALLED_APPS = [
"dvadmin.system",
"drf_yasg",
"captcha",
'channels',
]
MIDDLEWARE = [
@ -164,6 +165,19 @@ CORS_ORIGIN_ALLOW_ALL = True
# 允许cookie
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中后端是否支持对cookie的操作
# ================================================= #
# ********************* channels配置 ******************* #
# ================================================= #
ASGI_APPLICATION = 'application.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)], #需修改
},
},
}
# ================================================= #
# ********************* 日志配置 ******************* #
# ================================================= #

View File

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
import urllib
from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer
import json
from jwt import InvalidSignatureError
from application import settings
from dvadmin.system.models import MessageCenter
send_dict = {}
# 发送消息结构体
def message(sender, msg_type, msg):
text = {
'sender': sender,
'contentType': msg_type,
'content': msg,
}
return text
#异步获取消息中心的目标用户
@database_sync_to_async
def _get_message_center_instance(message_id):
_MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user',flat=True)
if _MessageCenter:
return _MessageCenter
else:
return []
def request_data(scope):
query_string = scope.get('query_string', b'').decode('utf-8')
qs = urllib.parse.parse_qs(query_string)
return qs
class DvadminWebSocket(AsyncJsonWebsocketConsumer):
async def connect(self):
try:
import jwt
self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"]
params = request_data(self.scope)
room = params.get('room')[0]
decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"])
if decoded_result:
self.user_id = decoded_result.get('user_id')
self.chat_group_name = room
#收到连接时候处理,
await self.channel_layer.group_add(
self.chat_group_name,
self.channel_name
)
# 将该客户端的信息发送函数与客户端的唯一身份标识绑定,保存至自定义的字典中
if len(send_dict)==0:
send_dict.setdefault(self.chat_group_name, {})
for room in send_dict.keys():
if room == self.chat_group_name:
send_dict[self.chat_group_name][self.user_id] = self.send
else:
send_dict.setdefault(self.chat_group_name,{})
await self.accept()
await self.send_json(message('system', 'INFO', '连接成功'))
except InvalidSignatureError:
await self.disconnect(None)
async def disconnect(self, close_code):
# 删除 send_dict 中对应的信息
del send_dict[self.chat_group_name][self.user_id]
# Leave room group
await self.channel_layer.group_discard(self.chat_group_name, self.channel_name)
print("连接关闭")
await self.close(close_code)
async def receive(self, text_data=None, byte_text_data=None):
print(text_data)
try:
text_data_json = json.loads(text_data)
except Exception as e:
print('数据无法被json格式化', e)
await self.disconnect(400)
else:
print(123,text_data_json)
# 获取将要推送信息的目标身份标识,调用保存在 send_dict中的信息发送函数
message_id = text_data_json.get('message_id', None)
user_list = await _get_message_center_instance(message_id)
for send_user in user_list:
await send_dict[self.chat_group_name][send_user](text_data=json.dumps(text_data_json))

View File

@ -1,6 +1,7 @@
[
{
"name": "DVAdmin团队",
"key": "dvadmin",
"sort": 1,
"owner": "",
"phone": "",
@ -10,6 +11,7 @@
"children": [
{
"name": "运营部",
"key": "",
"sort": 2,
"owner": "",
"phone": "",
@ -20,6 +22,7 @@
},
{
"name": "技术部",
"key": "technology",
"sort": 1,
"owner": "",
"phone": "",

View File

@ -7,5 +7,14 @@
"admin": true,
"data_range": 3,
"remark": null
},
{
"name": "用户",
"key": "public",
"sort": 2,
"status": true,
"admin": true,
"data_range": 3,
"remark": null
}
]

View File

@ -1,4 +1,25 @@
[
{
"username": "superadmin",
"email": "dvadmin@django-vue-admin.com",
"mobile": "13333333333",
"avatar": null,
"name": "超级管理员",
"gender": 1,
"user_type": 0,
"role": [],
"role_key": [
"admin"
],
"dept_key": "dvadmin",
"first_name": "",
"last_name": "",
"is_staff": true,
"is_active": true,
"password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=",
"last_login": null,
"is_superuser": true
},
{
"username": "admin",
"email": "dvadmin@django-vue-admin.com",
@ -7,7 +28,6 @@
"name": "管理员",
"gender": 1,
"user_type": 0,
"dept": 1,
"role": [],
"first_name": "",
"last_name": "",
@ -18,21 +38,22 @@
"is_superuser": false
},
{
"username": "superadmin",
"username": "test",
"email": "dvadmin@django-vue-admin.com",
"mobile": "13333333333",
"avatar": null,
"name": "超级管理员",
"mobile": "18888888888",
"avatar": "",
"name": "测试人员",
"gender": 1,
"user_type": 0,
"dept": 1,
"role": [],
"role_key": ["public"],
"dept_key": "technology",
"first_name": "",
"last_name": "",
"is_staff": true,
"is_active": true,
"password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=",
"last_login": null,
"is_superuser": true
"is_superuser": false
}
]

View File

@ -22,7 +22,7 @@ class Initialize(CoreInitialize):
"""
初始化部门信息
"""
self.init_base(DeptInitSerializer, unique_fields=['name', 'parent'])
self.init_base(DeptInitSerializer, unique_fields=['name', 'parent','key'])
def init_role(self):
"""

View File

@ -103,6 +103,7 @@ class Role(CoreModel):
class Dept(CoreModel):
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
key = models.CharField(max_length=64, unique=True,null=True,blank=True, verbose_name="关联字符", help_text="关联字符")
sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
@ -259,7 +260,7 @@ def media_file_name(instance, filename):
class FileList(CoreModel):
name = models.CharField(max_length=50, null=True, blank=True, verbose_name="名称", help_text="名称")
name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称")
url = models.FileField(upload_to=media_file_name)
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from django.urls import path
from . import consumers
websocket_urlpatterns = [
path('ws/<str:service_uid>/', consumers.DvadminWebSocket.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者
]

View File

@ -6,9 +6,11 @@
@Remark: 部门管理
"""
from rest_framework import serializers
from rest_framework.decorators import action
from dvadmin.system.models import Dept
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.permission import AnonymousUserPermission
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@ -18,18 +20,17 @@ class DeptSerializer(CustomModelSerializer):
部门-序列化器
"""
parent_name = serializers.CharField(read_only=True, source='parent.name')
has_children = serializers.SerializerMethodField()
status_label = serializers.SerializerMethodField()
has_children = serializers.SerializerMethodField()
def get_status_label(self, obj: Dept):
if obj.status:
return "启用"
return "禁用"
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__'
@ -58,7 +59,8 @@ class DeptInitSerializer(CustomModelSerializer):
menu_data['parent'] = instance.id
filter_data = {
"name": menu_data['name'],
"parent": menu_data['parent']
"parent": menu_data['parent'],
"key": menu_data['key']
}
instance_obj = Dept.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
@ -72,7 +74,7 @@ class DeptInitSerializer(CustomModelSerializer):
class Meta:
model = Dept
fields = ['name', 'sort', 'owner', 'phone', 'email', 'status', 'parent', 'creator', 'dept_belong_id',
'children']
'children', 'key']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
@ -122,7 +124,13 @@ class DeptViewSet(CustomModelViewSet):
if lazy:
# 如果懒加载模式,返回全部
if not parent:
if self.request.user.is_superuser:
role_list = request.user.role.filter(status=1).values("admin", "data_range")
is_admin = False
for ele in role_list:
if 3 == ele.get("data_range") or ele.get("admin") == True:
is_admin = True
break
if self.request.user.is_superuser or is_admin:
queryset = queryset.filter(parent__isnull=True)
else:
queryset = queryset.filter(id=self.request.user.dept_id)
@ -146,3 +154,10 @@ class DeptViewSet(CustomModelViewSet):
queryset = queryset.filter(id=self.request.user.dept_id)
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')
return DetailResponse(data=data, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[AnonymousUserPermission])
def all_dept(self, request, *args, **kwargs):
self.extra_filter_backends = []
queryset = self.filter_queryset(self.get_queryset())
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')
return DetailResponse(data=data, msg="获取成功")

View File

@ -91,6 +91,16 @@ class LoginSerializer(TokenObtainPairSerializer):
data["name"] = self.user.name
data["userId"] = self.user.id
data["avatar"] = self.user.avatar
dept = getattr(self.user, 'dept', None)
if dept:
data['dept_info'] = {
'dept_id': dept.id,
'dept_name': dept.name,
'dept_key': dept.key
}
role = getattr(self.user, 'role', None)
if role:
data['role_info'] = role.values('id', 'name', 'key')
request = self.context.get("request")
request.user = self.user
# 记录登录日志

View File

@ -250,9 +250,29 @@ class InitSettingsViewSet(APIView):
authentication_classes = []
permission_classes = []
def filter_system_config_values(self, data: dict):
"""
过滤系统初始化配置
:param data:
:return:
"""
if not self.request.query_params.get('key', ''):
return data
new_data = {}
for key in self.request.query_params.get('key', '').split('|'):
if key:
new_data.update(**dict(filter(lambda x: x[0].startswith(key), data.items())))
return new_data
def get(self, request):
data = dispatch.get_system_config()
if not data:
dispatch.refresh_system_config()
data = dispatch.get_system_config()
# 不返回后端专用配置
backend_config = [f"{ele.get('parent__key')}.{ele.get('key')}" for ele in
SystemConfig.objects.filter(status=False, parent_id__isnull=False).values('parent__key',
'key')]
data = dict(filter(lambda x: x[0] not in backend_config, data.items()))
data = self.filter_system_config_values(data=data)
return DetailResponse(data=data)

View File

@ -3,7 +3,7 @@ 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.decorators import action, permission_classes
from rest_framework.permissions import IsAuthenticated
from application import dispatch
@ -15,12 +15,24 @@ from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet
def recursion(instance,parent,result):
new_instance = getattr(instance,parent,None)
res = []
data = getattr(instance, result, None)
if data:
res.append(data)
if new_instance:
array = recursion(new_instance,parent,result)
res+=(array)
return res
class UserSerializer(CustomModelSerializer):
"""
用户管理-序列化器
"""
dept_name = serializers.CharField(source='dept.name', read_only=True)
role_info = DynamicSerializerMethodField()
dept_name_all = serializers.SerializerMethodField()
class Meta:
model = Users
@ -30,6 +42,11 @@ class UserSerializer(CustomModelSerializer):
"post": {"required": False},
}
def get_dept_name_all(self, instance):
dept_name_all = recursion(instance.dept, "parent", "name")
dept_name_all.reverse()
return "/".join(dept_name_all)
def get_role_info(self, instance, parsed_query):
roles = instance.role.all()
# You can do what ever you want in here
@ -46,6 +63,16 @@ class UsersInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
def save(self, **kwargs):
instance = super().save(**kwargs)
role_key = self.initial_data.get('role_key',[])
role_ids = Role.objects.filter(key__in=role_key).values_list('id',flat=True)
instance.role.set(role_ids)
dept_key = self.initial_data.get('dept_key',None)
dept_id = Dept.objects.filter(key=dept_key).first()
instance.dept = dept_id
instance.save()
return instance
class Meta:
model = Users
@ -213,17 +240,17 @@ class UserViewSet(CustomModelViewSet):
}
search_fields = ["username", "name", "gender", "dept__name", "role__name"]
# 导出
export_field_label = [
"用户账号",
"用户名称",
"用户邮箱",
"手机号码",
"用户性别",
"帐号状态",
"最后登录时间",
"部门名称",
"部门负责人",
]
export_field_label = {
"username":"用户账号",
"name":"用户名称",
"email":"用户邮箱",
"mobile":"手机号码",
"gender":"用户性别",
"is_active":"帐号状态",
"last_login":"最后登录时间",
"dept_name":"部门名称",
"dept_owner":"部门负责人",
}
export_serializer_class = ExportUserProfileSerializer
# 导入
import_serializer_class = UserProfileImportSerializer
@ -254,12 +281,26 @@ class UserViewSet(CustomModelViewSet):
"""获取当前用户信息"""
user = request.user
result = {
"id": user.id,
"name": user.name,
"mobile": user.mobile,
"user_type": user.user_type,
"gender": user.gender,
"email": user.email,
"avatar": user.avatar,
"dept": user.dept.id,
"is_superuser": user.is_superuser,
"role": user.role.values_list('id', flat=True),
}
dept = getattr(user, 'dept', None)
if dept:
result['dept_info'] = {
'dept_id': dept.id,
'dept_name': dept.name
}
role = getattr(user, 'role', None)
if role:
result['role_info'] = role.values('id', 'name', 'key')
return DetailResponse(data=result, msg="获取成功")
@action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated])
@ -272,22 +313,23 @@ class UserViewSet(CustomModelViewSet):
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
def change_password(self, request, *args, **kwargs):
"""密码修改"""
instance = Users.objects.filter(id=kwargs.get("pk")).first()
data = request.data
old_pwd = data.get("oldPassword")
new_pwd = data.get("newPassword")
new_pwd2 = data.get("newPassword2")
if instance:
if old_pwd is None or new_pwd is None or new_pwd2 is None:
return ErrorResponse(msg="参数不能为空")
if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配")
elif instance.check_password(old_pwd):
instance.password = make_password(new_pwd)
instance.save()
check_password = request.user.check_password(old_pwd)
if not check_password:
check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest())
if check_password:
request.user.password = make_password(new_pwd)
request.user.save()
return DetailResponse(data=None, msg="修改成功")
else:
return ErrorResponse(msg="旧密码不正确")
else:
return ErrorResponse(msg="未获取到用户")
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
def reset_to_default_password(self, request, *args, **kwargs):

View File

@ -26,6 +26,21 @@ class ImportSerializerMixin:
# 表格表头最大宽度默认50个字符
export_column_width = 50
def is_number(self,num):
try:
float(num)
return True
except ValueError:
pass
try:
import unicodedata
unicodedata.numeric(num)
return True
except (TypeError, ValueError):
pass
return False
def get_string_len(self, string):
"""
获取字符串最大长度
@ -35,6 +50,8 @@ class ImportSerializerMixin:
length = 4
if string is None:
return length
if self.is_number(string):
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
@ -155,6 +172,21 @@ class ExportSerializerMixin:
# 表格表头最大宽度默认50个字符
export_column_width = 50
def is_number(self,num):
try:
float(num)
return True
except ValueError:
pass
try:
import unicodedata
unicodedata.numeric(num)
return True
except (TypeError, ValueError):
pass
return False
def get_string_len(self, string):
"""
获取字符串最大长度
@ -164,6 +196,8 @@ class ExportSerializerMixin:
length = 4
if string is None:
return length
if self.is_number(string):
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
@ -176,36 +210,35 @@ class ExportSerializerMixin:
:param kwargs:
:return:
"""
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
queryset = self.filter_queryset(self.get_queryset())
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
data = self.export_serializer_class(queryset, many=True).data
# 导出excel 表
response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition"
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()
ws = wb.active
header_data = ["序号", *self.export_field_label]
header_data = ["序号", *self.export_field_label.values()]
hidden_header = ["#", *self.export_field_label.keys()]
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(header_data)
for index, results in enumerate(data):
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)
for h_index, h_item in enumerate(hidden_header):
for key,val in results.items():
if key == h_item:
if val is None or val=="":
results_list.append("")
else:
results_list.append(val)
# 计算最大列宽度
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)
result_column_width = self.get_string_len(val)
if h_index !=0 and result_column_width > df_len_max[h_index]:
df_len_max[h_index] = result_column_width
ws.append([index + 1, *results_list])
column += 1
#  更新列宽

View File

@ -76,6 +76,16 @@ class CoreModel(models.Model):
verbose_name = '核心模型'
verbose_name_plural = verbose_name
def delete(self, using=None, soft_delete=True, *args, **kwargs):
"""
Soft delete object (set its ``is_deleted`` field to True).
Actually delete object if setting ``soft`` to False.
"""
if soft_delete:
self.is_deleted = True
self.save(using=using)
else:
return super(CoreModel, self).delete(using=using, *args, **kwargs)

View File

@ -38,7 +38,7 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
extra_filter_backends = [DataLevelPermissionsFilter]
permission_classes = [CustomPermission]
import_field_dict = {}
export_field_label = []
export_field_label = {}
def filter_queryset(self, queryset):
for backend in set(set(self.filter_backends) | set(self.extra_filter_backends or [])):

View File

@ -1,4 +1,4 @@
asgiref==3.3.4
asgiref==3.5.2
certifi==2021.5.30
chardet==4.0.0
coreapi==2.3.3
@ -41,3 +41,5 @@ urllib3==1.26.6
user-agents==2.2.0
whitenoise==5.3.0
openpyxl==3.0.9
channels==3.0.5
channels-redis==3.4.1

View File

@ -6,6 +6,9 @@ VUE_APP_TITLE=D2Admin
# 网络请求公用地址
VUE_APP_API=/api/
# websocket地址
VUE_APP_WEBSOCKET=""
# 仓库地址
VUE_APP_REPO=https://github.com/d2-projects/d2-admin-start-kit

View File

@ -6,4 +6,5 @@ VUE_APP_TITLE=企业级后台管理系统
VUE_APP_PM_ENABLED = true
# 后端接口地址及端口(域名)
VUE_APP_API = "http://127.0.0.1:8000"
VUE_APP_WEBSOCKET = "ws://127.0.0.1:8000"

View File

@ -1,6 +1,6 @@
{
"name": "django-vue-admin",
"version": "2.0.4",
"version": "2.0.6",
"scripts": {
"serve": "vue-cli-service serve --open",
"start": "npm run serve",
@ -37,6 +37,7 @@
"lowdb": "^1.0.0",
"nprogress": "^0.2.0",
"qiankun": "^2.7.2",
"qrcodejs2": "^0.0.2",
"screenfull": "^5.0.2",
"sortablejs": "^1.10.1",
"ua-parser-js": "^0.7.20",

View File

@ -224,7 +224,7 @@ export const downloadFile = function ({ url, params, method, filename }) {
responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then(res => {
const fileName = window.decodeURI(res.headers['content-disposition'].split('=')[1]) || filename + '.xls' || '文件导出.xls'
const fileName = window.decodeURI(filename + '.xls' || res.headers['content-disposition'].split('=')[1]) || '文件导出.xls'
if (res) {
const blob = new Blob([res.data], { type: 'charset=utf-8' })
const elink = document.createElement('a')

View File

@ -3,7 +3,7 @@ import util from '@/libs/util'
function initWebSocket (e) {
const token = util.cookies.get('token')
if (token) {
const wsUri = 'ws://127.0.0.1:8000/?auth=' + token
const wsUri = process.env.VUE_APP_WEBSOCKET + '/ws/' + token + '/?room=message_center'
this.socket = new WebSocket(wsUri)// 这里面的this都指向vue
this.socket.onerror = webSocketOnError
this.socket.onmessage = webSocketOnMessage
@ -53,18 +53,13 @@ function webSocketOnMessage (e) {
// 关闭websiocket
function closeWebsocket () {
console.log('连接已关闭...')
close()
}
function close () {
this.socket.close() // 关闭 websocket
this.socket.onclose = function (e) {
console.log(e)// 监听关闭事件
console.log('关闭')
}
// close()
this.socket.close()
}
function webSocketSend (message) {
this.socket.send(JSON.stringify(message))
}
export default {
initWebSocket, close, webSocketSend
initWebSocket, closeWebsocket, webSocketSend
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1666798594366" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2218" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 2C230.2 2 2 230.2 2 512s228.2 510 510 510 510-228.2 510-510S793.3 2 512 2z m235.9 442c-1 4.6-3.6 10.8-7.2 19.1l-0.5 0.5c-21.6 45.8-77.3 135.5-77.3 135.5l-0.5-0.5-16.5 28.3h78.8L574.3 826.8l34-136h-61.8l21.6-90.2c-17.5 4.1-38.1 9.8-62.3 18 0 0-33 19.1-94.8-37.1 0 0-41.7-37.1-17.5-45.8 10.3-4.1 50-8.8 81.4-12.9 42.2-5.7 68.5-8.8 68.5-8.8s-130.3 2.1-161.2-3.1c-30.9-4.6-70.1-56.7-78.3-102 0 0-12.9-24.7 27.8-12.9 40.2 11.8 209.2 45.8 209.2 45.8S321.4 375 307 358.5c-14.4-16.5-42.8-89.6-39.2-134.5 0 0 1.5-11.3 12.9-8.2 0 0 161.8 74.2 272.5 114.4C664.5 371.4 760.8 392 747.9 444z" fill="#ffffff" p-id="2219"></path></svg>

After

Width:  |  Height:  |  Size: 957 B

View File

@ -0,0 +1,7 @@
function install (Vue) {
Vue.component('dept-format', () => import('./lib/dept-format'))
}
export default {
install
}

View File

@ -0,0 +1,47 @@
<template>
<div>
{{$store.state.d2admin.dept.data[currentValue] || ''}}
</div>
</template>
<script>
//
//
export default {
name: 'dept-format',
props: {
// row.xxx
value: {
type: Number || String,
required: false
},
color: {
require: false
}
},
data () {
return {
currentValue: ''
}
},
watch: {
value (value) {
// this.$emit('change', value)
if (this.currentValue === value) {
return
}
this.setValue(value)
}
},
created () {
this.setValue(this.value)
},
methods: {
setValue (value) {
// value
this.currentValue = String(this.value)
// key
}
}
}
</script>
Footer

View File

@ -9,3 +9,4 @@ 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'))
Vue.component('dept-format', () => import('./dept-format/lib/dept-format.vue'))

View File

@ -201,7 +201,7 @@ export default {
// this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true)
// }
// this.initData()
console.log(this)
this.searchTableData()
},
computed: {
_elProps () {

View File

@ -16,7 +16,6 @@ import { request } from '@/api/service'
import util from '@/libs/util'
import XEUtils from 'xe-utils'
import store from '@/store/index'
import { urlPrefix as deptPrefix } from '@/views/system/dept/api'
import types from '@/config/d2p-extends/types'
import { checkPlugins, plugins } from '@/views/plugins'
@ -243,7 +242,8 @@ Vue.prototype.commonEndColumns = function (param = {}) {
},
dept_belong_id: {
showForm: (param.dept_belong_id && param.dept_belong_id.showForm) !== undefined ? param.dept_belong_id.showForm : false,
showTable: (param.dept_belong_id && param.dept_belong_id.showTable) !== undefined ? param.dept_belong_id.showTable : false
showTable: (param.dept_belong_id && param.dept_belong_id.showTable) !== undefined ? param.dept_belong_id.showTable : false,
showSearch: (param.dept_belong_id && param.dept_belong_id.showSearch) !== undefined ? param.dept_belong_id.showSearch : false
},
modifier_name: {
showForm: (param.modifier_name && param.modifier_name.showForm) !== undefined ? param.modifier_name.showForm : false,
@ -293,61 +293,41 @@ Vue.prototype.commonEndColumns = function (param = {}) {
}
},
{
title: '数据归属部门',
title: '属部门',
key: 'dept_belong_id',
show: showData.dept_belong_id.showTable,
width: 150,
search: {
disabled: true
disabled: !showData.dept_belong_id.showSearch
},
type: 'table-selector',
type: 'tree-selector',
dict: {
cache: true,
url: deptPrefix,
isTree: true,
cache: false,
url: '/api/system/dept/all_dept/',
// isTree: true,
// dept: true,
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
children: 'children', // 数据字典中children字段的属性名
getData: (url, dict, {
_,
component
}) => {
return request({
url: url,
params: { limit: 999, status: 1 }
}).then(ret => {
return ret.data.data
})
}
children: 'children' // 数据字典中children字段的属性名
// getData: (url, dict, {
// _,
// component
// }) => {
// return request({
// url: url
// }).then(ret => {
// return XEUtils.toArrayTree(ret.data, { parentKey: 'parent', strict: false })
// })
// }
},
component: {
name: 'dept-format',
props: { multiple: false, clearable: true }
},
form: {
disabled: !showData.dept_belong_id.showForm,
component: {
props: {
elProps: {
treeConfig: {
transform: true,
rowField: 'id',
parentField: 'parent',
expandAll: true
},
columns: [
{
field: 'name',
title: '部门名称',
treeNode: true
},
{
field: 'status',
title: '状态'
},
{
field: 'parent_name',
title: '父级部门'
}
]
}
}
props: { multiple: false, clearable: true }
},
helper: {
render (h) {
@ -355,6 +335,12 @@ Vue.prototype.commonEndColumns = function (param = {}) {
)
}
}
},
// 接收时,处理数据
valueBuilder (row, col) {
if (row[col.key]) {
row[col.key] = Number(row[col.key])
}
}
},
{

View File

@ -197,7 +197,7 @@ export default {
},
destroyed () {
// websocket
this.$websocket.close()
this.$websocket.closeWebsocket()
}
}
</script>

View File

@ -1 +1,9 @@
module.exports = file => () => import('@great-dream/' + file)
module.exports = file => {
var result
try {
result = require('@great-dream/' + file).default
} catch (error) {
result = require('@/views/plugins/' + file).default
}
return result
}

View File

@ -38,12 +38,24 @@ util.open = function (url) {
*/
util.baseURL = function () {
var baseURL = process.env.VUE_APP_API
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenant-web') !== -1) {
var param = baseURL.split('/')[3] || ''
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenant-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2. ip 地址替换成和前端一样域名
// 3. /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2]
var prot = host.split(':')[1] || 80
if (host) {
var prot = baseURL.split(':')[2] || 80
if (prot === 80 || prot === 443) {
host = document.domain
} else {
host = document.domain + ':' + prot
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + (baseURL.split('/')[3] || '')
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
}
if (!baseURL.endsWith('/')) {
baseURL += '/'
@ -79,4 +91,30 @@ util.randomString = function (e) {
return n
}
util.ArrayToTree = function (rootList, parentValue, parentName, list) {
for (const item of rootList) {
if (item.parent === parentValue) {
if (parentName) {
item.name = parentName + '/' + item.name
}
list.push(item)
}
}
for (const i of list) {
// 如果子元素里面存在children就直接递归不存在就生成一个children
if (i.children) {
util.ArrayToTree(rootList, i.id, i.name, i.children)
} else {
i.children = []
util.ArrayToTree(rootList, i.id, i.name, i.children)
}
if (i.children.length === 0) {
delete i.children
}
}
return list
}
export default util

View File

@ -17,6 +17,7 @@ import util from '@/libs/util.js'
// 路由数据
import routes from './routes'
import { getMenu, handleAsideMenu, handleRouter, checkRouter } from '@/menu'
import { request } from '@/api/service'
// fix vue-router NavigationDuplicated
const VueRouterPush = VueRouter.prototype.push
@ -55,6 +56,24 @@ router.beforeEach(async (to, from, next) => {
// 请根据自身业务需要修改
const token = util.cookies.get('token')
if (token && token !== 'undefined') {
if (!store.state.d2admin.user.info.name) {
var res = await request({
url: '/api/system/user/user_info/',
method: 'get',
params: {}
})
await store.dispatch('d2admin/user/set', {
name: res.data.name,
user_id: res.data.id,
avatar: res.data.avatar,
role_info: res.data.role_info,
dept_info: res.data.dept_info,
is_superuser: res.data.is_superuser
}, { root: true })
await store.dispatch('d2admin/account/load')
store.dispatch('d2admin/dept/load')
store.dispatch('d2admin/settings/init')
}
if (!store.state.d2admin.menu || store.state.d2admin.menu.aside.length === 0) {
// 动态添加路由
getMenu().then(ret => {

View File

@ -44,7 +44,13 @@ export default {
util.cookies.set('token', res.access)
util.cookies.set('refresh', res.refresh)
// 设置 vuex 用户信息
await dispatch('d2admin/user/set', { name: res.name, user_id: res.userId, avatar: res.avatar }, { root: true })
await dispatch('d2admin/user/set', {
name: res.name,
user_id: res.userId,
avatar: res.avatar,
role_info: res.role_info,
dept_info: res.dept_info
}, { root: true })
// 用户登录后从持久化数据加载一系列的设置
await dispatch('load')
},

View File

@ -0,0 +1,38 @@
import { request } from '@/api/service'
import util from '@/libs/util'
export default {
namespaced: true,
state: {
// 用户信息
data: undefined
},
actions: {
/**
* @description 初始化部门数据
* @param {Object} context
* @param {*} info info
*/
async getDeptName ({ state, dispatch }, { data }) {
const nameDict = {}
for (const items of data) {
if (items.children) {
const filterData = await dispatch('getDeptName', { data: items.children })
for (var key in filterData) {
nameDict[key] = filterData[key]
}
}
nameDict[items.id] = items.name
}
return nameDict
},
async load ({ state, dispatch }, info) {
// 持久化
const ret = await request({
url: '/api/system/dept/all_dept/'
})
const data = util.ArrayToTree(ret.data.data || ret.data, null, null, [])
state.data = await dispatch('getDeptName', { data: data })
}
}
}

View File

@ -14,12 +14,12 @@ export default {
// store 赋值
state.info = info
// 持久化
await dispatch('d2admin/db/set', {
dbName: 'sys',
path: 'user.info',
value: info,
user: true
}, { root: true })
// await dispatch('d2admin/db/set', {
// dbName: 'sys',
// path: 'user.info',
// value: info,
// user: true
// }, { root: true })
},
/**
* @description 从数据库取用户数据
@ -27,12 +27,12 @@ export default {
*/
async load ({ state, dispatch }) {
// store 赋值
state.info = await dispatch('d2admin/db/get', {
dbName: 'sys',
path: 'user.info',
defaultValue: {},
user: true
}, { root: true })
// state.info = await dispatch('d2admin/db/get', {
// dbName: 'sys',
// path: 'user.info',
// defaultValue: {},
// user: true
// }, { root: true })
}
}
}

View File

@ -2,16 +2,22 @@
<div>
<el-row :gutter="10">
<el-col :span="4">变量标题</el-col>
<el-col :span="12">变量值</el-col>
<el-col :span="4" :offset="4">变量名</el-col>
<el-col :span="10">变量值</el-col>
<el-col :span="4" >变量名</el-col>
<el-col :span="2">是否前端配置</el-col>
<el-col :span="3" :offset="1">操作</el-col>
</el-row>
<el-form ref="form" :model="form" label-width="140px" label-position="left" style="margin-top: 20px">
<el-form ref="form" :model="form" label-width="240px" label-position="left" style="margin-top: 20px">
<el-form-item :label="item.title" :prop="['array'].indexOf(item.form_item_type_label) >-1?'':item.key"
:key="index" :rules="item.rule || []"
v-for="(item,index) in formList"
>
<el-col :span="12" :offset="2">
<template slot="label">
<el-input v-if="item.edit" v-model="item.title" style="display: inline-block;width: 200px;" placeholder="请输入标题"></el-input>
<span v-else>{{item.title}}</span>
</template>
<el-col :span="11" >
<!-- 文本 -->
<el-input :key="index" v-if="['text','textarea'].indexOf(item.form_item_type_label) >-1"
:type="item.form_item_type_label"
@ -68,7 +74,7 @@
<el-radio
v-for="item in dictionary(item.setting) || []"
:key="item.value"
:label="item.label"
:label="item.value"
:value="item.value">
{{ item.label }}
</el-radio>
@ -183,7 +189,7 @@
<p>删除后无法恢复,确定删除吗</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="childRemoveVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="onRemoveChild(row,index)"></el-button>
<el-button type="primary" size="mini" @click="onRemoveChild(row,index,item.key)"></el-button>
</div>
<el-button type="text" slot="reference">删除</el-button>
</el-popover>
@ -195,7 +201,32 @@
</div>
</div>
</el-col>
<el-col :span="4" :offset="6">{{ editableTabsItem.key }}.{{ item.key }}</el-col>
<el-col :span="4" :offset="1">
<el-input v-if="item.edit" v-model="item.new_key" style="width: 200px;" placeholder="请输入变量key">
<template slot="prepend">
<span style="padding: 0px 5px">{{ editableTabsItem.key }}</span>
</template>
</el-input>
<span v-else>{{ editableTabsItem.key }}.{{ item.key }}</span>
</el-col>
<el-col :span="3" :offset="1">
<el-switch
v-model="item.status"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</el-col>
<el-col :span="2">
<el-button v-if="item.edit" size="mini" type="primary" icon="el-icon-success" @click="onEditSave(item)"></el-button>
<el-button v-else size="mini" type="primary" icon="el-icon-edit" @click="onEdit(index)"></el-button>
<el-popconfirm
title="确定删除该条数据吗?"
@confirm="onDelRow(item)"
>
<el-button size="mini" type="danger" icon="el-icon-delete" slot="reference"></el-button>
</el-popconfirm>
</el-col>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit"></el-button>
@ -272,7 +303,7 @@ export default {
//
getInit () {
const that = this
api.GetList({ parent: this.options.id }).then(res => {
api.GetList({ parent: this.options.id, limit: 999 }).then(res => {
const { data } = res.data
this.formList = data
const form = {}
@ -382,11 +413,17 @@ export default {
}
},
//
onRemoveChild (row, index) {
onRemoveChild (row, index, refName) {
console.log(row, index)
if (row.id) {
console.log(1, 'ok')
api.DelObj(row.id).then(res => {
this.refreshView()
})
} else {
this.childTableData.splice(index, 1)
const tableName = 'xTable_' + refName
const tableData = this.$refs[tableName][0].remove(row)
console.log(tableData)
}
},
//
@ -445,6 +482,25 @@ export default {
if (value.uid === file.uid) index = inx
})
this.form[key].splice(index, 1)
},
//
onDelRow (obj) {
api.DelObj(obj.id).then(res => {
this.refreshView()
})
},
//
onEdit (index) {
const that = this
that.$set(that.formList[index], 'new_key', that.formList[index].key)
that.$set(that.formList[index], 'edit', true)
},
//
onEditSave (obj) {
obj.key = JSON.parse(JSON.stringify(obj.new_key))
api.UpdateObj(obj).then(res => {
this.refreshView()
})
}
},
mounted () {

View File

@ -135,46 +135,6 @@ export const crudOptions = (vm) => {
}
}
},
// {
// title: '上级部门',
// key: 'parent',
// show: false,
// search: {
// disabled: true
// },
// type: 'cascader',
// dict: {
// cache: false,
// url: deptPrefix,
// isTree: true,
// value: 'id', // 数据字典中value字段的属性名
// label: 'name', // 数据字典中label字段的属性名
// children: 'children', // 数据字典中children字段的属性名
// getData: (url, dict) => { // 配置此参数会覆盖全局的getRemoteDictFunc
// return request({ url: url, params: { limit: 999, status: 1 } }).then(ret => {
// const data = XEUtils.toArrayTree(ret.data.data, { parentKey: 'parent', strict: true })
// return [{ id: null, name: '根节点', children: data }]
// })
// }
// },
// form: {
// component: {
// span: 12,
// props: {
// elProps: {
// clearable: true,
// showAllLevels: false, // 仅显示最后一级
// props: {
// checkStrictly: true, // 可以不需要选到最后一级
// emitPath: false,
// clearable: true
// }
// }
// }
// }
// }
// },
{
title: '部门名称',
key: 'name',
@ -207,6 +167,22 @@ export const crudOptions = (vm) => {
}
}
},
{
title: '部门标识',
key: 'key',
sortable: true,
form: {
component: {
props: {
clearable: true
},
placeholder: '请输入标识字符'
},
itemProps: {
class: { yxtInput: true }
}
}
},
{
title: '负责人',
key: 'owner',

View File

@ -49,13 +49,17 @@ export default {
},
addRequest (row) {
d2CrudPlus.util.dict.clear()
this.$store.dispatch('d2admin/dept/load')
return api.createObj(row)
},
updateRequest (row) {
d2CrudPlus.util.dict.clear()
this.$store.dispatch('d2admin/dept/load')
return api.UpdateObj(row)
},
delRequest (row) {
d2CrudPlus.util.dict.clear()
this.$store.dispatch('d2admin/dept/load')
return api.DelObj(row.id)
},
//

View File

@ -121,7 +121,7 @@ img {
/*-- form styling --*/
.w3l-form-info {
padding-top: 6em;
padding-top: 2em;
}
.w3l-signinform{
padding: 40px 40px;
@ -241,8 +241,10 @@ img {
.w3_info {
padding: 1em 1em;
background: transparent;
max-width: 450px;
width: 450px;
display: grid;
position: fixed;
right: 12vw;
margin-left: auto;
}
@ -317,7 +319,7 @@ h5 {
color: #000;
}
.footer {
padding-top: 3em;
padding-top: 1em;
}
.footer p {
text-align: center;

View File

@ -68,9 +68,10 @@
</el-input>
</el-form-item>
</el-form>
<button class="btn btn-primary btn-block" @click="submit">
<button class="btn btn-primary btn-block" style="padding: 10px 10px;" @click="submit">
登录
</button>
<component v-if="componentTag" :is="componentTag"></component>
</el-tab-pane>
</el-tabs>
</el-card>
@ -125,16 +126,25 @@
</template>
<script>
import base from './base.vue'
const pluginImport = require('@/libs/util.import.plugin')
export default {
extends: base,
name: 'page',
data () {
return {
activeName: 'first'
activeName: 'first',
componentTag: ''
}
},
created () {
//
var componentTag = ''
try {
componentTag = pluginImport('dvadmin-third-web/src/login/index')
} catch (error) {
componentTag = ''
}
this.componentTag = componentTag
},
mounted () {
},

View File

@ -1,9 +1,8 @@
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}'])
util.filterParams(vm, ['dept_name', 'role_info{name}', 'dept_name_all'])
return {
pageOptions: {
compact: true
@ -182,7 +181,7 @@ export const crudOptions = (vm) => {
dict: {
cache: false,
isTree: true,
url: deptPrefix,
url: '/api/system/dept/all_dept/',
value: 'id', // 数据字典中value字段的属性名
label: 'name' // 数据字典中label字段的属性名
},

File diff suppressed because it is too large Load Diff