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:
43
README.zh.md
43
README.zh.md
@@ -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
|
|||||||
|
|
||||||

|

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

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

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

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

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

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -98,5 +98,4 @@ media/
|
|||||||
__pypackages__/
|
__pypackages__/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
gunicorn.pid
|
gunicorn.pid
|
||||||
plugins/*
|
|
||||||
!plugins/__init__.py
|
!plugins/__init__.py
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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值
|
||||||
|
|||||||
@@ -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 是该路由的消费者
|
|
||||||
]
|
|
||||||
@@ -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 * # 升级中心
|
||||||
|
|||||||
33
backend/application/sse_views.py
Normal file
33
backend/application/sse_views.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 # 确保路径正确
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) # 设置永不超时的键值对
|
||||||
|
|||||||
@@ -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()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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="获取成功")
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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="添加成功")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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):
|
||||||
"""恢复默认密码"""
|
"""恢复默认密码"""
|
||||||
|
|||||||
62
backend/dvadmin/utils/aliyunoss.py
Normal file
62
backend/dvadmin/utils/aliyunoss.py
Normal 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
|
||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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="导入任务已创建,请前往‘下载中心’等待下载")
|
||||||
|
|||||||
@@ -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 = '核心模型'
|
||||||
|
|||||||
56
backend/dvadmin/utils/tencentcos.py
Normal file
56
backend/dvadmin/utils/tencentcos.py
Normal 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
|
||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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
87
crud-gen.sh
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
if ! [ -f ".env" ];then
|
||||||
|
echo ".env file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$3" ]; then
|
||||||
|
echo "Use: $0 <app_name> <view_name> <table_name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
DIR=./web/src/views/$1/$2
|
||||||
|
|
||||||
|
|
||||||
|
# 设置数据库连接信息
|
||||||
|
HOST="177.10.0.13"
|
||||||
|
USER="root"
|
||||||
|
PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g')
|
||||||
|
DATABASE="django-vue3-admin"
|
||||||
|
TABLE=$3
|
||||||
|
TARGET_FILE="./web/src/views/$1/$2/crud.tsx"
|
||||||
|
|
||||||
|
|
||||||
|
# 表是否存在
|
||||||
|
TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l)
|
||||||
|
|
||||||
|
if [ "$TABLE_EXISTS" -eq 0 ]; then
|
||||||
|
echo "Table $TABLE does not exist in database $DATABASE."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $DIR
|
||||||
|
cp -r ./web/src/views/template/* $DIR
|
||||||
|
sed -i "s/VIEWSETNAME/$2/g" $DIR/*
|
||||||
|
|
||||||
|
sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE
|
||||||
|
|
||||||
|
# 查询表结构
|
||||||
|
QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;"
|
||||||
|
|
||||||
|
# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置
|
||||||
|
mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do
|
||||||
|
# 映射 MySQL 数据类型到 fast-crud 类型
|
||||||
|
case "$DATA_TYPE" in
|
||||||
|
"int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double")
|
||||||
|
TYPE="number"
|
||||||
|
;;
|
||||||
|
"date"|"datetime"|"timestamp")
|
||||||
|
TYPE="date"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
TYPE="text"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " $COLUMN_NAME: {
|
||||||
|
title: '$COLUMN_NAME',
|
||||||
|
type: '$TYPE',
|
||||||
|
search: { show: true },
|
||||||
|
column: {
|
||||||
|
minWidth: 120,
|
||||||
|
sortable: 'custom',
|
||||||
|
},
|
||||||
|
form: {" >> $TARGET_FILE
|
||||||
|
|
||||||
|
if [ "$IS_NULLABLE" = "NO" ]; then
|
||||||
|
echo " helper: {
|
||||||
|
render() {
|
||||||
|
return <div style={"color:blue"}>$COLUMN_NAME 是必填的</div>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: [{
|
||||||
|
required: true, message: '$COLUMN_NAME 是必填的'
|
||||||
|
}]," >> $TARGET_FILE
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " component: {
|
||||||
|
placeholder: '请输入 $COLUMN_NAME',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}," >> $TARGET_FILE
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}" >> $TARGET_FILE
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine
|
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine
|
||||||
WORKDIR /web/
|
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
52
init.sh
@@ -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
|
||||||
|
|||||||
2
web/.env
2
web/.env
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
77
web/flowH5.config.ts
Normal 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': {}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
55
web/src/assets/iconfont/iconfont-01/iconfont.css
Normal file
55
web/src/assets/iconfont/iconfont-01/iconfont.css
Normal 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";
|
||||||
|
}
|
||||||
|
|
||||||
BIN
web/src/assets/iconfont/iconfont-01/iconfont.ttf
Normal file
BIN
web/src/assets/iconfont/iconfont-01/iconfont.ttf
Normal file
Binary file not shown.
BIN
web/src/assets/iconfont/iconfont-01/iconfont.woff
Normal file
BIN
web/src/assets/iconfont/iconfont-01/iconfont.woff
Normal file
Binary file not shown.
BIN
web/src/assets/iconfont/iconfont-01/iconfont.woff2
Normal file
BIN
web/src/assets/iconfont/iconfont-01/iconfont.woff2
Normal file
Binary file not shown.
427
web/src/assets/iconfont/iconfont-02/iconfont.css
Normal file
427
web/src/assets/iconfont/iconfont-02/iconfont.css
Normal 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";
|
||||||
|
}
|
||||||
|
|
||||||
BIN
web/src/assets/iconfont/iconfont-02/iconfont.ttf
Normal file
BIN
web/src/assets/iconfont/iconfont-02/iconfont.ttf
Normal file
Binary file not shown.
BIN
web/src/assets/iconfont/iconfont-02/iconfont.woff
Normal file
BIN
web/src/assets/iconfont/iconfont-02/iconfont.woff
Normal file
Binary file not shown.
BIN
web/src/assets/iconfont/iconfont-02/iconfont.woff2
Normal file
BIN
web/src/assets/iconfont/iconfont-02/iconfont.woff2
Normal file
Binary file not shown.
BIN
web/src/assets/login-bg.png
Normal file
BIN
web/src/assets/login-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
@@ -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 {
|
||||||
|
|||||||
403
web/src/components/calendar/index.vue
Normal file
403
web/src/components/calendar/index.vue
Normal 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 || ' ' }}</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>
|
||||||
|
|
||||||
84
web/src/components/fileSelector/fileItem.vue
Normal file
84
web/src/components/fileSelector/fileItem.vue
Normal 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>
|
||||||
521
web/src/components/fileSelector/index.vue
Normal file
521
web/src/components/fileSelector/index.vue
Normal 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">
|
||||||
|
一共选中 {{ data?.length || 0 }} 个文件
|
||||||
|
</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>
|
||||||
7
web/src/components/fileSelector/types.ts
Normal file
7
web/src/components/fileSelector/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const SHOW = {
|
||||||
|
IMAGE: 0b1000, // 图片
|
||||||
|
VIDEO: 0b0100, // 视频
|
||||||
|
AUDIO: 0b0010, // 音频
|
||||||
|
OTHER: 0b0001, // 其他
|
||||||
|
ALL: 0b1111, // 全部
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
one1: '用戶名登入',
|
one1: '用戶名登入',
|
||||||
two2: '手機號登入',
|
two2: '手機號登入',
|
||||||
|
changePwd: '密码修改',
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
one3: '協力廠商登入',
|
one3: '協力廠商登入',
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
|
next(`/login?redirect=${to.path}¶ms=${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) ){
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
1
web/src/types/layout.d.ts
vendored
1
web/src/types/layout.d.ts
vendored
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
57
web/src/utils/cores.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import mitt, { Emitter } from 'mitt';
|
||||||
|
|
||||||
|
export interface TaskProps {
|
||||||
|
name: string;
|
||||||
|
custom?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义自定义事件类型
|
||||||
|
export type BusEvents = {
|
||||||
|
onNewTask: TaskProps | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: number;
|
||||||
|
handle: string;
|
||||||
|
data: any;
|
||||||
|
createTime: Date;
|
||||||
|
custom?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Core {
|
||||||
|
bus: Emitter<BusEvents>;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
showNotification(body: string, title?: string): Notification | undefined;
|
||||||
|
taskList: Map<String, Task>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus = mitt<BusEvents>();
|
||||||
|
export function getSystemNotification(body: string, title?: string) {
|
||||||
|
if (!title) {
|
||||||
|
title = '通知';
|
||||||
|
}
|
||||||
|
return new Notification(title ?? '通知', {
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function showSystemNotification(body: string, title?: string): Notification | undefined {
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
return getSystemNotification(body, title);
|
||||||
|
} else if (Notification.permission !== 'denied') {
|
||||||
|
Notification.requestPermission().then((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
return getSystemNotification(body, title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const taskList = new Map<String, Task>();
|
||||||
|
|
||||||
|
export function useCore(): Core {
|
||||||
|
return {
|
||||||
|
bus,
|
||||||
|
showNotification: showSystemNotification,
|
||||||
|
taskList,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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){
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
122
web/src/views/system/config/components/components/crudTable.vue
Normal file
122
web/src/views/system/config/components/components/crudTable.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||||
|
</fs-crud>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, defineComponent, onMounted, watch} from "vue";
|
||||||
|
import {CreateCrudOptionsProps, CreateCrudOptionsRet, useFs, AddReq,
|
||||||
|
compute,
|
||||||
|
DelReq,
|
||||||
|
dict,
|
||||||
|
EditReq,
|
||||||
|
UserPageQuery,
|
||||||
|
UserPageRes} from "@fast-crud/fast-crud";
|
||||||
|
const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
mode: {
|
||||||
|
name: "local",
|
||||||
|
isMergeWhenUpdate: true,
|
||||||
|
isAppendWhenAdd: true
|
||||||
|
},
|
||||||
|
actionbar: { buttons: { add: { show: true }, addRow: { show: false } } },
|
||||||
|
editable: {
|
||||||
|
enabled: true,
|
||||||
|
mode: "row",
|
||||||
|
activeDefault:true
|
||||||
|
},
|
||||||
|
form:{
|
||||||
|
wrapper:{
|
||||||
|
width:"500px"
|
||||||
|
},
|
||||||
|
col:{
|
||||||
|
span:24
|
||||||
|
},
|
||||||
|
afterSubmit({mode}){
|
||||||
|
emit('update:modelValue', crudBinding.value.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toolbar:{
|
||||||
|
show:false
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
disabled: true,
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
title: {
|
||||||
|
title: "标题",
|
||||||
|
form:{
|
||||||
|
component:{
|
||||||
|
placeholder:"请输入标题"
|
||||||
|
},
|
||||||
|
rules:[{
|
||||||
|
required: true,
|
||||||
|
message: '必须填写',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
title: "键名",
|
||||||
|
form:{
|
||||||
|
component:{
|
||||||
|
placeholder:"请输入键名"
|
||||||
|
},
|
||||||
|
rules:[{
|
||||||
|
required: true,
|
||||||
|
message: '必须填写',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
title: "键值",
|
||||||
|
form:{
|
||||||
|
component:{
|
||||||
|
placeholder:"请输入键值"
|
||||||
|
},
|
||||||
|
rules:[{
|
||||||
|
required: true,
|
||||||
|
message: '必须填写',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
|
||||||
|
//通过导出modelValue, 可以导出成为一个input组件
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
(value = []) => {
|
||||||
|
crudBinding.value.data = value;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 页面打开后获取列表数据
|
||||||
|
// onMounted(() => {
|
||||||
|
// crudExpose.doRefresh();
|
||||||
|
// // crudExpose.setTableData([])
|
||||||
|
// // crudExpose.editable.enable();
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
@@ -175,48 +175,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 数组 -->
|
<!-- 数组 -->
|
||||||
<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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -303,6 +303,16 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
is_value: {
|
||||||
|
title: '是否值',
|
||||||
|
column: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
show: false,
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
276
web/src/views/system/login/component/changePwd.vue
Normal file
276
web/src/views/system/login/component/changePwd.vue
Normal 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>
|
||||||
139
web/src/views/system/login/component/oauth2.vue
Normal file
139
web/src/views/system/login/component/oauth2.vue
Normal 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>
|
||||||
@@ -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;
|
||||||
|
|||||||
8
web/src/views/system/login/types.ts
Normal file
8
web/src/views/system/login/types.ts
Normal 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
Reference in New Issue
Block a user