!79 正式发布2.0.7版本

1. 功能:websocket重构
2. 修复:用户管理中部门下拉无数据问题
3. 修复:filter_fields使用icontains导致条件查询失效
4. 修复:文件上传显示问题
5. 优化:软删除重构
6. 优化:菜单管理改为懒加载
pull/80/MERGE v2.0.7
dvadmin 2022-11-25 15:03:37 +00:00 committed by Gitee
commit 7ea806603f
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
55 changed files with 1670 additions and 444 deletions

View File

@ -8,20 +8,21 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
from application.websocketConfig import websocket_application
from application.routing import websocket_urlpatterns
http_application = get_asgi_application()
async def application(scope,receive,send):
if scope['type'] == 'http':
await http_application(scope, receive, send)
elif scope['type'] == 'websocket':
await websocket_application(scope, receive, send)
else:
raise Exception("未知的scope类型,"+ scope['type'])
application = ProtocolTypeRouter({
"http":http_application,
'websocket': AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns #指明路由文件是devops/routing.py
)
),
})

View File

@ -1,13 +1,8 @@
# -*- coding: utf-8 -*-
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from dvadmin.system import routing as dvadminRouting
from django.urls import path
from application.websocketConfig import MegCenter
websocket_urlpatterns = [
path('ws/<str:service_uid>/', MegCenter.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者
]
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter(
dvadminRouting.websocket_urlpatterns# 指明路由文件是devops/routing.py
)
),
})

View File

@ -93,6 +93,7 @@ TEMPLATES = [
WSGI_APPLICATION = "application.wsgi.application"
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
@ -168,15 +169,21 @@ CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持
# ================================================= #
# ********************* channels配置 ******************* #
# ================================================= #
ASGI_APPLICATION = 'application.routing.application'
ASGI_APPLICATION = 'application.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)], #需修改
},
},
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
# CHANNEL_LAYERS = {
# 'default': {
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
# 'CONFIG': {
# "hosts": [('127.0.0.1', 6379)], #需修改
# },
# },
# }
# ================================================= #
# ********************* 日志配置 ******************* #

View File

@ -1,15 +1,42 @@
# -*- coding: utf-8 -*-
import django
django.setup()
import json
import urllib
#处理websocket传参
from asgiref.sync import sync_to_async, async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer
import json
from channels.layers import get_channel_layer
from jwt import InvalidSignatureError
from application import settings
send_dict = {}
# 发送消息结构体
def set_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):
from dvadmin.system.models import MessageCenter
_MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user',flat=True)
if _MessageCenter:
return _MessageCenter
else:
return []
@database_sync_to_async
def _get_message_unread(user_id):
from dvadmin.system.models import MessageCenterTargetUser
count = MessageCenterTargetUser.objects.filter(users=user_id,is_read=False).count()
return count or 0
def request_data(scope):
@ -17,98 +44,69 @@ def request_data(scope):
qs = urllib.parse.parse_qs(query_string)
return qs
# 全部的websocket sender
CONNECTIONS = {}
# 判断用户是否已经连接
def check_connection(key):
return key in CONNECTIONS
# 发送消息结构体
def message(sender, msg_type, msg):
text = json.dumps({
'sender': sender,
'contentType': msg_type,
'content': msg,
})
return {
'type': 'websocket.send',
'text': text
}
async def websocket_application(scope, receive, send):
while True:
event = await receive()
# print('[event] ', event)
qs = request_data(scope)
print(1,qs)
auth = qs.get('auth', [''])[0]
user_id = None
# 收到建立WebSocket连接的消息
if event['type'] == 'websocket.connect':
# 昵称验证
if not auth:
break
else:
class DvadminWebSocket(AsyncJsonWebsocketConsumer):
async def connect(self):
try:
import jwt
decoded_result = jwt.decode(auth, settings.SECRET_KEY, algorithms=["HS256"])
self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"]
decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"])
if decoded_result:
user_id = decoded_result.get('user_id')
# 记录
CONNECTIONS[user_id] = send
self.user_id = decoded_result.get('user_id')
self.chat_group_name = "user_"+str(self.user_id)
#收到连接时候处理,
await self.channel_layer.group_add(
self.chat_group_name,
self.channel_name
)
await self.accept()
# 发送连接成功
await self.send_json(set_message('system', 'SYSTEM', '连接成功'))
# 主动推送消息
unread_count = await _get_message_unread(self.user_id)
await self.send_json(set_message('system', 'TEXT', {"model":'message_center',"unread":unread_count}))
except InvalidSignatureError:
break
if auth in CONNECTIONS:
break
await send({'type': 'websocket.accept'})
await send(message('system', 'INFO', '连接成功'))
# # 发送好友列表
# friends_list = list(CONNECTIONS.keys())
# await send(message('system', 'INFO', friends_list))
#
# # 向其他人群发消息, 有人登录了
# for other in CONNECTIONS.values():
# await other(message('system', 'addFriend', auth))
await self.disconnect(None)
# 收到中断WebSocket连接的消息
elif event['type'] == 'websocket.disconnect':
# 移除记录
if user_id in CONNECTIONS:
CONNECTIONS.pop(user_id)
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(self.chat_group_name, self.channel_name)
print("连接关闭")
await self.close(close_code)
# # 向其他人群发消息, 有人离线了
# for other in CONNECTIONS.values():
# await other(message('system', 'removeFriend', user_id))
# 其他情况,正常的WebSocket消息
elif event['type'] == 'websocket.receive':
print(11,event)
if event['text'] == 'ping':
await send(message('system', 'text', 'pong!'))
else:
receive_msg = json.loads(event['text'])
message_id = receive_msg.get('message_id', None)
_MessageCenter = MessageCenter.objects.filter(id=message_id).first()
if _MessageCenter:
user_list = _MessageCenter.target_user.values_list('id',flat=True)
class MegCenter(DvadminWebSocket):
"""
消息中心
"""
async def receive(self, text_data):
# 接受客户端的信息,你处理的函数
text_data_json = json.loads(text_data)
message_id = text_data_json.get('message_id', None)
user_list = await _get_message_center_instance(message_id)
for send_user in user_list:
if send_user in CONNECTIONS:
content_type = receive_msg.get('contentType', 'TEXT')
content = receive_msg.get('content', '')
msg = message(user_id, content_type, content)
await CONNECTIONS[send_user](msg)
else:
msg = message('system', 'text', '对方已下线或不存在')
await send(msg)
else:
print('a1a1a1')
pass
await self.channel_layer.group_send(
"user_" + str(send_user),
{'type': 'push.message', 'json': text_data_json}
)
print('[disconnect]')
async def push_message(self, event):
message = event['json']
await self.send(text_data=json.dumps(message))
def websocket_push(user_id, message):
"""
主动推送消息
"""
username = "user_"+str(user_id)
print(103,message)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
username,
{
"type": "push.message",
"json": message
}
)

View File

@ -1,3 +1,4 @@
#!/bin/bash
# python manage.py makemigrations
# python manage.py migrate
# python manage.py init -y

View File

@ -1,91 +0,0 @@
# -*- 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

@ -387,7 +387,7 @@ class SystemConfig(CoreModel):
class LoginLog(CoreModel):
LOGIN_TYPE_CHOICES = ((1, "普通登录"),)
LOGIN_TYPE_CHOICES = ((1, "普通登录"), (2, "微信扫码登录"),)
username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录用户名")
ip = models.CharField(max_length=32, verbose_name="登录ip", null=True, blank=True, help_text="登录ip")
agent = models.TextField(verbose_name="agent信息", null=True, blank=True, help_text="agent信息")
@ -417,15 +417,24 @@ 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.ManyToManyField(to=Users,related_name="target_user",blank=True,db_constraint=False,verbose_name="目标用户",help_text="目标用户")
target_user = models.ManyToManyField(to=Users,related_name='user',through='MessageCenterTargetUser', through_fields=('messagecenter','users'),blank=True,verbose_name="目标用户",help_text="目标用户")
target_dept = models.ManyToManyField(to=Dept, blank=True, db_constraint=False,
verbose_name="目标部门", help_text="目标部门")
target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False,
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",)
class MessageCenterTargetUser(CoreModel):
users = models.ForeignKey(Users,related_name="target_user", on_delete=models.CASCADE,db_constraint=False,verbose_name="关联用户表",help_text="关联用户表")
messagecenter = models.ForeignKey(MessageCenter, on_delete=models.CASCADE,db_constraint=False,verbose_name="关联消息中心表",help_text="关联消息中心表")
is_read = models.BooleanField(default=False,blank=True,null=True,verbose_name="是否已读",help_text="是否已读")
class Meta:
db_table = table_prefix + "message_center_target_user"
verbose_name = "消息中心目标用户表"
verbose_name_plural = verbose_name

View File

@ -1,7 +0,0 @@
# -*- 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

@ -37,6 +37,17 @@ class DeptSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class DeptImportSerializer(CustomModelSerializer):
"""
部门-导入-序列化器
"""
class Meta:
model = Dept
fields = '__all__'
read_only_fields = ["id"]
class DeptInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
@ -113,8 +124,12 @@ class DeptViewSet(CustomModelViewSet):
update_serializer_class = DeptCreateUpdateSerializer
filter_fields = ['name', 'id', 'parent']
search_fields = []
# extra_filter_backends = []
import_serializer_class = DeptImportSerializer
import_field_dict = {
"name": "部门名称",
"key": "部门标识",
}
def list(self, request, *args, **kwargs):
# 如果懒加载,则只返回父级

View File

@ -21,6 +21,7 @@ class MenuSerializer(CustomModelSerializer):
菜单表的简单序列化器
"""
menuPermission = serializers.SerializerMethodField(read_only=True)
hasChild = serializers.SerializerMethodField()
def get_menuPermission(self, instance):
queryset = instance.menuPermission.order_by('-name').values_list('name', flat=True)
@ -29,6 +30,12 @@ class MenuSerializer(CustomModelSerializer):
else:
return None
def get_hasChild(self, instance):
hasChild = Menu.objects.filter(parent=instance.id)
if hasChild:
return True
return False
class Meta:
model = Menu
fields = "__all__"
@ -171,3 +178,21 @@ class MenuViewSet(CustomModelViewSet):
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
def list(self,request):
"""
懒加载
"""
params = request.query_params
parent = params.get('parent', None)
if params:
if parent:
queryset = self.queryset.filter(status=1, parent=parent)
else:
queryset = self.queryset.filter(status=1)
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
queryset = self.filter_queryset(queryset)
serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data)

View File

@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
from itertools import chain
import json
from django_restql.fields import DynamicSerializerMethodField
from rest_framework import serializers
from rest_framework.decorators import action, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from dvadmin.system.models import MessageCenter, Users
from application.websocketConfig import websocket_push
from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser
from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.role import RoleSerializer
from dvadmin.system.views.user import UserSerializer
from dvadmin.utils.json_response import SuccessResponse
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@ -19,6 +21,9 @@ class MessageCenterSerializer(CustomModelSerializer):
"""
role_info = DynamicSerializerMethodField()
user_info = DynamicSerializerMethodField()
dept_info = DynamicSerializerMethodField()
is_read = serializers.BooleanField(read_only=True, source='target_user__is_read')
def get_role_info(self, instance, parsed_query):
roles = instance.target_role.all()
# You can do what ever you want in here
@ -41,12 +46,53 @@ class MessageCenterSerializer(CustomModelSerializer):
)
return serializer.data
def get_dept_info(self, instance, parsed_query):
dept = instance.target_dept.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
serializer = DeptSerializer(
dept,
many=True,
parsed_query=parsed_query
)
return serializer.data
class Meta:
model = MessageCenter
fields = "__all__"
read_only_fields = ["id"]
class MessageCenterTargetUserSerializer(CustomModelSerializer):
"""
目标用户序列化器-序列化器
"""
class Meta:
model = MessageCenterTargetUser
fields = "__all__"
read_only_fields = ["id"]
class MessageCenterTargetUserListSerializer(CustomModelSerializer):
"""
目标用户序列化器-序列化器
"""
class Meta:
model = MessageCenterTargetUser
fields = "__all__"
read_only_fields = ["id"]
def to_representation(self, instance):
data = super().to_representation(instance)
data['title'] = instance.messagecenter.title
data['content'] = instance.messagecenter.content
data['target_type'] = instance.messagecenter.target_type
data['id'] = instance.messagecenter.id
return data
class MessageCenterCreateSerializer(CustomModelSerializer):
"""
消息中心-新增-序列化器
@ -58,14 +104,27 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
target_type = initial_data.get('target_type')
# 在保存之前,根据目标类型,把目标用户查询出来并保存
users = initial_data.get('target_user', [])
if target_type in [1]:
if target_type in [1]: # 按角色
target_role = initial_data.get('target_role')
users = Users.objects.exclude(is_deleted=True).filter(role__id__in=target_role).values_list('id', flat=True)
if target_type in [2]:
if target_type in [2]: # 按部门
target_dept = initial_data.get('target_dept')
users = Users.objects.exclude(is_deleted=True).filter(dept__id__in=target_dept).values_list('id', flat=True)
data.save()
data.target_user.set(users)
if target_type in [3]: # 系统通知
users = Users.objects.exclude(is_deleted=True).values_list('id', flat=True)
targetuser_data = []
for user in users:
targetuser_data.append({
"messagecenter": data.id,
"users": user
})
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
targetuser_instance.is_valid(raise_exception=True)
targetuser_instance.save()
for user in users:
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
websocket_push(user, {"sender": 'system', "contentType": 'TEXT',
"content": {"model": 'message_center', "unread": unread_count}})
return data
class Meta:
@ -74,8 +133,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class MessageCenterViewSet(CustomModelViewSet):
"""
消息中心接口
@ -85,21 +142,58 @@ class MessageCenterViewSet(CustomModelViewSet):
retrieve:单例
destroy:删除
"""
queryset = MessageCenter.objects.all()
queryset = MessageCenter.objects.order_by('create_datetime')
serializer_class = MessageCenterSerializer
create_serializer_class = MessageCenterCreateSerializer
extra_filter_backends = []
def get_queryset(self):
if self.action == 'list':
return MessageCenter.objects.filter(creator=self.request.user.id).all()
return MessageCenter.objects.all()
def retrieve(self, request, *args, **kwargs):
"""
重写查看
"""
pk = kwargs.get('pk')
user_id = self.request.user.id
queryset = MessageCenterTargetUser.objects.filter(users__id=user_id, messagecenter__id=pk).first()
if queryset:
queryset.is_read = True
queryset.save()
instance = self.get_object()
serializer = self.get_serializer(instance)
# 主动推送消息
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
websocket_push(user_id, {"sender": 'system', "contentType": 'TEXT',
"content": {"model": 'message_center', "unread": unread_count}})
return DetailResponse(data=serializer.data, msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_self_receive(self, request):
"""
获取接收到的消息
"""
self_user_id = self.request.user.id
queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime')
# queryset = self.filter_queryset(queryset)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
serializer = MessageCenterTargetUserListSerializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
serializer = MessageCenterTargetUserListSerializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_newest_msg(self, request):
"""
获取最新的一条消息
"""
self_user_id = self.request.user.id
queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('create_datetime').last()
data = None
if queryset:
serializer = MessageCenterTargetUserListSerializer(queryset, many=False, request=request)
data = serializer.data
return DetailResponse(data=data, msg="获取成功")

View File

@ -108,10 +108,8 @@ class SystemConfigChinldernSerializer(CustomModelSerializer):
def get_chinldern(self, instance):
queryset = SystemConfig.objects.filter(parent=instance)
if queryset:
serializer = SystemConfigSerializer(queryset, many=True)
return serializer.data
return None
class Meta:
model = SystemConfig

View File

@ -231,7 +231,8 @@ class UserViewSet(CustomModelViewSet):
update_serializer_class = UserUpdateSerializer
# filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"]
filter_fields = {
"name": ["icontains"],
"name": ["exact"],
"mobile": ["exact"],
"username": ["exact"],
"gender": ["icontains"],
"is_active": ["icontains"],
@ -288,7 +289,7 @@ class UserViewSet(CustomModelViewSet):
"gender": user.gender,
"email": user.email,
"avatar": user.avatar,
"dept": user.dept.id,
"dept": user.dept_id,
"is_superuser": user.is_superuser,
"role": user.role.values_list('id', flat=True),
}

View File

@ -292,11 +292,6 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
)
else:
orm_lookups.append(search_field)
orm_lookups = (
orm_lookups
if isinstance(filterset.__class__._meta.fields, (list, tuple))
else filterset.filters.keys()
)
conditions = []
queries = []
for search_term_key in filterset.data.keys():

View File

@ -7,6 +7,7 @@ from openpyxl import Workbook
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.decorators import action
from rest_framework.request import Request
from dvadmin.utils.import_export import import_to_data
@ -56,6 +57,7 @@ class ImportSerializerMixin:
length += 2.1 if ord(char) > 256 else 1
return round(length, 1) if length <= self.export_column_width else self.export_column_width
@action(methods=['get','post'],detail=False)
@transaction.atomic # Django 事务,防止出错
def import_data(self, request: Request, *args, **kwargs):
"""
@ -151,13 +153,13 @@ class ImportSerializerMixin:
if queryset.model._meta.unique_together: # 判断是否存在联合主键
filter_dic = {i: ele.get(i) for i in list(queryset.model._meta.unique_together[0])}
else:
filter_dic = {i: ele.get(i) for i in list(set(self.import_field_dict.keys()) & set(unique_list))}
filter_dic = {i: ele.get(i) for i in list(set(unique_list)) if ele.get(i) is not None}
instance = filter_dic and queryset.filter(**filter_dic).first()
if instance and not updateSupport:
continue
if not filter_dic:
instance = None
serializer = self.import_serializer_class(instance, data=ele)
serializer = self.import_serializer_class(instance, data=ele, request=request)
serializer.is_valid(raise_exception=True)
serializer.save()
return DetailResponse(msg=f"导入成功!")
@ -216,7 +218,7 @@ class ExportSerializerMixin:
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
data = self.export_serializer_class(queryset, many=True, request=request).data
# 导出excel 表
response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition"

View File

@ -17,17 +17,8 @@ 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()
pass
@ -53,6 +44,27 @@ class SoftDeleteManager(models.Manager):
return SoftDeleteQuerySet(self.model).get(username=name)
class SoftDeleteModel(models.Model):
"""
软删除模型
一旦继承,就将开启软删除
"""
is_deleted = models.BooleanField(verbose_name="是否软删除", help_text='是否软删除', default=False, db_index=True)
objects = SoftDeleteManager()
class Meta:
abstract = True
verbose_name = '软删除模型'
verbose_name_plural = verbose_name
def delete(self, using=None, soft_delete=True, *args, **kwargs):
"""
重写删除方法,直接开启软删除
"""
self.is_deleted = True
self.save(using=using)
class CoreModel(models.Model):
"""
核心标准抽象模型模型,可直接继承使用
@ -67,25 +79,12 @@ 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
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

@ -154,7 +154,7 @@ def get_verbose_name(queryset=None, view=None, model=None):
:return:
"""
try:
if queryset and hasattr(queryset, 'model'):
if queryset is not None and hasattr(queryset, 'model'):
model = queryset.model
elif view and hasattr(view.get_queryset(), 'model'):
model = view.get_queryset().model

View File

@ -8,6 +8,7 @@
"""
import uuid
from django.db import transaction
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
@ -50,6 +51,7 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
return self.values_queryset
return super().get_queryset()
def get_serializer_class(self):
action_serializer_name = f"{self.action}_serializer_class"
action_serializer_class = getattr(self, action_serializer_name, None)
@ -57,6 +59,16 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
return action_serializer_class
return super().get_serializer_class()
# 通过many=True直接改造原有的API使其可以批量创建
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs.setdefault('context', self.get_serializer_context())
if isinstance(self.request.data, list):
with transaction.atomic():
return serializer_class(many=True, *args, **kwargs)
else:
return serializer_class(*args, **kwargs)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request)
serializer.is_valid(raise_exception=True)
@ -92,13 +104,7 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
def destroy(self, request, *args, **kwargs):
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)
instance.delete()
return DetailResponse(data=[], msg="删除成功")

View File

@ -91,6 +91,27 @@ services:
# network:
# ipv4_address: 177.8.0.14
# dvadmin-redis:
# image: redis:6.2.6-alpine # 指定服务镜像最好是与之前下载的redis配置文件保持一致
# container_name: dvadmin-redis # 容器名称
# restart: on-failure # 重启方式
# environment:
# - TZ=Asia/Shanghai # 设置时区
# volumes: # 配置数据卷
# - ./docker_env/redis/data:/data
# - ./docker_env/redis/redis.conf:/etc/redis/redis.conf
# ports: # 映射端口
# - "6379:6379"
# sysctls: # 设置容器中的内核参数
# - net.core.somaxconn=1024
# command: /bin/sh -c "echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf && redis-server /etc/redis/redis.conf --appendonly yes" # 指定配置文件并开启持久化
# privileged: true # 使用该参数container内的root拥有真正的root权限。否则container内的root只是外部的一个普通用户权限
# networks:
# network:
# ipv4_address: 177.8.0.15
networks:
network:
ipam:

View File

@ -3,4 +3,4 @@ WORKDIR /backend
COPY ./backend/ .
RUN awk 'BEGIN { cmd="cp -i ./conf/env.example.py ./conf/env.py "; print "n" |cmd; }'
RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt
CMD ["daphne","-b","0.0.0.0","-p","8000","application.asgi:application"]
CMD ["/backend/docker_start.sh"]

View File

@ -14,11 +14,18 @@ server {
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Nginx-Proxy true;
set_real_ip_from 0.0.0.0/0;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
real_ip_header X-Forwarded-For;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://177.8.0.12:8000/; # 设置代理服务器的协议和地址

View File

@ -1,13 +1,11 @@
# 所有环境默认
# 页面 title 前缀
VUE_APP_TITLE=D2Admin
VUE_APP_TITLE=DvAdmin
# 网络请求公用地址
VUE_APP_API=/api/
# websocket地址
VUE_APP_WEBSOCKET=""
# 仓库地址
VUE_APP_REPO=https://github.com/d2-projects/d2-admin-start-kit

View File

@ -6,5 +6,4 @@ 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.6",
"version": "2.0.7",
"scripts": {
"serve": "vue-cli-service serve --open",
"start": "npm run serve",
@ -41,10 +41,10 @@
"screenfull": "^5.0.2",
"sortablejs": "^1.10.1",
"ua-parser-js": "^0.7.20",
"vue": "^2.6.11",
"vue": "^2.7.10",
"vue-i18n": "^8.15.1",
"vue-infinite-scroll": "^2.0.2",
"vue-router": "^3.1.3",
"vue-router": "^3.6.5",
"vue-splitpane": "^1.0.6",
"vuex": "^3.1.2",
"vxe-table": "^3.3.2",

View File

@ -1,9 +1,10 @@
import ElementUI from 'element-ui'
import util from '@/libs/util'
import store from '@/store'
function initWebSocket (e) {
const token = util.cookies.get('token')
if (token) {
const wsUri = process.env.VUE_APP_WEBSOCKET + '/ws/' + token + '/?room=message_center'
const wsUri = util.wsBaseURL() + 'ws/' + token + '/'
this.socket = new WebSocket(wsUri)// 这里面的this都指向vue
this.socket.onerror = webSocketOnError
this.socket.onmessage = webSocketOnMessage
@ -20,9 +21,15 @@ function webSocketOnError (e) {
duration: 3000
})
}
/**
* 接收消息
* @param e
* @returns {any}
*/
function webSocketOnMessage (e) {
const data = JSON.parse(e.data)
if (data.contentType === 'INFO') {
if (data.contentType === 'SYSTEM') {
ElementUI.Notification({
title: 'websocket',
message: data.content,
@ -38,7 +45,7 @@ function webSocketOnMessage (e) {
position: 'bottom-right',
duration: 0
})
} else if (data.contentType === 'TEXT') {
} else if (data.contentType === 'INFO') {
ElementUI.Notification({
title: '温馨提示',
message: data.content,
@ -47,16 +54,29 @@ function webSocketOnMessage (e) {
duration: 0
})
} else {
console.log(data.content)
const { content } = data
if (content.model === 'message_center') {
const unread = content.unread
store.dispatch('d2admin/messagecenter/setUnread', unread)
}
}
}
// 关闭websiocket
function closeWebsocket () {
console.log('连接已关闭...')
// close()
this.socket.close()
ElementUI.Notification({
title: 'websocket',
message: '连接已关闭...',
type: 'danger',
position: 'bottom-right',
duration: 3000
})
}
/**
* 发送消息
* @param message
*/
function webSocketSend (message) {
this.socket.send(JSON.stringify(message))
}

View File

@ -1,5 +1,8 @@
<template>
<el-tag :type="color">{{ currentValue }}</el-tag>
<div>
<el-tag v-if="currentValue" :type="color">{{ currentValue }}</el-tag>
<span v-else></span>
</div>
</template>
<script>
//

View File

@ -0,0 +1,100 @@
<!--
* @创建文件时间: 2021-11-09 15:41:29
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-12-08 14:41:20
* 联系Qq:1638245306
* @文件介绍:
-->
# 表格选择框配置说明
## crud.js
```
{
title: '单选本地',
key: 'select1',
sortable: true,
search: {
disabled: true
},
type: 'table-selector',
dict: {
url: '/api/system/user/',
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
getData: (url, dict, { form, component }) => {
return request({ url: url, params: { page: 1, limit: 1 } }).then(ret => {
component._elProps.page = ret.data.page
component._elProps.limit = ret.data.limit
component._elProps.total = ret.data.total
return ret.data.data
})
}
},
form: {
component: {
span: 12,
props: { multiple: true },
elProps: {
pagination: true,
columns: [
{
field: "name",
title: "名称",
},
{
field: "username",
title: "账号",
},
{
field: "role",
title: "角色Id",
},
{
field: "dept",
title: "部门Id",
},
]
}
}
}
}
```
## 配置说明
```
详细文档:
1.http://d2-crud-plus.docmirror.cn/d2-crud-plus/guide/dict.html
2.https://xuliangzhan_admin.gitee.io/vxe-table/#/grid/api
```
| Name | Description | Type | Required | Default |
| ---------- | ---------------- | ------- | -------- | -------------- |
| type | 字段所使用的组件 | String | true | table-selector |
| dict | 字典的配置 | Object | true | {} |
| multiple | 是否多选 | Boolean | false | false |
| pagination | 是否分页 | Boolean | false | false |
| columns | 表格的列配置 | Array | true | [] |
| field | 字段 | String | true | '' |
| title | 字段名称 | String | true | '' |
___
## events
| Name | Description | Params |
|------|-------------|--------|
| radioChange| 表格行单选的点击事件| row,rowIndex|
```
form:{
component:{
on: { //单选事件监听
radioChange({ event, scope }) {
scope.form.channel_number = event.row.channel_number
}
},
}
}
```

View File

@ -0,0 +1,12 @@
export default {
// 字段类型配置注册之后即可在crud.js中使用了
'table-list-selector': {
// 表单组件配置
form: { component: { name: 'table-list-selector-input', props: { color: 'danger' } } },
// 行组件配置
component: { name: 'values-format', props: {} },
// 行展示时居中
align: 'center'
// 您还可以写更多默认配置
}
}

View File

@ -0,0 +1,24 @@
/*
* @创建文件时间: 2021-08-02 23:56:15
* @Auther: 猿小天
* @最后修改人: 猿小天
* @最后修改时间: 2021-08-09 22:15:56
* 联系Qq:1638245306
* @文件介绍:
*/
import { d2CrudPlus } from 'd2-crud-plus'
import group from './group'
function install (Vue, options) {
Vue.component('table-list-selector-input', () => import('./table-list-selector'))
// Vue.component('d2p-row-format', () => import('./row'))
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,653 @@
<template>
<div>
<el-button-group>
<el-button size="mini" type="success" round @click="openDialog"></el-button>
</el-button-group>
<el-dialog
custom-class="d2p-tree-selector-dialog"
:title="dialogTitle"
:visible.sync="dialogVisible"
width="50%"
append-to-body
>
<div>
<div v-if="treeFilter" class="filter-bar" style="padding-bottom: 20px">
<el-input
prefix-icon="el-icon-search"
:placeholder="filterPlaceholder"
v-model="filterText"
size="small"
>
</el-input>
</div>
<vxe-grid
v-bind="_elProps"
:data="_options"
ref="elTree"
:auto-resize="true"
@radio-change="radioChange"
@checkbox-change="checkboxChange"
>
<template #pager>
<vxe-pager
v-if="pagination"
style="margin-top: 10px"
:layouts="[
'Sizes',
'PrevJump',
'PrevPage',
'Number',
'NextPage',
'NextJump',
'FullJump',
'Total',
]"
:current-page.sync="_elProps.page"
:page-size.sync="_elProps.limit"
:total.sync="_elProps.total"
@page-change="handlePageChange"
>
</vxe-pager>
</template>
</vxe-grid>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">{{ cancelText }}</el-button>
<el-button type="primary" @click="selectSubmit">{{
confirmText
}}</el-button>
</span>
</el-dialog>
<el-table
:data="tableData"
style="width: 100%"
height="280"
align="center"
size="small">
<el-table-column
type="index"
width="40"
label="#">
</el-table-column>
<template v-for="(item,index) in gridOptions.columns">
<el-table-column
v-if="item.types && item.types=='img'"
:key="index"
:label="item.title"
width="120">
<template slot-scope="scope">
<img :src="scope.row.images" style='width: 30px' />
</template>
</el-table-column>
<el-table-column
v-else-if="item.types && item.types=='dict'"
:key="index"
:label="item.title"
width="120">
<template slot-scope="scope">
<span v-for="(data,index) in item.dictData" :key="index">
<span v-if="data.value===scope.row[item.field]">{{data.label}}</span>
<span v-else></span>
</span>
</template>
</el-table-column>
<el-table-column
v-else
:key="index"
:prop="item.field"
:label="item.title"
width="120">
</el-table-column>
</template>
<el-table-column
label="操作"
fixed="right"
v-show="colButtons.show"
:width="colButtons.width">
<template slot-scope="scopes">
<el-button style="padding: 0" :disabled="item.disabled?item.disabled({...scopes,tableData}):false" type="text"
size="small" :circle="item.circle?item.circle:false" v-for="(item,index) in colButtons.btns"
:icon="item.icon?item.icon:''" :key="index" @click="item.click({...scopes,tableData})">
{{ item.text }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import lodash from 'lodash'
import { d2CrudPlus } from 'd2-crud-plus'
import { request } from '@/api/service'
import XEUtils from 'xe-utils'
//
export default {
name: 'table-list-selector-input',
mixins: [d2CrudPlus.input, d2CrudPlus.inputDict],
props: {
//
value: {
type: [Number, String, Boolean, Array, Object]
},
// valuenodes nodes
filter: {
type: Function,
require: false
},
// placeholder
filterPlaceholder: {
type: String,
default: '输入关键字进行过滤'
},
placeholder: {
type: String,
default: '请选择'
},
dialogTitle: {
type: String,
default: '选择'
},
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
},
// elProps.filterNodeMethod
treeFilter: {
type: Boolean,
require: false,
default: true
},
// false
multiple: {
type: Boolean,
default: false
},
//
ignoreFullCheckedChildren: {
type: Boolean,
default: true
},
//
leafOnly: {
type: Boolean,
default: false
},
//
includeHalfChecked: {
type: Boolean,
default: false
},
//
elProps: {
type: Object
},
//
colButtons: {
type: Object,
default () {
return {
width: 150,
show: true,
btn: []
}
}
},
/**
* 是否可以清除
*/
clearable: {
type: Boolean,
default: true
},
//
dict: {
type: Object,
require: false
},
//
pagination: {
type: Boolean,
default: false
}
},
data () {
return {
currentValue: undefined,
collapseTags: false,
selected: [],
dialogVisible: false,
filterText: undefined,
requestUrl: null,
gridOptions: undefined,
tableData: []
}
},
created () {
// if (this.dict) {
// this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true)
// }
// this.initData()
},
computed: {
_elProps () {
const defaultElProps = {
// showCheckbox: this.multiple,
highlightCurrent: !this.multiple,
props: {},
columns: [],
border: true,
resizable: true
}
if (this.dict != null) {
if (this.dict.label != null) {
defaultElProps.props.label = this.dict.label
}
if (this.dict.value != null) {
defaultElProps.props.value = this.dict.value
}
if (this.dict.children != null) {
defaultElProps.props.children = this.dict.children
}
//
if (this.dict.isTree) {
defaultElProps.treeConfig = this.elProps.treeConfig
}
}
defaultElProps.nodeKey = defaultElProps.props.value
lodash.merge(defaultElProps, this.elProps)
//
const gridProps = JSON.parse(JSON.stringify(defaultElProps))
// gridProps.columns = [...gridProps.columns,{ title: '', width: this.colButtons.width, slots: { default: 'operate' } }]
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.gridOptions = gridProps
if (this.multiple) {
defaultElProps.checkboxConfig = this.elProps.checkboxConfig
? this.elProps.checkboxConfig
: {}
defaultElProps.columns = [
{
type: 'checkbox',
width: 60
},
...defaultElProps.columns
]
} else {
defaultElProps.radioConfig = this.elProps
? this.elProps.radioConfig
: {}
defaultElProps.columns = [
{
type: 'radio',
width: 60
},
...defaultElProps.columns
]
}
return defaultElProps
},
collapseTagSize () {
return ['small', 'mini'].indexOf(this.selectSize) > -1 ? 'mini' : 'small'
}
},
watch: {
filterText (val) {
// this.$refs.elTree.filter(val);
this.searchTableData()
}
},
methods: {
// initData () {
// d2CrudPlus.util.dict.get(this.dict).then(ret => {
// this.$set(this, 'data', ret)
// this.setValue(this.value)
// })
// },
onDictLoaded () {
// log.danger("onDictLoaded", this.dict, this.value);
// this.setValue(this.value)
this.tableData = this.value
},
setValue (value) {
// log.danger("setValue:", this.currentValue, this.value, this._options);
if (this.currentValue === this.value) {
return
}
let arrValue = value
if (value == null) {
this.selected = []
}
if (!(arrValue instanceof Array)) {
arrValue = [arrValue]
}
if (this.dict && this.dict.getNodes) {
// log.danger("getNodes:", arrValue);
this.dict.getNodes(arrValue).then((nodes) => {
this.selectedNodes(nodes, value)
})
} else {
const nodes = []
if (this._options == null || this._options.length === 0) {
return
}
for (const item of arrValue) {
const data = this._options
const node = d2CrudPlus.util.dict.getByValue(item, data, this.dict)
if (node != null) {
nodes.push(node)
}
}
this.selectedNodes(nodes, value)
}
},
selectedNodes (nodes, value) {
const selected = []
for (const node of nodes) {
node.id = node[this.dict.value]
selected.push(node)
}
// log.danger("selected:", selected);
this.$set(this, 'selected', selected)
this.resetInputHeight()
},
handleCheckChange (event) {
this.$emit('check-change', event)
},
handleCurrentChange (event) {
this.$emit('current-change', event)
},
//
openDialog () {
const that = this
if (this.disabled) {
return
}
this.dialogVisible = true
setTimeout(() => {
if (that._options.length > 0) {
that._options.map(
(item) => item[that._elProps.props.value]
)
// ids.forEach((id) => {
// console.log(111, id)
// const current = that.$refs.elTree.store.nodesMap[id]
// console.log(22, current)
// if (current != null) {
// this.doExpandParent(current)
// }
// })
// this.$nextTick(() => {
// if (that.multiple) {
// // this.$refs.elTree.setCheckedKeys(ids, this.leafOnly);
// that.$refs.elTree.setCheckboxRow(that.tableData, true)
// } else if (ids.length > 0) {
// // this.$refs.elTree.setCurrentKey(ids[0]);
// that.$refs.elTree.setRadioRow(that.tableData[0], true)
// }
// })
}
}, 1)
},
doExpandParent (node) {
if (node.parent != null) {
this.doExpandParent(node.parent)
}
node.expanded = true
},
// value,valuevalue
getValueKey (item) {
if (this._elProps.props.value != null) {
return item[this._elProps.props.value]
} else {
return item.value
}
},
// label,labellabel
getValueLabel (item) {
if (this._elProps.props.label != null) {
return item[this._elProps.props.label]
} else {
return item.label
}
},
// children,childrenchildren
getValueChildren (item) {
let children = 'children'
if (this._elProps.props.children != null) {
children = this._elProps.props.children
}
return item[children]
},
//
selectSubmit () {
const that = this
const nodes = this.refreshSelected()
if (that.tableData === undefined) {
that.tableData = nodes
} else {
that.tableData = this.tableData.concat(nodes) //
}
this.tableData = XEUtils.uniq(this.tableData) //
that.dialogVisible = false
that.doValueInputChanged(this.tableData)
},
//
doValueInputChanged (nodes) {
// let values = this.formatValue(nodes)
let values = nodes
this.resetInputHeight()
if (!this.multiple) {
values = values && values.length > 0 ? values[0] : undefined
}
this.currentValue = values
if (this.dispatch) {
this.dispatch('ElFormItem', 'el.form.blur')
}
this.$emit('input', values)
},
itemClosed (item) {
const newNodes = lodash.without(this.selected, item)
// console.log("new value", item, newNodes);
this.$set(this, 'selected', newNodes)
this.doValueInputChanged(newNodes)
},
//
refreshSelected () {
let nodes = null
if (this.multiple) {
nodes = this.$refs.elTree.getCheckboxRecords()
} else {
const node = this.$refs.elTree.getRadioRecord()
if (node == null) {
nodes = []
} else {
nodes = [node]
}
}
if (this.ignoreFullCheckedChildren) {
nodes = this.filterFullCheckedChildren(nodes)
}
if (this.filter != null) {
nodes = this.filter(nodes)
}
// log.danger("selected", this.selected);
this.$set(this, 'selected', nodes)
return nodes
},
resetInputHeight () {
if (this.collapseTags && !this.filterable) return
this.$nextTick(() => {
if (!this.$refs.reference) return
const inputChildNodes = this.$refs.reference.$el.childNodes
const input = [].filter.call(
inputChildNodes,
(item) => item.tagName === 'INPUT'
)[0]
const tags = this.$refs.tags
const sizeInMap = this.initialInputHeight || 40
const height =
this.selected.length === 0
? sizeInMap + 'px'
: Math.max(
tags
? tags.clientHeight + (tags.clientHeight > sizeInMap ? 6 : 0)
: 0,
sizeInMap
) + 'px'
input.style.height = height
if (this.visible && this.emptyText !== false) {
this.broadcast('ElSelectDropdown', 'updatePopper')
}
})
},
//
filterFullCheckedChildren (nodes) {
const ignored = new Set()
for (const item of nodes) {
const children = this.getValueChildren(item)
if (children != null) {
for (const child of children) {
ignored.add(this.getValueKey(child))
}
}
}
const values = []
for (const item of nodes) {
const key = this.getValueKey(item)
if (!ignored.has(key)) {
values.push(item)
}
}
return values
},
formatValue (nodes) {
const values = []
for (const item of nodes) {
values.push(this.getValueKey(item))
}
return values
},
filterNode (value, data) {
if (!value) return true
return this.getValueLabel(data).indexOf(value) !== -1
},
onChange (value) {
this.$emit('change', value)
if (this.dispatch) {
this.dispatch('ElFormItem', 'el.form.blur')
}
},
//
handlePageChange ({
currentPage,
pageSize
}) {
const that = this
that._elProps.page = currentPage
that._elProps.limit = pageSize
that.searchTableData()
},
//
searchTableData () {
const that = this
let params
if (that.pagination) {
params = {
page: that._elProps.page,
limit: that._elProps.limit,
search: that.filterText
}
} else {
params = {
search: that.filterText
}
}
let url
if (typeof that.dict.url === 'function') {
const form = that.d2CrudContext.getForm()
url = that.dict.url(that.dict, { form })
} else {
url = that.dict.url
}
request({
url: url,
params: params
}).then((ret) => {
const { data } = ret
that._elProps.page = data.page
that._elProps.limit = data.limit
that._elProps.total = data.total
that.$set(that, 'dictOptions', data.data)
})
},
/**
* 表格单选事件
*/
radioChange ({ checked, row, rowIndex, $rowIndex, column, columnIndex, $columnIndex, $event }) {
this.$emit('radioChange', {
row,
rowIndex
})
},
/**
* 表格多选事件
*/
checkboxChange ({ checked, row, rowIndex, $rowIndex, column, columnIndex, $columnIndex, $event }) {
this.$emit('checkboxChange', {
checked, row, rowIndex, $rowIndex, column, columnIndex, $columnIndex, $event
})
}
}
}
</script>
<style lang="scss" scoped>
.d2p-tree-selector {
width: 100%;
.el-cascader {
width: 100%;
}
.is-disabled .el-tag__close.el-icon-close {
display: none;
}
}
.d2p-tree-selector-dialog {
&.el-dialog {
max-height: 80vh;
display: flex;
flex-direction: column;
.el-dialog__body {
flex: 1;
overflow-y: auto;
}
.el-dialog__header {
padding: 20px 20px 20px;
border-bottom: 1px solid #eee;
}
.el-dialog__footer {
padding: 10px 20px 10px;
border-top: 1px solid #eee;
}
}
}
</style>

View File

@ -70,3 +70,23 @@
| columns | 表格的列配置 | Array | true | [] |
| field | 字段 | String | true | '' |
| title | 字段名称 | String | true | '' |
___
## events
| Name | Description | Params |
|------|-------------|--------|
| radioChange| 表格行单选的点击事件| row,rowIndex|
```
form:{
component:{
on: { //单选事件监听
radioChange({ event, scope }) {
scope.form.channel_number = event.row.channel_number
}
},
}
}
```

View File

@ -201,6 +201,7 @@ export default {
// this.dict = d2CrudPlus.util.dict.mergeDefault(this.dict, true)
// }
// this.initData()
// console.log(this)
this.searchTableData()
},
computed: {
@ -337,7 +338,6 @@ export default {
that.$nextTick(() => {
const refs = Object.assign({}, that.$refs)
const { elTree } = refs
console.log(elTree)
if (that.multiple) {
elTree.setCheckboxRow(that.selected, true)
} else {

View File

@ -29,7 +29,9 @@ export default {
row[col.key] = value.split(',')
// 进行组装地址纠正地址
row[col.key].map((val, index) => {
if (val.startsWith('/')) {
if (val.startsWith('/api')) {
row[col.key][index] = val
} else if (val.startsWith('/')) {
row[col.key][index] = util.baseURL() + val.slice(1)
} else {
row[col.key][index] = !val.startsWith('http') ? util.baseURL() + val : val
@ -67,7 +69,9 @@ export default {
row[col.key] = value.split(',')
// 进行组装地址纠正地址
row[col.key].map((val, index) => {
if (val.startsWith('/')) {
if (val.startsWith('/api')) {
row[col.key][index] = val
} else if (val.startsWith('/')) {
row[col.key][index] = util.baseURL() + val.slice(1)
} else {
row[col.key][index] = !val.startsWith('http') ? util.baseURL() + val : val
@ -101,7 +105,9 @@ export default {
row[col.key] = value.split(',')
// 进行组装地址纠正地址
row[col.key].map((val, index) => {
if (val.startsWith('/')) {
if (val.startsWith('/api')) {
row[col.key][index] = val
} else if (val.startsWith('/')) {
row[col.key][index] = util.baseURL() + val.slice(1)
} else {
row[col.key][index] = !val.startsWith('http') ? util.baseURL() + val : val
@ -139,7 +145,9 @@ export default {
row[col.key] = value.split(',')
// 进行组装地址纠正地址
row[col.key].map((val, index) => {
if (val.startsWith('/')) {
if (val.startsWith('/api')) {
row[col.key][index] = val
} else if (val.startsWith('/')) {
row[col.key][index] = util.baseURL() + val.slice(1)
} else {
row[col.key][index] = !val.startsWith('http') ? util.baseURL() + val : val

View File

@ -252,6 +252,10 @@ Vue.prototype.commonEndColumns = function (param = {}) {
showForm: (param.update_datetime && param.update_datetime.showForm) !== undefined ? param.update_datetime.showForm : false,
showTable: (param.update_datetime && param.update_datetime.showTable) !== undefined ? param.update_datetime.showTable : true
},
creator_name: {
showForm: (param.creator_name && param.creator_name.showForm) !== undefined ? param.creator_name.showForm : false,
showTable: (param.creator_name && param.creator_name.showTable) !== undefined ? param.creator_name.showTable : false
},
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

View File

@ -0,0 +1,55 @@
<template>
<div>
<el-divider content-position="left">消息中心</el-divider>
<div v-if="msgObj">
<h3>{{msgObj.title}}</h3>
<div class="content-style">{{msgObj.content}}</div>
</div>
<div v-else>
<el-empty :image-size="100"></el-empty>
</div>
<el-divider></el-divider>
<div style="text-align: center">
<el-button type="text" @click="toPage"></el-button>
</div>
</div>
</template>
<script>
import {mapActions} from 'vuex'
export default {
name: 'msgList',
props: {
msgObj: {
type: Object,
default: null
}
},
methods: {
...mapActions('d2admin/page', [
'open'
]),
toPage () {
// this.open({name:'messageCenter'})
this.$router.push({
name:'messageCenter'
})
}
}
}
</script>
<style scoped>
.msg-list{
padding: 5px 0px;
border-bottom: 1px solid #eeeeee;
}
.content-style{
width: 370px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3; /*3表示只显示3行*/
-webkit-box-orient: vertical;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div>
<el-tooltip
effect="dark"
content="通知"
placement="bottom">
<el-popover
placement="bottom"
width="400"
trigger="click">
<msg-list :msgObj="msgObj"></msg-list>
<el-button
class="d2-ml-0 d2-mr btn-text can-hover"
type="text"
slot="reference" @click="getList">
<el-badge
v-if="unread > 0"
:max="99"
:value="unread"
:is-dot="unread === 0"
>
<d2-icon
:name="unread === 0 ? 'dot-circle-o' : 'bell-o'"
style="font-size: 20px"
/>
</el-badge>
<d2-icon
v-else
name="bell-o"
style="font-size: 16px"/>
</el-button>
</el-popover>
</el-tooltip>
</div>
</template>
<script>
import msgList from './components/msg-list'
import { request } from '@/api/service'
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters('d2admin', {
unread: 'messagecenter/unread'
})
},
components: {
msgList
},
data () {
return {
msgObj: null
}
},
methods: {
getList () {
request({
url: '/api/system/message_center/get_newest_msg/',
method: 'get',
params: {}
}).then(res => {
const { data } = res
this.msgObj = data
})
}
}
}
</script>

View File

@ -41,6 +41,7 @@
<d2-header-log />
<d2-header-fullscreen />
<d2-header-theme />
<d2-header-message />
<d2-header-size />
<d2-header-locales />
<d2-header-color />
@ -111,6 +112,7 @@ import d2HeaderTheme from './components/header-theme'
import d2HeaderUser from './components/header-user'
import d2HeaderLog from './components/header-log'
import d2HeaderColor from './components/header-color'
import d2HeaderMessage from './components/header-message'
import { mapState, mapGetters, mapActions } from 'vuex'
import mixinSearch from './mixins/search'
export default {
@ -127,7 +129,8 @@ export default {
d2HeaderTheme,
d2HeaderUser,
d2HeaderLog,
d2HeaderColor
d2HeaderColor,
d2HeaderMessage
},
provide () {
return {

View File

@ -62,6 +62,38 @@ util.baseURL = function () {
}
return baseURL
}
util.wsBaseURL = function () {
var baseURL = process.env.VUE_APP_API
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]
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 + '/' + param
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
} else if (param !== '' || baseURL.startsWith('/')) {
baseURL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
if (!baseURL.endsWith('/')) {
baseURL += '/'
}
if (baseURL.startsWith('http')) { // https 也默认会被替换成 wss
baseURL = baseURL.replace('http', 'ws')
}
return baseURL
}
/**
* 自动生成ID
*/

View File

@ -53,7 +53,7 @@ export const checkRouter = function (menuData) {
for (const item of menuData) {
try {
if (item.path !== '' && item.component) {
(item.component && item.component.indexOf('@great-dream/') !== -1) ? pluginImport(item.component.replace('@great-dream/', '')) : _import(item.component)
(item.component && item.component.substr(0, 8) === 'plugins/') ? pluginImport(item.component.replace('plugins/', '')) : _import(item.component)
}
result.push(item)
} catch (err) {
@ -72,7 +72,7 @@ export const handleRouter = function (menuData) {
const obj = {
path: item.path,
name: item.component_name,
component: (item.component && item.component.indexOf('@great-dream/') !== -1) ? pluginImport(item.component.replace('@great-dream/', '')) : _import(item.component),
component: (item.component && item.component.substr(0, 8) === 'plugins/') ? pluginImport(item.component.replace('plugins/', '')) : _import(item.component),
meta: {
title: item.name,
auth: true,

View File

@ -0,0 +1,35 @@
export default {
namespaced: true,
state: {
// 未读消息
unread: 0
},
getters: {
unread (state) {
return state.unread
}
},
actions: {
/**
* @description 添加一个日志
* @param {Object} context
* @param {String} param message {String} 信息
* @param {String} param type {String} 类型
* @param {Object} payload meta {Object} 附带的信息
*/
async setUnread ({ state,commit },number) {
commit('set',number)
}
},
mutations: {
/**
* 设置未读消息
* @param state
* @param number
*/
async set (state, number) {
state.unread = number
}
}
}

View File

@ -3,8 +3,9 @@ import { request } from '@/api/service'
export const urlPrefix = '/api/system/area/'
export function GetList (query) {
if (query.pcode === undefined || query.pcode === null || query.pcode.length === 0) {
if ((!query.pcode || query.pcode.length === 0) && !query.name && !query.code) {
query.level = 1
delete query.pcode
}
return request({
url: urlPrefix,

View File

@ -70,24 +70,6 @@ export const crudOptions = (vm) => {
width: 100
},
columns: [
{
title: '关键词',
key: 'search',
show: false,
disabled: true,
search: {
disabled: false
},
form: {
disabled: true,
component: {
placeholder: '请输入关键词'
}
},
view: {
disabled: true
}
},
{
title: 'ID',
key: 'id',

View File

@ -13,6 +13,14 @@ export function GetList (query) {
})
}
export function GetListAll (query) {
return request({
url: urlPrefix + 'all_dept/',
method: 'get',
params: query
})
}
/**
* 新增
*/

View File

@ -16,6 +16,11 @@
@click="addRow"
><i class="el-icon-plus" /> 新增</el-button
>
<importExcel
importApi="api/system/dept/import_data/"
v-permission="'Import'"
>导入
</importExcel>
</el-button-group>
<crud-toolbar
:search.sync="crud.searchOptions.show"

View File

@ -240,7 +240,7 @@ export const crudOptions = (vm) => {
disabled: false
},
dict: {
data: [{ label: '普通登录', value: 1 }]
data: [{ label: '普通登录', value: 1 }, { label: '微信扫码登录', value: 2 }]
},
form: {
component: {

View File

@ -14,14 +14,12 @@ export const urlPrefix = '/api/system/menu/'
* 列表查询
*/
export function GetList (query) {
query.limit = 999
return request({
url: urlPrefix,
method: 'get',
params: { ...query, limit: 999 }
params: { ...query }
}).then(res => {
// 将列表数据转换为树形数据
// res.data.data = XEUtils.toArrayTree(res.data.data, { parentKey: 'parent', strict: false })
return res
})
}

View File

@ -34,7 +34,10 @@ export const crudOptions = (vm) => {
transform: true,
rowField: 'id',
parentField: 'parent',
expandAll: true
expandAll: true,
hasChild: 'hasChild',
lazy: true,
loadMethod: vm.loadContentMethod
}
},
rowHandle: {

View File

@ -86,6 +86,18 @@ export default {
})
})
return result
},
/**
* 懒加载
* @param row
* @returns {Promise<unknown>}
*/
loadContentMethod ({ row }) {
return new Promise(resolve => {
api.GetList({ parent: row.id }).then(res => {
resolve(res.data.data)
})
})
}
}
}

View File

@ -52,3 +52,5 @@ export function DelObj (id) {
data: { id }
})
}

View File

@ -3,13 +3,52 @@ import { request } from '@/api/service'
export const crudOptions = (vm) => {
return {
indexRow: { // 或者直接传true,不显示title不居中
width: 60,
title: '序号',
align: 'center'
},
options: {
tableType: 'vxe-table',
rowKey: true, // 必须设置true or false
height: '100%' // 表格高度100%, 使用toolbar必须设置
},
viewOptions: {
rowHandle: {
width: 160,
fixed: 'right',
view: false,
edit: {
thin: true,
text: '',
show () {
return vm.tabActivted !== 'receive'
},
disabled () {
return !vm.hasPermissions('Update')
}
},
remove: {
thin: true,
text: '',
show () {
return vm.tabActivted !== 'receive'
},
disabled () {
return !vm.hasPermissions('Delete')
}
},
custom: [
{
thin: true,
text: null,
icon: 'el-icon-view',
size: 'small',
disabled () {
return !vm.hasPermissions('Retrieve')
},
order: 1,
emit: 'onView'
}
]
},
columns: [
{
@ -24,7 +63,7 @@ export const crudOptions = (vm) => {
search: {
disabled: false
},
width: 400,
width: 200,
form: {
rules: [ // 表单校验规则
{
@ -35,12 +74,40 @@ export const crudOptions = (vm) => {
component: { span: 24, placeholder: '请输入标题' }
}
},
{
title: '是否已读',
key: 'is_read',
type: 'select',
width: 100,
show () {
return vm.tabActivted === 'receive'
},
dict: {
data: [
{ label: '已读', value: true, color: 'success' },
{ label: '未读', value: false, color: 'danger' }
]
},
form: {
disabled: true
}
},
{
title: '目标类型',
key: 'target_type',
type: 'radio',
dict: { data: [{ value: 0, label: '按用户' }, { value: 1, label: '按角色' }, { value: 2, label: '按部门' }] },
width: 120,
show () {
return vm.tabActivted === 'send'
},
dict: { data: [{ value: 0, label: '按用户' }, { value: 1, label: '按角色' }, { value: 2, label: '按部门' }, { value: 3, label: '通知公告' }] },
form: {
component: {
span: 24,
props: {
type: 'el-radio-button'
}
},
rules: [
{
required: true,
@ -56,8 +123,9 @@ export const crudOptions = (vm) => {
search: {
disabled: true
},
minWidth: 130,
width: 130,
type: 'table-selector',
disabled: true,
dict: {
cache: false,
url: '/api/system/user/',
@ -124,8 +192,8 @@ export const crudOptions = (vm) => {
search: {
disabled: true
},
minWidth: 130,
disabled: true,
width: 130,
type: 'table-selector',
dict: {
cache: false,
@ -193,11 +261,11 @@ export const crudOptions = (vm) => {
search: {
disabled: true
},
minWidth: 130,
width: 130,
type: 'table-selector',
dict: {
cache: false,
url: '/api/system/dept/',
url: '/api/system/dept/all_dept/',
isTree: true,
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
@ -207,16 +275,13 @@ export const crudOptions = (vm) => {
component
}) => {
return request({
url: url,
params: {
page: 1,
limit: 999
}
url: url
}).then(ret => {
return ret.data.data
})
}
},
disabled: true,
form: {
rules: [ // 表单校验规则
{
@ -269,7 +334,7 @@ export const crudOptions = (vm) => {
{
title: '内容',
key: 'content',
width: 300,
minWidth: 300,
type: 'editor-quill', // 富文本图片上传依赖file-uploader请先配置好file-uploader
form: {
rules: [ // 表单校验规则
@ -295,6 +360,9 @@ export const crudOptions = (vm) => {
}
}
}
]
].concat(vm.commonEndColumns({
create_datetime: { showTable: true },
update_datetime: { showTable: false }
}))
}
}

View File

@ -5,7 +5,7 @@
ref="d2Crud"
v-bind="_crudProps"
v-on="_crudListeners"
@form-component-ready="handleFormComponentReady"
@onView="onView"
>
<div slot="header">
<crud-search ref="search" :options="crud.searchOptions" @submit="handleSearch" />
@ -29,6 +29,7 @@
import { AddObj, GetObj, GetList, UpdateObj, DelObj, GetSelfReceive } from './api'
import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus'
import viewTemplate from './viewTemplate.js'
export default {
name: 'messageCenter',
components: {},
@ -38,10 +39,7 @@ export default {
tabActivted: 'send'
}
},
created () {
//
this.crud.options.fetchDetail = this.fetchDetail
},
computed: {
},
methods: {
@ -61,7 +59,7 @@ export default {
return AddObj(row).then(res => {
const message = {
message_id: res.data.id,
contentType: 'TEXT',
contentType: 'INFO',
content: '您有新的消息,请到消息中心查看~'
}
this.$websocket.webSocketSend(message)
@ -73,20 +71,18 @@ export default {
delRequest (row) {
return DelObj(row.id)
},
//
fetchDetail (index, row) {
if (index == null) {
//
return {}
}
return GetObj(row).then(res => {
return res.data
onView ({ row, index }) {
this.getD2Crud().showDialog({
mode: 'view',
rowIndex: index,
template: viewTemplate
})
this.infoRequest(row)
this.doRefresh()
},
handleFormComponentReady (event, key, form) {
// console.log('form component ready:', event, key, form)
},
onTabClick (obj) {
onTabClick (tab) {
const { name } = tab
this.tabActivted = name
this.doRefresh()
}
}

View File

@ -0,0 +1,40 @@
export default {
title: {
title: '标题',
key: 'title',
component: {
span: 24,
placeholder: '请输入标题',
disabled: true
},
rules: [
{
required: true,
message: '必填项'
}
],
order: 10
},
content: {
title: '内容',
key: 'content',
component: {
name: 'd2p-quill',
span: 24,
disabled: true,
props: {
uploader: {
type: 'form'
}
},
events: {}
},
rules: [
{
required: true,
message: '必填项'
}
],
order: 10
}
}

View File

@ -163,7 +163,7 @@ export default {
},
{
value: 4,
label: '自定数据权限'
label: '自定数据权限'
}
],
dataAuthorizationTips: '授权用户可操作的数据范围',
@ -212,8 +212,8 @@ export default {
},
//
getDeptData () {
deptApi.GetList({ status: 1 }).then(ret => {
this.deptOptions = ret.data.data
deptApi.GetListAll().then(ret => {
this.deptOptions = XEUtils.toArrayTree(ret.data, { parentKey: 'parent', strict: false })
})
},
//

View File

@ -206,11 +206,76 @@ export const crudOptions = (vm) => {
valueBinding: 'dept_name'
}
},
{
title: '角色',
key: 'role',
search: {
disabled: true
},
minWidth: 130,
type: 'table-selector',
dict: {
cache: false,
url: '/api/system/role/',
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
getData: (url, dict, {
form,
component
}) => {
return request({
url: url,
params: {
page: 1,
limit: 10
}
}).then(ret => {
component._elProps.page = ret.data.page
component._elProps.limit = ret.data.limit
component._elProps.total = ret.data.total
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
itemProps: {
class: { yxtInput: true }
},
component: {
span: 12,
pagination: true,
props: { multiple: true },
elProps: {
columns: [
{
field: 'name',
title: '角色名称'
},
{
field: 'key',
title: '权限标识'
}
]
}
}
},
component: {
name: 'manyToMany',
valueBinding: 'role_info',
children: 'name'
}
},
{
title: '手机号码',
key: 'mobile',
search: {
disabled: true
disabled: false
},
minWidth: 110,
type: 'input',
@ -320,71 +385,6 @@ export const crudOptions = (vm) => {
},
helper: '限制文件大小不能超过500k'
}
},
{
title: '角色',
key: 'role',
search: {
disabled: true
},
minWidth: 130,
type: 'table-selector',
dict: {
cache: false,
url: '/api/system/role/',
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
getData: (url, dict, {
form,
component
}) => {
return request({
url: url,
params: {
page: 1,
limit: 10
}
}).then(ret => {
component._elProps.page = ret.data.page
component._elProps.limit = ret.data.limit
component._elProps.total = ret.data.total
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
itemProps: {
class: { yxtInput: true }
},
component: {
span: 12,
pagination: true,
props: { multiple: true },
elProps: {
columns: [
{
field: 'name',
title: '角色名称'
},
{
field: 'key',
title: '权限标识'
}
]
}
}
},
component: {
name: 'manyToMany',
valueBinding: 'role_info',
children: 'name'
}
}
].concat(vm.commonEndColumns({
create_datetime: { showTable: false },