212 Commits
v3.8.3 ... main

Author SHA1 Message Date
RuoYi
168ad6a3b6 行内表单默认设置固定宽度 2025-12-10 09:37:56 +08:00
RuoYi
d2b4332589 优化表单构建关闭页签销毁复制插件 2025-12-04 13:03:38 +08:00
RuoYi
b086e172ae 优化生成代码下载的zip文件名 2025-12-03 10:26:48 +08:00
RuoYi
e7d8010390 登录/注册页面底部版权信息修改为读取配置 2025-12-02 15:29:17 +08:00
RuoYi
b51b7cfa04 修正命名Default 2025-12-02 13:11:06 +08:00
RuoYi
199c9951f5 修复表单构建移除所有控件后切换路由回来空白问题 2025-12-02 13:07:15 +08:00
RuoYi
b8019c9f8e 添加新群号:174569686 2025-10-05 20:10:57 +08:00
RuoYi
ccc452eb6b 修复固定头部时出现的导航栏偏移问题(ICV9OH) 2025-09-04 19:58:43 +08:00
RuoYi
abe8095320 用户导入添加验证提示 2025-08-23 11:45:28 +08:00
RuoYi
215fab2ea6 优化布局设置显示 2025-08-23 11:45:03 +08:00
RuoYi
0547a82b71 升级element-plus到最新版本2.10.7 2025-08-21 14:43:36 +08:00
RuoYi
91527eb654 用户归属部门新增清除 2025-08-21 14:40:34 +08:00
RuoYi
5b2b495da5 优化checkbox废弃API 2025-08-14 10:43:32 +08:00
RuoYi
f1be984efb columns default value 2025-08-09 16:13:36 +08:00
RuoYi
03dd370d1b 显示列信息支持对象格式 2025-08-09 13:23:04 +08:00
RuoYi
d231e28847 update transition enter 2025-07-14 12:52:18 +08:00
RuoYi
4ac33ed630 添加新群号:191164766 2025-06-20 11:39:37 +08:00
RuoYi
65b97a9018 消除控制台警告信息 2025-06-06 10:39:42 +08:00
RuoYi
4da85f6491 update element-plus version 2025-06-06 10:17:36 +08:00
RuoYi
f237f27861 升级组件依赖到最新版本 2025-06-04 15:31:48 +08:00
RuoYi
5723e17eef 若依 3.9.0 2025-05-28 08:50:55 +08:00
RuoYi
b75fd760e8 添加底部版权信息及开关 2025-05-24 14:25:13 +08:00
RuoYi
40037b8c3b 添加页签图标显示开关功能 2025-05-23 14:57:04 +08:00
RuoYi
f5b8eba57a 账号密码支持自定义更新周期 2025-05-23 09:05:10 +08:00
RuoYi
37c94666fb 初始密码支持自定义修改策略 2025-05-22 23:09:24 +08:00
RuoYi
dbe3df5edd 新增用户默认初始化密码 2025-05-22 22:41:17 +08:00
RuoYi
1428299f55 优化代码 2025-05-15 10:01:04 +08:00
RuoYi
ebe0df0d37 优化导航栏显示昵称&设置 2025-05-09 13:54:07 +08:00
RuoYi
19348408d3 菜单搜索支持键盘选择&悬浮主题背景 2025-05-07 13:23:15 +08:00
RuoYi
9ee89f7085 图片上传组件新增disabled属性 2025-05-06 19:14:23 +08:00
RuoYi
060adcbdc1 代码生成列支持拖动排序 2025-05-06 14:41:20 +08:00
RuoYi
8eee596292 修复上传组件被多次引用拖动仅对第一个有效的问题 2025-05-06 13:43:05 +08:00
RuoYi
dae8c5016c 上传组件新增拖动排序属性 2025-04-30 10:29:32 +08:00
RuoYi
c88c3d6c69 外链加载时遮罩信息提示 2025-04-30 08:25:06 +08:00
RuoYi
2d0fc59fc7 添加页签openPage支持传递参数 2025-04-27 13:43:28 +08:00
RuoYi
2f8a257efd remove all semicolons 2025-04-27 09:58:29 +08:00
RuoYi
7de94e2ea3 富文本复制粘贴图片上传至url 2025-04-24 14:21:14 +08:00
RuoYi
2aa0b4e521 update vite.config.js 2025-04-24 11:22:10 +08:00
RuoYi
a4c4653802 优化富文本控制台警告异常 2025-04-22 11:49:44 +08:00
RuoYi
06d52deb60 优化代码 2025-04-22 11:48:56 +08:00
RuoYi
f2a0f69465 显隐列组件支持全选/全不选 2025-04-21 15:29:14 +08:00
RuoYi
589151d1ed 优化菜单搜索查询页 2025-04-21 13:23:19 +08:00
RuoYi
2dbc91653a 新增默认打包配置项 2025-04-18 14:43:50 +08:00
RuoYi
ff14fa5e4e 支持文件&图片组件自定义地址&参数 2025-04-18 12:55:34 +08:00
RuoYi
c2005614bc 优化角色禁用不允许分配 2025-04-17 15:08:32 +08:00
RuoYi
42c1ea3c89 添加新群号:287842588 2025-04-01 19:14:59 +08:00
RuoYi
4b038efabf 登录页和注册页表头使用VUE_APP_TITLE配置值 2025-03-18 15:56:20 +08:00
RuoYi
ea552797b5 优化代码 2025-03-14 16:10:00 +08:00
RuoYi
d9ecdc9a53 优化代码 2025-03-04 20:31:30 +08:00
RuoYi
11889edf70 优化顶部菜单搜索栏为多层级显示 2025-03-03 12:06:57 +08:00
RuoYi
93fe80dc68 优化前端处理路由函数代码 2025-03-01 15:04:28 +08:00
RuoYi
537fc97695 优化前端树结构性能问题 2025-03-01 14:54:05 +08:00
RuoYi
ca5ae82f9e pagination更换成flex布局 2025-03-01 14:38:00 +08:00
RuoYi
094e300939 代码生成列表支持按时间排序 2025-02-28 19:45:29 +08:00
RuoYi
3393757b8c 文件上传组件新增类型 2025-02-28 19:45:03 +08:00
RuoYi
33e2eb7642 文件上传组件新增disabled属性 2025-02-28 13:09:13 +08:00
RuoYi
ec7183d094 update index.vue 2025-02-28 13:08:59 +08:00
RuoYi
865100e487 添加新群号 2025-01-15 15:07:18 +08:00
RuoYi
ef417f3e90 copyright 2025 2025-01-07 10:41:45 +08:00
RuoYi
31cffcb9d4 若依 3.8.9 2024-12-30 08:35:42 +08:00
RuoYi
7701ec465c 优化特殊字符密码修改失败问题 2024-12-17 14:26:57 +08:00
RuoYi
8b97a0c09b 优化TopNav内链菜单点击没有高亮(IB8WHJ) 2024-12-17 14:26:23 +08:00
RuoYi
5382a52d22 update README 2024-12-13 20:07:07 +08:00
RuoYi
c89ba258bc 用户管理过滤掉已禁用部门(IB5H7F) 2024-12-11 11:20:59 +08:00
RuoYi
7644a87718 优化代码 2024-12-05 16:45:54 +08:00
RuoYi
4db1eedc6a 新增表单构建功能 2024-12-05 10:52:33 +08:00
RuoYi
e212a0ab75 支持开启暗黑模式 2024-12-04 20:32:06 +08:00
RuoYi
535c5bd814 白名单支持对通配符路径匹配 2024-12-04 08:49:18 +08:00
RuoYi
497f98a2a8 修复默认关闭Tags-Views时,内链页面打不开 2024-11-27 19:50:47 +08:00
RuoYi
6061c3548f 优化代码 2024-11-25 22:20:30 +08:00
RuoYi
f8c4968b33 面板兼容移动端显示 2024-11-25 15:43:05 +08:00
RuoYi
e986a4c537 参数键值更换为多行文本 2024-11-25 12:16:10 +08:00
RuoYi
3e029957af 菜单面包屑导航支持多层级显示 2024-11-22 20:59:14 +08:00
RuoYi
d981cac3f3 分栏参数微调 2024-11-22 14:49:14 +08:00
RuoYi
341728774c 用户管理支持分栏拖动 2024-11-22 12:58:50 +08:00
RuoYi
e82ee73704 用户头像http(s)链接支持 2024-11-20 10:47:31 +08:00
RuoYi
2a59dbfbb3 校检文件名是否包含特殊字符 2024-11-05 12:50:44 +08:00
RuoYi
341e3be3a9 优化字典数组条件判断 2024-10-29 16:34:56 +08:00
RuoYi
7507a187aa 优化上传图片带域名不增加前缀 2024-10-21 16:56:24 +08:00
RuoYi
d02ba57ffb 修复代码生成上级菜单显示问题 2024-09-27 16:02:40 +08:00
RuoYi
f476e35522 删除多余的判断 2024-09-27 12:43:54 +08:00
RuoYi
1c2da6dfb1 修改导出文件名称 2024-09-11 14:19:21 +08:00
RuoYi
bbb45eb72e avatar add headers 2024-07-02 16:08:11 +08:00
RuoYi
5d8172a117 cron生成的表达式hour优化 2024-07-01 16:16:35 +08:00
RuoYi
c01ac5ccac 若依 3.8.8 2024-06-30 08:01:12 +08:00
RuoYi
0780d9659b 修复移动端左侧菜单无法显示问题 2024-06-29 21:44:21 +08:00
RuoYi
c227751acb 菜单管理新增路由名称 2024-06-29 18:53:38 +08:00
RuoYi
30f52f9946 fix repeat getList 2024-06-29 16:21:36 +08:00
RuoYi
4805cc9ea5 remove merge dictTag type 2024-06-29 15:59:59 +08:00
若依
1abf0641b0 Merge pull request #287 from 593496637/main
修复升级element-plus出现的警告⚠️提示
2024-06-29 15:39:18 +08:00
likaikai
e3d4018c14 chore:1.修复升级element-plus后el-radio使用label过期的问题。2.修复DictTag组件缺少type的问题 2024-06-29 14:11:19 +08:00
RuoYi
cae65bb74b 升级组件依赖到最新版本 2024-06-28 17:02:24 +08:00
RuoYi
6666ce5526 优化代码 2024-06-28 17:01:39 +08:00
RuoYi
49d0fc0bdc 添加新群号:151450850 2024-05-29 14:49:15 +08:00
RuoYi
84b3565732 update copyright 2024 2024-03-11 10:47:10 +08:00
RuoYi
ea4a870302 添加新群号:138988063 2024-03-11 10:46:45 +08:00
RuoYi
6a10846b6d 用户密码新增非法字符验证 2024-03-01 21:54:12 +08:00
RuoYi
69c189a0c9 代码生成新增创建表结构功能 2024-03-01 20:08:52 +08:00
RuoYi
1270e6e3c9 添加新群号:161281055 2023-12-13 11:18:51 +08:00
RuoYi
7d09f25218 若依 3.8.7 2023-12-08 09:02:39 +08:00
RuoYi
353f76740a 修复定时任务年表达式出现NaN问题 2023-12-01 21:54:17 +08:00
RuoYi
5186e2be77 TopNav开关事件控制 2023-12-01 21:53:23 +08:00
RuoYi
64036d520f 升级组件依赖到最新版本 2023-12-01 14:20:45 +08:00
RuoYi
15b2acbcd5 显隐列组件支持复选框弹出类型 2023-12-01 12:02:01 +08:00
RuoYi
51db1ec865 代码生成支持选择前端模板类型 2023-11-30 09:41:14 +08:00
RuoYi
0b124ef12b 优化菜单图标选择后点击其他区域闪烁问题 2023-11-30 09:41:05 +08:00
RuoYi
9e1616b075 优化控制台提示路由重复问题 2023-11-30 09:40:51 +08:00
RuoYi
ad0c7f5c94 优化头像上传参数新增文件名称 2023-11-29 12:45:00 +08:00
RuoYi
cb80de3742 优化字典标签支持自定义分隔符 2023-11-29 12:44:42 +08:00
RuoYi
438cb0d1dd 优化下载zip方法新增遮罩层 2023-11-29 12:44:01 +08:00
RuoYi
6ec98f2e64 优化缓存监控图表支持跟随屏幕大小自适应调整 2023-11-29 12:43:44 +08:00
RuoYi
dfb8096387 优化代码 2023-11-29 12:43:28 +08:00
RuoYi
e6f582bd1a 优化白名单页面放行逻辑 2023-11-28 13:00:15 +08:00
RuoYi
afd8de5094 优化个人中心/基本资料修改时数据显示问题 2023-11-28 11:26:51 +08:00
RuoYi
87be9b7acf 修复五级路由缓存无效问题 2023-11-10 15:34:08 +08:00
RuoYi
d3672b2714 优化样式 2023-11-10 14:48:44 +08:00
RuoYi
46da1d5ed1 修复内链iframe没有传递参数问题(I8DUOJ) 2023-11-10 11:13:38 +08:00
RuoYi
29355eb630 优化pagination样式 2023-11-01 10:13:19 +08:00
RuoYi
a4004968ec 修复字典数据重置选项置空问题 2023-11-01 09:55:42 +08:00
RuoYi
aea6ee2900 更换表格内预览图片被覆盖问题 2023-10-27 12:56:16 +08:00
RuoYi
98cba51541 登录不做数据重复提交验证 2023-10-21 14:31:24 +08:00
RuoYi
40648703fc 添加新群号:174951577 2023-10-09 21:28:31 +08:00
RuoYi
c12a0976ef 去除多余的参数 2023-10-09 21:28:17 +08:00
RuoYi
e8fe5f1455 修复HeaderSearch组件跳转query参数丢失问题 2023-09-28 22:12:58 +08:00
RuoYi
8e7754c5d7 富文本编辑器支持fileSize和type 2023-09-28 11:30:32 +08:00
RuoYi
895d9a37d5 升级vue-quill到最新版本1.2.0 2023-09-28 11:29:40 +08:00
RuoYi
27a1153b5a 操作日志列表新增IP地址查询 2023-09-27 15:22:27 +08:00
RuoYi
205a67504b 全局数据存储用户编号 2023-09-27 15:22:15 +08:00
RuoYi
8491d97001 优化菜单管理类型为按钮状态可选 2023-09-18 15:05:09 +08:00
RuoYi
1b45f0a1cd 修复自定义字典样式不生效的问题(I81F03) 2023-09-14 16:54:43 +08:00
RuoYi
accdc1be7a LICENSE 2023-09-01 08:44:50 +08:00
RuoYi
df9c850302 优化代码 2023-09-01 08:44:35 +08:00
RuoYi
54ad578ec0 TopNav更多菜单arrow样式优化 2023-08-31 18:00:47 +08:00
RuoYi
c5fe5bf3cb 修改未登录访问需要登录的资源,在登录后重定向丢失请求参数问题 2023-08-31 11:47:54 +08:00
RuoYi
a9196bbdf7 优化菜单样式 2023-08-31 10:02:21 +08:00
RuoYi
64014ba81a 优化控制台debuger位置错误问题 2023-08-28 13:02:33 +08:00
RuoYi
2e1bd34d58 防重复提交数据大小限制(I7KZDA) 2023-08-21 11:57:30 +08:00
RuoYi
38c52a9265 登录页面redirect重定向功能(I7DBUP) 2023-08-15 16:31:59 +08:00
RuoYi
d6aa40cf3b 优化定时任务状态页面显示 2023-08-14 19:21:29 +08:00
RuoYi
cce8921b52 添加新群号:143961921 2023-07-28 11:09:34 +08:00
RuoYi
7b8d2db756 若依 3.8.6 2023-06-30 08:24:18 +08:00
RuoYi
fab5c92d26 修改侧边栏的平台标题内容与process.env.VITE_APP_TITLE保持同步 2023-06-29 10:25:10 +08:00
RuoYi
9e7d46217a 添加新群号:136919097 2023-04-23 16:43:50 +08:00
RuoYi
78526a4bab 恢复翻页/切换路由滚动功能 2023-04-23 16:43:38 +08:00
RuoYi
8552850e2c DictTag组件,当value没有匹配的值时,展示value 2023-04-23 16:43:15 +08:00
RuoYi
4250792a2a 修复没有重叠的判断 2023-04-23 16:42:33 +08:00
RuoYi
ab98f29c3f 缓存列表:多次清除操作,提示不变的问题 2023-04-23 16:42:03 +08:00
RuoYi
3535b4d67f 修复开启TopNav后一级菜单路由参数设置无效问题(I6T1DK) 2023-04-11 16:52:09 +08:00
RuoYi
4610953b11 优化已选择下拉图标高亮回显 2023-04-10 18:33:56 +08:00
RuoYi
1d14ade6b5 优化避免鼠标移出时无法隐藏滚动条的问题 2023-04-10 18:33:40 +08:00
RuoYi
a4289fa8c3 添加新群号:101046199 2023-04-05 18:30:39 +08:00
RuoYi
c46abc1c79 优化固定头部页签滚动条被隐藏的问题 2023-04-05 18:30:25 +08:00
RuoYi
f61979f419 移除vue-multiselect样式 2023-04-05 18:23:43 +08:00
RuoYi
0fca81b350 关闭页签后存在其他页签时不应该跳转首页 2023-03-17 14:31:27 +08:00
RuoYi
3286323c8c 优化弹窗后导航栏偏移的问题(I3YMWW) 2023-03-17 14:24:54 +08:00
RuoYi
16c38acb10 修复页面切换时布局错乱的问题(I6LR1D) 2023-03-17 14:24:02 +08:00
RuoYi
ee2205d4f1 新增富文本组件支持 2023-03-06 14:19:40 +08:00
RuoYi
612164abde 优化文件下载出现的异常(I6DLNU) 2023-02-28 14:42:03 +08:00
RuoYi
e7ad4f5048 日志管理使用索引提升查询性能 2023-02-23 10:55:28 +08:00
RuoYi
0fd409dd58 支持登录IP黑名单限制 2023-02-21 09:02:36 +08:00
RuoYi
bce42bf13c 新增监控页面图标显示 2023-02-17 08:54:46 +08:00
RuoYi
10b387ff94 操作日志新增消耗时间属性 2023-02-16 11:05:31 +08:00
RuoYi
d70239952c 操作日志新增消耗时间属性 2023-02-16 10:23:43 +08:00
RuoYi
6a5e441155 首页页签右键选择时不显示关闭左侧 2023-02-04 22:40:20 +08:00
RuoYi
1f25c95c05 update copyright 2023 2023-02-04 22:33:47 +08:00
RuoYi
d32db45e36 重置时取消部门选中 2023-01-19 11:08:57 +08:00
RuoYi
1aba7f61d9 添加新群号:108482800 2023-01-11 12:55:12 +08:00
RuoYi
b1780776ff 若依 3.8.5 2023-01-01 09:09:01 +08:00
RuoYi
7c258f8e7e 升级element-plus到最新版本2.2.27 2022-12-23 16:03:46 +08:00
RuoYi
d9451837df 删除fuse选项maxPatternLength 2022-12-08 09:00:05 +08:00
RuoYi
b87927b75d 修复树形下拉不能默认选中(I64ESN) 2022-12-07 20:24:23 +08:00
RuoYi
5237cd281c 删除cron表达式组件的重复使用类名 2022-12-07 20:21:18 +08:00
RuoYi
882f49144f 修复closeSidebar这个方法定义的参数没有解构问题 2022-12-07 20:16:44 +08:00
RuoYi
13503c3289 添加新群号:170801498 2022-12-03 12:36:21 +08:00
RuoYi
7fdd053638 修复文件名包含特殊字符(+、-、*...)的文件无法下载问题 2022-12-02 09:50:25 +08:00
RuoYi
e03183eb19 修复某些特性的环境生成代码变乱码TXT文件问题 2022-11-22 09:23:32 +08:00
RuoYi
0effee74b1 消除控制台出现的警告信息 2022-11-21 18:57:42 +08:00
RuoYi
89e93a986e 开启TopNav没有子菜单隐藏侧边栏 2022-11-17 14:18:14 +08:00
RuoYi
750a09d5b7 升级组件依赖到最新版本 2022-11-17 13:50:35 +08:00
RuoYi
baa3cdffb5 优化开启TopNav后,菜单数量少时,显示。。。而不是更多菜单(I60WJG) 2022-11-16 09:37:31 +08:00
RuoYi
51b5d9537c 修复回显数据字典数组异常问题 2022-11-15 14:24:50 +08:00
RuoYi
69a246ac8a 修复调度日志点击多次数据不变化的问题 2022-11-15 14:24:34 +08:00
RuoYi
c7fa74116c 新增返回警告消息提示 2022-10-30 11:55:15 +08:00
RuoYi
1ffc739253 定时任务cron表达式非必填 2022-10-30 11:55:06 +08:00
RuoYi
ba5b8c77c3 定时任务支持在线生成cron表达式 2022-10-28 20:35:50 +08:00
RuoYi
fba2763edb 删除多余的字符 2022-10-28 20:34:07 +08:00
RuoYi
f5a16e4534 修复使用透明底png图片时,自动填充黑色背景 2022-10-28 20:13:44 +08:00
RuoYi
70d5cd2587 重置时取消部门选中 2022-10-28 20:13:12 +08:00
RuoYi
628f46535f utils/index.js引入parseTime路径错误 2022-10-28 20:12:34 +08:00
RuoYi
dccb755ae3 去除某些svg图标的fill="#bfbfbf"属性,避免菜单激活无法修改其填充颜色 2022-10-28 20:12:18 +08:00
RuoYi
082794c1de 修复文件上传组件格式验证问题(I5V32H) 2022-10-12 19:34:29 +08:00
RuoYi
c82eb94a67 修复代码生成勾选属性无效问题 2022-09-28 21:07:30 +08:00
RuoYi
13ce7966b4 若依 3.8.4 2022-09-27 20:02:10 +08:00
RuoYi
a5f02187f9 若依 3.8.4 2022-09-26 08:07:15 +08:00
RuoYi
7148a8e428 优化日志操作中重置按钮时重复查询的问题 2022-09-19 14:47:49 +08:00
RuoYi
aad94b236e 通用下载方法新增config配置选项 2022-09-19 13:22:52 +08:00
RuoYi
9290e3c318 添加新群号:160110482 2022-08-28 20:11:00 +08:00
RuoYi
fabc195e90 修复多文件上传报错出现的异常问题 2022-08-24 13:40:52 +08:00
RuoYi
439b134c9b 优化页面内嵌iframe切换tab不刷新数据 2022-08-23 20:47:41 +08:00
RuoYi
2952cb2c4f 优化多角色数据权限匹配规则 2022-08-21 22:54:25 +08:00
RuoYi
1d2f37bfe1 修复图片预览组件src属性为null值控制台报错问题(I5KBAS) 2022-08-15 12:07:45 +08:00
RuoYi
0c66bc0624 个人中心修改密码去除多余的user传递 2022-08-12 12:18:15 +08:00
RuoYi
6b236930fd 登录日志新增解锁账户功能 2022-08-08 09:24:33 +08:00
RuoYi
f8e6431684 支持全局主题颜色设置 2022-07-31 17:51:45 +08:00
RuoYi
de7e21457d 升级vue到最新版本3.2.37 2022-07-31 17:51:04 +08:00
RuoYi
d6129f8471 升级vite到最新版本2.9.14 2022-07-31 17:50:44 +08:00
RuoYi
3d29929b57 优化表格上右侧工具条(搜索按钮显隐&右侧样式凸出) 2022-07-29 20:38:55 +08:00
RuoYi
8b49d6572a 修复菜单图标文本框搜索消失问题(I5IHZV) 2022-07-27 14:56:51 +08:00
RuoYi
56680d26e1 添加新群号:104748341 2022-07-25 17:47:08 +08:00
RuoYi
cafff38a6e 优化字典数据使用store存取 2022-07-20 19:38:44 +08:00
RuoYi
6c19bfbd90 修改验证码开关变量名 2022-07-14 19:49:20 +08:00
RuoYi
331dbc44b2 修改验证码开关变量名 2022-07-12 18:13:54 +08:00
143 changed files with 10386 additions and 3595 deletions

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 RuoYi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,21 +1,20 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.8.3</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.0</h1>
<h4 align="center">基于SpringBoot+Vue3前后端分离的Java快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.8.3-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.0-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
## 平台简介
* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
* 配套后端代码仓库地址[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 或 [RuoYi-Vue-fast](https://github.com/yangzongzhuan/RuoYi-Vue-fast) 版本。
* 配套后端代码仓库地址[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 或 [RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast) 版本。
* 前端技术栈([Vue2](https://cn.vuejs.org) + [Element](https://github.com/ElemeFE/element) + [Vue CLI](https://cli.vuejs.org/zh)),请移步[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue/tree/master/ruoyi-ui)。
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
* 阿里云优惠券:[点我领取](https://www.aliyun.com/minisite/goods?userCode=brki8iof&share_source=copy_link),腾讯云优惠券:[点我领取](https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console)&nbsp;&nbsp;
## 前端运行
@@ -106,4 +105,4 @@ yarn dev
## 若依前后端分离交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/139821253-blue.svg)](https://jq.qq.com/?_wv=1027&k=njiWDmkj) 点击按钮入群。
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) 点击按钮入群。

View File

@@ -1,9 +1,10 @@
{
"name": "ruoyi",
"version": "3.8.3",
"version": "3.9.0",
"description": "若依管理系统",
"author": "若依",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build:prod": "vite build",
@@ -15,29 +16,36 @@
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
},
"dependencies": {
"@element-plus/icons-vue": "1.1.4",
"@vueuse/core": "8.5.0",
"axios": "0.26.1",
"echarts": "5.3.2",
"element-plus": "2.1.8",
"@element-plus/icons-vue": "2.3.1",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
"clipboard": "2.0.11",
"echarts": "5.6.0",
"element-plus": "2.10.7",
"file-saver": "2.0.5",
"fuse.js": "6.5.3",
"js-cookie": "3.0.1",
"jsencrypt": "3.2.1",
"fuse.js": "6.6.2",
"js-beautify": "1.14.11",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"nprogress": "0.2.0",
"pinia": "2.0.14",
"vue": "3.2.31",
"vue-cropper": "1.0.3",
"vue-router": "4.0.14"
"pinia": "3.0.2",
"splitpanes": "4.0.4",
"vue": "3.5.16",
"vue-cropper": "1.1.1",
"vue-router": "4.5.1",
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.36",
"sass": "1.52.1",
"unplugin-auto-import": "0.8.5",
"vite": "2.9.9",
"@vitejs/plugin-vue": "5.2.4",
"sass-embedded": "1.89.1",
"unplugin-auto-import": "0.18.6",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "6.3.5",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-setup-extend": "0.4.0"
"vite-plugin-svg-icons": "2.0.1"
},
"overrides": {
"quill": "2.0.2"
}
}

View File

@@ -1,3 +1,15 @@
<template>
<router-view />
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
onMounted(() => {
nextTick(() => {
// 初始化主题样式
handleThemeStyle(useSettingsStore().theme)
})
})
</script>

View File

@@ -11,7 +11,8 @@ export function login(username, password, code, uuid) {
return request({
url: '/login',
headers: {
isToken: false
isToken: false,
repeatSubmit: false
},
method: 'post',
data: data

View File

@@ -17,6 +17,14 @@ export function delLogininfor(infoId) {
})
}
// 解锁用户登录状态
export function unlockLogininfor(userName) {
return request({
url: '/monitor/logininfor/unlock/' + userName,
method: 'get'
})
}
// 清空登录日志
export function cleanLogininfor() {
return request({

View File

@@ -25,22 +25,6 @@ export function getDept(deptId) {
})
}
// 查询部门下拉树结构
export function treeselect() {
return request({
url: '/system/dept/treeselect',
method: 'get'
})
}
// 根据角色ID查询部门树结构
export function roleDeptTreeselect(roleId) {
return request({
url: '/system/dept/roleDeptTreeselect/' + roleId,
method: 'get'
})
}
// 新增部门
export function addDept(data) {
return request({

View File

@@ -109,3 +109,11 @@ export function authUserSelectAll(data) {
params: data
})
}
// 根据角色ID查询部门树结构
export function deptTreeSelect(roleId) {
return request({
url: '/system/role/deptTree/' + roleId,
method: 'get'
})
}

View File

@@ -96,7 +96,7 @@ export function updateUserPwd(oldPassword, newPassword) {
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
params: data
data: data
})
}
@@ -105,6 +105,7 @@ export function uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
method: 'post',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: data
})
}
@@ -125,3 +126,11 @@ export function updateAuthRole(data) {
params: data
})
}
// 查询部门下拉树结构
export function deptTreeSelect() {
return request({
url: '/system/user/deptTree',
method: 'get'
})
}

View File

@@ -43,6 +43,15 @@ export function importTable(data) {
})
}
// 创建表
export function createTable(data) {
return request({
url: '/tool/gen/createTable',
method: 'post',
params: data
})
}
// 预览生成代码
export function previewTable(tableId) {
return request({

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1546567861908" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2422" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M318.577778 819.2L17.066667 512l301.511111-307.2 45.511111 45.511111L96.711111 512l267.377778 261.688889zM705.422222 819.2l-45.511111-45.511111L927.288889 512l-267.377778-261.688889 45.511111-45.511111L1006.933333 512zM540.785778 221.866667l55.751111 11.150222L483.157333 802.133333l-55.751111-11.093333z" fill="#bfbfbf" p-id="2423"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1546567861908" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2422" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M318.577778 819.2L17.066667 512l301.511111-307.2 45.511111 45.511111L96.711111 512l267.377778 261.688889zM705.422222 819.2l-45.511111-45.511111L927.288889 512l-267.377778-261.688889 45.511111-45.511111L1006.933333 512zM540.785778 221.866667l55.751111 11.150222L483.157333 802.133333l-55.751111-11.093333z" p-id="2423"></path></svg>

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 717 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746590936918" class="icon" viewBox="0 0 1194 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5378" xmlns:xlink="http://www.w3.org/1999/xlink" width="233.203125" height="200"><path d="M1151.9144 325.11999969V89.12a57.04000031 57.04000031 0 0 0-28.8-49.44 58.15999969 58.15999969 0 0 0-57.76000031 0 57.04000031 57.04000031 0 0 0-28.8 49.44v235.99999969c0.24 84.31999969-33.6 152.56000031-94.08 212.00000062-60.07999969 59.83999969-141.84 80.64-227.04 80.4H225.91440031L388.07439969 457.11999969a56.80000031 56.80000031 0 0 0 12.40000031-62.16 57.76000031 57.76000031 0 0 0-94.00000031-18.63999938L48.8744 631.20000031a56.88 56.88 0 0 0 0 80.79999938l264.96 262.56a58.08 58.08 0 0 0 96.55999969-25.59999938 56.80000031 56.80000031 0 0 0-14.95999969-55.2L232.07439969 731.67999969h483.44000062c116.56000031 0 226.15999969-32.08000031 308.64-113.76 82.15999969-80.80000031 128.23999969-178.15999969 127.83999938-292.87999969" p-id="5379"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303018722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M368.832 67.2c51.328-16.384 89.216 34.112 75.712 76.416a346.816 346.816 0 0 0 435.84 435.84c42.304-13.44 92.8 24.384 76.48 75.712A467.968 467.968 0 1 1 368.832 67.2z m-35.776 122.688a368.832 368.832 0 1 0 501.056 501.056 445.952 445.952 0 0 1-501.056-501.056z" p-id="1448"></path></svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746760911144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M395.21211 182.914448c0 62.669318 49.541323 113.472378 110.642936 113.472378 61.093427 0 110.652146-50.80306 110.65214601-113.472378 0-62.685691-49.559742-113.487727-110.65214601-113.487727C444.75241 69.426721 395.21211 120.22978 395.21211 182.914448zM395.21211 523.34693101c0 62.668295 49.541323 113.487727 110.642936 113.48772699 61.093427 0 110.652146-50.820456 110.652146-113.487727 0-62.669318-49.559742-113.472378-110.652146-113.472378C444.75241 409.874553 395.21211 460.67761301 395.21211 523.34693101zM395.21211 841.084529c0 62.686714 49.541323 113.488751 110.642936 113.488751 61.093427 0 110.652146-50.80203599 110.65214601-113.488751 0-62.669318-49.559742-113.471354-110.65214601-113.471354C444.75241 727.614198 395.21211 778.416234 395.21211 841.084529z" p-id="12538"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1547360688278" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M890 120H134a70 70 0 0 0-70 70v500a70 70 0 0 0 70 70h756a70 70 0 0 0 70-70V190a70 70 0 0 0-70-70z m-10 520a40 40 0 0 1-40 40H712V448a40 40 0 0 0-80 0v232h-80V368a40 40 0 0 0-80 0v312h-80V512a40 40 0 0 0-80 0v168H184a40 40 0 0 1-40-40V240a40 40 0 0 1 40-40h656a40 40 0 0 1 40 40zM696 824H328a40 40 0 0 0 0 80h368a40 40 0 0 0 0-80z" fill="#bfbfbf" p-id="6718"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1547360688278" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M890 120H134a70 70 0 0 0-70 70v500a70 70 0 0 0 70 70h756a70 70 0 0 0 70-70V190a70 70 0 0 0-70-70z m-10 520a40 40 0 0 1-40 40H712V448a40 40 0 0 0-80 0v232h-80V368a40 40 0 0 0-80 0v312h-80V512a40 40 0 0 0-80 0v168H184a40 40 0 0 1-40-40V240a40 40 0 0 1 40-40h656a40 40 0 0 1 40 40zM696 824H328a40 40 0 0 0 0 80h368a40 40 0 0 0 0-80z" p-id="6718"></path></svg>

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 742 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303115132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12397" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 890.432c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 0 1-66.816 0v-66.752c0-18.432 14.976-33.408 33.408-33.408z m-267.52-110.848a33.408 33.408 0 0 1 0 47.232l-47.296 47.232a33.408 33.408 0 0 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0z m582.336 0l47.232 47.232a33.408 33.408 0 0 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 1 1 47.232-47.232zM512 200.32a311.68 311.68 0 1 1 0 623.296 311.68 311.68 0 0 1 0-623.36z m0 66.752a244.864 244.864 0 1 0 0 489.728 244.864 244.864 0 0 0 0-489.728zM100.16 478.592a33.408 33.408 0 1 1 0 66.816H33.408a33.408 33.408 0 0 1 0-66.816h66.752z m890.432 0a33.408 33.408 0 0 1 0 66.816h-66.752a33.408 33.408 0 1 1 0-66.816h66.752zM197.184 149.952l47.232 47.232a33.408 33.408 0 1 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 0 1 47.232-47.232z m676.864 0a33.408 33.408 0 0 1 0 47.232l-47.232 47.232a33.408 33.408 0 1 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0zM512 0c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 1 1-66.816 0V33.408C478.592 14.976 493.568 0 512 0z" p-id="12398"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,2 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543827724451" class="icon" style="" viewBox="0 0 1084 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10233" xmlns:xlink="http://www.w3.org/1999/xlink" width="211.71875" height="200"><defs><style type="text/css">@font-face { font-family: rbicon; src: url("chrome-extension://dipiagiiohfljcicegpgffpbnjmgjcnf/fonts/rbicon.woff2") format("woff2"); font-weight: normal; font-style: normal; }
</style></defs><path d="M1080.09609 434.500756c-4.216302-23.731757-26.9241-47.945376-50.595623-53.185637l-17.648235-4.095836a175.940257 175.940257 0 0 1-101.612877-80.832531 177.807476 177.807476 0 0 1-18.732427-129.801867l5.541425-16.684509c7.10748-23.129428-2.108151-54.992624-20.599646-70.833873 0 0-16.624276-14.094495-63.244529-41.199293-46.800951-26.984332-66.858502-34.513443-66.858502-34.513443-22.76803-8.372371-54.631227-0.361397-71.255503 17.407304l-12.287509 13.251234a173.470708 173.470708 0 0 1-120.465769 48.065842A174.13327 174.13327 0 0 1 421.329029 33.590675L409.583617 20.761071C393.140039 2.99237 361.096144-4.898138 338.267881 3.353767c0 0-20.358715 7.529111-67.099434 34.513443-46.800951 27.34573-63.244529 41.440225-63.244529 41.440225-18.431263 15.66055-27.646894 47.222582-20.539413 70.592941l5.059562 16.865207a178.048407 178.048407 0 0 1-18.672194 129.621169 174.916297 174.916297 0 0 1-102.275439 81.073463l-17.045906 3.854904c-23.310126 5.42096-46.258856 29.333415-50.595623 53.185637 0 0-3.854905 21.382674-3.854905 75.712737 0 54.330062 3.854905 75.712736 3.854905 75.712736 4.216302 23.972688 26.9241 47.945376 50.595623 53.185637l16.624276 3.854905a174.253736 174.253736 0 0 1 102.395904 81.314394c23.310126 40.837896 28.911785 87.337683 18.732427 129.801867l-4.81863 16.443578c-7.10748 23.129428 2.108151 54.992624 20.599646 70.833872 0 0 16.624276 14.094495 63.244529 41.199293 46.800951 27.104798 66.918735 34.513443 66.918735 34.513443 22.707798 8.372371 54.631227 0.361397 71.255503-17.407303l11.624947-12.588673a175.096996 175.096996 0 0 1 242.256662 0.120465l11.624947 12.648906c16.383345 17.708468 48.427239 25.598976 71.255503 17.347071 0 0 20.358715-7.529111 67.159666-34.513443 46.740719-27.104798 63.124063-41.199293 63.124064-41.199293 18.491496-15.600317 27.707127-47.463513 20.599646-70.833873l-5.059562-17.106139a176.723284 176.723284 0 0 1 18.672194-129.139305 176.060722 176.060722 0 0 1 102.395904-81.314394l16.68451-3.854905c23.310126-5.42096 46.258856-29.333415 50.595623-53.185637 0 0 3.854905-21.382674 3.854904-75.712737-0.240932-54.330062-4.095836-75.833202-4.095836-75.833202z m-537.819428 293.334149c-119.261112 0-216.175824-97.336342-216.175824-217.621412a216.657687 216.657687 0 0 1 216.236057-217.320249c119.200879 0 216.115591 97.276109 216.11559 217.56118-0.240932 120.044139-96.974945 217.320248-216.175823 217.320249z" p-id="10234" fill="#bfbfbf"></path></svg>
</style></defs><path d="M1080.09609 434.500756c-4.216302-23.731757-26.9241-47.945376-50.595623-53.185637l-17.648235-4.095836a175.940257 175.940257 0 0 1-101.612877-80.832531 177.807476 177.807476 0 0 1-18.732427-129.801867l5.541425-16.684509c7.10748-23.129428-2.108151-54.992624-20.599646-70.833873 0 0-16.624276-14.094495-63.244529-41.199293-46.800951-26.984332-66.858502-34.513443-66.858502-34.513443-22.76803-8.372371-54.631227-0.361397-71.255503 17.407304l-12.287509 13.251234a173.470708 173.470708 0 0 1-120.465769 48.065842A174.13327 174.13327 0 0 1 421.329029 33.590675L409.583617 20.761071C393.140039 2.99237 361.096144-4.898138 338.267881 3.353767c0 0-20.358715 7.529111-67.099434 34.513443-46.800951 27.34573-63.244529 41.440225-63.244529 41.440225-18.431263 15.66055-27.646894 47.222582-20.539413 70.592941l5.059562 16.865207a178.048407 178.048407 0 0 1-18.672194 129.621169 174.916297 174.916297 0 0 1-102.275439 81.073463l-17.045906 3.854904c-23.310126 5.42096-46.258856 29.333415-50.595623 53.185637 0 0-3.854905 21.382674-3.854905 75.712737 0 54.330062 3.854905 75.712736 3.854905 75.712736 4.216302 23.972688 26.9241 47.945376 50.595623 53.185637l16.624276 3.854905a174.253736 174.253736 0 0 1 102.395904 81.314394c23.310126 40.837896 28.911785 87.337683 18.732427 129.801867l-4.81863 16.443578c-7.10748 23.129428 2.108151 54.992624 20.599646 70.833872 0 0 16.624276 14.094495 63.244529 41.199293 46.800951 27.104798 66.918735 34.513443 66.918735 34.513443 22.707798 8.372371 54.631227 0.361397 71.255503-17.407303l11.624947-12.588673a175.096996 175.096996 0 0 1 242.256662 0.120465l11.624947 12.648906c16.383345 17.708468 48.427239 25.598976 71.255503 17.347071 0 0 20.358715-7.529111 67.159666-34.513443 46.740719-27.104798 63.124063-41.199293 63.124064-41.199293 18.491496-15.600317 27.707127-47.463513 20.599646-70.833873l-5.059562-17.106139a176.723284 176.723284 0 0 1 18.672194-129.139305 176.060722 176.060722 0 0 1 102.395904-81.314394l16.68451-3.854905c23.310126-5.42096 46.258856-29.333415 50.595623-53.185637 0 0 3.854905-21.382674 3.854904-75.712737-0.240932-54.330062-4.095836-75.833202-4.095836-75.833202z m-537.819428 293.334149c-119.261112 0-216.175824-97.336342-216.175824-217.621412a216.657687 216.657687 0 0 1 216.236057-217.320249c119.200879 0 216.115591 97.276109 216.11559 217.56118-0.240932 120.044139-96.974945 217.320248-216.175823 217.320249z" p-id="10234"></path></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1553828490559" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1684" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M898.831744 900.517641 103.816972 900.517641c-36.002982 0-65.363683-29.286-65.363683-65.313541l0-554.949184c0-36.041868 29.361725-65.326844 65.363683-65.326844l795.015795 0c36.002982 0 65.198931 29.284977 65.198931 65.326844l0 554.949184C964.030675 871.231641 934.834726 900.517641 898.831744 900.517641L898.831744 900.517641zM103.816972 255.593236c-13.576203 0-24.711821 11.085476-24.711821 24.662703l0 554.949184c0 13.576203 11.136641 24.662703 24.711821 24.662703l795.015795 0c13.577227 0 24.547069-11.086499 24.547069-24.662703l0-554.949184c0-13.577227-10.970866-24.662703-24.547069-24.662703L103.816972 255.593236 103.816972 255.593236zM664.346245 251.774257c-11.161201 0-20.332071-9.080819-20.332071-20.332071l0-101.278661c0-13.576203-11.047614-24.623817-24.699542-24.623817L383.181611 105.539708c-13.576203 0-24.712845 11.04659-24.712845 24.623817l0 101.278661c0 11.252275-9.041934 20.332071-20.332071 20.332071-11.20111 0-20.319791-9.080819-20.319791-20.332071l0-101.278661c0-35.989679 29.323862-65.275679 65.364707-65.275679l236.133022 0c36.06745 0 65.402569 29.284977 65.402569 65.275679l0 101.278661C684.717202 242.694461 675.636383 251.774257 664.346245 251.774257L664.346245 251.774257zM413.233044 521.725502 75.694471 521.725502c-11.163247 0-20.333094-9.117658-20.333094-20.35663 0-11.252275 9.169847-20.332071 20.333094-20.332071l337.538573 0c11.277858 0 20.319791 9.080819 20.319791 20.332071C433.552835 512.607844 424.510902 521.725502 413.233044 521.725502L413.233044 521.725502zM912.894018 521.725502 575.367725 521.725502c-11.213389 0-20.332071-9.117658-20.332071-20.35663 0-11.252275 9.118682-20.332071 20.332071-20.332071l337.526293 0c11.290137 0 20.332071 9.080819 20.332071 20.332071C933.226089 512.607844 924.184155 521.725502 912.894018 521.725502L912.894018 521.725502zM557.56322 634.217552 445.085496 634.217552c-11.213389 0-20.332071-9.079796-20.332071-20.331048l0-168.763658c0-11.251252 9.118682-20.332071 20.332071-20.332071l112.478747 0c11.290137 0 20.370956 9.080819 20.370956 20.332071l0 168.763658C577.934177 625.137757 568.853357 634.217552 557.56322 634.217552L557.56322 634.217552zM465.417567 593.514525l71.827909 0L537.245476 465.454918l-71.827909 0L465.417567 593.514525 465.417567 593.514525z" p-id="1685" fill="#bfbfbf"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1553828490559" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1684" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M898.831744 900.517641 103.816972 900.517641c-36.002982 0-65.363683-29.286-65.363683-65.313541l0-554.949184c0-36.041868 29.361725-65.326844 65.363683-65.326844l795.015795 0c36.002982 0 65.198931 29.284977 65.198931 65.326844l0 554.949184C964.030675 871.231641 934.834726 900.517641 898.831744 900.517641L898.831744 900.517641zM103.816972 255.593236c-13.576203 0-24.711821 11.085476-24.711821 24.662703l0 554.949184c0 13.576203 11.136641 24.662703 24.711821 24.662703l795.015795 0c13.577227 0 24.547069-11.086499 24.547069-24.662703l0-554.949184c0-13.577227-10.970866-24.662703-24.547069-24.662703L103.816972 255.593236 103.816972 255.593236zM664.346245 251.774257c-11.161201 0-20.332071-9.080819-20.332071-20.332071l0-101.278661c0-13.576203-11.047614-24.623817-24.699542-24.623817L383.181611 105.539708c-13.576203 0-24.712845 11.04659-24.712845 24.623817l0 101.278661c0 11.252275-9.041934 20.332071-20.332071 20.332071-11.20111 0-20.319791-9.080819-20.319791-20.332071l0-101.278661c0-35.989679 29.323862-65.275679 65.364707-65.275679l236.133022 0c36.06745 0 65.402569 29.284977 65.402569 65.275679l0 101.278661C684.717202 242.694461 675.636383 251.774257 664.346245 251.774257L664.346245 251.774257zM413.233044 521.725502 75.694471 521.725502c-11.163247 0-20.333094-9.117658-20.333094-20.35663 0-11.252275 9.169847-20.332071 20.333094-20.332071l337.538573 0c11.277858 0 20.319791 9.080819 20.319791 20.332071C433.552835 512.607844 424.510902 521.725502 413.233044 521.725502L413.233044 521.725502zM912.894018 521.725502 575.367725 521.725502c-11.213389 0-20.332071-9.117658-20.332071-20.35663 0-11.252275 9.118682-20.332071 20.332071-20.332071l337.526293 0c11.290137 0 20.332071 9.080819 20.332071 20.332071C933.226089 512.607844 924.184155 521.725502 912.894018 521.725502L912.894018 521.725502zM557.56322 634.217552 445.085496 634.217552c-11.213389 0-20.332071-9.079796-20.332071-20.331048l0-168.763658c0-11.251252 9.118682-20.332071 20.332071-20.332071l112.478747 0c11.290137 0 20.370956 9.080819 20.370956 20.332071l0 168.763658C577.934177 625.137757 568.853357 634.217552 557.56322 634.217552L557.56322 634.217552zM465.417567 593.514525l71.827909 0L537.245476 465.454918l-71.827909 0L465.417567 593.514525 465.417567 593.514525z" p-id="1685"></path></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
src/assets/images/pay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -1,4 +1,4 @@
@import './variables.module.scss';
@use './variables.module.scss' as *;
@mixin colorBtn($color) {
background: $color;

View File

@@ -1,10 +1,9 @@
@import './variables.module.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './btn.scss';
@import './ruoyi.scss';
@use './mixin.scss';
@use './transition.scss';
@use './element-ui.scss';
@use './sidebar.scss';
@use './btn.scss';
@use './ruoyi.scss';
body {
height: 100%;
@@ -131,10 +130,6 @@ aside {
position: relative;
}
.pagination-container {
margin-top: 30px;
}
.text-center {
text-align: center
}
@@ -182,12 +177,3 @@ aside {
margin-bottom: 10px;
}
}
//refine vue-multiselect plugin
.multiselect {
line-height: 16px;
}
.multiselect--active {
z-index: 1000 !important;
}

View File

@@ -1,193 +1,212 @@
/**
/**
* 通用css样式布局处理
* Copyright (c) 2019 ruoyi
*/
/** 基础通用 **/
/** 基础通用 **/
.pt5 {
padding-top: 5px;
padding-top: 5px;
}
.pr5 {
padding-right: 5px;
padding-right: 5px;
}
.pb5 {
padding-bottom: 5px;
padding-bottom: 5px;
}
.mt5 {
margin-top: 5px;
margin-top: 5px;
}
.mr5 {
margin-right: 5px;
margin-right: 5px;
}
.mb5 {
margin-bottom: 5px;
margin-bottom: 5px;
}
.mb8 {
margin-bottom: 8px;
margin-bottom: 8px;
}
.ml5 {
margin-left: 5px;
margin-left: 5px;
}
.mt10 {
margin-top: 10px;
margin-top: 10px;
}
.mr10 {
margin-right: 10px;
margin-right: 10px;
}
.mb10 {
margin-bottom: 10px;
margin-bottom: 10px;
}
.ml10 {
margin-left: 10px;
margin-left: 10px;
}
.mt20 {
margin-top: 20px;
margin-top: 20px;
}
.mr20 {
margin-right: 20px;
margin-right: 20px;
}
.mb20 {
margin-bottom: 20px;
margin-bottom: 20px;
}
.ml20 {
margin-left: 20px;
margin-left: 20px;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
font-family: inherit;
font-weight: 500;
line-height: 1.1;
color: inherit;
font-family: inherit;
font-weight: 500;
line-height: 1.1;
color: inherit;
}
.el-form--inline {
.el-form-item {
.el-input, .el-cascader, .el-select, .el-autocomplete {
width: 200px;
}
}
}
.el-form .el-form-item__label {
font-weight: 700;
}
.el-dialog:not(.is-fullscreen) {
margin-top: 6vh !important;
margin-top: 6vh !important;
}
.el-dialog.scrollbar .el-dialog__body {
overflow: auto;
overflow-x: hidden;
max-height: 70vh;
padding: 10px 20px 0;
overflow: auto;
overflow-x: hidden;
max-height: 70vh;
padding: 10px 20px 0;
}
.el-table {
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
th {
word-break: break-word;
background-color: #f8f8f9 !important;
color: #515a6e;
height: 40px !important;
font-size: 13px;
}
}
.el-table__body-wrapper {
.el-button [class*="el-icon-"] + span {
margin-left: 1px;
}
}
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
th {
word-break: break-word;
background-color: #f8f8f9 !important;
color: #515a6e;
height: 40px !important;
font-size: 13px;
}
}
.el-table__body-wrapper {
.el-button [class*="el-icon-"] + span {
margin-left: 1px;
}
}
}
/** 表单布局 **/
.form-header {
font-size:15px;
color:#6379bb;
border-bottom:1px solid #ddd;
margin:8px 10px 25px 10px;
padding-bottom:5px
font-size:15px;
color:#6379bb;
border-bottom:1px solid #ddd;
margin:8px 10px 25px 10px;
padding-bottom:5px
}
/** 表格布局 **/
.pagination-container {
// position: relative;
height: 25px;
margin-bottom: 10px;
margin-top: 15px;
padding: 10px 20px !important;
display: flex;
justify-content: flex-end;
margin-top: 20px;
background-color: transparent !important;
}
/* 弹窗中的分页器 */
.el-dialog .pagination-container {
position: static !important;
margin: 10px 0 0 0;
padding: 0 !important;
.el-pagination {
position: static;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.pagination-container {
.el-pagination {
> .el-pagination__jump {
display: none !important;
}
> .el-pagination__sizes {
display: none !important;
}
}
}
}
/* tree border */
.tree-border {
margin-top: 5px;
border: 1px solid #e5e6e7;
background: #FFFFFF none;
border-radius:4px;
width: 100%;
}
.pagination-container .el-pagination {
right: 0;
position: absolute;
}
@media ( max-width : 768px) {
.pagination-container .el-pagination > .el-pagination__jump {
display: none !important;
}
.pagination-container .el-pagination > .el-pagination__sizes {
display: none !important;
}
margin-top: 5px;
border: 1px solid var(--el-border-color-light, #e5e6e7);
background: var(--el-bg-color, #FFFFFF) none;
border-radius:4px;
width: 100%;
}
.el-table .fixed-width .el-button--small {
padding-left: 0;
padding-right: 0;
width: inherit;
padding-left: 0;
padding-right: 0;
width: inherit;
}
/** 表格更多操作下拉样式 */
.el-table .el-dropdown-link {
cursor: pointer;
color: #409EFF;
margin-left: 10px;
cursor: pointer;
color: #409EFF;
margin-left: 10px;
}
.el-table .el-dropdown, .el-icon-arrow-down {
font-size: 12px;
font-size: 12px;
}
.el-tree-node__content > .el-checkbox {
margin-right: 8px;
margin-right: 8px;
}
.list-group-striped > .list-group-item {
border-left: 0;
border-right: 0;
border-radius: 0;
padding-left: 0;
padding-right: 0;
border-left: 0;
border-right: 0;
border-radius: 0;
padding-left: 0;
padding-right: 0;
}
.list-group {
padding-left: 0px;
list-style: none;
padding-left: 0px;
list-style: none;
}
.list-group-item {
border-bottom: 1px solid #e7eaec;
border-top: 1px solid #e7eaec;
margin-bottom: -1px;
padding: 11px 0px;
font-size: 13px;
border-bottom: 1px solid #e7eaec;
border-top: 1px solid #e7eaec;
margin-bottom: -1px;
padding: 11px 0px;
font-size: 13px;
}
.pull-right {
float: right !important;
float: right !important;
}
.el-card__header {
padding: 14px 15px 7px !important;
min-height: 40px;
padding: 14px 15px 7px !important;
min-height: 40px;
}
.el-card__body {
padding: 15px 20px 20px 20px !important;
padding: 15px 20px 20px 20px !important;
}
.card-box {
padding-right: 15px;
padding-left: 15px;
margin-bottom: 10px;
margin-bottom: 10px;
}
/* button color */
@@ -213,62 +232,67 @@
/* text color */
.text-navy {
color: #1ab394;
color: #1ab394;
}
.text-primary {
color: inherit;
color: inherit;
}
.text-success {
color: #1c84c6;
color: #1c84c6;
}
.text-info {
color: #23c6c8;
color: #23c6c8;
}
.text-warning {
color: #f8ac59;
color: #f8ac59;
}
.text-danger {
color: #ed5565;
color: #ed5565;
}
.text-muted {
color: #888888;
color: #888888;
}
/* image */
.img-circle {
border-radius: 50%;
border-radius: 50%;
}
.img-lg {
width: 120px;
height: 120px;
width: 120px;
height: 120px;
}
.avatar-upload-preview {
position: absolute;
top: 50%;
transform: translate(50%, -50%);
width: 200px;
height: 200px;
border-radius: 50%;
box-shadow: 0 0 4px #ccc;
overflow: hidden;
position: absolute;
top: 50%;
transform: translate(50%, -50%);
width: 200px;
height: 200px;
border-radius: 50%;
box-shadow: 0 0 4px #ccc;
overflow: hidden;
}
/* 拖拽列样式 */
.sortable-ghost{
opacity: .8;
color: #fff!important;
background: #42b983!important;
opacity: .8;
color: #fff!important;
background: #42b983!important;
}
/* 表格右侧工具栏样式 */
.top-right-btn {
margin-left: auto;
margin-left: auto;
}
/* 分割面板样式 */
.splitpanes.default-theme .splitpanes__pane {
background-color: var(--splitpanes-default-bg) !important;
}

View File

@@ -1,9 +1,11 @@
@use './variables.module.scss' as vars;
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: $base-sidebar-width;
margin-left: vars.$base-sidebar-width;
position: relative;
}
@@ -12,10 +14,8 @@
}
.sidebar-container {
-webkit-transition: width .28s;
transition: width 0.28s;
width: $base-sidebar-width !important;
background-color: $base-menu-background;
width: vars.$base-sidebar-width !important;
height: 100%;
position: fixed;
font-size: 0px;
@@ -25,7 +25,7 @@
z-index: 1001;
overflow: hidden;
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
box-shadow: 2px 0 6px rgba(0,21,41,.35);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
// reset element-ui css
.horizontal-collapse-transition {
@@ -89,12 +89,12 @@
}
& .theme-dark .is-active > .el-sub-menu__title {
color: $base-menu-color-active !important;
color: vars.$base-menu-color-active !important;
}
& .nest-menu .el-sub-menu>.el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: $base-sidebar-width !important;
min-width: vars.$base-sidebar-width !important;
&:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
@@ -103,10 +103,10 @@
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background !important;
background-color: vars.$base-sub-menu-background;
&:hover {
background-color: $base-sub-menu-hover !important;
background-color: vars.$base-sub-menu-hover !important;
}
}
}
@@ -169,7 +169,7 @@
}
.el-menu--collapse .el-menu .el-sub-menu {
min-width: $base-sidebar-width !important;
min-width: vars.$base-sidebar-width !important;
}
// mobile responsive
@@ -180,14 +180,14 @@
.sidebar-container {
transition: transform .28s;
width: $base-sidebar-width !important;
width: vars.$base-sidebar-width !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$base-sidebar-width, 0, 0);
transform: translate3d(-(vars.$base-sidebar-width), 0, 0);
}
}
}

View File

@@ -6,18 +6,19 @@
transition: opacity 0.28s;
}
.fade-enter,
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform--move,
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
@@ -33,7 +34,7 @@
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);

View File

@@ -1,6 +1,6 @@
// base color
$blue: #324157;
$light-blue: #3A71A8;
$light-blue: #333c46;
$red: #C03639;
$pink: #E65D6E;
$green: #30B08F;
@@ -8,58 +8,214 @@ $tiffany: #4AB7BD;
$yellow: #FEC171;
$panGreen: #30B08F;
// 默认菜单主题风格
// 默认主题变量
$menuText: #bfcbd9;
$menuActiveText: #409eff;
$menuBg: #304156;
$menuHover: #263445;
// 浅色主题theme-light
$menuLightBg: #ffffff;
$menuLightHover: #f0f1f5;
$menuLightText: #303133;
$menuLightActiveText: #409EFF;
// 基础变量
$base-sidebar-width: 200px;
$sideBarWidth: 200px;
// 菜单暗色变量
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-logo-title-color: #ffffff;
$base-menu-light-color: rgba(0, 0, 0, 0.7);
$base-menu-light-background: #ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
// 自定义暗色菜单风格
/**
$base-menu-color:hsla(0,0%,100%,.65);
$base-menu-color-active:#fff;
$base-menu-background:#001529;
$base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70);
$base-menu-light-background:#ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
// 组件变量
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
$base-sidebar-width: 200px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuColor: $base-menu-color;
menuLightColor: $base-menu-light-color;
menuColorActive: $base-menu-color-active;
menuBackground: $base-menu-background;
menuLightBackground: $base-menu-light-background;
subMenuBackground: $base-sub-menu-background;
subMenuHover: $base-sub-menu-hover;
sideBarWidth: $base-sidebar-width;
logoTitleColor: $base-logo-title-color;
logoLightTitleColor: $base-logo-light-title-color;
primaryColor: $--color-primary;
successColor: $--color-success;
dangerColor: $--color-danger;
infoColor: $--color-info;
warningColor: $--color-warning;
menuText: $menuText;
menuActiveText: $menuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
menuLightBg: $menuLightBg;
menuLightHover: $menuLightHover;
menuLightText: $menuLightText;
menuLightActiveText: $menuLightActiveText;
sideBarWidth: $sideBarWidth;
// 导出基础颜色
blue: $blue;
lightBlue: $light-blue;
red: $red;
pink: $pink;
green: $green;
tiffany: $tiffany;
yellow: $yellow;
panGreen: $panGreen;
// 导出组件颜色
colorPrimary: $--color-primary;
colorSuccess: $--color-success;
colorWarning: $--color-warning;
colorDanger: $--color-danger;
colorInfo: $--color-info;
}
// CSS变量定义
:root {
/* 亮色模式变量 */
--sidebar-bg: #{$menuBg};
--sidebar-text: #{$menuText};
--menu-hover: #{$menuHover};
--navbar-bg: #ffffff;
--navbar-text: #303133;
/* splitpanes default-theme 变量 */
--splitpanes-default-bg: #ffffff;
}
// 暗黑模式变量
html.dark {
/* 默认通用 */
--el-bg-color: #141414;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #ffffff;
--el-text-color-regular: #d0d0d0;
--el-border-color: #434343;
--el-border-color-light: #434343;
/* 侧边栏 */
--sidebar-bg: #141414;
--sidebar-text: #ffffff;
--menu-hover: #2d2d2d;
--menu-active-text: #{$menuActiveText};
/* 顶部导航栏 */
--navbar-bg: #141414;
--navbar-text: #ffffff;
--navbar-hover: #141414;
/* 标签栏 */
--tags-bg: #141414;
--tags-item-bg: #1d1e1f;
--tags-item-border: #303030;
--tags-item-text: #d0d0d0;
--tags-item-hover: #2d2d2d;
--tags-close-hover: #64666a;
/* splitpanes 组件暗黑模式变量 */
--splitpanes-bg: #141414;
--splitpanes-border: #303030;
--splitpanes-splitter-bg: #1d1e1f;
--splitpanes-splitter-hover-bg: #2d2d2d;
/* blockquote 暗黑模式变量 */
--blockquote-bg: #1d1e1f;
--blockquote-border: #303030;
--blockquote-text: #d0d0d0;
/* Cron 时间表达式 模式变量 */
--cron-border: #303030;
/* splitpanes default-theme 暗黑模式变量 */
--splitpanes-default-bg: #141414;
/* 侧边栏菜单覆盖 */
.sidebar-container {
.el-menu-item, .menu-title {
color: var(--el-text-color-regular);
}
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: var(--el-bg-color) !important;
}
}
/* 顶部栏栏菜单覆盖 */
.el-menu--horizontal {
.el-menu-item {
&:not(.is-disabled) {
&:hover,
&:focus {
background-color: var(--navbar-hover) !important;
}
}
}
}
/* 分割窗格覆盖 */
.splitpanes {
background-color: var(--splitpanes-bg);
.splitpanes__pane {
background-color: var(--splitpanes-bg);
border-color: var(--splitpanes-border);
}
.splitpanes__splitter {
background-color: var(--splitpanes-splitter-bg);
border-color: var(--splitpanes-border);
&:hover {
background-color: var(--splitpanes-splitter-hover-bg);
}
&:before,
&:after {
background-color: var(--splitpanes-border);
}
}
}
/* 表格样式覆盖 */
.el-table {
--el-table-header-bg-color: var(--el-bg-color-overlay) !important;
--el-table-header-text-color: var(--el-text-color-regular) !important;
--el-table-border-color: var(--el-border-color-light) !important;
--el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important;
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
th {
background-color: var(--el-bg-color-overlay, #f8f8f9) !important;
color: var(--el-text-color-regular, #515a6e);
}
}
}
/* 树组件高亮样式覆盖 */
.el-tree {
.el-tree-node.is-current > .el-tree-node__content {
background-color: var(--el-bg-color-overlay) !important;
color: var(--el-color-primary);
}
.el-tree-node__content:hover {
background-color: var(--el-bg-color-overlay);
}
}
/* 下拉菜单样式覆盖 */
.el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{
background-color: var(--navbar-hover) !important;
}
/* blockquote样式覆盖 */
blockquote {
background-color: var(--blockquote-bg) !important;
border-left-color: var(--blockquote-border) !important;
color: var(--blockquote-text) !important;
}
/* 时间表达式标题样式覆盖 */
.popup-result .title {
background: var(--cron-border);
}
}

View File

@@ -1,7 +1,7 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
@@ -10,21 +10,53 @@
</template>
<script setup>
const route = useRoute();
const router = useRouter();
import usePermissionStore from '@/store/modules/permission'
const route = useRoute()
const router = useRouter()
const permissionStore = usePermissionStore()
const levelList = ref([])
function getBreadcrumb() {
// only show routes with meta.title
let matched = route.matched.filter(item => item.meta && item.meta.title);
const first = matched[0]
// 判断是否为首页
if (!isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
let matched = []
const pathNum = findPathNum(route.path)
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi
const pathList = route.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1)
return item
})
getMatched(pathList, permissionStore.defaultRoutes, matched)
} else {
matched = route.matched.filter((item) => item.meta && item.meta.title)
}
// 判断是否为首页
if (!isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function findPathNum(str, char = "/") {
let index = str.indexOf(char)
let num = 0
while (index !== -1) {
num++
index = str.indexOf(char, index + 1)
}
return num
}
function getMatched(pathList, routeList, matched) {
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
if (data) {
matched.push(data)
if (data.children && pathList.length) {
pathList.shift()
getMatched(pathList, data.children, matched)
}
}
}
function isDashboard(route) {
const name = route && route.name
if (!name) {
@@ -48,7 +80,7 @@ watchEffect(() => {
}
getBreadcrumb()
})
getBreadcrumb();
getBreadcrumb()
</script>
<style lang='scss' scoped>

View File

@@ -0,0 +1,174 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * ? / L W]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
不指定
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
周期从
<el-input-number v-model='cycle01' :min="1" :max="30" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
<el-input-number v-model='average01' :min="1" :max="30" /> 号开始
<el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="5">
每月
<el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="6">
本月最后一天
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="7">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 31" :key="item" :label="item" :value="item" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(1)
const cycle02 = ref(2)
const average01 = ref(1)
const average02 = ref(1)
const workday = ref(1)
const checkboxList = ref([])
const checkCopy = ref([1])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 30)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 30)
average02.value = props.check(average02.value, 1, 31 - average01.value)
return average01.value + '/' + average02.value
})
const workdayTotal = computed(() => {
workday.value = props.check(workday.value, 1, 31)
return workday.value + 'W'
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.day, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === "*") {
radioValue.value = 1
} else if (value === "?") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else if (value.indexOf("W") > -1) {
const indexArr = value.split("W")
workday.value = Number(indexArr[0])
radioValue.value = 5
} else if (value === "L") {
radioValue.value = 6
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 7
}
}
// 单选按钮值变化时
function onRadioChange() {
if (radioValue.value === 2 && props.cron.week === '?') {
emit('update', 'week', '*', 'day')
}
if (radioValue.value !== 2 && props.cron.week !== '?') {
emit('update', 'week', '?', 'day')
}
switch (radioValue.value) {
case 1:
emit('update', 'day', '*', 'day')
break
case 2:
emit('update', 'day', '?', 'day')
break
case 3:
emit('update', 'day', cycleTotal.value, 'day')
break
case 4:
emit('update', 'day', averageTotal.value, 'day')
break
case 5:
emit('update', 'day', workdayTotal.value, 'day')
break
case 6:
emit('update', 'day', 'L', 'day')
break
case 7:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'day', checkboxString.value, 'day')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
小时允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="0" :max="22" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="0" :max="22" /> 时开始
<el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 22)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 22)
average02.value = props.check(average02.value, 1, 23 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.hour, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (props.cron.min === '*') {
emit('update', 'min', '0', 'hour')
}
if (props.cron.second === '*') {
emit('update', 'second', '0', 'hour')
}
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'hour', '*', 'hour')
break
case 2:
emit('update', 'hour', cycleTotal.value, 'hour')
break
case 3:
emit('update', 'hour', averageTotal.value, 'hour')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'hour', checkboxString.value, 'hour')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane label="秒" v-if="shouldHide('second')">
<CrontabSecond
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronsecond"
/>
</el-tab-pane>
<el-tab-pane label="分钟" v-if="shouldHide('min')">
<CrontabMin
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmin"
/>
</el-tab-pane>
<el-tab-pane label="小时" v-if="shouldHide('hour')">
<CrontabHour
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronhour"
/>
</el-tab-pane>
<el-tab-pane label="日" v-if="shouldHide('day')">
<CrontabDay
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronday"
/>
</el-tab-pane>
<el-tab-pane label="月" v-if="shouldHide('month')">
<CrontabMonth
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmonth"
/>
</el-tab-pane>
<el-tab-pane label="周" v-if="shouldHide('week')">
<CrontabWeek
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronweek"
/>
</el-tab-pane>
<el-tab-pane label="年" v-if="shouldHide('year')">
<CrontabYear
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronyear"
/>
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">时间表达式</p>
<table>
<thead>
<tr>
<th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
</td>
<td class="result">
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
</tr>
</tbody>
</table>
</div>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn">
<el-button type="primary" @click="submitFill">确定</el-button>
<el-button type="warning" @click="clearCron">重置</el-button>
<el-button @click="hidePopup">取消</el-button>
</div>
</div>
</div>
</template>
<script setup>
import CrontabSecond from "./second.vue"
import CrontabMin from "./min.vue"
import CrontabHour from "./hour.vue"
import CrontabDay from "./day.vue"
import CrontabMonth from "./month.vue"
import CrontabWeek from "./week.vue"
import CrontabYear from "./year.vue"
import CrontabResult from "./result.vue"
const { proxy } = getCurrentInstance()
const emit = defineEmits(['hide', 'fill'])
const props = defineProps({
hideComponent: {
type: Array,
default: () => [],
},
expression: {
type: String,
default: ""
}
})
const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
const tabActive = ref(0)
const hideComponent = ref([])
const expression = ref('')
const crontabValueObj = ref({
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
})
const crontabValueString = computed(() => {
const obj = crontabValueObj.value
return obj.second
+ " "
+ obj.min
+ " "
+ obj.hour
+ " "
+ obj.day
+ " "
+ obj.month
+ " "
+ obj.week
+ (obj.year === "" ? "" : " " + obj.year)
})
watch(expression, () => resolveExp())
function shouldHide(key) {
return !(hideComponent.value && hideComponent.value.includes(key))
}
function resolveExp() {
// 反解析 表达式
if (expression.value) {
const arr = expression.value.split(/\s+/)
if (arr.length >= 6) {
//6 位以上是合法表达式
let obj = {
second: arr[0],
min: arr[1],
hour: arr[2],
day: arr[3],
month: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : ""
}
crontabValueObj.value = {
...obj,
}
}
} else {
// 没有传入的表达式 则还原
clearCron()
}
}
// tab切换值
function tabCheck(index) {
tabActive.value = index
}
// 由子组件触发,更改表达式组成的字段值
function updateCrontabValue(name, value, from) {
crontabValueObj.value[name] = value
}
// 表单选项的子组件校验数字格式(通过-props传递
function checkNumber(value, minLimit, maxLimit) {
// 检查必须为整数
value = Math.floor(value)
if (value < minLimit) {
value = minLimit
} else if (value > maxLimit) {
value = maxLimit
}
return value
}
// 隐藏弹窗
function hidePopup() {
emit("hide")
}
// 填充表达式
function submitFill() {
emit("fill", crontabValueString.value)
hidePopup()
}
function clearCron() {
// 还原选择项
crontabValueObj.value = {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
}
onMounted(() => {
expression.value = props.expression
hideComponent.value = props.hideComponent
})
</script>
<style lang="scss" scoped>
.pop_btn {
text-align: center;
margin-top: 20px;
}
.popup-main {
position: relative;
margin: 10px auto;
border-radius: 5px;
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
box-sizing: border-box;
line-height: 24px;
margin: 25px auto;
padding: 15px 10px 10px;
border: 1px solid #ccc;
position: relative;
}
.popup-result .title {
position: absolute;
top: -28px;
left: 50%;
width: 140px;
font-size: 14px;
margin-left: -70px;
text-align: center;
line-height: 30px;
background: #fff;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table td:not(.result) {
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 30px;
height: 30px;
white-space: nowrap;
overflow: hidden;
border: 1px solid #e8e8e8;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
分钟允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 58)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 58)
average02.value = props.check(average02.value, 1, 59 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.min, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'min', '*', 'min')
break
case 2:
emit('update', 'min', cycleTotal.value, 'min')
break
case 3:
emit('update', 'min', averageTotal.value, 'min')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'min', checkboxString.value, 'min')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 19.8rem;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="1" :max="11" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="1" :max="11" /> 月开始
<el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(1)
const cycle02 = ref(2)
const average01 = ref(1)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([1])
const monthList = ref([
{key: 1, value: '一月'},
{key: 2, value: '二月'},
{key: 3, value: '三月'},
{key: 4, value: '四月'},
{key: 5, value: '五月'},
{key: 6, value: '六月'},
{key: 7, value: '七月'},
{key: 8, value: '八月'},
{key: 9, value: '九月'},
{key: 10, value: '十月'},
{key: 11, value: '十一月'},
{key: 12, value: '十二月'}
])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 11)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 11)
average02.value = props.check(average02.value, 1, 12 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.month, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'month', '*', 'month')
break
case 2:
emit('update', 'month', cycleTotal.value, 'month')
break
case 3:
emit('update', 'month', averageTotal.value, 'month')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'month', checkboxString.value, 'month')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,540 @@
<template>
<div class="popup-result">
<p class="title">最近5次运行时间</p>
<ul class="popup-result-scroll">
<template v-if='isShow'>
<li v-for='item in resultList' :key="item">{{item}}</li>
</template>
<li v-else>计算结果中...</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps({
ex: {
type: String,
default: ''
}
})
const dayRule = ref('')
const dayRuleSup = ref('')
const dateArr = ref([])
const resultList = ref([])
const isShow = ref(false)
watch(() => props.ex, () => expressionChange())
// 表达式值变化时,开始去计算结果
function expressionChange() {
// 计算开始-隐藏结果
isShow.value = false
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
let ruleArr = props.ex.split(' ')
// 用于记录进入循环的次数
let nums = 0
// 用于暂时存符号时间规则结果的数组
let resultArr = []
// 获取当前时间精确至[年、月、日、时、分、秒]
let nTime = new Date()
let nYear = nTime.getFullYear()
let nMonth = nTime.getMonth() + 1
let nDay = nTime.getDate()
let nHour = nTime.getHours()
let nMin = nTime.getMinutes()
let nSecond = nTime.getSeconds()
// 根据规则获取到近100年可能年数组、月数组等等
getSecondArr(ruleArr[0])
getMinArr(ruleArr[1])
getHourArr(ruleArr[2])
getDayArr(ruleArr[3])
getMonthArr(ruleArr[4])
getWeekArr(ruleArr[5])
getYearArr(ruleArr[6], nYear)
// 将获取到的数组赋值-方便使用
let sDate = dateArr.value[0]
let mDate = dateArr.value[1]
let hDate = dateArr.value[2]
let DDate = dateArr.value[3]
let MDate = dateArr.value[4]
let YDate = dateArr.value[5]
// 获取当前时间在数组中的索引
let sIdx = getIndex(sDate, nSecond)
let mIdx = getIndex(mDate, nMin)
let hIdx = getIndex(hDate, nHour)
let DIdx = getIndex(DDate, nDay)
let MIdx = getIndex(MDate, nMonth)
let YIdx = getIndex(YDate, nYear)
// 重置月日时分秒的函数(后面用的比较多)
const resetSecond = function () {
sIdx = 0
nSecond = sDate[sIdx]
}
const resetMin = function () {
mIdx = 0
nMin = mDate[mIdx]
resetSecond()
}
const resetHour = function () {
hIdx = 0
nHour = hDate[hIdx]
resetMin()
}
const resetDay = function () {
DIdx = 0
nDay = DDate[DIdx]
resetHour()
}
const resetMonth = function () {
MIdx = 0
nMonth = MDate[MIdx]
resetDay()
}
// 如果当前年份不为数组中当前值
if (nYear !== YDate[YIdx]) {
resetMonth()
}
// 如果当前月份不为数组中当前值
if (nMonth !== MDate[MIdx]) {
resetDay()
}
// 如果当前“日”不为数组中当前值
if (nDay !== DDate[DIdx]) {
resetHour()
}
// 如果当前“时”不为数组中当前值
if (nHour !== hDate[hIdx]) {
resetMin()
}
// 如果当前“分”不为数组中当前值
if (nMin !== mDate[mIdx]) {
resetSecond()
}
// 循环年份数组
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
let YY = YDate[Yi]
// 如果到达最大值时
if (nMonth > MDate[MDate.length - 1]) {
resetMonth()
continue
}
// 循环月份数组
goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
// 赋值、方便后面运算
let MM = MDate[Mi];
MM = MM < 10 ? '0' + MM : MM
// 如果到达最大值时
if (nDay > DDate[DDate.length - 1]) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue
}
// 循环日期数组
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
// 赋值、方便后面运算
let DD = DDate[Di]
let thisDD = DD < 10 ? '0' + DD : DD
// 如果到达最大值时
if (nHour > hDate[hDate.length - 1]) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue
}
// 判断日期的合法性,不合法的话也是跳出当前循环
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
resetDay()
continue goMonth
}
// 如果日期规则中有值时
if (dayRule.value === 'lastDay') {
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
} else if (dayRule.value === 'workDay') {
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取达到条件的日期是星期X
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 当星期日时
if (thisWeek === 1) {
// 先找下一个日,并判断是否为月底
DD++
thisDD = DD < 10 ? '0' + DD : DD
// 判断下一日已经不是合法日期
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3
}
} else if (thisWeek === 7) {
// 当星期6时只需判断不是1号就可进行操作
if (dayRuleSup.value !== 1) {
DD--
} else {
DD += 2
}
}
} else if (dayRule.value === 'weekDay') {
// 如果指定了是星期几
// 获取当前日期是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
// 校验当前星期是否在星期池dayRuleSup
if (dayRuleSup.value.indexOf(thisWeek) < 0) {
// 如果到达最大值时
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue
}
} else if (dayRule.value === 'assWeek') {
// 如果指定了是第几周的星期几
// 获取每月1号是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (dayRuleSup.value[1] >= thisWeek) {
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
} else {
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
}
} else if (dayRule.value === 'lastWeek') {
// 如果指定了每月最后一个星期几
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取月末最后一天是星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 找到要求中最近的那个星期几
if (dayRuleSup.value < thisWeek) {
DD -= thisWeek - dayRuleSup.value
} else if (dayRuleSup.value > thisWeek) {
DD -= 7 - (dayRuleSup.value - thisWeek)
}
}
// 判断时间值是否小于10置换成“05”这种格式
DD = DD < 10 ? '0' + DD : DD
// 循环“时”数组
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
// 如果到达最大值时
if (nMin > mDate[mDate.length - 1]) {
resetMin()
if (hi === hDate.length - 1) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue goDay
}
continue
}
// 循环"分"数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
// 如果到达最大值时
if (nSecond > sDate[sDate.length - 1]) {
resetSecond()
if (mi === mDate.length - 1) {
resetMin()
if (hi === hDate.length - 1) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue goDay
}
continue goHour
}
continue
}
// 循环"秒"数组
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
// 添加当前时间(时间合法性在日期循环时已经判断)
if (MM !== '00' && DD !== '00') {
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
nums++
}
// 如果条数满了就退出循环
if (nums === 5) break goYear
// 如果到达最大值时
if (si === sDate.length - 1) {
resetSecond()
if (mi === mDate.length - 1) {
resetMin()
if (hi === hDate.length - 1) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue goDay
}
continue goHour
}
continue goMin
}
} //goSecond
} //goMin
}//goHour
}//goDay
}//goMonth
}
// 判断100年内的结果条数
if (resultArr.length === 0) {
resultList.value = ['没有达到条件的结果!']
} else {
resultList.value = resultArr
if (resultArr.length !== 5) {
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
}
}
// 计算完成-显示结果
isShow.value = true
}
// 用于计算某位数字在数组中的索引
function getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0
} else {
for (let i = 0; i < arr.length - 1; i++) {
if (value > arr[i] && value <= arr[i + 1]) {
return i + 1
}
}
}
}
// 获取"年"数组
function getYearArr(rule, year) {
dateArr.value[5] = getOrderArr(year, year + 100)
if (rule !== undefined) {
if (rule.indexOf('-') >= 0) {
dateArr.value[5] = getCycleArr(rule, year + 100, false)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[5] = getAverageArr(rule, year + 100)
} else if (rule !== '*') {
dateArr.value[5] = getAssignArr(rule)
}
}
}
// 获取"月"数组
function getMonthArr(rule) {
dateArr.value[4] = getOrderArr(1, 12)
if (rule.indexOf('-') >= 0) {
dateArr.value[4] = getCycleArr(rule, 12, false)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[4] = getAverageArr(rule, 12)
} else if (rule !== '*') {
dateArr.value[4] = getAssignArr(rule)
}
}
// 获取"日"数组-主要为日期规则
function getWeekArr(rule) {
// 只有当日期规则的两个值均为“”时则表达日期是有选项的
if (dayRule.value === '' && dayRuleSup.value === '') {
if (rule.indexOf('-') >= 0) {
dayRule.value = 'weekDay'
dayRuleSup.value = getCycleArr(rule, 7, false)
} else if (rule.indexOf('#') >= 0) {
dayRule.value = 'assWeek'
let matchRule = rule.match(/[0-9]{1}/g)
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
dateArr.value[3] = [1]
if (dayRuleSup.value[1] === 7) {
dayRuleSup.value[1] = 0
}
} else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastWeek'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [31]
if (dayRuleSup.value === 7) {
dayRuleSup.value = 0
}
} else if (rule !== '*' && rule !== '?') {
dayRule.value = 'weekDay'
dayRuleSup.value = getAssignArr(rule)
}
}
}
// 获取"日"数组-少量为日期规则
function getDayArr(rule) {
dateArr.value[3] = getOrderArr(1, 31)
dayRule.value = ''
dayRuleSup.value = ''
if (rule.indexOf('-') >= 0) {
dateArr.value[3] = getCycleArr(rule, 31, false)
dayRuleSup.value = 'null'
} else if (rule.indexOf('/') >= 0) {
dateArr.value[3] = getAverageArr(rule, 31)
dayRuleSup.value = 'null'
} else if (rule.indexOf('W') >= 0) {
dayRule.value = 'workDay'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [dayRuleSup.value]
} else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastDay'
dayRuleSup.value = 'null'
dateArr.value[3] = [31]
} else if (rule !== '*' && rule !== '?') {
dateArr.value[3] = getAssignArr(rule)
dayRuleSup.value = 'null'
} else if (rule === '*') {
dayRuleSup.value = 'null'
}
}
// 获取"时"数组
function getHourArr(rule) {
dateArr.value[2] = getOrderArr(0, 23)
if (rule.indexOf('-') >= 0) {
dateArr.value[2] = getCycleArr(rule, 24, true)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[2] = getAverageArr(rule, 23)
} else if (rule !== '*') {
dateArr.value[2] = getAssignArr(rule)
}
}
// 获取"分"数组
function getMinArr(rule) {
dateArr.value[1] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
dateArr.value[1] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[1] = getAverageArr(rule, 59)
} else if (rule !== '*') {
dateArr.value[1] = getAssignArr(rule)
}
}
// 获取"秒"数组
function getSecondArr(rule) {
dateArr.value[0] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
dateArr.value[0] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[0] = getAverageArr(rule, 59)
} else if (rule !== '*') {
dateArr.value[0] = getAssignArr(rule)
}
}
// 根据传进来的min-max返回一个顺序的数组
function getOrderArr(min, max) {
let arr = []
for (let i = min; i <= max; i++) {
arr.push(i)
}
return arr
}
// 根据规则中指定的零散值返回一个数组
function getAssignArr(rule) {
let arr = []
let assiginArr = rule.split(',')
for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i])
}
arr.sort(compare)
return arr
}
// 根据一定算术规则计算返回一个数组
function getAverageArr(rule, limit) {
let arr = []
let agArr = rule.split('/')
let min = Number(agArr[0])
let step = Number(agArr[1])
while (min <= limit) {
arr.push(min)
min += step
}
return arr
}
// 根据规则返回一个具有周期性的数组
function getCycleArr(rule, limit, status) {
// status--表示是否从0开始则从1开始
let arr = []
let cycleArr = rule.split('-')
let min = Number(cycleArr[0])
let max = Number(cycleArr[1])
if (min > max) {
max += limit
}
for (let i = min; i <= max; i++) {
let add = 0
if (status === false && i % limit === 0) {
add = limit
}
arr.push(Math.round(i % limit + add))
}
arr.sort(compare)
return arr
}
// 比较数字大小用于Array.sort
function compare(value1, value2) {
if (value2 - value1 > 0) {
return -1
} else {
return 1
}
}
// 格式化日期格式如2017-9-19 18:04:33
function formatDate(value, type) {
// 计算日期相关值
let time = typeof value == 'number' ? new Date(value) : value
let Y = time.getFullYear()
let M = time.getMonth() + 1
let D = time.getDate()
let h = time.getHours()
let m = time.getMinutes()
let s = time.getSeconds()
let week = time.getDay()
// 如果传递了type的话
if (type === undefined) {
return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
} else if (type === 'week') {
// 在quartz中 1为星期日
return week + 1
}
}
// 检查日期是否存在
function checkDate(value) {
let time = new Date(value)
let format = formatDate(time)
return value === format
}
onMounted(() => {
expressionChange()
})
</script>

View File

@@ -0,0 +1,128 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 58)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 58)
average02.value = props.check(average02.value, 1, 59 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.second, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
// 单选按钮值变化时
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'second', '*', 'second')
break
case 2:
emit('update', 'second', cycleTotal.value, 'second')
break
case 3:
emit('update', 'second', averageTotal.value, 'second')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'second', checkboxString.value, 'second')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * ? / L #]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
不指定
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
周期从
<el-select clearable v-model="cycle01">
<el-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key === 7"
>{{item.value}}</el-option>
</el-select>
-
<el-select clearable v-model="cycle02">
<el-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key <= cycle01"
>{{item.value}}</el-option>
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
<el-input-number v-model='average01' :min="1" :max="4" /> 周的
<el-select clearable v-model="average02">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="5">
本月最后一个
<el-select clearable v-model="weekday">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="6">
指定
<el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: ""
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(2)
const cycle01 = ref(2)
const cycle02 = ref(3)
const average01 = ref(1)
const average02 = ref(2)
const weekday = ref(2)
const checkboxList = ref([])
const checkCopy = ref([2])
const weekList = ref([
{key: 1, value: '星期日'},
{key: 2, value: '星期一'},
{key: 3, value: '星期二'},
{key: 4, value: '星期三'},
{key: 5, value: '星期四'},
{key: 6, value: '星期五'},
{key: 7, value: '星期六'}
])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 6)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 4)
average02.value = props.check(average02.value, 1, 7)
return average02.value + '#' + average01.value
})
const weekdayTotal = computed(() => {
weekday.value = props.check(weekday.value, 1, 7)
return weekday.value + 'L'
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.week, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === "*") {
radioValue.value = 1
} else if (value === "?") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("#") > -1) {
const indexArr = value.split('#')
average01.value = Number(indexArr[1])
average02.value = Number(indexArr[0])
radioValue.value = 4
} else if (value.indexOf("L") > -1) {
const indexArr = value.split("L")
weekday.value = Number(indexArr[0])
radioValue.value = 5
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 6
}
}
function onRadioChange() {
if (radioValue.value === 2 && props.cron.day === '?') {
emit('update', 'day', '*', 'week')
}
if (radioValue.value !== 2 && props.cron.day !== '?') {
emit('update', 'day', '?', 'week')
}
switch (radioValue.value) {
case 1:
emit('update', 'week', '*', 'week')
break
case 2:
emit('update', 'week', '?', 'week')
break
case 3:
emit('update', 'week', cycleTotal.value, 'week')
break
case 4:
emit('update', 'week', averageTotal.value, 'week')
break
case 5:
emit('update', 'week', weekdayTotal.value, 'week')
break
case 6:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'week', checkboxString.value, 'week')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.5rem;
}
.el-select, .el-select--small {
width: 8rem;
}
.el-select.multiselect, .el-select--small.multiselect {
width: 17.8rem;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<el-form>
<el-form-item>
<el-radio :value="1" v-model='radioValue'>
不填允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="2" v-model='radioValue'>
每年
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="3" v-model='radioValue'>
周期从
<el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="4" v-model='radioValue'>
<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="5" v-model='radioValue'>
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: ""
}
},
check: {
type: Function,
default: () => {
}
}
})
const fullYear = Number(new Date().getFullYear())
const maxFullYear = fullYear + 10
const radioValue = ref(1)
const cycle01 = ref(fullYear)
const cycle02 = ref(fullYear + 1)
const average01 = ref(fullYear)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([fullYear])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
average02.value = props.check(average02.value, 1, 10)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.year, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '') {
radioValue.value = 1
} else if (value === "*") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 5
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'year', '', 'year')
break
case 2:
emit('update', 'year', '*', 'year')
break
case 3:
emit('update', 'year', cycleTotal.value, 'year')
break
case 4:
emit('update', 'year', averageTotal.value, 'year')
break
case 5:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'year', checkboxString.value, 'year')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -3,25 +3,31 @@
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<span
v-if="item.elTagType == 'default' || item.elTagType == ''"
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
:key="item.value"
:index="index"
:class="item.elTagClass"
>{{ item.label }}</span>
>{{ item.label + " " }}</span>
<el-tag
v-else
:disable-transitions="true"
:key="item.value + ''"
:index="index"
:type="item.elTagType === 'primary' ? '' : item.elTagType"
:type="item.elTagType"
:class="item.elTagClass"
>{{ item.label }}</el-tag>
>{{ item.label + " " }}</el-tag>
</template>
</template>
<template v-if="unmatch && showValue">
{{ unmatchArray | handleArray }}
</template>
</div>
</template>
<script setup>
// 记录未匹配的项
const unmatchArray = ref([])
const props = defineProps({
// 数据
options: {
@@ -30,20 +36,47 @@ const props = defineProps({
},
// 当前的值
value: [Number, String, Array],
})
const values = computed(() => {
if (props.value !== null && typeof props.value !== 'undefined') {
return Array.isArray(props.value) ? props.value : [String(props.value)];
} else {
return [];
// 当未找到匹配的数据时显示value
showValue: {
type: Boolean,
default: true,
},
separator: {
type: String,
default: ",",
}
})
const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
})
const unmatch = computed(() => {
unmatchArray.value = []
// 没有value不显示
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
// 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
values.value.forEach(item => {
if (!props.options.some(v => v.value === item)) {
unmatchArray.value.push(item)
unmatch = true // 如果有未匹配项将标志设置为true
}
})
return unmatch // 返回标志的值
})
function handleArray(array) {
if (array.length === 0) return ""
return array.reduce((pre, cur) => {
return pre + " " + cur
})
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
name="file"
:show-file-list="false"
:headers="headers"
class="editor-img-uploader"
v-if="type == 'url'"
>
<i ref="uploadRef" class="editor-img-uploader"></i>
</el-upload>
</div>
<div class="editor">
<quill-editor
ref="quillEditorRef"
v-model:content="content"
contentType="html"
@textChange="(e) => $emit('update:modelValue', content)"
:options="options"
:style="styles"
/>
</div>
</template>
<script setup>
import axios from 'axios'
import { QuillEditor } from "@vueup/vue-quill"
import "@vueup/vue-quill/dist/vue-quill.snow.css"
import { getToken } from "@/utils/auth"
const { proxy } = getCurrentInstance()
const quillEditorRef = ref()
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
const headers = ref({
Authorization: "Bearer " + getToken()
})
const props = defineProps({
/* 编辑器的内容 */
modelValue: {
type: String,
},
/* 高度 */
height: {
type: Number,
default: null,
},
/* 最小高度 */
minHeight: {
type: Number,
default: null,
},
/* 只读 */
readOnly: {
type: Boolean,
default: false,
},
/* 上传文件大小限制(MB) */
fileSize: {
type: Number,
default: 5,
},
/* 类型base64格式、url格式 */
type: {
type: String,
default: "url",
}
})
const options = ref({
theme: "snow",
bounds: document.body,
debug: "warn",
modules: {
// 工具栏配置
toolbar: [
["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
["blockquote", "code-block"], // 引用 代码块
[{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
[{ indent: "-1" }, { indent: "+1" }], // 缩进
[{ size: ["small", false, "large", "huge"] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
[{ align: [] }], // 对齐方式
["clean"], // 清除文本格式
["link", "image", "video"] // 链接、图片、视频
],
},
placeholder: "请输入内容",
readOnly: props.readOnly
})
const styles = computed(() => {
let style = {}
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`
}
if (props.height) {
style.height = `${props.height}px`
}
return style
})
const content = ref("")
watch(() => props.modelValue, (v) => {
if (v !== content.value) {
content.value = v == undefined ? "<p></p>" : v
}
}, { immediate: true })
// 如果设置了上传地址则自定义图片上传事件
onMounted(() => {
if (props.type == 'url') {
let quill = quillEditorRef.value.getQuill()
let toolbar = quill.getModule("toolbar")
toolbar.addHandler("image", (value) => {
if (value) {
proxy.$refs.uploadRef.click()
} else {
quill.format("image", false)
}
})
quill.root.addEventListener('paste', handlePasteCapture, true)
}
})
// 上传前校检格式和大小
function handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
const isJPG = type.includes(file.type)
//检验文件格式
if (!isJPG) {
proxy.$modal.msgError(`图片格式错误!`)
return false
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
return true
}
// 上传成功处理
function handleUploadSuccess(res, file) {
// 如果上传成功
if (res.code == 200) {
// 获取富文本实例
let quill = toRaw(quillEditorRef.value).getQuill()
// 获取光标位置
let length = quill.selection.savedRange.index
// 插入图片res.url为服务器返回的图片链接地址
quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName)
// 调整光标到最后
quill.setSelection(length + 1)
} else {
proxy.$modal.msgError("图片插入失败")
}
}
// 上传失败处理
function handleUploadError() {
proxy.$modal.msgError("图片插入失败")
}
// 复制粘贴图片处理
function handlePasteCapture(e) {
const clipboard = e.clipboardData || window.clipboardData
if (clipboard && clipboard.items) {
for (let i = 0; i < clipboard.items.length; i++) {
const item = clipboard.items[i]
if (item.type.indexOf('image') !== -1) {
e.preventDefault()
const file = item.getAsFile()
insertImage(file)
}
}
}
}
function insertImage(file) {
const formData = new FormData()
formData.append("file", file)
axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
handleUploadSuccess(res.data)
})
}
</script>
<style>
.editor-img-uploader {
display: none;
}
.editor, .ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
}
.quill-img {
display: none;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: "保存";
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
content: "32px";
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体";
}
</style>

View File

@@ -5,6 +5,7 @@
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
@@ -12,26 +13,27 @@
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
ref="upload"
ref="fileUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
<el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">&nbsp;删除</el-link>
</div>
</li>
</transition-group>
@@ -39,145 +41,199 @@
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { getToken } from "@/utils/auth"
import Sortable from 'sortablejs'
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
default: "/common/upload"
},
// 上传携带的参数
data: {
type: Object
},
// 数量限制
limit: {
type: Number,
default: 5,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 禁用组件(仅查看文件)
disabled: {
type: Boolean,
default: false
},
// 拖动排序
drag: {
type: Boolean,
default: true
}
});
})
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
)
watch(() => props.modelValue, val => {
if (val) {
let temp = 1;
let temp = 1
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(',');
const list = Array.isArray(val) ? val : props.modelValue.split(',')
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
item = { name: item, url: item }
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
item.uid = item.uid || new Date().getTime() + temp++
return item
})
} else {
fileList.value = [];
return [];
fileList.value = []
return []
}
},{ deep: true, immediate: true });
},{ deep: true, immediate: true })
// 上传前校检格式和大小
function handleBeforeUpload(file) {
// 校检文件类型
if (props.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
const isTypeOk = props.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
const fileName = file.name.split('.')
const fileExt = fileName[fileName.length - 1]
const isTypeOk = props.fileType.indexOf(fileExt) >= 0
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
return false;
proxy.$modal.msgError(`文件格式不正确请上传${props.fileType.join("/")}格式文件!`)
return false
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading("正在上传文件,请稍候...");
number.value++;
return true;
proxy.$modal.loading("正在上传文件,请稍候...")
number.value++
return true
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传失败
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
proxy.$modal.msgError("上传文件失败")
proxy.$modal.closeLoading()
}
// 上传成功回调
function handleUploadSuccess(res, file) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
if (uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.fileUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除文件
function handleDelete(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
fileList.value.splice(index, 1)
emit("update:modelValue", listToString(fileList.value))
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading()
}
}
// 获取文件名称
function getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
return name.slice(name.lastIndexOf("/") + 1)
} else {
return "";
return name
}
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
let strs = ""
separator = separator || ","
for (let i in list) {
if(undefined !== list[i].url) {
strs += list[i].url + separator;
if (list[i].url) {
strs += list[i].url + separator
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
return strs != '' ? strs.substr(0, strs.length - 1) : ''
}
</script>
// 初始化拖拽排序
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
Sortable.create(element, {
ghostClass: 'file-upload-darg',
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
.file-upload-darg {
opacity: 0.5;
background: #c8ebfb;
}
.upload-file-uploader {
margin-bottom: 5px;
}
@@ -186,6 +242,7 @@ function listToString(list, separator) {
line-height: 2;
margin-bottom: 10px;
position: relative;
transition: none !important;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;

View File

@@ -7,6 +7,7 @@
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
fill="currentColor"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
@@ -23,7 +24,7 @@ defineProps({
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick');
emit('toggleClick')
}
</script>

View File

@@ -1,19 +1,46 @@
<template>
<div :class="{ 'show': show }" class="header-search">
<div class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelectRef"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
<el-dialog
v-model="show"
width="600"
@close="close"
:show-close="false"
append-to-body
>
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
</el-select>
<el-input
v-model="search"
ref="headerSearchSelectRef"
size="large"
@input="querySearch"
prefix-icon="Search"
placeholder="菜单搜索支持标题、URL模糊查询"
clearable
@keyup.enter="selectActiveResult"
@keydown.up.prevent="navigateResult('up')"
@keydown.down.prevent="navigateResult('down')"
>
</el-input>
<div class="result-wrap">
<el-scrollbar>
<div class="search-item" tabindex="1" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1">
<div class="left">
<svg-icon class="menu-icon" :icon-class="item.icon" />
</div>
<div class="search-info" @click="change(item)">
<div class="menu-title">
{{ item.title.join(" / ") }}
</div>
<div class="menu-path">
{{ item.path }}
</div>
</div>
<svg-icon icon-class="enter" v-show="index === activeIndex"/>
</div>
</el-scrollbar>
</div>
</el-dialog>
</div>
</template>
@@ -21,36 +48,49 @@
import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
const search = ref('');
const options = ref([]);
const searchPool = ref([]);
const show = ref(false);
const fuse = ref(undefined);
const headerSearchSelectRef = ref(null);
const router = useRouter();
const routes = computed(() => usePermissionStore().routes);
const search = ref('')
const options = ref([])
const searchPool = ref([])
const activeIndex = ref(-1)
const show = ref(false)
const fuse = ref(undefined)
const headerSearchSelectRef = ref(null)
const router = useRouter()
const theme = computed(() => useSettingsStore().theme)
const routes = computed(() => usePermissionStore().defaultRoutes)
function click() {
show.value = !show.value
if (show.value) {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
options.value = searchPool.value
}
};
}
function close() {
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
search.value = ''
options.value = []
show.value = false
activeIndex.value = -1
}
function change(val) {
const path = val.path;
const path = val.path
const query = val.query
if (isHttp(path)) {
// http(s):// 路径新窗口打开
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
const pindex = path.indexOf("http")
window.open(path.substr(pindex, path.length), "_blank")
} else {
router.push(path)
if (query) {
router.push({ path: path, query: JSON.parse(query) })
} else {
router.push(path)
}
}
search.value = ''
@@ -59,13 +99,13 @@ function change(val) {
show.value = false
})
}
function initFuse(list) {
fuse.value = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: 'title',
@@ -76,6 +116,7 @@ function initFuse(list) {
}]
})
}
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) {
@@ -84,21 +125,25 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
for (const r of routes) {
// skip hidden router
if (r.hidden) { continue }
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle]
title: [...prefixTitle],
icon: ''
}
if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title]
if (r.redirect !== 'noRedirect') {
data.icon = r.meta.icon
if (r.redirect !== "noRedirect") {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
if (r.query) {
data.query = r.query
}
// recursive child routes
if (r.children) {
@@ -110,30 +155,42 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
}
return res
}
function querySearch(query) {
activeIndex.value = -1
if (query !== '') {
options.value = fuse.value.search(query)
options.value = fuse.value.search(query).map((item) => item.item) ?? searchPool.value
} else {
options.value = []
options.value = searchPool.value
}
}
function activeStyle(index) {
if (index !== activeIndex.value) return {}
return {
"background-color": theme.value,
"color": "#fff"
}
}
function navigateResult(direction) {
if (direction === "up") {
activeIndex.value = activeIndex.value <= 0 ? options.value.length - 1 : activeIndex.value - 1
} else if (direction === "down") {
activeIndex.value = activeIndex.value >= options.value.length - 1 ? 0 : activeIndex.value + 1
}
}
function selectActiveResult() {
if (options.value.length > 0 && activeIndex.value >= 0) {
change(options.value[activeIndex.value])
}
}
onMounted(() => {
searchPool.value = generateRoutes(routes.value);
})
watchEffect(() => {
searchPool.value = generateRoutes(routes.value)
})
watch(show, (value) => {
if (value) {
document.body.addEventListener('click', close)
} else {
document.body.removeEventListener('click', close)
}
})
watch(searchPool, (list) => {
initFuse(list)
})
@@ -141,40 +198,55 @@ watch(searchPool, (list) => {
<style lang='scss' scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
.result-wrap {
height: 280px;
margin: 6px 0;
:deep(.el-input__inner) {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
.search-item {
display: flex;
height: 48px;
align-items: center;
padding-right: 10px;
.left {
width: 60px;
text-align: center;
.menu-icon {
width: 18px;
height: 18px;
}
}
.search-info {
padding-left: 5px;
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
.menu-title,
.menu-path {
height: 20px;
}
.menu-path {
color: #ccc;
font-size: 10px;
}
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
.search-item:hover {
cursor: pointer;
}
}
</style>
</style>

View File

@@ -2,7 +2,7 @@
<div class="icon-body">
<el-input
v-model="iconName"
style="position: relative;"
class="icon-search"
clearable
placeholder="请输入图标名称"
@clear="filterIcons"
@@ -11,9 +11,13 @@
<template #suffix><i class="el-icon-search el-input__icon" /></template>
</el-input>
<div class="icon-list">
<div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
<svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
<span>{{ item }}</span>
<div class="list-container">
<div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
<div :class="['icon-item', { active: activeIcon === item }]">
<svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
<span>{{ item }}</span>
</div>
</div>
</div>
</div>
</div>
@@ -22,9 +26,15 @@
<script setup>
import icons from './requireIcons'
const iconName = ref('');
const iconList = ref(icons);
const emit = defineEmits(['selected']);
const props = defineProps({
activeIcon: {
type: String
}
})
const iconName = ref('')
const iconList = ref(icons)
const emit = defineEmits(['selected'])
function filterIcons() {
iconList.value = icons
@@ -49,26 +59,53 @@ defineExpose({
</script>
<style lang='scss' scoped>
.icon-body {
width: 100%;
padding: 10px;
.icon-list {
height: 200px;
overflow-y: scroll;
div {
height: 30px;
line-height: 30px;
margin-bottom: -5px;
cursor: pointer;
width: 33%;
float: left;
.icon-body {
width: 100%;
padding: 10px;
.icon-search {
position: relative;
margin-bottom: 5px;
}
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
.icon-list {
height: 200px;
overflow: auto;
.list-container {
display: flex;
flex-wrap: wrap;
.icon-item-wrapper {
width: calc(100% / 3);
height: 25px;
line-height: 25px;
cursor: pointer;
display: flex;
.icon-item {
display: flex;
max-width: 100%;
height: 100%;
padding: 0 5px;
&:hover {
background: #ececec;
border-radius: 5px;
}
.icon {
flex-shrink: 0;
}
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
padding-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.icon-item.active {
background: #ececec;
border-radius: 5px;
}
}
}
}
}
}
</style>

View File

@@ -1,8 +1,8 @@
let icons = []
const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
const modules = import.meta.glob('./../../assets/icons/svg/*.svg')
for (const path in modules) {
const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
icons.push(p);
const p = path.split('assets/icons/svg/')[1].split('.svg')[0]
icons.push(p)
}
export default icons

View File

@@ -4,7 +4,7 @@
fit="cover"
:style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList"
append-to-body="true"
preview-teleported
>
<template #error>
<div class="image-slot">
@@ -15,12 +15,12 @@
</template>
<script setup>
import { isExternal } from "@/utils/validate";
import { isExternal } from "@/utils/validate"
const props = defineProps({
src: {
type: String,
required: true
default: ""
},
width: {
type: [Number, String],
@@ -30,35 +30,41 @@ const props = defineProps({
type: [Number, String],
default: ""
}
});
})
const realSrc = computed(() => {
let real_src = props.src.split(",")[0];
if (isExternal(real_src)) {
return real_src;
if (!props.src) {
return
}
return import.meta.env.VITE_APP_BASE_API + real_src;
});
let real_src = props.src.split(",")[0]
if (isExternal(real_src)) {
return real_src
}
return import.meta.env.VITE_APP_BASE_API + real_src
})
const realSrcList = computed(() => {
let real_src_list = props.src.split(",");
let srcList = [];
if (!props.src) {
return
}
let real_src_list = props.src.split(",")
let srcList = []
real_src_list.forEach(item => {
if (isExternal(item)) {
return srcList.push(item);
return srcList.push(item)
}
return srcList.push(import.meta.env.VITE_APP_BASE_API + item);
});
return srcList;
});
return srcList.push(import.meta.env.VITE_APP_BASE_API + item)
})
return srcList
})
const realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px`
);
)
const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px`
);
)
</script>
<style lang="scss" scoped>

View File

@@ -2,15 +2,17 @@
<div class="component-upload-image">
<el-upload
multiple
:disabled="disabled"
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
name="file"
:on-remove="handleRemove"
ref="imageUpload"
:before-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
@@ -20,7 +22,7 @@
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
@@ -46,145 +48,202 @@
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { getToken } from "@/utils/auth"
import { isExternal } from "@/utils/validate"
import Sortable from 'sortablejs'
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
default: "/common/upload"
},
// 上传携带的参数
data: {
type: Object
},
// 图片数量限制
limit: {
type: Number,
default: 5,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
default: () => ["png", "jpg", "jpeg"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
});
// 禁用组件(仅查看图片)
disabled: {
type: Boolean,
default: false
},
// 拖动排序
drag: {
type: Boolean,
default: true
}
})
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref("")
const dialogVisible = ref(false)
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
)
watch(() => props.modelValue, val => {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(",");
const list = Array.isArray(val) ? val : props.modelValue.split(",")
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1) {
item = { name: baseUrl + item, url: baseUrl + item };
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item }
} else {
item = { name: item, url: item };
item = { name: item, url: item }
}
}
return item;
});
return item
})
} else {
fileList.value = [];
return [];
fileList.value = []
return []
}
},{ deep: true, immediate: true });
// 删除图片
function handleRemove(file, files) {
emit("update:modelValue", listToString(fileList.value));
}
// 上传成功回调
function handleUploadSuccess(res) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
if (uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
}
}
},{ deep: true, immediate: true })
// 上传前loading加载
function handleBeforeUpload(file) {
let isImg = false;
let isImg = false
if (props.fileType.length) {
let fileExtension = "";
let fileExtension = ""
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
}
isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
})
} else {
isImg = file.type.indexOf("image") > -1;
isImg = file.type.indexOf("image") > -1
}
if (!isImg) {
proxy.$modal.msgError(
`文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
);
return false;
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
return false
}
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false;
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading("正在上传图片,请稍候...");
number.value++;
proxy.$modal.loading("正在上传图片,请稍候...")
number.value++
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.imageUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除图片
function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name)
if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1)
emit("update:modelValue", listToString(fileList.value))
return false
}
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading()
}
}
// 上传失败
function handleUploadError() {
proxy.$modal.msgError("上传图片失败");
proxy.$modal.closeLoading();
proxy.$modal.msgError("上传图片失败")
proxy.$modal.closeLoading()
}
// 预览
function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
dialogImageUrl.value = file.url
dialogVisible.value = true
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
let strs = ""
separator = separator || ","
for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator;
strs += list[i].url.replace(baseUrl, "") + separator
}
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
return strs != "" ? strs.substr(0, strs.length - 1) : ""
}
// 初始化拖拽排序
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
Sortable.create(element, {
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
@@ -192,4 +251,8 @@ function listToString(list, separator) {
:deep(.hide .el-upload--picture-card) {
display: none;
}
:deep(.el-upload.el-upload--picture-card.is-disabled) {
display: none !important;
}
</style>

View File

@@ -59,7 +59,7 @@ const props = defineProps({
}
})
const emit = defineEmits();
const emit = defineEmits()
const currentPage = computed({
get() {
return props.page
@@ -76,6 +76,7 @@ const pageSize = computed({
emit('update:limit', val)
}
})
function handleSizeChange(val) {
if (currentPage.value * val > props.total) {
currentPage.value = 1
@@ -85,19 +86,18 @@ function handleSizeChange(val) {
scrollTo(0, 800)
}
}
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;

View File

@@ -1,21 +1,38 @@
<template>
<div class="top-right-btn">
<div class="top-right-btn" :style="style">
<el-row>
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
<el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
<el-button circle icon="Menu" @click="showColumn()" />
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="Object.keys(columns).length > 0">
<el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button circle icon="Menu" />
<template #dropdown>
<el-dropdown-menu>
<!-- 全选/反选 按钮 -->
<el-dropdown-item>
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
</el-dropdown-item>
<div class="check-line"></div>
<template v-for="(item, key) in columns" :key="item.key">
<el-dropdown-item>
<el-checkbox v-model="item.visible" @change="checkboxChange($event, key)" :label="item.label" />
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</el-row>
<el-dialog :title="title" v-model="open" append-to-body>
<el-transfer
:titles="['显示', '隐藏']"
v-model="value"
:data="columns"
:data="transferData"
@change="dataChange"
></el-transfer>
</el-dialog>
@@ -24,51 +41,120 @@
<script setup>
const props = defineProps({
/* 是否显示检索条件 */
showSearch: {
type: Boolean,
default: true,
default: true
},
/* 显隐列信息(数组格式、对象格式) */
columns: {
type: Array,
type: [Array, Object],
default: () => ({})
},
/* 是否显示检索图标 */
search: {
type: Boolean,
default: true
},
/* 显隐列类型transfer穿梭框、checkbox复选框 */
showColumnsType: {
type: String,
default: "checkbox"
},
/* 右外边距 */
gutter: {
type: Number,
default: 10
},
})
const emits = defineEmits(['update:showSearch', 'queryTable']);
const emits = defineEmits(['update:showSearch', 'queryTable'])
// 显隐数据
const value = ref([]);
const value = ref([])
// 弹出层标题
const title = ref("显示/隐藏");
const title = ref("显示/隐藏")
// 是否显示弹出层
const open = ref(false);
const open = ref(false)
const style = computed(() => {
const ret = {}
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`
}
return ret
})
// 是否全选/半选 状态
const isChecked = computed({
get: () => Array.isArray(props.columns) ? props.columns.every(col => col.visible) : Object.values(props.columns).every((col) => col.visible),
set: () => {}
})
const isIndeterminate = computed(() => Array.isArray(props.columns) ? props.columns.some((col) => col.visible) && !isChecked.value : Object.values(props.columns).some((col) => col.visible) && !isChecked.value)
const transferData = computed(() => Array.isArray(props.columns) ? props.columns.map((item, index) => ({ key: index, label: item.label })) : Object.keys(props.columns).map((key, index) => ({ key: index, label: props.columns[key].label })))
// 搜索
function toggleSearch() {
emits("update:showSearch", !props.showSearch);
emits("update:showSearch", !props.showSearch)
}
// 刷新
function refresh() {
emits("queryTable");
emits("queryTable")
}
// 右侧列表元素变化
function dataChange(data) {
for (let item in props.columns) {
const key = props.columns[item].key;
props.columns[item].visible = !data.includes(key);
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
const key = props.columns[item].key
props.columns[item].visible = !data.includes(key)
}
} else {
Object.keys(props.columns).forEach((key, index) => {
props.columns[key].visible = !data.includes(index)
})
}
}
// 打开显隐列dialog
function showColumn() {
open.value = true;
open.value = true
}
// 显隐列初始默认隐藏列
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item));
if (props.showColumnsType == "transfer") {
// transfer穿梭显隐列初始默认隐藏列
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item))
}
}
} else {
Object.keys(props.columns).forEach((key, index) => {
if (props.columns[key].visible === false) {
value.value.push(index)
}
})
}
}
// 单勾选
function checkboxChange(event, key) {
if (Array.isArray(props.columns)) {
props.columns.filter(item => item.key == key)[0].visible = event
} else {
props.columns[key].visible = event
}
}
// 切换全选/反选
function toggleCheckAll() {
const newValue = !isChecked.value
if (Array.isArray(props.columns)) {
props.columns.forEach((col) => (col.visible = newValue))
} else {
Object.values(props.columns).forEach((col) => (col.visible = newValue))
}
}
</script>
@@ -82,8 +168,14 @@ for (let item in props.columns) {
:deep(.el-transfer__button:first-child) {
margin-bottom: 10px;
}
.my-el-transfer {
text-align: center;
:deep(.el-dropdown-menu__item) {
line-height: 30px;
padding: 0 17px;
}
</style>
.check-line {
width: 90%;
height: 1px;
background-color: #ccc;
margin: 3px auto;
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
<script setup>
const url = ref('http://doc.ruoyi.vip/ruoyi-vue');
const url = ref('http://doc.ruoyi.vip/ruoyi-vue')
function goto() {
window.open(url.value)

View File

@@ -5,7 +5,7 @@
</template>
<script setup>
const url = ref('https://gitee.com/y_project/RuoYi-Vue');
const url = ref('https://gitee.com/y_project/RuoYi-Vue')
function goto() {
window.open(url.value)

View File

@@ -7,7 +7,7 @@
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen();
const { isFullscreen, enter, exit, toggle } = useFullscreen()
</script>
<style lang='scss' scoped>

View File

@@ -16,23 +16,23 @@
</template>
<script setup>
import useAppStore from "@/store/modules/app";
import useAppStore from "@/store/modules/app"
const appStore = useAppStore();
const size = computed(() => appStore.size);
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const appStore = useAppStore()
const size = computed(() => appStore.size)
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const sizeOptions = ref([
{ label: "较大", value: "large" },
{ label: "默认", value: "default" },
{ label: "稍小", value: "small" },
]);
])
function handleSetSize(size) {
proxy.$modal.loading("正在设置布局大小,请稍候...");
appStore.setSize(size);
setTimeout("window.location.reload()", 1000);
proxy.$modal.loading("正在设置布局大小,请稍候...")
appStore.setSize(size)
setTimeout("window.location.reload()", 1000)
}
</script>

View File

@@ -1,10 +1,10 @@
import * as components from '@element-plus/icons-vue'
export default {
install: (app) => {
for (const key in components) {
const componentConfig = components[key];
app.component(componentConfig.name, componentConfig);
}
},
};
install: (app) => {
for (const key in components) {
const componentConfig = components[key]
app.component(componentConfig.name, componentConfig)
}
}
}

View File

@@ -3,12 +3,15 @@
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
:ellipsis="false"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item
>
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
<!-- 顶部菜单超出数量折叠 -->
@@ -18,10 +21,12 @@
<el-menu-item
:index="item.path"
:key="index"
v-if="index >= visibleNumber"
><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item
>
v-if="index >= visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
</el-sub-menu>
</el-menu>
@@ -35,116 +40,127 @@ import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
// 顶部栏初始数
const visibleNumber = ref(null);
const visibleNumber = ref(null)
// 当前激活菜单的 index
const currentIndex = ref(null);
const currentIndex = ref(null)
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
const hideList = ['/index', '/user/profile']
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute();
const router = useRouter();
const route = useRoute()
const router = useRouter()
// 主题颜色
const theme = computed(() => settingsStore.theme);
const theme = computed(() => settingsStore.theme)
// 所有的路由信息
const routers = computed(() => permissionStore.topbarRouters);
const routers = computed(() => permissionStore.topbarRouters)
// 顶部显示菜单
const topMenus = computed(() => {
let topMenus = [];
let topMenus = []
routers.value.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === "/") {
topMenus.push(menu.children[0]);
if (menu.path === '/' && menu.children) {
topMenus.push(menu.children[0])
} else {
topMenus.push(menu);
topMenus.push(menu)
}
}
})
return topMenus;
return topMenus
})
// 设置子路由
const childrenMenus = computed(() => {
let childrenMenus = [];
let childrenMenus = []
routers.value.map((router) => {
for (let item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
router.children[item].path = "/" + router.children[item].path
} else {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
router.children[item].path = router.path + "/" + router.children[item].path
}
}
router.children[item].parentPath = router.path;
router.children[item].parentPath = router.path
}
childrenMenus.push(router.children[item]);
childrenMenus.push(router.children[item])
}
})
return constantRoutes.concat(childrenMenus);
return constantRoutes.concat(childrenMenus)
})
// 默认激活的菜单
const activeMenu = computed(() => {
const path = route.path;
let activePath = path;
const path = route.path
let activePath = path
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
appStore.toggleSideBarHide(false);
const tmpPath = path.substring(1, path.length)
if (!route.meta.link) {
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
appStore.toggleSideBarHide(false)
}
} else if(!route.children) {
activePath = path;
appStore.toggleSideBarHide(true);
activePath = path
appStore.toggleSideBarHide(true)
}
activeRoutes(activePath);
return activePath;
activeRoutes(activePath)
return activePath
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
visibleNumber.value = parseInt(width / 85);
const width = document.body.getBoundingClientRect().width / 3
visibleNumber.value = parseInt(width / 85)
}
function handleSelect(key, keyPath) {
currentIndex.value = key;
const route = routers.value.find(item => item.path === key);
currentIndex.value = key
const route = routers.value.find(item => item.path === key)
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank");
window.open(key, "_blank")
} else if (!route || !route.children) {
// 没有子路由路径内部打开
router.push({ path: key });
appStore.toggleSideBarHide(true);
const routeMenu = childrenMenus.value.find(item => item.path === key)
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query)
router.push({ path: key, query: query })
} else {
router.push({ path: key })
}
appStore.toggleSideBarHide(true)
} else {
// 显示左侧联动菜单
activeRoutes(key);
appStore.toggleSideBarHide(false);
activeRoutes(key)
appStore.toggleSideBarHide(false)
}
}
function activeRoutes(key) {
let routes = [];
let routes = []
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
routes.push(item)
}
});
})
}
if(routes.length > 0) {
permissionStore.setSidebarRouters(routes);
permissionStore.setSidebarRouters(routes)
} else {
appStore.toggleSideBarHide(true)
}
return routes;
return routes
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
@@ -178,4 +194,24 @@ onMounted(() => {
padding: 0 5px !important;
margin: 0 10px !important;
}
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
background-color: #ffffff;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
margin-right: 4px;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
}
</style>

View File

@@ -1,156 +0,0 @@
<template>
<div class="el-tree-select">
<el-select
style="width: 100%"
v-model="valueId"
ref="treeSelect"
:filterable="true"
:clearable="true"
@clear="clearHandle"
:filter-method="selectFilterData"
:placeholder="placeholder"
>
<el-option :value="valueId" :label="valueTitle">
<el-tree
id="tree-option"
ref="selectTree"
:accordion="accordion"
:data="options"
:props="objMap"
:node-key="objMap.value"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKey"
:filter-node-method="filterNode"
@node-click="handleNodeClick"
></el-tree>
</el-option>
</el-select>
</div>
</template>
<script setup>
const { proxy } = getCurrentInstance();
const props = defineProps({
/* 配置项 */
objMap: {
type: Object,
default: () => {
return {
value: 'id', // ID字段名
label: 'label', // 显示名称
children: 'children' // 子级字段名
}
}
},
/* 自动收起 */
accordion: {
type: Boolean,
default: () => {
return false
}
},
/**当前双向数据绑定的值 */
value: {
type: [String, Number],
default: ''
},
/**当前的数据 */
options: {
type: Array,
default: () => []
},
/**输入框内部的文字 */
placeholder: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:value']);
const valueId = computed({
get: () => props.value,
set: (val) => {
emit('update:value', val)
}
});
const valueTitle = ref('');
const defaultExpandedKey = ref([]);
function initHandle() {
nextTick(() => {
const selectedValue = valueId.value;
if(selectedValue !== null && typeof (selectedValue) !== 'undefined') {
const node = proxy.$refs.selectTree.getNode(selectedValue)
if (node) {
valueTitle.value = node.data[props.objMap.label]
proxy.$refs.selectTree.setCurrentKey(selectedValue) // 设置默认选中
defaultExpandedKey.value = [selectedValue] // 设置默认展开
}
} else {
clearHandle()
}
})
}
function handleNodeClick(node) {
valueTitle.value = node[props.objMap.label]
valueId.value = node[props.objMap.value];
defaultExpandedKey.value = [];
proxy.$refs.treeSelect.blur()
selectFilterData('')
}
function selectFilterData(val) {
proxy.$refs.selectTree.filter(val)
}
function filterNode(value, data) {
if (!value) return true
return data[props.objMap['label']].indexOf(value) !== -1
}
function clearHandle() {
valueTitle.value = ''
valueId.value = ''
defaultExpandedKey.value = [];
clearSelected()
}
function clearSelected() {
const allNode = document.querySelectorAll('#tree-option .el-tree-node')
allNode.forEach((element) => element.classList.remove('is-current'))
}
onMounted(() => {
initHandle()
})
watch(valueId, () => {
initHandle();
})
</script>
<style lang='scss' scoped>
@import "@/assets/styles/variables.module.scss";
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
padding: 0;
background-color: #fff;
height: auto;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
ul li .el-tree .el-tree-node__content {
height: auto;
padding: 0 20px;
box-sizing: border-box;
}
:deep(.el-tree-node__content:hover),
:deep(.el-tree-node__content:active),
:deep(.is-current > div:first-child),
:deep(.el-tree-node__content:focus) {
background-color: mix(#fff, $--color-primary, 90%);
color: $--color-primary;
}
</style>

View File

@@ -22,10 +22,10 @@ const url = computed(() => props.src)
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 300);
loading.value = false
}, 300)
window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + "px;";
};
height.value = document.documentElement.clientHeight - 94.5 + "px;"
}
})
</script>

View File

@@ -2,65 +2,64 @@
* v-copyText 复制文本内容
* Copyright (c) 2022 ruoyi
*/
export default {
beforeMount(el, { value, arg }) {
if (arg === "callback") {
el.$copyCallback = value;
el.$copyCallback = value
} else {
el.$copyValue = value;
el.$copyValue = value
const handler = () => {
copyTextToClipboard(el.$copyValue);
copyTextToClipboard(el.$copyValue)
if (el.$copyCallback) {
el.$copyCallback(el.$copyValue);
el.$copyCallback(el.$copyValue)
}
};
el.addEventListener("click", handler);
el.$destroyCopy = () => el.removeEventListener("click", handler);
}
el.addEventListener("click", handler)
el.$destroyCopy = () => el.removeEventListener("click", handler)
}
}
}
function copyTextToClipboard(input, { target = document.body } = {}) {
const element = document.createElement('textarea');
const previouslyFocusedElement = document.activeElement;
const element = document.createElement('textarea')
const previouslyFocusedElement = document.activeElement
element.value = input;
element.value = input
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '');
element.setAttribute('readonly', '')
element.style.contain = 'strict';
element.style.position = 'absolute';
element.style.left = '-9999px';
element.style.fontSize = '12pt'; // Prevent zooming on iOS
element.style.contain = 'strict'
element.style.position = 'absolute'
element.style.left = '-9999px'
element.style.fontSize = '12pt' // Prevent zooming on iOS
const selection = document.getSelection();
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
const selection = document.getSelection()
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0)
target.append(element);
element.select();
target.append(element)
element.select()
// Explicit selection workaround for iOS
element.selectionStart = 0;
element.selectionEnd = input.length;
element.selectionStart = 0
element.selectionEnd = input.length
let isSuccess = false;
let isSuccess = false
try {
isSuccess = document.execCommand('copy');
isSuccess = document.execCommand('copy')
} catch { }
element.remove();
element.remove()
if (originalRange) {
selection.removeAllRanges();
selection.addRange(originalRange);
selection.removeAllRanges()
selection.addRange(originalRange)
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
previouslyFocusedElement.focus()
}
return isSuccess;
return isSuccess
}

View File

@@ -2,13 +2,12 @@
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/
import useUserStore from '@/store/modules/user'
export default {
mounted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*";
const all_permission = "*:*:*"
const permissions = useUserStore().permissions
if (value && value instanceof Array && value.length > 0) {

View File

@@ -2,13 +2,12 @@
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/
import useUserStore from '@/store/modules/user'
export default {
mounted(el, binding, vnode) {
const { value } = binding
const super_admin = "admin";
const super_admin = "admin"
const roles = useUserStore().roles
if (value && value instanceof Array && value.length > 0) {
@@ -22,7 +21,7 @@ export default {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置角色权限标签值"`)
throw new Error(`请设置角色权限标签值`)
}
}
}

View File

@@ -2,23 +2,37 @@
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path"/>
<keep-alive :include="tagsViewStore.cachedViews">
<component v-if="!route.meta.link" :is="Component" :key="route.path"/>
</keep-alive>
</transition>
</router-view>
<iframe-toggle />
<copyright />
</section>
</template>
<script setup>
import copyright from "./Copyright/index"
import iframeToggle from "./IframeToggle/index"
import useTagsViewStore from '@/store/modules/tagsView'
const tagsViewStore = useTagsViewStore()
const route = useRoute()
tagsViewStore.addCachedView(route)
const cachedViews = computed(() => {
return tagsViewStore.cachedViews
const tagsViewStore = useTagsViewStore()
onMounted(() => {
addIframe()
})
watchEffect(() => {
addIframe()
})
function addIframe() {
if (route.meta.link) {
useTagsViewStore().addIframeView(route)
}
}
</script>
<style lang="scss" scoped>
@@ -31,7 +45,18 @@ const cachedViews = computed(() => {
}
.fixed-header + .app-main {
padding-top: 50px;
overflow-y: auto;
scrollbar-gutter: auto;
height: calc(100vh - 50px);
min-height: 0px;
}
.app-main:has(.copyright) {
padding-bottom: 36px;
}
.fixed-header + .app-main {
margin-top: 50px;
}
.hasTagsView {
@@ -41,16 +66,25 @@ const cachedViews = computed(() => {
}
.fixed-header + .app-main {
padding-top: 84px;
margin-top: 84px;
height: calc(100vh - 84px);
min-height: 0px;
}
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 17px;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
</style>
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #c0c0c0;
border-radius: 3px;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<footer v-if="visible" class="copyright">
<span>{{ content }}</span>
</footer>
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const visible = computed(() => settingsStore.footerVisible)
const content = computed(() => settingsStore.footerContent)
</script>
<style scoped>
.copyright {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 36px;
padding: 10px 20px;
text-align: right;
background-color: #f8f8f8;
color: #666;
font-size: 14px;
border-top: 1px solid #e7e7e7;
z-index: 999;
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<inner-link
v-for="(item, index) in tagsViewStore.iframeViews"
:key="item.path"
:iframeId="'iframe' + index"
v-show="route.path === item.path"
:src="iframeUrl(item.meta.link, item.query)"
></inner-link>
</template>
<script setup>
import InnerLink from "../InnerLink/index"
import useTagsViewStore from "@/store/modules/tagsView"
const route = useRoute()
const tagsViewStore = useTagsViewStore()
function iframeUrl(url, query) {
if (Object.keys(query).length > 0) {
let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&")
return url + "?" + params
}
return url
}
</script>

View File

@@ -1,30 +1,35 @@
<script>
export default {
setup() {
const route = useRoute();
const link = route.meta.link;
if (link === "") {
return "404";
}
let url = link;
const height = document.documentElement.clientHeight - 94.5 + "px";
const style = { height: height };
<template>
<div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!">
<iframe
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
ref="iframeRef"
frameborder="no"
></iframe>
</div>
</template>
// 返回渲染函数
return () =>
h(
"div",
{
style: style,
},
h("iframe", {
src: url,
frameborder: "no",
width: "100%",
height: "100%",
scrolling: "auto",
})
);
<script setup>
const props = defineProps({
src: {
type: String,
default: "/"
},
};
iframeId: {
type: String
}
})
const loading = ref(true)
const height = ref(document.documentElement.clientHeight - 94.5 + 'px')
const iframeRef = ref(null)
onMounted(() => {
if (iframeRef.value) {
iframeRef.value.onload = () => {
loading.value = false
}
}
})
</script>

View File

@@ -1,8 +1,8 @@
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
<top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
<breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
<div class="right-menu">
<template v-if="appStore.device !== 'mobile'">
@@ -18,31 +18,37 @@
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="主题模式" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
<svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
<svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
</div>
</el-tooltip>
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<div class="avatar-container">
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
<el-icon><caret-bottom /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item command="setLayout">
<el-dropdown @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover">
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
<span class="user-nickname"> {{ userStore.nickName }} </span>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-dropdown-item divided command="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
@@ -72,13 +78,13 @@ function toggleSideBar() {
function handleCommand(command) {
switch (command) {
case "setLayout":
setLayout();
break;
setLayout()
break
case "logout":
logout();
break;
logout()
break
default:
break;
break
}
}
@@ -89,14 +95,18 @@ function logout() {
type: 'warning'
}).then(() => {
userStore.logOut().then(() => {
location.href = '/index';
location.href = '/index'
})
}).catch(() => { });
}).catch(() => { })
}
const emits = defineEmits(['setLayout'])
function setLayout() {
emits('setLayout');
emits('setLayout')
}
function toggleTheme() {
settingsStore.toggleTheme()
}
</script>
@@ -105,7 +115,7 @@ function setLayout() {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
background: var(--navbar-bg);
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
@@ -161,20 +171,44 @@ function setLayout() {
background: rgba(0, 0, 0, 0.025);
}
}
&.theme-switch-wrapper {
display: flex;
align-items: center;
svg {
transition: transform 0.3s;
&:hover {
transform: scale(1.15);
}
}
}
}
.avatar-container {
margin-right: 40px;
margin-right: 0px;
padding-right: 0px;
.avatar-wrapper {
margin-top: 5px;
margin-top: 10px;
right: 8px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
width: 30px;
height: 30px;
margin-right: 8px;
border-radius: 50%;
}
.user-nickname{
position: relative;
left: 0px;
bottom: 10px;
font-size: 14px;
font-weight: bold;
}
i {

View File

@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
<el-drawer v-model="showSettings" :withHeader="false" :lock-scroll="false" direction="rtl" size="300px">
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
@@ -38,35 +38,49 @@
<div class="drawer-item">
<span>开启 TopNav</span>
<span class="comp-style">
<el-switch v-model="topNav" class="drawer-switch" />
<el-switch v-model="settingsStore.topNav" @change="topNavChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span class="comp-style">
<el-switch v-model="tagsView" class="drawer-switch" />
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示页签图标</span>
<span class="comp-style">
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<span class="comp-style">
<el-switch v-model="fixedHeader" class="drawer-switch" />
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<span class="comp-style">
<el-switch v-model="sidebarLogo" class="drawer-switch" />
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>动态标题</span>
<span class="comp-style">
<el-switch v-model="dynamicTitle" class="drawer-switch" />
<el-switch v-model="settingsStore.dynamicTitle" @change="dynamicTitleChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>底部版权</span>
<span class="comp-style">
<el-switch v-model="settingsStore.footerVisible" class="drawer-switch" />
</span>
</div>
@@ -79,113 +93,88 @@
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import originElementPlus from 'element-plus/theme-chalk/index.css'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { handleThemeStyle } from '@/utils/theme'
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
const showSettings = ref(false)
const theme = ref(settingsStore.theme)
const sideTheme = ref(settingsStore.sideTheme)
const storeSettings = computed(() => settingsStore)
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"])
/** 是否需要topnav */
const topNav = computed({
get: () => storeSettings.value.topNav,
set: (val) => {
settingsStore.changeSetting({ key: 'topNav', value: val })
if (!val) {
appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
}
function topNavChange(val) {
if (!val) {
appStore.toggleSideBarHide(false)
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
}
})
/** 是否需要tagview */
const tagsView = computed({
get: () => storeSettings.value.tagsView,
set: (val) => {
settingsStore.changeSetting({ key: 'tagsView', value: val })
}
})
/**是否需要固定头部 */
const fixedHeader = computed({
get: () => storeSettings.value.fixedHeader,
set: (val) => {
settingsStore.changeSetting({ key: 'fixedHeader', value: val })
}
})
/**是否需要侧边栏的logo */
const sidebarLogo = computed({
get: () => storeSettings.value.sidebarLogo,
set: (val) => {
settingsStore.changeSetting({ key: 'sidebarLogo', value: val })
}
})
/**是否需要侧边栏的动态网页的title */
const dynamicTitle = computed({
get: () => storeSettings.value.dynamicTitle,
set: (val) => {
settingsStore.changeSetting({ key: 'dynamicTitle', value: val })
// 动态设置网页标题
useDynamicTitle()
}
})
}
/** 是否需要dynamicTitle */
function dynamicTitleChange() {
useSettingsStore().setTitle(useSettingsStore().title)
}
function themeChange(val) {
settingsStore.changeSetting({ key: 'theme', value: val })
theme.value = val;
settingsStore.theme = val
handleThemeStyle(val)
}
function handleTheme(val) {
settingsStore.changeSetting({ key: 'sideTheme', value: val })
sideTheme.value = val;
settingsStore.sideTheme = val
sideTheme.value = val
}
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
proxy.$modal.loading("正在保存到本地,请稍候...")
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
"tagsIcon": storeSettings.value.tagsIcon,
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"footerVisible": storeSettings.value.footerVisible,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
}
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting))
setTimeout(proxy.$modal.closeLoading(), 1000)
}
function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...")
localStorage.removeItem("layout-setting")
setTimeout("window.location.reload()", 1000)
}
function openSetting() {
showSettings.value = true;
showSettings.value = true
}
defineExpose({
openSetting,
openSetting
})
</script>
<style lang='scss' scoped>
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
line-height: 22px;
font-weight: bold;
.drawer-title {
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
@@ -204,13 +193,6 @@ defineExpose({
height: 48px;
}
.custom-img {
width: 48px;
height: 38px;
border-radius: 5px;
box-shadow: 1px 1px 2px #898484;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
@@ -227,7 +209,7 @@ defineExpose({
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
padding: 12px 0;
font-size: 14px;

View File

@@ -1,22 +1,22 @@
<template>
<div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
<h1 v-else class="sidebar-title">{{ title }}</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
<h1 class="sidebar-title">{{ title }}</h1>
</router-link>
</transition>
</div>
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import logo from '@/assets/logo/logo.png'
import useSettingsStore from '@/store/modules/settings'
import variables from '@/assets/styles/variables.module.scss'
defineProps({
collapse: {
@@ -25,9 +25,25 @@ defineProps({
}
})
const title = ref('若依管理系统');
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
const title = import.meta.env.VITE_APP_TITLE
const settingsStore = useSettingsStore()
const sideTheme = computed(() => settingsStore.sideTheme)
// 获取Logo背景色
const getLogoBackground = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-bg)'
}
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
})
// 获取Logo文字颜色
const getLogoTextColor = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-text)'
}
return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText
})
</script>
<style lang="scss" scoped>
@@ -45,7 +61,7 @@ const sideTheme = computed(() => settingsStore.sideTheme);
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
background: v-bind(getLogoBackground);
text-align: center;
overflow: hidden;
@@ -63,7 +79,7 @@ const sideTheme = computed(() => settingsStore.sideTheme);
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
color: v-bind(getLogoTextColor);
font-weight: 600;
line-height: 50px;
font-size: 14px;

View File

@@ -9,15 +9,15 @@
</app-link>
</template>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template v-if="item.meta" #title>
<svg-icon :icon-class="item.meta && item.meta.icon" />
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
v-for="(child, index) in item.children"
:key="child.path + index"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
@@ -48,20 +48,18 @@ const props = defineProps({
}
})
const onlyOneChild = ref({});
const onlyOneChild = ref({})
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = [];
children = []
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
}
onlyOneChild.value = item
return true
})
// When there is only one child router, the child router is displayed by default
@@ -76,7 +74,7 @@ function hasOneShowingChild(children = [], parent) {
}
return false
};
}
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
@@ -86,7 +84,7 @@ function resolvePath(routePath, routeQuery) {
return props.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
let query = JSON.parse(routeQuery)
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
}
return getNormalPath(props.basePath + '/' + routePath)
@@ -94,9 +92,9 @@ function resolvePath(routePath, routeQuery) {
function hasTitle(title){
if (title.length > 5) {
return title;
return title
} else {
return "";
return ""
}
}
</script>

View File

@@ -1,16 +1,17 @@
<template>
<div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<div :class="{ 'has-logo': showLogo }" class="sidebar-container">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:background-color="getMenuBackground"
:text-color="getMenuTextColor"
:unique-opened="true"
:active-text-color="theme"
:collapse-transition="false"
mode="vertical"
:class="sideTheme"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
@@ -31,24 +32,73 @@ import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
const route = useRoute();
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters);
const showLogo = computed(() => settingsStore.sidebarLogo);
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const isCollapse = computed(() => !appStore.sidebar.opened);
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const showLogo = computed(() => settingsStore.sidebarLogo)
const sideTheme = computed(() => settingsStore.sideTheme)
const theme = computed(() => settingsStore.theme)
const isCollapse = computed(() => !appStore.sidebar.opened)
const activeMenu = computed(() => {
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu;
// 获取菜单背景色
const getMenuBackground = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-bg)'
}
return path;
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
})
// 获取菜单文字颜色
const getMenuTextColor = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-text)'
}
return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
})
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})
</script>
<style lang="scss" scoped>
.sidebar-container {
background-color: v-bind(getMenuBackground);
.scrollbar-wrapper {
background-color: v-bind(getMenuBackground);
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
.el-menu-item, .el-sub-menu__title {
&:hover {
background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
}
}
.el-menu-item {
color: v-bind(getMenuTextColor);
&.is-active {
color: var(--menu-active-text, #409eff);
background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
}
}
.el-sub-menu__title {
color: v-bind(getMenuTextColor);
}
}
}
</style>

View File

@@ -12,35 +12,37 @@
<script setup>
import useTagsViewStore from '@/store/modules/tagsView'
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance();
const tagAndTagSpacing = ref(4)
const { proxy } = getCurrentInstance()
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrap$);
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef)
onMounted(() => {
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
})
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
function handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = scrollWrapper.value;
const $scrollWrapper = scrollWrapper.value
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const emits = defineEmits()
const emitScroll = () => {
emits('scroll')
}
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.visitedViews);
const visitedViews = computed(() => tagsViewStore.visitedViews)
function moveToTarget(currentTag) {
const $container = proxy.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = scrollWrapper.value;
const $scrollWrapper = scrollWrapper.value
let firstTag = null
let lastTag = null
@@ -56,17 +58,17 @@ function moveToTarget(currentTag) {
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
const tagListDom = document.getElementsByClassName('tags-view-item');
const tagListDom = document.getElementsByClassName('tags-view-item')
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
let prevTag = null
let nextTag = null
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
prevTag = tagListDom[k];
prevTag = tagListDom[k]
}
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
nextTag = tagListDom[k];
nextTag = tagListDom[k]
}
}
}
@@ -99,7 +101,7 @@ defineExpose({
bottom: 0px;
}
:deep(.el-scrollbar__wrap) {
height: 49px;
height: 39px;
}
}
</style>

View File

@@ -5,13 +5,14 @@
v-for="tag in visitedViews"
:key="tag.path"
:data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
class="tags-view-item"
:style="activeStyle(tag)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
{{ tag.title }}
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
@@ -48,25 +49,27 @@ import useTagsViewStore from '@/store/modules/tagsView'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const scrollPaneRef = ref(null);
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const selectedTag = ref({})
const affixTags = ref([])
const scrollPaneRef = ref(null)
const { proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const visitedViews = computed(() => useTagsViewStore().visitedViews);
const routes = computed(() => usePermissionStore().routes);
const theme = computed(() => useSettingsStore().theme);
const visitedViews = computed(() => useTagsViewStore().visitedViews)
const routes = computed(() => usePermissionStore().routes)
const theme = computed(() => useSettingsStore().theme)
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
watch(route, () => {
addTags()
moveToCurrentTag()
})
watch(visible, (value) => {
if (value) {
document.body.addEventListener('click', closeMenu)
@@ -74,6 +77,7 @@ watch(visible, (value) => {
document.body.removeEventListener('click', closeMenu)
}
})
onMounted(() => {
initTags()
addTags()
@@ -82,23 +86,27 @@ onMounted(() => {
function isActive(r) {
return r.path === route.path
}
function activeStyle(tag) {
if (!isActive(tag)) return {};
if (!isActive(tag)) return {}
return {
"background-color": theme.value,
"border-color": theme.value
};
}
}
function isAffix(tag) {
return tag.meta && tag.meta.affix
}
function isFirstView() {
try {
return selectedTag.value.fullPath === visitedViews.value[1].fullPath || selectedTag.value.fullPath === '/index'
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
} catch (err) {
return false
}
}
function isLastView() {
try {
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
@@ -106,6 +114,7 @@ function isLastView() {
return false
}
}
function filterAffixTags(routes, basePath = '') {
let tags = []
routes.forEach(route => {
@@ -127,9 +136,10 @@ function filterAffixTags(routes, basePath = '') {
})
return tags
}
function initTags() {
const res = filterAffixTags(routes.value);
affixTags.value = res;
const res = filterAffixTags(routes.value)
affixTags.value = res
for (const tag of res) {
// Must have tag name
if (tag.name) {
@@ -137,18 +147,19 @@ function initTags() {
}
}
}
function addTags() {
const { name } = route
if (name) {
useTagsViewStore().addView(route)
}
return false
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of visitedViews.value) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
scrollPaneRef.value.moveToTarget(r)
// when query is different then update
if (r.fullPath !== route.fullPath) {
useTagsViewStore().updateVisitedView(route)
@@ -157,9 +168,14 @@ function moveToCurrentTag() {
}
})
}
function refreshSelectedTag(view) {
proxy.$tab.refreshPage(view);
proxy.$tab.refreshPage(view)
if (route.meta.link) {
useTagsViewStore().delIframeView(route)
}
}
function closeSelectedTag(view) {
proxy.$tab.closePage(view).then(({ visitedViews }) => {
if (isActive(view)) {
@@ -167,6 +183,7 @@ function closeSelectedTag(view) {
}
})
}
function closeRightTags() {
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
@@ -174,6 +191,7 @@ function closeRightTags() {
}
})
}
function closeLeftTags() {
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
@@ -181,12 +199,14 @@ function closeLeftTags() {
}
})
}
function closeOthersTags() {
router.push(selectedTag.value).catch(() => { });
router.push(selectedTag.value).catch(() => { })
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag()
})
}
function closeAllTags(view) {
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
if (affixTags.value.some(tag => tag.path === route.path)) {
@@ -195,6 +215,7 @@ function closeAllTags(view) {
toLastView(visitedViews, view)
})
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
@@ -210,6 +231,7 @@ function toLastView(visitedViews, view) {
}
}
}
function openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
@@ -227,21 +249,24 @@ function openMenu(tag, e) {
visible.value = true
selectedTag.value = tag
}
function closeMenu() {
visible.value = false
}
function handleScroll() {
closeMenu()
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
background: var(--tags-bg, #fff);
border-bottom: 1px solid var(--tags-item-border, #d8dce5);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
@@ -249,39 +274,48 @@ function handleScroll() {
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
border: 1px solid var(--tags-item-border, #d8dce5);
color: var(--tags-item-text, #495060);
background: var(--tags-item-bg, #fff);
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: "";
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
margin-right: 5px;
}
}
}
}
.tags-view-item.active.has-icon::before {
content: none !important;
}
.contextmenu {
margin: 0;
background: #fff;
background: var(--el-bg-color-overlay, #fff);
z-index: 3000;
position: absolute;
list-style-type: none;
@@ -289,14 +323,17 @@ function handleScroll() {
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
color: var(--tags-item-text, #333);
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
border: 1px solid var(--el-border-color-light, #e4e7ed);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
background: var(--tags-item-hover, #eee);
}
}
}
@@ -313,15 +350,17 @@ function handleScroll() {
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
transform: scale(.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
background-color: var(--tags-close-hover, #b4bccc);
color: #fff;
width: 12px !important;
height: 12px !important;

View File

@@ -17,18 +17,16 @@
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const theme = computed(() => settingsStore.theme)
const sideTheme = computed(() => settingsStore.sideTheme)
const sidebar = computed(() => useAppStore().sidebar)
const device = computed(() => useAppStore().device)
const needTagsView = computed(() => settingsStore.tagsView)
const fixedHeader = computed(() => settingsStore.fixedHeader)
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
@@ -37,13 +35,16 @@ const classObj = computed(() => ({
mobile: device.value === 'mobile'
}))
const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
const { width, height } = useWindowSize()
const WIDTH = 992 // refer to Bootstrap's responsive design
watchEffect(() => {
watch(() => device.value, () => {
if (device.value === 'mobile' && sidebar.value.opened) {
useAppStore().closeSideBar({ withoutAnimation: false })
}
})
watchEffect(() => {
if (width.value - 1 < WIDTH) {
useAppStore().toggleDevice('mobile')
useAppStore().closeSideBar({ withoutAnimation: true })
@@ -56,18 +57,18 @@ function handleClickOutside() {
useAppStore().closeSideBar({ withoutAnimation: false })
}
const settingRef = ref(null);
const settingRef = ref(null)
function setLayout() {
settingRef.value.openSetting();
settingRef.value.openSetting()
}
</script>
<style lang="scss" scoped>
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
@use "@/assets/styles/mixin.scss" as mix;
@use "@/assets/styles/variables.module.scss" as vars;
.app-wrapper {
@include clearfix;
@include mix.clearfix;
position: relative;
height: 100%;
width: 100%;
@@ -78,6 +79,11 @@ function setLayout() {
}
}
.main-container:has(.fixed-header) {
height: 100vh;
overflow: hidden;
}
.drawer-bg {
background: #000;
opacity: 0.3;
@@ -93,7 +99,7 @@ function setLayout() {
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
width: calc(100% - #{vars.$base-sidebar-width});
transition: width 0.28s;
}

View File

@@ -3,7 +3,9 @@ import { createApp } from 'vue'
import Cookies from 'js-cookie'
import ElementPlus from 'element-plus'
import locale from 'element-plus/lib/locale/lang/zh-cn' // 中文语言
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import locale from 'element-plus/es/locale/lang/zh-cn'
import '@/assets/styles/index.scss' // global css
@@ -12,7 +14,6 @@ import store from './store'
import router from './router'
import directive from './directive' // directive
// 注册指令
import plugins from './plugins' // plugins
import { download } from '@/utils/request'
@@ -25,20 +26,21 @@ import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control
import { useDict } from '@/utils/dict'
import { getConfigKey } from "@/api/system/config"
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 分页组件
import Pagination from '@/components/Pagination'
// 自定义表格工具组件
import RightToolbar from '@/components/RightToolbar'
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
// 自定义树选择组件
import TreeSelect from '@/components/TreeSelect'
// 字典标签组件
import DictTag from '@/components/DictTag'
@@ -51,17 +53,18 @@ app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.getConfigKey = getConfigKey
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels
// 全局组件挂载
app.component('DictTag', DictTag)
app.component('Pagination', Pagination)
app.component('TreeSelect', TreeSelect)
app.component('FileUpload', FileUpload)
app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor)
app.use(router)
app.use(store)

View File

@@ -3,15 +3,19 @@ import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
import { isHttp, isPathMatch } from '@/utils/validate'
import { isRelogin } from '@/utils/request'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
NProgress.configure({ showSpinner: false });
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect', '/bind', '/register'];
const whiteList = ['/login', '/register']
const isWhiteList = (path) => {
return whiteList.some(pattern => isPathMatch(pattern, path))
}
router.beforeEach((to, from, next) => {
NProgress.start()
@@ -21,6 +25,8 @@ router.beforeEach((to, from, next) => {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else if (isWhiteList(to.path)) {
next()
} else {
if (useUserStore().roles.length === 0) {
isRelogin.show = true
@@ -48,7 +54,7 @@ router.beforeEach((to, from, next) => {
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
if (isWhiteList(to.path)) {
// 在免登录白名单,直接进入
next()
} else {

View File

@@ -1,7 +1,7 @@
import useUserStore from '@/store/modules/user'
function authPermission(permission) {
const all_permission = "*:*:*";
const all_permission = "*:*:*"
const permissions = useUserStore().permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
@@ -13,7 +13,7 @@ function authPermission(permission) {
}
function authRole(role) {
const super_admin = "admin";
const super_admin = "admin"
const roles = useUserStore().roles
if (role && role.length > 0) {
return roles.some(v => {
@@ -27,7 +27,7 @@ function authRole(role) {
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
return authPermission(permission)
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
@@ -43,7 +43,7 @@ export default {
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
return authRole(role)
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {

View File

@@ -26,9 +26,10 @@ const sessionCache = {
if (value != null) {
return JSON.parse(value)
}
return null
},
remove (key) {
sessionStorage.removeItem(key);
sessionStorage.removeItem(key)
}
}
const localCache = {
@@ -59,9 +60,10 @@ const localCache = {
if (value != null) {
return JSON.parse(value)
}
return null
},
remove (key) {
localStorage.removeItem(key);
localStorage.removeItem(key)
}
}

View File

@@ -1,72 +1,79 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { blobValidate } from '@/utils/ruoyi'
const baseURL = import.meta.env.VITE_APP_BASE_API
let downloadLoadingInstance
export default {
name(name, isDelete = true) {
var url = baseURL + "/common/download?fileName=" + encodeURI(name) + "&delete=" + isDelete
var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
}).then((res) => {
const isBlob = blobValidate(res.data)
if (isBlob) {
const blob = new Blob([res.data])
this.saveAs(blob, decodeURI(res.headers['download-filename']))
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
} else {
this.printErrMsg(res.data);
this.printErrMsg(res.data)
}
})
},
resource(resource) {
var url = baseURL + "/common/download/resource?resource=" + encodeURI(resource);
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource)
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
}).then((res) => {
const isBlob = blobValidate(res.data)
if (isBlob) {
const blob = new Blob([res.data])
this.saveAs(blob, decodeURI(res.headers['download-filename']))
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
} else {
this.printErrMsg(res.data);
this.printErrMsg(res.data)
}
})
},
zip(url, name) {
var url = baseURL + url
downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
}).then((res) => {
const isBlob = blobValidate(res.data)
if (isBlob) {
const blob = new Blob([res.data], { type: 'application/zip' })
this.saveAs(blob, name)
} else {
this.printErrMsg(res.data);
this.printErrMsg(res.data)
}
downloadLoadingInstance.close()
}).catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close()
})
},
saveAs(text, name, opts) {
saveAs(text, name, opts);
saveAs(text, name, opts)
},
async printErrMsg(data) {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg);
ElMessage.error(errMsg)
}
}

View File

@@ -1,6 +1,6 @@
import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
let loadingInstance;
let loadingInstance
export default {
// 消息提示
@@ -41,7 +41,7 @@ export default {
},
// 错误通知
notifyError(content) {
ElNotification.error(content);
ElNotification.error(content)
},
// 成功通知
notifySuccess(content) {
@@ -77,6 +77,6 @@ export default {
},
// 关闭遮罩层
closeLoading() {
loadingInstance.close();
loadingInstance.close()
}
}

View File

@@ -4,15 +4,15 @@ import router from '@/router'
export default {
// 刷新当前tab页签
refreshPage(obj) {
const { path, query, matched } = router.currentRoute.value;
const { path, query, matched } = router.currentRoute.value
if (obj === undefined) {
matched.forEach((m) => {
if (m.components && m.components.default && m.components.default.name) {
if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
obj = { name: m.components.default.name, path: path, query: query };
obj = { name: m.components.default.name, path: path, query: query }
}
}
});
})
}
return useTagsViewStore().delCachedView(obj).then(() => {
const { path, query } = obj
@@ -24,42 +24,48 @@ export default {
},
// 关闭当前tab页签打开新页签
closeOpenPage(obj) {
useTagsViewStore().delView(router.currentRoute.value);
useTagsViewStore().delView(router.currentRoute.value)
if (obj !== undefined) {
return router.push(obj);
return router.push(obj)
}
},
// 关闭指定tab页签
closePage(obj) {
if (obj === undefined) {
return useTagsViewStore().delView(router.currentRoute.value).then(({ lastPath }) => {
return router.push(lastPath || '/index');
});
return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
return router.push(latestView.fullPath)
}
return router.push('/')
})
}
return useTagsViewStore().delView(obj);
return useTagsViewStore().delView(obj)
},
// 关闭所有tab页签
closeAllPage() {
return useTagsViewStore().delAllViews();
return useTagsViewStore().delAllViews()
},
// 关闭左侧tab页签
closeLeftPage(obj) {
return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
return useTagsViewStore().delLeftTags(obj || router.currentRoute.value)
},
// 关闭右侧tab页签
closeRightPage(obj) {
return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
return useTagsViewStore().delRightTags(obj || router.currentRoute.value)
},
// 关闭其他tab页签
closeOtherPage(obj) {
return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
return useTagsViewStore().delOthersViews(obj || router.currentRoute.value)
},
// 打开tab页签
openPage(url) {
return router.push(url);
openPage(title, url, params) {
const obj = { path: url, meta: { title: title } }
useTagsViewStore().addView(obj)
return router.push({ path: url, query: params })
},
// 修改tab页签
updatePage(obj) {
return useTagsViewStore().updateVisitedView(obj);
return useTagsViewStore().updateVisitedView(obj)
}
}

View File

@@ -77,7 +77,7 @@ export const constantRoutes = [
redirect: 'noredirect',
children: [
{
path: 'profile',
path: 'profile/:activeTab?',
component: () => import('@/views/system/user/profile/index'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user' }
@@ -137,7 +137,7 @@ export const dynamicRoutes = [
permissions: ['monitor:job:list'],
children: [
{
path: 'index',
path: 'index/:jobId(\\d+)',
component: () => import('@/views/monitor/job/log'),
name: 'JobLog',
meta: { title: '调度日志', activeMenu: '/monitor/job' }
@@ -166,10 +166,9 @@ const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
return { top: 0 }
},
});
})
export default router;
export default router

View File

@@ -3,14 +3,16 @@ export default {
* 网页标题
*/
title: import.meta.env.VITE_APP_TITLE,
/**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/
sideTheme: 'theme-dark',
/**
* 是否系统布局配置
*/
showSettings: false,
showSettings: true,
/**
* 是否显示顶部导航
@@ -21,6 +23,11 @@ export default {
* 是否显示 tagsView
*/
tagsView: true,
/**
* 显示页签图标
*/
tagsIcon: false,
/**
* 是否固定头部
@@ -38,10 +45,13 @@ export default {
dynamicTitle: false,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
* 是否显示底部版权
*/
errorLog: 'production'
footerVisible: false,
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2018-2025 RuoYi. All Rights Reserved.'
}

View File

@@ -1,6 +1,5 @@
import Cookies from 'js-cookie'
const useAppStore = defineStore(
'app',
{
@@ -16,7 +15,7 @@ const useAppStore = defineStore(
actions: {
toggleSideBar(withoutAnimation) {
if (this.sidebar.hide) {
return false;
return false
}
this.sidebar.opened = !this.sidebar.opened
this.sidebar.withoutAnimation = withoutAnimation
@@ -26,7 +25,7 @@ const useAppStore = defineStore(
Cookies.set('sidebarStatus', 0)
}
},
closeSideBar(withoutAnimation) {
closeSideBar({ withoutAnimation }) {
Cookies.set('sidebarStatus', 0)
this.sidebar.opened = false
this.sidebar.withoutAnimation = withoutAnimation
@@ -35,7 +34,7 @@ const useAppStore = defineStore(
this.device = device
},
setSize(size) {
this.size = size;
this.size = size
Cookies.set('size', size)
},
toggleSideBarHide(status) {

57
src/store/modules/dict.js Normal file
View File

@@ -0,0 +1,57 @@
const useDictStore = defineStore(
'dict',
{
state: () => ({
dict: new Array()
}),
actions: {
// 获取字典
getDict(_key) {
if (_key == null && _key == "") {
return null
}
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
return this.dict[i].value
}
}
} catch (e) {
return null
}
},
// 设置字典
setDict(_key, value) {
if (_key !== null && _key !== "") {
this.dict.push({
key: _key,
value: value
})
}
},
// 删除字典
removeDict(_key) {
var bln = false
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
this.dict.splice(i, 1)
return true
}
}
} catch (e) {
bln = false
}
return bln
},
// 清空字典
cleanDict() {
this.dict = new Array()
},
// 初始字典
initDict() {
}
}
})
export default useDictStore

View File

@@ -85,24 +85,13 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach((el, index) => {
if (el.children && el.children.length) {
if (el.component === 'ParentView' && !lastRouter) {
el.children.forEach(c => {
c.path = el.path + '/' + c.path
if (c.children && c.children.length) {
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
childrenMap.forEach(el => {
el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
if (el.children && el.children.length && el.component === 'ParentView') {
children = children.concat(filterChildren(el.children, el))
} else {
children.push(el)
}
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
}
children = children.concat(el)
})
return children
}
@@ -125,14 +114,14 @@ export function filterDynamicRoutes(routes) {
}
export const loadView = (view) => {
let res;
let res
for (const path in modules) {
const dir = path.split('views/')[1].split('.vue')[0];
const dir = path.split('views/')[1].split('.vue')[0]
if (dir === view) {
res = () => modules[path]();
res = () => modules[path]()
}
}
return res;
return res
}
export default usePermissionStore

View File

@@ -1,7 +1,11 @@
import defaultSettings from '@/settings'
import { useDark, useToggle } from '@vueuse/core'
import { useDynamicTitle } from '@/utils/dynamicTitle'
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
const isDark = useDark()
const toggleDark = useToggle(isDark)
const { sideTheme, showSettings, topNav, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
@@ -15,9 +19,13 @@ const useSettingsStore = defineStore(
showSettings: showSettings,
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible,
footerContent: footerContent,
isDark: isDark.value
}),
actions: {
// 修改布局设置
@@ -30,7 +38,12 @@ const useSettingsStore = defineStore(
// 设置网页标题
setTitle(title) {
this.title = title
useDynamicTitle();
useDynamicTitle()
},
// 切换暗黑模式
toggleTheme() {
this.isDark = !this.isDark
toggleDark()
}
}
})

View File

@@ -3,13 +3,22 @@ const useTagsViewStore = defineStore(
{
state: () => ({
visitedViews: [],
cachedViews: []
cachedViews: [],
iframeViews: []
}),
actions: {
addView(view) {
this.addVisitedView(view)
this.addCachedView(view)
},
addIframeView(view) {
if (this.iframeViews.some(v => v.path === view.path)) return
this.iframeViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
addVisitedView(view) {
if (this.visitedViews.some(v => v.path === view.path)) return
this.visitedViews.push(
@@ -42,9 +51,16 @@ const useTagsViewStore = defineStore(
break
}
}
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.visitedViews])
})
},
delIframeView(view) {
return new Promise(resolve => {
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.iframeViews])
})
},
delCachedView(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
@@ -67,6 +83,7 @@ const useTagsViewStore = defineStore(
this.visitedViews = this.visitedViews.filter(v => {
return v.meta.affix || v.path === view.path
})
this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
resolve([...this.visitedViews])
})
},
@@ -95,6 +112,7 @@ const useTagsViewStore = defineStore(
return new Promise(resolve => {
const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
this.visitedViews = affixTags
this.iframeViews = []
resolve([...this.visitedViews])
})
},
@@ -126,6 +144,10 @@ const useTagsViewStore = defineStore(
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
@@ -145,6 +167,10 @@ const useTagsViewStore = defineStore(
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])

View File

@@ -1,5 +1,8 @@
import router from '@/router'
import { ElMessageBox, } from 'element-plus'
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
const useUserStore = defineStore(
@@ -7,7 +10,9 @@ const useUserStore = defineStore(
{
state: () => ({
token: getToken(),
id: '',
name: '',
nickName: '',
avatar: '',
roles: [],
permissions: []
@@ -34,16 +39,32 @@ const useUserStore = defineStore(
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar;
let avatar = user.avatar || ""
if (!isHttp(avatar)) {
avatar = (isEmpty(avatar)) ? defAva : import.meta.env.VITE_APP_BASE_API + avatar
}
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
this.roles = res.roles
this.permissions = res.permissions
} else {
this.roles = ['ROLE_DEFAULT']
}
this.id = user.userId
this.name = user.userName
this.avatar = avatar;
this.nickName = user.nickName
this.avatar = avatar
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
}
resolve(res)
}).catch(error => {
reject(error)

View File

@@ -1,17 +1,24 @@
import useDictStore from '@/store/modules/dict'
import { getDicts } from '@/api/system/dict/data'
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
const res = ref({})
return (() => {
args.forEach((d, index) => {
res.value[d] = [];
getDicts(d).then(resp => {
res.value[d] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
})
args.forEach((dictType, index) => {
res.value[dictType] = []
const dicts = useDictStore().getDict(dictType)
if (dicts) {
res.value[dictType] = dicts
} else {
getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
useDictStore().setDict(dictType, res.value[dictType])
})
}
})
return toRefs(res.value);
return toRefs(res.value)
})()
}

View File

@@ -1,4 +1,3 @@
import store from '@/store'
import defaultSettings from '@/settings'
import useSettingsStore from '@/store/modules/settings'
@@ -6,10 +5,10 @@ import useSettingsStore from '@/store/modules/settings'
* 动态修改标题
*/
export function useDynamicTitle() {
const settingsStore = useSettingsStore();
const settingsStore = useSettingsStore()
if (settingsStore.dynamicTitle) {
document.title = settingsStore.title + ' - ' + defaultSettings.title;
document.title = settingsStore.title + ' - ' + defaultSettings.title
} else {
document.title = defaultSettings.title;
document.title = defaultSettings.title
}
}

View File

@@ -0,0 +1,452 @@
export const formConf = {
formRef: 'formRef',
formModel: 'formData',
size: 'default',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true,
}
export const inputComponents = [
{
label: '单行文本',
tag: 'el-input',
tagIcon: 'input',
type: 'text',
placeholder: '请输入',
defaultValue: undefined,
span: 24,
labelWidth: null,
style: { width: '100%' },
clearable: true,
prepend: '',
append: '',
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input',
},
{
label: '多行文本',
tag: 'el-input',
tagIcon: 'textarea',
type: 'textarea',
placeholder: '请输入',
defaultValue: undefined,
span: 24,
labelWidth: null,
autosize: {
minRows: 4,
maxRows: 4,
},
style: { width: '100%' },
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input',
},
{
label: '密码',
tag: 'el-input',
tagIcon: 'password',
type: 'password',
placeholder: '请输入',
defaultValue: undefined,
span: 24,
'show-password': true,
labelWidth: null,
style: { width: '100%' },
clearable: true,
prepend: '',
append: '',
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input',
},
{
label: '计数器',
tag: 'el-input-number',
tagIcon: 'number',
placeholder: '',
defaultValue: undefined,
span: 24,
labelWidth: null,
min: undefined,
max: undefined,
step: undefined,
'step-strictly': false,
precision: undefined,
'controls-position': '',
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input-number',
},
]
export const selectComponents = [
{
label: '下拉选择',
tag: 'el-select',
tagIcon: 'select',
placeholder: '请选择',
defaultValue: undefined,
span: 24,
labelWidth: null,
style: { width: '100%' },
clearable: true,
disabled: false,
required: true,
filterable: false,
multiple: false,
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
],
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/select',
},
{
label: '级联选择',
tag: 'el-cascader',
tagIcon: 'cascader',
placeholder: '请选择',
defaultValue: [],
span: 24,
labelWidth: null,
style: { width: '100%' },
props: {
props: {
multiple: false,
},
},
'show-all-levels': true,
disabled: false,
clearable: true,
filterable: false,
required: true,
options: [
{
id: 1,
value: 1,
label: '选项1',
children: [
{
id: 2,
value: 2,
label: '选项1-1',
},
],
},
],
dataType: 'dynamic',
labelKey: 'label',
valueKey: 'value',
childrenKey: 'children',
separator: '/',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/cascader',
},
{
label: '单选框组',
tag: 'el-radio-group',
tagIcon: 'radio',
defaultValue: 0,
span: 24,
labelWidth: null,
style: {},
optionType: 'default',
border: false,
size: 'default',
disabled: false,
required: true,
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
],
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/radio',
},
{
label: '多选框组',
tag: 'el-checkbox-group',
tagIcon: 'checkbox',
defaultValue: [],
span: 24,
labelWidth: null,
style: {},
optionType: 'default',
border: false,
size: 'default',
disabled: false,
required: true,
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
],
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/checkbox',
},
{
label: '开关',
tag: 'el-switch',
tagIcon: 'switch',
defaultValue: false,
span: 24,
labelWidth: null,
style: {},
disabled: false,
required: true,
'active-text': '',
'inactive-text': '',
'active-color': null,
'inactive-color': null,
'active-value': true,
'inactive-value': false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/switch',
},
{
label: '滑块',
tag: 'el-slider',
tagIcon: 'slider',
defaultValue: null,
span: 24,
labelWidth: null,
disabled: false,
required: true,
min: 0,
max: 100,
step: 1,
'show-stops': false,
range: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/slider',
},
{
label: '时间选择',
tag: 'el-time-picker',
tagIcon: 'time',
placeholder: '请选择',
defaultValue: '',
span: 24,
labelWidth: null,
style: { width: '100%' },
disabled: false,
clearable: true,
required: true,
format: 'HH:mm:ss',
'value-format': 'HH:mm:ss',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/time-picker',
},
{
label: '时间范围',
tag: 'el-time-picker',
tagIcon: 'time-range',
defaultValue: null,
span: 24,
labelWidth: null,
style: { width: '100%' },
disabled: false,
clearable: true,
required: true,
'is-range': true,
'range-separator': '至',
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
format: 'HH:mm:ss',
'value-format': 'HH:mm:ss',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/time-picker',
},
{
label: '日期选择',
tag: 'el-date-picker',
tagIcon: 'date',
placeholder: '请选择',
defaultValue: null,
type: 'date',
span: 24,
labelWidth: null,
style: { width: '100%' },
disabled: false,
clearable: true,
required: true,
format: 'YYYY-MM-DD',
'value-format': 'YYYY-MM-DD',
readonly: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/date-picker',
},
{
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
labelWidth: null,
style: { width: '100%' },
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
required: true,
format: 'YYYY-MM-DD',
'value-format': 'YYYY-MM-DD',
readonly: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/date-picker',
},
{
label: '评分',
tag: 'el-rate',
tagIcon: 'rate',
defaultValue: 0,
span: 24,
labelWidth: null,
style: {},
max: 5,
'allow-half': false,
'show-text': false,
'show-score': false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/rate',
},
{
label: '颜色选择',
tag: 'el-color-picker',
tagIcon: 'color',
defaultValue: null,
labelWidth: null,
'show-alpha': false,
'color-format': '',
disabled: false,
required: true,
size: 'default',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/color-picker',
},
{
label: '上传',
tag: 'el-upload',
tagIcon: 'upload',
action: 'https://jsonplaceholder.typicode.com/posts/',
defaultValue: null,
labelWidth: null,
disabled: false,
required: true,
accept: '',
name: 'file',
'auto-upload': true,
showTip: false,
buttonText: '点击上传',
fileSize: 2,
sizeUnit: 'MB',
'list-type': 'text',
multiple: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/upload',
tip: '只能上传不超过 2MB 的文件',
style: { width: '100%' },
},
]
export const layoutComponents = [
{
layout: 'rowFormItem',
tagIcon: 'row',
type: 'default',
justify: 'start',
align: 'top',
label: '行容器',
layoutTree: true,
children: [],
document: 'https://element-plus.org/zh-CN/component/layout',
},
{
layout: 'colFormItem',
label: '按钮',
changeTag: true,
labelWidth: null,
tag: 'el-button',
tagIcon: 'button',
span: 24,
default: '主要按钮',
type: 'primary',
icon: 'Search',
size: 'default',
disabled: false,
document: 'https://element-plus.org/zh-CN/component/button',
},
]
// 组件rule的触发方式无触发方式的组件不生成rule
export const trigger = {
'el-input': 'blur',
'el-input-number': 'blur',
'el-select': 'change',
'el-radio-group': 'change',
'el-checkbox-group': 'change',
'el-cascader': 'change',
'el-time-picker': 'change',
'el-date-picker': 'change',
'el-rate': 'change',
}

View File

@@ -0,0 +1,18 @@
const styles = {
'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}',
'el-upload': '.el-upload__tip{line-height: 1.2;}'
}
function addCss(cssList, el) {
const css = styles[el.tag]
css && cssList.indexOf(css) === -1 && cssList.push(css)
if (el.children) {
el.children.forEach(el2 => addCss(cssList, el2))
}
}
export function makeUpCss(conf) {
const cssList = []
conf.fields.forEach(el => addCss(cssList, el))
return cssList.join('\n')
}

View File

@@ -0,0 +1,37 @@
export const drawingDefaultValue = []
export function initDrawingDefaultValue() {
if (drawingDefaultValue.length === 0) {
drawingDefaultValue.push({
layout: 'colFormItem',
tagIcon: 'input',
label: '手机号',
vModel: 'mobile',
formId: 6,
tag: 'el-input',
placeholder: '请输入手机号',
defaultValue: '',
span: 24,
style: {width: '100%'},
clearable: true,
prepend: '',
append: '',
'prefix-icon': 'Cellphone',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false,
required: true,
changeTag: true,
regList: [{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}]
})
}
}
export function cleanDrawingDefaultValue() {
drawingDefaultValue.splice(0, drawingDefaultValue.length)
}

359
src/utils/generator/html.js Normal file
View File

@@ -0,0 +1,359 @@
/* eslint-disable max-len */
import { trigger } from './config'
let confGlobal
let someSpanIsNot24
export function dialogWrapper(str) {
return `<el-dialog v-model="dialogVisible" @open="onOpen" @close="onClose" title="Dialog Titile">
${str}
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="handelConfirm">确定</el-button>
</template>
</el-dialog>`
}
export function vueTemplate(str) {
return `<template>
<div class="app-container">
${str}
</div>
</template>`
}
export function vueScript(str) {
return `<script setup>
${str}
</script>`
}
export function cssStyle(cssStr) {
return `<style>
${cssStr}
</style>`
}
function buildFormTemplate(conf, child, type) {
let labelPosition = ''
if (conf.labelPosition !== 'right') {
labelPosition = `label-position="${conf.labelPosition}"`
}
const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : ''
let str = `<el-form ref="${conf.formRef}" :model="${conf.formModel}" :rules="${conf.formRules}" size="${conf.size}" ${disabled} label-width="${conf.labelWidth}px" ${labelPosition}>
${child}
${buildFromBtns(conf, type)}
</el-form>`
if (someSpanIsNot24) {
str = `<el-row :gutter="${conf.gutter}">
${str}
</el-row>`
}
return str
}
function buildFromBtns(conf, type) {
let str = ''
if (conf.formBtns && type === 'file') {
str = `<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>`
if (someSpanIsNot24) {
str = `<el-col :span="24">
${str}
</el-col>`
}
}
return str
}
// span不为24的用el-col包裹
function colWrapper(element, str) {
if (someSpanIsNot24 || element.span !== 24) {
return `<el-col :span="${element.span}">
${str}
</el-col>`
}
return str
}
const layouts = {
colFormItem(element) {
let labelWidth = ''
if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) {
labelWidth = `label-width="${element.labelWidth}px"`
}
const required = !trigger[element.tag] && element.required ? 'required' : ''
const tagDom = tags[element.tag] ? tags[element.tag](element) : null
let str = `<el-form-item ${labelWidth} label="${element.label}" prop="${element.vModel}" ${required}>
${tagDom}
</el-form-item>`
str = colWrapper(element, str)
return str
},
rowFormItem(element) {
const type = element.type === 'default' ? '' : `type="${element.type}"`
const justify = element.type === 'default' ? '' : `justify="${element.justify}"`
const align = element.type === 'default' ? '' : `align="${element.align}"`
const gutter = element.gutter ? `gutter="${element.gutter}"` : ''
const children = element.children.map(el => layouts[el.layout](el))
let str = `<el-row ${type} ${justify} ${align} ${gutter}>
${children.join('\n')}
</el-row>`
str = colWrapper(element, str)
return str
}
}
const tags = {
'el-button': el => {
const {
tag, disabled
} = attrBuilder(el)
const type = el.type ? `type="${el.type}"` : ''
const icon = el.icon ? `icon="${el.icon}"` : ''
const size = el.size ? `size="${el.size}"` : ''
let child = buildElButtonChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}</${el.tag}>`
},
'el-input': el => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
const readonly = el.readonly ? 'readonly' : ''
const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
const showPassword = el['show-password'] ? 'show-password' : ''
const type = el.type ? `type="${el.type}"` : ''
const autosize = el.autosize && el.autosize.minRows
? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
: ''
let child = buildElInputChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${el.tag}>`
},
'el-input-number': el => {
const { disabled, vModel, placeholder } = attrBuilder(el)
const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
const min = el.min ? `:min='${el.min}'` : ''
const max = el.max ? `:max='${el.max}'` : ''
const step = el.step ? `:step='${el.step}'` : ''
const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
const precision = el.precision ? `:precision='${el.precision}'` : ''
return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}></${el.tag}>`
},
'el-select': el => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const filterable = el.filterable ? 'filterable' : ''
const multiple = el.multiple ? 'multiple' : ''
let child = buildElSelectChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}</${el.tag}>`
},
'el-radio-group': el => {
const { disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
let child = buildElRadioGroupChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${size} ${disabled}>${child}</${el.tag}>`
},
'el-checkbox-group': el => {
const { disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
const min = el.min ? `:min="${el.min}"` : ''
const max = el.max ? `:max="${el.max}"` : ''
let child = buildElCheckboxGroupChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}</${el.tag}>`
},
'el-switch': el => {
const { disabled, vModel } = attrBuilder(el)
const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}></${el.tag}>`
},
'el-cascader': el => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const options = el.options ? `:options="${el.vModel}Options"` : ''
const props = el.props ? `:props="${el.vModel}Props"` : ''
const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
const filterable = el.filterable ? 'filterable' : ''
const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}></${el.tag}>`
},
'el-slider': el => {
const { disabled, vModel } = attrBuilder(el)
const min = el.min ? `:min='${el.min}'` : ''
const max = el.max ? `:max='${el.max}'` : ''
const step = el.step ? `:step='${el.step}'` : ''
const range = el.range ? 'range' : ''
const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}></${el.tag}>`
},
'el-time-picker': el => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
const isRange = el['is-range'] ? 'is-range' : ''
const format = el.format ? `format="${el.format}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}></${el.tag}>`
},
'el-date-picker': el => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
const format = el.format ? `format="${el.format}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
const type = el.type === 'date' ? '' : `type="${el.type}"`
const readonly = el.readonly ? 'readonly' : ''
return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}></${el.tag}>`
},
'el-rate': el => {
const { disabled, vModel } = attrBuilder(el)
const max = el.max ? `:max='${el.max}'` : ''
const allowHalf = el['allow-half'] ? 'allow-half' : ''
const showText = el['show-text'] ? 'show-text' : ''
const showScore = el['show-score'] ? 'show-score' : ''
return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}></${el.tag}>`
},
'el-color-picker': el => {
const { disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}></${el.tag}>`
},
'el-upload': el => {
const disabled = el.disabled ? ':disabled=\'true\'' : ''
const action = el.action ? `:action="${el.vModel}Action"` : ''
const multiple = el.multiple ? 'multiple' : ''
const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
const accept = el.accept ? `accept="${el.accept}"` : ''
const name = el.name !== 'file' ? `name="${el.name}"` : ''
const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"`
const fileList = `:file-list="${el.vModel}fileList"`
const ref = `ref="${el.vModel}"`
let child = buildElUploadChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}</${el.tag}>`
}
}
function attrBuilder(el) {
return {
vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`,
clearable: el.clearable ? 'clearable' : '',
placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
disabled: el.disabled ? ':disabled=\'true\'' : ''
}
}
// el-buttin 子级
function buildElButtonChild(conf) {
const children = []
if (conf.default) {
children.push(conf.default)
}
return children.join('\n')
}
// el-input innerHTML
function buildElInputChild(conf) {
const children = []
if (conf.prepend) {
children.push(`<template slot="prepend">${conf.prepend}</template>`)
}
if (conf.append) {
children.push(`<template slot="append">${conf.append}</template>`)
}
return children.join('\n')
}
function buildElSelectChild(conf) {
const children = []
if (conf.options && conf.options.length) {
children.push(`<el-option v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>`)
}
return children.join('\n')
}
function buildElRadioGroupChild(conf) {
const children = []
if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :value="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
}
return children.join('\n')
}
function buildElCheckboxGroupChild(conf) {
const children = []
if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :value="item.label" :disabled="item.disabled" ${border} />`)
}
return children.join('\n')
}
function buildElUploadChild(conf) {
const list = []
if (conf['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${conf.buttonText}</el-button>`)
if (conf.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${conf.fileSize}${conf.sizeUnit}${conf.accept}文件</div>`)
return list.join('\n')
}
export function makeUpHtml(conf, type) {
const htmlList = []
confGlobal = conf
someSpanIsNot24 = conf.fields.some(item => item.span !== 24)
conf.fields.forEach(el => {
htmlList.push(layouts[el.layout](el))
})
const htmlStr = htmlList.join('\n')
let temp = buildFormTemplate(conf, htmlStr, type)
if (type === 'dialog') {
temp = dialogWrapper(temp)
}
confGlobal = null
return temp
}

View File

@@ -0,0 +1 @@
["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"]

370
src/utils/generator/js.js Normal file
View File

@@ -0,0 +1,370 @@
import { titleCase } from '@/utils/index'
import { trigger } from './config'
// 文件大小设置
const units = {
KB: '1024',
MB: '1024 / 1024',
GB: '1024 / 1024 / 1024',
}
/**
* @name: 生成js需要的数据
* @description: 生成js需要的数据
* @param {*} conf
* @param {*} type 弹窗或表单
* @return {*}
*/
export function makeUpJs(conf, type) {
conf = JSON.parse(JSON.stringify(conf))
const dataList = []
const ruleList = []
const optionsList = []
const propsList = []
const methodList = []
const uploadVarList = []
conf.fields.forEach((el) => {
buildAttributes(
el,
dataList,
ruleList,
optionsList,
methodList,
propsList,
uploadVarList
)
})
const script = buildexport(
conf,
type,
dataList.join('\n'),
ruleList.join('\n'),
optionsList.join('\n'),
uploadVarList.join('\n'),
propsList.join('\n'),
methodList.join('\n')
)
return script
}
/**
* @name: 生成参数
* @description: 生成参数,包括表单数据表单验证数据,多选选项数据,上传数据等
* @return {*}
*/
function buildAttributes(
el,
dataList,
ruleList,
optionsList,
methodList,
propsList,
uploadVarList
){
buildData(el, dataList)
buildRules(el, ruleList)
if (el.options && el.options.length) {
buildOptions(el, optionsList)
if (el.dataType === 'dynamic') {
const model = `${el.vModel}Options`
const options = titleCase(model)
buildOptionMethod(`get${options}`, model, methodList)
}
}
if (el.props && el.props.props) {
buildProps(el, propsList)
}
if (el.action && el.tag === 'el-upload') {
uploadVarList.push(
`
// 上传请求路径
const ${el.vModel}Action = ref('${el.action}')
// 上传文件列表
const ${el.vModel}fileList = ref([])`
)
methodList.push(buildBeforeUpload(el))
if (!el['auto-upload']) {
methodList.push(buildSubmitUpload(el))
}
}
if (el.children) {
el.children.forEach((el2) => {
buildAttributes(
el2,
dataList,
ruleList,
optionsList,
methodList,
propsList,
uploadVarList
)
})
}
}
/**
* @name: 生成表单数据formData
* @description: 生成表单数据formData
* @param {*} conf
* @param {*} dataList 数据列表
* @return {*}
*/
function buildData(conf, dataList) {
if (conf.vModel === undefined) return
let defaultValue
if (typeof conf.defaultValue === 'string' && !conf.multiple) {
defaultValue = `'${conf.defaultValue}'`
} else {
defaultValue = `${JSON.stringify(conf.defaultValue)}`
}
dataList.push(`${conf.vModel}: ${defaultValue},`)
}
/**
* @name: 生成表单验证数据rule
* @description: 生成表单验证数据rule
* @param {*} conf
* @param {*} ruleList 验证数据列表
* @return {*}
*/
function buildRules(conf, ruleList) {
if (conf.vModel === undefined) return
const rules = []
if (trigger[conf.tag]) {
if (conf.required) {
const type = Array.isArray(conf.defaultValue) ? "type: 'array'," : ''
let message = Array.isArray(conf.defaultValue)
? `请至少选择一个${conf.vModel}`
: conf.placeholder
if (message === undefined) message = `${conf.label}不能为空`
rules.push(
`{ required: true, ${type} message: '${message}', trigger: '${
trigger[conf.tag]
}' }`
)
}
if (conf.regList && Array.isArray(conf.regList)) {
conf.regList.forEach((item) => {
if (item.pattern) {
rules.push(
`{ pattern: new RegExp(${item.pattern}), message: '${
item.message
}', trigger: '${trigger[conf.tag]}' }`
)
}
})
}
ruleList.push(`${conf.vModel}: [${rules.join(',')}],`)
}
}
/**
* @name: 生成选项数据
* @description: 生成选项数据,单选多选下拉等
* @param {*} conf
* @param {*} optionsList 选项数据列表
* @return {*}
*/
function buildOptions(conf, optionsList) {
if (conf.vModel === undefined) return
if (conf.dataType === 'dynamic') {
conf.options = []
}
const str = `const ${conf.vModel}Options = ref(${JSON.stringify(conf.options)})`
optionsList.push(str)
}
/**
* @name: 生成方法
* @description: 生成方法
* @param {*} methodName 方法名
* @param {*} model
* @param {*} methodList 方法列表
* @return {*}
*/
function buildOptionMethod(methodName, model, methodList) {
const str = `function ${methodName}() {
// TODO 发起请求获取数据
${model}.value
}`
methodList.push(str)
}
/**
* @name: 生成表单组件需要的props设置
* @description: 生成表单组件需要的props设置级联组件
* @param {*} conf
* @param {*} propsList
* @return {*}
*/
function buildProps(conf, propsList) {
if (conf.dataType === 'dynamic') {
conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey)
conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey)
conf.childrenKey !== 'children' &&
(conf.props.props.children = conf.childrenKey)
}
const str = `
// props设置
const ${conf.vModel}Props = ref(${JSON.stringify(conf.props.props)})`
propsList.push(str)
}
/**
* @name: 生成上传组件的相关内容
* @description: 生成上传组件的相关内容
* @param {*} conf
* @return {*}
*/
function buildBeforeUpload(conf) {
const unitNum = units[conf.sizeUnit]
let rightSizeCode = ''
let acceptCode = ''
const returnList = []
if (conf.fileSize) {
rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize}
if(!isRightSize){
proxy.$modal.msgError('文件大小超过 ${conf.fileSize}${conf.sizeUnit}')
}`
returnList.push('isRightSize')
}
if (conf.accept) {
acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type)
if(!isAccept){
proxy.$modal.msgError('应该选择${conf.accept}类型的文件')
}`
returnList.push('isAccept')
}
const str = `
/**
* @name: 上传之前的文件判断
* @description: 上传之前的文件判断,判断文件大小文件类型等
* @param {*} file
* @return {*}
*/
function ${conf.vModel}BeforeUpload(file) {
${rightSizeCode}
${acceptCode}
return ${returnList.join('&&')}
}`
return returnList.length ? str : ''
}
/**
* @name: 生成提交表单方法
* @description: 生成提交表单方法
* @param {Object} conf vModel 表单ref
* @return {*}
*/
function buildSubmitUpload(conf) {
const str = `function submitUpload() {
this.$refs['${conf.vModel}'].submit()
}`
return str
}
/**
* @name: 组装js代码
* @description: 组装js代码方法
* @return {*}
*/
function buildexport(
conf,
type,
data,
rules,
selectOptions,
uploadVar,
props,
methods
) {
let str = `
const { proxy } = getCurrentInstance()
const ${conf.formRef} = ref()
const data = reactive({
${conf.formModel}: {
${data}
},
${conf.formRules}: {
${rules}
}
})
const {${conf.formModel}, ${conf.formRules}} = toRefs(data)
${selectOptions}
${uploadVar}
${props}
${methods}
`
if(type === 'dialog') {
str += `
// 弹窗设置
const dialogVisible = defineModel()
// 弹窗确认回调
const emit = defineEmits(['confirm'])
/**
* @name: 弹窗打开后执行
* @description: 弹窗打开后执行方法
* @return {*}
*/
function onOpen(){
}
/**
* @name: 弹窗关闭时执行
* @description: 弹窗关闭方法,重置表单
* @return {*}
*/
function onClose(){
${conf.formRef}.value.resetFields()
}
/**
* @name: 弹窗取消
* @description: 弹窗取消方法
* @return {*}
*/
function close(){
dialogVisible.value = false
}
/**
* @name: 弹窗表单提交
* @description: 弹窗表单提交方法
* @return {*}
*/
function handelConfirm(){
${conf.formRef}.value.validate((valid) => {
if (!valid) return
// TODO 提交表单
close()
// 回调父级组件
emit('confirm')
})
}
`
} else {
str += `
/**
* @name: 表单提交
* @description: 表单提交方法
* @return {*}
*/
function submitForm() {
${conf.formRef}.value.validate((valid) => {
if (!valid) return
// TODO 提交表单
})
}
/**
* @name: 表单重置
* @description: 表单重置方法
* @return {*}
*/
function resetForm() {
${conf.formRef}.value.resetFields()
}
`
}
return str
}

View File

@@ -0,0 +1,156 @@
import { defineComponent, h } from 'vue'
import { makeMap } from '@/utils/index'
const isAttr = makeMap(
'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' +
'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
'target,title,type,usemap,value,width,wrap' + 'prefix-icon'
)
const isNotProps = makeMap(
'layout,prepend,regList,tag,document,changeTag,defaultValue'
)
function useVModel(props, emit) {
return {
modelValue: props.defaultValue,
'onUpdate:modelValue': (val) => emit('update:modelValue', val),
}
}
const componentChild = {
'el-button': {
default(h, conf, key) {
return conf[key]
},
},
'el-select': {
options(h, conf, key) {
return conf.options.map(item => h(resolveComponent('el-option'), {
label: item.label,
value: item.value,
}))
}
},
'el-radio-group': {
options(h, conf, key) {
return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), {
label: item.value,
}, () => item.label)) : conf.options.map(item => h(resolveComponent('el-radio'), {
label: item.value,
border: conf.border,
}, () => item.label))
}
},
'el-checkbox-group': {
options(h, conf, key) {
return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), {
label: item.value,
}, () => item.label)) : conf.options.map(item => h(resolveComponent('el-checkbox'), {
label: item.value,
border: conf.border,
}, () => item.label))
}
},
'el-upload': {
'list-type': (h, conf, key) => {
const option = {}
// if (conf.showTip) {
// tip = h('div', {
// class: "el-upload__tip"
// }, () => '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件')
// }
if (conf['list-type'] === 'picture-card') {
return h(resolveComponent('el-icon'), option, () => h(resolveComponent('Plus')))
} else {
// option.size = "small"
option.type = "primary"
option.icon = "Upload"
return h(resolveComponent('el-button'), option, () => conf.buttonText)
}
},
}
}
const componentSlot = {
'el-upload': {
'tip': (h, conf, key) => {
if (conf.showTip) {
return () => h('div', {
class: "el-upload__tip"
}, '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件')
}
},
}
}
export default defineComponent({
// 使用 render 函数
render() {
const dataObject = {
attrs: {},
props: {},
on: {},
style: {}
}
const confClone = JSON.parse(JSON.stringify(this.conf))
const children = []
const slot = {}
const childObjs = componentChild[confClone.tag]
if (childObjs) {
Object.keys(childObjs).forEach(key => {
const childFunc = childObjs[key]
if (confClone[key]) {
children.push(childFunc(h, confClone, key))
}
})
}
const slotObjs = componentSlot[confClone.tag]
if (slotObjs) {
Object.keys(slotObjs).forEach(key => {
const childFunc = slotObjs[key]
if (confClone[key]) {
slot[key] = childFunc(h, confClone, key)
}
})
}
Object.keys(confClone).forEach(key => {
const val = confClone[key]
if (dataObject[key]) {
dataObject[key] = val
} else if (isAttr(key)) {
dataObject.attrs[key] = val
} else if (!isNotProps(key)) {
dataObject.props[key] = val
}
})
if(children.length > 0){
slot.default = () => children
}
return h(resolveComponent(this.conf.tag),
{
modelValue: this.$attrs.modelValue,
...dataObject.props,
...dataObject.attrs,
style: {
...dataObject.style
},
}
, slot ?? null)
},
props: {
conf: {
type: Object,
required: true,
},
}
})

View File

@@ -1,10 +1,10 @@
import { parseTime } from '@/ruoyi'
import { parseTime } from './ruoyi'
/**
* 表格时间格式化
*/
export function formatDate(cellValue) {
if (cellValue == null || cellValue == "") return "";
if (cellValue == null || cellValue == "") return ""
var date = new Date(cellValue)
var year = date.getFullYear()
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1

View File

@@ -9,7 +9,7 @@ export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = useUserStore().permissions
const permissionDatas = value
const all_permission = "*:*:*";
const all_permission = "*:*:*"
const hasPermission = permissions.some(permission => {
return all_permission === permission || permissionDatas.includes(permission)
@@ -34,7 +34,7 @@ export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = useUserStore().roles
const permissionRoles = value
const super_admin = "admin";
const super_admin = "admin"
const hasRole = roles.some(role => {
return super_admin === role || permissionRoles.includes(role)

View File

@@ -7,9 +7,9 @@ import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/store/modules/user'
let downloadLoadingInstance;
let downloadLoadingInstance
// 是否显示重新登录
export let isRelogin = { show: false };
export let isRelogin = { show: false }
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
@@ -31,10 +31,10 @@ service.interceptors.request.use(config => {
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.params = {}
config.url = url
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
@@ -42,16 +42,22 @@ service.interceptors.request.use(config => {
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小
const limitSize = 5 * 1024 * 1024 // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制无法进行防重复提交验证。')
return config
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
const s_url = sessionObj.url // 请求地址
const s_data = sessionObj.data // 请求数据
const s_time = sessionObj.time // 请求时间
const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
const message = '数据正在处理,请勿重复提交'
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
@@ -68,41 +74,34 @@ service.interceptors.request.use(config => {
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
const code = res.data.code || 200
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if(res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer'){
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
isRelogin.show = false;
useUserStore().logOut().then(() => {
location.href = '/index';
})
isRelogin.show = true
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false
useUserStore().logOut().then(() => {
location.href = '/index'
})
}).catch(() => {
isRelogin.show = false;
});
isRelogin.show = false
})
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
ElMessage({
message: msg,
type: 'error'
})
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200) {
ElNotification.error({
title: msg
})
ElNotification.error({ title: msg })
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
@@ -110,48 +109,43 @@ service.interceptors.response.use(res => {
},
error => {
console.log('err' + error)
let { message } = error;
let { message } = error
if (message == "Network Error") {
message = "后端接口连接异常";
message = "后端接口连接异常"
} else if (message.includes("timeout")) {
message = "系统接口请求超时"
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常"
}
else if (message.includes("timeout")) {
message = "系统接口请求超时";
}
else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
ElMessage({
message: message,
type: 'error',
duration: 5 * 1000
})
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
// 通用下载方法
export function download(url, params, filename) {
export function download(url, params, filename, config) {
downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob'
responseType: 'blob',
...config
}).then(async (data) => {
const isLogin = await blobValidate(data);
if (isLogin) {
const isBlob = blobValidate(data)
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg);
ElMessage.error(errMsg)
}
downloadLoadingInstance.close();
downloadLoadingInstance.close()
}).catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close();
downloadLoadingInstance.close()
})
}

View File

@@ -1,5 +1,3 @@
/**
* 通用js方法封装处理
* Copyright (c) 2019 ruoyi
@@ -18,7 +16,7 @@ export function parseTime(time, pattern) {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '')
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
@@ -49,86 +47,89 @@ export function parseTime(time, pattern) {
// 表单重置
export function resetForm(refName) {
if (this.$refs[refName]) {
this.$refs[refName].resetFields();
this.$refs[refName].resetFields()
}
}
// 添加日期范围
export function addDateRange(params, dateRange, propName) {
let search = params;
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
dateRange = Array.isArray(dateRange) ? dateRange : [];
let search = params
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}
dateRange = Array.isArray(dateRange) ? dateRange : []
if (typeof (propName) === 'undefined') {
search.params['beginTime'] = dateRange[0];
search.params['endTime'] = dateRange[1];
search.params['beginTime'] = dateRange[0]
search.params['endTime'] = dateRange[1]
} else {
search.params['begin' + propName] = dateRange[0];
search.params['end' + propName] = dateRange[1];
search.params['begin' + propName] = dateRange[0]
search.params['end' + propName] = dateRange[1]
}
return search;
return search
}
// 回显数据字典
export function selectDictLabel(datas, value) {
if (value === undefined) {
return "";
return ""
}
var actions = [];
var actions = []
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + value)) {
actions.push(datas[key].label);
return true;
actions.push(datas[key].label)
return true
}
})
if (actions.length === 0) {
actions.push(value);
actions.push(value)
}
return actions.join('');
return actions.join('')
}
// 回显数据字典(字符串数组)
// 回显数据字典(字符串数组)
export function selectDictLabels(datas, value, separator) {
if (value === undefined) {
return "";
if (value === undefined || value.length ===0) {
return ""
}
var actions = [];
var currentSeparator = undefined === separator ? "," : separator;
var temp = value.split(currentSeparator);
if (Array.isArray(value)) {
value = value.join(",")
}
var actions = []
var currentSeparator = undefined === separator ? "," : separator
var temp = value.split(currentSeparator)
Object.keys(value.split(currentSeparator)).some((val) => {
var match = false;
var match = false
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + temp[val])) {
actions.push(datas[key].label + currentSeparator);
match = true;
actions.push(datas[key].label + currentSeparator)
match = true
}
})
if (!match) {
actions.push(temp[val] + currentSeparator);
actions.push(temp[val] + currentSeparator)
}
})
return actions.join('').substring(0, actions.join('').length - 1);
return actions.join('').substring(0, actions.join('').length - 1)
}
// 字符串格式化(%s )
export function sprintf(str) {
var args = arguments, flag = true, i = 1;
var args = arguments, flag = true, i = 1
str = str.replace(/%s/g, function () {
var arg = args[i++];
var arg = args[i++]
if (typeof arg === 'undefined') {
flag = false;
return '';
flag = false
return ''
}
return arg;
});
return flag ? str : '';
return arg
})
return flag ? str : ''
}
// 转换字符串undefined,null等转化为""
export function parseStrEmpty(str) {
if (!str || str == "undefined" || str == "null") {
return "";
return ""
}
return str;
return str
}
// 数据合并
@@ -136,16 +137,16 @@ export function mergeRecursive(source, target) {
for (var p in target) {
try {
if (target[p].constructor == Object) {
source[p] = mergeRecursive(source[p], target[p]);
source[p] = mergeRecursive(source[p], target[p])
} else {
source[p] = target[p];
source[p] = target[p]
}
} catch (e) {
source[p] = target[p];
source[p] = target[p]
}
}
return source;
};
return source
}
/**
* 构造树型结构数据
@@ -159,43 +160,28 @@ export function handleTree(data, id, parentId, children) {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
};
var childrenListMap = {};
var nodeIds = {};
var tree = [];
}
var childrenListMap = {}
var tree = []
for (let d of data) {
let parentId = d[config.parentId];
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = [];
let id = d[config.id]
childrenListMap[id] = d
if (!d[config.childrenList]) {
d[config.childrenList] = []
}
nodeIds[d[config.id]] = d;
childrenListMap[parentId].push(d);
}
for (let d of data) {
let parentId = d[config.parentId];
if (nodeIds[parentId] == null) {
tree.push(d);
let parentId = d[config.parentId]
let parentObj = childrenListMap[parentId]
if (!parentObj) {
tree.push(d)
} else {
parentObj[config.childrenList].push(d)
}
}
for (let t of tree) {
adaptToChildrenList(t);
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]];
}
if (o[config.childrenList]) {
for (let c of o[config.childrenList]) {
adaptToChildrenList(c);
}
}
}
return tree;
return tree
}
/**
@@ -205,45 +191,38 @@ export function handleTree(data, id, parentId, children) {
export function tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
const value = params[propName]
var part = encodeURIComponent(propName) + "="
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
let params = propName + '[' + key + ']'
var subPart = encodeURIComponent(params) + "="
result += subPart + encodeURIComponent(value[key]) + "&"
}
}
} else {
result += part + encodeURIComponent(value) + "&";
result += part + encodeURIComponent(value) + "&"
}
}
}
return result
}
// 返回项目路径
export function getNormalPath(p) {
if (p.length === 0 || !p || p == 'undefined') {
return p
};
}
let res = p.replace('//', '/')
if (res[res.length - 1] === '/') {
return res.slice(0, res.length - 1)
}
return res;
return res
}
// 验证是否为blob格式
export async function blobValidate(data) {
try {
const text = await data.text();
JSON.parse(text);
return false;
} catch (error) {
return true;
}
}
export function blobValidate(data) {
return data.type !== 'application/json'
}

49
src/utils/theme.js Normal file
View File

@@ -0,0 +1,49 @@
// 处理主题样式
export function handleThemeStyle(theme) {
document.documentElement.style.setProperty('--el-color-primary', theme)
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
}
}
// hex颜色转rgb颜色
export function hexToRgb(str) {
str = str.replace('#', '')
let hexs = str.match(/../g)
for (let i = 0; i < 3; i++) {
hexs[i] = parseInt(hexs[i], 16)
}
return hexs
}
// rgb颜色转Hex颜色
export function rgbToHex(r, g, b) {
let hexs = [r.toString(16), g.toString(16), b.toString(16)]
for (let i = 0; i < 3; i++) {
if (hexs[i].length == 1) {
hexs[i] = `0${hexs[i]}`
}
}
return `#${hexs.join('')}`
}
// 变浅颜色值
export function getLightColor(color, level) {
let rgb = hexToRgb(color)
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
}
return rgbToHex(rgb[0], rgb[1], rgb[2])
}
// 变深颜色值
export function getDarkColor(color, level) {
let rgb = hexToRgb(color)
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor(rgb[i] * (1 - level))
}
return rgbToHex(rgb[0], rgb[1], rgb[2])
}

View File

@@ -1,9 +1,33 @@
/**
* 判断url是否是http或https
* 路径匹配器
* @param {string} pattern
* @param {string} path
* @returns {Boolean}
*/
export function isHttp(url) {
export function isPathMatch(pattern, path) {
const regexPattern = pattern.replace(/\//g, '\\/').replace(/\*\*/g, '.*').replace(/\*/g, '[^\\/]*')
const regex = new RegExp(`^${regexPattern}$`)
return regex.test(path)
}
/**
* 判断value字符串是否为空
* @param {string} value
* @returns {Boolean}
*/
export function isEmpty(value) {
if (value == null || value == "" || value == undefined || value == "undefined") {
return true
}
return false
}
/**
* 判断url是否是http或https
* @param {string} url
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
@@ -12,7 +36,7 @@
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
@@ -75,10 +99,7 @@ export function validEmail(email) {
* @returns {Boolean}
*/
export function isString(str) {
if (typeof str === 'string' || str instanceof String) {
return true
}
return false
return typeof str === 'string' || str instanceof String
}
/**

View File

@@ -26,17 +26,17 @@
</template>
<script setup>
import errImage from "@/assets/401_images/401.gif";
import errImage from "@/assets/401_images/401.gif"
let { proxy } = getCurrentInstance();
let { proxy } = getCurrentInstance()
const errGif = ref(errImage + "?" + +new Date());
const errGif = ref(errImage + "?" + +new Date())
function back() {
if (proxy.$route.query.noGoBack) {
proxy.$router.push({ path: "/" });
proxy.$router.push({ path: "/" })
} else {
proxy.$router.go(-1);
proxy.$router.go(-1)
}
}
</script>

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