292 Commits

Author SHA1 Message Date
H0nGzA1
dd98b98775 feat: 更新fast-crud版本 2023-10-27 16:05:06 +08:00
李强
8e107d9944 feat: 优化默认头像显示 2023-10-19 16:55:19 +08:00
猿小天
7159253c6b Merge remote-tracking branch 'origin/master' 2023-10-19 16:10:15 +08:00
猿小天
075d457dc7 fix: 修复列管理的问题 2023-10-19 16:10:05 +08:00
李强
564e9a112b feat: 更新requirements.txt 2023-10-19 16:01:59 +08:00
李强
3b84773a7d feat: 更新requirements.txt 2023-10-19 16:01:16 +08:00
李强
996d644d9b 删除 .DS_Store 2023-10-19 15:36:07 +08:00
china_ahhui
97f7bfd3f0 chore(用户管理): 添加了直接重置密码的约束,只允许超管可以操作 2023-10-10 17:15:40 +08:00
china_ahhui
d1fdbd7e51 Merge branches 'master' and 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-10-10 17:12:50 +08:00
china_ahhui
802ee7f50e fix(设置): 修复env的引入bug 2023-10-10 17:12:33 +08:00
sheng
ddf9fcd8e8 feat(部门管理): 查询框多行模式添加 2023-10-10 16:11:49 +08:00
sheng
6f15c2f669 feat(部门管理): 重设密码添加 2023-10-10 15:44:13 +08:00
李强
084d1c0c3e 删除lock文件 2023-10-03 23:26:15 +08:00
李强
6ded198206 Merge branch 'master' of e.coding.net:dvadmin/dvadmin3/dvadmin3
# Conflicts:
#	README.md
#	backend/conf/env.example.py
#	backend/docker_start.sh
#	backend/requirements.txt
#	docker_env/README.md
#	docker_env/mysql/launch.sh
#	docker_env/web/Dockerfile
#	docker_env/web/DockerfileBuild
#	web/.env.development
#	web/package.json
2023-10-03 23:22:43 +08:00
李强
e549d33a48 更新启动方式 2023-10-03 23:11:51 +08:00
china_ahhui
cbba5a60f4 fix(用户管理): 修复在部门显示子级的情况下搜索失效的bug 2023-10-03 23:09:55 +08:00
china_ahhui
773b56a5e6 style: 修改忽略文件 2023-10-03 23:09:55 +08:00
H0nGzA1
fb29929f55 perf: 优化首页登录 2023-10-03 23:09:55 +08:00
H0nGzA1
0772a605bf perf: 登录验证码自动刷新 2023-10-03 23:09:55 +08:00
H0nGzA1
496c453615 fix: transition组件热重载显示页面空白bug 2023-10-03 23:09:55 +08:00
sheng
58b59c2ed0 部门管理隐藏统计图表添加 2023-10-03 23:09:55 +08:00
sheng
94815f4b20 列权限添加 2023-10-03 23:09:55 +08:00
猿小天
ad6ec5ca58 1.修改字段权限判断是否为超级管理员 2023-10-03 23:09:51 +08:00
猿小天
4bcaf8d45f 1.新增字段权限接口 2023-10-03 23:09:51 +08:00
猿小天
be5db33a55 1.修复单例查询时,报404问题; 2023-10-03 23:09:51 +08:00
猿小天
cfc5f8597a 1.字段权限配置完成; 2023-10-03 23:09:51 +08:00
猿小天
209e2ecced 1.菜单和按钮权限范围授权完成; 2023-10-03 23:09:51 +08:00
猿小天
4b948fe5c3 1.菜单和按钮权限范围授权完成; 2023-10-03 23:09:51 +08:00
猿小天
ea335922b2 1.菜单和按钮基本授权完成;
2. todo:
a.按钮数据权限;
b.字段权限
2023-10-03 23:09:51 +08:00
符锦辉
d290836f4f 更新文件 init_menu.json 2023-10-03 23:09:36 +08:00
符锦辉
9a426dbd72 更新文件 routing.py 2023-10-03 23:09:36 +08:00
符锦辉
a0b6e57a3c 更新文件 urls.py 2023-10-03 23:09:36 +08:00
符锦辉
5a89cdf889 删除 device 2023-10-03 23:09:36 +08:00
ahhui
b6b970f7a2 feat: 物联网相关代码 2023-10-03 23:09:36 +08:00
ahhui
2722631276 test: 列权限测试 2023-10-03 23:09:36 +08:00
ahhui
478aea32f1 chore: 更新依赖文件 2023-10-03 23:09:36 +08:00
sheng
cf55897552 chore: 设备管理去除 2023-10-03 23:09:36 +08:00
sheng
8020da03e2 feat( 角色管理 ): 新权限配置页面完成 2023-10-03 23:09:36 +08:00
ahhui
c646563d26 chore: 更新初始化文件 2023-10-03 23:09:36 +08:00
sheng
43a4572a71 feat( 角色管理 ): 权限配置重写 2023-10-03 23:09:36 +08:00
ahhui
1582353279 fix: 修复部门和菜单新增父级下的第一个子级菜单时sort找不到的bug 2023-10-03 23:09:36 +08:00
sheng
1df2101658 feat(设备管理): 文件添加 2023-10-03 23:09:36 +08:00
ahhui
0679198d0f perf: 优化列权限新增代码 2023-10-03 23:09:36 +08:00
sheng
e3c478a263 feat(列管理): 样式修改 2023-10-03 23:09:36 +08:00
ahhui
07cf81c9fe chore: 更改jwt有效期 2023-10-03 23:09:36 +08:00
ahhui
1b98fbc982 feat: 新增列权限配置功能
新增列权限配置,优化代码结构,更新初始化文件
2023-10-03 23:09:36 +08:00
ahhui
85040c808f chore: 更新初始化文件 2023-10-03 23:09:36 +08:00
ahhui
00b2cac7a5 chore: 更新初始化文件 2023-10-03 23:09:36 +08:00
ahhui
7c27f95353 refactor: 重构配置文件
所有项目的app都放在env.py里的CUSTOM_APPS列表里注册,为了支持按照项目app查找model
2023-10-03 23:09:36 +08:00
ahhui
d59df6db7c perf: 优化部分代码结构 2023-10-03 23:08:41 +08:00
ahhui
34c55cfd7e fix(菜单): 修复新增菜单时排序错误的bug 2023-10-03 23:08:41 +08:00
sheng
191e57dcd9 feat(列管理): 列管理页面添加 2023-10-03 23:08:41 +08:00
ahhui
9be50863d9 chore(优化): 优化代码结构 2023-10-03 23:08:41 +08:00
ahhui
9c218a856c feat(部门): 添加部门头信息查询以及递归的头信息查询,优化部分代码 2023-10-03 23:08:41 +08:00
ahhui
b4ebb14843 fix(用户): 重写user视图集的list视图,支持递归查询 2023-10-03 23:08:41 +08:00
sheng
17d80b0b00 revert(角色管理): 回滚角色管理代码 2023-10-03 23:08:41 +08:00
sheng
01394c6904 feat(部门管理): 部门详情与统计的添加 2023-10-03 23:08:41 +08:00
ahhui
59b1b54da1 fix(部门): 修复部门排序bug
修复添加新部门后排序值依旧是1导致前端上移下移部门时不生效的bug,添加部门上移下移的权限校验
2023-10-03 23:08:41 +08:00
sheng
8084ca7b0b feat(部门管理): 部门信息布局完成 2023-10-03 23:08:41 +08:00
sheng
427b6f8478 perf(菜单管理,部门管理): 编辑时父级菜单懒加载的修改 2023-10-03 23:08:37 +08:00
sheng
41a9888441 feat(菜单管理): 新建编辑完成 2023-10-03 23:08:37 +08:00
sheng
5139d37007 feat(菜单管理): 父级菜单改为树形结构 2023-10-03 23:08:37 +08:00
ahhui
44bceb6e22 chore: 更新初始化用户数据 2023-10-03 23:08:37 +08:00
sheng
c2f8c43905 refactor(菜单管理管理): 组件抽离 2023-10-03 23:08:37 +08:00
ahhui
9f479e476d perf: 微调验证码配置 2023-10-03 23:08:37 +08:00
ahhui
f5a7b157f9 build: 增加pillow依赖 2023-10-03 23:08:37 +08:00
sheng
7110d10432 feat(部门管理): 新增编辑实现 2023-10-03 23:08:37 +08:00
sheng
05f4e2970d feat(部门管理): 上移下移实现 2023-10-03 23:08:33 +08:00
H0nGzA1
5353a27187 feat: 后端-部门管理-上移下移排序功能&新增部门人数字段 2023-10-03 23:08:33 +08:00
H0nGzA1
f4ff5d87af fix: 移动菜单子级问题 2023-10-03 23:08:33 +08:00
sheng
ceac44813f fix(菜单管理): 上移下移修改 2023-10-03 23:08:33 +08:00
H0nGzA1
a490ced370 feat: 菜单管理-上下移动功能 2023-10-03 23:07:30 +08:00
sheng
20188261c5 refactor: 菜单管理新增重构 2023-10-03 23:06:26 +08:00
sheng
15ef01aed9 feat: 部门管理新增用户列表 2023-10-03 23:06:26 +08:00
sheng
1dea6ed29c refactor: 部门新增与编辑 2023-10-03 23:06:22 +08:00
sheng
2bad36c982 feat:部门管理重构 2023-10-03 23:05:59 +08:00
sheng
941dbbc783 feat: 菜单管理菜单列表上移下移添加 2023-10-03 23:05:59 +08:00
sheng
14881707e2 页面美化:菜单管理页面优化 2023-10-03 23:05:53 +08:00
sheng
d60c795ea2 页面美化:菜单管理树的美化 2023-10-03 23:05:42 +08:00
sheng
9cf5d13f1c 页面美化:登陆页面优化 2023-10-03 23:05:32 +08:00
china_ahhui
e1618359d7 fix(用户管理): 修复在部门显示子级的情况下搜索失效的bug 2023-09-19 11:45:37 +08:00
china_ahhui
abe7972ab4 style: 修改忽略文件 2023-09-19 10:20:19 +08:00
H0nGzA1
0709e72ae1 perf: 优化首页登录 2023-09-17 22:25:55 +08:00
H0nGzA1
2e676d64bb perf: 登录验证码自动刷新 2023-09-17 19:42:50 +08:00
H0nGzA1
98e974b55e fix: transition组件热重载显示页面空白bug 2023-09-16 02:00:22 +08:00
sheng
e7f78fbae0 部门管理隐藏统计图表添加 2023-09-11 14:58:34 +08:00
sheng
111edb3f4a 列权限添加 2023-09-11 12:02:33 +08:00
猿小天
86f539a193 1.修改字段权限判断是否为超级管理员 2023-09-11 11:33:09 +08:00
李强
3b9dc7c171 更新dockerfile 2023-09-09 22:38:41 +08:00
猿小天
8175c620ed 1.新增字段权限接口 2023-09-09 10:42:55 +08:00
猿小天
de5884d28d 1.修复单例查询时,报404问题; 2023-09-07 23:24:21 +08:00
猿小天
e63a7cab6f 1.字段权限配置完成; 2023-08-19 22:06:58 +08:00
猿小天
cdfdf0a483 1.菜单和按钮权限范围授权完成; 2023-08-18 00:42:26 +08:00
猿小天
515f720a59 1.菜单和按钮权限范围授权完成; 2023-08-18 00:39:14 +08:00
猿小天
5524446dbc 1.菜单和按钮基本授权完成;
2. todo:
a.按钮数据权限;
b.字段权限
2023-08-15 23:55:46 +08:00
sheng
9c32c7f60b Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-11 18:03:05 +08:00
sheng
6eed2814f8 chore: 设备管理去除 2023-08-11 18:02:59 +08:00
符锦辉
f03e61ce63 更新文件 init_menu.json 2023-08-11 18:01:22 +08:00
符锦辉
5450102275 更新文件 routing.py 2023-08-11 17:44:27 +08:00
符锦辉
689ce4b43b 更新文件 urls.py 2023-08-11 17:41:59 +08:00
符锦辉
65af240757 删除 device 2023-08-11 17:41:25 +08:00
ahhui
f61025c6db feat: 物联网相关代码 2023-08-08 17:40:28 +08:00
ahhui
516f0ac675 test: 列权限测试 2023-08-08 16:31:55 +08:00
ahhui
b1b5ae5fcd chore: 更新依赖文件 2023-08-08 16:31:18 +08:00
sheng
9a847d078f feat( 角色管理 ): 新权限配置页面完成 2023-08-08 16:16:32 +08:00
sheng
dcac5c759a Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-07 18:26:14 +08:00
sheng
13999142aa feat( 角色管理 ): 权限配置重写 2023-08-07 18:26:08 +08:00
ahhui
cd6318c6fd chore: 更新初始化文件 2023-08-07 16:31:06 +08:00
sheng
3c58bcf467 Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-07 16:22:25 +08:00
sheng
b9bdf878de feat(设备管理): 文件添加 2023-08-07 16:22:17 +08:00
ahhui
db989671c0 Merge remote-tracking branch 'origin/master' 2023-08-07 14:50:40 +08:00
ahhui
f67869d428 fix: 修复部门和菜单新增父级下的第一个子级菜单时sort找不到的bug 2023-08-07 14:50:34 +08:00
sheng
78c693718b Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-07 13:42:13 +08:00
sheng
653483e5de feat(列管理): 样式修改 2023-08-07 13:42:03 +08:00
ahhui
89ba768932 Merge remote-tracking branch 'origin/master' 2023-08-04 18:26:50 +08:00
ahhui
e4326b6d4e perf: 优化列权限新增代码 2023-08-04 18:26:44 +08:00
sheng
97020ba4e8 Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-04 18:13:09 +08:00
sheng
945db3344c feat(列管理): 列管理页面添加 2023-08-04 18:13:04 +08:00
ahhui
19df23ce5f chore: 更改jwt有效期 2023-08-04 18:12:58 +08:00
ahhui
97c76b6afe feat: 新增列权限配置功能
新增列权限配置,优化代码结构,更新初始化文件
2023-08-04 18:10:51 +08:00
ahhui
6597826fdd chore: 更新初始化文件 2023-08-04 12:01:12 +08:00
ahhui
ee419e282e chore: 更新初始化文件 2023-08-04 11:55:34 +08:00
ahhui
3d12d7a5c5 Merge remote-tracking branch 'origin/master' 2023-08-03 18:04:43 +08:00
ahhui
b2aa22ce84 refactor: 重构配置文件
所有项目的app都放在env.py里的CUSTOM_APPS列表里注册,为了支持按照项目app查找model
2023-08-03 18:04:34 +08:00
ahhui
06d7d718e1 perf: 优化部分代码结构 2023-08-03 18:02:55 +08:00
ahhui
6067dfac2f fix(菜单): 修复新增菜单时排序错误的bug 2023-08-03 18:01:49 +08:00
sheng
637e45ef83 Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-03 17:39:09 +08:00
sheng
202f67460e revert(角色管理): 回滚角色管理代码 2023-08-03 17:39:01 +08:00
ahhui
7da23ab6a6 chore(优化): 优化代码结构 2023-08-03 10:44:57 +08:00
ahhui
be51124bb6 feat(部门): 添加部门头信息查询以及递归的头信息查询,优化部分代码 2023-08-02 18:21:14 +08:00
ahhui
7e7637f10b Merge remote-tracking branch 'origin/master' 2023-08-02 18:19:59 +08:00
ahhui
dbb94fb9ff fix(用户): 重写user视图集的list视图,支持递归查询 2023-08-02 18:19:52 +08:00
sheng
fea61e35ef feat(部门管理): 部门详情与统计的添加 2023-08-02 17:40:57 +08:00
sheng
8ecfc1143b Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-08-01 18:29:41 +08:00
sheng
7c72cc74df feat(部门管理): 部门信息布局完成 2023-08-01 18:29:31 +08:00
ahhui
12f5125207 Merge remote-tracking branch 'origin/master' 2023-08-01 15:25:48 +08:00
ahhui
770c5f73c0 fix(部门): 修复部门排序bug
修复添加新部门后排序值依旧是1导致前端上移下移部门时不生效的bug,添加部门上移下移的权限校验
2023-08-01 15:25:42 +08:00
sheng
98b81a789d perf(菜单管理,部门管理): 编辑时父级菜单懒加载的修改 2023-08-01 15:13:50 +08:00
sheng
c13a5f0dda feat(菜单管理): 新建编辑完成 2023-08-01 13:50:29 +08:00
sheng
a524137e18 feat(菜单管理): 父级菜单改为树形结构 2023-07-31 17:58:52 +08:00
sheng
2c9d8766e3 Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-07-31 17:06:38 +08:00
sheng
8a0f18eab1 refactor(菜单管理管理): 组件抽离 2023-07-31 17:05:15 +08:00
ahhui
d8511eeb7b Merge remote-tracking branch 'origin/master' 2023-07-31 16:43:42 +08:00
ahhui
52a91f1703 chore: 更新初始化用户数据 2023-07-31 16:43:37 +08:00
sheng
14395621c0 Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-07-31 14:49:18 +08:00
sheng
c4e2451ac8 feat(部门管理): 新增编辑实现 2023-07-31 14:49:12 +08:00
ahhui
936928e854 perf: 微调验证码配置 2023-07-31 14:45:09 +08:00
ahhui
619ebeacf1 build: 增加pillow依赖 2023-07-31 14:43:59 +08:00
sheng
b95679a986 feat(部门管理): 上移下移实现 2023-07-31 10:33:57 +08:00
sheng
8e930c6a10 Merge branch 'master' of https://e.coding.net/dvadmin/dvadmin3/dvadmin3 2023-07-31 10:23:55 +08:00
sheng
d2811a1f87 fix(菜单管理): 上移下移修改 2023-07-31 10:23:46 +08:00
H0nGzA1
7ac5c86ed3 feat: 后端-部门管理-上移下移排序功能&新增部门人数字段 2023-07-30 22:43:30 +08:00
H0nGzA1
eea5cdadfe Merge remote-tracking branch 'origin/master' 2023-07-28 18:12:06 +08:00
H0nGzA1
4e5f9ce946 fix: 移动菜单子级问题 2023-07-28 18:11:57 +08:00
sheng
e790882ab6 Merge remote-tracking branch 'origin/master' 2023-07-28 17:24:06 +08:00
sheng
0f86f5c6fc refactor: 菜单管理新增重构 2023-07-28 17:09:41 +08:00
H0nGzA1
4018113161 Merge remote-tracking branch 'origin/master' 2023-07-28 16:14:13 +08:00
H0nGzA1
252b6a8328 feat: 菜单管理-上下移动功能 2023-07-28 16:13:33 +08:00
sheng
c26abef364 feat: 部门管理新增用户列表 2023-07-28 14:37:42 +08:00
sheng
a40dc9afb1 refactor: 部门新增与编辑 2023-07-28 11:16:07 +08:00
sheng
9114c6bf81 feat:部门管理重构 2023-07-27 18:31:28 +08:00
sheng
7b0a6bc2be feat: 菜单管理菜单列表上移下移添加 2023-07-26 18:12:15 +08:00
sheng
e4ca462153 页面美化:菜单管理页面优化 2023-07-26 16:55:22 +08:00
sheng
840323ff07 页面美化:菜单管理树的美化 2023-07-26 13:35:47 +08:00
H0nGzA1
ed0dca7346 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	web/yarn.lock
2023-07-25 18:33:45 +08:00
H0nGzA1
2b63966ee2 feat: update fast-crud 2023-07-25 18:33:11 +08:00
sheng
55a2f73420 页面美化:登陆页面优化 2023-07-25 16:45:47 +08:00
sheng
7ccbcebe77 init 2023-07-25 15:25:57 +08:00
李一民
8f57a240db Initial Commit 2023-07-25 14:53:35 +08:00
猿小天
1ccfd619f3 fix: 查询时空字符串问题 2023-07-12 21:34:57 +08:00
猿小天
dcdc7bbe3b fix: 权限问题,文件管理页面,查询时TF问题
1.权限编辑提交时,值不变问题;
2.恢复文件管理页面;
3.查询时T/F后台报错问题;
2023-07-12 21:18:49 +08:00
猿小天
90557ea039 fix: 部门懒加载
修复部门懒加载报data_range错误
2023-06-10 22:39:58 +08:00
猿小天
a462e15643 refactor: ♻️ 授权页面重构
重构授权页面
2023-06-09 20:35:06 +08:00
猿小天
6a24683769 refactor: ♻️ 授权页面重构
重构授权页面
2023-06-08 13:20:31 +08:00
猿小天
9fa1775b77 Merge remote-tracking branch 'origin/dev' into dev 2023-06-08 13:17:18 +08:00
猿小天
16605779ed refactor: ♻️ 授权页面重构
重构授权页面
2023-06-08 13:17:10 +08:00
unknown
186658858d Merge branch 'dev' of https://gitee.com/huge-dream/django-vue3-admin into dev 2023-06-07 17:20:38 +08:00
unknown
996cb1447d fix: 🐛 url false拼接bug修复 2023-06-07 17:20:31 +08:00
猿小天
e8b84ad71f refactor: ♻️ 授权页面重构
重构授权页面
2023-06-06 20:57:07 +08:00
猿小天
e7065ab89e Merge remote-tracking branch 'origin/dev' into dev 2023-06-06 20:53:08 +08:00
猿小天
e962ca6559 refactor: ♻️ 授权页面重构
重构授权页面
2023-06-06 20:52:25 +08:00
unknown
4bdc256311 perf: 💄 部门列表和菜单列表字体修改,switch颜色修改 2023-06-06 17:52:47 +08:00
猿小天
f7b94e496c refactor: ♻️ 授权页面重构
重构授权页面
2023-06-05 22:21:19 +08:00
李强
8c0a04ce61 perf: 🐛 用户登录后,每次都会变化密码加密值 2023-05-28 13:28:36 +08:00
李强
31f20a766f perf: 🐛 filter_fields使用exact无效,使用icontains等报错
fix: https://gitee.com/liqianglog/django-vue-admin/issues/I6QZW5
2023-05-28 13:24:38 +08:00
李强
65a34a683c 配置变化: 验证码接口bug 2023-05-26 20:07:36 +08:00
H0nGzA1
66effde1d9 perf: 用户-重设密码 2023-05-18 00:15:39 +08:00
H0nGzA1
f4f958d2e9 perf: 优化细节 2023-05-17 23:48:59 +08:00
H0nGzA1
d10127eafc feat: keepAlive优化 2023-05-17 23:43:05 +08:00
H0nGzA1
60035767ad feat: keepAlive功能 2023-05-17 23:34:41 +08:00
H0nGzA1
adc522c180 fix: 字典管理-配置:无搜索框问题(https://gitee.com/huge-dream/django-vue3-admin/issues/I6ZGNK) 2023-05-17 21:37:17 +08:00
H0nGzA1
17b67e43c9 refactor: 前端布局 菜单,部门:重构 2023-05-17 21:25:59 +08:00
猿小天
3cccfaa7f4 feat(文件管理): 文件管理修改 2023-05-16 00:26:49 +08:00
猿小天
ce4d410e53 feat(文件管理): 文件管理修改 2023-05-16 00:24:22 +08:00
猿小天
7b0341b527 feat(文件管理): 文件管理修改 2023-05-15 22:11:40 +08:00
猿小天
31f7175186 feat(文件管理): 文件管理修改 2023-05-15 22:11:12 +08:00
猿小天
2c22ecbac5 feat: 🎉 导入导出
用户管理加入导入导出
2023-05-13 00:36:16 +08:00
猿小天
ad59bc68aa fix: 🐛 菜单管理
1.修复菜单管理问题;
2023-05-09 00:14:29 +08:00
猿小天
8d7d775164 fix: 🐛 菜单管理和字典管理
1.修复菜单管理问题;
2.修复字典管理问题;
2023-05-09 00:05:41 +08:00
H0nGzA1
9c2dea9db5 ci: 🐳 web env文件线上环境地址配置 2023-04-29 01:03:57 +08:00
H0nGzA1
8c31bda3da ci: 🐳 docker环境 nginx代理地址修改 2023-04-29 01:02:20 +08:00
H0nGzA1
7f7d88fe5b feat: 新增支持新版租户功能 2023-04-29 00:55:23 +08:00
猿小天
19e18a7b6f feat(全局配置): 🔧 字典配置和文件上传配置 2023-04-28 18:19:29 +08:00
猿小天
cd6cd775e7 fix(个人中心): 🐛 性别选项和字典对应问题 2023-04-28 17:48:16 +08:00
猿小天
111ca9554c fix(登录): 🐛 自动登录导致登录错误问题 2023-04-28 17:34:05 +08:00
猿小天
c9fddbd4a7 fix(角色授权): 🐛 权限删除失败问题 2023-04-28 12:02:11 +08:00
猿小天
d3b057f75a fix(系统配置): 🐛 系统配置中文件上传,内容添加,分组添加
1.文件上传;
2.内容添加;
3.添加完成后刷新
2023-04-28 11:54:50 +08:00
猿小天
096f5919af fix: 🐛 系统配置中switch的文件 2023-04-28 11:05:56 +08:00
H0nGzA1
25cc2c83c1 feat: 全局crud的ElNotification提示,ElNotification的封装 2023-04-21 20:49:48 +08:00
H0nGzA1
054bcf3eea build: ⬆️ 更新依赖:fast-crud版本 2023-04-21 15:07:52 +08:00
H0nGzA1
9c24e61123 fix: 🐛 修复菜单管理的添加权限按钮bug 2023-04-21 14:37:02 +08:00
H0nGzA1
cc36ff3d8f Merge branch 'master' into dev 2023-04-21 14:30:12 +08:00
H0nGzA1
93e74dbaa2 Merge remote-tracking branch 'origin/master' 2023-04-21 14:29:53 +08:00
H0nGzA1
5ab3ad4ed9 fix: 🐛 get查询params参数问题,导致分页不成功 2023-04-21 14:27:03 +08:00
李强
9468abd4a2 新功能: 取消loguru日志,使用简单Django日志 2023-04-17 23:24:43 +08:00
H0nGzA1
1080af6413 Merge remote-tracking branch 'origin/dev' into dev 2023-04-14 23:09:15 +08:00
H0nGzA1
967e4e76c2 Merge remote-tracking branch 'origin/master' 2023-04-14 23:08:56 +08:00
H0nGzA1
f7b93c349e fix: 🐛 部门删除bug 2023-04-14 23:04:30 +08:00
H0nGzA1
72f1d2eae7 feat: 用户管理细节优化 2023-04-14 23:04:30 +08:00
H0nGzA1
592bc947a0 feat: 账号锁定功能 2023-04-14 23:04:30 +08:00
H0nGzA1
953fcc3437 fix: 🐛 修复添加用户bug 2023-04-14 23:04:30 +08:00
H0nGzA1
13aa6dbb99 fix: 🐛 头像更新问题 2023-04-14 23:04:30 +08:00
猿小天
460ae171dd fix(文件上传): 🐛 文件上传 2023-04-14 23:04:30 +08:00
H0nGzA1
0d556bfb2b fix: 🐛 头像上传,文件上传问题 2023-04-14 23:04:29 +08:00
猿小天
61ae6e8eb3 refactor(所有页面): ♻️ 所有页面的按钮权限配置 2023-04-14 23:04:29 +08:00
猿小天
175b151f7f refactor(所有页面): ♻️ 所有页面的表格列宽优化 2023-04-14 23:04:29 +08:00
H0nGzA1
a50f73d466 refactor: ♻️ 重构部门管理前端 2023-04-14 23:04:29 +08:00
猿小天
eea9d320af fix(登录): 登录优化 2023-04-14 23:04:29 +08:00
猿小天
15c4808bbb refactor(登录页面): ♻️ 登录优化 2023-04-14 23:04:29 +08:00
H0nGzA1
d2d6ba3460 refactor: ♻️ 重构菜单管理前端 2023-04-14 23:04:29 +08:00
猿小天
4f0295acb1 refactor(用户管理): ♻️ 用户管理优化 2023-04-14 23:04:29 +08:00
H0nGzA1
255c405e59 feat: 前端自动注册插件功能 2023-04-14 23:04:29 +08:00
H0nGzA1
96ad956efd refactor: ♻️ 更新优化精简fast-crud结构 2023-04-14 23:04:29 +08:00
H0nGzA1
b586b46016 feat: 更新fast-crud版本以及依赖 2023-04-14 23:04:29 +08:00
H0nGzA1
875146e588 feat: 所有菜单页面状态改为可编辑开关 2023-04-14 23:04:29 +08:00
H0nGzA1
1ee709b9eb feat: 所有菜单页面样式优化~ 2023-04-14 23:04:29 +08:00
H0nGzA1
f89a5228cd refactor: ♻️ 菜单管理重构完成 2023-04-14 23:04:29 +08:00
H0nGzA1
795f621637 feat: 精简化初始化菜单json文件 2023-04-14 23:04:29 +08:00
H0nGzA1
c5d7a70f46 feat: 登录页面,自动刷新验证码 2023-04-14 23:04:29 +08:00
H0nGzA1
8710b047b1 feat: 登录页面回车登录功能 2023-04-14 23:04:28 +08:00
H0nGzA1
300b6c6bb8 refactor: ♻️ 菜单管理重构完成 2023-04-14 23:04:28 +08:00
raymond
296640cb2a feat: 支持通过python manage.py createsuperuser创建管理员用户 2023-04-14 23:04:28 +08:00
H0nGzA1
24861fda42 refactor: ♻️ 用户管理重构 2023-04-14 23:04:28 +08:00
H0nGzA1
0e3fac37e9 feat: 🚀 更新nginx配置文件 2023-04-14 23:04:28 +08:00
H0nGzA1
93d8d94049 feat: 🔧 tailwind css 配置 2023-04-14 23:04:28 +08:00
victory
9d4f007d48 feat:添加对url中排序参数的解析 2023-04-14 23:04:28 +08:00
victory
4d17a7d9df fix:优化角色管理用户管理和接口白名单的页面显示 2023-04-14 23:04:28 +08:00
H0nGzA1
c4c9a81ac8 fix: 🐛 部门删除bug 2023-04-13 02:24:31 +08:00
H0nGzA1
d1223dddd3 feat: 用户管理细节优化 2023-04-12 02:13:57 +08:00
H0nGzA1
3dd68e5d21 feat: 账号锁定功能 2023-04-12 01:58:34 +08:00
H0nGzA1
0668cb087f fix: 🐛 修复添加用户bug 2023-04-12 00:25:36 +08:00
H0nGzA1
38494edea5 fix: 🐛 头像更新问题 2023-04-11 22:43:33 +08:00
猿小天
2f04f22904 fix(文件上传): 🐛 文件上传 2023-04-11 19:09:12 +08:00
H0nGzA1
0ccf2e3725 fix: 🐛 头像上传,文件上传问题 2023-04-11 18:40:20 +08:00
H0nGzA1
68cd42246d Merge remote-tracking branch 'origin/dev' into dev 2023-04-10 17:59:42 +08:00
H0nGzA1
dc6ddda4ab refactor: ♻️ 重构部门管理前端 2023-04-10 17:59:30 +08:00
猿小天
08b56ee663 refactor(所有页面): ♻️ 所有页面的按钮权限配置 2023-04-10 16:45:49 +08:00
猿小天
82d7193dcb refactor(所有页面): ♻️ 所有页面的表格列宽优化 2023-04-10 09:45:25 +08:00
H0nGzA1
4b609ed7e4 Merge remote-tracking branch 'origin/dev' into dev 2023-04-10 00:04:00 +08:00
H0nGzA1
61066b8caa refactor: ♻️ 重构菜单管理前端 2023-04-10 00:03:17 +08:00
猿小天
160c376f81 fix(登录): 登录优化 2023-04-09 23:09:35 +08:00
猿小天
b96c220f1d refactor(登录页面): ♻️ 登录优化 2023-04-09 23:08:30 +08:00
猿小天
42884685ff refactor(用户管理): ♻️ 用户管理优化 2023-04-09 22:26:26 +08:00
李强
c126c704a4 新功能: 删除gunicorn.pid 2023-03-31 10:31:38 +08:00
李强
662c314518 Merge remote-tracking branch 'origin/master' 2023-03-31 10:31:03 +08:00
李强
1715fcb4d8 新功能: 完善日志信息 2023-03-31 10:30:39 +08:00
H0nGzA1
58e611cb22 feat: 前端自动注册插件功能 2023-03-31 00:25:23 +08:00
H0nGzA1
0b2fa1e92c refactor: ♻️ 更新优化精简fast-crud结构 2023-03-30 21:42:45 +08:00
H0nGzA1
125dfd4dd5 feat: 更新fast-crud版本以及依赖 2023-03-30 18:21:38 +08:00
H0nGzA1
af7bfcd215 feat: 所有菜单页面状态改为可编辑开关 2023-03-30 18:12:38 +08:00
H0nGzA1
6917786864 feat: 所有菜单页面样式优化~ 2023-03-30 16:43:57 +08:00
H0nGzA1
cd60b3d9be refactor: ♻️ 菜单管理重构完成 2023-03-30 15:37:27 +08:00
H0nGzA1
cfae363f53 feat: 精简化初始化菜单json文件 2023-03-29 18:29:19 +08:00
H0nGzA1
4f8686b35e feat: 登录页面,自动刷新验证码 2023-03-28 21:21:05 +08:00
H0nGzA1
1407a25b6a feat: 登录页面回车登录功能 2023-03-28 20:35:35 +08:00
H0nGzA1
7c78c7e53f refactor: ♻️ 菜单管理重构完成 2023-03-28 20:09:49 +08:00
H0nGzA1
f1da0507dd Merge remote-tracking branch 'origin/dev' into dev 2023-03-24 18:46:42 +08:00
H0nGzA1
df74c792f6 refactor: ♻️ 用户管理重构 2023-03-24 18:45:58 +08:00
raymond
4c697ef9d4 Merge remote-tracking branch 'origin/dev' into dev 2023-03-24 16:42:12 +08:00
raymond
ab1d1c87e2 feat: 支持通过python manage.py createsuperuser创建管理员用户 2023-03-24 16:41:22 +08:00
H0nGzA1
b6b3b4a39a feat: 🚀 更新nginx配置文件 2023-03-24 15:17:43 +08:00
H0nGzA1
581e4cd92f feat: 🔧 tailwind css 配置 2023-03-23 21:10:07 +08:00
H0nGzA1
fb76d4271b Merge remote-tracking branch 'origin/dev' into dev 2023-03-23 18:13:49 +08:00
H0nGzA1
a304a49107 feat: 引入tailwind css样式 2023-03-23 18:08:41 +08:00
H0nGzA1
fc1ab98b2b build: ⬆️ 更新fast-crud版本 2023-03-23 12:44:09 +08:00
victory
4f176cf88a Merge branch 'dev' of gitee.com:huge-dream/django-vue3-admin into dev 2023-03-22 22:39:22 +08:00
victory
af5f3b7137 feat:添加对url中排序参数的解析 2023-03-22 22:39:19 +08:00
victory
b03037b866 fix:优化角色管理用户管理和接口白名单的页面显示 2023-03-22 22:39:08 +08:00
H0nGzA1
92f57f8608 build: ⬆️ 更新fast-crud版本 2023-03-22 21:08:43 +08:00
H0nGzA1
5ab2aaa066 fix(修复登录界面验证码关闭issue): 🐛 https://gitee.com/huge-dream/django-vue3-admin/issues/I6OS75 2023-03-22 19:56:54 +08:00
H0nGzA1
b5f50bdf30 fix(修复登录界面验证码关闭issue): 🐛 https://gitee.com/huge-dream/django-vue3-admin/issues/I6OS75 2023-03-21 18:01:42 +08:00
H0nGzA1
256c6e4ab9 fix(修复issue): 🐛 https://gitee.com/huge-dream/django-vue3-admin/issues/I6OS1E 2023-03-21 13:43:24 +08:00
168 changed files with 9684 additions and 15059 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@
.idea
.history/
.vscode/
.vscode/
web/package-lock.json

View File

@@ -2,7 +2,7 @@
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[preview](https://demo.django-vue-admin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
[preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「About」**

View File

@@ -2,7 +2,7 @@
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[预 览](https://demo.django-vue-admin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)

3
backend/.gitignore vendored
View File

@@ -91,9 +91,10 @@ ENV/
**/migrations/*.py
!**/migrations/__init__.py
*.pyc
conf/
conf/*
!conf/env.example.py
db.sqlite3
media/
__pypackages__/
package-lock.json
gunicorn.pid

View File

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

View File

@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
import os
import sys
from pathlib import Path
from datetime import timedelta
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -43,6 +44,9 @@ DEBUG = locals().get("DEBUG", True)
ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"])
# Application definition
CUSTOM_APPS = [
"dvadmin.system",
]
INSTALLED_APPS = [
"django.contrib.auth",
@@ -54,10 +58,10 @@ INSTALLED_APPS = [
"rest_framework",
"django_filters",
"corsheaders", # 注册跨域app
"dvadmin.system",
"drf_yasg",
"captcha",
'channels',
*locals().get("CUSTOM_APPS", []), # 所有项目里写的app需要在env.py文件里的CUSTOM_APPS中
]
MIDDLEWARE = [
@@ -165,9 +169,9 @@ CORS_ORIGIN_ALLOW_ALL = True
# 允许cookie
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中后端是否支持对cookie的操作
# ================================================= #
# ===================================================== #
# ********************* channels配置 ******************* #
# ================================================= #
# ===================================================== #
ASGI_APPLICATION = 'application.asgi.application'
CHANNEL_LAYERS = {
"default": {
@@ -187,69 +191,83 @@ CHANNEL_LAYERS = {
# ================================================= #
# ********************* 日志配置 ******************* #
# ================================================= #
# log 配置部分BEGIN #
# # log 配置部分BEGIN #
SERVER_LOGS_FILE = os.path.join(BASE_DIR, "logs", "server.log")
ERROR_LOGS_FILE = os.path.join(BASE_DIR, "logs", "error.log")
LOGS_FILE = os.path.join(BASE_DIR, "logs")
if not os.path.exists(os.path.join(BASE_DIR, "logs")):
os.makedirs(os.path.join(BASE_DIR, "logs"))
# 格式:[2020-04-22 23:33:01][micoservice.apps.ready():16] [INFO] 这是一条日志:
# 格式:[日期][模块.函数名称():行号] [级别] 信息
STANDARD_LOG_FORMAT = (
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
)
CONSOLE_LOG_FORMAT = (
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"servers": {
"format": "%(message)s",
"standard": {"format": STANDARD_LOG_FORMAT},
"console": {
"format": CONSOLE_LOG_FORMAT,
"datefmt": "%Y-%m-%d %H:%M:%S",
}
},
"file": {
"format": CONSOLE_LOG_FORMAT,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"servers": {
"logging_levels": ["info", "error", "warning"],
"class": "dvadmin.utils.log.InterceptTimedRotatingFileHandler", # 这个路径看你本地放在哪里(下面的log文件)
"filename": os.path.join(LOGS_FILE, "server.log"),
"when": "D",
"interval": 1,
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": SERVER_LOGS_FILE,
"maxBytes": 1024 * 1024 * 100, # 100 MB
"backupCount": 1,
"formatter": "servers",
"backupCount": 5, # 最多备份5个
"formatter": "standard",
"encoding": "utf-8",
}
},
"error": {
"level": "ERROR",
"class": "logging.handlers.RotatingFileHandler",
"filename": ERROR_LOGS_FILE,
"maxBytes": 1024 * 1024 * 100, # 100 MB
"backupCount": 3, # 最多备份3个
"formatter": "standard",
"encoding": "utf-8",
},
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "console",
},
},
"loggers": {
# default日志
'django': {
'handlers': ['servers'],
'propagate': False,
'level': "INFO"
"": {
"handlers": ["console", "error", "file"],
"level": "INFO",
},
'': {
'handlers': ['servers'],
'propagate': False,
'level': "ERROR"
},
'celery': {
'handlers': ['servers'],
'propagate': False,
'level': "INFO"
"django": {
"handlers": ["console", "error", "file"],
"level": "INFO",
"propagate": False,
},
'django.db.backends': {
'handlers': ['servers'],
'handlers': ["console", "error", "file"],
'propagate': False,
'level': "INFO"
},
'django.request': {
'handlers': ['servers'],
'propagate': False,
'level': "DEBUG"
},
"uvicorn.error": {
"level": "INFO",
"handlers": ["servers"],
"handlers": ["console", "error", "file"],
},
"uvicorn.access": {
"handlers": ["servers"],
"level": "INFO",
"propagate": False
"handlers": ["console", "error", "file"],
"level": "INFO"
},
},
}
@@ -261,6 +279,7 @@ LOGGING = {
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
),
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置
"DATE_FORMAT": "%Y-%m-%d",
@@ -288,11 +307,9 @@ AUTHENTICATION_BACKENDS = ["dvadmin.utils.backends.CustomBackend"]
# ================================================= #
# ****************** simplejwt配置 ***************** #
# ================================================= #
from datetime import timedelta
SIMPLE_JWT = {
# token有效时长
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=120),
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=1440),
# token刷新后的有效时间
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
# 设置前缀
@@ -328,11 +345,11 @@ SWAGGER_SETTINGS = {
# ================================================= #
# **************** 验证码配置 ******************* #
# ================================================= #
CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小
CAPTCHA_IMAGE_SIZE = (160, 46) # 设置 captcha 图片大小
CAPTCHA_LENGTH = 4 # 字符个数
CAPTCHA_TIMEOUT = 1 # 超时(minutes)
CAPTCHA_OUTPUT_FORMAT = "%(image)s %(text_field)s %(hidden_field)s "
CAPTCHA_FONT_SIZE = 40 # 字体大小
CAPTCHA_FONT_SIZE = 36 # 字体大小
CAPTCHA_FOREGROUND_COLOR = "#64DAAA" # 前景色
CAPTCHA_BACKGROUND_COLOR = "#F5F7F4" # 背景色
CAPTCHA_NOISE_FUNCTIONS = (

View File

@@ -30,6 +30,7 @@ from dvadmin.system.views.login import (
CaptchaView,
ApiLogin,
LogoutView,
LoginTokenView
)
from dvadmin.system.views.system_config import InitSettingsViewSet
from dvadmin.utils.swagger import CustomOpenAPISchemaGenerator
@@ -81,6 +82,9 @@ urlpatterns = (
path("api/init/dictionary/", InitDictionaryViewSet.as_view()),
path("api/init/settings/", InitSettingsViewSet.as_view()),
path("apiLogin/", ApiLogin.as_view()),
# 仅用于开发,上线需关闭
path("api/token/", LoginTokenView.as_view()),
]
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL)

View File

@@ -122,7 +122,8 @@ class MessageCreateSerializer(CustomModelSerializer):
fields = "__all__"
read_only_fields = ["id"]
def websocket_push(user_id,message):
def websocket_push(user_id, message):
username = "user_" + str(user_id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
@@ -133,8 +134,9 @@ def websocket_push(user_id,message):
}
)
def create_message_push(title: str, content: str, target_type: int=0, target_user: list=[], target_dept=None, target_role=None,
message: dict = {'contentType': 'INFO', 'content': '测试~'}, request= Request):
def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None,
target_role=None, message: dict = None, request=Request):
if message is None:
message = {"contentType": "INFO", "content": None}
if target_role is None:
@@ -145,11 +147,11 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use
"title": title,
"content": content,
"target_type": target_type,
"target_user":target_user,
"target_dept":target_dept,
"target_role":target_role
"target_user": target_user,
"target_dept": target_dept,
"target_role": target_role
}
message_center_instance = MessageCreateSerializer(data=data,request=request)
message_center_instance = MessageCreateSerializer(data=data, request=request)
message_center_instance.is_valid(raise_exception=True)
message_center_instance.save()
users = target_user or []
@@ -176,6 +178,6 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use
username,
{
"type": "push.message",
"json": {**message,'unread':unread_count}
"json": {**message, 'unread': unread_count}
}
)

View File

@@ -44,6 +44,6 @@ LOGIN_NO_CAPTCHA_AUTH = True
# ================================================= #
ALLOWED_HOSTS = ["*"]
# daphne启动命令
#daphne application.asgi:application -b 0.0.0.0 -p 8000
CUSTOM_APPS = [
"dvadmin.system",
]

View File

@@ -4,7 +4,7 @@ import os
exclude = ["venv"] # 需要排除的文件目录
for root, dirs, files in os.walk('.'):
dirs[:] = [d for d in set(dirs) - set(exclude)]
dirs[:] = list(set(dirs) - set(exclude))
if 'migrations' in dirs:
dir = dirs[dirs.index('migrations')]
for root_j, dirs_j, files_j in os.walk(os.path.join(root, dir)):

View File

@@ -2,4 +2,4 @@
# python manage.py makemigrations
# python manage.py migrate
# python manage.py init -y
gunicorn -c gunicorn_conf.py application.asgi:application
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 4

View File

@@ -6,8 +6,11 @@ from rest_framework import serializers
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
import django
django.setup()
from dvadmin.system.models import Role, Dept, Users, Menu, MenuButton, ApiWhiteList, Dictionary, SystemConfig, \
from dvadmin.system.models import (
Role, Dept, Users, Menu, MenuButton,
ApiWhiteList, Dictionary, SystemConfig,
RoleMenuPermission, RoleMenuButtonPermission
)
from dvadmin.utils.serializers import CustomModelSerializer
@@ -50,7 +53,6 @@ class MenuButtonInitSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class MenuInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
@@ -139,8 +141,8 @@ class RoleMenuInitSerializer(CustomModelSerializer):
"""
初始化角色菜单(用于生成初始化json文件)
"""
role_key = serializers.CharField(max_length=100,required=True)
menu_component_name = serializers.CharField(max_length=100,required=True)
role_key = serializers.CharField(max_length=100, required=True)
menu_component_name = serializers.CharField(max_length=100, required=True)
def create(self, validated_data):
init_data = self.initial_data
@@ -154,7 +156,7 @@ class RoleMenuInitSerializer(CustomModelSerializer):
class Meta:
model = RoleMenuPermission
fields = ['role_key','menu_component_name','creator', 'dept_belong_id']
fields = ['role_key', 'menu_component_name', 'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'role': {'required': False},
@@ -168,8 +170,8 @@ 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(max_length=100, required=True)
menu_button_value = serializers.CharField(max_length=100, required=True)
data_range = serializers.CharField(max_length=100, required=False)
def create(self, validated_data):
@@ -186,7 +188,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
class Meta:
model = RoleMenuButtonPermission
fields = ['role_key','menu_button_value','data_range','dept','creator', 'dept_belong_id']
fields = ['role_key', 'menu_button_value', 'data_range', 'dept', 'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'role': {'required': False},
@@ -196,7 +198,6 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
}
class ApiWhiteListInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)

View File

@@ -6,12 +6,11 @@
"is_link": false,
"is_catalog": true,
"web_path": "/system",
"component": "layout/routerView/parent",
"component_name": "menu",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
"parent": null,
"children": [
{
"name": "菜单管理",
@@ -25,7 +24,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -57,6 +55,30 @@
"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:MoveUp",
"api": "/api/system/menu/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "menu:MoveDown",
"api": "/api/system/menu/mode_down/",
"method": 1
}
]
},
@@ -72,7 +94,6 @@
"status": true,
"cache": false,
"visible": false,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -113,7 +134,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -145,6 +165,36 @@
"value": "dept:Delete",
"api": "/api/system/dept/{id}/",
"method": 3
},
{
"name": "查询所有",
"value": "dept:SearchAll",
"api": "/api/system/dept/all_dept/",
"method": 0
},
{
"name": "懒加载查询所有",
"value": "dept:LazySearchAll",
"api": "/api/system/dept/dept_lazy_tree/",
"method": 0
},
{
"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:HeaderInfo",
"api": "/api/system/dept/dept_info/",
"method": 0
}
]
},
@@ -160,7 +210,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -201,6 +250,64 @@
}
]
},
{
"name": "列管理",
"icon": "iconfont icon-bolangneng",
"sort": 5,
"is_link": false,
"is_catalog": false,
"web_path": "/columns",
"component": "system/columns/index",
"component_name": "columns",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "column:Search",
"api": "/api/system/column/",
"method": 0
},
{
"name": "详情",
"value": "column:Retrieve",
"api": "/api/system/column/{id}/",
"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:AllModel",
"api": "/api/system/column/get_models/",
"method": 0
},
{
"name": "自动匹配所有字段",
"value": "column:AutoMatch",
"api": "/api/system/column/auto_match_fields/",
"method": 1
}
]
},
{
"name": "用户管理",
"icon": "iconfont icon-icon-",
@@ -213,7 +320,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -284,7 +390,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -331,7 +436,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"children": [],
"menu_button": [
{
@@ -376,12 +480,11 @@
"is_link": false,
"is_catalog": true,
"web_path": "/generalConfig",
"component": "layout/routerView/parent",
"component_name": "config",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
"parent": null,
"children": [
{
"name": "系统配置",
@@ -395,7 +498,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"children": [],
"menu_button": [
{
@@ -442,7 +544,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"children": [],
"menu_button": [
{
@@ -489,7 +590,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"children": [],
"menu_button": [
{
@@ -536,7 +636,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"children": [],
"menu_button": [
{
@@ -575,12 +674,11 @@
"is_link": false,
"is_catalog": true,
"web_path": "/log",
"component": "layout/routerView/parent",
"component_name": "log",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
"parent": null,
"children": [
{
"name": "登录日志",
@@ -594,7 +692,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 32,
"children": [],
"menu_button": [
{
@@ -623,7 +720,6 @@
"status": true,
"cache": false,
"visible": true,
"parent": 32,
"children": [],
"menu_button": [
{

View File

@@ -29,6 +29,7 @@
"gender": 1,
"user_type": 0,
"role": [],
"dept_key": "dvadmin",
"first_name": "",
"last_name": "",
"is_staff": true,

View File

@@ -8,9 +8,11 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
django.setup()
from dvadmin.utils.core_initialize import CoreInitialize
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
RoleMenuInitSerializer, RoleMenuButtonInitSerializer
from dvadmin.system.fixtures.initSerializer import (
UsersInitSerializer, DeptInitSerializer, RoleInitSerializer,
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer,
SystemConfigInitSerializer, RoleMenuInitSerializer, RoleMenuButtonInitSerializer
)
class Initialize(CoreInitialize):
@@ -51,7 +53,6 @@ class Initialize(CoreInitialize):
"""
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role', 'menu_button'])
def init_api_white_list(self):
"""
初始API白名单

View File

@@ -1,16 +1,39 @@
import hashlib
import os
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from application import dispatch
from dvadmin.utils.models import CoreModel, table_prefix
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
STATUS_CHOICES = (
(0, "禁用"),
(1, "启用"),
)
class Role(CoreModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
class Meta:
db_table = table_prefix + "system_role"
verbose_name = "角色表"
verbose_name_plural = verbose_name
ordering = ("sort",)
class CustomUserManager(UserManager):
def create_superuser(self, username, email=None, password=None, **extra_fields):
user = super(CustomUserManager, self).create_superuser(username, email, password, **extra_fields)
user.set_password(password)
try:
user.role.add(Role.objects.get(name="管理员"))
user.save(using=self._db)
return user
except ObjectDoesNotExist:
user.delete()
raise ValidationError("角色`管理员`不存在, 创建失败, 请先执行python manage.py init")
class Users(CoreModel, AbstractUser):
@@ -48,6 +71,7 @@ class Users(CoreModel, AbstractUser):
blank=True,
help_text="关联部门",
)
objects = CustomUserManager()
def set_password(self, raw_password):
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
@@ -76,24 +100,9 @@ class Post(CoreModel):
ordering = ("sort",)
class Role(CoreModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
class Meta:
db_table = table_prefix + "system_role"
verbose_name = "角色表"
verbose_name_plural = verbose_name
ordering = ("sort",)
class Dept(CoreModel):
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符",
help_text="关联字符")
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符", help_text="关联字符")
sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
@@ -171,6 +180,24 @@ class Menu(CoreModel):
ordering = ("sort",)
class Columns(CoreModel):
role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
app = models.CharField(max_length=64, verbose_name='应用名')
model = models.CharField(max_length=64, verbose_name='表名')
menu = models.ForeignKey(to='Menu', on_delete=models.CASCADE, verbose_name='菜单', db_constraint=False)
field_name = models.CharField(max_length=64, verbose_name='模型表字段名')
title = models.CharField(max_length=64, verbose_name='字段显示名')
is_query = models.BooleanField(default=1, verbose_name='是否可查询')
is_create = models.BooleanField(default=1, verbose_name='是否可创建')
is_update = models.BooleanField(default=1, verbose_name='是否可更新')
class Meta:
db_table = table_prefix + "system_columns"
verbose_name = "列权限表"
verbose_name_plural = verbose_name
ordering = ("id",)
class MenuButton(CoreModel):
menu = models.ForeignKey(
to="Menu",
@@ -221,7 +248,7 @@ class RoleMenuPermission(CoreModel):
db_table = table_prefix + "role_menu_permission"
verbose_name = "角色菜单权限表"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
# ordering = ("-create_datetime",)
class RoleMenuButtonPermission(CoreModel):
@@ -274,8 +301,7 @@ class Dictionary(CoreModel):
(7, "images"),
)
label = models.CharField(max_length=100, blank=True, null=True, verbose_name="字典名称", help_text="字典名称")
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号",
help_text="字典编号/实际值")
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号", help_text="字典编号/实际值")
parent = models.ForeignKey(
to="self",
related_name="sublist",
@@ -339,12 +365,16 @@ class OperationLog(CoreModel):
def media_file_name(instance, filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
return os.path.join("files", h[0:1], h[1:2], h + ext.lower())
return os.path.join("files", h[:1], h[1:2], h + ext.lower())
class FileList(CoreModel):
name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称")
url = models.FileField(upload_to=media_file_name)
url = models.FileField(upload_to=media_file_name, null=True, blank=True,)
file_url = models.CharField(max_length=255, blank=True, verbose_name="文件地址", help_text="文件地址")
engine = models.CharField(max_length=100, default='local', blank=True, verbose_name="引擎", help_text="引擎")
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
def save(self, *args, **kwargs):
@@ -353,6 +383,11 @@ class FileList(CoreModel):
for chunk in self.url.chunks():
md5.update(chunk)
self.md5sum = md5.hexdigest()
if not self.size:
self.size = self.url.size
if not self.file_url:
url = media_file_name(self, self.name)
self.file_url = f'media/{url}'
super(FileList, self).save(*args, **kwargs)
class Meta:

View File

@@ -16,6 +16,7 @@ from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermissionViewSet
from dvadmin.system.views.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet
from dvadmin.system.views.column import ColumnViewSet
system_url = routers.SimpleRouter()
system_url.register(r'menu', MenuViewSet)
@@ -29,11 +30,10 @@ system_url.register(r'area', AreaViewSet)
system_url.register(r'file', FileViewSet)
system_url.register(r'api_white_list', ApiWhiteListViewSet)
system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center',MessageCenterViewSet)
system_url.register(r'message_center', MessageCenterViewSet)
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', ColumnViewSet)
urlpatterns = [

View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Columns, Role
from dvadmin.utils.models import get_custom_app_models
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse
class ColumnSerializer(CustomModelSerializer):
"""
列权限序列化器
"""
class Meta:
model = Columns
fields = '__all__'
read_only_fields = ['id']
class ColumnViewSet(CustomModelViewSet):
"""
列权限视图集
"""
queryset = Columns.objects.all()
serializer_class = ColumnSerializer
def list(self, request, *args, **kwargs):
role_id = request.query_params.get('role')
app_name = request.query_params.get('app')
model_name = request.query_params.get('model')
menu = request.query_params.get('menu')
if not role_id or not model_name or not app_name or not menu:
return SuccessResponse([])
queryset = self.filter_queryset(self.get_queryset().filter(role_id=role_id, model=model_name, app=app_name,menu_id=menu))
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")
def create(self, request, *args, **kwargs):
payload = request.data
for model in get_custom_app_models(payload.get('app')):
if payload.get('model') == model['model']:
break
else:
return ErrorResponse(msg='模型表不存在')
if Columns.objects.filter(app=model['app'], model=model['model'], field_name=payload.get('field_name')).exists():
return ErrorResponse(msg='%s 字段权限已有,不可重复创建' % payload.get('title'))
return super().create(request, *args, **kwargs)
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_models(self, request):
"""获取所有项目app下的model"""
res = []
for app in get_custom_app_models():
for model in app:
res.append({
'app': model['app'],
'title': model['verbose'],
'key': model['model']
})
return DetailResponse(res)
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
def auto_match_fields(self, request):
"""自动匹配已有的字段"""
role_id = request.data.get('role')
app_name = request.data.get('app')
model_name = request.data.get('model')
if not role_id or not model_name or not app_name:
return DetailResponse([], msg='无操作')
for model in get_custom_app_models(app_name):
if model['model'] != model_name:
continue
for field in model['fields']:
if Columns.objects.filter(
role_id=role_id, app=app_name, model=model_name, field_name=field['name']
).exists():
continue
data = {
'role': role_id,
'app': app_name,
'model': model_name,
'field_name': field['name'],
'title': str(field['title']),
}
serializer = self.get_serializer(data=data, request=request)
serializer.is_valid(raise_exception=True)
serializer.save()
return SuccessResponse(msg='匹配成功')

View File

@@ -9,9 +9,8 @@ from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Dept
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.permission import AnonymousUserPermission
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -25,6 +24,11 @@ class DeptSerializer(CustomModelSerializer):
has_children = serializers.SerializerMethodField()
hasChild = serializers.SerializerMethodField()
dept_user_count = serializers.SerializerMethodField()
def get_dept_user_count(self, obj: Dept):
return Users.objects.filter(dept=obj).count()
def get_hasChild(self, instance):
hasChild = Dept.objects.filter(parent=instance.id)
if hasChild:
@@ -56,18 +60,18 @@ class DeptImportSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class DeptCreateUpdateSerializer(CustomModelSerializer):
"""
部门管理 创建/更新时的列化器
"""
def create(self, validated_data):
value = validated_data.get('parent',None)
value = validated_data.get('parent', None)
if value is None:
validated_data['parent'] = self.request.user.dept
dept_obj = Dept.objects.filter(parent=self.request.user.dept).order_by('-sort').first()
last_sort = dept_obj.sort if dept_obj else 0
validated_data['sort'] = last_sort + 1
instance = super().create(validated_data)
instance.dept_belong_id = instance.id
instance.save()
@@ -123,33 +127,114 @@ class DeptViewSet(CustomModelViewSet):
data = serializer.data
return SuccessResponse(data=data)
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
def dept_lazy_tree(self, request, *args, **kwargs):
parent = self.request.query_params.get('parent')
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Dept.objects.values('id', 'name', 'parent')
else:
data_range = request.user.role.values_list('data_range', flat=True)
role_ids = request.user.role.values_list('id', flat=True)
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
user_dept_id = request.user.dept.id
dept_list = [user_dept_id]
data_range_list = list(set(data_range))
for item in data_range_list:
if item in [0,2]:
if item in [0, 2]:
dept_list = [user_dept_id]
elif item == 1:
dept_list = Dept.recursion_dept_info(dept_id=user_dept_id)
elif item == 3:
dept_list = Dept.objects.values_list('id',flat=True)
dept_list = Dept.objects.values_list('id', flat=True)
elif item == 4:
dept_list = request.user.role.values_list('dept',flat=True)
dept_list = request.user.role.values_list('dept', flat=True)
else:
dept_list = []
queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent')
return DetailResponse(data=queryset, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated],extra_filter_class=[])
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
def all_dept(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')
return DetailResponse(data=data, msg="获取成功")
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
def move_up(self, request):
"""部门上移"""
dept_id = request.data.get('dept_id')
try:
dept = Dept.objects.get(id=dept_id)
except Dept.DoesNotExist:
return ErrorResponse(msg="部门不存在")
previous_menu = Dept.objects.filter(sort__lt=dept.sort, parent=dept.parent).order_by('-sort').first()
if previous_menu:
previous_menu.sort, dept.sort = dept.sort, previous_menu.sort
previous_menu.save()
dept.save()
return SuccessResponse(data=[], msg="上移成功")
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
def move_down(self, request):
"""部门下移"""
dept_id = request.data['dept_id']
try:
dept = Dept.objects.get(id=dept_id)
except Dept.DoesNotExist:
return ErrorResponse(msg="部门不存在")
next_menu = Dept.objects.filter(sort__gt=dept.sort, parent=dept.parent).order_by('sort').first()
if next_menu:
next_menu.sort, dept.sort = dept.sort, next_menu.sort
next_menu.save()
dept.save()
return SuccessResponse(data=[], msg="下移成功")
@action(methods=['GET'], detail=False, permission_classes=[])
def dept_info(self, request):
"""部门信息"""
def inner(did, li):
sub = Dept.objects.filter(parent_id=did)
if not sub.exists():
return li
for i in sub:
li.append(i.pk)
inner(i, li)
return li
dept_id = request.query_params.get('dept_id')
show_all = request.query_params.get('show_all')
if dept_id is None:
return ErrorResponse(msg="部门不存在")
if not show_all:
show_all = 0
if int(show_all): # 递归当前部门下的所有部门,查询用户
all_did = [dept_id]
inner(dept_id, all_did)
users = Users.objects.filter(dept_id__in=all_did)
else:
if dept_id != '':
users = Users.objects.filter(dept_id=dept_id)
else:
users = Users.objects.none()
dept_obj = Dept.objects.get(id=dept_id) if dept_id != '' else None
sub_dept = Dept.objects.filter(parent_id=dept_obj.pk) if dept_id != '' else []
data = {
'dept_name': dept_obj and dept_obj.name,
'dept_user': users.count(),
'owner': dept_obj and dept_obj.owner,
'description': dept_obj and dept_obj.description,
'gender': {
'male': users.filter(gender=1).count(),
'female': users.filter(gender=2).count(),
'unknown': users.filter(gender=0).count(),
},
'sub_dept_map': []
}
for dept in sub_dept:
all_did = [dept.pk]
inner(dept.pk, all_did)
sub_data = {
'name': dept.name,
'count': Users.objects.filter(dept_id__in=all_did).count()
}
data['sub_dept_map'].append(sub_data)
return SuccessResponse(data)

View File

@@ -34,6 +34,19 @@ class DictionaryCreateUpdateSerializer(CustomModelSerializer):
"""
字典管理 创建/更新时的列化器
"""
value = serializers.CharField(max_length=100)
def validate_value(self, value):
"""
在父级的字典编号验证重复性
"""
initial_data = self.initial_data
parent = initial_data.get('parent',None)
if parent is None:
unique = Dictionary.objects.filter(value=value).exists()
if unique:
raise serializers.ValidationError("字典编号不能重复")
return value
class Meta:
model = Dictionary
@@ -51,20 +64,24 @@ class DictionaryViewSet(CustomModelViewSet):
"""
queryset = Dictionary.objects.all()
serializer_class = DictionarySerializer
create_serializer_class = DictionaryCreateUpdateSerializer
extra_filter_class = []
search_fields = ['label']
def get_queryset(self):
params = self.request.query_params
parent = params.get('parent', None)
if params:
if parent:
queryset = self.queryset.filter(status=1, parent=parent)
if self.action =='list':
params = self.request.query_params
parent = params.get('parent', None)
if params:
if parent:
queryset = self.queryset.filter(parent=parent)
else:
queryset = self.queryset.filter(parent__isnull=True)
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
queryset = self.queryset.filter(parent__isnull=True)
return queryset
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
return queryset
return self.queryset
class InitDictionaryViewSet(APIView):

View File

@@ -1,6 +1,12 @@
from rest_framework import serializers
import hashlib
import mimetypes
from rest_framework import serializers
from rest_framework.decorators import action
from application import dispatch
from dvadmin.system.models import FileList
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -9,15 +15,52 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
return 'media/' + str(instance.url)
# return 'media/' + str(instance.url)
return instance.file_url or (f'media/{str(instance.url)}')
class Meta:
model = FileList
fields = "__all__"
def create(self, validated_data):
validated_data['name'] = str(self.initial_data.get('file'))
validated_data['url'] = self.initial_data.get('file')
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
file = self.initial_data.get('file')
file_size = file.size
validated_data['name'] = str(file)
validated_data['size'] = file_size
md5 = hashlib.md5()
for chunk in file.chunks():
md5.update(chunk)
validated_data['md5sum'] = md5.hexdigest()
validated_data['engine'] = file_engine
validated_data['mime_type'] = file.content_type
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)
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)
if file_path:
validated_data['file_url'] = file_path
else:
raise ValueError("上传失败")
else:
validated_data['url'] = file
# 审计字段
try:
request_user = self.request.user
validated_data['dept_belong_id'] = request_user.dept.id
validated_data['creator'] = request_user.id
validated_data['modifier'] = request_user.id
except:
pass
return super().create(validated_data)
@@ -33,4 +76,4 @@ class FileViewSet(CustomModelViewSet):
queryset = FileList.objects.all()
serializer_class = FileSerializer
filter_fields = ['name', ]
permission_classes = []
permission_classes = []

View File

@@ -1,11 +1,9 @@
import base64
import hashlib
from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image
from django.contrib import auth
from django.contrib.auth import login
from django.contrib.auth.hashers import make_password, check_password
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
@@ -14,9 +12,7 @@ from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
from django.conf import settings
from application import dispatch
from dvadmin.system.models import Users
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
@@ -58,6 +54,7 @@ class LoginSerializer(TokenObtainPairSerializer):
captcha = serializers.CharField(
max_length=6, required=False, allow_null=True, allow_blank=True
)
class Meta:
model = Users
fields = "__all__"
@@ -79,13 +76,18 @@ class LoginSerializer(TokenObtainPairSerializer):
raise CustomValidationError("验证码过期")
else:
if self.image_code and (
self.image_code.response == captcha
or self.image_code.challenge == captcha
self.image_code.response == captcha
or self.image_code.challenge == captcha
):
self.image_code and self.image_code.delete()
else:
self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误")
user = Users.objects.get(username=attrs['username'])
if not user.is_active:
raise CustomValidationError("账号被锁定")
data = super().validate(attrs)
data["name"] = self.user.name
data["userId"] = self.user.id
@@ -107,6 +109,7 @@ class LoginSerializer(TokenObtainPairSerializer):
save_login_log(request=request)
return {"code": 2000, "msg": "请求成功", "data": data}
class LoginView(TokenObtainPairView):
"""
登录接口

View File

@@ -9,8 +9,9 @@
from rest_framework import serializers
from rest_framework.decorators import action
from dvadmin.system.models import Menu, MenuButton, RoleMenuPermission
from dvadmin.utils.json_response import SuccessResponse
from dvadmin.system.models import Menu, RoleMenuPermission
from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -23,7 +24,8 @@ class MenuSerializer(CustomModelSerializer):
hasChild = serializers.SerializerMethodField()
def get_menuPermission(self, instance):
queryset = instance.menuPermission.order_by('-name').values_list('name', flat=True)
queryset = instance.menuPermission.order_by('-name').values('id', 'name', 'value')
# MenuButtonSerializer(instance.menuPermission.all(), many=True)
if queryset:
return queryset
else:
@@ -47,15 +49,18 @@ class MenuCreateSerializer(CustomModelSerializer):
"""
name = serializers.CharField(required=False)
def create(self, validated_data):
menu_obj = Menu.objects.filter(parent_id=validated_data.get('parent', None)).order_by('-sort').first()
last_sort = menu_obj.sort if menu_obj else 0
validated_data['sort'] = last_sort + 1
return super().create(validated_data)
class Meta:
model = Menu
fields = "__all__"
read_only_fields = ["id"]
class WebRouterSerializer(CustomModelSerializer):
"""
前端菜单路由的简单序列化器
@@ -63,11 +68,11 @@ class WebRouterSerializer(CustomModelSerializer):
path = serializers.CharField(source="web_path")
title = serializers.CharField(source="name")
class Meta:
model = Menu
fields = ('id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible')
fields = (
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible', 'status')
read_only_fields = ["id"]
@@ -86,13 +91,50 @@ class MenuViewSet(CustomModelViewSet):
update_serializer_class = MenuCreateSerializer
search_fields = ['name', 'status']
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
# extra_filter_class = []
def list(self, request):
"""懒加载"""
request.query_params._mutable = True
params = request.query_params
parent = params.get('parent', None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params:
if parent:
queryset = self.queryset.filter(parent=parent)
else:
queryset = self.queryset.filter()
else:
queryset = self.queryset.filter(parent__isnull=True)
queryset = self.filter_queryset(queryset)
serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data)
@action(methods=['GET'], detail=False, permission_classes=[])
def web_router(self, request):
"""用于前端获取当前角色的路由"""
user = request.user
queryset = self.queryset.filter(status=1)
is_admin = user.role.values_list('admin', flat=True)
if user.is_superuser or True in is_admin:
queryset = self.queryset.filter(status=1)
else:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
queryset = Menu.objects.filter(id__in=menu_list)
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[])
def get_all_menu(self, request):
"""用于菜单管理获取所有的菜单"""
user = request.user
queryset = self.queryset.all()
if not user.is_superuser:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
@@ -101,25 +143,32 @@ class MenuViewSet(CustomModelViewSet):
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
def list(self,request):
"""懒加载"""
request.query_params._mutable = True
params = request.query_params
parent = params.get('parent', None)
page = params.get('page',None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params:
if parent:
queryset = self.queryset.filter(status=1, parent=parent)
else:
queryset = self.queryset.filter(status=1)
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
queryset = self.filter_queryset(queryset)
serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data)
@action(methods=['POST'], detail=False, permission_classes=[])
def move_up(self, request):
"""菜单上移"""
menu_id = request.data.get('menu_id')
try:
menu = Menu.objects.get(id=menu_id)
except Menu.DoesNotExist:
return ErrorResponse(msg="菜单不存在")
previous_menu = Menu.objects.filter(sort__lt=menu.sort, parent=menu.parent).order_by('-sort').first()
if previous_menu:
previous_menu.sort, menu.sort = menu.sort, previous_menu.sort
previous_menu.save()
menu.save()
return SuccessResponse(data=[], msg="上移成功")
@action(methods=['POST'], detail=False, permission_classes=[])
def move_down(self, request):
"""菜单下移"""
menu_id = request.data['menu_id']
try:
menu = Menu.objects.get(id=menu_id)
except Menu.DoesNotExist:
return ErrorResponse(msg="菜单不存在")
next_menu = Menu.objects.filter(sort__gt=menu.sort, parent=menu.parent).order_by('sort').first()
if next_menu:
next_menu.sort, menu.sort = menu.sort, next_menu.sort
next_menu.save()
menu.save()
return SuccessResponse(data=[], msg="下移成功")

View File

@@ -23,7 +23,7 @@ class MenuButtonSerializer(CustomModelSerializer):
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method']
fields = ['id', 'name', 'value', 'api', 'method','menu']
read_only_fields = ["id"]

View File

@@ -85,7 +85,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
def get_is_read(self, instance):
user_id = self.request.user.id
message_center_id = instance.id
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id,users_id=user_id).first()
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id, users_id=user_id).first()
if queryset:
return queryset.is_read
return False
@@ -95,21 +95,22 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
fields = "__all__"
read_only_fields = ["id"]
def websocket_push(user_id, message):
"""
主动推送消息
"""
username = "user_"+str(user_id)
print(103,message)
username = "user_" + str(user_id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
username,
{
"type": "push.message",
"json": message
}
username,
{
"type": "push.message",
"json": message
}
)
class MessageCenterCreateSerializer(CustomModelSerializer):
"""
消息中心-新增-序列化器
@@ -122,10 +123,10 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
# 在保存之前,根据目标类型,把目标用户查询出来并保存
users = initial_data.get('target_user', [])
if target_type in [1]: # 按角色
target_role = initial_data.get('target_role',[])
target_role = initial_data.get('target_role', [])
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
if target_type in [2]: # 按部门
target_dept = initial_data.get('target_dept',[])
target_dept = initial_data.get('target_dept', [])
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
if target_type in [3]: # 系统通知
users = Users.objects.values_list('id', flat=True)
@@ -141,7 +142,7 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
for user in users:
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM',
"content": '您有一条新消息~', "unread": unread_count})
"content": '您有一条新消息~', "unread": unread_count})
return data
class Meta:
@@ -184,7 +185,7 @@ class MessageCenterViewSet(CustomModelViewSet):
# 主动推送消息
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT',
"content": '您查看了一条消息~', "unread": unread_count})
"content": '您查看了一条消息~', "unread": unread_count})
return DetailResponse(data=serializer.data, msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
@@ -195,7 +196,6 @@ class MessageCenterViewSet(CustomModelViewSet):
self_user_id = self.request.user.id
# queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime')
queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
print(queryset)
# queryset = self.filter_queryset(queryset)
page = self.paginate_queryset(queryset)
if page is not None:

View File

@@ -15,6 +15,7 @@ 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.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator
@@ -32,9 +33,6 @@ class RoleSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class RoleCreateUpdateSerializer(CustomModelSerializer):
"""
角色管理 创建/更新时的列化器
@@ -61,7 +59,7 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
fields = '__all__'
class MenuPermissonSerializer(CustomModelSerializer):
class MenuPermissionSerializer(CustomModelSerializer):
"""
菜单的按钮权限
"""
@@ -72,9 +70,9 @@ class MenuPermissonSerializer(CustomModelSerializer):
if is_superuser:
queryset = MenuButton.objects.filter(menu__id=instance.id)
else:
menu_permission_id_list = self.request.user.role.values_list('permission',flat=True)
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list,menu__id=instance.id)
serializer = MenuButtonSerializer(queryset,many=True, read_only=True)
menu_permission_id_list = self.request.user.role.values_list('permission', flat=True)
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list, menu__id=instance.id)
serializer = MenuButtonSerializer(queryset, many=True, read_only=True)
return serializer.data
class Meta:
@@ -82,7 +80,29 @@ class MenuPermissonSerializer(CustomModelSerializer):
fields = ['id', 'parent', 'name', 'menuPermission']
class RoleViewSet(CustomModelViewSet,FastCrudMixin):
class MenuButtonPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
isCheck = serializers.SerializerMethodField()
def get_isCheck(self, instance):
is_superuser = self.request.user.is_superuser
if is_superuser:
return True
else:
return MenuButton.objects.filter(
menu__id=instance.id,
role__id__in=self.request.user.role.values_list('id', flat=True),
).exists()
class Meta:
model = Menu
fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
"""
角色管理接口
list:查询

View File

@@ -72,7 +72,14 @@ class RoleMenuPermissionViewSet(CustomModelViewSet):
menu_list = body.get('menu',None)
if menu_list is None:
return ErrorResponse(msg="未获取到菜单参数")
data = [{"role":role_id,"menu":item} for item in menu_list]
obj_list = RoleMenuPermission.objects.filter(role__id=role_id).values_list('menu__id',flat=True)
old_set = set(obj_list)
new_set = set(menu_list)
#need_update = old_set.intersection(new_set) # 需要更新的
need_del = old_set.difference(new_set) # 需要删除的
need_add = new_set.difference(old_set) # 需要新增的
RoleMenuPermission.objects.filter(role__id=role_id,menu__in=list(need_del)).delete()
data = [{"role": role_id, "menu": item} for item in list(need_add)]
serializer = RoleMenuPermissionSerializer(data=data,many=True,request=request)
if serializer.is_valid(raise_exception=True):
serializer.save()

View File

@@ -6,11 +6,13 @@
@Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理
"""
from django.db.models import F
from django.db.models import F, Subquery, OuterRef, Exists
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, Columns
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -20,27 +22,19 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
"""
菜单按钮-序列化器
"""
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuButtonPermissionInitSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
class Meta:
model = RoleMenuButtonPermission
@@ -48,6 +42,74 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class RoleButtonPermissionSerializer(CustomModelSerializer):
"""
角色按钮权限
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_data_range(self, instance):
params = self.request.query_params
obj = RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).first()
if obj is None:
return None
return obj.data_range
class Meta:
model = MenuButton
fields = ['id','name','value','isCheck','data_range']
class RoleColumnsSerializer(CustomModelSerializer):
class Meta:
model = Columns
fields = "__all__"
class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuPermission.objects.filter(
menu__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_btns(self, instance):
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request)
return serializer.data
def get_columns(self, instance):
params = self.request.query_params
col_list = Columns.objects.filter(role__id=params.get('role'),menu__id=instance['id'])
serializer = RoleColumnsSerializer(col_list,many=True,request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id','name','isCheck','btns','columns']
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
"""
菜单按钮接口
@@ -64,38 +126,71 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_get_menu(self, request):
"""根据当前用户的角色返回角色拥有的菜单"""
def get_role_premission(self, request):
"""
角色授权获取:
:param request: role
:return: menu,btns,columns
"""
params = request.query_params
role = params.get('role',None)
if role is None:
return ErrorResponse(msg="未获取到角色信息")
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = Menu.objects.filter(status=1).values('id','name','parent','is_catalog')
if is_superuser:
queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all()
else:
role_id = request.user.role.values_list('id',flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values(id=F('menu__id'),name=F('menu__name'),parent=F('menu__parent'),is_catalog=F('menu__is_catalog'))
return DetailResponse(data=queryset)
role_id = request.user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id').all()
serializer = RoleMenuPermissionSerializer(queryset,many=True,request=request)
data = serializer.data
return DetailResponse(data=data)
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_premission(self,request,pk):
"""
对角色的菜单和按钮及按钮范围授权:
:param request:
:param pk: role
:return:
"""
body = request.data
RoleMenuPermission.objects.filter(role=pk).delete()
RoleMenuButtonPermission.objects.filter(role=pk).delete()
for menu in body:
if menu.get('isCheck'):
menu_parent = Menu.objects.filter(id=menu.get('id')).values('parent').first()
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu_parent.get('parent'))
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
for btn in menu.get('btns'):
if btn.get('isCheck'):
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=btn.get('data_range'))
instance.dept.set(btn.get('dept',[]))
for col in menu.get('columns'):
Columns.objects.filter(id=col.get('id')).update(is_query=col.get('is_query'),is_create=col.get('is_create'),is_update=col.get('is_update'))
return DetailResponse(msg="授权成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_menu_get_button(self,request):
def role_menu_get_button(self, request):
"""
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
if params:
menu_id = params.get('menu',None)
if menu_id:
if params := request.query_params:
if menu_id := params.get('menu', None):
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
else:
role_list = request.user.role.values_list('id',flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role_in=role_list,menu_button__menu=menu_id).values(
id=F('menu_button__id'),
name=F('menu_button__name')
)
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__menu=menu_id
).values(btn_id=F('menu_button__id'), name=F('menu_button__name'))
return DetailResponse(data=queryset)
return ErrorResponse(msg="参数错误")
@@ -133,12 +228,12 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
return DetailResponse(data=data)
else:
data = []
role_id = request.user.role.id
params = request.query_params
if params:
menu_button_id = params.get('menu_button', None)
if menu_button_id:
role_queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button=menu_button_id).values_list('data_range',flat=True)
role_list = request.user.role.values_list('id', flat=True)
if params := request.query_params:
if menu_button_id := params.get('menu_button', None):
role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__id=menu_button_id
).values_list('data_range', flat=True)
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
@@ -201,44 +296,70 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = Dept.objects.values('id','name','parent')
return DetailResponse(data=queryset)
queryset = Dept.objects.values('id', 'name', 'parent')
else:
if params:
menu_button = params.get('menu_button')
if menu_button is None:
return ErrorResponse(msg="参数错误")
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role_in=role_list,menu_button=None).values(
id=F('dept__id'),
name=F('dept__name'),
parent=F('dept__parent')
)
return DetailResponse(data=queryset)
else:
if not params:
return ErrorResponse(msg="参数错误")
menu_button = params.get('menu_button')
if menu_button is None:
return ErrorResponse(msg="参数错误")
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values(
dept_id=F('dept__id'),
name=F('dept__name'),
parent=F('dept__parent')
)
return DetailResponse(data=queryset)
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
def menu_to_button(self,request):
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def menu_to_button(self, request):
"""
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
if params:
menu_id = params.get('menu',None)
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button__menu=menu_id).values(
menu_id = params.get('menu', None)
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button'
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="未获取到参数")
else:
if params:
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="未获取到参数")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_menu(self, request):
"""
获取角色对应的按钮权限
:param request:
:return:
"""
params = request.query_params
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
return DetailResponse(data=queryset)

View File

@@ -61,10 +61,10 @@ class SystemConfigChinldernSerializer(CustomModelSerializer):
"""
系统配置子级-序列化器
"""
chinldern = serializers.SerializerMethodField()
children = serializers.SerializerMethodField()
form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True)
def get_chinldern(self, instance):
def get_children(self, instance):
queryset = SystemConfig.objects.filter(parent=instance)
serializer = SystemConfigSerializer(queryset, many=True)
return serializer.data

View File

@@ -1,15 +1,16 @@
import hashlib
from django.contrib.auth.hashers import make_password
from django.contrib.auth.hashers import make_password, check_password
from django_restql.fields import DynamicSerializerMethodField
from rest_framework import serializers
from rest_framework.decorators import action, permission_classes
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from django.db import connection
from django.db.models import Q
from application import dispatch
from dvadmin.system.models import Users, Role, Dept
from dvadmin.system.views.role import RoleSerializer
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
from dvadmin.utils.json_response import ErrorResponse, DetailResponse, SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet
@@ -23,7 +24,7 @@ def recursion(instance, parent, result):
res.append(data)
if new_instance:
array = recursion(new_instance, parent, result)
res += (array)
res += array
return res
@@ -41,6 +42,7 @@ class UserSerializer(CustomModelSerializer):
exclude = ["password"]
extra_kwargs = {
"post": {"required": False},
"mobile": {"required": False},
}
def get_dept_name_all(self, instance):
@@ -60,9 +62,6 @@ class UserSerializer(CustomModelSerializer):
return serializer.data
class UserCreateSerializer(CustomModelSerializer):
"""
用户新增-序列化器
@@ -82,10 +81,10 @@ class UserCreateSerializer(CustomModelSerializer):
"""
对密码进行验证
"""
password = self.initial_data.get("password")
if password:
return make_password(value)
return value
md5 = hashlib.md5()
md5.update(value.encode('utf-8'))
md5_password = md5.hexdigest()
return make_password(md5_password)
def save(self, **kwargs):
data = super().save(**kwargs)
@@ -100,6 +99,7 @@ class UserCreateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
extra_kwargs = {
"post": {"required": False},
"mobile": {"required": False},
}
@@ -114,14 +114,15 @@ class UserUpdateSerializer(CustomModelSerializer):
CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一")
],
)
# password = serializers.CharField(required=False, allow_blank=True)
mobile = serializers.CharField(
max_length=50,
validators=[
CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
],
allow_blank=True
)
# mobile = serializers.CharField(
# max_length=50,
# validators=[
# CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
# ],
# allow_blank=True
# )
def save(self, **kwargs):
data = super().save(**kwargs)
@@ -136,6 +137,7 @@ class UserUpdateSerializer(CustomModelSerializer):
fields = "__all__"
extra_kwargs = {
"post": {"required": False, "read_only": True},
"mobile": {"required": False},
}
@@ -159,6 +161,7 @@ class UserInfoUpdateSerializer(CustomModelSerializer):
fields = ['email', 'mobile', 'avatar', 'name', 'gender']
extra_kwargs = {
"post": {"required": False, "read_only": True},
"mobile": {"required": False},
}
@@ -194,10 +197,12 @@ class ExportUserProfileSerializer(CustomModelSerializer):
class UserProfileImportSerializer(CustomModelSerializer):
password = serializers.CharField(read_only=True, required=False)
def save(self, **kwargs):
data = super().save(**kwargs)
password = hashlib.new(
"md5", str(self.initial_data.get("password", "")).encode(encoding="UTF-8")
"md5", str(self.initial_data.get("password", "admin123456")).encode(encoding="UTF-8")
).hexdigest()
data.set_password(password)
data.save()
@@ -229,7 +234,7 @@ class UserViewSet(CustomModelViewSet):
create_serializer_class = UserCreateSerializer
update_serializer_class = UserUpdateSerializer
filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"]
search_fields = ["username", "name", "gender", "dept__name", "role__name"]
search_fields = ["username", "name", "dept__name", "role__name"]
# 导出
export_field_label = {
"username": "用户账号",
@@ -262,7 +267,6 @@ class UserViewSet(CustomModelViewSet):
"data": {"启用": True, "禁用": False},
}
},
"password": "登录密码",
"dept": {"title": "部门", "choices": {"queryset": Dept.objects.filter(status=True), "values_name": "name"}},
"role": {"title": "角色", "choices": {"queryset": Role.objects.filter(status=True), "values_name": "name"}},
}
@@ -293,6 +297,11 @@ class UserViewSet(CustomModelViewSet):
'dept_id': dept.id,
'dept_name': dept.name
}
else:
result['dept_info'] = {
'dept_id': None,
'dept_name': "暂无部门"
}
role = getattr(user, 'role', None)
if role:
result['role_info'] = role.values('id', 'name', 'key')
@@ -317,11 +326,10 @@ class UserViewSet(CustomModelViewSet):
return ErrorResponse(msg="参数不能为空")
if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配")
check_password = request.user.check_password(old_pwd)
if not check_password:
check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest())
if check_password:
new_pwd = hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(old_pwd, self.request.user.password)
if not verify_password:
verify_password = check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest(), self.request.user.password)
if verify_password:
request.user.password = make_password(new_pwd)
request.user.save()
return DetailResponse(data=None, msg="修改成功")
@@ -344,6 +352,8 @@ class UserViewSet(CustomModelViewSet):
"""
密码重置
"""
if not self.request.user.is_superuser:
return ErrorResponse(msg="只允许超级管理员对其进行密码重置")
instance = Users.objects.filter(id=pk).first()
data = request.data
new_pwd = data.get("newPassword")
@@ -357,3 +367,43 @@ class UserViewSet(CustomModelViewSet):
return DetailResponse(data=None, msg="修改成功")
else:
return ErrorResponse(msg="未获取到用户")
def list(self, request, *args, **kwargs):
dept_id = request.query_params.get('dept')
show_all = request.query_params.get('show_all')
if not dept_id:
dept_id = ''
if not show_all:
show_all = 0
if int(show_all):
all_did = [dept_id]
def inner(did):
sub = Dept.objects.filter(parent_id=did)
if not sub.exists():
return
for i in sub:
all_did.append(i.pk)
inner(i)
if dept_id != '':
inner(dept_id)
searchs = [
Q(**{f+'__icontains':i})
for f in self.search_fields
] if (i:=request.query_params.get('search')) else []
q_obj = []
if searchs:
q = searchs[0]
for i in searchs[1:]:
q |= i
q_obj.append(Q(q))
queryset = Users.objects.filter(*q_obj, dept_id__in=all_did)
else:
queryset = self.filter_queryset(self.get_queryset())
else:
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")

View File

@@ -3,8 +3,11 @@ import logging
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.hashers import check_password
from django.utils import timezone
from dvadmin.utils.validator import CustomValidationError
logger = logging.getLogger(__name__)
UserModel = get_user_model()
@@ -24,10 +27,13 @@ class CustomBackend(ModelBackend):
except UserModel.DoesNotExist:
UserModel().set_password(password)
else:
check_password = user.check_password(password)
if not check_password:
check_password = user.check_password(hashlib.md5(password.encode(encoding='UTF-8')).hexdigest())
if check_password and self.user_can_authenticate(user):
user.last_login = timezone.now()
user.save()
return user
verify_password = check_password(password, user.password)
if not verify_password:
password = hashlib.md5(password.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(password, user.password)
if verify_password:
if self.user_can_authenticate(user):
user.last_login = timezone.now()
user.save()
return user
raise CustomValidationError("当前用户已被禁用,请联系管理员!")

View File

@@ -38,12 +38,16 @@ def CustomExceptionHandler(ex, context):
# 调用默认的异常处理函数
response = exception_handler(ex, context)
if isinstance(ex, AuthenticationFailed):
code = 401
code_type = response.data.get('detail').code
if code_type == 'no_active_account':
code=400
# 如果是身份验证错误
if response and response.data.get('detail') == "Given token not valid for any token type":
code = 401
msg = ex.detail
elif response and response.data.get('detail') == "Token is blacklisted":
# token在黑名单
return ErrorResponse(status=HTTP_401_UNAUTHORIZED)
msg = ex.detail
else:
code = 401
msg = ex.detail
elif isinstance(ex,Http404):
code = 400
msg = "接口地址不正确"

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Columns
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.models import get_custom_app_models
class FieldPermissionMixin:
@action(methods=['get'], detail=False,permission_classes=[IsAuthenticated])
def field_permission(self, request):
"""
获取字段权限
"""
finded = False
for app in get_custom_app_models():
for model in app:
if model['object'] is self.serializer_class.Meta.model:
finded = True
break
if finded:
break
if finded is False:
return []
roles = request.user.role.values_list('id', flat=True)
user = request.user
if user.is_superuser==1:
data = Columns.objects.filter(app=model['app'], model=model['model']).values('field_name', 'is_create', 'is_query', 'is_update')
for item in data:
item['is_create'] = True
item['is_query'] = True
item['is_update'] = True
else:
data= Columns.objects.filter(
app=model['app'], model=model['model'],role__in=roles
).values('field_name', 'is_create', 'is_query', 'is_update')
return DetailResponse(data=data)

View File

@@ -75,7 +75,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if item.get("permission__api")
]
for item in api_white_list:
new_api = api + ":" + str(method)
new_api = f"{api}:{method}"
matchObj = re.match(item, new_api, re.M | re.I)
if matchObj is None:
continue
@@ -86,74 +86,80 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
如果不是超级管理员,则进入下一步权限判断
"""
if request.user.is_superuser == 0:
# 0. 获取用户的部门id没有部门则返回空
user_dept_id = getattr(request.user, "dept_id", None)
if not user_dept_id:
return queryset.none()
# 1. 判断过滤的数据是否有创建人所在部门 "dept_belong_id" 字段
if not getattr(queryset.model, "dept_belong_id", None):
return queryset
# 2. 如果用户没有关联角色则返回本部门数据
if not hasattr(request.user, "role"):
return queryset.filter(dept_belong_id=user_dept_id)
# 3. 根据所有角色 获取所有权限范围
# (0, "仅本人数据权限"),
# (1, "本部门及以下数据权限"),
# (2, "本部门数据权限"),
# (3, "全部数据权限"),
# (4, "自定数据权限")
replace_str = re.compile('\d')
re_api = replace_str.sub('{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',
role_admin=F('role__admin')
)
dataScope_list = [] # 权限范围列表
for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
if 3 == ele.get("data_range") or ele.get("role_admin") == True:
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=user_dept_id
)
# 5. 自定数据权限 获取部门,根据部门过滤
dept_list = []
for ele in dataScope_list:
if ele == 4:
dept_list.extend(
request.user.role.filter(status=1).values_list(
"dept__id", flat=True
)
)
elif ele == 2:
dept_list.append(user_dept_id)
elif ele == 1:
dept_list.append(user_dept_id)
dept_list.extend(
get_dept(
user_dept_id,
)
)
if queryset.model._meta.model_name == 'dept':
return queryset.filter(id__in=list(set(dept_list)))
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
return self._extracted_from_filter_queryset_33(request, queryset, api, method)
else:
return queryset
# TODO Rename this here and in `filter_queryset`
def _extracted_from_filter_queryset_33(self, request, queryset, api, method):
# 0. 获取用户的部门id没有部门则返回空
user_dept_id = getattr(request.user, "dept_id", None)
if not user_dept_id:
return queryset.none()
# 1. 判断过滤的数据是否有创建人所在部门 "dept_belong_id" 字段
if not getattr(queryset.model, "dept_belong_id", None):
return queryset
# 2. 如果用户没有关联角色则返回本部门数据
if not hasattr(request.user, "role"):
return queryset.filter(dept_belong_id=user_dept_id)
# 3. 根据所有角色 获取所有权限范围
# (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)
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',
role_admin=F('role__admin')
)
dataScope_list = [] # 权限范围列表
for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
if ele.get("data_range") == 3 or ele.get("role_admin") == True:
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=user_dept_id
)
# 5. 自定数据权限 获取部门,根据部门过滤
dept_list = []
for ele in dataScope_list:
if ele == 1:
dept_list.append(user_dept_id)
dept_list.extend(
get_dept(
user_dept_id,
)
)
elif ele == 2:
dept_list.append(user_dept_id)
elif ele == 4:
dept_list.extend(
request.user.role.filter(status=1).values_list(
"dept__id", flat=True
)
)
if queryset.model._meta.model_name == 'dept':
return queryset.filter(id__in=list(set(dept_list)))
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
class CustomDjangoFilterBackend(DjangoFilterBackend):
lookup_prefixes = {
@@ -164,12 +170,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
"~": "icontains",
}
def construct_search(self, field_name):
def construct_search(self, field_name, lookup_expr=None):
lookup = self.lookup_prefixes.get(field_name[0])
if lookup:
field_name = field_name[1:]
else:
lookup = "icontains"
lookup = lookup_expr
if field_name.endswith(lookup):
return field_name
return LOOKUP_SEP.join([field_name, lookup])
def find_filter_lookups(self, orm_lookups, search_term_key):
@@ -249,7 +257,10 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# warn if the field doesn't exist.
if field is None:
undefined.append(field_name)
# 更新默认字符串搜索为模糊搜索
if isinstance(field, (models.CharField)) and filterset_fields == '__all__' and lookups == [
'exact']:
lookups = ['icontains']
for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr)
@@ -298,7 +309,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
for search_field in filterset.filters:
if isinstance(filterset.filters[search_field], CharFilter):
orm_lookups.append(
self.construct_search(six.text_type(search_field))
self.construct_search(six.text_type(search_field), filterset.filters[search_field].lookup_expr)
)
else:
orm_lookups.append(search_field)

View File

@@ -1,112 +0,0 @@
import logging
import os.path
from logging import LogRecord
from django.core.servers.basehttp import WSGIRequestHandler
from loguru import logger
from logging.handlers import RotatingFileHandler
# 1.🎖先声明一个类继承logging.Handler(制作一件品如的衣服)
from loguru._defaults import LOGURU_FORMAT
class InterceptTimedRotatingFileHandler(RotatingFileHandler):
"""
自定义反射时间回滚日志记录器
缺少命名空间
"""
def __init__(self, filename, when='d', interval=1, backupCount=5, encoding="utf-8", delay=False, utc=False,
maxBytes=1024 * 1024 * 100, atTime=None, logging_levels="all"):
super(InterceptTimedRotatingFileHandler, self).__init__(filename)
filename = os.path.abspath(filename)
when = when.lower()
# 2.🎖️需要本地用不同的文件名做为不同日志的筛选器
self.logger_ = logger.bind(sime=filename, ip="-", port="-", username="张三")
self.filename = filename
key_map = {
'h': 'hour',
'w': 'week',
's': 'second',
'm': 'minute',
'd': 'day',
}
# 根据输入文件格式及时间回滚设立文件名称
rotation = f"{maxBytes / 1024 / 1024}MB"
retention = "%d %ss" % (backupCount, key_map[when])
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
if when == "s":
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
elif when == "m":
time_format = "{time:%Y-%m-%d_%H-%M}"
elif when == "h":
time_format = "{time:%Y-%m-%d_%H}"
elif when == "d":
time_format = "{time:%Y-%m-%d}"
elif when == "w":
time_format = "{time:%Y-%m-%d}"
level_keys = ["info"]
# 3.🎖️构建一个筛选器
levels = {
"debug": lambda x: "DEBUG" == x['level'].name.upper() and x['extra'].get('sime') == filename,
"error": lambda x: "ERROR" == x['level'].name.upper() and x['extra'].get('sime') == filename,
"info": lambda x: "INFO" == x['level'].name.upper() and x['extra'].get('sime') == filename,
"warning": lambda x: "WARNING" == x['level'].name.upper() and x['extra'].get('sime') == filename
}
# 4. 🎖️根据输出构建筛选器
if isinstance(logging_levels, str):
if logging_levels.lower() == "all":
level_keys = levels.keys()
elif logging_levels.lower() in levels:
level_keys = [logging_levels]
elif isinstance(logging_levels, (list, tuple)):
level_keys = logging_levels
for k, f in {_: levels[_] for _ in level_keys}.items():
# 5.🎖为防止重复添加sink而重复写入日志需要判断是否已经装载了对应sink防止其使用秘技反复横跳。
filename_fmt = filename.replace(".log", "_%s_%s.log" % (time_format, k))
# noinspection PyUnresolvedReferences,PyProtectedMember
file_key = {_._name: han_id for han_id, _ in self.logger_._core.handlers.items()}
filename_fmt_key = "'{}'".format(filename_fmt)
if filename_fmt_key in file_key:
continue
# self.logger_.remove(file_key[filename_fmt_key])
self.logger_.add(
filename_fmt,
# format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <green>{extra[ip]}:{extra[port]}</green> | <level>{level: <8}</level>| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
retention=retention,
encoding=encoding,
level=self.level,
rotation=rotation,
compression="zip", # 日志归档自行压缩文件
delay=delay,
enqueue=True,
backtrace=True,
filter=f
)
def emit(self, record):
try:
level = self.logger_.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
# 6.🎖️把当前帧的栈深度回到发生异常的堆栈深度,不然就是当前帧发生异常而无法回溯
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
# 设置自定义属性
port = "-"
ip = "-"
locals_self = frame.f_locals.get('self', None)
msg = self.format(record)
if locals_self and hasattr(locals_self, 'client_address'):
ip, port = locals_self.client_address
# - 127.0.0.1:56525 -
msg = f"{ip}:{port} - {msg}"
self.logger_ \
.opt(depth=depth, exception=record.exc_info, colors=True) \
.bind(ip=ip, port=port) \
.log(level, msg)

View File

@@ -6,22 +6,20 @@
@Created on: 2021/5/31 031 22:08
@Remark: 公共基础model类
"""
import uuid
from importlib import import_module
from django.apps import apps
from django.db import models
from django.db.models import QuerySet
from django.conf import settings
from application import settings
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
class SoftDeleteQuerySet(QuerySet):
class SoftDeleteQuerySet(models.QuerySet):
pass
class SoftDeleteManager(models.Manager):
"""支持软删除"""
@@ -40,7 +38,7 @@ class SoftDeleteManager(models.Manager):
return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=False)
return SoftDeleteQuerySet(self.model).exclude(is_deleted=True)
def get_by_natural_key(self,name):
def get_by_natural_key(self, name):
return SoftDeleteQuerySet(self.model).get(username=name)
@@ -86,8 +84,6 @@ class CoreModel(models.Model):
verbose_name_plural = verbose_name
def get_all_models_objects(model_name=None):
"""
获取所有 models 对象
@@ -111,4 +107,39 @@ def get_all_models_objects(model_name=None):
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
if model_name:
return settings.ALL_MODELS_OBJECTS[model_name] or {}
return settings.ALL_MODELS_OBJECTS or {}
return settings.ALL_MODELS_OBJECTS or {}
def get_model_from_app(app_name):
"""获取模型里的字段"""
model_module = import_module(app_name + '.models')
filter_model = [
getattr(model_module, item) for item in dir(model_module)
if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase)
]
model_list = []
for model in filter_model:
if model.__name__ == 'AbstractUser':
continue
fields = [
{'title': field.verbose_name, 'name': field.name, 'object': field}
for field in model._meta.fields
]
model_list.append({
'app': app_name,
'verbose': model._meta.verbose_name,
'model': model.__name__,
'object': model,
'fields': fields
})
return model_list
def get_custom_app_models(app_name=None):
"""获取所有项目写的app里的models"""
if app_name:
return get_model_from_app(app_name)
res = []
for app in settings.CUSTOM_APPS:
res.append(get_model_from_app(app))
return res

View File

@@ -40,13 +40,11 @@ class CustomPagination(PageNumberPagination):
try:
self.page = paginator.page(page_number)
except InvalidPage as exc:
# msg = self.invalid_page_message.format(
# page_number=page_number, message=str(exc)
# )
# raise NotFound(msg)
empty = False
pass
if paginator.num_pages > 1 and self.template is not None:
# The browsable API should display pagination controls.
@@ -58,15 +56,15 @@ class CustomPagination(PageNumberPagination):
self.page = []
return list(self.page)
def get_paginated_response(self, data):
code = 2000
msg = 'success'
page =int(self.get_page_number(self.request, paginator)) or 1
total=self.page.paginator.count if self.page else 0
limit= int(self.get_page_size(self.request)) or 10
is_next= self.page.has_next()
is_previous= self.page.has_previous()
data=data
page = int(self.get_page_number(self.request, paginator)) or 1
total = self.page.paginator.count if self.page else 0
limit = int(self.get_page_size(self.request)) or 10
is_next = self.page.has_next() if self.page else False
is_previous = self.page.has_previous() if self.page else False
if not data:
code = 2000
@@ -78,8 +76,9 @@ class CustomPagination(PageNumberPagination):
('msg', msg),
('page', page),
('limit', limit),
('total',total),
('is_next',is_next),
('total', total),
('is_next', is_next),
('is_previous', is_previous),
('data', data)
('data', data),
('permission', self.request.permission_fields)
]))

View File

@@ -6,8 +6,6 @@
@Created on: 2021/6/1 001 22:57
@Remark: 自定义视图集
"""
import uuid
from django.db import transaction
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
@@ -18,6 +16,8 @@ from dvadmin.utils.filters import DataLevelPermissionsFilter
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
from dvadmin.utils.permission import CustomPermission
from dvadmin.utils.models import get_custom_app_models
from dvadmin.system.models import Columns
from django_restql.mixins import QueryArgumentsMixin
@@ -63,12 +63,38 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准
can_see = self.get_column_permission(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
# 在分页器中使用
self.request.permission_fields = can_see
if isinstance(self.request.data, list):
with transaction.atomic():
return serializer_class(many=True, *args, **kwargs)
else:
return serializer_class(*args, **kwargs)
def get_column_permission(self, serializer_class):
"""获取列权限"""
finded = False
for app in get_custom_app_models():
for model in app:
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded:
break
if finded is False:
return []
return Columns.objects.filter(
app=model['app'], model=model['model']
).values('field_name', 'is_create', 'is_query', 'is_update')
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request)
serializer.is_valid(raise_exception=True)

View File

@@ -1 +0,0 @@
7

View File

@@ -1,31 +1,30 @@
Django==4.1.5
Django==4.2.6
django-comment-migrate==0.1.7
django-cors-headers==3.13.0
django-filter==21.1
django-cors-headers==4.3.0
django-filter==23.3
django-ranged-response==0.2.0
djangorestframework==3.14.0
django-restql==0.15.3
django-simple-captcha==0.5.17
django-timezone-field==5.0
djangorestframework-simplejwt==5.2.2
drf-yasg==1.21.4
mysqlclient==2.1.1
pypinyin==0.48.0
ua-parser==0.16.1
pyparsing==3.0.9
openpyxl==3.0.10
requests==2.28.2
loguru==0.6.0
typing-extensions==4.4.0
smmap==5.0.0
tzlocal==4.1
django-simple-captcha==0.5.20
django-timezone-field==6.0.1
djangorestframework-simplejwt==5.3.0
drf-yasg==1.21.7
mysqlclient==2.2.0
pypinyin==0.49.0
ua-parser==0.18.0
pyparsing==3.1.1
openpyxl==3.1.2
requests==2.31.0
typing-extensions==4.8.0
tzlocal==5.1
channels==4.0.0
channels-redis==4.0.0
websockets==10.4
channels-redis==4.1.0
websockets==11.0.3
user-agents==2.2.0
six==1.16.0
whitenoise==6.3.0
psycopg2==2.9.5
uvicorn==0.20.0
gunicorn==20.1.0
gevent==22.10.2
whitenoise==6.6.0
psycopg2==2.9.9
uvicorn==0.23.2
gunicorn==21.2.0
gevent==23.9.1
Pillow==10.1.0

View File

@@ -4,9 +4,9 @@
~~~sh
# 编译打包到本地
docker build -f ./docker_env/web/DockerfileBuild -t registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:latest .
docker build -f ./docker_env/web/DockerfileBuild -t registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine .
# 上传到阿里云仓库
docker push registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:latest
docker push registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine
~~~

View File

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

View File

@@ -1,4 +1,4 @@
FROM node:16.19-alpine
WORKDIR /
COPY ./web/package.json .
RUN npm install -g yarn
RUN yarn install --registry=https://registry.npm.taobao.org

View File

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

View File

@@ -2,7 +2,7 @@
ENV = 'production'
# 线上环境接口地址
VITE_API_URL = ''
VITE_API_URL = '/api' # docker-compose部署不需要修改nginx容器自动代理了这个地址
# 是否启用按钮权限
VITE_PM_ENABLED = true

2
web/.gitignore vendored
View File

@@ -1,7 +1,7 @@
.DS_Store
node_modules
/dist
yarn.lock
# local env files
.env.local

6818
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,14 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"@fast-crud/fast-crud": "^1.9.0",
"@fast-crud/fast-extends": "^1.9.0",
"@fast-crud/ui-element": "^1.9.0",
"@fast-crud/fast-crud": "^1.18.3",
"@fast-crud/fast-extends": "^1.18.3",
"@fast-crud/ui-element": "^1.18.3",
"@fast-crud/ui-interface": "^1.18.3",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"autoprefixer": "^10.4.14",
"axios": "^1.2.1",
"countup.js": "^2.3.2",
"cropperjs": "^1.5.13",
@@ -23,7 +25,8 @@
"echarts": "^5.4.1",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.2.26",
"element-plus": "^2.3.9",
"element-tree-line": "^0.2.1",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"js-table2excel": "^1.0.3",
@@ -32,19 +35,23 @@
"nprogress": "^0.2.0",
"pinia": "^2.0.28",
"pinia-plugin-persist": "^1.0.0",
"postcss": "^8.4.21",
"print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.0",
"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",
"vue": "^3.2.45",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vxe-table": "^4.3.10",
"vxe-table": "^4.4.1",
"xe-utils": "^3.5.7"
},
"devDependencies": {

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,5 +1,6 @@
<template>
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
<!-- v-show="themeConfig.lockScreenTime > 1" -->
<router-view v-show="themeConfig.lockScreenTime > 1" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
@@ -9,7 +10,7 @@
</template>
<script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch,onBeforeUnmount } from 'vue';
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
@@ -33,8 +34,8 @@ const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from "/@/utils/websocket";
import {ElNotification} from "element-plus";
import websocket from '/@/utils/websocket';
import { ElNotification } from 'element-plus';
// 获取版本号
const getVersion = computed(() => {
let isVersion = false;
@@ -58,8 +59,12 @@ onBeforeMount(() => {
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
//websockt 模块
websocket.init(wsReceive)
//websockt 模块
try {
//websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
});
// 页面加载时
onMounted(() => {
@@ -95,26 +100,24 @@ watch(
);
// websocket相关代码
import {messageCenterStore} from "/@/stores/messageCenter";
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,
});
}
};
onBeforeUnmount(() => {
// 关闭连接
websocket.close()
})
// 关闭连接
websocket.close();
});
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

View File

@@ -0,0 +1,9 @@
.fs-crud-container {
.el-table thead {
color: #606266;
}
.el-input__inner::placeholder {
color: #dcdfe6;
font-size: 12px;
}
}

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,196 @@
<template>
<div class="user-info-head" @click="editCropper()">
<el-avatar :size="100" :src="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">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
:outputType="options.outputType"
@realTime="realTime"
:centerBox="true"
v-if="visible"
class="cropper"
/>
</el-col>
</el-row>
<br />
<el-row class="flex justify-center">
<el-col :lg="2" :md="2">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-button type="success">
选择
<el-icon class="el-icon--right"><Plus /></el-icon>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
</el-col>
<el-col :lg="{ span: 2, offset: 2 }" :md="2">
<el-button type="primary" @click="uploadImg()">更新头像</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script setup>
import 'vue-cropper/dist/index.css';
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';
const userStore = useUserInfo();
const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false);
const title = ref('修改头像');
const emit = defineEmits(['uploadImg']);
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
required: true,
},
});
const dialogVisiable = computed({
get() {
return props.modelValue;
},
set(newVal) {
emit('update:modelValue', newVal);
},
});
//图片裁剪数据
const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址
fileName: '',
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
outputType: 'png', // 默认生成截图为PNG格式
});
/** 编辑头像 */
function editCropper() {
dialogVisiable.value = true;
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
nextTick(() => {
visible.value = true;
});
}
/** 覆盖默认上传行为 */
function requestUpload() {}
/** 向左旋转 */
function rotateLeft() {
proxy.$refs.cropper.rotateLeft();
}
/** 向右旋转 */
function rotateRight() {
proxy.$refs.cropper.rotateRight();
}
/** 图片缩放 */
function changeScale(num) {
num = num || 1;
proxy.$refs.cropper.changeScale(num);
}
/** 上传预处理 */
function beforeUpload(file) {
if (file.type.indexOf('image/') == -1) {
proxy.$modal.msgError('文件格式错误,请上传图片类型,如JPGPNG后缀的文件。');
} else {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
options.img = reader.result;
options.fileName = file.name;
};
}
}
/** 上传图片 */
function uploadImg() {
// 获取截图的 base64 数据
proxy.$refs.cropper.getCropData((data) => {
let img = new Image();
img.src = data;
img.onload = async () => {
let _data = compress(img);
const imgFile = base64ToFile(_data, options.fileName);
emit('uploadImg', imgFile);
};
});
}
// 压缩图片
function compress(img) {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
// let initSize = img.src.length;
let width = img.width;
let height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
// 进行压缩
let ndata = canvas.toDataURL('image/jpeg', 0.8);
return ndata;
}
/** 关闭窗口 */
function closeDialog() {
options.visible = false;
options.img = userStore.userInfos.avatar;
}
const updateAvatar = (img) => {
options.img = img;
};
defineExpose({
updateAvatar,
});
</script>
<style lang="scss" scoped>
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {
content: '修改头像';
position: absolute;
text-align: center;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #000000;
font-size: 20px;
font-style: normal;
cursor: pointer;
line-height: 110px;
}
.cropper {
height: 400px;
width: 400px;
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<div style="display: inline-block">
<el-button size="default" type="success" @click="handleImport()">
<slot>导入</slot>
</el-button>
<el-dialog :title="props.upload.title" v-model="uploadShow" width="400px" append-to-body>
<div v-loading="loading">
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="props.upload.headers"
:action="props.upload.url"
:disabled="isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"/>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip" style="color:red">提示仅允许导入xlsxlsx格式文件</div>
</template>
</el-upload>
<div>
<el-button type="warning" style="font-size:14px;margin-top: 20px" @click="importTemplate">下载导入模板</el-button>
<el-button type="warning" style="font-size:14px;margin-top: 20px" @click="updateTemplate">批量更新模板</el-button>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :disabled="loading" @click="submitFileForm"> </el-button>
<el-button :disabled="loading" @click="uploadShow = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="importExcel">
import { request, downloadFile } from '/@/utils/service';
import {inject,ref} from "vue";
import { getBaseURL } from '/@/utils/baseUrl';
import { Session } from '/@/utils/storage';
import { ElMessageBox } from 'element-plus'
import type { Action } from 'element-plus'
const refreshView = inject('refreshView')
let props = defineProps({
upload: {
type: Object,
default () {
return {
// 是否显示弹出层
open: true,
// 弹出层标题
title: '',
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: 'JWT ' + Session.get('token') },
// 上传的地址
url: getBaseURL() + 'api/system/file/'
}
}
},
api: { // 导入接口地址
type: String,
default () {
return undefined
}
}
})
let loading = ref(false)
const uploadRef = ref()
const uploadShow = ref(false)
const isUploading = ref(false)
/** 导入按钮操作 */
const handleImport = function () {
uploadShow.value = true
}
/** 下载模板操作 */
const importTemplate=function () {
downloadFile({
url: props.api + 'import_data/',
params: {},
method: 'get'
})
}
/***
* 批量更新模板
*/
const updateTemplate=function () {
downloadFile({
url: props.api + 'update_template/',
params: {},
method: 'get'
})
}
// 文件上传中处理
const handleFileUploadProgress=function (event:any, file:any, fileList:any) {
isUploading.value = true
}
// 文件上传成功处理
const handleFileSuccess=function (response:any, file:any, fileList:any) {
isUploading.value = false
loading.value = true
uploadRef.value.clearFiles()
// 是否更新已经存在的用户数据
return request({
url: props.api + 'import_data/',
method: 'post',
data: {
url: response.data.url
}
}).then((response:any) => {
loading.value = false
ElMessageBox.alert('导入成功', '导入完成', {
confirmButtonText: 'OK',
callback: (action: Action) => {
refreshView()
},
})
}).catch(()=>{
loading.value = false
})
}
// 提交上传文件
const submitFileForm=function () {
uploadRef.value.submit()
}
</script>
<style scoped>
</style>

View File

@@ -14,9 +14,9 @@ import { useThemeConfig } from '/@/stores/themeConfig';
*/
// element plus 自带国际化
import enLocale from 'element-plus/lib/locale/lang/en';
import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
import enLocale from 'element-plus/es/locale/lang/en';
import zhcnLocale from 'element-plus/es/locale/lang/zh-cn';
import zhtwLocale from 'element-plus/es/locale/lang/zh-tw';
// 定义变量内容
const messages = {};

View File

@@ -9,8 +9,8 @@ export default {
two4: 'Links',
},
account: {
accountPlaceholder1: 'The user name admin or not is common',
accountPlaceholder2: 'Password: 123456',
accountPlaceholder1: 'Please enter your login account',
accountPlaceholder2: 'Please enter your login password',
accountPlaceholder3: 'Please enter the verification code',
accountBtnText: 'Sign in',
},

View File

@@ -9,8 +9,8 @@ export default {
two4: '友情链接',
},
account: {
accountPlaceholder1: '用户名 admin 或不输均为 common',
accountPlaceholder2: '密码123456',
accountPlaceholder1: '请输入登录账号',
accountPlaceholder2: '请输入登录密码',
accountPlaceholder3: '请输入验证码',
accountBtnText: '登 录',
},

View File

@@ -9,8 +9,8 @@ export default {
two4: '友情連結',
},
account: {
accountPlaceholder1: '用戶名admin或不輸均為common',
accountPlaceholder2: '密碼123456',
accountPlaceholder1: '請輸入登入賬號',
accountPlaceholder2: '請輸入登入密碼',
accountPlaceholder3: '請輸入驗證碼',
accountBtnText: '登入',
},

View File

@@ -1,13 +1,10 @@
<template>
<el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
<el-scrollbar
ref="layoutMainScrollbarRef"
class="layout-main-scroll layout-backtop-header-fixed"
wrap-class="layout-main-scroll"
view-class="layout-main-scroll"
>
<el-main class="layout-main"
:style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll layout-backtop-header-fixed"
wrap-class="layout-main-scroll" view-class="layout-main-scroll">
<LayoutParentView />
<LayoutFooter v-if="isFooter" />
<!-- <LayoutFooter v-if="isFooter" /> -->
</el-scrollbar>
<el-backtop :target="setBacktopClass" />
</el-main>

View File

@@ -39,7 +39,7 @@
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread===0">
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')">
<ele-Bell />
</el-icon>
@@ -59,8 +59,8 @@
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link">
<img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfos.username === '' ? 'common' : userInfos.username }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
</el-icon>
@@ -68,7 +68,7 @@
<template #dropdown>
<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="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu>
@@ -90,7 +90,7 @@ import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage';
import headerImage from '/@/assets/img/headerImage.png';
// 引入组件
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
@@ -208,8 +208,8 @@ onMounted(() => {
});
//消息中心的未读数量
import {messageCenterStore} from "/@/stores/messageCenter";
const messageCenter = messageCenterStore()
import { messageCenterStore } from '/@/stores/messageCenter';
const messageCenter = messageCenterStore();
</script>
<style scoped lang="scss">

View File

@@ -3,18 +3,21 @@
<router-view v-slot="{ Component }">
<transition :name="setTransitionName" mode="out-in">
<keep-alive :include="getKeepAliveNames" v-if="showView">
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
<div>
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
</div>
</keep-alive>
</transition>
</router-view>
<transition :name="setTransitionName" mode="out-in">
<Iframes class="w100" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="setTransitionName" :list="state.iframeList" />
<Iframes class="w100" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="setTransitionName"
:list="state.iframeList" />
</transition>
</div>
</template>
<script setup lang="ts" name="layoutParentView">
import { defineAsyncComponent, computed, reactive, onBeforeMount, onUnmounted, nextTick, watch, onMounted,ref,provide } from 'vue';
import { defineAsyncComponent, computed, reactive, onBeforeMount, onUnmounted, nextTick, watch, onMounted, ref, provide } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useKeepALiveNames } from '/@/stores/keepAliveNames';
@@ -44,13 +47,13 @@ const showView = ref(true)
/**
* 刷新页面
*/
const refreshView=function () {
showView.value = false // 通过v-if移除router-view节点
nextTick(() => {
showView.value = true // DOM更新后再通过v-if添加router-view节点
})
const refreshView = function () {
showView.value = false // 通过v-if移除router-view节点
nextTick(() => {
showView.value = true // DOM更新后再通过v-if添加router-view节点
})
}
provide('refreshView',refreshView)
provide('refreshView', refreshView)
// 设置主界面切换动画
const setTransitionName = computed(() => {
@@ -105,7 +108,7 @@ onMounted(() => {
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('onTagsViewRefreshRouterView', () => {});
mittBus.off('onTagsViewRefreshRouterView', () => { });
});
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
// https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/38/files

View File

@@ -1,9 +1,10 @@
import {createApp} from 'vue';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import {directive} from '/@/utils/directive';
import {i18n} from '/@/i18n/index';
import { directive } from '/@/utils/directive';
import { i18n } from '/@/i18n';
import other from '/@/utils/other';
import '/@/assets/style/tailwind.css'; // 先引入tailwind css, 以免element-plus冲突
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import '/@/theme/index.scss';
@@ -15,7 +16,7 @@ import fastCrud from './settings.ts';
import pinia from './stores';
import permission from '/@/plugin/permission/index';
// @ts-ignore
import eIconPicker, { iconList,analyzingIconForIconfont } from 'e-icon-picker';
import eIconPicker, { iconList, analyzingIconForIconfont } from 'e-icon-picker';
import 'e-icon-picker/icon/default-icon/symbol.js'; //基本彩色图标库
import 'e-icon-picker/index.css'; // 基本样式,包含基本图标
import 'font-awesome/css/font-awesome.min.css';
@@ -24,6 +25,14 @@ 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 { scanAndInstallPlugins } from '/@/views/plugins/index';
import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css'
import '/@/assets/style/reset.scss';
import 'element-tree-line/dist/style.css'
let forIconfont = analyzingIconForIconfont(iconfont); //解析class
iconList.addIcon(forIconfont.list); // 添加iconfont dvadmin3的icon
iconList.addIcon(elementPlus); // 添加element plus的图标
@@ -31,17 +40,21 @@ iconList.addIcon(fontAwesome470); // 添加fontAwesome 470版本的图标
let app = createApp(App);
scanAndInstallPlugins(app);
app.use(eIconPicker, {
addIconList: eIconList, //全局添加图标
removeIconList: [], //全局删除图标
zIndex: 3100, //选择器弹层的最低层,全局配置
addIconList: eIconList, //全局添加图标
removeIconList: [], //全局删除图标
zIndex: 3100, //选择器弹层的最低层,全局配置
});
pinia.use(piniaPersist);
directive(app);
other.elSvg(app);
app.use(VXETable)
app.use(permission);
app.use(pinia).use(router).use(ElementPlus, {i18n: i18n.global.t}).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
app.use(pinia).use(router).use(ElementPlus, { i18n: i18n.global.t }).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
app.config.globalProperties.mittBus = mitt();

View File

@@ -9,7 +9,7 @@ export default {
return XEUtils.includeArrays(BtnPermission, value)
}else if(typeof value === 'string'){
const index = XEUtils.arrayIndexOf(BtnPermission, value)
return index>0?true:false
return index>-1?true:false
}
}
return true

View File

@@ -11,7 +11,7 @@ import { useRoutesList } from '/@/stores/routesList';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useMenuApi } from '/@/api/menu/index';
import { handleMenu } from '../utils/menu';
import {BtnPermissionStore} from "/@/plugin/permission/store.permission";
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
const menuApi = useMenuApi();

View File

@@ -1,15 +1,15 @@
import { createRouter, createWebHashHistory } from 'vue-router';
import {createRouter, createWebHashHistory} from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import pinia from '/@/stores/index';
import { storeToRefs } from 'pinia';
import { useKeepALiveNames } from '/@/stores/keepAliveNames';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { Session } from '/@/utils/storage';
import { staticRoutes } from '/@/router/route';
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
import { initBackEndControlRoutes } from '/@/router/backEnd';
import {storeToRefs} from 'pinia';
import {useKeepALiveNames} from '/@/stores/keepAliveNames';
import {useRoutesList} from '/@/stores/routesList';
import {useThemeConfig} from '/@/stores/themeConfig';
import {Session} from '/@/utils/storage';
import {staticRoutes} from '/@/router/route';
import {initFrontEndControlRoutes} from '/@/router/frontEnd';
import {initBackEndControlRoutes} from '/@/router/backEnd';
/**
* 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。
@@ -22,8 +22,8 @@ import { initBackEndControlRoutes } from '/@/router/backEnd';
// 读取 `/src/stores/themeConfig.ts` 是否开启后端控制路由配置
const storesThemeConfig = useThemeConfig(pinia);
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isRequestRoutes } = themeConfig.value;
const {themeConfig} = storeToRefs(storesThemeConfig);
const {isRequestRoutes} = themeConfig.value;
/**
* 创建一个可以被 Vue 应用程序使用的路由实例
@@ -31,8 +31,8 @@ const { isRequestRoutes } = themeConfig.value;
* @link 参考https://next.router.vuejs.org/zh/api/#createrouter
*/
export const router = createRouter({
history: createWebHashHistory(),
routes: staticRoutes,
history: createWebHashHistory(),
routes: staticRoutes,
});
/**
@@ -41,13 +41,13 @@ export const router = createRouter({
* @returns 返回处理后的一维路由菜单数组
*/
export function formatFlatteningRoutes(arr: any) {
if (arr.length <= 0) return false;
for (let i = 0; i < arr.length; i++) {
if (arr[i].children) {
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
}
}
return arr;
if (arr.length <= 0) return false;
for (let i = 0; i < arr.length; i++) {
if (arr[i].children) {
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
}
}
return arr;
}
/**
@@ -58,73 +58,80 @@ export function formatFlatteningRoutes(arr: any) {
* @returns 返回将一维数组重新处理成 `定义动态路由dynamicRoutes` 的格式
*/
export function formatTwoStageRoutes(arr: any) {
if (arr.length <= 0) return false;
const newArr: any = [];
const cacheList: Array<string> = [];
arr.forEach((v: any) => {
if (v.path === '/') {
newArr.push({ component: v.component, name: v.name, path: v.path, redirect: v.redirect, meta: v.meta, children: [] });
} else {
// 判断是否是动态路由xx/:id/:name用于 tagsView 等中使用
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
if (v.path.indexOf('/:') > -1) {
v.meta['isDynamic'] = true;
v.meta['isDynamicPath'] = v.path;
}
newArr[0].children.push({ ...v });
// 存 name 值keep-alive 中 include 使用,实现路由的缓存
// 路径:/@/layout/routerView/parent.vue
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
cacheList.push(v.name);
const stores = useKeepALiveNames(pinia);
stores.setCacheKeepAlive(cacheList);
}
}
});
return newArr;
if (arr.length <= 0) return false;
const newArr: any = [];
const cacheList: Array<string> = [];
arr.forEach((v: any) => {
if (v.path === '/') {
newArr.push({
component: v.component,
name: v.name,
path: v.path,
redirect: v.redirect,
meta: v.meta,
children: []
});
} else {
// 判断是否是动态路由xx/:id/:name用于 tagsView 等中使用
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
if (v.path.indexOf('/:') > -1) {
v.meta['isDynamic'] = true;
v.meta['isDynamicPath'] = v.path;
}
newArr[0].children.push({...v});
// 存 name 值keep-alive 中 include 使用,实现路由的缓存
// 路径:/@/layout/routerView/parent.vue
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive && v.component_name != "") {
cacheList.push(v.name);
const stores = useKeepALiveNames(pinia);
stores.setCacheKeepAlive(cacheList);
}
}
});
return newArr;
}
// 路由加载前
router.beforeEach(async (to, from, next) => {
NProgress.configure({ showSpinner: false });
if (to.meta.title) NProgress.start();
const token = Session.get('token');
if (to.path === '/login' && !token) {
next();
NProgress.done();
} else {
if (!token) {
next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
Session.clear();
NProgress.done();
} else if (token && to.path === '/login') {
next('/home');
NProgress.done();
} else {
const storesRoutesList = useRoutesList(pinia);
const { routesList } = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) {
if (isRequestRoutes) {
// 后端控制路由:路由数据初始化,防止刷新时丢失
await initBackEndControlRoutes();
// 动态添加路由:防止非首页刷新时跳转回首页的问题
// 确保 addRoute() 时动态添加的路由已经被完全加载上去
next({ ...to, replace: true });
} else {
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
await initFrontEndControlRoutes();
next({ ...to, replace: true });
}
} else {
next();
}
}
}
NProgress.configure({showSpinner: false});
if (to.meta.title) NProgress.start();
const token = Session.get('token');
if (to.path === '/login' && !token) {
next();
NProgress.done();
} else {
if (!token) {
next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
Session.clear();
NProgress.done();
} else if (token && to.path === '/login') {
next('/home');
NProgress.done();
} else {
const storesRoutesList = useRoutesList(pinia);
const {routesList} = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) {
if (isRequestRoutes) {
// 后端控制路由:路由数据初始化,防止刷新时丢失
await initBackEndControlRoutes();
// 动态添加路由:防止非首页刷新时跳转回首页的问题
// 确保 addRoute() 时动态添加的路由已经被完全加载上去
next({...to, replace: true});
} else {
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
await initFrontEndControlRoutes();
next({...to, replace: true});
}
} else {
next();
}
}
}
});
// 路由加载后
router.afterEach(() => {
NProgress.done();
NProgress.done();
});
// 导出路由

View File

@@ -7,8 +7,9 @@ import { setLogger } from '@fast-crud/fast-crud';
import ui from '@fast-crud/ui-element';
import { request } from '/@/utils/service';
//扩展包
import {FsExtendsEditor} from "@fast-crud/fast-extends";
import "@fast-crud/fast-extends/dist/style.css";
import { FsExtendsEditor,FsExtendsUploader } from '@fast-crud/fast-extends';
import '@fast-crud/fast-extends/dist/style.css';
import { successMessage, successNotification } from '/@/utils/message';
export default {
async install(app: any, options: any) {
// 先安装ui
@@ -18,7 +19,10 @@ export default {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求
async dictRequest({ dict }: any) {
return await request({ url: dict.url,params:dict.params || {} }); //根据dict的url异步返回一个字典数组
//根据dict的url异步返回一个字典数组
return await request({ url: dict.url, params: dict.params || {} }).then((res:any)=>{
return res.data
});
},
//公共crud配置
commonOptions() {
@@ -28,6 +32,9 @@ export default {
//你项目后台接口大概率与fast-crud所需要的返回结构不一致所以需要配置此项
//请参考文档http://fast-crud.docmirror.cn/api/crud-options/request.html
transformQuery: ({ page, form, sort }: any) => {
if (sort.asc !== undefined) {
form['ordering'] = `${sort.asc ? '' : '-'}${sort.prop}`;
}
//转换为你pageRequest所需要的请求参数结构
return { page: page.currentPage, limit: page.pageSize, ...form };
},
@@ -37,6 +44,14 @@ export default {
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
},
},
form: {
afterSubmit(ctx: any) {
// 增加crud提示
if (ctx.res.code == 2000) {
successNotification(ctx.res.msg);
}
},
},
/* search: {
layout: 'multi-line',
collapse: true,
@@ -55,9 +70,42 @@ export default {
},
});
//富文本
app.use(FsExtendsEditor,{
wangEditor:{
width:300,
app.use(FsExtendsEditor, {
wangEditor: {
width: 300,
},
});
// 文件上传
app.use(FsExtendsUploader, {
defaultType: "form",
form: {
action: `/api/system/file/`,
name: "file",
withCredentials: false,
uploadRequest: async ({ action, file, onProgress }) => {
// @ts-ignore
const data = new FormData();
data.append("file", file);
return await request({
url: action,
method: "post",
timeout: 60000,
headers: {
"Content-Type": "multipart/form-data"
},
data,
onUploadProgress: (p) => {
onProgress({ percent: Math.round((p.loaded / p.total) * 100) });
}
});
},
successHandle(ret) {
// 上传完成后的结果处理, 此处应返回格式为{url:xxx,key:xxx}
return {
url: getBaseURL() + ret.data.url,
key: ret.data.id
};
}
}
})
setLogger({ level: 'error' });

View File

@@ -0,0 +1,20 @@
import { defineStore } from 'pinia';
export interface DataItemType {
field_name: string;
is_create: boolean;
is_query: boolean;
is_update: boolean;
}
export const useColumnPermission = defineStore('columnPermission', {
state: () => ({
permission: [] as DataItemType[],
}),
actions: {
setPermissionData(data: DataItemType[]) {
this.permission = data;
},
},
});

View File

@@ -5,11 +5,17 @@
// 用户信息
export interface UserInfosState {
authBtnList: string[];
photo: string;
roles: string[];
time: number;
userName: string;
avatar: string;
username: string;
name: string;
email: string;
mobile: string;
gender: string;
dept_info: {
dept_id: number;
dept_name: string;
};
role_info: any[];
}
export interface UserInfosStates {
userInfos: UserInfosState;
@@ -91,4 +97,7 @@ export interface ThemeConfigStates {
export interface DictionaryStates {
data: any;
}
}
export interface ConfigStates {
systemConfig: any;
}

View File

@@ -0,0 +1,28 @@
import { defineStore } from 'pinia';
import { ConfigStates } from './interface';
import { request } from '../utils/service';
export const urlPrefix = '/api/init/settings/';
/**
* 系统配置数据
* @methods getSystemConfig 获取系统配置数据
*/
export const SystemConfigStore = defineStore('SystemConfig', {
state: (): ConfigStates => ({
systemConfig: {},
}),
actions: {
async getSystemConfigs() {
request({
url: urlPrefix,
method: 'get',
}).then((ret: { data: [] }) => {
// 转换数据格式并保存到pinia
this.systemConfig = JSON.parse(JSON.stringify(ret.data));
});
},
},
persist: {
enabled: true,
},
});

View File

@@ -1,9 +1,7 @@
import { defineStore } from 'pinia';
import Cookies from 'js-cookie';
import { UserInfosStates } from './interface';
import { Session } from '/@/utils/storage';
import { request } from '../utils/service';
/**
* 用户信息
* @methods setUserInfos 设置用户信息
@@ -11,32 +9,59 @@ import { request } from '../utils/service';
export const useUserInfo = defineStore('userInfo', {
state: (): UserInfosStates => ({
userInfos: {
userName: '',
photo: '',
time: 0,
roles: [],
authBtnList: [],
avatar: '',
username: '',
name: '',
email: '',
mobile: '',
gender: '',
dept_info: {
dept_id: 0,
dept_name: '',
},
role_info: [
{
id: 0,
name: '',
},
],
},
}),
actions: {
async updateUserInfos() {
let userInfos: any = await this.getApiUserInfo();
this.userInfos.username = userInfos.data.name;
this.userInfos.avatar = userInfos.data.avatar;
this.userInfos.name = userInfos.data.name;
this.userInfos.email = userInfos.data.email;
this.userInfos.mobile = userInfos.data.mobile;
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
Session.set('userInfo', this.userInfos);
},
async setUserInfos() {
// 存储用户信息到浏览器缓存
if (Session.get('userInfo')) {
this.userInfos = Session.get('userInfo');
} else {
let userInfos: any = await this.getApiUserInfo();
this.userInfos.userName = userInfos.data.name;
this.userInfos.photo = userInfos.data.avatar || 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500'
this.userInfos.time = new Date().getTime()
this.userInfos.roles = ['admin']
Session.set('userInfo', this.userInfos)
this.userInfos.username = userInfos.data.name;
this.userInfos.avatar = userInfos.data.avatar;
this.userInfos.name = userInfos.data.name;
this.userInfos.email = userInfos.data.email;
this.userInfos.mobile = userInfos.data.mobile;
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
Session.set('userInfo', this.userInfos);
}
},
async getApiUserInfo() {
return request({
url: '/api/system/user/user_info/',
method: 'get',
})
}
});
},
},
});

View File

@@ -9,7 +9,7 @@ declare interface UserInfosState<T = any> {
photo: string;
roles: string[];
time: number;
userName: string;
username: string;
[key: string]: T;
};
}

View File

@@ -90,7 +90,7 @@ declare type TreeType = {
// user
declare type RowUserType<T = any> = {
userName: string;
username: string;
userNickname: string;
roleSign: string;
department: string[];

View File

@@ -1,61 +1,67 @@
import { pluginsAll } from '/@/views/plugins/index';
/**
* @description 校验是否为租户模式。租户模式把域名替换成 域名 加端口
*/
export const getBaseURL = function () {
var baseURL = import.meta.env.VITE_API_URL as any
var param = baseURL.split('/')[3] || ''
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2.把 ip 地址替换成和前端一样域名
// 3.把 /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2]
if (host) {
var prot = baseURL.split(':')[2] || 80
if (prot === 80 || prot === 443) {
host = document.domain
} else {
host = document.domain + ':' + prot
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
}
if (!baseURL.endsWith('/')) {
baseURL += '/'
}
return baseURL
}
var baseURL = import.meta.env.VITE_API_URL as any;
var param = baseURL.split('/')[3] || '';
// @ts-ignore
if (pluginsAll && pluginsAll.indexOf('dvadmin3-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2.把 ip 地址替换成和前端一样域名
// 3.把 /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2];
if (host) {
var port = baseURL.split(':')[2] || 80;
if (port === 80 || port === 443) {
host = document.domain;
} else {
host = document.domain + ':' + port;
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param;
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
}
}
if (!baseURL.endsWith('/')) {
baseURL += '/';
}
return baseURL;
};
export const getWsBaseURL = function () {
let baseURL = import.meta.env.VITE_API_URL as any
let param = baseURL.split('/')[3] || ''
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2.把 ip 地址替换成和前端一样域名
// 3.把 /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2]
if (host) {
var prot = baseURL.split(':')[2] || 80
if (prot === 80 || prot === 443) {
host = document.domain
} else {
host = document.domain + ':' + prot
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
} else if (param !== '' || baseURL.startsWith('/')) {
baseURL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + (location.port ? ':' : '') + location.port + baseURL
}
if (!baseURL.endsWith('/')) {
baseURL += '/'
}
if (baseURL.startsWith('http')) { // https 也默认会被替换成 wss
baseURL = baseURL.replace('http', 'ws')
}
return baseURL
}
let baseURL = import.meta.env.VITE_API_URL as any;
let param = baseURL.split('/')[3] || '';
// @ts-ignore
if (pluginsAll && pluginsAll.indexOf('dvadmin3-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
// 2.把 ip 地址替换成和前端一样域名
// 3.把 /api 或其他类似的替换成和前端一样域名
// document.domain
var host = baseURL.split('/')[2];
if (host) {
var port = baseURL.split(':')[2] || 80;
if (port === 80 || port === 443) {
host = document.domain;
} else {
host = document.domain + ':' + port;
}
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param;
} else {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
}
} else if (param !== '' || baseURL.startsWith('/')) {
baseURL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
}
if (!baseURL.endsWith('/')) {
baseURL += '/';
}
if (baseURL.startsWith('http')) {
// https 也默认会被替换成 wss
baseURL = baseURL.replace('http', 'ws');
}
return baseURL;
};

View File

@@ -0,0 +1,9 @@
import { useColumnPermission } from '/@/stores/columnPermission';
type permissionType = 'is_create' | 'is_query' | 'is_update';
export const columnPermission = (key: string, type: permissionType): boolean => {
const permissions = useColumnPermission().permission || [];
return !!permissions.some((i) => i.field_name === key && i[type]);
};

View File

@@ -12,13 +12,13 @@ export const handleMenu = (menuData: Array<any>) => {
title: item.title,
isLink: item.is_link,
isHide: !item.visible,
isKeepAlive: true,
isKeepAlive: item.cache,
isAffix: false,
isIframe: false,
roles: ['admin'],
icon: item.icon
}
item.name = item.component_name
return item
}
menuData.forEach((val) => {
@@ -46,4 +46,4 @@ export const handleMenu = (menuData: Array<any>) => {
...data
]
return menu
}
}

View File

@@ -1,10 +1,10 @@
import { ElMessage, MessageOptions } from 'element-plus';
import { ElMessage, ElNotification, MessageOptions } from 'element-plus';
export function message(message: string, option?: MessageOptions) {
ElMessage({ message, ...option });
}
export function successMessage(message: string, option?: MessageOptions) {
ElMessage({ message, ...option, type: 'success' });
ElMessage({ message, type: 'success' });
}
export function warningMessage(message: string, option?: MessageOptions) {
ElMessage({ message, ...option, type: 'warning' });
@@ -15,3 +15,19 @@ export function errorMessage(message: string, option?: MessageOptions) {
export function infoMessage(message: string, option?: MessageOptions) {
ElMessage({ message, ...option, type: 'info' });
}
export function notification(message: string) {
ElNotification({ message });
}
export function successNotification(message: string) {
ElNotification({ message, type: 'success' });
}
export function warningNotification(message: string) {
ElNotification({ message, type: 'warning' });
}
export function errorNotification(message: string) {
ElNotification({ message, type: 'error' });
}
export function infoNotification(message: string) {
ElNotification({ message, type: 'info' });
}

View File

@@ -1,7 +1,7 @@
import axios from 'axios';
import { get } from 'lodash-es';
import { ElMessage, ElMessageBox } from 'element-plus'
import type { Action } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus';
import type { Action } from 'element-plus';
// @ts-ignore
import { errorLog, errorCreate } from './tools.ts';
@@ -9,6 +9,7 @@ import { errorLog, errorCreate } from './tools.ts';
// import { useUserStore } from "../store/modules/user";
import { Local, Session } from '/@/utils/storage';
import qs from 'qs';
import { getBaseURL } from './baseUrl';
/**
* @description 创建请求实例
*/
@@ -17,16 +18,23 @@ function createService() {
const service = axios.create({
timeout: 20000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
'Content-Type': 'application/json;charset=utf-8',
},
paramsSerializer: {
serialize(params) {
return qs.stringify(params, { indices: false,encoder: (val:string) => {
if (typeof val === 'boolean') {
return val ? 1 : 0;
}
return val;
} });
interface paramsObj {
[key: string]: any;
}
let result: paramsObj = {};
for (const [key, value] of Object.entries(params)) {
if (value !== '') {
result[key] = value;
}
if (typeof value === 'boolean') {
result[key] = value ? 'True' : 'False';
}
}
return qs.stringify(result);
},
},
});
@@ -62,21 +70,21 @@ function createService() {
// 有 code 代表这是一个后端接口 可以进行进一步的判断
switch (code) {
case 400:
Local.clear();
Session.clear();
// Local.clear();
// Session.clear();
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
window.location.reload();
// window.location.reload();
break;
case 401:
Local.clear();
Session.clear();
dataAxios.msg = '登录授权过期,请重新登录';
dataAxios.msg = '登录认证失败,请重新登录';
ElMessageBox.alert(dataAxios.msg, '提示', {
confirmButtonText: 'OK',
callback: (action: Action) => {
window.location.reload();
},
})
});
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
break;
case 2000:
@@ -86,6 +94,9 @@ function createService() {
return dataAxios;
}
return dataAxios;
case 4000:
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
return dataAxios;
default:
// 不是正确的 code
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
@@ -100,7 +111,15 @@ function createService() {
error.message = '请求错误';
break;
case 401:
error.message = '未授权,请登录';
Local.clear();
Session.clear();
error.message = '登录授权过期,请重新登录';
ElMessageBox.alert(error.message, '提示', {
confirmButtonText: 'OK',
callback: (action: Action) => {
window.location.reload();
},
});
break;
case 403:
error.message = '拒绝访问';
@@ -154,7 +173,7 @@ function createRequestFunction(service: any) {
'Content-Type': get(config, 'headers.Content-Type', 'application/json'),
},
timeout: 5000,
baseURL: import.meta.env.VITE_API_URL as any,
baseURL: getBaseURL(),
data: {},
};
@@ -175,3 +194,34 @@ export const request = createRequestFunction(service);
// 用于模拟网络请求的实例和请求方法
export const serviceForMock = createService();
export const requestForMock = createRequestFunction(serviceForMock);
/**
* 下载文件
* @param url
* @param params
* @param method
* @param filename
*/
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
request({
url: url,
method: method,
params: params,
responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then((res: any) => {
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
const fileName = xlsxName || `${filename}.xlsx`
if (res) {
const blob = new Blob([res.data], { type: 'charset=utf-8' })
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象0
document.body.removeChild(elink)
}
})
}

View File

@@ -2,7 +2,6 @@
const cssCdnUrlList: Array<string> = [
'//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
const jsCdnUrlList: Array<string> = [];

View File

@@ -3,16 +3,16 @@
* @param {String} jsonString 需要解析的 json 字符串
* @param {String} defaultValue 默认值
*/
import { uiContext } from "@fast-crud/fast-crud";
import { uiContext } from '@fast-crud/fast-crud';
export function parse(jsonString = "{}", defaultValue = {}) {
let result = defaultValue;
try {
result = JSON.parse(jsonString);
} catch (error) {
console.log(error);
}
return result;
export function parse(jsonString = '{}', defaultValue = {}) {
let result = defaultValue;
try {
result = JSON.parse(jsonString);
} catch (error) {
console.log(error);
}
return result;
}
/**
@@ -21,8 +21,8 @@ export function parse(jsonString = "{}", defaultValue = {}) {
* @param {String} msg 状态信息
* @param {Number} code 状态码
*/
export function response(data = {}, msg = "", code = 0) {
return [200, { code, msg, data }];
export function response(data = {}, msg = '', code = 0) {
return [200, { code, msg, data }];
}
/**
@@ -30,8 +30,8 @@ export function response(data = {}, msg = "", code = 0) {
* @param {Any} data 返回值
* @param {String} msg 状态信息
*/
export function responseSuccess(data = {}, msg = "成功") {
return response(data, msg);
export function responseSuccess(data = {}, msg = '成功') {
return response(data, msg);
}
/**
@@ -40,30 +40,62 @@ export function responseSuccess(data = {}, msg = "成功") {
* @param {String} msg 状态信息
* @param {Number} code 状态码
*/
export function responseError(data = {}, msg = "请求失败", code = 500) {
return response(data, msg, code);
export function responseError(data = {}, msg = '请求失败', code = 500) {
return response(data, msg, code);
}
/**
* @description 记录和显示错误
* @param {Error} error 错误对象
*/
export function errorLog(error:any,notification=true) {
// 打印到控制台
console.error(error);
// 显示提示
if(notification){
uiContext.get().notification.error({ message: error.message });
}
export function errorLog(error: any, notification = true) {
// 打印到控制台
console.error(error);
// 显示提示
if (notification) {
uiContext.get().notification.error({ message: error.message });
}
}
/**
* @description 创建一个错误
* @param {String} msg 错误信息
*/
export function errorCreate(msg:any,notification=true) {
const error = new Error(msg);
errorLog(error,notification);
// throw error;
export function errorCreate(msg: any, notification = true) {
const error = new Error(msg);
errorLog(error, notification);
// throw error;
}
/**
* @description base64转file
* @param {String} base64 base64字符串
* @param {String} fileName 文件名
*/
export function base64ToFile(base64: any, fileName: string) {
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
let data = base64.split(',');
// 利用正则表达式 从前缀中获取图片的类型信息image/png、image/jpeg、image/webp等
let type = data[0].match(/:(.*?);/)[1];
// 从图片的类型信息中 获取具体的文件格式后缀png、jpeg、webp
let suffix = type.split('/')[1];
// 使用atob()对base64数据进行解码 结果是一个文件数据流 以字符串的格式输出
const bstr = window.atob(data[1]);
// 获取解码结果字符串的长度
let n = bstr.length;
// 根据解码结果字符串的长度创建一个等长的整形数字数组
// 但在创建时 所有元素初始值都为 0
const u8arr = new Uint8Array(n);
// 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
while (n--) {
// charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
u8arr[n] = bstr.charCodeAt(n);
}
// 利用构造函数创建File文件对象
// new File(bits, name, options)
const file = new File([u8arr], `${fileName}.${suffix}`, {
type: type,
});
// 将File文件对象返回给方法的调用者
return file;
}

View File

@@ -30,7 +30,7 @@ const websocket: socket = {
}
const token = Session.get('token')
if(!token){
message.warning('websocket认证失败')
// message.warning('websocket认证失败')
return null
}
const wsUrl = `${getWsBaseURL()}ws/${token}/`

View File

@@ -0,0 +1,9 @@
/*
*
* 后端API接口响应数据
*/
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}

View File

@@ -0,0 +1,16 @@
import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
export let pluginsAll: any = [];
// 扫描插件目录并注册插件
export const scanAndInstallPlugins = (app: any) => {
const components = import.meta.glob('./**/*.vue');
const pluginNames = new Set();
// 遍历对象并注册异步组件
for (const [key, value] of Object.entries(components)) {
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);
};

View File

@@ -1,41 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/area/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -1,13 +1,10 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { successMessage } from '/@/utils/message';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -26,8 +23,8 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
* @param row
* @returns {Promise<unknown>}
*/
const loadContentMethod = (tree: any, treeNode: any, resolve: any) => {
api.GetList({ pcode: tree.code }).then((res: any) => {
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
resolve(res.data);
});
};
@@ -40,6 +37,24 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
pagination: {
show: false,
},
@@ -90,8 +105,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: true,
},
treeNode: true,
width: 160,
type: 'input',
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -108,6 +125,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: true,
},
type: 'input',
column:{
minWidth: 90,
},
form: {
rules: [
// 表单校验规则
@@ -124,6 +144,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -140,6 +163,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: false,
rules: [
@@ -153,6 +179,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
initials: {
title: '首字母',
column:{
minWidth: 100,
},
form: {
rules: [
// 表单校验规则
@@ -169,14 +198,26 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
show: true,
},
width: 90,
type: 'dict-radio',
column: {
minWidth:90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
form: {
value: true,
},
},
},
},

View File

@@ -4,20 +4,12 @@
</fs-page>
</template>
<script lang="ts" setup>
<script lang="ts" setup name="areas">
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -0,0 +1,25 @@
import { request } from '/@/utils/service';
import { PageQuery } from './types'
export function getRoleList(query: PageQuery) {
return request({
url: '/api/system/role/',
method: 'get',
params: query,
});
}
export function getMenuList(query: PageQuery) {
return request({
url: '/api/system/menu/',
method: 'get',
params: {is_catalog:0,...query},
});
}
export function getModelList() {
return request({
url: '/api/system/column/get_models/',
method: 'get',
});
}

View File

@@ -0,0 +1,115 @@
<template>
<div class="columns-form-com">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item label="字段名" prop="field_name">
<el-input v-model="formData.field_name" placeholder="请输入字段名" />
</el-form-item>
<el-form-item label="列名" prop="title">
<el-input v-model="formData.title" placeholder="请输入列名" />
</el-form-item>
<el-form-item label="创建显示">
<el-switch v-model="formData.is_create" />
</el-form-item>
<el-form-item label="编辑显示">
<el-switch v-model="formData.is_update" />
</el-form-item>
<el-form-item label="查询显示">
<el-switch v-model="formData.is_query" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit" :loading="btnLoading"> 确定 </el-button>
<el-button @click="handleClose">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue';
import { addColumnsData, updateColumnsData } from '../ColumnsTableCom/api';
import { successNotification } from '/@/utils/message';
import { CurrentInfoType, ColumnsFormDataType } from '../../types';
import type { FormInstance } from 'element-plus';
const props = defineProps({
currentInfo: {
type: Object as () => CurrentInfoType,
required: true,
default: () => {},
},
initFormData: {
type: Object as () => Partial<ColumnsFormDataType>,
default: () => {},
},
});
const emit = defineEmits(['drawerClose']);
const formRef = ref<FormInstance>();
const formRules = reactive({
field_name: [{ required: true, message: '请输入字段名!', trigger: 'blur' }],
title: [{ required: true, message: '请输入列名!', trigger: 'blur' }],
});
let formData = reactive<ColumnsFormDataType>({
field_name: '',
title: '',
is_create: true,
is_update: true,
is_query: true,
});
let btnLoading = ref(false);
const setMenuFormData = () => {
if (props.initFormData?.id) {
formData.id = props.initFormData?.id || '';
formData.field_name = props.initFormData.field_name || '';
formData.title = props.initFormData.title || '';
formData.is_create = !!props.initFormData.is_create;
formData.is_update = !!props.initFormData.is_update;
formData.is_query = !!props.initFormData.is_query;
}
};
const handleSubmit = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return;
try {
btnLoading.value = true;
let res;
if (formData.id) {
res = await updateColumnsData({ ...formData, ...props.currentInfo });
} else {
res = await addColumnsData({ ...formData, ...props.currentInfo });
}
if (res?.code === 2000) {
successNotification(res.msg as string);
handleClose('submit');
}
} finally {
btnLoading.value = false;
}
});
};
const handleClose = (type: string = '') => {
emit('drawerClose', type);
formRef.value?.resetFields();
};
onMounted(() => {
setMenuFormData();
});
</script>
<style lang="scss" scoped>
.columns-form-com {
height: 100%;
padding: 20px;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { CurrentInfoType, AddColumnsDataType } from '../../types'
export function getColumnsData(query: any) {
return request({
url: '/api/system/column/',
method: 'get',
params: query,
});
}
export function automatchColumnsData(data: CurrentInfoType) {
return request({
url: '/api/system/column/auto_match_fields/',
method: 'post',
data,
});
}
export function addColumnsData(data: AddColumnsDataType) {
return request({
url: '/api/system/column/',
method: 'post',
data
});
}
export function deleteColumnsData(id: number) {
return request({
url: `/api/system/column/${id}/`,
method: 'delete',
});
}
export function updateColumnsData(data: AddColumnsDataType) {
return request({
url: `/api/system/column/${data.id}/`,
method: 'put',
data
});
}

View File

@@ -0,0 +1,179 @@
<template>
<div class="columns-table-com">
<p class="ctc-title">字段权限</p>
<div class="ctc-head">
<el-button type="primary" @click="handleUpdateColumn('create')">新增</el-button>
<el-button type="primary" @click="handleAutomatch">自动匹配</el-button>
</div>
<el-table :data="state.data" border v-loading="state.loading" class="ctc-table">
<el-table-column prop="field_name" label="字段名" />
<el-table-column prop="title" label="列名" />
<el-table-column label="操作" width="180" align="center">
<template #default="scope">
<el-button type="primary" @click="handleUpdateColumn('update', scope.row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="ctc-pagination">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.limit"
:page-sizes="[5, 10, 20, 50]"
:total="state.total"
background
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<el-drawer v-model="drawerVisible" title="字段权限" direction="rtl" size="500px" :close-on-click-modal="false" :before-close="handleDrawerClose">
<ColumnsFormCom v-if="drawerVisible" :currentInfo="props.currentInfo" :initFormData="drawerFormData" @drawerClose="handleDrawerClose" />
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { ElMessageBox } from 'element-plus';
import ColumnsFormCom from '../ColumnsFormCom/index.vue';
import { getColumnsData, automatchColumnsData, deleteColumnsData, updateColumnsData } from './api';
import { successNotification, warningNotification } from '/@/utils/message';
import { APIResponseData, CurrentInfoType, ColumnsFormDataType, AddColumnsDataType } from '../../types';
const props = defineProps({
currentInfo: {
type: Object as () => CurrentInfoType,
required: true,
default: () => {},
},
});
let searchParams = reactive({
page: 1,
limit: 20,
});
let state = reactive({
loading: false,
data: [],
total: 0,
});
let drawerVisible = ref(false);
let drawerFormData = ref<Partial<ColumnsFormDataType>>({});
const fetchData = async (query: CurrentInfoType = props.currentInfo) => {
try {
state.loading = true;
const res = await getColumnsData({ ...searchParams, ...query });
if (res?.code === 2000) {
state.data = res.data;
state.total = res.total;
}
} finally {
state.loading = false;
}
};
/**
* 自动匹配列
*/
const handleAutomatch = async () => {
if (props.currentInfo?.role && props.currentInfo?.model && props.currentInfo?.app) {
const res = await automatchColumnsData(props.currentInfo);
if (res?.code === 2000) {
successNotification('匹配成功');
fetchData();
}
return;
}
warningNotification('请选择角色和模型表!');
};
/**
* 新增 or 编辑
*/
const handleUpdateColumn = (type: string, record?: ColumnsFormDataType) => {
if (props.currentInfo?.role && props.currentInfo?.model && props.currentInfo?.app) {
if (type === 'update' && record) {
drawerFormData.value = record;
}
drawerVisible.value = true;
return;
}
warningNotification('请选择角色和模型表!');
};
const handleDrawerClose = (type?: string) => {
if (type === 'submit') {
fetchData();
}
drawerVisible.value = false;
drawerFormData.value = {};
};
/**
* 删除 deleteColumnsData
*/
const handleDelete = ({ id }: { id: number }) => {
ElMessageBox.confirm('确定删除该字段吗?', '提示', {
type: 'error',
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(async () => {
const res = await deleteColumnsData(id);
if (res?.code === 2000) {
successNotification('删除成功');
fetchData();
}
})
.catch(() => {});
};
const handleChange = (record: AddColumnsDataType) => {
updateColumnsData(record).then((res: APIResponseData) => {
successNotification(res.msg || '更新成功');
});
};
/**
* 分页
*/
const handleSizeChange = (limit: number) => {
searchParams.limit = limit;
fetchData();
};
const handleCurrentChange = (page: number) => {
searchParams.page = page;
fetchData();
};
defineExpose({ fetchData });
</script>
<style lang="scss" scoped>
.columns-table-com {
height: 100%;
.ctc-title {
font-size: 16px;
font-weight: 900;
padding-bottom: 10px;
border-bottom: 1px solid #dcdfe6;
}
.ctc-head {
height: 35px;
margin-top: 10px;
}
.ctc-table {
width: 100%;
height: calc(100% - 135px);
margin: 10px 0;
}
.ctc-pagination {
height: 35px;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<div class="item-com">
<p class="item-com-title">{{ props.title }}</p>
<ul class="item-com-list" :style="{ height: showPagination ? 'calc(100% - 75px)' : 'calc(100% - 45px)' }">
<li
v-for="item in state.data"
:key="item[props.value]"
@click="handleClick(item)"
:class="state.current === item[props.value] ? 'item-com-item active' : 'item-com-item'"
>
{{ item[props.label] }}
</li>
</ul>
<div v-if="showPagination" class="item-com-pagination">
<el-pagination
background
small
hide-on-single-page
v-model:current-page="state.page"
v-model:page-size="state.limit"
layout="prev, pager, next"
:pager-count="5"
:total="state.total"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, onMounted } from 'vue';
import { RoleInfoStateType } from './types';
const props = defineProps({
type: {
type: String,
default: 'role',
},
title: {
type: String,
default: '标题',
},
label: {
type: String,
default: 'name',
},
value: {
type: String,
default: 'id',
},
showPagination: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['fetchData', 'itemClick']);
const state = reactive<RoleInfoStateType>({
current: '',
page: 1,
limit: 20,
data: [],
total: 10,
});
const fetchData = () => {
emit(
'fetchData',
{
page: state.page,
limit: state.limit,
},
(res: { code: number; data: any[]; total: number }) => {
if (res?.code === 2000) {
state.data = res.data;
state.total = res?.total || 10;
}
}
);
};
const handleClick = (record: any) => {
state.current = record[props.value];
emit('itemClick', props.type, record);
};
const handleCurrentChange = (page: number) => {
state.page = page;
fetchData();
};
onMounted(() => {
fetchData();
});
</script>
<style lang="scss" scoped>
.item-com {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
.item-com-title {
font-size: 16px;
font-weight: 900;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #dcdfe6;
}
.item-com-list {
width: 100%;
white-space: nowrap;
overflow: auto;
.item-com-item {
width: fit-content;
min-width: 100%;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 500ms;
}
.active {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-8);
transition: all 500ms;
}
.item-com-item:hover {
color: var(--el-color-primary);
transition: all 500ms;
}
}
.item-com-pagination {
width: 100%;
height: 25px;
position: absolute;
bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,29 @@
export interface PageQuery {
page: number;
limit: number;
}
export interface RoleItemType {
id: number | string;
modifier_name: string;
creator_name: string;
create_datetime: string;
update_datetime: string;
description: string;
modifier: string;
dept_belong_id: number | string | null,
name: string;
key: string;
sort: number;
status: boolean;
admin: boolean;
creator: string;
}
export interface RoleInfoStateType {
current: string;
page: number;
limit: number;
data: any[],
total: number;
}

View File

@@ -0,0 +1,124 @@
<template>
<fs-page class="columns">
<el-row class="columns-el-row" :gutter="10">
<el-col :span="6">
<div class="columns-box columns-left">
<ItemCom title="角色" type="role" showPagination @fetchData="fetchRoleData" @itemClick="handleClick" />
</div>
</el-col>
<!-- <el-col :span="4">-->
<!-- <div class="columns-box columns-left">-->
<!-- <ItemCom title="菜单" type="menu" showPagination @fetchData="fetchMenuData" @itemClick="handleClick" />-->
<!-- </div>-->
<!-- </el-col>-->
<el-col :span="8">
<div class="columns-box columns-center">
<ItemCom title="模型表" type="model" label="showText" value="key" @fetchData="fetchModelData" @itemClick="handleClick" />
</div>
</el-col>
<el-col :span="10">
<div class="columns-box columns-right">
<ColumnsTableCom ref="columnsTableRef" :currentInfo="currentInfo" />
</div>
</el-col>
</el-row>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import ItemCom from './components/ItemCom/index.vue';
import ColumnsTableCom from './components/ColumnsTableCom/index.vue';
import { getRoleList, getModelList,getMenuList } from './api';
import { PageQuery, CurrentInfoType, ModelItemType } from './types';
const columnsTableRef = ref<InstanceType<typeof ColumnsTableCom> | null>(null);
let currentInfo = reactive<CurrentInfoType>({
role: '',
model: '',
app: '',
menu:''
});
/**
* 获取角色
* @param query
* @param callback
*/
const fetchRoleData = async (query: PageQuery, callback: Function) => {
const res = await getRoleList(query);
callback(res);
};
/**
* 获取菜单
* @param query
* @param callback
*/
const fetchMenuData= async (query: PageQuery, callback: Function) => {
const res = await getMenuList(query);
callback(res);
};
const fetchModelData = async (query: PageQuery, callback: Function) => {
const res = await getModelList();
res.data.forEach((item: ModelItemType) => {
item.showText = `${item.app}-${item.title}(${item.key})`;
});
callback(res);
};
const fetchTableData = () => {
if (currentInfo.role && currentInfo.model && currentInfo.app) {
columnsTableRef.value?.fetchData(currentInfo);
return;
}
};
const handleClick = (type: string, record: any) => {
if (type === 'role') {
currentInfo.role = record.id;
}
if(type === 'menu'){
currentInfo.menu = record.id;
}
if (type === 'model') {
currentInfo.model = record.key;
currentInfo.app = record.app;
}
fetchTableData();
};
</script>
<style lang="scss" scoped>
.columns {
.columns-el-row {
height: 100%;
overflow: hidden;
.el-col {
height: 100%;
padding: 10px 0;
box-sizing: border-box;
}
}
.columns-box {
height: 100%;
padding: 10px;
background-color: #fff;
box-sizing: border-box;
}
.columns-left {
border-radius: 0 8px 8px 0;
}
.columns-center {
border-radius: 8px;
}
.columns-right {
position: relative;
border-radius: 8px 0 0 8px;
}
}
</style>

View File

@@ -0,0 +1,43 @@
export interface PageQuery {
page: number;
limit: number;
}
export interface APIResponseData {
code?: number;
data: any;
msg?: string;
}
export interface CurrentInfoType {
role: string;
model: string;
app: string;
menu: string;
}
export interface ModelItemType {
app: string;
key: string;
title: string;
showText?: string;
}
export interface AddColumnsDataType extends CurrentInfoType {
id?: number | string;
field_name: string;
title: string;
is_query: boolean;
is_create: boolean;
is_update: boolean;
}
export interface ColumnsFormDataType {
id?: number | string;
field_name: string;
title: string;
is_create: boolean;
is_update: boolean;
is_query: boolean;
}

View File

@@ -1,9 +1,9 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import XEUtils from 'xe-utils';
export const apiPrefix = '/api/system/system_config/';
export function GetList(query: PageQuery) {
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
@@ -52,10 +52,10 @@ export function GetAssociationTable() {
});
}
export function saveContent (data:any) {
export function saveContent(data: any) {
return request({
url: apiPrefix + 'save_content/',
method: 'put',
data: data
})
data: data,
});
}

View File

@@ -40,7 +40,7 @@
<el-input-number v-model="form.sort" :min="0" :max="99"></el-input-number>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">立即创建</el-button>
<el-button type="primary" @click="onSubmit(formRef)">立即创建</el-button>
</el-form-item>
</el-form>
</div>
@@ -49,7 +49,7 @@
<script setup lang="ts">
import * as api from '../api';
import associationTable from './components/associationTable.vue';
import { ref, reactive, onMounted } from 'vue';
import {ref, reactive, onMounted, inject} from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { successMessage } from '/@/utils/message';
import { dictionary } from '/@/utils/dictionary';
@@ -118,12 +118,17 @@ const getParent = () => {
parentOptions.value = res.data;
});
};
const refreshView:any = inject('refreshView')
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
api.AddObj(form).then((res: any) => {
if (res.code == 2000) successMessage('新增成功');
if (res.code == 2000) {
successMessage('新增成功');
refreshView()
}
});
} else {
console.log('error submit!', fields);

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