!111 发布到正式版本

Merge pull request !111 from dvadmin/develop
This commit is contained in:
dvadmin
2025-03-20 20:43:47 +00:00
committed by Gitee
53 changed files with 2391 additions and 858 deletions

View File

@@ -54,16 +54,21 @@
## 交流 ## 交流
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦 - 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦 - 插件市场:[戳我](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交流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交流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框架开发的应用和插件。 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稳定版本 主分支master稳定版本
@@ -210,5 +227,19 @@ docker-compose up -d --build
![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)
## 审批流插件
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/97fbbf29673edfd66a1edd49237791bb.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/c43aa51278cbc478287c718d22397479.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/9732a5cca9c1166d1a65c35e313ab90d.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/3ca9dd0801ce76d21435abcc8a3d505a.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/a87a8d2329ef66880af5b0f16c5ff823.png)

1
backend/.gitignore vendored
View File

@@ -98,5 +98,4 @@ media/
__pypackages__/ __pypackages__/
package-lock.json package-lock.json
gunicorn.pid gunicorn.pid
plugins/*
!plugins/__init__.py !plugins/__init__.py

View File

@@ -1,6 +1,8 @@
import functools import functools
import os import os
from celery.signals import task_postrun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings from django.conf import settings
@@ -38,3 +40,12 @@ def retry_base_task_error():
return wrapper return wrapper
return wraps 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)

View File

@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
# ********** 一键导入插件配置开始 ********** # ********** 一键导入插件配置开始 **********
# 例如: # 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心 # 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_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理 # from dvadmin_tenants.settings import * # 租户管理

View File

@@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer):
""" """
初始化获取数信息(用于生成初始化json文件) 初始化获取数信息(用于生成初始化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): def save(self, **kwargs):
instance = super().save(**kwargs) instance = super().save(**kwargs)
@@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer):
model = Users model = Users
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type', 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', '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'] read_only_fields = ['id']
extra_kwargs = { extra_kwargs = {
'creator': {'write_only': True}, 'creator': {'write_only': True},
@@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer):
""" """
初始化角色菜单(用于生成初始化json文件) 初始化角色菜单(用于生成初始化json文件)
""" """
role__key = serializers.CharField(max_length=100, required=True) role__key = serializers.CharField(source='role.key')
menu__web_path = serializers.CharField(max_length=100, required=True) menu__web_path = serializers.CharField(source='menu.web_path')
menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True) 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): def create(self, validated_data):
init_data = self.initial_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() 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() 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['role'] = role_id
@@ -192,7 +212,7 @@ class RoleMenuInitSerializer(CustomModelSerializer):
class Meta: class Meta:
model = RoleMenuPermission model = RoleMenuPermission
fields = ['role__key', 'menu__web_path', 'menu__component_name', 'creator', 'dept_belong_id'] fields = ['role__key', 'menu__web_path', 'menu__component_name','creator', 'dept_belong_id']
read_only_fields = ["id"] read_only_fields = ["id"]
extra_kwargs = { extra_kwargs = {
'role': {'required': False}, 'role': {'required': False},
@@ -206,14 +226,22 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
""" """
初始化角色菜单按钮(用于生成初始化json文件) 初始化角色菜单按钮(用于生成初始化json文件)
""" """
role__key = serializers.CharField(max_length=100, required=True) role__key = serializers.CharField(source='role.key')
menu_button__value = serializers.CharField(max_length=100, required=True) menu_button__value = serializers.CharField(source='menu_button.value')
data_range = serializers.CharField(max_length=100, required=False) 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): def create(self, validated_data):
init_data = self.initial_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() role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id validated_data['role'] = role_id
@@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
return instance return instance
def save(self, **kwargs): 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 super().save(**kwargs)
return self.instance return self.instance

View File

@@ -10,7 +10,7 @@ django.setup()
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from application.settings import BASE_DIR 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, \ from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
RoleMenuInitSerializer, RoleMenuButtonInitSerializer RoleMenuInitSerializer, RoleMenuButtonInitSerializer
@@ -57,6 +57,12 @@ class Command(BaseCommand):
def generate_system_config(self): def generate_system_config(self):
self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) 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): def handle(self, *args, **options):
generate_name = options.get('generate_name') generate_name = options.get('generate_name')
generate_name_dict = { generate_name_dict = {
@@ -67,6 +73,8 @@ class Command(BaseCommand):
"api_white_list": self.generate_api_white_list, "api_white_list": self.generate_api_white_list,
"dictionary": self.generate_dictionary, "dictionary": self.generate_dictionary,
"system_config": self.generate_system_config, "system_config": self.generate_system_config,
"role_menu": self.generate_role_menu,
"role_menu_button": self.generate_role_menu_button,
} }
if not generate_name: if not generate_name:
for ele in generate_name_dict.keys(): for ele in generate_name_dict.keys():

View File

@@ -49,7 +49,7 @@ urlpatterns = [
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})), # 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/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()),
] ]

View File

@@ -10,16 +10,17 @@ from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from 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.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin 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.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer): class RoleSerializer(CustomModelSerializer):
@@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
""" """
角色管理接口 角色管理接口
@@ -141,4 +141,63 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
# right : 添加用户权限 # right : 添加用户权限
role.users_set.add(*movedKeys) role.users_set.add(*movedKeys)
serializer = RoleSerializer(role) serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功") 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="添加成功")

View File

@@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
isCheck = data.get('isCheck', None) isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None) roleId = data.get('roleId', None)
btnId = data.get('btnId', None) btnId = data.get('btnId', None)
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
dept = data.get('dept', None) or [] # 默认空部门
if isCheck: 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: else:
# 删除权限:移除关联记录 # 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()

View File

@@ -336,7 +336,7 @@ class UserViewSet(CustomModelViewSet):
verify_password = check_password(str(old_pwd_md5), request.user.password) verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_password: if verify_password:
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) # 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.pwd_change_count += 1
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")

View File

@@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from django_filters.conf import settings 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 from dvadmin.utils.models import CoreModel
class CoreModelFilterBankend(BaseFilterBackend): class CoreModelFilterBankend(BaseFilterBackend):
@@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend):
create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', 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_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]): if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q() create_filter = Q()
if create_datetime_after and create_datetime_before: if create_datetime_after and create_datetime_before:
@@ -149,13 +149,16 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if _pk: # 判断是否是单例查询 if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api) re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True) role_id_list = request.user.role.values_list('id', flat=True)
role_permission_list=RoleMenuButtonPermission.objects.filter( # 修复权限获取bug
role__in=role_id_list, menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
role__status=1, role_permission_list = []
menu_button__api=re_api, if menu_button_ids:
menu_button__method=method).values( role_permission_list=RoleMenuButtonPermission.objects.filter(
'data_range' role__in=role_id_list,
) role__status=1,
menu_button_id__in=menu_button_ids).values(
'data_range'
)
dataScope_list = [] # 权限范围列表 dataScope_list = [] # 权限范围列表
for ele in role_permission_list: for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据 # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据

View File

@@ -6,6 +6,8 @@
@Created on: 2021/6/1 001 22:57 @Created on: 2021/6/1 001 22:57
@Remark: 自定义视图集 @Remark: 自定义视图集
""" """
import copy
from django.db import transaction from django.db import transaction
from django_filters import DateTimeFromToRangeFilter from django_filters import DateTimeFromToRangeFilter
from django_filters.rest_framework import FilterSet from django_filters.rest_framework import FilterSet
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
kwargs.setdefault('context', self.get_serializer_context()) kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准 # 全部以可见字段为准
can_see = self.get_menu_field(serializer_class) 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: # 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 self.request.permission_fields = can_see
if isinstance(self.request.data, list): if isinstance(self.request.data, list):
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
def get_menu_field(self, serializer_class): def get_menu_field(self, serializer_class):
"""获取字段权限""" """获取字段权限"""
finded = False
for model in get_custom_app_models(): if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded is False:
return [] 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): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request) serializer = self.get_serializer(data=request.data, request=request)
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
instance.delete() instance.delete()
return DetailResponse(data=[], msg="删除成功") 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( @swagger_auto_schema(request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
required=['keys'], required=['keys'],

View File

@@ -28,4 +28,5 @@ uvicorn==0.30.3
gunicorn==22.0.0 gunicorn==22.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
dvadmin3-celery==3.1.6

87
crud-gen.sh Normal file
View 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

View 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/ WORKDIR /web/
COPY web/. . COPY web/. .
RUN yarn install --registry=https://registry.npmmirror.com RUN yarn install --registry=https://registry.npmmirror.com

58
init.sh
View File

@@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
ENV_FILE=".env" ENV_FILE=".env"
HOST="177.10.0.13"
# 检查 .env 文件是否存在 # 检查 .env 文件是否存在
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE 文件已存在。" echo "$ENV_FILE 文件已存在。"
@@ -15,17 +16,60 @@ else
echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。" echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。"
awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }' 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|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|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 sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py
echo "初始化密码创建成功" echo "初始化密码创建成功"
fi fi
echo "正在启动容器..."
docker-compose up -d docker-compose up -d
docker exec dvadmin3-django python manage.py makemigrations
docker exec dvadmin3-django python manage.py migrate if [ $? -ne 0 ]; then
docker exec dvadmin3-django python manage.py init echo "docker-compose up -d 执行失败!"
echo "欢迎使用dvadmin3项目" exit 1
echo "登录地址http://ip:8080" fi
echo "如访问不到,请检查防火墙配置"
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

View File

@@ -1,6 +1,6 @@
# port 端口号 # port 端口号
VITE_PORT = 8080 VITE_PORT = 8080
VITE_API_URL = 'http://dvadmin3api.django.icu:8001' VITE_API_URL = 'http://127.0.0.1:8000'
# open 运行 npm run dev 时自动打开浏览器 # open 运行 npm run dev 时自动打开浏览器
VITE_OPEN = false VITE_OPEN = false

View File

@@ -2,7 +2,7 @@
ENV = 'development' ENV = 'development'
# 本地环境接口地址 # 本地环境接口地址
VITE_API_URL = 'http://127.0.0.1:8000' VITE_API_URL = 'http://127.0.0.1:8001'
# 是否启用按钮权限 # 是否启用按钮权限
VITE_PM_ENABLED = true VITE_PM_ENABLED = true

View File

@@ -49,6 +49,10 @@
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2) 👩‍👦‍👦文档地址:[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'>
## 交流 ## 交流

View File

@@ -5,6 +5,7 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite --force", "dev": "vite --force",
"build:dev":"vite build --mode development",
"build": "vite build", "build": "vite build",
"build:local": "vite build --mode local_prod", "build:local": "vite build --mode local_prod",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
@@ -15,6 +16,7 @@
"@fast-crud/fast-extends": "^1.21.2", "@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",

View File

@@ -11,7 +11,7 @@
<script setup lang="ts" name="app"> <script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue'; 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 { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; 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 Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue')); const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.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 { messages, locale } = useI18n();
const setingsRef = ref(); const setingsRef = ref();
@@ -35,7 +36,8 @@ const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket'; import websocket from '/@/utils/websocket';
import { ElNotification } from 'element-plus'; const core = useCore();
const router = useRouter();
// 获取版本号 // 获取版本号
const getVersion = computed(() => { const getVersion = computed(() => {
let isVersion = false; let isVersion = false;
@@ -67,7 +69,15 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => { mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer(); 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')) { if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') }); storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle'); document.documentElement.style.cssText = Local.get('themeConfigStyle');
@@ -117,7 +127,25 @@ const wsReceive = (message: any) => {
position: 'bottom-right', position: 'bottom-right',
duration: 5000, 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(() => { onBeforeUnmount(() => {
// 关闭连接 // 关闭连接

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="user-info-head" @click="editCropper()"> <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-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row> <el-row>
<el-col class="flex justify-center"> <el-col class="flex justify-center">
@@ -50,10 +50,11 @@ import { VueCropper } from 'vue-cropper';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue'; import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
import { base64ToFile } from '/@/utils/tools'; import { base64ToFile } from '/@/utils/tools';
import headerImage from "/@/assets/img/headerImage.png";
import {getBaseURL} from "/@/utils/baseUrl";
const userStore = useUserInfo(); const userStore = useUserInfo();
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false); const visible = ref(false);
const title = ref('修改头像'); const title = ref('修改头像');
const emit = defineEmits(['uploadImg']); const emit = defineEmits(['uploadImg']);
@@ -75,7 +76,7 @@ const dialogVisiable = computed({
//图片裁剪数据 //图片裁剪数据
const options = reactive({ const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址 img: userStore.userInfos.avatar || headerImage, // 裁剪图片的地址
fileName: '', fileName: '',
autoCrop: true, // 是否默认生成截图框 autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度 autoCropWidth: 200, // 默认生成截图框宽度
@@ -165,6 +166,7 @@ const updateAvatar = (img) => {
defineExpose({ defineExpose({
updateAvatar, updateAvatar,
editCropper
}); });
</script> </script>
@@ -172,7 +174,6 @@ defineExpose({
.user-info-head { .user-info-head {
position: relative; position: relative;
display: inline-block; display: inline-block;
height: 120px;
} }
.user-info-head:hover:after { .user-info-head:hover:after {

View File

@@ -8,7 +8,29 @@
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])" <el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
:label="item.name" /> :label="item.name" />
</el-select> </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" @mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }"> :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }"> <el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
@@ -24,10 +46,11 @@
</div> </div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover" <div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div> :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 /> <Close />
</el-icon> </el-icon>
</div> </div>
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter" <div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave" @mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;" style="position: relative; display: flex; align-items: center; justify-items: center;"
@@ -46,6 +69,7 @@
<Close /> <Close />
</el-icon> </el-icon>
</div> </div>
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter" <div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave" @mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;" style="position: relative; display: flex; align-items: center; justify-items: center;"
@@ -199,7 +223,7 @@ const props = defineProps({
tabsShow: { type: Number, default: SHOW.ALL }, tabsShow: { type: Number, default: SHOW.ALL },
// 是否可以多选,默认单选 // 是否可以多选,默认单选
// 该值为true时inputType必须是selector暂不支持其他type的多选 // 该值为true时inputType必须是selector或image暂不支持其他type的多选
multiple: { type: Boolean, default: false }, multiple: { type: Boolean, default: false },
// 是否可选该参数用于只上传和展示而不选择和绑定model的情况 // 是否可选该参数用于只上传和展示而不选择和绑定model的情况
@@ -274,6 +298,7 @@ const onItemClick = async (e: MouseEvent) => {
while (!target.dataset.id) target = target.parentElement as HTMLElement; while (!target.dataset.id) target = target.parentElement as HTMLElement;
let fileId = target.dataset.id; let fileId = target.dataset.id;
if (props.multiple) { if (props.multiple) {
if (!!!data.value) data.value = [];
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; } if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
else { target.classList.add('active'); flat = 1; } else { target.classList.add('active'); flat = 1; }
if (data.value.length) { if (data.value.length) {
@@ -327,8 +352,12 @@ const clearState = () => {
// all数据不能清因为all只会在挂载的时候赋值一次 // all数据不能清因为all只会在挂载的时候赋值一次
// listAllData.value = []; // 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); const netLoading = ref<boolean>(false);
@@ -386,7 +415,15 @@ watch(
const { ui } = useUi(); const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext(); const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => { 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.onChange();
formValidator.onBlur(); formValidator.onBlur();
}; };
@@ -394,7 +431,8 @@ const onDataChange = (value: any) => {
defineExpose({ data, onDataChange, selectVisiable, clearState, clear }); defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
onMounted(() => { onMounted(() => {
if (props.multiple && props.inputType !== 'selector')
if (props.multiple && !['selector', 'image'].includes(props.inputType))
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector'); throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
listRequestAll(); listRequestAll();
console.log('fileselector tenentmdoe', isTenentMode); console.log('fileselector tenentmdoe', isTenentMode);
@@ -475,4 +513,9 @@ onMounted(() => {
top: 2px; top: 2px;
cursor: pointer; cursor: pointer;
} }
.itemList {
border: 1px solid #dcdfe6;
border-radius: 8px;
}
</style> </style>

View File

@@ -3,6 +3,7 @@
popper-class="popperClass" popper-class="popperClass"
class="tableSelector" class="tableSelector"
multiple multiple
:collapseTags="props.tableConfig.collapseTags"
@remove-tag="removeTag" @remove-tag="removeTag"
v-model="data" v-model="data"
placeholder="请选择" placeholder="请选择"
@@ -18,20 +19,22 @@
<el-table <el-table
ref="tableRef" ref="tableRef"
:data="tableData" :data="tableData"
size="mini" :size="props.tableConfig.size"
border border
row-key="id" row-key="id"
:lazy="props.tableConfig.lazy" :lazy="props.tableConfig.lazy"
:load="props.tableConfig.load" :load="props.tableConfig.load"
:tree-props="props.tableConfig.treeProps" :tree-props="props.tableConfig.treeProps"
style="width: 400px" style="width: 600px"
max-height="200" max-height="200"
height="200" height="200"
:highlight-current-row="!props.tableConfig.isMultiple" :highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@select="handleSelectionChange"
@selectAll="handleSelectionChange"
@current-change="handleCurrentChange" @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 fixed type="index" label="#" width="50" />
<el-table-column <el-table-column
:prop="item.prop" :prop="item.prop"
@@ -56,24 +59,32 @@
</template> </template>
<script setup lang="ts"> <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 XEUtils from 'xe-utils';
import { request } from '/@/utils/service'; import { request } from '/@/utils/service';
const props = defineProps({ const props = defineProps({
modelValue: {}, modelValue: {
type: Array || String || Number,
default: () => []
},
tableConfig: { tableConfig: {
url: null, type: Object,
label: null, //显示值 default:{
value: null, //数据值 url: null,
isTree: false, label: null, //显示值
lazy: true, value: null, //数据值
load: () => {}, isTree: false,
data: [], //默认数据 lazy: true,
isMultiple: false, //是否多选 size:'default',
treeProps: { children: 'children', hasChildren: 'hasChildren' }, load: () => {},
columns: [], //每一项对应的列表项 data: [], //默认数据
}, isMultiple: false, //是否多选
collapseTags:false,
treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项
},
},
displayLabel: {}, displayLabel: {},
} as any); } as any);
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@@ -86,7 +97,7 @@ const multipleSelection = ref();
// 搜索值 // 搜索值
const search = ref(undefined); const search = ref(undefined);
//表格数据 //表格数据
const tableData = ref(); const tableData = ref([]);
// 分页的配置 // 分页的配置
const pageConfig = reactive({ const pageConfig = reactive({
page: 1, page: 1,
@@ -99,7 +110,6 @@ const pageConfig = reactive({
* @param val:Array * @param val:Array
*/ */
const handleSelectionChange = (val: any) => { const handleSelectionChange = (val: any) => {
multipleSelection.value = val;
const { tableConfig } = props; const { tableConfig } = props;
const result = val.map((item: any) => { const result = val.map((item: any) => {
return item[tableConfig.value]; return item[tableConfig.value];
@@ -117,7 +127,7 @@ const handleSelectionChange = (val: any) => {
const handleCurrentChange = (val: any) => { const handleCurrentChange = (val: any) => {
const { tableConfig } = props; const { tableConfig } = props;
if (!tableConfig.isMultiple && val) { if (!tableConfig.isMultiple && val) {
data.value = [val[tableConfig.label]]; // data.value = [val[tableConfig.label]];
emit('update:modelValue', val[tableConfig.value]); 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 * @param bool
@@ -169,20 +205,12 @@ const handlePageChange = (page: any) => {
getDict(); getDict();
}; };
// 监听displayLabel的变化更新数据 onMounted(()=>{
watch( setTimeout(()=>{
() => { getNodeValues()
return props.displayLabel; },1000)
}, })
(value) => {
const { tableConfig } = props;
const result = value
? value.map((item: any) => { return item[tableConfig.label];})
: null;
data.value = result;
},
{ immediate: true }
);
</script> </script>
<style scoped> <style scoped>

View File

@@ -17,13 +17,15 @@ import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
import { useRoute } from 'vue-router';
const route = useRoute();
// 引入组件 // 引入组件
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue')); const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue')); const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
// 定义变量内容 // 定义变量内容
const layoutAsideScrollbarRef = ref(); const layoutAsideScrollbarRef = ref();
const routesIndex = ref(0);
const stores = useRoutesList(); const stores = useRoutesList();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes(); const storesTagsViewRoutes = useTagsViewRoutes();
@@ -83,10 +85,36 @@ const closeLayoutAsideMobileMode = () => {
if (clientWidth < 1000) themeConfig.value.isCollapse = false; if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', ''); 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; if (themeConfig.value.layout === 'columns') return false;
state.menuList = filterRoutesFun(routesList.value); 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[] => { const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
@@ -122,7 +150,8 @@ onBeforeMount(() => {
let { layout, isClassicSplitMenu } = themeConfig.value; let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) { if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = []; state.menuList = [];
state.menuList = res.children; // state.menuList = res.children;
setFilterRoutes(res.path);
} }
}); });
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => { mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {

View File

@@ -102,6 +102,5 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
background: var(--next-bg-topBar); background: var(--next-bg-topBar);
border-bottom: 1px solid var(--next-border-color-light);
} }
</style> </style>

View File

@@ -37,7 +37,7 @@
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i> <i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon"> <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> <template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0"> <el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')"> <el-icon :title="$t('message.user.title4')">
@@ -58,7 +58,7 @@
></i> ></i>
</div> </div>
<div> <div>
<span v-if="!isSocketOpen"> <span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm <el-popconfirm
width="250" width="250"
ref="onlinePopoverRef" ref="onlinePopoverRef"
@@ -71,7 +71,7 @@
> >
<template #reference> <template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}"> <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> </el-badge>
</template> </template>
</el-popconfirm> </el-popconfirm>
@@ -93,7 +93,7 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> <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="/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-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -250,6 +250,7 @@ onMounted(() => {
//消息中心的未读数量 //消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter'; import { messageCenterStore } from '/@/stores/messageCenter';
import {getBaseURL} from "/@/utils/baseUrl";
const messageCenter = messageCenterStore(); const messageCenter = messageCenterStore();
</script> </script>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="el-menu-horizontal-warp"> <div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef"> <!-- <el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">-->
<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal"> <el-menu :default-active="defaultActive" background-color="transparent" mode="horizontal">
<template v-for="val in menuLists"> <template v-for="(val,index) in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path"> <el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title> <template #title>
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
@@ -11,7 +11,7 @@
<SubItem :chil="val.children" /> <SubItem :chil="val.children" />
</el-sub-menu> </el-sub-menu>
<template v-else> <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)"> <template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }} {{ $t(val.meta.title) }}
@@ -26,22 +26,25 @@
</template> </template>
</template> </template>
</el-menu> </el-menu>
</el-scrollbar> <!-- </el-scrollbar>-->
</div> </div>
</template> </template>
<script setup lang="ts" name="navMenuHorizontal"> <script setup lang="ts" name="navMenuHorizontal">
import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue'; 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 { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other'; import other from '/@/utils/other';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
const router = useRouter()
// 引入组件 // 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')); const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
const state = reactive<AsideState>({
menuList: [],
clientWidth: 0
});
// 定义父组件传过来的值 // 定义父组件传过来的值
const props = defineProps({ const props = defineProps({
// 菜单列表 // 菜单列表
@@ -58,19 +61,33 @@ const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores); const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute(); const route = useRoute();
const state = reactive({ const defaultActive = ref('')
defaultActive: '' as string | undefined,
});
// 获取父级菜单数据 // 获取父级菜单数据
const menuLists = computed(() => { const menuLists = computed(() => {
<RouteItems>props.menuList.shift()
return <RouteItems>props.menuList; return <RouteItems>props.menuList;
}); });
// 设置横向滚动条可以鼠标滚轮滚动 // 递归获取当前路由的顶级索引
const onElMenuHorizontalScroll = (e: WheelEventType) => { const findFirstLevelIndex = (data, path) => {
const eventDelta = e.wheelDelta || -e.deltaY * 40; for (let index = 0; index < data.length; index++) {
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft + eventDelta / 4; 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 = () => { const initElMenuOffsetLeft = () => {
nextTick(() => { nextTick(() => {
@@ -107,17 +124,41 @@ const setSendClassicChildren = (path: string) => {
const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => { const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
const { path, meta } = currentRoute; const { path, meta } = currentRoute;
if (themeConfig.value.layout === 'classic') { 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 { } else {
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/'); const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/'); if (pathSplit.length >= 4 && meta?.isHide) defaultActive.value = pathSplit.splice(0, 3).join('/');
else state.defaultActive = path; else defaultActive.value = path;
} }
}; };
// 打开外部链接 // 打开外部链接
const onALinkClick = (val: RouteItem) => { const onALinkClick = (val: RouteItem) => {
other.handleOpenLink(val); 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(() => { onBeforeMount(() => {
setCurrentRouterHighlight(route); setCurrentRouterHighlight(route);
@@ -126,16 +167,6 @@ onBeforeMount(() => {
onMounted(() => { onMounted(() => {
initElMenuOffsetLeft(); 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -8,7 +8,21 @@
<sub-item :chil="val.children" /> <sub-item :chil="val.children" />
</el-sub-menu> </el-sub-menu>
<template v-else> <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)"> <template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span> <span>{{ $t(val.meta.title) }}</span>

View File

@@ -21,13 +21,14 @@ const menuApi = useMenuApi();
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}'); const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}'); const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
const greatDream: any = import.meta.glob('@great-dream/**/*.{vue,tsx}');
/** /**
* 获取目录下的 .vue、.tsx 全部文件 * 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob * @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json * @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 keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => { const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, ''); 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) { if (matchKeys?.length === 1) {
const matchKey = matchKeys[0]; const matchKey = matchKeys[0];

View File

@@ -53,9 +53,12 @@ export default {
}, },
form: { form: {
afterSubmit(ctx: any) { afterSubmit(ctx: any) {
const {res} = ctx
// 增加crud提示 // 增加crud提示
if (ctx.res.code == 2000) { if (res?.code == 2000) {
successNotification(ctx.res.msg); successNotification(ctx.res.msg);
}else{
return
} }
}, },
}, },

View File

@@ -1,259 +1,286 @@
import {dict} from "@fast-crud/fast-crud"; import {dict} from '@fast-crud/fast-crud';
import {shallowRef} from 'vue' import {shallowRef} from 'vue';
import deptFormat from "/@/components/dept-format/index.vue"; import deptFormat from '/@/components/dept-format/index.vue';
export const commonCrudConfig = (options = { /** 1. 每个字段可选属性 */
create_datetime: { export interface CrudFieldOption {
form: false, form?: boolean;
table: false, table?: boolean;
search: false search?: boolean;
}, width?: number;
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
},
}) => {
return {
dept_belong_id: {
title: '所属部门',
type: 'dict-tree',
search: {
show: options.dept_belong_id?.search || false
},
dict: dict({
url: '/api/system/dept/all_dept/',
isTree: true,
value: 'id',
label: 'name',
children: 'children',
}),
column: {
align: 'center',
width: 300,
show: options.dept_belong_id?.table || false,
component: {
name: shallowRef(deptFormat),
vModel: "modelValue",
}
},
form: {
show: options.dept_belong_id?.form || false,
component: {
multiple: false,
clearable: true,
props: {
checkStrictly: true,
props: {
// 为什么这里要写两层props
// 因为props属性名与fs的动态渲染的props命名冲突所以要多写一层
label: "name",
value: "id",
}
}
},
helper: "默认不填则为当前创建用户的部门ID"
}
},
description: {
title: '备注',
search: {
show: options.description?.search || false
},
type: 'textarea',
column: {
width: 100,
show: options.description?.table || false,
},
form: {
show: options.description?.form || false,
component: {
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
}
},
viewForm: {
show: true
}
},
modifier_name: {
title: '修改人',
search: {
show: options.modifier_name?.search || false
},
column: {
width: 100,
show: options.modifier_name?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
creator_name: {
title: '创建人',
search: {
show: options.creator_name?.search || false
},
column: {
width: 100,
show: options.creator_name?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
update_datetime: {
title: '更新时间',
type: 'datetime',
search: {
show: options.update_datetime?.search || false,
col: {span: 8},
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
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表单点击保存按钮后提交到后台之前执行转化
if (value) {
context.form.update_datetime_after = value[0]
context.form.update_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
}
},
column: {
width: 160,
show: options.update_datetime?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
create_datetime: {
title: '创建时间',
type: 'datetime',
search: {
show: options.create_datetime?.search || false,
col: {span: 8},
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
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表单点击保存按钮后提交到后台之前执行转化
if (value) {
context.form.create_datetime_after = value[0]
context.form.create_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
}
},
column: {
width: 160,
show: options.create_datetime?.table || false,
},
form: {
show: false
},
viewForm: {
show: true
}
}
}
} }
/** 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: merged.dept_belong_id.search,
},
dict: dict({
url: '/api/system/dept/all_dept/',
isTree: true,
value: 'id',
label: 'name',
children: 'children',
}),
column: {
align: 'center',
width: merged.dept_belong_id.width,
show: merged.dept_belong_id.table,
component: {
// fast-crud里自定义组件常用"component.is"
is: shallowRef(deptFormat),
vModel: 'modelValue',
},
},
form: {
show: merged.dept_belong_id.form,
component: {
multiple: false,
clearable: true,
props: {
checkStrictly: true,
props: {
label: 'name',
value: 'id',
},
},
},
helper: '默认不填则为当前创建用户的部门ID',
},
},
description: {
title: '备注',
search: {
show: merged.description.search,
},
type: 'textarea',
column: {
width: merged.description.width,
show: merged.description.table,
},
form: {
show: merged.description.form,
component: {
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
},
},
viewForm: {
show: true,
},
},
modifier_name: {
title: '修改人',
search: {
show: merged.modifier_name.search,
},
column: {
width: merged.modifier_name.width,
show: merged.modifier_name.table,
},
form: {
show: merged.modifier_name.form,
},
viewForm: {
show: true,
},
},
creator_name: {
title: '创建人',
search: {
show: merged.creator_name.search,
},
column: {
width: merged.creator_name.width,
show: merged.creator_name.table,
},
form: {
show: merged.creator_name.form,
},
viewForm: {
show: true,
},
},
update_datetime: {
title: '更新时间',
type: 'datetime',
search: {
show: merged.update_datetime.search,
col: { span: 8 },
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [
{
text: '最近一周',
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: 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: 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 { value } = context;
if (value) {
context.form.update_datetime_after = value[0];
context.form.update_datetime_before = value[1];
delete context.form.update_datetime;
}
},
},
column: {
width: merged.update_datetime.width,
show: merged.update_datetime.table,
},
form: {
show: merged.update_datetime.form,
},
viewForm: {
show: true,
},
},
create_datetime: {
title: '创建时间',
type: 'datetime',
search: {
show: merged.create_datetime.search,
col: { span: 8 },
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [
{
text: '最近一周',
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: 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: 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 { value } = context;
if (value) {
context.form.create_datetime_after = value[0];
context.form.create_datetime_before = value[1];
delete context.form.create_datetime;
}
},
},
column: {
width: merged.create_datetime.width,
show: merged.create_datetime.table,
},
form: {
show: merged.create_datetime.form,
},
viewForm: {
show: true,
},
},
};
};

57
web/src/utils/cores.tsx Normal file
View 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,
};
}

View File

@@ -1,55 +1,58 @@
import axios from "axios"; import axios from 'axios';
import * as process from "process"; import * as process from 'process';
import {Local, Session} from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import {ElNotification} from "element-plus"; import { ElNotification } from 'element-plus';
import fs from "fs"; import fs from 'fs';
// 是否显示升级提示信息框 // 是否显示升级提示信息框
const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade'; const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade';
const VERSION_KEY = 'DVADMIN3_VERSION' const VERSION_KEY = 'DVADMIN3_VERSION';
const VERSION_FILE_NAME = 'version-build' const VERSION_FILE_NAME = 'version-build';
export function showUpgrade () { const META_ENV = import.meta.env;
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false
if (isShowUpgrade) { export function showUpgrade() {
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY) const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false;
ElNotification({ if (isShowUpgrade) {
title: '新版本升级', Session.remove(IS_SHOW_UPGRADE_SESSION_KEY);
message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!", ElNotification({
type: 'success', title: '新版本升级',
duration: 5000, message: '检测到系统新版本,正在更新中!不用担心,更新很快的哦!',
}); type: 'success',
} duration: 5000,
});
}
} }
// 生产环境前端版本校验, // 生产环境前端版本校验,
export async function checkVersion(){ export async function checkVersion() {
if (process.env.NODE_ENV === 'development') { if (META_ENV.MODE === 'development') {
// 开发环境无需校验前端版本 // 开发环境无需校验前端版本
return return;
} }
// 获取线上版本号 t为时间戳防止缓存 // 获取线上版本号 t为时间戳防止缓存
await axios.get(`${import.meta.env.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => { await axios.get(`${META_ENV.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then((res) => {
const {status, data} = res || {} const { status, data } = res || {};
if (status === 200) { 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) { if (localVersion && localVersion !== data) {
// 本地缓存版本号和线上版本号不一致,弹出升级提示框 // 本地缓存版本号和线上版本号不一致,弹出升级提示框
// 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框 // 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true) Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true);
window.location.reload() window.location.reload();
}
} }
} });
})
} }
export function generateVersionFile (){ export function generateVersionFile() {
// 生成版本文件到public目录下version文件中 // 生成版本文件到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;
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
const version = `${package_version}.${new Date().getTime()}`;
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
} }

View File

@@ -2,7 +2,7 @@ import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
export let pluginsAll: any = []; export let pluginsAll: any = [];
// 扫描插件目录并注册插件 // 扫描插件目录并注册插件
export const scanAndInstallPlugins = (app: any) => { export const scanAndInstallPlugins = (app: any) => {
const components = import.meta.glob('./**/*.vue'); const components = import.meta.glob('./**/*.ts');
const pluginNames = new Set(); const pluginNames = new Set();
// 遍历对象并注册异步组件 // 遍历对象并注册异步组件
for (const [key, value] of Object.entries(components)) { for (const [key, value] of Object.entries(components)) {
@@ -11,6 +11,15 @@ export const scanAndInstallPlugins = (app: any) => {
const pluginsName = key.match(/\/([^\/]*)\//)?.[1]; const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
pluginNames.add(pluginsName); 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); pluginsAll = Array.from(pluginNames);
console.log('已发现插件:', pluginsAll); console.log('已发现插件:', pluginsAll);
}; };

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

View File

@@ -175,48 +175,7 @@
</div> </div>
<!-- 数组 --> <!-- 数组 -->
<div v-else-if="item.form_item_type_label === 'array'" :key="index + 10"> <div v-else-if="item.form_item_type_label === 'array'" :key="index + 10">
<vxe-table <crudTable v-model="formData[item.key]"></crudTable>
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>
</div> </div>
</el-col> </el-col>
<el-col :span="2" :offset="1"> <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 { successMessage, errorMessage } from '/@/utils/message';
import { Session } from '/@/utils/storage'; import { Session } from '/@/utils/storage';
import {Edit,Finished,Delete} from "@element-plus/icons-vue"; import {Edit,Finished,Delete} from "@element-plus/icons-vue";
import crudTable from "./components/crudTable.vue"
const props = defineProps(['options', 'editableTabsItem']); const props = defineProps(['options', 'editableTabsItem']);
let formData: any = reactive({}); let formData: any = ref({});
let formList: 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>() const formRef = ref<FormInstance>()
let uploadUrl = ref(getBaseURL() + 'api/system/file/'); let uploadUrl = ref(getBaseURL() + 'api/system/file/');
let uploadHeaders = ref({ let uploadHeaders = ref({
@@ -294,65 +232,27 @@ const getInit = () => {
if (item.value) { if (item.value) {
_formData[key] = item.value; _formData[key] = item.value;
} else { } else {
if ([5, 12, 14].indexOf(item.form_item_type) !== -1) { if ([5, 12,11, 14].indexOf(item.form_item_type) !== -1) {
_formData[key] = []; _formData[key] = item.value || [];
} else { } else {
_formData[key] = item.value; _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 onSubmit = (formEl: FormInstance | undefined) => {
// const form = JSON.parse(JSON.stringify(form)); const keys = Object.keys(formData.value);
const keys = Object.keys(formData); const values = Object.values(formData.value);
const values = Object.values(formData);
for (const index in formList.value) { for (const index in formList.value) {
const item = formList.value[index]; 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 (mapKey === item.key) {
if (item.form_item_type_label !== 'array') { item.value = values[mapIndex];
item.value = values[mapIndex];
}
// 必填项的验证 // 必填项的验证
if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) { if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) {
for (const arr of item.rule) { 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) => { const handlePictureCardPreview = (file: any) => {

View File

@@ -2,7 +2,8 @@ import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from
import * as api from './api'; import * as api from './api';
import { dictionary } from '/@/utils/dictionary'; import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message'; import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction' import { auth } from '/@/utils/authFunction';
import { getBaseURL } from '/@/utils/baseUrl';
interface CreateCrudOptionsTypes { interface CreateCrudOptionsTypes {
output: any; output: any;
@@ -27,7 +28,6 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
//权限判定 //权限判定
// @ts-ignore
// @ts-ignore // @ts-ignore
return { return {
crudOptions: { crudOptions: {
@@ -72,7 +72,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: compute(ctx => ctx.row.task_status === 2), show: compute(ctx => ctx.row.task_status === 2),
text: '下载文件', text: '下载文件',
type: 'warning', type: 'warning',
click: (ctx) => window.open(ctx.row.url, '_blank') click: (ctx) => window.open(getBaseURL(ctx.row.url), '_blank')
} }
}, },
}, },

View File

@@ -1,253 +1,260 @@
import * as api from './api'; import * as api from './api';
import { import {
UserPageQuery, UserPageQuery,
AddReq, AddReq,
DelReq, DelReq,
EditReq, EditReq,
CrudExpose, CrudExpose,
CrudOptions, CrudOptions,
CreateCrudOptionsProps, CreateCrudOptionsProps,
CreateCrudOptionsRet, CreateCrudOptionsRet,
dict dict
} from '@fast-crud/fast-crud'; } from '@fast-crud/fast-crud';
import fileSelector from '/@/components/fileSelector/index.vue'; import fileSelector from '/@/components/fileSelector/index.vue';
import { shallowRef } from 'vue'; import { getBaseURL } from '/@/utils/baseUrl';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => { const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query); return await api.GetList(query);
}; };
const editRequest = async ({ form, row }: EditReq) => { const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id; form.id = row.id;
return await api.UpdateObj(form); return await api.UpdateObj(form);
}; };
const delRequest = async ({ row }: DelReq) => { const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id); return await api.DelObj(row.id);
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form); return await api.AddObj(form);
}; };
return { return {
crudOptions: { crudOptions: {
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
show: true, show: true,
click: () => context.openAddHandle?.() click: () => context.openAddHandle?.()
}, },
}, },
}, },
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest, delRequest,
}, },
tabs: { tabs: {
show: true, show: true,
name: 'file_type', name: 'file_type',
type: '', type: '',
options: [ options: [
{ value: 0, label: '图片' }, { value: 0, label: '图片' },
{ value: 1, label: '视频' }, { value: 1, label: '视频' },
{ value: 2, label: '音频' }, { value: 2, label: '音频' },
{ value: 3, label: '其他' }, { value: 3, label: '其他' },
] ]
}, },
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: 200, width: 200,
show: false, show: false,
buttons: { buttons: {
view: { view: {
show: false, show: false,
}, },
edit: { edit: {
iconRight: 'Edit', iconRight: 'Edit',
type: 'text', type: 'text',
}, },
remove: { remove: {
iconRight: 'Delete', iconRight: 'Delete',
type: 'text', type: 'text',
}, },
}, },
}, },
columns: { columns: {
_index: { _index: {
title: '序号', title: '序号',
form: { show: false }, form: { show: false },
column: { column: {
//type: 'index', //type: 'index',
align: 'center', align: 'center',
width: '70px', width: '70px',
columnSetDisabled: true, //禁止在列设置中选择 columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => { formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加 //计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1; let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination; let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
}, },
}, },
}, },
search: { search: {
title: '关键词', title: '关键词',
column: { column: {
show: false, show: false,
}, },
search: { search: {
show: true, show: true,
component: { component: {
props: { props: {
clearable: true, clearable: true,
}, },
placeholder: '请输入关键词', placeholder: '请输入关键词',
}, },
}, },
form: { form: {
show: false, show: false,
component: { component: {
props: { props: {
clearable: true, clearable: true,
}, },
}, },
}, },
}, },
name: { name: {
title: '文件名称', title: '文件名称',
search: { search: {
show: true, show: true,
}, },
type: 'input', type: 'input',
column: { column: {
minWidth: 200, minWidth: 200,
}, },
form: { form: {
component: { component: {
placeholder: '请输入文件名称', placeholder: '请输入文件名称',
clearable: true clearable: true
}, },
}, },
}, },
preview: { preview: {
title: '预览', title: '预览',
column: { column: {
minWidth: 120, minWidth: 120,
align: 'center' align: 'center'
}, },
form: { form: {
show: false show: false
} }
}, },
url: { url: {
title: '文件地址', title: '文件地址',
type: 'file-uploader', type: 'file-uploader',
search: { search: {
disabled: true, disabled: true,
}, },
column: { column: {
minWidth: 360, minWidth: 360,
}, component: {
}, async buildUrl(value: any) {
md5sum: { return getBaseURL(value);
title: '文件MD5', }
search: { }
disabled: true, },
}, },
column: { md5sum: {
minWidth: 300, title: '文件MD5',
}, search: {
form: { disabled: true,
disabled: false },
}, column: {
}, minWidth: 300,
mime_type: { },
title: '文件类型', form: {
type: 'input', disabled: false
form: { },
show: false, },
}, mime_type: {
column: { title: '文件类型',
minWidth: 160 type: 'input',
} form: {
}, show: false,
file_type: { },
title: '文件类型', column: {
type: 'dict-select', minWidth: 160
dict: dict({ }
data: [ },
{ label: '图片', value: 0, color: 'success' }, file_type: {
{ label: '视频', value: 1, color: 'warning' }, title: '文件类型',
{ label: '音频', value: 2, color: 'danger' }, type: 'dict-select',
{ label: '其他', value: 3, color: 'primary' }, dict: dict({
] data: [
}), { label: '图片', value: 0, color: 'success' },
column: { { label: '视频', value: 1, color: 'warning' },
show: false { label: '音频', value: 2, color: 'danger' },
}, { label: '其他', value: 3, color: 'primary' },
search: { ]
show: true }),
}, column: {
form: { show: false
show: false, },
component: { search: {
placeholder: '请选择文件类型' show: true
} },
} form: {
}, show: false,
size: { component: {
title: '文件大小', placeholder: '请选择文件类型'
column: { }
minWidth: 120 }
}, },
form: { size: {
show: false title: '文件大小',
} column: {
}, minWidth: 120
upload_method: { },
title: '上传方式', form: {
type: 'dict-select', show: false
dict: dict({ }
data: [ },
{ label: '默认上传', value: 0, color: 'primary' }, upload_method: {
{ label: '文件选择器上传', value: 1, color: 'warning' }, title: '上传方式',
] type: 'dict-select',
}), dict: dict({
column: { data: [
minWidth: 140 { label: '默认上传', value: 0, color: 'primary' },
}, { label: '文件选择器上传', value: 1, color: 'warning' },
search: { ]
show: true }),
} column: {
}, minWidth: 140
create_datetime: { },
title: '创建时间', search: {
column: { show: true
minWidth: 160 }
}, },
form: { create_datetime: {
show: false title: '创建时间',
} column: {
}, minWidth: 160
// fileselectortest: { },
// title: '文件选择器测试', form: {
// type: 'file-selector', show: false
// width: 200, }
// form: { },
// component: { // fileselectortest: {
// name: shallowRef(fileSelector), // title: '文件选择器测试',
// vModel: 'modelValue', // type: 'file-selector',
// tabsShow: 0b0100, // column: {
// itemSize: 100, // minWidth: 200
// multiple: false, // },
// selectable: true, // form: {
// showInput: true, // component: {
// inputType: 'video', // name: fileSelector,
// valueKey: 'url', // vModel: 'modelValue',
// } // tabsShow: 0b1111,
// } // itemSize: 100,
// } // multiple: true,
}, // selectable: true,
}, // showInput: true,
}; // inputType: 'image',
// valueKey: 'url',
// }
// }
// }
},
},
};
}; };

View File

@@ -1,9 +1,39 @@
<template> <template>
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0"> <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)"> <el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
<div class="btn-item"> <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)"> <span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
<el-icon> <el-icon>
<Setting /> <Setting />
@@ -48,10 +78,26 @@ import { RoleMenuBtnType } from '../types';
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api'; import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { Local } from '/@/utils/storage';
const RoleDrawer = RoleDrawerStores(); // 角色-菜单 const RoleDrawer = RoleDrawerStores(); // 角色-菜单
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单 const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const dialogVisible = ref(false); 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>({ const selectBtn = ref<RoleMenuBtnType>({
id: 0, id: 0,
@@ -83,6 +129,29 @@ const defaultTreeProps = {
value: 'id', 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(() => { const formatDataRange = computed(() => {
return function (datarange: number) { return function (datarange: number, dept: Array<number>) {
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => { const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
if (item.value === datarange) { if (item.value === datarange) {
return item.label; 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; return datarangeitem.label;
}; };
}); });
@@ -108,11 +186,14 @@ const formatDataRange = computed(() => {
* 勾选按钮 * 勾选按钮
*/ */
const handleCheckChange = async (btn: RoleMenuBtnType) => { const handleCheckChange = async (btn: RoleMenuBtnType) => {
selectBtn.value = default_selectBtn.value;
const put_data = { const put_data = {
isCheck: btn.isCheck, isCheck: btn.isCheck,
roleId: RoleDrawer.roleId, roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id, menuId: RoleMenuTree.id,
btnId: btn.id, btnId: btn.id,
data_range: default_selectBtn.value.data_range,
dept: default_selectBtn.value.dept,
}; };
const { data, msg } = await setRoleMenuBtn(put_data); const { data, msg } = await setRoleMenuBtn(put_data);
RoleMenuBtn.updateState(data); RoleMenuBtn.updateState(data);
@@ -168,9 +249,10 @@ onMounted(async () => {
background-color: var(--el-color-primary); background-color: var(--el-color-primary);
} }
} }
// .el-checkbox {
// width: 200px; .el-checkbox {
// } width: 20%;
}
.btn-item { .btn-item {
display: flex; display: flex;
align-items: center; align-items: center;

View 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},
});
}

View 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'
}),
},
},
},
};
};

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

View 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},
});
}

View 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'
}),
},
},
},
};
};

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

View File

@@ -3,6 +3,7 @@ import * as api from './api';
import { dictionary } from '/@/utils/dictionary'; import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message'; import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction'; import { auth } from '/@/utils/authFunction';
import { nextTick, computed } from 'vue';
/** /**
* *
@@ -46,7 +47,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: 320, width: computed(() => {
if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){
return 420;
}
return 320;
}),
buttons: { buttons: {
view: { view: {
show: true, show: true,
@@ -57,6 +63,19 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
remove: { remove: {
show: auth('role:Delete'), 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: { permission: {
type: 'primary', type: 'primary',
text: '权限配置', text: '权限配置',

View File

@@ -2,17 +2,22 @@
<fs-page> <fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud> <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<PermissionDrawerCom /> <PermissionDrawerCom />
<RoleUser ref="RoleUserRef" />
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup name="role"> <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 { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud'; import { createCrudOptions } from './crud';
import { RoleDrawerStores } from './stores/RoleDrawerStores'; import { RoleDrawerStores } from './stores/RoleDrawerStores';
import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores'; import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores'; import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
import { RoleUsersStores } from './stores/RoleUsersStores'; 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')); const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
@@ -20,9 +25,11 @@ const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段 const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
const RoleUsers = RoleUsersStores();// 角色-用户 const RoleUsers = RoleUsersStores();// 角色-用户
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
const { crudBinding, crudRef, crudExpose } = useFs({ const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions, createCrudOptions,
context: { RoleDrawer, RoleMenuBtn, RoleMenuField }, context: { RoleDrawer, RoleMenuBtn, RoleMenuField, RoleUserDrawer, RoleUserRef },
}); });
// 页面打开后获取列表数据 // 页面打开后获取列表数据

View 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;
},
},
});

View 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'
})
}

View 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
},
},
};
}

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

View File

@@ -11,6 +11,7 @@ const pathResolve = (dir: string) => {
const alias: Record<string, string> = { const alias: Record<string, string> = {
'/@': pathResolve('./src/'), '/@': pathResolve('./src/'),
'@great-dream': pathResolve('./node_modules/@great-dream/'),
'@views': pathResolve('./src/views'), '@views': pathResolve('./src/views'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
'@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/') '@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/')