43
README.zh.md
43
README.zh.md
@@ -54,16 +54,21 @@
|
||||
## 交流
|
||||
|
||||
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩👦👦
|
||||
|
||||
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩👦👦
|
||||
|
||||
- django-vue-admin交流01群(已满):812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
|
||||
- django-vue-admin交流02群(已满):687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
|
||||
- django-vue-admin交流03群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
- django-vue-admin交流03群(已满):442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
- django-vue-admin交流04群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
|
||||
- 二维码
|
||||
|
||||
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
|
||||
|
||||
## 给框架点赞
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<img src='https://django-vue-admin.com/alipay.jpg' width='200'>
|
||||
<img src='https://django-vue-admin.com/wechat.jpg' width='200'>
|
||||
</div>
|
||||
|
||||
|
||||
## 源码地址
|
||||
|
||||
@@ -88,7 +93,19 @@ github地址:[https://github.com/huge-dream/django-vue3-admin](https://github.
|
||||
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。
|
||||
|
||||
## 插件市场 🔌
|
||||
更新中...
|
||||
1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html)
|
||||
|
||||
2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html)
|
||||
|
||||
3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html)
|
||||
|
||||
4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html)
|
||||
|
||||
5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git)
|
||||
|
||||
6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线)
|
||||
|
||||
|
||||
|
||||
## 仓库分支说明 💈
|
||||
主分支:master(稳定版本)
|
||||
@@ -210,5 +227,19 @@ docker-compose up -d --build
|
||||
|
||||

|
||||
|
||||
## 审批流插件
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -98,5 +98,4 @@ media/
|
||||
__pypackages__/
|
||||
package-lock.json
|
||||
gunicorn.pid
|
||||
plugins/*
|
||||
!plugins/__init__.py
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import functools
|
||||
import os
|
||||
|
||||
from celery.signals import task_postrun
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
|
||||
from django.conf import settings
|
||||
@@ -38,3 +40,12 @@ def retry_base_task_error():
|
||||
return wrapper
|
||||
|
||||
return wraps
|
||||
|
||||
|
||||
@task_postrun.connect
|
||||
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
|
||||
periodic_task_name = kwargs.get('periodic_task_name')
|
||||
if periodic_task_name:
|
||||
from django_celery_results.models import TaskResult
|
||||
# 更新 TaskResult 表中的 periodic_task_name 字段
|
||||
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)
|
||||
|
||||
@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
|
||||
# ********** 一键导入插件配置开始 **********
|
||||
# 例如:
|
||||
# from dvadmin_upgrade_center.settings import * # 升级中心
|
||||
# from dvadmin3_celery.settings import * # celery 异步任务
|
||||
from dvadmin3_celery.settings import * # celery 异步任务
|
||||
# from dvadmin_third.settings import * # 第三方用户管理
|
||||
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
|
||||
# from dvadmin_tenants.settings import * # 租户管理
|
||||
|
||||
@@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化获取数信息(用于生成初始化json文件)
|
||||
"""
|
||||
role_key = serializers.SerializerMethodField()
|
||||
dept_key = serializers.SerializerMethodField()
|
||||
|
||||
def get_dept_key(self, obj):
|
||||
if obj.dept:
|
||||
return obj.dept.key
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_role_key(self, obj):
|
||||
if obj.role.all():
|
||||
return [role.key for role in obj.role.all()]
|
||||
else:
|
||||
return []
|
||||
|
||||
def save(self, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
@@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer):
|
||||
model = Users
|
||||
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
|
||||
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
|
||||
'password', 'last_login', 'is_superuser']
|
||||
'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key']
|
||||
read_only_fields = ['id']
|
||||
extra_kwargs = {
|
||||
'creator': {'write_only': True},
|
||||
@@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化角色菜单(用于生成初始化json文件)
|
||||
"""
|
||||
role__key = serializers.CharField(max_length=100, required=True)
|
||||
menu__web_path = serializers.CharField(max_length=100, required=True)
|
||||
menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True)
|
||||
role__key = serializers.CharField(source='role.key')
|
||||
menu__web_path = serializers.CharField(source='menu.web_path')
|
||||
menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
init_data = self.initial_data
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu'] = menu_id
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
def create(self, validated_data):
|
||||
init_data = self.initial_data
|
||||
validated_data.pop('menu__web_path')
|
||||
validated_data.pop('menu__component_name')
|
||||
validated_data.pop('role__key')
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
|
||||
validated_data['role'] = role_id
|
||||
@@ -206,14 +226,22 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化角色菜单按钮(用于生成初始化json文件)
|
||||
"""
|
||||
role__key = serializers.CharField(max_length=100, required=True)
|
||||
menu_button__value = serializers.CharField(max_length=100, required=True)
|
||||
role__key = serializers.CharField(source='role.key')
|
||||
menu_button__value = serializers.CharField(source='menu_button.value')
|
||||
data_range = serializers.CharField(max_length=100, required=False)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
init_data = self.initial_data
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
|
||||
validated_data['role'] = role_id
|
||||
validated_data['menu_button'] = menu_button_id
|
||||
instance = super().create(validated_data)
|
||||
instance.dept.set([])
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def create(self, validated_data):
|
||||
init_data = self.initial_data
|
||||
validated_data.pop('menu_button__value')
|
||||
validated_data.pop('role__key')
|
||||
role_id = Role.objects.filter(key=init_data['role__key']).first()
|
||||
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
|
||||
validated_data['role'] = role_id
|
||||
@@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
return instance
|
||||
|
||||
def save(self, **kwargs):
|
||||
if self.instance and self.initial_data.get('reset'):
|
||||
if not self.instance or self.initial_data.get('reset'):
|
||||
return super().save(**kwargs)
|
||||
return self.instance
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ django.setup()
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from application.settings import BASE_DIR
|
||||
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig
|
||||
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission
|
||||
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
|
||||
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
|
||||
RoleMenuInitSerializer, RoleMenuButtonInitSerializer
|
||||
@@ -57,6 +57,12 @@ class Command(BaseCommand):
|
||||
def generate_system_config(self):
|
||||
self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True))
|
||||
|
||||
def generate_role_menu(self):
|
||||
self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all())
|
||||
|
||||
def generate_role_menu_button(self):
|
||||
self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all())
|
||||
|
||||
def handle(self, *args, **options):
|
||||
generate_name = options.get('generate_name')
|
||||
generate_name_dict = {
|
||||
@@ -67,6 +73,8 @@ class Command(BaseCommand):
|
||||
"api_white_list": self.generate_api_white_list,
|
||||
"dictionary": self.generate_dictionary,
|
||||
"system_config": self.generate_system_config,
|
||||
"role_menu": self.generate_role_menu,
|
||||
"role_menu_button": self.generate_role_menu_button,
|
||||
}
|
||||
if not generate_name:
|
||||
for ele in generate_name_dict.keys():
|
||||
|
||||
@@ -49,7 +49,7 @@ urlpatterns = [
|
||||
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
|
||||
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
|
||||
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
|
||||
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
|
||||
# path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
|
||||
path('clause/privacy.html', PrivacyView.as_view()),
|
||||
path('clause/terms_service.html', TermsServiceView.as_view()),
|
||||
]
|
||||
|
||||
@@ -10,16 +10,17 @@ from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Role, Menu, MenuButton, Dept
|
||||
from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
|
||||
from dvadmin.system.views.dept import DeptSerializer
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.system.views.menu_button import MenuButtonSerializer
|
||||
from dvadmin.utils.crud_mixin import FastCrudMixin
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
|
||||
from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
|
||||
|
||||
class RoleSerializer(CustomModelSerializer):
|
||||
@@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
"""
|
||||
角色管理接口
|
||||
@@ -142,3 +142,62 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
role.users_set.add(*movedKeys)
|
||||
serializer = RoleSerializer(role)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
|
||||
def get_role_users(self, request):
|
||||
"""
|
||||
获取角色已授权、未授权的用户
|
||||
已授权的用户:1
|
||||
未授权的用户:0
|
||||
"""
|
||||
role_id = request.query_params.get('role_id', None)
|
||||
|
||||
if not role_id:
|
||||
return ErrorResponse(msg="请选择角色")
|
||||
|
||||
if request.query_params.get('authorized', 0) == "1":
|
||||
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
|
||||
else:
|
||||
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
|
||||
|
||||
if name := request.query_params.get('name', None):
|
||||
queryset = queryset.filter(name__icontains=name)
|
||||
|
||||
if dept := request.query_params.get('dept', None):
|
||||
queryset = queryset.filter(dept=dept)
|
||||
|
||||
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
|
||||
if page is not None:
|
||||
return self.get_paginated_response(page)
|
||||
|
||||
return SuccessResponse(data=page)
|
||||
|
||||
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
|
||||
def remove_role_user(self, request, pk):
|
||||
"""
|
||||
角色-删除用户
|
||||
"""
|
||||
user_id = request.data.get('user_id', None)
|
||||
|
||||
if not user_id:
|
||||
return ErrorResponse(msg="请选择用户")
|
||||
|
||||
role = self.get_object()
|
||||
role.users_set.remove(*user_id)
|
||||
|
||||
return SuccessResponse(msg="删除成功")
|
||||
|
||||
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
|
||||
def add_role_users(self, request, pk):
|
||||
"""
|
||||
角色-添加用户
|
||||
"""
|
||||
users_id = request.data.get('users_id', None)
|
||||
|
||||
if not users_id:
|
||||
return ErrorResponse(msg="请选择用户")
|
||||
|
||||
role = self.get_object()
|
||||
role.users_set.add(*users_id)
|
||||
|
||||
return DetailResponse(msg="添加成功")
|
||||
|
||||
@@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
isCheck = data.get('isCheck', None)
|
||||
roleId = data.get('roleId', None)
|
||||
btnId = data.get('btnId', None)
|
||||
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
|
||||
dept = data.get('dept', None) or [] # 默认空部门
|
||||
|
||||
if isCheck:
|
||||
# 添加权限:创建关联记录
|
||||
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId)
|
||||
instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
|
||||
menu_button_id=btnId,
|
||||
data_range=data_range)
|
||||
# 自定义部门权限
|
||||
if data_range == 4 and dept:
|
||||
instance.dept.set(dept)
|
||||
else:
|
||||
# 删除权限:移除关联记录
|
||||
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()
|
||||
|
||||
@@ -336,7 +336,7 @@ class UserViewSet(CustomModelViewSet):
|
||||
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
||||
if verify_password:
|
||||
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.password = make_password(new_pwd)
|
||||
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.pwd_change_count += 1
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
|
||||
@@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters.utils import get_model_field
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from django_filters.conf import settings
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton
|
||||
from dvadmin.utils.models import CoreModel
|
||||
|
||||
class CoreModelFilterBankend(BaseFilterBackend):
|
||||
@@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend):
|
||||
create_datetime_after = request.query_params.get('create_datetime_after', None)
|
||||
create_datetime_before = request.query_params.get('create_datetime_before', None)
|
||||
update_datetime_after = request.query_params.get('update_datetime_after', None)
|
||||
update_datetime_before = request.query_params.get('update_datetime_after', None)
|
||||
update_datetime_before = request.query_params.get('update_datetime_before', None)
|
||||
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
|
||||
create_filter = Q()
|
||||
if create_datetime_after and create_datetime_before:
|
||||
@@ -149,11 +149,14 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
if _pk: # 判断是否是单例查询
|
||||
re_api = re.sub(_pk,'{id}', api)
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
# 修复权限获取bug
|
||||
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
|
||||
role_permission_list = []
|
||||
if menu_button_ids:
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button__api=re_api,
|
||||
menu_button__method=method).values(
|
||||
menu_button_id__in=menu_button_ids).values(
|
||||
'data_range'
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
@Created on: 2021/6/1 001 22:57
|
||||
@Remark: 自定义视图集
|
||||
"""
|
||||
import copy
|
||||
|
||||
from django.db import transaction
|
||||
from django_filters import DateTimeFromToRangeFilter
|
||||
from django_filters.rest_framework import FilterSet
|
||||
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
kwargs.setdefault('context', self.get_serializer_context())
|
||||
# 全部以可见字段为准
|
||||
can_see = self.get_menu_field(serializer_class)
|
||||
# 排除掉序列化器级的字段
|
||||
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
|
||||
# for field in sub_set:
|
||||
# serializer_class._declared_fields.pop(field)
|
||||
# 排除掉序列化器级的字段(排除字段权限中未授权的字段)
|
||||
# if not self.request.user.is_superuser:
|
||||
# serializer_class.Meta.fields = can_see
|
||||
# exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see)
|
||||
# for field in exclude_set:
|
||||
# serializer_class._declared_fields.pop(field)
|
||||
# meta = copy.deepcopy(serializer_class.Meta)
|
||||
# meta.fields = list(can_see)
|
||||
# serializer_class.Meta = meta
|
||||
# 在分页器中使用
|
||||
self.request.permission_fields = can_see
|
||||
if isinstance(self.request.data, list):
|
||||
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
|
||||
def get_menu_field(self, serializer_class):
|
||||
"""获取字段权限"""
|
||||
finded = False
|
||||
for model in get_custom_app_models():
|
||||
if model['object'] is serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded is False:
|
||||
|
||||
if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
|
||||
return []
|
||||
return MenuField.objects.filter(model=model['model']
|
||||
).values('field_name', 'title')
|
||||
|
||||
# 匿名用户没有角色
|
||||
ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__)
|
||||
if hasattr(self.request.user, 'role'):
|
||||
roles = self.request.user.role.values_list('id', flat=True)
|
||||
ret = ret.filter(is_query=True, role__in=roles)
|
||||
|
||||
return ret.values_list('field__field_name', flat=True)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data, request=request)
|
||||
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
instance.delete()
|
||||
return DetailResponse(data=[], msg="删除成功")
|
||||
|
||||
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING)
|
||||
|
||||
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING))
|
||||
@swagger_auto_schema(request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['keys'],
|
||||
|
||||
@@ -29,3 +29,4 @@ gunicorn==22.0.0
|
||||
gevent==24.2.1
|
||||
Pillow==10.4.0
|
||||
pyinstaller==6.9.0
|
||||
dvadmin3-celery==3.1.6
|
||||
87
crud-gen.sh
Normal file
87
crud-gen.sh
Normal file
@@ -0,0 +1,87 @@
|
||||
if ! [ -f ".env" ];then
|
||||
echo ".env file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$3" ]; then
|
||||
echo "Use: $0 <app_name> <view_name> <table_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
DIR=./web/src/views/$1/$2
|
||||
|
||||
|
||||
# 设置数据库连接信息
|
||||
HOST="177.10.0.13"
|
||||
USER="root"
|
||||
PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g')
|
||||
DATABASE="django-vue3-admin"
|
||||
TABLE=$3
|
||||
TARGET_FILE="./web/src/views/$1/$2/crud.tsx"
|
||||
|
||||
|
||||
# 表是否存在
|
||||
TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l)
|
||||
|
||||
if [ "$TABLE_EXISTS" -eq 0 ]; then
|
||||
echo "Table $TABLE does not exist in database $DATABASE."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p $DIR
|
||||
cp -r ./web/src/views/template/* $DIR
|
||||
sed -i "s/VIEWSETNAME/$2/g" $DIR/*
|
||||
|
||||
sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE
|
||||
|
||||
# 查询表结构
|
||||
QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;"
|
||||
|
||||
# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置
|
||||
mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do
|
||||
# 映射 MySQL 数据类型到 fast-crud 类型
|
||||
case "$DATA_TYPE" in
|
||||
"int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double")
|
||||
TYPE="number"
|
||||
;;
|
||||
"date"|"datetime"|"timestamp")
|
||||
TYPE="date"
|
||||
;;
|
||||
*)
|
||||
TYPE="text"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " $COLUMN_NAME: {
|
||||
title: '$COLUMN_NAME',
|
||||
type: '$TYPE',
|
||||
search: { show: true },
|
||||
column: {
|
||||
minWidth: 120,
|
||||
sortable: 'custom',
|
||||
},
|
||||
form: {" >> $TARGET_FILE
|
||||
|
||||
if [ "$IS_NULLABLE" = "NO" ]; then
|
||||
echo " helper: {
|
||||
render() {
|
||||
return <div style={"color:blue"}>$COLUMN_NAME 是必填的</div>;
|
||||
}
|
||||
},
|
||||
rules: [{
|
||||
required: true, message: '$COLUMN_NAME 是必填的'
|
||||
}]," >> $TARGET_FILE
|
||||
fi
|
||||
|
||||
echo " component: {
|
||||
placeholder: '请输入 $COLUMN_NAME',
|
||||
},
|
||||
},
|
||||
}," >> $TARGET_FILE
|
||||
done
|
||||
|
||||
echo " },
|
||||
},
|
||||
};
|
||||
}" >> $TARGET_FILE
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine
|
||||
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine
|
||||
WORKDIR /web/
|
||||
COPY web/. .
|
||||
RUN yarn install --registry=https://registry.npmmirror.com
|
||||
|
||||
52
init.sh
52
init.sh
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
ENV_FILE=".env"
|
||||
HOST="177.10.0.13"
|
||||
# 检查 .env 文件是否存在
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "$ENV_FILE 文件已存在。"
|
||||
@@ -15,17 +16,60 @@ else
|
||||
echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。"
|
||||
|
||||
awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }'
|
||||
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py
|
||||
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py
|
||||
sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py
|
||||
sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py
|
||||
sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py
|
||||
echo "初始化密码创建成功"
|
||||
fi
|
||||
|
||||
echo "正在启动容器..."
|
||||
docker-compose up -d
|
||||
docker exec dvadmin3-django python manage.py makemigrations
|
||||
docker exec dvadmin3-django python manage.py migrate
|
||||
docker exec dvadmin3-django python manage.py init
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "docker-compose up -d 执行失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MYSQL_PORT=3306
|
||||
REDIS_PORT=6379
|
||||
|
||||
check_mysql() {
|
||||
if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then
|
||||
echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_redis() {
|
||||
if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then
|
||||
echo "Redis 服务正在运行在 $HOST:$REDIS_PORT"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
i=1
|
||||
while [ $i -le 8 ]; do
|
||||
if check_mysql || check_redis; then
|
||||
echo "正在迁移数据..."
|
||||
docker exec dvadmin3-django python3 manage.py makemigrations
|
||||
docker exec dvadmin3-django python3 manage.py migrate
|
||||
echo "正在初始化数据..."
|
||||
docker exec dvadmin3-django python3 manage.py init
|
||||
echo "欢迎使用dvadmin3项目"
|
||||
echo "登录地址:http://ip:8080"
|
||||
echo "如访问不到,请检查防火墙配置"
|
||||
exit 0
|
||||
else
|
||||
echo "第 $i 次尝试:MySQL 或 REDIS服务未运行,等待 2 秒后重试..."
|
||||
sleep 2
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
|
||||
echo "尝试 5 次后,MySQL 或 REDIS服务仍未运行"
|
||||
exit 1
|
||||
|
||||
2
web/.env
2
web/.env
@@ -1,6 +1,6 @@
|
||||
# port 端口号
|
||||
VITE_PORT = 8080
|
||||
VITE_API_URL = 'http://dvadmin3api.django.icu:8001'
|
||||
VITE_API_URL = 'http://127.0.0.1:8000'
|
||||
# open 运行 npm run dev 时自动打开浏览器
|
||||
VITE_OPEN = false
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
ENV = 'development'
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = 'http://127.0.0.1:8000'
|
||||
VITE_API_URL = 'http://127.0.0.1:8001'
|
||||
|
||||
# 是否启用按钮权限
|
||||
VITE_PM_ENABLED = true
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
👩👦👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
|
||||
|
||||
|
||||
## 给框架点赞
|
||||
<img src='https://django-vue-admin.com/alipay.jpg' width='200'>
|
||||
<img src='https://django-vue-admin.com/wechat.jpg' width='200'>
|
||||
|
||||
|
||||
## 交流
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --force",
|
||||
"build:dev":"vite build --mode development",
|
||||
"build": "vite build",
|
||||
"build:local": "vite build --mode local_prod",
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
@@ -15,6 +16,7 @@
|
||||
"@fast-crud/fast-extends": "^1.21.2",
|
||||
"@fast-crud/ui-element": "^1.21.2",
|
||||
"@fast-crud/ui-interface": "^1.21.2",
|
||||
"@great-dream/dvadmin3-celery-web": "^3.1.3",
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script setup lang="ts" name="app">
|
||||
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
@@ -26,7 +26,8 @@ const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index
|
||||
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
|
||||
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
|
||||
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
|
||||
|
||||
import { ElMessageBox, ElNotification, NotificationHandle } from 'element-plus';
|
||||
import { useCore } from '/@/utils/cores';
|
||||
// 定义变量内容
|
||||
const { messages, locale } = useI18n();
|
||||
const setingsRef = ref();
|
||||
@@ -35,7 +36,8 @@ const stores = useTagsViewRoutes();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
import websocket from '/@/utils/websocket';
|
||||
import { ElNotification } from 'element-plus';
|
||||
const core = useCore();
|
||||
const router = useRouter();
|
||||
// 获取版本号
|
||||
const getVersion = computed(() => {
|
||||
let isVersion = false;
|
||||
@@ -67,7 +69,15 @@ onMounted(() => {
|
||||
mittBus.on('openSetingsDrawer', () => {
|
||||
setingsRef.value.openDrawer();
|
||||
});
|
||||
// 设置皮肤缓存版本,每次更新版本可以所有用户清空缓存
|
||||
const themeConfigVersion = '1.0.0'
|
||||
// 获取缓存中的布局配置
|
||||
if (Local.get('themeConfigVersion') !== themeConfigVersion) {
|
||||
Local.clear();
|
||||
Local.set('themeConfigVersion', themeConfigVersion);
|
||||
window.location.reload();
|
||||
return
|
||||
}
|
||||
if (Local.get('themeConfig')) {
|
||||
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
|
||||
document.documentElement.style.cssText = Local.get('themeConfigStyle');
|
||||
@@ -117,7 +127,25 @@ const wsReceive = (message: any) => {
|
||||
position: 'bottom-right',
|
||||
duration: 5000,
|
||||
});
|
||||
} else if (data.contentType === 'Content') {
|
||||
ElMessageBox.confirm(data.content, data.notificationTitle, {
|
||||
confirmButtonText: data.notificationButton,
|
||||
dangerouslyUseHTMLString: true,
|
||||
cancelButtonText: '关闭',
|
||||
type: 'info',
|
||||
closeOnClickModal: false,
|
||||
}).then(() => {
|
||||
ElMessageBox.close();
|
||||
const path = data.path;
|
||||
if (route.path === path) {
|
||||
core.bus.emit('onNewTask', { name: 'onNewTask' });
|
||||
} else {
|
||||
router.push({ path});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
// 关闭连接
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="user-info-head" @click="editCropper()">
|
||||
<el-avatar :size="100" :src="options.img" />
|
||||
<el-avatar :size="100" :src="getBaseURL(options.img)" />
|
||||
<el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
|
||||
<el-row>
|
||||
<el-col class="flex justify-center">
|
||||
@@ -50,10 +50,11 @@ import { VueCropper } from 'vue-cropper';
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
|
||||
import { base64ToFile } from '/@/utils/tools';
|
||||
import headerImage from "/@/assets/img/headerImage.png";
|
||||
import {getBaseURL} from "/@/utils/baseUrl";
|
||||
const userStore = useUserInfo();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const open = ref(false);
|
||||
const visible = ref(false);
|
||||
const title = ref('修改头像');
|
||||
const emit = defineEmits(['uploadImg']);
|
||||
@@ -75,7 +76,7 @@ const dialogVisiable = computed({
|
||||
|
||||
//图片裁剪数据
|
||||
const options = reactive({
|
||||
img: userStore.userInfos.avatar, // 裁剪图片的地址
|
||||
img: userStore.userInfos.avatar || headerImage, // 裁剪图片的地址
|
||||
fileName: '',
|
||||
autoCrop: true, // 是否默认生成截图框
|
||||
autoCropWidth: 200, // 默认生成截图框宽度
|
||||
@@ -165,6 +166,7 @@ const updateAvatar = (img) => {
|
||||
|
||||
defineExpose({
|
||||
updateAvatar,
|
||||
editCropper
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -172,7 +174,6 @@ defineExpose({
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-info-head:hover:after {
|
||||
|
||||
@@ -8,7 +8,29 @@
|
||||
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
|
||||
:label="item.name" />
|
||||
</el-select>
|
||||
<div v-if="props.inputType === 'image'" style="position: relative;" class="form-display"
|
||||
|
||||
<div v-if="props.inputType === 'image' && props.multiple"
|
||||
style="width: 100%; display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px;">
|
||||
<div v-for="item, index in (data || [])" style="position: relative;"
|
||||
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
|
||||
<el-image :src="item" :key="index" fit="scale-down" class="itemList"
|
||||
:style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }" />
|
||||
<el-icon v-show="(!!data && !props.disabled)" class="closeHover" :size="16" @click="clearOne(item)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div style="position: relative;" :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
|
||||
<div
|
||||
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||
<el-icon :size="24">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.inputType === 'image' && !props.multiple" class="form-display" style="position: relative;"
|
||||
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
|
||||
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
|
||||
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
|
||||
@@ -24,10 +46,11 @@
|
||||
</div>
|
||||
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||
<el-icon v-show="(!!data && !props.disabled) && !props.multiple" class="closeHover" :size="16" @click="clear">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
|
||||
@mouseleave="formDisplayLeave"
|
||||
style="position: relative; display: flex; align-items: center; justify-items: center;"
|
||||
@@ -46,6 +69,7 @@
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
|
||||
@mouseleave="formDisplayLeave"
|
||||
style="position: relative; display: flex; align-items: center; justify-items: center;"
|
||||
@@ -199,7 +223,7 @@ const props = defineProps({
|
||||
tabsShow: { type: Number, default: SHOW.ALL },
|
||||
|
||||
// 是否可以多选,默认单选
|
||||
// 该值为true时inputType必须是selector(暂不支持其他type的多选)
|
||||
// 该值为true时inputType必须是selector或image(暂不支持其他type的多选)
|
||||
multiple: { type: Boolean, default: false },
|
||||
|
||||
// 是否可选,该参数用于只上传和展示而不选择和绑定model的情况
|
||||
@@ -274,6 +298,7 @@ const onItemClick = async (e: MouseEvent) => {
|
||||
while (!target.dataset.id) target = target.parentElement as HTMLElement;
|
||||
let fileId = target.dataset.id;
|
||||
if (props.multiple) {
|
||||
if (!!!data.value) data.value = [];
|
||||
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
|
||||
else { target.classList.add('active'); flat = 1; }
|
||||
if (data.value.length) {
|
||||
@@ -327,8 +352,12 @@ const clearState = () => {
|
||||
// all数据不能清,因为all只会在挂载的时候赋值一次
|
||||
// listAllData.value = [];
|
||||
};
|
||||
const clear = () => { data.value = null; onDataChange(null); }
|
||||
|
||||
const clear = () => { data.value = null; onDataChange(null); };
|
||||
const clearOne = (item: any) => {
|
||||
let _l = (JSON.parse(JSON.stringify(data.value)) as any[]).filter((i: any) => i !== item)
|
||||
data.value = _l;
|
||||
onDataChange(_l);
|
||||
};
|
||||
|
||||
// 网络文件部分
|
||||
const netLoading = ref<boolean>(false);
|
||||
@@ -386,7 +415,15 @@ watch(
|
||||
const { ui } = useUi();
|
||||
const formValidator = ui.formItem.injectFormItemContext();
|
||||
const onDataChange = (value: any) => {
|
||||
emit('update:modelValue', value);
|
||||
let _v = null;
|
||||
if (value) {
|
||||
if (typeof value === 'string') _v = value.replace(/\\/g, '/');
|
||||
else {
|
||||
_v = [];
|
||||
for (let i of value) _v.push(i.replace(/\\/g, '/'));
|
||||
}
|
||||
}
|
||||
emit('update:modelValue', _v);
|
||||
formValidator.onChange();
|
||||
formValidator.onBlur();
|
||||
};
|
||||
@@ -394,7 +431,8 @@ const onDataChange = (value: any) => {
|
||||
defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
|
||||
|
||||
onMounted(() => {
|
||||
if (props.multiple && props.inputType !== 'selector')
|
||||
|
||||
if (props.multiple && !['selector', 'image'].includes(props.inputType))
|
||||
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
|
||||
listRequestAll();
|
||||
console.log('fileselector tenentmdoe', isTenentMode);
|
||||
@@ -475,4 +513,9 @@ onMounted(() => {
|
||||
top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.itemList {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
popper-class="popperClass"
|
||||
class="tableSelector"
|
||||
multiple
|
||||
:collapseTags="props.tableConfig.collapseTags"
|
||||
@remove-tag="removeTag"
|
||||
v-model="data"
|
||||
placeholder="请选择"
|
||||
@@ -18,20 +19,22 @@
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="tableData"
|
||||
size="mini"
|
||||
:size="props.tableConfig.size"
|
||||
border
|
||||
row-key="id"
|
||||
:lazy="props.tableConfig.lazy"
|
||||
:load="props.tableConfig.load"
|
||||
:tree-props="props.tableConfig.treeProps"
|
||||
style="width: 400px"
|
||||
style="width: 600px"
|
||||
max-height="200"
|
||||
height="200"
|
||||
:highlight-current-row="!props.tableConfig.isMultiple"
|
||||
@selection-change="handleSelectionChange"
|
||||
@select="handleSelectionChange"
|
||||
@selectAll="handleSelectionChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55" />
|
||||
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
|
||||
<el-table-column fixed type="index" label="#" width="50" />
|
||||
<el-table-column
|
||||
:prop="item.prop"
|
||||
@@ -56,23 +59,31 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, reactive, ref, watch } from 'vue';
|
||||
import {computed, defineProps, onMounted, reactive, ref, watch} from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { request } from '/@/utils/service';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {},
|
||||
modelValue: {
|
||||
type: Array || String || Number,
|
||||
default: () => []
|
||||
},
|
||||
tableConfig: {
|
||||
type: Object,
|
||||
default:{
|
||||
url: null,
|
||||
label: null, //显示值
|
||||
value: null, //数据值
|
||||
isTree: false,
|
||||
lazy: true,
|
||||
size:'default',
|
||||
load: () => {},
|
||||
data: [], //默认数据
|
||||
isMultiple: false, //是否多选
|
||||
collapseTags:false,
|
||||
treeProps: { children: 'children', hasChildren: 'hasChildren' },
|
||||
columns: [], //每一项对应的列表项
|
||||
},
|
||||
},
|
||||
displayLabel: {},
|
||||
} as any);
|
||||
@@ -86,7 +97,7 @@ const multipleSelection = ref();
|
||||
// 搜索值
|
||||
const search = ref(undefined);
|
||||
//表格数据
|
||||
const tableData = ref();
|
||||
const tableData = ref([]);
|
||||
// 分页的配置
|
||||
const pageConfig = reactive({
|
||||
page: 1,
|
||||
@@ -99,7 +110,6 @@ const pageConfig = reactive({
|
||||
* @param val:Array
|
||||
*/
|
||||
const handleSelectionChange = (val: any) => {
|
||||
multipleSelection.value = val;
|
||||
const { tableConfig } = props;
|
||||
const result = val.map((item: any) => {
|
||||
return item[tableConfig.value];
|
||||
@@ -117,7 +127,7 @@ const handleSelectionChange = (val: any) => {
|
||||
const handleCurrentChange = (val: any) => {
|
||||
const { tableConfig } = props;
|
||||
if (!tableConfig.isMultiple && val) {
|
||||
data.value = [val[tableConfig.label]];
|
||||
// data.value = [val[tableConfig.label]];
|
||||
emit('update:modelValue', val[tableConfig.value]);
|
||||
}
|
||||
};
|
||||
@@ -150,6 +160,32 @@ const getDict = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取节点值
|
||||
const getNodeValues = () => {
|
||||
request({
|
||||
url:props.tableConfig.valueUrl,
|
||||
method:'post',
|
||||
data:{ids:props.modelValue}
|
||||
}).then(res=>{
|
||||
if(res.data.length>0){
|
||||
data.value = res.data.map((item:any)=>{
|
||||
return item[props.tableConfig.label]
|
||||
})
|
||||
|
||||
tableRef.value!.clearSelection()
|
||||
res.data.forEach((row) => {
|
||||
tableRef.value!.toggleRowSelection(
|
||||
row,
|
||||
true,
|
||||
false
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下拉框展开/关闭
|
||||
* @param bool
|
||||
@@ -169,20 +205,12 @@ const handlePageChange = (page: any) => {
|
||||
getDict();
|
||||
};
|
||||
|
||||
// 监听displayLabel的变化,更新数据
|
||||
watch(
|
||||
() => {
|
||||
return props.displayLabel;
|
||||
},
|
||||
(value) => {
|
||||
const { tableConfig } = props;
|
||||
const result = value
|
||||
? value.map((item: any) => { return item[tableConfig.label];})
|
||||
: null;
|
||||
data.value = result;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(()=>{
|
||||
setTimeout(()=>{
|
||||
getNodeValues()
|
||||
},1000)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -17,13 +17,15 @@ import { useRoutesList } from '/@/stores/routesList';
|
||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
import mittBus from '/@/utils/mitt';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
// 引入组件
|
||||
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
|
||||
const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
|
||||
|
||||
// 定义变量内容
|
||||
const layoutAsideScrollbarRef = ref();
|
||||
const routesIndex = ref(0);
|
||||
const stores = useRoutesList();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const storesTagsViewRoutes = useTagsViewRoutes();
|
||||
@@ -83,10 +85,36 @@ const closeLayoutAsideMobileMode = () => {
|
||||
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
|
||||
document.body.setAttribute('class', '');
|
||||
};
|
||||
const findFirstLevelIndex = (data, path) => {
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const item = data[index];
|
||||
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 检查子菜单中是否有匹配的路径
|
||||
const childIndex = item.children.findIndex((child) => child.path === path);
|
||||
if (childIndex !== -1) {
|
||||
return index; // 返回当前一级菜单的索引
|
||||
}
|
||||
// 递归查找子菜单
|
||||
const foundIndex = findFirstLevelIndex(item.children, path);
|
||||
if (foundIndex !== null) {
|
||||
return index; // 返回找到的索引
|
||||
}
|
||||
}
|
||||
}
|
||||
return null; // 找不到路径时返回 null
|
||||
};
|
||||
// 设置/过滤路由(非静态路由/是否显示在菜单中)
|
||||
const setFilterRoutes = () => {
|
||||
const setFilterRoutes = (path='') => {
|
||||
if (themeConfig.value.layout === 'columns') return false;
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
// 获取当前地址的索引,不用从参数选取
|
||||
routesIndex.value = findFirstLevelIndex(routesList.value,path || route.path) || 0
|
||||
state.menuList = filterRoutesFun(routesList.value[routesIndex.value].children || [routesList.value[routesIndex.value]]);
|
||||
} else {
|
||||
state.menuList = filterRoutesFun(routesList.value);
|
||||
}
|
||||
};
|
||||
// 路由过滤递归函数
|
||||
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
|
||||
@@ -122,7 +150,8 @@ onBeforeMount(() => {
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
state.menuList = [];
|
||||
state.menuList = res.children;
|
||||
// state.menuList = res.children;
|
||||
setFilterRoutes(res.path);
|
||||
}
|
||||
});
|
||||
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
|
||||
|
||||
@@ -102,6 +102,5 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--next-bg-topBar);
|
||||
border-bottom: 1px solid var(--next-border-color-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
|
||||
</div>
|
||||
<div class="layout-navbars-breadcrumb-user-icon">
|
||||
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
||||
<el-popover placement="bottom" trigger="hover" transition="el-zoom-in-top" :width="300" :persistent="false">
|
||||
<template #reference>
|
||||
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
|
||||
<el-icon :title="$t('message.user.title4')">
|
||||
@@ -58,7 +58,7 @@
|
||||
></i>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="!isSocketOpen">
|
||||
<span v-if="!isSocketOpen" class="online-status-span">
|
||||
<el-popconfirm
|
||||
width="250"
|
||||
ref="onlinePopoverRef"
|
||||
@@ -71,7 +71,7 @@
|
||||
>
|
||||
<template #reference>
|
||||
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
|
||||
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
<img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
@@ -93,7 +93,7 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="/versionUpgradeLog">更新日志</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -250,6 +250,7 @@ onMounted(() => {
|
||||
|
||||
//消息中心的未读数量
|
||||
import { messageCenterStore } from '/@/stores/messageCenter';
|
||||
import {getBaseURL} from "/@/utils/baseUrl";
|
||||
const messageCenter = messageCenterStore();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="el-menu-horizontal-warp">
|
||||
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
|
||||
<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal">
|
||||
<template v-for="val in menuLists">
|
||||
<!-- <el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">-->
|
||||
<el-menu :default-active="defaultActive" background-color="transparent" mode="horizontal">
|
||||
<template v-for="(val,index) in menuLists">
|
||||
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
|
||||
<template #title>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
@@ -11,7 +11,7 @@
|
||||
<SubItem :chil="val.children" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="val.path" :key="val.path">
|
||||
<el-menu-item :index="val.path" :key="val.path" style="--el-menu-active-color: #fff" @click="onToRouteClick(val,index)">
|
||||
<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ $t(val.meta.title) }}
|
||||
@@ -26,22 +26,25 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<!-- </el-scrollbar>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="navMenuHorizontal">
|
||||
import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue';
|
||||
import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
|
||||
import {useRoute, onBeforeRouteUpdate, RouteRecordRaw, useRouter} from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRoutesList } from '/@/stores/routesList';
|
||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
import other from '/@/utils/other';
|
||||
import mittBus from '/@/utils/mitt';
|
||||
|
||||
const router = useRouter()
|
||||
// 引入组件
|
||||
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
|
||||
|
||||
const state = reactive<AsideState>({
|
||||
menuList: [],
|
||||
clientWidth: 0
|
||||
});
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
// 菜单列表
|
||||
@@ -58,19 +61,33 @@ const storesThemeConfig = useThemeConfig();
|
||||
const { routesList } = storeToRefs(stores);
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const route = useRoute();
|
||||
const state = reactive({
|
||||
defaultActive: '' as string | undefined,
|
||||
});
|
||||
|
||||
const defaultActive = ref('')
|
||||
// 获取父级菜单数据
|
||||
const menuLists = computed(() => {
|
||||
<RouteItems>props.menuList.shift()
|
||||
return <RouteItems>props.menuList;
|
||||
});
|
||||
// 设置横向滚动条可以鼠标滚轮滚动
|
||||
const onElMenuHorizontalScroll = (e: WheelEventType) => {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40;
|
||||
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft + eventDelta / 4;
|
||||
// 递归获取当前路由的顶级索引
|
||||
const findFirstLevelIndex = (data, path) => {
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const item = data[index];
|
||||
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 检查子菜单中是否有匹配的路径
|
||||
const childIndex = item.children.findIndex((child) => child.path === path);
|
||||
if (childIndex !== -1) {
|
||||
return index; // 返回当前一级菜单的索引
|
||||
}
|
||||
// 递归查找子菜单
|
||||
const foundIndex = findFirstLevelIndex(item.children, path);
|
||||
if (foundIndex !== null) {
|
||||
return index; // 返回找到的索引
|
||||
}
|
||||
}
|
||||
}
|
||||
return null; // 找不到路径时返回 null
|
||||
};
|
||||
|
||||
// 初始化数据,页面刷新时,滚动条滚动到对应位置
|
||||
const initElMenuOffsetLeft = () => {
|
||||
nextTick(() => {
|
||||
@@ -107,17 +124,41 @@ const setSendClassicChildren = (path: string) => {
|
||||
const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
|
||||
const { path, meta } = currentRoute;
|
||||
if (themeConfig.value.layout === 'classic') {
|
||||
state.defaultActive = `/${path?.split('/')[1]}`;
|
||||
let firstLevelIndex = (findFirstLevelIndex(routesList.value, route.path) || 0) - 1
|
||||
defaultActive.value = firstLevelIndex < 0 ? defaultActive.value : menuLists.value[firstLevelIndex].path
|
||||
} else {
|
||||
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
|
||||
if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
|
||||
else state.defaultActive = path;
|
||||
if (pathSplit.length >= 4 && meta?.isHide) defaultActive.value = pathSplit.splice(0, 3).join('/');
|
||||
else defaultActive.value = path;
|
||||
}
|
||||
};
|
||||
// 打开外部链接
|
||||
const onALinkClick = (val: RouteItem) => {
|
||||
other.handleOpenLink(val);
|
||||
};
|
||||
// 跳转页面
|
||||
const onToRouteClick = (val: RouteItem,index) => {
|
||||
// 跳转到子级页面
|
||||
let children = val.children
|
||||
if (children === undefined){
|
||||
defaultActive.value = val.path
|
||||
children = setSendClassicChildren(val.path).children
|
||||
}
|
||||
if (children.length >= 1){
|
||||
if (children[0].is_catalog) {
|
||||
onToRouteClick(children[0],index)
|
||||
return
|
||||
}
|
||||
router.push(children[0].path)
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
mittBus.emit('setSendClassicChildren', children[0]);
|
||||
}
|
||||
} else {
|
||||
router.push('/home')
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
setCurrentRouterHighlight(route);
|
||||
@@ -126,16 +167,6 @@ onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
initElMenuOffsetLeft();
|
||||
});
|
||||
// 路由更新时
|
||||
onBeforeRouteUpdate((to) => {
|
||||
// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
|
||||
setCurrentRouterHighlight(to);
|
||||
// 修复经典布局开启切割菜单时,点击tagsView后左侧导航菜单数据不变的问题
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -8,7 +8,21 @@
|
||||
<sub-item :chil="val.children" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="val.path" :key="val.path">
|
||||
<a v-if="val.name==='templateCenter'" href="#/templateCenter" target="_blank">
|
||||
<el-menu-item :key="val.path">
|
||||
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<span>{{ $t(val.meta.title) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="w100" @click.prevent="onALinkClick(val)">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ $t(val.meta.title) }}
|
||||
</a>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</a>
|
||||
<el-menu-item v-else :index="val.path" :key="val.path">
|
||||
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<span>{{ $t(val.meta.title) }}</span>
|
||||
|
||||
@@ -21,13 +21,14 @@ const menuApi = useMenuApi();
|
||||
|
||||
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
|
||||
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
|
||||
const greatDream: any = import.meta.glob('@great-dream/**/*.{vue,tsx}');
|
||||
|
||||
/**
|
||||
* 获取目录下的 .vue、.tsx 全部文件
|
||||
* @method import.meta.glob
|
||||
* @link 参考:https://cn.vitejs.dev/guide/features.html#json
|
||||
*/
|
||||
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules });
|
||||
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }, { ...greatDream });
|
||||
|
||||
/**
|
||||
* 后端控制路由:初始化方法,防止刷新时路由丢失
|
||||
@@ -198,7 +199,10 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
|
||||
const keys = Object.keys(dynamicViewsModules);
|
||||
const matchKeys = keys.filter((key) => {
|
||||
const k = key.replace(/..\/views|../, '');
|
||||
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
|
||||
const k0 = k.replace("ode_modules/@great-dream/", '')
|
||||
const k1 = k0.replace("/plugins", '')
|
||||
const newComponent = component.replace("plugins/", "")
|
||||
return k1.startsWith(`${newComponent}`) || k1.startsWith(`/${newComponent}`);
|
||||
});
|
||||
if (matchKeys?.length === 1) {
|
||||
const matchKey = matchKeys[0];
|
||||
|
||||
@@ -53,9 +53,12 @@ export default {
|
||||
},
|
||||
form: {
|
||||
afterSubmit(ctx: any) {
|
||||
const {res} = ctx
|
||||
// 增加crud提示
|
||||
if (ctx.res.code == 2000) {
|
||||
if (res?.code == 2000) {
|
||||
successNotification(ctx.res.msg);
|
||||
}else{
|
||||
return
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,45 +1,65 @@
|
||||
import {dict} from "@fast-crud/fast-crud";
|
||||
import {shallowRef} from 'vue'
|
||||
import deptFormat from "/@/components/dept-format/index.vue";
|
||||
import {dict} from '@fast-crud/fast-crud';
|
||||
import {shallowRef} from 'vue';
|
||||
import deptFormat from '/@/components/dept-format/index.vue';
|
||||
|
||||
export const commonCrudConfig = (options = {
|
||||
create_datetime: {
|
||||
form: false,
|
||||
table: false,
|
||||
search: false
|
||||
},
|
||||
update_datetime: {
|
||||
form: false,
|
||||
table: false,
|
||||
search: false
|
||||
},
|
||||
creator_name: {
|
||||
form: false,
|
||||
table: false,
|
||||
search: false
|
||||
},
|
||||
modifier_name: {
|
||||
form: false,
|
||||
table: false,
|
||||
search: false
|
||||
},
|
||||
dept_belong_id: {
|
||||
form: false,
|
||||
table: false,
|
||||
search: false
|
||||
},
|
||||
description: {
|
||||
form: false,
|
||||
table: false,
|
||||
search: false
|
||||
},
|
||||
}) => {
|
||||
/** 1. 每个字段可选属性 */
|
||||
export interface CrudFieldOption {
|
||||
form?: boolean;
|
||||
table?: boolean;
|
||||
search?: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/** 2. 总配置接口 */
|
||||
export interface CrudOptions {
|
||||
create_datetime?: CrudFieldOption;
|
||||
update_datetime?: CrudFieldOption;
|
||||
creator_name?: CrudFieldOption;
|
||||
modifier_name?: CrudFieldOption;
|
||||
dept_belong_id?: CrudFieldOption;
|
||||
description?: CrudFieldOption;
|
||||
}
|
||||
|
||||
/** 3. 默认完整配置 */
|
||||
const defaultOptions: Required<CrudOptions> = {
|
||||
create_datetime: { form: false, table: false, search: false, width: 160 },
|
||||
update_datetime: { form: false, table: false, search: false, width: 160 },
|
||||
creator_name: { form: false, table: false, search: false, width: 100 },
|
||||
modifier_name: { form: false, table: false, search: false, width: 100 },
|
||||
dept_belong_id: { form: false, table: false, search: false, width: 300 },
|
||||
description: { form: false, table: false, search: false, width: 100 },
|
||||
};
|
||||
|
||||
/** 4. mergeOptions 函数 */
|
||||
function mergeOptions(baseOptions: Required<CrudOptions>, userOptions: CrudOptions = {}): Required<CrudOptions> {
|
||||
const result = { ...baseOptions };
|
||||
for (const key in userOptions) {
|
||||
if (Object.prototype.hasOwnProperty.call(userOptions, key)) {
|
||||
const baseField = result[key as keyof CrudOptions];
|
||||
const userField = userOptions[key as keyof CrudOptions];
|
||||
if (baseField && userField) {
|
||||
result[key as keyof CrudOptions] = { ...baseField, ...userField };
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最终暴露的 commonCrudConfig
|
||||
* @param options 用户自定义配置(可传可不传,不传就用默认)
|
||||
*/
|
||||
export const commonCrudConfig = (options: CrudOptions = {}) => {
|
||||
// ① 合并
|
||||
const merged = mergeOptions(defaultOptions, options);
|
||||
|
||||
// ② 用 merged 中的值生成真正的 CRUD 配置
|
||||
return {
|
||||
dept_belong_id: {
|
||||
title: '所属部门',
|
||||
type: 'dict-tree',
|
||||
search: {
|
||||
show: options.dept_belong_id?.search || false
|
||||
show: merged.dept_belong_id.search,
|
||||
},
|
||||
dict: dict({
|
||||
url: '/api/system/dept/all_dept/',
|
||||
@@ -50,90 +70,92 @@ export const commonCrudConfig = (options = {
|
||||
}),
|
||||
column: {
|
||||
align: 'center',
|
||||
width: 300,
|
||||
show: options.dept_belong_id?.table || false,
|
||||
width: merged.dept_belong_id.width,
|
||||
show: merged.dept_belong_id.table,
|
||||
component: {
|
||||
name: shallowRef(deptFormat),
|
||||
vModel: "modelValue",
|
||||
}
|
||||
// fast-crud里自定义组件常用"component.is"
|
||||
is: shallowRef(deptFormat),
|
||||
vModel: 'modelValue',
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: options.dept_belong_id?.form || false,
|
||||
show: merged.dept_belong_id.form,
|
||||
component: {
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
props: {
|
||||
checkStrictly: true,
|
||||
props: {
|
||||
// 为什么这里要写两层props
|
||||
// 因为props属性名与fs的动态渲染的props命名冲突,所以要多写一层
|
||||
label: "name",
|
||||
value: "id",
|
||||
}
|
||||
}
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
helper: '默认不填则为当前创建用户的部门ID',
|
||||
},
|
||||
helper: "默认不填则为当前创建用户的部门ID"
|
||||
}
|
||||
},
|
||||
description: {
|
||||
title: '备注',
|
||||
search: {
|
||||
show: options.description?.search || false
|
||||
show: merged.description.search,
|
||||
},
|
||||
type: 'textarea',
|
||||
column: {
|
||||
width: 100,
|
||||
show: options.description?.table || false,
|
||||
width: merged.description.width,
|
||||
show: merged.description.table,
|
||||
},
|
||||
form: {
|
||||
show: options.description?.form || false,
|
||||
show: merged.description.form,
|
||||
component: {
|
||||
placeholder: '请输入内容',
|
||||
showWordLimit: true,
|
||||
maxlength: '200',
|
||||
}
|
||||
},
|
||||
},
|
||||
viewForm: {
|
||||
show: true
|
||||
}
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
|
||||
modifier_name: {
|
||||
title: '修改人',
|
||||
search: {
|
||||
show: options.modifier_name?.search || false
|
||||
show: merged.modifier_name.search,
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
show: options.modifier_name?.table || false,
|
||||
width: merged.modifier_name.width,
|
||||
show: merged.modifier_name.table,
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
show: merged.modifier_name.form,
|
||||
},
|
||||
viewForm: {
|
||||
show: true
|
||||
}
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
|
||||
creator_name: {
|
||||
title: '创建人',
|
||||
search: {
|
||||
show: options.creator_name?.search || false
|
||||
show: merged.creator_name.search,
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
show: options.creator_name?.table || false,
|
||||
width: merged.creator_name.width,
|
||||
show: merged.creator_name.table,
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
show: merged.creator_name.form,
|
||||
},
|
||||
viewForm: {
|
||||
show: true
|
||||
}
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
|
||||
update_datetime: {
|
||||
title: '更新时间',
|
||||
type: 'datetime',
|
||||
search: {
|
||||
show: options.update_datetime?.search || false,
|
||||
show: merged.update_datetime.search,
|
||||
col: { span: 8 },
|
||||
component: {
|
||||
type: 'datetimerange',
|
||||
@@ -142,61 +164,64 @@ export const commonCrudConfig = (options = {
|
||||
'end-placeholder': '结束时间',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
'picker-options': {
|
||||
shortcuts: [{
|
||||
shortcuts: [
|
||||
{
|
||||
text: '最近一周',
|
||||
onClick(picker) {
|
||||
onClick(picker: any) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近一个月',
|
||||
onClick(picker) {
|
||||
onClick(picker: any) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近三个月',
|
||||
onClick(picker) {
|
||||
onClick(picker: any) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
valueResolve(context: any) {
|
||||
const {key, value} = context
|
||||
//value解析,就是把组件的值转化为后台所需要的值
|
||||
//在form表单点击保存按钮后,提交到后台之前执行转化
|
||||
const { value } = context;
|
||||
if (value) {
|
||||
context.form.update_datetime_after = value[0]
|
||||
context.form.update_datetime_before = value[1]
|
||||
}
|
||||
// ↑↑↑↑↑ 注意这里是form,不是row
|
||||
context.form.update_datetime_after = value[0];
|
||||
context.form.update_datetime_before = value[1];
|
||||
delete context.form.update_datetime;
|
||||
}
|
||||
},
|
||||
},
|
||||
column: {
|
||||
width: 160,
|
||||
show: options.update_datetime?.table || false,
|
||||
width: merged.update_datetime.width,
|
||||
show: merged.update_datetime.table,
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
show: merged.update_datetime.form,
|
||||
},
|
||||
viewForm: {
|
||||
show: true
|
||||
}
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
|
||||
create_datetime: {
|
||||
title: '创建时间',
|
||||
type: 'datetime',
|
||||
search: {
|
||||
show: options.create_datetime?.search || false,
|
||||
show: merged.create_datetime.search,
|
||||
col: { span: 8 },
|
||||
component: {
|
||||
type: 'datetimerange',
|
||||
@@ -205,55 +230,57 @@ export const commonCrudConfig = (options = {
|
||||
'end-placeholder': '结束时间',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
'picker-options': {
|
||||
shortcuts: [{
|
||||
shortcuts: [
|
||||
{
|
||||
text: '最近一周',
|
||||
onClick(picker) {
|
||||
onClick(picker: any) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近一个月',
|
||||
onClick(picker) {
|
||||
onClick(picker: any) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近三个月',
|
||||
onClick(picker) {
|
||||
onClick(picker: any) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
valueResolve(context: any) {
|
||||
const {key, value} = context
|
||||
//value解析,就是把组件的值转化为后台所需要的值
|
||||
//在form表单点击保存按钮后,提交到后台之前执行转化
|
||||
const { value } = context;
|
||||
if (value) {
|
||||
context.form.create_datetime_after = value[0]
|
||||
context.form.create_datetime_before = value[1]
|
||||
}
|
||||
// ↑↑↑↑↑ 注意这里是form,不是row
|
||||
context.form.create_datetime_after = value[0];
|
||||
context.form.create_datetime_before = value[1];
|
||||
delete context.form.create_datetime;
|
||||
}
|
||||
},
|
||||
},
|
||||
column: {
|
||||
width: 160,
|
||||
show: options.create_datetime?.table || false,
|
||||
width: merged.create_datetime.width,
|
||||
show: merged.create_datetime.table,
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
show: merged.create_datetime.form,
|
||||
},
|
||||
viewForm: {
|
||||
show: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
57
web/src/utils/cores.tsx
Normal file
57
web/src/utils/cores.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import mitt, { Emitter } from 'mitt';
|
||||
|
||||
export interface TaskProps {
|
||||
name: string;
|
||||
custom?: any;
|
||||
}
|
||||
|
||||
// 定义自定义事件类型
|
||||
export type BusEvents = {
|
||||
onNewTask: TaskProps | undefined;
|
||||
};
|
||||
|
||||
export interface Task {
|
||||
id: number;
|
||||
handle: string;
|
||||
data: any;
|
||||
createTime: Date;
|
||||
custom?: any;
|
||||
}
|
||||
|
||||
export interface Core {
|
||||
bus: Emitter<BusEvents>;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
showNotification(body: string, title?: string): Notification | undefined;
|
||||
taskList: Map<String, Task>;
|
||||
}
|
||||
|
||||
const bus = mitt<BusEvents>();
|
||||
export function getSystemNotification(body: string, title?: string) {
|
||||
if (!title) {
|
||||
title = '通知';
|
||||
}
|
||||
return new Notification(title ?? '通知', {
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
export function showSystemNotification(body: string, title?: string): Notification | undefined {
|
||||
if (Notification.permission === 'granted') {
|
||||
return getSystemNotification(body, title);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission().then((permission) => {
|
||||
if (permission === 'granted') {
|
||||
return getSystemNotification(body, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
const taskList = new Map<String, Task>();
|
||||
|
||||
export function useCore(): Core {
|
||||
return {
|
||||
bus,
|
||||
showNotification: showSystemNotification,
|
||||
taskList,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
import axios from "axios";
|
||||
import * as process from "process";
|
||||
import axios from 'axios';
|
||||
import * as process from 'process';
|
||||
import { Local, Session } from '/@/utils/storage';
|
||||
import {ElNotification} from "element-plus";
|
||||
import fs from "fs";
|
||||
import { ElNotification } from 'element-plus';
|
||||
import fs from 'fs';
|
||||
|
||||
// 是否显示升级提示信息框
|
||||
const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade';
|
||||
const VERSION_KEY = 'DVADMIN3_VERSION'
|
||||
const VERSION_FILE_NAME = 'version-build'
|
||||
const VERSION_KEY = 'DVADMIN3_VERSION';
|
||||
const VERSION_FILE_NAME = 'version-build';
|
||||
|
||||
const META_ENV = import.meta.env;
|
||||
|
||||
export function showUpgrade() {
|
||||
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false
|
||||
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false;
|
||||
if (isShowUpgrade) {
|
||||
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY)
|
||||
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY);
|
||||
ElNotification({
|
||||
title: '新版本升级',
|
||||
message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!",
|
||||
message: '检测到系统新版本,正在更新中!不用担心,更新很快的哦!',
|
||||
type: 'success',
|
||||
duration: 5000,
|
||||
});
|
||||
@@ -24,32 +26,33 @@ export function showUpgrade () {
|
||||
|
||||
// 生产环境前端版本校验,
|
||||
export async function checkVersion() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (META_ENV.MODE === 'development') {
|
||||
// 开发环境无需校验前端版本
|
||||
return
|
||||
return;
|
||||
}
|
||||
// 获取线上版本号 t为时间戳,防止缓存
|
||||
await axios.get(`${import.meta.env.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => {
|
||||
const {status, data} = res || {}
|
||||
await axios.get(`${META_ENV.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then((res) => {
|
||||
const { status, data } = res || {};
|
||||
if (status === 200) {
|
||||
// 获取当前版本号
|
||||
const localVersion = Local.get(VERSION_KEY)
|
||||
const localVersion = Local.get(VERSION_KEY);
|
||||
// 将当前版本号持久缓存至本地
|
||||
Local.set(VERSION_KEY, data)
|
||||
Local.set(VERSION_KEY, data);
|
||||
// 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作
|
||||
if (localVersion && localVersion !== data) {
|
||||
// 本地缓存版本号和线上版本号不一致,弹出升级提示框
|
||||
// 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框
|
||||
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true)
|
||||
window.location.reload()
|
||||
|
||||
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function generateVersionFile() {
|
||||
// 生成版本文件到public目录下version文件中
|
||||
const version = `${process.env.npm_package_version}.${new Date().getTime()}`;
|
||||
const package_version = META_ENV?.npm_package_version ?? process.env?.npm_package_version;
|
||||
|
||||
const version = `${package_version}.${new Date().getTime()}`;
|
||||
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
|
||||
export let pluginsAll: any = [];
|
||||
// 扫描插件目录并注册插件
|
||||
export const scanAndInstallPlugins = (app: any) => {
|
||||
const components = import.meta.glob('./**/*.vue');
|
||||
const components = import.meta.glob('./**/*.ts');
|
||||
const pluginNames = new Set();
|
||||
// 遍历对象并注册异步组件
|
||||
for (const [key, value] of Object.entries(components)) {
|
||||
@@ -11,6 +11,15 @@ export const scanAndInstallPlugins = (app: any) => {
|
||||
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
|
||||
pluginNames.add(pluginsName);
|
||||
}
|
||||
const dreamComponents = import.meta.glob('/node_modules/@great-dream/**/*.ts');
|
||||
// 遍历对象并注册异步组件
|
||||
for (let [key, value] of Object.entries(dreamComponents)) {
|
||||
key = key.replace('node_modules/@great-dream/', '');
|
||||
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'));
|
||||
app.component(name, defineAsyncComponent(value as AsyncComponentLoader));
|
||||
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
|
||||
pluginNames.add(pluginsName);
|
||||
}
|
||||
pluginsAll = Array.from(pluginNames);
|
||||
console.log('已发现插件:', pluginsAll);
|
||||
};
|
||||
|
||||
122
web/src/views/system/config/components/components/crudTable.vue
Normal file
122
web/src/views/system/config/components/components/crudTable.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
</fs-crud>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, defineComponent, onMounted, watch} from "vue";
|
||||
import {CreateCrudOptionsProps, CreateCrudOptionsRet, useFs, AddReq,
|
||||
compute,
|
||||
DelReq,
|
||||
dict,
|
||||
EditReq,
|
||||
UserPageQuery,
|
||||
UserPageRes} from "@fast-crud/fast-crud";
|
||||
const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
return {
|
||||
crudOptions: {
|
||||
mode: {
|
||||
name: "local",
|
||||
isMergeWhenUpdate: true,
|
||||
isAppendWhenAdd: true
|
||||
},
|
||||
actionbar: { buttons: { add: { show: true }, addRow: { show: false } } },
|
||||
editable: {
|
||||
enabled: true,
|
||||
mode: "row",
|
||||
activeDefault:true
|
||||
},
|
||||
form:{
|
||||
wrapper:{
|
||||
width:"500px"
|
||||
},
|
||||
col:{
|
||||
span:24
|
||||
},
|
||||
afterSubmit({mode}){
|
||||
emit('update:modelValue', crudBinding.value.data);
|
||||
}
|
||||
},
|
||||
toolbar:{
|
||||
show:false
|
||||
},
|
||||
search: {
|
||||
disabled: true,
|
||||
show: false
|
||||
},
|
||||
pagination: {
|
||||
show: false
|
||||
},
|
||||
columns: {
|
||||
title: {
|
||||
title: "标题",
|
||||
form:{
|
||||
component:{
|
||||
placeholder:"请输入标题"
|
||||
},
|
||||
rules:[{
|
||||
required: true,
|
||||
message: '必须填写',
|
||||
}]
|
||||
}
|
||||
},
|
||||
key: {
|
||||
title: "键名",
|
||||
form:{
|
||||
component:{
|
||||
placeholder:"请输入键名"
|
||||
},
|
||||
rules:[{
|
||||
required: true,
|
||||
message: '必须填写',
|
||||
}]
|
||||
}
|
||||
},
|
||||
value: {
|
||||
title: "键值",
|
||||
form:{
|
||||
component:{
|
||||
placeholder:"请输入键值"
|
||||
},
|
||||
rules:[{
|
||||
required: true,
|
||||
message: '必须填写',
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
|
||||
//通过导出modelValue, 可以导出成为一个input组件
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue;
|
||||
},
|
||||
(value = []) => {
|
||||
crudBinding.value.data = value;
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
// onMounted(() => {
|
||||
// crudExpose.doRefresh();
|
||||
// // crudExpose.setTableData([])
|
||||
// // crudExpose.editable.enable();
|
||||
// });
|
||||
</script>
|
||||
@@ -175,48 +175,7 @@
|
||||
</div>
|
||||
<!-- 数组 -->
|
||||
<div v-else-if="item.form_item_type_label === 'array'" :key="index + 10">
|
||||
<vxe-table
|
||||
border
|
||||
resizable
|
||||
auto-resize
|
||||
show-overflow
|
||||
keep-source
|
||||
:ref="'xTable_' + item.key"
|
||||
height="200"
|
||||
:edit-rules="validRules"
|
||||
:edit-config="{ trigger: 'click', mode: 'row', showStatus: true }"
|
||||
>
|
||||
<vxe-column field="title" title="标题" :edit-render="{ autofocus: '.vxe-input--inner' }">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.title" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="key" title="键名" :edit-render="{ autofocus: '.vxe-input--inner' }">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.key" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="value" title="键值" :edit-render="{}">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.value" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="100" show-overflow>
|
||||
<template #default="{ row, index }">
|
||||
<el-popover placement="top" width="160" v-model="childRemoveVisible">
|
||||
<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, item.key)">确定</el-button>
|
||||
</div>
|
||||
<el-button type="text" slot="reference">删除</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<div>
|
||||
<el-button size="mini" @click="onAppend('xTable_' + item.key)">追加</el-button>
|
||||
</div>
|
||||
<crudTable v-model="formData[item.key]"></crudTable>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="2" :offset="1">
|
||||
@@ -248,32 +207,11 @@ import type { FormInstance, FormRules, TableInstance } from 'element-plus';
|
||||
import { successMessage, errorMessage } from '/@/utils/message';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import {Edit,Finished,Delete} from "@element-plus/icons-vue";
|
||||
import crudTable from "./components/crudTable.vue"
|
||||
const props = defineProps(['options', 'editableTabsItem']);
|
||||
|
||||
let formData: any = reactive({});
|
||||
let formData: any = ref({});
|
||||
let formList: any = ref([]);
|
||||
let childTableData = ref([]);
|
||||
let childRemoveVisible = ref(false);
|
||||
const validRules = reactive<FormRules>({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
message: '必须填写',
|
||||
},
|
||||
],
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: '必须填写',
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: '必须填写',
|
||||
},
|
||||
],
|
||||
});
|
||||
const formRef = ref<FormInstance>()
|
||||
let uploadUrl = ref(getBaseURL() + 'api/system/file/');
|
||||
let uploadHeaders = ref({
|
||||
@@ -294,65 +232,27 @@ const getInit = () => {
|
||||
if (item.value) {
|
||||
_formData[key] = item.value;
|
||||
} else {
|
||||
if ([5, 12, 14].indexOf(item.form_item_type) !== -1) {
|
||||
_formData[key] = [];
|
||||
if ([5, 12,11, 14].indexOf(item.form_item_type) !== -1) {
|
||||
_formData[key] = item.value || [];
|
||||
} else {
|
||||
_formData[key] = item.value;
|
||||
}
|
||||
}
|
||||
if (item.form_item_type_label === 'array') {
|
||||
console.log('test');
|
||||
nextTick(() => {
|
||||
const tableName = 'xTable_' + key;
|
||||
const tabelRef = ref<TableInstance>();
|
||||
console.log(tabelRef);
|
||||
// const $table = this.$refs[tableName][0];
|
||||
// $table.loadData(item.chinldern);
|
||||
});
|
||||
}
|
||||
}
|
||||
formData = Object.assign(formData, _formData)
|
||||
formData.value = Object.assign({}, _formData)
|
||||
});
|
||||
};
|
||||
|
||||
// 提交数据
|
||||
const onSubmit = (formEl: FormInstance | undefined) => {
|
||||
// const form = JSON.parse(JSON.stringify(form));
|
||||
const keys = Object.keys(formData);
|
||||
const values = Object.values(formData);
|
||||
const keys = Object.keys(formData.value);
|
||||
const values = Object.values(formData.value);
|
||||
for (const index in formList.value) {
|
||||
const item = formList.value[index];
|
||||
// eslint-disable-next-line camelcase
|
||||
const form_item_type_label = item.form_item_type_label;
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
if (form_item_type_label === 'array') {
|
||||
const parentId = item.id;
|
||||
const tableName = 'xTable_' + item.key;
|
||||
// const $table = this.$refs[tableName][0];
|
||||
// const { tableData } = $table.getTableData();
|
||||
// for (const child of tableData) {
|
||||
// if (!child.id && child.key && child.value) {
|
||||
// child.parent = parentId;
|
||||
// child.id = null;
|
||||
// formList.push(child);
|
||||
// }
|
||||
// }
|
||||
// // 必填项的判断
|
||||
// for (const arr of item.rule) {
|
||||
// if (arr.required && tableData.length === 0) {
|
||||
// errorMessage(item.title + '不能为空');
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// item.value = tableData;
|
||||
}
|
||||
// 赋值操作
|
||||
keys.map((mapKey, mapIndex) => {
|
||||
keys.forEach((mapKey, mapIndex) => {
|
||||
if (mapKey === item.key) {
|
||||
if (item.form_item_type_label !== 'array') {
|
||||
item.value = values[mapIndex];
|
||||
}
|
||||
// 必填项的验证
|
||||
if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) {
|
||||
for (const arr of item.rule) {
|
||||
@@ -380,39 +280,6 @@ const onSubmit = (formEl: FormInstance | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 追加
|
||||
const onAppend = (tableName: any) => {
|
||||
// const $table = this.$refs[tableName][0];
|
||||
// const { tableData } = $table.getTableData();
|
||||
// const tableLength = tableData.length;
|
||||
// if (tableLength === 0) {
|
||||
// const { row: newRow } = $table.insert();
|
||||
// console.log(newRow);
|
||||
// } else {
|
||||
// const errMap = $table.validate().catch((errMap: any) => errMap);
|
||||
// if (errMap) {
|
||||
// errorMessage('校验不通过!');
|
||||
// } else {
|
||||
// const { row: newRow } = $table.insert();
|
||||
// console.log(newRow);
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
// 子表删除
|
||||
const onRemoveChild = (row: any, index: any, refName: any) => {
|
||||
console.log(row, index);
|
||||
if (row.id) {
|
||||
api.DelObj(row.id).then((res: any) => {
|
||||
// this.refreshView();
|
||||
});
|
||||
} else {
|
||||
// this.childTableData.splice(index, 1);
|
||||
// const tableName = 'xTable_' + refName;
|
||||
// const tableData = this.$refs[tableName][0].remove(row);
|
||||
// console.log(tableData);
|
||||
}
|
||||
};
|
||||
|
||||
// 图片预览
|
||||
const handlePictureCardPreview = (file: any) => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from
|
||||
import * as api from './api';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
import { successMessage } from '../../../utils/message';
|
||||
import { auth } from '/@/utils/authFunction'
|
||||
import { auth } from '/@/utils/authFunction';
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
|
||||
interface CreateCrudOptionsTypes {
|
||||
output: any;
|
||||
@@ -27,7 +28,6 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
|
||||
//权限判定
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
return {
|
||||
crudOptions: {
|
||||
@@ -72,7 +72,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
show: compute(ctx => ctx.row.task_status === 2),
|
||||
text: '下载文件',
|
||||
type: 'warning',
|
||||
click: (ctx) => window.open(ctx.row.url, '_blank')
|
||||
click: (ctx) => window.open(getBaseURL(ctx.row.url), '_blank')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
dict
|
||||
} from '@fast-crud/fast-crud';
|
||||
import fileSelector from '/@/components/fileSelector/index.vue';
|
||||
import { shallowRef } from 'vue';
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
|
||||
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
@@ -147,6 +147,11 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
},
|
||||
column: {
|
||||
minWidth: 360,
|
||||
component: {
|
||||
async buildUrl(value: any) {
|
||||
return getBaseURL(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
md5sum: {
|
||||
@@ -232,17 +237,19 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
// fileselectortest: {
|
||||
// title: '文件选择器测试',
|
||||
// type: 'file-selector',
|
||||
// width: 200,
|
||||
// column: {
|
||||
// minWidth: 200
|
||||
// },
|
||||
// form: {
|
||||
// component: {
|
||||
// name: shallowRef(fileSelector),
|
||||
// name: fileSelector,
|
||||
// vModel: 'modelValue',
|
||||
// tabsShow: 0b0100,
|
||||
// tabsShow: 0b1111,
|
||||
// itemSize: 100,
|
||||
// multiple: false,
|
||||
// multiple: true,
|
||||
// selectable: true,
|
||||
// showInput: true,
|
||||
// inputType: 'video',
|
||||
// inputType: 'image',
|
||||
// valueKey: 'url',
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
<template>
|
||||
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
|
||||
<div class="menu-form-alert">配置操作功能接口权限,配置数据权限点击小齿轮</div>
|
||||
<div class="menu-form-alert">
|
||||
<div style="display:flex; align-items: center; white-space: nowrap; margin-bottom: 10px;">
|
||||
<span>默认接口权限:</span>
|
||||
<el-select
|
||||
v-model="default_selectBtn.data_range"
|
||||
@change="defaulthandlePermissionRangeChange"
|
||||
placeholder="请选择"
|
||||
style="margin-left: 5px; width: 250px; min-width: 250px;"
|
||||
>
|
||||
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-tree-select
|
||||
v-show="default_selectBtn.data_range === 4"
|
||||
node-key="id"
|
||||
v-model="default_selectBtn.dept"
|
||||
:props="defaultTreeProps"
|
||||
:data="deptData"
|
||||
@change="customhandlePermissionRangeChange(default_selectBtn.dept)"
|
||||
placeholder="请选择自定义部门"
|
||||
multiple
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
show-checkbox
|
||||
class="dialog-tree"
|
||||
style="margin-left: 15px; width: AUTO; min-width: 250px; margin-top: 0;"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<span>配置操作功能接口权限,配置数据权限点击小齿轮</span>
|
||||
</div>
|
||||
|
||||
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
|
||||
<div class="btn-item">
|
||||
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
|
||||
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range, btn.dept)})` : btn.name }}
|
||||
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
|
||||
<el-icon>
|
||||
<Setting />
|
||||
@@ -48,10 +78,26 @@ import { RoleMenuBtnType } from '../types';
|
||||
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Local } from '/@/utils/storage';
|
||||
|
||||
const RoleDrawer = RoleDrawerStores(); // 角色-菜单
|
||||
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
|
||||
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
// 默认按钮
|
||||
const default_selectBtn = ref<RoleMenuBtnType>({
|
||||
id: 0,
|
||||
menu_btn_pre_id: 0,
|
||||
/** 是否选中 */
|
||||
isCheck: false,
|
||||
/** 按钮名称 */
|
||||
name: '',
|
||||
/** 数据权限范围 */
|
||||
data_range: Local.get('role_default_data_range'),
|
||||
dept: Local.get('role_default_custom_dept'),
|
||||
});
|
||||
|
||||
// 选中的按钮
|
||||
const selectBtn = ref<RoleMenuBtnType>({
|
||||
id: 0,
|
||||
@@ -83,6 +129,29 @@ const defaultTreeProps = {
|
||||
value: 'id',
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 默认数据权限下拉选择事件
|
||||
* 保留数据到cache
|
||||
*/
|
||||
const defaulthandlePermissionRangeChange = async (val: number) => {
|
||||
if (val < 4) {
|
||||
// default_selectBtn.value.dept = [];
|
||||
// Local.set('role_default_custom_dept', []);
|
||||
}
|
||||
default_selectBtn.value.data_range = val;
|
||||
Local.set('role_default_data_range', val);
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认部门下拉选择事件
|
||||
* 保留数据到cache
|
||||
*/
|
||||
const customhandlePermissionRangeChange = async (dept: Array<number>) => {
|
||||
default_selectBtn.value.dept = dept;
|
||||
Local.set('role_default_custom_dept', dept);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定数据权限下拉选择事件
|
||||
*/
|
||||
@@ -95,12 +164,21 @@ const handlePermissionRangeChange = async (val: number) => {
|
||||
* 格式化按钮数据范围
|
||||
*/
|
||||
const formatDataRange = computed(() => {
|
||||
return function (datarange: number) {
|
||||
return function (datarange: number, dept: Array<number>) {
|
||||
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
|
||||
if (item.value === datarange) {
|
||||
return item.label;
|
||||
}
|
||||
});
|
||||
// 数据权限与默认数据权限一致
|
||||
if (datarange === default_selectBtn.value.data_range) {
|
||||
// 判断选择的部门是否一致
|
||||
if (datarange !== 4 || JSON.stringify(dept) === JSON.stringify(default_selectBtn.value.dept)) {
|
||||
|
||||
return "默认接口权限"
|
||||
}
|
||||
}
|
||||
// datarange === 4 选择的部门不一致返回datarangeitem.label
|
||||
return datarangeitem.label;
|
||||
};
|
||||
});
|
||||
@@ -108,11 +186,14 @@ const formatDataRange = computed(() => {
|
||||
* 勾选按钮
|
||||
*/
|
||||
const handleCheckChange = async (btn: RoleMenuBtnType) => {
|
||||
selectBtn.value = default_selectBtn.value;
|
||||
const put_data = {
|
||||
isCheck: btn.isCheck,
|
||||
roleId: RoleDrawer.roleId,
|
||||
menuId: RoleMenuTree.id,
|
||||
btnId: btn.id,
|
||||
data_range: default_selectBtn.value.data_range,
|
||||
dept: default_selectBtn.value.dept,
|
||||
};
|
||||
const { data, msg } = await setRoleMenuBtn(put_data);
|
||||
RoleMenuBtn.updateState(data);
|
||||
@@ -168,9 +249,10 @@ onMounted(async () => {
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
// .el-checkbox {
|
||||
// width: 200px;
|
||||
// }
|
||||
|
||||
.el-checkbox {
|
||||
width: 20%;
|
||||
}
|
||||
.btn-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
30
web/src/views/system/role/components/addUsers/api.ts
Normal file
30
web/src/views/system/role/components/addUsers/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { UserPageQuery} from '@fast-crud/fast-crud';
|
||||
|
||||
/**
|
||||
* 当前角色查询未授权的用户
|
||||
* @param role_id 角色id
|
||||
* @param query 查询条件 需要有角色id
|
||||
* @returns
|
||||
*/
|
||||
export function getRoleUsersUnauthorized(query: UserPageQuery) {
|
||||
query["authorized"] = 0; // 未授权的用户
|
||||
return request({
|
||||
url: '/api/system/role/get_role_users/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 当前用户角色添加用户
|
||||
* @param role_id 角色id
|
||||
* @param users_id 用户id数组
|
||||
* @returns
|
||||
*/
|
||||
export function addRoleUsers(role_id: number, users_id: Array<Number>) {
|
||||
return request({
|
||||
url: `/api/system/role/${role_id}/add_role_users/`,
|
||||
method: 'post',
|
||||
data: {users_id: users_id},
|
||||
});
|
||||
}
|
||||
184
web/src/views/system/role/components/addUsers/crud.tsx
Normal file
184
web/src/views/system/role/components/addUsers/crud.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import {getRoleUsersUnauthorized} from './api';
|
||||
import {
|
||||
compute,
|
||||
dict,
|
||||
UserPageQuery,
|
||||
AddReq,
|
||||
DelReq,
|
||||
EditReq,
|
||||
CrudOptions,
|
||||
CreateCrudOptionsProps,
|
||||
CreateCrudOptionsRet
|
||||
} from '@fast-crud/fast-crud';
|
||||
|
||||
import { ref , nextTick} from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
|
||||
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await getRoleUsersUnauthorized(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
return undefined;
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return undefined;
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 记录选中的行
|
||||
const selectedRows = ref<any>([]);
|
||||
|
||||
const onSelectionChange = (changed: any) => {
|
||||
const tableData = crudExpose.getTableData();
|
||||
const unChanged = tableData.filter((row: any) => !changed.includes(row));
|
||||
// 添加已选择的行
|
||||
XEUtils.arrayEach(changed, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
if (!ids.includes(item.id)) {
|
||||
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
|
||||
}
|
||||
});
|
||||
// 剔除未选择的行
|
||||
XEUtils.arrayEach(unChanged, (unItem: any) => {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
|
||||
});
|
||||
};
|
||||
const toggleRowSelection = () => {
|
||||
// 多选后,回显默认勾选
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
const selected = XEUtils.filter(tableData, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
return ids.includes(item.id);
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
XEUtils.arrayEach(selected, (item) => {
|
||||
tableRef.toggleRowSelection(item, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
actionbar: {
|
||||
show: false,
|
||||
buttons: {
|
||||
add: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
show: false,
|
||||
//固定右侧
|
||||
fixed: 'left',
|
||||
width: 150,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
show: false,
|
||||
},
|
||||
remove: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
rowKey: "id",
|
||||
onSelectionChange,
|
||||
onRefreshed: () => toggleRowSelection(),
|
||||
},
|
||||
columns: {
|
||||
$checked: {
|
||||
title: "选择",
|
||||
form: { show: false},
|
||||
column: {
|
||||
show: true,
|
||||
type: "selection",
|
||||
align: "center",
|
||||
width: "55px",
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
}
|
||||
},
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: { show: false },
|
||||
column: {
|
||||
//type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
formatter: (context) => {
|
||||
//计算序号,你可以自定义计算规则,此处为翻页累加
|
||||
let index = context.index ?? 1;
|
||||
let pagination = crudExpose!.crudBinding.value.pagination;
|
||||
// @ts-ignore
|
||||
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: '用户名',
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'text',
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dept: {
|
||||
title: '部门',
|
||||
show: true,
|
||||
type: 'dict-tree',
|
||||
column: {
|
||||
name: 'text',
|
||||
formatter({value,row,index}){
|
||||
return row.dept__name
|
||||
}
|
||||
},
|
||||
search: {
|
||||
show: true,
|
||||
disabled: true,
|
||||
col:{span: 6},
|
||||
component: {
|
||||
multiple: false,
|
||||
props: {
|
||||
checkStrictly: true,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
},
|
||||
dict: dict({
|
||||
isTree: true,
|
||||
url: '/api/system/dept/all_dept/',
|
||||
value: 'id',
|
||||
label: 'name'
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
91
web/src/views/system/role/components/addUsers/index.vue
Normal file
91
web/src/views/system/role/components/addUsers/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialog" title="添加授权用户" direction="rtl" destroy-on-close :before-close="handleDialogClose">
|
||||
<div style="height: 500px;" >
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #pagination-right>
|
||||
<el-popover placement="top" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
|
||||
</template>
|
||||
<el-table :data="selectedRows" size="small" :max-height="500">
|
||||
<!-- <el-table-column width="100" property="id" label="id" /> -->
|
||||
<el-table-column width="100" property="name" label="用户名" />
|
||||
<el-table-column fixed="right" label="操作" min-width="50">
|
||||
<template #default="scope">
|
||||
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
</fs-crud>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
|
||||
<el-button @click="handleDialogClose"> 取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { successNotification } from '/@/utils/message';
|
||||
import { addRoleUsers } from './api';
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
|
||||
const props = defineProps({
|
||||
refreshCallback: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
//对话框是否显示
|
||||
const dialog = ref(false);
|
||||
|
||||
// 父组件刷新回调函数
|
||||
const parentRefreshCallbackFunc = props.refreshCallback;
|
||||
|
||||
//抽屉关闭确认
|
||||
const handleDialogClose = () => {
|
||||
dialog.value = false;
|
||||
selectedRows.value = [];
|
||||
};
|
||||
|
||||
const handleDialogConfirm = async () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
await addRoleUsers(crudRef.value.getSearchFormData().role_id, XEUtils.pluck(selectedRows.value, 'id')).then(res => {
|
||||
successNotification(res.msg);
|
||||
})
|
||||
parentRefreshCallbackFunc && parentRefreshCallbackFunc(); // 刷新父组件
|
||||
handleDialogClose();
|
||||
};
|
||||
|
||||
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, context: {} });
|
||||
const { setSearchFormData, doRefresh } = crudExpose;
|
||||
|
||||
// 选中行的条数
|
||||
const selectedRowsCount = computed(() => {
|
||||
return selectedRows.value.length;
|
||||
});
|
||||
|
||||
const removeSelectedRows = (row: any) => {
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
|
||||
tableRef.toggleRowSelection(row, false);
|
||||
} else {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ dialog, setSearchFormData, doRefresh, parentRefreshCallbackFunc});
|
||||
</script>
|
||||
44
web/src/views/system/role/components/searchUsers/api.ts
Normal file
44
web/src/views/system/role/components/searchUsers/api.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { UserPageQuery} from '@fast-crud/fast-crud';
|
||||
|
||||
/**
|
||||
* 当前角色查询授权的用户
|
||||
* @param query 查询条件 需要有角色id
|
||||
* @returns
|
||||
*/
|
||||
export function getRoleUsersAuthorized(query: UserPageQuery) {
|
||||
query["authorized"] = 1; // 授权的用户
|
||||
return request({
|
||||
url: '/api/system/role/get_role_users/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 当前角色删除授权的用户
|
||||
* @param role_id 角色id
|
||||
* @param user_id 用户id数组
|
||||
* @returns
|
||||
*/
|
||||
export function removeRoleUser(role_id: number, user_id: Array<number>) {
|
||||
return request({
|
||||
url: `/api/system/role/${role_id}/remove_role_user/`,
|
||||
method: 'delete',
|
||||
data: {user_id: user_id},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 当前用户角色添加用户
|
||||
* @param role_id 角色id
|
||||
* @param data 用户id数组
|
||||
* @returns
|
||||
*/
|
||||
export function addRoleUsers(role_id: number, data: Array<Number>) {
|
||||
return request({
|
||||
url: `/api/system/role/${role_id}/add_role_users/`,
|
||||
method: 'post',
|
||||
data: {users_id: data},
|
||||
});
|
||||
}
|
||||
193
web/src/views/system/role/components/searchUsers/crud.tsx
Normal file
193
web/src/views/system/role/components/searchUsers/crud.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import {getRoleUsersAuthorized, removeRoleUser} from './api';
|
||||
import {
|
||||
compute,
|
||||
dict,
|
||||
UserPageQuery,
|
||||
AddReq,
|
||||
DelReq,
|
||||
EditReq,
|
||||
CrudOptions,
|
||||
CreateCrudOptionsProps,
|
||||
CreateCrudOptionsRet
|
||||
} from '@fast-crud/fast-crud';
|
||||
|
||||
import {auth} from "/@/utils/authFunction";
|
||||
import { ref , nextTick} from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
|
||||
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await getRoleUsersAuthorized(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
return undefined;
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return await removeRoleUser(crudExpose.crudRef.value.getSearchFormData().role_id, [row.id]);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 记录选中的行
|
||||
const selectedRows = ref<any>([]);
|
||||
|
||||
const onSelectionChange = (changed: any) => {
|
||||
const tableData = crudExpose.getTableData();
|
||||
const unChanged = tableData.filter((row: any) => !changed.includes(row));
|
||||
// 添加已选择的行
|
||||
XEUtils.arrayEach(changed, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
if (!ids.includes(item.id)) {
|
||||
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
|
||||
}
|
||||
});
|
||||
// 剔除未选择的行
|
||||
XEUtils.arrayEach(unChanged, (unItem: any) => {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
|
||||
});
|
||||
};
|
||||
const toggleRowSelection = () => {
|
||||
// 多选后,回显默认勾选
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
const selected = XEUtils.filter(tableData, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
return ids.includes(item.id);
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
XEUtils.arrayEach(selected, (item) => {
|
||||
tableRef.toggleRowSelection(item, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: auth('role:AuthorizedAdd'),
|
||||
click: (ctx: any) => {
|
||||
context!.subUserRef.value.dialog = true;
|
||||
nextTick(() => {
|
||||
context!.subUserRef.value.setSearchFormData({ form: { role_id: crudExpose.crudRef.value.getSearchFormData().role_id } });
|
||||
context!.subUserRef.value.doRefresh();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
show: auth('role:AuthorizedDel'),
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
show: false,
|
||||
},
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
rowKey: "id",
|
||||
onSelectionChange,
|
||||
onRefreshed: () => toggleRowSelection(),
|
||||
},
|
||||
columns: {
|
||||
$checked: {
|
||||
title: "选择",
|
||||
form: { show: false},
|
||||
column: {
|
||||
show: auth('role:AuthorizedDel'),
|
||||
type: "selection",
|
||||
align: "center",
|
||||
width: "55px",
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
}
|
||||
},
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: { show: false },
|
||||
column: {
|
||||
//type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
formatter: (context) => {
|
||||
//计算序号,你可以自定义计算规则,此处为翻页累加
|
||||
let index = context.index ?? 1;
|
||||
let pagination = crudExpose!.crudBinding.value.pagination;
|
||||
// @ts-ignore
|
||||
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: '用户名',
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'text',
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dept: {
|
||||
title: '部门',
|
||||
show: true,
|
||||
type: 'dict-tree',
|
||||
column: {
|
||||
name: 'text',
|
||||
formatter({value,row,index}){
|
||||
return row.dept__name
|
||||
}
|
||||
},
|
||||
search: {
|
||||
show: true,
|
||||
disabled: true,
|
||||
col:{span: 6},
|
||||
component: {
|
||||
multiple: false,
|
||||
props: {
|
||||
checkStrictly: true,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
},
|
||||
dict: dict({
|
||||
isTree: true,
|
||||
url: '/api/system/dept/all_dept/',
|
||||
value: 'id',
|
||||
label: 'name'
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
98
web/src/views/system/role/components/searchUsers/index.vue
Normal file
98
web/src/views/system/role/components/searchUsers/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<el-drawer size="70%" v-model="RoleUserDrawer.drawerVisible" direction="rtl" destroy-on-close :before-close="handleClose">
|
||||
<template #header>
|
||||
<div>
|
||||
当前授权角色:
|
||||
<el-tag>{{ RoleUserDrawer.role_name }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #pagination-right>
|
||||
<el-popover placement="top" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
|
||||
</template>
|
||||
<el-table :data="selectedRows" size="small" :max-height="500" >
|
||||
<!-- <el-table-column width="100" property="id" label="id" /> -->
|
||||
<el-table-column width="100" property="name" label="用户名" />
|
||||
<el-table-column fixed="right" label="操作" min-width="60">
|
||||
<template #default="scope">
|
||||
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
<template #pagination-left>
|
||||
<el-tooltip content="批量删除所选择的用户权限">
|
||||
<el-button v-show="selectedRowsCount > 0 && auth('role:AuthorizedDel')" type="danger" @click="multipleDel" :icon="Delete">批量删除</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</fs-crud>
|
||||
<subUser ref="subUserRef" :refreshCallback="refreshData"> </subUser>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {auth} from "/@/utils/authFunction";
|
||||
import { ref, onMounted, defineAsyncComponent, computed } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { Close, Delete } from '@element-plus/icons-vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
import {removeRoleUser} from "./api"
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { errorMessage, successNotification } from '/@/utils/message';
|
||||
|
||||
import { RoleUserStores } from '../../stores/RoleUserStores';
|
||||
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
|
||||
|
||||
const subUser = defineAsyncComponent(() => import('../addUsers/index.vue'));
|
||||
const subUserRef = ref();
|
||||
|
||||
const refreshData = () => {
|
||||
crudExpose.doRefresh();
|
||||
};
|
||||
|
||||
//抽屉是否显示
|
||||
const drawer = ref(false);
|
||||
|
||||
//抽屉关闭确认
|
||||
const handleClose = (done: () => void) => {
|
||||
selectedRows.value = [];
|
||||
done();
|
||||
};
|
||||
|
||||
// 选中行的条数
|
||||
const selectedRowsCount = computed(() => {
|
||||
return selectedRows.value.length;
|
||||
});
|
||||
|
||||
const removeSelectedRows = (row: any) => {
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
|
||||
tableRef.toggleRowSelection(row, false);
|
||||
} else {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
|
||||
}
|
||||
};
|
||||
|
||||
const multipleDel = async () => {
|
||||
if (selectedRows.value.length < 1) {
|
||||
errorMessage("请先勾选用户");
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定要删除这 “${selectedRows.value.length}” 位用户的权限吗`, "确认");
|
||||
const req = await removeRoleUser(crudRef.value.getSearchFormData().role_id, XEUtils.pluck(selectedRows.value, 'id'));
|
||||
selectedRows.value = [];
|
||||
successNotification(req.msg)
|
||||
crudExpose.doRefresh()
|
||||
}
|
||||
|
||||
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, context: {subUserRef} });
|
||||
const { setSearchFormData, doRefresh } = crudExpose;
|
||||
|
||||
defineExpose({ drawer, setSearchFormData, doRefresh });
|
||||
|
||||
</script>
|
||||
@@ -3,6 +3,7 @@ import * as api from './api';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
import { successMessage } from '../../../utils/message';
|
||||
import { auth } from '/@/utils/authFunction';
|
||||
import { nextTick, computed } from 'vue';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -46,7 +47,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 320,
|
||||
width: computed(() => {
|
||||
if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){
|
||||
return 420;
|
||||
}
|
||||
return 320;
|
||||
}),
|
||||
buttons: {
|
||||
view: {
|
||||
show: true,
|
||||
@@ -57,6 +63,19 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
remove: {
|
||||
show: auth('role:Delete'),
|
||||
},
|
||||
assignment: {
|
||||
type: 'primary',
|
||||
text: '授权用户',
|
||||
show: auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch'),
|
||||
click: (ctx: any) => {
|
||||
const { row } = ctx;
|
||||
context!.RoleUserDrawer.handleDrawerOpen(row);
|
||||
nextTick(() => {
|
||||
context!.RoleUserRef.value.setSearchFormData({ form: { role_id: row.id } });
|
||||
context!.RoleUserRef.value.doRefresh();
|
||||
});
|
||||
},
|
||||
},
|
||||
permission: {
|
||||
type: 'primary',
|
||||
text: '权限配置',
|
||||
|
||||
@@ -2,17 +2,22 @@
|
||||
<fs-page>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
<PermissionDrawerCom />
|
||||
<RoleUser ref="RoleUserRef" />
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="role">
|
||||
import { defineAsyncComponent, onMounted } from 'vue';
|
||||
import { defineAsyncComponent, onMounted, ref} from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { RoleDrawerStores } from './stores/RoleDrawerStores';
|
||||
import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
|
||||
import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
|
||||
import { RoleUsersStores } from './stores/RoleUsersStores';
|
||||
import { RoleUserStores } from './stores/RoleUserStores';
|
||||
|
||||
const RoleUser = defineAsyncComponent(() => import('./components/searchUsers/index.vue'));
|
||||
const RoleUserRef = ref();
|
||||
|
||||
const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
|
||||
|
||||
@@ -20,9 +25,11 @@ const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
|
||||
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
|
||||
const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
|
||||
const RoleUsers = RoleUsersStores();// 角色-用户
|
||||
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({
|
||||
createCrudOptions,
|
||||
context: { RoleDrawer, RoleMenuBtn, RoleMenuField },
|
||||
context: { RoleDrawer, RoleMenuBtn, RoleMenuField, RoleUserDrawer, RoleUserRef },
|
||||
});
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
|
||||
29
web/src/views/system/role/stores/RoleUserStores.ts
Normal file
29
web/src/views/system/role/stores/RoleUserStores.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**
|
||||
* 权限抽屉:角色-用户
|
||||
*/
|
||||
|
||||
export const RoleUserStores = defineStore('RoleUserStores', {
|
||||
state: (): any => ({
|
||||
drawerVisible: false,
|
||||
role_id: undefined,
|
||||
role_name: undefined,
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
* 打开权限修改抽屉
|
||||
*/
|
||||
handleDrawerOpen(row: any) {
|
||||
this.drawerVisible = true;
|
||||
this.role_name = row.name;
|
||||
this.role_id = row.id;
|
||||
},
|
||||
/**
|
||||
* 关闭权限修改抽屉
|
||||
*/
|
||||
handleDrawerClose() {
|
||||
this.drawerVisible = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
50
web/src/views/template/api.ts
Normal file
50
web/src/views/template/api.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { request,downloadFile } from '/@/utils/service';
|
||||
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
|
||||
|
||||
export const apiPrefix = '/api/VIEWSETNAME/';
|
||||
|
||||
export function GetList(query: PageQuery) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
export function GetObj(id: InfoReq) {
|
||||
return request({
|
||||
url: apiPrefix + id,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function AddObj(obj: AddReq) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'post',
|
||||
data: obj,
|
||||
});
|
||||
}
|
||||
|
||||
export function UpdateObj(obj: EditReq) {
|
||||
return request({
|
||||
url: apiPrefix + obj.id + '/',
|
||||
method: 'put',
|
||||
data: obj,
|
||||
});
|
||||
}
|
||||
|
||||
export function DelObj(id: DelReq) {
|
||||
return request({
|
||||
url: apiPrefix + id + '/',
|
||||
method: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export function exportData(params:any){
|
||||
return downloadFile({
|
||||
url: apiPrefix + 'export_data/',
|
||||
params: params,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
86
web/src/views/template/crud.tsx
Normal file
86
web/src/views/template/crud.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, UserPageQuery, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
||||
import _ from 'lodash-es';
|
||||
import * as api from './api';
|
||||
import { request } from '/@/utils/service';
|
||||
import { auth } from "/@/utils/authFunction";
|
||||
|
||||
//此处为crudOptions配置
|
||||
export default function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: any) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
if (row.id) {
|
||||
form.id = row.id;
|
||||
}
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
|
||||
const exportRequest = async (query: UserPageQuery) => {
|
||||
return await api.exportData(query)
|
||||
};
|
||||
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
export: {
|
||||
// 注释编号:django-vue3-admin-crud210716:注意这个auth里面的值,最好是使用index.vue文件里面的name值并加上请求动作的单词
|
||||
show: auth('VIEWSETNAME:Export'),
|
||||
text: "导出",//按钮文字
|
||||
title: "导出",//鼠标停留显示的信息
|
||||
click() {
|
||||
return exportRequest(crudExpose.getSearchFormData())
|
||||
// return exportRequest(crudExpose!.getSearchFormData()) // 注意这个crudExpose!.getSearchFormData(),一些低版本的环境是需要添加!的
|
||||
}
|
||||
},
|
||||
add: {
|
||||
show: auth('VIEWSETNAME:Create'),
|
||||
},
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
buttons: {
|
||||
view: {
|
||||
type: 'text',
|
||||
order: 1,
|
||||
show: auth('VIEWSETNAME:Retrieve')
|
||||
},
|
||||
edit: {
|
||||
type: 'text',
|
||||
order: 2,
|
||||
show: auth('VIEWSETNAME:Update')
|
||||
},
|
||||
copy: {
|
||||
type: 'text',
|
||||
order: 3,
|
||||
show: auth('VIEWSETNAME:Copy')
|
||||
},
|
||||
remove: {
|
||||
type: 'text',
|
||||
order: 4,
|
||||
show: auth('VIEWSETNAME:Delete')
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
// COLUMNS_CONFIG
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
56
web/src/views/template/index.vue
Normal file
56
web/src/views/template/index.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<fs-page class="PageFeatureSearchMulti">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #cell_url="scope">
|
||||
<el-tag size="small">{{ scope.row.url }}</el-tag>
|
||||
</template>
|
||||
<!-- 注释编号: django-vue3-admin-index442216: -->
|
||||
<!-- 注释编号:django-vue3-admin-index39263917:代码开始行-->
|
||||
<!-- 功能说明:使用导入组件,并且修改api地址为当前对应的api,当前是demo的api="api/CarModelViewSet/"-->
|
||||
<template #actionbar-right>
|
||||
<importExcel api="api/VIEWSETNAME/" v-auth="'user:Import'">导入</importExcel>
|
||||
</template>
|
||||
<!-- 注释编号:django-vue3-admin-index263917:代码结束行-->
|
||||
|
||||
</fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMounted, getCurrentInstance, defineComponent} from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import createCrudOptions from './crud';
|
||||
|
||||
// 注释编号: django-vue3-admin-index192316:导入组件
|
||||
import importExcel from '/@/components/importExcel/index.vue'
|
||||
|
||||
|
||||
export default defineComponent({ //这里配置defineComponent
|
||||
name: "VIEWSETNAME", //把name放在这里进行配置了
|
||||
components: {importExcel}, //注释编号: django-vue3-admin-index552416: 注册组件,把importExcel组件放在这里,这样<template></template>中才能正确的引用到组件
|
||||
setup() { //这里配置了setup()
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const context: any = {
|
||||
componentName: instance?.type.name
|
||||
};
|
||||
|
||||
const { crudBinding, crudRef, crudExpose, resetCrudOptions } = useFs({ createCrudOptions, context});
|
||||
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
return {
|
||||
//增加了return把需要给上面<template>内调用的<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
crudBinding,
|
||||
crudRef,
|
||||
};
|
||||
|
||||
|
||||
} //这里关闭setup()
|
||||
}); //关闭defineComponent
|
||||
|
||||
</script>
|
||||
@@ -11,6 +11,7 @@ const pathResolve = (dir: string) => {
|
||||
|
||||
const alias: Record<string, string> = {
|
||||
'/@': pathResolve('./src/'),
|
||||
'@great-dream': pathResolve('./node_modules/@great-dream/'),
|
||||
'@views': pathResolve('./src/views'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
|
||||
'@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/')
|
||||
|
||||
Reference in New Issue
Block a user