Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
猿小天
2025-06-01 19:55:05 +08:00
127 changed files with 7141 additions and 2915 deletions

View File

@@ -88,7 +88,19 @@ github地址[https://github.com/huge-dream/django-vue3-admin](https://github.
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌
更新中...
1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html)
2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html)
3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html)
4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html)
5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git)
6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线)
## 仓库分支说明 💈
主分支master稳定版本
@@ -114,7 +126,7 @@ cd web
# 安装依赖
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
yarn install --registry=https://registry.npmmirror.com
# 启动服务
yarn build
@@ -210,5 +222,19 @@ docker-compose up -d --build
![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)

View File

@@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

View File

@@ -1,6 +1,8 @@
import functools
import os
from celery.signals import task_postrun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings
@@ -15,7 +17,7 @@ else:
from celery import Celery
app = Celery(f"application")
app.config_from_object('django.conf:settings')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
platforms.C_FORCE_ROOT = True
@@ -38,3 +40,12 @@ def retry_base_task_error():
return wrapper
return wraps
@task_postrun.connect
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
periodic_task_name = kwargs.get('periodic_task_name')
if periodic_task_name:
from django_celery_results.models import TaskResult
# 更新 TaskResult 表中的 periodic_task_name 字段
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)

View File

@@ -2,6 +2,10 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.db import connection
from django.core.cache import cache
from dvadmin.utils.validator import CustomValidationError
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
def is_tenants_mode():
@@ -68,6 +72,9 @@ def init_dictionary():
:return:
"""
try:
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -88,7 +95,9 @@ def init_system_config():
:return:
"""
try:
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -107,6 +116,9 @@ def refresh_dictionary():
刷新字典配置
:return:
"""
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -122,6 +134,9 @@ def refresh_system_config():
刷新系统配置
:return:
"""
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
init_dictionary_data = cache.get(f"init_dictionary")
if not init_dictionary_data:
refresh_dictionary()
return cache.get(f"init_dictionary") or {}
if not settings.DICTIONARY_CONFIG:
refresh_dictionary()
if is_tenants_mode():
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
dictionary_config = cache.get(f"init_dictionary")
if not dictionary_config:
refresh_dictionary()
dictionary_config = cache.get(f"init_dictionary")
return dictionary_config.get(key)
dictionary_config = get_dictionary_config(schema_name)
return dictionary_config.get(key)
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
children = get_dictionary_values(key, schema_name) or []
for ele in children:
res = get_dictionary_values(key, schema_name) or []
for ele in res.get('children'):
if ele.get("value") == str(name):
return ele.get("label")
return ""
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
init_dictionary_data = cache.get(f"init_system_config")
if not init_dictionary_data:
refresh_system_config()
return cache.get(f"init_system_config") or {}
if not settings.SYSTEM_CONFIG:
refresh_system_config()
if is_tenants_mode():
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
:param schema_name: 对应系统配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
system_config = cache.get(f"init_system_config")
if not system_config:
refresh_system_config()
system_config = cache.get(f"init_system_config")
return system_config.get(key)
system_config = get_system_config(schema_name)
return system_config.get(key)
def get_system_config_values_to_dict(key, schema_name=None):
"""
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
:param key: 对应系统配置的key值(字典编号)
:param schema_name: 对应系统配置的租户schema_name值
:return:
"""
values_dict = {}
config_values = get_system_config_values(key, schema_name)
if not isinstance(config_values, list):
raise CustomValidationError("该方式仅限于数组类型系统配置")
for ele in get_system_config_values(key, schema_name):
values_dict[ele.get('key')] = ele.get('value')
return values_dict
def get_system_config_label(key, name, schema_name=None):
"""
获取获取系统配置label值

View File

@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
# ********** 一键导入插件配置开始 **********
# 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心
# from dvadmin_celery.settings import * # celery 异步任务
from dvadmin3_celery.settings import * # celery 异步任务
# from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理

View File

@@ -50,7 +50,7 @@ schema_view = get_schema_view(
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
permission_classes=(permissions.IsAuthenticated,),
generator_class=CustomOpenAPISchemaGenerator,
)
# 前端页面映射

View File

@@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
role_key = serializers.SerializerMethodField()
dept_key = serializers.SerializerMethodField()
def get_dept_key(self, obj):
if obj.dept:
return obj.dept.key
else:
return None
def get_role_key(self, obj):
if obj.role.all():
return [role.key for role in obj.role.all()]
else:
return []
def save(self, **kwargs):
instance = super().save(**kwargs)
@@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer):
model = Users
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
'password', 'last_login', 'is_superuser']
'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key']
read_only_fields = ['id']
extra_kwargs = {
'creator': {'write_only': True},
@@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer):
"""
初始化角色菜单(用于生成初始化json文件)
"""
role__key = serializers.CharField(max_length=100, required=True)
menu__web_path = serializers.CharField(max_length=100, required=True)
menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True)
role__key = serializers.CharField(source='role.key')
menu__web_path = serializers.CharField(source='menu.web_path')
menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True)
def update(self, instance, validated_data):
init_data = self.initial_data
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
validated_data['role'] = role_id
validated_data['menu'] = menu_id
return super().update(instance, validated_data)
def create(self, validated_data):
init_data = self.initial_data
validated_data.pop('menu__web_path')
validated_data.pop('menu__component_name')
validated_data.pop('role__key')
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
validated_data['role'] = role_id
@@ -206,14 +226,22 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
"""
初始化角色菜单按钮(用于生成初始化json文件)
"""
role__key = serializers.CharField(max_length=100, required=True)
menu_button__value = serializers.CharField(max_length=100, required=True)
role__key = serializers.CharField(source='role.key')
menu_button__value = serializers.CharField(source='menu_button.value')
data_range = serializers.CharField(max_length=100, required=False)
def update(self, instance, validated_data):
init_data = self.initial_data
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id
validated_data['menu_button'] = menu_button_id
instance = super().create(validated_data)
instance.dept.set([])
return super().update(instance, validated_data)
def create(self, validated_data):
init_data = self.initial_data
validated_data.pop('menu_button__value')
validated_data.pop('role__key')
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id
@@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
return instance
def save(self, **kwargs):
if self.instance and self.initial_data.get('reset'):
if not self.instance or self.initial_data.get('reset'):
return super().save(**kwargs)
return self.instance

View File

@@ -672,6 +672,53 @@
"model": "ApiWhiteList"
}
]
},
{
"name": "下载中心",
"icon": "ele-Download",
"sort": 9,
"is_link": false,
"is_catalog": false,
"web_path": "/downloadCenter",
"component": "system/downloadCenter/index",
"component_name": "downloadCenter",
"status": true,
"cache": false,
"visible": true,
"parent": 277,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "Search",
"api": "/api/system/downloadCenter/",
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/downloadCenter/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/downloadCenter/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/downloadCenter/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/downloadCenter/{id}/",
"method": 3
}
]
}
],
"menu_button": [],

View File

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

View File

@@ -1,5 +1,7 @@
import hashlib
import os
from time import time
from pathlib import PurePosixPath
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
@@ -7,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from application import dispatch
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
class Role(CoreModel):
from dvadmin3_flow.base_model import FlowBaseModel
class Role(CoreModel,FlowBaseModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
@@ -71,6 +73,7 @@ class Users(CoreModel, AbstractUser):
help_text="关联部门",
)
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
objects = CustomUserManager()
def set_password(self, raw_password):
@@ -119,6 +122,27 @@ class Dept(CoreModel):
help_text="上级部门",
)
@classmethod
def _recursion(cls, instance, parent, result):
new_instance = getattr(instance, parent, None)
res = []
data = getattr(instance, result, None)
if data:
res.append(data)
if new_instance:
array = cls._recursion(new_instance, parent, result)
res += array
return res
@classmethod
def get_region_name(cls, obj):
"""
获取某个用户的递归所有部门名称
"""
dept_name_all = cls._recursion(obj, "parent", "name")
dept_name_all.reverse()
return "/".join(dept_name_all)
@classmethod
def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
"""
@@ -405,6 +429,18 @@ class FileList(CoreModel):
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
UPLOAD_METHOD_CHOIDES = (
(0, '默认上传'),
(1, '文件选择器上传'),
)
upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式')
FILE_TYPE_CHOIDES = (
(0, '图片'),
(1, '视频'),
(2, '音频'),
(3, '其他'),
)
file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型')
def save(self, *args, **kwargs):
if not self.md5sum: # file is new
@@ -595,3 +631,41 @@ class MessageCenterTargetUser(CoreModel):
db_table = table_prefix + "message_center_target_user"
verbose_name = "消息中心目标用户表"
verbose_name_plural = verbose_name
def media_file_name_downloadcenter(instance:'DownloadCenter', filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower())
class DownloadCenter(CoreModel):
TASK_STATUS_CHOICES = [
(0, '任务已创建'),
(1, '任务进行中'),
(2, '任务完成'),
(3, '任务失败'),
]
task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称")
task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载')
file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名")
url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True)
size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5")
def save(self, *args, **kwargs):
if self.url:
if not self.md5sum: # file is new
md5 = hashlib.md5()
for chunk in self.url.chunks():
md5.update(chunk)
self.md5sum = md5.hexdigest()
if not self.size:
self.size = self.url.size
super(DownloadCenter, self).save(*args, **kwargs)
class Meta:
db_table = table_prefix + "download_center"
verbose_name = "下载中心"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)

View File

@@ -0,0 +1,12 @@
from django.dispatch import Signal
# 初始化信号
pre_init_complete = Signal()
detail_init_complete = Signal()
post_init_complete = Signal()
# 租户初始化信号
pre_tenants_init_complete = Signal()
detail_tenants_init_complete = Signal()
post_tenants_init_complete = Signal()
post_tenants_all_init_complete = Signal()
# 租户创建完成信号
tenants_create_complete = Signal()

View File

@@ -0,0 +1,107 @@
from hashlib import md5
from io import BytesIO
from datetime import datetime
from time import sleep
from openpyxl import Workbook
from openpyxl.worksheet.table import Table, TableStyleInfo
from openpyxl.utils import get_column_letter
from django.core.files.base import ContentFile
from application.celery import app
from dvadmin.system.models import DownloadCenter
def is_number(num):
try:
float(num)
return True
except ValueError:
pass
try:
import unicodedata
unicodedata.numeric(num)
return True
except (TypeError, ValueError):
pass
return False
def get_string_len(string):
"""
获取字符串最大长度
:param string:
:return:
"""
length = 4
if string is None:
return length
if is_number(string):
return length
for char in string:
length += 2.1 if ord(char) > 256 else 1
return round(length, 1) if length <= 50 else 50
@app.task
def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict):
instance = DownloadCenter.objects.get(pk=dcid)
instance.task_status = 1
instance.save()
sleep(2)
try:
wb = Workbook()
ws = wb.active
header_data = ["序号", *export_field_label.values()]
hidden_header = ["#", *export_field_label.keys()]
df_len_max = [get_string_len(ele) for ele in header_data]
row = get_column_letter(len(export_field_label) + 1)
column = 1
ws.append(header_data)
for index, results in enumerate(data):
results_list = []
for h_index, h_item in enumerate(hidden_header):
for key, val in results.items():
if key == h_item:
if val is None or val == "":
results_list.append("")
elif isinstance(val, datetime):
val = val.strftime("%Y-%m-%d %H:%M:%S")
results_list.append(val)
else:
results_list.append(val)
# 计算最大列宽度
result_column_width = get_string_len(val)
if h_index != 0 and result_column_width > df_len_max[h_index]:
df_len_max[h_index] = result_column_width
ws.append([index + 1, *results_list])
column += 1
#  更新列宽
for index, width in enumerate(df_len_max):
ws.column_dimensions[get_column_letter(index + 1)].width = width
tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器
style = TableStyleInfo(
name="TableStyleLight11",
showFirstColumn=True,
showLastColumn=True,
showRowStripes=True,
showColumnStripes=True,
)
tab.tableStyleInfo = style
ws.add_table(tab)
stream = BytesIO()
wb.save(stream)
stream.seek(0)
s = md5()
while True:
chunk = stream.read(1024)
if not chunk:
break
s.update(chunk)
stream.seek(0)
instance.md5sum = s.hexdigest()
instance.file_name = filename
instance.url.save(filename, ContentFile(stream.read()))
instance.task_status = 2
except Exception as e:
instance.task_status = 3
instance.description = str(e)[:250]
instance.save()

View File

@@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi
from dvadmin.system.views.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet
from dvadmin.system.views.menu_field import MenuFieldViewSet
from dvadmin.system.views.download_center import DownloadCenterViewSet
system_url = routers.SimpleRouter()
system_url.register(r'menu', MenuViewSet)
@@ -36,6 +37,7 @@ system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionView
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', MenuFieldViewSet)
system_url.register(r'login_log', LoginLogViewSet)
system_url.register(r'download_center', DownloadCenterViewSet)
urlpatterns = [
@@ -47,7 +49,7 @@ urlpatterns = [
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
# path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
path('clause/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()),
]

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import pypinyin
from django.db.models import Q
from rest_framework import serializers
@@ -15,6 +16,11 @@ class AreaSerializer(CustomModelSerializer):
"""
pcode_count = serializers.SerializerMethodField(read_only=True)
hasChild = serializers.SerializerMethodField()
pcode_info = serializers.SerializerMethodField()
def get_pcode_info(self, instance):
pcode = Area.objects.filter(code=instance.pcode_id).values("name", "code")
return pcode
def get_pcode_count(self, instance: Area):
return Area.objects.filter(pcode=instance).count()
@@ -36,6 +42,18 @@ class AreaCreateUpdateSerializer(CustomModelSerializer):
地区管理 创建/更新时的列化器
"""
def to_internal_value(self, data):
pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(data["name"], style=pypinyin.NORMAL)])
data["level"] = 1
data["pinyin"] = pinyin
data["initials"] = pinyin[0].upper() if pinyin else "#"
pcode = data["pcode"] if 'pcode' in data else None
if pcode:
pcode = Area.objects.get(pk=pcode)
data["pcode"] = pcode.code
data["level"] = pcode.level + 1
return super().to_internal_value(data)
class Meta:
model = Area
fields = '__all__'
@@ -52,20 +70,28 @@ class AreaViewSet(CustomModelViewSet, FieldPermissionMixin):
"""
queryset = Area.objects.all()
serializer_class = AreaSerializer
create_serializer_class = AreaCreateUpdateSerializer
update_serializer_class = AreaCreateUpdateSerializer
extra_filter_class = []
def get_queryset(self):
def list(self, request, *args, **kwargs):
self.request.query_params._mutable = True
params = self.request.query_params
known_params = {'page', 'limit', 'pcode'}
# 使用集合操作检查是否有未知参数
other_params_exist = any(param not in known_params for param in params)
if other_params_exist:
queryset = self.queryset.filter(enable=True)
else:
pcode = params.get('pcode', None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
params['limit'] = 999
if params and pcode:
queryset = self.queryset.filter(enable=True, pcode=pcode)
else:
queryset = self.queryset.filter(enable=True)
return queryset
queryset = self.queryset.filter(enable=True, level=1)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")

View File

@@ -0,0 +1,49 @@
from rest_framework import serializers
from django.conf import settings
from django_filters.rest_framework import FilterSet, CharFilter
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.system.models import DownloadCenter
class DownloadCenterSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
if self.request.query_params.get('prefix'):
if settings.ENVIRONMENT in ['local']:
prefix = 'http://127.0.0.1:8000'
elif settings.ENVIRONMENT in ['test']:
prefix = 'http://{host}/api'.format(host=self.request.get_host())
else:
prefix = 'https://{host}/api'.format(host=self.request.get_host())
return (f'{prefix}/media/{str(instance.url)}')
return f'media/{str(instance.url)}'
class Meta:
model = DownloadCenter
fields = "__all__"
read_only_fields = ["id"]
class DownloadCenterFilterSet(FilterSet):
task_name = CharFilter(field_name='task_name', lookup_expr='icontains')
file_name = CharFilter(field_name='file_name', lookup_expr='icontains')
class Meta:
model = DownloadCenter
fields = ['task_status', 'task_name', 'file_name']
class DownloadCenterViewSet(CustomModelViewSet):
queryset = DownloadCenter.objects.all()
serializer_class = DownloadCenterSerializer
filter_class = DownloadCenterFilterSet
permission_classes = []
extra_filter_class = []
def get_queryset(self):
if self.request.user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(creator=self.request.user)

View File

@@ -1,12 +1,15 @@
import hashlib
import mimetypes
import django_filters
from django.conf import settings
from django.db import connection
from rest_framework import serializers
from rest_framework.decorators import action
from application import dispatch
from dvadmin.system.models import FileList
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -15,8 +18,17 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
base_url = f"{self.request.scheme}://{self.request.get_host()}/"
return base_url + (instance.file_url or (f'media/{str(instance.url)}'))
if self.request.query_params.get('prefix'):
if settings.ENVIRONMENT in ['local']:
prefix = 'http://127.0.0.1:8000'
elif settings.ENVIRONMENT in ['test']:
prefix = 'http://{host}/api'.format(host=self.request.get_host())
else:
prefix = 'https://{host}/api'.format(host=self.request.get_host())
if instance.file_url:
return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}"
return (f'{prefix}/media/{str(instance.url)}')
return instance.file_url or (f'media/{str(instance.url)}')
class Meta:
model = FileList
@@ -35,6 +47,8 @@ class FileSerializer(CustomModelSerializer):
validated_data['md5sum'] = md5.hexdigest()
validated_data['engine'] = file_engine
validated_data['mime_type'] = file.content_type
ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None)
validated_data['file_type'] = 3 if ft is None else ft
if file_backup:
validated_data['url'] = file
if file_engine == 'oss':
@@ -64,6 +78,22 @@ class FileSerializer(CustomModelSerializer):
return super().create(validated_data)
class FileAllSerializer(CustomModelSerializer):
class Meta:
model = FileList
fields = ['id', 'name']
class FileFilter(django_filters.FilterSet):
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名")
mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型")
class Meta:
model = FileList
fields = ['name', 'mime_type', 'upload_method', 'file_type']
class FileViewSet(CustomModelViewSet):
"""
文件管理接口
@@ -75,5 +105,22 @@ class FileViewSet(CustomModelViewSet):
"""
queryset = FileList.objects.all()
serializer_class = FileSerializer
filter_fields = ['name', ]
filter_class = FileFilter
permission_classes = []
@action(methods=['GET'], detail=False)
def get_all(self, request):
data1 = self.get_serializer(self.get_queryset(), many=True).data
data2 = []
if dispatch.is_tenants_mode():
from django_tenants.utils import schema_context
with schema_context('public'):
data2 = self.get_serializer(FileList.objects.all(), many=True).data
return DetailResponse(data=data2+data1)
def list(self, request, *args, **kwargs):
if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode():
from django_tenants.utils import schema_context
with schema_context('public'):
return super().list(request, *args, **kwargs)
return super().list(request, *args, **kwargs)

View File

@@ -4,12 +4,15 @@ from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image
from django.contrib import auth
from django.contrib.auth import login
from django.contrib.auth.hashers import check_password, make_password
from django.db.models import Q
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
@@ -97,16 +100,17 @@ class LoginSerializer(TokenObtainPairSerializer):
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
attrs['username'] = user.username
data = super().validate(attrs)
data["username"] = self.user.username
data["name"] = self.user.name
data["userId"] = self.user.id
data["avatar"] = self.user.avatar
data['user_type'] = self.user.user_type
data['pwd_change_count'] = self.user.pwd_change_count
dept = getattr(self.user, 'dept', None)
if dept:
data['dept_info'] = {
'dept_id': dept.id,
'dept_name': dept.name,
}
role = getattr(self.user, 'role', None)
if role:
@@ -124,6 +128,7 @@ class LoginSerializer(TokenObtainPairSerializer):
user.is_active = False
user.save()
raise CustomValidationError("账号已被锁定,联系管理员解锁")
user.save()
count = 5 - user.login_error_count
raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~")

View File

@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
"""用于前端获取当前角色的路由"""
user = request.user
if user.is_superuser:
queryset = self.queryset.filter(status=1)
queryset = self.queryset.filter(status=1).order_by("sort")
else:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
queryset = Menu.objects.filter(id__in=menu_list)
queryset = Menu.objects.filter(id__in=menu_list).order_by("sort")
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")

View File

@@ -36,6 +36,8 @@ class MessageCenterSerializer(CustomModelSerializer):
return serializer.data
def get_user_info(self, instance, parsed_query):
if instance.target_type in (1,2,3):
return []
users = instance.target_user.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
@@ -106,6 +108,8 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
return serializer.data
def get_user_info(self, instance, parsed_query):
if instance.target_type in (1,2,3):
return []
users = instance.target_user.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying

View File

@@ -10,22 +10,29 @@ from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept
from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer):
"""
角色-序列化器
"""
users = serializers.SerializerMethodField()
@staticmethod
def get_users(instance):
users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name')
return users
class Meta:
model = Role
@@ -101,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
"""
角色管理接口
@@ -116,3 +122,82 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
create_serializer_class = RoleCreateUpdateSerializer
update_serializer_class = RoleCreateUpdateSerializer
search_fields = ['name', 'key']
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_users(self, request, pk):
"""
设置 角色-用户
:param request:
:return:
"""
data = request.data
direction = data.get('direction')
movedKeys = data.get('movedKeys')
role = Role.objects.get(pk=pk)
if direction == "left":
# left : 移除用户权限
role.users_set.remove(*movedKeys)
else:
# right : 添加用户权限
role.users_set.add(*movedKeys)
serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
def get_role_users(self, request):
"""
获取角色已授权、未授权的用户
已授权的用户:1
未授权的用户:0
"""
role_id = request.query_params.get('role_id', None)
if not role_id:
return ErrorResponse(msg="请选择角色")
if request.query_params.get('authorized', 0) == "1":
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
else:
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
if name := request.query_params.get('name', None):
queryset = queryset.filter(name__icontains=name)
if dept := request.query_params.get('dept', None):
queryset = queryset.filter(dept=dept)
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
if page is not None:
return self.get_paginated_response(page)
return SuccessResponse(data=page)
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def remove_role_user(self, request, pk):
"""
角色-删除用户
"""
user_id = request.data.get('user_id', None)
if not user_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.remove(*user_id)
return SuccessResponse(msg="删除成功")
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def add_role_users(self, request, pk):
"""
角色-添加用户
"""
users_id = request.data.get('users_id', None)
if not users_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.add(*users_id)
return DetailResponse(msg="添加成功")

View File

@@ -6,24 +6,20 @@
@Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理
"""
from django.db.models import F, Subquery, OuterRef, Exists, BooleanField, Q, Case, Value, When
from django.db.models.functions import Coalesce
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.fields import ListField
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \
MenuField
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \
MenuField, FieldPermission
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
"""
菜单按钮-序列化
角色-菜单-按钮-权限 查询序列化
"""
class Meta:
@@ -34,7 +30,7 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化
角色-菜单-按钮-权限 创建/修改序列化
"""
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
@@ -45,63 +41,99 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class RoleButtonPermissionSerializer(CustomModelSerializer):
class RoleMenuSerializer(CustomModelSerializer):
"""
角色按钮权限
角色-菜单 序列化
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
data = self.request.data
return RoleMenuPermission.objects.filter(
menu_id=instance.id,
role_id=params.get('roleId', data.get('roleId')),
).exists()
class Meta:
model = Menu
fields = ["id", "name", "parent", "is_catalog", "isCheck"]
class RoleMenuButtonSerializer(CustomModelSerializer):
"""
角色-菜单-按钮 序列化
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
role_menu_btn_perm_id = serializers.SerializerMethodField()
dept = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
data = self.request.data
return RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
menu_button_id=instance.id,
role_id=params.get('roleId', data.get('roleId')),
).exists()
def get_data_range(self, instance):
params = self.request.query_params
obj = RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).first()
obj = self.get_role_menu_btn_prem(instance)
if obj is None:
return None
return obj.data_range
def get_role_menu_btn_perm_id(self, instance):
obj = self.get_role_menu_btn_prem(instance)
if obj is None:
return None
return obj.id
def get_dept(self, instance):
obj = self.get_role_menu_btn_prem(instance)
if obj is None:
return None
return obj.dept.all().values_list('id', flat=True)
def get_role_menu_btn_prem(self, instance):
params = self.request.query_params
data = self.request.data
obj = RoleMenuButtonPermission.objects.filter(
menu_button_id=instance.id,
role_id=params.get('roleId', data.get('roleId')),
).first()
return obj
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'isCheck', 'data_range']
class RoleFieldPermissionSerializer(CustomModelSerializer):
class Meta:
model = FieldPermission
fields = "__all__"
fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept']
class RoleMenuFieldSerializer(CustomModelSerializer):
"""
角色-菜单-字段 序列化
"""
is_query = serializers.SerializerMethodField()
is_create = serializers.SerializerMethodField()
is_update = serializers.SerializerMethodField()
def get_is_query(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
if queryset:
return queryset.is_query
return False
def get_is_create(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
if queryset:
return queryset.is_create
return False
def get_is_update(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
if queryset:
return queryset.is_update
return False
@@ -111,54 +143,6 @@ class RoleMenuFieldSerializer(CustomModelSerializer):
fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update']
class RoleMenuSerializer(CustomModelSerializer):
menus = serializers.SerializerMethodField()
def get_menus(self, instance):
menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name')
serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id', 'name', 'menus']
class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
# name = serializers.SerializerMethodField()
isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
# def get_name(self, instance):
# parent_list = Menu.get_all_parent(instance['id'])
# names = [d["name"] for d in parent_list]
# return "/".join(names)
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuPermission.objects.filter(
menu__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_btns(self, instance):
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
serializer = RoleButtonPermissionSerializer(btn_list, many=True, request=self.request)
return serializer.data
def get_columns(self, instance):
col_list = MenuField.objects.filter(menu=instance['id'])
serializer = RoleMenuFieldSerializer(col_list, many=True, request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id', 'name', 'isCheck', 'btns', 'columns']
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
"""
菜单按钮接口
@@ -174,202 +158,111 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer
extra_filter_class = []
# @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
# def get_role_premission(self, request):
# """
# 角色授权获取:
# :param request: role
# :return: menu,btns,columns
# """
# params = request.query_params
# is_superuser = request.user.is_superuser
# if is_superuser:
# queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all()
# else:
# role_id = request.user.role.values_list('id', flat=True)
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('menu__id', flat=True)
# queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all()
# serializer = RoleMenuSerializer(queryset, many=True, request=request)
# data = serializer.data
# return DetailResponse(data=data)
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_role_menu(self, request):
"""
获取 角色-菜单
:param request:
:return:
"""
menu_queryset = Menu.objects.all()
serializer = RoleMenuSerializer(menu_queryset, many=True, request=request)
return DetailResponse(data=serializer.data)
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu(self, request):
"""
设置 角色-菜单
:param request:
:return:
"""
data = request.data
roleId = data.get('roleId')
menuId = data.get('menuId')
isCheck = data.get('isCheck')
if isCheck:
# 添加权限:创建关联记录
instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId)
else:
# 删除权限:移除关联记录
RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete()
menu_instance = Menu.objects.get(id=menuId)
serializer = RoleMenuSerializer(menu_instance, request=request)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_role_permission(self, request):
def get_role_menu_btn_field(self, request):
"""
获取 角色-菜单-按钮-列字段
:param request:
:return:
"""
params = request.query_params
# 需要授权的角色信息
current_role = params.get('role', None)
# 当前登录用户的角色
role_list = request.user.role.values_list('id', flat=True)
if current_role is None:
return ErrorResponse(msg='参数错误')
is_superuser = request.user.is_superuser
if is_superuser:
menu_queryset = Menu.objects.prefetch_related('menuPermission').prefetch_related(
'menufield_set')
else:
role_id_list = request.user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_id_list).values_list('menu__id', flat=True)
# 当前角色已授权的菜单
menu_queryset = Menu.objects.filter(id__in=menu_list).prefetch_related('menuPermission').prefetch_related(
'menufield_set')
result = []
for menu_item in menu_queryset:
isCheck = RoleMenuPermission.objects.filter(
menu_id=menu_item.id,
role_id=current_role
).exists()
dicts = {
'name': menu_item.name,
'id': menu_item.id,
'parent': menu_item.parent.id if menu_item.parent else None,
'isCheck': isCheck,
'btns': [],
'columns': []
}
for mb_item in menu_item.menuPermission.all():
rolemenubuttonpermission_queryset = RoleMenuButtonPermission.objects.filter(
menu_button_id=mb_item.id,
role_id=current_role
).first()
dicts['btns'].append(
{
'id': mb_item.id,
'name': mb_item.name,
'value': mb_item.value,
'data_range': rolemenubuttonpermission_queryset.data_range
if rolemenubuttonpermission_queryset
else None,
'isCheck': bool(rolemenubuttonpermission_queryset),
'dept': rolemenubuttonpermission_queryset.dept.all().values_list('id', flat=True)
if rolemenubuttonpermission_queryset
else [],
}
)
for column_item in menu_item.menufield_set.all():
# 需要授权角色已拥有的列权限
fieldpermission_queryset = column_item.menu_field.filter(role_id=current_role).first()
is_query = fieldpermission_queryset.is_query if fieldpermission_queryset else False
is_create = fieldpermission_queryset.is_create if fieldpermission_queryset else False
is_update = fieldpermission_queryset.is_update if fieldpermission_queryset else False
# 当前登录用户角色可分配的列权限
fieldpermission_queryset_disabled = column_item.menu_field.filter(role_id__in=role_list).first()
disabled_query = fieldpermission_queryset_disabled.is_query if fieldpermission_queryset_disabled else True
disabled_create = fieldpermission_queryset_disabled.is_create if fieldpermission_queryset_disabled else True
disabled_update = fieldpermission_queryset_disabled.is_update if fieldpermission_queryset_disabled else True
dicts['columns'].append({
'id': column_item.id,
'field_name': column_item.field_name,
'title': column_item.title,
'is_query': is_query,
'is_create': is_create,
'is_update': is_update,
'disabled_query': False if is_superuser else not disabled_query,
'disabled_create': False if is_superuser else not disabled_create,
'disabled_update': False if is_superuser else not disabled_update,
})
result.append(dicts)
return DetailResponse(data=result)
menuId = params.get('menuId', None)
menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId)
menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request)
menu_field_queryset = MenuField.objects.filter(menu_id=menuId)
menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request)
return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data})
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_premission(self, request, pk):
def set_role_menu_field(self, request, pk):
"""
角色菜单和按钮及按钮范围授权:
:param request:
:param pk: role
:return:
设置 角色-菜单-列字段
"""
body = request.data
RoleMenuPermission.objects.filter(role=pk).delete()
RoleMenuButtonPermission.objects.filter(role=pk).delete()
for item in body:
if item.get('isCheck'):
RoleMenuPermission.objects.create(role_id=pk, menu_id=item["id"])
for btn in item.get('btns'):
if btn.get('isCheck'):
data_range = btn.get('data_range', 0) or 0
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),
data_range=data_range)
instance.dept.set(btn.get('dept', []))
for col in item.get('columns'):
FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'),
data = request.data
for col in data:
FieldPermission.objects.update_or_create(
role_id=pk, field_id=col.get('id'),
defaults={
'is_query': col.get('is_query'),
'is_create': col.get('is_create'),
'is_update': col.get('is_update')
'is_update': col.get('is_update'),
'is_query': col.get('is_query'),
})
return DetailResponse(msg="授权成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_menu_get_button(self, request):
"""
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
:param request:
:return:
"""
if params := request.query_params:
if menu_id := params.get('menu', None):
is_superuser = request.user.is_superuser
if is_superuser:
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
else:
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__menu=menu_id
).values(btn_id=F('menu_button__id'), name=F('menu_button__name'))
return DetailResponse(data=queryset)
return ErrorResponse(msg="参数错误")
return DetailResponse(data=[], msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request):
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu_btn(self, request):
"""
获取数据权限范围:角色授权页面使用
:param request:
:return:
设置 角色-菜单-按钮
"""
is_superuser = request.user.is_superuser
if is_superuser:
data = [
{"value": 0, "label": '仅本人数据权限'},
{"value": 1, "label": '本部门及以下数据权限'},
{"value": 2, "label": '本部门数据权限'},
{"value": 3, "label": '全部数据权限'},
{"value": 4, "label": '自定义数据权限'}
]
return DetailResponse(data=data)
else:
params = request.query_params
data = [{"value": 0, "label": '仅本人数据权限'}]
role_list = request.user.role.values_list('id', flat=True)
# 权限页面进入初始化获取所有的数据权限范围
role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list
).values_list('data_range', flat=True)
# 通过按钮小齿轮获取指定按钮的权限
if menu_button_id := params.get('menu_button', None):
role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__id=menu_button_id
).values_list('data_range', flat=True)
data = request.data
isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None)
btnId = data.get('btnId', None)
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
dept = data.get('dept', None) or [] # 默认空部门
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
data = data
elif item == 1:
data.extend([
{"value": 1, "label": '本部门及以下数据权限'},
{"value": 2, "label": '本部门数据权限'}
])
elif item == 2:
data.extend([{"value": 2, "label": '本部门数据权限'}])
elif item == 3:
data.extend([{"value": 3, "label": '全部数据权限'}])
elif item == 4:
data.extend([{"value": 4, "label": '自定义数据权限'}])
if isCheck:
# 添加权限:创建关联记录
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:
data = []
return DetailResponse(data=data)
# 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()
menu_btn_instance = MenuButton.objects.get(id=btnId)
serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu_btn_data_range(self, request):
"""
设置 角色-菜单-按钮-权限
"""
data = request.data
instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id'))
instance.data_range = data.get('data_range')
instance.dept.add(*data.get('dept'))
if not data.get('dept'):
instance.dept.clear()
instance.save()
serializer = RoleMenuButtonPermissionSerializer(instance, request=request)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request):
@@ -395,55 +288,3 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled
data.append(dept)
return DetailResponse(data=data)
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def menu_to_button(self, request):
"""
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
menu_id = params.get('menu', None)
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
is_superuser = request.user.is_superuser
if is_superuser:
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
else:
if params:
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="未获取到参数")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_menu(self, request):
"""
获取角色对应的按钮权限
:param request:
:return:
"""
params = request.query_params
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
return DetailResponse(data=queryset)

View File

@@ -286,6 +286,7 @@ class UserViewSet(CustomModelViewSet):
"dept": user.dept_id,
"is_superuser": user.is_superuser,
"role": user.role.values_list('id', flat=True),
"pwd_change_count":user.pwd_change_count
}
if hasattr(connection, 'tenant'):
result['tenant_id'] = connection.tenant and connection.tenant.id
@@ -319,7 +320,6 @@ class UserViewSet(CustomModelViewSet):
"""密码修改"""
data = request.data
old_pwd = data.get("oldPassword")
print(old_pwd)
new_pwd = data.get("newPassword")
new_pwd2 = data.get("newPassword2")
if old_pwd is None or new_pwd is None or new_pwd2 is None:
@@ -330,13 +330,33 @@ class UserViewSet(CustomModelViewSet):
if not verify_password:
old_pwd_md5 = hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(str(old_pwd_md5), request.user.password)
# 创建用户时、自定义密码无法修改问题
if not verify_password:
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_password:
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.pwd_change_count += 1
request.user.save()
return DetailResponse(data=None, msg="修改成功")
else:
return ErrorResponse(msg="旧密码不正确")
@action(methods=["post"], detail=False, permission_classes=[IsAuthenticated])
def login_change_password(self, request, *args, **kwargs):
"""初次登录进行密码修改"""
data = request.data
new_pwd = data.get("password")
new_pwd2 = data.get("password_regain")
if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配")
else:
request.user.password = make_password(new_pwd)
request.user.pwd_change_count += 1
request.user.save()
return DetailResponse(data=None, msg="修改成功")
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
def reset_to_default_password(self, request,pk):
"""恢复默认密码"""

View File

@@ -5,7 +5,21 @@ from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import FieldPermission, MenuField
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.models import get_custom_app_models
def merge_permission(data):
"""
合并权限
"""
result = {}
for item in data:
field_name = item.pop('field_name')
if field_name not in result:
result[field_name] = item
else:
for key, value in item.items():
result[field_name][key] = result[field_name][key] or value
return result
class FieldPermissionMixin:
@@ -14,25 +28,16 @@ class FieldPermissionMixin:
"""
获取字段权限
"""
finded = False
for model in get_custom_app_models():
if model['object'] is self.serializer_class.Meta.model:
finded = True
break
if finded:
break
if finded is False:
return []
model = self.serializer_class.Meta.model.__name__
user = request.user
# 创建一个默认字典来存储最终的结果
if user.is_superuser == 1:
data = MenuField.objects.filter( model=model['model']).values('field_name')
for item in data:
item['is_create'] = True
item['is_query'] = True
item['is_update'] = True
data = MenuField.objects.filter(model=model).values('field_name')
result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data}
else:
roles = request.user.role.values_list('id', flat=True)
data = FieldPermission.objects.filter(
field__model=model['model'],role__in=roles
field__model=model, role__in=roles
).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name'))
return DetailResponse(data=data)
result = merge_permission(data)
return DetailResponse(data=result)

View File

@@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend
from django_filters.conf import settings
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton
from dvadmin.utils.models import CoreModel
class CoreModelFilterBankend(BaseFilterBackend):
@@ -33,15 +33,15 @@ class CoreModelFilterBankend(BaseFilterBackend):
create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', None)
update_datetime_after = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_before', None)
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q()
if create_datetime_after and create_datetime_before:
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=create_datetime_before)
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
elif create_datetime_after:
create_filter &= Q(create_datetime__gte=create_datetime_after)
elif create_datetime_before:
create_filter &= Q(create_datetime__lte=create_datetime_before)
create_filter &= Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
# 更新时间范围过滤条件
update_filter = Q()
@@ -149,11 +149,14 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True)
# 修复权限获取bug
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
role_permission_list = []
if menu_button_ids:
role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
menu_button__api=re_api,
menu_button__method=method).values(
menu_button_id__in=menu_button_ids).values(
'data_range'
)
dataScope_list = [] # 权限范围列表
@@ -340,7 +343,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
from timezone_field import TimeZoneField
# 不进行 过滤的model 类
if isinstance(field, (models.JSONField, TimeZoneField)):
if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)):
continue
# warn if the field doesn't exist.
if field is None:

View File

@@ -86,4 +86,5 @@ def import_to_data(file_url, field_data, m2m_fields=None):
else:
array[key] = cell_value
tables.append(array)
return tables
data = [i for i in tables if len(i) != 0]
return data

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import datetime
from urllib.parse import quote
from django.db import transaction
@@ -11,8 +12,10 @@ from rest_framework.decorators import action
from rest_framework.request import Request
from dvadmin.utils.import_export import import_to_data
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.request_util import get_verbose_name
from dvadmin.system.tasks import async_export_data
from dvadmin.system.models import DownloadCenter
class ImportSerializerMixin:
@@ -301,6 +304,16 @@ class ExportSerializerMixin:
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
data = self.export_serializer_class(queryset, many=True, request=request).data
try:
async_export_data.delay(
data,
str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"),
DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务', dept_belong_id=request.user.dept_id).pk,
self.export_field_label
)
return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载")
except:
pass
# 导出excel 表
response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition"

View File

@@ -32,6 +32,14 @@ class ApiLoggingMiddleware(MiddlewareMixin):
request.request_path = get_request_path(request)
def __handle_response(self, request, response):
# 判断有无log_id属性使用All记录时会出现此情况
if request.request_data.get('log_id', None) is None:
return
# 移除log_id不记录此ID
log_id = request.request_data.pop('log_id')
# request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性
body = getattr(request, 'request_data', {})
# 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数)
@@ -60,7 +68,7 @@ class ApiLoggingMiddleware(MiddlewareMixin):
'status': True if response.data.get('code') in [2000, ] else False,
'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')},
}
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=self.operation_log_id)
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=log_id)
if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None):
operation_log.request_modular = settings.API_MODEL_MAP[request.request_path]
operation_log.save()
@@ -71,7 +79,8 @@ class ApiLoggingMiddleware(MiddlewareMixin):
if self.methods == 'ALL' or request.method in self.methods:
log = OperationLog(request_modular=get_verbose_name(view_func.cls.queryset))
log.save()
self.operation_log_id = log.id
# self.operation_log_id = log.id
request.request_data['log_id'] = log.id
return

View File

@@ -61,8 +61,24 @@ class SoftDeleteModel(models.Model):
"""
重写删除方法,直接开启软删除
"""
if soft_delete:
self.is_deleted = True
self.save(using=using)
# 级联软删除关联对象
for related_object in self._meta.related_objects:
related_model = getattr(self, related_object.get_accessor_name())
# 处理一对多和多对多的关联对象
if related_object.one_to_many or related_object.many_to_many:
related_objects = related_model.all()
elif related_object.one_to_one:
related_objects = [related_model]
else:
continue
for obj in related_objects:
obj.delete(soft_delete=True)
else:
super().delete(using=using, *args, **kwargs)
class CoreModel(models.Model):
@@ -216,9 +232,13 @@ def get_all_models_objects(model_name=None):
def get_model_from_app(app_name):
"""获取模型里的字段"""
model_module = import_module(app_name + '.models')
exclude_models = getattr(model_module, 'exclude_models', [])
filter_model = [
getattr(model_module, item) for item in dir(model_module)
if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase)
value for key, value in model_module.__dict__.items()
if key != 'CoreModel'
and isinstance(value, type)
and issubclass(value, models.Model)
and key not in exclude_models
]
model_list = []
for model in filter_model:

View File

@@ -44,6 +44,35 @@ class AnonymousUserPermission(BasePermission):
return True
class SuperuserPermission(BasePermission):
"""
超级管理员权限类
"""
def has_permission(self, request, view):
if isinstance(request.user, AnonymousUser):
return False
# 判断是否是超级管理员
if request.user.is_superuser:
return True
class AdminPermission(BasePermission):
"""
普通管理员权限类
"""
def has_permission(self, request, view):
if isinstance(request.user, AnonymousUser):
return False
# 判断是否是超级管理员
is_superuser = request.user.is_superuser
# 判断是否是管理员角色
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
return True
def ReUUID(api):
"""
将接口的uuid替换掉
@@ -82,7 +111,8 @@ class CustomPermission(BasePermission):
if not hasattr(request.user, "role"):
return False
role_id_list = request.user.role.values_list('id', flat=True)
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(
permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
ApiList = [
str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')]

View File

@@ -26,7 +26,6 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
modifier_field_id = "modifier"
modifier_name = serializers.SerializerMethodField(read_only=True)
dept_belong_id = serializers.IntegerField(required=False, allow_null=True)
def get_modifier_name(self, instance):
if not hasattr(instance, "modifier"):
@@ -52,7 +51,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
)
update_datetime = serializers.DateTimeField(
format="%Y-%m-%d %H:%M:%S", required=False
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
)
def __init__(self, instance=None, data=empty, request=None, **kwargs):
@@ -75,7 +74,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
and validated_data.get(self.dept_belong_id_field_name, None) is None
):
validated_data[self.dept_belong_id_field_name] = getattr(
self.request.user, "dept_id", None
self.request.user, "dept_id", validated_data.get(self.dept_belong_id_field_name, None)
)
return super().create(validated_data)

View File

@@ -6,6 +6,8 @@
@Created on: 2021/6/1 001 22:57
@Remark: 自定义视图集
"""
import copy
from django.db import transaction
from django_filters import DateTimeFromToRangeFilter
from django_filters.rest_framework import FilterSet
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准
can_see = self.get_menu_field(serializer_class)
# 排除掉序列化器级的字段
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in sub_set:
# serializer_class._declared_fields.pop(field)
# 排除掉序列化器级的字段(排除字段权限中未授权的字段)
# if not self.request.user.is_superuser:
# serializer_class.Meta.fields = can_see
# exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in exclude_set:
# serializer_class._declared_fields.pop(field)
# meta = copy.deepcopy(serializer_class.Meta)
# meta.fields = list(can_see)
# serializer_class.Meta = meta
# 在分页器中使用
self.request.permission_fields = can_see
if isinstance(self.request.data, list):
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
def get_menu_field(self, serializer_class):
"""获取字段权限"""
finded = False
for model in get_custom_app_models():
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded is False:
if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
return []
return MenuField.objects.filter(model=model['model']
).values('field_name', 'title')
# 匿名用户没有角色
ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__)
if hasattr(self.request.user, 'role'):
roles = self.request.user.role.values_list('id', flat=True)
ret = ret.filter(is_query=True, role__in=roles)
return ret.values_list('field__field_name', flat=True)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request)
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
instance.delete()
return DetailResponse(data=[], msg="删除成功")
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING)
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING))
@swagger_auto_schema(request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['keys'],

View File

@@ -1,32 +1,32 @@
Django==4.2.7
Django==4.2.14
django-comment-migrate==0.1.7
django-cors-headers==4.4.0
django-filter==23.3
django-filter==24.2
django-ranged-response==0.2.0
djangorestframework==3.14.0
django-restql==0.15.3
django-simple-captcha==0.5.20
django-timezone-field==6.0.1
djangorestframework-simplejwt==5.3.0
djangorestframework==3.15.2
django-restql==0.15.4
django-simple-captcha==0.6.0
django-timezone-field==7.0
djangorestframework-simplejwt==5.3.1
drf-yasg==1.21.7
mysqlclient==2.2.0
pypinyin==0.49.0
pypinyin==0.51.0
ua-parser==0.18.0
pyparsing==3.1.1
openpyxl==3.1.2
requests==2.31.0
typing-extensions==4.8.0
tzlocal==5.1
channels==3.0.5
channels-redis==4.1.0
pyparsing==3.1.2
openpyxl==3.1.5
requests==2.32.3
typing-extensions==4.12.2
tzlocal==5.2
channels==4.1.0
channels-redis==4.2.0
websockets==11.0.3
user-agents==2.2.0
six==1.16.0
whitenoise==6.6.0
whitenoise==6.7.0
psycopg2==2.9.9
uvicorn==0.23.2
gunicorn==21.2.0
gevent==23.9.1
Pillow==10.1.0
dvadmin-celery==1.0.5
pyinstaller==6.8.0
uvicorn==0.30.3
gunicorn==22.0.0
gevent==24.2.1
Pillow==10.4.0
pyinstaller==6.9.0
dvadmin3-celery==3.1.6

87
crud-gen.sh Normal file
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/
COPY web/. .
RUN yarn install --registry=https://registry.npmmirror.com

52
init.sh
View File

@@ -1,5 +1,6 @@
#!/bin/bash
ENV_FILE=".env"
HOST="177.10.0.13"
# 检查 .env 文件是否存在
if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE 文件已存在。"
@@ -15,17 +16,60 @@ else
echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。"
awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }'
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py
sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py
sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py
sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py
echo "初始化密码创建成功"
fi
echo "正在启动容器..."
docker-compose up -d
docker exec dvadmin3-django python manage.py makemigrations
docker exec dvadmin3-django python manage.py migrate
docker exec dvadmin3-django python manage.py init
if [ $? -ne 0 ]; then
echo "docker-compose up -d 执行失败!"
exit 1
fi
MYSQL_PORT=3306
REDIS_PORT=6379
check_mysql() {
if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then
echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT"
return 0
else
return 1
fi
}
check_redis() {
if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then
echo "Redis 服务正在运行在 $HOST:$REDIS_PORT"
return 0
else
return 1
fi
}
i=1
while [ $i -le 8 ]; do
if check_mysql || check_redis; then
echo "正在迁移数据..."
docker exec dvadmin3-django python3 manage.py makemigrations
docker exec dvadmin3-django python3 manage.py migrate
echo "正在初始化数据..."
docker exec dvadmin3-django python3 manage.py init
echo "欢迎使用dvadmin3项目"
echo "登录地址http://ip:8080"
echo "如访问不到,请检查防火墙配置"
exit 0
else
echo "$i 次尝试MySQL 或 REDIS服务未运行等待 2 秒后重试..."
sleep 2
fi
i=$((i+1))
done
echo "尝试 5 次后MySQL 或 REDIS服务仍未运行"
exit 1

View File

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

View File

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

View File

@@ -49,6 +49,10 @@
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
## 给框架点赞
<img src='https://django-vue-admin.com/alipay.jpg' width='200'>
<img src='https://django-vue-admin.com/wechat.jpg' width='200'>
## 交流

77
web/flowH5.config.ts Normal file
View File

@@ -0,0 +1,77 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path, {resolve} from 'path';
import vueJsx from "@vitejs/plugin-vue-jsx";
import vueSetupExtend from "vite-plugin-vue-setup-extend";
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import pxtorem from 'postcss-pxtorem';
const pathResolve = (dir: string) => {
return resolve(__dirname, '.', dir);
};
export default defineConfig({
build: {
// outDir: '../backend/static/previewer',
lib: {
entry: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), // 库的入口文件
name: 'previewer', // 库的全局变量名称
fileName: (format) => `index.${format}.js`, // 输出文件名格式
},
rollupOptions: {
input:{
previewer: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'),
},
external: ['vue','xe-utils'], // 指定外部依赖
output:{
// dir: '../backend/static/previewer', // 输出目录
entryFileNames: 'index.[format].js', // 入口文件名格式
format: 'commonjs',
globals: {
vue: 'Vue'
},
chunkFileNames: `[name].[hash].js`
},
plugins: [
terser({
compress: {
drop_console: false, // 确保不移除 console.log
},
}),
postcss({
plugins: [
pxtorem({
rootValue: 37.5,
unitPrecision: 5,
propList: ['*'],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
exclude: /node_modules/i,
}),
],
}),
],
},
},
plugins: [
vue(),
vueJsx(),
vueSetupExtend(),
],
resolve: {
alias: {
'/@': path.resolve(__dirname, 'src'), // '@' 别名指向 'src' 目录
'@views': pathResolve('./src/views'),
'/src':path.resolve(__dirname, 'src')
},
},
css:{
postcss:{
}
},
define: {
'process.env': {}
}
});

View File

@@ -1,41 +1,45 @@
{
"name": "django-vue3-admin",
"version": "3.0.3",
"version": "3.1.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT",
"scripts": {
"dev": "vite --force",
"build:dev": "vite build --mode development",
"build": "vite build",
"build:local": "vite build --mode local_prod",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"@fast-crud/fast-crud": "^1.20.1",
"@fast-crud/fast-extends": "^1.20.1",
"@fast-crud/ui-element": "^1.20.1",
"@fast-crud/ui-interface": "^1.20.1",
"@iconify/vue": "^4.1.1",
"@types/lodash": "^4.14.202",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@element-plus/icons-vue": "^2.3.1",
"@fast-crud/fast-crud": "^1.21.2",
"@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2",
"@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"autoprefixer": "^10.4.14",
"axios": "^1.2.1",
"countup.js": "^2.3.2",
"cropperjs": "^1.5.13",
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.2",
"date-holidays": "^3.24.1",
"e-icon-picker": "2.1.1",
"echarts": "^5.4.1",
"echarts": "^5.5.1",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.5.5",
"element-plus": "^2.8.0",
"element-tree-line": "^0.2.1",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"js-table2excel": "^1.0.3",
"js-cookie": "^3.0.5",
"js-table2excel": "^1.1.2",
"jsplumb": "^2.15.6",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"lunar-javascript": "^1.7.1",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.0.28",
"pinia-plugin-persist": "^1.0.0",
@@ -49,31 +53,31 @@
"tailwindcss": "^3.2.7",
"ts-md5": "^1.3.1",
"upgrade": "^1.1.0",
"vue": "^3.2.45",
"vue": "^3.4.38",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vxe-table": "^4.4.1",
"xe-utils": "^3.5.7"
"vue-i18n": "^9.14.0",
"vue-router": "^4.4.3",
"vxe-table": "^4.6.18",
"xe-utils": "^3.5.30"
},
"devDependencies": {
"@types/node": "^18.11.13",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.0",
"@types/node": "^18.19.42",
"@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.2.45",
"eslint": "^8.54.0",
"eslint-plugin-vue": "^9.8.0",
"@vitejs/plugin-vue": "^5.1.2",
"@vue/compiler-sfc": "^3.4.38",
"eslint": "^9.9.0",
"eslint-plugin-vue": "^9.27.0",
"prettier": "^2.8.1",
"sass": "^1.56.2",
"typescript": "^4.9.4",
"vite": "^4.0.0",
"vite": "^5.4.1",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-eslint-parser": "^9.1.0"
"vue-eslint-parser": "^9.4.3"
},
"browserslist": [
"> 1%",

View File

@@ -11,7 +11,7 @@
<script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
@@ -26,7 +26,8 @@ const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
import { ElMessageBox, ElNotification, NotificationHandle } from 'element-plus';
import { useCore } from '/@/utils/cores';
// 定义变量内容
const { messages, locale } = useI18n();
const setingsRef = ref();
@@ -35,7 +36,8 @@ const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket';
import { ElNotification } from 'element-plus';
const core = useCore();
const router = useRouter();
// 获取版本号
const getVersion = computed(() => {
let isVersion = false;
@@ -67,7 +69,15 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer();
});
// 设置皮肤缓存版本,每次更新版本可以所有用户清空缓存
const themeConfigVersion = '1.0.0'
// 获取缓存中的布局配置
if (Local.get('themeConfigVersion') !== themeConfigVersion) {
Local.clear();
Local.set('themeConfigVersion', themeConfigVersion);
window.location.reload();
return
}
if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle');
@@ -117,7 +127,25 @@ const wsReceive = (message: any) => {
position: 'bottom-right',
duration: 5000,
});
} else if (data.contentType === 'Content') {
ElMessageBox.confirm(data.content, data.notificationTitle, {
confirmButtonText: data.notificationButton,
dangerouslyUseHTMLString: true,
cancelButtonText: '关闭',
type: 'info',
closeOnClickModal: false,
}).then(() => {
ElMessageBox.close();
const path = data.path;
if (route.path === path) {
core.bus.emit('onNewTask', { name: 'onNewTask' });
} else {
router.push({ path});
}
})
.catch(() => {});
}
};
onBeforeUnmount(() => {
// 关闭连接

BIN
web/src/assets/login-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View File

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

View File

@@ -0,0 +1,403 @@
<template>
<div style="width: 100%; height: 100%;">
<div class="selected-show" v-if="props.modelValue && props.selectable">
<el-text>已选择:</el-text>
<el-tag v-if="props.multiple" v-for="item in data" closable @close="handleTagClose(item)">
{{ item.toLocaleDateString('en-CA') }}
</el-tag>
<el-tag v-else closable @close="handleTagClose(data)">{{ data?.toLocaleDateString('en-CA') }}</el-tag>
<el-button v-if="props.modelValue" size="small" type="text" @click="clear">清空</el-button>
</div>
<div class="controls">
<div>
今天<el-text size="large">{{ today.toLocaleDateString('en-CA') }}</el-text>
</div>
<!-- <div class="current-month">
<el-tag size="large" type="primary">
{{ currentCalendarDate.getFullYear() }}{{ currentCalendarDate.getMonth() + 1 }}
</el-tag>
</div> -->
<div class="control-button" v-if="!(!!props.range && props.range[0] && props.range[1]) && props.showPageTurn">
<el-button-group size="small" type="default" v-if="props.pageTurn">
<el-popover trigger="click" width="160px">
<template #reference>
<el-button type="text" size="small">节假日设置</el-button>
</template>
<el-switch v-model="showHoliday" active-text="显示节日" inactive-text="关闭节日" inline-prompt />
<el-checkbox v-model="showLunarHoliday" label="农历节日" />
<el-checkbox v-model="showJieQi" label="节气" />
<el-checkbox v-model="showDetailedHoliday" label="更多节日" />
</el-popover>
<el-button icon="DArrowLeft" @click="turnToPreY">上年</el-button>
<el-button icon="ArrowLeft" @click="turnToPreM">上月</el-button>
<el-button @click="turnToToday">今天</el-button>
<el-button icon="ArrowRight" @click="turnToNextM">下月</el-button>
<el-button icon="DArrowRight" @click="turnToNextY">下年</el-button>
</el-button-group>
</div>
</div>
<el-divider style="margin: 4px;" />
<div class="calender">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th class="calender-header" v-for="item, ind in ['日', '一', '二', '三', '四', '五', '六']" :key="ind">
{{ item }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="week in calendarList">
<td class="calender-td" v-for="item in week">
<div class="calender-cell" :data-date="item.date.toLocaleDateString('en-CA')" :class="{
'no-current-month': item.date.getMonth() !== currentCalendarDate.getMonth(),
'today': item.date.toDateString() === today.toDateString(),
'selected': item.selected,
'disabled': item.disabled,
}" @mouseenter="onCalenderCellHover" @mouseleave="onCalenderCellUnhover"
@click="(e: MouseEvent) => item.disabled ? null : onCalenderCellClick(e)">
<div class="calender-cell-header calender-cell-line">
<span>{{ item.date.getDate() }}</span>
<span v-if="item.selected">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496z">
</path>
</svg>
</span>
</div>
<div class="calender-cell-body calender-cell-line">
<slot name="cell-body" v-bind="item">
</slot>
</div>
<div class="calender-cell-footer calender-cell-line">
<span>{{ item.holiday || '&nbsp;' }}</span>
<el-text v-if="item.date.toDateString() === today.toDateString()" type="danger">今天</el-text>
</div>
<!-- {{ item }} -->
</div>
</td>
</tr>
</tbody>
</table>
<div class="watermark" v-if="props.watermark" :style="watermarkPositionMap[props.watermarkPosition]">
{{ (currentCalendarDate.toLocaleDateString('en-CA').split('-').slice(0, 2)).join('-') }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useUi } from '@fast-crud/fast-crud';
import { ref, defineProps, PropType, watch, computed, onMounted } from 'vue';
import Holidays from 'date-holidays';
import Lunar from 'lunar-javascript';
const LUNAR = Lunar.Lunar; // 农历
const SOLAR = Lunar.Solar; // 阳历
const props = defineProps({
modelValue: {},
// 日期多选
multiple: { type: Boolean, default: false },
// 日期范围
range: { type: Object as PropType<[Date, Date]> },
// 可以翻页
pageTurn: { type: Boolean, default: true },
// 跨页选择
crossPage: { type: Boolean, default: false },
// 显示年月水印和水印位置
watermark: { type: Boolean, default: true },
watermarkPosition: { type: Object as PropType<PositionType>, default: 'bottom-right' },
// 显示翻页控件
showPageTurn: { type: Boolean, default: true },
// 是否可选
selectable: { type: Boolean, default: true },
// 验证日期是否有效
validDate: { type: Object as PropType<ValidDateFunc>, default: () => ((d: Date) => true) }
});
type ValidDateFunc = (d: Date) => boolean;
type PositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'center-left' | 'center-right' | 'center-top';
const today = new Date();
const showHoliday = ref<boolean>(true); // 显示节日
const showDetailedHoliday = ref<boolean>(false); // 显示详细的国际节日
const showJieQi = ref<boolean>(true); // 显示节气
const showLunarHoliday = ref<boolean>(true); // 显示农历节日
const watermarkPositionMap: { [key: string]: any } = {
'top-left': { top: '40px', left: 0, transformOrigin: '0 0' },
'top-right': { top: '40px', right: 0, transformOrigin: '100% 0' },
'bottom-left': { bottom: 0, left: 0, transformOrigin: '0 100%' },
'bottom-right': { bottom: 0, right: 0, transformOrigin: '100% 100%' },
'center': { top: '50%', left: '50%', transformOrigin: '50% 50%', transform: 'translate(-50%, -50%) scale(10)' },
'center-left': { top: '50%', left: 0, transformOrigin: '0 50%' },
'center-right': { top: '50%', right: 0, transformOrigin: '100% 50%' },
'center-top': { top: 0, left: '50%', transformOrigin: '50% 0', transform: 'translate(-50%, 40px) scale(10)' },
'center-bottom': { bottom: 0, left: '50%', transformOrigin: '50% 100%', transform: 'translate(-50%, 0) scale(10)' },
};
// 获取当月第一周的第一天(包括上个月)
const calendarFirstDay = (current: Date = new Date()) => {
let today = new Date(current); // 指定天
let firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // 月初天
let weekOfFirstDay = firstDayOfMonth.getDay(); // 周几0日
if (weekOfFirstDay === 0) return new Date(firstDayOfMonth); // 是周日则直接返回
let firstDayOfWeek = new Date(firstDayOfMonth);
// 月初减去周几,不+1是因为从日历周日开始
firstDayOfWeek.setDate(firstDayOfMonth.getDate() - weekOfFirstDay);
return new Date(firstDayOfWeek);
};
// 获取当月最后一周的最后一天(包括下个月)
const calendarLastDay = (current: Date = new Date()) => {
let today = new Date(current); // 指定天
let lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1); // 月末天
lastDayOfMonth.setDate(lastDayOfMonth.getDate() - 1);
let weekOfFirstDay = lastDayOfMonth.getDay();
if (weekOfFirstDay === 6) return new Date(lastDayOfMonth); // 是周六则直接返回
let lastDayOfWeek = new Date(lastDayOfMonth);
// 月末加剩下周几,要-1是因为日历到周六结束
lastDayOfWeek.setDate(lastDayOfMonth.getDate() + (7 - weekOfFirstDay - 1));
return new Date(lastDayOfWeek);
};
const generateDateList = (startDate: Date, endDate: Date): Date[] => { // 生成日期列表
let dates = [];
let s = new Date(startDate);
let e = new Date(endDate);
while (s <= e) {
dates.push(new Date(s));
s.setDate(s.getDate() + 1);
}
return dates;
};
// 日历当前页范围
interface CalendarCell {
date: Date;
selected: boolean;
disabled: boolean;
currentMonth: boolean;
holiday: string;
};
const currentCalendarDate = ref<Date>(new Date());
const calendarList = computed(() => {
let dates = (!!props.range && props.range[0] && props.range[1]) ?
generateDateList(props.range[0], props.range[1]) :
generateDateList(calendarFirstDay(currentCalendarDate.value), calendarLastDay(currentCalendarDate.value));
let proce_dates: CalendarCell[] = dates.map((value) => {
let solarDate = SOLAR.fromDate(value);
let lunarDate = solarDate.getLunar();
let solarHolidays: string[] = solarDate.getFestivals(); // 国历节日
let lunarHolidays: string[] = lunarDate.getFestivals(); // 农历节日
let jieQi: string = lunarDate.getJieQi(); // 节气
// 农历节日、国际节日、节气三选一
let holiday = showHoliday.value ? ((showLunarHoliday.value ? lunarHolidays[0] : '') || (showJieQi.value ? jieQi : '') ||
(showDetailedHoliday.value ? solarHolidays[0] : yearHolidays.value[value.toLocaleDateString('en-CA')])) : ''; // yearHolidays国际的
return {
date: value,
selected: props.multiple ?
(data.value as Date[]).findIndex((v) => v.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA')) !== -1 :
data.value?.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA'),
disabled: !props.validDate(value),
currentMonth: value.getMonth() === currentCalendarDate.value.getMonth(),
// 农历节日、节气、法定日三选一
holiday: holiday
}
});
let res: CalendarCell[][] = [];
for (let i = 0; i < 6; i++) res.push(proce_dates.slice(i * 7, (i + 1) * 7));
return res;
});
// 控件
const turnToPreM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() - 1, 1);
const turnToNextM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() + 1, 1);
const turnToPreY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() - 1, currentCalendarDate.value.getMonth(), 1);
const turnToNextY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() + 1, currentCalendarDate.value.getMonth(), 1);
const turnToToday = () => currentCalendarDate.value = new Date();
// 如果禁止跨页则跨页时清空选择
watch(
() => currentCalendarDate.value,
(v, ov) => props.crossPage ? {} :
(v.toLocaleDateString('en-CA') === ov.toLocaleDateString('en-CA') ? {} : clear())
);
// 单元格事件
const onCalenderCellHover = ({ target }: MouseEvent) => (target as HTMLElement).classList.add('onhover');
const onCalenderCellUnhover = ({ target }: MouseEvent) => (target as HTMLElement).classList.remove('onhover');
const onCalenderCellClick = (e: MouseEvent) => {
if (!props.selectable) return;
let strValue = (e.target as HTMLElement).dataset.date as string;
if (strValue === undefined) return;
let value = new Date(strValue);
if (props.multiple) {
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
let ind = d.findIndex((v) => v === strValue);
if (ind === -1) d.push(strValue);
else d.splice(ind, 1);
onDataChange(d);
}
// 这里阻止了点击取消选中需要通过tag的x来取消
else (data.value?.toLocaleDateString('en-CA') === strValue ? {} : onDataChange(value));
};
// 选择回显
const handleTagClose = (d: Date) => {
let strValue = d.toLocaleDateString('en-CA');
if (props.multiple) {
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
d.splice(d.findIndex((v) => v === strValue), 1);
onDataChange(d);
}
else onDataChange(null);
};
// 节假日
const holidays = new Holidays('CN');
const yearHolidays = computed(() => {
let h = holidays.getHolidays(currentCalendarDate.value.getFullYear());
let proce_h: { [key: string]: string } = {};
let _h: string[] = [];
for (let i of h) {
let d = i.date.split(' ')[0];
let hn = i.name.split(' ')[0];
if (_h.includes(hn)) continue;
proce_h[d] = hn;
_h.push(hn);
}
return proce_h
});
// fs-crud部分
const data = ref<any>();
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
watch(
() => props.modelValue,
(val) => {
if (val === undefined) data.value = props.multiple ? [] : null;
else data.value = props.multiple ? (val as Date[]).map((v: Date) => new Date(v)) : val;
},
{ immediate: true }
);
const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => {
emit('update:modelValue', value);
formValidator.onChange();
formValidator.onBlur();
};
const reset = () => { // 重置日历
currentCalendarDate.value = new Date();
onDataChange(props.multiple ? [] : null);
};
const clear = () => onDataChange(props.multiple ? [] : null); // 清空数据
defineExpose({
data,
onDataChange,
reset,
clear,
showHoliday,
showDetailedHoliday,
showJieQi,
showLunarHoliday
});
</script>
<style lang="scss" scoped>
.selected-show {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.controls {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
}
.calender {
position: relative;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
.watermark {
position: absolute;
font-size: 16px;
transform: scale(10);
color: #aaa;
opacity: 0.1;
pointer-events: none;
}
table {
position: relative;
}
.calender-header {
padding: 16px 0;
}
.calender-td {
border: 1px solid #eee;
width: calc(100% / 7);
}
.calender-cell {
min-height: 96px;
min-width: 100px;
box-sizing: border-box;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
&.today {
color: var(--el-color-warning) !important;
background-color: var(--el-color-warning-light-9) !important;
}
&.onhover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
&.disabled {
cursor: not-allowed;
color: #bbb;
background: none;
}
&.no-current-month {
color: #bbb;
}
&.selected {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.calender-cell-line {
min-height: 0px;
&.calender-cell-header {
display: flex;
gap: 4px;
align-items: center;
pointer-events: none;
}
&.calender-cell-body {
display: flex;
gap: 4px;
align-items: center;
}
&.calender-cell-footer {
display: flex;
justify-content: space-between;
pointer-events: none;
}
}
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div ref="itemRef" class="file-item" :title="data.name" @mouseenter="isShow = true" @mouseleave="isShow = false">
<div v-if="showTitle" class="file-name" :class="{ show: isShow }">{{ data.name }}</div>
<component :is="FileTypes[data.file_type].tag" v-bind="FileTypes[data.file_type].attr" />
<div v-if="props.showClose" class="file-del" :class="{ show: isShow }">
<el-icon :size="24" color="white" @click.stop="delFileHandle" style="cursor: pointer;">
<CircleClose style="mix-blend-mode: difference;" />
</el-icon>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { ref, defineProps, PropType, watch, onMounted, h } from 'vue';
import { successNotification } from '/@/utils/message';
import { getBaseURL } from '/@/utils/baseUrl';
const props = defineProps({
fileData: { type: Object as PropType<any>, required: true },
api: { type: Object as PropType<any>, required: true },
showTitle: { type: Boolean, default: true },
showClose: { type: Boolean, default: true },
});
const _OtherFileComponent = defineComponent({ template: '<el-icon><Files /></el-icon>' });
const FileTypes = [
{ tag: 'img', attr: { src: getBaseURL(props.fileData.url), draggable: false } },
{ tag: 'video', attr: { src: getBaseURL(props.fileData.url), controls: false, autoplay: true, muted: true, loop: true } },
{ tag: 'audio', attr: { src: getBaseURL(props.fileData.url), controls: true, autoplay: false, muted: false, loop: false, volume: 0 } },
{ tag: _OtherFileComponent, attr: { style: { fontSize: '2rem' } } },
];
const isShow = ref<boolean>(false);
const itemRef = ref<HTMLDivElement>();
const data = ref<any>(null);
const delFileHandle = () => props.api.DelObj(props.fileData.id).then(() => {
successNotification('删除成功');
emit('onDelFile');
});
watch(props.fileData, (nVal) => data.value = nVal, { immediate: true, deep: true });
const emit = defineEmits(['onDelFile']);
defineExpose({});
onMounted(() => { });
</script>
<style scoped>
.file-item {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-items: center;
}
.file-item>* {
width: 100% !important;
}
.file-name {
display: none;
position: absolute;
top: 0;
left: 0;
padding: 4px 12px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
word-break: break-all;
white-space: normal;
color: white;
background-color: rgba(0, 0, 0, .5);
overflow: hidden;
text-overflow: ellipsis;
}
.file-del {
display: none;
position: absolute;
left: 0;
bottom: 0;
justify-content: flex-end;
}
.show {
display: flex !important;
}
</style>

View File

@@ -0,0 +1,521 @@
<template>
<div style="width: 100%;" :class="props.class" :style="props.style">
<slot name="input" v-bind="{}">
<div v-if="props.showInput" style="width: 100%;" :class="props.inputClass" :style="props.inputStyle">
<el-select v-if="props.inputType === 'selector'" v-model="data" suffix-icon="arrow-down" clearable
:multiple="props.multiple" placeholder="请选择文件" @click="selectVisiable = true && !props.disabled"
:disabled="props.disabled" @clear="selectedInit" @remove-tag="selectedInit">
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
:label="item.name" />
</el-select>
<div v-if="props.inputType === 'image' && props.multiple"
style="width: 100%; display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px;">
<div v-for="item, index in (data || [])" style="position: relative;"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="item" :key="index" fit="scale-down" class="itemList"
:style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }" />
<el-icon v-show="(!!data && !props.disabled)" class="closeHover" :size="16" @click="clearOne(item)">
<Close />
</el-icon>
</div>
<div style="position: relative;" :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<div
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
</div>
</div>
<div v-if="props.inputType === 'image' && !props.multiple" class="form-display" style="position: relative;"
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
<template #error>
<div></div>
</template>
</el-image>
<div v-show="!(!!data)"
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>
<el-icon v-show="(!!data && !props.disabled) && !props.multiple" class="closeHover" :size="16" @click="clear">
<Close />
</el-icon>
</div>
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;"
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
<video :src="data" :controls="false" :autoplay="true" :muted="true" :loop="true"
:style="{ maxWidth: props.inputSize * 2 + 'px', maxHeight: props.inputSize + 'px', margin: '0 auto' }"></video>
<div v-show="!(!!data)"
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>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
<Close />
</el-icon>
</div>
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;"
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
<audio :src="data" :controls="!!data" :autoplay="false" :muted="true" :loop="true"
style="width: 100%; z-index: 1;"></audio>
<div v-show="!(!!data)"
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>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
<Close />
</el-icon>
</div>
</div>
</slot>
<el-dialog v-model="selectVisiable" :draggable="true" width="50%" :align-center="false" :append-to-body="true"
@open="if (listData.length === 0) listRequest();" @close="onClose" @closed="onClosed" modal-class="_overlay">
<template #header>
<span class="el-dialog__title">文件选择</span>
<el-divider style="margin: 0;" />
</template>
<div style="padding: 4px;">
<div style="width: 100%; display: flex; justify-content: space-between; gap: 12px;">
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
@tab-change="handleTabChange" v-if="!isSuperTenent">
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="0" label="图片" />
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="1" label="视频" />
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="2" label="音频" />
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="3" label="其他" />
</el-tabs>
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
@tab-change="handleTabChange" v-if="isTenentMode">
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="4" label="系统图片" />
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="5" label="系统视频" />
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="6" label="系统音频" />
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="7" label="系统其他" />
</el-tabs>
</div>
<el-row justify="space-between" class="headerBar">
<el-col :span="12">
<slot name="actionbar-left">
<el-input v-model="filterForm.name" :placeholder="`请输入${TypeLabel[tabsActived % 4]}名`"
prefix-icon="search" clearable @change="listRequest" />
<div>
<el-tag v-if="props.multiple" type="primary" effect="light">
一共选中&nbsp;{{ data?.length || 0 }}&nbsp;个文件
</el-tag>
</div>
</slot>
</el-col>
<el-col :span="12" style="width: 100%; display: flex; gap: 12px; justify-content: flex-end;">
<slot name="actionbar-right" v-bind="{}">
<el-button type="default" circle icon="refresh" @click="listRequest" />
<template v-if="tabsActived > 3 ? isSuperTenent : true">
<el-upload ref="uploadRef" :action="getBaseURL() + 'api/system/file/'" :multiple="false" :drag="false"
:data="{ upload_method: 1 }" :show-file-list="true" :accept="AcceptList[tabsActived % 4]"
:on-success="() => { listRequest(); listRequestAll(); uploadRef.clearFiles(); }"
v-if="props.showUploadButton">
<el-button type="primary" icon="plus">上传{{ TypeLabel[tabsActived % 4] }}</el-button>
</el-upload>
<el-button type="info" icon="link" @click="netVisiable = true" v-if="props.showNetButton">
网络{{ TypeLabel[tabsActived % 4] }}
</el-button>
</template>
</slot>
</el-col>
</el-row>
<div v-if="!listData.length">
<slot name="empty">
<el-empty description="无内容请上传" style="width: 100%; height: calc(50vh); margin-top: 24px; padding: 4px;" />
</slot>
</div>
<div ref="listContainerRef" class="listContainer" v-else>
<div v-for="item, index in listData" :key="index" @click="onItemClick($event)" :data-id="item[props.valueKey]"
:style="{ width: (props.itemSize || 100) + 'px', cursor: props.selectable ? 'pointer' : 'normal' }">
<slot name="item" :data="item">
<FileItem :fileData="item" :api="fileApi" :showClose="tabsActived < 4 || isSuperTenent"
@onDelFile="listRequest(); listRequestAll();" />
</slot>
</div>
</div>
<div class="listPaginator">
<el-pagination background size="small" layout="total, sizes, prev, pager, next" :total="pageForm.total"
v-model:page-size="pageForm.limit" :page-sizes="[10, 20, 30, 40, 50]" v-model:current-page="pageForm.page"
:hide-on-single-page="false" @change="handlePageChange" />
</div>
</div>
<!-- 只要在获取中就最大程度阻止关闭dialog -->
<el-dialog v-model="netVisiable" :draggable="false" width="50%" :align-center="false" :append-to-body="true"
:title="'网络' + TypeLabel[tabsActived % 4] + '上传'" @closed="netUrl = ''" :close-on-click-modal="!netLoading"
:close-on-press-escape="!netLoading" :show-close="!netLoading" modal-class="_overlay">
<el-form-item :label="TypeLabel[tabsActived % 4] + '链接'">
<el-input v-model="netUrl" placeholder="请输入网络连接" clearable @input="netChange">
<template #prepend>
<el-select v-model="netPrefix" style="width: 110px;">
<el-option v-for="item, index in ['HTTP://', 'HTTPS://']" :key="index" :label="item" :value="item" />
</el-select>
</template>
</el-input>
</el-form-item>
<template #footer>
<el-button v-if="!netLoading" type="default" @click="netVisiable = false">取消</el-button>
<el-button type="primary" @click="confirmNetUrl" :loading="netLoading">
{{ netLoading ? '网络文件获取中...' : '确定' }}
</el-button>
</template>
</el-dialog>
<template #footer v-if="props.showInput">
<el-button type="default" @click="onClose">取消</el-button>
<el-button type="primary" @click="onSave">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useUi, UserPageQuery, AddReq, EditReq, DelReq } from '@fast-crud/fast-crud';
import { ref, reactive, defineProps, PropType, watch, onMounted, nextTick } from 'vue';
import { getBaseURL } from '/@/utils/baseUrl';
import { request } from '/@/utils/service';
import { SHOW } from './types';
import FileItem from './fileItem.vue';
import { pluginsAll } from '/@/views/plugins/index';
import { storeToRefs } from "pinia";
import { useUserInfo } from "/@/stores/userInfo";
import { errorNotification, successNotification } from '/@/utils/message';
const userInfos = storeToRefs(useUserInfo()).userInfos;
const isTenentMode = !!(pluginsAll && pluginsAll.length && pluginsAll.indexOf('dvadmin3-tenants-web') >= 0);
const isSuperTenent = (userInfos.value as any).schema_name === 'public';
const TypeLabel = ['图片', '视频', '音频', '文件']
const AcceptList = ['image/*', 'video/*', 'audio/*', ''];
const props = defineProps({
modelValue: {},
class: { type: Object as PropType<String | Object>, default: '' },
inputClass: { type: Object as PropType<String | Object>, default: '' },
style: { type: Object as PropType<Object | string>, default: {} },
inputStyle: { type: Object as PropType<Object | string>, default: {} },
disabled: { type: Boolean, default: false },
tabsType: { type: Object as PropType<'' | 'card' | 'border-card'>, default: '' },
itemSize: { type: Number, default: 100 },
// 1000图片 100视频 10音频 1 其他 控制tabs的显示
tabsShow: { type: Number, default: SHOW.ALL },
// 是否可以多选,默认单选
// 该值为true时inputType必须是selector或image暂不支持其他type的多选
multiple: { type: Boolean, default: false },
// 是否可选该参数用于只上传和展示而不选择和绑定model的情况
selectable: { type: Boolean, default: true },
// 该参数用于控制是否显示表单item。若赋值为false则不会显示表单item也不会显示底部按钮
// 如果不显示表单item则无法触发dialog需要父组件通过修改本组件暴露的 selectVisiable 状态来控制dialog
showInput: { type: Boolean, default: true },
// 表单item类型不为selector是需要设置valueKey否则可能获取不到媒体数据
inputType: { type: Object as PropType<'selector' | 'image' | 'video' | 'audio'>, default: 'selector' },
// inputType不为selector时生效
inputSize: { type: Number, default: 100 },
// v-model绑定的值是file数据的哪个key默认是url
valueKey: { type: String, default: 'url' },
showUploadButton: { type: Boolean, default: true },
showNetButton: { type: Boolean, default: true },
} as any);
const selectVisiable = ref<boolean>(false);
const tabsActived = ref<number>([3, 2, 1, 0][((props.tabsShow & (props.tabsShow - 1)) === 0) ? Math.log2(props.tabsShow) : 3]);
const fileApiPrefix = '/api/system/file/';
const fileApi = {
GetList: (query: UserPageQuery) => request({ url: fileApiPrefix, method: 'get', params: query }),
AddObj: (obj: AddReq) => request({ url: fileApiPrefix, method: 'post', data: obj }),
DelObj: (id: DelReq) => request({ url: fileApiPrefix + id + '/', method: 'delete', data: { id } }),
GetAll: () => request({ url: fileApiPrefix + 'get_all/' }),
};
// 过滤表单
const filterForm = reactive({ name: '' });
// 分页表单
const pageForm = reactive({ page: 1, limit: 10, total: 0 });
// 展示的数据列表
const listData = ref<any[]>([]);
const listAllData = ref<any[]>([]);
const listRequest = async () => {
let res = await fileApi.GetList({
page: pageForm.page,
limit: pageForm.limit,
file_type: isTenentMode ? tabsActived.value % 4 : tabsActived.value,
system: tabsActived.value > 3,
upload_method: 1,
...filterForm
});
listData.value = [];
await nextTick();
listData.value = (res.data as any[]).map((item: any) => ({ ...item, url: getBaseURL(item.url) }));
pageForm.total = res.total;
pageForm.page = res.page;
pageForm.limit = res.limit;
selectedInit();
};
const formDisplayEnter = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'block');
const formDisplayLeave = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'none');
const listRequestAll = async () => {
if (props.inputType !== 'selector') return;
let res = await fileApi.GetAll();
listAllData.value = res.data;
};
// tab改变时触发
const handleTabChange = (name: string) => { pageForm.page = 1; listRequest(); };
// 分页器改变时触发
const handlePageChange = (currentPage: number, pageSize: number) => { pageForm.page = currentPage; pageForm.limit = pageSize; listRequest(); };
// 选择的行为
const listContainerRef = ref<any>();
const onItemClick = async (e: MouseEvent) => {
if (!props.selectable) return;
let target = e.target as HTMLElement;
let flat = 0; // -1删除 0不变 1添加
while (!target.dataset.id) target = target.parentElement as HTMLElement;
let fileId = target.dataset.id;
if (props.multiple) {
if (!!!data.value) data.value = [];
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
else { target.classList.add('active'); flat = 1; }
if (data.value.length) {
let _l = JSON.parse(JSON.stringify(data.value));
if (flat === 1) _l.push(fileId);
else _l.splice(_l.indexOf(fileId), 1);
data.value = _l;
} else data.value = [fileId];
// 去重排序,<降序,>升序
data.value = Array.from(new Set(data.value)).sort();
} else {
for (let i of listContainerRef.value?.children) (i as HTMLElement).classList.remove('active');
target.classList.add('active');
data.value = fileId;
}
// onDataChange(data.value);
};
// 每次列表刷新都得更新一下选择状态,因为所有标签页共享列表
const selectedInit = async () => {
if (!props.selectable) return;
await nextTick(); // 不等待一次不会刷新
for (let i of (listContainerRef.value?.children || [])) {
i.classList.remove('active');
let fid = (i as HTMLElement).dataset.id;
if (props.multiple) { if (data.value?.includes(fid)) i.classList.add('active'); }
else { if (fid === data.value) i.classList.add('active'); }
}
};
const uploadRef = ref<any>();
const onSave = () => {
onDataChange(data.value);
emit('onSave', data.value);
selectVisiable.value = false;
};
const onClose = () => {
data.value = props.modelValue;
emit('onClose');
selectVisiable.value = false;
};
const onClosed = () => {
clearState();
emit('onClosed');
};
// 清空状态
const clearState = () => {
filterForm.name = '';
pageForm.page = 1;
pageForm.limit = 10;
pageForm.total = 0;
listData.value = [];
// all数据不能清因为all只会在挂载的时候赋值一次
// listAllData.value = [];
};
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 netVisiable = ref<boolean>(false);
const netUrl = ref<string>('');
const netPrefix = ref<string>('HTTP://');
const netChange = () => {
let s = netUrl.value.trim();
if (s.toUpperCase().startsWith('HTTP://') || s.toUpperCase().startsWith('HTTPS://')) s = s.split('://')[1];
if (s.startsWith('/')) s = s.substring(1);
netUrl.value = s;
};
const confirmNetUrl = () => {
if (!netUrl.value) return;
netLoading.value = true;
let controller = new AbortController();
let timeout = setTimeout(() => {
controller.abort();
}, 10 * 1000);
fetch(netPrefix.value + netUrl.value, { signal: controller.signal }).then(async (res: Response) => {
clearTimeout(timeout);
if (!res.ok) errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
const _ = res.url.split('?')[0].split('/');
let filename = _[_.length - 1];
// let filetype = res.headers.get('content-type')?.split('/')[1] || '';
let blob = await res.blob();
let file = new File([blob], filename, { type: blob.type });
let form = new FormData();
form.append('file', file);
form.append('upload_method', '1');
fetch(getBaseURL() + 'api/system/file/', { method: 'post', body: form })
.then(() => successNotification('网络文件上传成功!'))
.then(() => { netVisiable.value = false; listRequest(); listRequestAll(); })
.catch(() => errorNotification('网络文件上传失败!'))
.then(() => netLoading.value = false);
}).catch((err: any) => {
console.log(err);
clearTimeout(timeout);
errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
netLoading.value = false;
});
};
// fs-crud部分
const data = ref<any>(null);
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
watch(
() => props.modelValue,
(val) => data.value = props.multiple ? JSON.parse(JSON.stringify(val)) : val,
{ immediate: true }
);
const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => {
let _v = null;
if (value) {
if (typeof value === 'string') _v = value.replace(/\\/g, '/');
else {
_v = [];
for (let i of value) _v.push(i.replace(/\\/g, '/'));
}
}
emit('update:modelValue', _v);
formValidator.onChange();
formValidator.onBlur();
};
defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
onMounted(() => {
if (props.multiple && !['selector', 'image'].includes(props.inputType))
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
listRequestAll();
console.log('fileselector tenentmdoe', isTenentMode);
console.log('fileselector supertenent', isSuperTenent);
});
</script>
<style scoped>
.form-display {
--fileselector-close-display: none;
overflow: hidden;
}
._overlay {
width: unset !important;
}
.headerBar>* {
display: flex;
justify-content: space-between;
gap: 12px;
}
:deep(.el-input-group__prepend) {
padding: 0 20px;
}
.listContainer {
display: grid;
justify-items: center;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: min-content;
grid-gap: 36px;
margin-top: 24px;
padding: 8px;
height: calc(50vh);
overflow-y: auto;
scrollbar-width: thin;
}
.listContainer>* {
aspect-ratio: 1 / 1;
box-shadow: 0 0 4px rgba(0, 0, 0, .2);
border-radius: 8px;
overflow: hidden;
}
.active {
box-shadow: 0 0 8px var(--el-color-primary);
}
.listPaginator {
display: flex;
justify-content: flex-end;
justify-items: center;
padding-top: 24px;
}
.addControllorHover {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
cursor: pointer;
border-radius: 8px;
border: 1px solid #dcdfe6;
}
.addControllorHover:hover {
border-color: #c0c4cc;
}
.closeHover {
display: var(--fileselector-close-display);
position: absolute;
right: 2px;
top: 2px;
cursor: pointer;
}
.itemList {
border: 1px solid #dcdfe6;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,7 @@
export const SHOW = {
IMAGE: 0b1000, // 图片
VIDEO: 0b0100, // 视频
AUDIO: 0b0010, // 音频
OTHER: 0b0001, // 其他
ALL: 0b1111, // 全部
};

View File

@@ -1,10 +1,17 @@
<template>
<el-select popper-class="popperClass" class="tableSelector" :multiple="props.tableConfig.isMultiple"
@remove-tag="removeTag" v-model="data" placeholder="请选择" @visible-change="visibleChange">
<el-select
popper-class="popperClass"
class="tableSelector"
multiple
:collapseTags="props.tableConfig.collapseTags"
@remove-tag="removeTag"
v-model="data"
placeholder="请选择"
@visible-change="visibleChange"
>
<template #empty>
<div class="option">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict"
@clear="getDict">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict" @clear="getDict">
<template #append>
<el-button type="primary" icon="Search" />
</template>
@@ -12,22 +19,34 @@
<el-table
ref="tableRef"
:data="tableData"
size="mini"
:size="props.tableConfig.size"
border
row-key="id"
style="width: 400px"
:lazy="props.tableConfig.lazy"
:load="props.tableConfig.load"
:tree-props="props.tableConfig.treeProps"
style="width: 600px"
max-height="200"
height="200"
:highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange"
@select="handleSelectionChange"
@selectAll="handleSelectionChange"
@current-change="handleCurrentChange"
>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55"/>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
<el-table-column fixed type="index" label="#" width="50" />
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
v-for="(item,index) in props.tableConfig.columns" :key="index"/>
<el-table-column
:prop="item.prop"
:label="item.label"
:width="item.width"
v-for="(item, index) in props.tableConfig.columns"
:key="index"
/>
</el-table>
<el-pagination style="margin-top: 10px" background
<el-pagination
style="margin-top: 10px"
background
v-model:current-page="pageConfig.page"
v-model:page-size="pageConfig.limit"
layout="prev, pager, next"
@@ -40,140 +59,157 @@
</template>
<script setup lang="ts">
import {defineProps, onMounted, reactive, ref, toRaw, watch} from 'vue'
import {dict} from '@fast-crud/fast-crud'
import XEUtils from 'xe-utils'
import {request} from '/@/utils/service'
import {computed, defineProps, onMounted, reactive, ref, watch} from 'vue';
import XEUtils from 'xe-utils';
import { request } from '/@/utils/service';
const props = defineProps({
modelValue: {},
modelValue: {
type: Array || String || Number,
default: () => []
},
tableConfig: {
type: Object,
default:{
url: null,
label: null, //显示值
value: null, //数据值
isTree: false,
lazy: true,
size:'default',
load: () => {},
data: [], //默认数据
isMultiple: false, //是否多选
collapseTags:false,
treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项
},
displayLabel: {}
} as any)
const emit = defineEmits(['update:modelValue'])
},
displayLabel: {},
} as any);
const emit = defineEmits(['update:modelValue']);
// tableRef
const tableRef = ref()
const tableRef = ref();
// template上使用data
const data = ref()
const data = ref();
// 多选值
const multipleSelection = ref()
watch(multipleSelection, // 监听multipleSelection的变化
(value) => {
const {tableConfig} = props
//是否多选
if (!tableConfig.isMultiple) {
data.value = value ? value[tableConfig.label] : null
} else {
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}
}, // 当multipleSelection值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
const multipleSelection = ref();
// 搜索值
const search = ref(undefined)
const search = ref(undefined);
//表格数据
const tableData = ref()
const tableData = ref([]);
// 分页的配置
const pageConfig = reactive({
page: 1,
limit: 10,
total: 0
})
total: 0,
});
/**
* 表格多选
* @param val:Array
*/
const handleSelectionChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
const { tableConfig } = props;
const result = val.map((item: any) => {
return item[tableConfig.value]
})
emit('update:modelValue', result)
}
return item[tableConfig.value];
});
data.value = val.map((item: any) => {
return item[tableConfig.label];
});
emit('update:modelValue', result);
};
/**
* 表格单选
* @param val:Object
*/
const handleCurrentChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
emit('update:modelValue', val[tableConfig.value])
const { tableConfig } = props;
if (!tableConfig.isMultiple && val) {
// data.value = [val[tableConfig.label]];
emit('update:modelValue', val[tableConfig.value]);
}
};
/**
* 获取字典值
*/
const getDict = async () => {
const url = props.tableConfig.url
const url = props.tableConfig.url;
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
search: search.value
}
search: search.value,
};
const { data, page, limit, total } = await request({
url: url,
params:params
})
pageConfig.page = page
pageConfig.limit = limit
pageConfig.total = total
params: params,
});
pageConfig.page = page;
pageConfig.limit = limit;
pageConfig.total = total;
if (props.tableConfig.data === undefined || props.tableConfig.data.length === 0) {
if (props.tableConfig.isTree) {
tableData.value = XEUtils.toArrayTree(data, {parentKey: 'parent', key: 'id', children: 'children'})
tableData.value = XEUtils.toArrayTree(data, { parentKey: 'parent', key: 'id', children: 'children' });
} else {
tableData.value = data
tableData.value = data;
}
} else {
tableData.value = props.tableConfig.data
tableData.value = props.tableConfig.data;
}
};
// 获取节点值
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
*/
const visibleChange = (bool: any) => {
if (bool) {
getDict()
}
getDict();
}
};
/**
* 分页
* @param page
*/
const handlePageChange = (page: any) => {
pageConfig.page = page
getDict()
}
// 监听displayLabel的变化更新数据
watch(() => {
return props.displayLabel
}, (value) => {
const {tableConfig} = props
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}, {immediate: true})
pageConfig.page = page;
getDict();
};
onMounted(()=>{
setTimeout(()=>{
getNodeValues()
},1000)
})
</script>
@@ -184,7 +220,6 @@ watch(() => {
padding: 5px;
background-color: #fff;
}
</style>
<style lang="scss">
.popperClass {
@@ -196,7 +231,8 @@ watch(() => {
}
.tableSelector {
.el-icon, .el-tag__close {
.el-icon,
.el-tag__close {
display: none;
}
}

View File

@@ -3,6 +3,7 @@ export default {
label: {
one1: 'User name login',
two2: 'Mobile number',
changePwd: 'Change The Password',
},
link: {
one3: 'Third party login',

View File

@@ -3,6 +3,7 @@ export default {
label: {
one1: '账号密码登录',
two2: '手机号登录',
changePwd: '密码修改',
},
link: {
one3: '第三方登录',
@@ -12,6 +13,8 @@ export default {
accountPlaceholder1: '请输入登录账号/邮箱/手机号',
accountPlaceholder2: '请输入登录密码',
accountPlaceholder3: '请输入验证码',
accountPlaceholder4:'请输入新密码',
accountPlaceholder5:'请再次输入新密码',
accountBtnText: '登 录',
},
mobile: {

View File

@@ -3,6 +3,7 @@ export default {
label: {
one1: '用戶名登入',
two2: '手機號登入',
changePwd: '密码修改',
},
link: {
one3: '協力廠商登入',

View File

@@ -17,13 +17,15 @@ import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import mittBus from '/@/utils/mitt';
import { useRoute } from 'vue-router';
const route = useRoute();
// 引入组件
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
// 定义变量内容
const layoutAsideScrollbarRef = ref();
const routesIndex = ref(0);
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes();
@@ -83,10 +85,36 @@ const closeLayoutAsideMobileMode = () => {
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', '');
};
const findFirstLevelIndex = (data, path) => {
for (let index = 0; index < data.length; index++) {
const item = data[index];
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
if (item.children && item.children.length > 0) {
// 检查子菜单中是否有匹配的路径
const childIndex = item.children.findIndex((child) => child.path === path);
if (childIndex !== -1) {
return index; // 返回当前一级菜单的索引
}
// 递归查找子菜单
const foundIndex = findFirstLevelIndex(item.children, path);
if (foundIndex !== null) {
return index; // 返回找到的索引
}
}
}
return null; // 找不到路径时返回 null
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
const setFilterRoutes = (path='') => {
if (themeConfig.value.layout === 'columns') return false;
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
// 获取当前地址的索引,不用从参数选取
routesIndex.value = findFirstLevelIndex(routesList.value,path || route.path) || 0
state.menuList = filterRoutesFun(routesList.value[routesIndex.value].children || [routesList.value[routesIndex.value]]);
} else {
state.menuList = filterRoutesFun(routesList.value);
}
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
@@ -122,7 +150,8 @@ onBeforeMount(() => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = [];
state.menuList = res.children;
// state.menuList = res.children;
setFilterRoutes(res.path);
}
});
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {

View File

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

View File

@@ -37,7 +37,7 @@
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<el-popover placement="bottom" trigger="hover" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')">
@@ -58,7 +58,7 @@
></i>
</div>
<div>
<span v-if="!isSocketOpen">
<span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm
width="250"
ref="onlinePopoverRef"
@@ -71,7 +71,7 @@
>
<template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
<img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge>
</template>
</el-popconfirm>
@@ -93,7 +93,7 @@
<el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item command="/versionUpgradeLog">更新日志</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -250,6 +250,7 @@ onMounted(() => {
//消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter';
import {getBaseURL} from "/@/utils/baseUrl";
const messageCenter = messageCenterStore();
</script>

View File

@@ -1,8 +1,8 @@
<template>
<div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal">
<template v-for="val in menuLists">
<!-- <el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">-->
<el-menu :default-active="defaultActive" background-color="transparent" mode="horizontal">
<template v-for="(val,index) in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon" />
@@ -11,7 +11,7 @@
<SubItem :chil="val.children" />
</el-sub-menu>
<template v-else>
<el-menu-item :index="val.path" :key="val.path">
<el-menu-item :index="val.path" :key="val.path" style="--el-menu-active-color: #fff" @click="onToRouteClick(val,index)">
<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
@@ -26,22 +26,25 @@
</template>
</template>
</el-menu>
</el-scrollbar>
<!-- </el-scrollbar>-->
</div>
</template>
<script setup lang="ts" name="navMenuHorizontal">
import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue';
import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
import {useRoute, onBeforeRouteUpdate, RouteRecordRaw, useRouter} from 'vue-router';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
const router = useRouter()
// 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
const state = reactive<AsideState>({
menuList: [],
clientWidth: 0
});
// 定义父组件传过来的值
const props = defineProps({
// 菜单列表
@@ -58,19 +61,33 @@ const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const state = reactive({
defaultActive: '' as string | undefined,
});
const defaultActive = ref('')
// 获取父级菜单数据
const menuLists = computed(() => {
<RouteItems>props.menuList.shift()
return <RouteItems>props.menuList;
});
// 设置横向滚动条可以鼠标滚轮滚动
const onElMenuHorizontalScroll = (e: WheelEventType) => {
const eventDelta = e.wheelDelta || -e.deltaY * 40;
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft + eventDelta / 4;
// 递归获取当前路由的顶级索引
const findFirstLevelIndex = (data, path) => {
for (let index = 0; index < data.length; index++) {
const item = data[index];
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
if (item.children && item.children.length > 0) {
// 检查子菜单中是否有匹配的路径
const childIndex = item.children.findIndex((child) => child.path === path);
if (childIndex !== -1) {
return index; // 返回当前一级菜单的索引
}
// 递归查找子菜单
const foundIndex = findFirstLevelIndex(item.children, path);
if (foundIndex !== null) {
return index; // 返回找到的索引
}
}
}
return null; // 找不到路径时返回 null
};
// 初始化数据,页面刷新时,滚动条滚动到对应位置
const initElMenuOffsetLeft = () => {
nextTick(() => {
@@ -107,17 +124,41 @@ const setSendClassicChildren = (path: string) => {
const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
const { path, meta } = currentRoute;
if (themeConfig.value.layout === 'classic') {
state.defaultActive = `/${path?.split('/')[1]}`;
let firstLevelIndex = (findFirstLevelIndex(routesList.value, route.path) || 0) - 1
defaultActive.value = firstLevelIndex < 0 ? defaultActive.value : menuLists.value[firstLevelIndex].path
} else {
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
else state.defaultActive = path;
if (pathSplit.length >= 4 && meta?.isHide) defaultActive.value = pathSplit.splice(0, 3).join('/');
else defaultActive.value = path;
}
};
// 打开外部链接
const onALinkClick = (val: RouteItem) => {
other.handleOpenLink(val);
};
// 跳转页面
const onToRouteClick = (val: RouteItem,index) => {
// 跳转到子级页面
let children = val.children
if (children === undefined){
defaultActive.value = val.path
children = setSendClassicChildren(val.path).children
}
if (children.length >= 1){
if (children[0].is_catalog) {
onToRouteClick(children[0],index)
return
}
router.push(children[0].path)
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', children[0]);
}
} else {
router.push('/home')
}
};
// 页面加载前
onBeforeMount(() => {
setCurrentRouterHighlight(route);
@@ -126,16 +167,6 @@ onBeforeMount(() => {
onMounted(() => {
initElMenuOffsetLeft();
});
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
setCurrentRouterHighlight(to);
// 修复经典布局开启切割菜单时点击tagsView后左侧导航菜单数据不变的问题
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
}
});
</script>
<style scoped lang="scss">

View File

@@ -8,7 +8,21 @@
<sub-item :chil="val.children" />
</el-sub-menu>
<template v-else>
<el-menu-item :index="val.path" :key="val.path">
<a v-if="val.name==='templateCenter'" href="#/templateCenter" target="_blank">
<el-menu-item :key="val.path">
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<template v-else>
<a class="w100" @click.prevent="onALinkClick(val)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</a>
</template>
</el-menu-item>
</a>
<el-menu-item v-else :index="val.path" :key="val.path">
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>

View File

@@ -23,6 +23,7 @@
<script setup lang="ts" name="layoutIframeView">
import { computed, watch, ref, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import {cookie} from "xe-utils";
// 定义父组件传过来的值
const props = defineProps({
@@ -49,7 +50,15 @@ const route = useRoute();
// 处理 list 列表,当打开时,才进行加载
const setIframeList = computed(() => {
return (<RouteItems>props.list).filter((v: RouteItem) => v.meta?.isIframeOpen);
return (<RouteItems>props.list).filter((v: RouteItem) => {
if (v.meta?.isIframeOpen) {
const isLink = v.meta?.isLink || '';
if (isLink.includes("{{token}}")) {
v.meta.isLink = isLink.replace("{{token}}", cookie.get('token'))
}
}
return v.meta?.isIframeOpen
});
});
// 获取 iframe 当前路由 path
const getRoutePath = computed(() => {

View File

@@ -17,19 +17,31 @@
import { reactive, watch } from 'vue';
import { useRoute } from 'vue-router';
import { verifyUrl } from '/@/utils/toolsValidate';
import {cookie} from "xe-utils";
// 定义变量内容
const route = useRoute();
const state = reactive<LinkViewState>({
title: '',
isLink: '',
query: null
});
// 立即前往
const onGotoFullPage = () => {
const { origin, pathname } = window.location;
if (state.isLink.includes("{{token}}")) {
state.isLink = state.isLink.replace("{{token}}", cookie.get('token'))
}
if (verifyUrl(<string>state.isLink)) window.open(state.isLink);
else window.open(`${origin}${pathname}#${state.isLink}`);
else {
function objectToUrlParams(obj: { [key: string]: string | number }): string {
return Object.keys(obj)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
.join('&');
}
window.open(`${origin}${pathname}#${state.isLink}?${objectToUrlParams(state.query)}`)
};
};
// 监听路由的变化,设置内容
watch(
@@ -37,6 +49,7 @@ watch(
() => {
state.title = <string>route.meta.title;
state.isLink = <string>route.meta.isLink;
state.query = <any>route.query;
},
{
immediate: true,

View File

@@ -21,13 +21,14 @@ const menuApi = useMenuApi();
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
const greatDream: any = import.meta.glob('@great-dream/**/*.{vue,tsx}');
/**
* 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules });
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }, { ...greatDream });
/**
* 后端控制路由:初始化方法,防止刷新时路由丢失
@@ -44,7 +45,7 @@ export async function initBackEndControlRoutes() {
if (!Session.get('token')) return false;
// 触发初始化用户信息 pinia
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
await useUserInfo().setUserInfos();
await useUserInfo().getApiUserInfo();
// 获取路由菜单数据
const res = await getBackEndControlRoutes();
// 无登录权限时,添加判断
@@ -198,7 +199,10 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
const k0 = k.replace("ode_modules/@great-dream/", '')
const k1 = k0.replace("/plugins", '')
const newComponent = component.replace("plugins/", "")
return k1.startsWith(`${newComponent}`) || k1.startsWith(`/${newComponent}`);
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];

View File

@@ -28,6 +28,8 @@ import {checkVersion} from "/@/utils/upgrade";
const storesThemeConfig = useThemeConfig(pinia);
const {themeConfig} = storeToRefs(storesThemeConfig);
const {isRequestRoutes} = themeConfig.value;
import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo());
/**
* 创建一个可以被 Vue 应用程序使用的路由实例
@@ -94,6 +96,8 @@ export function formatTwoStageRoutes(arr: any) {
return newArr;
}
const frameOutRoutes = staticRoutes.map(item => item.path)
// 路由加载前
router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
@@ -109,11 +113,15 @@ router.beforeEach(async (to, from, next) => {
next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
Session.clear();
NProgress.done();
} else if (token && to.path === '/login') {
}else if (token && to.path === '/login' && userInfos.value.pwd_change_count===0 ) {
next('/login');
NProgress.done();
} else if (token && to.path === '/login' && userInfos.value.pwd_change_count>0) {
next('/home');
NProgress.done();
}else if(token && frameOutRoutes.includes(to.path) ){
next()
} else {
const storesRoutesList = useRoutesList(pinia);
const {routesList} = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) {

View File

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

View File

@@ -12,6 +12,7 @@ export interface UserInfosState {
email: string;
mobile: string;
gender: string;
pwd_change_count:null|number;
dept_info: {
dept_id: number;
dept_name: string;

View File

@@ -2,6 +2,9 @@ import { defineStore } from 'pinia';
import { UserInfosStates } from './interface';
import { Session } from '/@/utils/storage';
import { request } from '../utils/service';
import { getBaseURL } from '../utils/baseUrl';
import headerImage from '/@/assets/img/headerImage.png';
/**
* 用户信息
* @methods setUserInfos 设置用户信息
@@ -15,6 +18,7 @@ export const useUserInfo = defineStore('userInfo', {
email: '',
mobile: '',
gender: '',
pwd_change_count:null,
dept_info: {
dept_id: 0,
dept_name: '',
@@ -29,16 +33,19 @@ export const useUserInfo = defineStore('userInfo', {
isSocketOpen: false
}),
actions: {
async updateUserInfos() {
let userInfos: any = await this.getApiUserInfo();
this.userInfos.username = userInfos.data.name;
this.userInfos.avatar = userInfos.data.avatar;
this.userInfos.name = userInfos.data.name;
this.userInfos.email = userInfos.data.email;
this.userInfos.mobile = userInfos.data.mobile;
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
async setPwdChangeCount(count: number) {
this.userInfos.pwd_change_count = count;
},
async updateUserInfos(userInfos:any) {
this.userInfos.username = userInfos.name;
this.userInfos.avatar = userInfos.avatar;
this.userInfos.name = userInfos.name;
this.userInfos.email = userInfos.email;
this.userInfos.mobile = userInfos.mobile;
this.userInfos.gender = userInfos.gender;
this.userInfos.dept_info = userInfos.dept_info;
this.userInfos.role_info = userInfos.role_info;
this.userInfos.pwd_change_count = userInfos.pwd_change_count;
Session.set('userInfo', this.userInfos);
},
async setUserInfos() {
@@ -55,6 +62,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
Session.set('userInfo', this.userInfos);
}
},
@@ -65,7 +73,18 @@ export const useUserInfo = defineStore('userInfo', {
return request({
url: '/api/system/user/user_info/',
method: 'get',
});
}).then((res:any)=>{
this.userInfos.username = res.data.name;
this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage;
this.userInfos.name = res.data.name;
this.userInfos.email = res.data.email;
this.userInfos.mobile = res.data.mobile;
this.userInfos.gender = res.data.gender;
this.userInfos.dept_info = res.data.dept_info;
this.userInfos.role_info = res.data.role_info;
this.userInfos.pwd_change_count = res.data.pwd_change_count;
Session.set('userInfo', this.userInfos);
})
},
},
});

View File

@@ -56,4 +56,5 @@ declare type ParentViewState<T = any> = {
declare type LinkViewState = {
title: string;
isLink: string;
query: any;
};

View File

@@ -1,3 +1,4 @@
import XEUtils from 'xe-utils';
import {useColumnPermission} from '/@/stores/columnPermission';
type permissionType = 'is_create' | 'is_query' | 'is_update';
@@ -22,41 +23,18 @@ export const handleColumnPermission = async (func: Function, crudOptions: any,ex
}
}
const columns = crudOptions.columns;
const excludeColumns = ['_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
for (let col in columns) {
if (excludeColumns.includes(col)) {
continue
}else{
if (columns[col].column) {
columns[col].column.show = false
} else {
columns[col]['column'] = {
show: false
}
}
columns[col].addForm = {
show: false
}
columns[col].editForm = {
show: false
}
}
for (let item of res.data) {
if (excludeColumns.includes(item.field_name)) {
continue
} else if(item.field_name === col) {
columns[col].column.show = item['is_query']
const excludeColumns = ['checked','_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
XEUtils.eachTree(columns, (item, key) => {
if (!excludeColumns.includes(String(key)) && key in res.data) {
// 如果列表不可见,则禁止在列设置中选择
if(!item['is_query'])columns[col].column.columnSetDisabled = true
columns[col].addForm = {
show: item['is_create']
}
columns[col].editForm = {
show: item['is_update']
}
}
// 只有列表不可见,才修改列配置,这样才不影响默认的配置
if (!res.data[key]['is_query']) {
item.column.show = false;
item.column.columnSetDisabled = true;
}
item.addForm = { show: res.data[key]['is_create'] };
item.editForm = { show: res.data[key]['is_update'] };
}
});
return crudOptions
}

View File

@@ -1,45 +1,65 @@
import {dict} from "@fast-crud/fast-crud";
import {shallowRef} from 'vue'
import deptFormat from "/@/components/dept-format/index.vue";
import {dict} from '@fast-crud/fast-crud';
import {shallowRef} from 'vue';
import deptFormat from '/@/components/dept-format/index.vue';
export const commonCrudConfig = (options = {
create_datetime: {
form: false,
table: false,
search: false
},
update_datetime: {
form: false,
table: false,
search: false
},
creator_name: {
form: false,
table: false,
search: false
},
modifier_name: {
form: false,
table: false,
search: false
},
dept_belong_id: {
form: false,
table: false,
search: false
},
description: {
form: false,
table: false,
search: false
},
}) => {
/** 1. 每个字段可选属性 */
export interface CrudFieldOption {
form?: boolean;
table?: boolean;
search?: boolean;
width?: number;
}
/** 2. 总配置接口 */
export interface CrudOptions {
create_datetime?: CrudFieldOption;
update_datetime?: CrudFieldOption;
creator_name?: CrudFieldOption;
modifier_name?: CrudFieldOption;
dept_belong_id?: CrudFieldOption;
description?: CrudFieldOption;
}
/** 3. 默认完整配置 */
const defaultOptions: Required<CrudOptions> = {
create_datetime: { form: false, table: false, search: false, width: 160 },
update_datetime: { form: false, table: false, search: false, width: 160 },
creator_name: { form: false, table: false, search: false, width: 100 },
modifier_name: { form: false, table: false, search: false, width: 100 },
dept_belong_id: { form: false, table: false, search: false, width: 300 },
description: { form: false, table: false, search: false, width: 100 },
};
/** 4. mergeOptions 函数 */
function mergeOptions(baseOptions: Required<CrudOptions>, userOptions: CrudOptions = {}): Required<CrudOptions> {
const result = { ...baseOptions };
for (const key in userOptions) {
if (Object.prototype.hasOwnProperty.call(userOptions, key)) {
const baseField = result[key as keyof CrudOptions];
const userField = userOptions[key as keyof CrudOptions];
if (baseField && userField) {
result[key as keyof CrudOptions] = { ...baseField, ...userField };
}
}
}
return result;
}
/**
* 最终暴露的 commonCrudConfig
* @param options 用户自定义配置(可传可不传,不传就用默认)
*/
export const commonCrudConfig = (options: CrudOptions = {}) => {
// ① 合并
const merged = mergeOptions(defaultOptions, options);
// ② 用 merged 中的值生成真正的 CRUD 配置
return {
dept_belong_id: {
title: '所属部门',
type: 'dict-tree',
search: {
show: options.dept_belong_id?.search || false
show: merged.dept_belong_id.search,
},
dict: dict({
url: '/api/system/dept/all_dept/',
@@ -50,90 +70,92 @@ export const commonCrudConfig = (options = {
}),
column: {
align: 'center',
width: 300,
show: options.dept_belong_id?.table || false,
width: merged.dept_belong_id.width,
show: merged.dept_belong_id.table,
component: {
name: shallowRef(deptFormat),
vModel: "modelValue",
}
// fast-crud里自定义组件常用"component.is"
is: shallowRef(deptFormat),
vModel: 'modelValue',
},
},
form: {
show: options.dept_belong_id?.form || false,
show: merged.dept_belong_id.form,
component: {
multiple: false,
clearable: true,
props: {
checkStrictly: true,
props: {
// 为什么这里要写两层props
// 因为props属性名与fs的动态渲染的props命名冲突所以要多写一层
label: "name",
value: "id",
}
}
label: 'name',
value: 'id',
},
},
},
helper: '默认不填则为当前创建用户的部门ID',
},
helper: "默认不填则为当前创建用户的部门ID"
}
},
description: {
title: '备注',
search: {
show: options.description?.search || false
show: merged.description.search,
},
type: 'textarea',
column: {
width: 100,
show: options.description?.table || false,
width: merged.description.width,
show: merged.description.table,
},
form: {
show: options.description?.form || false,
show: merged.description.form,
component: {
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
}
},
},
viewForm: {
show: true
}
show: true,
},
},
modifier_name: {
title: '修改人',
search: {
show: options.modifier_name?.search || false
show: merged.modifier_name.search,
},
column: {
width: 100,
show: options.modifier_name?.table || false,
width: merged.modifier_name.width,
show: merged.modifier_name.table,
},
form: {
show: false,
show: merged.modifier_name.form,
},
viewForm: {
show: true
}
show: true,
},
},
creator_name: {
title: '创建人',
search: {
show: options.creator_name?.search || false
show: merged.creator_name.search,
},
column: {
width: 100,
show: options.creator_name?.table || false,
width: merged.creator_name.width,
show: merged.creator_name.table,
},
form: {
show: false,
show: merged.creator_name.form,
},
viewForm: {
show: true
}
show: true,
},
},
update_datetime: {
title: '更新时间',
type: 'datetime',
search: {
show: options.update_datetime?.search || false,
show: merged.update_datetime.search,
col: { span: 8 },
component: {
type: 'datetimerange',
@@ -142,61 +164,64 @@ export const commonCrudConfig = (options = {
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
shortcuts: [
{
text: '最近一周',
onClick(picker) {
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
},
},
{
text: '最近一个月',
onClick(picker) {
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
},
},
{
text: '最近三个月',
onClick(picker) {
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
},
],
},
},
},
valueResolve(context: any) {
const {key, value} = context
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
const { value } = context;
if (value) {
context.form.update_datetime_after = value[0]
context.form.update_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
context.form.update_datetime_after = value[0];
context.form.update_datetime_before = value[1];
delete context.form.update_datetime;
}
},
},
column: {
width: 160,
show: options.update_datetime?.table || false,
width: merged.update_datetime.width,
show: merged.update_datetime.table,
},
form: {
show: false,
show: merged.update_datetime.form,
},
viewForm: {
show: true
}
show: true,
},
},
create_datetime: {
title: '创建时间',
type: 'datetime',
search: {
show: options.create_datetime?.search || false,
show: merged.create_datetime.search,
col: { span: 8 },
component: {
type: 'datetimerange',
@@ -205,55 +230,57 @@ export const commonCrudConfig = (options = {
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
shortcuts: [
{
text: '最近一周',
onClick(picker) {
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
},
},
{
text: '最近一个月',
onClick(picker) {
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
},
},
{
text: '最近三个月',
onClick(picker) {
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
},
],
},
},
},
valueResolve(context: any) {
const {key, value} = context
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
const { value } = context;
if (value) {
context.form.create_datetime_after = value[0]
context.form.create_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
context.form.create_datetime_after = value[0];
context.form.create_datetime_before = value[1];
delete context.form.create_datetime;
}
},
},
column: {
width: 160,
show: options.create_datetime?.table || false,
width: merged.create_datetime.width,
show: merged.create_datetime.table,
},
form: {
show: false
show: merged.create_datetime.form,
},
viewForm: {
show: true
}
}
}
}
show: true,
},
},
};
};

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

@@ -4,7 +4,7 @@ import { DictionaryStore } from '/@/stores/dictionary';
/**
* @method 获取指定name字典
*/
export const dictionary = (name: string,key:string|number|undefined) => {
export const dictionary = (name: string,key?:string|number|undefined) => {
const dict = DictionaryStore()
const dictionary = toRaw(dict.data)
if(key!=undefined){

View File

@@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts';
import { Local, Session } from '/@/utils/storage';
import qs from 'qs';
import { getBaseURL } from './baseUrl';
import { successMessage } from './message.js';
/**
* @description 创建请求实例
*/
@@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock);
* @param filename
*/
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
// return request({ url: url, method: method, params: params })
// .then((res: any) => successMessage(res.msg));
request({
url: url,
method: method,
@@ -211,6 +214,9 @@ export const downloadFile = function ({ url, params, method, filename = '文件
responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then((res: any) => {
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载
// if (res.headers && res.headers['Content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
const fileName = xlsxName || `${filename}.xlsx`
if (res) {

View File

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

View File

@@ -1,46 +0,0 @@
import axios from 'axios'
import VFormRender from '@/components/form-render/index.vue'
import ContainerItems from '@/components/form-render/container-item/index'
import {registerIcon} from '@/utils/el-icons'
import 'virtual:svg-icons-register'
import '@/iconfont/iconfont.css'
import { installI18n } from '@/utils/i18n'
import { loadExtension } from '@/extension/extension-loader'
VFormRender.install = function (app) {
installI18n(app)
loadExtension(app)
app.use(ContainerItems)
registerIcon(app)
app.component(VFormRender.name, VFormRender)
}
const components = [
VFormRender
]
const install = (app) => {
installI18n(app)
loadExtension(app)
app.use(ContainerItems)
registerIcon(app)
components.forEach(component => {
app.component(component.name, component)
})
window.axios = axios
}
if (typeof window !== 'undefined' && window.Vue) { /* script<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>ֵaxios<EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
//window.axios = axios
}
export default {
install,
VFormRender
}

View File

@@ -2,7 +2,7 @@ import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
export let pluginsAll: any = [];
// 扫描插件目录并注册插件
export const scanAndInstallPlugins = (app: any) => {
const components = import.meta.glob('./**/*.vue');
const components = import.meta.glob('./**/*.ts');
const pluginNames = new Set();
// 遍历对象并注册异步组件
for (const [key, value] of Object.entries(components)) {
@@ -11,6 +11,24 @@ export const scanAndInstallPlugins = (app: any) => {
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
pluginNames.add(pluginsName);
}
const dreamComponents = import.meta.glob('/node_modules/@great-dream/**/*.ts');
// 遍历对象并注册异步组件
for (let [key, value] of Object.entries(dreamComponents)) {
key = key.replace('node_modules/@great-dream/', '');
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'));
app.component(name, defineAsyncComponent(value as AsyncComponentLoader));
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
pluginNames.add(pluginsName);
}
pluginsAll = Array.from(pluginNames);
console.log('已发现插件:', pluginsAll);
for (const pluginName of pluginsAll) {
const plugin = import(`./${pluginName}/index.ts`);
plugin.then((module) => {
app.use(module.default)
console.log(`${pluginName}插件已加载`)
}).catch((error) => {
console.log(`${pluginName}插件下无index.ts`)
})
}
};

View File

@@ -1,17 +1,10 @@
import * as api from './api';
import {
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
compute,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import {auth} from "/@/utils/authFunction";
import { auth } from '/@/utils/authFunction';
import tableSelector from '/@/components/tableSelector/index.vue';
import { shallowRef } from 'vue';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
@@ -51,8 +44,8 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps)
buttons: {
add: {
show: auth('area:Create'),
}
}
},
},
},
rowHandle: {
//固定右侧
@@ -65,12 +58,12 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps)
edit: {
iconRight: 'Edit',
type: 'text',
show: auth('area:Update')
show: auth('area:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: auth('area:Delete')
show: auth('area:Delete'),
},
},
},
@@ -94,30 +87,6 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps)
columnSetDisabled: true, //禁止在列设置中选择
},
},
// pcode: {
// title: '父级地区',
// show: false,
// search: {
// show: true,
// },
// type: 'dict-tree',
// form: {
// component: {
// showAllLevels: false, // 仅显示最后一级
// props: {
// elProps: {
// clearable: true,
// showAllLevels: false, // 仅显示最后一级
// props: {
// checkStrictly: true, // 可以不需要选到最后一级
// emitPath: false,
// clearable: true,
// },
// },
// },
// },
// },
// },
name: {
title: '名称',
search: {
@@ -138,6 +107,50 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps)
},
},
},
pcode: {
title: '父级地区',
search: {
disabled: true,
},
width: 130,
type: 'table-selector',
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ row }) => {
if (row) {
return row.pcode_info;
}
return null;
}),
tableConfig: {
url: '/api/system/area/',
label: 'name',
value: 'id',
isTree: true,
isMultiple: false,
lazy: true,
load: loadContentMethod,
treeProps: { children: 'children', hasChildren: 'hasChild' },
columns: [
{
prop: 'name',
label: '地区',
width: 150,
},
{
prop: 'code',
label: '地区编码',
},
],
},
},
},
column: {
show: false,
},
},
code: {
title: '地区编码',
search: {
@@ -157,61 +170,6 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps)
},
},
},
pinyin: {
title: '拼音',
search: {
disabled: true,
},
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{required: true, message: '拼音必填项'},
],
component: {
placeholder: '请输入拼音',
},
},
},
level: {
title: '地区层级',
search: {
disabled: true,
},
type: 'input',
column: {
minWidth: 100,
},
form: {
disabled: false,
rules: [
// 表单校验规则
{required: true, message: '拼音必填项'},
],
component: {
placeholder: '请输入拼音',
},
},
},
initials: {
title: '首字母',
column: {
minWidth: 100,
},
form: {
rules: [
// 表单校验规则
{required: true, message: '首字母必填项'},
],
component: {
placeholder: '请输入首字母',
},
},
},
enable: {
title: '是否启用',
search: {

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 v-else-if="item.form_item_type_label === 'array'" :key="index + 10">
<vxe-table
border
resizable
auto-resize
show-overflow
keep-source
:ref="'xTable_' + item.key"
height="200"
:edit-rules="validRules"
:edit-config="{ trigger: 'click', mode: 'row', showStatus: true }"
>
<vxe-column field="title" title="标题" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.title" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="key" title="键名" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.key" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="value" title="键值" :edit-render="{}">
<template #edit="{ row }">
<vxe-input v-model="row.value" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column title="操作" width="100" show-overflow>
<template #default="{ row, index }">
<el-popover placement="top" width="160" v-model="childRemoveVisible">
<p>删除后无法恢复,确定删除吗?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="childRemoveVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="onRemoveChild(row, index, item.key)">确定</el-button>
</div>
<el-button type="text" slot="reference">删除</el-button>
</el-popover>
</template>
</vxe-column>
</vxe-table>
<div>
<el-button size="mini" @click="onAppend('xTable_' + item.key)">追加</el-button>
</div>
<crudTable v-model="formData[item.key]"></crudTable>
</div>
</el-col>
<el-col :span="2" :offset="1">
@@ -248,32 +207,11 @@ import type { FormInstance, FormRules, TableInstance } from 'element-plus';
import { successMessage, errorMessage } from '/@/utils/message';
import { Session } from '/@/utils/storage';
import {Edit,Finished,Delete} from "@element-plus/icons-vue";
import crudTable from "./components/crudTable.vue"
const props = defineProps(['options', 'editableTabsItem']);
let formData: any = reactive({});
let formData: any = ref({});
let formList: any = ref([]);
let childTableData = ref([]);
let childRemoveVisible = ref(false);
const validRules = reactive<FormRules>({
title: [
{
required: true,
message: '必须填写',
},
],
key: [
{
required: true,
message: '必须填写',
},
],
value: [
{
required: true,
message: '必须填写',
},
],
});
const formRef = ref<FormInstance>()
let uploadUrl = ref(getBaseURL() + 'api/system/file/');
let uploadHeaders = ref({
@@ -294,65 +232,27 @@ const getInit = () => {
if (item.value) {
_formData[key] = item.value;
} else {
if ([5, 12, 14].indexOf(item.form_item_type) !== -1) {
_formData[key] = [];
if ([5, 12,11, 14].indexOf(item.form_item_type) !== -1) {
_formData[key] = item.value || [];
} else {
_formData[key] = item.value;
}
}
if (item.form_item_type_label === 'array') {
console.log('test');
nextTick(() => {
const tableName = 'xTable_' + key;
const tabelRef = ref<TableInstance>();
console.log(tabelRef);
// const $table = this.$refs[tableName][0];
// $table.loadData(item.chinldern);
});
}
}
formData = Object.assign(formData, _formData)
formData.value = Object.assign({}, _formData)
});
};
// 提交数据
const onSubmit = (formEl: FormInstance | undefined) => {
// const form = JSON.parse(JSON.stringify(form));
const keys = Object.keys(formData);
const values = Object.values(formData);
const keys = Object.keys(formData.value);
const values = Object.values(formData.value);
for (const index in formList.value) {
const item = formList.value[index];
// eslint-disable-next-line camelcase
const form_item_type_label = item.form_item_type_label;
// eslint-disable-next-line camelcase
if (form_item_type_label === 'array') {
const parentId = item.id;
const tableName = 'xTable_' + item.key;
// const $table = this.$refs[tableName][0];
// const { tableData } = $table.getTableData();
// for (const child of tableData) {
// if (!child.id && child.key && child.value) {
// child.parent = parentId;
// child.id = null;
// formList.push(child);
// }
// }
// // 必填项的判断
// for (const arr of item.rule) {
// if (arr.required && tableData.length === 0) {
// errorMessage(item.title + '不能为空');
// return;
// }
// }
// item.value = tableData;
}
// 赋值操作
keys.map((mapKey, mapIndex) => {
keys.forEach((mapKey, mapIndex) => {
if (mapKey === item.key) {
if (item.form_item_type_label !== 'array') {
item.value = values[mapIndex];
}
// 必填项的验证
if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) {
for (const arr of item.rule) {
@@ -380,39 +280,6 @@ const onSubmit = (formEl: FormInstance | undefined) => {
});
};
// 追加
const onAppend = (tableName: any) => {
// const $table = this.$refs[tableName][0];
// const { tableData } = $table.getTableData();
// const tableLength = tableData.length;
// if (tableLength === 0) {
// const { row: newRow } = $table.insert();
// console.log(newRow);
// } else {
// const errMap = $table.validate().catch((errMap: any) => errMap);
// if (errMap) {
// errorMessage('校验不通过!');
// } else {
// const { row: newRow } = $table.insert();
// console.log(newRow);
// }
// }
};
// 子表删除
const onRemoveChild = (row: any, index: any, refName: any) => {
console.log(row, index);
if (row.id) {
api.DelObj(row.id).then((res: any) => {
// this.refreshView();
});
} else {
// this.childTableData.splice(index, 1);
// const tableName = 'xTable_' + refName;
// const tableData = this.$refs[tableName][0].remove(row);
// console.log(tableData);
}
};
// 图片预览
const handlePictureCardPreview = (file: any) => {

View File

@@ -100,10 +100,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
placement: 'top',
content: '重设密码',
},
click: (ctx: any) => {
const { row } = ctx;
context?.handleResetPwdOpen(row);
},
click: (ctx: any) => context?.handleResetPwdOpen(ctx.row),
},
},
},
@@ -263,10 +260,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
}),
column: {
minWidth: 200, //最小列宽
formatter({value,row,index}){
const values = row.role_info.map((item:any) => item.name);
return values.join(',')
}
// formatter({ value, row, index }) {
// const values = row.role_info.map((item: any) => item.name);
// return values.join(',')
// }
},
form: {
rules: [
@@ -385,12 +382,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
},
avatar: {
title: '头像',
type: 'avatar-cropper',
type: 'avatar-uploader',
form: {
show: false,
},
column: {
width:150,
width: 100,
showOverflowTooltip: true,
}
},

View File

@@ -42,6 +42,15 @@
<template #actionbar-right>
<importExcel api="api/system/user/" v-auth="'user:Import'">导入 </importExcel>
</template>
<template #cell_avatar="scope">
<div v-if="scope.row.avatar" style="display: flex; justify-content: center; align-items: center;">
<el-image
style="width: 50px; height: 50px; border-radius: 50%; aspect-ratio: 1 /1 ; "
:src="getBaseURL(scope.row.avatar)"
:preview-src-list="[getBaseURL(scope.row.avatar)]"
:preview-teleported="true" />
</div>
</template>
</fs-crud>
<el-dialog v-model="resetPwdVisible" title="重设密码" width="400px" draggable :before-close="handleResetPwdClose">
@@ -69,6 +78,7 @@ import { ECharts, EChartsOption, init } from 'echarts';
import { getDeptInfoById, resetPwd } from './api';
import { warningNotification, successNotification } from '/@/utils/message';
import { HeadDeptInfoType } from '../../types';
import {getBaseURL} from '/@/utils/baseUrl';
let deptCountChart: ECharts;
let deptSexChart: ECharts;
@@ -277,7 +287,8 @@ const { resetCrudOptions } = useCrud({
padding: 0 10px;
border-radius: 8px 0 0 8px;
box-sizing: border-box;
background-color: #fff;
color: var(--next-bg-topBarColor);
background-color: var(--el-fill-color-blank);;
}
.dept-user-com-table {
height: calc(100% - 200px);

View File

@@ -133,7 +133,7 @@ onMounted(() => {
}
.dept-left {
background-color: #fff;
background-color: var(--el-fill-color-blank);;
border-radius: 0 8px 8px 0;
padding: 10px;
}

View File

@@ -303,6 +303,16 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
},
},
},
is_value: {
title: '是否值',
column: {
show: false
},
form: {
show: false,
value: true
}
}
},
},
};

View File

@@ -0,0 +1,49 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/download_center/';
export function GetPermission() {
return request({
url: apiPrefix + 'field_permission/',
method: 'get',
});
}
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 },
});
}

View File

@@ -0,0 +1,160 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction';
import { getBaseURL } from '/@/utils/baseUrl';
interface CreateCrudOptionsTypes {
output: any;
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose; }): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
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);
};
//权限判定
// @ts-ignore
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: true
},
actionbar: {
buttons: {
add: {
show: false
}
}
},
toolbar: {
buttons: {
export: {
show: false
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 120,
buttons: {
view: {
show: false
},
edit: {
show: false
},
remove: {
show: false
},
download: {
show: compute(ctx => ctx.row.task_status === 2),
text: '下载文件',
type: 'warning',
click: (ctx) => window.open(getBaseURL(ctx.row.url), '_blank')
}
},
},
form: {
col: { span: 24 },
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
task_name: {
title: '任务名',
type: 'text',
column: {
minWidth: 160,
align: 'left'
},
search: {
show: true
}
},
file_name: {
title: '文件名',
type: 'text',
column: {
minWidth: 160,
align: 'left'
},
search: {
show: true
}
},
size: {
title: '文件大小(b)',
type: 'number',
column: {
width: 100
}
},
task_status: {
title: '任务状态',
type: 'dict-select',
dict: dict({
data: [
{ label: '任务已创建', value: 0 },
{ label: '任务进行中', value: 1 },
{ label: '任务完成', value: 2 },
{ label: '任务失败', value: 3 },
]
}),
column: {
width: 120
},
search: {
show: true
}
},
create_datetime: {
title: '创建时间',
column: {
width: 160
}
},
update_datetime: {
title: '创建时间',
column: {
width: 160
}
}
},
},
};
};

View File

@@ -0,0 +1,42 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #cell_url="scope">
<el-tag size="small">{{ scope.row.url }}</el-tag>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="downloadCenter">
import { ref, onMounted, inject, onBeforeUpdate } from 'vue';
import { GetPermission } from './api';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import PermissionComNew from './components/PermissionComNew/index.vue';
import _ from "lodash-es";
import { handleColumnPermission } from "/@/utils/columnPermission";
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
// 页面打开后获取列表数据
onMounted(async () => {
crudExpose.doRefresh();
});
</script>

View File

@@ -1,7 +1,19 @@
import * as api from './api';
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import {
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudExpose,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet,
dict
} from '@fast-crud/fast-crud';
import fileSelector from '/@/components/fileSelector/index.vue';
import { getBaseURL } from '/@/utils/baseUrl';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
@@ -20,7 +32,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
actionbar: {
buttons: {
add: {
show: false,
show: true,
click: () => context.openAddHandle?.()
},
},
},
@@ -30,6 +43,17 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
editRequest,
delRequest,
},
tabs: {
show: true,
name: 'file_type',
type: '',
options: [
{ value: 0, label: '图片' },
{ value: 1, label: '视频' },
{ value: 2, label: '音频' },
{ value: 3, label: '其他' },
]
},
rowHandle: {
//固定右侧
fixed: 'right',
@@ -96,14 +120,25 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
},
type: 'input',
column: {
minWidth: 120,
minWidth: 200,
},
form: {
component: {
placeholder: '请输入文件名称',
clearable: true
},
},
},
preview: {
title: '预览',
column: {
minWidth: 120,
align: 'center'
},
form: {
show: false
}
},
url: {
title: '文件地址',
type: 'file-uploader',
@@ -111,7 +146,12 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
disabled: true,
},
column: {
minWidth: 200,
minWidth: 360,
component: {
async buildUrl(value: any) {
return getBaseURL(value);
}
}
},
},
md5sum: {
@@ -120,12 +160,100 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
disabled: true,
},
column: {
minWidth: 120,
minWidth: 300,
},
form: {
disabled: false,
disabled: false
},
},
mime_type: {
title: '文件类型',
type: 'input',
form: {
show: false,
},
column: {
minWidth: 160
}
},
file_type: {
title: '文件类型',
type: 'dict-select',
dict: dict({
data: [
{ label: '图片', value: 0, color: 'success' },
{ label: '视频', value: 1, color: 'warning' },
{ label: '音频', value: 2, color: 'danger' },
{ label: '其他', value: 3, color: 'primary' },
]
}),
column: {
show: false
},
search: {
show: true
},
form: {
show: false,
component: {
placeholder: '请选择文件类型'
}
}
},
size: {
title: '文件大小',
column: {
minWidth: 120
},
form: {
show: false
}
},
upload_method: {
title: '上传方式',
type: 'dict-select',
dict: dict({
data: [
{ label: '默认上传', value: 0, color: 'primary' },
{ label: '文件选择器上传', value: 1, color: 'warning' },
]
}),
column: {
minWidth: 140
},
search: {
show: true
}
},
create_datetime: {
title: '创建时间',
column: {
minWidth: 160
},
form: {
show: false
}
},
// fileselectortest: {
// title: '文件选择器测试',
// type: 'file-selector',
// column: {
// minWidth: 200
// },
// form: {
// component: {
// name: fileSelector,
// vModel: 'modelValue',
// tabsShow: 0b1111,
// itemSize: 100,
// multiple: true,
// selectable: true,
// showInput: true,
// inputType: 'image',
// valueKey: 'url',
// }
// }
// }
},
},
};

View File

@@ -1,13 +1,85 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<FileSelector v-model="selected" :showInput="false" ref="fileSelectorRef" :tabsShow="SHOW.ALL" :itemSize="120"
:multiple="false" :selectable="true" valueKey="url" inputType="image">
<!-- <template #input="scope">
input{{ scope }}
</template> -->
<!-- <template #actionbar-left="scope">
actionbar-left{{ scope }}
</template> -->
<!-- <template #actionbar-right="scope">
actionbar-right{{ scope }}
</template> -->
<!-- <template #empty="scope">
empty{{ scope }}
</template> -->
<!-- <template #item="{ data }">
{{ data }}
</template> -->
</FileSelector>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-left="scope">
<el-upload :action="getBaseURL() + 'api/system/file/'" :multiple="false"
:on-success="() => crudExpose.doRefresh()" :drag="false" :show-file-list="false">
<el-button type="primary" icon="plus">上传</el-button>
</el-upload>
</template>
<template #cell_size="scope">
<span>{{ scope.row.size ? getSizeDisplay(scope.row.size) : '0b' }}</span>
</template>
<template #cell_preview="scope">
<div v-if="scope.row.file_type === 0">
<el-image style="width: 100%; aspect-ratio: 1 /1 ;" :src="getBaseURL(scope.row.url)"
:preview-src-list="[getBaseURL(scope.row.url)]" :preview-teleported="true" />
</div>
<div v-if="scope.row.file_type === 1" class="_preview"
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
<el-icon :size="60">
<VideoCamera />
</el-icon>
</div>
<div v-if="scope.row.file_type === 2" class="_preview"
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
<el-icon :size="60">
<Headset />
</el-icon>
</div>
<el-icon v-if="scope.row.file_type === 3" :size="60">
<Document />
</el-icon>
<div v-if="scope.row.file_type > 3">未知类型</div>
</template>
</fs-crud>
<div class="preview" :class="{ show: openPreview }">
<video v-show="videoPreviewSrc" :src="videoPreviewSrc" class="previewItem" :controls="true" :autoplay="true"
:muted="true" :loop="false" ref="videoPreviewRef"></video>
<audio v-show="audioPreviewSrc" :src="audioPreviewSrc" class="previewItem" :controls="true" :autoplay="false"
:muted="true" :loop="false" ref="audioPreviewRef"></audio>
<div class="closePreviewBtn">
<el-icon :size="48" color="white" style="cursor: pointer;" @click="closePreview">
<CircleClose />
</el-icon>
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, nextTick } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { getBaseURL } from '/@/utils/baseUrl';
import FileSelector from '/@/components/fileSelector/index.vue';
import { SHOW } from '/@/components/fileSelector/types';
const fileSelectorRef = ref<any>(null);
const getSizeDisplay = (n: number) => n < 1024 ? n + 'b' : (n < 1024 * 1024 ? (n / 1024).toFixed(2) + 'Kb' : (n / (1024 * 1024)).toFixed(2) + 'Mb');
const openAddHandle = async () => {
fileSelectorRef.value.selectVisiable = true;
await nextTick();
};
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
@@ -15,12 +87,81 @@ const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
const { crudOptions } = createCrudOptions({ crudExpose, context: { openAddHandle } });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
const selected = ref<any>([]);
const openPreview = ref<boolean>(false);
const videoPreviewSrc = ref<string>('');
const audioPreviewSrc = ref<string>('');
const videoPreviewRef = ref<HTMLVideoElement>();
const audioPreviewRef = ref<HTMLAudioElement>();
const openPreviewHandle = (src: string, type: string) => {
openPreview.value = true;
(videoPreviewRef.value as HTMLVideoElement).muted = true;
(audioPreviewRef.value as HTMLAudioElement).muted = true;
if (type === 'video') videoPreviewSrc.value = src;
else audioPreviewSrc.value = src;
window.addEventListener('keydown', onPreviewKeydown);
};
const closePreview = () => {
openPreview.value = false;
videoPreviewSrc.value = '';
audioPreviewSrc.value = '';
window.removeEventListener('keydown', onPreviewKeydown);
};
const onPreviewKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
openPreview.value = false;
videoPreviewSrc.value = '';
audioPreviewSrc.value = '';
window.removeEventListener('keydown', onPreviewKeydown);
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="css" scoped>
.preview {
display: none;
position: fixed;
top: 0;
height: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
z-index: 9999;
}
.show {
display: block !important;
}
.previewItem {
width: 50%;
position: absolute;
top: 50%;
right: 50%;
transform: translate(25%, -50%);
}
.closePreviewBtn {
width: 50%;
position: absolute;
bottom: 10%;
left: 50%;
transform: translate(-75%);
display: flex;
justify-content: center;
}
._preview {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
</style>

View File

@@ -13,6 +13,15 @@ export function login(params: object) {
data: params
});
}
export function loginChangePwd(data: object) {
return request({
url: '/api/system/user/login_change_password/',
method: 'post',
data: data
});
}
export function getUserInfo() {
return request({
url: '/api/system/user/user_info/',

View File

@@ -45,6 +45,12 @@
</el-button>
</el-form-item>
</el-form>
<!-- 申请试用-->
<div style="text-align: center" v-if="showApply()">
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
<span>申请试用</span>
</el-button>
</div>
</template>
<script lang="ts">
@@ -67,6 +73,7 @@ import { SystemConfigStore } from '/@/stores/systemConfig';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import { Md5 } from 'ts-md5';
import { errorMessage } from '/@/utils/message';
import {getBaseURL} from "/@/utils/baseUrl";
export default defineComponent({
name: 'loginAccount',
@@ -125,6 +132,9 @@ export default defineComponent({
state.ruleForm.captchaKey = ret.data.key;
});
};
const applyBtnClick = async () => {
window.open(getBaseURL('/api/system/apply_for_trial/'));
};
const refreshCaptcha = async () => {
state.ruleForm.captcha=''
loginApi.getCaptcha().then((ret: any) => {
@@ -138,8 +148,13 @@ export default defineComponent({
if (valid) {
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
if (res.code === 2000) {
const {data} = res
Cookies.set('username', res.data.username);
Session.set('token', res.data.access);
Cookies.set('username', res.data.name);
useUserInfo().setPwdChangeCount(data.pwd_change_count)
if(data.pwd_change_count==0){
return router.push('/login');
}
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
@@ -162,21 +177,18 @@ export default defineComponent({
})
};
const getUserInfo = () => {
useUserInfo().setUserInfos();
};
// 登录成功后的跳转
const loginSuccess = () => {
//登录成功获取用户信息,获取系统字典数据
getUserInfo();
//获取所有字典
DictionaryStore().getSystemDictionarys();
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
const pwd_change_count = userInfos.value.pwd_change_count
if(pwd_change_count>0){
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
router.push({
@@ -191,6 +203,7 @@ export default defineComponent({
state.loading.signIn = true;
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
}
// 添加 loading防止第一次进入界面时出现短暂空白
NextLoading.start();
};
@@ -199,7 +212,10 @@ export default defineComponent({
//获取系统配置
SystemConfigStore().getSystemConfigs();
});
// 是否显示申请试用按钮
const showApply = () => {
return window.location.href.indexOf('public') != -1
}
return {
refreshCaptcha,
@@ -209,6 +225,8 @@ export default defineComponent({
state,
formRef,
rules,
applyBtnClick,
showApply,
...toRefs(state),
};
},
@@ -249,7 +267,7 @@ export default defineComponent({
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 300;
font-weight: 800;
margin-top: 15px;
}
}

View File

@@ -0,0 +1,276 @@
<template>
<el-form ref="formRef" size="large" class="login-content-form" :model="state.ruleForm" :rules="rules"
@keyup.enter="loginClick">
<el-form-item class="login-animation1" prop="username">
<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" readonly
v-model="ruleForm.username" clearable autocomplete="off">
<template #prefix>
<el-icon class="el-input__icon"><ele-User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation2" prop="password">
<el-input :type="isShowPassword ? 'text' : 'password'"
:placeholder="$t('message.account.accountPlaceholder4')" v-model="ruleForm.password">
<template #prefix>
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
</template>
<template #suffix>
<i class="iconfont el-input__icon login-content-password"
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="isShowPassword = !isShowPassword">
</i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation3" prop="password_regain">
<el-input :type="isShowPassword ? 'text' : 'password'"
:placeholder="$t('message.account.accountPlaceholder5')" v-model="ruleForm.password_regain">
<template #prefix>
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
</template>
<template #suffix>
<i class="iconfont el-input__icon login-content-password"
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="isShowPassword = !isShowPassword">
</i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation4">
<el-button type="primary" class="login-content-submit" round @click="loginClick" :loading="loading.signIn">
<span>{{ $t('message.account.accountBtnText') }}</span>
</el-button>
</el-form-item>
</el-form>
<!-- 申请试用-->
<div style="text-align: center" v-if="showApply()">
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
<span>申请试用</span>
</el-button>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, FormInstance, FormRules } from 'element-plus';
import { useI18n } from 'vue-i18n';
import Cookies from 'js-cookie';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
import { initBackEndControlRoutes } from '/@/router/backEnd';
import { Session } from '/@/utils/storage';
import { formatAxis } from '/@/utils/formatTime';
import { NextLoading } from '/@/utils/loading';
import * as loginApi from '/@/views/system/login/api';
import { useUserInfo } from '/@/stores/userInfo';
import { DictionaryStore } from '/@/stores/dictionary';
import { SystemConfigStore } from '/@/stores/systemConfig';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import { Md5 } from 'ts-md5';
import { errorMessage } from '/@/utils/message';
import { getBaseURL } from "/@/utils/baseUrl";
import { loginChangePwd } from "/@/views/system/login/api";
export default defineComponent({
name: 'changePwd',
setup() {
const { t } = useI18n();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { userInfos } = storeToRefs(useUserInfo());
const route = useRoute();
const router = useRouter();
const state = reactive({
isShowPassword: false,
ruleForm: {
username: '',
password: '',
password_regain: ''
},
loading: {
signIn: false,
},
});
const validatePass = (rule, value, callback) => {
const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}');
if (value === '') {
callback(new Error('请输入密码'));
} else if (!pwdRegex.test(value)) {
callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'));
} else {
if (state.ruleForm.password !== '') {
formRef.value.validateField('password');
}
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== state.ruleForm.password) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请填写账号', trigger: 'blur' },
],
password: [
{
required: true,
message: '请填写密码',
trigger: 'blur',
},
{
validator: validatePass,
trigger: 'blur',
},
],
password_regain: [
{
required: true,
message: '请填写密码',
trigger: 'blur',
},
{
validator: validatePass2,
trigger: 'blur',
},
],
})
const formRef = ref();
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
const applyBtnClick = async () => {
window.open(getBaseURL('/api/system/apply_for_trial/'));
};
const loginClick = async () => {
if (!formRef.value) return
await formRef.value.validate((valid: any) => {
if (valid) {
loginApi.loginChangePwd({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password), password_regain: Md5.hashStr(state.ruleForm.password_regain) }).then((res: any) => {
if (res.code === 2000) {
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
loginSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
initBackEndControlRoutes();
// 执行完 initBackEndControlRoutes再执行 signInSuccess
loginSuccess();
}
}
}).catch((err: any) => {
// 登录错误之后,刷新验证码
errorMessage("登录失败")
});
} else {
errorMessage("请填写登录信息")
}
})
};
// 登录成功后的跳转
const loginSuccess = () => {
//获取所有字典
DictionaryStore().getSystemDictionarys();
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else {
router.push('/');
}
// 登录成功提示
// 关闭 loading
state.loading.signIn = true;
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
// 添加 loading防止第一次进入界面时出现短暂空白
NextLoading.start();
};
onMounted(() => {
state.ruleForm.username = Cookies.get('username')
//获取系统配置
SystemConfigStore().getSystemConfigs();
});
// 是否显示申请试用按钮
const showApply = () => {
return window.location.href.indexOf('public') != -1
}
return {
loginClick,
loginSuccess,
state,
formRef,
rules,
applyBtnClick,
showApply,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
@for $i from 1 through 5 {
.login-animation#{$i} {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
animation-delay: calc($i/10) + s;
}
}
.login-content-password {
display: inline-block;
width: 20px;
cursor: pointer;
&:hover {
color: #909399;
}
}
.login-content-captcha {
width: 100%;
padding: 0;
font-weight: bold;
letter-spacing: 5px;
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 800;
margin-top: 15px;
}
}
</style>

View File

@@ -5,51 +5,52 @@
<img :src="siteLogo" />
<div class="login-left-logo-text">
<span>{{ getSystemConfig['login.site_title'] || getThemeConfig.globalViceTitle }}</span>
<span class="login-left-logo-text-msg">{{
<span class="login-left-logo-text-msg" style="margin-top: 5px;">{{
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
</div>
</div>
<div class="login-left-img">
<img :src="loginMain" />
</div>
<img :src="loginBg" class="login-left-waves" />
</div>
<div class="login-right flex z-10">
<div class="login-right-warp flex-margin">
<span class="login-right-warp-one"></span>
<span class="login-right-warp-two"></span>
<!-- <span class="login-right-warp-one"></span>-->
<!-- <span class="login-right-warp-two"></span>-->
<div class="login-right-warp-mian">
<div class="login-right-warp-main-title">{{ getSystemConfig['login.site_title'] ||
getThemeConfig.globalTitle }} 欢迎您</div>
<div class="login-right-warp-main-title">
{{userInfos.pwd_change_count===0?'初次登录修改密码':'欢迎登录'}}
</div>
<div class="login-right-warp-main-form">
<div v-if="!state.isScan">
<el-tabs v-model="state.tabsActiveName">
<el-tab-pane :label="$t('message.label.one1')" name="account">
<el-tab-pane :label="$t('message.label.changePwd')" name="changePwd" v-if="userInfos.pwd_change_count===0">
<ChangePwd />
</el-tab-pane>
<el-tab-pane :label="$t('message.label.one1')" name="account" v-else>
<Account />
</el-tab-pane>
<!-- TODO 手机号码登录未接入展示隐藏 -->
<!-- <el-tab-pane :label="$t('message.label.two2')" name="mobile">
<Mobile />
</el-tab-pane> -->
</el-tabs>
</div>
<Scan v-if="state.isScan" />
<div class="login-content-main-sacn" @click="state.isScan = !state.isScan">
<i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
<div class="login-content-main-sacn-delta"></div>
</div>
<!-- <Scan v-if="state.isScan" />-->
<!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">-->
<!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
<!-- <div class="login-content-main-sacn-delta"></div>-->
<!-- </div>-->
</div>
</div>
</div>
</div>
<div class="login-authorization z-10">
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 django-vue-admin.com' }} 版权所有</p>
<p class="la-other">
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 北京信码新创科技有限公司' }} 版权所有</p>
<p class="la-other" style="margin-top: 5px;">
<a href="https://beian.miit.gov.cn" target="_blank">{{ getSystemConfig['login.keep_record'] ||
'ICP备18005113号-3' }}</a>
'ICP备2021031018号' }}</a>
|
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : 'https://django-vue-admin.com'"
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : '#'"
target="_blank">帮助</a>
|
<a
@@ -60,26 +61,29 @@
</p>
</div>
</div>
<div v-if="siteBg">
<img :src="siteBg" class="fixed inset-0 z-1 w-full h-full" />
<div v-if="loginBg">
<img :src="loginBg" class="loginBg fixed inset-0 z-1 w-full h-full" />
</div>
</template>
<script setup lang="ts" name="loginIndex">
import { defineAsyncComponent, onMounted, reactive, computed } from 'vue';
import {defineAsyncComponent, onMounted, reactive, computed, watch} from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
import logoMini from '/@/assets/logo-mini.svg';
import loginMain from '/@/assets/login-main.svg';
import loginBg from '/@/assets/login-bg.svg';
import loginBg from '/@/assets/login-bg.png';
import { SystemConfigStore } from '/@/stores/systemConfig'
import { getBaseURL } from "/@/utils/baseUrl";
// 引入组件
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.vue'));
import _ from "lodash-es";
import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo());
// 定义变量内容
const storesThemeConfig = useThemeConfig();
@@ -89,6 +93,16 @@ const state = reactive({
isScan: false,
});
watch(()=>userInfos.value.pwd_change_count,(val)=>{
if(val===0){
state.tabsActiveName ='changePwd'
}else{
state.tabsActiveName ='account'
}
},{deep:true,immediate:true})
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
@@ -187,13 +201,13 @@ onMounted(() => {
width: 700px;
.login-right-warp {
border: 1px solid var(--el-color-primary-light-3);
//border: 1px solid var(--el-color-primary-light-3);
border-radius: 3px;
width: 500px;
height: 500px;
position: relative;
overflow: hidden;
background-color: var(--el-color-white);
//background-color: var(--el-color-white);
.login-right-warp-one,
.login-right-warp-two {
@@ -265,7 +279,8 @@ onMounted(() => {
.login-right-warp-main-title {
height: 130px;
line-height: 130px;
font-size: 27px;
font-size: 32px;
font-weight: 600;
text-align: center;
letter-spacing: 3px;
animation: logoAnimation 0.3s ease;
@@ -321,7 +336,7 @@ onMounted(() => {
}
.login-authorization {
position: fixed;
position: absolute;
bottom: 30px;
left: 0;
right: 0;

View File

@@ -48,3 +48,10 @@ export function BatchAdd(obj: AddReq) {
});
}
export function BatchDelete(keys: any) {
return request({
url: apiPrefix + 'multiple_delete/',
method: 'delete',
data: { keys },
});
}

View File

@@ -4,6 +4,8 @@ import {auth} from '/@/utils/authFunction'
import {request} from '/@/utils/service';
import { successNotification } from '/@/utils/message';
import { ElMessage } from 'element-plus';
import { nextTick, ref } from 'vue';
import XEUtils from 'xe-utils';
//此处为crudOptions配置
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async () => {
@@ -22,7 +24,42 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
const addRequest = async ({form}: AddReq) => {
return await api.AddObj({...form, ...{menu: context!.selectOptions.value.id}});
};
// 记录选中的行
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: {
pagination:{
show:false
@@ -84,6 +121,11 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
editRequest,
delRequest,
},
table: {
rowKey: 'id', //设置你的主键id 默认rowKey=id
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
form: {
col: {span: 24},
labelWidth: '100px',
@@ -93,6 +135,16 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
},
},
columns: {
$checked: {
title: '选择',
form: { show: false },
column: {
type: 'selection',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
_index: {
title: '序号',
form: {show: false},

View File

@@ -1,19 +1,72 @@
<template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<el-tooltip content="批量删除">
<el-button text type="danger" :disabled="selectedRowsCount === 0" :icon="Delete" circle @click="handleBatchDelete" />
</el-tooltip>
</template>
<template #pagination-right>
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small">
<el-table-column width="150" property="id" label="id" />
<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>
</fs-crud>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { MenuTreeItemType } from '../../types';
import { ElMessage, ElMessageBox } from 'element-plus';
import XEUtils from 'xe-utils';
import { BatchDelete } from './api';
import { Close, Delete } from '@element-plus/icons-vue';
// 当前选择的菜单信息
let selectOptions: any = ref({ name: null });
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
const { crudRef, crudBinding, crudExpose, context,selectedRows } = useFs({ createCrudOptions, context: { selectOptions } });
const { doRefresh, setTableData } = crudExpose;
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
// 批量删除
const handleBatchDelete = async () => {
await ElMessageBox.confirm(`确定要批量删除这${selectedRows.value.length}条记录吗`, '确认', {
distinguishCancelAndClose: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
});
await BatchDelete(XEUtils.pluck(selectedRows.value, 'id'));
ElMessage.info('删除成功');
selectedRows.value = [];
await crudExpose.doRefresh();
};
// 移除已选中的行
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 handleRefreshTable = (record: MenuTreeItemType) => {
if (!record.is_catalog && record.id) {
selectOptions.value = record;

View File

@@ -42,6 +42,13 @@ export function DelObj(id: DelReq) {
});
}
export function BatchDelete(keys: any) {
return request({
url: apiPrefix + 'multiple_delete/',
method: 'delete',
data: { keys },
});
}
/**
* 获取所有model
*/

View File

@@ -2,8 +2,9 @@ import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { inject } from 'vue';
import { inject, nextTick, ref } from 'vue';
import {auth} from "/@/utils/authFunction";
import XEUtils from 'xe-utils';
@@ -27,8 +28,41 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
form.menu = selectOptions.value.id;
return await api.AddObj(form);
};
// 记录选中的行
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,
@@ -77,7 +111,22 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
width: '600px',
},
},
table: {
rowKey: 'id', //设置你的主键id 默认rowKey=id
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
columns: {
$checked: {
title: '选择',
form: { show: false },
column: {
type: 'selection',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
_index: {
title: '序号',
form: { show: false },

View File

@@ -5,11 +5,7 @@
<el-tag>已选择:{{ props.model }}</el-tag>
</div>
<!-- 搜索输入框 -->
<el-input
v-model="searchQuery"
placeholder="搜索模型..."
style="margin-bottom: 10px;"
></el-input>
<el-input v-model="searchQuery" placeholder="搜索模型..." style="margin-bottom: 10px"></el-input>
<div class="model-card">
<!--注释编号:django-vue3-admin-index483211: 对请求回来的allModelData进行computed计算返加搜索框匹配到的内容-->
<div v-for="(item, index) in filteredModelData" :value="item.key" :key="index">
@@ -21,16 +17,33 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="modelDialog = false">取消</el-button>
<el-button type="primary" @click="handleAutomatch">
确定
</el-button>
<el-button type="primary" @click="handleAutomatch"> 确定 </el-button>
</span>
</template>
</el-dialog>
<div style="height: 80vh">
<div style="height: 72vh">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<el-tooltip content="批量删除">
<el-button text type="danger" :disabled="selectedRowsCount === 0" :icon="Delete" circle @click="handleBatchDelete" />
</el-tooltip>
</template>
<template #pagination-right>
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small">
<el-table-column width="150" property="id" label="id" />
<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>
</fs-crud>
</div>
</div>
</template>
@@ -39,30 +52,32 @@
import { ref, onMounted, reactive, computed } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import {getModelList} from './api'
import {MenuTreeItemType} from "/@/views/system/menu/types";
import { BatchDelete, getModelList } from './api';
import { Close, Delete } from '@element-plus/icons-vue';
import { MenuTreeItemType } from '/@/views/system/menu/types';
import { successMessage, successNotification, warningNotification } from '/@/utils/message';
import { automatchColumnsData } from '/@/views/system/columns/components/ColumnsTableCom/api';
import XEUtils from 'xe-utils';
import { ElMessage, ElMessageBox } from 'element-plus';
// 当前选择的菜单信息
let selectOptions: any = ref({ name: null });
const props = reactive({
model: '',
app: '',
menu: ''
})
menu: '',
});
//model弹窗
const modelDialog = ref(false)
const modelDialog = ref(false);
// 获取所有model
const allModelData = ref<any[]>([]);
const modelCheckIndex = ref(null)
const modelCheckIndex = ref(null);
const onModelChecked = (row, index) => {
modelCheckIndex.value = index
props.model = row.key
props.app = row.app
}
modelCheckIndex.value = index;
props.model = row.key;
props.app = row.app;
};
// 注释编号:django-vue3-admin-index083311:代码开始行
// 功能说明:搭配搜索的处理,返回搜索结果
@@ -73,15 +88,12 @@ const filteredModelData = computed(() => {
return allModelData.value;
}
const query = searchQuery.value.toLowerCase();
return allModelData.value.filter(item =>
item.app.toLowerCase().includes(query) ||
item.title.toLowerCase().includes(query) ||
item.key.toLowerCase().includes(query)
return allModelData.value.filter(
(item) => item.app.toLowerCase().includes(query) || item.title.toLowerCase().includes(query) || item.key.toLowerCase().includes(query)
);
});
// 注释编号:django-vue3-admin-index083311:代码结束行
/**
* 菜单选中时,加载表格数据
* @param record
@@ -99,8 +111,8 @@ const handleRefreshTable = (record: MenuTreeItemType) => {
* 自动匹配列
*/
const handleAutomatch = async () => {
props.menu = selectOptions.value.id
modelDialog.value = false
props.menu = selectOptions.value.id;
modelDialog.value = false;
if (props.menu && props.model) {
const res = await automatchColumnsData(props);
if (res?.code === 2000) {
@@ -110,11 +122,39 @@ const handleAutomatch = async () => {
} else {
warningNotification('请选择角色和模型表!');
}
};
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, props, modelDialog, selectOptions,allModelData});
// 批量删除
const handleBatchDelete = async () => {
await ElMessageBox.confirm(`确定要批量删除这${selectedRows.value.length}条记录吗`, '确认', {
distinguishCancelAndClose: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
});
await BatchDelete(XEUtils.pluck(selectedRows.value, 'id'));
ElMessage.info('删除成功');
selectedRows.value = [];
await crudExpose.doRefresh();
};
// 移除已选中的行
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 { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, props, modelDialog, selectOptions, allModelData });
onMounted(async () => {
const res = await getModelList();
allModelData.value = res.data;

View File

@@ -85,6 +85,7 @@
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="外链接" prop="link_url">
<el-input v-model="menuFormData.link_url" placeholder="请输入外链接地址" />
<el-alert :title="`输入{{token}}可自动替换系统 token `" type="info" />
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog" label="缓存">

View File

@@ -16,12 +16,12 @@
<el-col :span="18">
<el-tabs type="border-card">
<el-tab-pane label="按钮权限配置" >
<div style="height: 80vh">
<div style="height: 72vh">
<MenuButtonCom ref="menuButtonRef" />
</div>
</el-tab-pane>
<el-tab-pane label="列权限配置">
<div style="height: 80vh">
<div style="height: 72vh">
<MenuFieldCom ref="menuFieldRef"></MenuFieldCom>
</div>
</el-tab-pane>
@@ -138,7 +138,7 @@ onMounted(() => {
.menu-box {
height: 100%;
padding: 10px;
background-color: #fff;
background-color: var(--el-fill-color-blank);;
box-sizing: border-box;
}

View File

@@ -32,7 +32,7 @@
<el-col :xs="24" :sm="24" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index">{{ item.name }}</el-tag>
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index" style="margin-right: 5px;">{{ item.name }}</el-tag>
</div>
</el-col>
</el-row>
@@ -153,7 +153,7 @@
center
>
<el-form-item label="原密码" required prop="oldPassword">
<el-input v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" clearable></el-input>
<el-input type="password" v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" show-password clearable></el-input>
</el-form-item>
<el-form-item required prop="newPassword" label="新密码">
<el-input type="password" v-model="userPasswordInfo.newPassword" placeholder="请输入新密码" show-password clearable></el-input>
@@ -354,7 +354,8 @@ const uploadImg = (data: any) => {
api.uploadAvatar(formdata).then((res: any) => {
if (res.code === 2000) {
selectImgVisible.value = false;
state.personalForm.avatar = getBaseURL() + res.data.url;
// state.personalForm.avatar = getBaseURL() + res.data.url;
state.personalForm.avatar = res.data.url;
api.updateUserInfo(state.personalForm).then((res: any) => {
successMessage('更新成功');
getUserInfo();

View File

@@ -1,70 +0,0 @@
import { request } from "/@/utils/service";
import XEUtils from "xe-utils";
/**
* 获取角色的授权列表
* @param roleId
* @param query
*/
export function getRolePermission(query:object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_permission/',
method: 'get',
params:query
}).then((res:any)=>{
return XEUtils.toArrayTree(res.data, {key: 'id', parentKey: 'parent',children: 'children',strict: false})
})
}
/***
* 设置角色的权限
* @param roleId
* @param data
*/
export function setRolePremission(roleId:any,data:object) {
return request({
url: `/api/system/role_menu_button_permission/${roleId}/set_role_premission/`,
method: 'put',
data
})
}
export function getDataPermissionRange(query:object) {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
params:query
})
}
export function getDataPermissionRangeAll() {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
})
}
export function getDataPermissionDept(query:object) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params:query
})
}
export function getDataPermissionMenu() {
return request({
url: '/api/system/role_menu_button_permission/get_role_permissions/',
method: 'get'
})
}
/**
* 设置按钮的数据范围
*/
export function setBtnDatarange(roleId:number,data:object) {
return request({
url: `/api/system/role_menu_button_permission/${roleId}/set_btn_datarange/`,
method: 'put',
data
})
}

View File

@@ -1,451 +0,0 @@
<template>
<el-drawer
v-model="drawerVisibleNew"
title="权限配置"
direction="rtl"
size="60%"
:close-on-click-modal="false"
:before-close="handleDrawerClose"
:destroy-on-close="true"
>
<template #header>
<el-row>
<el-col :span="4">
<div>
当前授权角色
<el-tag>{{ props.roleName }}</el-tag>
</div>
</el-col>
<el-col :span="6">
<div>
<el-button size="small" type="primary" class="pc-save-btn" @click="handleSavePermission">保存菜单授权 </el-button>
</div>
</el-col>
</el-row>
</template>
<div class="permission-com">
<el-row class="menu-el-row" :gutter="20">
<el-col :span="6">
<div class="menu-box menu-left-box">
<el-tree
ref="treeRef"
:data="menuData"
:props="defaultTreeProps"
:default-checked-keys="menuDefaultCheckedKeys"
@check="handleMenuCheck"
@node-click="handleMenuClick"
node-key="id"
check-strictly
highlight-current
show-checkbox
default-expand-all
>
</el-tree>
</div>
</el-col>
<el-col :span="18">
<div class="pc-collapse-main" v-if="menuCurrent.btns && menuCurrent.btns.length > 0">
<div class="pccm-item">
<div class="menu-form-alert">配置操作功能接口权限,配置数据权限点击小齿轮</div>
<el-checkbox v-for="(btn, bIndex) in menuCurrent.btns" :key="bIndex" v-model="btn.isCheck" :label="btn.value">
<div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(menuCurrent, btn)">
<el-icon>
<Setting />
</el-icon>
</span>
</div>
</el-checkbox>
</div>
<div class="pccm-item" v-if="menuCurrent.columns && menuCurrent.columns.length > 0">
<div class="menu-form-alert">配置数据列字段权限</div>
<ul class="columns-list">
<li class="columns-head">
<div class="width-txt">
<span>字段</span>
</div>
<div v-for="(head, hIndex) in column.header" :key="hIndex" class="width-check">
<el-checkbox :label="head.value" @change="handleColumnChange($event, menuCurrent, head.value, head.disabled)">
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
<li v-for="(c_item, c_index) in menuCurrent.columns" :key="c_index" class="columns-item">
<div class="width-txt">{{ c_item.title }}</div>
<div v-for="(col, cIndex) in column.header" :key="cIndex" class="width-check">
<el-checkbox v-model="c_item[col.value]" class="ci-checkout" :disabled="c_item[col.disabled]"></el-checkbox>
</div>
</li>
</ul>
</div>
</div>
</el-col>
</el-row>
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" :before-close="handleDialogClose">
<div class="pc-dialog">
<el-select v-model="dataPermission" @change="handlePermissionRangeChange" class="dialog-select" placeholder="请选择">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-tree-select
v-show="dataPermission === 4"
node-key="id"
v-model="customDataPermission"
:props="defaultTreeProps"
:data="deptData"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
/>
</div>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, onMounted, defineProps, watch, computed, reactive } from 'vue';
import XEUtils from 'xe-utils';
import { errorNotification } from '/@/utils/message';
import { getDataPermissionRange, getDataPermissionDept, getRolePermission, setRolePremission, setBtnDatarange } from './api';
import { MenuDataType, DataPermissionRangeType, CustomDataPermissionDeptType } from './types';
import { ElMessage, ElTree } from 'element-plus';
const props = defineProps({
roleId: {
type: Number,
default: -1,
},
roleName: {
type: String,
default: '',
},
drawerVisible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:drawerVisible']);
const drawerVisibleNew = ref(false);
watch(
() => props.drawerVisible,
(val) => {
drawerVisibleNew .value = val;
getMenuBtnPermission();
getDataPermissionRangeLable();
menuCurrent.value = {};
}
);
const handleDrawerClose = () => {
emit('update:drawerVisible', false);
};
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
let menuData = ref<MenuDataType[]>([]); // 菜单列表数据
let menuDefaultCheckedKeys = ref<number[]>([]); // 默认选中的菜单列表
let menuCurrent = ref<Partial<MenuDataType>>({}); // 当前选中的菜单
let menuBtnCurrent = ref<number>(-1);
let dialogVisible = ref(false);
let dataPermissionRange = ref<DataPermissionRangeType[]>([]);
let dataPermissionRangeLabel = ref<DataPermissionRangeType[]>([]);
const formatDataRange = computed(() => {
return function (datarange: number) {
const findItem = dataPermissionRangeLabel.value.find((i) => i.value === datarange);
return findItem?.label || '';
};
});
let deptData = ref<CustomDataPermissionDeptType[]>([]);
let dataPermission = ref();
let customDataPermission = ref([]);
/**
* 菜单复选框选中
* @param node
* @param data
*/
const handleMenuCheck = (node: any, data: any) => {
XEUtils.eachTree(menuData.value, (item) => {
item.isCheck = data.checkedKeys.includes(item.id);
});
};
/**
* 菜单点击
* @param node
* @param data
*/
const handleMenuClick = (selectNode: MenuDataType) => {
menuCurrent.value = selectNode;
};
//获取菜单,按钮,权限
const getMenuBtnPermission = async () => {
const resMenu = await getRolePermission({ role: props.roleId });
menuData.value = resMenu;
menuDefaultCheckedKeys.value = XEUtils.toTreeArray(resMenu)
.filter((i) => i.isCheck)
.map((i) => i.id);
};
// 获取按钮的数据权限下拉选项
const getDataPermissionRangeLable = async () => {
const resRange = await getDataPermissionRange({ role: props.roleId });
dataPermissionRangeLabel.value = resRange.data;
};
/**
* 获取按钮数据权限下拉选项
* @param btnId 按钮id
*/
const fetchData = async (btnId: number) => {
try {
const resRange = await getDataPermissionRange({ menu_button: btnId });
if (resRange?.code === 2000) {
dataPermissionRange.value = resRange.data;
}
} catch {
return;
}
};
/**
* 设置按钮数据权限
* @param record 当前菜单
* @param btnType 按钮类型
*/
const handleSettingClick = (record: any, btn: MenuDataType['btns'][number]) => {
menuCurrent.value = record;
menuBtnCurrent.value = btn.id;
dialogVisible.value = true;
dataPermission.value = btn.data_range;
handlePermissionRangeChange(btn.data_range);
fetchData(btn.id);
};
/**
* 设置列权限
* @param val 是否选中
* @param record 当前菜单
* @param btnType 按钮类型
* @param disabledType 禁用类型
*/
const handleColumnChange = (val: boolean, record: any, btnType: string, disabledType: string) => {
for (const iterator of record.columns) {
iterator[btnType] = iterator[disabledType] ? iterator[btnType] : val;
}
};
/**
* 数据权限设置
*/
const handlePermissionRangeChange = async (val: number) => {
if (val === 4) {
const res = await getDataPermissionDept({ role: props.roleId, menu_button: menuBtnCurrent.value });
const depts = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
deptData.value = depts;
const btnObj = XEUtils.find(menuCurrent.value.btns, (item) => item.id === menuBtnCurrent.value);
customDataPermission.value = btnObj.dept;
}
};
/**
* 数据权限设置确认
*/
const handleDialogConfirm = () => {
if (dataPermission.value !== 0 && !dataPermission.value) {
errorNotification('请选择');
return;
}
for (const btn of menuCurrent.value?.btns || []) {
if (btn.id === menuBtnCurrent.value) {
const findItem = dataPermissionRange.value.find((i) => i.value === dataPermission.value);
btn.data_range = findItem?.value || 0;
if (btn.data_range === 4) {
btn.dept = customDataPermission.value;
}
}
}
handleDialogClose();
};
const handleDialogClose = () => {
dialogVisible.value = false;
customDataPermission.value = [];
dataPermission.value = null;
};
//保存菜单授权
const handleSavePermission = () => {
setRolePremission(props.roleId, XEUtils.toTreeArray(menuData.value)).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',
});
});
};
const column = reactive({
header: [
{ value: 'is_create', label: '新增可见', disabled: 'disabled_create' },
{ value: 'is_update', label: '编辑可见', disabled: 'disabled_update' },
{ value: 'is_query', label: '列表可见', disabled: 'disabled_query' },
],
});
onMounted(() => {});
</script>
<style lang="scss" scoped>
.permission-com {
margin: 15px;
box-sizing: border-box;
.pc-save-btn {
margin-bottom: 15px;
}
.pc-collapse-title {
line-height: 32px;
text-align: left;
span {
font-size: 16px;
}
}
.pc-collapse-main {
box-sizing: border-box;
.pccm-item {
margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
.btn-item {
display: flex;
align-items: center;
span {
margin-left: 5px;
}
}
.columns-list {
.width-txt {
width: 200px;
}
.width-check {
width: 100px;
}
.width-icon {
cursor: pointer;
}
.columns-head {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
span {
font-weight: 900;
}
}
.columns-item {
display: flex;
align-items: center;
padding: 6px 0;
box-sizing: border-box;
.ci-checkout {
height: auto !important;
}
}
}
}
}
.pc-dialog {
.dialog-select {
width: 100%;
}
.dialog-tree {
width: 100%;
margin-top: 20px;
}
}
}
</style>
<style lang="scss">
.permission-com {
.el-collapse {
border-top: none;
border-bottom: none;
}
.el-collapse-item {
margin-bottom: 15px;
}
.el-collapse-item__header {
height: auto;
padding: 15px;
border-radius: 8px;
border-top: 1px solid #ebeef5;
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
box-sizing: border-box;
background-color: #fafafa;
}
.el-collapse-item__header.is-active {
border-radius: 8px 8px 0 0;
background-color: #fafafa;
}
.el-collapse-item__wrap {
padding: 15px;
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
border-top: 1px solid #ebeef5;
border-radius: 0 0 8px 8px;
background-color: #fafafa;
box-sizing: border-box;
.el-collapse-item__content {
padding-bottom: 0;
}
}
}
</style>

View File

@@ -1,29 +0,0 @@
export interface DataPermissionRangeType {
label: string;
value: number;
}
export interface CustomDataPermissionDeptType {
id: number;
name: string;
patent: number;
children: CustomDataPermissionDeptType[];
}
export interface CustomDataPermissionMenuType {
id: number;
name: string;
is_catalog: boolean;
menuPermission: { id: number; name: string; value: string }[] | null;
columns: { id: number; name: string; title: string }[] | null;
children: CustomDataPermissionMenuType[];
}
export interface MenuDataType {
id: string;
name: string;
isCheck: boolean;
btns: { id: number; name: string; value: string; isCheck: boolean; data_range: number; dept: object }[];
columns: { [key: string]: boolean | string; }[];
children: MenuDataType[];
}

Some files were not shown because too many files have changed in this diff Show More