53 Commits

Author SHA1 Message Date
liqiang
3a2739774a refactor(file): 调整文件列表视图的认证类配置 2025-12-07 00:23:15 +08:00
liqiang
012f3a2f63 refactor(file): 调整文件列表视图的认证类配置 2025-12-06 23:03:50 +08:00
ahui
64c5d505e7 Merge branch 'develop' of https://gitee.com/huge-dream/django-vue3-admin into develop 2025-12-01 10:41:43 +08:00
ahui
3914432d9f 修复权限过滤器DataLevelPermissionsSubFilter单独使用时没有经过数据权限过滤的问题 2025-12-01 10:28:51 +08:00
ahui
cc6ac68223 角色授权按钮更新 2025-12-01 10:24:45 +08:00
1638245306
05ee833fe5 chore(layout): 移除用户头像在线状态徽章
- 注释掉了用户头像的在线状态徽章组件
- 移除了相关的头像图片显示逻辑
-保留了用户名显示功能
2025-11-01 22:28:48 +08:00
1638245306
ff736aae93 feat(layout): 添加搜索功能图标并调整布局
- 在用户面包屑导航中添加搜索图标
- 调整图标点击事件处理- 移除重复的搜索图标代码
- 优化布局结构和样式
2025-11-01 22:15:55 +08:00
liyimin
c0a68f91ca 首页优化 2025-11-01 16:14:36 +08:00
liyimin
8d6abeb891 登陆界面美化 2025-10-31 23:26:27 +08:00
1638245306
163e5eb2db fix(role): 更新角色授权用户权限检查
- 将授权用户按钮的权限检查从 role:AuthorizedAdd 更改为 role:SetUserRole
-保持授权用户搜索权限检查不变- 确保只有具有适当权限的用户才能访问角色分配功能
2025-10-23 22:16:03 +08:00
1638245306
e786f60cdd feat(websocket): 实现 WebSocket 消息推送功能
- 配置 ASGI 支持 WebSocket 连接
- 新增 WebSocket 路由和消费者类 MegCenter
- 实现消息序列化和推送逻辑
- 前端集成 WebSocket 连接状态管理和重连机制
- 添加用户在线状态提示和未读消息提醒- 更新角色权限配置显示条件
- 扩展用户信息存储结构支持 WebSocket 状态字段
2025-10-19 16:03:59 +08:00
1638245306
abe2db3c82 feat(websocket): 实现 WebSocket 消息推送功能
- 配置 ASGI 支持 WebSocket 连接
- 新增 WebSocket 路由和消费者类 MegCenter
- 实现消息序列化和推送逻辑
- 前端集成 WebSocket 连接状态管理和重连机制
- 添加用户在线状态提示和未读消息提醒- 更新角色权限配置显示条件
- 扩展用户信息存储结构支持 WebSocket 状态字段
2025-10-19 16:03:16 +08:00
ahui
fa734dd479 管理部门数据级过滤器优化 2025-08-22 18:03:51 +08:00
ahui
6e9b94aed2 用户的管理部门权限功能 2025-08-14 10:35:51 +08:00
ahui
2a9f5be895 页面优化 2025-08-13 10:29:52 +08:00
ahui
2ea34bfbd5 工具函数更新 2025-08-07 14:10:07 +08:00
ahui
edbd6862a2 用户表增加当前角色字段 2025-08-07 14:09:48 +08:00
liqiang
6c99a78821 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	web/src/views/system/login/index.vue
2025-08-07 13:54:07 +08:00
dvadmin
3db048e650 !128 【轻量PR】: 修复前端请求配置与自定义配置合并逻辑
Merge pull request !128 from lorenzo/develop_v2
2025-08-07 05:45:06 +00:00
ahui
e470a716e5 忽略文件更新 2025-08-07 11:30:21 +08:00
lorenzo
95bf503529 fix(web): 修复前端请求配置与自定义配置合并逻辑 2025-08-01 21:37:25 +08:00
猿小天
4e9155f09b feat(viewset): 通过 IDS列表获取数据
- 在通用的 ViewSet 中添加 get_by_ids 方法
- 接收 POST 请求,从请求体中获取 ids列表
- 根据 ids 列表查询数据并返回- 若 ids 列表为空或只包含空字符串,则返回空数据
2025-07-21 13:45:09 +08:00
ahui
3e60c5b5f9 工具函数同步 2025-07-17 11:00:28 +08:00
dvadmin
b72fb5238f !125 fix(application): 修复普通用户接收消息数量统计错误
Merge pull request !125 from 木子-李/sse
2025-07-15 14:34:58 +00:00
1638245306
74d89c3968 refactor(web): 优化代码和构建配置
- 修改用户服务条款中的企业用户描述
- 在 package.json 中添加新的构建命令 build:flowH5
- 引入新的依赖库 @meetjs/vant4-kit 和 vue-qr
- 添加 rollup 插件以优化构建过程
2025-07-10 15:18:16 +08:00
1638245306
5eab2b6e4f refactor(system): 更新登录页面版权信息
- 将版权年份从 2024 修改为 2023
- 更新版权公司名称为北京巨梦科技有限公司
- 修改备案号为晋ICP备18005113号-3
2025-07-10 15:04:39 +08:00
1638245306
d2f14e41ad refactor(system): 更新登录页面版权信息
- 将版权年份从 2024 修改为 2023
- 更新版权公司名称为北京巨梦科技有限公司
- 修改备案号为晋ICP备18005113号-3
2025-07-10 15:04:21 +08:00
阿辉
483863e238 同步权限 2025-07-06 16:54:26 +08:00
李小涛
ad95bea301 fix(application): 修复普通用户接收消息数量统计错误
- 移除了对 user_id == 1 的特殊判断,统一了消息数量统计逻辑
- 优化了代码结构,提高了代码的可读性和维护性
2025-06-26 17:51:36 +08:00
李小涛
344f754fc7 actor(webref): 优化代码结构和功能
- 注释掉 oauth2.vue 中的 getBackends() 调用
- 修改 sse_views.py 中的消息中心未读消息计数逻辑
- 优化 userNews.vue 中的新闻列表数据加载
2025-06-26 17:50:53 +08:00
dvadmin
749f5d80d2 !124 refactor(theme): 优化主题样式并移除冗余代码
Merge pull request !124 from 木子-李/sse
2025-06-25 08:03:25 +00:00
李小涛
ac3bfb6b80 refactor(theme): 优化主题样式并移除冗余代码
- 将 @import 替换为 @use,提高代码的可维护性
- 统一使用 index 作为命名空间,避免变量名冲突
- 移除不必要的注释和空格,精简代码
- 删除未使用的 isSocketOpen 属性,简化数据结构
2025-06-25 10:42:01 +08:00
liqiang
ef48bdd0df refactor: 更新登录页面版权信息和备案号
- 将默认版权信息修改为 "2021-2025 django-vue-admin.com"
- 将默认备案号修改为 "晋ICP备18005113号-3"
2025-06-25 06:55:15 +08:00
liqiang
a2e07a89e1 build(web): 更新图标字体并调整相关配置
- 新增 iconfont-01 和 iconfont-02 两个图标字体文件夹- 在 setIconfont.ts 中注释掉原有的图标字体 URL
- 在 main.ts 中引入新的图标字体 CSS 文件
- 更新 package.json
2025-06-24 09:59:29 +08:00
dvadmin
7b55a3db64 !116 兼容OAuth2
Merge pull request !116 from 木子-李/N/A
2025-06-22 13:18:46 +00:00
dvadmin
3c8b4b6ac0 !122 refactor(system): 重构消息中心功能
Merge pull request !122 from 木子-李/sse
2025-06-22 12:25:31 +00:00
李小涛
e8212501e2 refactor(system): 重构消息中心功能
- 移除 WebSocket相关代码
- 新增 SSE (Server-Sent Events) 实现消息推送
- 优化消息中心未读数量展示和更新逻辑- 调整消息中心相关 API 和前端展示
2025-06-22 13:09:49 +08:00
1638245306
fa063a8611 Merge remote-tracking branch 'origin/develop' into develop 2025-06-19 22:59:47 +08:00
1638245306
b89f1671c3 fix(system): 修复新增菜单未选择父菜单时的提交问题- 在提交菜单表单时,如果未选择父菜单,将 parent 字段设置为 null
-确保在更新或添加菜单时,父菜单字段的值是正确的
2025-06-19 22:59:14 +08:00
阿辉
c6c54d8013 用户信息更新 2025-06-19 13:41:57 +08:00
1638245306
0005d45d85 style(icon-selector): 调整图标选择器标题的位置属性
- 将 .icon-selector-warp-title 类的 position 属性从 absolute改为 relative
- 此修改解决了标题在某些情况下的定位问题,确保布局的稳定性
2025-06-18 19:03:24 +08:00
1638245306
1052f6a07b feat(user): 添加用户数据导出功能
- 新增导出数据功能,位于用户管理页面的导出按钮
- 点击导出按钮后,弹出确认框,提示用户是否确定导出数据
- 确定导出后,调用 exportData 函数执行导出操作
2025-06-18 18:59:44 +08:00
1638245306
5dcbae292a refactor(system): 修复角色权限字典类型错误 2025-06-18 18:47:51 +08:00
1638245306
ed915aa2cb feat(core): 新增软删除和工作流状态筛选功能
- 添加 CoreModelManager 类,实现软删除和工作流状态的筛选
- 在 CoreModel 中集成新功能- 增加 objects 和 all_objects 两个 Manager,支持不同查询需求
2025-06-17 11:35:46 +08:00
liqiang
82a0ef612a Merge remote-tracking branch 'origin/develop' into develop 2025-06-15 17:55:24 +08:00
liqiang
8ea49866bc feat(system): 添加文件存储引擎功能
- 新增文件存储引擎配置选项,支持本地、阿里云oss和腾讯云cos
- 在系统配置中添加文件存储相关设置- 实现阿里云oss和腾讯云cos的文件上传功能
- 更新文件列表视图,支持不同存储引擎的文件上传和访问
2025-06-12 06:10:47 +08:00
liqiang
a0a7c25b18 refactor(del_migrations):增加 .venv 目录排除
- 在需要排除的目录列表中添加了 .venv,以避免删除虚拟环境目录中的迁移文件
2025-06-11 15:58:25 +08:00
1638245306
f8c8f2963b Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 15:09:04 +08:00
1638245306
5a980f3b54 feat(user): 添加用户 ID 属性并进行相关处理
- 在 UserInfosState 接口中添加 id属性
- 在 userInfo store 中添加用户 ID 相关逻辑
- 更新 getUserInfos 和 updateUserInfos 方法以处理用户 ID
- 注释掉水平菜单滚动定位代码
2025-06-09 15:08:51 +08:00
猿小天
c0be63afcd Merge remote-tracking branch 'origin/develop' into develop 2025-06-01 19:55:05 +08:00
猿小天
150b92163f 功能变化:
初始化生成utf-8编码
2025-06-01 19:51:25 +08:00
liqiang
3a25cdb53c refactor(system): 移除 Role 模型中的 FlowBaseModel继承
- 从 Role 类中删除了对 FlowBaseModel 的继承
- 这个改动简化了 Role 模型的结构,可能会影响与流程相关的功能
2025-05-08 05:22:32 +08:00
木子-李
45dcda0cc0 兼容OAuth2
Signed-off-by: 木子-李 <1537080775@qq.com>
2025-05-06 05:49:02 +00:00
88 changed files with 2801 additions and 1222 deletions

4
.gitignore vendored
View File

@@ -4,4 +4,6 @@
.history/ .history/
.vscode/ .vscode/
web/package-lock.json web/package-lock.json
*.bat

View File

@@ -8,25 +8,25 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
""" """
import os import os
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
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')
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
http_application = get_asgi_application() http_application = get_asgi_application()
from application.ws_routing import websocket_urlpatterns
from application.routing import websocket_urlpatterns
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
"http": http_application, "http": http_application,
'websocket': AllowedHostsOriginValidator( 'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack( AuthMiddlewareStack(
URLRouter( URLRouter(
websocket_urlpatterns # 指明路由文件是devops/routing.py websocket_urlpatterns # 指明路由文件是devops/routing.py
)
) )
) ),
),
}) })

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ Including another URLconf
""" """
from django.conf.urls.static import static from django.conf.urls.static import static
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.views.static import serve
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from rest_framework import permissions from rest_framework import permissions
@@ -24,6 +25,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 +42,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 +53,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.IsAuthenticated,), permission_classes=permission_classes,
generator_class=CustomOpenAPISchemaGenerator, generator_class=CustomOpenAPISchemaGenerator,
) )
# 前端页面映射 # 前端页面映射
@@ -115,6 +118,8 @@ urlpatterns = (
# 前端页面映射 # 前端页面映射
path('web/', web_view, name='web_view'), path('web/', web_view, name='web_view'),
path('web/<path:filename>', serve_web_files, name='serve_web_files'), path('web/<path:filename>', serve_web_files, name='serve_web_files'),
# sse
path('sse/', sse_view, name='sse'),
] ]
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL) + static(settings.STATIC_URL, document_root=settings.STATIC_URL)

View File

@@ -180,4 +180,4 @@ def create_message_push(title: str, content: str, target_type: int = 0, target_u
"type": "push.message", "type": "push.message",
"json": {**message, 'unread': unread_count} "json": {**message, 'unread': unread_count}
} }
) )

View File

@@ -4,4 +4,4 @@ from application.websocketConfig import MegCenter
websocket_urlpatterns = [ websocket_urlpatterns = [
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者 path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
] ]

View File

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

View File

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

View File

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

View File

@@ -11,326 +11,11 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": null,
"children": [ "children": [
{
"name": "菜单管理",
"icon": "iconfont icon-caidan",
"sort": 1,
"is_link": false,
"is_catalog": false,
"web_path": "/menu",
"component": "system/menu/index",
"component_name": "menu",
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "menu:Search",
"api": "/api/system/menu/",
"method": 0
},
{
"name": "详情",
"value": "menu:Retrieve",
"api": "/api/system/menu/{id}/",
"method": 0
},
{
"name": "查询所有",
"value": "menu:SearchAll",
"api": "/api/system/menu/get_all_menu/",
"method": 0
},
{
"name": "路由",
"value": "menu:router",
"api": "/api/system/menu/web_router/",
"method": 0
},
{
"name": "查询按钮权限",
"value": "btn:Search",
"api": "/api/system/menu_button/",
"method": 0
},
{
"name": "查询列权限",
"value": "column:Search",
"api": "/api/system/column/",
"method": 0
},
{
"name": "新增",
"value": "menu:Create",
"api": "/api/system/menu/",
"method": 1
},
{
"name": "上移",
"value": "menu:MoveUp",
"api": "/api/system/menu/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "menu:MoveDown",
"api": "/api/system/menu/mode_down/",
"method": 1
},
{
"name": "新增按钮权限",
"value": "btn:Create",
"api": "/api/system/menu_button/",
"method": 1
},
{
"name": "新增列权限",
"value": "column:Create",
"api": "/api/system/column/",
"method": 1
},
{
"name": "自动匹配列权限",
"value": "column:Match",
"api": "/api/system/column/auto_match_fields/",
"method": 1
},
{
"name": "编辑",
"value": "menu:Update",
"api": "/api/system/menu/{id}/",
"method": 2
},
{
"name": "修改按钮权限",
"value": "btn:Update",
"api": "/api/system/menu_button/{id}/",
"method": 2
},
{
"name": "编辑列权限",
"value": "column:Update",
"api": "/api/system/column/{id}/",
"method": 2
},
{
"name": "删除",
"value": "menu:Delete",
"api": "/api/system/menu/{id}/",
"method": 3
},
{
"name": "删除按钮权限",
"value": "btn:Delete",
"api": "/api/system/menu_button/{id}/",
"method": 3
},
{
"name": "删除列权限",
"value": "column:Delete",
"api": "/api/system/column/{id}/",
"method": 3
}
],
"menu_field": []
},
{
"name": "部门管理",
"icon": "ele-OfficeBuilding",
"sort": 3,
"is_link": false,
"is_catalog": false,
"web_path": "/dept",
"component": "system/dept/index",
"component_name": "dept",
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "dept:Search",
"api": "/api/system/dept/",
"method": 0
},
{
"name": "详情",
"value": "dept:Retrieve",
"api": "/api/system/dept/{id}/",
"method": 0
},
{
"name": "获取所有部门",
"value": "dept:SearchAll",
"api": "/api/system/dept/all_dept/",
"method": 0
},
{
"name": "部门顶部信息",
"value": "dept:HeaderInfo",
"api": "/api/system/dept/dept_info/",
"method": 0
},
{
"name": "新增",
"value": "dept:Create",
"api": "/api/system/dept/",
"method": 1
},
{
"name": "上移",
"value": "dept:MoveUp",
"api": "/api/system/dept/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "dept:MoveDown",
"api": "/api/system/dept/mode_down/",
"method": 1
},
{
"name": "编辑",
"value": "dept:Update",
"api": "/api/system/dept/{id}/",
"method": 2
},
{
"name": "删除",
"value": "dept:Delete",
"api": "/api/system/dept/{id}/",
"method": 3
}
],
"menu_field": []
},
{
"name": "角色管理",
"icon": "ele-ColdDrink",
"sort": 4,
"is_link": false,
"is_catalog": false,
"web_path": "/role",
"component": "system/role/index",
"component_name": "role",
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "role:Search",
"api": "/api/system/role/",
"method": 0
},
{
"name": "详情",
"value": "role:Retrieve",
"api": "/api/system/role/{id}/",
"method": 0
},
{
"name": "权限配置",
"value": "role:Permission",
"api": "/api/system/role/{id}/",
"method": 0
},
{
"name": "新增",
"value": "role:Create",
"api": "/api/system/role/",
"method": 1
},
{
"name": "编辑",
"value": "role:Update",
"api": "/api/system/role/{id}/",
"method": 2
},
{
"name": "保存",
"value": "role:Save",
"api": "/api/system/role/{id}/",
"method": 2
},
{
"name": "删除",
"value": "role:Delete",
"api": "/api/system/role/{id}/",
"method": 3
}
],
"menu_field": [
{
"field_name": "create_datetime",
"title": "创建时间",
"model": "Role"
},
{
"field_name": "creator",
"title": "创建人",
"model": "Role"
},
{
"field_name": "dept_belong_id",
"title": "数据归属部门",
"model": "Role"
},
{
"field_name": "description",
"title": "描述",
"model": "Role"
},
{
"field_name": "id",
"title": "Id",
"model": "Role"
},
{
"field_name": "key",
"title": "权限字符",
"model": "Role"
},
{
"field_name": "modifier",
"title": "修改人",
"model": "Role"
},
{
"field_name": "name",
"title": "角色名称",
"model": "Role"
},
{
"field_name": "sort",
"title": "角色顺序",
"model": "Role"
},
{
"field_name": "status",
"title": "角色状态",
"model": "Role"
},
{
"field_name": "update_datetime",
"title": "修改时间",
"model": "Role"
}
]
},
{ {
"name": "用户管理", "name": "用户管理",
"icon": "iconfont icon-icon-", "icon": "iconfont icon-icon-",
"sort": 6, "sort": 1,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
"web_path": "/user", "web_path": "/user",
@@ -339,7 +24,6 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 1,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
@@ -348,18 +32,24 @@
"api": "/api/system/user/", "api": "/api/system/user/",
"method": 0 "method": 0
}, },
{
"name": "详情",
"value": "user:Retrieve",
"api": "/api/system/user/{id}/",
"method": 0
},
{ {
"name": "新增", "name": "新增",
"value": "user:Create", "value": "user:Create",
"api": "/api/system/user/", "api": "/api/system/user/",
"method": 1 "method": 1
}, },
{
"name": "编辑",
"value": "user:Update",
"api": "/api/system/user/{id}/",
"method": 2
},
{
"name": "删除",
"value": "user:Delete",
"api": "/api/system/user/{id}/",
"method": 3
},
{ {
"name": "导出", "name": "导出",
"value": "user:Export", "value": "user:Export",
@@ -373,10 +63,16 @@
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "获取导入模板",
"value": "user:Update", "value": "user:ImportTemplate",
"api": "/api/system/user/{id}/", "api": "/api/system/user/import/",
"method": 2 "method": 0
},
{
"name": "批量更新模板",
"value": "user:BatchUpdateTemplate",
"api": "/api/system/user/update_template/",
"method": 0
}, },
{ {
"name": "重设密码", "name": "重设密码",
@@ -386,15 +82,9 @@
}, },
{ {
"name": "重置密码", "name": "重置密码",
"value": "user:DefaultPassword", "value": "user:ResetDefaultPassword",
"api": "/api/system/user/{id}/reset_to_default_password/", "api": "/api/system/user/{id}/reset_to_default_password/",
"method": 2 "method": 2
},
{
"name": "删除",
"value": "user:Delete",
"api": "/api/system/user/{id}/",
"method": 3
} }
], ],
"menu_field": [ "menu_field": [
@@ -475,6 +165,359 @@
} }
] ]
}, },
{
"name": "菜单管理",
"icon": "iconfont icon-caidan",
"sort": 2,
"is_link": false,
"is_catalog": false,
"web_path": "/menu",
"component": "system/menu/index",
"component_name": "menu",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "menu:Search",
"api": "/api/system/menu/",
"method": 0
},
{
"name": "单例",
"value": "menu:Retrieve",
"api": "/api/system/menu/{id}/",
"method": 0
},
{
"name": "新增",
"value": "menu:Create",
"api": "/api/system/menu/",
"method": 1
},
{
"name": "编辑",
"value": "menu:Update",
"api": "/api/system/menu/{id}/",
"method": 2
},
{
"name": "删除",
"value": "menu:Delete",
"api": "/api/system/menu/{id}/",
"method": 3
},
{
"name": "查询所有",
"value": "menu:SearchAll",
"api": "/api/system/menu/get_all_menu/",
"method": 0
},
{
"name": "路由",
"value": "menu:router",
"api": "/api/system/menu/web_router/",
"method": 0
},
{
"name": "查询按钮",
"value": "menu:SearchButton",
"api": "/api/system/menu_button/",
"method": 0
},
{
"name": "新增按钮",
"value": "menu:CreateButton",
"api": "/api/system/menu_button/",
"method": 1
},
{
"name": "编辑按钮",
"value": "menu:UpdateButton",
"api": "/api/system/menu_button/{id}/",
"method": 2
},
{
"name": "删除按钮",
"value": "menu:DeleteButton",
"api": "/api/system/menu_button/{id}/",
"method": 3
},
{
"name": "上移",
"value": "menu:MoveUp",
"api": "/api/system/menu/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "menu:MoveDown",
"api": "/api/system/menu/mode_down/",
"method": 1
},
{
"name": "查询列权限",
"value": "column:Search",
"api": "/api/system/column/",
"method": 0
},
{
"name": "新增列权限",
"value": "column:Create",
"api": "/api/system/column/",
"method": 1
},
{
"name": "编辑列权限",
"value": "column:Update",
"api": "/api/system/column/{id}/",
"method": 2
},
{
"name": "删除列权限",
"value": "column:Delete",
"api": "/api/system/column/{id}/",
"method": 3
},
{
"name": "自动匹配列权限",
"value": "column:Match",
"api": "/api/system/column/auto_match_fields/",
"method": 1
}
],
"menu_field": []
},
{
"name": "部门管理",
"icon": "ele-OfficeBuilding",
"sort": 3,
"is_link": false,
"is_catalog": false,
"web_path": "/dept",
"component": "system/dept/index",
"component_name": "dept",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "dept:Search",
"api": "/api/system/dept/",
"method": 0
},
{
"name": "详情",
"value": "dept:Retrieve",
"api": "/api/system/dept/{id}/",
"method": 0
},
{
"name": "获取所有部门",
"value": "dept:SearchAll",
"api": "/api/system/dept/all_dept/",
"method": 0
},
{
"name": "部门顶部信息",
"value": "dept:HeaderInfo",
"api": "/api/system/dept/dept_info/",
"method": 0
},
{
"name": "新增",
"value": "dept:Create",
"api": "/api/system/dept/",
"method": 1
},
{
"name": "上移",
"value": "dept:MoveUp",
"api": "/api/system/dept/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "dept:MoveDown",
"api": "/api/system/dept/mode_down/",
"method": 1
},
{
"name": "编辑",
"value": "dept:Update",
"api": "/api/system/dept/{id}/",
"method": 2
},
{
"name": "删除",
"value": "dept:Delete",
"api": "/api/system/dept/{id}/",
"method": 3
}
],
"menu_field": []
},
{
"name": "角色管理",
"icon": "ele-ColdDrink",
"sort": 4,
"is_link": false,
"is_catalog": false,
"web_path": "/role",
"component": "system/role/index",
"component_name": "role",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "role:Search",
"api": "/api/system/role/",
"method": 0
},
{
"name": "单例",
"value": "role:Retrieve",
"api": "/api/system/role/{id}/",
"method": 0
},
{
"name": "新增",
"value": "role:Create",
"api": "/api/system/role/",
"method": 1
},
{
"name": "编辑",
"value": "role:Update",
"api": "/api/system/role/{id}/",
"method": 2
},
{
"name": "删除",
"value": "role:Delete",
"api": "/api/system/role/{id}/",
"method": 3
},
{
"name": "获取所有可授权数据范围的部门",
"value": "role:AllDataRangeDept",
"api": "/api/system/role_menu_button_permision/role_to_dept_all/",
"method": 0
},
{
"name": "获取所有可授权菜单",
"value": "role:AllCanMenu",
"api": "/api/system/role_menu_button_permision/get_role_menu/",
"method": 0
},
{
"name": "获取所有已授权用户",
"value": "role:AllAuthorizedUser",
"api": "/api/system/role/get_role_users/",
"method": 0
},
{
"name": "获取菜单所有可授权按钮",
"value": "role:AllMenuButton",
"api": "/api/system/role_menu_button_permision/get_role_menu_btn_field/",
"method": 0
},
{
"name": "授权菜单",
"value": "role:SetMenu",
"api": "/api/system/role_menu_button_permision/set_role_menu/",
"method": 2
},
{
"name": "授权菜单按钮",
"value": "role:SetMenuButton",
"api": "/api/system/role_menu_button_permision/set_role_menu_btn/",
"method": 2
},
{
"name": "授权数据范围",
"value": "role:SetDataRange",
"api": "/api/system/role_menu_button_permision/set_role_menu_btn_data_range/",
"method": 2
},
{
"name": "获取所有用户",
"value": "role:AllUser",
"api": "/api/system/user/",
"method": 0
},
{
"name": "授权用户予角色",
"value": "role:SetUserRole",
"api": "/api/system/role/{id}/set_role_users/",
"method": 2
}
],
"menu_field": [
{
"field_name": "create_datetime",
"title": "创建时间",
"model": "Role"
},
{
"field_name": "creator",
"title": "创建人",
"model": "Role"
},
{
"field_name": "dept_belong_id",
"title": "数据归属部门",
"model": "Role"
},
{
"field_name": "description",
"title": "描述",
"model": "Role"
},
{
"field_name": "id",
"title": "Id",
"model": "Role"
},
{
"field_name": "key",
"title": "权限字符",
"model": "Role"
},
{
"field_name": "modifier",
"title": "修改人",
"model": "Role"
},
{
"field_name": "name",
"title": "角色名称",
"model": "Role"
},
{
"field_name": "sort",
"title": "角色顺序",
"model": "Role"
},
{
"field_name": "status",
"title": "角色状态",
"model": "Role"
},
{
"field_name": "update_datetime",
"title": "修改时间",
"model": "Role"
}
]
},
{ {
"name": "消息中心", "name": "消息中心",
"icon": "iconfont icon-xiaoxizhongxin", "icon": "iconfont icon-xiaoxizhongxin",
@@ -690,35 +733,11 @@
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "downloadCenter:Search",
"api": "/api/system/downloadCenter/", "api": "/api/system/download_center/"
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/downloadCenter/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/downloadCenter/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/downloadCenter/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/downloadCenter/{id}/",
"method": 3
} }
] ],
"menu_field": []
} }
], ],
"menu_button": [], "menu_button": [],

View File

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

View File

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

View File

@@ -9,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from application import dispatch from application import dispatch
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
from dvadmin3_flow.base_model import FlowBaseModel
class Role(CoreModel,FlowBaseModel): class Role(CoreModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称") name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符") key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序") sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
@@ -63,6 +63,8 @@ class Users(CoreModel, AbstractUser):
help_text="关联岗位") help_text="关联岗位")
role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False, role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
help_text="关联角色") help_text="关联角色")
current_role = models.ForeignKey(to=Role, null=True, blank=True, db_constraint=False, on_delete=models.SET_NULL,
verbose_name="当前登录角色", help_text="当前登录角色", related_name='current_role_set')
dept = models.ForeignKey( dept = models.ForeignKey(
to="Dept", to="Dept",
verbose_name="所属部门", verbose_name="所属部门",
@@ -72,12 +74,26 @@ class Users(CoreModel, AbstractUser):
blank=True, blank=True,
help_text="关联部门", help_text="关联部门",
) )
manage_dept = models.ManyToManyField(
to="Dept",
verbose_name="管理部门",
db_constraint=False,
blank=True,
help_text="管理部门",
related_name='manage_dept_set'
)
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="密码修改次数") 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):
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) if raw_password:
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"
@@ -162,6 +178,23 @@ class Dept(CoreModel):
cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list) cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list)
return list(set(dept_list)) return list(set(dept_list))
@classmethod
def recursion_all_parent_dept(cls, dept_id: int, dept_list=None):
"""
递归获取部门的所有上级部门
:param dept_id: 需要获取的id
:param dept_list: 递归list
:return:
"""
if dept_list is None:
dept_list = [dept_id]
current_dept = Dept.objects.filter(id=dept_id).values('parent').first()
if current_dept and current_dept.get('parent'):
parent_id = current_dept.get('parent')
dept_list.append(parent_id)
cls.recursion_all_parent_dept(parent_id, dept_list)
return list(set(dept_list))
class Meta: class Meta:
db_table = table_prefix + "system_dept" db_table = table_prefix + "system_dept"
verbose_name = "部门表" verbose_name = "部门表"

View File

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

View File

@@ -44,6 +44,11 @@ class DownloadCenterViewSet(CustomModelViewSet):
extra_filter_class = [] 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: if self.request.user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(creator=self.request.user) return super().get_queryset().filter(creator=self.request.user)

View File

@@ -6,6 +6,7 @@ from django.conf import settings
from django.db import connection 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 rest_framework.permissions import IsAuthenticated
from application import dispatch from application import dispatch
from dvadmin.system.models import FileList from dvadmin.system.models import FileList
@@ -35,8 +36,8 @@ class FileSerializer(CustomModelSerializer):
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)
@@ -52,15 +53,15 @@ class FileSerializer(CustomModelSerializer):
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:
@@ -106,7 +107,7 @@ class FileViewSet(CustomModelViewSet):
queryset = FileList.objects.all() queryset = FileList.objects.all()
serializer_class = FileSerializer serializer_class = FileSerializer
filter_class = FileFilter filter_class = FileFilter
permission_classes = [] permission_classes = [IsAuthenticated]
@action(methods=['GET'], detail=False) @action(methods=['GET'], detail=False)
def get_all(self, request): def get_all(self, request):

View File

@@ -36,7 +36,7 @@ class MessageCenterSerializer(CustomModelSerializer):
return serializer.data return serializer.data
def get_user_info(self, instance, parsed_query): def get_user_info(self, instance, parsed_query):
if instance.target_type in (1,2,3): if instance.target_type in (1, 2, 3):
return [] return []
users = instance.target_user.all() users = instance.target_user.all()
# You can do what ever you want in here # You can do what ever you want in here
@@ -108,7 +108,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
return serializer.data return serializer.data
def get_user_info(self, instance, parsed_query): def get_user_info(self, instance, parsed_query):
if instance.target_type in (1,2,3): if instance.target_type in (1, 2, 3):
return [] return []
users = instance.target_user.all() users = instance.target_user.all()
# You can do what ever you want in here # You can do what ever you want in here
@@ -138,7 +138,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
fields = "__all__" fields = "__all__"
read_only_fields = ["id"] read_only_fields = ["id"]
def websocket_push(user_id, message): def websocket_push(user_id, message):
""" """
主动推送消息 主动推送消息
@@ -153,7 +152,6 @@ def websocket_push(user_id, message):
} }
) )
class MessageCenterCreateSerializer(CustomModelSerializer): class MessageCenterCreateSerializer(CustomModelSerializer):
""" """
消息中心-新增-序列化器 消息中心-新增-序列化器

View File

@@ -90,6 +90,8 @@ class UserCreateSerializer(CustomModelSerializer):
data = super().save(**kwargs) data = super().save(**kwargs)
data.dept_belong_id = data.dept_id data.dept_belong_id = data.dept_id
data.save() data.save()
if not self.validated_data.get('manage_dept', None):
data.manage_dept.add(data.dept_id)
data.post.set(self.initial_data.get("post", [])) data.post.set(self.initial_data.get("post", []))
return data return data
@@ -127,6 +129,8 @@ class UserUpdateSerializer(CustomModelSerializer):
data = super().save(**kwargs) data = super().save(**kwargs)
data.dept_belong_id = data.dept_id data.dept_belong_id = data.dept_id
data.save() data.save()
if not self.validated_data.get('manage_dept', None):
data.manage_dept.add(data.dept_id)
data.post.set(self.initial_data.get("post", [])) data.post.set(self.initial_data.get("post", []))
return data return data
@@ -426,12 +430,9 @@ class UserViewSet(CustomModelViewSet):
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
else: else:
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
# print(queryset.values('id','name','dept__id'))
page = self.paginate_queryset(queryset) page = self.paginate_queryset(queryset)
if page is not None: if page is not None:
serializer = self.get_serializer(page, many=True, request=request) serializer = self.get_serializer(page, many=True, request=request)
# print(serializer.data)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request) serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功") return SuccessResponse(data=serializer.data, msg="获取成功")

View File

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

View File

@@ -15,15 +15,16 @@ import six
from django.db import models from django.db import models
from django.db.models import Q, F from django.db.models import Q, F
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django_filters import utils, FilterSet
from django_filters.constants import ALL_FIELDS from django_filters.constants import ALL_FIELDS
from django_filters.filters import CharFilter, DateTimeFromToRangeFilter
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field, translate_validation, deprecate
from rest_framework.request import Request
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, MenuButton
from dvadmin.utils.models import CoreModel from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton, Users
from util.currency import recursion_down_fast
class CoreModelFilterBankend(BaseFilterBackend): class CoreModelFilterBankend(BaseFilterBackend):
""" """
@@ -200,6 +201,86 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
return queryset.filter(dept_belong_id__in=list(set(dept_list))) return queryset.filter(dept_belong_id__in=list(set(dept_list)))
class DataLevelPermissionsSubFilter(DataLevelPermissionsFilter):
"""数据级权限过滤的子过滤器过滤管理部门字段manage_dept"""
def _extracted_from_filter_queryset_33(self, request:Request, queryset, api, method):
u:Users = request.user
if u.is_superuser:
return queryset
# (0, "仅本人数据权限"),
# (1, "本部门及以下数据权限"),
# (2, "本部门数据权限"),
# (3, "全部数据权限"),
# (4, "自定数据权限")
re_api = api
_pk = request.parser_context["kwargs"].get('pk')
if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True)
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
role_permission_list = []
if menu_button_ids:
role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
menu_button_id__in=menu_button_ids).values(
'data_range'
)
dataScope_list = [] # 权限范围列表
for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
if ele.get("data_range") == 3:
return queryset
dataScope_list.append(ele.get("data_range"))
dataScope_list = list(set(dataScope_list))
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
if 0 in dataScope_list:
return queryset.filter(
creator=request.user, dept_belong_id=u.dept_id
)
dept_list = []
# 5. 自定数据权限 获取部门,根据部门过滤
for ele in dataScope_list:
if ele == 1:
dept_list.append(u.dept_id)
dept_list.extend(
get_dept(
u.dept_id,
)
)
elif ele == 2:
dept_list.append(u.dept_id)
elif ele == 4:
dept_ids = RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
data_range=4).values_list(
'dept__id',flat=True
)
dept_list.extend(
dept_ids
)
# 自己部门 交 管理部门
if u.manage_dept.exists(): # 兼容旧数据
for dept in u.manage_dept.all():
dept_list.extend(recursion_down_fast(dept, 'parent', 'id'))
else:
dept_list = recursion_down_fast(u.dept, 'parent', 'id')
dept_list = set(recursion_down_fast(u.dept)) & set(dept_list)
# 自己创建的数据要能看到
# 应对归属a管b、c等情况如果自己创建数据则是a不显式指定自己的数据就查不到
if queryset.model._meta.model_name == 'dept':
return queryset.filter(Q(id__in=dept_list) | Q(creator=u))
return queryset.filter(Q(dept_belong_id__in=dept_list) | Q(creator=u))
class DataLevelPermissionMargeFilter(DataLevelPermissionsFilter):
def _extracted_from_filter_queryset_33(self, request, queryset, api, method):
queryset = super()._extracted_from_filter_queryset_33(request, queryset, api, method)
return DataLevelPermissionsSubFilter._extracted_from_filter_queryset_33(self, request, queryset, api, method)
class CustomDjangoFilterBackend(DjangoFilterBackend): class CustomDjangoFilterBackend(DjangoFilterBackend):
lookup_prefixes = { lookup_prefixes = {
"^": "istartswith", "^": "istartswith",
@@ -240,14 +321,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# TODO: remove assertion in 2.1 # TODO: remove assertion in 2.1
if filterset_class is None and hasattr(view, "filter_class"): if filterset_class is None and hasattr(view, "filter_class"):
utils.deprecate( deprecate(
"`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__ "`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__
) )
filterset_class = getattr(view, "filter_class", None) filterset_class = getattr(view, "filter_class", None)
# TODO: remove assertion in 2.1 # TODO: remove assertion in 2.1
if filterset_fields is None and hasattr(view, "filter_fields"): if filterset_fields is None and hasattr(view, "filter_fields"):
utils.deprecate( deprecate(
"`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__ "`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
) )
self.filter_fields = getattr(view, "filter_fields", None) self.filter_fields = getattr(view, "filter_fields", None)
@@ -427,5 +508,5 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
return queryset return queryset
if not filterset.is_valid() and self.raise_exception: if not filterset.is_valid() and self.raise_exception:
raise utils.translate_validation(filterset.errors) raise translate_validation(filterset.errors)
return filterset.qs return filterset.qs

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend from dvadmin.utils.filters import CoreModelFilterBankend, DataLevelPermissionMargeFilter
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
from dvadmin.utils.permission import CustomPermission from dvadmin.utils.permission import CustomPermission
@@ -41,7 +41,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
update_serializer_class = None update_serializer_class = None
filter_fields = '__all__' filter_fields = '__all__'
search_fields = () search_fields = ()
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter] extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionMargeFilter]
permission_classes = [CustomPermission] permission_classes = [CustomPermission]
import_field_dict = {} import_field_dict = {}
export_field_label = {} export_field_label = {}
@@ -152,3 +152,13 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
return SuccessResponse(data=[], msg="删除成功") return SuccessResponse(data=[], msg="删除成功")
else: else:
return ErrorResponse(msg="未获取到keys字段") return ErrorResponse(msg="未获取到keys字段")
@action(methods=['post'], detail=False)
def get_by_ids(self, request):
"""通过IDS列表获取数据"""
ids = request.data.get('ids', [])
if ids and ids != ['']:
queryset = self.get_queryset().filter(id__in=ids)
serializer = self.get_serializer(queryset, many=True)
return DetailResponse(data=serializer.data)
return DetailResponse(data=None)

View File

@@ -7,26 +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 dvadmin3-celery==3.1.6
oss2==2.19.1
cos-python-sdk-v5==1.9.37

View File

@@ -82,7 +82,7 @@
<br> <br>
<h2>巨梦科技企业客户服务说明</h2> <h2>巨梦科技企业客户服务说明</h2>
<h3> <h3>
1巨梦科技平台提供给多家企业客户使用企业客户通过巨梦科技平台进行发布用户活动等如果功夫企火星企业用户违反了隐私政策或发生其它侵犯用户权益的行为用户可要求巨梦科技提供必要的配合与巨梦科技企业用户进行沟通和维权维权过程中产生的费用由用户自行承担</h3> 1巨梦科技平台提供给多家企业客户使用企业客户通过巨梦科技平台进行发布用户活动等如果企业用户违反了隐私政策或发生其它侵犯用户权益的行为用户可要求巨梦科技提供必要的配合与巨梦科技企业用户进行沟通和维权维权过程中产生的费用由用户自行承担</h3>
<br> <br>
<h2>其他事宜</h2> <h2>其他事宜</h2>
<h3> <h3>

78
backend/util/currency.py Normal file
View File

@@ -0,0 +1,78 @@
from uuid import uuid4
from datetime import datetime
from django.db import connection
from django.db.models import Model
from django.core.cache import cache
def create_code(model,prefix):
current_date = datetime.now().strftime('%Y%m%d%H%M%S')
code = f"{prefix}{current_date}" + str(uuid4().int)[:6]
return code
def lock(key):
if callable(key): # @lock
def inner(*args, **kwargs):
with cache.lock(key='lock'):
return key(*args, **kwargs)
inner.__name__ = key.__name__
else: # @lock(key='aaa')
def inner(func):
def _inner(*args, **kwargs):
with cache.lock(key=key):
return func(*args, **kwargs)
_inner.__name__ = func.__name__
return _inner
return inner
def recursion_down_fast(instance:Model, parent='parent', key='id') -> list[int]:
"""向下递归instance的所有子级且返回一维列表使用sql优化速度非常快"""
if not instance:
return []
sql = f"""
WITH RECURSIVE children AS (
SELECT id, {key} AS param_{key} FROM {instance.__class__._meta.db_table} WHERE {parent}_id = %s UNION ALL
SELECT a.id, a.{key} AS param_{key} FROM {instance.__class__._meta.db_table} a
INNER JOIN children b ON a.{parent}_id = b.id
) SELECT param_{key} FROM children;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [getattr(instance, key)])
data = cursor.fetchall()
return [getattr(instance, key), *[i[0] for i in data]]
def recursion_up_fast(instance: Model, parent='parent', key='id') -> list[int]:
"""向上递归instance的所有父级使用sql优化速度非常快"""
if not instance:
return []
sql = f"""
WITH RECURSIVE parents AS (
SELECT id, {key} as param_{key}, {parent}_id FROM {instance.__class__._meta.db_table} WHERE id = %s UNION ALL
SELECT a.id, a.{key} as param_{key}, a.{parent}_id FROM {instance.__class__._meta.db_table} a
INNER JOIN parents b ON a.id = b.{parent}_id
) SELECT param_{key} FROM parents;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [getattr(instance, key)])
data = cursor.fetchall()
return [i[0] for i in data]
def recursion_up_joint(instance: Model, parent='parent', key='name', joint='/') -> str:
"""向上递归instance所有父级并拼接需要的值返回完整路径使用sql优化速度非常快"""
if instance is None:
return ''
sql = f"""
WITH RECURSIVE parents AS (
SELECT id, {parent}_id, {key}::TEXT AS path FROM {instance.__class__._meta.db_table} WHERE {key} = %s AND id = %s UNION ALL
SELECT a.id, a.{parent}_id, (a.{key} || '{joint}' || b.path)::TEXT FROM {instance.__class__._meta.db_table} a
INNER JOIN parents b ON a.id = b.{parent}_id
) SELECT path FROM parents where {parent}_id IS NULL;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [getattr(instance, key), instance.pk])
data = cursor.fetchall()
try:
return data[0][0]
except IndexError:
raise Exception('找不到初始数据')

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "django-vue3-admin", "name": "django-vue3-admin",
"version": "3.1.0", "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": {
@@ -8,7 +8,8 @@
"build:dev": "vite build --mode development", "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/",
"build:flowH5": "vite build --config flowH5.config.ts"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@@ -18,6 +19,7 @@
"@fast-crud/ui-interface": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3", "@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@meetjs/vant4-kit": "^1.0.1",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
@@ -37,6 +39,7 @@
"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", "lunar-javascript": "^1.7.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
@@ -47,17 +50,23 @@
"print-js": "^1.6.0", "print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2", "qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.0", "qs": "^6.11.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"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-qr": "^4.0.9",
"vue-router": "^4.4.3", "vue-router": "^4.4.3",
"vxe-table": "^4.6.18", "vxe-table": "^4.6.18",
"xe-utils": "^3.5.30" "xe-utils": "^3.5.30"

View File

@@ -20,6 +20,7 @@ import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont'; import setIntroduction from '/@/utils/setIconfont';
import websocket from '/@/utils/websocket';
// 引入组件 // 引入组件
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue')); const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
@@ -35,7 +36,6 @@ 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(); const core = useCore();
const router = useRouter(); const router = useRouter();
// 获取版本号 // 获取版本号
@@ -94,61 +94,61 @@ onUnmounted(() => {
}); });
// 监听路由的变化,设置网站标题 // 监听路由的变化,设置网站标题
watch( watch(
() => route.path, () => route.path,
() => { () => {
other.useTitle(); other.useTitle();
other.useFavicon(); other.useFavicon();
if (!websocket.websocket) { if (!websocket.websocket) {
//websockt 模块 //websockt 模块
try { try {
websocket.init(wsReceive) websocket.init(wsReceive)
} catch (e) { } catch (e) {
console.log('websocket错误'); console.log('websocket错误');
}
} }
},
{
deep: true,
} }
},
{
deep: true,
}
); );
// websocket相关代码 // websocket相关代码
import { messageCenterStore } from '/@/stores/messageCenter'; import { messageCenterStore } from '/@/stores/messageCenter';
const wsReceive = (message: any) => { const wsReceive = (message: any) => {
const data = JSON.parse(message.data); const data = JSON.parse(message.data);
const { unread } = data; const { unread } = data;
const messageCenter = messageCenterStore(); const messageCenter = messageCenterStore();
messageCenter.setUnread(unread); messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') { if (data.contentType === 'SYSTEM') {
ElNotification({ ElNotification({
title: '系统消息', title: '系统消息',
message: data.content, message: data.content,
type: 'success', type: 'success',
position: 'bottom-right', position: 'bottom-right',
duration: 5000, duration: 5000,
}); });
} else if (data.contentType === 'Content') { } else if (data.contentType === 'Content') {
ElMessageBox.confirm(data.content, data.notificationTitle, { ElMessageBox.confirm(data.content, data.notificationTitle, {
confirmButtonText: data.notificationButton, confirmButtonText: data.notificationButton,
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
cancelButtonText: '关闭', cancelButtonText: '关闭',
type: 'info', type: 'info',
closeOnClickModal: false, closeOnClickModal: false,
}).then(() => { }).then(() => {
ElMessageBox.close(); ElMessageBox.close();
const path = data.path; const path = data.path;
if (route.path === path) { if (route.path === path) {
core.bus.emit('onNewTask', { name: 'onNewTask' }); core.bus.emit('onNewTask', { name: 'onNewTask' });
} else { } else {
router.push({ path}); router.push({ path});
} }
}) })
.catch(() => {}); .catch(() => {});
} }
}; };
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 关闭连接 // 关闭连接
websocket.close(); websocket.close();
}); });
</script> </script>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -1,6 +1,6 @@
<template> <template>
<div style="display: inline-block"> <div style="display: inline-block">
<el-button size="default" type="success" @click="handleImport()"> <el-button type="success" @click="handleImport()">
<slot>导入</slot> <slot>导入</slot>
</el-button> </el-button>
<el-dialog :title="props.upload.title" v-model="uploadShow" width="400px" append-to-body> <el-dialog :title="props.upload.title" v-model="uploadShow" width="400px" append-to-body>

View File

@@ -4,7 +4,6 @@
class="tableSelector" class="tableSelector"
multiple multiple
:collapseTags="props.tableConfig.collapseTags" :collapseTags="props.tableConfig.collapseTags"
@remove-tag="removeTag"
v-model="data" v-model="data"
placeholder="请选择" placeholder="请选择"
@visible-change="visibleChange" @visible-change="visibleChange"
@@ -29,9 +28,9 @@
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" @select="handleSelectionChange"
@selectAll="handleSelectionChange" @selectAll="handleSelectionChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" /> <el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
@@ -59,34 +58,36 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, defineProps, onMounted, 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, type: Array || String || Number,
default: () => [] default: () => [],
}, },
tableConfig: { tableConfig: {
type: Object, type: Object,
default:{ default: {
url: null, url: null,
label: null, //显示值 label: null, //显示值
value: null, //数据值 value: null, //数据值
isTree: false, isTree: false,
lazy: true, lazy: true,
size:'default', size: 'default',
load: () => {}, load: () => {},
data: [], //默认数据 data: [], //默认数据
isMultiple: false, //是否多选 isMultiple: false, //是否多选
collapseTags: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();
@@ -137,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,
@@ -162,29 +165,25 @@ const getDict = async () => {
// 获取节点值 // 获取节点值
const getNodeValues = () => { const getNodeValues = () => {
console.log(props.tableConfig.url);
request({ request({
url:props.tableConfig.valueUrl, url: props.tableConfig.url,
method:'post', method: 'post',
data:{ids:props.modelValue} data: { ids: props.modelValue },
}).then(res=>{ }).then((res) => {
if(res.data.length>0){ if (res.data.length > 0) {
data.value = res.data.map((item:any)=>{ data.value = res.data.map((item: any) => {
return item[props.tableConfig.label] return item[props.tableConfig.label];
}) });
tableRef.value!.clearSelection()
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(
row,
true,
false
)
})
}
})
}
tableRef.value!.clearSelection();
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(row, true, false);
});
}
});
};
/** /**
* 下拉框展开/关闭 * 下拉框展开/关闭
@@ -205,12 +204,11 @@ const handlePageChange = (page: any) => {
getDict(); getDict();
}; };
onMounted(()=>{ onMounted(() => {
setTimeout(()=>{ // setTimeout(() => {
getNodeValues() // getNodeValues();
},1000) // }, 1000);
}) });
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange"> <div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img :src="siteLogo" class="layout-logo-medium-img" /> <img :src="siteLogo" class="layout-logo-medium-img" />
<span style="font-size: x-large">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span> <span style="font-size: x-large; ">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span>
</div> </div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange"> <div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img :src="siteLogo" class="layout-logo-size-img" /> <img :src="siteLogo" class="layout-logo-size-img" />

View File

@@ -1,5 +1,10 @@
<template> <template>
<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }"> <div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon :title="$t('message.user.title2')">
<ele-Search />
</el-icon>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange"> <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i> <i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
@@ -28,11 +33,7 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon :title="$t('message.user.title2')">
<ele-Search />
</el-icon>
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick"> <div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i> <i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div> </div>
@@ -60,14 +61,14 @@
<div> <div>
<span v-if="!isSocketOpen" class="online-status-span"> <span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm <el-popconfirm
width="250" width="250"
ref="onlinePopoverRef" ref="onlinePopoverRef"
:confirm-button-text="$t('message.user.retry')" :confirm-button-text="$t('message.user.retry')"
:icon="InfoFilled" :icon="InfoFilled"
trigger="hover" trigger="hover"
icon-color="#626AEF" icon-color="#626AEF"
:title="$t('message.user.onlinePrompt')" :title="$t('message.user.onlinePrompt')"
@confirm="onlineConfirmEvent" @confirm="onlineConfirmEvent"
> >
<template #reference> <template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}"> <el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
@@ -77,13 +78,12 @@
</el-popconfirm> </el-popconfirm>
</span> </span>
</div> </div>
<div></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"> <!-- <el-badge is-dot class="item online-status">-->
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}"> <!-- <img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />-->
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" /> <!-- </el-badge>-->
</el-badge>
</span>
{{ userInfos.username === '' ? 'common' : userInfos.username }} {{ userInfos.username === '' ? 'common' : userInfos.username }}
<el-icon class="el-icon--right"> <el-icon class="el-icon--right">
<ele-ArrowDown /> <ele-ArrowDown />
@@ -115,8 +115,8 @@ 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 { InfoFilled } from '@element-plus/icons-vue';
import websocket from '/@/utils/websocket'; import websocket from '/@/utils/websocket';
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'));
@@ -152,13 +152,14 @@ const { isSocketOpen } = storeToRefs(useUserInfo());
const onlinePopoverRef = ref() const onlinePopoverRef = ref()
const onlineConfirmEvent = () => { const onlineConfirmEvent = () => {
if (!isSocketOpen.value) { if (!isSocketOpen.value) {
websocket.is_reonnect = true websocket.is_reonnect = true
websocket.reconnect_current = 1 websocket.reconnect_current = 1
websocket.reconnect() websocket.reconnect()
} }
// 手动隐藏弹出 // 手动隐藏弹出
unref(onlinePopoverRef).popperRef?.delayHide?.() unref(onlinePopoverRef).popperRef?.delayHide?.()
} }
// 全屏点击时 // 全屏点击时
const onScreenfullClick = () => { const onScreenfullClick = () => {
if (!screenfull.isEnabled) { if (!screenfull.isEnabled) {
@@ -237,8 +238,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,12 +249,41 @@ 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"; import { getBaseURL } from '/@/utils/baseUrl';
const messageCenter = messageCenterStore(); const messageCenter = messageCenterStore();
let eventSource: EventSource | null = null; // 存储 EventSource 实例
const token = Session.get('token');
const isConnected = ref(false); // 标志变量,记录是否已连接过
const getMessageCenterCount = () => {
// 创建 EventSource 实例并连接到后端 SSE 端点
eventSource = new EventSource(`${getBaseURL()}sse/?token=${token}`); // 替换为你的后端地址
// 首次连接成功时打印一次
eventSource.onopen = function () {
if (!isConnected.value) {
console.log('SSE 首次连接成功');
isConnected.value = true; // 设置标志为已连接
}
};
// 监听消息事件
eventSource.onmessage = function (event) {
console.log(event.data);
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">
@@ -298,29 +330,29 @@ const messageCenter = messageCenterStore();
:deep(.el-badge__content.is-fixed) { :deep(.el-badge__content.is-fixed) {
top: 12px; top: 12px;
} }
.online-status{ .online-status {
cursor: pointer; cursor: pointer;
:deep(.el-badge__content.is-fixed) { :deep(.el-badge__content.is-fixed) {
top: 30px; top: 30px;
font-size: 14px; font-size: 14px;
left: 5px; left: 5px;
height: 12px; height: 12px;
width: 12px; width: 12px;
padding: 0; padding: 0;
background-color: #18bc9c; background-color: #18bc9c;
} }
} }
.online-down{ .online-down {
cursor: pointer; cursor: pointer;
:deep(.el-badge__content.is-fixed) { :deep(.el-badge__content.is-fixed) {
top: 30px; top: 30px;
font-size: 14px; font-size: 14px;
left: 5px; left: 5px;
height: 12px; height: 12px;
width: 12px; width: 12px;
padding: 0; padding: 0;
background-color: #979b9c; background-color: #979b9c;
} }
} }
} }
</style> </style>

View File

@@ -2,7 +2,8 @@
<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">
<template v-if="state.newsList.length > 0"> <template v-if="state.newsList.length > 0">
@@ -21,7 +22,7 @@
</template> </template>
<script setup lang="ts" name="layoutBreadcrumbUserNews"> <script setup lang="ts" name="layoutBreadcrumbUserNews">
import { reactive,onBeforeMount,ref,onMounted } from 'vue'; import { reactive, onBeforeMount, ref, onMounted } from 'vue';
// 定义变量内容 // 定义变量内容
const state = reactive({ const state = reactive({
@@ -33,27 +34,27 @@ 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] if (data) state.newsList = [data];
})
} });
onMounted(()=>{ };
getLastMsg() onMounted(() => {
}) getLastMsg();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -591,10 +591,13 @@ watch(
<style scoped lang="scss"> <style scoped lang="scss">
.layout-navbars-tagsview { .layout-navbars-tagsview {
background-color: var(--el-color-white);
border-bottom: 1px solid var(--next-border-color-light);
position: relative; position: relative;
z-index: 4; z-index: 4;
height: 45px;
border-radius: 8px;
margin-left: 15px;
margin-right: 15px;
:deep(.el-scrollbar__wrap) { :deep(.el-scrollbar__wrap) {
overflow-x: auto !important; overflow-x: auto !important;
} }
@@ -602,7 +605,7 @@ watch(
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 34px; height: 36px;
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
@@ -610,7 +613,7 @@ watch(
white-space: nowrap; white-space: nowrap;
padding: 0 15px; padding: 0 15px;
&-li { &-li {
height: 26px; height: 30px;
line-height: 26px; line-height: 26px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -93,7 +93,7 @@ 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;
}); });
}; };
// 路由过滤递归函数 // 路由过滤递归函数

View File

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

View File

@@ -98,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');

View File

@@ -2,17 +2,19 @@
* 定义接口来定义对象的类型 * 定义接口来定义对象的类型
* `stores` 全部类型定义在这里 * `stores` 全部类型定义在这里
*/ */
import {useFrontendMenuStore} from "/@/stores/frontendMenu"; 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; pwd_change_count: null | number;
dept_info: { dept_info: {
dept_id: number; dept_id: number;
dept_name: string; dept_name: string;
@@ -107,9 +109,9 @@ export interface ConfigStates {
export interface FrontendMenu { export interface FrontendMenu {
arrayRouter: Array<any>; arrayRouter: Array<any>;
treeRouter:Array<any>; treeRouter: Array<any>;
frameOutRoutes:Array<any>; frameOutRoutes: Array<any>;
frameInRoutes:Array<any>; frameInRoutes: Array<any>;
} }

View File

@@ -18,7 +18,7 @@ export const useThemeConfig = defineStore('themeConfig', {
* 全局主题 * 全局主题
*/ */
// 默认 primary 主题颜色 // 默认 primary 主题颜色
primary: '#409eff', primary: "#193755",
// 是否开启深色模式 // 是否开启深色模式
isIsDark: false, isIsDark: false,
@@ -26,9 +26,9 @@ export const useThemeConfig = defineStore('themeConfig', {
* 顶栏设置 * 顶栏设置
*/ */
// 默认顶栏导航背景颜色 // 默认顶栏导航背景颜色
topBar: '#ffffff', topBar: "#f8f8f8",
// 默认顶栏导航字体颜色 // 默认顶栏导航字体颜色
topBarColor: '#606266', topBarColor: "#000000",
// 是否开启顶栏背景颜色渐变 // 是否开启顶栏背景颜色渐变
isTopBarColorGradual: false, isTopBarColorGradual: false,
@@ -36,11 +36,11 @@ export const useThemeConfig = defineStore('themeConfig', {
* 菜单设置 * 菜单设置
*/ */
// 默认菜单导航背景颜色 // 默认菜单导航背景颜色
menuBar: '#334054', menuBar: "#f8f8f8",
// 默认菜单导航字体颜色 // 默认菜单导航字体颜色
menuBarColor: '#eaeaea', menuBarColor: "#000000",
// 默认菜单高亮背景色 // 默认菜单高亮背景色
menuBarActiveColor: 'rgba(0, 0, 0, 0.2)', menuBarActiveColor: "rgba(0, 48, 255, 0.38)",
// 是否开启菜单背景颜色渐变 // 是否开启菜单背景颜色渐变
isMenuBarColorGradual: false, isMenuBarColorGradual: false,
@@ -48,9 +48,9 @@ export const useThemeConfig = defineStore('themeConfig', {
* 分栏设置 * 分栏设置
*/ */
// 默认分栏菜单背景颜色 // 默认分栏菜单背景颜色
columnsMenuBar: '#334054', columnsMenuBar:"#334054",
// 默认分栏菜单字体颜色 // 默认分栏菜单字体颜色
columnsMenuBarColor: '#e6e6e6', columnsMenuBarColor: "#e6e6e6",
// 是否开启分栏菜单背景颜色渐变 // 是否开启分栏菜单背景颜色渐变
isColumnsMenuBarColorGradual: false, isColumnsMenuBarColorGradual: false,
// 是否开启分栏菜单鼠标悬停预加载(预览菜单) // 是否开启分栏菜单鼠标悬停预加载(预览菜单)

View File

@@ -12,6 +12,7 @@ import headerImage from '/@/assets/img/headerImage.png';
export const useUserInfo = defineStore('userInfo', { export const useUserInfo = defineStore('userInfo', {
state: (): UserInfosStates => ({ state: (): UserInfosStates => ({
userInfos: { userInfos: {
id:'',
avatar: '', avatar: '',
username: '', username: '',
name: '', name: '',
@@ -19,6 +20,7 @@ export const useUserInfo = defineStore('userInfo', {
mobile: '', mobile: '',
gender: '', gender: '',
pwd_change_count:null, pwd_change_count:null,
is_superuser: false,
dept_info: { dept_info: {
dept_id: 0, dept_id: 0,
dept_name: '', dept_name: '',
@@ -37,6 +39,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.pwd_change_count = count; this.userInfos.pwd_change_count = count;
}, },
async updateUserInfos(userInfos:any) { async updateUserInfos(userInfos:any) {
this.userInfos.id = userInfos.id;
this.userInfos.username = userInfos.name; this.userInfos.username = userInfos.name;
this.userInfos.avatar = userInfos.avatar; this.userInfos.avatar = userInfos.avatar;
this.userInfos.name = userInfos.name; this.userInfos.name = userInfos.name;
@@ -46,6 +49,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.dept_info = userInfos.dept_info; this.userInfos.dept_info = userInfos.dept_info;
this.userInfos.role_info = userInfos.role_info; this.userInfos.role_info = userInfos.role_info;
this.userInfos.pwd_change_count = userInfos.pwd_change_count; 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() {
@@ -54,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;
@@ -63,6 +68,7 @@ export const useUserInfo = defineStore('userInfo', {
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.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);
} }
}, },
@@ -74,6 +80,7 @@ export const useUserInfo = defineStore('userInfo', {
url: '/api/system/user/user_info/', url: '/api/system/user/user_info/',
method: 'get', method: 'get',
}).then((res:any)=>{ }).then((res:any)=>{
this.userInfos.id = res.data.id;
this.userInfos.username = res.data.name; this.userInfos.username = res.data.name;
this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage; this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage;
this.userInfos.name = res.data.name; this.userInfos.name = res.data.name;
@@ -83,6 +90,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.dept_info = res.data.dept_info; this.userInfos.dept_info = res.data.dept_info;
this.userInfos.role_info = res.data.role_info; this.userInfos.role_info = res.data.role_info;
this.userInfos.pwd_change_count = res.data.pwd_change_count; this.userInfos.pwd_change_count = res.data.pwd_change_count;
this.userInfos.is_superuser = res.data.is_superuser;
Session.set('userInfo', this.userInfos); Session.set('userInfo', this.userInfos);
}) })
}, },

View File

@@ -1,4 +1,4 @@
@import 'mixins/index.scss'; @use 'mixins/index.scss' as index;
/* Button 按钮 /* Button 按钮
------------------------------- */ ------------------------------- */
@@ -29,7 +29,7 @@
.el-form { .el-form {
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容 // 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type { .el-form-item:last-of-type {
margin-bottom: 22px !important; margin-bottom: 24px !important;
} }
// 修复行内表单最后一个 el-form-item 位置下移问题 // 修复行内表单最后一个 el-form-item 位置下移问题
&.el-form--inline { &.el-form--inline {
@@ -38,12 +38,12 @@
} }
.el-form-item--default.el-form-item:last-of-type, .el-form-item--default.el-form-item:last-of-type,
.el-form-item--small.el-form-item:last-of-type { .el-form-item--small.el-form-item:last-of-type {
margin-bottom: 18px !important; margin-bottom: 6px !important;
} }
} }
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM // https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon { .el-form-item .el-form-item__label .el-icon {
margin-right: 0px; margin-right: 0;
} }
} }
@@ -76,12 +76,16 @@
width: 220px; width: 220px;
} }
.el-menu-item { .el-menu-item {
height: 56px !important; height: 46px !important;
line-height: 56px !important; line-height: 46px !important;
border-radius:12px;
} }
.el-menu-item, .el-menu-item,
.el-sub-menu__title { .el-sub-menu__title {
height: 46px !important;
line-height: 46px !important;
color: var(--next-bg-menuBarColor); color: var(--next-bg-menuBarColor);
border-radius:12px;
} }
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题 // 修复点击左侧菜单折叠再展开时,宽度不跟随问题
.el-menu--collapse { .el-menu--collapse {
@@ -100,7 +104,7 @@
.el-sub-menu .iconfont, .el-sub-menu .iconfont,
.el-menu-item .fa, .el-menu-item .fa,
.el-sub-menu .fa { .el-sub-menu .fa {
@include generalIcon; @include index.generalIcon;
} }
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色 // 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
.el-menu-item.is-active, .el-menu-item.is-active,

View File

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

View File

@@ -1,8 +1,8 @@
@import './app.scss'; @use './app.scss';
@import 'common/transition.scss'; @use 'common/transition.scss';
@import './other.scss'; @use './other.scss';
@import './element.scss'; @use './element.scss';
@import './media/media.scss'; @use './media/media.scss';
@import './waves.scss'; @use './waves.scss';
@import './dark.scss'; @use './dark.scss';
@import './fa/css/font-awesome.min.css'; @use './fa/css/font-awesome.min.css';

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
.big-data-down-left { .big-data-down-left {
width: 100% !important; width: 100% !important;
flex-direction: unset !important; flex-direction: unset !important;
@@ -51,7 +51,7 @@
/* 页面宽度大于768px小于1200px /* 页面宽度大于768px小于1200px
------------------------------- */ ------------------------------- */
@media screen and (min-width: $sm) and (max-width: $lg) { @media screen and (min-width: index.$sm) and (max-width: index.$lg) {
.chart-warp-bottom { .chart-warp-bottom {
.big-data-down-left { .big-data-down-left {
width: 50% !important; width: 50% !important;
@@ -72,7 +72,7 @@
/* 页面宽度小于1200px /* 页面宽度小于1200px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $lg) { @media screen and (max-width: index.$lg) {
.chart-warp-top { .chart-warp-top {
.up-left { .up-left {
display: none; display: none;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于576px /* 页面宽度小于576px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $xs) { @media screen and (max-width: index.$xs) {
.el-cascader__dropdown.el-popper { .el-cascader__dropdown.el-popper {
overflow: auto; overflow: auto;
max-width: 100%; max-width: 100%;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
// 时间选择器适配 // 时间选择器适配
.el-date-range-picker { .el-date-range-picker {
width: 100vw; width: 100vw;

View File

@@ -1,4 +1,4 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于800px /* 页面宽度小于800px
------------------------------- */ ------------------------------- */

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
.error { .error {
.error-flex { .error-flex {
flex-direction: column-reverse !important; flex-direction: column-reverse !important;
@@ -26,7 +26,7 @@
/* 页面宽度大于768px小于992px /* 页面宽度大于768px小于992px
------------------------------- */ ------------------------------- */
@media screen and (min-width: $sm) and (max-width: $md) { @media screen and (min-width: index.$sm) and (max-width: index.$md) {
.error { .error {
.error-flex { .error-flex {
padding-left: 30px !important; padding-left: 30px !important;
@@ -36,7 +36,7 @@
/* 页面宽度小于1200px /* 页面宽度小于1200px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $lg) { @media screen and (max-width: index.$lg) {
.error { .error {
.error-flex { .error-flex {
padding: 0 30px; padding: 0 30px;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于576px /* 页面宽度小于576px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $xs) { @media screen and (max-width: index.$xs) {
.el-form-item__label { .el-form-item__label {
width: 100% !important; width: 100% !important;
text-align: left !important; text-align: left !important;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
.home-media, .home-media,
.home-media-sm { .home-media-sm {
margin-top: 15px; margin-top: 15px;
@@ -11,7 +11,7 @@
/* 页面宽度小于1200px /* 页面宽度小于1200px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $lg) { @media screen and (max-width: index.$lg) {
.home-media-lg { .home-media-lg {
margin-top: 15px; margin-top: 15px;
} }

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于576px /* 页面宽度小于576px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $xs) { @media screen and (max-width: index.$xs) {
// MessageBox 弹框 // MessageBox 弹框
.el-message-box { .el-message-box {
width: 80% !important; width: 80% !important;
@@ -11,7 +11,7 @@
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
// Breadcrumb 面包屑 // Breadcrumb 面包屑
.layout-navbars-breadcrumb-hide { .layout-navbars-breadcrumb-hide {
display: none; display: none;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于1200px /* 页面宽度小于1200px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $lg) and (min-width: $xs) { @media screen and (max-width: index.$lg) and (min-width: index.$xs) {
.login-container { .login-container {
.login-left { .login-left {
.login-left-img { .login-left-img {
@@ -23,7 +23,7 @@
/* 页面宽度小于576px /* 页面宽度小于576px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $xs) { @media screen and (max-width: index.$xs) {
.login-container { .login-container {
.login-left { .login-left {
display: none; display: none;
@@ -59,7 +59,7 @@
/* 页面宽度小于375px /* 页面宽度小于375px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $us) { @media screen and (max-width: index.$us) {
.login-container { .login-container {
.login-right { .login-right {
.login-right-warp { .login-right-warp {

View File

@@ -1,13 +1,13 @@
@import './login.scss'; @use './login.scss';
@import './error.scss'; @use './error.scss';
@import './layout.scss'; @use './layout.scss';
@import './personal.scss'; @use './personal.scss';
@import './tagsView.scss'; @use './tagsView.scss';
@import './home.scss'; @use './home.scss';
@import './chart.scss'; @use './chart.scss';
@import './form.scss'; @use './form.scss';
@import './scrollbar.scss'; @use './scrollbar.scss';
@import './pagination.scss'; @use './pagination.scss';
@import './dialog.scss'; @use './dialog.scss';
@import './cityLinkage.scss'; @use './cityLinkage.scss';
@import './date.scss'; @use './date.scss';

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于576px /* 页面宽度小于576px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $xs) { @media screen and (max-width: index.$xs) {
.el-pager, .el-pager,
.el-pagination__jump { .el-pagination__jump {
display: none !important; display: none !important;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
.personal-info { .personal-info {
padding-left: 0 !important; padding-left: 0 !important;
margin-top: 15px; margin-top: 15px;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
// 滚动条的宽度 // 滚动条的宽度
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 3px !important; width: 3px !important;

View File

@@ -1,8 +1,8 @@
@import './index.scss'; @use './index.scss' as index;
/* 页面宽度小于768px /* 页面宽度小于768px
------------------------------- */ ------------------------------- */
@media screen and (max-width: $sm) { @media screen and (max-width: index.$sm) {
.tags-view-form { .tags-view-form {
.tags-view-form-col { .tags-view-form-col {
margin-bottom: 20px; margin-bottom: 20px;

View File

@@ -172,20 +172,20 @@ function createRequestFunction(service: any) {
return function (config: any) { return function (config: any) {
const configDefault = { const configDefault = {
headers: { headers: {
'Content-Type': get(config, 'headers.Content-Type', 'application/json'), 'Content-Type': 'application/json',
}, },
timeout: 5000, timeout: 5000,
baseURL: getBaseURL(), baseURL: getBaseURL(),
data: {}, data: {},
}; };
Object.assign(configDefault, config);
// const token = userStore.getToken; // const token = userStore.getToken;
const token = Session.get('token'); const token = Session.get('token');
if (token != null) { if (token != null) {
// @ts-ignore // @ts-ignore
configDefault.headers.Authorization = 'JWT ' + token; configDefault.headers.Authorization = 'JWT ' + token;
} }
return service(Object.assign(configDefault, config)); return service(configDefault);
}; };
} }

View File

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

View File

@@ -1,54 +1,96 @@
<template> <template>
<div class="home-container"> <div class="home-container">
<el-row :gutter="15" class="home-card-one mb15"> <div style="margin: 15px; font-size: 16px; font-weight: 700">
<el-col 欢迎回来{{ userInfo.userInfos.name }}
:xs="24" <span style="font-size: 12px; color: grey"> 这里是您的工作台请愉快的工作吧</span>
:sm="12" </div>
:md="12" <el-row>
:lg="6" <el-col :span="16">
:xl="6" <el-row :gutter="15" class="home-card-one mb15">
v-for="(v, k) in homeOne" <el-col
:key="k" :xs="24"
:class="{ 'home-media home-media-lg': k > 1, 'home-media-sm': k === 1 }" :sm="12"
:md="12"
:lg="8"
:xl="6"
v-for="(v, k) in homeOne"
:key="k"
:class="{ 'home-media home-media-lg': k > 1, 'home-media-sm': k === 1 }"
> >
<div class="home-card-item flex"> <div class="home-card-item flex" style="padding: 0;">
<div class="flex-margin flex w100" :class="` home-one-animation${k}`"> <div class="flex-margin flex w100" :class="` home-one-animation${k}`">
<div class="flex-auto"> <div class="home-card-item-icon flex" style="margin: 10px;" :style="{ background: `var(${v.color2})` }">
<span class="font30">{{ v.num1 }}</span> <i class="flex-margin font24" :class="v.num4" :style="{ color: `var(${v.color3})` }"></i>
<span class="ml5 font16" :style="{ color: v.color1 }">{{ v.num2 }}%</span>
<div class="mt10">{{ v.num3 }}</div>
</div> </div>
<div class="home-card-item-icon flex" :style="{ background: `var(${v.color2})` }"> <div class="flex-auto">
<i class="flex-margin font32" :class="v.num4" :style="{ color: `var(${v.color3})` }"></i> <span class="font24">{{ v.num1 }}</span>
<span class="ml5 font14" :style="{ color: v.color1 }">{{ v.num2 }}%</span>
<div class="mt10">{{ v.num3 }}</div>
</div> </div>
</div> </div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="15" class="home-card-two mb15"> <el-row :gutter="15" class="home-card-two mb15">
<el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16"> <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<div class="home-card-item">
<div style="height: 100%" ref="homeLineRef"></div>
</div>
</el-col>
</el-row>
<el-row :gutter="15" class="home-card-three">
<el-col :span="24" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="home-media">
<div class="home-card-item"> <div class="home-card-item">
<div style="height: 100%" ref="homeLineRef"></div> <div style="height: 100%" ref="homeBarRef"></div>
</div>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8" class="home-media">
<div class="home-card-item">
<div style="height: 100%" ref="homePieRef"></div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="15" class="home-card-three"> </el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8"> <el-col :span="8">
<div class="home-card-item"> <div style="margin-left: 15px">
<el-row :gutter="100" class="home-card-one mb15">
<el-col :span="24">
<div class="home-card-item flex" style="padding: 0; width:350px; height:200px">
<img :src="HomeBg" style="padding: 0; margin: 0;">
</div>
</el-col>
</el-row>
<el-row :gutter="100" class="home-card-one mb15">
<el-col :span="24" >
<div class="home-card-item" style=" width:350px; height: 100%; max-height: 320px">
<div class="home-card-item-title">消息通知
<button type="button" class="el-button" style=" float: right; border-color: transparent; margin-top: -2px;" @click="msgMore">
<span>更多
<i class="el-icon fs-icon fs-button-icon-right"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288m0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.19 160.19 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160"></path></svg></i>
</span>
</button>
</div>
<div v-for="(v, k) in newsInfoList" :key="k" class="personal-info-li flex-margin flex w100" >
<div class="home-card-item-icon flex" style="margin: 5px;" :style="{ background: `#f8f8f8` }">
<i class="flex-margin font24" :class="`fa fa-commenting-o`" :style="{ color: `#5d8b22` }"></i>
</div>
<div class="flex-auto" style="margin-top: 10px">
<span class="font14">[{{ v.creator_name }}]</span>
<span style=" color: grey; float: right; font-style:italic;">&nbsp;{{ v.create_datetime }}&nbsp;&nbsp;</span>
<div class="text-container"> {{ v.title }}</div>
</div>
</div>
</div>
</el-col>
</el-row>
<el-row :gutter="100" class="home-card-one mb15">
<el-col :span="24">
<div class="home-card-item" style=" width:350px; height: 100%">
<div class="home-card-item-title">快捷导航工具</div> <div class="home-card-item-title">快捷导航工具</div>
<div class="home-monitor"> <div class="home-monitor">
<div class="flex-warp"> <div class="flex-warp">
<div class="flex-warp-item" v-for="(v, k) in homeThree" :key="k"> <div class="flex-warp-item" v-for="(v, k) in homeThree" :key="k">
<div class="flex-warp-item-box" :class="`home-animation${k}`"> <div class="flex-warp-item-box" :class="`home-animation${k}`">
<div class="flex-margin"> <div class="flex-margin">
<i :class="v.icon" :style="{ color: v.iconColor }"></i> <div class="home-card-item-icon flex" style="margin: 20px;" :style="{ background: '#f8f8f8' }">
<span class="pl5">{{ v.label }}</span> <i class="flex-margin font24" :class="v.icon" :style="{ color: v.iconColor}"></i>
<div class="mt10">{{ v.value }}</div> </div>
<span class="pl20" :style="{ fontSize: 'clamp(0.875rem, 2vw, 1rem)' }">{{ v.label }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -56,13 +98,11 @@
</div> </div>
</div> </div>
</el-col> </el-col>
<el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16" class="home-media"> </el-row>
<div class="home-card-item"> </div>
<div style="height: 100%" ref="homeBarRef"></div> </el-col>
</div> </el-row>
</el-col> </div>
</el-row>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -71,6 +111,7 @@ import * as echarts from 'echarts';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import HomeBg from '/@/assets/home-bg.png';
let global: any = { let global: any = {
homeChartOne: null, homeChartOne: null,
@@ -79,9 +120,21 @@ let global: any = {
dispose: [null, '', undefined], dispose: [null, '', undefined],
}; };
import { useUserInfo } from '/@/stores/userInfo';
import {useRouter} from "vue-router";
import * as api from "/@/views/system/personal/api";
// 定义消息类型
interface NewsItem {
creator_name: string;
create_datetime: string;
title: string;
}
export default defineComponent({ export default defineComponent({
name: 'home', name: 'home',
setup() { setup() {
const userInfo = useUserInfo();
const homeLineRef = ref(); const homeLineRef = ref();
const homePieRef = ref(); const homePieRef = ref();
const homeBarRef = ref(); const homeBarRef = ref();
@@ -89,7 +142,11 @@ export default defineComponent({
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes); const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const router = useRouter(); // 将router移到组件级别
const defaultNewsItems: NewsItem[] = [];
const state = reactive({ const state = reactive({
newsInfoList: [...defaultNewsItems] as NewsItem[],
homeOne: [ homeOne: [
{ {
num1: '125,12', num1: '125,12',
@@ -109,15 +166,6 @@ export default defineComponent({
color2: '--next-color-success-lighter', color2: '--next-color-success-lighter',
color3: '--el-color-success', color3: '--el-color-success',
}, },
{
num1: '125,65',
num2: '+17.32',
num3: '年度计划信息',
num4: 'iconfont icon-zaosheng',
color1: '#6690F9',
color2: '--next-color-warning-lighter',
color3: '--el-color-warning',
},
{ {
num1: '520,43', num1: '520,43',
num2: '-10.01', num2: '-10.01',
@@ -131,57 +179,48 @@ export default defineComponent({
homeThree: [ homeThree: [
{ {
icon: 'iconfont icon-yangan', icon: 'iconfont icon-yangan',
label: '浅粉红', label: '用户管理',
value: '2.1%OBS/M', iconColor: 'gray',
iconColor: '#F72B3F',
}, },
{ {
icon: 'iconfont icon-wendu', icon: 'iconfont icon-wendu',
label: '深红(猩红)', label: '部门管理',
value: '30℃', iconColor: 'gray',
iconColor: '#91BFF8',
}, },
{ {
icon: 'iconfont icon-shidu', icon: 'iconfont icon-shidu',
label: '淡紫红', label: '权限管理',
value: '57%RH', iconColor: 'gray',
iconColor: '#88D565',
}, },
{ {
icon: 'iconfont icon-shidu', icon: 'iconfont icon-shidu',
label: '弱紫罗兰红', label: '日志管理',
value: '107w', iconColor: 'gray',
iconColor: '#88D565',
}, },
{ {
icon: 'iconfont icon-zaosheng', icon: 'iconfont icon-zaosheng',
label: '中紫罗兰红', label: '菜单管理',
value: '57DB', iconColor: 'gray',
iconColor: '#FBD4A0',
}, },
{ {
icon: 'iconfont icon-zaosheng', icon: 'iconfont icon-zaosheng',
label: '紫罗兰', label: '消息中心',
value: '57PV', iconColor: 'gray',
iconColor: '#FBD4A0',
}, },
{ {
icon: 'iconfont icon-zaosheng', icon: 'iconfont icon-zaosheng',
label: '暗紫罗兰', label: '接口管理',
value: '517Cpd', iconColor: 'gray',
iconColor: '#FBD4A0',
}, },
{ {
icon: 'iconfont icon-zaosheng', icon: 'iconfont icon-zaosheng',
label: '幽灵白', label: '下载中心',
value: '12kg', iconColor: 'gray',
iconColor: '#FBD4A0',
}, },
{ {
icon: 'iconfont icon-zaosheng', icon: 'iconfont icon-zaosheng',
label: '海军蓝', label: '系统管理',
value: '64fm', iconColor: 'gray',
iconColor: '#FBD4A0',
}, },
], ],
myCharts: [], myCharts: [],
@@ -506,6 +545,7 @@ export default defineComponent({
// 页面加载时 // 页面加载时
onMounted(() => { onMounted(() => {
initEchartsResize(); initEchartsResize();
getMsg(); // 确保组件挂载时立即获取消息列表
}); });
// 由于页面缓存原因keep-alive // 由于页面缓存原因keep-alive
onActivated(() => { onActivated(() => {
@@ -542,11 +582,47 @@ export default defineComponent({
immediate: true, immediate: true,
} }
); );
// 获取消息列表
const getMsg = (): void => {
// 先重置为默认数据
state.newsInfoList = [...defaultNewsItems];
// 尝试从API获取最新数据
api.GetSelfReceive({}).then((res: any) => {
const { data } = res || {};
// 严格检查返回数据的有效性
if (data && Array.isArray(data) && data.length > 0) {
try {
// 安全地进行类型转换并更新状态
state.newsInfoList = data.map((item: any): NewsItem => ({
creator_name: String(item.creator_name || '未知用户'),
create_datetime: String(item.create_datetime || ''),
title: String(item.title || '')
}));
} catch (typeError) {
console.error('数据类型转换失败:', typeError);
// 类型转换失败时保持默认数据
}
}
}).catch((error: Error) => {
console.error('获取消息列表失败,保持默认数据:', error);
// 错误时保持已设置的默认数据
});
};
// 跳转消息中心
const msgMore = (): void => {
router.push({ path: '/messageCenter' });
};
return { return {
homeLineRef, homeLineRef,
homePieRef, homePieRef,
homeBarRef, homeBarRef,
...toRefs(state), userInfo,
...toRefs(state),
HomeBg,
msgMore,
}; };
}, },
}); });
@@ -554,6 +630,20 @@ export default defineComponent({
<style scoped lang="scss"> <style scoped lang="scss">
$homeNavLengh: 8; $homeNavLengh: 8;
.text-container {
width: 420px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-container:hover {
animation: scrollText 5s linear infinite;
}
@keyframes scrollText {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
.home-container { .home-container {
overflow: hidden; overflow: hidden;
.home-card-one, .home-card-one,
@@ -561,21 +651,21 @@ $homeNavLengh: 8;
.home-card-three { .home-card-three {
.home-card-item { .home-card-item {
width: 100%; width: 100%;
height: 130px; height: 120px;
border-radius: 4px;
transition: all ease 0.3s; transition: all ease 0.3s;
padding: 20px; padding: 20px;
overflow: hidden; overflow: hidden;
background: var(--el-color-white); background: var(--el-color-white);
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
border: 1px solid var(--next-border-color-light); border: 1px solid var(--next-border-color-light);
border-radius: 24px;
&:hover { &:hover {
box-shadow: 0 2px 12px var(--next-color-dark-hover); box-shadow: 0 2px 12px var(--next-color-dark-hover);
transition: all ease 0.3s; transition: all ease 0.3s;
} }
&-icon { &-icon {
width: 70px; width: 55px;
height: 70px; height: 55px;
border-radius: 100%; border-radius: 100%;
flex-shrink: 1; flex-shrink: 1;
i { i {
@@ -583,13 +673,15 @@ $homeNavLengh: 8;
} }
} }
&-title { &-title {
font-size: 15px; font-size: 16px;
font-weight: bold; font-weight: bold;
height: 30px; height: 30px;
} }
} }
} }
.home-card-one { .home-card-one {
left:15px;
right: 15px;
@for $i from 0 through 3 { @for $i from 0 through 3 {
.home-one-animation#{$i} { .home-one-animation#{$i} {
opacity: 0; opacity: 0;
@@ -602,6 +694,9 @@ $homeNavLengh: 8;
} }
.home-card-two, .home-card-two,
.home-card-three { .home-card-three {
position: relative;
left: 15px;
right: 15px;
.home-card-item { .home-card-item {
height: 400px; height: 400px;
width: 100%; width: 100%;
@@ -626,7 +721,7 @@ $homeNavLengh: 8;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
} }
@for $i from 0 through $homeNavLengh { @for $i from 0 through 3 {
.home-animation#{$i} { .home-animation#{$i} {
opacity: 0; opacity: 0;
animation-name: error-num; animation-name: error-num;
@@ -641,3 +736,4 @@ $homeNavLengh: 8;
} }
} }
</style> </style>

View File

@@ -28,3 +28,10 @@ export function getUserInfo() {
method: 'get', method: 'get',
}); });
} }
export function getBackends() {
return request({
url: '/api/dvadmin3_social_oauth2/backend/get_login_backend/',
method: 'get',
});
}

View File

@@ -187,8 +187,8 @@ export default defineComponent({
// 初始化登录成功时间问候语 // 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value; let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页 // 登录成功,跳到转首页
const pwd_change_count = userInfos.value.pwd_change_count const pwd_change_count = userInfos.value.pwd_change_count ?? 0
if(pwd_change_count>0){ if(pwd_change_count > 0){
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中 // 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) { if (route.query?.redirect) {
router.push({ router.push({
@@ -234,16 +234,37 @@ export default defineComponent({
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// 定义error-num动画的关键帧
@keyframes error-num {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-content-form { .login-content-form {
margin-top: 20px; margin-top: 20px;
// 为输入框添加圆角和设置字体大小
:deep(.el-input__wrapper) {
border-radius: 8px !important;
}
// 设置输入框文字大小
:deep(.el-input__inner) {
font-size: 12px !important; // Element Plus large尺寸的默认字体大小
}
@for $i from 1 through 4 { @for $i from 1 through 4 {
.login-animation#{$i} { .login-animation#{$i} {
opacity: 0; opacity: 0;
animation-name: error-num; animation-name: error-num;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-fill-mode: forwards; animation-fill-mode: forwards;
animation-delay: calc($i/10) + s; animation-delay: #{$i/10}s;
} }
} }
@@ -253,7 +274,7 @@ export default defineComponent({
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: #909399; color: #909397;
} }
} }
@@ -262,6 +283,7 @@ export default defineComponent({
padding: 0; padding: 0;
font-weight: bold; font-weight: bold;
letter-spacing: 5px; letter-spacing: 5px;
border-radius: 8px !important;
} }
.login-content-submit { .login-content-submit {
@@ -269,6 +291,7 @@ export default defineComponent({
letter-spacing: 2px; letter-spacing: 2px;
font-weight: 800; font-weight: 800;
margin-top: 15px; margin-top: 15px;
border-radius:8px;
} }
} }
</style> </style>

View File

@@ -239,6 +239,15 @@ export default defineComponent({
.login-content-form { .login-content-form {
margin-top: 20px; margin-top: 20px;
// 为输入框添加圆角和设置字体大小
:deep(.el-input__wrapper) {
border-radius: 8px !important;
}
// 设置输入框文字大小
:deep(.el-input__inner) {
font-size: 12px !important; // Element Plus large尺寸的默认字体大小
}
@for $i from 1 through 5 { @for $i from 1 through 5 {
.login-animation#{$i} { .login-animation#{$i} {
opacity: 0; opacity: 0;
@@ -264,6 +273,7 @@ export default defineComponent({
padding: 0; padding: 0;
font-weight: bold; font-weight: bold;
letter-spacing: 5px; letter-spacing: 5px;
border-radius: 8px !important;
} }
.login-content-submit { .login-content-submit {
@@ -271,6 +281,7 @@ export default defineComponent({
letter-spacing: 2px; letter-spacing: 2px;
font-weight: 800; font-weight: 800;
margin-top: 15px; margin-top: 15px;
border-radius:8px;
} }
} }
</style> </style>

View File

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

View File

@@ -5,8 +5,6 @@
<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" style="margin-top: 5px;">{{
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -16,7 +14,9 @@
<!-- <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"> <div class="login-right-warp-main-title">
{{userInfos.pwd_change_count===0?'初次登录修改密码':'欢迎登录'}} <span>{{getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
<br>
<span>{{userInfos.pwd_change_count===0?'初次登录请修改密码':'欢迎登录'}}</span>
</div> </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">
@@ -34,7 +34,9 @@
</el-tab-pane> --> </el-tab-pane> -->
</el-tabs> </el-tabs>
</div> </div>
<!-- <Scan v-if="state.isScan" />--> <OAuth2 />
<!-- <Scan v-if="state.isScan" />-->
<!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">--> <!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">-->
<!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>--> <!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
<!-- <div class="login-content-main-sacn-delta"></div>--> <!-- <div class="login-content-main-sacn-delta"></div>-->
@@ -45,10 +47,10 @@
</div> </div>
<div class="login-authorization z-10"> <div class="login-authorization z-10">
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 北京信码新创科技有限公司' }} 版权所有</p> <p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2025 django-vue-admin.com' }} 版权所有</p>
<p class="la-other" style="margin-top: 5px;"> <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备2021031018号' }}</a> 'ICP备18005113号-3' }}</a>
| |
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : '#'" <a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : '#'"
target="_blank">帮助</a> target="_blank">帮助</a>
@@ -81,6 +83,8 @@ const Account = defineAsyncComponent(() => import('/@/views/system/login/compone
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 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"; import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo()); const { userInfos } = storeToRefs(useUserInfo());
@@ -164,14 +168,11 @@ onMounted(() => {
span { span {
margin-left: 10px; margin-left: 10px;
font-size: 16px; font-size: 24px;
color: var(--el-color-primary); color: var(--el-color-primary);
} }
.login-left-logo-text-msg {
font-size: 12px;
color: var(--el-color-primary);
}
} }
} }
@@ -276,10 +277,10 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
.login-right-warp-main-title { .login-right-warp-main-title {
height: 130px; height: 130px;
line-height: 130px; font-size: 24px;
font-size: 32px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
letter-spacing: 3px; letter-spacing: 3px;
@@ -343,7 +344,7 @@ onMounted(() => {
text-align: center; text-align: center;
p { p {
font-size: 14px; font-size: 12px;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
} }

View File

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

View File

@@ -77,7 +77,7 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
show: auth('btn:Create') show: auth('menu:CreateButton')
}, },
batchAdd: { batchAdd: {
show: true, show: true,
@@ -108,10 +108,10 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
edit: { edit: {
icon: '', icon: '',
type: 'primary', type: 'primary',
show: auth('btn:Update') show: auth('menu:UpdateButton')
}, },
remove: { remove: {
show: auth('btn:Delete') show: auth('menu:DeleteButton')
}, },
}, },
}, },

View File

@@ -254,6 +254,9 @@ const handleSubmit = () => {
let res; let res;
menuBtnLoading.value = true; menuBtnLoading.value = true;
if (menuFormData.id) { if (menuFormData.id) {
if (menuFormData.parent == undefined) {
menuFormData.parent = null
}
res = await UpdateObj(menuFormData); res = await UpdateObj(menuFormData);
} else { } else {
res = await AddObj(menuFormData); res = await AddObj(menuFormData);

View File

@@ -131,10 +131,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
data: [ data: [
{ value: 0, label: '按用户' }, { value: 0, label: '按用户' },
{ value: 1, label: '按角色' }, { value: 1, label: '按角色' },
{ { value: 2, label: '按部门' },
value: 2,
label: '按部门',
},
{ value: 3, label: '通知公告' }, { value: 3, label: '通知公告' },
], ],
}), }),
@@ -142,14 +139,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
component: { component: {
optionName: 'el-radio-button', optionName: 'el-radio-button',
}, },
rules: [ rules: [{ required: true, message: '必选项', trigger: ['blur', 'change'] }],
{
required: true,
message: '必选项',
// @ts-ignore
trigger: ['blur', 'change'],
},
],
}, },
}, },
target_user: { target_user: {
@@ -191,10 +181,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}), }),
rules: [ rules: [
// 表单校验规则 // 表单校验规则
{ { required: true, message: '必填项' },
required: true,
message: '必填项',
},
], ],
}, },
column: { column: {

View File

@@ -32,7 +32,7 @@
<el-col :xs="24" :sm="24" class="personal-item mb6"> <el-col :xs="24" :sm="24" class="personal-item mb6">
<div class="personal-item-label">角色</div> <div class="personal-item-label">角色</div>
<div class="personal-item-value"> <div class="personal-item-value">
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index" style="margin-right: 5px;">{{ item.name }}</el-tag> <el-tag v-for="(item, index) in state.personalForm.role_info" :key="index" style="margin-right: 5px">{{ item.name }}</el-tag>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
@@ -84,10 +84,10 @@
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20"> <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="性别"> <el-form-item label="性别">
<el-select v-model="state.personalForm.gender" placeholder="请选择性别" clearable class="w100"> <el-select v-model="state.personalForm.gender" placeholder="请选择性别" clearable class="w100">
<!-- <el-option label="男" :value="1"></el-option>--> <!-- <el-option label="男" :value="1"></el-option>-->
<!-- <el-option label="女" :value="0"></el-option>--> <!-- <el-option label="女" :value="0"></el-option>-->
<!-- <el-option label="保密" :value="2"></el-option>--> <!-- <el-option label="保密" :value="2"></el-option>-->
<el-option v-for="(item,index) in genderList" :key="index" :label="item.label" :value="item.value"></el-option> <el-option v-for="(item, index) in genderList" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -181,8 +181,8 @@ import { Session } from '/@/utils/storage';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
import { successMessage } from '/@/utils/message'; import { successMessage } from '/@/utils/message';
import {dictionary} from "/@/utils/dictionary"; import { dictionary } from '/@/utils/dictionary';
import {Md5} from "ts-md5"; import { Md5 } from 'ts-md5';
const router = useRouter(); const router = useRouter();
// 头像裁剪组件 // 头像裁剪组件
@@ -237,7 +237,7 @@ const genderList = ref();
const getUserInfo = function () { const getUserInfo = function () {
api.GetUserInfo({}).then((res: any) => { api.GetUserInfo({}).then((res: any) => {
const { data } = res; const { data } = res;
genderList.value = dictionary('gender') genderList.value = dictionary('gender');
state.personalForm.avatar = data.avatar || ''; state.personalForm.avatar = data.avatar || '';
state.personalForm.username = data.username || ''; state.personalForm.username = data.username || '';
state.personalForm.name = data.name || ''; state.personalForm.name = data.name || '';
@@ -335,10 +335,10 @@ const settingPassword = () => {
if (valid) { if (valid) {
api.UpdatePassword(userPasswordInfo).then((res: any) => { api.UpdatePassword(userPasswordInfo).then((res: any) => {
ElMessage.success('密码修改成功'); ElMessage.success('密码修改成功');
setTimeout(() => { setTimeout(() => {
Session.remove('token'); Session.remove('token');
router.push('/login'); router.push('/login');
}, 1000); }, 1000);
}); });
} else { } else {
// 校验失败 // 校验失败
@@ -369,7 +369,7 @@ const uploadImg = (data: any) => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '/@/theme/mixins/index.scss'; @use '/@/theme/mixins/index.scss' as mixins;
.personal { .personal {
.personal-user { .personal-user {
height: 130px; height: 130px;
@@ -400,7 +400,7 @@ const uploadImg = (data: any) => {
padding: 0 15px; padding: 0 15px;
.personal-title { .personal-title {
font-size: 18px; font-size: 18px;
@include text-ellipsis(1); @include mixins.text-ellipsis(1);
} }
.personal-item { .personal-item {
display: flex; display: flex;
@@ -408,10 +408,10 @@ const uploadImg = (data: any) => {
font-size: 13px; font-size: 13px;
.personal-item-label { .personal-item-label {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
@include text-ellipsis(1); @include mixins.text-ellipsis(1);
} }
.personal-item-value { .personal-item-value {
@include text-ellipsis(1); @include mixins.text-ellipsis(1);
} }
} }
} }
@@ -436,7 +436,7 @@ const uploadImg = (data: any) => {
padding-bottom: 10px; padding-bottom: 10px;
.personal-info-li-title { .personal-info-li-title {
display: inline-block; display: inline-block;
@include text-ellipsis(1); @include mixins.text-ellipsis(1);
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
text-decoration: none; text-decoration: none;
} }
@@ -518,7 +518,7 @@ const uploadImg = (data: any) => {
} }
.personal-edit-safe-item-left-value { .personal-edit-safe-item-left-value {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
@include text-ellipsis(1); @include mixins.text-ellipsis(1);
margin-right: 15px; margin-right: 15px;
} }
} }

View File

@@ -11,6 +11,7 @@
highlight-current highlight-current
show-checkbox show-checkbox
default-expand-all default-expand-all
:check-on-click-leaf="false"
> >
</el-tree> </el-tree>
</template> </template>

View File

@@ -75,7 +75,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
show: auth('role:AuthorizedAdd'), show: auth('role:SetMenu'),
click: (ctx: any) => { click: (ctx: any) => {
context!.subUserRef.value.dialog = true; context!.subUserRef.value.dialog = true;
nextTick(() => { nextTick(() => {
@@ -91,7 +91,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
//固定右侧 //固定右侧
fixed: 'left', fixed: 'left',
width: 120, width: 120,
show: auth('role:AuthorizedDel'), show: auth('role:SetMenu'),
buttons: { buttons: {
view: { view: {
show: false, show: false,
@@ -115,7 +115,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
title: "选择", title: "选择",
form: { show: false}, form: { show: false},
column: { column: {
show: auth('role:AuthorizedDel'), show: auth('role:SetMenu'),
type: "selection", type: "selection",
align: "center", align: "center",
width: "55px", width: "55px",

View File

@@ -25,7 +25,7 @@
</template> </template>
<template #pagination-left> <template #pagination-left>
<el-tooltip content="批量删除所选择的用户权限"> <el-tooltip content="批量删除所选择的用户权限">
<el-button v-show="selectedRowsCount > 0 && auth('role:AuthorizedDel')" type="danger" @click="multipleDel" :icon="Delete">批量删除</el-button> <el-button v-show="selectedRowsCount > 0 && auth('role:SetMenu')" type="danger" @click="multipleDel" :icon="Delete">批量删除</el-button>
</el-tooltip> </el-tooltip>
</template> </template>
</fs-crud> </fs-crud>

View File

@@ -48,7 +48,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: computed(() => { width: computed(() => {
if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){ if (auth('role:AllAuthorizedUser') || auth('role:AllCanMenu')){
return 420; return 420;
} }
return 320; return 320;
@@ -66,7 +66,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
assignment: { assignment: {
type: 'primary', type: 'primary',
text: '授权用户', text: '授权用户',
show: auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch'), show: auth('role:AllAuthorizedUser'),
click: (ctx: any) => { click: (ctx: any) => {
const { row } = ctx; const { row } = ctx;
context!.RoleUserDrawer.handleDrawerOpen(row); context!.RoleUserDrawer.handleDrawerOpen(row);
@@ -79,7 +79,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
permission: { permission: {
type: 'primary', type: 'primary',
text: '权限配置', text: '权限配置',
show: auth('role:Permission'), show: auth('role:SetMenu'),
click: (clickContext: any): void => { click: (clickContext: any): void => {
const { row } = clickContext; const { row } = clickContext;
context.RoleDrawer.handleDrawerOpen(row); context.RoleDrawer.handleDrawerOpen(row);
@@ -181,7 +181,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
}, },
}, },
dict: dict({ dict: dict({
value: dictionary('button_status_bool'), data: dictionary('button_status_bool'),
}), }),
}, },
}, },

View File

@@ -19,6 +19,7 @@ import { computed } from "vue";
import { Md5 } from 'ts-md5'; import { Md5 } from 'ts-md5';
import { commonCrudConfig } from "/@/utils/commonCrud"; import { commonCrudConfig } from "/@/utils/commonCrud";
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { exportData } from "./api";
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => { const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query); return await api.GetList(query);
@@ -81,16 +82,16 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
title: "导出",//鼠标停留显示的信息 title: "导出",//鼠标停留显示的信息
show: auth('user:Export'), show: auth('user:Export'),
click: (ctx: any) => ElMessageBox.confirm( click: (ctx: any) => ElMessageBox.confirm(
'确定重设密码吗?', '提示', '确定导出数据吗?', '提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(() => resetToDefaultPasswordRequest(ctx.row)) ).then(() => exportData(ctx.row))
} }
} }
}, },
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: 200, width: 220,
buttons: { buttons: {
view: { view: {
show: false, show: false,
@@ -105,19 +106,15 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
type: 'text', type: 'text',
show: auth('user:Delete'), show: auth('user:Delete'),
}, },
custom: { resetDefaultPwd: {
text: '重密码', text: '重密码',
type: 'text', type: 'text',
show: auth('user:ResetPassword'), iconRight: 'Setting',
tooltip: { show: auth('user:ResetDefaultPassword'),
placement: 'top', click: (ctx: any) => ElMessageBox.confirm(
content: '重设密码', '确定重置为系统默认密码吗?', '提示',
}, { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
//@ts-ignore ).then(() => resetToDefaultPasswordRequest(ctx.row))
click: (ctx: any) => {
const { row } = ctx;
resetToDefaultPasswordRequest(row)
},
}, },
}, },
}, },
@@ -208,10 +205,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
}, },
}, },
dept: { dept: {
title: '部门', title: '所属部门',
search: {
disabled: true,
},
type: 'dict-tree', type: 'dict-tree',
dict: dict({ dict: dict({
isTree: true, isTree: true,
@@ -220,7 +214,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
label: 'name' label: 'name'
}), }),
column: { column: {
minWidth: 200, //最小列宽 minWidth: 300, //最小列宽
formatter({ value, row, index }) { formatter({ value, row, index }) {
return row.dept_name_all return row.dept_name_all
} }
@@ -246,6 +240,39 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
}, },
}, },
}, },
manage_dept: {
title: '管理部门',
type: 'dict-tree',
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name'
}),
column: {
minWidth: 300
},
form: {
value: [],
component: {
filterable: true,
multiple: true,
placeholder: '请选择',
clearable: true,
collapseTags: true,
maxCollapseTags: 2,
collapseTagsTooltip: true,
props: {
checkStrictly: true,
props: {
value: 'id',
label: 'name',
},
},
},
helper: '不选则默认为所属部门',
},
},
role: { role: {
title: '角色', title: '角色',
search: { search: {
@@ -381,6 +408,9 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
dict: dict({ dict: dict({
data: dictionary('button_status_bool'), data: dictionary('button_status_bool'),
}), }),
form: {
value: true
}
}, },
avatar: { avatar: {
title: '头像', title: '头像',
@@ -395,8 +425,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
}, },
...commonCrudConfig({ ...commonCrudConfig({
dept_belong_id: { dept_belong_id: {
form: true, form: false,
table: true table: false
} }
}) })
}, },

View File

@@ -2,7 +2,7 @@ import vue from '@vitejs/plugin-vue';
import { resolve } from 'path'; import { resolve } from 'path';
import { defineConfig, loadEnv, ConfigEnv } from 'vite'; import { defineConfig, loadEnv, ConfigEnv } from 'vite';
import vueSetupExtend from 'vite-plugin-vue-setup-extend'; import vueSetupExtend from 'vite-plugin-vue-setup-extend';
import vueJsx from '@vitejs/plugin-vue-jsx' // import vueJsx from '@vitejs/plugin-vue-jsx'
import { generateVersionFile } from "/@/utils/upgrade"; import { generateVersionFile } from "/@/utils/upgrade";
const pathResolve = (dir: string) => { const pathResolve = (dir: string) => {
@@ -22,7 +22,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
// 当Vite构建时生成版本文件 // 当Vite构建时生成版本文件
generateVersionFile() generateVersionFile()
return { return {
plugins: [vue(), vueJsx(), vueSetupExtend()], plugins: [vue(), /* vueJsx(), */ vueSetupExtend()],
root: process.cwd(), root: process.cwd(),
resolve: { alias }, resolve: { alias },
base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH, base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,