Accept Merge Request #27: (develop -> master)

Merge Request: build(web): 更新图标字体并调整相关配置

Created By: @dvadmin-开发-李强
Accepted By: @dvadmin-开发-李强
URL: https://dvadmin-private.coding.net/p/code/d/dvadmin3/git/merge/27?initial=true
This commit is contained in:
dvadmin-开发-李强
2025-06-25 05:12:31 +08:00
committed by Coding
133 changed files with 7075 additions and 2748 deletions

View File

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

1
backend/.gitignore vendored
View File

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

View File

@@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
""" """
import os import os
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
@@ -18,15 +16,6 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
http_application = get_asgi_application() http_application = get_asgi_application()
from application.routing import websocket_urlpatterns
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
"http": http_application, "http": http_application,
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns # 指明路由文件是devops/routing.py
)
)
),
}) })

View File

@@ -1,6 +1,8 @@
import functools import functools
import os import os
from celery.signals import task_postrun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings from django.conf import settings
@@ -38,3 +40,12 @@ def retry_base_task_error():
return wrapper return wrapper
return wraps return wraps
@task_postrun.connect
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
periodic_task_name = kwargs.get('periodic_task_name')
if periodic_task_name:
from django_celery_results.models import TaskResult
# 更新 TaskResult 表中的 periodic_task_name 字段
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)

View File

@@ -2,6 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.conf import settings from django.conf import settings
from django.db import connection 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(): def is_tenants_mode():
@@ -68,6 +72,9 @@ def init_dictionary():
:return: :return:
""" """
try: try:
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -88,7 +95,9 @@ def init_system_config():
:return: :return:
""" """
try: try:
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -107,6 +116,9 @@ def refresh_dictionary():
刷新字典配置 刷新字典配置
:return: :return:
""" """
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -122,6 +134,9 @@ def refresh_system_config():
刷新系统配置 刷新系统配置
:return: :return:
""" """
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model 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值 :param schema_name: 对应字典配置的租户schema_name值
:return: :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: if not settings.DICTIONARY_CONFIG:
refresh_dictionary() refresh_dictionary()
if is_tenants_mode(): if is_tenants_mode():
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :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) dictionary_config = get_dictionary_config(schema_name)
return dictionary_config.get(key) return dictionary_config.get(key)
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :return:
""" """
children = get_dictionary_values(key, schema_name) or [] res = get_dictionary_values(key, schema_name) or []
for ele in children: for ele in res.get('children'):
if ele.get("value") == str(name): if ele.get("value") == str(name):
return ele.get("label") return ele.get("label")
return "" return ""
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :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: if not settings.SYSTEM_CONFIG:
refresh_system_config() refresh_system_config()
if is_tenants_mode(): if is_tenants_mode():
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
:param schema_name: 对应系统配置的租户schema_name值 :param schema_name: 对应系统配置的租户schema_name值
:return: :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) system_config = get_system_config(schema_name)
return system_config.get(key) 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): def get_system_config_label(key, name, schema_name=None):
""" """
获取获取系统配置label值 获取获取系统配置label值

View File

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

View File

@@ -399,8 +399,12 @@ DICTIONARY_CONFIG = {}
# ================================================= # # ================================================= #
# 租户共享app # 租户共享app
TENANT_SHARED_APPS = [] TENANT_SHARED_APPS = []
# 普通租户独有app
TENANT_EXCLUSIVE_APPS = []
# 插件 urlpatterns # 插件 urlpatterns
PLUGINS_URL_PATTERNS = [] PLUGINS_URL_PATTERNS = []
# 所有模式有的
SHARED_APPS = []
# ********** 一键导入插件配置开始 ********** # ********** 一键导入插件配置开始 **********
# 例如: # 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心 # from dvadmin_upgrade_center.settings import * # 升级中心

View File

@@ -0,0 +1,33 @@
# views.py
import time
import jwt
from django.http import StreamingHttpResponse
from application import settings
from dvadmin.system.models import MessageCenterTargetUser
from django.core.cache import cache
def event_stream(user_id):
last_sent_time = 0
while True:
# 从 Redis 中获取最后数据库变更时间
last_db_change_time = cache.get('last_db_change_time', 0)
# 只有当数据库发生变化时才检查总数
if last_db_change_time and last_db_change_time > last_sent_time:
count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count()
yield f"data: {count}\n\n"
last_sent_time = time.time()
time.sleep(1)
def sse_view(request):
token = request.GET.get('token')
decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
user_id = decoded.get('user_id')
response = StreamingHttpResponse(event_stream(user_id), content_type='text/event-stream')
response['Cache-Control'] = 'no-cache'
return response

View File

@@ -24,6 +24,7 @@ from rest_framework_simplejwt.views import (
from application import dispatch from application import dispatch
from application import settings from application import settings
from application.sse_views import sse_view
from dvadmin.system.views.dictionary import InitDictionaryViewSet from dvadmin.system.views.dictionary import InitDictionaryViewSet
from dvadmin.system.views.login import ( from dvadmin.system.views.login import (
LoginView, LoginView,
@@ -40,6 +41,7 @@ dispatch.init_system_config()
dispatch.init_dictionary() dispatch.init_dictionary()
# =========== 初始化系统配置 ================= # =========== 初始化系统配置 =================
permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ]
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
title="Snippets API", title="Snippets API",
@@ -50,7 +52,7 @@ schema_view = get_schema_view(
license=openapi.License(name="BSD License"), license=openapi.License(name="BSD License"),
), ),
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=permission_classes,
generator_class=CustomOpenAPISchemaGenerator, generator_class=CustomOpenAPISchemaGenerator,
) )
# 前端页面映射 # 前端页面映射
@@ -115,6 +117,8 @@ urlpatterns = (
# 前端页面映射 # 前端页面映射
path('web/', web_view, name='web_view'), path('web/', web_view, name='web_view'),
path('web/<path:filename>', serve_web_files, name='serve_web_files'), path('web/<path:filename>', serve_web_files, name='serve_web_files'),
# sse
path('sse/', sse_view, name='sse'),
] ]
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL) + static(settings.STATIC_URL, document_root=settings.STATIC_URL)

View File

@@ -1,183 +0,0 @@
# -*- coding: utf-8 -*-
import urllib
from asgiref.sync import sync_to_async, async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer
import json
from channels.layers import get_channel_layer
from jwt import InvalidSignatureError
from rest_framework.request import Request
from application import settings
from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser
from dvadmin.system.views.message_center import MessageCenterTargetUserSerializer
from dvadmin.utils.serializers import CustomModelSerializer
send_dict = {}
# 发送消息结构体
def set_message(sender, msg_type, msg, unread=0):
text = {
'sender': sender,
'contentType': msg_type,
'content': msg,
'unread': unread
}
return text
# 异步获取消息中心的目标用户
@database_sync_to_async
def _get_message_center_instance(message_id):
from dvadmin.system.models import MessageCenter
_MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user', flat=True)
if _MessageCenter:
return _MessageCenter
else:
return []
@database_sync_to_async
def _get_message_unread(user_id):
"""获取用户的未读消息数量"""
from dvadmin.system.models import MessageCenterTargetUser
count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count()
return count or 0
def request_data(scope):
query_string = scope.get('query_string', b'').decode('utf-8')
qs = urllib.parse.parse_qs(query_string)
return qs
class DvadminWebSocket(AsyncJsonWebsocketConsumer):
async def connect(self):
try:
import jwt
self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"]
decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"])
if decoded_result:
self.user_id = decoded_result.get('user_id')
self.chat_group_name = "user_" + str(self.user_id)
# 收到连接时候处理,
await self.channel_layer.group_add(
self.chat_group_name,
self.channel_name
)
await self.accept()
# 主动推送消息
unread_count = await _get_message_unread(self.user_id)
if unread_count == 0:
# 发送连接成功
await self.send_json(set_message('system', 'SYSTEM', '您已上线'))
else:
await self.send_json(
set_message('system', 'SYSTEM', "请查看您的未读消息~",
unread=unread_count))
except InvalidSignatureError:
await self.disconnect(None)
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(self.chat_group_name, self.channel_name)
print("连接关闭")
try:
await self.close(close_code)
except Exception:
pass
class MegCenter(DvadminWebSocket):
"""
消息中心
"""
async def receive(self, text_data):
# 接受客户端的信息,你处理的函数
text_data_json = json.loads(text_data)
message_id = text_data_json.get('message_id', None)
user_list = await _get_message_center_instance(message_id)
for send_user in user_list:
await self.channel_layer.group_send(
"user_" + str(send_user),
{'type': 'push.message', 'json': text_data_json}
)
async def push_message(self, event):
"""消息发送"""
message = event['json']
await self.send(text_data=json.dumps(message))
class MessageCreateSerializer(CustomModelSerializer):
"""
消息中心-新增-序列化器
"""
class Meta:
model = MessageCenter
fields = "__all__"
read_only_fields = ["id"]
def websocket_push(user_id, message):
username = "user_" + str(user_id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
username,
{
"type": "push.message",
"json": message
}
)
def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None,
target_role=None, message: dict = None, request=Request):
if message is None:
message = {"contentType": "INFO", "content": None}
if target_role is None:
target_role = []
if target_dept is None:
target_dept = []
data = {
"title": title,
"content": content,
"target_type": target_type,
"target_user": target_user,
"target_dept": target_dept,
"target_role": target_role
}
message_center_instance = MessageCreateSerializer(data=data, request=request)
message_center_instance.is_valid(raise_exception=True)
message_center_instance.save()
users = target_user or []
if target_type in [1]: # 按角色
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
if target_type in [2]: # 按部门
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
if target_type in [3]: # 系统通知
users = Users.objects.values_list('id', flat=True)
targetuser_data = []
for user in users:
targetuser_data.append({
"messagecenter": message_center_instance.instance.id,
"users": user
})
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=request)
targetuser_instance.is_valid(raise_exception=True)
targetuser_instance.save()
for user in users:
username = "user_" + str(user)
unread_count = async_to_sync(_get_message_unread)(user)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
username,
{
"type": "push.message",
"json": {**message, 'unread': unread_count}
}
)

View File

@@ -2,7 +2,7 @@
import os import os
exclude = ["venv"] # 需要排除的文件目录 exclude = ["venv", ".venv"] # 需要排除的文件目录
for root, dirs, files in os.walk('.'): for root, dirs, files in os.walk('.'):
dirs[:] = list(set(dirs) - set(exclude)) dirs[:] = list(set(dirs) - set(exclude))
if 'migrations' in dirs: if 'migrations' in dirs:

View File

@@ -4,3 +4,7 @@ from django.apps import AppConfig
class SystemConfig(AppConfig): class SystemConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'dvadmin.system' name = 'dvadmin.system'
def ready(self):
# 注册信号
import dvadmin.system.signals # 确保路径正确

View File

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

View File

@@ -546,5 +546,50 @@
"children": [] "children": []
} }
] ]
},
{
"label": "文件存储引擎",
"value": "file_engine",
"type": 0,
"color": null,
"is_value": false,
"status": true,
"sort": 9,
"remark": null,
"children": [
{
"label": "本地",
"value": "local",
"type": 0,
"color": "primary",
"is_value": true,
"status": true,
"sort": 1,
"remark": null,
"children": []
},
{
"label": "阿里云oss",
"value": "oss",
"type": 0,
"color": "success",
"is_value": true,
"status": true,
"sort": 2,
"remark": null,
"children": []
},
{
"label": "腾讯cos",
"value": "cos",
"type": 0,
"color": "warning",
"is_value": true,
"status": true,
"sort": 3,
"remark": null,
"children": []
}
]
} }
] ]

View File

@@ -235,5 +235,252 @@
"children": [] "children": []
} }
] ]
},
{
"title": "文件存储配置",
"key": "file_storage",
"value": null,
"sort": 0,
"status": true,
"data_options": null,
"form_item_type": 0,
"rule": null,
"placeholder": null,
"setting": null,
"children": [
{
"title": "存储引擎",
"key": "file_engine",
"value": "local",
"sort": 1,
"status": true,
"data_options": null,
"form_item_type": 4,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请选择存储引擎",
"setting": "file_engine",
"children": []
},
{
"title": "文件是否备份",
"key": "file_backup",
"value": false,
"sort": 2,
"status": true,
"data_options": null,
"form_item_type": 9,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "启用云存储时,文件是否备份到本地",
"setting": null,
"children": []
},
{
"title": "阿里云-AccessKey",
"key": "aliyun_access_key",
"value": null,
"sort": 3,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入AccessKey",
"setting": null,
"children": []
},
{
"title": "阿里云-Secret",
"key": "aliyun_access_secret",
"value": null,
"sort": 4,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入Secret",
"setting": null,
"children": []
},
{
"title": "阿里云-Endpoint",
"key": "aliyun_endpoint",
"value": null,
"sort": 5,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入Endpoint",
"setting": null,
"children": []
},
{
"title": "阿里云-上传路径",
"key": "aliyun_path",
"value": "/media/",
"sort": 5,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入上传路径",
"setting": null,
"children": []
},
{
"title": "阿里云-Bucket",
"key": "aliyun_bucket",
"value": null,
"sort": 7,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入Bucket",
"setting": null,
"children": []
},{
"title": "阿里云-cdn地址",
"key": "aliyun_cdn_url",
"value": null,
"sort": 7,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入cdn地址",
"setting": null,
"children": []
},
{
"title": "腾讯云-SecretId",
"key": "tencent_secret_id",
"value": null,
"sort": 8,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入SecretId",
"setting": null,
"children": []
},
{
"title": "腾讯云-SecretKey",
"key": "tencent_secret_key",
"value": null,
"sort": 9,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入SecretKey",
"setting": null,
"children": []
},
{
"title": "腾讯云-Region",
"key": "tencent_region",
"value": null,
"sort": 10,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入Region",
"setting": null,
"children": []
},
{
"title": "腾讯云-Bucket",
"key": "tencent_bucket",
"value": null,
"sort": 11,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入Bucket",
"setting": null,
"children": []
},
{
"title": "腾讯云-上传路径",
"key": "tencent_path",
"value": "/media/",
"sort": 12,
"status": false,
"data_options": null,
"form_item_type": 0,
"rule": [
{
"required": false,
"message": "必填项不能为空"
}
],
"placeholder": "请输入上传路径",
"setting": null,
"children": []
}
]
} }
] ]

View File

@@ -10,7 +10,7 @@ django.setup()
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from application.settings import BASE_DIR from application.settings import BASE_DIR
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \ from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
RoleMenuInitSerializer, RoleMenuButtonInitSerializer RoleMenuInitSerializer, RoleMenuButtonInitSerializer
@@ -29,7 +29,7 @@ class Command(BaseCommand):
def serializer_data(self, serializer, query_set: QuerySet): def serializer_data(self, serializer, query_set: QuerySet):
serializer = serializer(query_set, many=True) serializer = serializer(query_set, many=True)
data = json.loads(json.dumps(serializer.data, ensure_ascii=False)) data = json.loads(json.dumps(serializer.data, ensure_ascii=False))
with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w') as f: with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w',encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False) json.dump(data, f, indent=4, ensure_ascii=False)
return return
@@ -57,6 +57,12 @@ class Command(BaseCommand):
def generate_system_config(self): def generate_system_config(self):
self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True))
def generate_role_menu(self):
self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all())
def generate_role_menu_button(self):
self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all())
def handle(self, *args, **options): def handle(self, *args, **options):
generate_name = options.get('generate_name') generate_name = options.get('generate_name')
generate_name_dict = { generate_name_dict = {
@@ -67,6 +73,8 @@ class Command(BaseCommand):
"api_white_list": self.generate_api_white_list, "api_white_list": self.generate_api_white_list,
"dictionary": self.generate_dictionary, "dictionary": self.generate_dictionary,
"system_config": self.generate_system_config, "system_config": self.generate_system_config,
"role_menu": self.generate_role_menu,
"role_menu_button": self.generate_role_menu_button,
} }
if not generate_name: if not generate_name:
for ele in generate_name_dict.keys(): for ele in generate_name_dict.keys():

View File

@@ -73,11 +73,18 @@ class Users(CoreModel, AbstractUser):
help_text="关联部门", help_text="关联部门",
) )
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", 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() objects = CustomUserManager()
def set_password(self, raw_password): def set_password(self, raw_password):
if raw_password:
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
def save(self, *args, **kwargs):
if self.name == "":
self.name = self.username
super().save(*args, **kwargs)
class Meta: class Meta:
db_table = table_prefix + "system_users" db_table = table_prefix + "system_users"
verbose_name = "用户表" verbose_name = "用户表"
@@ -121,6 +128,27 @@ class Dept(CoreModel):
help_text="上级部门", 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 @classmethod
def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None): def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
""" """
@@ -407,6 +435,18 @@ class FileList(CoreModel):
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型") 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="文件大小") 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") 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): def save(self, *args, **kwargs):
if not self.md5sum: # file is new if not self.md5sum: # file is new

View File

@@ -1,4 +1,10 @@
from django.dispatch import Signal import time
from django.db.models.signals import post_save, post_delete
from django.dispatch import Signal, receiver
from django.core.cache import cache
from dvadmin.system.models import MessageCenterTargetUser
# 初始化信号 # 初始化信号
pre_init_complete = Signal() pre_init_complete = Signal()
detail_init_complete = Signal() detail_init_complete = Signal()
@@ -10,3 +16,12 @@ post_tenants_init_complete = Signal()
post_tenants_all_init_complete = Signal() post_tenants_all_init_complete = Signal()
# 租户创建完成信号 # 租户创建完成信号
tenants_create_complete = Signal() tenants_create_complete = Signal()
# 全局变量用于标记最后修改时间
last_db_change_time = time.time()
@receiver(post_save, sender=MessageCenterTargetUser)
@receiver(post_delete, sender=MessageCenterTargetUser)
def update_last_change_time(sender, **kwargs):
cache.set('last_db_change_time', time.time(), timeout=None) # 设置永不超时的键值对

View File

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

View File

@@ -41,6 +41,14 @@ class DownloadCenterViewSet(CustomModelViewSet):
serializer_class = DownloadCenterSerializer serializer_class = DownloadCenterSerializer
filter_class = DownloadCenterFilterSet filter_class = DownloadCenterFilterSet
permission_classes = [] permission_classes = []
extra_filter_class = []
def get_queryset(self): def get_queryset(self):
# 判断是否是 Swagger 文档生成阶段,防止报错
if getattr(self, 'swagger_fake_view', False):
return self.queryset.model.objects.none()
# 正常请求下的逻辑
if self.request.user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(creator=self.request.user) return super().get_queryset().filter(creator=self.request.user)

View File

@@ -1,12 +1,15 @@
import hashlib import hashlib
import mimetypes import mimetypes
import django_filters
from django.conf import settings
from django.db import connection
from rest_framework import serializers from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from application import dispatch from application import dispatch
from dvadmin.system.models import FileList 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.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
@@ -15,16 +18,25 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True) url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance): def get_url(self, instance):
base_url = f"{self.request.scheme}://{self.request.get_host()}/" if self.request.query_params.get('prefix'):
return base_url + (instance.file_url or (f'media/{str(instance.url)}')) 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: class Meta:
model = FileList model = FileList
fields = "__all__" fields = "__all__"
def create(self, validated_data): def create(self, validated_data):
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local' file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup") file_backup = dispatch.get_system_config_values("file_storage.file_backup")
file = self.initial_data.get('file') file = self.initial_data.get('file')
file_size = file.size file_size = file.size
validated_data['name'] = str(file) validated_data['name'] = str(file)
@@ -35,18 +47,20 @@ class FileSerializer(CustomModelSerializer):
validated_data['md5sum'] = md5.hexdigest() validated_data['md5sum'] = md5.hexdigest()
validated_data['engine'] = file_engine validated_data['engine'] = file_engine
validated_data['mime_type'] = file.content_type 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: if file_backup:
validated_data['url'] = file validated_data['url'] = file
if file_engine == 'oss': if file_engine == 'oss':
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload from dvadmin.utils.aliyunoss import ali_oss_upload
file_path = ali_oss_upload(file) file_path = ali_oss_upload(file, file_name=validated_data['name'])
if file_path: if file_path:
validated_data['file_url'] = file_path validated_data['file_url'] = file_path
else: else:
raise ValueError("上传失败") raise ValueError("上传失败")
elif file_engine == 'cos': elif file_engine == 'cos':
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload from dvadmin.utils.tencentcos import tencent_cos_upload
file_path = tencent_cos_upload(file) file_path = tencent_cos_upload(file, file_name=validated_data['name'])
if file_path: if file_path:
validated_data['file_url'] = file_path validated_data['file_url'] = file_path
else: else:
@@ -64,6 +78,22 @@ class FileSerializer(CustomModelSerializer):
return super().create(validated_data) 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): class FileViewSet(CustomModelViewSet):
""" """
文件管理接口 文件管理接口
@@ -75,5 +105,22 @@ class FileViewSet(CustomModelViewSet):
""" """
queryset = FileList.objects.all() queryset = FileList.objects.all()
serializer_class = FileSerializer serializer_class = FileSerializer
filter_fields = ['name', ] filter_class = FileFilter
permission_classes = [] 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 captcha.views import CaptchaStore, captcha_image
from django.contrib import auth from django.contrib import auth
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.hashers import check_password, make_password
from django.db.models import Q from django.db.models import Q
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers 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.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView
@@ -97,16 +100,17 @@ class LoginSerializer(TokenObtainPairSerializer):
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误 # 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
attrs['username'] = user.username attrs['username'] = user.username
data = super().validate(attrs) data = super().validate(attrs)
data["username"] = self.user.username
data["name"] = self.user.name data["name"] = self.user.name
data["userId"] = self.user.id data["userId"] = self.user.id
data["avatar"] = self.user.avatar data["avatar"] = self.user.avatar
data['user_type'] = self.user.user_type data['user_type'] = self.user.user_type
data['pwd_change_count'] = self.user.pwd_change_count
dept = getattr(self.user, 'dept', None) dept = getattr(self.user, 'dept', None)
if dept: if dept:
data['dept_info'] = { data['dept_info'] = {
'dept_id': dept.id, 'dept_id': dept.id,
'dept_name': dept.name, 'dept_name': dept.name,
} }
role = getattr(self.user, 'role', None) role = getattr(self.user, 'role', None)
if role: if role:

View File

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

View File

@@ -139,21 +139,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
def websocket_push(user_id, message):
"""
主动推送消息
"""
username = "user_" + str(user_id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
username,
{
"type": "push.message",
"json": message
}
)
class MessageCenterCreateSerializer(CustomModelSerializer): class MessageCenterCreateSerializer(CustomModelSerializer):
""" """
消息中心-新增-序列化器 消息中心-新增-序列化器
@@ -182,10 +167,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request) targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
targetuser_instance.is_valid(raise_exception=True) targetuser_instance.is_valid(raise_exception=True)
targetuser_instance.save() targetuser_instance.save()
for user in users:
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM',
"content": '您有一条新消息~', "unread": unread_count})
return data return data
class Meta: class Meta:
@@ -225,10 +206,6 @@ class MessageCenterViewSet(CustomModelViewSet):
queryset.save() queryset.save()
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
# 主动推送消息
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT',
"content": '您查看了一条消息~', "unread": unread_count})
return DetailResponse(data=serializer.data, msg="获取成功") return DetailResponse(data=serializer.data, msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])

View File

@@ -10,22 +10,29 @@ from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
from dvadmin.system.views.dept import DeptSerializer from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer): class RoleSerializer(CustomModelSerializer):
""" """
角色-序列化器 角色-序列化器
""" """
users = serializers.SerializerMethodField()
@staticmethod
def get_users(instance):
users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name')
return users
class Meta: class Meta:
model = Role model = Role
@@ -101,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
""" """
角色管理接口 角色管理接口
@@ -116,3 +122,82 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
create_serializer_class = RoleCreateUpdateSerializer create_serializer_class = RoleCreateUpdateSerializer
update_serializer_class = RoleCreateUpdateSerializer update_serializer_class = RoleCreateUpdateSerializer
search_fields = ['name', 'key'] 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 @Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理 @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 import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ListField
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \ from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \
MenuField MenuField, FieldPermission
from dvadmin.system.views.menu import MenuSerializer from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
class RoleMenuButtonPermissionSerializer(CustomModelSerializer): class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
""" """
菜单按钮-序列化 角色-菜单-按钮-权限 查询序列化
""" """
class Meta: class Meta:
@@ -34,7 +30,7 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer): class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
""" """
初始化菜单按钮-序列化 角色-菜单-按钮-权限 创建/修改序列化
""" """
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True) menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
menu_button__value = serializers.CharField(source='menu_button.value', 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"] read_only_fields = ["id"]
class RoleButtonPermissionSerializer(CustomModelSerializer): class RoleMenuSerializer(CustomModelSerializer):
""" """
角色按钮权限 角色-菜单 序列化
""" """
isCheck = serializers.SerializerMethodField() isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
def get_isCheck(self, instance): def get_isCheck(self, instance):
params = self.request.query_params 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( return RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'], menu_button_id=instance.id,
role__id=params.get('role'), role_id=params.get('roleId', data.get('roleId')),
).exists() ).exists()
def get_data_range(self, instance): def get_data_range(self, instance):
params = self.request.query_params obj = self.get_role_menu_btn_prem(instance)
obj = RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).first()
if obj is None: if obj is None:
return None return None
return obj.data_range 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: class Meta:
model = MenuButton model = MenuButton
fields = ['id', 'name', 'value', 'isCheck', 'data_range'] fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept']
class RoleFieldPermissionSerializer(CustomModelSerializer):
class Meta:
model = FieldPermission
fields = "__all__"
class RoleMenuFieldSerializer(CustomModelSerializer): class RoleMenuFieldSerializer(CustomModelSerializer):
"""
角色-菜单-字段 序列化
"""
is_query = serializers.SerializerMethodField() is_query = serializers.SerializerMethodField()
is_create = serializers.SerializerMethodField() is_create = serializers.SerializerMethodField()
is_update = serializers.SerializerMethodField() is_update = serializers.SerializerMethodField()
def get_is_query(self, instance): def get_is_query(self, instance):
params = self.request.query_params 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: if queryset:
return queryset.is_query return queryset.is_query
return False return False
def get_is_create(self, instance): def get_is_create(self, instance):
params = self.request.query_params 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: if queryset:
return queryset.is_create return queryset.is_create
return False return False
def get_is_update(self, instance): def get_is_update(self, instance):
params = self.request.query_params 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: if queryset:
return queryset.is_update return queryset.is_update
return False return False
@@ -111,54 +143,6 @@ class RoleMenuFieldSerializer(CustomModelSerializer):
fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update'] 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): class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
""" """
菜单按钮接口 菜单按钮接口
@@ -174,202 +158,111 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer
extra_filter_class = [] extra_filter_class = []
# @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
# def get_role_premission(self, request): def get_role_menu(self, request):
# """ """
# 角色授权获取: 获取 角色-菜单
# :param request: role :param request:
# :return: menu,btns,columns :return:
# """ """
# params = request.query_params menu_queryset = Menu.objects.all()
# is_superuser = request.user.is_superuser serializer = RoleMenuSerializer(menu_queryset, many=True, request=request)
# if is_superuser: return DetailResponse(data=serializer.data)
# queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all()
# else: @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
# role_id = request.user.role.values_list('id', flat=True) def set_role_menu(self, request):
# 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) :param request:
# data = serializer.data :return:
# return DetailResponse(data=data) """
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]) @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 params = request.query_params
# 需要授权的角色信息 menuId = params.get('menuId', None)
current_role = params.get('role', None) menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId)
# 当前登录用户的角色 menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request)
role_list = request.user.role.values_list('id', flat=True) menu_field_queryset = MenuField.objects.filter(menu_id=menuId)
if current_role is None: menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request)
return ErrorResponse(msg='参数错误') return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data})
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)
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) @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 data = request.data
RoleMenuPermission.objects.filter(role=pk).delete() for col in data:
RoleMenuButtonPermission.objects.filter(role=pk).delete() FieldPermission.objects.update_or_create(
for item in body: role_id=pk, field_id=col.get('id'),
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'),
defaults={ defaults={
'is_query': col.get('is_query'),
'is_create': col.get('is_create'), '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]) return DetailResponse(data=[], msg="更新成功")
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="参数错误")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request): def set_role_menu_btn(self, request):
""" """
获取数据权限范围:角色授权页面使用 设置 角色-菜单-按钮
:param request:
:return:
""" """
is_superuser = request.user.is_superuser data = request.data
if is_superuser: isCheck = data.get('isCheck', None)
data = [ roleId = data.get('roleId', None)
{"value": 0, "label": '仅本人数据权限'}, btnId = data.get('btnId', None)
{"value": 1, "label": '本部门及以下数据权限'}, data_range = data.get('data_range', None) or 0 # 默认仅本人权限
{"value": 2, "label": '本部门数据权限'}, dept = data.get('dept', None) or [] # 默认空部门
{"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_range_list = list(set(role_queryset)) if isCheck:
for item in data_range_list: # 添加权限:创建关联记录
if item == 0: instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
data = data menu_button_id=btnId,
elif item == 1: data_range=data_range)
data.extend([ # 自定义部门权限
{"value": 1, "label": '本部门及以下数据权限'}, if data_range == 4 and dept:
{"value": 2, "label": '本部门数据权限'} instance.dept.set(dept)
])
elif item == 2:
data.extend([{"value": 2, "label": '本部门数据权限'}])
elif item == 3:
data.extend([{"value": 3, "label": '全部数据权限'}])
elif item == 4:
data.extend([{"value": 4, "label": '自定义数据权限'}])
else: 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]) @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request): 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 dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled
data.append(dept) data.append(dept)
return DetailResponse(data=data) 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, "dept": user.dept_id,
"is_superuser": user.is_superuser, "is_superuser": user.is_superuser,
"role": user.role.values_list('id', flat=True), "role": user.role.values_list('id', flat=True),
"pwd_change_count":user.pwd_change_count
} }
if hasattr(connection, 'tenant'): if hasattr(connection, 'tenant'):
result['tenant_id'] = connection.tenant and connection.tenant.id result['tenant_id'] = connection.tenant and connection.tenant.id
@@ -319,7 +320,6 @@ class UserViewSet(CustomModelViewSet):
"""密码修改""" """密码修改"""
data = request.data data = request.data
old_pwd = data.get("oldPassword") old_pwd = data.get("oldPassword")
print(old_pwd)
new_pwd = data.get("newPassword") new_pwd = data.get("newPassword")
new_pwd2 = data.get("newPassword2") new_pwd2 = data.get("newPassword2")
if old_pwd is None or new_pwd is None or new_pwd2 is None: if old_pwd is None or new_pwd is None or new_pwd2 is None:
@@ -335,12 +335,28 @@ class UserViewSet(CustomModelViewSet):
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest() old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(str(old_pwd_md5), request.user.password) verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_password: if verify_password:
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.pwd_change_count += 1
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")
else: else:
return ErrorResponse(msg="旧密码不正确") 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]) @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
def reset_to_default_password(self, request,pk): def reset_to_default_password(self, request,pk):
"""恢复默认密码""" """恢复默认密码"""

View File

@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
import oss2
from rest_framework.exceptions import ValidationError
from application import dispatch
# 进度条
# 当无法确定待上传的数据长度时total_bytes的值为None。
def percentage(consumed_bytes, total_bytes):
if total_bytes:
rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
print('\r{0}% '.format(rate), end='')
def ali_oss_upload(file, file_name):
"""
阿里云OSS上传
"""
try:
file.seek(0)
file_read = file.read()
except Exception as e:
file_read = file
if not file:
raise ValidationError('请上传文件')
# 转存到oss
path_prefix = dispatch.get_system_config_values("file_storage.aliyun_path")
if not path_prefix.endswith('/'):
path_prefix = path_prefix + '/'
if path_prefix.startswith('/'):
path_prefix = path_prefix[1:]
base_fil_name = f'{path_prefix}{file_name}'
# 获取OSS配置
# 获取的AccessKey
access_key_id = dispatch.get_system_config_values("file_storage.aliyun_access_key")
access_key_secret = dispatch.get_system_config_values("file_storage.aliyun_access_secret")
auth = oss2.Auth(access_key_id, access_key_secret)
# 这个是需要用特定的地址,不同地域的服务器地址不同,不要弄错了
# 参考官网给的地址配置https://www.alibabacloud.com/help/zh/object-storage-service/latest/regions-and-endpoints#concept-zt4-cvy-5db
endpoint = dispatch.get_system_config_values("file_storage.aliyun_endpoint")
bucket_name = dispatch.get_system_config_values("file_storage.aliyun_bucket")
if bucket_name.endswith(endpoint):
bucket_name = bucket_name.replace(f'.{endpoint}', '')
# 你的项目名称类似于不同的项目上传的图片前缀url不同
bucket = oss2.Bucket(auth, endpoint, bucket_name) # 项目名称
# 生成外网访问的文件路径
aliyun_cdn_url = dispatch.get_system_config_values("file_storage.aliyun_cdn_url")
if aliyun_cdn_url:
if aliyun_cdn_url.endswith('/'):
aliyun_cdn_url = aliyun_cdn_url[1:]
file_path = f"{aliyun_cdn_url}/{base_fil_name}"
else:
file_path = f"https://{bucket_name}.{endpoint}/{base_fil_name}"
# 这个是阿里提供的SDK方法
res = bucket.put_object(base_fil_name, file_read, progress_callback=percentage)
# 如果上传状态是200 代表成功 返回文件外网访问路径
if res.status == 200:
return file_path
else:
return None

View File

@@ -1,14 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from itertools import groupby
from django.db.models import F from django.db.models import F
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import FieldPermission, MenuField from dvadmin.system.models import FieldPermission, MenuField
from dvadmin.utils.json_response import DetailResponse 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: class FieldPermissionMixin:
@@ -17,55 +28,16 @@ class FieldPermissionMixin:
""" """
获取字段权限 获取字段权限
""" """
finded = False model = self.serializer_class.Meta.model.__name__
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 []
user = request.user user = request.user
# 创建一个默认字典来存储最终的结果
if user.is_superuser == 1: if user.is_superuser == 1:
data = MenuField.objects.filter( model=model['model']).values('field_name') data = MenuField.objects.filter(model=model).values('field_name')
for item in data: result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data}
item['is_create'] = True
item['is_query'] = True
item['is_update'] = True
else: else:
roles = request.user.role.values_list('id', flat=True) roles = request.user.role.values_list('id', flat=True)
data = FieldPermission.objects.filter( 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')) ).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name'))
result = merge_permission(data)
""" return DetailResponse(data=result)
合并权限
这段代码首先根据 field_name 对列表进行排序,
然后使用 groupby 按 field_name 进行分组。
对于每个组,它创建一个新的字典 merged
并遍历组中的每个字典将布尔值字段使用逻辑或or操作符进行合并如果 merged 中还没有该字段,则默认为 False
其他字段(如 field_name则直接取组的关键字即 key
"""
# 使用field_name对列表进行分组, # groupby 需要先对列表进行排序,因为它只能对连续相同的元素进行分组。
grouped = groupby(sorted(list(data), key=lambda x: x['field_name']), key=lambda x: x['field_name'])
data = []
# 遍历分组,合并权限
for key, group in grouped:
# 初始化一个空字典来存储合并后的结果
merged = {}
for item in group:
# 合并权限, True值优先
merged['is_create'] = merged.get('is_create', False) or item['is_create']
merged['is_query'] = merged.get('is_query', False) or item['is_query']
merged['is_update'] = merged.get('is_update', False) or item['is_update']
merged['field_name'] = key
data.append(merged)
return DetailResponse(data=data)

View File

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

View File

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

View File

@@ -305,11 +305,10 @@ class ExportSerializerMixin:
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
data = self.export_serializer_class(queryset, many=True, request=request).data data = self.export_serializer_class(queryset, many=True, request=request).data
try: try:
from dvadmin3_celery import settings
async_export_data.delay( async_export_data.delay(
data, data,
str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"), 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)}数据导出任务').pk, 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 self.export_field_label
) )
return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载") return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载")

View File

@@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model):
super().delete(using=using, *args, **kwargs) super().delete(using=using, *args, **kwargs)
class CoreModelManager(models.Manager):
def get_queryset(self):
is_deleted = getattr(self.model, 'is_soft_delete', False)
flow_work_status = getattr(self.model, 'flow_work_status', False)
queryset = super().get_queryset()
if flow_work_status:
queryset = queryset.filter(flow_work_status=1)
if is_deleted:
queryset = queryset.filter(is_deleted=False)
return queryset
def create(self,request: Request=None, **kwargs):
data = {**kwargs}
if request:
request_user = request.user
data["creator"] = request_user
data["modifier"] = request_user.id
data["dept_belong_id"] = request_user.dept_id
# 调用父类的create方法执行实际的创建操作
return super().create(**data)
class CoreModel(models.Model): class CoreModel(models.Model):
""" """
核心标准抽象模型模型,可直接继承使用 核心标准抽象模型模型,可直接继承使用
@@ -98,7 +118,8 @@ class CoreModel(models.Model):
verbose_name="修改时间") verbose_name="修改时间")
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间") verbose_name="创建时间")
objects = CoreModelManager()
all_objects = models.Manager()
class Meta: class Meta:
abstract = True abstract = True
verbose_name = '核心模型' verbose_name = '核心模型'

View File

@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from rest_framework.exceptions import ValidationError
from application import dispatch
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
# 进度条
# 当无法确定待上传的数据长度时total_bytes的值为None。
def percentage(consumed_bytes, total_bytes):
if total_bytes:
rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
print('\r{0}% '.format(rate), end='')
def tencent_cos_upload(file, file_name):
try:
file.seek(0)
file_read = file.read()
except Exception as e:
file_read = file
if not file:
raise ValidationError('请上传文件')
# 生成文件名
path_prefix = dispatch.get_system_config_values("file_storage.tencent_path")
if not path_prefix.endswith('/'):
path_prefix = path_prefix + '/'
if path_prefix.startswith('/'):
path_prefix = path_prefix[1:]
base_fil_name = f'{path_prefix}{file_name}'
# 获取cos配置
# 1. 设置用户属性, 包括 secret_id, secret_key, region等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成
secret_id = dispatch.get_system_config_values("file_storage.tencent_secret_id") # 用户的 SecretId建议使用子账号密钥授权遵循最小权限指引降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
secret_key = dispatch.get_system_config_values("file_storage.tencent_secret_key") # 用户的 SecretKey建议使用子账号密钥授权遵循最小权限指引降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
region = dispatch.get_system_config_values("file_storage.tencent_region") # 替换为用户的 region已创建桶归属的 region 可以在控制台查看https://console.cloud.tencent.com/cos5/bucket # COS 支持的所有 region 列表参见https://cloud.tencent.com/document/product/436/6224
bucket = dispatch.get_system_config_values("file_storage.tencent_bucket") # 要访问的桶名称
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
client = CosS3Client(config)
# 访问地址
base_file_url = f'https://{bucket}.cos.{region}.myqcloud.com'
# 生成外网访问的文件路径
if base_file_url.endswith('/'):
file_path = base_file_url + base_fil_name
else:
file_path = f'{base_file_url}/{base_fil_name}'
# 这个是阿里提供的SDK方法 bucket是调用的4.1中配置的变量名
try:
response = client.put_object(
Bucket=bucket,
Body=file_read,
Key=base_fil_name,
EnableMD5=False
)
return file_path
except:
return None

View File

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

View File

@@ -7,25 +7,27 @@ djangorestframework==3.15.2
django-restql==0.15.4 django-restql==0.15.4
django-simple-captcha==0.6.0 django-simple-captcha==0.6.0
django-timezone-field==7.0 django-timezone-field==7.0
djangorestframework-simplejwt==5.3.1 djangorestframework_simplejwt==5.4.0
drf-yasg==1.21.7 drf-yasg==1.21.7
mysqlclient==2.2.0 mysqlclient==2.2.0
pypinyin==0.51.0 pypinyin==0.51.0
ua-parser==0.18.0 ua-parser==0.18.0
pyparsing==3.1.2 pyparsing==3.1.2
openpyxl==3.1.5 openpyxl==3.1.5
requests==2.32.3 requests==2.32.4
typing-extensions==4.12.2 typing-extensions==4.12.2
tzlocal==5.2 tzlocal==5.2
channels==4.1.0 channels==4.1.0
channels-redis==4.2.0 channels-redis==4.2.0
websockets==11.0.3
user-agents==2.2.0 user-agents==2.2.0
six==1.16.0 six==1.16.0
whitenoise==6.7.0 whitenoise==6.7.0
psycopg2==2.9.9 psycopg2==2.9.9
uvicorn==0.30.3 uvicorn==0.30.3
gunicorn==22.0.0 gunicorn==23.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
dvadmin3-celery==3.1.6
oss2==2.19.1
cos-python-sdk-v5==1.9.37

87
crud-gen.sh Normal file
View File

@@ -0,0 +1,87 @@
if ! [ -f ".env" ];then
echo ".env file not found"
exit 1
fi
if [ -z "$3" ]; then
echo "Use: $0 <app_name> <view_name> <table_name>"
exit 1
fi
DIR=./web/src/views/$1/$2
# 设置数据库连接信息
HOST="177.10.0.13"
USER="root"
PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g')
DATABASE="django-vue3-admin"
TABLE=$3
TARGET_FILE="./web/src/views/$1/$2/crud.tsx"
# 表是否存在
TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l)
if [ "$TABLE_EXISTS" -eq 0 ]; then
echo "Table $TABLE does not exist in database $DATABASE."
exit 1
fi
mkdir -p $DIR
cp -r ./web/src/views/template/* $DIR
sed -i "s/VIEWSETNAME/$2/g" $DIR/*
sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE
# 查询表结构
QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;"
# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置
mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do
# 映射 MySQL 数据类型到 fast-crud 类型
case "$DATA_TYPE" in
"int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double")
TYPE="number"
;;
"date"|"datetime"|"timestamp")
TYPE="date"
;;
*)
TYPE="text"
;;
esac
echo " $COLUMN_NAME: {
title: '$COLUMN_NAME',
type: '$TYPE',
search: { show: true },
column: {
minWidth: 120,
sortable: 'custom',
},
form: {" >> $TARGET_FILE
if [ "$IS_NULLABLE" = "NO" ]; then
echo " helper: {
render() {
return <div style={"color:blue"}>$COLUMN_NAME 是必填的</div>;
}
},
rules: [{
required: true, message: '$COLUMN_NAME 是必填的'
}]," >> $TARGET_FILE
fi
echo " component: {
placeholder: '请输入 $COLUMN_NAME',
},
},
}," >> $TARGET_FILE
done
echo " },
},
};
}" >> $TARGET_FILE

View File

@@ -1,4 +1,4 @@
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine
WORKDIR /web/ WORKDIR /web/
COPY web/. . COPY web/. .
RUN yarn install --registry=https://registry.npmmirror.com RUN yarn install --registry=https://registry.npmmirror.com

52
init.sh
View File

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

View File

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

View File

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

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,10 +1,11 @@
{ {
"name": "django-vue3-admin", "name": "django-vue3-admin",
"version": "3.0.4", "version": "3.2.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite --force", "dev": "vite --force",
"build:dev": "vite build --mode development",
"build": "vite build", "build": "vite build",
"build:local": "vite build --mode local_prod", "build:local": "vite build --mode local_prod",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
@@ -15,6 +16,7 @@
"@fast-crud/fast-extends": "^1.21.2", "@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
@@ -24,6 +26,7 @@
"axios": "^1.7.4", "axios": "^1.7.4",
"countup.js": "^2.8.0", "countup.js": "^2.8.0",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"date-holidays": "^3.24.1",
"e-icon-picker": "2.1.1", "e-icon-picker": "2.1.1",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"echarts-gl": "^2.0.9", "echarts-gl": "^2.0.9",
@@ -34,7 +37,9 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"js-table2excel": "^1.1.2", "js-table2excel": "^1.1.2",
"jsplumb": "^2.15.6", "jsplumb": "^2.15.6",
"less": "^4.3.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lunar-javascript": "^1.7.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.28", "pinia": "^2.0.28",
@@ -49,9 +54,12 @@
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"ts-md5": "^1.3.1", "ts-md5": "^1.3.1",
"upgrade": "^1.1.0", "upgrade": "^1.1.0",
"vant": "^4.9.19",
"vant4-kit": "^1.0.3",
"vue": "^3.4.38", "vue": "^3.4.38",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8", "vue-cropper": "^1.0.8",
"vue-draggable-plus": "^0.6.0",
"vue-grid-layout": "^3.0.0-beta1", "vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.14.0", "vue-i18n": "^9.14.0",
"vue-router": "^4.4.3", "vue-router": "^4.4.3",

View File

@@ -11,7 +11,7 @@
<script setup lang="ts" name="app"> <script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue'; import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
@@ -26,7 +26,8 @@ const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue')); const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue')); const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue')); const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
import { ElMessageBox, ElNotification, NotificationHandle } from 'element-plus';
import { useCore } from '/@/utils/cores';
// 定义变量内容 // 定义变量内容
const { messages, locale } = useI18n(); const { messages, locale } = useI18n();
const setingsRef = ref(); const setingsRef = ref();
@@ -34,8 +35,8 @@ const route = useRoute();
const stores = useTagsViewRoutes(); const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket'; const core = useCore();
import { ElNotification } from 'element-plus'; const router = useRouter();
// 获取版本号 // 获取版本号
const getVersion = computed(() => { const getVersion = computed(() => {
let isVersion = false; let isVersion = false;
@@ -67,7 +68,15 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => { mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer(); setingsRef.value.openDrawer();
}); });
// 设置皮肤缓存版本,每次更新版本可以所有用户清空缓存
const themeConfigVersion = '1.0.0'
// 获取缓存中的布局配置 // 获取缓存中的布局配置
if (Local.get('themeConfigVersion') !== themeConfigVersion) {
Local.clear();
Local.set('themeConfigVersion', themeConfigVersion);
window.location.reload();
return
}
if (Local.get('themeConfig')) { if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') }); storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle'); document.documentElement.style.cssText = Local.get('themeConfigStyle');
@@ -82,45 +91,5 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {}); mittBus.off('openSetingsDrawer', () => {});
}); });
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
other.useFavicon();
if (!websocket.websocket) {
//websockt 模块
try {
websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
}
},
{
deep: true,
}
);
// websocket相关代码
import { messageCenterStore } from '/@/stores/messageCenter';
const wsReceive = (message: any) => {
const data = JSON.parse(message.data);
const { unread } = data;
const messageCenter = messageCenterStore();
messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') {
ElNotification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 5000,
});
}
};
onBeforeUnmount(() => {
// 关闭连接
websocket.close();
});
</script> </script>

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: "iconfont"; /* Project id 3882322 */
src: url('iconfont.woff2?t=1676037377315') format('woff2'),
url('iconfont.woff?t=1676037377315') format('woff'),
url('iconfont.ttf?t=1676037377315') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiaoxizhongxin:before {
content: "\e665";
}
.icon-xitongshezhi:before {
content: "\e7ba";
}
.icon-caozuorizhi:before {
content: "\e611";
}
.icon-guanlidenglurizhi:before {
content: "\ea45";
}
.icon-rizhi:before {
content: "\e60c";
}
.icon-system:before {
content: "\e684";
}
.icon-Area:before {
content: "\eaa2";
}
.icon-file:before {
content: "\e671";
}
.icon-dict:before {
content: "\e626";
}
.icon-configure:before {
content: "\e733";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,427 @@
@font-face {
font-family: "iconfont"; /* Project id 2298093 */
src: url('iconfont.woff2?t=1627014681704') format('woff2'),
url('iconfont.woff?t=1627014681704') format('woff'),
url('iconfont.ttf?t=1627014681704') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-diannao101:before {
content: "\e670";
}
.icon-diannao:before {
content: "\e618";
}
.icon-diannao1:before {
content: "\e622";
}
.icon-diannao-shuju:before {
content: "\e63e";
}
.icon-shoujidiannao:before {
content: "\e62e";
}
.icon-diannaobangong:before {
content: "\e647";
}
.icon-LoggedinPC:before {
content: "\e604";
}
.icon-barcode-qr:before {
content: "\e61e";
}
.icon-zhongduancanshuchaxun:before {
content: "\e638";
}
.icon-shouye_dongtaihui:before {
content: "\e606";
}
.icon-putong:before {
content: "\e603";
}
.icon-dongtai:before {
content: "\e659";
}
.icon-wenducanshu-05:before {
content: "\e634";
}
.icon-zhongduancanshu:before {
content: "\e63b";
}
.icon-tongzhi1:before {
content: "\e63a";
}
.icon-tongzhi2:before {
content: "\e649";
}
.icon-tongzhi3:before {
content: "\e648";
}
.icon-tongzhi4:before {
content: "\e60c";
}
.icon-dianhua:before {
content: "\e615";
}
.icon-xianshimima:before {
content: "\e63c";
}
.icon-yincangmima:before {
content: "\e63d";
}
.icon-shuxing:before {
content: "\e67a";
}
.icon-juxingkaobei:before {
content: "\e7a5";
}
.icon-shuxingtu:before {
content: "\e685";
}
.icon-bolangneng:before {
content: "\e745";
}
.icon-bolangnengshiyanchang:before {
content: "\e746";
}
.icon--chaifenhang:before {
content: "\e6d1";
}
.icon--chaifenlie:before {
content: "\e6d0";
}
.icon-tupianyulan:before {
content: "\e67e";
}
.icon-15tupianyulan:before {
content: "\e624";
}
.icon-728bianjiqi_zitidaxiao:before {
content: "\e660";
}
.icon-ziti:before {
content: "\e7b1";
}
.icon-font-size:before {
content: "\eaef";
}
.icon-tuodong:before {
content: "\e6a8";
}
.icon-zhongyingwen1:before {
content: "\e7a3";
}
.icon-fuhao-yingwen:before {
content: "\e714";
}
.icon-fuhao-zhongwen:before {
content: "\e712";
}
.icon-diqiu:before {
content: "\e689";
}
.icon-xingqiu:before {
content: "\e65c";
}
.icon-diqiu1:before {
content: "\e631";
}
.icon-huanjingxingqiu:before {
content: "\e617";
}
.icon-zidingyibuju:before {
content: "\e637";
}
.icon-dayin:before {
content: "\e612";
}
.icon-step:before {
content: "\e601";
}
.icon-30xuanzhongyuanxingfill:before {
content: "\e677";
}
.icon-shibai:before {
content: "\e60b";
}
.icon-7_round_solid:before {
content: "\e64d";
}
.icon-6_round_solid:before {
content: "\e64e";
}
.icon-9_round_solid:before {
content: "\e64f";
}
.icon-1_round_solid:before {
content: "\e650";
}
.icon-5_round_solid:before {
content: "\e651";
}
.icon-2_round_solid:before {
content: "\e654";
}
.icon-0_round_solid:before {
content: "\e655";
}
.icon-3_round_solid:before {
content: "\e656";
}
.icon-4_round_solid:before {
content: "\e657";
}
.icon-8_round_solid:before {
content: "\e658";
}
.icon-radio-off-full:before {
content: "\ea6b";
}
.icon-tongzhi:before {
content: "\e600";
}
.icon-ditu:before {
content: "\e8bc";
}
.icon-ico:before {
content: "\e646";
}
.icon-chazhaobiaodanliebiao:before {
content: "\e76a";
}
.icon-biaodan:before {
content: "\e61d";
}
.icon-siweidaotu:before {
content: "\e614";
}
.icon-jiliandongxuanzeqi:before {
content: "\e616";
}
.icon-caijian:before {
content: "\e611";
}
.icon-fuwenben:before {
content: "\e7e4";
}
.icon-fuwenbenkuang:before {
content: "\e66f";
}
.icon-shangchuan:before {
content: "\e663";
}
.icon-xuanzeqi:before {
content: "\e635";
}
.icon-fangkuang:before {
content: "\e642";
}
.icon-gouxuan-weixuanzhong-xianxingfangkuang:before {
content: "\e77b";
}
.icon-shidu:before {
content: "\e60a";
}
.icon-yangan:before {
content: "\e67d";
}
.icon-wendu:before {
content: "\e686";
}
.icon-zaosheng:before {
content: "\e61c";
}
.icon-jinridaiban:before {
content: "\e60f";
}
.icon-AIshiyanshi:before {
content: "\e609";
}
.icon-shenqingkaiban:before {
content: "\e639";
}
.icon-zhongyingwenqiehuan:before {
content: "\e611";
}
.icon-zhongyingwen:before {
content: "\e605";
}
.icon-zhongyingzhuanhuan:before {
content: "\e6a2";
}
.icon-zhongyingwenyuyan:before {
content: "\e609";
}
.icon-shuju:before {
content: "\e613";
}
.icon-ico_shuju:before {
content: "\e6ff";
}
.icon-shuju1:before {
content: "\e60e";
}
.icon-fuzhiyemian:before {
content: "\e772";
}
.icon-caozuo-wailian:before {
content: "\e711";
}
.icon-icon-:before {
content: "\e620";
}
.icon-gerenzhongxin:before {
content: "\e60d";
}
.icon-caidan:before {
content: "\e652";
}
.icon-xitongshezhi:before {
content: "\e69b";
}
.icon-neiqianshujuchucun:before {
content: "\e62f";
}
.icon-shouye:before {
content: "\e653";
}
.icon-quanxian:before {
content: "\e610";
}
.icon-zujian:before {
content: "\e85e";
}
.icon-crew_feature:before {
content: "\e602";
}
.icon-gongju:before {
content: "\e62d";
}
.icon-skin:before {
content: "\e636";
}
.icon-shixinyuan:before {
content: "\e669";
}
.icon-webicon318:before {
content: "\e6a9";
}
.icon-dian:before {
content: "\e608";
}
.icon-fullscreen:before {
content: "\e623";
}
.icon-tuichuquanping:before {
content: "\e641";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

View File

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

@@ -3,7 +3,7 @@
popper-class="popperClass" popper-class="popperClass"
class="tableSelector" class="tableSelector"
multiple multiple
@remove-tag="removeTag" :collapseTags="props.tableConfig.collapseTags"
v-model="data" v-model="data"
placeholder="请选择" placeholder="请选择"
@visible-change="visibleChange" @visible-change="visibleChange"
@@ -18,20 +18,22 @@
<el-table <el-table
ref="tableRef" ref="tableRef"
:data="tableData" :data="tableData"
size="mini" :size="props.tableConfig.size"
border border
row-key="id" row-key="id"
:lazy="props.tableConfig.lazy" :lazy="props.tableConfig.lazy"
:load="props.tableConfig.load" :load="props.tableConfig.load"
:tree-props="props.tableConfig.treeProps" :tree-props="props.tableConfig.treeProps"
style="width: 400px" style="width: 600px"
max-height="200" max-height="200"
height="200" height="200"
:highlight-current-row="!props.tableConfig.isMultiple" :highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@select="handleSelectionChange"
@selectAll="handleSelectionChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55" /> <el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
<el-table-column fixed type="index" label="#" width="50" /> <el-table-column fixed type="index" label="#" width="50" />
<el-table-column <el-table-column
:prop="item.prop" :prop="item.prop"
@@ -56,26 +58,36 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, reactive, ref, watch } from 'vue'; import { computed, defineProps, onMounted, reactive, ref, watch } from 'vue';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import { request } from '/@/utils/service'; import { request } from '/@/utils/service';
const props = defineProps({ const props = defineProps({
modelValue: {}, modelValue: {
type: Array || String || Number,
default: () => [],
},
tableConfig: { tableConfig: {
type: Object,
default: {
url: null, url: null,
label: null, //显示值 label: null, //显示值
value: null, //数据值 value: null, //数据值
isTree: false, isTree: false,
lazy: true, lazy: true,
size: 'default',
load: () => {}, load: () => {},
data: [], //默认数据 data: [], //默认数据
isMultiple: false, //是否多选 isMultiple: false, //是否多选
collapseTags: false,
treeProps: { children: 'children', hasChildren: 'hasChildren' }, treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项 columns: [], //每一项对应的列表项
}, },
},
displayLabel: {}, displayLabel: {},
} as any); } as any);
console.log(props.tableConfig);
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
// tableRef // tableRef
const tableRef = ref(); const tableRef = ref();
@@ -86,7 +98,7 @@ const multipleSelection = ref();
// 搜索值 // 搜索值
const search = ref(undefined); const search = ref(undefined);
//表格数据 //表格数据
const tableData = ref(); const tableData = ref([]);
// 分页的配置 // 分页的配置
const pageConfig = reactive({ const pageConfig = reactive({
page: 1, page: 1,
@@ -99,7 +111,6 @@ const pageConfig = reactive({
* @param val:Array * @param val:Array
*/ */
const handleSelectionChange = (val: any) => { const handleSelectionChange = (val: any) => {
multipleSelection.value = val;
const { tableConfig } = props; const { tableConfig } = props;
const result = val.map((item: any) => { const result = val.map((item: any) => {
return item[tableConfig.value]; return item[tableConfig.value];
@@ -117,7 +128,7 @@ const handleSelectionChange = (val: any) => {
const handleCurrentChange = (val: any) => { const handleCurrentChange = (val: any) => {
const { tableConfig } = props; const { tableConfig } = props;
if (!tableConfig.isMultiple && val) { if (!tableConfig.isMultiple && val) {
data.value = [val[tableConfig.label]]; // data.value = [val[tableConfig.label]];
emit('update:modelValue', val[tableConfig.value]); emit('update:modelValue', val[tableConfig.value]);
} }
}; };
@@ -127,6 +138,8 @@ const handleCurrentChange = (val: any) => {
*/ */
const getDict = async () => { const getDict = async () => {
const url = props.tableConfig.url; const url = props.tableConfig.url;
console.log(url);
const params = { const params = {
page: pageConfig.page, page: pageConfig.page,
limit: pageConfig.limit, limit: pageConfig.limit,
@@ -150,6 +163,28 @@ const getDict = async () => {
} }
}; };
// 获取节点值
const getNodeValues = () => {
console.log(props.tableConfig.url);
request({
url: props.tableConfig.url,
method: 'post',
data: { ids: props.modelValue },
}).then((res) => {
if (res.data.length > 0) {
data.value = res.data.map((item: any) => {
return item[props.tableConfig.label];
});
tableRef.value!.clearSelection();
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(row, true, false);
});
}
});
};
/** /**
* 下拉框展开/关闭 * 下拉框展开/关闭
* @param bool * @param bool
@@ -169,20 +204,11 @@ const handlePageChange = (page: any) => {
getDict(); getDict();
}; };
// 监听displayLabel的变化更新数据 onMounted(() => {
watch( // setTimeout(() => {
() => { // getNodeValues();
return props.displayLabel; // }, 1000);
}, });
(value) => {
const { tableConfig } = props;
const result = value
? value.map((item: any) => { return item[tableConfig.label];})
: null;
data.value = result;
},
{ immediate: true }
);
</script> </script>
<style scoped> <style scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,7 +37,7 @@
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i> <i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false"> <el-popover placement="bottom" trigger="hover" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference> <template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0"> <el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')"> <el-icon :title="$t('message.user.title4')">
@@ -57,26 +57,7 @@
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'" :class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
></i> ></i>
</div> </div>
<div> <div></div>
<span v-if="!isSocketOpen">
<el-popconfirm
width="250"
ref="onlinePopoverRef"
:confirm-button-text="$t('message.user.retry')"
:icon="InfoFilled"
trigger="hover"
icon-color="#626AEF"
:title="$t('message.user.onlinePrompt')"
@confirm="onlineConfirmEvent"
>
<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" />
</el-badge>
</template>
</el-popconfirm>
</span>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick"> <el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link"> <span class="layout-navbars-breadcrumb-user-link">
<span v-if="isSocketOpen"> <span v-if="isSocketOpen">
@@ -93,7 +74,7 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> <el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item> <el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item> <el-dropdown-item command="/versionUpgradeLog">更新日志</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item> <el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -115,8 +96,7 @@ import other from '/@/utils/other';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage'; import { Session, Local } from '/@/utils/storage';
import headerImage from '/@/assets/img/headerImage.png'; import headerImage from '/@/assets/img/headerImage.png';
import websocket from '/@/utils/websocket'; import { InfoFilled } from '@element-plus/icons-vue';
import { InfoFilled } from '@element-plus/icons-vue'
// 引入组件 // 引入组件
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue')); const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue')); const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
@@ -148,17 +128,6 @@ const layoutUserFlexNum = computed(() => {
// 定义变量内容 // 定义变量内容
const { isSocketOpen } = storeToRefs(useUserInfo()); const { isSocketOpen } = storeToRefs(useUserInfo());
// websocket状态
const onlinePopoverRef = ref()
const onlineConfirmEvent = () => {
if (!isSocketOpen.value) {
websocket.is_reonnect = true
websocket.reconnect_current = 1
websocket.reconnect()
}
// 手动隐藏弹出
unref(onlinePopoverRef).popperRef?.delayHide?.()
}
// 全屏点击时 // 全屏点击时
const onScreenfullClick = () => { const onScreenfullClick = () => {
if (!screenfull.isEnabled) { if (!screenfull.isEnabled) {
@@ -237,8 +206,10 @@ const onLanguageChange = (lang: string) => {
initI18nOrSize('globalI18n', 'disabledI18n'); initI18nOrSize('globalI18n', 'disabledI18n');
}; };
// 初始化组件大小/i18n // 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => { const initI18nOrSize = (value: string, attr: keyof typeof state) => {
state[attr] = Local.get('themeConfig')[value]; const themeConfig = Local.get('themeConfig') as { [key: string]: any } | null;
const configValue = ((themeConfig && themeConfig[value]) as string) || '';
state[attr] = configValue as unknown as never;
}; };
// 页面加载时 // 页面加载时
onMounted(() => { onMounted(() => {
@@ -246,11 +217,32 @@ onMounted(() => {
initI18nOrSize('globalComponentSize', 'disabledSize'); initI18nOrSize('globalComponentSize', 'disabledSize');
initI18nOrSize('globalI18n', 'disabledI18n'); initI18nOrSize('globalI18n', 'disabledI18n');
} }
getMessageCenterCount();
}); });
//消息中心的未读数量 //消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter'; import { messageCenterStore } from '/@/stores/messageCenter';
import { getBaseURL } from '/@/utils/baseUrl';
const messageCenter = messageCenterStore(); const messageCenter = messageCenterStore();
let eventSource: EventSource | null = null; // 存储 EventSource 实例
const token = Session.get('token');
const getMessageCenterCount = () => {
// 创建 EventSource 实例并连接到后端 SSE 端点
eventSource = new EventSource(`${getBaseURL()}/sse/?token=${token}`); // 替换为你的后端地址
// 监听消息事件
eventSource.onmessage = function (event) {
messageCenter.setUnread(+event.data); // 更新总记录数
};
// 错误处理
eventSource.onerror = function (err) {
console.error('SSE 错误:', err);
if (eventSource !== null && eventSource.readyState === EventSource.CLOSED) {
console.log('连接已关闭');
}
};
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -2,6 +2,7 @@
<div class="layout-navbars-breadcrumb-user-news"> <div class="layout-navbars-breadcrumb-user-news">
<div class="head-box"> <div class="head-box">
<div class="head-box-title">{{ $t('message.user.newTitle') }}</div> <div class="head-box-title">{{ $t('message.user.newTitle') }}</div>
<!-- <div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div> --> <!-- <div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div> -->
</div> </div>
<div class="content-box"> <div class="content-box">
@@ -33,27 +34,28 @@ const onAllReadClick = () => {
state.newsList = []; state.newsList = [];
}; };
// 前往通知中心点击 // 前往通知中心点击
import {useRouter } from "vue-router"; import { useRouter } from 'vue-router';
const route = useRouter() const route = useRouter();
const onGoToGiteeClick = () => { const onGoToGiteeClick = () => {
route.push('/messageCenter') route.push('/messageCenter');
}; };
//获取最新消息 //获取最新消息
import { request } from "/@/utils/service"; import { request } from '/@/utils/service';
const getLastMsg = () => { const getLastMsg = () => {
request({ request({
url: '/api/system/message_center/get_newest_msg/', url: '/api/system/message_center/get_newest_msg/',
method: 'get', method: 'get',
params: {} params: {},
}).then((res: any) => { }).then((res: any) => {
const { data } = res const { data } = res;
state.newsList= [data] console.log(data);
})
}
onMounted(()=>{
getLastMsg()
})
state.newsList = [data];
});
};
onMounted(() => {
getLastMsg();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

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

View File

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

View File

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

View File

@@ -17,19 +17,31 @@
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { verifyUrl } from '/@/utils/toolsValidate'; import { verifyUrl } from '/@/utils/toolsValidate';
import {cookie} from "xe-utils";
// 定义变量内容 // 定义变量内容
const route = useRoute(); const route = useRoute();
const state = reactive<LinkViewState>({ const state = reactive<LinkViewState>({
title: '', title: '',
isLink: '', isLink: '',
query: null
}); });
// 立即前往 // 立即前往
const onGotoFullPage = () => { const onGotoFullPage = () => {
const { origin, pathname } = window.location; 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); 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( watch(
@@ -37,6 +49,7 @@ watch(
() => { () => {
state.title = <string>route.meta.title; state.title = <string>route.meta.title;
state.isLink = <string>route.meta.isLink; state.isLink = <string>route.meta.isLink;
state.query = <any>route.query;
}, },
{ {
immediate: true, immediate: true,

View File

@@ -25,6 +25,8 @@ import fontAwesome470 from 'e-icon-picker/icon/fontawesome/font-awesome.v4.7.0.j
import eIconList from 'e-icon-picker/icon/default-icon/eIconList.js'; import eIconList from 'e-icon-picker/icon/default-icon/eIconList.js';
import iconfont from '/@/assets/iconfont/iconfont.json'; //引入json文件 import iconfont from '/@/assets/iconfont/iconfont.json'; //引入json文件
import '/@/assets/iconfont/iconfont.css'; //引入css import '/@/assets/iconfont/iconfont.css'; //引入css
import '/@/assets/iconfont/iconfont-01/iconfont.css'; //引入css
import '/@/assets/iconfont/iconfont-02/iconfont.css'; //引入css
// 自动注册插件 // 自动注册插件
import { scanAndInstallPlugins } from '/@/views/plugins/index'; import { scanAndInstallPlugins } from '/@/views/plugins/index';
import VXETable from 'vxe-table' import VXETable from 'vxe-table'

View File

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

View File

@@ -28,6 +28,8 @@ import {checkVersion} from "/@/utils/upgrade";
const storesThemeConfig = useThemeConfig(pinia); const storesThemeConfig = useThemeConfig(pinia);
const {themeConfig} = storeToRefs(storesThemeConfig); const {themeConfig} = storeToRefs(storesThemeConfig);
const {isRequestRoutes} = themeConfig.value; const {isRequestRoutes} = themeConfig.value;
import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo());
/** /**
* 创建一个可以被 Vue 应用程序使用的路由实例 * 创建一个可以被 Vue 应用程序使用的路由实例
@@ -96,10 +98,22 @@ export function formatTwoStageRoutes(arr: any) {
const frameOutRoutes = staticRoutes.map(item => item.path) const frameOutRoutes = staticRoutes.map(item => item.path)
const checkToken = ()=>{
const urlParams = new URLSearchParams(window.location.search);
const _oauth2_token = urlParams.get('_oauth2_token');
if (_oauth2_token) {
Session.set('token', _oauth2_token);
const cleanUrl = window.location.href.split('?')[0];
window.history.replaceState({}, '', cleanUrl);
useUserInfo(pinia).setUserInfos();
}
}
// 路由加载前 // 路由加载前
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新 // 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
await checkVersion() await checkVersion()
checkToken()
NProgress.configure({showSpinner: false}); NProgress.configure({showSpinner: false});
if (to.meta.title) NProgress.start(); if (to.meta.title) NProgress.start();
const token = Session.get('token'); const token = Session.get('token');
@@ -111,7 +125,10 @@ router.beforeEach(async (to, from, next) => {
next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`); next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
Session.clear(); Session.clear();
NProgress.done(); 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'); next('/home');
NProgress.done(); NProgress.done();
}else if(token && frameOutRoutes.includes(to.path) ){ }else if(token && frameOutRoutes.includes(to.path) ){

View File

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

View File

@@ -6,12 +6,15 @@ import {useFrontendMenuStore} from "/@/stores/frontendMenu";
// 用户信息 // 用户信息
export interface UserInfosState { export interface UserInfosState {
id: '',
avatar: string; avatar: string;
is_superuser: boolean,
username: string; username: string;
name: string; name: string;
email: string; email: string;
mobile: string; mobile: string;
gender: string; gender: string;
pwd_change_count: null | number;
dept_info: { dept_info: {
dept_id: number; dept_id: number;
dept_name: string; dept_name: string;

View File

@@ -2,6 +2,9 @@ import { defineStore } from 'pinia';
import { UserInfosStates } from './interface'; import { UserInfosStates } from './interface';
import { Session } from '/@/utils/storage'; import { Session } from '/@/utils/storage';
import { request } from '../utils/service'; import { request } from '../utils/service';
import { getBaseURL } from '../utils/baseUrl';
import headerImage from '/@/assets/img/headerImage.png';
/** /**
* 用户信息 * 用户信息
* @methods setUserInfos 设置用户信息 * @methods setUserInfos 设置用户信息
@@ -9,12 +12,15 @@ import { request } from '../utils/service';
export const useUserInfo = defineStore('userInfo', { export const useUserInfo = defineStore('userInfo', {
state: (): UserInfosStates => ({ state: (): UserInfosStates => ({
userInfos: { userInfos: {
id:'',
avatar: '', avatar: '',
username: '', username: '',
name: '', name: '',
email: '', email: '',
mobile: '', mobile: '',
gender: '', gender: '',
pwd_change_count:null,
is_superuser: false,
dept_info: { dept_info: {
dept_id: 0, dept_id: 0,
dept_name: '', dept_name: '',
@@ -29,16 +35,21 @@ export const useUserInfo = defineStore('userInfo', {
isSocketOpen: false isSocketOpen: false
}), }),
actions: { actions: {
async updateUserInfos() { async setPwdChangeCount(count: number) {
let userInfos: any = await this.getApiUserInfo(); this.userInfos.pwd_change_count = count;
this.userInfos.username = userInfos.data.name; },
this.userInfos.avatar = userInfos.data.avatar; async updateUserInfos(userInfos:any) {
this.userInfos.name = userInfos.data.name; this.userInfos.id = userInfos.id;
this.userInfos.email = userInfos.data.email; this.userInfos.username = userInfos.name;
this.userInfos.mobile = userInfos.data.mobile; this.userInfos.avatar = userInfos.avatar;
this.userInfos.gender = userInfos.data.gender; this.userInfos.name = userInfos.name;
this.userInfos.dept_info = userInfos.data.dept_info; this.userInfos.email = userInfos.email;
this.userInfos.role_info = userInfos.data.role_info; 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;
this.userInfos.is_superuser = userInfos.is_superuser;
Session.set('userInfo', this.userInfos); Session.set('userInfo', this.userInfos);
}, },
async setUserInfos() { async setUserInfos() {
@@ -47,6 +58,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos = Session.get('userInfo'); this.userInfos = Session.get('userInfo');
} else { } else {
let userInfos: any = await this.getApiUserInfo(); let userInfos: any = await this.getApiUserInfo();
this.userInfos.id = userInfos.id;
this.userInfos.username = userInfos.data.name; this.userInfos.username = userInfos.data.name;
this.userInfos.avatar = userInfos.data.avatar; this.userInfos.avatar = userInfos.data.avatar;
this.userInfos.name = userInfos.data.name; this.userInfos.name = userInfos.data.name;
@@ -55,17 +67,29 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.gender = userInfos.data.gender; this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info; this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info; this.userInfos.role_info = userInfos.data.role_info;
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
this.userInfos.is_superuser = userInfos.data.is_superuser;
Session.set('userInfo', this.userInfos); Session.set('userInfo', this.userInfos);
} }
}, },
async setWebSocketState(socketState: boolean) {
this.isSocketOpen = socketState;
},
async getApiUserInfo() { async getApiUserInfo() {
return request({ return request({
url: '/api/system/user/user_info/', url: '/api/system/user/user_info/',
method: 'get', method: 'get',
}); }).then((res:any)=>{
this.userInfos.id = res.data.id;
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;
this.userInfos.is_superuser = res.data.is_superuser;
Session.set('userInfo', this.userInfos);
})
}, },
}, },
}); });

View File

@@ -7,7 +7,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
.icon-selector-warp-title { .icon-selector-warp-title {
position: absolute; position: relative;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
left: 15px; left: 15px;

View File

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

View File

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

View File

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

View File

@@ -215,6 +215,7 @@ export const downloadFile = function ({ url, params, method, filename = '文件
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'} // headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then((res: any) => { }).then((res: any) => {
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载 // 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('导入任务已创建,请前往‘下载中心’等待下载'); if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1]) const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
const fileName = xlsxName || `${filename}.xlsx` const fileName = xlsxName || `${filename}.xlsx`

View File

@@ -1,7 +1,7 @@
// 字体图标 url // 字体图标 url
const cssCdnUrlList: Array<string> = [ const cssCdnUrlList: Array<string> = [
'//at.alicdn.com/t/font_2298093_y6u00apwst.css', // '//at.alicdn.com/t/font_2298093_y6u00apwst.css',
'//at.alicdn.com/t/c/font_3882322_9ah7y8m9175.css', //dvadmin3项目用icon // '//at.alicdn.com/t/c/font_3882322_9ah7y8m9175.css', //dvadmin3项目用icon
//'//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css' //'//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'
]; ];
// 第三方 js url // 第三方 js url

View File

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

View File

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

@@ -0,0 +1,122 @@
<template>
<div>
<fs-crud ref="crudRef" v-bind="crudBinding">
</fs-crud>
</div>
</template>
<script setup lang="ts">
import {computed, defineComponent, onMounted, watch} from "vue";
import {CreateCrudOptionsProps, CreateCrudOptionsRet, useFs, AddReq,
compute,
DelReq,
dict,
EditReq,
UserPageQuery,
UserPageRes} from "@fast-crud/fast-crud";
const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
return {
crudOptions: {
mode: {
name: "local",
isMergeWhenUpdate: true,
isAppendWhenAdd: true
},
actionbar: { buttons: { add: { show: true }, addRow: { show: false } } },
editable: {
enabled: true,
mode: "row",
activeDefault:true
},
form:{
wrapper:{
width:"500px"
},
col:{
span:24
},
afterSubmit({mode}){
emit('update:modelValue', crudBinding.value.data);
}
},
toolbar:{
show:false
},
search: {
disabled: true,
show: false
},
pagination: {
show: false
},
columns: {
title: {
title: "标题",
form:{
component:{
placeholder:"请输入标题"
},
rules:[{
required: true,
message: '必须填写',
}]
}
},
key: {
title: "键名",
form:{
component:{
placeholder:"请输入键名"
},
rules:[{
required: true,
message: '必须填写',
}]
}
},
value: {
title: "键值",
form:{
component:{
placeholder:"请输入键值"
},
rules:[{
required: true,
message: '必须填写',
}]
}
}
}
}
};
}
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue'])
//通过导出modelValue, 可以导出成为一个input组件
watch(
() => {
return props.modelValue;
},
(value = []) => {
crudBinding.value.data = value;
},
{
immediate: true
}
);
// 页面打开后获取列表数据
// onMounted(() => {
// crudExpose.doRefresh();
// // crudExpose.setTableData([])
// // crudExpose.editable.enable();
// });
</script>

View File

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

View File

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

View File

@@ -42,6 +42,15 @@
<template #actionbar-right> <template #actionbar-right>
<importExcel api="api/system/user/" v-auth="'user:Import'">导入 </importExcel> <importExcel api="api/system/user/" v-auth="'user:Import'">导入 </importExcel>
</template> </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> </fs-crud>
<el-dialog v-model="resetPwdVisible" title="重设密码" width="400px" draggable :before-close="handleResetPwdClose"> <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 { getDeptInfoById, resetPwd } from './api';
import { warningNotification, successNotification } from '/@/utils/message'; import { warningNotification, successNotification } from '/@/utils/message';
import { HeadDeptInfoType } from '../../types'; import { HeadDeptInfoType } from '../../types';
import {getBaseURL} from '/@/utils/baseUrl';
let deptCountChart: ECharts; let deptCountChart: ECharts;
let deptSexChart: ECharts; let deptSexChart: ECharts;

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

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

View File

@@ -1,7 +1,19 @@
import * as api from './api'; 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) => { const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query); return await api.GetList(query);
}; };
@@ -20,7 +32,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
show: false, show: true,
click: () => context.openAddHandle?.()
}, },
}, },
}, },
@@ -30,6 +43,17 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
editRequest, editRequest,
delRequest, delRequest,
}, },
tabs: {
show: true,
name: 'file_type',
type: '',
options: [
{ value: 0, label: '图片' },
{ value: 1, label: '视频' },
{ value: 2, label: '音频' },
{ value: 3, label: '其他' },
]
},
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
@@ -96,14 +120,25 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
}, },
type: 'input', type: 'input',
column: { column: {
minWidth: 120, minWidth: 200,
}, },
form: { form: {
component: { component: {
placeholder: '请输入文件名称', placeholder: '请输入文件名称',
clearable: true
}, },
}, },
}, },
preview: {
title: '预览',
column: {
minWidth: 120,
align: 'center'
},
form: {
show: false
}
},
url: { url: {
title: '文件地址', title: '文件地址',
type: 'file-uploader', type: 'file-uploader',
@@ -111,7 +146,12 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
disabled: true, disabled: true,
}, },
column: { column: {
minWidth: 200, minWidth: 360,
component: {
async buildUrl(value: any) {
return getBaseURL(value);
}
}
}, },
}, },
md5sum: { md5sum: {
@@ -120,12 +160,100 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
disabled: true, disabled: true,
}, },
column: { column: {
minWidth: 120, minWidth: 300,
}, },
form: { 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> <template>
<fs-page> <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> </fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted, nextTick } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud'; import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './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 // crud组件的ref
const crudRef = ref(); const crudRef = ref();
// crud 配置的ref // crud 配置的ref
@@ -15,12 +87,81 @@ const crudBinding = ref();
// 暴露的方法 // 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding }); const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置 // 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose }); const { crudOptions } = createCrudOptions({ crudExpose, context: { openAddHandle } });
// 初始化crud配置 // 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions }); 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(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });
</script> </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,9 +13,25 @@ export function login(params: object) {
data: params data: params
}); });
} }
export function loginChangePwd(data: object) {
return request({
url: '/api/system/user/login_change_password/',
method: 'post',
data: data
});
}
export function getUserInfo() { export function getUserInfo() {
return request({ return request({
url: '/api/system/user/user_info/', url: '/api/system/user/user_info/',
method: 'get', method: 'get',
}); });
} }
export function getBackends() {
return request({
url: '/api/dvadmin3_social_oauth2/backend/get_login_backend/',
method: 'get',
});
}

View File

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

@@ -0,0 +1,139 @@
<template>
<div class="other-fast-way" v-if="backends.length">
<div class="fast-title"><span>其他快速方式登录</span></div>
<ul class="fast-list">
<li v-for="(v, k) in backends" :key="v">
<a @click.once="handleOAuth2LoginClick(v)" style="width: 50px;color: #18bc9c">
<img :src="v.icon" :alt="v.app_name" />
<p>{{ v.app_name }}</p>
</a>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, reactive, toRefs } from 'vue';
import * as loginApi from '../api';
import { OAuth2Backend } from '/@/views/system/login/types';
export default defineComponent({
name: 'loginOAuth2',
setup() {
const handleOAuth2LoginClick = (backend: OAuth2Backend) => {
history.replaceState(null, '', location.pathname + location.search);
window.location.href = backend.authentication_url + '?next=' + window.location.href;
};
const state = reactive({
handleOAuth2LoginClick: handleOAuth2LoginClick,
backends: [],
});
const getBackends = async () => {
loginApi.getBackends().then((ret: any) => {
state.backends = ret.data;
});
};
// const handleTreeClick = (record: MenuTreeItemType) => {
// menuButtonRef.value?.handleRefreshTable(record);
// menuFieldRef.value?.handleRefreshTable(record)
// };
onMounted(() => {
getBackends();
});
return {
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
@for $i from 1 through 4 {
.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-code {
width: 100%;
padding: 0;
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 300;
margin-top: 15px;
}
.login-msg {
color: var(--el-text-color-placeholder);
}
}
.other-fast-way {
//height: 240px;
position: relative;
z-index: 1;
//display: flex;
//align-items: center;
//justify-content: center;
.fast-title {
display: flex;
align-items: center;
justify-content: center;
span {
color: #999;
font-size: 14px;
padding: 0 20px;
}
&:before,
&:after {
content: '';
flex: 1;
height: 1px;
background: #ddd;
}
}
}
.fast-list {
display: flex;
justify-content: center;
margin-top: 10px;
li {
margin-left: 20px;
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
animation-delay: 0.1s;
a {
display: block;
text-align: center;
cursor: pointer;
img {
width: 35px;
margin: 0 auto;
max-width: 100%;
margin-bottom: 6px;
}
p {
font-size: 14px;
color: #333;
}
}
&:first-child {
margin-left: 0;
}
}
}
</style>

View File

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

View File

@@ -0,0 +1,8 @@
export interface OAuth2Backend {
app_name: string;
backend_name: string;
icon: string;
authentication_url: string;
}

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