Compare commits

...

131 Commits

Author SHA1 Message Date
d7d4c67156 解决vxe-table菜单管理修改数据后折叠的问题 2023-12-08 10:52:01 +08:00
不做码农
c5869e3233 优化代码生成 2023-12-08 08:29:13 +08:00
不做码农
c47cb4a508 差异日志新增对比功能 2023-12-07 21:35:22 +08:00
不做码农
1fe328fbee 代码生成新增差异日志记录配置 2023-12-07 21:34:42 +08:00
不做码农
e964662f7a 💄修改按钮样式 2023-12-01 17:29:51 +08:00
不做码农
7c6e21531b 优化路由参数 2023-11-26 21:23:46 +08:00
不做码农
43485dddfa 优化未登录访问需要登录的资源,在登录后重定向丢失请求参数问题 2023-11-26 12:27:13 +08:00
不做码农
e1879c706c 优化内链iframe没有传递参数 2023-11-26 12:15:52 +08:00
不做码农
fb517dd799 fix通知公告权限异常 2023-11-23 18:27:08 +08:00
不做码农
ad87248ab3 :sparks:DictTag组件新增自定义标签 2023-11-23 07:39:15 +08:00
不做码农
53ca3df03f 优化代码生成编辑 2023-11-22 21:42:04 +08:00
不做码农
833598bed1 优化ui 2023-11-21 17:16:52 +08:00
不做码农
580740ff0a 新增邮件发送日志 2023-11-21 17:16:51 +08:00
不做码农
6d44b481fc :swap:基础数据优化 2023-11-21 08:16:10 +08:00
不做码农
07b165f0c0 :wap:新增邮件发送日志 2023-11-20 21:56:23 +08:00
不做码农
2bd1e39457 :sparks:代码生成新增是否使用雪花id 2023-11-20 20:14:33 +08:00
不做码农
181ba7fa21 新增手机号短信发送&短信发送日志记录管理 2023-11-19 21:18:56 +08:00
不做码农
8151ba9d2f 邮件新增模板管理 2023-11-13 19:59:22 +08:00
不做码农
685597f921 用户管理优化 2023-11-13 19:58:41 +08:00
不做码农
5471a13628 通知公告新增预览 2023-11-13 19:58:05 +08:00
不做码农
833698168b 优化ui 2023-10-25 19:31:23 +08:00
不做码农
405a8ce511 新增全局异常提示 2023-09-29 11:07:56 +08:00
不做码农
b4a37d06c1 💄ui优化 2023-09-29 11:07:31 +08:00
不做码农
6eb2f3da8c 💄vxe-table新增黑色主题 2023-09-29 10:59:46 +08:00
不做码农
12e23a9264 新增tagView持久化配置 2023-09-28 18:11:40 +08:00
不做码农
35cc1579d8 系统通知新增小红点提醒 2023-09-28 14:43:11 +08:00
不做码农
9df1b77aee 更改iconfont 2023-09-28 13:31:13 +08:00
不做码农
4f8ddaf416 💄全屏按钮替换 2023-09-27 21:36:09 +08:00
不做码农
d48866b248 消息新增红点、已读功能 2023-09-27 21:35:46 +08:00
不做码农
c1bfaabcb2 新增聊天功能 2023-09-27 18:26:36 +08:00
不做码农
e6f98d4a2d 优化图片、文件上传组件 2023-09-27 17:47:09 +08:00
不做码农
93555320bb 优化403错误提示 2023-09-27 17:46:22 +08:00
不做码农
db4e67bb73 dev:系统通知优化 2023-09-26 21:57:20 +08:00
不做码农
cd3a90c8f3 💄菜单管理ui优化 2023-09-26 18:29:19 +08:00
不做码农
001a61ce8e 💄优化ui 2023-09-26 18:26:55 +08:00
不做码农
f63d3a5bca 💄角色管理菜单权限ui修改 2023-09-26 18:21:49 +08:00
不做码农
4551427965 移除动画 2023-09-25 18:28:39 +08:00
不做码农
5c26108a4e 优化 2023-09-25 13:54:30 +08:00
不做码农
98c87425af 🐛fix菜单管理图标列显示异常 2023-09-25 13:53:20 +08:00
不做码农
3dba400a80 菜单管理替换成vxe-table 2023-09-24 16:54:00 +08:00
不做码农
9e2a566f67 优化菜单图标选择 2023-09-24 10:44:53 +08:00
不做码农
9dfe258421 优化组件zr-dialog 2023-09-23 17:29:45 +08:00
不做码农
b827c47497 新增自定义组件增加引用提示 2023-09-23 17:26:26 +08:00
不做码农
03839d3d1d 定时任务新增卡片模式显示 2023-09-23 13:58:25 +08:00
不做码农
911d2ec8d0 🔖v20230920 2023-09-20 17:56:41 +08:00
不做码农
1f46b6a95e 优化功能 2023-09-20 17:53:32 +08:00
不做码农
f5da308123 🐛fix导入失败没提示 2023-09-20 17:52:53 +08:00
不做码农
452fb89dc3 💄界面优化 2023-09-15 19:58:58 +08:00
不做码农
1cc6193504 字典组件新增翻译 2023-09-15 19:58:33 +08:00
不做码农
2c922c5fdd :style:优化登录页ui 2023-09-10 08:13:35 +08:00
不做码农
a6fc1168d7 新增其他登录配置 2023-09-08 20:24:08 +08:00
不做码农
1f8ecd5756 💄ui优化 2023-09-08 18:36:16 +08:00
不做码农
8e83c51106 🔥删除无用的传参 2023-09-08 18:35:40 +08:00
不做码农
fc3bcbf225 🐛修改未登录访问需要登录的资源,在登录后重定向丢失请求参数问题 2023-09-08 18:32:27 +08:00
不做码农
05fdc8d4a2 左侧菜单New标记新增可配置 2023-09-08 18:30:00 +08:00
不做码农
740061502e 新增手机号登录ui 2023-09-07 18:42:24 +08:00
不做码农
aef0b436c4 优化开发环境中切换路由会白屏问题 2023-09-07 18:39:10 +08:00
不做码农
cde0d3ab54 💄更新样式 2023-09-07 07:37:02 +08:00
不做码农
e90e8a0904 ⬇️降级vue-i18n包至9.2.2 2023-09-06 13:39:53 +08:00
不做码农
5869d43985 🐛多语言切换行列模式隐藏分页栏 2023-09-06 09:29:22 +08:00
不做码农
bbcbf37eea 💄在线用户新增卡片模式 2023-08-30 18:29:24 +08:00
不做码农
94d7cb492a 💄更新登录页面ui 2023-08-30 18:20:55 +08:00
不做码农
38cf81bcfd 优化强退功能 2023-08-30 08:36:29 +08:00
不做码农
e1a4f0faf8 ✏️拼写错误 2023-08-30 07:12:26 +08:00
不做码农
a481bec282 优化强退功能新增批量强退在线用户 2023-08-29 21:36:11 +08:00
不做码农
f9ee814adc 优化ui、布局 2023-08-29 17:00:26 +08:00
不做码农
673a773741 新增翻译 2023-08-29 17:00:25 +08:00
不做码农
51ef89bcb7 🎨修改iconfont图标 2023-08-29 17:00:24 +08:00
不做码农
05715b40c4 新增自动续期token 2023-08-29 17:00:22 +08:00
不做码农
460594f116 🐛 fix二维码扫码登录异常 2023-08-28 18:35:59 +08:00
不做码农
352161ac96 新增在线时长、单点登录 2023-08-28 18:17:33 +08:00
不做码农
9b7a1e870c ⬆️升级包 2023-08-27 13:35:43 +08:00
不做码农
b2d7a3fc14 🎨优化ui 2023-08-26 17:18:28 +08:00
不做码农
e40da9d7cd 新增移动端扫码登录 2023-08-26 15:53:37 +08:00
不做码农
92e032c948 新增剔出在线用户 2023-08-26 15:53:05 +08:00
不做码农
a59b4c1328 代码生成新增组件 2023-08-26 07:39:49 +08:00
不做码农
761a75ed40 登录过期后跳转到过期前页面 2023-08-23 18:36:19 +08:00
不做码农
e7fd1cfaa6 优化cron表达式组件 2023-08-23 18:35:29 +08:00
不做码农
97b480c143 角色权限分配tree ui修改 2023-08-22 18:22:30 +08:00
不做码农
0797a69b9a 🐛fix dict-tag组件split为null报错 2023-08-22 18:22:30 +08:00
不做码农
beb98ce525 🐛修复控制台警告 2023-08-18 20:14:50 +08:00
不做码农
7a989d3db5 优化侧边栏菜单显示 2023-08-18 12:14:55 +08:00
不做码农
8eb8480265 新增数据库差异日志查询 2023-08-18 11:23:11 +08:00
不做码农
61a842b0cd 侧边栏新增isNew标记 2023-08-17 18:17:57 +08:00
不做码农
b378aca3ab :style:菜单图标选择布局改为grid 2023-08-17 18:17:57 +08:00
不做码农
50156e770d 新增zr-dialog组件可以实现全屏dialog 2023-08-17 18:17:47 +08:00
不做码农
a1845585a6 🎨侧边栏优化 2023-08-15 18:18:57 +08:00
不做码农
9bd6f19581 update tool/gen/index.vue 2023-08-10 19:42:39 +08:00
不做码农
343235bc40 代码生成表单编辑优化 2023-08-07 18:18:43 +08:00
不做码农
f411cda24e 富文本编辑器优化 2023-08-07 18:17:45 +08:00
不做码农
e62a10a6f2 登录新增二维码 2023-08-07 18:16:50 +08:00
不做码农
a5a4a42a96 多语言新增导入、删除 2023-08-02 18:45:36 +08:00
不做码农
c379a8300c 优化ui 2023-08-02 18:44:53 +08:00
不做码农
53d5b044db 🎨update tool/file/index.vue 2023-08-02 08:02:29 +08:00
不做码农
bca5d80cb3 代码生成新增switch组件 2023-08-01 12:12:16 +08:00
不做码农
b57ef56c19 优化代码 2023-08-01 12:12:15 +08:00
不做码农
b6ea3e685c 代码生成新增导入 2023-07-29 11:34:21 +08:00
不做码农
4b101f97d7 🎨优化ui 2023-07-29 11:34:20 +08:00
不做码农
56b8deaea4 🎨优化ui 2023-07-22 10:31:47 +08:00
不做码农
99801bd01d 优化首页 2023-07-21 07:38:28 +08:00
不做码农
a503de8d5d update qrcodeH5.png 2023-07-21 07:29:09 +08:00
不做码农
b2e5ea03f6 新增移动端体验二维码 2023-07-19 11:45:24 +08:00
不做码农
00ec12bac3 优化登录弹框 2023-07-19 11:44:56 +08:00
不做码农
5e19346641 🐛 fix 升级element-ui后翻译文件找不到 2023-07-17 11:50:59 +08:00
不做码农
1fa0fff2cf 网络请求错误提示优化 2023-07-17 11:50:59 +08:00
不做码农
7a9635921c update publish.vue 2023-07-13 19:27:52 +08:00
不做码农
851ba00b0d 🐛 fix分页组件样式异常 2023-07-13 19:27:52 +08:00
不做码农
05b7b4d6db 主子表选择过滤当前表 2023-07-13 19:27:52 +08:00
不做码农
5e2232d7a1 feat:代码生成新增主子表 2023-07-13 19:27:52 +08:00
不做码农
44041620a1 优化复制指令 2023-07-13 19:27:51 +08:00
不做码农
89b6be286d 图片上传组件预览新增复制地址 2023-07-13 19:27:51 +08:00
不做码农
b45d893a60 update Pagination/index.vue 2023-07-13 19:27:51 +08:00
不做码农
b8713729d3 update element-ui.scss 2023-07-13 19:27:51 +08:00
不做码农
10c67a15bf update dept/index.vue 2023-07-13 19:26:29 +08:00
不做码农
dd87d9f305 字典组件新增分割字符串属性 2023-07-12 22:18:30 +08:00
不做码农
09c57e6ac1 🐛 fix:文件存储路由切换不能缓存问题 2023-07-10 14:07:52 +08:00
不做码农
44eed48054 🐛 fix 任务排序问题 2023-07-06 21:53:26 +08:00
不做码农
d95fdf0021 🐛 fix头像、文章资源上传失败 2023-07-05 20:28:45 +08:00
不做码农
032d88c996 ⬆️ 升级element-plus包 2023-07-05 20:21:37 +08:00
不做码农
91ded061e7 优化文件存储 2023-07-05 12:20:41 +08:00
不做码农
ce81beaf8c 优化代码生成 2023-07-04 21:08:37 +08:00
不做码农
0c561459e9 🐛 fix代码生成清空上级菜单后提交数据异常 2023-07-04 19:49:32 +08:00
不做码农
9dd69d768d 优化任务字典 2023-07-03 21:45:30 +08:00
不做码农
15d09bdf3f 🎨 优化登录信息过期 2023-07-03 21:42:33 +08:00
不做码农
7a22b57038 feat:代码生成预览显示路径 2023-07-03 20:43:05 +08:00
不做码农
0a208ede08 fix:croon表达式生成错误 2023-07-03 20:32:49 +08:00
不做码农
67916c5a9e 移除md5包 2023-06-18 21:56:19 +08:00
不做码农
57a2564bda 菜单搜索优化 2023-06-17 16:04:41 +08:00
不做码农
19edbc6703 网络请求新增移除null类型参数 2023-06-17 16:04:31 +08:00
不做码农
d1b1575535 新增角色菜单数据导出 2023-06-17 16:03:29 +08:00
不做码农
e6508d1a11 自定义字典数据可显示 2023-06-17 16:02:41 +08:00
124 changed files with 4429 additions and 1313 deletions

19
.vscode/settings.json vendored
View File

@ -5,11 +5,7 @@
"editor.formatOnPaste": true,
"editor.formatOnType": true,
// eslintvue
"eslint.validate": [
"javascript",
"typescript",
"vue"
],
"eslint.validate": ["javascript", "typescript", "vue"],
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
@ -22,12 +18,15 @@
"[js]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// prettier
"editor.formatOnSave": true,
// eslint --fix
"editor.codeActionsOnSave": {
"source.fixAll": true,
"eslint.autoFixOnSave": true,
"eslint.autoFixOnSave": true
},
"eslint.options": {
"overrideConfig": {
@ -49,16 +48,12 @@
},
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.enabledParsers": ["json", "js"],
"i18n-ally.localesPaths": [
"src/i18n/lang",
"src/i18n/pages/login",
"src/i18n/pages/menu",
],
"i18n-ally.localesPaths": ["src/i18n/lang", "src/i18n/pages/login", "src/i18n/pages/menu"],
"i18n-ally.extract.parsers.html": {
"attributes": ["text", "title", "alt", "placeholder", "label", "aria-label"],
"ignoredTags": ["script", "style"],
"vBind": true,
"inlineText": true
},
"i18n-ally.keystyle": "nested",
"i18n-ally.keystyle": "nested"
}

View File

@ -130,7 +130,7 @@
<div class="loading-wrp">
<span class="dot dot-spin"> <i></i> <i></i> <i></i> <i></i> </span>
</div>
<h5>Loading...</h5>
<h5>正在加载系统资源...</h5>
</div>
</div>
<script type="module" src="/src/main.js"></script>

View File

@ -1,6 +1,6 @@
{
"name": "zr.admin",
"version": "3.8.2",
"version": "v20230920",
"description": "ZRAdmin.NET管理系统",
"author": "ZR",
"license": "MIT",
@ -16,38 +16,45 @@
},
"dependencies": {
"@element-plus/icons-vue": "^0.2.7",
"@microsoft/signalr": "^6.0.5",
"@microsoft/signalr": "^6.0.21",
"@vueuse/core": "^8.9.4",
"@wangeditor/editor": "^5.1.1",
"@wangeditor/editor-for-vue": "^5.1.11",
"axios": "^0.27.2",
"countup.js": "^2.1.0",
"crypto-js": "^4.1.1",
"echarts": "5.2.2",
"element-plus": "^2.3.6",
"element-plus": "^2.3.12",
"file-saver": "2.0.5",
"fuse.js": "6.4.6",
"highlight.js": "^11.5.1",
"js-cookie": "3.0.1",
"js-md5": "^0.7.3",
"jsencrypt": "3.2.1",
"md-editor-v3": "^1.11.11",
"nprogress": "0.2.0",
"pinia": "^2.0.33",
"pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0",
"qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.0",
"sortablejs": "^1.15.0",
"v-code-diff": "^1.8.0",
"vue": "^3.3.4",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "1.0.2",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.2"
"vue-i18n": "9.2.2",
"vue-router": "^4.2.2",
"vxe-table": "^4.5.12",
"xe-utils": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"consola": "^3.2.3",
"sass": "1.45.0",
"unplugin-auto-import": "0.5.3",
"vite": "^4.3.9",
"vite-plugin-compression": "^0.3.6",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "1.0.5",
"vite-plugin-vue-setup-extend": "^0.4.0"
}

View File

@ -7,9 +7,9 @@
import useUserStore from './store/modules/user'
import useAppStore from './store/modules/app'
import { ElConfigProvider } from 'element-plus'
import zh from 'element-plus/lib/locale/lang/zh-cn' //
import en from 'element-plus/lib/locale/lang/en' //
import tw from 'element-plus/lib/locale/lang/zh-tw' //
import zh from 'element-plus/dist/locale/zh-cn.mjs' //
import en from 'element-plus/dist/locale/en.mjs' //
import tw from 'element-plus/dist/locale/zh-tw.mjs' //
import defaultSettings from '@/settings'
const { proxy } = getCurrentInstance()
@ -28,7 +28,11 @@ watch(
token,
(val) => {
if (val) {
proxy.signalr.start()
proxy.signalr.start().then(async (res) => {
if (res) {
await proxy.signalr.SR.invoke('logOut')
}
})
}
},
{
@ -53,4 +57,8 @@ watch(
immediate: true
}
)
console.log('🎉源码地址: https://gitee.com/izory/ZrAdminNetCore')
console.log('📖官方文档http://www.izhaorui.cn/doc')
console.log('💰打赏作者http://www.izhaorui.cn/doc/support.html')
console.log('📱移动端体验http://www.izhaorui.cn/h5')
</script>

View File

@ -5,7 +5,7 @@ export function upload(data) {
url: '/common/UploadFile',
method: 'POST',
data: data,
headers: { "Content-Type": "multipart/form-data" },
headers: { 'Content-Type': 'multipart/form-data' }
})
}
@ -18,6 +18,6 @@ export function sendEmail(data) {
return request({
url: '/common/SendEmail',
method: 'POST',
data: data,
data: data
})
}

View File

@ -10,9 +10,19 @@ export function listOnline(query) {
}
// 强退用户
export function forceLogout(tokenId) {
export function forceLogout(data) {
return request({
url: '/monitor/online/' + tokenId,
method: 'delete'
url: '/monitor/online/force',
method: 'delete',
data: data
})
}
// 批量强退用户
export function forceLogoutAll(data) {
return request({
url: '/monitor/online/batchForce',
method: 'delete',
data: data
})
}

View File

@ -0,0 +1,29 @@
import request from '@/utils/request'
import { downFile } from '@/utils/request'
/**
* 数据差异日志分页查询
* @param {查询条件} data
*/
export function listSqlDiffLog(query) {
return request({
url: 'monitor/SqlDiffLog/list',
method: 'get',
params: query
})
}
/**
* 删除数据差异日志
* @param {主键} pid
*/
export function delSqlDiffLog(pid) {
return request({
url: 'monitor/SqlDiffLog/' + pid,
method: 'delete'
})
}
// 导出数据差异日志
export async function exportSqlDiffLog(query) {
await downFile('monitor/SqlDiffLog/export', { ...query })
}

View File

@ -8,7 +8,7 @@ export function listCommonLang(query) {
return request({
url: 'system/CommonLang/list',
method: 'get',
params: query,
params: query
})
}
/**
@ -18,7 +18,7 @@ export function listCommonLang(query) {
export function listLangByLocale(locale) {
return request({
url: 'system/CommonLang/list/' + locale,
method: 'get',
method: 'get'
})
}
@ -30,7 +30,7 @@ export function addCommonLang(data) {
return request({
url: 'system/CommonLang',
method: 'post',
data: data,
data: data
})
}
@ -42,7 +42,7 @@ export function updateCommonLang(data) {
return request({
url: 'system/CommonLang',
method: 'PUT',
data: data,
data: data
})
}
@ -67,7 +67,6 @@ export function getCommonLangByKey(key) {
})
}
/**
* 删除多语言配置
* @param {主键} pid
@ -79,6 +78,18 @@ export function delCommonLang(pid) {
})
}
/**
* 删除多语言配置
* @param {key} langkey
*/
export function delCommonLangByKey(langkey) {
return request({
url: 'system/CommonLang/ByKey',
method: 'delete',
params: { langkey }
})
}
// 导出多语言配置
export function exportCommonLang(query) {
return request({

View File

@ -0,0 +1,46 @@
import request from '@/utils/request'
/**
* 邮件发送记录分页查询
* @param {查询条件} data
*/
export function listEmailLog(query) {
return request({
url: 'system/EmailLog/list',
method: 'get',
params: query,
})
}
/**
* 新增邮件发送记录
* @param data
*/
export function sendEmail(data) {
return request({
url: 'system/EmailLog/sendEmail',
method: 'post',
data: data,
})
}
/**
* 获取邮件发送记录详情
* @param {Id}
*/
export function getEmailLog(id) {
return request({
url: 'system/EmailLog/' + id,
method: 'get'
})
}
/**
* 删除邮件发送记录
* @param {主键} pid
*/
export function delEmailLog(pid) {
return request({
url: 'system/EmailLog/' + pid,
method: 'delete'
})
}

View File

@ -0,0 +1,57 @@
import request from '@/utils/request'
/**
* 邮件模板分页查询
* @param {查询条件} data
*/
export function listEmailTpl(query) {
return request({
url: 'system/EmailTpl/list',
method: 'get',
params: query,
})
}
/**
* 新增邮件模板
* @param data
*/
export function addEmailTpl(data) {
return request({
url: 'system/EmailTpl',
method: 'post',
data: data,
})
}
/**
* 修改邮件模板
* @param data
*/
export function updateEmailTpl(data) {
return request({
url: 'system/EmailTpl',
method: 'PUT',
data: data,
})
}
/**
* 获取邮件模板详情
* @param {Id}
*/
export function getEmailTpl(id) {
return request({
url: 'system/EmailTpl/' + id,
method: 'get'
})
}
/**
* 删除邮件模板
* @param {主键} pid
*/
export function delEmailTpl(pid) {
return request({
url: 'system/EmailTpl/' + pid,
method: 'delete'
})
}

View File

@ -1,12 +1,13 @@
import request from '@/utils/request'
// 登录方法
export function login(username, password, code, uuid) {
export function login(username, password, code, uuid, clientId) {
const data = {
username,
password,
code,
uuid
uuid,
clientId
}
return request({
url: '/login',
@ -67,3 +68,51 @@ export function oauthCallback(data, params) {
params: params
})
}
/**
* 生成二维码
* @param {*} data
* @returns
*/
export function generateQrcode(data) {
return request({
url: '/GenerateQrcode',
method: 'GET',
params: data
})
}
/**
* 刷新二维码
* @param {*} data
* @returns
*/
export function verifyScan(data) {
return request({
url: '/VerifyScan',
method: 'post',
data: data
})
}
/**
* 发送短信验证码
* @param {*} data
* @returns
*/
export function checkMobile(data) {
return request({
method: 'post',
data: data,
url: '/checkMobile'
})
}
// 登录方法
export function phoneLogin(data) {
return request({
url: '/phoneLogin',
method: 'POST',
data: data
})
}

View File

@ -3,7 +3,7 @@ import request from '@/utils/request'
// 查询菜单列表
export function listMenu(query) {
return request({
url: '/system/menu/list',
url: '/system/menu/treelist',
method: 'get',
params: query
})

View File

@ -1,4 +1,5 @@
import request from '@/utils/request'
import { downFile } from '@/utils/request'
// 查询角色列表
export function listRole(query) {
@ -22,7 +23,7 @@ export const addRole = (data) => {
return request({
url: '/system/role/edit',
method: 'post',
data: data,
data: data
})
}
@ -73,3 +74,7 @@ export function exportRole(query) {
params: query
})
}
// 导出角色菜单
export async function exportRoleMenu(query) {
await downFile('/system/role/exportRoleMenu', { ...query })
}

View File

@ -0,0 +1,40 @@
import request from '@/utils/request'
import { downFile } from '@/utils/request'
/**
* 短信验证码记录分页查询
* @param {查询条件} data
*/
export function listSmscodeLog(query) {
return request({
url: 'system/SmscodeLog/list',
method: 'get',
params: query
})
}
/**
* 获取短信验证码记录详情
* @param {Id}
*/
export function getSmscodeLog(id) {
return request({
url: 'system/SmscodeLog/' + id,
method: 'get'
})
}
/**
* 删除短信验证码记录
* @param {主键} pid
*/
export function delSmscodeLog(pid) {
return request({
url: 'system/SmscodeLog/' + pid,
method: 'delete'
})
}
// 导出短信验证码记录
export async function exportSmscodeLog(query) {
await downFile('system/SmscodeLog/export', { ...query })
}

View File

@ -22,7 +22,7 @@ export function getUser(userId) {
// 新增用户
export function addUser(data) {
return request({
url: '/system/user/edit',
url: '/system/user/add',
method: 'post',
data: data
})
@ -116,7 +116,8 @@ export function uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
method: 'post',
data: data
data: data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4017520 */
src: url('iconfont.woff2?t=1681548759797') format('woff2'),
url('iconfont.woff?t=1681548759797') format('woff'),
url('iconfont.ttf?t=1681548759797') format('truetype');
src: url('iconfont.woff2?t=1695878634619') format('woff2'),
url('iconfont.woff?t=1695878634619') format('woff'),
url('iconfont.ttf?t=1695878634619') format('truetype');
}
.iconfont {
@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-exit-fullscreen:before {
content: "\e66d";
}
.icon-validCode:before {
content: "\e621";
}
.icon-index:before {
content: "\e61f";
}
.icon-tree-table:before {
content: "\e637";
}
@ -89,10 +101,6 @@
content: "\e63a";
}
.icon-index:before {
content: "\e63b";
}
.icon-ipvisits:before {
content: "\e63c";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,27 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "37140510",
"name": "exit-fullscreen",
"font_class": "exit-fullscreen",
"unicode": "e66d",
"unicode_decimal": 58989
},
{
"icon_id": "37139279",
"name": "validCode",
"font_class": "validCode",
"unicode": "e621",
"unicode_decimal": 58913
},
{
"icon_id": "35896169",
"name": "index",
"font_class": "index",
"unicode": "e61f",
"unicode_decimal": 58911
},
{
"icon_id": "35076965",
"name": "tree-table",
@ -138,13 +159,6 @@
"unicode": "e63a",
"unicode_decimal": 58938
},
{
"icon_id": "35076497",
"name": "index",
"font_class": "index",
"unicode": "e63b",
"unicode_decimal": 58939
},
{
"icon_id": "35076498",
"name": "ipvisits",

Binary file not shown.

Binary file not shown.

Binary file not shown.

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="1700531242903" class="icon" viewBox="0 0 1027 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4238" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.5859375" height="200"><path d="M984.064 461.824q20.48-12.288 31.232-9.728t10.752 16.896l0 318.464q0 36.864-8.704 58.88t-23.552 32.768-34.816 13.824-43.52 3.072l-45.056 0q-33.792 0-81.408-0.512t-105.984-0.512l-119.808 0-120.832 0-110.592 0-90.112 0-57.344 0q-36.864 0-60.416-9.728t-36.864-25.088-18.432-35.328-5.12-41.472l0-304.128q0-20.48 10.24-26.112t25.6 4.608q4.096 3.072 18.432 12.288t33.28 20.992 40.448 25.088 39.936 24.576 31.744 19.968 17.408 10.752q13.312 8.192 12.288 17.92t-6.144 17.92q-4.096 8.192-12.288 25.6t-17.408 36.352-17.408 35.84-12.288 24.064q-5.12 8.192-3.584 13.824t5.632 8.704 10.24 1.536 11.264-7.68q3.072-3.072 15.872-17.92t28.16-32.256 29.184-32.256 17.92-18.944q5.12-5.12 15.872-11.776t22.016-1.536q6.144 4.096 20.992 13.824t32.768 20.992 35.84 23.04 30.208 18.944q12.288 8.192 26.112 10.24t26.112 1.024 22.528-4.608 15.36-6.656q6.144-3.072 23.552-13.312t37.888-23.04 38.4-24.064 25.088-15.36q11.264-6.144 19.456-3.584t17.408 10.752q4.096 3.072 16.384 18.432t27.136 33.28 27.648 34.304 17.92 22.528q6.144 7.168 12.8 7.68t11.264-3.072 6.144-10.24-3.584-14.848q-2.048-4.096-8.192-19.968t-13.824-35.328-15.872-37.888-12.288-26.624q-8.192-15.36-8.704-24.576t10.752-16.384q3.072-2.048 25.6-16.384t50.176-32.256 53.76-33.792 35.328-22.016zM544.768 637.952q-5.12 1.024-17.408-5.12t-27.648-15.36q-54.272-29.696-108.544-61.44-47.104-26.624-100.864-57.856t-99.84-57.856l0-184.32 2.048 0 0-1.024q0-26.624 10.24-49.664t27.648-40.448 40.448-27.648 49.664-10.24l448.512 0q26.624 0 50.176 10.24t40.96 27.648 27.648 40.448 10.24 49.664l0 1.024 2.048 0 0 184.32q-47.104 26.624-100.864 57.856t-100.864 57.856q-55.296 31.744-108.544 62.464-9.216 5.12-24.064 12.288t-20.992 7.168zM385.024 319.488l320.512 0 0-64.512-320.512 0 0 64.512zM385.024 447.488l320.512 0 0-64.512-320.512 0 0 64.512z" p-id="4239"></path></svg>

After

Width:  |  Height:  |  Size: 2.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="1700531099119" class="icon" viewBox="0 0 1044 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4078" xmlns:xlink="http://www.w3.org/1999/xlink" width="203.90625" height="200"><path d="M16.384 157.696q-11.264-8.192-14.336-13.312t-3.072-14.336l0-9.216q0-26.624 13.312-41.984t46.08-15.36l843.776 1.024q33.792 0 47.104 14.336t13.312 39.936l1.024 11.264q0 9.216-2.048 12.8t-16.384 14.848l-418.816 251.904q-10.24 4.096-25.088 11.264t-19.968 8.192q-6.144 0-18.432-5.632t-27.648-14.848zM646.144 572.416q-21.504 0-34.816 9.216t-20.992 23.552-10.24 32.256-2.56 34.304l0 71.68 0 12.288q0 6.144 1.024 11.264l-141.312 0q-72.704 0-136.192-0.512t-111.616-0.512l-70.656 0q-36.864 0-60.416-9.728t-36.864-25.088-18.432-35.328-5.12-41.472l0-378.88q0-21.504 10.24-27.136t25.6 5.632q5.12 3.072 18.432 11.776t31.744 19.968 38.4 24.064 37.888 24.064 30.72 19.456 16.896 10.24q14.336 9.216 16.384 23.04t-3.072 24.064q-4.096 10.24-12.288 26.624t-17.408 33.28-17.92 32.256-11.776 23.552q-5.12 14.336 2.048 19.456t22.528-4.096q3.072-2.048 16.896-15.872t29.184-30.72 29.184-31.744 18.944-17.92q9.216-8.192 24.064-11.776t26.112 2.56q7.168 4.096 19.456 12.288t27.136 17.92 30.208 19.968l27.648 18.432q12.288 8.192 26.112 10.24t26.624 1.024 23.04-4.608 15.36-6.656 19.456-11.776 31.232-18.944 31.744-19.456 22.016-13.312l129.024-79.872q2.048-1.024 12.8-8.192t26.624-17.408 34.816-22.528 35.84-23.04 31.232-19.968 20.48-13.312q19.456-12.288 30.208-9.728t10.752 16.896l0 266.24q-28.672-23.552-55.808-44.032t-49.664-34.816q-22.528-15.36-39.424-10.752t-27.648 19.968-16.384 35.84-5.632 36.864q0 11.264-0.512 18.432t-0.512 12.288q-1.024 5.12-1.024 8.192l-15.36 0-104.448 0zM1028.096 679.936q13.312 10.24 13.824 28.672t-10.752 26.624q-15.36 12.288-35.84 29.184t-42.496 34.304-43.008 34.816-38.4 30.72q-21.504 17.408-30.208 18.432t-8.704-26.624l0-46.08q0-17.408-8.704-28.672t-23.04-11.264l-118.784 0q-14.336 0-28.16-10.24t-13.824-26.624l0-52.224q0-28.672 9.216-34.816t32.768-6.144l20.48 0q9.216 0 20.992 0.512t28.16 0.512l43.008 0q20.48 0 29.184-7.168t8.704-25.6l0-45.056q0-18.432 6.144-23.552t22.528 8.192q16.384 12.288 37.888 29.696t44.544 35.328 45.056 35.84 39.424 31.232z" p-id="4079"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1 +0,0 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 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="1692263225130" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4039" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M245.76 286.72h552.96c124.928 0 225.28 100.352 225.28 225.28s-100.352 225.28-225.28 225.28H0V532.48c0-135.168 110.592-245.76 245.76-245.76z m133.12 348.16V401.408H348.16v178.176l-112.64-178.176H204.8V634.88h30.72v-178.176L348.16 634.88h30.72z m182.272-108.544v-24.576h-96.256v-75.776h110.592v-24.576h-141.312V634.88h143.36v-24.576h-112.64v-83.968h96.256z m100.352 28.672l-34.816-151.552h-34.816l55.296 233.472H675.84l47.104-161.792 4.096-20.48 4.096 20.48 47.104 161.792h28.672l57.344-233.472h-34.816l-32.768 151.552-4.096 30.72-6.144-30.72-40.96-151.552h-30.72l-40.96 151.552-6.144 30.72-6.144-30.72z" fill="#EE502F" p-id="4040"></path></svg>

After

Width:  |  Height:  |  Size: 976 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="1691394989170" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8595" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.515625" height="200"><path d="M585.142857 365.714286m-36.571428 0a36.571429 36.571429 0 1 0 73.142857 0 36.571429 36.571429 0 1 0-73.142857 0Z" fill="#54C3F1" p-id="8596"></path><path d="M731.428571 365.714286m-36.571428 0a36.571429 36.571429 0 1 0 73.142857 0 36.571429 36.571429 0 1 0-73.142857 0Z" fill="#54C3F1" p-id="8597"></path><path d="M438.857143 365.714286m-36.571429 0a36.571429 36.571429 0 1 0 73.142857 0 36.571429 36.571429 0 1 0-73.142857 0Z" fill="#54C3F1" p-id="8598"></path><path d="M1060.571429 877.714286H109.714286a109.714286 109.714286 0 0 1-109.714286-109.714286V109.714286a109.714286 109.714286 0 0 1 109.714286-109.714286h950.857143a109.714286 109.714286 0 0 1 109.714285 109.714286v658.285714a109.714286 109.714286 0 0 1-109.714285 109.714286zM109.714286 73.142857a36.571429 36.571429 0 0 0-36.571429 36.571429v658.285714a36.571429 36.571429 0 0 0 36.571429 36.571429h950.857143a36.571429 36.571429 0 0 0 36.571428-36.571429V109.714286a36.571429 36.571429 0 0 0-36.571428-36.571429z" fill="#54C3F1" p-id="8599"></path><path d="M36.571429 658.285714h1097.142857v73.142857H36.571429zM402.285714 841.142857h73.142857v146.285714h-73.142857zM694.857143 841.142857h73.142857v146.285714h-73.142857z" fill="#54C3F1" p-id="8600"></path><path d="M292.571429 950.857143h585.142857v73.142857H292.571429z" fill="#54C3F1" p-id="8601"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 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="1691394871411" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2379" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M480 544H368v-64h112V368h64v288h-64V544z m368 304V592h64v320H592v-64h256zM656 656v144h-64V592h208v64H656zM176 176v192h192V176H176z m-64-64h320v320H112V112z m544 64v192h192V176H656z m-64-64h320v320H592V112zM112 480h160v64H112v-64z m640 0h160v64H752v-64zM544 112v160h-64V112h64z m0 640v160h-64V752h64z m-368-96v192h192V656H176z m-64-64h320v320H112V592z m112-368h96v96h-96v-96z m0 480h96v96h-96v-96z m480-480h96v96h-96v-96z" fill="#5090F1" p-id="2380"></path></svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@ -1 +0,0 @@
<?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="1569580729849" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1939" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M513.3 958.5c-142.2 0-397.9-222.1-401.6-440.5V268c1.7-39.6 31.7-72.3 71.1-77.3 49-4.6 97.1-16.5 142.7-35.3 47.8-14 91.9-38.3 129.4-71.1 30.3-24.4 72.9-26.3 105.3-4.6 39.9 30.7 83.8 55.9 130.5 74.6 48.6 14.7 98.2 25.9 148.4 33.7 38.5 7.6 67.1 40.3 69.5 79.5 3.3 84.9 2.5 169.9-2.6 254.7-33.7 281.6-253.7 436.4-392.7 436.3z m-0.1-813.7c-7.2-0.2-14.3 2-20 6.4-39.7 35.2-86.8 61.1-137.7 75.7-46.8 19.2-96.2 31-146.6 35.2-11 3.2-18.8 13-19.5 24.4v230.1c3.5 180.3 223.3 361 323.9 361s287.3-120.2 317.6-360.5c7.3-142.7 0-228.6 0-229.6-1.3-13.3-11-24.3-24-27.3-49.6-7.7-98.6-19-146.5-33.7-46.3-19.5-89.7-45.3-129-76.7-5.8-3.8-12.7-5.5-19.5-4.9l1.3-0.1z" fill="#C6CCDA" p-id="1940"></path><path d="M750.1 428L490.7 673.2c-11.7 11.1-29.5 12.9-43.1 4.2l-6.8-5.8-141.2-149.4c-9.3-9.3-12.7-22.9-9-35.5 3.8-12.6 14.1-22.1 27-24.8 12.9-2.7 26.1 1.9 34.6 11.9L469 597.5l233.7-221c14.6-12.8 36.8-11.6 49.9 2.7 13.2 14.2 11.5 35.3-2.5 48.8" fill="#C6CCDA" p-id="1941"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -128,3 +128,23 @@
justify-content: space-between;
}
}
// el-table 表头样式
.el-table-header-cell {
text-align: center !important;
}
.el-dialog__headerbtn {
width: 47px !important;
}
// el-tree
.tree-item-flex {
&.is-expanded .el-tree-node__children {
display: flex !important;
flex-direction: column;
flex-direction: row;
flex-wrap: wrap;
}
}

View File

@ -1,4 +1,4 @@
.login {
.login-wrap {
background: radial-gradient(220% 105% at top center, #1b2947 10%, #4b76a7 40%, #81acae 65%, #f7f7b6);
background-attachment: fixed;
overflow: hidden;
@ -8,24 +8,31 @@
height: 100%;
// background-image: url('@/assets/images/login-bg.jpg');
background-size: cover;
flex-direction: column;
.login {
margin: 0 auto;
background: #ffffff;
border-radius: 6px;
width: var(--base-login-width);
position: relative;
// height: 420px;
}
}
.title {
margin: 0px auto 30px auto;
margin: 10px auto 15px auto;
text-align: center;
// color: #fff;
}
.login-form {
border-radius: 6px;
background: #ffffff;
// background-color: hsla(0, 0%, 100%, 0.3);
width: var(--base-login-width);
padding: 25px 15px 5px 15px;
padding: 5px 25px 5px 25px;
position: relative;
height: 230px;
.input-icon {
height: 39px;
height: 30px;
width: 14px;
margin-left: 0px;
}
@ -68,6 +75,49 @@
}
.langSet {
position: absolute;
right: 20px;
left: 20px;
top: 10px;
}
.scan-wrap {
position: absolute;
right: 0;
top: 0;
width: 50px;
height: 50px;
cursor: pointer;
transition: all ease 0.3s;
overflow: hidden;
.icon {
width: 48px;
height: 50px;
display: inline-block;
font-size: 48px;
position: absolute;
right: 1px;
top: 0px;
}
.scan-delta {
position: absolute;
width: 35px;
height: 70px;
z-index: 2;
top: 2px;
right: 21px;
background: var(--el-color-white);
transform: rotate(-45deg);
}
}
.login-scan-container {
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
justify-content: space-around;
margin-bottom: 20px;
}
@media screen and (max-width: 500px) {
}

View File

@ -11,6 +11,7 @@ $panGreen: #30b08f;
// 默认菜单主题风格
:root {
--base-text-color-rgba: rgba(0, 0, 0, 0.85);
--base-menu-background: #fff;
--base-sidebar-width: 220px;
// 左侧菜单宽度
--el-aside-width: 220px;
@ -19,7 +20,10 @@ $panGreen: #30b08f;
--base-tags-height: 34px;
--base-header-height: 50px;
//登录框宽度
--base-login-width: 280px;
--base-login-width: 360px;
// 侧边栏图标大小
--el-menu-icon-width: 14px;
}
/***侧边栏深色配置***/
@ -31,11 +35,29 @@ $panGreen: #30b08f;
--el-text-color-primary: #e5eaf3;
--el-menu-text-color: var(--el-text-color-primary);
}
// 黑色主题
html.dark {
/* custom dark bg color */
// --el-bg-color: #141414;
--base-color-white: #ffffff;
--base-text-color-rgba: #ffffff;
--base-menu-background: #000;
// vxe-table黑色样式
--vxe-font-color: #98989e;
--vxe-primary-color: #2c7ecf;
--vxe-icon-background-color: #98989e;
--vxe-table-font-color: #98989e;
--vxe-table-resizable-color: #95969a;
--vxe-table-header-background-color: #28282a;
--vxe-table-body-background-color: #151518;
--vxe-table-background-color: #4a5663;
--vxe-table-border-width: 1px;
--vxe-table-border-color: #37373a;
.current-row {
color: #e65d6e;
}
}
html.cafe {
filter: sepia(0.9) hue-rotate(315deg) brightness(0.9);

16
src/components.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
import SvgIcon from '@/components/SvgIcon/index.vue'
import UploadImage from '@/components/ImageUpload.vue'
import UploadFile from '@/components/FileUpload'
import ImagePreview from '@/components/ImagePreview'
import DictTag from '@/components/DictTag'
import Dialog from '@/components/Dialog'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
SvgIcon: typeof SvgIcon,
UploadImage: typeof UploadImage,
DictTag: typeof DictTag,
ImagePreview: typeof ImagePreview,
UploadFile: typeof UploadFile,
ZrDialog: typeof Dialog
}
}

View File

@ -35,12 +35,10 @@
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="7">
指定
<el-radio v-model="radioValue" :label="7"> 指定 </el-radio>
<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>

View File

@ -20,12 +20,10 @@
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-radio v-model="radioValue" :label="4"> 指定 </el-radio>
<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>

View File

@ -20,12 +20,10 @@
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-radio v-model="radioValue" :label="4"> 指定 </el-radio>
<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>

View File

@ -15,17 +15,15 @@
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="1" :max="11" /> 月开始
<el-input-number v-model="average02" :min="1" :max="12 - average01" /> 月执行一次
<el-input-number v-model="average02" :min="1" :max="maxMonth" /> 月执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-radio v-model="radioValue" :label="4"> 指定 </el-radio>
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8" style="width: 80%">
<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>
@ -81,6 +79,9 @@ const averageTotal = computed(() => {
average02.value = props.check(average02.value, 1, 12 - average01.value)
return average01.value + '/' + average02.value
})
const maxMonth = computed(() => {
return 12 - average01.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})

View File

@ -20,12 +20,10 @@
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-radio v-model="radioValue" :label="4"> 指定 </el-radio>
<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>

View File

@ -8,7 +8,7 @@
<el-radio :label="2" v-model="radioValue"> 每年 </el-radio>
</el-form-item>
<el-form-item>
<!-- <el-form-item>
<el-radio :label="3" v-model="radioValue">
周期从
<el-input-number v-model="cycle01" :min="fullYear" :max="maxFullYear - 1" /> -
@ -22,7 +22,7 @@
<el-input-number v-model="average01" :min="fullYear" :max="maxFullYear - 1" /> 年开始
<el-input-number v-model="average02" :min="1" :max="10" /> 年执行一次
</el-radio>
</el-form-item>
</el-form-item> -->
<el-form-item>
<el-radio :label="5" v-model="radioValue">
@ -74,6 +74,9 @@ const averageTotal = computed(() => {
average02.value = props.check(average02.value, 1, 10)
return average01.value + '/' + average02.value
})
const maxFullYearLabel = computed(() => {
return maxFullYear.value - 1
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})

View File

@ -0,0 +1,75 @@
<template>
<el-dialog v-bind="$attrs" :fullscreen="isFullscreen" ref="myDialogRef" v-model="open">
<template #header="{ close, titleId, titleClass }">
<div class="custom-header">
<span :id="titleId" :class="titleClass">{{ title }}</span>
<span class="fullscreen" @click="handleScreen()">
<svg-icon :name="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" />
</span>
</div>
</template>
<slot></slot>
<template v-slot:footer>
<slot name="footer"> </slot>
</template>
</el-dialog>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
title: {
type: String
},
fullScreen: {
type: Boolean,
default: false
}
})
const isFullscreen = ref(false)
const open = ref(false)
watch(
() => props.modelValue,
(val) => {
open.value = val
},
{ deep: true, immediate: true }
)
watch(
() => props.fullScreen,
(val) => {
isFullscreen.value = val
},
{ deep: true, immediate: true }
)
function handleScreen() {
isFullscreen.value = !isFullscreen.value
}
const expose = {}
const myDialogRef = ref()
onMounted(() => {
const entries = Object.entries(myDialogRef.value)
for (const [method, fn] of entries) {
expose[method] = fn
}
})
defineExpose(expose)
</script>
<style>
.custom-header {
position: relative;
}
.fullscreen {
position: absolute;
right: 13px;
top: 5px;
cursor: pointer;
width: 27px;
height: 27px;
text-align: center;
}
</style>

View File

@ -1,17 +1,28 @@
<template>
<template v-for="(item, index) in props.options">
<template v-for="(item, index) in dataList">
<template v-if="values.includes(item.dictValue)">
<span v-if="item.listClass == 'default' || item.listClass == ''" :key="item.dictValue" :index="index" :class="item.cssClass">
{{ item.dictLabel }} <i v-if="showValue">#{{ item.dictValue }}</i>
<template v-if="item.langKey">
{{ $t(item.langKey) }}
</template>
<template v-else>
{{ item.dictLabel }}
</template>
<i v-if="showValue">#{{ item.dictValue }}</i>
</span>
<el-tag
size="small"
v-else
size="small"
:disable-transitions="true"
:index="index"
:type="item.listClass == 'primary' ? '' : item.listClass"
:class="item.cssClass">
<template v-if="item.langKey">
{{ $t(item.langKey) }}
</template>
<template v-else>
{{ item.dictLabel }}
</template>
<i v-if="showValue">#{{ item.dictValue }}</i>
</el-tag>
</template>
@ -23,16 +34,43 @@ const props = defineProps({
//
options: {
type: Array,
default: null,
default: null
},
//
value: [Number, String, Array, Boolean],
showValue: false,
split: {
type: String,
default: null
},
// { label: 'name', value: 'id'}
config: {
type: Object,
default: null
}
})
const dataList = computed(() => {
if (props.config) {
let config = props.config
var newList = []
for (let d of props.options) {
let label = d[config.label]
let value = d[config.value]
newList.push({ dictLabel: label, dictValue: value, ...d })
}
return newList
}
return props.options
})
const values = computed(() => {
if (props.value !== null && typeof props.value !== 'undefined') {
if (props.split != null && props.split != '') {
return props.value.split(props.split) ?? []
} else {
return Array.isArray(props.value) ? props.value : [String(props.value)]
}
} else {
return []
}

View File

@ -21,17 +21,21 @@ export default {
props: {
placeholder: {
type: String,
default: () => '请输入内容',
default: () => '请输入内容'
},
modelValue: String,
//
toolbarConfig: {
type: [Object],
default: () => {}
}
},
setup(props, { emit }) {
const editorRef = shallowRef()
const valueHtml = ref(props.modelValue)
const toolbarConfig = {}
const editorConfig = {
MENU_CONF: {},
placeholder: props.placeholder,
placeholder: props.placeholder
}
//
editorConfig.MENU_CONF['uploadImage'] = {
@ -49,7 +53,7 @@ export default {
// http header
headers: {
Authorization: 'Bearer ' + getToken(),
userid: useUserStore().userId,
userid: useUserStore().userId
},
// cookie false
withCredentials: true,
@ -61,7 +65,7 @@ export default {
// res url alt href
insertFn(res.data.url)
)
},
}
}
//
editorConfig.MENU_CONF['uploadVideo'] = {
@ -84,7 +88,7 @@ export default {
// http header
headers: {
Authorization: 'Bearer ' + getToken(),
userid: useUserStore().userId,
userid: useUserStore().userId
},
// cookie false
@ -97,7 +101,7 @@ export default {
// res url alt href
insertFn(res.data.url)
)
},
}
}
onBeforeUnmount(() => {
const editor = editorRef.value
@ -118,18 +122,18 @@ export default {
editor.clear()
return
}
valueHtml.value = value;
},
valueHtml.value = value
}
)
return {
editorRef,
valueHtml,
mode: 'default',
toolbarConfig,
editorConfig,
handleCreated,
handleChange,
toolbarConfig: props.toolbarConfig
}
}
},
}
</script>

View File

@ -26,16 +26,16 @@
<!-- 上传按钮 -->
<el-button type="primary" icon="upload" v-if="!drag">选取文件</el-button>
<!-- 上传提示 -->
<template #tip>
<template v-slot:tip>
<div class="el-upload__tip" v-if="showTip">
请上传
<slot name="tip">
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
大小不超过 <b class="text-danger">{{ fileSize }}MB</b>
</template>
<template v-if="fileType && fileType.length > 0">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
<template v-if="fileType">
格式为 <b class="text-danger">{{ fileType.join('/') }}</b>
</template>
的文件
</slot>
</div>
</template>
</el-upload>

View File

@ -68,13 +68,18 @@ function close() {
}
function change(val) {
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')
} else {
if (query) {
router.push({ path: path, query: JSON.parse(query) })
} else {
router.push(path)
}
}
search.value = ''
options.value = []
@ -104,7 +109,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 = []) {
function generateRoutes(routes, basePath = '', prefixTitle = [], query = {}) {
let res = []
for (const r of routes) {
@ -130,6 +135,9 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
res.push(data)
}
}
if (r.query) {
data.query = r.query
}
// recursive child routes
if (r.children) {

View File

@ -66,7 +66,7 @@ function reset() {
}
defineExpose({
reset,
reset
})
</script>
@ -76,25 +76,14 @@ defineExpose({
padding: 10px;
.icon-list {
overflow-y: scroll;
display: flex;
display: grid;
flex-wrap: wrap;
justify-content: space-around;
height: 200px;
grid-template-columns: repeat(5, 90px);
.icon-item {
// height: 30px;
// line-height: 30px;
// margin-bottom: -5px;
cursor: pointer;
width: 19%;
text-align: center;
// float: left;
}
.name {
// display: inline-block;
// vertical-align: -0.15em;
// fill: currentColor;
// overflow: hidden;
}
}
}

View File

@ -2,6 +2,7 @@
<div class="component-upload-image">
<el-upload
multiple
v-bind="$attrs"
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
@ -10,7 +11,7 @@
:on-error="handleUploadError"
:on-exceed="handleExceed"
name="file"
:data="data"
:data="uploadData"
:on-remove="handleRemove"
:show-file-list="true"
:headers="headers"
@ -18,21 +19,38 @@
:on-preview="handlePictureCardPreview"
:class="{ hide: fileList.length >= limit }">
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload>
<!-- 上传提示 -->
<template v-slot:tip>
<div class="el-upload__tip" v-if="showTip">
请上传
<slot name="tip">
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
大小不超过 <b class="text-danger">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
格式为 <b class="text-danger">{{ fileType.join('/') }}</b>
</template>
的文件
</slot>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
<el-dialog v-model="dialogVisible" append-to-body>
<el-form label-width="100px">
<el-form-item label="预览">
<el-image style="display: block; max-width: 50%" :src="dialogImageUrl">
<template #error>
<div class="image-slot">加载失败</div>
</template>
</el-image>
</el-form-item>
<el-form-item label="访问路径">
<el-link type="warning" :href="dialogImageUrl" target="_blank">{{ dialogImageUrl }}</el-link>
<el-button type="danger" text icon="document-copy" plain class="ml10" v-clipboard:success="copySuccess" v-clipboard:copy="dialogImageUrl"
>复制</el-button
>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
@ -45,27 +63,27 @@ const props = defineProps({
//
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,
default: true
},
//
data: {
type: Object,
},
type: Object
}
})
const { proxy } = getCurrentInstance()
@ -79,6 +97,7 @@ const uploadImgUrl = ref(baseUrl + import.meta.env.VITE_APP_UPLOAD_URL) // 上
const headers = ref({ Authorization: 'Bearer ' + getToken() })
const fileList = ref([])
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize))
const uploadData = computed(() => props.data)
watch(
() => props.modelValue,
@ -89,11 +108,7 @@ watch(
//
fileList.value = list.map((item) => {
if (typeof item === 'string') {
// if (item.indexOf(baseUrl) === -1) {
// item = { name: baseUrl + item, url: baseUrl + item }
// } else {
item = { name: item, url: item }
// }
}
return item
})
@ -102,7 +117,7 @@ watch(
return []
}
},
{ deep: true, immediate: true },
{ deep: true, immediate: true }
)
//
@ -187,4 +202,7 @@ function listToString(list, separator) {
}
return strs != '' ? strs.substr(0, strs.length - 1) : ''
}
function copySuccess() {
proxy.$modal.msgSuccess('复制成功')
}
</script>

View File

@ -0,0 +1,81 @@
<template>
<div class="uploadData">
<el-upload
ref="uploadRef"
:limit="1"
name="file"
accept=".xlsx,.xls"
:data="uploadData"
:headers="headers"
:action="uploadFileUrl"
:disabled="isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="true">
<el-button type="primary" icon="Upload">上传文件</el-button>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入xlsxlsx格式文件</span>
<el-link type="primary" @click="importTemplate" icon="Bottom"> 下载模板 </el-link>
</div>
</template>
</el-upload>
</div>
</template>
<script setup>
import { getToken } from '@/utils/auth'
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const props = defineProps({
importUrl: {
type: String
},
templateUrl: {
type: String
},
//
data: {
type: Object
}
})
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(baseUrl + props.importUrl) // url
const headers = ref({ Authorization: 'Bearer ' + getToken() })
const uploadData = computed(() => props.data)
const isUploading = ref(false)
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
isUploading.value = true
}
/** 文件上传成功处理 */
const handleFileSuccess = (response, file, fileList) => {
const { code, msg } = response
isUploading.value = false
proxy.$refs['uploadRef'].clearFiles()
proxy.$refs['uploadRef'].handleRemove(file)
if (code != 200) {
proxy.$modal.msgError('导入数据失败,原因:' + msg)
} else {
emit('success', response)
}
}
const handleFileError = function (error) {
proxy.$modal.msgError('导入数据失败,原因:' + error)
}
function importTemplate() {
proxy.downFile(props.templateUrl)
}
</script>
<style lang="scss" scoped>
.uploadData {
padding: 10px;
}
</style>

View File

@ -1,55 +1,109 @@
<template>
<div>
<el-popover placement="bottom" trigger="hover" width="300px" popper-class="el-popover-pupop-user-news">
<el-popover placement="bottom" trigger="click" width="400px" popper-class="el-popover-pupop-user-news">
<template #reference>
<el-badge :is-dot="noticeDot" style="line-height: 18px">
<el-badge :hidden="allDotNum <= 0" :value="allDotNum" style="line-height: 18px">
<el-icon><bell /></el-icon>
</el-badge>
</template>
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">通知</div>
<div class="head-box-btn" v-if="noticeList.length > 0" @click="onAllReadClick">全部已读</div>
</div>
<div class="content-box">
<template v-if="noticeList.length > 0">
<div class="content-box-item" v-for="(v, k) in noticeList" :key="k">
<div>{{ v.noticeTitle }}</div>
<div class="content-box-msg" v-html="v.noticeContent"></div>
<div class="content-box-time">{{ v.updateTime }}</div>
</div>
<div class="read" @click="onAllReadClick">全部已读</div>
<el-tabs v-model="noticeType">
<el-tab-pane name="0">
<template #label>
<el-badge :hidden="newsDot <= 0" :value="newsDot" class="new-item"> 通知 </el-badge>
</template>
<div class="content-box-empty" v-else>
<div class="content-box-empty-margin">
<el-icon><Promotion /></el-icon>
<div class="mt15">全部已读</div>
<div class="content-box">
<div class="content-box-item" v-for="item in noticeList" @click="handleDetails(item, 0)">
<el-icon :size="30" color="#409EFF"><bell /></el-icon>
<div class="content">
<div class="title">{{ item.noticeTitle }}</div>
<div class="content-box-time">{{ dayjs(item.create_time).format('YYYY-MM-DD') }}</div>
</div>
</div>
<el-empty v-if="noticeList.length <= 0" :image-size="60" description="暂无公告"></el-empty>
</div>
</el-tab-pane>
<el-tab-pane name="1">
<template #label>
<el-badge :hidden="chatDotNum <= 0" :value="chatDotNum" class="new-item"> 私信 </el-badge>
</template>
<div class="content-box">
<div class="content-box-item" v-for="item in chatList" @click="handleDetails(item, 1)">
<el-avatar :src="item.fromUser.avatar"></el-avatar>
<div class="content">
<div class="title">
<span class="name">{{ item.fromUser.nickName }}</span>
{{ item.message }}
</div>
<div class="content-box-time">{{ formatTime(item.chatTime) }}</div>
</div>
</div>
<el-empty v-if="chatList.length <= 0" :image-size="60" description="暂无私信"></el-empty>
</div>
</el-tab-pane>
</el-tabs>
<div class="foot-box">
<div @click="onGoToGiteeClick" v-if="noticeList.length > 0">前往通知中心</div>
</div>
<div class="foot-box" @click="onGoToGiteeClick" v-if="noticeList.length > 0">前往通知中心</div>
</div>
</el-popover>
<el-dialog draggable v-model="show" append-to-body>
<template #header> {{ info.title }} </template>
<template v-if="info">
<template v-if="noticeType == 0">
<div v-html="info.item.noticeContent"></div>
<div class="n_right">{{ info.item.create_by }}</div>
<div class="n_right">{{ dayjs(info.item.create_time).format('YYYY-MM-DD HH:mm') }}</div>
</template>
<msgList v-if="noticeType == 1" v-model="info.userId"> </msgList>
</template>
</el-dialog>
</div>
</template>
<script setup name="noticeIndex">
import msgList from '@/views/components/msgList.vue'
import useSocketStore from '@/store/modules/socket'
import useUserStore from '@/store/modules/user'
import { dayjs } from 'element-plus'
import { formatTime } from '@/utils/index'
const { proxy } = getCurrentInstance()
const noticeType = ref('0')
//
const newsDot = ref(false)
const newsDot = computed(() => {
return useSocketStore().newNotice
})
const show = ref(false)
const noticeList = computed(() => {
return useSocketStore().noticeList
})
const noticeDot = computed(() => {
return useSocketStore().noticeDot
const allDotNum = computed(() => {
return useSocketStore().getAllDotNum()
})
const chatList = computed(() => {
return useSocketStore().getSessionList(useUserStore().userId)
})
const chatDotNum = computed(() => {
return useSocketStore().newChat
})
const info = ref({})
function handleDetails(item, type) {
show.value = true
if (type == 0) {
info.value = { type, item, title: item.noticeTitle }
} else if (type == 1) {
info.value = { type, title: item.fromUser.nickName, userId: item.userId }
}
}
//
function onAllReadClick() {
newsDot.value = false
proxy.$modal.msg('请自行实现!!!')
useSocketStore().readAll(noticeType.value)
}
//
function onGoToGiteeClick() {
@ -57,71 +111,78 @@ function onGoToGiteeClick() {
}
</script>
<style lang="scss">
.head-box {
display: flex;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
color: #333333;
justify-content: space-between;
height: 35px;
align-items: center;
.head-box-btn {
color: #1890ff;
font-size: 13px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
<style lang="scss" scoped>
.content-box {
font-size: 13px;
min-height: 160px;
max-height: 230px;
overflow: auto;
.content-box-item {
padding-top: 12px;
display: flex;
margin-bottom: 20px;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
&:last-of-type {
padding-bottom: 12px;
}
.content-box-msg {
color: #999999;
margin-top: 5px;
margin-bottom: 5px;
.content {
margin-left: 8px;
.name {
color: var(--el-color-primary);
}
}
.icon {
width: 30px;
height: 30px;
margin: 2px 10px 0 0;
}
.content-box-time {
color: #999999;
}
}
.content-box-empty {
height: 260px;
display: flex;
.content-box-empty-margin {
margin: auto;
text-align: center;
i {
font-size: 60px;
}
margin-top: 3px;
}
}
}
.foot-box {
height: 35px;
color: #1890ff;
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
display: flex;
align-items: center;
justify-content: center;
justify-content: space-around;
border-top: 1px solid #ebeef5;
&:hover {
opacity: 1;
}
}
:deep(.el-empty__description p) {
font-size: 13px;
.layout-navbars-breadcrumb-user-news {
position: relative;
.read {
position: absolute;
top: 7px;
right: 0;
color: var(--el-color-primary);
cursor: pointer;
z-index: 2;
font-size: 12px;
}
}
.n_right {
text-align: right;
margin: 10px;
}
</style>
<style>
.new-item {
.is-fixed {
right: -3px !important;
}
.head-box-title {
color: var(--base-color-white);
}
</style>

View File

@ -23,43 +23,43 @@ export default {
props: {
total: {
required: true,
type: Number,
type: Number
},
page: {
type: Number,
default: 1,
default: 1
},
limit: {
type: Number,
default: 20,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50, 100]
},
}
},
// 5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper',
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true,
default: true
},
autoScroll: {
type: Boolean,
default: true,
default: true
},
hidden: {
type: Boolean,
default: false,
},
default: false
}
},
setup(props, { ctx, emit }) {
const currentPage = computed({
@ -68,7 +68,7 @@ export default {
},
set(val) {
emit('update:page', val)
},
}
})
const pageSize = computed({
get() {
@ -76,7 +76,7 @@ export default {
},
set(val) {
emit('update:limit', val)
},
}
})
function handleSizeChange(val) {
@ -96,15 +96,17 @@ export default {
currentPage,
pageSize,
handleSizeChange,
handleCurrentChange,
handleCurrentChange
}
}
},
}
</script>
<style scoped>
.pagination-container {
/* background: #fff; */
padding: 32px 16px;
padding: 20px 16px 0;
display: flex;
justify-content: flex-end;
}
.pagination-container.hidden {
display: none;

View File

@ -1,6 +1,7 @@
<template>
<div>
<svg-icon :name="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
<svg-icon v-if="isFullscreen" name="exit-fullscreen" @click="toggle" />
<svg-icon v-else name="ele-FullScreen" @click="toggle" />
</div>
</template>
@ -9,14 +10,3 @@ import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen()
</script>
<style lang="scss" scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>

View File

@ -7,17 +7,17 @@ export default defineComponent({
name: {
type: String,
required: true,
default: '',
default: ''
},
className: {
type: String,
default: '',
default: ''
},
// svg
color: {
type: String,
default: '',
},
default: ''
}
},
setup(props) {
if (props.name?.startsWith('ele')) {
@ -25,9 +25,9 @@ export default defineComponent({
h(
'i',
{
class: 'el-icon',
class: 'el-icon'
},
[h(resolveComponent(props.name.replace('ele-', '')))],
[h(resolveComponent(props.name.replace('ele-', '')))]
)
} else if (props.name != undefined && props.name != '') {
return () =>
@ -38,16 +38,17 @@ export default defineComponent({
'aria-hidden': true,
style: `color: ${props.color}`,
class: `svg-icon ${props.className}`,
'shape-rendering': 'geometricPrecision'
},
h('use', {
'xlink:href': `#icon-${props.name}`,
fill: `${props.color}`,
}),
fill: `${props.color}`
})
)
} else {
return () => h('i')
}
},
}
})
</script>

View File

@ -126,7 +126,13 @@ function handleSelect(key, keyPath) {
window.open(key, '_blank')
} else if (!route || !route.children) {
//
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 {
//

View File

@ -3,7 +3,6 @@ import hasPermi from './permission/hasPermi'
import clipboard from './module/clipboard'
import drag from './module/drag'
import waves from './module/waves'
// import copyText from './module/copyText'
export default function directive(app) {
app.directive('hasRole', hasRole)
@ -11,5 +10,4 @@ export default function directive(app) {
app.directive('clipboard', clipboard)
app.directive('drag', drag)
app.directive('waves', waves)
// app.directive('copyText', copyText)
}

View File

@ -1,77 +1,67 @@
/**
* v-clipboard 文字复制剪贴
*
作者 CodePlayer
链接 https: //juejin.cn/post/7052968352007847972
来源 稀土掘金
著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处
*/
// import Clipboard from 'clipboard'
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import useClipboard from 'vue-clipboard3'
const { toClipboard } = useClipboard()
export default {
// 挂载
mounted(el, binding) {
// binding.arg 为动态指令参数
// 由于 指令是支持响应式的 因此我们指令需要有一个“全局”对象这里我们为了不借助其他对象浪费资源就直接使用el自身了
// 将copy的值 成功回调 失败回调 及 click事件都绑定到el上 这样在更新和卸载时方便操作
switch (binding.arg) {
case "copy":
const { value, arg } = binding
switch (arg) {
case 'copy':
// copy值
el.clipValue = binding.value;
el.clipValue = value
// click事件
el.clipCopy = function () {
toClipboard(el.clipValue)
.then(result => {
el.clipSuccess && el.clipSuccess(result);
.then((result) => {
el.clipSuccess && el.clipSuccess(result)
})
.catch(err => {
el.clipError && el.clipError(err);
});
};
.catch((err) => {
el.clipError && el.clipError(err)
})
}
// 绑定click事件
el.addEventListener("click", el.clipCopy);
break;
case "success":
el.addEventListener('click', el.clipCopy)
break
case 'success':
// 成功回调
el.clipSuccess = binding.value;
break;
case "error":
el.clipSuccess = binding.value
break
case 'error':
// 失败回调
el.clipError = binding.value;
break;
el.clipError = binding.value
break
}
},
// 相应修改 这里比较简单 重置相应的值即可
updated(el, binding) {
switch (binding.arg) {
case "copy":
el.clipValue = binding.value;
break;
case "success":
el.clipSuccess = binding.value;
break;
case "error":
el.clipError = binding.value;
break;
case 'copy':
el.clipValue = binding.value
break
case 'success':
el.clipSuccess = binding.value
break
case 'error':
el.clipError = binding.value
break
}
},
// 卸载 删除click事件 删除对应的自定义属性
unmounted(el, binding) {
switch (binding.arg) {
case "copy":
el.removeEventListener("click", el.clipCopy);
delete el.clipValue;
delete el.clipCopy;
break;
case "success":
delete el.clipSuccess;
break;
case "error":
delete el.clipError;
break;
case 'copy':
el.removeEventListener('click', el.clipCopy)
delete el.clipValue
delete el.clipCopy
break
case 'success':
delete el.clipSuccess
break
case 'error':
delete el.clipError
break
}
}
},
}

View File

@ -1,66 +0,0 @@
/**
* v-copyText 复制文本内容
* Copyright (c) 2022 ruoyi
*/
export default {
beforeMount(el, { value, arg }) {
if (arg === "callback") {
el.$copyCallback = value;
} else {
el.$copyValue = value;
const handler = () => {
copyTextToClipboard(el.$copyValue);
if (el.$copyCallback) {
el.$copyCallback(el.$copyValue);
}
};
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;
element.value = input;
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '');
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);
target.append(element);
element.select();
// Explicit selection workaround for iOS
element.selectionStart = 0;
element.selectionEnd = input.length;
let isSuccess = false;
try {
isSuccess = document.execCommand('copy');
} catch { }
element.remove();
if (originalRange) {
selection.removeAllRanges();
selection.addRange(originalRange);
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
return isSuccess;
}

View File

@ -5,30 +5,31 @@
*/
export default {
mounted(el, binding) {
el.classList.add('waves-effect');
binding.value && el.classList.add(`waves-${binding.value}`);
el.classList.add('waves-effect')
binding.value && el.classList.add(`waves-${binding.value}`)
function setConvertStyle(obj) {
let style = '';
let style = ''
for (let i in obj) {
if (obj.hasOwnProperty(i)) style += `${i}:${obj[i]};`;
if (obj.hasOwnProperty(i)) style += `${i}:${obj[i]};`
}
return style;
return style
}
function onCurrentClick(e) {
let elDiv = document.createElement('div');
elDiv.classList.add('waves-ripple');
el.appendChild(elDiv);
let elDiv = document.createElement('div')
elDiv.classList.add('waves-ripple')
el.appendChild(elDiv)
let styles = {
left: `${e.layerX}px`,
top: `${e.layerY}px`,
opacity: 1,
transform: `scale(${(el.clientWidth / 100) * 10})`,
'transition-duration': `750ms`,
'transition-timing-function': `cubic-bezier(0.250, 0.460, 0.450, 0.940)`,
};
elDiv.setAttribute('style', setConvertStyle(styles));
'transition-timing-function': `cubic-bezier(0.250, 0.460, 0.450, 0.940)`
}
elDiv.setAttribute('style', setConvertStyle(styles))
setTimeout(() => {
elDiv.setAttribute(
'style',
@ -36,17 +37,17 @@ export default {
opacity: 0,
transform: styles.transform,
left: styles.left,
top: styles.top,
top: styles.top
})
);
)
setTimeout(() => {
elDiv && el.removeChild(elDiv);
}, 750);
}, 450);
elDiv && el.removeChild(elDiv)
}, 750)
}, 450)
}
el.addEventListener('mousedown', onCurrentClick, false);
el.addEventListener('mousedown', onCurrentClick, false)
},
unmounted(el) {
el.addEventListener('mousedown', () => {});
},
el.addEventListener('mousedown', () => {})
}
}

View File

@ -87,9 +87,13 @@
"bottomBar": "Footer",
"identity": "Identity",
"content1": "The code is completely free and open source, easy to read and understand, and the interface is simple and beautiful, giving you one more choice and reference for your project.",
"topNav": "Top nav",
"topNav": "top nav",
"commonFuncs": "Common Functions",
"openWatermark": "Open Watermark"
"openWatermark": "Open Watermark",
"workTime": "Today's working times(min)",
"onlineClientNum": "Number of online devices",
"tagsView": "tabs",
"tagsPersist": "tabs Persist"
},
"common": {
"ok": "Ok",
@ -114,7 +118,15 @@
"female": "Female",
"male": "male",
"sex": "gender",
"systemTips": "system hint"
"systemTips": "system hint",
"default": "default",
"hidden": "hide",
"show": "show",
"system": "system",
"abnormal": "abnormal",
"unknow": "unknown",
"normal": "normal",
"disable": "deactivate"
},
"btn": {
"add": "Add",

View File

@ -89,7 +89,11 @@
"content1": "代码完全免费开源,易读易懂、界面简洁美观,给你的项目多一种选择与参考。",
"topNav": "顶部导航",
"commonFuncs": "常用功能",
"openWatermark": "开启水印"
"openWatermark": "开启水印",
"workTime": "今日工作时长",
"onlineClientNum": "在线设备数",
"tagsView": "标签页",
"tagsPersist": "标签持久化"
},
"common": {
"ok": "确定",
@ -114,7 +118,15 @@
"sex": "性别",
"male": "男",
"female": "女",
"systemTips": "系统提示"
"unknow": "未知",
"systemTips": "系统提示",
"show": "显示",
"hidden": "隐藏",
"default": "默认",
"system": "系统",
"abnormal": "异常",
"normal": "正常",
"disable": "停用"
},
"btn": {
"add": "新增",

View File

@ -89,7 +89,11 @@
"content1": "代碼完全免費開源,易讀易懂、界面簡潔美觀,給你的項目多一種選擇與參考。",
"topNav": "頂部導航",
"commonFuncs": "常用功能",
"openWatermark": "開啟水印"
"openWatermark": "開啟水印",
"workTime": "今日工作時長(分)",
"onlineClientNum": "在線設備數",
"tagsView": "標簽頁",
"tagsPersist": "標簽持久化"
},
"common": {
"ok": "確定",
@ -114,7 +118,15 @@
"female": "女",
"male": "男",
"sex": "性別",
"systemTips": "系統提示"
"systemTips": "系統提示",
"abnormal": "異常",
"normal": "正常",
"disable": "停用",
"hidden": "隱藏",
"show": "顯示",
"system": "系統",
"default": "默認",
"unknow": "未知"
},
"btn": {
"add": "新增",

View File

@ -10,6 +10,14 @@
"reLogin": "re-register",
"invalidSession": "Invalid session, or session has expired, please log in again.",
"otherLoginWay": "Other",
"register": "Sign up now"
"register": "Sign up now",
"forgotPwd": "forget",
"loginway1": "account",
"loginway2": "Phone",
"loginway3": "Scan code",
"phoneCode": "Please enter SMS verification code",
"sendPhoneCode": "Send",
"input_phoneNum": "Please enter phone number",
"tip_scan_code": "Please use the mobile app to scan the code to log in"
}
}

View File

@ -10,6 +10,14 @@
"reLogin": "重新登录",
"invalidSession": "无效的会话,或者会话已过期,请重新登录。",
"otherLoginWay": "其他登录方式",
"register": "注册"
"register": "立即注册",
"forgotPwd": "忘记密码",
"phoneCode": "请输入短信验证码",
"sendPhoneCode": "发送验证码",
"loginway1": "账号密码",
"loginway2": "手机号",
"loginway3": "扫码登录",
"input_phoneNum": "请输入手机号",
"tip_scan_code": "请使用移动端app扫码登录"
}
}

View File

@ -10,6 +10,14 @@
"reLogin": "重新登錄",
"invalidSession": "無效的會話,或者會話已過期,請重新登錄。",
"otherLoginWay": "其他登錄方式",
"register": "註冊"
"register": "註冊",
"forgotPwd": "忘記密碼",
"loginway1": "賬號密碼",
"loginway2": "手機號",
"loginway3": "掃碼登錄",
"phoneCode": "請輸入短信驗證碼",
"sendPhoneCode": "發送驗證碼",
"input_phoneNum": "請輸入手機號",
"tip_scan_code": "請使用移動端app掃碼登錄"
}
}

View File

@ -5,7 +5,7 @@ export default {
icon: '图标',
menuid: '菜单id',
menuType: '菜单类型',
sort: '排序',
sort: '菜单排序',
authorityID: '权限标识',
componentPath: '组件路径',
isShow: '是否显示',

View File

@ -5,7 +5,8 @@
:key="item.path"
:iframeId="'iframe' + index"
v-show="route.path === item.path"
:src="item.meta.link"></inner-link>
:src="iframeUrl(item.meta.link, item.query)">
</inner-link>
</transition-group>
</template>
@ -15,4 +16,14 @@ 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

@ -8,6 +8,7 @@
<div class="right-menu">
<header-search id="header-search" class="right-menu-item" />
<Notice title="通知" class="right-menu-item" />
<template v-if="appStore.device == 'desktop'">
<zr-git title="源码地址" class="right-menu-item" />
<zr-doc title="文档地址" class="right-menu-item" />
@ -15,7 +16,6 @@
</template>
<size-select title="布局大小" class="right-menu-item" />
<LangSelect title="语言设置" class="right-menu-item" />
<Notice title="通知" class="right-menu-item" />
<el-dropdown @command="handleCommand" class="right-menu-item avatar-container" trigger="hover">
<span class="avatar-wrapper">

View File

@ -21,19 +21,10 @@
<el-radio-group v-model="mode" size="small">
<el-radio label="dark">{{ $t('layout.darkMode') }}</el-radio>
<el-radio label="light">{{ $t('layout.lightMode') }}</el-radio>
<el-radio label="cafe">cafe</el-radio>
<!-- <el-radio label="contrast">contrast</el-radio> -->
<!-- <el-radio label="cafe">cafe</el-radio>
<el-radio label="contrast">contrast</el-radio> -->
</el-radio-group>
</div>
<!-- <div class="drawer-item">
<span>暗黑模式</span>
<span class="comp-style">
<el-switch v-model="isDark" class="mt-2" inline-prompt />
</span>
</div> -->
<!-- <h3 class="drawer-title">
{{ $t('layout.themeColor') }}
</h3> -->
<div class="drawer-item">
<span>{{ $t('layout.themeColor') }}</span>
<span class="comp-style quick-color-wrap">
@ -52,7 +43,7 @@
</div>
<div class="drawer-item">
<span>{{ $t('layout.open') }} Tags-Views</span>
<span>{{ $t('layout.open') }} {{ $t('layout.tagsView') }}</span>
<span class="comp-style">
<el-switch v-model="tagsView" class="drawer-switch" />
</span>
@ -90,6 +81,13 @@
</span>
</div>
<div class="drawer-item">
<span>{{ $t('layout.tagsPersist') }}</span>
<span class="comp-style">
<el-switch v-model="tabsPersist" class="drawer-switch" />
</span>
</div>
<el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">{{ $t('layout.saveConfig') }}</el-button>
@ -100,7 +98,7 @@
<script setup>
import 'element-plus/theme-chalk/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import { useDark, useCycleList, useColorMode } from '@vueuse/core'
import { useColorMode } from '@vueuse/core'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import { getLightColor } from '@/utils/index'
import { getmark } from '@/utils/wartermark'
@ -123,12 +121,10 @@ const mode = useColorMode({
modes: {
// custom colors
contrast: 'dark contrast',
cafe: 'cafe'
cafe: 'cafe',
auto: 'auto'
}
})
const { next } = useCycleList(['light', 'dark', 'cafe', 'contrast'], { initialValue: mode })
// const isDark= useDark()
/** 是否需要topnav */
const topNav = computed({
get: () => storeSettings.value.topNav,
@ -185,6 +181,14 @@ const showWatermark = computed({
changeWatermark()
}
})
/**标签持久化 */
const tabsPersist = computed({
get: () => storeSettings.value.tagsViewPersist,
set: (val) => {
settingsStore.changeSetting({ key: 'tagsViewPersist', value: val })
}
})
const changeWatermark = () => {
storeSettings.value.showWatermark ? setWatermark(useUserStore().userInfo.userName) : removeWatermark()
}
@ -254,7 +258,8 @@ function saveSetting() {
sideTheme: storeSettings.value.sideTheme,
theme: storeSettings.value.theme,
showFooter: storeSettings.value.showFooter,
showWatermark: storeSettings.value.showWatermark
showWatermark: storeSettings.value.showWatermark,
tagsViewPersist: storeSettings.value.tagsViewPersist
}
localStorage.setItem('layout-setting', JSON.stringify(layoutSetting))
setTimeout(proxy.$modal.closeLoading(), 100)
@ -302,13 +307,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;

View File

@ -5,11 +5,15 @@
<el-menu-item :index="resolvePath(onlyOneChild.path)">
<svg-icon :name="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<template v-if="onlyOneChild.meta.titleKey" #title>
{{ $t(onlyOneChild.meta.titleKey) }}
</template>
<template v-else-if="onlyOneChild.meta.title" #title>
{{ onlyOneChild.meta.title }}
<span v-if="props.isCollapse && !onlyOneChild.meta.icon">{{ hasTitle2(onlyOneChild.meta.title) }}</span>
<template #title>
<span v-if="onlyOneChild.meta.titleKey">{{ $t(onlyOneChild.meta.titleKey) }}</span>
<span v-else-if="onlyOneChild.meta.title">{{ onlyOneChild.meta.title }}</span>
<svg-icon
name="new"
color="#fff"
style="width: 50px; height: 25px"
v-if="onlyOneChild.meta.title && onlyOneChild.meta.isNew == 1 && defaultSettings.menuShowNew" />
</template>
</el-menu-item>
</app-link>
@ -20,6 +24,11 @@
<svg-icon :name="item.meta && item.meta.icon" />
<span v-if="item.meta && item.meta.titleKey">{{ $t(item.meta.titleKey) }}</span>
<span v-else-if="item.meta && item.meta.title">{{ item.meta.title }}</span>
<svg-icon
name="new"
color="#fff"
style="width: 50px; height: 25px"
v-if="item.meta.title && item.meta.isNew == 1 && defaultSettings.menuShowNew" />
</template>
<sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" />
@ -31,7 +40,7 @@
import { isExternal } from '@/utils/validate'
import AppLink from './Link'
import { getNormalPath } from '@/utils/ruoyi'
import defaultSettings from '@/settings'
const props = defineProps({
// route object
item: {
@ -45,6 +54,10 @@ const props = defineProps({
basePath: {
type: String,
default: ''
},
isCollapse: {
type: Boolean,
default: false
}
})
@ -102,4 +115,12 @@ function hasTitle(title) {
return ''
}
}
function hasTitle2(title) {
if (title.length >= 1) {
return title.charAt(0) + '...'
} else {
return ''
}
}
</script>

View File

@ -11,7 +11,12 @@
:collapse-transition="false"
background-color="transparent"
mode="vertical">
<sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:item="route"
:base-path="route.path"
:isCollapse="isCollapse" />
</el-menu>
</el-scrollbar>
</el-aside>

View File

@ -38,7 +38,6 @@
import ScrollPane from './ScrollPane'
import { getNormalPath } from '@/utils/ruoyi'
import useTagsViewStore from '@/store/modules/tagsView'
// import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { isHttp } from '@/utils/validate'
const visible = ref(false)
@ -54,7 +53,6 @@ const router = useRouter()
const visitedViews = computed(() => useTagsViewStore().visitedViews)
const routes = computed(() => usePermissionStore().routes)
// const theme = computed(() => useSettingsStore().theme);
watch(route, () => {
addTags()
@ -77,10 +75,6 @@ function isActive(r) {
}
function activeStyle(tag) {
if (!isActive(tag)) return {}
// return {
// 'background-color': theme.value,
// 'border-color': theme.value,
// }
}
function isAffix(tag) {
return tag.meta && tag.meta.affix

View File

@ -20,11 +20,14 @@
</el-header>
<el-main class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<transition name="fade-transform" mode="out-in" v-if="!dev">
<keep-alive :include="cachedViews">
<component v-if="!route.meta.link" :is="Component" :key="route.path" />
</keep-alive>
</transition>
<keep-alive :include="cachedViews" v-else>
<component v-if="!route.meta.link" :is="Component" :key="route.path" />
</keep-alive>
</router-view>
<iframe-toggle />
</el-main>
@ -46,6 +49,7 @@ import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import useTagsViewStore from '@/store/modules/tagsView'
const dev = import.meta.env.DEV
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme)
const sidebar = computed(() => useAppStore().sidebar)

View File

@ -8,10 +8,11 @@ import '@/assets/styles/index.scss' // global css
import App from './App'
import router from './router'
import directive from './directive' // directive
import vxetb from './vxe-tb'
// 注册指令
import plugins from './plugins' // plugins
import { downFile } from '@/utils/request'
import signalR from '@/utils/signalR'
import signalR from '@/signalr/signalr'
import vueI18n from './i18n/index'
import pinia from '@/store/index'
@ -41,6 +42,8 @@ import ImagePreview from '@/components/ImagePreview'
import DictTag from '@/components/DictTag'
// el-date-picker 快捷选项
import dateOptions from '@/utils/dateOptions'
// Dialog组件
import Dialog from '@/components/Dialog'
const app = createApp(App)
signalR.init(import.meta.env.VITE_APP_SOCKET_API)
@ -65,7 +68,8 @@ app.component('UploadImage', ImageUpload)
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
app.component('svg-icon', SvgIcon)
app.component('ZrDialog', Dialog)
directive(app)
vxetb(app)
app.use(pinia).use(router).use(plugins).use(ElementPlus, {}).use(elementIcons).use(vueI18n).mount('#app')

View File

@ -1,5 +1,5 @@
import router from './router'
import { ElMessage } from 'element-plus'
// import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
@ -7,9 +7,9 @@ import { isHttp } from '@/utils/validate'
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', '/socialLogin'];
const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/socialLogin', '/error']
router.beforeEach((to, from, next) => {
NProgress.start()
@ -22,19 +22,26 @@ router.beforeEach((to, from, next) => {
} else {
if (useUserStore().roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
useUserStore().getInfo().then(() => {
usePermissionStore().generateRoutes().then(accessRoutes => {
useUserStore()
.getInfo()
.then(() => {
usePermissionStore()
.generateRoutes()
.then((accessRoutes) => {
// 根据roles权限生成可访问的路由表
accessRoutes.forEach(route => {
accessRoutes.forEach((route) => {
if (!isHttp(route.path)) {
router.addRoute(route) // 动态添加可访问路由表
}
})
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
useUserStore().logOut().then(() => {
ElMessage.error(err != undefined ? err : '登录失败')
})
.catch((err) => {
useUserStore()
.logOut()
.then(() => {
// ElMessage.error(err != undefined ? err : '登录失败')
next({ path: '/' })
})
})
@ -48,7 +55,8 @@ router.beforeEach((to, from, next) => {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
console.log('to login')
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
NProgress.done()
}
}

View File

@ -59,6 +59,11 @@ export const constantRoutes = [
component: () => import('@/views/error/401'),
hidden: true
},
{
path: '/error',
component: () => import('@/views/error/Error'),
hidden: true
},
{
path: '',
component: Layout,
@ -68,7 +73,7 @@ export const constantRoutes = [
path: '/index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true, titleKey: 'menu.home' }
meta: { title: '首页', icon: 'index', affix: true, titleKey: 'menu.home' }
}
]
},
@ -85,22 +90,6 @@ export const constantRoutes = [
meta: { title: '个人中心', icon: 'user', titleKey: 'menu.personalCenter' }
}
]
},
// 不用可删掉
{
path: '',
component: Layout,
hidden: false,
meta: { title: '组件示例', icon: 'icon', noCache: 'fasle' },
children: [
{
path: 'icon',
component: () => import('@/views/components/icons/index'),
//component: () => import('@/views/business/GenDemo'),
name: 'icon',
meta: { title: '图标icon', icon: 'icon1', noCache: 'fasle', titleKey: 'menu.icon' }
}
]
}
]

View File

@ -2,7 +2,7 @@ export default {
/**
* 框架版本号
*/
version: '3.8.2',
version: '20230920',
/**
* 网页标题
*/
@ -14,7 +14,7 @@ export default {
/**
* 框架主题颜色值
*/
theme: '#FF8C00',
theme: '#409EFF',
/**
* 是否系统布局配置
*/
@ -71,7 +71,7 @@ export default {
/**
* 是否显示其他登录
*/
showOtherLogin: false,
showOtherLogin: true,
/**
* 默认大小
*/
@ -79,5 +79,21 @@ export default {
/**
* 默认语言
*/
defaultLang: 'zh-cn'
defaultLang: 'zh-cn',
/**
* 左侧菜单是否显示New标记
*/
menuShowNew: false,
/**
* 是否显示QR登录
*/
showQrLogin: true,
/**
* 是否显示手机号登录
*/
showPhoneLogin: true,
/**
* 标签页持久化
*/
tagsViewPersist: false
}

95
src/signalr/analysis.js Normal file
View File

@ -0,0 +1,95 @@
import { ElNotification, ElMessageBox } from 'element-plus'
import useSocketStore from '@/store/modules/socket'
import useUserStore from '@/store/modules/user'
import { webNotify } from '@/utils/index'
export default {
onMessage(connection) {
connection.on(MsgType.M001, (data) => {
useSocketStore().setOnlineUsers(data)
})
connection.on(MsgType.M002, (data) => {
// useUserStore().saveConnId(data)
})
// 接收后台手动推送消息
connection.on('receiveNotice', (title, data) => {
ElNotification({
type: 'info',
title: title,
message: data,
dangerouslyUseHTMLString: true,
duration: 0
})
webNotify({ title: title, body: data })
})
// 接收系统通知/公告
connection.on('moreNotice', (data) => {
if (data.code == 200) {
useSocketStore().setNoticeList(data.data)
}
})
// 接收在线用户
// connection.on('onlineUser', (data) => {
// useSocketStore().setOnlineUsers(data)
// })
// 接收强退通知
connection.on('forceUser', (data) => {
// connection.stop().then(() => {
// console.log('Connection stoped')
// })
// ElMessageBox.alert(`你的账号已被强退,原因:${data.reason || '无'}`, '提示', {
// confirmButtonText: '确定',
// callback: () => {
// }
// })
useSocketStore().setGlobalError({ code: 0, msg: `你的账号已被强退,原因:${data.reason || '无'}` })
useUserStore()
.logOut()
.then(() => {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'error'
})
})
// 接收聊天数据
connection.on('receiveChat', (data) => {
const { fromUser, message } = data
useSocketStore().setChat(data)
if (data.userid != useUserStore().userId) {
ElNotification({
title: fromUser.nickName,
message: message,
type: 'success',
duration: 3000
})
}
webNotify({ title: fromUser.nickName, body: message })
})
connection.on('onlineInfo', (data) => {
useSocketStore().getOnlineInfo(data)
})
connection.on(MsgType.LogOut, () => {
useUserStore()
.logOut()
.then(() => {
ElMessageBox.alert(`你的账号已在其他设备登录,如果不是你的操作请尽快修改密码`, '提示', {
confirmButtonText: '确定',
callback: () => {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'index'
}
})
})
})
}
}
const MsgType = {
M001: 'onlineNum',
M002: 'connId',
LogOut: 'logOut'
}

View File

@ -1,17 +1,17 @@
// 官方文档https://docs.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-6.0&viewFallbackFrom=aspnetcore-2.2&tabs=visual-studio
import * as signalR from '@microsoft/signalr'
import { getToken } from '@/utils/auth'
import { ElNotification, ElMessage } from 'element-plus'
import useSocketStore from '@/store/modules/socket'
import { webNotify } from './index'
import { ElMessage } from 'element-plus'
import cache from '@/plugins/cache'
import analysis from '@/signalr/analysis'
export default {
// signalR对象
SR: {},
// 失败连接重试次数
failNum: 4,
baseUrl: '',
init(url) {
var socketUrl = window.location.origin + url
var socketUrl = window.location.origin + url + '?clientId=' + cache.local.get('clientId')
const connection = new signalR.HubConnectionBuilder()
.withUrl(socketUrl, { accessTokenFactory: () => getToken() })
.withAutomaticReconnect() //自动重新连接
@ -19,20 +19,20 @@ export default {
.build()
this.SR = connection
// 断线重连
connection.onclose(async () => {
console.log('断开连接了')
connection.onclose(async (error) => {
console.error('断开连接了' + error)
console.assert(connection.state === signalR.HubConnectionState.Disconnected)
// 建议用户重新刷新浏览器
await this.start()
})
connection.onreconnected(() => {
connection.onreconnected((connectionId) => {
ElMessage({
message: '与服务器通讯已连接成功',
type: 'success',
duration: 2000
})
console.log('断线重新连接成功')
console.log('断线重新连接成功' + connectionId)
})
connection.onreconnecting(async () => {
@ -40,8 +40,7 @@ export default {
await this.start()
})
this.receiveMsg(connection)
analysis.onMessage(connection)
// 启动
// this.start();
},
@ -70,50 +69,5 @@ export default {
}
return false
}
},
// 接收消息处理
receiveMsg(connection) {
connection.on('onlineNum', (data) => {
useSocketStore().setOnlineUserNum(data)
})
// 接收欢迎语
connection.on('welcome', (data) => {
ElNotification.info(data)
})
// 接收后台手动推送消息
connection.on('receiveNotice', (title, data) => {
ElNotification({
type: 'info',
title: title,
message: data,
dangerouslyUseHTMLString: true,
duration: 0
})
webNotify({ title: title, body: data })
})
// 接收系统通知/公告
connection.on('moreNotice', (data) => {
if (data.code == 200) {
useSocketStore().setNoticeList(data.data)
}
})
// 接收在线用户
// connection.on('onlineUser', (data) => {
// useSocketStore().setOnlineUsers(data)
// })
// 接收聊天数据
connection.on('receiveChat', (data) => {
const title = `来自${data.userName}的消息通知`
ElNotification({
title: title,
message: data.message,
type: 'success',
duration: 0
})
webNotify({ title: title, body: data.message })
})
}
}

View File

@ -1,3 +1,5 @@
const store = createPinia()
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export default store

View File

@ -1,8 +1,20 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
const { sideTheme, theme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, showFooter, showWatermark, watermarkText } =
defaultSettings
const {
sideTheme,
theme,
showSettings,
topNav,
tagsView,
fixedHeader,
sidebarLogo,
dynamicTitle,
showFooter,
showWatermark,
watermarkText,
tagsViewPersist
} = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const useSettingsStore = defineStore('settings', {
@ -18,13 +30,13 @@ const useSettingsStore = defineStore('settings', {
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
showFooter: storageSetting.showFooter === undefined ? showFooter : storageSetting.showFooter,
showWatermark: storageSetting.showWatermark === undefined ? showWatermark : storageSetting.showWatermark,
watermarkText: storageSetting.watermarkText === undefined ? watermarkText : storageSetting.watermarkText
watermarkText: storageSetting.watermarkText === undefined ? watermarkText : storageSetting.watermarkText,
tagsViewPersist: storageSetting.tagsViewPersist === undefined ? tagsViewPersist : storageSetting.tagsViewPersist
}),
actions: {
// 修改布局设置
changeSetting(data) {
const { key, value } = data
// if (this.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(this.$state, key)) {
this[key] = value
}

View File

@ -1,10 +1,41 @@
import useUserStore from './user'
import signalR from '@/signalr/signalr'
const useSocketStore = defineStore('socket', {
persist: {
paths: ['chatMessage', 'chatList', 'sessionList', 'newChat', 'noticeIdArr', 'newNotice', 'globalErrorMsg'] //存储指定key
},
state: () => ({
onlineNum: 0,
onlineUsers: [],
noticeList: [],
noticeDot: false
//在线用户信息
onlineInfo: {},
// 聊天数据
chatList: {},
leaveUser: {},
sessionList: {},
newChat: 0,
newNotice: 0,
noticeIdArr: [],
// 全局错误提醒
globalErrorMsg: {}
}),
getters: {
/**
* 返回当前会话的消息
* @param {*} state
* @returns
*/
getMessageList(state) {
return (conversationId) => state.chatList[conversationId]
},
getSessionList(state) {
return (userid) => state.sessionList[userid] || []
},
getAllDotNum(state) {
return () => state.newChat + state.newNotice
}
},
actions: {
//更新在线人数
setOnlineUserNum(num) {
@ -13,16 +44,80 @@ const useSocketStore = defineStore('socket', {
// 更新系统通知
setNoticeList(data) {
this.noticeList = data
this.noticeDot = data.length > 0
const idArr = []
data.forEach((ele) => {
idArr.push(ele.noticeId)
})
var diffArr = idArr.filter((v) => !this.noticeIdArr.some((item) => item == v))
if (diffArr.length > 0) {
this.newNotice = diffArr.length
this.noticeIdArr = idArr
}
},
setOnlineUsers(data) {
const { onlineClients, num, leaveUser } = data
this.onlineUsers = onlineClients
this.onlineNum = num
if (leaveUser != null) {
this.leaveUser = leaveUser
}
},
getOnlineInfo(data) {
this.onlineInfo = data
},
setChat(data) {
const userStore = useUserStore()
var selfUserId = userStore.userId
var sessionId = data.toUserId
if (data.userId != selfUserId) {
sessionId = data.userId
}
var obj = this.chatList[sessionId]
if (obj && obj.length >= 50) {
this.chatList[sessionId].shift()
}
if (obj == null || obj == undefined) {
this.chatList[sessionId] = []
}
// 判断消息是否是自己发的
data.self = data.userId == selfUserId
this.chatList[sessionId].push(data)
if (selfUserId == data.userId) return
this.newChat++
if (this.sessionList[selfUserId] == undefined) {
this.sessionList[selfUserId] = []
}
var index = this.getSessionList(selfUserId).findIndex((x) => x.userId == data.userId)
this.getSessionList(selfUserId).splice(index, 1)
this.getSessionList(selfUserId).push(data)
},
// setOnlineUsers(data) {
// const { onlineNum, users } = data
// this.onlineUsers = users
// this.onlineNum = onlineNum
// },
sendChat(data) {
const { proxy } = getCurrentInstance()
console.log(data)
// console.log(JSON.stringify(data))
return new Promise((resolve, reject) => {
signalR.SR.invoke('sendMessage', data.toUserId, data.message)
.then(() => {
resolve(true)
})
.catch((err) => {
reject(false)
console.error(err.toString())
})
})
},
readAll(type) {
if (type == 0) {
this.newNotice = 0
} else if (type == 1) {
this.newChat = 0
}
},
setGlobalError(data) {
this.globalErrorMsg = data
}
}
})

View File

@ -1,4 +1,9 @@
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const useTagsViewStore = defineStore('tagsView', {
persist: {
paths: [storageSetting.tagsViewPersist ? 'visitedViews' : ''] //存储指定key
},
state: () => ({
visitedViews: [],
cachedViews: [],

View File

@ -1,7 +1,10 @@
import { login, logout, getInfo, oauthCallback } from '@/api/system/login'
import { login, logout, getInfo, oauthCallback, phoneLogin } from '@/api/system/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import useTagsViewStore from './tagsView'
import defAva from '@/assets/images/profile.jpg'
import md5 from 'js-md5'
import cache from '@/plugins/cache'
import md5 from 'crypto-js/md5'
const useUserStore = defineStore('user', {
state: () => ({
userInfo: '',
@ -12,7 +15,8 @@ const useUserStore = defineStore('user', {
permissions: [],
userId: 0,
authSource: '',
userName: ''
userName: '',
clientId: cache.local.get('clientId')
}),
actions: {
setAuthSource(source) {
@ -21,11 +25,13 @@ const useUserStore = defineStore('user', {
// 登录
login(userInfo) {
const username = userInfo.username.trim()
const password = md5(userInfo.password)
const password = md5(userInfo.password).toString()
const code = userInfo.code
const uuid = userInfo.uuid
const clientId = this.clientId
return new Promise((resolve, reject) => {
login(username, password, code, uuid)
login(username, password, code, uuid, clientId)
.then((res) => {
if (res.code == 200) {
setToken(res.data)
@ -69,6 +75,34 @@ const useUserStore = defineStore('user', {
})
})
},
// 扫码登录
scanLogin(data) {
return new Promise((resolve, reject) => {
setToken(data.token)
this.token = data.token
resolve(data.token) //then处理
})
},
// 手机号登录
phoneNumLogin(userInfo) {
return new Promise((resolve, reject) => {
phoneLogin(userInfo)
.then((res) => {
if (res.code == 200) {
setToken(res.data)
this.token = res.data
resolve() //then处理
} else {
console.log('login error ', res)
reject(res) //catch处理
}
})
.catch((error) => {
reject(error)
})
})
},
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
@ -93,7 +127,7 @@ const useUserStore = defineStore('user', {
resolve(res)
})
.catch((error) => {
console.error(error)
console.warn(error)
reject('获取用户信息失败')
})
})
@ -107,6 +141,7 @@ const useUserStore = defineStore('user', {
this.roles = []
this.permissions = []
removeToken()
useTagsViewStore().visitedViews = []
resolve(res)
})
.catch((error) => {
@ -121,6 +156,14 @@ const useUserStore = defineStore('user', {
removeToken()
resolve()
})
},
setClientId(clientId) {
this.clientId = clientId
cache.local.set('clientId', clientId)
},
refreshToken(token) {
setToken(token)
this.token = token
}
}
})

View File

@ -30,17 +30,7 @@ export function formatTime(time, option) {
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
}
}
@ -85,7 +75,7 @@ export function cleanArray(actual) {
export function param(json) {
if (!json) return ''
return cleanArray(
Object.keys(json).map(key => {
Object.keys(json).map((key) => {
if (json[key] === undefined) return ''
return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
})
@ -103,7 +93,7 @@ export function param2Obj(url) {
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
searchArr.forEach((v) => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
@ -137,7 +127,7 @@ export function objectMerge(target, source) {
if (Array.isArray(source)) {
return source.slice()
}
Object.keys(source).forEach(property => {
Object.keys(source).forEach((property) => {
const sourceProperty = source[property]
if (typeof sourceProperty === 'object') {
target[property] = objectMerge(target[property], sourceProperty)
@ -161,9 +151,7 @@ export function toggleClass(element, className) {
if (nameIndex === -1) {
classString += '' + className
} else {
classString =
classString.substr(0, nameIndex) +
classString.substr(nameIndex + className.length)
classString = classString.substr(0, nameIndex) + classString.substr(nameIndex + className.length)
}
element.className = classString
}
@ -233,7 +221,7 @@ export function deepClone(source) {
throw new Error('error arguments', 'deepClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {
Object.keys(source).forEach((keys) => {
if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = deepClone(source[keys])
} else {
@ -297,19 +285,17 @@ export function makeMap(str, expectsLowerCase) {
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase ?
val => map[val.toLowerCase()] :
val => map[val]
return expectsLowerCase ? (val) => map[val.toLowerCase()] : (val) => map[val]
}
// 首字母大小
export function titleCase(str) {
return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())
return str.replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
}
// 下划转驼峰
export function camelCase(str) {
return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase())
return str.replace(/_[a-z]/g, (str1) => str1.substr(-1).toUpperCase())
}
// 是否数字
@ -324,11 +310,11 @@ export function isNumberStr(str) {
* @returns 返回处理后的颜色值
*/
export function getLightColor(color, level) {
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(color)) return color;
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]);
let reg = /^\#?[0-9A-Fa-f]{6}$/
if (!reg.test(color)) return color
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])
}
/**
@ -337,13 +323,13 @@ export function getLightColor(color, level) {
* @returns 返回处理后的颜色值
*/
export function hexToRgb(str) {
let hexs = '';
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(str)) return str;
str = str.replace('#', '');
hexs = str.match(/../g);
for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
return hexs;
let hexs = ''
let reg = /^\#?[0-9A-Fa-f]{6}$/
if (!reg.test(str)) return str
str = str.replace('#', '')
hexs = str.match(/../g)
for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16)
return hexs
}
/**
@ -354,12 +340,11 @@ export function hexToRgb(str) {
* @returns 返回处理后的颜色值
*/
export function rgbToHex(r, g, b) {
let reg = /^\d{1,3}$/;
if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return "";
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('')}`;
let reg = /^\d{1,3}$/
if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ''
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('')}`
}
/**

View File

@ -3,7 +3,7 @@ import { ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import useUserStore from '@/store/modules/user'
import { blobValidate } from '@/utils/ruoyi'
import { blobValidate, delEmptyQueryNodes } from '@/utils/ruoyi'
import { saveAs } from 'file-saver'
let downloadLoadingInstance
@ -26,7 +26,13 @@ service.interceptors.request.use(
//将token放到请求头发送给服务器,将tokenkey放在请求头中
config.headers['Authorization'] = 'Bearer ' + getToken()
config.headers['userid'] = useUserStore().userId
config.headers['userName'] = useUserStore().userName
config.headers['userName'] = encodeURIComponent(useUserStore().userName)
}
const method = config?.method || 'get'
const header = config?.headers['Content-Type'] ?? ''
if ((method.toLowerCase() === 'post' || method.toLowerCase() === 'put') && header != 'multipart/form-data') {
config.data = delEmptyQueryNodes(config.data)
}
return config
},
@ -49,6 +55,10 @@ service.interceptors.response.use(
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res
}
var token = res.headers['x-refresh-token']
if (token) {
useUserStore().refreshToken(token)
}
if (code == 401) {
ElMessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
confirmButtonText: '重新登陆',
@ -58,7 +68,10 @@ service.interceptors.response.use(
useUserStore()
.logOut()
.then(() => {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'index'
var redirectUrl = window.location.pathname
if (location.pathname.indexOf('/login') != 0) {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'index?redirect=' + redirectUrl
}
})
})
@ -75,24 +88,34 @@ service.interceptors.response.use(
}
},
(error) => {
console.log('axios err', error)
let { message } = error
if (message == 'Network Error') {
console.error('axios err', error)
var duration = 3000
let { message, response } = error
if (response.status == 403) {
window.location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + '401'
} else if (message == 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
message = '系统接口请求超时'
} else if (message.includes('Request failed with status code 429')) {
} else if (message.includes('code 429')) {
message = '请求过于频繁,请稍后再试'
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常,请联系管理员'
if (import.meta.env.DEV) {
message = 'Oops,后端出错了,你不会连错误日志都不会看吧'
duration = 0
}
}
ElMessage({
message: message,
type: 'error',
duration: 3 * 1000,
duration: duration,
showClose: true,
grouping: true
})
return Promise.reject(error)
return Promise.reject()
}
)
@ -189,13 +212,14 @@ export async function downFile(url, params, config) {
type: 'error'
})
}
downloadLoadingInstance.close()
})
.catch((err) => {
.catch(() => {
ElMessage({
message: '下载文件出现错误,请联系管理员!',
type: 'error'
})
})
.finally(() => {
downloadLoadingInstance.close()
})
}

View File

@ -180,6 +180,29 @@ export function handleTree(data, id, parentId, children) {
}
return tree
}
/**
* 将自定义数据转换成字典
* @param {*} data 数据源
* @param {*} dictLabel dictLabel
* @param {*} dictValue dictValue
*/
export function toDict(data, dictLabel, dictValue) {
let config = {
label: dictLabel || 'dictLabel',
value: dictValue || 'dictValue'
}
var tree = []
for (let d of data) {
let label = d[config.label]
let value = d[config.value]
tree.push({ dictLabel: label, dictValue: value })
}
return tree
}
/**
* 参数处理
@ -293,3 +316,21 @@ export function getWeek(num = 0) {
var week = ['日', '一', '二', '三', '四', '五', '六']
return '星期' + week[datas]
}
// 移除空字符串null, undefined
export const delEmptyQueryNodes = (obj = {}) => {
if (Array.isArray(obj)) {
return obj
}
const params = Object.keys(obj)
.filter((key) => obj[key] !== null && obj[key] !== undefined)
.reduce(
(acc, key) => ({
...acc,
[key]: obj[key]
}),
{}
)
// console.log('过滤后参数=', params)
return params
}

View File

@ -0,0 +1,43 @@
<template>
<div class="other-login" v-if="defaultSettings.showOtherLogin">
<div class="other-tip">{{ $t('login.otherLoginWay') }}</div>
<span @click="onAuth('GITHUB')" title="github"><svg-icon name="github" className="login-icon"></svg-icon></span>
<span @click="onAuth('GITEE')" title="gitee"><svg-icon name="gitee" className="login-icon"></svg-icon></span>
</div>
</template>
<script setup>
import defaultSettings from '@/settings'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const { proxy } = getCurrentInstance()
function onAuth(type) {
userStore.setAuthSource(type)
switch (type) {
default:
// window.location.href = import.meta.env.VITE_APP_BASE_API + '/auth/Authorization?authSource=' + type
proxy.$modal.msg('请看文档怎么接入')
break
}
}
</script>
<style lang="scss" scoped>
.other-login {
padding: 0px 20px 10px;
.other-tip {
text-align: center;
color: #ccc;
font-size: 13px;
margin-top: 10px;
}
.login-icon {
width: 25px;
height: 25px;
margin-right: 20px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form" v-show="loginType == 1">
<el-form-item prop="phoneNum">
<el-input v-model="loginForm.phoneNum" type="phone" :maxlength="11" auto-complete="off" :placeholder="$t('login.input_phoneNum')">
<template #prefix>
<svg-icon name="phone" class="input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaOnOff != 'off'" :style="{ 'margin-top': captchaOnOff == 'off' ? '40px' : '' }">
<el-input v-model="loginForm.code" auto-complete="off" :placeholder="$t('login.captcha')" style="width: 63%" @keyup.enter="handleLogin">
<template #prefix>
<svg-icon name="validCode" class="input-icon" />
</template>
</el-input>
<div class="login-code">
<el-image :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</el-form-item>
<el-form-item prop="phoneCode">
<el-input v-model="loginForm.phoneCode" type="number" auto-complete="off" :placeholder="$t('login.phoneCode')" @keyup.enter="handleLogin">
<template #prefix>
<svg-icon name="validCode" class="input-icon" />
</template>
<template #append>
<el-button @click="handleSendCode" v-if="!showCounddown">{{ $t('login.sendPhoneCode') }}</el-button>
<el-countdown :value="countdownValue" format="mm:ss" @finish="handleFinish" v-else />
</template>
</el-input>
</el-form-item>
<el-form-item style="width: 100%" :style="{ 'margin-top': captchaOnOff == 'off' ? '40px' : '' }">
<el-button :loading="loading" size="default" round type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading">{{ $t('login.btnLogin') }}</span>
<span v-else> 中...</span>
</el-button>
</el-form-item>
</el-form>
</template>
<script setup name="phonelogin">
import { getCodeImg, checkMobile } from '@/api/system/login'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const loginForm = ref({
code: '',
uuid: '',
phoneCode: '',
phoneNum: ''
})
const loginRules = {
phoneNum: [{ required: true, trigger: 'blur', message: '请输入手机号码', pattern: /^1\d{10}$/ }],
phoneCode: [{ required: true, trigger: 'blur', message: '请输入短信验证码' }],
code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
}
const loginType = ref(1)
const codeUrl = ref('')
const loading = ref(false)
//
const captchaOnOff = ref('')
const redirect = ref()
redirect.value = route.query.redirect
function getCode() {
getCodeImg().then((res) => {
codeUrl.value = 'data:image/gif;base64,' + res.data.img
loginForm.value.uuid = res.data.uuid
captchaOnOff.value = res.data.captchaOff
})
}
const showCounddown = ref(false)
const countdownValue = ref(0)
function handleSendCode() {
const updateArr = ['phoneNum', 'code']
proxy.$refs.loginRef.validateField(updateArr, async (valid) => {
//
if (!valid) {
// proxy.$modal.msg('')
return
} else {
checkMobile(loginForm.value)
.then((res) => {
if (res.code == 200) {
showCounddown.value = true
countdownValue.value = Date.now() + 1000 * 60
}
})
.catch((err) => {
console.log(err)
// proxy.$modal.msgError(err.msg)
//
if (captchaOnOff.value) {
getCode()
}
})
}
})
}
function handleLogin() {
proxy.$refs.loginRef.validate((valid) => {
if (valid) {
loading.value = true
userStore
.phoneNumLogin(loginForm.value)
.then(() => {
proxy.$modal.msgSuccess(proxy.$t('login.loginSuccess'))
router.push({ path: redirect.value || '/' })
})
.catch((error) => {
console.error(error)
// proxy.$modal.msgError(error.msg)
loading.value = false
})
}
})
}
function handleFinish() {
showCounddown.value = false
}
getCode()
</script>
<style lang="scss" scoped>
@import '@/assets/styles/login.scss';
</style>

View File

@ -32,6 +32,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="翻译键值" align="center" prop="langKey" />
<el-table-column label="字典键值" align="center" prop="dictValue" sortable />
<el-table-column label="字典排序" align="center" prop="dictSort" sortable />
<el-table-column label="状态" align="center" prop="status">
@ -42,27 +43,42 @@
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="130px">
<template #default="scope">
<div v-if="scope.row.dictCode > 0">
<el-button text size="small" icon="edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">编辑</el-button>
<el-button text size="small" icon="delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除 </el-button>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" v-model="open" draggable width="500px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-row :gutter="20">
<el-col :lg="24">
<el-form-item label="字典类型">
<el-input v-model="form.dictType" :disabled="true" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="数据标签" prop="dictLabel">
<el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="翻译键值" prop="langKey">
<el-input v-model="form.langKey" placeholder="请输入翻译键值" />
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="数据键值" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入数据键值" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="样式属性" prop="cssClass">
<!-- <el-input v-model="form.cssClass" placeholder="请输入样式属性" /> -->
<el-select v-model="form.cssClass" allow-create filterable clearable="">
<el-option v-for="dict in cssClassOptions" :class="dict.value" :key="dict.value" :label="dict.label" :value="dict.value">
<span style="float: left" :class="dict.value">{{ dict.label }}</span>
@ -70,23 +86,34 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="显示排序" prop="dictSort">
<el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="回显样式" prop="listClass">
<el-select v-model="form.listClass">
<el-option v-for="item in listClassOptions" :key="item.value" :label="item.label + '(' + item.value + ')'" :value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="显示排序" prop="dictSort">
<el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in statusOptions" :key="dict.dictValue" :label="dict.dictValue">{{ dict.dictLabel }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
@ -225,7 +252,8 @@ const state = reactive({
rules: {
dictLabel: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
dictValue: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
langKey: [{ pattern: /^[A-Za-z].+$/, message: '输入格式不正确,格式login.ok', trigger: 'blur' }]
}
})

View File

@ -7,6 +7,7 @@
<template #content> {{ generateIconCode(item) }} </template>
<div class="icon-item">
<svg-icon :name="item" style="height: 40px; width: 40px" />
<span>{{ item }}</span>
</div>
</el-tooltip>
</div>
@ -17,6 +18,7 @@
<template #content> {{ generateElementIconCode(item) }} </template>
<div class="icon-item">
<el-icon><component :is="item" /></el-icon>
<span>{{ item }}</span>
</div>
</el-tooltip>
</div>
@ -54,7 +56,7 @@ function generateElementIconCode(symbol) {
margin: 20px;
height: 60px;
text-align: center;
width: 60px;
width: 77px;
float: left;
font-size: 30px;
color: #24292e;

View File

@ -0,0 +1,179 @@
<template>
<div style="height: 400px">
<div class="message_content">
<el-scrollbar height="100%" ref="scrollContainer">
<template v-for="item in conversionMsgList">
<div class="talk_item talk_primary" v-if="item.self">
<div class="head">
<el-avatar shape="square"> </el-avatar>
</div>
<div class="content">
<div class="bubble">{{ item.message }}</div>
</div>
</div>
<div class="talk_item talk_other" v-else>
<div class="head">
<el-avatar :src="item.fromUser.avatar" shape="square" />
</div>
<div class="content">
<div class="bubble">{{ item.message }}</div>
</div>
</div>
</template>
</el-scrollbar>
</div>
<div class="talk_bottom">
<div class="talk_area">
<textarea class="textarea" placeholder="请输入聊天内容" @keyup.enter="handleSend" v-model="content"></textarea>
<div class="talk_btn">
<el-button type="success" size="default" @click="handleSend">发送</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup name="msglist">
import useSocketStore from '@/store/modules/socket'
const socketStore = useSocketStore()
const { proxy } = getCurrentInstance()
const props = defineProps({
modelValue: {}
})
const conversionMsgList = computed(() => {
return socketStore.getMessageList(props.modelValue)
})
const content = ref('')
function handleSend() {
if (content.value.trim().length <= 0) {
proxy.$modal.msgError('请输入聊天内容')
return
}
var obj = {
toUserId: props.modelValue,
message: content.value
}
socketStore
.sendChat(obj)
.then(() => {
content.value = ''
scrollBottom()
})
.catch(() => {
proxy.$modal.msgError('发送失败')
})
}
/**
* 滚动聊天记录
* @param {*} type
*/
function scrollBottom(type) {
setTimeout(() => {
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef)
const height = scrollWrapper.value.scrollHeight
if (type == 1) {
scrollWrapper.value.scrollTop = height
} else {
scrollWrapper.value.scrollTo({ top: height + 120, behavior: 'smooth' })
}
}, 100)
}
watch(
() => props.modelValue,
() => {
scrollBottom(1)
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
.message_content {
width: 100%;
height: 300px;
border-top: 1px solid #ddd;
padding-top: 10px;
}
.talk_item {
position: relative;
display: flex;
height: auto;
margin-bottom: 1rem;
.head {
display: block;
width: 3rem;
height: 3rem;
float: left;
cursor: pointer;
img {
border-radius: 0.3rem;
width: 90%;
height: 90%;
}
}
.content {
display: block;
overflow: hidden;
margin-right: 0.4rem;
max-width: 70%;
.bubble {
display: inline-block;
position: relative;
text-align: left;
font-size: 1rem;
margin-right: 0.2rem;
border-radius: 0.4rem;
background: var(--el-color-success);
color: #fff;
padding: 4px 10px;
}
}
}
.talk_primary {
flex-direction: row-reverse;
padding-right: 5px;
}
.talk_item:after {
clear: both;
}
.talk_bottom {
border-top: 1px solid #ddd;
.talk_btn {
position: absolute;
right: 10px;
padding: 0 10px;
bottom: 20px;
}
.talk_area {
position: relative;
height: 100px;
.textarea {
padding: 10px 0.2rem;
width: 100%;
flex: 1;
resize: none;
background: none;
border: none;
outline: none;
height: 100%;
}
.textarea:focus {
outline: none;
}
}
}
</style>

69
src/views/error/Error.vue Normal file
View File

@ -0,0 +1,69 @@
<template>
<div class="errPage-container">
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">提示!</h1>
<h3 class="text-danger">{{ msgObj.msg }}</h3>
<!-- <h6>{{ msgObj }}</h6> -->
<div class="list-unstyled">
<router-link to="/"> 回首页 </router-link>
</div>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." />
</el-col>
</el-row>
</div>
</template>
<script setup>
import errImage from '@/assets/401_images/401.gif'
import useSocketStore from '@/store/modules/socket'
let { proxy } = getCurrentInstance()
const socketStore = useSocketStore()
const msgObj = computed(() => {
return socketStore.globalErrorMsg
})
const errGif = ref(errImage + '?' + +new Date())
</script>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none !important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>

View File

@ -39,7 +39,7 @@
<h2>ZRAdmin.NET {{ $t('layout.backstageManagement') }}</h2>
<p>
ZRAdmin.NET借鉴了很多开源项目的优点让你开发Web管理系统更简单所以我也把它给开源了前端
<code>vue页面</code>主要参考若依在此表示感谢.)
<code>vue页面</code>主要使用了若依后端参考Ruoyi SpringBoot版本在此表示感谢.)
</p>
<p>{{ $t('layout.content1') }}</p>
<p>
@ -111,7 +111,34 @@
</div>
</el-card>
</el-col>
<el-col :sm="24" :lg="8">
<el-col :sm="24" :lg="10">
<el-card>
<template #header>
<span>
移动端体验
<span style="color: red">如有需要联系作者</span>
</span>
</template>
<div class="body">
<table style="width: 100%; text-align: center">
<tr>
<td>微信小程序</td>
<td>H5</td>
</tr>
<tr>
<td>
<img src="@/assets/images/qrcode.jpg" alt="donate" style="width: 160px" />
</td>
<td>
<img src="@/assets/images/qrcodeH5.png" alt="donate" style="width: 160px" />
</td>
</tr>
</table>
</div>
</el-card>
</el-col>
<el-col :sm="24" :lg="6">
<el-card>
<template #header>
<span>{{ $t('layout.contactUs') }}</span>

View File

@ -2,7 +2,7 @@
<div class="home">
<!-- 用户信息 -->
<el-row :gutter="15">
<el-col :md="24" :lg="18" :xl="24" class="mb10">
<el-col :md="24" :lg="16" :xl="24" class="mb10">
<el-card shadow="hover">
<div class="user-item">
<div class="user-item-left">
@ -12,7 +12,10 @@
<div class="user-item-right">
<el-row>
<el-col :xs="24" :md="24" class="right-title mb20 one-text-overflow">
{{ userInfo.welcomeMessage }} <strong>{{ userInfo.nickName }}</strong> {{ userInfo.welcomeContent }}
<div class="mb10">
{{ userInfo.welcomeMessage }} <strong>{{ userInfo.nickName }}</strong>
<span>({{ userInfo.welcomeContent }})</span>
</div>
</el-col>
</el-row>
<el-row>
@ -24,10 +27,13 @@
</div>
</el-card>
</el-col>
<el-col :lg="6" class="mb10">
<el-col :lg="8" class="mb10">
<el-card style="height: 100%">
<div class="text-warning mb10">{{ currentTime }} {{ weekName }}</div>
<div>上次登录时间{{ userInfo.loginDate }}</div>
<div class="work-wrap">
<el-statistic :title="$t('layout.workTime')" :formatter="workTimeFormatter" :value="onlineInfo.todayOnlineTime" />
<el-statistic :title="$t('layout.onlineClientNum')" :value="onlineInfo.clientNum" />
</div>
</el-card>
</el-col>
</el-row>
@ -41,7 +47,7 @@
<el-button text @click="handleAdd()">{{ $t('btn.add') }}</el-button>
</div>
</template>
<div class="info">
<div>
<el-scrollbar wrap-class="scrollbar-wrapper"> <CommonMenu v-model="showEdit"></CommonMenu></el-scrollbar>
</div>
</el-card>
@ -92,8 +98,13 @@ import PieChart from './dashboard/PieChart'
import BarChart from './dashboard/BarChart'
// import WordCloudChat from './dashboard/WordCloud.vue'
import CommonMenu from './components/CommonMenu'
import dayjs from 'dayjs'
//
import duration from 'dayjs/plugin/duration'
dayjs.extend(duration)
import useUserStore from '@/store/modules/user'
import useSocketStore from '@/store/modules/socket'
import { getWeek } from '@/utils/ruoyi'
const showEdit = ref(false)
const data = {
@ -118,6 +129,9 @@ const { proxy } = getCurrentInstance()
const userInfo = computed(() => {
return useUserStore().userInfo
})
const onlineInfo = computed(() => {
return useSocketStore().onlineInfo
})
const currentTime = computed(() => {
return proxy.parseTime(new Date(), 'YYYY-MM-DD')
})
@ -134,6 +148,9 @@ handleSetLineChartData('newVisitis')
function handleAdd() {
proxy.$modal.msg('请通过搜索添加')
}
function workTimeFormatter(val) {
return dayjs.duration(val * 60, 'second').format('HH时mm分')
}
</script>
<style lang="scss" scoped>
@ -165,6 +182,19 @@ function handleAdd() {
height: 200px;
// overflow-y: scroll;
}
.work-wrap {
display: grid;
grid-template-columns: repeat(2, 50%);
.item {
text-align: center;
.name {
color: #606666;
}
}
}
}
.chart-wrapper {
background: var(--base-bg-main);

View File

@ -1,10 +1,20 @@
<template>
<starBackground></starBackground>
<div class="login-wrap">
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ defaultSettings.title }}</h3>
<LangSelect title="多语言设置" class="langSet" />
<div style="padding: 0 25px 5px 25px">
<el-tabs v-model="loginType" @tab-click="handleLoginType">
<el-tab-pane :label="$t('login.loginway1')" :name="1"></el-tab-pane>
<el-tab-pane :label="$t('login.loginway2')" :name="2" v-if="defaultSettings.showPhoneLogin"></el-tab-pane>
<el-tab-pane :label="$t('login.loginway3')" :name="3" v-if="defaultSettings.showQrLogin"></el-tab-pane>
</el-tabs>
</div>
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form" v-show="loginType == 1">
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" :placeholder="$t('login.account')">
<template #prefix>
@ -26,33 +36,36 @@
</template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
<el-image :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</el-form-item>
<div style="display: flex; justify-content: space-between">
<el-checkbox v-model="loginForm.rememberMe">{{ $t('login.rememberMe') }}</el-checkbox>
<span style="font-size: 12px">
<span @click="handleForgetPwd()" class="forget-pwd">忘记密码</span>
<router-link class="link-type" :to="'/register'">{{ $t('login.register') }}</router-link>
</span>
</div>
<el-form-item style="width: 100%">
<el-button :loading="loading" size="default" type="primary" style="width: 100%" @click.prevent="handleLogin">
<el-form-item style="width: 100%" :style="{ 'margin-top': captchaOnOff == 'off' ? '40px' : '' }">
<el-button :loading="loading" size="default" round type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading">{{ $t('login.btnLogin') }}</span>
<span v-else> 中...</span>
</el-button>
</el-form-item>
<div class="other-login" v-if="defaultSettings.showOtherLogin">
<el-divider>{{ $t('login.otherLoginWay') }}</el-divider>
<span @click="onAuth('GITHUB')" title="github"><svg-icon name="github" className="login-icon"></svg-icon></span>
<span @click="onAuth('GITEE')" title="gitee"><svg-icon name="gitee" className="login-icon"></svg-icon></span>
<div style="display: flex; justify-content: space-between; align-items: center">
<el-checkbox v-model="loginForm.rememberMe">{{ $t('login.rememberMe') }}</el-checkbox>
<span style="font-size: 12px">
<router-link class="link-type" :to="'/register'">{{ $t('login.register') }}</router-link>
<span @click="handleForgetPwd()" class="forget-pwd">{{ $t('login.forgotPwd') }}</span>
</span>
</div>
</el-form>
<div class="qr-wrap login-form" v-show="loginType == 3">
<div class="login-scan-container">
<div ref="imgContainerRef" id="imgContainer" class="qrCode"></div>
<div class="mt10 text-muted">{{ $t('login.tip_scan_code') }}</div>
</div>
</div>
<phoneLogin v-show="loginType == 2"></phoneLogin>
<oauthLogin v-show="defaultSettings.showOtherLogin"></oauthLogin>
</div>
<!-- 底部 -->
<div class="el-login-footer">
<div v-html="defaultSettings.copyright"></div>
</div>
@ -67,6 +80,13 @@ import defaultSettings from '@/settings'
import starBackground from '@/views/components/starBackground.vue'
import LangSelect from '@/components/LangSelect/index.vue'
import useUserStore from '@/store/modules/user'
import QRCode from 'qrcodejs2-fixes'
import { verifyScan, generateQrcode } from '@/api/system/login'
import oauthLogin from './components/Login/oauthLogin.vue'
import phoneLogin from './components/Login/phoneLogin.vue'
var visitorId = ''
const fpPromise = import('https://openfpcdn.io/fingerprintjs/v3').then((FingerprintJS) => FingerprintJS.load())
const userStore = useUserStore()
const router = useRouter()
@ -86,7 +106,7 @@ const loginRules = {
password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
}
const loginType = ref(1)
const codeUrl = ref('')
const loading = ref(false)
//
@ -95,7 +115,21 @@ const captchaOnOff = ref('')
const register = ref(false)
const redirect = ref()
redirect.value = route.query.redirect
// Get the visitor identifier when you need it.
fpPromise
.then((fp) => fp.get())
.then((result) => {
// This is the visitor identifier:
visitorId = result.visitorId
userStore.setClientId(visitorId)
})
watch(
route,
(newRoute) => {
redirect.value = newRoute.query && newRoute.query.redirect
},
{ immediate: true }
)
function handleLogin() {
proxy.$refs.loginRef.validate((valid) => {
if (valid) {
@ -116,7 +150,14 @@ function handleLogin() {
.login(loginForm.value)
.then(() => {
proxy.$modal.msgSuccess(proxy.$t('login.loginSuccess'))
router.push({ path: redirect.value || '/' })
const query = route.query
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== 'redirect') {
acc[cur] = query[cur]
}
return acc
}, {})
router.push({ path: redirect.value || '/', query: otherQueryParams })
})
.catch((error) => {
console.error(error)
@ -149,18 +190,80 @@ function getCookie() {
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
}
function onAuth(type) {
userStore.setAuthSource(type)
switch (type) {
default:
window.location.href = import.meta.env.VITE_APP_BASE_API + '/auth/Authorization?authSource=' + type
break
}
}
function handleForgetPwd() {
proxy.$modal.msg('请联系管理员')
}
const interval = ref(null)
function handleShowQrLogin() {
nextTick(() => {
generateCode()
})
}
//
function generateCode() {
clearQr()
var uuid = getUuid()
document.getElementById('imgContainer').innerHTML = '正在生成中...'
generateQrcode({ uuid, deviceId: visitorId }).then((res) => {
const { code, data } = res
document.getElementById('imgContainer').innerHTML = ''
if (code == 200) {
new QRCode(document.getElementById('imgContainer'), {
// text: 'https://qm.qq.com/cgi-bin/qm/qr?k=kgt4HsckdljU0VM-0kxND6d_igmfuPlL&authKey=r55YUbruiKQ5iwC/folG7KLCmZ++Y4rQVgNlvLbUniUMkbk24Y9+zNuOmOnjAjRc&noverify=0',
text: JSON.stringify(data.codeContent),
width: 160,
height: 160
})
}
})
interval.value = setInterval(() => {
verifyScan({ uuid: uuid, deviceId: userStore.clientId })
.then((res) => {
const { code, data } = res
if (data.status == -1) {
clearQr()
document.getElementById('imgContainer').innerHTML = '二维码已过期'
} else if (data.status == 2) {
userStore
.scanLogin(data)
.then(() => {
proxy.$modal.msgSuccess(proxy.$t('login.loginSuccess'))
router.push({ path: redirect.value || '/' })
})
.catch((error) => {
console.error(error)
proxy.$modal.msgError(error.msg)
})
clearQr()
}
})
.catch(() => {
clearQr()
})
}, 1000)
}
function clearQr() {
clearInterval(interval.value)
interval.value = null
}
function getUuid() {
var temp_url = URL.createObjectURL(new Blob())
var uuid = temp_url.toString().replace('-', '') // blob:https://xxx.com/b250d159-e1b6-4a87-9002-885d90033be3
URL.revokeObjectURL(temp_url)
return uuid.substr(uuid.lastIndexOf('/') + 1)
}
function handleLoginType(t) {
const val = t.paneName
if (val == 3) {
handleShowQrLogin()
} else {
clearQr()
}
}
getCode()
getCookie()
</script>
@ -169,16 +272,14 @@ getCookie()
@import '@/assets/styles/login.scss';
.forget-pwd {
color: #ccc;
margin-right: 10px;
margin-left: 10px;
cursor: pointer;
border-left: 1px solid;
padding-left: 10px;
}
.login-icon {
width: 30px;
height: 30px;
margin-right: 20px;
cursor: pointer;
}
.other-login {
padding: 0px 10px 5px;
.qrCode {
width: 160px;
height: 160px;
line-height: 160px;
}
</style>

View File

@ -0,0 +1,204 @@
<!--
* @Descripttion: (短信验证码记录/smsCode_log)
* @Author: (zz)
* @Date: (2023-11-19)
-->
<template>
<div>
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
<el-form-item label="用户id" prop="userid">
<el-input v-model.number="queryParams.userid" placeholder="请输入用户id" />
</el-form-item>
<el-form-item label="手机号" prop="phoneNum">
<el-input v-model.number="queryParams.phoneNum" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="发送时间">
<el-date-picker
v-model="dateRangeAddTime"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="defaultTime"
:shortcuts="dateOptions">
</el-date-picker>
</el-form-item>
<el-form-item label="发送类型" prop="sendType">
<el-select clearable v-model="queryParams.sendType" placeholder="请选择发送类型">
<el-option v-for="item in options.sendTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue">
<span class="fl">{{ item.dictLabel }}</span>
<span class="fr" style="color: var(--el-text-color-secondary)">{{ item.dictValue }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
</el-form-item>
</el-form>
<!-- 工具区域 -->
<el-row :gutter="15" class="mb10">
<el-col :span="1.5">
<el-button type="warning" plain icon="download" @click="handleExport" v-hasPermi="['smscodelog:export']">
{{ $t('btn.export') }}
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table
:data="dataList"
v-loading="loading"
ref="table"
border
header-cell-class-name="el-table-header-cell"
highlight-current-row
@sort-change="sortChange">
<el-table-column prop="id" label="Id" align="center" width="170" v-if="columns.showColumn('id')" />
<el-table-column prop="userid" label="用户id" align="center" v-if="columns.showColumn('userid')" />
<el-table-column prop="userIP" label="用户IP" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('userIP')" />
<el-table-column prop="location" label="位置" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('userIP')" />
<el-table-column prop="phoneNum" label="手机号" align="center" v-if="columns.showColumn('phoneNum')" />
<el-table-column prop="smsCode" label="短信验证码" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('smsCode')" />
<el-table-column prop="smsContent" label="短信内容" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('smsContent')" />
<el-table-column prop="addTime" label="发送时间" :show-overflow-tooltip="true" v-if="columns.showColumn('addTime')" />
<el-table-column prop="sendType" label="发送类型" align="center" v-if="columns.showColumn('sendType')">
<template #default="scope">
<dict-tag :options="options.sendTypeOptions" :value="scope.row.sendType" />
</template>
</el-table-column>
<el-table-column label="操作" width="60">
<template #default="scope">
<el-button
type="danger"
size="small"
icon="delete"
title="删除"
v-hasPermi="['smscodelog:delete']"
@click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup name="smscodelog">
import { listSmscodeLog, delSmscodeLog } from '@/api/system/smscodelog.js'
const { proxy } = getCurrentInstance()
const ids = ref([])
const loading = ref(false)
const showSearch = ref(true)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
sort: 'Id',
sortType: 'desc',
userid: undefined,
phoneNum: undefined,
addTime: undefined,
sendType: undefined
})
const columns = ref([
{ visible: true, prop: 'id', label: 'Id' },
{ visible: true, prop: 'smsCode', label: '短信验证码' },
{ visible: true, prop: 'userid', label: '用户id' },
{ visible: true, prop: 'phoneNum', label: '手机号' },
{ visible: true, prop: 'smsContent', label: '短信内容' },
{ visible: true, prop: 'addTime', label: '发送时间' },
{ visible: true, prop: 'userIP', label: '用户IP' },
{ visible: true, prop: 'sendType', label: '发送类型' }
])
const total = ref(0)
const dataList = ref([])
const queryRef = ref()
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
//
const dateRangeAddTime = ref([])
var dictParams = []
function getList() {
proxy.addDateRange(queryParams, dateRangeAddTime.value, 'AddTime')
loading.value = true
listSmscodeLog(queryParams).then((res) => {
const { code, data } = res
if (code == 200) {
dataList.value = data.result
total.value = data.totalNum
loading.value = false
}
})
}
//
function handleQuery() {
queryParams.pageNum = 1
getList()
}
//
function resetQuery() {
//
dateRangeAddTime.value = []
proxy.resetForm('queryRef')
handleQuery()
}
//
function sortChange(column) {
var sort = undefined
var sortType = undefined
if (column.prop != null && column.order != null) {
sort = column.prop
sortType = column.order
}
queryParams.sort = sort
queryParams.sortType = sortType
handleQuery()
}
/*************** form操作 ***************/
const state = reactive({
single: true,
multiple: true,
form: {},
options: {
// eg:{ dictLabel: '', dictValue: '0'}
sendTypeOptions: [{ dictLabel: '登录', dictValue: '1' }]
}
})
const { form, options } = toRefs(state)
//
function handleDelete(row) {
const Ids = row.id || ids.value
proxy
.$confirm('是否确认删除参数编号为"' + Ids + '"的数据项?')
.then(function () {
return delSmscodeLog(Ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
}
//
function handleExport() {
proxy
.$confirm('是否确认导出短信验证码记录数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await proxy.downFile('/system/SmscodeLog/export', { ...queryParams })
})
}
handleQuery()
</script>

View File

@ -0,0 +1,335 @@
<!--
* @Descripttion: (数据差异日志/SqlDiffLog)
* @Author: (zz)
* @Date: (2023-08-17)
-->
<template>
<div>
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
<el-form-item label="表名" prop="tableName">
<el-input v-model="queryParams.tableName" placeholder="请输入表名" />
</el-form-item>
<el-form-item label="差异类型" prop="diffType">
<el-select clearable v-model="queryParams.diffType" placeholder="请选择差异类型">
<el-option v-for="item in options.diffTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue">
<span class="fl">{{ item.dictLabel }}</span>
<span class="fr" style="color: var(--el-text-color-secondary)">{{ item.dictValue }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="操作用户名" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入操作用户名" />
</el-form-item>
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRangeAddTime"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="defaultTime"
:shortcuts="dateOptions">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
</el-form-item>
</el-form>
<!-- 工具区域 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="download" @click="handleExport" v-hasPermi="['sqldifflog:export']">
{{ $t('btn.export') }}
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table
:data="dataList"
v-loading="loading"
ref="table"
border
header-cell-class-name="el-table-header-cell"
highlight-current-row
@sort-change="sortChange">
<el-table-column prop="pId" label="主键" align="center" v-if="columns.showColumn('pId')" width="150" />
<el-table-column prop="tableName" label="表名" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('tableName')" />
<el-table-column prop="diffType" label="操作类型" align="center" v-if="columns.showColumn('diffType')">
<template #default="scope">
<dict-tag :options="options.diffTypeOptions" :value="scope.row.diffType" />
</template>
</el-table-column>
<el-table-column
prop="businessData"
label="业务数据内容"
align="center"
:show-overflow-tooltip="true"
v-if="columns.showColumn('businessData')" />
<el-table-column prop="sql" label="执行sql语句" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('sql')" />
<el-table-column prop="beforeData" label="变更前数据" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('beforeData')" />
<el-table-column prop="afterData" label="变更后数据" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('afterData')" />
<el-table-column prop="userName" label="操作用户名" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('userName')" />
<el-table-column prop="addTime" label="操作时间" :show-overflow-tooltip="true" v-if="columns.showColumn('addTime')" />
<el-table-column prop="configId" label="数据库配置id" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('configId')" />
<el-table-column label="操作" width="130">
<template #default="scope">
<el-button text type="primary" icon="view" title="详情" @click="handlePreview(scope.row)">详细</el-button>
<el-button v-hasPermi="['sqldifflog:delete']" type="danger" icon="delete" title="删除" text @click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改数据差异日志对话框 -->
<zr-dialog :title="title" :lock-scroll="false" v-model="open" @close="cancel">
<el-form ref="formRef" :model="form" label-width="100px">
<el-row :gutter="20">
<el-col :lg="12">
<el-form-item label="主键" prop="pId">
<el-input v-model.number="form.pId" placeholder="请输入主键" :disabled="opertype != 1" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="表名" prop="tableName">
<el-input v-model="form.tableName" disabled placeholder="请输入表名" />
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="业务数据内容" prop="businessData">
<el-input type="textarea" v-model="form.businessData" placeholder="请输入业务数据内容" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="操作类型" prop="diffType">
<dict-tag :options="options.diffTypeOptions" :value="form.diffType"></dict-tag>
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="执行sql语句" prop="sql">
<code class="hljs" v-html="highlightedCode(form.sql)"></code>
<!-- <el-input type="textarea" v-model="form.sql" placeholder="请输入执行sql语句" /> -->
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="变更前数据" prop="beforeData">
<code class="hljs" v-html="highlightedCode(form.beforeData)"></code>
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="变更后数据">
<code class="hljs" v-html="highlightedCode(form.afterData)"></code>
</el-form-item>
</el-col>
<el-col :lg="24">
<code-diff :old-string="form.beforeData" :new-string="form.afterData" language="json" output-format="side-by-side" />
</el-col>
<el-col :lg="12">
<el-form-item label="操作用户名" prop="userName">
<el-input v-model="form.userName" placeholder="请输入操作用户名" disabled />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="记录时间" prop="addTime">
<el-date-picker v-model="form.addTime" disabled type="datetime" :teleported="false" placeholder="选择日期时间"></el-date-picker>
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="数据库配置id" prop="configId">
<el-input v-model="form.configId" disabled placeholder="请输入数据库配置id" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer v-if="opertype != 3">
<el-button text @click="cancel">{{ $t('btn.cancel') }}</el-button>
</template>
</zr-dialog>
</div>
</template>
<script setup name="sqldifflog">
import { listSqlDiffLog, delSqlDiffLog } from '@/api/monitor/sqldifflog.js'
import { CodeDiff } from 'v-code-diff'
import hljs from 'highlight.js'
import 'highlight.js/styles/default.css' //
const { proxy } = getCurrentInstance()
const ids = ref([])
const loading = ref(false)
const showSearch = ref(true)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
sort: 'PId',
sortType: 'desc',
tableName: undefined,
diffType: undefined,
userName: undefined,
addTime: undefined
})
const columns = ref([
{ visible: true, prop: 'pId', label: '主键' },
{ visible: true, prop: 'tableName', label: '表名' },
{ visible: true, prop: 'businessData', label: '业务数据内容' },
{ visible: true, prop: 'diffType', label: '差异类型' },
{ visible: true, prop: 'sql', label: '执行sql语句' },
{ visible: true, prop: 'beforeData', label: '变更前数据' },
{ visible: true, prop: 'afterData', label: '变更后数据' },
{ visible: true, prop: 'userName', label: '操作用户名' },
{ visible: false, prop: 'addTime', label: '操作时间' },
{ visible: false, prop: 'configId', label: '数据库配置id' }
])
const total = ref(0)
const dataList = ref([])
const queryRef = ref()
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
// AddTime
const dateRangeAddTime = ref([])
var dictParams = []
function getList() {
proxy.addDateRange(queryParams, dateRangeAddTime.value, 'AddTime')
loading.value = true
listSqlDiffLog(queryParams).then((res) => {
const { code, data } = res
if (code == 200) {
dataList.value = data.result
total.value = data.totalNum
loading.value = false
}
})
}
//
function handleQuery() {
queryParams.pageNum = 1
getList()
}
//
function resetQuery() {
// AddTime
dateRangeAddTime.value = []
proxy.resetForm('queryRef')
handleQuery()
}
//
function sortChange(column) {
var sort = undefined
var sortType = undefined
if (column.prop != null && column.order != null) {
sort = column.prop
sortType = column.order
}
queryParams.sort = sort
queryParams.sortType = sortType
handleQuery()
}
/*************** form操作 ***************/
const formRef = ref()
const title = ref('')
// 1add 2edit 3view
const opertype = ref(0)
const open = ref(false)
const state = reactive({
single: true,
multiple: true,
form: {},
options: {
// eg:{ dictLabel: '', dictValue: '0'}
diffTypeOptions: [
{ dictLabel: 'insert', dictValue: 'insert' },
{ dictLabel: 'update', dictValue: 'update', listClass: 'success' },
{ dictLabel: 'delete', dictValue: 'delete', listClass: 'danger' }
]
}
})
const { form, options } = toRefs(state)
// dialog
function cancel() {
open.value = false
reset()
}
//
function reset() {
form.value = {
pId: null,
tableName: null,
businessData: null,
diffType: null,
sql: null,
beforeData: null,
afterData: null,
userName: null,
addTime: null,
configId: null
}
proxy.resetForm('formRef')
}
//
function handleDelete(row) {
const Ids = row.pId || ids.value
proxy
.$confirm('是否确认删除参数编号为"' + Ids + '"的数据项?')
.then(function () {
return delSqlDiffLog(Ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
/**
* 查看
* @param {*} row
*/
function handlePreview(row) {
reset()
open.value = true
title.value = '查看'
opertype.value = 3
form.value = { ...row }
}
//
function handleExport() {
proxy
.$confirm('是否确认导出数据差异日志数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await proxy.downFile('/monitor/SqlDiffLog/export', { ...queryParams })
})
}
function highlightedCode(code) {
const result = hljs.highlightAuto(code)
return result.value || '&nbsp;'
}
handleQuery()
</script>

View File

@ -7,13 +7,13 @@
</el-select>
</el-form-item>
<el-form-item prop="queryText">
<el-input
v-model="queryParams.queryText"
placeholder="请输入计划任务名称"
clearable
prefix-icon="el-icon-search"
@keyup.enter="handleQuery"
@clear="handleQuery" />
<el-input v-model="queryParams.queryText" placeholder="请输入计划任务名称" clearable @keyup.enter="handleQuery" @clear="handleQuery" />
</el-form-item>
<el-form-item>
<el-radio-group v-model="viewSwitch">
<el-radio-button label="1">表格</el-radio-button>
<el-radio-button label="2">卡片</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">{{ $t('btn.search') }}</el-button>
@ -39,8 +39,7 @@
</el-col>
<right-toolbar :showSearch="searchToggle" :columns="columns" @queryTable="handleQuery"></right-toolbar>
</el-row>
<el-row>
<el-table ref="tasks" v-loading="loading" :data="dataTasks" border="" row-key="id" @sort-change="handleSortable">
<el-table v-if="viewSwitch == 1" ref="tasks" v-loading="loading" :data="dataTasks" border row-key="id" @sort-change="handleSortable">
<!-- <el-table-column type="index" :index="handleIndexCalc" label="#" align="center" /> -->
<el-table-column prop="id" label="id" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('id')" />
<el-table-column prop="name" label="任务名称" width="100" />
@ -56,7 +55,7 @@
</el-table-column>
<el-table-column sortable prop="isStart" align="center" label="任务状态" width="100">
<template #default="scope">
<dict-tag :value="scope.row.isStart" :options="isStartOptions"></dict-tag>
<dict-tag :value="scope.row.isStart" :options="options.isStartOptions"></dict-tag>
</template>
</el-table-column>
<el-table-column
@ -138,8 +137,51 @@
</template>
</el-table-column>
</el-table>
<pagination v-model:total="total" v-model:page="queryParams.PageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-row :gutter="20" v-if="viewSwitch == 2">
<el-col v-for="item in dataTasks" :lg="8" :span="24">
<el-card :body-style="{ padding: '15px 15px 0' }">
<el-descriptions :column="1" :title="item.name" size="small" border>
<el-descriptions-item label="任务类型">
<dict-tag :options="options.taskTypeOptions" :value="item.taskType" />
</el-descriptions-item>
<el-descriptions-item label="触发器类型" width="90px">
<dict-tag :options="options.triggerTypeOptions" :value="item.triggerType" />
</el-descriptions-item>
<el-descriptions-item label="任务状态" width="90px">
<dict-tag :options="options.isStartOptions" :value="item.isStart"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="任务分组" width="90px">
{{ item.jobGroup }}
</el-descriptions-item>
<el-descriptions-item label="程序集" width="90px">
{{ item.assemblyName }}
</el-descriptions-item>
<el-descriptions-item label="最后运行时间" width="90px">
{{ item.lastRunTime }}
</el-descriptions-item>
<el-descriptions-item label="运行表达式" width="90px">
{{ item.cron }}
</el-descriptions-item>
<el-descriptions-item label="运行次数" width="90px">
{{ item.runTimes }}
</el-descriptions-item>
<el-descriptions-item label="apiUrl" width="90px">
{{ item.apiUrl }}
</el-descriptions-item>
</el-descriptions>
<div>
<el-button text icon="view" v-hasPermi="['monitor:job:query']" @click="handleDetails(item)">
{{ $t('btn.details') }}
</el-button>
<el-button text icon="view" v-hasPermi="['monitor:job:query']" @click="handleJobLog(item)">
{{ $t('btn.log') }}
</el-button>
</div>
</el-card>
</el-col>
</el-row>
<pagination v-model:total="total" v-model:page="queryParams.PageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="title" v-model="open" width="600px" draggable append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
@ -168,7 +210,11 @@
<el-col :lg="12">
<el-form-item label="任务分组" maxlength="200" prop="jobGroup">
<el-select v-model="form.jobGroup" placeholder="请选择任务分组">
<el-option v-for="dict in jobGroupOptions" :key="dict.dictValue" :label="dict.dictLabel" :value="dict.dictValue"></el-option>
<el-option
v-for="dict in options.jobGroupOptions"
:key="dict.dictValue"
:label="dict.dictLabel"
:value="dict.dictValue"></el-option>
</el-select>
</el-form-item>
</el-col>
@ -368,15 +414,7 @@ const jobLogList = ref([])
const logTitle = ref('')
const formRef = ref(null)
const queryRef = ref(null)
//
const isStartOptions = ref([
{ dictLabel: '运行中', dictValue: '1', listClass: 'success' },
{ dictLabel: '已停止', dictValue: '0', listClass: 'danger' }
])
//
const jobGroupOptions = ref([])
const viewSwitch = ref(1)
const state = reactive({
form: {},
//
@ -404,7 +442,14 @@ const state = reactive({
{ dictLabel: '程序集', dictValue: '1' },
{ dictLabel: 'api请求', dictValue: '2', listClass: 'danger' },
{ dictLabel: 'sql脚本', dictValue: '3', listClass: 'info' }
]
],
//
isStartOptions: [
{ dictLabel: '运行中', dictValue: '1', listClass: 'success' },
{ dictLabel: '已停止', dictValue: '0', listClass: 'danger' }
],
//
jobGroupOptions: []
}
})
//
@ -540,10 +585,17 @@ function submitForm() {
})
}
//
function handleSortable(val) {
queryParams.orderby = val.prop
queryParams.sort = val.order
getList()
function handleSortable(column) {
var sort = undefined
var sortType = undefined
if (column.prop != null && column.order != null) {
sort = column.prop
sortType = column.order
}
queryParams.sort = sort
queryParams.sortType = sortType
handleQuery()
}
//
function reset() {
@ -588,8 +640,9 @@ function handleExport() {
getList()
proxy.getDicts('sys_job_group').then((response) => {
jobGroupOptions.value = response.data
state.options.jobGroupOptions = response.data
})
watch(
() => form.value.triggerType,
(val) => {

View File

@ -15,7 +15,6 @@
<el-form-item label="登录时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
@ -42,12 +41,17 @@
<right-toolbar :showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table v-loading="loading" :data="list" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="访问编号" align="center" prop="infoId" width="80" />
<el-table-column label="用户名称" align="center" prop="userName" />
<el-table-column label="登录地址" align="center" prop="ipaddr" width="130" :show-overflow-tooltip="true" />
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
<el-table-column label="登录地址" align="center" prop="ipaddr" width="130">
<template #default="{ row }">
<div>{{ row.loginLocation }}</div>
<div>{{ row.ipaddr }}</div>
</template>
</el-table-column>
<!-- <el-table-column label="登录地点" align="center" prop="loginLocation" /> -->
<el-table-column label="浏览器" align="center" prop="browser" />
<el-table-column label="操作系统" align="center" prop="os" />
<el-table-column label="操作状态" align="center" prop="status">
@ -61,6 +65,11 @@
<span>{{ scope.row.loginTime }}</span>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button type="danger" text plain icon="delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
@ -92,7 +101,7 @@ const queryParams = reactive({
pageSize: 10,
ipaddr: undefined,
userName: undefined,
status: undefined,
status: undefined
})
const { proxy } = getCurrentInstance()
@ -142,7 +151,7 @@ function handleDelete(row) {
.$confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
type: 'warning'
})
.then(function () {
return delLogininfor(infoIds)
@ -158,7 +167,7 @@ function handleClean() {
.$confirm('是否确认清空所有登录日志数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
type: 'warning'
})
.then(function () {
return cleanLogininfor()
@ -174,7 +183,7 @@ function handleExport() {
.$confirm('是否确认导出所有操作日志数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
type: 'warning'
})
.then(function () {
return exportLogininfor(queryParams)

View File

@ -1,62 +1,84 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form-item>
<el-button plain type="primary" @click="onLockAll()" icon="lock" v-hasPermi="['monitor:online:forceLogout']">全部强退</el-button>
</el-form-item>
<el-form-item>
<el-radio-group v-model="viewSwitch">
<el-radio-button label="1">表格</el-radio-button>
<el-radio-button label="2">卡片</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">刷新</el-button>
<!-- <el-button icon="Refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button> -->
</el-form-item>
</el-form>
<el-table :data="onlineUsers" ref="tableRef" border highlight-current-row>
<!-- <el-table-column prop="connnectionId" label="连接id"></el-table-column> -->
<el-table :data="onlineUsers" v-loading="loading" ref="tableRef" border highlight-current-row v-if="viewSwitch == 1">
<el-table-column label="No" type="index" width="50" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="name" label="用户名" align="center" />
<el-table-column prop="userIP" label="用户IP" align="center" />
<el-table-column prop="location" label="登录地点" align="center" />
<el-table-column prop="browser" label="登录浏览器"></el-table-column>
<el-table-column prop="loginTime" label="登录时间">
<el-table-column label="登录地点" prop="location" align="center"> </el-table-column>
<el-table-column label="登录IP" prop="userIP" align="center"></el-table-column>
<el-table-column prop="browser" label="登录浏览器" width="210"></el-table-column>
<el-table-column prop="platform" label="登录平台" align="center"></el-table-column>
<el-table-column prop="loginTime" label="登录时间" witdh="280px">
<template #default="scope">
{{ dayjs(scope.row.loginTime).format('MM/DD日HH:mm:ss') }}
<div>在线时长{{ scope.row.onlineTime }}分钟</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="140">
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button text @click="onChat(scope.row)" icon="bell" v-hasRole="['admin']">通知</el-button>
<el-button text @click="onChat(scope.row)" icon="ChatDotRound" v-hasRole="['admin']">私信</el-button>
<el-button text @click="onLock(scope.row)" icon="lock" v-hasPermi="['monitor:online:forceLogout']">强退</el-button>
</template>
</el-table-column>
</el-table>
<pagination
class="mt10"
background
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList" />
<el-row :gutter="20" v-if="viewSwitch == 2">
<el-col v-for="item in onlineUsers" :lg="4" :span="24">
<el-card :body-style="{ padding: '15px 15px 0' }">
<el-descriptions :column="1" :title="item.name">
<el-descriptions-item label="登录平台">{{ item.platform }}</el-descriptions-item>
<el-descriptions-item label="登录地点">{{ item.location }}</el-descriptions-item>
<el-descriptions-item label="在线时长" :span="2">
<el-tag type="success">{{ item.onlineTime }}分钟</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-text truncated>{{ item.browser }}</el-text>
<div>
<el-button text @click="onChat(item)" size="small" icon="ChatDotRound" title="私信" v-hasRole="['admin']">私信</el-button>
<el-button text @click="onLock(item)" size="small" icon="lock" title="强退" v-hasPermi="['monitor:online:forceLogout']">强退</el-button>
</div>
</el-card>
</el-col>
<el-empty v-show="total == 0" description="no data" />
</el-row>
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup name="onlineuser">
import { listOnline } from '@/api/monitor/online'
import { listOnline, forceLogout, forceLogoutAll } from '@/api/monitor/online'
import dayjs from 'dayjs'
import useSocketStore from '@/store/modules/socket'
const { proxy } = getCurrentInstance()
const queryRef = ref(null)
const queryParams = reactive({
pageNum: 1,
pageSize: 10
})
// const total = computed(() => {
// return useSocketStore().onlineNum
// })
// const onlineUsers = computed(() => {
// return useSocketStore().onlineUsers
// })
const onlineNum = computed(() => {
return useSocketStore().onlineNum
})
const viewSwitch = ref(1)
const loading = ref(false)
const onlineUsers = ref([])
const total = ref(0)
function handleQuery() {
@ -64,13 +86,14 @@ function handleQuery() {
getList()
}
function getList() {
// proxy.signalr.SR.invoke('GetOnlineUsers', queryParams.pageNum, queryParams.pageSize).catch(function (err) {
// console.error(err.toString())
// })
loading.value = true
listOnline(queryParams).then((res) => {
if (res.code == 200) {
total.value = res.data.totalNum
onlineUsers.value = res.data.result
setTimeout(() => {
loading.value = false
}, 200)
}
})
}
@ -78,18 +101,57 @@ getList()
function onChat(item) {
proxy
.$prompt('请输入通知内容', '', {
.$prompt('请输入消息内容', '', {
confirmButtonText: '发送',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '消息内容不能为空'
})
.then(({ value }) => {
proxy.signalr.SR.invoke('SendMessage', item.connnectionId, item.name, value).catch(function (err) {
proxy.signalr.SR.invoke('sendMessage', item.userid, value).catch(function (err) {
console.error(err.toString())
})
})
.catch(() => {})
}
function resetQuery() {}
function onLock(row) {
proxy
.$prompt('请输入强退原因', '', {
confirmButtonText: '发送',
cancelButtonText: '取消'
})
.then((val) => {
forceLogout({ ...row, time: 10, reason: val.value, clientId: row.clientId }).then(() => {
proxy.$modal.msgSuccess('强退成功')
})
})
}
// 退
function onLockAll() {
proxy
.$prompt('请输入强退原因', '', {
confirmButtonText: '发送',
cancelButtonText: '取消'
})
.then((val) => {
forceLogoutAll({ time: 10, reason: val.value }).then((res) => {
proxy.$modal.msgSuccess('强退成功')
})
})
}
watch(
onlineNum,
() => {
handleQuery()
},
{
immediate: true
}
)
</script>
<style>
.el-col {
margin-bottom: 10px;
}
</style>

View File

@ -27,6 +27,7 @@
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="defaultTime"
value-format="YYYY-MM-DD HH:mm:ss"
:shortcuts="dateOptions"></el-date-picker>
</el-form-item>
@ -62,13 +63,13 @@
</el-table-column>
<el-table-column label="请求方法" align="center" prop="requestMethod" v-if="columns.showColumn('requestMethod')" />
<el-table-column label="操作人员" align="center" prop="operName" v-if="columns.showColumn('operName')" />
<el-table-column label="主机" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" v-if="columns.showColumn('operIP')" />
<el-table-column
label="操作地点"
align="center"
prop="operLocation"
:show-overflow-tooltip="true"
v-if="columns.showColumn('operLocation')" />
<el-table-column label="操作地址" align="center" prop="operIp" width="120">
<template #default="{ row }">
<div>{{ row.operLocation }}</div>
<div>{{ row.operIp }}</div>
</template>
</el-table-column>
<el-table-column label="操作状态" align="center" prop="status" v-if="columns.showColumn('status')">
<template #default="{ row }">
<dict-tag :options="options.statusOptions" :value="row.status"></dict-tag>
@ -91,11 +92,12 @@
<el-table-column prop="method" label="操作方法" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('method')" />
<el-table-column prop="operParam" label="请求参数" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('operParam')" />
<el-table-column prop="jsonResult" label="返回结果" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('jsonResult')" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template #default="scope">
<el-button size="small" text icon="view" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">
详细
</el-button>
<el-button size="small" text icon="delete" @click="handleDelete(scope.row)" v-hasPermi="['monitor:operlog:remove']"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
@ -132,18 +134,20 @@
<el-col :lg="12">
<el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
</el-col>
<el-col :lg="24">
<el-col :lg="24" v-if="form.operParam">
<el-form-item label="请求参数:">
<el-input type="textarea" rows="5" v-model="form.operParam"> </el-input>
</el-form-item>
</el-col>
<el-col :lg="24">
<el-col :lg="24" v-if="form.jsonResult">
<el-form-item label="返回结果:">
<el-input type="textarea" rows="10" v-model="form.jsonResult"> </el-input>
</el-form-item>
</el-col>
<el-col :lg="24">
<el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
<el-form-item label="异常信息:" v-if="form.status === 1">
<div class="text-danger">{{ form.errorMsg }}</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
@ -155,8 +159,8 @@
</template>
<script setup name="operlog">
import { list as listOperLog, delOperlog, cleanOperlog, exportOperlog } from '@/api/monitor/operlog'
import { list as listOperLog, delOperlog, cleanOperlog } from '@/api/monitor/operlog'
import dayjs from 'dayjs'
const { proxy } = getCurrentInstance()
//
const loading = ref(true)
@ -177,7 +181,8 @@ const statusOptions = ref([])
// 0 1 2 3 eg:{ dictLabel: '', dictValue: '0'}
const businessTypeOptions = ref([])
//
const dateRange = ref([])
const dateRange = ref([dayjs().format('YYYY-MM-DD 00:00:00'), dayjs().format('YYYY-MM-DD 23:59:59')])
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
const state = reactive({
form: {},
@ -205,9 +210,9 @@ const columns = ref([
{ visible: true, prop: 'operName', label: '操作人员' },
// { visible: true, prop: 'deptName', label: '' },
// { visible: true, prop: 'operUrl', label: '' },
{ visible: true, prop: 'operIP', label: '请求IP' },
// { visible: true, prop: 'operIP', label: 'IP' },
{ visible: true, prop: 'status', label: '操作状态' },
{ visible: true, prop: 'operLocation', label: '操作人地址' },
// { visible: true, prop: 'operLocation', label: '' },
{ visible: true, prop: 'operTime', label: '操作时间' },
{ visible: false, prop: 'method', label: '操作方法' },
{ visible: false, prop: 'operParam', label: '请求参数' },

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