123 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
liqiang
b4ffb2105f Merge remote-tracking branch 'origin/develop' into develop 2025-05-06 10:06:22 +08:00
1638245306
3398aa3ba9 build(flowH5): 更新库入口文件路径和外部依赖
- 修改入口文件路径为 src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts
- 添加 xe-utils 到外部依赖列表
2025-05-01 00:04:01 +08:00
1638245306
94149161b3 build(flowH5): 添加 flowH5 项目的配置文件
- 新增 flowH5.config.ts 文件,配置 Vite 构建项目- 设置 Vue 插件和路径别名
- 配置 Roll
2025-04-30 12:55:58 +08:00
liqiang
1907f1ac0a Merge remote-tracking branch 'origin/develop' into develop 2025-04-30 10:35:52 +08:00
1638245306
bda002398c refactor: 修改 API 文档权限为仅允许认证用户访问
- 将 API 文档的权限设置从 AllowAny 更改为 IsAuthenticated- 确保只有经过身份验证的用户才能访问 API 文档
2025-04-29 14:45:45 +08:00
阿辉
66c28bd389 日历选择器组件 2025-04-07 15:20:52 +08:00
1638245306
8eacc4aad3 feat(system): 角色模型添加工作流支持
- 引入 FlowBaseModel 以支持工作流功能
- 使 Role 模型继承 CoreModel 和 FlowBaseModel
2025-03-28 18:24:13 +08:00
1638245306
7a152d3591 feat(system): 添加获取用户递归部门名称功能
- 在 Department模型中添加了 _recursion 类方法,用于递归获取指定属性值
- 新增 get_region_name 类方法,用于获取用户的所有上级部门名称
- 该功能可以用于显示用户所在的完整部门路径
2025-03-28 15:08:56 +08:00
1638245306
4b05a28f4c fix(system): 修改用户密码设置逻辑
- 移除了对新密码进行 MD5 加密的步骤- 直接使用明文密码进行加密存储
- 保留了密码修改次数的计数功能
2025-03-28 14:44:39 +08:00
1638245306
0392b3b101 feat(plugins): 动态加载插件
- 新增插件动态加载逻辑,遍历已发现的插件列表
- 对于每个插件,尝试导入其 index.ts 文件
- 导入成功后,将插件注册到应用中,并打印加载信息
-导入失败时,打印错误信息,提示插件下无 index.ts 文件
2025-03-28 13:39:44 +08:00
liqiang
fad2cb2e18 README.zh.md更新 2025-03-21 04:42:46 +08:00
liqiang
df0b78cafc README.zh.md更新 2025-03-21 04:41:36 +08:00
liqiang
c69ea7b33e Merge remote-tracking branch 'origin/develop' into develop 2025-03-21 04:34:30 +08:00
liqiang
ce5c4c9d8d README.zh.md更新 2025-03-21 04:33:45 +08:00
liqiang
3f7aaa0228 Merge remote-tracking branch 'origin/develop' into develop 2025-03-21 04:09:33 +08:00
liqiang
e37909d478 feat(core): 新增核心工具模块并优化通知功能
- 新增 cores.tsx 文件,实现核心工具模块
- 添加任务列表和事件总线功能
- 实现系统通知和任务处理逻辑
- 在 App.vue 中集成新功能
- 优化通知显示逻辑,支持不同内容类型的通知
2025-03-21 04:09:26 +08:00
dvadmin
1578c9d710 !108 修复字段权限筛选错误,update backend/dvadmin/utils/viewset.py.
Merge pull request !108 from lxy/N/A
2025-03-20 19:54:37 +00:00
dvadmin
01604a27db !109 修复无法下载问题 update web/src/views/system/downloadCenter/crud.tsx.
Merge pull request !109 from lxy/N/A
2025-03-20 19:53:57 +00:00
dvadmin
58036dbeb9 !110 修复文件管理点击地址无法下载问题 update web/src/views/system/fileList/crud.tsx.
Merge pull request !110 from lxy/N/A
2025-03-20 19:53:34 +00:00
lxy
7917a118c8 修复文件管理点击地址无法下载问题 update web/src/views/system/fileList/crud.tsx.
Signed-off-by: lxy <46486798@qq.com>
2025-03-20 11:33:01 +00:00
lxy
86c202b94a 修复无法下载问题 update web/src/views/system/downloadCenter/crud.tsx.
Signed-off-by: lxy <46486798@qq.com>
2025-03-20 11:28:09 +00:00
lxy
a5cc87eb55 修复字段权限筛选错误,update backend/dvadmin/utils/viewset.py.
解决不是超级管理员用户加载报错,匿名用户没有角色报错

Signed-off-by: lxy <46486798@qq.com>
2025-03-20 06:47:21 +00:00
1638245306
2fabc210b2 refactor(system): 恢复密码加密逻辑
- 将明文密码修改为经过 MD5 加密的密码
- 保留了原有的密码加密方式,提高了系统安全性
2025-03-19 15:54:58 +08:00
1638245306
4c644ae8cb build(development): 修改开发环境 API 地址并添加新的构建脚本
- 将开发环境 API 地址从 http://127.0.0.1:8000 改为 http://127.0.0.1:8001- 在 package.json 中添加了 build:dev 脚本,用于开发环境的构建
2025-03-17 14:40:12 +08:00
liqiang
351bae1fea build(.env): 修改 API 请求基础 URL
- 将 VITE_API_URL 从 'http://dvadmin3api.django.icu:8001' 修改为 'http://127.0.0.1:8000'- 此修改便于本地开发和测试
2025-03-15 14:52:29 +08:00
1638245306
bc7bc3cda6 feat(system): 优化配置页面表单组件
- 在 crudTable组件中添加 afterSubmit 钩子,更新 modelValue
-重构 formContent 组件中的 formData,使用 ref 替代 reactive
- 优化 formContent 组件的提交逻辑,简化代码结构
2025-03-14 17:33:51 +08:00
liqiang
aea5ca938b Merge branch 'develop' of gitee.com:huge-dream/django-vue3-admin into develop 2025-03-14 16:59:46 +08:00
1638245306
8706de5ef4 feat(system): 优化配置组件的样式和功能
- 调整表单布局,设置宽度为 500px,栏数为 24
- 在标题、键名和键值字段的表单中添加占位符
2025-03-14 16:42:04 +08:00
dvadmin
ed96740ab1 !106 完善初始化配置导出
Merge pull request !106 from lorenzo/dev
2025-03-13 10:32:43 +00:00
dvadmin
f719241938 !105 feat: 从表结构生成前端基础文件
Merge pull request !105 from lorenzo/master
2025-03-13 10:31:04 +00:00
1638245306
ac22e47883 docs(README): 添加给框架点赞二维码图片
- 在 README.md 中添加了支付宝和微信的二维码图片链接
- 通过此修改,方便用户扫描二维码给框架进行赞赏
2025-03-13 17:49:35 +08:00
1638245306
7555ba1707 Merge remote-tracking branch 'origin/develop' into develop 2025-03-13 17:40:39 +08:00
1638245306
341ea62412 feat(system): 优化配置管理数组展示方式
- 新增 crudTable 组件用于数组数据的展示和编辑
- 替换原有的 vxe-table 实现,简化代码结构
- 优化数组类型表单项的渲染逻辑
- 调整环境变量中的 API 地址
2025-03-13 17:40:30 +08:00
liqiang
2ab80758f0 fix(permission): 修复权限获取 bug
- 将 MenuButton 查询结果改为 values_list,获取多个匹配项的 ID
- 更新 RoleMenuButtonPermission 查询条件,支持多个菜单按钮 ID
2025-03-02 01:00:43 +08:00
liqiang
a030409c46 fix(system): 修复权限获取 bug
- 优化权限查询逻辑,先判断菜单按钮对象是否存在
- 修复了在某些情况下可能导致权限获取失败的问题
2025-03-02 00:05:44 +08:00
liqiang
036d8da9b6 build(web): 更新基础镜像版本
- 将 dvadmin3-base-web 镜像版本从16.19-alpine升级到 18.20-alpine
2025-03-02 00:04:42 +08:00
阿辉
7fb219bb31 文件选择器修复清空时会报错null的bug 2025-02-28 18:42:28 +08:00
阿辉
b10c3ebdc0 文件选择器修复windows下文件路径反斜杠问题 2025-02-24 10:26:49 +08:00
阿辉
778401dd2d Merge branch 'develop' of https://e.coding.net/dvadmin-private/code/dvadmin3 into develop 2025-02-21 17:11:44 +08:00
阿辉
418c78fa83 文件选择器多选图片情况下无法删除图片 2025-02-21 17:11:41 +08:00
1638245306
48d3d86017 Merge remote-tracking branch 'origin/develop' into develop 2025-02-09 23:17:35 +08:00
1638245306
c6a2073537 feat(utils): 完善字段权限控制并添加角色过滤
- 添加了对普通用户进行字段权限过滤的逻辑
- 使用 deepcopy 复制 serializer_class.Meta 以避免直接修改原类
- 修改 get_menu_field 方法,根据用户角色过滤字段权限
2025-02-09 23:17:26 +08:00
7emotions
78ec0ce069 fix: 修复导出Users配置缺失的role与dept 2025-02-06 20:20:12 +08:00
7emotions
857d2940f8 fix: 修复RoleMenuButtonInitSerializer重置逻辑 2025-02-06 19:56:20 +08:00
7emotions
82e689fd83 feat: 导出角色权限配置与按钮权限 2025-02-06 19:21:14 +08:00
liqiang
0cda4c04f7 默认添加dvadmin3-celery 2025-02-06 16:37:52 +08:00
7emotions
91742ee27b feat: 从表结构生成前端基础文件 2025-02-04 00:10:46 +08:00
liqiang
3c6afdac76 支持npm 安装的@great-dream插件动态菜单 2025-02-02 09:04:13 +08:00
liqiang
f299889c4a 支持npm 安装的@great-dream插件动态菜单 2025-02-01 23:29:52 +08:00
liqiang
c82fcbb468 celery 优化 2025-02-01 18:29:51 +08:00
liqiang
eaf5fdcc55 框架优化 2025-02-01 18:29:43 +08:00
liqiang
40b7bd2c94 组件更新 2025-01-30 12:55:58 +08:00
liqiang
868621a3f1 Merge branch 'master' into develop 2025-01-30 12:47:26 +08:00
dvadmin
eefc43d863 !95 角色授权增加默认接口权限
1.授权时带入默认权限
2.查看权限,如果单接口权限与默认接口权限一致,显示默认接口权限
3.复选框等宽显示
2025-01-30 04:44:58 +00:00
dvadmin
8b21a69a15 !104 feat: 优化docker部署
Merge pull request !104 from lorenzo/master
2025-01-30 04:44:28 +00:00
dvadmin
0964c6300a !103 [fix]修复bug
[fix]修复更新时间晒选取值错误bug
[fix]优化commonCrudConfig写法,兼容ts语法,修复使用commonCrudConfig IDE提示飘红的问题&增加width可自定义功能
[fix]修复版本号生成可能为undefind的bug&开发环境版本不进行版本校验代码不生效bug
2025-01-30 04:43:07 +00:00
dvadmin
8c2db4f5ff !101 增加角色批量授权用户
Merge pull request !101 from lxy/cherry-pick-1736306630
2025-01-30 04:42:23 +00:00
liqiang
e15a49a2bd 支持外链接 2025-01-30 12:40:30 +08:00
lorenzo
ecfad1952a feat: 优化docker部署
Signed-off-by: lorenzo <paradise_c@qq.com>
2025-01-27 15:57:12 +00:00
lorenzo
92b8cde7ae feat: 优化docker部署
Signed-off-by: lorenzo <paradise_c@qq.com>
2025-01-27 15:54:13 +00:00
vFeng
6fbe3a214d [fix]注释无效接口dept_lazy_tree
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 09:01:17 +00:00
vFeng
4695066e11 [fix]修复开发环境无需校验前端版本bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 06:33:37 +00:00
vFeng
d125eac5a6 [fix]修复版本号生成可能为undefind的bug&开发环境版本不进行版本校验代码不生效bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 06:22:21 +00:00
vFeng
83d6565cad [fix]优化commonCrudConfig写法,兼容ts语法,修复使用commonCrudConfig IDE提示飘红的问题&增加width可自定义功能
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 05:33:54 +00:00
vFeng
1e2a9a652e [fix]修复更新时间晒选取值错误bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 05:25:49 +00:00
阿辉
c781d1f559 文件选择器支持image类型的多选 2025-01-22 15:53:01 +08:00
lxy
ffc6246105 增加角色批量授权用户
(cherry picked commit from <gitee.com//lxy0722/django-vue3-admin/commit/702eb7d7aeb2a9c30416a8ebb503a4b8e3367492>
2025-01-08 03:23:50 +00:00
1638245306
77bfc87679 Merge remote-tracking branch 'origin/develop' into develop 2025-01-07 21:38:22 +08:00
1638245306
6ab0c3e758 refactor: 修复主键列表字段的 Swagger 文档生成
- 更新 keys 变量定义,使用 Schema 嵌套来正确表示主键列表的类型
-优化 Swagger 文档中的请求体定义,提高 API 文档的准确性和可读性
2025-01-07 20:06:24 +08:00
lxy
9d230e4752 角色授权增加默认设置
(cherry picked commit from <gitee.com//huge-dream/django-vue3-admin/commit/06a772fcd5335c272159d9c45590cf8b559210d1>
2025-01-02 06:28:06 +00:00
128 changed files with 5268 additions and 1620 deletions

4
.gitignore vendored
View File

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

View File

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

1
backend/.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -399,12 +399,16 @@ DICTIONARY_CONFIG = {}
# ================================================= #
# 租户共享app
TENANT_SHARED_APPS = []
# 普通租户独有app
TENANT_EXCLUSIVE_APPS = []
# 插件 urlpatterns
PLUGINS_URL_PATTERNS = []
# 所有模式有的
SHARED_APPS = []
# ********** 一键导入插件配置开始 **********
# 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心
# from dvadmin3_celery.settings import * # celery 异步任务
from dvadmin3_celery.settings import * # celery 异步任务
# from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.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.urls import path, include, re_path
from django.views.static import serve
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
@@ -24,6 +25,7 @@ from rest_framework_simplejwt.views import (
from application import dispatch
from application import settings
from application.sse_views import sse_view
from dvadmin.system.views.dictionary import InitDictionaryViewSet
from dvadmin.system.views.login import (
LoginView,
@@ -40,6 +42,7 @@ dispatch.init_system_config()
dispatch.init_dictionary()
# =========== 初始化系统配置 =================
permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ]
schema_view = get_schema_view(
openapi.Info(
title="Snippets API",
@@ -50,7 +53,7 @@ schema_view = get_schema_view(
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
permission_classes=permission_classes,
generator_class=CustomOpenAPISchemaGenerator,
)
# 前端页面映射
@@ -115,6 +118,8 @@ urlpatterns = (
# 前端页面映射
path('web/', web_view, name='web_view'),
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.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",
"json": {**message, 'unread': unread_count}
}
)
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -546,5 +546,50 @@
"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,
"cache": false,
"visible": true,
"parent": null,
"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": "用户管理",
"icon": "iconfont icon-icon-",
"sort": 6,
"sort": 1,
"is_link": false,
"is_catalog": false,
"web_path": "/user",
@@ -339,7 +24,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
@@ -348,18 +32,24 @@
"api": "/api/system/user/",
"method": 0
},
{
"name": "详情",
"value": "user:Retrieve",
"api": "/api/system/user/{id}/",
"method": 0
},
{
"name": "新增",
"value": "user:Create",
"api": "/api/system/user/",
"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": "导出",
"value": "user:Export",
@@ -373,10 +63,16 @@
"method": 1
},
{
"name": "编辑",
"value": "user:Update",
"api": "/api/system/user/{id}/",
"method": 2
"name": "获取导入模板",
"value": "user:ImportTemplate",
"api": "/api/system/user/import/",
"method": 0
},
{
"name": "批量更新模板",
"value": "user:BatchUpdateTemplate",
"api": "/api/system/user/update_template/",
"method": 0
},
{
"name": "重设密码",
@@ -386,15 +82,9 @@
},
{
"name": "重置密码",
"value": "user:DefaultPassword",
"value": "user:ResetDefaultPassword",
"api": "/api/system/user/{id}/reset_to_default_password/",
"method": 2
},
{
"name": "删除",
"value": "user:Delete",
"api": "/api/system/user/{id}/",
"method": 3
}
],
"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": "消息中心",
"icon": "iconfont icon-xiaoxizhongxin",
@@ -690,35 +733,11 @@
"menu_button": [
{
"name": "查询",
"value": "Search",
"api": "/api/system/downloadCenter/",
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/downloadCenter/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/downloadCenter/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/downloadCenter/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/downloadCenter/{id}/",
"method": 3
"value": "downloadCenter:Search",
"api": "/api/system/download_center/"
}
]
],
"menu_field": []
}
],
"menu_button": [],

View File

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

View File

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

View File

@@ -63,6 +63,8 @@ class Users(CoreModel, AbstractUser):
help_text="关联岗位")
role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
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(
to="Dept",
verbose_name="所属部门",
@@ -72,12 +74,26 @@ class Users(CoreModel, AbstractUser):
blank=True,
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="登录错误次数")
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
objects = CustomUserManager()
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:
db_table = table_prefix + "system_users"
@@ -122,6 +138,27 @@ class Dept(CoreModel):
help_text="上级部门",
)
@classmethod
def _recursion(cls, instance, parent, result):
new_instance = getattr(instance, parent, None)
res = []
data = getattr(instance, result, None)
if data:
res.append(data)
if new_instance:
array = cls._recursion(new_instance, parent, result)
res += array
return res
@classmethod
def get_region_name(cls, obj):
"""
获取某个用户的递归所有部门名称
"""
dept_name_all = cls._recursion(obj, "parent", "name")
dept_name_all.reverse()
return "/".join(dept_name_all)
@classmethod
def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
"""
@@ -141,6 +178,23 @@ class Dept(CoreModel):
cls.recursion_all_dept(ele.get("id"), dept_all_list, 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:
db_table = table_prefix + "system_dept"
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()
detail_init_complete = Signal()
@@ -10,3 +16,12 @@ post_tenants_init_complete = Signal()
post_tenants_all_init_complete = Signal()
# 租户创建完成信号
tenants_create_complete = Signal()
# 全局变量用于标记最后修改时间
last_db_change_time = time.time()
@receiver(post_save, sender=MessageCenterTargetUser)
@receiver(post_delete, sender=MessageCenterTargetUser)
def update_last_change_time(sender, **kwargs):
cache.set('last_db_change_time', time.time(), timeout=None) # 设置永不超时的键值对

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ from django.conf import settings
from django.db import connection
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from application import dispatch
from dvadmin.system.models import FileList
@@ -35,8 +36,8 @@ class FileSerializer(CustomModelSerializer):
fields = "__all__"
def create(self, validated_data):
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("file_storage.file_backup")
file = self.initial_data.get('file')
file_size = file.size
validated_data['name'] = str(file)
@@ -52,15 +53,15 @@ class FileSerializer(CustomModelSerializer):
if file_backup:
validated_data['url'] = file
if file_engine == 'oss':
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
file_path = ali_oss_upload(file)
from dvadmin.utils.aliyunoss import ali_oss_upload
file_path = ali_oss_upload(file, file_name=validated_data['name'])
if file_path:
validated_data['file_url'] = file_path
else:
raise ValueError("上传失败")
elif file_engine == 'cos':
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload
file_path = tencent_cos_upload(file)
from dvadmin.utils.tencentcos import tencent_cos_upload
file_path = tencent_cos_upload(file, file_name=validated_data['name'])
if file_path:
validated_data['file_url'] = file_path
else:
@@ -106,7 +107,7 @@ class FileViewSet(CustomModelViewSet):
queryset = FileList.objects.all()
serializer_class = FileSerializer
filter_class = FileFilter
permission_classes = []
permission_classes = [IsAuthenticated]
@action(methods=['GET'], detail=False)
def get_all(self, request):

View File

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

View File

@@ -10,16 +10,17 @@ from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept
from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer):
@@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
"""
角色管理接口
@@ -141,4 +141,63 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
# right : 添加用户权限
role.users_set.add(*movedKeys)
serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功")
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
def get_role_users(self, request):
"""
获取角色已授权、未授权的用户
已授权的用户:1
未授权的用户:0
"""
role_id = request.query_params.get('role_id', None)
if not role_id:
return ErrorResponse(msg="请选择角色")
if request.query_params.get('authorized', 0) == "1":
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
else:
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
if name := request.query_params.get('name', None):
queryset = queryset.filter(name__icontains=name)
if dept := request.query_params.get('dept', None):
queryset = queryset.filter(dept=dept)
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
if page is not None:
return self.get_paginated_response(page)
return SuccessResponse(data=page)
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def remove_role_user(self, request, pk):
"""
角色-删除用户
"""
user_id = request.data.get('user_id', None)
if not user_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.remove(*user_id)
return SuccessResponse(msg="删除成功")
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def add_role_users(self, request, pk):
"""
角色-添加用户
"""
users_id = request.data.get('users_id', None)
if not users_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.add(*users_id)
return DetailResponse(msg="添加成功")

View File

@@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None)
btnId = data.get('btnId', None)
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
dept = data.get('dept', None) or [] # 默认空部门
if isCheck:
# 添加权限:创建关联记录
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId)
instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
menu_button_id=btnId,
data_range=data_range)
# 自定义部门权限
if data_range == 4 and dept:
instance.dept.set(dept)
else:
# 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()

View File

@@ -90,6 +90,8 @@ class UserCreateSerializer(CustomModelSerializer):
data = super().save(**kwargs)
data.dept_belong_id = data.dept_id
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", []))
return data
@@ -127,6 +129,8 @@ class UserUpdateSerializer(CustomModelSerializer):
data = super().save(**kwargs)
data.dept_belong_id = data.dept_id
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", []))
return data
@@ -336,7 +340,7 @@ class UserViewSet(CustomModelViewSet):
verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_password:
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.password = make_password(new_pwd)
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.pwd_change_count += 1
request.user.save()
return DetailResponse(data=None, msg="修改成功")
@@ -352,7 +356,7 @@ class UserViewSet(CustomModelViewSet):
if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配")
else:
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.password = make_password(new_pwd)
request.user.pwd_change_count += 1
request.user.save()
return DetailResponse(data=None, msg="修改成功")
@@ -426,12 +430,9 @@ class UserViewSet(CustomModelViewSet):
queryset = self.filter_queryset(self.get_queryset())
else:
queryset = self.filter_queryset(self.get_queryset())
# print(queryset.values('id','name','dept__id'))
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
# print(serializer.data)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")

View File

@@ -0,0 +1,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.models import Q, F
from django.db.models.constants import LOOKUP_SEP
from django_filters import utils, FilterSet
from django_filters.constants import ALL_FIELDS
from django_filters.filters import CharFilter, DateTimeFromToRangeFilter
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 django_filters.conf import settings
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
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):
"""
@@ -33,7 +34,7 @@ class CoreModelFilterBankend(BaseFilterBackend):
create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', None)
update_datetime_after = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_before', None)
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q()
if create_datetime_after and create_datetime_before:
@@ -149,13 +150,16 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True)
role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
menu_button__api=re_api,
menu_button__method=method).values(
'data_range'
)
# 修复权限获取bug
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
role_permission_list = []
if menu_button_ids:
role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
menu_button_id__in=menu_button_ids).values(
'data_range'
)
dataScope_list = [] # 权限范围列表
for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
@@ -197,6 +201,86 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
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):
lookup_prefixes = {
"^": "istartswith",
@@ -237,14 +321,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# TODO: remove assertion in 2.1
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__
)
filterset_class = getattr(view, "filter_class", None)
# TODO: remove assertion in 2.1
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__
)
self.filter_fields = getattr(view, "filter_fields", None)
@@ -424,5 +508,5 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
return queryset
if not filterset.is_valid() and self.raise_exception:
raise utils.translate_validation(filterset.errors)
raise translate_validation(filterset.errors)
return filterset.qs

View File

@@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model):
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):
"""
核心标准抽象模型模型,可直接继承使用
@@ -98,7 +118,8 @@ class CoreModel(models.Model):
verbose_name="修改时间")
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间")
objects = CoreModelManager()
all_objects = models.Manager()
class Meta:
abstract = True
verbose_name = '核心模型'

View File

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

View File

@@ -6,6 +6,8 @@
@Created on: 2021/6/1 001 22:57
@Remark: 自定义视图集
"""
import copy
from django.db import transaction
from django_filters import DateTimeFromToRangeFilter
from django_filters.rest_framework import FilterSet
@@ -14,7 +16,7 @@ from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
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.json_response import SuccessResponse, ErrorResponse, DetailResponse
from dvadmin.utils.permission import CustomPermission
@@ -39,7 +41,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
update_serializer_class = None
filter_fields = '__all__'
search_fields = ()
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter]
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionMargeFilter]
permission_classes = [CustomPermission]
import_field_dict = {}
export_field_label = {}
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准
can_see = self.get_menu_field(serializer_class)
# 排除掉序列化器级的字段
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in sub_set:
# serializer_class._declared_fields.pop(field)
# 排除掉序列化器级的字段(排除字段权限中未授权的字段)
# if not self.request.user.is_superuser:
# serializer_class.Meta.fields = can_see
# exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in exclude_set:
# serializer_class._declared_fields.pop(field)
# meta = copy.deepcopy(serializer_class.Meta)
# meta.fields = list(can_see)
# serializer_class.Meta = meta
# 在分页器中使用
self.request.permission_fields = can_see
if isinstance(self.request.data, list):
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
def get_menu_field(self, serializer_class):
"""获取字段权限"""
finded = False
for model in get_custom_app_models():
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded is False:
if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
return []
return MenuField.objects.filter(model=model['model']
).values('field_name', 'title')
# 匿名用户没有角色
ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__)
if hasattr(self.request.user, 'role'):
roles = self.request.user.role.values_list('id', flat=True)
ret = ret.filter(is_query=True, role__in=roles)
return ret.values_list('field__field_name', flat=True)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request)
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
instance.delete()
return DetailResponse(data=[], msg="删除成功")
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING)
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING))
@swagger_auto_schema(request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['keys'],
@@ -147,3 +152,13 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
return SuccessResponse(data=[], msg="删除成功")
else:
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,25 +7,27 @@ djangorestframework==3.15.2
django-restql==0.15.4
django-simple-captcha==0.6.0
django-timezone-field==7.0
djangorestframework-simplejwt==5.3.1
djangorestframework_simplejwt==5.4.0
drf-yasg==1.21.7
mysqlclient==2.2.0
pypinyin==0.51.0
ua-parser==0.18.0
pyparsing==3.1.2
openpyxl==3.1.5
requests==2.32.3
requests==2.32.4
typing-extensions==4.12.2
tzlocal==5.2
channels==4.1.0
channels-redis==4.2.0
websockets==11.0.3
user-agents==2.2.0
six==1.16.0
whitenoise==6.7.0
psycopg2==2.9.9
uvicorn==0.30.3
gunicorn==22.0.0
gunicorn==23.0.0
gevent==24.2.1
Pillow==10.4.0
pyinstaller==6.9.0
pyinstaller==6.9.0
dvadmin3-celery==3.1.6
oss2==2.19.1
cos-python-sdk-v5==1.9.37

View File

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

87
crud-gen.sh Normal file
View File

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

View File

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

58
init.sh
View File

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

View File

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

View File

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

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

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

View File

@@ -1,13 +1,15 @@
{
"name": "django-vue3-admin",
"version": "3.0.4",
"version": "3.2.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT",
"scripts": {
"dev": "vite --force",
"build:dev": "vite build --mode development",
"build": "vite build",
"build:local": "vite build --mode local_prod",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/",
"build:flowH5": "vite build --config flowH5.config.ts"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
@@ -15,7 +17,9 @@
"@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2",
"@meetjs/vant4-kit": "^1.0.1",
"@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@wangeditor/editor": "^5.1.23",
@@ -24,6 +28,7 @@
"axios": "^1.7.4",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.2",
"date-holidays": "^3.24.1",
"e-icon-picker": "2.1.1",
"echarts": "^5.5.1",
"echarts-gl": "^2.0.9",
@@ -34,7 +39,9 @@
"js-cookie": "^3.0.5",
"js-table2excel": "^1.1.2",
"jsplumb": "^2.15.6",
"less": "^4.3.0",
"lodash-es": "^4.17.21",
"lunar-javascript": "^1.7.1",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.0.28",
@@ -43,17 +50,23 @@
"print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.0",
"splitpanes": "^3.1.5",
"tailwindcss": "^3.2.7",
"ts-md5": "^1.3.1",
"upgrade": "^1.1.0",
"vant": "^4.9.19",
"vant4-kit": "^1.0.3",
"vue": "^3.4.38",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8",
"vue-draggable-plus": "^0.6.0",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.14.0",
"vue-qr": "^4.0.9",
"vue-router": "^4.4.3",
"vxe-table": "^4.6.18",
"xe-utils": "^3.5.30"

View File

@@ -11,7 +11,7 @@
<script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
@@ -20,13 +20,15 @@ import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont';
import websocket from '/@/utils/websocket';
// 引入组件
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
import { ElMessageBox, ElNotification, NotificationHandle } from 'element-plus';
import { useCore } from '/@/utils/cores';
// 定义变量内容
const { messages, locale } = useI18n();
const setingsRef = ref();
@@ -34,8 +36,8 @@ const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket';
import { ElNotification } from 'element-plus';
const core = useCore();
const router = useRouter();
// 获取版本号
const getVersion = computed(() => {
let isVersion = false;
@@ -67,7 +69,15 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer();
});
// 设置皮肤缓存版本,每次更新版本可以所有用户清空缓存
const themeConfigVersion = '1.0.0'
// 获取缓存中的布局配置
if (Local.get('themeConfigVersion') !== themeConfigVersion) {
Local.clear();
Local.set('themeConfigVersion', themeConfigVersion);
window.location.reload();
return
}
if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle');
@@ -84,43 +94,61 @@ onUnmounted(() => {
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
other.useFavicon();
if (!websocket.websocket) {
//websockt 模块
try {
websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
() => route.path,
() => {
other.useTitle();
other.useFavicon();
if (!websocket.websocket) {
//websockt 模块
try {
websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
}
},
{
deep: true,
}
},
{
deep: true,
}
);
// websocket相关代码
import { messageCenterStore } from '/@/stores/messageCenter';
const wsReceive = (message: any) => {
const data = JSON.parse(message.data);
const { unread } = data;
const messageCenter = messageCenterStore();
messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') {
ElNotification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 5000,
});
}
const data = JSON.parse(message.data);
const { unread } = data;
const messageCenter = messageCenterStore();
messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') {
ElNotification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 5000,
});
} else if (data.contentType === 'Content') {
ElMessageBox.confirm(data.content, data.notificationTitle, {
confirmButtonText: data.notificationButton,
dangerouslyUseHTMLString: true,
cancelButtonText: '关闭',
type: 'info',
closeOnClickModal: false,
}).then(() => {
ElMessageBox.close();
const path = data.path;
if (route.path === path) {
core.bus.emit('onNewTask', { name: 'onNewTask' });
} else {
router.push({ path});
}
})
.catch(() => {});
}
};
onBeforeUnmount(() => {
// 关闭连接
websocket.close();
// 关闭连接
websocket.close();
});
</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,6 +1,6 @@
<template>
<div class="user-info-head" @click="editCropper()">
<el-avatar :size="100" :src="options.img" />
<el-avatar :size="100" :src="getBaseURL(options.img)" />
<el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col class="flex justify-center">
@@ -50,10 +50,11 @@ import { VueCropper } from 'vue-cropper';
import { useUserInfo } from '/@/stores/userInfo';
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
import { base64ToFile } from '/@/utils/tools';
import headerImage from "/@/assets/img/headerImage.png";
import {getBaseURL} from "/@/utils/baseUrl";
const userStore = useUserInfo();
const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false);
const title = ref('修改头像');
const emit = defineEmits(['uploadImg']);
@@ -75,7 +76,7 @@ const dialogVisiable = computed({
//图片裁剪数据
const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址
img: userStore.userInfos.avatar || headerImage, // 裁剪图片的地址
fileName: '',
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
@@ -165,6 +166,7 @@ const updateAvatar = (img) => {
defineExpose({
updateAvatar,
editCropper
});
</script>
@@ -172,7 +174,6 @@ defineExpose({
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<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 class="layout-logo-size" v-else @click="onThemeConfigChange">
<img :src="siteLogo" class="layout-logo-size-img" />

View File

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

View File

@@ -1,5 +1,10 @@
<template>
<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">
<div class="layout-navbars-breadcrumb-user-icon">
<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
@@ -28,16 +33,12 @@
</el-dropdown-menu>
</template>
</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">
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<el-popover placement="bottom" trigger="hover" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')">
@@ -58,32 +59,31 @@
></i>
</div>
<div>
<span v-if="!isSocketOpen">
<span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm
width="250"
ref="onlinePopoverRef"
:confirm-button-text="$t('message.user.retry')"
:icon="InfoFilled"
trigger="hover"
icon-color="#626AEF"
:title="$t('message.user.onlinePrompt')"
@confirm="onlineConfirmEvent"
width="250"
ref="onlinePopoverRef"
:confirm-button-text="$t('message.user.retry')"
:icon="InfoFilled"
trigger="hover"
icon-color="#626AEF"
:title="$t('message.user.onlinePrompt')"
@confirm="onlineConfirmEvent"
>
<template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
<img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge>
</template>
</el-popconfirm>
</span>
</div>
<div></div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link">
<span v-if="isSocketOpen">
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge>
</span>
<!-- <el-badge is-dot class="item online-status">-->
<!-- <img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />-->
<!-- </el-badge>-->
{{ userInfos.username === '' ? 'common' : userInfos.username }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
@@ -93,7 +93,7 @@
<el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item command="/versionUpgradeLog">更新日志</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -115,8 +115,8 @@ import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage';
import headerImage from '/@/assets/img/headerImage.png';
import { InfoFilled } from '@element-plus/icons-vue';
import websocket from '/@/utils/websocket';
import { InfoFilled } from '@element-plus/icons-vue'
// 引入组件
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
@@ -152,13 +152,14 @@ const { isSocketOpen } = storeToRefs(useUserInfo());
const onlinePopoverRef = ref()
const onlineConfirmEvent = () => {
if (!isSocketOpen.value) {
websocket.is_reonnect = true
websocket.reconnect_current = 1
websocket.reconnect()
websocket.is_reonnect = true
websocket.reconnect_current = 1
websocket.reconnect()
}
// 手动隐藏弹出
unref(onlinePopoverRef).popperRef?.delayHide?.()
}
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
@@ -237,8 +238,10 @@ const onLanguageChange = (lang: string) => {
initI18nOrSize('globalI18n', 'disabledI18n');
};
// 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => {
state[attr] = Local.get('themeConfig')[value];
const initI18nOrSize = (value: string, attr: keyof typeof state) => {
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(() => {
@@ -246,11 +249,41 @@ onMounted(() => {
initI18nOrSize('globalComponentSize', 'disabledSize');
initI18nOrSize('globalI18n', 'disabledI18n');
}
getMessageCenterCount();
});
//消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter';
import { getBaseURL } from '/@/utils/baseUrl';
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>
<style scoped lang="scss">
@@ -297,29 +330,29 @@ const messageCenter = messageCenterStore();
:deep(.el-badge__content.is-fixed) {
top: 12px;
}
.online-status{
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #18bc9c;
}
}
.online-down{
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #979b9c;
}
}
.online-status {
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #18bc9c;
}
}
.online-down {
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #979b9c;
}
}
}
</style>

View File

@@ -2,7 +2,8 @@
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<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 class="content-box">
<template v-if="state.newsList.length > 0">
@@ -21,7 +22,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { reactive,onBeforeMount,ref,onMounted } from 'vue';
import { reactive, onBeforeMount, ref, onMounted } from 'vue';
// 定义变量内容
const state = reactive({
@@ -33,27 +34,27 @@ const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
import {useRouter } from "vue-router";
const route = useRouter()
import { useRouter } from 'vue-router';
const route = useRouter();
const onGoToGiteeClick = () => {
route.push('/messageCenter')
route.push('/messageCenter');
};
//获取最新消息
import { request } from "/@/utils/service";
const getLastMsg= ()=>{
request({
url: '/api/system/message_center/get_newest_msg/',
method: 'get',
params: {}
}).then((res:any) => {
const { data } = res
state.newsList= [data]
})
}
onMounted(()=>{
getLastMsg()
})
import { request } from '/@/utils/service';
const getLastMsg = () => {
request({
url: '/api/system/message_center/get_newest_msg/',
method: 'get',
params: {},
}).then((res: any) => {
const { data } = res;
if (data) state.newsList = [data];
});
};
onMounted(() => {
getLastMsg();
});
</script>
<style scoped lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 iconfont from '/@/assets/iconfont/iconfont.json'; //引入json文件
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 VXETable from 'vxe-table'

View File

@@ -21,13 +21,14 @@ const menuApi = useMenuApi();
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
const greatDream: any = import.meta.glob('@great-dream/**/*.{vue,tsx}');
/**
* 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules });
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }, { ...greatDream });
/**
* 后端控制路由:初始化方法,防止刷新时路由丢失
@@ -198,7 +199,10 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
const k0 = k.replace("ode_modules/@great-dream/", '')
const k1 = k0.replace("/plugins", '')
const newComponent = component.replace("plugins/", "")
return k1.startsWith(`${newComponent}`) || k1.startsWith(`/${newComponent}`);
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];

View File

@@ -98,10 +98,22 @@ export function formatTwoStageRoutes(arr: any) {
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) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
await checkVersion()
checkToken()
NProgress.configure({showSpinner: false});
if (to.meta.title) NProgress.start();
const token = Session.get('token');

View File

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

View File

@@ -2,17 +2,19 @@
* 定义接口来定义对象的类型
* `stores` 全部类型定义在这里
*/
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
import { useFrontendMenuStore } from "/@/stores/frontendMenu";
// 用户信息
export interface UserInfosState {
id: '',
avatar: string;
is_superuser: boolean,
username: string;
name: string;
email: string;
mobile: string;
gender: string;
pwd_change_count:null|number;
pwd_change_count: null | number;
dept_info: {
dept_id: number;
dept_name: string;
@@ -107,9 +109,9 @@ export interface ConfigStates {
export interface FrontendMenu {
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: '#409eff',
primary: "#193755",
// 是否开启深色模式
isIsDark: false,
@@ -26,9 +26,9 @@ export const useThemeConfig = defineStore('themeConfig', {
* 顶栏设置
*/
// 默认顶栏导航背景颜色
topBar: '#ffffff',
topBar: "#f8f8f8",
// 默认顶栏导航字体颜色
topBarColor: '#606266',
topBarColor: "#000000",
// 是否开启顶栏背景颜色渐变
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,
@@ -48,9 +48,9 @@ export const useThemeConfig = defineStore('themeConfig', {
* 分栏设置
*/
// 默认分栏菜单背景颜色
columnsMenuBar: '#334054',
columnsMenuBar:"#334054",
// 默认分栏菜单字体颜色
columnsMenuBarColor: '#e6e6e6',
columnsMenuBarColor: "#e6e6e6",
// 是否开启分栏菜单背景颜色渐变
isColumnsMenuBarColorGradual: false,
// 是否开启分栏菜单鼠标悬停预加载(预览菜单)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

57
web/src/utils/cores.tsx Normal file
View File

@@ -0,0 +1,57 @@
import mitt, { Emitter } from 'mitt';
export interface TaskProps {
name: string;
custom?: any;
}
// 定义自定义事件类型
export type BusEvents = {
onNewTask: TaskProps | undefined;
};
export interface Task {
id: number;
handle: string;
data: any;
createTime: Date;
custom?: any;
}
export interface Core {
bus: Emitter<BusEvents>;
// eslint-disable-next-line no-unused-vars
showNotification(body: string, title?: string): Notification | undefined;
taskList: Map<String, Task>;
}
const bus = mitt<BusEvents>();
export function getSystemNotification(body: string, title?: string) {
if (!title) {
title = '通知';
}
return new Notification(title ?? '通知', {
body: body,
});
}
export function showSystemNotification(body: string, title?: string): Notification | undefined {
if (Notification.permission === 'granted') {
return getSystemNotification(body, title);
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
return getSystemNotification(body, title);
}
});
}
return void 0;
}
const taskList = new Map<String, Task>();
export function useCore(): Core {
return {
bus,
showNotification: showSystemNotification,
taskList,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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