Compare commits

...
Author SHA1 Message Date
疯狂的狮子Li cc9b67d467 !72 发布 vue 版本 5.1.2 与 cloud 版本 2.1.2
Merge pull request !72 from 疯狂的狮子Li/temp
2023-12-22 05:26:26 +00:00
疯狂的狮子Li 2c95f86963 🧨发布 vue 版本 5.1.2 与 cloud 版本 2.1.2 2023-12-22 13:20:44 +08:00
疯狂的狮子Li 1272e8fe7d fix 修复 append-to-body 编写错误 2023-12-20 14:44:50 +08:00
NickLuo 58d7e50de3 !63 关闭动态路由tab页签时不清理组件缓存
* 动态路由页面关闭时不清理组件缓存,避免清理已打开的多个路由页面缓存
2023-12-13 00:55:46 +00:00
疯狂的狮子Li c045276a7c update 优化 明确打包命令 2023-12-08 18:04:02 +08:00
棉花 6a7ec81168 fix: 删除重复环境变量ElUploadInstance
Signed-off-by: 棉花 <12559203+radish-hi_0@user.noreply.gitee.com>
2023-12-08 17:55:11 +08:00
疯狂的狮子Li 183e0e9212 !61 fix: 删除重复环境变量ElUploadInstance
Merge pull request !61 from 棉花/N/A
2023-12-08 09:53:55 +00:00
疯狂的狮子Li c020c1affc update 优化 用户头像 img 变量无确定类型问题 2023-12-08 17:01:19 +08:00
疯狂的狮子Li 9673380f60 update 优化 细化oss配置管理权限控制 2023-12-07 14:52:33 +08:00
疯狂的狮子Li 9b28d14bc3 fix 修复 在线用户 强推按钮点击取消控制台警告问题 2023-12-06 00:18:31 +08:00
棉花 01a4dd0b45 fix: 删除重复环境变量ElUploadInstance
Signed-off-by: 棉花 <12559203+radish-hi_0@user.noreply.gitee.com>
2023-12-05 12:57:44 +00:00
疯狂的狮子Li e1023bb331 update 优化 代码中存在的警告 2023-11-30 11:13:29 +08:00
疯狂的狮子Li 5c701801db update 优化 前端白名单页面放行逻辑 2023-11-29 16:02:37 +08:00
疯狂的狮子Li 9b4692b625 !59 响应加密功能
Merge pull request !59 from MichelleChung/dev
2023-11-27 02:59:00 +00:00
疯狂的狮子Li aaec0f7bfe fix 修复 字典使用 default 样式报警告问题 2023-11-27 10:34:42 +08:00
疯狂的狮子Li 25e2e0e018 update 优化 页面关于权限标识符说明 2023-11-23 16:50:31 +08:00
疯狂的狮子Li 5165541780 update 优化 开发环境默认关闭ws 因vite的bug导致如ws无法连接则会崩溃 2023-11-22 16:22:15 +08:00
Michelle.Chung b3c1b95437 add 新增响应解密私钥 ;
add 新增 crypto#decryptBase64 Base64解码方法 ;
update 更新响应拦截器增加响应解密逻辑 ;
2023-11-20 19:27:03 +08:00
疯狂的狮子Li d84fee9319 !58 修复vite环境变量类型原因导致websocket关闭失效问题
Merge pull request !58 from Lau/N/A
2023-11-16 04:24:52 +00:00
Lau d034236ce3 fix: 修改环境变量类型原因导致websocket无法关闭问题
Signed-off-by: Lau <1807121535@qq.com>
2023-11-16 03:25:35 +00:00
疯狂的狮子Li 9e89ab5bae !57 发布 vue 版本 5.1.1 与 cloud 版本2.1.1
Merge pull request !57 from 疯狂的狮子Li/dev
2023-11-14 03:11:07 +00:00
疯狂的狮子Li 6e6062e824 😴发布 vue 版本 5.1.1 与 cloud 版本2.1.1 2023-11-14 10:51:12 +08:00
疯狂的狮子Li 35bebf403c update 适配 websocket在https下的连接方式 2023-11-13 13:02:09 +08:00
疯狂的狮子Li 0a8c33c3cb fix 修复 五级路由缓存无效问题 2023-11-10 15:59:09 +08:00
疯狂的狮子Li 8a7de17b58 fix 修复 内链iframe没有传递参数问题 2023-11-10 11:35:40 +08:00
疯狂的狮子Li f65eb2aee5 !55 绑定第三方帐号参数“wechar”更正为“wechat”
Merge pull request !55 from scmiot/dev
2023-11-07 01:30:47 +00:00
scmiot efb53d4bcb !1 绑定第三方帐号参数“wechar”更正为“wechat”
Merge pull request !1 from scmiot/N/A
2023-11-04 22:51:31 +00:00
scmiot ad2134f07d 绑定第三方帐号参数“wechar”更正为“wechat”
Signed-off-by: scmiot <10322731+scmiot@user.noreply.gitee.com>
2023-11-04 08:46:06 +00:00
疯狂的狮子Li caa9d0fcdc fix 修复 环境变量书写错误 2023-11-03 22:40:18 +08:00
疯狂的狮子Li b829ba4aa0 fix 修复 websocket 初始化无法连接进程死掉问题(vue代理问题) 2023-11-03 18:36:11 +08:00
疯狂的狮子Li a232437abc update 优化 !pr50 bug与代码实现 2023-11-02 14:00:48 +08:00
疯狂的狮子Li f2372d3243 update 优化 !pr50 bug与代码实现 2023-11-02 13:53:44 +08:00
三个三 a8a334b3c3 !50 add 新增 前端接入websocket接收消息
*  add 新增 前端接入websocket接收消息
2023-11-02 04:36:31 +00:00
疯狂的狮子Li 1e3f18ce22 !54 update 优化表格列的显示与隐藏小组件
Merge pull request !54 from bestrevens/dev
2023-11-01 04:09:35 +00:00
疯狂的狮子Li 631f338be2 fix 修复 用户注册缺失 clientid 问题 2023-11-01 10:46:42 +08:00
bestrevens bf977fe00a update 优化表格列的显示与隐藏小组件
优化表格列的显示与隐藏小组件,解决小组件之间距离间隔不一样的问题

Signed-off-by: bestrevens <201840026@qq.com>
2023-10-31 08:54:22 +00:00
疯狂的狮子Li 5bc7044032 update 优化 补全操作日志部门数据 2023-10-27 22:44:05 +08:00
疯狂的狮子Li 6ad0ed5316 update 优化 去掉多余的参数 2023-10-26 17:02:12 +08:00
疯狂的狮子Li ccb2ecc5ee update 更新 表单构建不能使用说明 2023-10-26 17:01:38 +08:00
疯狂的狮子Li ec0cb472df update 优化 去掉多余的参数 2023-10-25 23:05:32 +08:00
疯狂的狮子Li cd0a459c2b update 更新 表单构建不能使用说明 2023-10-25 16:21:21 +08:00
疯狂的狮子Li 18fea86a2c !53 更新登录日志以及在线用户展示信息
Merge pull request !53 from MichelleChung/dev
2023-10-23 12:46:48 +00:00
Michelle.Chung 66bfff450c update 更新 logininfor/index, online/index 字段显示 (客户端, 设备类型) ; 2023-10-23 20:32:38 +08:00
疯狂的狮子Li ec34c759ec !52 fix 优化表格列的显示与隐藏小组件不显示tip和浏览器告警
Merge pull request !52 from bestrevens/ts
2023-10-16 07:36:23 +00:00
bestrevens c1c5ea5e9d fix 优化表格列的显示与隐藏小组件不显示字段和告警
fix 优化表格列的显示与隐藏小组件不显示字段和告警

Signed-off-by: bestrevens <201840026@qq.com>
2023-10-16 07:30:14 +00:00
疯狂的狮子Li 6fe269efc7 fix 修复 !pr47 导致的打包异常 2023-10-16 15:13:27 +08:00
疯狂的狮子Li 920e079727 update 优化 富文本Editor组件检验图片格式 2023-10-09 12:00:15 +08:00
疯狂的狮子Li a2f13185e1 fix 修复 HeaderSearch组件跳转query参数丢失问题 2023-10-09 11:57:22 +08:00
疯狂的狮子Li 078bb50130 update vue-quill 1.1.0 => 1.2.0 2023-10-09 11:47:29 +08:00
疯狂的狮子Li 4854d8df00 update 优化 操作日志列表新增IP地址查询 2023-10-09 11:46:53 +08:00
疯狂的狮子Li ecf062605d update 优化 菜单管理类型为按钮状态可选 2023-10-09 11:41:53 +08:00
疯狂的狮子Li b989b07121 update 优化 用户初始密码从参数配置查询 2023-10-09 11:19:32 +08:00
疯狂的狮子Li bf957f06a0 !51 update 优化 通过参数键名获取键值接口的返回体
Merge pull request !51 from David Wei/update/config_response
2023-10-08 08:56:00 +00:00
David Wei aed8c89ded update 优化 通过参数键名获取键值接口的返回体 2023-10-08 16:05:58 +08:00
疯狂的狮子Li 4085811d24 !48 修复#I8550V
Merge pull request !48 from ahaos/ts
2023-09-28 16:51:40 +00:00
ahao fe384a3020 fix #I8550V https://gitee.com/dromara/RuoYi-Vue-Plus/issues/I8550V 2023-09-29 00:06:57 +08:00
ahaos c6f3cd068e !47 优化表格列的显示与隐藏小组件
Merge pull request !47 from 森爷/ts
2023-09-28 14:15:14 +00:00
DESKTOP-IGCH756\PC 99a6b56233 优化表格列的显示与隐藏小组件 2023-09-28 14:58:34 +08:00
疯狂的狮子Li 8ed32775cb !44 fix 个人信息修改密码接口隐藏新旧密码参数明文
Merge pull request !44 from Bleachtred/ts
2023-09-26 08:06:46 +00:00
bleachtred 395db813b5 fix 个人信息修改密码接口,隐藏新旧密码参数明文 2023-09-25 15:29:32 +08:00
疯狂的狮子Li ab1a7204fa update 优化 页面输入框样式 2023-09-17 15:53:25 +08:00
疯狂的狮子Li 92b2b489a4 fix 修复 自定义字典样式不生效的问题 2023-09-14 17:22:36 +08:00
疯狂的狮子Li 956d2ad495 update 优化 !pr40 代码结构 2023-09-09 11:44:56 +08:00
疯狂的狮子Li cb6cbc4f40 !40 优化字典标签支持数组和多标签
Merge pull request !40 from 抓蛙师/ts
2023-09-09 03:42:40 +00:00
抓蛙师 14538ce399 优化字典标签支持数组和多标签 2023-09-08 18:35:17 +08:00
疯狂的狮子Li 6c16d023f4 fix 修复 login 页面 loading 未关闭问题 2023-09-08 12:28:09 +08:00
55 changed files with 735 additions and 289 deletions
+5
View File
@@ -20,6 +20,11 @@ VITE_APP_PORT = 80
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
# websocket 开关(开发环境默认关闭ws 因vite的bug导致如ws无法连接则会崩溃)
VITE_APP_WEBSOCKET = false
+5
View File
@@ -23,6 +23,11 @@ VITE_APP_PORT = 80
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
# websocket 开关
VITE_APP_WEBSOCKET = true
+3 -1
View File
@@ -17,7 +17,9 @@ npm install --registry=https://registry.npmmirror.com
# 启动服务 # 启动服务
npm run dev npm run dev
# 构建生产环境 yarn build:prod # 构建生产环境
npm run build:prod
# 前端访问地址 http://localhost:80 # 前端访问地址 http://localhost:80
``` ```
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "ruoyi-vue-plus", "name": "ruoyi-vue-plus",
"version": "5.1.0", "version": "5.1.2",
"description": "RuoYi-Vue-Plus多租户管理系统", "description": "RuoYi-Vue-Plus多租户管理系统",
"author": "LionLi", "author": "LionLi",
"license": "MIT", "license": "MIT",
@@ -18,7 +18,7 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "2.1.0", "@element-plus/icons-vue": "2.1.0",
"@vueup/vue-quill": "1.1.0", "@vueup/vue-quill": "1.2.0",
"@vueuse/core": "9.5.0", "@vueuse/core": "9.5.0",
"animate.css": "4.1.1", "animate.css": "4.1.1",
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
+6 -1
View File
@@ -29,6 +29,11 @@ export function login(data: LoginData): AxiosPromise<LoginResult> {
// 注册方法 // 注册方法
export function register(data: any) { export function register(data: any) {
const params = {
...data,
clientId: clientId,
grantType: 'password'
};
return request({ return request({
url: '/auth/register', url: '/auth/register',
headers: { headers: {
@@ -36,7 +41,7 @@ export function register(data: any) {
isEncrypt: true isEncrypt: true
}, },
method: 'post', method: 'post',
data: data data: params
}); });
} }
+1
View File
@@ -1,4 +1,5 @@
export interface OperLogQuery extends PageQuery { export interface OperLogQuery extends PageQuery {
operIp: string;
title: string; title: string;
operName: string; operName: string;
businessType: string; businessType: string;
+1 -1
View File
@@ -20,7 +20,7 @@ export function getConfig(configId: string | number): AxiosPromise<ConfigVO> {
} }
// 根据参数键名查询参数值 // 根据参数键名查询参数值
export function getConfigKey(configKey: string): AxiosPromise<ConfigVO> { export function getConfigKey(configKey: string): AxiosPromise<String> {
return request({ return request({
url: '/system/config/configKey/' + configKey, url: '/system/config/configKey/' + configKey,
method: 'get' method: 'get'
+1 -1
View File
@@ -136,7 +136,7 @@ export const updateUserPwd = (oldPassword: string, newPassword: string) => {
headers: { headers: {
isEncrypt: true isEncrypt: true
}, },
params: data data: data
}); });
}; };
-19
View File
@@ -227,23 +227,4 @@
margin-right: 16px; margin-right: 16px;
} }
} }
// the scroll bar appears when the sub-menu is too long
> .el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
} }
+16 -28
View File
@@ -2,7 +2,8 @@
<div> <div>
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="values.includes(item.value)"> <template v-if="values.includes(item.value)">
<span v-if="item.elTagType == 'default' || item.elTagType == ''" :key="item.value" :index="index" :class="item.elTagClass"> <span v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
:key="item.value" :index="index" :class="item.elTagClass">
{{ item.label + " " }} {{ item.label + " " }}
</span> </span>
<el-tag <el-tag
@@ -10,7 +11,7 @@
:disable-transitions="true" :disable-transitions="true"
:key="item.value + ''" :key="item.value + ''"
:index="index" :index="index"
:type="item.elTagType === 'primary' ? '' : item.elTagType" :type="(item.elTagType === 'primary' || item.elTagType === 'default')? '' : item.elTagType"
:class="item.elTagClass" :class="item.elTagClass"
> >
{{ item.label + " " }} {{ item.label + " " }}
@@ -37,46 +38,33 @@ const props = defineProps({
value: [Number, String, Array] as PropType<number | string | Array<number | string>>, value: [Number, String, Array] as PropType<number | string | Array<number | string>>,
// 当未找到匹配的数据时,显示value // 当未找到匹配的数据时,显示value
showValue: propTypes.bool.def(true), showValue: propTypes.bool.def(true),
separator: propTypes.string.def(","),
}); });
const values = computed(() => { const values = computed(() => {
if (props.value !== null && typeof props.value !== "undefined") { if (props.value === '' || props.value === null || typeof props.value === "undefined") return []
return Array.isArray(props.value) ? props.value : [String(props.value)]; return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
} else {
return [];
}
}); });
const unmatch = computed(() => { const unmatch = computed(() => {
if (props.value !== null && typeof props.value !== "undefined") { if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === "undefined") return false
// 传入值为非数组 // 传入值为非数组
if (!Array.isArray(props.value)) { values.value.forEach(item => {
if (props.options.some((v) => v.value == props.value)) { if (!props.options.some(v => v.value === item)) {
return false; return true // 如果有未匹配项,将标志设置为true
} }
return true; })
} return false // 返回标志的值
return true;
}
// 没有value不显示
return false;
}); });
const unmatchArray = computed(() => { const unmatchArray = computed(() => {
// 记录未匹配的项 // 记录未匹配的项
const itemUnmatchArray: Array<string | number> = []; const itemUnmatchArray: Array<string | number> = [];
if (props.value !== null && typeof props.value !== "undefined") { if (props.value !== '' && props.value !== null && typeof props.value !== "undefined") {
// 传入值为非数组 values.value.forEach(item => {
if (!Array.isArray(props.value)) { if (!props.options.some(v => v.value === item)) {
itemUnmatchArray.push(props.value);
} else {
// 传入值为Array
props.value.forEach((item) => {
if (!props.options.some((v) => v.value == item)) {
itemUnmatchArray.push(item); itemUnmatchArray.push(item);
} }
}); })
}
} }
// 没有value不显示 // 没有value不显示
return handleArray(itemUnmatchArray); return handleArray(itemUnmatchArray);
+16 -24
View File
@@ -9,13 +9,13 @@
name="file" name="file"
:show-file-list="false" :show-file-list="false"
:headers="upload.headers" :headers="upload.headers"
style="display: none" ref="uploadRef"
v-if="type === 'url'" v-if="type === 'url'"
> >
</el-upload> </el-upload>
<div class="editor"> <div class="editor">
<quill-editor <quill-editor
ref="myQuillEditor" ref="quillEditorRef"
v-model:content="content" v-model:content="content"
contentType="html" contentType="html"
@textChange="(e: any) => $emit('update:modelValue', content)" @textChange="(e: any) => $emit('update:modelValue', content)"
@@ -53,7 +53,7 @@ const upload = reactive<UploadOption>({
headers: globalHeaders(), headers: globalHeaders(),
url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload' url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
}) })
const myQuillEditor = ref(); const quillEditorRef = ref();
const options = ref({ const options = ref({
theme: "snow", theme: "snow",
@@ -86,7 +86,7 @@ const options = ref({
}, },
} }
}, },
placeholder: '请输入内容', placeholder: "请输入内容",
readOnly: props.readOnly, readOnly: props.readOnly,
}); });
@@ -110,10 +110,10 @@ watch(() => props.modelValue, (v) => {
// 图片上传成功返回图片地址 // 图片上传成功返回图片地址
const handleUploadSuccess = (res: any) => { const handleUploadSuccess = (res: any) => {
// 获取富文本实例
let quill = toRaw(myQuillEditor.value).getQuill();
// 如果上传成功 // 如果上传成功
if (res.code === 200) { if (res.code === 200) {
// 获取富文本实例
let quill = toRaw(quillEditorRef.value).getQuill();
// 获取光标位置 // 获取光标位置
let length = quill.selection.savedRange.index; let length = quill.selection.savedRange.index;
// 插入图片,res为服务器返回的图片链接地址 // 插入图片,res为服务器返回的图片链接地址
@@ -129,6 +129,13 @@ const handleUploadSuccess = (res: any) => {
// 图片上传前拦截 // 图片上传前拦截
const handleBeforeUpload = (file: any) => { const handleBeforeUpload = (file: any) => {
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) { if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize; const isLt = file.size / 1024 / 1024 < props.fileSize;
@@ -149,95 +156,80 @@ const handleUploadError = (err: any) => {
</script> </script>
<style> <style>
.editor-img-uploader {
display: none;
}
.editor, .editor,
.ql-toolbar { .ql-toolbar {
white-space: pre-wrap !important; white-space: pre-wrap !important;
line-height: normal !important; line-height: normal !important;
} }
.quill-img { .quill-img {
display: none; display: none;
} }
.ql-snow .ql-tooltip[data-mode="link"]::before { .ql-snow .ql-tooltip[data-mode="link"]::before {
content: "请输入链接地址:"; content: "请输入链接地址:";
} }
.ql-snow .ql-tooltip.ql-editing a.ql-action::after { .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0; border-right: 0;
content: "保存"; content: "保存";
padding-right: 0; padding-right: 0;
} }
.ql-snow .ql-tooltip[data-mode="video"]::before { .ql-snow .ql-tooltip[data-mode="video"]::before {
content: "请输入视频地址:"; content: "请输入视频地址:";
} }
.ql-snow .ql-picker.ql-size .ql-picker-label::before, .ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before { .ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px"; content: "14px";
} }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before, .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 { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
content: "10px"; content: "10px";
} }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before, .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 { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
content: "18px"; content: "18px";
} }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before, .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 { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
content: "32px"; content: "32px";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label::before, .ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before { .ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "文本"; content: "文本";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, .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 { .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1"; content: "标题1";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, .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 { .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2"; content: "标题2";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, .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 { .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3"; content: "标题3";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, .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 { .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4"; content: "标题4";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, .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 { .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5"; content: "标题5";
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, .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 { .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6"; content: "标题6";
} }
.ql-snow .ql-picker.ql-font .ql-picker-label::before, .ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before { .ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "标准字体"; content: "标准字体";
} }
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before, .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 { .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
content: "衬线字体"; content: "衬线字体";
} }
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before, .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 { .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体"; content: "等宽字体";
+14 -2
View File
@@ -12,7 +12,8 @@
class="header-search-select" class="header-search-select"
@change="change" @change="change"
> >
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" /> <el-option v-for="option in options" :key="option.item.path" :value="option.item"
:label="option.item.title.join(' > ')"/>
</el-select> </el-select>
</div> </div>
</template> </template>
@@ -51,13 +52,18 @@ const close = () => {
} }
const change = (val: any) => { const change = (val: any) => {
const path = val.path; const path = val.path;
const query = val.query;
if (isHttp(path)) { if (isHttp(path)) {
// http(s):// 路径新窗口打开 // http(s):// 路径新窗口打开
const pindex = path.indexOf("http"); const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank"); window.open(path.substr(pindex, path.length), "_blank");
} else {
if (query) {
router.push({ path: path, query: JSON.parse(query) });
} else { } else {
router.push(path) router.push(path)
} }
}
search.value = '' search.value = ''
options.value = [] options.value = []
nextTick(() => { nextTick(() => {
@@ -90,7 +96,8 @@ const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: strin
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 = { const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path, path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle] title: [...prefixTitle],
query: ''
} }
if (r.meta && r.meta.title) { if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title]; data.title = [...data.title, r.meta.title];
@@ -100,6 +107,11 @@ const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: strin
res.push(data); res.push(data);
} }
} }
if (r.query) {
data.query = r.query
}
// recursive child routes // recursive child routes
if (r.children) { if (r.children) {
const tempRoutes = generateRoutes(r.children, data.path, data.title); const tempRoutes = generateRoutes(r.children, data.path, data.title);
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="relative" :style="{ width: width }"> <div class="relative" :style="{ width: width }">
<el-input v-model="modelValue" readonly @click="visible = !visible" placeholder="点击选择图标"> <el-input v-model="modelValue" readonly @click="visible = !visible" placeholder="点击选择图标">
<template #prepend> <template #prepend>
<svg-icon :icon-class="modelValue as string" /> <svg-icon :icon-class="modelValue" />
</template> </template>
</el-input> </el-input>
+32 -22
View File
@@ -7,13 +7,25 @@
<el-tooltip class="item" effect="dark" content="刷新" placement="top"> <el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" /> <el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns"> <el-tooltip class="item" effect="dark" content="显示/隐藏列" placement="top" v-if="columns">
<el-button circle icon="Menu" @click="showColumn()" /> <div class="show-btn">
<el-popover placement="bottom" trigger="click">
<div class="tree-header">显示/隐藏列</div>
<el-tree
ref="columnRef"
:data="columns"
show-checkbox
@check="columnChange"
node-key="key"
:props="{ label: 'label', children: 'children' }"
></el-tree>
<template #reference>
<el-button circle icon="Menu" />
</template>
</el-popover>
</div>
</el-tooltip> </el-tooltip>
</el-row> </el-row>
<el-dialog :title="title" v-model="open" append-to-body>
<el-transfer :titles="['显示', '隐藏']" v-model="value" :data="columns" @change="dataChange"></el-transfer>
</el-dialog>
</div> </div>
</template> </template>
@@ -29,15 +41,9 @@ const props = defineProps({
gutter: propTypes.number.def(10), gutter: propTypes.number.def(10),
}) })
const columnRef = ref<ElTreeInstance>();
const emits = defineEmits(['update:showSearch', 'queryTable']); const emits = defineEmits(['update:showSearch', 'queryTable']);
// 显隐数据
const value = ref<Array<number>>([]);
// 弹出层标题
const title = ref("显示/隐藏");
// 是否显示弹出层
const open = ref(false);
const style = computed(() => { const style = computed(() => {
const ret: any = {}; const ret: any = {};
if (props.gutter) { if (props.gutter) {
@@ -56,23 +62,19 @@ function refresh() {
emits("queryTable"); emits("queryTable");
} }
// 右侧列表元素变化 // 更改数据列的显示和隐藏
function dataChange(data: TransferKey[]) { function columnChange(...args: any[]) {
props.columns?.forEach((item) => { props.columns?.forEach((item) => {
item.visible = !data.includes(item.key); item.visible = args[1].checkedKeys.includes(item.key);
}) })
} }
// 打开显隐列dialog
const showColumn = () => {
open.value = true;
}
// 显隐列初始默认隐藏列 // 显隐列初始默认隐藏列
onMounted(() => { onMounted(() => {
props.columns?.forEach((item) => { props.columns?.forEach((item) => {
if (!item.visible) { if (item.visible) {
value.value.push(item.key); columnRef.value?.setChecked(item.key, true, false);
// value.value.push(item.key);
} }
}) })
}) })
@@ -91,4 +93,12 @@ onMounted(() => {
.my-el-transfer { .my-el-transfer {
text-align: center; text-align: center;
} }
.tree-header{
width: 100%;
line-height: 24px;
text-align: center;
}
.show-btn {
margin-left: 12px;
}
</style> </style>
+1
View File
@@ -18,6 +18,7 @@ export default {
language: 'Language', language: 'Language',
dashboard: 'Dashboard', dashboard: 'Dashboard',
document: 'Document', document: 'Document',
message: 'Message',
layoutSize: 'Layout Size', layoutSize: 'Layout Size',
selectTenant: 'Select Tenant', selectTenant: 'Select Tenant',
layoutSetting: 'Layout Setting', layoutSetting: 'Layout Setting',
+1
View File
@@ -17,6 +17,7 @@ export default {
language: '语言', language: '语言',
dashboard: '首页', dashboard: '首页',
document: '项目文档', document: '项目文档',
message: '消息',
layoutSize: '布局大小', layoutSize: '布局大小',
selectTenant: '选择租户', selectTenant: '选择租户',
layoutSetting: '布局设置', layoutSetting: '布局设置',
+10 -2
View File
@@ -5,7 +5,7 @@
:key="item.path" :key="item.path"
:iframeId="'iframe' + index" :iframeId="'iframe' + index"
v-show="route.path === item.path" v-show="route.path === item.path"
:src="item.meta ? item.meta.link : ''" :src="iframeUrl(item.meta ? item.meta.link : '', item.query)"
></inner-link> ></inner-link>
</transition-group> </transition-group>
</template> </template>
@@ -15,5 +15,13 @@ import InnerLink from "../InnerLink/index.vue";
import useTagsViewStore from '@/store/modules/tagsView'; import useTagsViewStore from '@/store/modules/tagsView';
const route = useRoute(); const route = useRoute();
const tagsViewStore = useTagsViewStore() const tagsViewStore = useTagsViewStore();
function iframeUrl(url: string, query: any) {
if (Object.keys(query).length > 0) {
let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
return url + "?" + params;
}
return url;
}
</script> </script>
+28
View File
@@ -27,6 +27,21 @@
<svg-icon class-name="search-icon" icon-class="search" /> <svg-icon class-name="search-icon" icon-class="search" />
</div> </div>
</el-tooltip> </el-tooltip>
<!-- 消息 -->
<el-tooltip :content="$t('navbar.message')" effect="dark" placement="bottom">
<div>
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
<svg-icon icon-class="message" />
</el-badge>
</template>
<template #default>
<notice></notice>
</template>
</el-popover>
</div>
</el-tooltip>
<el-tooltip content="Github" effect="dark" placement="bottom"> <el-tooltip content="Github" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
@@ -81,10 +96,14 @@ import { getTenantList } from "@/api/login";
import { dynamicClear, dynamicTenant } from "@/api/system/tenant"; import { dynamicClear, dynamicTenant } from "@/api/system/tenant";
import { ComponentInternalInstance } from "vue"; import { ComponentInternalInstance } from "vue";
import { TenantVO } from "@/api/types"; import { TenantVO } from "@/api/types";
import notice from './notice/index.vue';
import useNoticeStore from '@/store/modules/notice';
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore(); const userStore = useUserStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const noticeStore = storeToRefs(useNoticeStore());
const newNotice = ref(<number>0);
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -161,6 +180,11 @@ const handleCommand = (command: string) => {
commandMap[command](); commandMap[command]();
} }
} }
//用深度监听 消息
watch(() => noticeStore.state.value.notices, (newVal, oldVal) => {
newNotice.value = newVal.filter((item: any) => !item.read).length;
}, { deep: true });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -169,6 +193,10 @@ const handleCommand = (command: string) => {
height:30px; height:30px;
} }
:deep(.el-badge__content.is-fixed){
top: 12px;
}
.flex { .flex {
display: flex; display: flex;
} }
@@ -1,6 +1,6 @@
<template> <template>
<div v-if="!item.hidden"> <div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow"> <template v-if="hasOneShowingChild(item, item.children) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" /> <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
@@ -18,10 +18,10 @@
</template> </template>
<sidebar-item <sidebar-item
v-for="child in item.children" v-for="(child, index) in item.children"
:key="child.path" :key="child.path + index"
:is-nest="true" :is-nest="true"
:item="child as RouteOption" :item="child"
:base-path="resolvePath(child.path)" :base-path="resolvePath(child.path)"
class="nest-menu" class="nest-menu"
/> />
@@ -34,7 +34,6 @@ import { isExternal } from '@/utils/validate'
import AppLink from './Link.vue' import AppLink from './Link.vue'
import { getNormalPath } from '@/utils/ruoyi' import { getNormalPath } from '@/utils/ruoyi'
import { RouteOption } from "vue-router"; import { RouteOption } from "vue-router";
import { PropType } from "vue";
const props = defineProps({ const props = defineProps({
// route object // route object
@@ -54,7 +53,7 @@ const props = defineProps({
const onlyOneChild = ref<any>({}); const onlyOneChild = ref<any>({});
const hasOneShowingChild = (children:RouteOption[] = [], parent: RouteOption) => { const hasOneShowingChild = (parent: RouteOption, children?:RouteOption[]) => {
if (!children) { if (!children) {
children = []; children = [];
} }
@@ -76,9 +75,13 @@ const hasOneShowingChild = (children:RouteOption[] = [], parent: RouteOption) =>
// Show parent if there are no child router to display // Show parent if there are no child router to display
if (showingChildren.length === 0) { if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
if (parent.name === '2222') {
console.log(onlyOneChild.value)
}
return true return true
} }
return false return false
}; };
+3 -2
View File
@@ -4,7 +4,7 @@
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper"> <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<transition :enter-active-class="proxy?.animate.menuSearchAnimate.enter" mode="out-in"> <transition :enter-active-class="proxy?.animate.menuSearchAnimate.enter" mode="out-in">
<el-menu <el-menu
:default-active="activeMenu as string" :default-active="activeMenu"
:collapse="isCollapse" :collapse="isCollapse"
:background-color="bgColor" :background-color="bgColor"
:text-color="textColor" :text-color="textColor"
@@ -27,6 +27,7 @@ import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app' import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
import { RouteOption } from "vue-router";
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute(); const route = useRoute();
@@ -34,7 +35,7 @@ const appStore = useAppStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters); const sidebarRouters = computed<RouteOption[]>(() => permissionStore.sidebarRouters);
const showLogo = computed(() => settingsStore.sidebarLogo); const showLogo = computed(() => settingsStore.sidebarLogo);
const sideTheme = computed(() => settingsStore.sideTheme); const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme);
+134
View File
@@ -0,0 +1,134 @@
<template>
<div class="layout-navbars-breadcrumb-user-news" v-loading="state.loading">
<div class="head-box">
<div class="head-box-title">通知公告</div>
<div class="head-box-btn" @click="readAll">全部已读</div>
</div>
<div class="content-box" v-loading="state.loading">
<template v-if="newsList.length > 0">
<div class="content-box-item" v-for="(v, k) in newsList" :key="k" @click="onNewsClick(k)">
<div class="item-conten">
<div>{{ v.message }}</div>
<div class="content-box-msg"></div>
<div class="content-box-time">{{ v.time }}</div>
</div>
<!-- 已读/未读 -->
<span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span>
<span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span>
</div>
</template>
<el-empty :description="'消息为空'" v-else></el-empty>
</div>
<div class="foot-box" @click="onGoToGiteeClick" v-if="newsList.length > 0">前往gitee</div>
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { ref } from "vue";
import { storeToRefs } from 'pinia'
import { nextTick, onMounted, reactive } from "vue";
import useNoticeStore from '@/store/modules/notice';
const noticeStore = storeToRefs(useNoticeStore());
const {readAll} = useNoticeStore();
// 定义变量内容
const state = reactive({
loading: false,
});
const newsList =ref([]) as any;
/**
* 初始化数据
* @returns
*/
const getTableData = async () => {
state.loading = true;
newsList.value = noticeStore.state.value.notices;
state.loading = false;
};
//点击消息,写入已读
const onNewsClick = (item: any) => {
newsList.value[item].read = true;
//并且写入pinia
noticeStore.state.value.notices = newsList.value;
};
// 前往通知中心点击
const onGoToGiteeClick = () => {
window.open("https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/");
};
onMounted(() => {
nextTick(() => {
getTableData();
});
});
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb-user-news {
.head-box {
display: flex;
border-bottom: 1px solid var(--el-border-color-lighter);
box-sizing: border-box;
color: var(--el-text-color-primary);
justify-content: space-between;
height: 35px;
align-items: center;
.head-box-btn {
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
.content-box {
height: 300px;
overflow: auto;
font-size: 13px;
.content-box-item {
padding-top: 12px;
display: flex;
&:last-of-type {
padding-bottom: 12px;
}
.content-box-msg {
color: var(--el-text-color-secondary);
margin-top: 5px;
margin-bottom: 5px;
}
.content-box-time {
color: var(--el-text-color-secondary);
}
.item-conten {
width: 100%;
display: flex;
flex-direction: column;
}
}
}
.foot-box {
height: 35px;
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid var(--el-border-color-lighter);
&:hover {
opacity: 1;
}
}
:deep(.el-empty__description p) {
font-size: 13px;
}
}
</style>
+2
View File
@@ -20,6 +20,8 @@ router.beforeEach(async (to, from, next) => {
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }); next({ path: '/' });
NProgress.done(); NProgress.done();
} else if (whiteList.indexOf(to.path) !== -1) {
next()
} else { } else {
if (useUserStore().roles.length === 0) { if (useUserStore().roles.length === 0) {
isRelogin.show = true; isRelogin.show = true;
+1 -1
View File
@@ -139,7 +139,7 @@ export const dynamicRoutes: RouteOption[] = [
path: '/system/oss-config', path: '/system/oss-config',
component: Layout, component: Layout,
hidden: true, hidden: true,
permissions: ['system:oss:list'], permissions: ['system:ossConfig:list'],
children: [ children: [
{ {
path: 'index', path: 'index',
+42
View File
@@ -0,0 +1,42 @@
import { defineStore } from 'pinia';
interface NoticeItem {
title?: string;
read: boolean;
message: any;
time: string;
}
export const useNoticeStore = defineStore('notice', () => {
const state = reactive({
notices: [] as NoticeItem[]
});
const addNotice = (notice: NoticeItem) => {
state.notices.push(notice);
};
const removeNotice = (notice: NoticeItem) => {
state.notices.splice(state.notices.indexOf(notice), 1);
};
//实现全部已读
const readAll = () => {
state.notices.forEach((item) => {
item.read = true;
});
};
const clearNotice = () => {
state.notices = [];
};
return {
state,
addNotice,
removeNotice,
readAll,
clearNotice
};
});
export default useNoticeStore;
+4
View File
@@ -100,6 +100,10 @@ export const usePermissionStore = defineStore('permission', () => {
} }
if (lastRouter) { if (lastRouter) {
el.path = lastRouter.path + '/' + el.path; el.path = lastRouter.path + '/' + el.path;
if (el.children && el.children.length) {
children = children.concat(filterChildren(el.children, el))
return
}
} }
children = children.concat(el); children = children.concat(el);
}); });
+8 -1
View File
@@ -1,4 +1,4 @@
import { TagView } from 'vue-router'; import { TagView, RouteRecordNormalized } from 'vue-router';
export const useTagsViewStore = defineStore('tagsView', () => { export const useTagsViewStore = defineStore('tagsView', () => {
const visitedViews = ref<TagView[]>([]); const visitedViews = ref<TagView[]>([]);
@@ -35,7 +35,9 @@ export const useTagsViewStore = defineStore('tagsView', () => {
const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => { const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
return new Promise((resolve) => { return new Promise((resolve) => {
delVisitedView(view); delVisitedView(view);
if (!isDynamicRoute(view)) {
delCachedView(view); delCachedView(view);
}
resolve({ resolve({
visitedViews: [...visitedViews.value], visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value] cachedViews: [...cachedViews.value]
@@ -177,6 +179,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
} }
}; };
const isDynamicRoute = (view: any): boolean => {
// 检查匹配的路由记录中是否有动态段
return view.matched.some((m: RouteRecordNormalized) => m.path.includes(':'));
};
return { return {
visitedViews, visitedViews,
cachedViews, cachedViews,
+6 -1
View File
@@ -63,6 +63,10 @@ export const useUserStore = defineStore('user', () => {
removeToken(); removeToken();
}; };
const setAvatar = (value: string) => {
avatar.value = value;
};
return { return {
userId, userId,
token, token,
@@ -72,7 +76,8 @@ export const useUserStore = defineStore('user', () => {
permissions, permissions,
login, login,
getInfo, getInfo,
logout logout,
setAvatar
}; };
}); });
-1
View File
@@ -24,7 +24,6 @@ declare global {
declare type ElColorPickerInstance = InstanceType<typeof ep.ElColorPicker>; declare type ElColorPickerInstance = InstanceType<typeof ep.ElColorPicker>;
declare type ElRateInstance = InstanceType<typeof ep.ElRate>; declare type ElRateInstance = InstanceType<typeof ep.ElRate>;
declare type ElSliderInstance = InstanceType<typeof ep.ElSlider>; declare type ElSliderInstance = InstanceType<typeof ep.ElSlider>;
declare type ElUploadInstance = InstanceType<typeof ep.ElUpload>;
declare type ElScrollbarInstance = InstanceType<typeof ep.ElScrollbar>; declare type ElScrollbarInstance = InstanceType<typeof ep.ElScrollbar>;
declare type TransferKey = ep.TransferKey; declare type TransferKey = ep.TransferKey;
+2
View File
@@ -68,7 +68,9 @@ interface ImportMetaEnv {
VITE_APP_POWERJOB_ADMIN: string; VITE_APP_POWERJOB_ADMIN: string;
VITE_APP_ENV: string; VITE_APP_ENV: string;
VITE_APP_RSA_PUBLIC_KEY: string; VITE_APP_RSA_PUBLIC_KEY: string;
VITE_APP_RSA_PRIVATE_KEY: string;
VITE_APP_CLIENT_ID: string; VITE_APP_CLIENT_ID: string;
VITE_APP_WEBSOCKET: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
+1
View File
@@ -13,6 +13,7 @@ declare global {
key: number; key: number;
label: string; label: string;
visible: boolean; visible: boolean;
children?: Array<FieldOption>;
} }
/** /**
+21
View File
@@ -30,6 +30,13 @@ export const encryptBase64 = (str: CryptoJS.lib.WordArray) => {
return CryptoJS.enc.Base64.stringify(str); return CryptoJS.enc.Base64.stringify(str);
}; };
/**
* 解密base64
*/
export const decryptBase64 = (str: string) => {
return CryptoJS.enc.Base64.parse(str);
};
/** /**
* 使用密钥对数据进行加密 * 使用密钥对数据进行加密
* @param message * @param message
@@ -43,3 +50,17 @@ export const encryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray)
}); });
return encrypted.toString(); return encrypted.toString();
}; };
/**
* 使用密钥对数据进行解密
* @param message
* @param aesKey
* @returns {string}
*/
export const decryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray) => {
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
};
+1 -1
View File
@@ -4,7 +4,7 @@ import JSEncrypt from 'jsencrypt';
const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY; const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY;
// 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大 // 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大
const privateKey = '**********'; const privateKey = import.meta.env.VITE_APP_RSA_PRIVATE_KEY;
// 加密 // 加密
export const encrypt = (txt: string) => { export const encrypt = (txt: string) => {
+18 -3
View File
@@ -8,9 +8,10 @@ import { errorCode } from '@/utils/errorCode';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading'; import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { getLanguage } from '@/lang'; import { getLanguage } from '@/lang';
import { encryptBase64, encryptWithAes, generateAesKey } from '@/utils/crypto'; import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto';
import { encrypt } from '@/utils/jsencrypt'; import { encrypt, decrypt } from '@/utils/jsencrypt';
const encryptHeader = 'encrypt-key';
let downloadLoadingInstance: LoadingInstance; let downloadLoadingInstance: LoadingInstance;
// 是否显示重新登录 // 是否显示重新登录
export const isRelogin = { show: false }; export const isRelogin = { show: false };
@@ -78,7 +79,7 @@ service.interceptors.request.use(
if (isEncrypt && (config.method === 'post' || config.method === 'put')) { if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
// 生成一个 AES 密钥 // 生成一个 AES 密钥
const aesKey = generateAesKey(); const aesKey = generateAesKey();
config.headers['encrypt-key'] = encrypt(encryptBase64(aesKey)); config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey); config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
} }
// FormData数据去请求头Content-Type // FormData数据去请求头Content-Type
@@ -96,6 +97,20 @@ service.interceptors.request.use(
// 响应拦截器 // 响应拦截器
service.interceptors.response.use( service.interceptors.response.use(
(res: AxiosResponse) => { (res: AxiosResponse) => {
// 加密后的 AES 秘钥
const keyStr = res.headers[encryptHeader];
// 加密
if (keyStr != null && keyStr != '') {
const data = res.data;
// 请求体 AES 解密
const base64Str = decrypt(keyStr);
// base64 解码 得到请求头的 AES 秘钥
const aesKey = decryptBase64(base64Str.toString());
// aesKey 解码 data
const decryptData = decryptWithAes(data, aesKey);
// 将结果 (得到的是 JSON 字符串) 转为 JSON
res.data = JSON.parse(decryptData);
}
// 未设置状态码则默认成功状态 // 未设置状态码则默认成功状态
const code = res.data.code || HttpStatus.SUCCESS; const code = res.data.code || HttpStatus.SUCCESS;
// 获取错误信息 // 获取错误信息
+141
View File
@@ -0,0 +1,141 @@
/**
* @module initWebSocket 初始化
* @module websocketonopen 连接成功
* @module websocketonerror 连接失败
* @module websocketclose 断开连接
* @module resetHeart 重置心跳
* @module sendSocketHeart 心跳发送
* @module reconnect 重连
* @module sendMsg 发送数据
* @module websocketonmessage 接收数据
* @module test 测试收到消息传递
* @description socket 通信
* @param {any} url socket地址
* @param {any} websocket websocket 实例
* @param {any} heartTime 心跳定时器实例
* @param {number} socketHeart 心跳次数
* @param {number} HeartTimeOut 心跳超时时间
* @param {number} socketError 错误次数
*/
import { getToken } from '@/utils/auth';
import useNoticeStore from '@/store/modules/notice';
import { ElNotification } from "element-plus";
const { addNotice } = useNoticeStore();
let socketUrl: any = ''; // socket地址
let websocket: any = null; // websocket 实例
let heartTime: any = null; // 心跳定时器实例
let socketHeart = 0 as number; // 心跳次数
const HeartTimeOut = 10000; // 心跳超时时间 10000 = 10s
let socketError = 0 as number; // 错误次数
// 初始化socket
export const initWebSocket = (url: any) => {
if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
return;
}
socketUrl = url;
// 初始化 websocket
websocket = new WebSocket(url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID);
websocketonopen();
websocketonmessage();
websocketonerror();
websocketclose();
sendSocketHeart();
return websocket;
};
// socket 连接成功
export const websocketonopen = () => {
websocket.onopen = function () {
console.log('连接 websocket 成功');
resetHeart();
};
};
// socket 连接失败
export const websocketonerror = () => {
websocket.onerror = function (e: any) {
console.log('连接 websocket 失败', e);
};
};
// socket 断开链接
export const websocketclose = () => {
websocket.onclose = function (e: any) {
console.log('断开连接', e);
};
};
// socket 重置心跳
export const resetHeart = () => {
socketHeart = 0;
socketError = 0;
clearInterval(heartTime);
sendSocketHeart();
};
// socket心跳发送
export const sendSocketHeart = () => {
heartTime = setInterval(() => {
// 如果连接正常则发送心跳
if (websocket.readyState == 1) {
// if (socketHeart <= 30) {
websocket.send(
JSON.stringify({
type: 'ping'
})
);
socketHeart = socketHeart + 1;
} else {
// 重连
reconnect();
}
}, HeartTimeOut);
};
// socket重连
export const reconnect = () => {
if (socketError <= 2) {
clearInterval(heartTime);
initWebSocket(socketUrl);
socketError = socketError + 1;
// eslint-disable-next-line prettier/prettier
console.log('socket重连', socketError);
} else {
// eslint-disable-next-line prettier/prettier
console.log('重试次数已用完');
clearInterval(heartTime);
}
};
// socket 发送数据
export const sendMsg = (data: any) => {
websocket.send(data);
};
// socket 接收数据
export const websocketonmessage = () => {
websocket.onmessage = function (e: any) {
if (e.data.indexOf('heartbeat') > 0) {
resetHeart();
}
if (e.data.indexOf('ping') > 0) {
return;
}
addNotice({
message: e.data,
read: false,
time: new Date().toLocaleString()
});
ElNotification({
title: '消息',
message: e.data,
type: 'success',
duration: 3000
})
return e.data;
};
};
+5 -5
View File
@@ -5,19 +5,19 @@
<el-card shadow="hover"> <el-card shadow="hover">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="部门id" prop="deptId"> <el-form-item label="部门id" prop="deptId">
<el-input v-model="queryParams.deptId" placeholder="请输入部门id" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.deptId" placeholder="请输入部门id" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="用户id" prop="userId"> <el-form-item label="用户id" prop="userId">
<el-input v-model="queryParams.userId" placeholder="请输入用户id" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.userId" placeholder="请输入用户id" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="排序号" prop="orderNum"> <el-form-item label="排序号" prop="orderNum">
<el-input v-model="queryParams.orderNum" placeholder="请输入排序号" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.orderNum" placeholder="请输入排序号" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="key键" prop="testKey"> <el-form-item label="key键" prop="testKey">
<el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="值" prop="value"> <el-form-item label="值" prop="value">
<el-input v-model="queryParams.value" placeholder="请输入值" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.value" placeholder="请输入值" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+1 -1
View File
@@ -5,7 +5,7 @@
<el-card shadow="hover"> <el-card shadow="hover">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="树节点名" prop="treeName"> <el-form-item label="树节点名" prop="treeName">
<el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+8 -2
View File
@@ -33,7 +33,7 @@
* 部署方式 Docker 容器编排 一键部署业务集群<br /> * 部署方式 Docker 容器编排 一键部署业务集群<br />
* 国际化 SpringMessage Spring标准国际化方案<br /> * 国际化 SpringMessage Spring标准国际化方案<br />
</p> </p>
<p><b>当前版本:</b> <span>v5.1.0</span></p> <p><b>当前版本:</b> <span>v5.1.2</span></p>
<p> <p>
<el-tag type="danger">&yen;免费开源</el-tag> <el-tag type="danger">&yen;免费开源</el-tag>
</p> </p>
@@ -78,7 +78,7 @@
* 分布式监控 Prometheus、Grafana 全方位性能监控<br /> * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
* 其余与 Vue 版本一致<br /> * 其余与 Vue 版本一致<br />
</p> </p>
<p><b>当前版本:</b> <span>v2.1.0</span></p> <p><b>当前版本:</b> <span>v2.1.2</span></p>
<p> <p>
<el-tag type="danger">&yen;免费开源</el-tag> <el-tag type="danger">&yen;免费开源</el-tag>
</p> </p>
@@ -96,6 +96,12 @@
</template> </template>
<script setup name="Index" lang="ts"> <script setup name="Index" lang="ts">
import { initWebSocket } from '@/utils/websocket';
onMounted(() => {
let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'
initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + "/resource/websocket");
});
const goTarget = (url:string) => { const goTarget = (url:string) => {
window.open(url, '__blank') window.open(url, '__blank')
+1
View File
@@ -125,6 +125,7 @@ const handleLogin = () => {
const [err] = await to(userStore.login(loginForm.value)); const [err] = await to(userStore.login(loginForm.value));
if (!err) { if (!err) {
await router.push({ path: redirect.value || '/' }); await router.push({ path: redirect.value || '/' });
loading.value = false;
} else { } else {
loading.value = false; loading.value = false;
// 重新获取验证码 // 重新获取验证码
+7
View File
@@ -76,6 +76,12 @@
sortable="custom" sortable="custom"
:sort-orders="['descending', 'ascending']" :sort-orders="['descending', 'ascending']"
/> />
<el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
<el-table-column label="设备类型" align="center">
<template #default="scope">
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" /> <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" /> <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
<el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" /> <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
@@ -103,6 +109,7 @@ import { list, delLoginInfo, cleanLoginInfo, unlockLoginInfo } from "@/api/monit
import { LoginInfoQuery, LoginInfoVO } from "@/api/monitor/loginInfo/types"; import { LoginInfoQuery, LoginInfoVO } from "@/api/monitor/loginInfo/types";
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
const { sys_common_status } = toRefs<any>(proxy?.useDict("sys_common_status")); const { sys_common_status } = toRefs<any>(proxy?.useDict("sys_common_status"));
const loginInfoList = ref<LoginInfoVO[]>([]); const loginInfoList = ref<LoginInfoVO[]>([]);
+12 -1
View File
@@ -29,6 +29,12 @@
</el-table-column> </el-table-column>
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" /> <el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" /> <el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
<el-table-column label="设备类型" align="center">
<template #default="scope">
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" /> <el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" /> <el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" /> <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
@@ -57,8 +63,11 @@
<script setup name="Online" lang="ts"> <script setup name="Online" lang="ts">
import { forceLogout, list as initData } from "@/api/monitor/online"; import { forceLogout, list as initData } from "@/api/monitor/online";
import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types"; import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types";
import api from "@/api/system/user";
import {to} from "await-to-js";
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
const onlineList = ref<OnlineVO[]>([]); const onlineList = ref<OnlineVO[]>([]);
const loading = ref(true); const loading = ref(true);
@@ -93,11 +102,13 @@ const resetQuery = () => {
} }
/** 强退按钮操作 */ /** 强退按钮操作 */
const handleForceLogout = async (row: OnlineVO) => { const handleForceLogout = async (row: OnlineVO) => {
await proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?'); const [err] = await to(proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?') as any);
if (!err) {
await forceLogout(row.tokenId); await forceLogout(row.tokenId);
await getList(); await getList();
proxy?.$modal.msgSuccess("删除成功"); proxy?.$modal.msgSuccess("删除成功");
} }
}
onMounted(() => { onMounted(() => {
getList(); getList();
+12 -6
View File
@@ -4,6 +4,9 @@
<div class="mb-[10px]"> <div class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="操作地址" prop="operIp">
<el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable style="width: 240px;" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="系统模块" prop="title"> <el-form-item label="系统模块" prop="title">
<el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" @keyup.enter="handleQuery" /> <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
@@ -83,7 +86,8 @@
sortable="custom" sortable="custom"
:sort-orders="['descending', 'ascending']" :sort-orders="['descending', 'ascending']"
/> />
<el-table-column label="主机" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" /> <el-table-column label="部门" align="center" prop="deptName" width="130" :show-overflow-tooltip="true" />
<el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
<el-table-column label="操作状态" align="center" prop="status"> <el-table-column label="操作状态" align="center" prop="status">
<template #default="scope"> <template #default="scope">
<dict-tag :options="sys_common_status" :value="scope.row.status" /> <dict-tag :options="sys_common_status" :value="scope.row.status" />
@@ -122,13 +126,14 @@
<el-dialog title="操作日志详细" v-model="dialog.visible" width="700px" append-to-body> <el-dialog title="操作日志详细" v-model="dialog.visible" width="700px" append-to-body>
<el-form :model="form" label-width="100px"> <el-form :model="form" label-width="100px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="24">
<el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item> <el-form-item label="登录信息:">{{ form.operName }} / {{form.deptName}} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
<el-form-item label="登录信息:">{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item> <el-form-item label="请求信息:">{{ form.requestMethod }} {{ form.operUrl }}</el-form-item>
<el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item> </el-col>
<el-col :span="12">
<el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="操作方法:">{{ form.method }}</el-form-item> <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
@@ -215,6 +220,7 @@ const data = reactive<PageData<OperLogForm, OperLogQuery>>({
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
operIp: '',
title: '', title: '',
operName: '', operName: '',
businessType: '', businessType: '',
+2 -2
View File
@@ -105,12 +105,12 @@ const registerRules: ElFormRules = {
const codeUrl = ref(""); const codeUrl = ref("");
const loading = ref(false); const loading = ref(false);
const captchaEnabled = ref(true); const captchaEnabled = ref(true);
const registerRef = ref(ElForm); const registerRef = ref<ElFormInstance>();
// 租户列表 // 租户列表
const tenantList = ref<TenantVO[]>([]); const tenantList = ref<TenantVO[]>([]);
const handleRegister = () => { const handleRegister = () => {
registerRef.value.validate(async (valid: boolean) => { registerRef.value?.validate(async (valid: boolean) => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
const [err] = await to(register(registerForm.value)); const [err] = await to(register(registerForm.value));
+3 -7
View File
@@ -4,10 +4,10 @@
<div class="search" v-show="showSearch"> <div class="search" v-show="showSearch">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
<el-form-item label="客户端key" prop="clientKey"> <el-form-item label="客户端key" prop="clientKey">
<el-input v-model="queryParams.clientKey" placeholder="请输入客户端key" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.clientKey" placeholder="请输入客户端key" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="客户端秘钥" prop="clientSecret"> <el-form-item label="客户端秘钥" prop="clientSecret">
<el-input v-model="queryParams.clientSecret" placeholder="请输入客户端秘钥" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.clientSecret" placeholder="请输入客户端秘钥" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable> <el-select v-model="queryParams.status" placeholder="状态" clearable>
@@ -53,11 +53,7 @@
<el-table-column label="客户端秘钥" align="center" prop="clientSecret" /> <el-table-column label="客户端秘钥" align="center" prop="clientSecret" />
<el-table-column label="授权类型" align="center"> <el-table-column label="授权类型" align="center">
<template #default="scope"> <template #default="scope">
<div> <dict-tag :options="sys_grant_type" :value="scope.row.grantTypeList" />
<template v-for="(type, index) in scope.row.grantTypeList" :key="index">
<dict-tag class="el-check-tag" :options="sys_grant_type" :value="type" />
</template>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="设备类型" align="center"> <el-table-column label="设备类型" align="center">
+2 -2
View File
@@ -5,7 +5,7 @@
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
<el-form-item label="部门名称" prop="deptName"> <el-form-item label="部门名称" prop="deptName">
<el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="部门状态" clearable> <el-select v-model="queryParams.status" placeholder="部门状态" clearable>
@@ -70,7 +70,7 @@
</el-table> </el-table>
</el-card> </el-card>
<el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-bod width="600px"> <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-body width="600px">
<el-form ref="deptFormRef" :model="form" :rules="rules" label-width="80px"> <el-form ref="deptFormRef" :model="form" :rules="rules" label-width="80px">
<el-row> <el-row>
<el-col :span="24" v-if="form.parentId !== 0"> <el-col :span="24" v-if="form.parentId !== 0">
+2 -3
View File
@@ -49,9 +49,8 @@
<el-table-column label="字典编码" align="center" prop="dictCode" v-if="false" /> <el-table-column label="字典编码" align="center" prop="dictCode" v-if="false" />
<el-table-column label="字典标签" align="center" prop="dictLabel"> <el-table-column label="字典标签" align="center" prop="dictLabel">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.listClass === '' || scope.row.listClass === 'default'">{{ scope.row.dictLabel }}</span> <span v-if="(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
<el-tag v-else :type="scope.row.listClass === 'primary' ? '' : scope.row.listClass">{{ scope.row.dictLabel <el-tag v-else :type="(scope.row.listClass === 'primary' || scope.row.listClass === 'default') ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字典键值" align="center" prop="dictValue" /> <el-table-column label="字典键值" align="center" prop="dictValue" />
+3 -3
View File
@@ -5,7 +5,7 @@
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
<el-form-item label="菜单名称" prop="menuName"> <el-form-item label="菜单名称" prop="menuName">
<el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable> <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
@@ -169,7 +169,7 @@
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" /> <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<template #label> <template #label>
<span> <span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top"> <el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
<el-icon> <el-icon>
<question-filled /> <question-filled />
</el-icon> </el-icon>
@@ -229,7 +229,7 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" v-if="form.menuType !== 'F'"> <el-col :span="12">
<el-form-item> <el-form-item>
<template #label> <template #label>
<span> <span>
+5 -5
View File
@@ -29,13 +29,13 @@
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:oss:add']">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:ossConfig:add']">新增</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:oss:edit']">修改</el-button> <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:ossConfig:edit']">修改</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:oss:remove']"> <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:ossConfig:remove']">
删除 删除
</el-button> </el-button>
</el-col> </el-col>
@@ -67,10 +67,10 @@
<el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding"> <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
<template #default="scope"> <template #default="scope">
<el-tooltip content="修改" placement="top"> <el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:oss:edit']"></el-button> <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:ossConfig:edit']"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']"></el-button> <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:ossConfig:remove']"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
+1 -1
View File
@@ -194,7 +194,7 @@ const { queryParams, form, rules } = toRefs(data);
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
const res = await proxy?.getConfigKey("sys.oss.previewListResource"); const res = await proxy?.getConfigKey("sys.oss.previewListResource");
previewListResource.value = res?.msg === undefined ? true : res.msg === 'true'; previewListResource.value = res?.data === undefined ? true : res.data === 'true';
const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, "CreateTime")); const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, "CreateTime"));
ossList.value = response.rows; ossList.value = response.rows;
total.value = response.total; total.value = response.total;
+1 -1
View File
@@ -107,7 +107,7 @@
<el-form-item prop="roleKey"> <el-form-item prop="roleKey">
<template #label> <template #label>
<span> <span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasRole('admin')`)" placement="top"> <el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
<el-icon><question-filled /></el-icon> <el-icon><question-filled /></el-icon>
</el-tooltip> </el-tooltip>
权限字符 权限字符
+12 -9
View File
@@ -320,7 +320,7 @@ const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']); const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const deptName = ref(''); const deptName = ref('');
const deptOptions = ref<DeptVO[]>([]); const deptOptions = ref<DeptVO[]>([]);
const initPassword = ref('123456'); const initPassword = ref<String>('');
const postOptions = ref<PostVO[]>([]); const postOptions = ref<PostVO[]>([]);
const roleOptions = ref<RoleVO[]>([]); const roleOptions = ref<RoleVO[]>([]);
/*** 用户导入参数 */ /*** 用户导入参数 */
@@ -340,13 +340,13 @@ const upload = reactive<ImportOption>({
}) })
// 列显隐信息 // 列显隐信息
const columns = ref<FieldOption[]>([ const columns = ref<FieldOption[]>([
{ key: 0, label: `用户编号`, visible: false }, { key: 0, label: `用户编号`, visible: false,children: [] },
{ key: 1, label: `用户名称`, visible: true }, { key: 1, label: `用户名称`, visible: true,children: [] },
{ key: 2, label: `用户昵称`, visible: true }, { key: 2, label: `用户昵称`, visible: true,children: [] },
{ key: 3, label: `部门`, visible: true }, { key: 3, label: `部门`, visible: true,children: [] },
{ key: 4, label: `手机号码`, visible: true }, { key: 4, label: `手机号码`, visible: true,children: [] },
{ key: 5, label: `状态`, visible: true }, { key: 5, label: `状态`, visible: true,children: [] },
{ key: 6, label: `创建时间`, visible: true } { key: 6, label: `创建时间`, visible: true,children: [] }
]) ])
@@ -561,7 +561,7 @@ const handleAdd = async () => {
await initTreeData(); await initTreeData();
postOptions.value = data.posts; postOptions.value = data.posts;
roleOptions.value = data.roles; roleOptions.value = data.roles;
form.value.password = initPassword.value; form.value.password = initPassword.value.toString();
} }
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: UserForm) => { const handleUpdate = async (row?: UserForm) => {
@@ -613,6 +613,9 @@ const resetForm = () => {
onMounted(() => { onMounted(() => {
getTreeSelect() // 初始化部门数据 getTreeSelect() // 初始化部门数据
getList() // 初始化列表数据 getList() // 初始化列表数据
proxy?.getConfigKey("sys.user.initPassword").then(response => {
initPassword.value = response.data;
});
}); });
</script> </script>
+1 -1
View File
@@ -20,7 +20,7 @@
<div id="git-user-binding"> <div id="git-user-binding">
<h4 class="provider-desc">你可以绑定以下第三方帐号</h4> <h4 class="provider-desc">你可以绑定以下第三方帐号</h4>
<div id="authlist" class="user-bind"> <div id="authlist" class="user-bind">
<a class="third-app" href="#" @click="authUrl('wechar');" title="使用 微信 账号授权登录"> <a class="third-app" href="#" @click="authUrl('wechat');" title="使用 微信 账号授权登录">
<div class="git-other-login-icon"> <div class="git-other-login-icon">
<svg-icon icon-class="wechat" /> <svg-icon icon-class="wechat" />
</div> </div>
+3 -3
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="user-info-head" @click="editCropper()"> <div class="user-info-head" @click="editCropper()">
<img :src="options.img as string" title="点击上传头像" class="img-circle img-lg" /> <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
<el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog"> <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row> <el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }"> <el-col :xs="24" :md="12" :style="{ height: '350px' }">
@@ -62,7 +62,7 @@ import { uploadAvatar } from "@/api/system/user";
import useUserStore from "@/store/modules/user"; import useUserStore from "@/store/modules/user";
interface Options { interface Options {
img: string | ArrayBuffer | null; // 裁剪图片的地址 img: string | any; // 裁剪图片的地址
autoCrop: boolean; // 是否默认生成截图框 autoCrop: boolean; // 是否默认生成截图框
autoCropWidth: number; // 默认生成截图框宽度 autoCropWidth: number; // 默认生成截图框宽度
autoCropHeight: number; // 默认生成截图框高度 autoCropHeight: number; // 默认生成截图框高度
@@ -140,7 +140,7 @@ const uploadImg = async () => {
const res = await uploadAvatar(formData); const res = await uploadAvatar(formData);
open.value = false; open.value = false;
options.img = res.data.imgUrl; options.img = res.data.imgUrl;
userStore.avatar = options.img as string userStore.setAvatar(options.img as string)
proxy?.$modal.msgSuccess("修改成功"); proxy?.$modal.msgSuccess("修改成功");
visible.value = false; visible.value = false;
}); });
+1 -1
View File
@@ -1,3 +1,3 @@
<template> <template>
<div>表单构建 <svg-icon icon-class="build" /></div> <div>表单构建(由于此功能的开源组件不支持 VUE3+TS 故暂时无法使用) <svg-icon icon-class="build" /></div>
</template> </template>
+2 -2
View File
@@ -97,8 +97,8 @@
<el-tabs v-model="preview.activeName"> <el-tabs v-model="preview.activeName">
<el-tab-pane <el-tab-pane
v-for="(value, key) in preview.data" v-for="(value, key) in preview.data"
:label="(key as any).substring((key as any).lastIndexOf('/') + 1, (key as any).indexOf('.vm'))" :label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
:name="(key as any).substring((key as any).lastIndexOf('/') + 1, (key as any).indexOf('.vm'))" :name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
:key="value" :key="value"
> >
<el-link :underline="false" icon="DocumentCopy" v-copyText="value" v-copyText:callback="copyTextSuccess" style="float:right"> <el-link :underline="false" icon="DocumentCopy" v-copyText="value" v-copyText:callback="copyTextSuccess" style="float:right">
+1
View File
@@ -28,6 +28,7 @@ export default defineConfig(({ mode, command }: ConfigEnv): UserConfig => {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
target: 'http://localhost:8080', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
} }
} }