init: 初始系统模板
This commit is contained in:
80
src/App.vue
Normal file
80
src/App.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { ConfigProvider } from 'ant-design-vue/lib';
|
||||
import { usePrimaryColor } from '@/hooks/useTheme';
|
||||
import zhCN from 'ant-design-vue/lib/locale/zh_CN';
|
||||
import enUS from 'ant-design-vue/lib/locale/en_US';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { ref, watch } from 'vue';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
const { currentLocale } = useI18n();
|
||||
|
||||
dayjs.locale('zh-cn'); // 默认中文
|
||||
usePrimaryColor(); // 载入用户自定义主题色
|
||||
|
||||
let locale = ref(zhCN); // 国际化初始中文
|
||||
|
||||
// 国际化切换语言
|
||||
function fnChangeLocale(v: string) {
|
||||
switch (v) {
|
||||
case 'zh_CN':
|
||||
locale.value = zhCN;
|
||||
dayjs.locale(zhCN.locale);
|
||||
break;
|
||||
case 'en_US':
|
||||
locale.value = enUS;
|
||||
dayjs.locale(enUS.locale);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载多语言并进行监听
|
||||
fnChangeLocale(currentLocale.value);
|
||||
watch(currentLocale, val => {
|
||||
fnChangeLocale(val);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="locale">
|
||||
<RouterView />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
body .ant-pro-basicLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.ant-pro-sider {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition-duration: 0.5s;
|
||||
transition-property: height, opacity, transform;
|
||||
transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-left-enter,
|
||||
.slide-right-leave-active {
|
||||
opacity: 0;
|
||||
transform: translate(2em, 0);
|
||||
}
|
||||
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter {
|
||||
opacity: 0;
|
||||
transform: translate(-2em, 0);
|
||||
}
|
||||
</style>
|
||||
59
src/api/login.ts
Normal file
59
src/api/login.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
// 登录方法
|
||||
export function login(data: Record<string, string>) {
|
||||
return request({
|
||||
url: '/login',
|
||||
method: 'post',
|
||||
data: data,
|
||||
whithToken: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册方法
|
||||
* @param data 注册对象
|
||||
* @returns object
|
||||
*/
|
||||
export function register(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/register',
|
||||
method: 'post',
|
||||
data: data,
|
||||
whithToken: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详细信息
|
||||
* @returns object
|
||||
*/
|
||||
export function getInfo() {
|
||||
return request({
|
||||
url: '/getInfo',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出方法
|
||||
* @returns object
|
||||
*/
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/logout',
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns object
|
||||
*/
|
||||
export function getCaptchaImage() {
|
||||
return request({
|
||||
url: '/captchaImage',
|
||||
method: 'get',
|
||||
whithToken: false,
|
||||
});
|
||||
}
|
||||
86
src/api/monitor/cache.ts
Normal file
86
src/api/monitor/cache.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询缓存详细
|
||||
* @returns object
|
||||
*/
|
||||
export function getCache() {
|
||||
return request({
|
||||
url: '/monitor/cache',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存名称列表
|
||||
* @returns object
|
||||
*/
|
||||
export function listCacheName() {
|
||||
return request({
|
||||
url: '/monitor/cache/getNames',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存名称下键名列表
|
||||
* @param cacheName 缓存名称列表中得到的缓存名称
|
||||
* @returns object
|
||||
*/
|
||||
export function listCacheKey(cacheName: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/getKeys/${cacheName}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存内容
|
||||
* @param cacheName 键名列表中得到的缓存名称
|
||||
* @param cacheKey 键名列表中得到的缓存键名
|
||||
* @returns object
|
||||
*/
|
||||
export function getCacheValue(cacheName: string, cacheKey: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/getValue/${cacheName}/${cacheKey}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存名称下键名列表
|
||||
* @param cacheName 缓存名称列表中得到的缓存名称
|
||||
* @returns object
|
||||
*/
|
||||
export function clearCacheName(cacheName: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/clearCacheName/${cacheName}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存键名
|
||||
* @param cacheName 键名列表中得到的缓存名称
|
||||
* @param cacheKey 键名列表中得到的缓存键名
|
||||
* @returns object
|
||||
*/
|
||||
export function clearCacheKey(cacheName: string, cacheKey: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/clearCacheKey/${cacheName}/${cacheKey}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全清理缓存名称
|
||||
*
|
||||
* 指定可清理的缓存key
|
||||
* @returns object
|
||||
*/
|
||||
export function clearCacheSafe() {
|
||||
return request({
|
||||
url: '/monitor/cache/clearCacheSafe',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
121
src/api/monitor/job.ts
Normal file
121
src/api/monitor/job.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 定时任务调度列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportJob(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定时任务调度列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listJob(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定时任务调度详细
|
||||
* @param jobId 任务ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getJob(jobId: string | number) {
|
||||
return request({
|
||||
url: `/monitor/job/${jobId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增定时任务调度
|
||||
* @param data 任务对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addJob(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改定时任务调度
|
||||
* @param data 任务对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateJob(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定时任务调度
|
||||
* @param jobId 任务ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delJob(jobId: string | number) {
|
||||
return request({
|
||||
url: `/monitor/job/${jobId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务状态修改
|
||||
* @param jobId 任务ID
|
||||
* @param status 变更状态值
|
||||
* @returns
|
||||
*/
|
||||
export function changeJobStatus(
|
||||
jobId: string | number,
|
||||
status: string | number
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/job/changeStatus',
|
||||
method: 'put',
|
||||
data: {
|
||||
jobId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务立即执行一次
|
||||
* @param jobId 任务ID
|
||||
* @returns object
|
||||
*/
|
||||
export function runJob(jobId: string) {
|
||||
return request({
|
||||
url: `/monitor/job/run/${jobId}`,
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置刷新队列
|
||||
* @returns object
|
||||
*/
|
||||
export function resetQueueJob() {
|
||||
return request({
|
||||
url: '/monitor/job/resetQueueJob',
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
53
src/api/monitor/jobLog.ts
Normal file
53
src/api/monitor/jobLog.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 定时任务调度日志列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportJobLog(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/jobLog/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询调度日志列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listJobLog(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/jobLog/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除调度日志
|
||||
* @param jobLogId 任务日志Id
|
||||
* @returns object
|
||||
*/
|
||||
export function delJobLog(jobLogId: string) {
|
||||
return request({
|
||||
url: `/monitor/jobLog/${jobLogId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空调度日志
|
||||
* @returns object
|
||||
*/
|
||||
export function cleanJobLog() {
|
||||
return request({
|
||||
url: '/monitor/jobLog/clean',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
67
src/api/monitor/logininfor.ts
Normal file
67
src/api/monitor/logininfor.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 登录日志列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportLogininfor(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询登录日志列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listLogininfor(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除登录日志
|
||||
* @param infoId 登录日志Id
|
||||
* @returns object
|
||||
*/
|
||||
export function delLogininfor(infoId: string) {
|
||||
return request({
|
||||
url: `/monitor/logininfor/${infoId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空登录日志
|
||||
* @returns object
|
||||
*/
|
||||
export function cleanLogininfor() {
|
||||
return request({
|
||||
url: '/monitor/logininfor/clean',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁用户登录状态
|
||||
* @param userName 登录账号
|
||||
* @returns object
|
||||
*/
|
||||
export function unlockLogininfor(userName: string) {
|
||||
return request({
|
||||
url: `/monitor/logininfor/unlock/${userName}`,
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
26
src/api/monitor/online.ts
Normal file
26
src/api/monitor/online.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询在线用户列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listOnline(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/online/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 强退用户
|
||||
* @param tokenId 授权标识
|
||||
* @returns object
|
||||
*/
|
||||
export function forceLogout(tokenId: string) {
|
||||
return request({
|
||||
url: `/monitor/online/${tokenId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
55
src/api/monitor/operlog.ts
Normal file
55
src/api/monitor/operlog.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 操作日志列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportOperlog(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/operlog/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询操作日志列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listOperlog(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/operlog/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
* @param operId 操作日志ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delOperlog(operId: string) {
|
||||
return request({
|
||||
url: `/monitor/operlog/${operId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空操作日志
|
||||
* @returns object
|
||||
*/
|
||||
export function cleanOperlog() {
|
||||
return request({
|
||||
url: '/monitor/operlog/clean',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
9
src/api/monitor/server.ts
Normal file
9
src/api/monitor/server.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**获取服务信息 */
|
||||
export function getServer() {
|
||||
return request({
|
||||
url: '/monitor/server',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
56
src/api/profile.ts
Normal file
56
src/api/profile.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询用户个人信息
|
||||
* @returns object
|
||||
*/
|
||||
export function getUserProfile() {
|
||||
return request({
|
||||
url: '/system/user/profile',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户个人信息
|
||||
* @param data 用户对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateUserProfile(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user/profile',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户密码重置
|
||||
* @param userId 用户ID
|
||||
* @param status 变更状态值
|
||||
* @returns object
|
||||
*/
|
||||
export function updateUserPwd(oldPassword: string, newPassword: string) {
|
||||
return request({
|
||||
url: '/system/user/profile/updatePwd',
|
||||
method: 'put',
|
||||
data: {
|
||||
oldPassword,
|
||||
newPassword,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户头像上传
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function uploadAvatar(data: FormData) {
|
||||
return request({
|
||||
url: '/system/user/profile/avatar',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
12
src/api/router.ts
Normal file
12
src/api/router.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 获取路由
|
||||
* @returns object
|
||||
*/
|
||||
export const getRouters = () => {
|
||||
return request({
|
||||
url: '/getRouters',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
103
src/api/system/config.ts
Normal file
103
src/api/system/config.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 参数配置列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportConfig(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/system/config/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数配置列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listConfig(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/config/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数详细
|
||||
* @param configId 参数配置ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getConfig(configId: string | number) {
|
||||
return request({
|
||||
url: `/system/config/${configId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数键名查询参数值
|
||||
* @param configKey 参数键名
|
||||
* @returns object
|
||||
*/
|
||||
export function getConfigKey(configKey: string) {
|
||||
return request({
|
||||
url: `/system/config/configKey/${configKey}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增参数配置
|
||||
* @param data 参数配置对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addConfig(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/config',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改参数配置
|
||||
* @param data 参数配置对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateConfig(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/config',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除参数配置
|
||||
* @param configId 参数配置ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delConfig(configId: string | number) {
|
||||
return request({
|
||||
url: `/system/config/${configId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新参数缓存
|
||||
* @returns object
|
||||
*/
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: '/system/config/refreshCache',
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
99
src/api/system/dept.ts
Normal file
99
src/api/system/dept.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询部门列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listDept(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dept/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门列表(排除节点)
|
||||
* @param deptId 部门ID
|
||||
* @returns object
|
||||
*/
|
||||
export function listDeptExcludeChild(deptId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/list/exclude/${deptId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门详细
|
||||
* @param deptId 部门ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getDept(deptId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/${deptId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增部门
|
||||
* @param data 部门对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addDept(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dept',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改部门
|
||||
* @param data 部门对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateDept(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dept',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param deptId 部门ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delDept(deptId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/${deptId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门下拉树结构
|
||||
* @returns object
|
||||
*/
|
||||
export function deptTreeSelect() {
|
||||
return request({
|
||||
url: '/system/dept/treeSelect',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门树结构列表(指定角色)
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function roleDeptTreeSelect(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/roleDeptTreeSelect/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
90
src/api/system/dict/data.ts
Normal file
90
src/api/system/dict/data.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 字典数据列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportData(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典数据列表
|
||||
* @param query 查询值
|
||||
* @returns
|
||||
*/
|
||||
export function listData(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典数据详细
|
||||
* @param dictCode 字典代码值
|
||||
* @returns object
|
||||
*/
|
||||
export function getData(dictCode: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/data/${dictCode}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增字典数据
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addData(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改字典数据
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateData(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典数据
|
||||
* @param dictCode 字典代码值
|
||||
* @returns object
|
||||
*/
|
||||
export function delData(dictCode: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/data/${dictCode}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据列表(指定字典类型)
|
||||
* @param dictType 字典类型
|
||||
* @returns object
|
||||
*/
|
||||
export function getDictDataType(dictType: string) {
|
||||
return request({
|
||||
url: `/system/dict/data/type/${dictType}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
102
src/api/system/dict/type.ts
Normal file
102
src/api/system/dict/type.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 字典类型列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportType(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典类型列表
|
||||
* @param query 查询值
|
||||
* @returns
|
||||
*/
|
||||
export function listType(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典类型详细
|
||||
* @param dictId 字典编号
|
||||
* @returns object
|
||||
*/
|
||||
export function getType(dictId: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/type/${dictId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增字典类型
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addType(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改字典类型
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateType(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典类型
|
||||
* @param dictCode 字典代码值
|
||||
* @returns object
|
||||
*/
|
||||
export function delType(dictId: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/type/${dictId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新字典缓存
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: '/system/dict/type/refreshCache',
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典选择框列表
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function getDictOptionselect() {
|
||||
return request({
|
||||
url: '/system/dict/type/getDictOptionselect',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
87
src/api/system/menu.ts
Normal file
87
src/api/system/menu.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询菜单列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listMenu(query?: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/menu/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询菜单详细
|
||||
* @param menuId 菜单ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getMenu(menuId: string | number) {
|
||||
return request({
|
||||
url: `/system/menu/${menuId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询菜单下拉树结构
|
||||
* @returns object
|
||||
*/
|
||||
export function menuTreeSelect() {
|
||||
return request({
|
||||
url: '/system/menu/treeSelect',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色ID查询菜单下拉树结构
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function roleMenuTreeSelect(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/menu/roleMenuTreeSelect/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增菜单
|
||||
* @param data 菜单对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addMenu(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/menu',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改菜单
|
||||
* @param data 菜单对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateMenu(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/menu',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* @param menuId 菜单ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delMenu(menuId: string | number) {
|
||||
return request({
|
||||
url: `/system/menu/${menuId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
64
src/api/system/notice.ts
Normal file
64
src/api/system/notice.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询公告列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listNotice(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/notice/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询公告详细
|
||||
* @param menuId 公告ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getNotice(noticeId: string | number) {
|
||||
return request({
|
||||
url: `/system/notice/${noticeId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增公告
|
||||
* @param data 公告对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addNotice(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/notice',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改公告
|
||||
* @param data 公告对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateNotice(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/notice',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除公告
|
||||
* @param noticeId 公告ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delNotice(noticeId: string | number) {
|
||||
return request({
|
||||
url: `/system/notice/${noticeId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
78
src/api/system/post.ts
Normal file
78
src/api/system/post.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 岗位列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportPost(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询岗位列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listPost(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询岗位详细
|
||||
* @param postId 岗位ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getPost(postId: string | number) {
|
||||
return request({
|
||||
url: `/system/post/${postId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增岗位
|
||||
* @param data 岗位对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addPost(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改岗位
|
||||
* @param data 岗位对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updatePost(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除岗位
|
||||
* @param postId 岗位ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delPost(postId: string | number) {
|
||||
return request({
|
||||
url: `/system/post/${postId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
134
src/api/system/role.ts
Normal file
134
src/api/system/role.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 角色列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportRole(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listRole(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色详细
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getRole(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/role/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增角色
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addRole(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改角色
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateRole(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delRole(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/role/${roleId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色状态修改
|
||||
* @param roleId 角色ID
|
||||
* @param status 角色状态
|
||||
* @returns object
|
||||
*/
|
||||
export function changeRoleStatus(roleId: string, status: string | number) {
|
||||
return request({
|
||||
url: '/system/role/changeStatus',
|
||||
method: 'put',
|
||||
data: {
|
||||
roleId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改角色数据权限
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function dataScope(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/dataScope',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色分配用户列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function authUserAllocatedList(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/authUser/allocatedList',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色分配选择授权
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function authUserChecked(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/authUser/checked',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
141
src/api/system/user.ts
Normal file
141
src/api/system/user.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 导入用户模板数据
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function importData(data: FormData) {
|
||||
return request({
|
||||
url: '/system/user/importData',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用户模板下载
|
||||
* @returns bolb
|
||||
*/
|
||||
export function importTemplate() {
|
||||
return request({
|
||||
url: '/system/user/importTemplate',
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportUser(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listUser(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户详细
|
||||
* @param userId 用户ID,新增0
|
||||
* @returns object
|
||||
*/
|
||||
export function getUser(userId: string | number = '0') {
|
||||
return request({
|
||||
url: `/system/user/${userId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
* @param data 用户对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addUser(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
* @param data 用户对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateUser(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param userId 用户ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delUser(userId: string | number) {
|
||||
return request({
|
||||
url: `/system/user/${userId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户密码重置
|
||||
* @param userId 用户ID
|
||||
* @param password 密码
|
||||
* @returns object
|
||||
*/
|
||||
export function resetUserPwd(userId: string | number, password: string) {
|
||||
return request({
|
||||
url: '/system/user/resetPwd',
|
||||
method: 'put',
|
||||
data: {
|
||||
userId,
|
||||
password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态修改
|
||||
* @param userId 用户ID
|
||||
* @param status 变更状态值
|
||||
* @returns object
|
||||
*/
|
||||
export function changeUserStatus(
|
||||
userId: string | number,
|
||||
status: string | number
|
||||
) {
|
||||
return request({
|
||||
url: '/system/user/changeStatus',
|
||||
method: 'put',
|
||||
data: {
|
||||
userId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
195
src/api/tool/file.ts
Normal file
195
src/api/tool/file.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png
|
||||
* @param range 断点续传标识,填入字符串 `bytes=${startByte}-${endByte}`
|
||||
* @returns object
|
||||
*/
|
||||
export async function downloadFile(filePath: string, range?: string) {
|
||||
return request({
|
||||
url: `/file/download/${encode(filePath)}`,
|
||||
method: 'get',
|
||||
headers: range ? { range } : {},
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件切片
|
||||
* @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png
|
||||
* @param chunkSize 数据块大小MB,默认1MB
|
||||
* @returns bolb
|
||||
*/
|
||||
export async function downloadFileChunk(
|
||||
filePath: string,
|
||||
chunkSize: number = 1
|
||||
): Promise<Blob> {
|
||||
chunkSize = chunkSize * 1024 * 1024;
|
||||
let start = 0; // 文件块的起始字节
|
||||
let end = chunkSize - 1; // 文件块的结束字节
|
||||
let totalSize = 0; // 文件总大小
|
||||
let downloadedSize = 0; // 已下载的文件大小
|
||||
let filePart: Blob[] = []; // 文件数据块内容
|
||||
|
||||
// 发送带有 Range 请求头的 HTTP 请求
|
||||
async function sendRequest() {
|
||||
const range = `bytes=${start}-${end}`;
|
||||
const res = await downloadFile(filePath, range);
|
||||
if (res.code === 200 && res.status === 206) {
|
||||
// 总大小
|
||||
const contentRange = res.headers.get('content-range') || '0/0';
|
||||
totalSize = parseInt(contentRange.split('/')[1]);
|
||||
// 已下载大小
|
||||
const contentLength = res.headers.get('content-length') || '0';
|
||||
const chunkSize = parseInt(contentLength);
|
||||
// 下一段数据块区间
|
||||
start += chunkSize;
|
||||
end = Math.min(start + chunkSize - 1, totalSize - 1);
|
||||
// 记录下载结果
|
||||
filePart.push(res.data);
|
||||
downloadedSize += chunkSize;
|
||||
// 小于总大小继续下载后续数据
|
||||
if (downloadedSize < totalSize) {
|
||||
await sendRequest();
|
||||
}
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
await sendRequest();
|
||||
return new Blob(filePart, { type: 'application/octet-stream' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function uploadFile(data: FormData) {
|
||||
return request({
|
||||
url: '/file/upload',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传切片文件
|
||||
* @param file 文件对象
|
||||
* @param chunkSize 数据块大小MB,默认1MB
|
||||
* @param subPath 归属子路径, 默认default
|
||||
* @returns
|
||||
*/
|
||||
export async function uploadFileChunk(
|
||||
fileData: File,
|
||||
chunkSize: number = 1,
|
||||
subPath: string = 'default'
|
||||
) {
|
||||
const { name, size } = fileData;
|
||||
const chunkSizeInBytes = chunkSize * 1024 * 1024;
|
||||
// 文件标识使用唯一编码 MD5(文件名+文件大小)
|
||||
const fileIdentifier = `${name}-${size}`;
|
||||
// 文件切分为多少份进行上传
|
||||
const chunksCount = Math.ceil(size / chunkSizeInBytes);
|
||||
// 切块的数据数据用于上传
|
||||
const fileChunks: { index: number; chunk: Blob }[] = [];
|
||||
|
||||
for (let i = 0; i < chunksCount; i++) {
|
||||
const start = i * chunkSizeInBytes;
|
||||
const end = Math.min(start + chunkSizeInBytes, size);
|
||||
fileChunks.push({
|
||||
index: i,
|
||||
chunk: fileData.slice(start, end),
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否已上传部分数据块
|
||||
const resCheck = await chunkCheck(fileIdentifier, name);
|
||||
if (resCheck.code !== 200) {
|
||||
return resCheck;
|
||||
}
|
||||
|
||||
let uploadedSize = 0;
|
||||
let uploadProgress = 0;
|
||||
|
||||
for (const { index, chunk } of fileChunks) {
|
||||
const chunksIndex = `${index}`;
|
||||
// 跳过已上传块
|
||||
if (resCheck.data.includes(chunksIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 上传数据块
|
||||
const formData = new FormData();
|
||||
formData.append('file', chunk, name);
|
||||
formData.append('index', chunksIndex);
|
||||
formData.append('identifier', fileIdentifier);
|
||||
|
||||
const resUpload = await chunkUpload(formData);
|
||||
if (resUpload.code === 200) {
|
||||
uploadedSize += chunk.size;
|
||||
uploadProgress = (uploadedSize / size) * 100;
|
||||
console.log(`上传进度:${uploadProgress}%`);
|
||||
} else {
|
||||
// 上传失败处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传数据完整后合并数据块
|
||||
if (uploadedSize === size) {
|
||||
return await chunkMerge(fileIdentifier, name, subPath);
|
||||
}
|
||||
return { code: 500, msg: '上传出错,请重试' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片文件检查
|
||||
* @param identifier 文件标识
|
||||
* @param fileName 原文件名称
|
||||
* @returns object
|
||||
*/
|
||||
export function chunkCheck(identifier: string, fileName: string) {
|
||||
return request({
|
||||
url: '/file/chunkCheck',
|
||||
method: 'post',
|
||||
data: { identifier, fileName },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片文件合并
|
||||
* @param identifier 文件标识
|
||||
* @param fileName 原文件名称
|
||||
* @param subPath 文件归属
|
||||
* @returns object
|
||||
*/
|
||||
export function chunkMerge(
|
||||
identifier: string,
|
||||
fileName: string,
|
||||
subPath: string = 'default'
|
||||
) {
|
||||
return request({
|
||||
url: '/file/chunkMerge',
|
||||
method: 'post',
|
||||
data: { identifier, fileName, subPath },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片文件上传
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function chunkUpload(data: FormData) {
|
||||
return request({
|
||||
url: '/file/chunkUpload',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
69
src/assets/background.svg
Normal file
69
src/assets/background.svg
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Ant-Design-Pro</title>
|
||||
<desc>mask-and-vue3 By TsMask</desc>
|
||||
<defs></defs>
|
||||
<g id="Ant-Design-Pro" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="mask-and-vue3" transform="translate(-79.000000, -82.000000)">
|
||||
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
|
||||
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
|
||||
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
|
||||
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
|
||||
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
|
||||
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
|
||||
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
|
||||
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||
</g>
|
||||
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
|
||||
</g>
|
||||
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
|
||||
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
|
||||
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
|
||||
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
</g>
|
||||
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
|
||||
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||
</g>
|
||||
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
|
||||
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/donate.jpg
Normal file
BIN
src/assets/donate.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 482 KiB |
BIN
src/assets/images/default_avatar.png
Normal file
BIN
src/assets/images/default_avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
130
src/assets/js/icon_font_8d5l8fzk5b87iudi.ts
Normal file
130
src/assets/js/icon_font_8d5l8fzk5b87iudi.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 字体图标文件-静态资源文件路径
|
||||
*/
|
||||
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
|
||||
export const scriptUrl = `${
|
||||
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
|
||||
? ''
|
||||
: baseUrl.indexOf('/') === -1
|
||||
? '/' + baseUrl
|
||||
: baseUrl
|
||||
}/font_8d5l8fzk5b87iudi.js`;
|
||||
|
||||
/**
|
||||
* 读取 font_8d5l8fzk5b87iudi.js 文件内svg图标名称
|
||||
*
|
||||
* JSON.stringify(txt.match(/icon-(\S*)"/gi).map(i=>i.slice(0,-1)))
|
||||
*/
|
||||
export const iconFonts = [
|
||||
'#',
|
||||
'icon-alibaba',
|
||||
'icon-alimama',
|
||||
'icon-aliyun',
|
||||
'icon-anzhuo',
|
||||
'icon-biaoqing',
|
||||
'icon-chexiao',
|
||||
'icon-chexiao2',
|
||||
'icon-daimayingyong',
|
||||
'icon-daishenhe',
|
||||
'icon-dashang',
|
||||
'icon-dianzan',
|
||||
'icon-dianzan1',
|
||||
'icon-facebook',
|
||||
'icon-fangda',
|
||||
'icon-fangda2',
|
||||
'icon-fanhui',
|
||||
'icon-fanhui1',
|
||||
'icon-fankui1',
|
||||
'icon-fenxiang',
|
||||
'icon-fuzhichenggong',
|
||||
'icon-fuzhidaima',
|
||||
'icon-fuzhidaima1',
|
||||
'icon-gengduo',
|
||||
'icon-gerenzhanghu',
|
||||
'icon-github',
|
||||
'icon-gonggao',
|
||||
'icon-gonggaodayi',
|
||||
'icon-gongnengjieshao',
|
||||
'icon-gouwuche',
|
||||
'icon-gouwuche2',
|
||||
'icon-guanbi',
|
||||
'icon-huidingbu',
|
||||
'icon-huifu',
|
||||
'icon-huizhiguize',
|
||||
'icon-iconfont1',
|
||||
'icon-ios',
|
||||
'icon-jieshi',
|
||||
'icon-jinggao',
|
||||
'icon-lishi',
|
||||
'icon-morentouxiang',
|
||||
'icon-paixu',
|
||||
'icon-pcduan',
|
||||
'icon-piliang',
|
||||
'icon-qingchu',
|
||||
'icon-qq',
|
||||
'icon-qunzhu',
|
||||
'icon-right',
|
||||
'icon-saoyisao',
|
||||
'icon-shanchu',
|
||||
'icon-shang',
|
||||
'icon-shang1',
|
||||
'icon-shang2',
|
||||
'icon-shangchuan',
|
||||
'icon-shenhejujue',
|
||||
'icon-shenhetongguo',
|
||||
'icon-shijian',
|
||||
'icon-shuoming',
|
||||
'icon-souren',
|
||||
'icon-sousuo',
|
||||
'icon-soutubiao',
|
||||
'icon-suofang',
|
||||
'icon-suoxiao',
|
||||
'icon-suoxiao2',
|
||||
'icon-taobaowang',
|
||||
'icon-tengxunwang',
|
||||
'icon-tianjiachengyuan',
|
||||
'icon-tianmao',
|
||||
'icon-tubiaohuizhi',
|
||||
'icon-tubiaoku',
|
||||
'icon-tuichu',
|
||||
'icon-twitter',
|
||||
'icon-weibo',
|
||||
'icon-weibo1',
|
||||
'icon-weibo2',
|
||||
'icon-weijiaru',
|
||||
'icon-weitijiao',
|
||||
'icon-weixin',
|
||||
'icon-wenjian',
|
||||
'icon-wocanyu',
|
||||
'icon-wofaqi',
|
||||
'icon-xia',
|
||||
'icon-xia2',
|
||||
'icon-xiangmu',
|
||||
'icon-xiangmuchengyuan',
|
||||
'icon-xiangxia',
|
||||
'icon-xiangxia1',
|
||||
'icon-xiangxia2',
|
||||
'icon-xiangyou',
|
||||
'icon-xiaomi',
|
||||
'icon-xiazai',
|
||||
'icon-xinjiantubiaoku',
|
||||
'icon-yingwen',
|
||||
'icon-you',
|
||||
'icon-you1',
|
||||
'icon-you2',
|
||||
'icon-youxiang',
|
||||
'icon-youxuan',
|
||||
'icon-youxuan2',
|
||||
'icon-yuzhanghao',
|
||||
'icon-yuzhanghao1',
|
||||
'icon-zhifubao',
|
||||
'icon-zhizuoliucheng',
|
||||
'icon-zhongguodianxin',
|
||||
'icon-zhuanrang',
|
||||
'icon-zhubajie',
|
||||
'icon-zuo',
|
||||
'icon-zuo1',
|
||||
'icon-zuo2',
|
||||
'icon-zuoxuan',
|
||||
'icon-zuoxuan2',
|
||||
];
|
||||
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
141
src/components/CronModal/components/Day.vue
Normal file
141
src/components/CronModal/components/Day.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 31 }, (_, i) => {
|
||||
let idx = i + 1;
|
||||
return {
|
||||
label: `${idx}`,
|
||||
value: `${idx}`,
|
||||
};
|
||||
});
|
||||
|
||||
/**数据 */
|
||||
let data = reactive({
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 2,
|
||||
incrementStart: 1,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定 */
|
||||
specific: ['1'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一天</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
天执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
日开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
日
|
||||
</a-radio>
|
||||
<a-radio value="4">指定日(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定日(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
<a-radio value="5">本月最后一天</a-radio>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
137
src/components/CronModal/components/Hour.vue
Normal file
137
src/components/CronModal/components/Hour.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 24 }, (_, i) => ({
|
||||
label: `${i}`.padStart(2, '0'),
|
||||
value: `${i}`,
|
||||
}));
|
||||
|
||||
/**数据 */
|
||||
let data = reactive({
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 2,
|
||||
incrementStart: 0,
|
||||
/**周期 */
|
||||
rangeStart: 0,
|
||||
rangeEnd: 2,
|
||||
/**指定 */
|
||||
specific: ['0'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一小时</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="0-23"
|
||||
></a-input-number>
|
||||
小时执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="0-23"
|
||||
></a-input-number>
|
||||
时开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="1-23"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="0-23"
|
||||
></a-input-number>
|
||||
小时
|
||||
</a-radio>
|
||||
<a-radio value="4">指定小时(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定小时(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
139
src/components/CronModal/components/Minute.vue
Normal file
139
src/components/CronModal/components/Minute.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 60 }, (_, i) => {
|
||||
return {
|
||||
label: `${i}`.padStart(2, '0'),
|
||||
value: `${i}`,
|
||||
};
|
||||
});
|
||||
|
||||
/**数据 */
|
||||
let data = reactive({
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 1,
|
||||
incrementStart: 0,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定 */
|
||||
specific: ['0'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一分钟</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
分钟执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
分钟开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
分钟
|
||||
</a-radio>
|
||||
<a-radio value="4">指定分钟(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定分钟(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
141
src/components/CronModal/components/Month.vue
Normal file
141
src/components/CronModal/components/Month.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 12 }, (_, i) => {
|
||||
let idx = i + 1;
|
||||
return {
|
||||
label: `${idx}`,
|
||||
value: `${idx}`,
|
||||
};
|
||||
});
|
||||
|
||||
/**构建数据 */
|
||||
let data = reactive({
|
||||
/**类型 */
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 1,
|
||||
incrementStart: 1,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定秒 */
|
||||
specific: ['1'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一月</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
月执行,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
月开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
月之间的每个月
|
||||
</a-radio>
|
||||
<a-radio value="4">指定月(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定月(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
138
src/components/CronModal/components/Second.vue
Normal file
138
src/components/CronModal/components/Second.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 60 }, (_, i) => ({
|
||||
label: `${i}`.padStart(2, '0'),
|
||||
value: `${i}`,
|
||||
}));
|
||||
|
||||
/**构建数据 */
|
||||
let data = reactive({
|
||||
/**类型 */
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 2,
|
||||
incrementStart: 0,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定秒 */
|
||||
specific: ['0'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一秒钟</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
秒执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
秒开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
秒
|
||||
</a-radio>
|
||||
<a-radio value="4">指定秒数(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定秒数(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
112
src/components/CronModal/index.vue
Normal file
112
src/components/CronModal/index.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-modal
|
||||
title="Cron表达式生成"
|
||||
:visible="props.visible"
|
||||
:body-style="{ padding: '0 24px' }"
|
||||
:destroy-on-close="true"
|
||||
@cancel="fnCronModal(false)"
|
||||
@ok="fnCronModal(true)"
|
||||
>
|
||||
<a-tabs tab-position="top" type="line">
|
||||
<a-tab-pane key="1" tab="秒">
|
||||
<CronSecond v-model:value="cronValue.second"></CronSecond>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="分钟">
|
||||
<CronMinute v-model:value="cronValue.minute"></CronMinute>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="小时">
|
||||
<CronHour v-model:value="cronValue.hour"></CronHour>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="日">
|
||||
<CronDay v-model:value="cronValue.day"></CronDay>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab="月">
|
||||
<CronMonth v-model:value="cronValue.month"></CronMonth>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-input
|
||||
class="reultBox"
|
||||
addon-before="表达式预览:"
|
||||
v-model:value="cronStr"
|
||||
disabled
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import CronSecond from './components/Second.vue';
|
||||
import CronMinute from './components/Minute.vue';
|
||||
import CronHour from './components/Hour.vue';
|
||||
import CronDay from './components/Day.vue';
|
||||
import CronMonth from './components/Month.vue';
|
||||
import { reactive, computed, watch } from 'vue';
|
||||
|
||||
const emit = defineEmits(['cancel', 'ok', 'update:visible']);
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
cron: {
|
||||
type: String,
|
||||
default: '* * * * * ?',
|
||||
},
|
||||
});
|
||||
|
||||
/**cron属性值 */
|
||||
let cronValue = reactive({
|
||||
second: '*',
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
month: '*',
|
||||
week: '?',
|
||||
});
|
||||
|
||||
/**组合cron结果 */
|
||||
const cronStr = computed(() => {
|
||||
let time = `${cronValue.second} ${cronValue.minute} ${cronValue.hour}`;
|
||||
let date = `${cronValue.day} ${cronValue.month} ${cronValue.week}`;
|
||||
return `${time} ${date}`;
|
||||
});
|
||||
|
||||
/**监听是否显示,初始cron属性 */
|
||||
watch(
|
||||
() => props.visible,
|
||||
val => {
|
||||
if (!val) return;
|
||||
const arr = props.cron.split(' ');
|
||||
// 6 位以上是合法表达式
|
||||
if (arr.length >= 6) {
|
||||
Object.assign(cronValue, {
|
||||
second: arr[0],
|
||||
minute: arr[1],
|
||||
hour: arr[2],
|
||||
day: arr[3],
|
||||
month: arr[4],
|
||||
week: arr[5],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 窗口事件
|
||||
* @param val modal触发事件
|
||||
*/
|
||||
function fnCronModal(val: boolean) {
|
||||
emit('update:visible', false);
|
||||
if (val) {
|
||||
emit('ok', cronStr.value);
|
||||
} else {
|
||||
emit('cancel');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.reultBox {
|
||||
margin-top: 48px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
48
src/components/DictTag/index.vue
Normal file
48
src/components/DictTag/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
/**数据 */
|
||||
options: {
|
||||
type: Array,
|
||||
},
|
||||
/**当前的值对应数据中的项字段 */
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
/**当前的值 */
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
/**遍历找到对应值数据项 */
|
||||
const item = computed(() => {
|
||||
if (Array.isArray(props.options) && props.options.length > 0) {
|
||||
const option = (props.options as any[]).find(
|
||||
item => `${item[props.valueField]}` === `${props.value}`
|
||||
);
|
||||
return option;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="item">
|
||||
<a-tag
|
||||
v-if="item.elTagType"
|
||||
:class="item.elTagClass"
|
||||
:color="item.elTagType"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-tag>
|
||||
<span v-else :class="item.elTagClass">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else></span>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
30
src/components/IconFont/index.vue
Normal file
30
src/components/IconFont/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
|
||||
import { createFromIconfontCN } from '@ant-design/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: '#',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
});
|
||||
|
||||
/**字体图标加载为组件 */
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: scriptUrl,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconFont
|
||||
v-if="type != '#'"
|
||||
:type="props.type"
|
||||
:style="{ fontSize: size + 'px' }"
|
||||
></IconFont>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
40
src/components/LinkiFrame/index.vue
Normal file
40
src/components/LinkiFrame/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const height = ref<string>(document.documentElement.clientHeight - 94.5 + 'px');
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
let iframe = reactive({
|
||||
id: 'link',
|
||||
src: props.src,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.name) {
|
||||
iframe.id = route.name.toString();
|
||||
}
|
||||
window.onresize = () => {
|
||||
height.value = document.documentElement.clientHeight - 94.5 + 'px;';
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="'height:' + height">
|
||||
<iframe
|
||||
:id="iframe.id"
|
||||
:src="iframe.src"
|
||||
frameborder="no"
|
||||
style="width: 100%; height: 100%"
|
||||
scrolling="auto"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
5
src/constants/admin-constants.ts
Normal file
5
src/constants/admin-constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**管理员-系统指定角色KEY */
|
||||
export const ADMIN_ROLE_KEY = 'admin';
|
||||
|
||||
/**管理员-系统指定权限 */
|
||||
export const ADMIN_PERMISSION = '*:*:*';
|
||||
5
src/constants/app-constants.ts
Normal file
5
src/constants/app-constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**应用-请求头-系统标识 */
|
||||
export const APP_REQUEST_HEADER_CODE = 'X-App-Code';
|
||||
|
||||
/**应用-请求头-系统版本 */
|
||||
export const APP_REQUEST_HEADER_VERSION = 'X-App-Version';
|
||||
11
src/constants/cache-keys-constants.ts
Normal file
11
src/constants/cache-keys-constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**会话缓存-网络请求 */
|
||||
export const CACHE_SESSION_FATCH = 'cache:session:fatch';
|
||||
|
||||
/**本地缓存-布局设置 */
|
||||
export const CACHE_LOCAL_PROCONFIG = 'cache:local:proConfig';
|
||||
|
||||
/**本地缓存-主题色 */
|
||||
export const CACHE_LOCAL_PRIMARY_COLOR = 'cache:local:primaryColor';
|
||||
|
||||
/**本地缓存-多语言 */
|
||||
export const CACHE_LOCAL_I18N = 'cache:local:i18n';
|
||||
20
src/constants/menu-constants.ts
Normal file
20
src/constants/menu-constants.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**组件布局类型-基础布局组件标识 */
|
||||
export const MENU_COMPONENT_LAYOUT_BASIC = 'BasicLayout';
|
||||
|
||||
/**组件布局类型-空白布局组件标识 */
|
||||
export const MENU_COMPONENT_LAYOUT_BLANK = 'BlankLayout';
|
||||
|
||||
/**组件布局类型-内链接布局组件标识 */
|
||||
export const MENU_COMPONENT_LAYOUT_LINK = 'LinkLayout';
|
||||
|
||||
/**菜单类型-目录 */
|
||||
export const MENU_TYPE_DIR = 'D';
|
||||
|
||||
/**菜单类型-菜单 */
|
||||
export const MENU_TYPE_MENU = 'M';
|
||||
|
||||
/**菜单类型-按钮 */
|
||||
export const MENU_TYPE_BUTTON = 'B';
|
||||
|
||||
/**菜单内嵌地址标识-带/前缀 */
|
||||
export const MENU_PATH_INLINE = '/inline';
|
||||
11
src/constants/token-constants.ts
Normal file
11
src/constants/token-constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**令牌-数据响应字段 */
|
||||
export const TOKEN_RESPONSE_FIELD = 'access_token';
|
||||
|
||||
/**令牌-请求头标识前缀 */
|
||||
export const TOKEN_KEY_PREFIX = 'Bearer ';
|
||||
|
||||
/**令牌-请求头标识 */
|
||||
export const TOKEN_KEY = 'Authorization';
|
||||
|
||||
/**令牌-存放Cookie标识 */
|
||||
export const TOKEN_COOKIE = 'AuthAntdv';
|
||||
10
src/directive/index.ts
Normal file
10
src/directive/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { App } from 'vue';
|
||||
import permsDirective from './perms-directive';
|
||||
import rolesDirective from './roles-directive';
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.directive('perms', permsDirective);
|
||||
app.directive('roles', rolesDirective);
|
||||
},
|
||||
};
|
||||
38
src/directive/perms-directive.ts
Normal file
38
src/directive/perms-directive.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { hasPermissions, matchPermissions } from '@/plugins/auth-user';
|
||||
import { DirectiveBinding } from 'vue';
|
||||
|
||||
/**
|
||||
* perms-权限标识
|
||||
*
|
||||
* 指令值:字符串数组
|
||||
*
|
||||
* 指令的参数:has/math,默认has
|
||||
*
|
||||
* v-perms="['monitor:server:query']"
|
||||
* 等同
|
||||
* v-perms:has="['monitor:server:query']"
|
||||
*
|
||||
* v-perms:math="['style:user:query', 'style:user:edit']"
|
||||
*
|
||||
* @param el 指令绑定到的元素
|
||||
* @param binding 一个对象
|
||||
*/
|
||||
export default function (el: any, binding: DirectiveBinding<any>) {
|
||||
const value = binding.value;
|
||||
let arg = binding.arg;
|
||||
let ok: boolean = false;
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// 匹配
|
||||
if (arg === 'math') {
|
||||
ok = matchPermissions(value);
|
||||
}
|
||||
// 含有
|
||||
if (!arg || arg === 'has') {
|
||||
ok = hasPermissions(value);
|
||||
}
|
||||
}
|
||||
// 没有权限就移除节点
|
||||
if (!ok) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
38
src/directive/roles-directive.ts
Normal file
38
src/directive/roles-directive.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { hasRoles, matchRoles } from '@/plugins/auth-user';
|
||||
import { DirectiveBinding } from 'vue';
|
||||
|
||||
/**
|
||||
* roles-权限标识
|
||||
*
|
||||
* 指令值:字符串数组
|
||||
*
|
||||
* 指令的参数:has/math,默认has
|
||||
*
|
||||
* v-roles="['admin']"
|
||||
* 等同
|
||||
* v-roles:has="['admin']"
|
||||
*
|
||||
* v-roles:math="['common', 'user']"
|
||||
*
|
||||
* @param el 指令绑定到的元素
|
||||
* @param binding 一个对象
|
||||
*/
|
||||
export default function (el: any, binding: DirectiveBinding<any>) {
|
||||
const value = binding.value;
|
||||
let arg = binding.arg;
|
||||
let ok: boolean = false;
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// 匹配
|
||||
if (arg === 'math') {
|
||||
ok = matchRoles(value);
|
||||
}
|
||||
// 含有
|
||||
if (!arg || arg === 'has') {
|
||||
ok = hasRoles(value);
|
||||
}
|
||||
}
|
||||
// 没有权限就移除节点
|
||||
if (!ok) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
26
src/hooks/useI18n.ts
Normal file
26
src/hooks/useI18n.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { localSet } from '@/utils/cache-local-utils';
|
||||
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
|
||||
|
||||
export default function useLocale() {
|
||||
//实例化i18n
|
||||
const i18n = useI18n();
|
||||
|
||||
// 获取当前语言设置
|
||||
const currentLocale = computed(() => {
|
||||
return i18n.locale.value;
|
||||
});
|
||||
|
||||
// 切换语言
|
||||
const changeLocale = (value: string) => {
|
||||
i18n.locale.value = value;
|
||||
localSet(CACHE_LOCAL_I18N, value);
|
||||
};
|
||||
|
||||
return {
|
||||
currentLocale,
|
||||
changeLocale,
|
||||
t: i18n.t,
|
||||
};
|
||||
}
|
||||
16
src/hooks/useLoading.ts
Normal file
16
src/hooks/useLoading.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ref, inject, provide } from 'vue';
|
||||
|
||||
const INJECT_LOADING_KEY = Symbol('loading_store');
|
||||
|
||||
export const createLoading = (v = false) => {
|
||||
const loading = ref<boolean>(v);
|
||||
const change = (bool: boolean) => {
|
||||
loading.value = bool;
|
||||
};
|
||||
provide(INJECT_LOADING_KEY, loading);
|
||||
return [loading, change];
|
||||
};
|
||||
|
||||
export const useLoading = () => {
|
||||
return inject(INJECT_LOADING_KEY);
|
||||
};
|
||||
58
src/hooks/useTheme.ts
Normal file
58
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { ConfigProvider } from 'ant-design-vue/lib';
|
||||
import { CACHE_LOCAL_PRIMARY_COLOR } from '@/constants/cache-keys-constants';
|
||||
import { localGet, localSet } from '@/utils/cache-local-utils';
|
||||
|
||||
/**
|
||||
* 初始主题色
|
||||
*/
|
||||
export const usePrimaryColor = () => {
|
||||
onBeforeMount(() => {
|
||||
changePrimaryColor(getLocalColor());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 改变主题色
|
||||
* @param color 颜色
|
||||
*/
|
||||
export function changePrimaryColor(color?: string) {
|
||||
if (!color) {
|
||||
color = getRandomColor();
|
||||
}
|
||||
ConfigProvider.config({
|
||||
theme: {
|
||||
primaryColor: color,
|
||||
},
|
||||
});
|
||||
localSet(CACHE_LOCAL_PRIMARY_COLOR, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题色-本地缓存
|
||||
* @returns 颜色
|
||||
*/
|
||||
export function getLocalColor() {
|
||||
return localGet(CACHE_LOCAL_PRIMARY_COLOR) || '#1890ff';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机颜色范围
|
||||
* @returns 颜色
|
||||
*/
|
||||
function getRandomColor(): string {
|
||||
const colors: string[] = [
|
||||
'#f5222d',
|
||||
'#fa541c',
|
||||
'#fa8c16',
|
||||
'#a0d911',
|
||||
'#13c2c2',
|
||||
'#1890ff',
|
||||
'#722ed1',
|
||||
'#eb2f96',
|
||||
'#faad14',
|
||||
'#52c41a',
|
||||
];
|
||||
const i = Math.floor(Math.random() * 10);
|
||||
return colors[i];
|
||||
}
|
||||
16
src/i18n/index.ts
Normal file
16
src/i18n/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { localGet } from '@/utils/cache-local-utils';
|
||||
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
|
||||
import zhCN from './locales/zh-CN';
|
||||
import enUS from './locales/en-US';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API 的方式创建 i18n 实例
|
||||
locale: localGet(CACHE_LOCAL_I18N) || 'zh_CN', // 默认显示语言
|
||||
messages: {
|
||||
zh_CN: zhCN,
|
||||
en_US: enUS,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
62
src/i18n/locales/en-US.ts
Normal file
62
src/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export default {
|
||||
// 语言
|
||||
i18n: 'English',
|
||||
hello: 'Hello',
|
||||
|
||||
// 通用
|
||||
common: {
|
||||
title: 'Mask Antd Vue3',
|
||||
desc: 'Management system based on ant design vue+vue3',
|
||||
loading: 'Please wait...',
|
||||
tipTitle: 'Prompt',
|
||||
},
|
||||
|
||||
// 全局页脚
|
||||
globalFooter: {
|
||||
help: 'Help',
|
||||
privacy: 'Privacy',
|
||||
term: 'Term',
|
||||
},
|
||||
|
||||
// 校验
|
||||
valid: {
|
||||
userNameReg:
|
||||
'The account cannot start with a number and can contain uppercase and lowercase letters, numbers, and no less than 5 digits.',
|
||||
userNamePlease: 'Please enter the correct login account',
|
||||
userNameHit: 'Login account',
|
||||
passwordReg:
|
||||
'The password should contain at least uppercase and lowercase letters, numbers, special symbols, and no less than 6 digits.',
|
||||
passwordPlease: 'Please enter the correct login password',
|
||||
passwordHit: 'Login password',
|
||||
passwordConfirmHit: 'Confirm login password',
|
||||
phoneReg: 'Incorrect phone number',
|
||||
phonePlease: 'Please enter the correct phone number',
|
||||
phoneHit: 'Mobile number',
|
||||
codePlease: 'Please enter the correct verification code',
|
||||
codeHit: 'Verification code',
|
||||
codeText: 'Obtain verification code',
|
||||
codeSmsSend: 'Successfully sent, please pay attention to checking the SMS',
|
||||
},
|
||||
|
||||
// 页面
|
||||
views: {
|
||||
login: {
|
||||
tabPane1: 'Account password login',
|
||||
tabPane2: 'Login with phone number',
|
||||
registerBtn: 'Register an account',
|
||||
loginBtn: 'Login',
|
||||
loginSuccess: 'Login successful',
|
||||
loginMethod: 'Other login methods:',
|
||||
loginMethodWX: 'WeChat Scan Login',
|
||||
loginMethodQQ: 'QQ Scan Code Login',
|
||||
},
|
||||
register: {
|
||||
registerBtn: 'Register',
|
||||
loginBtn: 'Log in with an existing account',
|
||||
passwordErr: 'Please enter the correct confirmation password',
|
||||
passwordConfirmErr: 'The two passwords entered do not match',
|
||||
tipContent: 'Congratulations, {username} account registration succeeded!',
|
||||
tipBtn: 'Go to login',
|
||||
},
|
||||
},
|
||||
};
|
||||
60
src/i18n/locales/zh-CN.ts
Normal file
60
src/i18n/locales/zh-CN.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export default {
|
||||
// 语言
|
||||
i18n: '中文',
|
||||
hello: '你好',
|
||||
|
||||
// 通用
|
||||
common: {
|
||||
title: 'Mask Antd Vue3',
|
||||
desc: '基于 ant-design-vue + vue3 的管理系统',
|
||||
loading: '请稍等...',
|
||||
tipTitle: '提示',
|
||||
},
|
||||
|
||||
// 全局页脚
|
||||
globalFooter: {
|
||||
help: '帮助',
|
||||
privacy: '隐私',
|
||||
term: '条款',
|
||||
},
|
||||
|
||||
// 校验
|
||||
valid: {
|
||||
userNameReg: '账号不能以数字开头,可包含大写小写字母,数字,且不少于5位',
|
||||
userNamePlease: '请输入正确登录账号',
|
||||
userNameHit: '登录账号',
|
||||
passwordReg: '密码至少包含大小写字母、数字、特殊符号,且不少于6位',
|
||||
passwordPlease: '请输入正确登录密码',
|
||||
passwordHit: '登录密码',
|
||||
passwordConfirmHit: '确认登录密码',
|
||||
phoneReg: '手机号码不正确',
|
||||
phonePlease: '请输入正确的手机号码',
|
||||
phoneHit: '手机号码',
|
||||
codePlease: '请输入正确的验证码',
|
||||
codeHit: '验证码',
|
||||
codeText: '获取验证码',
|
||||
codeSmsSend: '发送成功,请注意查看短信',
|
||||
},
|
||||
|
||||
// 页面
|
||||
views: {
|
||||
login: {
|
||||
tabPane1: '账号密码登录',
|
||||
tabPane2: '手机号登录',
|
||||
registerBtn: '注册账号',
|
||||
loginBtn: '登录',
|
||||
loginSuccess: '登录成功',
|
||||
loginMethod: '其他登录方式:',
|
||||
loginMethodWX: '微信扫一扫登录',
|
||||
loginMethodQQ: 'QQ扫码登录',
|
||||
},
|
||||
register: {
|
||||
registerBtn: '注册',
|
||||
loginBtn: '使用已有账号登录',
|
||||
passwordErr: '请正确输入确认密码',
|
||||
passwordConfirmErr: '两次输入的密码不一致',
|
||||
tipContent: '恭喜您,{username} 账号注册成功!',
|
||||
tipBtn: '前往登录',
|
||||
},
|
||||
},
|
||||
};
|
||||
212
src/layouts/BasicLayout.vue
Normal file
212
src/layouts/BasicLayout.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ProLayout,
|
||||
GlobalFooter,
|
||||
WaterMark,
|
||||
getMenuData,
|
||||
clearMenuItem,
|
||||
} from '@ant-design-vue/pro-layout';
|
||||
import RightContent from './components/RightContent.vue';
|
||||
import Tabs from './components/Tabs.vue';
|
||||
import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import useLayoutStore from '@/store/modules/layout';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useRouterStore from '@/store/modules/router';
|
||||
import useTabsStore from '@/store/modules/tabs';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
|
||||
const { proConfig, waterMarkContent } = useLayoutStore();
|
||||
const { appName } = useAppStore();
|
||||
const routerStore = useRouterStore();
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
|
||||
/**菜单面板 */
|
||||
let layoutState = reactive({
|
||||
collapsed: false, // 是否展开菜单面板
|
||||
openKeys: ['/'], // 打开菜单key
|
||||
selectedKeys: ['/index'], // 选中高亮菜单key
|
||||
});
|
||||
|
||||
/**监听路由变化改变菜单面板选项 */
|
||||
watch(
|
||||
router.currentRoute,
|
||||
v => {
|
||||
const matched = v.matched.concat();
|
||||
layoutState.openKeys = matched
|
||||
.filter(r => r.path !== v.path)
|
||||
.map(r => r.path);
|
||||
layoutState.selectedKeys = matched
|
||||
.filter(r => r.name !== 'Root')
|
||||
.map(r => r.path);
|
||||
// 路由地址含有内嵌地址标识又是隐藏菜单需要处理打开高亮菜单栏
|
||||
if (v.path.includes(MENU_PATH_INLINE) && v.meta.hideInMenu) {
|
||||
const idx = v.path.lastIndexOf(MENU_PATH_INLINE);
|
||||
layoutState.openKeys.splice(-1);
|
||||
layoutState.selectedKeys[matched.length - 1] = v.path.slice(0, idx);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 动态路由添加到菜单面板
|
||||
const rootRoute = router.getRoutes().find(r => r.name === 'Root');
|
||||
if (rootRoute) {
|
||||
const children = routerStore.setRootRouterData(rootRoute.children);
|
||||
const buildRouterData = routerStore.buildRouterData;
|
||||
if (buildRouterData.length > 0) {
|
||||
rootRoute.children = children.concat(buildRouterData);
|
||||
} else {
|
||||
rootRoute.children = children;
|
||||
}
|
||||
}
|
||||
|
||||
const { menuData } = getMenuData(clearMenuItem(router.getRoutes()));
|
||||
|
||||
/**面包屑数据对象,排除根节点和首页不显示 */
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = router.currentRoute.value.matched.concat();
|
||||
// 菜单中隐藏子节点不显示面包屑
|
||||
if (matched.length == 2) {
|
||||
const hideChildInMenu = matched[0].meta?.hideChildInMenu || false;
|
||||
if (hideChildInMenu) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return matched
|
||||
.filter(r => !['Root', 'Index'].includes(r.name as string))
|
||||
.map(item => {
|
||||
return {
|
||||
path: item.path,
|
||||
breadcrumbName: item.meta.title || '-',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 给页面组件设置路由名称
|
||||
*
|
||||
* 路由名称设为缓存key
|
||||
* @param component 页面组件
|
||||
* @param name 路由名称
|
||||
*/
|
||||
function fnComponentSetName(component: any, to: any) {
|
||||
if (component && component.type) {
|
||||
// 通过路由取最后匹配的,确认是缓存的才处理
|
||||
const matched = to.matched.concat();
|
||||
const lastRoute = matched[matched.length - 1];
|
||||
if (!lastRoute.meta.cache) return component;
|
||||
const routeName = lastRoute.name;
|
||||
const routeDef = lastRoute.components.default;
|
||||
// 有命名但不是跳转的路由文件
|
||||
const __name = component.type.__name;
|
||||
if (__name && __name !== routeName) {
|
||||
routeDef.name = routeName;
|
||||
routeDef.__name = routeName;
|
||||
Reflect.set(component, 'type', routeDef);
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
// 清空导航栏标签
|
||||
tabsStore.clear();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WaterMark :content="waterMarkContent" :z-index="100">
|
||||
<ProLayout
|
||||
v-model:collapsed="layoutState.collapsed"
|
||||
v-model:selectedKeys="layoutState.selectedKeys"
|
||||
v-model:openKeys="layoutState.openKeys"
|
||||
:menu-data="menuData"
|
||||
:breadcrumb="{ routes: breadcrumb }"
|
||||
disable-content-margin
|
||||
v-bind="proConfig"
|
||||
:iconfont-url="scriptUrl"
|
||||
>
|
||||
<!--插槽-菜单头-->
|
||||
<template #menuHeaderRender>
|
||||
<RouterLink :to="{ name: 'Index' }" :replace="true">
|
||||
<img class="logo" src="@/assets/logo.png" />
|
||||
<h1>{{ appName }}</h1>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!--插槽-顶部左侧,只对side布局有效-->
|
||||
<template #headerContentRender></template>
|
||||
|
||||
<!--插槽-顶部右侧-->
|
||||
<template #rightContentRender>
|
||||
<RightContent />
|
||||
</template>
|
||||
|
||||
<!--插槽-导航标签项-->
|
||||
<template #tabRender="{ width, fixedHeader, headerRender }">
|
||||
<Tabs
|
||||
:width="width"
|
||||
:fixed-header="fixedHeader"
|
||||
:header-render="headerRender"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!--插槽-页面路由导航面包屑-->
|
||||
<template #breadcrumbRender="{ route, params, routes }">
|
||||
<span v-if="routes.indexOf(route) === routes.length - 1">
|
||||
{{ route.breadcrumbName }}
|
||||
</span>
|
||||
<RouterLink v-else :to="{ path: route.path, params }">
|
||||
{{ route.breadcrumbName }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!--内容页面视图-->
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<transition name="slide-left" mode="out-in">
|
||||
<KeepAlive :include="tabsStore.getCaches">
|
||||
<component
|
||||
:is="fnComponentSetName(Component, route)"
|
||||
:key="route.path"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
</RouterView>
|
||||
|
||||
<!--插槽-内容底部-->
|
||||
<template #footerRender>
|
||||
<GlobalFooter
|
||||
:links="[
|
||||
{
|
||||
blankTarget: true,
|
||||
title: '开发手册',
|
||||
href: 'https://juejin.cn/column/7188761626017792056',
|
||||
},
|
||||
{
|
||||
blankTarget: true,
|
||||
title: '开源仓库',
|
||||
href: 'https://gitee.com/TsMask/',
|
||||
},
|
||||
{
|
||||
blankTarget: true,
|
||||
title: '接口文档',
|
||||
href: 'https://mask-api-midwayjs.apifox.cn/',
|
||||
},
|
||||
]"
|
||||
copyright="Copyright © 2023 Gitee For TsMask"
|
||||
>
|
||||
</GlobalFooter>
|
||||
</template>
|
||||
</ProLayout>
|
||||
</WaterMark>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logo {
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
border-radius: 6.66px;
|
||||
}
|
||||
</style>
|
||||
7
src/layouts/BlankLayout.vue
Normal file
7
src/layouts/BlankLayout.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
// 承载目录下级菜单页面,需要声明才会生成name
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
47
src/layouts/LinkLayout.vue
Normal file
47
src/layouts/LinkLayout.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { isValid, decode } from 'js-base64';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { validHttp } from '@/utils/regular-utils';
|
||||
const route = useRoute();
|
||||
const height = ref<string>(document.documentElement.clientHeight - 94.5 + 'px');
|
||||
|
||||
let iframe = reactive({
|
||||
id: 'link',
|
||||
src: '',
|
||||
});
|
||||
|
||||
// 设置Frame窗口名称并设置链接地址
|
||||
if (route.name) {
|
||||
const name = route.name.toString();
|
||||
const pathArr = route.matched.concat().map(i => i.path);
|
||||
const pathLen = pathArr.length;
|
||||
let path = pathArr[pathLen - 1].replace(pathArr[pathLen - 2], '');
|
||||
path = path.substring(1);
|
||||
if (isValid(path)) {
|
||||
const url = decode(path);
|
||||
if (validHttp(url)) {
|
||||
iframe.src = url;
|
||||
} else {
|
||||
let endS = name.substring(4, 5).endsWith('s');
|
||||
iframe.src = `${endS ? 'https://' : 'http://'}${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
iframe.id = name;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="'height:' + height">
|
||||
<iframe
|
||||
:id="iframe.id"
|
||||
:src="iframe.src"
|
||||
frameborder="no"
|
||||
style="width: 100%; height: 100%"
|
||||
scrolling="auto"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
155
src/layouts/components/RightContent.vue
Normal file
155
src/layouts/components/RightContent.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
const { t, changeLocale } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
/**头像展开项点击 */
|
||||
function fnClick({ key }: MenuInfo) {
|
||||
switch (key) {
|
||||
case 'settings':
|
||||
router.push({ name: 'Settings' });
|
||||
break;
|
||||
case 'profile':
|
||||
router.push({ name: 'Profile' });
|
||||
break;
|
||||
case 'logout':
|
||||
userStore.fnLogOut().finally(() => router.push({ name: 'Login' }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**改变多语言 */
|
||||
function fnChangeLocale(e: any) {
|
||||
changeLocale(e.key);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-space :size="12" align="center">
|
||||
<a-popover
|
||||
overlayClassName="head-notice"
|
||||
placement="bottomRight"
|
||||
:trigger="['click']"
|
||||
>
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
<a-badge :count="123" :overflow-count="99">
|
||||
<BellOutlined :style="{ fontSize: '20px' }" />
|
||||
</a-badge>
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content :style="{ padding: 0 }">
|
||||
<a-tabs centered :tabBarStyle="{ width: '336px' }">
|
||||
<a-tab-pane key="1" tab="通知(41)">
|
||||
Content of Tab 通知
|
||||
<a-button type="link" block>查看更多</a-button>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="消息(231)">
|
||||
Content of Tab 消息
|
||||
<a-button type="link" block>查看更多</a-button>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="待办(1)">
|
||||
Content of Tab 待办
|
||||
<a-button type="link" block>查看更多</a-button>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
</a-popover>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>开源仓库</template>
|
||||
<a-button type="text" href="https://gitee.com/TsMask" target="_blank">
|
||||
<template #icon>
|
||||
<GithubOutlined :style="{ fontSize: '20px' }" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>文档手册</template>
|
||||
<a-button
|
||||
type="text"
|
||||
href="https://juejin.cn/column/7188761626017792056"
|
||||
target="_blank"
|
||||
>
|
||||
<template #icon>
|
||||
<QuestionCircleOutlined :style="{ fontSize: '20px' }" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-dropdown :trigger="['click', 'hover']">
|
||||
<a-button size="small" type="default">
|
||||
{{ t('i18n') }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="fnChangeLocale">
|
||||
<a-menu-item key="zh_CN">中文</a-menu-item>
|
||||
<a-menu-item key="en_US">English</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-dropdown placement="bottomRight" :trigger="['click', 'hover']">
|
||||
<div class="user">
|
||||
<a-avatar
|
||||
shape="circle"
|
||||
size="default"
|
||||
:src="userStore.getAvatar"
|
||||
:alt="userStore.userName"
|
||||
></a-avatar>
|
||||
<span class="nick">
|
||||
{{ userStore.nickName }}
|
||||
</span>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu @click="fnClick">
|
||||
<a-menu-item key="profile">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
<span>个人中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
<span>个人设置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">
|
||||
<template #icon>
|
||||
<LogoutOutlined />
|
||||
</template>
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.nick {
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
font-size: 16px;
|
||||
max-width: 164px;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
187
src/layouts/components/Tabs.vue
Normal file
187
src/layouts/components/Tabs.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script lang="ts" setup>
|
||||
import IconFont from '@/components/IconFont/index.vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useTabsStore from '@/store/modules/tabs';
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
|
||||
defineProps({
|
||||
/**标签栏宽度 */
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
/**是否固定顶部栏 */
|
||||
fixedHeader: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**是否有顶部栏 */
|
||||
headerRender: {
|
||||
type: [Object, Function, Boolean],
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
/**导航标签项列表长度 */
|
||||
let tabLen = computed(() => tabsStore.getTabs.length);
|
||||
|
||||
/**
|
||||
* 标签更多菜单项
|
||||
* @param key 菜单key
|
||||
*/
|
||||
function fnTabMenu(key: string | number) {
|
||||
const route = router.currentRoute.value;
|
||||
// 刷新当前
|
||||
if (key === 'reload') {
|
||||
const name = (route.name && route.name.toString()) || '';
|
||||
tabsStore.cacheDelete(name);
|
||||
router.replace({
|
||||
path: `/redirect${route.path}`,
|
||||
query: route.query,
|
||||
});
|
||||
}
|
||||
// 关闭当前
|
||||
if (key === 'current') {
|
||||
const to = tabsStore.tabClose(route.path);
|
||||
if (!to) return;
|
||||
// 避免重复跳转
|
||||
if (route.path === to.path) {
|
||||
tabsStore.tabOpen(route);
|
||||
} else {
|
||||
router.push(to);
|
||||
}
|
||||
}
|
||||
// 关闭其他
|
||||
if (key === 'other') {
|
||||
tabsStore.clear();
|
||||
tabsStore.tabOpen(route);
|
||||
}
|
||||
// 关闭全部
|
||||
if (key === 'all') {
|
||||
tabsStore.clear();
|
||||
// 已经是首页的避免重复跳转,默认返回首页
|
||||
if (route.path === '/index') {
|
||||
tabsStore.tabOpen(route);
|
||||
} else {
|
||||
router.push({ name: 'Index' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航标签点击
|
||||
* @param path 标签的路由路径
|
||||
*/
|
||||
function fnTabClick(path: string) {
|
||||
const to = tabsStore.tabGoto(path);
|
||||
if (!to) return;
|
||||
router.push(to);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航标签关闭
|
||||
* @param path 标签的路由路径
|
||||
*/
|
||||
function fnTabClose(path: string) {
|
||||
const to = tabsStore.tabClose(path);
|
||||
if (!to) return;
|
||||
router.push(to);
|
||||
}
|
||||
|
||||
/**监听当前路由添加到导航标签列表 */
|
||||
watch(router.currentRoute, v => tabsStore.tabOpen(v), { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<header class="ant-layout-header tabs-header" v-if="fixedHeader"></header>
|
||||
<a-tabs
|
||||
class="tabs"
|
||||
:class="{ 'tabs-fixed': fixedHeader }"
|
||||
:style="{ width: width, top: headerRender === false ? 0 : undefined }"
|
||||
hide-add
|
||||
tab-position="top"
|
||||
type="editable-card"
|
||||
:tab-bar-gutter="8"
|
||||
:tab-bar-style="{ margin: '0', height: '28px', lineHeight: '28px' }"
|
||||
v-model:activeKey="tabsStore.activePath"
|
||||
@tab-click="path => fnTabClick(path as string)"
|
||||
@edit="path => fnTabClose(path as string)"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabsStore.getTabs"
|
||||
:key="tab.path"
|
||||
:closable="tabLen > 1"
|
||||
>
|
||||
<template #tab>
|
||||
<span>
|
||||
<IconFont :type="tab.icon" style="margin: 0"></IconFont>
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
|
||||
<template #rightExtra>
|
||||
<a-space :size="8" align="end">
|
||||
<a-tooltip placement="topRight">
|
||||
<template #title>刷新当前</template>
|
||||
<a-button
|
||||
type="ghost"
|
||||
shape="circle"
|
||||
size="small"
|
||||
@click="fnTabMenu('reload')"
|
||||
>
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="topRight">
|
||||
<template #title>更多选项</template>
|
||||
<a-dropdown :trigger="['click', 'hover']" placement="bottomRight">
|
||||
<a-button type="ghost" shape="circle" size="small">
|
||||
<template #icon><DownOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => fnTabMenu(key)">
|
||||
<a-menu-item key="current">关闭当前</a-menu-item>
|
||||
<a-menu-item key="other">关闭其他 </a-menu-item>
|
||||
<a-menu-item key="all">关闭全部</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tabs-header {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
z-index: 16;
|
||||
margin: 0px;
|
||||
padding: 4px 16px;
|
||||
width: calc(100% - 208px);
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px #0015291f;
|
||||
transition: background 0.3s, width 0.2s;
|
||||
position: relative;
|
||||
|
||||
&.tabs-fixed {
|
||||
right: 0px;
|
||||
top: 48px;
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs :deep(.ant-tabs-nav:before) {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
16
src/main.ts
Normal file
16
src/main.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
import store from './store';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import directive from './directive';
|
||||
import i18n from './i18n';
|
||||
import '@ant-design-vue/pro-layout/dist/style.css';
|
||||
import 'ant-design-vue/dist/antd.variable.min.css';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
app.use(directive);
|
||||
app.use(i18n);
|
||||
|
||||
app.mount('#app');
|
||||
17
src/plugins/auth-token.ts
Normal file
17
src/plugins/auth-token.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE } from '@/constants/token-constants';
|
||||
|
||||
/**获取cookis中Token字符串 */
|
||||
export function getToken(): string {
|
||||
return Cookies.get(TOKEN_COOKIE) || '';
|
||||
}
|
||||
|
||||
/**设置cookis中Token字符串 */
|
||||
export function setToken(token: string): void {
|
||||
Cookies.set(TOKEN_COOKIE, token);
|
||||
}
|
||||
|
||||
/**移除cookis中Token字符串 */
|
||||
export function removeToken(): void {
|
||||
Cookies.remove(TOKEN_COOKIE);
|
||||
}
|
||||
54
src/plugins/auth-user.ts
Normal file
54
src/plugins/auth-user.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ADMIN_PERMISSION, ADMIN_ROLE_KEY } from '@/constants/admin-constants';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
|
||||
/**
|
||||
* 只需含有其中权限
|
||||
* @param role 权限字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function hasPermissions(permissions: string[]): boolean {
|
||||
if (!permissions || permissions.length === 0) return false;
|
||||
const userPermissions = useUserStore().permissions;
|
||||
if (!userPermissions || userPermissions.length === 0) return false;
|
||||
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
|
||||
return permissions.some(p => userPermissions.some(up => up === p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时匹配其中权限
|
||||
* @param role 权限字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function matchPermissions(permissions: string[]): boolean {
|
||||
if (!permissions || permissions.length === 0) return false;
|
||||
const userPermissions = useUserStore().permissions;
|
||||
if (!userPermissions || userPermissions.length === 0) return false;
|
||||
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
|
||||
return permissions.every(p => userPermissions.some(up => up === p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 只需含有其中角色
|
||||
* @param role 角色字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function hasRoles(roles: string[]): boolean {
|
||||
if (!roles || roles.length === 0) return false;
|
||||
const userRoles = useUserStore().roles;
|
||||
if (!userRoles || userRoles.length === 0) return false;
|
||||
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
|
||||
return roles.some(r => userRoles.some(ur => ur === r));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时匹配其中角色
|
||||
* @param role 角色字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function matchRoles(roles: string[]): boolean {
|
||||
if (!roles || roles.length === 0) return false;
|
||||
const userRoles = useUserStore().roles;
|
||||
if (!userRoles || userRoles.length === 0) return false;
|
||||
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
|
||||
return roles.every(r => userRoles.some(ur => ur === r));
|
||||
}
|
||||
270
src/plugins/http-fetch.ts
Normal file
270
src/plugins/http-fetch.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { getToken, removeToken } from '@/plugins/auth-token';
|
||||
import { sessionGetJSON, sessionSetJSON } from '@/utils/cache-session-utils';
|
||||
import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants';
|
||||
import { CACHE_SESSION_FATCH } from '@/constants/cache-keys-constants';
|
||||
import {
|
||||
APP_REQUEST_HEADER_CODE,
|
||||
APP_REQUEST_HEADER_VERSION,
|
||||
} from '@/constants/app-constants';
|
||||
|
||||
/**响应结果类型 */
|
||||
export type ResultType = {
|
||||
/**响应码 */
|
||||
code: number | 200 | 500;
|
||||
/**信息 */
|
||||
msg: string;
|
||||
/**数据 */
|
||||
data?: any;
|
||||
/**未知属性 */
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**防止重复提交类型 */
|
||||
type RepeatSubmitType = {
|
||||
/**请求地址 */
|
||||
url: string;
|
||||
/**请求数据 */
|
||||
data: string;
|
||||
/**请求时间 */
|
||||
time: number;
|
||||
};
|
||||
|
||||
/**请求参数类型 */
|
||||
type OptionsType = {
|
||||
/**请求地址 */
|
||||
url: string;
|
||||
/**请求方法 */
|
||||
method: 'get' | 'post' | 'put' | 'delete';
|
||||
/**请求头 */
|
||||
headers?: HeadersInit;
|
||||
/**地址栏参数 */
|
||||
params?: Record<string, string | number | boolean | undefined>;
|
||||
/**发送数据 */
|
||||
data?: Record<string, any> | FormData | object;
|
||||
/**请求数据类型 */
|
||||
dataType?: 'form-data' | 'json';
|
||||
/**响应数据类型 */
|
||||
responseType?: 'text' | 'json' | 'blob' | 'arrayBuffer';
|
||||
/**请求缓存策略 */
|
||||
cache?: RequestCache;
|
||||
/**请求的凭证,如 omit、same-origin、include */
|
||||
credentials?: RequestCredentials;
|
||||
/**请求体 */
|
||||
body?: BodyInit;
|
||||
/**防止数据重复提交 */
|
||||
repeatSubmit?: boolean;
|
||||
/**携带授权Token请求头 */
|
||||
whithToken?: boolean;
|
||||
};
|
||||
|
||||
/**全局配置类型 */
|
||||
type ConfigType = {
|
||||
/**请求的根域名地址-不带/后缀 */
|
||||
baseUrl: string;
|
||||
/**超时时间,毫秒 */
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
/**默认配置 */
|
||||
const FATCH_CONFIG: ConfigType = {
|
||||
baseUrl: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10 * 1000,
|
||||
};
|
||||
|
||||
/**默认请求参数 */
|
||||
const FATCH_OPTIONS: OptionsType = {
|
||||
url: '',
|
||||
method: 'get',
|
||||
headers: {
|
||||
[APP_REQUEST_HEADER_CODE]: import.meta.env.VITE_APP_CODE,
|
||||
[APP_REQUEST_HEADER_VERSION]: import.meta.env.VITE_APP_VERSION,
|
||||
// 使用mock.apifox.cn时开启
|
||||
// apifoxToken: '8zCzh3vipdEwd1ukv9lQEuTekdWIH7xN',
|
||||
},
|
||||
dataType: 'json',
|
||||
responseType: 'json',
|
||||
cache: 'no-cache',
|
||||
credentials: undefined,
|
||||
repeatSubmit: true,
|
||||
whithToken: true,
|
||||
};
|
||||
|
||||
/**请求前的拦截 */
|
||||
function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
|
||||
options.headers = Object.assign({}, options.headers);
|
||||
//console.log('请求前的拦截', options);
|
||||
|
||||
// 给发送数据类型设置请求头
|
||||
if (options.dataType === 'json') {
|
||||
Reflect.set(
|
||||
options.headers,
|
||||
'content-type',
|
||||
'application/json;charset=utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
// 是否需要设置 token
|
||||
const token = getToken();
|
||||
if (options.whithToken && token) {
|
||||
Reflect.set(options.headers, TOKEN_KEY, TOKEN_KEY_PREFIX + token);
|
||||
}
|
||||
// 是否需要防止数据重复提交
|
||||
if (
|
||||
options.repeatSubmit &&
|
||||
options.dataType === 'json' &&
|
||||
['post', 'put'].includes(options.method)
|
||||
) {
|
||||
const requestObj: RepeatSubmitType = {
|
||||
url: options.url,
|
||||
data: JSON.stringify(options.data),
|
||||
time: Date.now(),
|
||||
};
|
||||
const sessionObj: RepeatSubmitType = sessionGetJSON(CACHE_SESSION_FATCH);
|
||||
if (sessionObj) {
|
||||
const { url, data, time } = sessionObj;
|
||||
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
|
||||
if (
|
||||
requestObj.url === url &&
|
||||
requestObj.data === data &&
|
||||
requestObj.time - time < interval
|
||||
) {
|
||||
const message = '数据正在处理,请勿重复提交';
|
||||
console.warn(`[${url}]: ${message}`);
|
||||
return Promise.reject(message);
|
||||
} else {
|
||||
sessionSetJSON(CACHE_SESSION_FATCH, requestObj);
|
||||
}
|
||||
} else {
|
||||
sessionSetJSON(CACHE_SESSION_FATCH, requestObj);
|
||||
}
|
||||
}
|
||||
|
||||
// get请求拼接地址栏参数
|
||||
if (options.method === 'get' && options.params) {
|
||||
let paramStr = '';
|
||||
const params = options.params;
|
||||
for (const key in params) {
|
||||
const value = params[key];
|
||||
// 空字符或未定义的值不作为参数发送
|
||||
if (value === '' || value === undefined) continue;
|
||||
paramStr += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
if (paramStr && paramStr.startsWith('&')) {
|
||||
options.url = `${options.url}?${paramStr.substring(1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 非get参数提交
|
||||
if (options.data instanceof FormData) {
|
||||
options.body = options.data;
|
||||
} else {
|
||||
options.body = JSON.stringify(options.data);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**请求后的拦截 */
|
||||
function interceptorResponse(res: ResultType): ResultType | Promise<any> {
|
||||
//console.log('请求后的拦截', res);
|
||||
// 登录失效时,移除授权令牌并重新刷新页面
|
||||
if (res.code === 401) {
|
||||
removeToken();
|
||||
window.location.reload();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求http
|
||||
*
|
||||
* @param options 请求参数
|
||||
*
|
||||
* responseType改变响应结果类型
|
||||
* @returns 返回 Promise<ResultType>
|
||||
*/
|
||||
export async function request(options: OptionsType): Promise<ResultType> {
|
||||
// 请求超时控制请求终止
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort(); // 终止请求
|
||||
}, FATCH_CONFIG.timeout);
|
||||
|
||||
options = Object.assign({ signal }, FATCH_OPTIONS, options);
|
||||
|
||||
// 检查请求拦截
|
||||
const beforeReq = beforeRequest(options);
|
||||
if (beforeReq instanceof Promise) {
|
||||
return await beforeReq;
|
||||
}
|
||||
options = beforeReq;
|
||||
|
||||
// 判断用户传递的URL是否http或/开头
|
||||
if (!options.url.startsWith('http')) {
|
||||
const uri = options.url.startsWith('/') ? options.url : `/${options.url}`;
|
||||
options.url = FATCH_CONFIG.baseUrl + uri;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(options.url, options);
|
||||
// console.log('请求结果:', res);
|
||||
if (res.status === 500) {
|
||||
return {
|
||||
code: 500,
|
||||
msg: '服务器连接出错!',
|
||||
};
|
||||
}
|
||||
|
||||
// 根据响应数据类型返回
|
||||
switch (options.responseType) {
|
||||
case 'text': // 文本数据
|
||||
const str = await res.text();
|
||||
return {
|
||||
code: 200,
|
||||
msg: str,
|
||||
};
|
||||
case 'json': // json格式数据
|
||||
const result = await res.json();
|
||||
// 请求后的拦截
|
||||
const beforeRes = interceptorResponse(result);
|
||||
if (beforeRes instanceof Promise) {
|
||||
return await beforeRes;
|
||||
}
|
||||
return result;
|
||||
case 'blob': // 二进制数据则直接返回
|
||||
case 'arrayBuffer':
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.startsWith('application/json')) {
|
||||
const result = await res.json();
|
||||
return result as ResultType;
|
||||
}
|
||||
const data =
|
||||
options.responseType === 'blob'
|
||||
? await res.blob()
|
||||
: await res.arrayBuffer();
|
||||
return {
|
||||
code: 200,
|
||||
msg: '成功',
|
||||
data: data,
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
code: 500,
|
||||
msg: '未知响应数据类型',
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 请求被终止时
|
||||
if (error.name === 'AbortError') {
|
||||
return {
|
||||
code: 500,
|
||||
msg: '网络连接超时!',
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId); // 请求成功,清除超时计时器
|
||||
}
|
||||
}
|
||||
289
src/router/index.ts
Normal file
289
src/router/index.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
RouteRecordRaw,
|
||||
} from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import BasicLayout from '../layouts/BasicLayout.vue';
|
||||
import BlankLayout from '../layouts/BlankLayout.vue';
|
||||
import LinkLayout from '../layouts/LinkLayout.vue';
|
||||
import { encode } from 'js-base64';
|
||||
import { getToken } from '@/plugins/auth-token';
|
||||
import { validHttp } from '@/utils/regular-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useRouterStore from '@/store/modules/router';
|
||||
|
||||
// NProgress Configuration
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
||||
// import { MetaRecord, MenuDataItem } from '@ant-design-vue/pro-layout';
|
||||
// mate数据类型 MetaRecord
|
||||
// 根据/路径构建菜单列表,列表项类型 MenuDataItem
|
||||
// https://github.com/vueComponent/pro-components/blob/a19279f3a28190bf11e8c36f316c92dbd3387a6d/packages/pro-layout/src/typings.ts#L16
|
||||
// 菜单图标来源 https://ant.design/components/icon 自定义iconfont
|
||||
|
||||
/**公共路由 */
|
||||
const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Root',
|
||||
meta: { title: '根节点' },
|
||||
component: BasicLayout,
|
||||
redirect: '/index',
|
||||
children: [
|
||||
{
|
||||
path: '/index',
|
||||
name: 'Index',
|
||||
meta: { title: '首页', icon: 'icon-pcduan', cache: true },
|
||||
component: () => import('@/views/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dome1',
|
||||
name: 'Dome1',
|
||||
meta: { title: '示例一', icon: 'icon-ios' },
|
||||
component: () => import('@/views/dome/dome1.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dome2',
|
||||
name: 'Dome2',
|
||||
meta: { title: '示例二', icon: 'icon-anzhuo' },
|
||||
component: () => import('@/views/dome/dome2.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dome3',
|
||||
name: 'Dome3',
|
||||
meta: { title: '示例三', icon: 'icon-qunzhu' },
|
||||
component: () => import('@/views/dome/dome3.vue'),
|
||||
},
|
||||
{
|
||||
path: '/domes',
|
||||
name: 'Domes',
|
||||
meta: {
|
||||
title: '示例目录',
|
||||
icon: 'icon-zhizuoliucheng',
|
||||
},
|
||||
component: BlankLayout,
|
||||
redirect: () => ({ name: 'PageInfo' }),
|
||||
children: [
|
||||
{
|
||||
path: 'page-info',
|
||||
name: 'PageInfo',
|
||||
meta: { title: '页面信息', icon: 'icon-huifu' },
|
||||
component: () => import('../views/domes/page-info.vue'),
|
||||
},
|
||||
{
|
||||
path: 'page-typography',
|
||||
name: 'PageTypography',
|
||||
meta: { title: '文本信息', icon: 'icon-huizhiguize' },
|
||||
component: () => import('../views/domes/page-typography.vue'),
|
||||
},
|
||||
{
|
||||
path: 'dynamic-match/:id(\\d+)',
|
||||
name: 'DynamicMatch',
|
||||
// 路由 path 默认参数再 meta.params 里
|
||||
meta: { title: '动态参数页面', params: { id: 1 }, cache: true },
|
||||
component: () => import('../views/domes/dynamic-match.vue'),
|
||||
},
|
||||
{
|
||||
path: 'disabled',
|
||||
name: 'Disabled',
|
||||
meta: { title: '禁止点击', disabled: true },
|
||||
component: () => {},
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/TsMask',
|
||||
name: 'BlankGithubTsMask',
|
||||
meta: {
|
||||
title: 'TsMask-打开新窗',
|
||||
icon: 'icon-github',
|
||||
target: '_blank',
|
||||
},
|
||||
component: () => {},
|
||||
},
|
||||
{
|
||||
path: encode('https://www.antdv.com/components/comment-cn'),
|
||||
name: 'HttpsAntDesignVue',
|
||||
meta: {
|
||||
title: 'Antdv-内嵌窗口',
|
||||
icon: 'icon-morentouxiang',
|
||||
target: null,
|
||||
},
|
||||
component: LinkLayout,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/',
|
||||
name: 'BlankGithub',
|
||||
meta: {
|
||||
title: 'Github-打开新窗',
|
||||
icon: 'icon-github',
|
||||
target: '_blank',
|
||||
},
|
||||
component: () => {},
|
||||
},
|
||||
{
|
||||
path: 'https://www.antdv.com/components/comment-cn?sdf=12321&id=12&sdnf',
|
||||
name: 'SelfAnt Design Vue',
|
||||
meta: {
|
||||
title: 'Antdv-当前窗口',
|
||||
icon: 'icon-morentouxiang',
|
||||
target: '_self',
|
||||
},
|
||||
component: LinkLayout,
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'Account',
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
},
|
||||
component: BlankLayout,
|
||||
redirect: '/account/profile',
|
||||
children: [
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'Profile',
|
||||
meta: { title: '个人信息', cache: true },
|
||||
component: () => import('@/views/account/profile.vue'),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
meta: { title: '个人设置', cache: true },
|
||||
component: () => import('@/views/account/settings.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
meta: { title: '登录' },
|
||||
component: () => import('@/views/login.vue'),
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
meta: { title: '注册' },
|
||||
component: () => import('@/views/register.vue'),
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'NotPermission',
|
||||
meta: { title: '没有访问权限' },
|
||||
component: () => import('@/views/error/403.vue'),
|
||||
},
|
||||
{
|
||||
path: '/redirect',
|
||||
name: 'Redirect',
|
||||
meta: { title: '重定向' },
|
||||
component: BasicLayout,
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path(.*)',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
meta: { title: '找不到匹配页面' },
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
// 根据.env配置获取是否带井号和基础路径
|
||||
const hasHash = import.meta.env.VITE_HISTORY_HASH;
|
||||
const bashUrl = import.meta.env.VITE_HISTORY_BASE_URL;
|
||||
|
||||
/**全局路由 */
|
||||
const router = createRouter({
|
||||
history:
|
||||
hasHash === 'true'
|
||||
? createWebHashHistory(bashUrl)
|
||||
: createWebHistory(bashUrl),
|
||||
routes: constantRoutes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
} else {
|
||||
return { top: 0 };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**全局路由-后置守卫 */
|
||||
router.afterEach((to, from, failure) => {
|
||||
NProgress.done();
|
||||
// 设置标题
|
||||
if (to.meta?.title) {
|
||||
useAppStore().setTitle(to.meta.title);
|
||||
}
|
||||
});
|
||||
|
||||
/**无Token可访问页面地址白名单 */
|
||||
const WHITE_LIST: string[] = ['/login', '/auth-redirect', '/bind', '/register'];
|
||||
|
||||
/**全局路由-前置守卫 */
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start();
|
||||
const token = getToken();
|
||||
|
||||
// 没有token
|
||||
if (!token) {
|
||||
if (WHITE_LIST.includes(to.path)) {
|
||||
// 在免登录白名单,直接进入
|
||||
next();
|
||||
} else {
|
||||
// 否则全部重定向到登录页
|
||||
next(`/login?redirect=${to.fullPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 有Token
|
||||
if (token) {
|
||||
// 防止重复访问登录页面
|
||||
if (to.path === '/login') {
|
||||
next({ name: 'Index' });
|
||||
} else {
|
||||
// 判断当前用户是否有角色信息
|
||||
const user = useUserStore();
|
||||
if (user.roles && user.roles.length === 0) {
|
||||
// 获取用户信息
|
||||
user
|
||||
.fnGetInfo()
|
||||
.then(() => {
|
||||
return useRouterStore().generateRoutes();
|
||||
})
|
||||
.then(accessRoutes => {
|
||||
// 根据后台配置生成可访问的路由表
|
||||
if (accessRoutes && accessRoutes.length !== 0) {
|
||||
for (const route of accessRoutes) {
|
||||
// 动态添加可访问路由表,http开头会异常
|
||||
if (!validHttp(route.path)) {
|
||||
router.addRoute(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 刷新替换原先路由,确保addRoutes已完成
|
||||
next({ ...to, replace: true });
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(`[${to.path}]: ${e.message}`);
|
||||
user.fnLogOut().finally(() => {
|
||||
next({ name: 'Login' });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
5
src/store/index.ts
Normal file
5
src/store/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
export default store;
|
||||
31
src/store/modules/app.ts
Normal file
31
src/store/modules/app.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**应用参数类型 */
|
||||
type AppStore = {
|
||||
/**应用名称 */
|
||||
appName: string;
|
||||
/**应用标识 */
|
||||
appCode: string;
|
||||
/**应用版本 */
|
||||
appVersion: string;
|
||||
};
|
||||
|
||||
const useAppStore = defineStore('app', {
|
||||
state: (): AppStore => ({
|
||||
appName: import.meta.env.VITE_APP_NAME,
|
||||
appCode: import.meta.env.VITE_APP_CODE,
|
||||
appVersion: import.meta.env.VITE_APP_VERSION,
|
||||
}),
|
||||
actions: {
|
||||
/**设置网页标题 */
|
||||
setTitle(title?: string) {
|
||||
if (title) {
|
||||
document.title = `${title} - ${this.appName}`;
|
||||
} else {
|
||||
document.title = this.appName;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useAppStore;
|
||||
63
src/store/modules/dict.ts
Normal file
63
src/store/modules/dict.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { getDictDataType } from '@/api/system/dict/data';
|
||||
|
||||
/**字典参数类型 */
|
||||
type DictStore = {
|
||||
/**字典数据 */
|
||||
dicts: Map<string, DictType[]>;
|
||||
};
|
||||
|
||||
const useDictStore = defineStore('dict', {
|
||||
state: (): DictStore => ({
|
||||
dicts: new Map(),
|
||||
}),
|
||||
actions: {
|
||||
/**清空字典 */
|
||||
clearDict() {
|
||||
this.dicts.clear();
|
||||
},
|
||||
/**删除字典 */
|
||||
removeDict(key: string) {
|
||||
if (!key) return;
|
||||
return this.dicts.delete(key);
|
||||
},
|
||||
/**
|
||||
* 处理字典数据对象用于回显标签
|
||||
* @param data 字典数据项
|
||||
* @returns
|
||||
*/
|
||||
parseDataDict(data: Record<string, any>) {
|
||||
return [
|
||||
{
|
||||
label: data.dictLabel,
|
||||
value: data.dictValue,
|
||||
elTagType: data.tagType,
|
||||
elTagClass: data.tagClass,
|
||||
},
|
||||
];
|
||||
},
|
||||
/**获取字典 */
|
||||
async getDict(key: string) {
|
||||
if (!key) return [];
|
||||
let disct = this.dicts.get(key);
|
||||
if (disct === undefined || disct.length === 0) {
|
||||
const res = await getDictDataType(key);
|
||||
if (res.code === 200 && Array.isArray(res.data)) {
|
||||
const dictData: DictType[] = res.data.map(d => ({
|
||||
label: d.dictLabel,
|
||||
value: d.dictValue,
|
||||
elTagType: d.tagType,
|
||||
elTagClass: d.tagClass,
|
||||
}));
|
||||
this.dicts.set(key, dictData);
|
||||
disct = dictData;
|
||||
} else {
|
||||
disct = [];
|
||||
}
|
||||
}
|
||||
return disct;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useDictStore;
|
||||
91
src/store/modules/layout.ts
Normal file
91
src/store/modules/layout.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { CACHE_LOCAL_PROCONFIG } from '@/constants/cache-keys-constants';
|
||||
import { localGetJSON, localSetJSON } from '@/utils/cache-local-utils';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**布局参数类型 */
|
||||
type LayoutStore = {
|
||||
/**布局设置抽屉显示 */
|
||||
visible: boolean;
|
||||
/**布局配置 */
|
||||
proConfig: {
|
||||
/**导航布局 */
|
||||
layout: 'side' | 'top' | 'mix';
|
||||
/**导航菜单主题色 */
|
||||
navTheme: 'dark' | 'light';
|
||||
/**顶部导航主题,仅导航布局为mix时生效 */
|
||||
headerTheme: 'dark' | 'light';
|
||||
/**固定顶部栏 */
|
||||
fixedHeader: boolean;
|
||||
/**固定菜单栏 */
|
||||
fixSiderbar: boolean;
|
||||
/**自动分割菜单 */
|
||||
splitMenus: boolean;
|
||||
/**内容区域-顶栏 */
|
||||
headerRender: any | boolean | undefined;
|
||||
/**内容区域-页脚 */
|
||||
footerRender: any | boolean | undefined;
|
||||
/**内容区域-菜单头 */
|
||||
menuHeaderRender: any | boolean | undefined;
|
||||
/**内容区域-导航标签项 */
|
||||
tabRender: any | boolean | undefined;
|
||||
};
|
||||
/**水印内容 */
|
||||
waterMarkContent: string;
|
||||
};
|
||||
|
||||
/**判断是否关闭内容区域 */
|
||||
const proRender = (render: any) => (render === false ? false : undefined);
|
||||
|
||||
/**本地缓存-布局配置设置 */
|
||||
const proConfigLocal: LayoutStore['proConfig'] = localGetJSON(
|
||||
CACHE_LOCAL_PROCONFIG
|
||||
) || {
|
||||
layout: 'side',
|
||||
headerTheme: 'light',
|
||||
navTheme: 'light',
|
||||
fixSiderbar: true,
|
||||
fixedHeader: true,
|
||||
splitMenus: true,
|
||||
};
|
||||
|
||||
const useLayoutStore = defineStore('layout', {
|
||||
state: (): LayoutStore => ({
|
||||
visible: false,
|
||||
proConfig: {
|
||||
layout: proConfigLocal.layout,
|
||||
navTheme: proConfigLocal.navTheme,
|
||||
headerTheme: proConfigLocal.headerTheme,
|
||||
fixedHeader: Boolean(proConfigLocal.fixedHeader),
|
||||
fixSiderbar: Boolean(proConfigLocal.fixSiderbar),
|
||||
splitMenus: Boolean(proConfigLocal.splitMenus),
|
||||
headerRender: proRender(proConfigLocal.headerRender),
|
||||
footerRender: proRender(proConfigLocal.footerRender),
|
||||
menuHeaderRender: proRender(proConfigLocal.menuHeaderRender),
|
||||
tabRender: proRender(proConfigLocal.tabRender),
|
||||
},
|
||||
waterMarkContent: import.meta.env.VITE_APP_NAME,
|
||||
}),
|
||||
actions: {
|
||||
/**改变显示状态 */
|
||||
changeVisibleLayoutSetting() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
/**修改水印文字 */
|
||||
changeWaterMark(text: string) {
|
||||
this.waterMarkContent = text;
|
||||
},
|
||||
/**修改布局设置 */
|
||||
changeConf(key: string, value: boolean | string | number | undefined) {
|
||||
if (Reflect.has(this.proConfig, key)) {
|
||||
// 同时修改mix混合菜单的导航主题
|
||||
if (key === 'navTheme') {
|
||||
Reflect.set(this.proConfig, 'headerTheme', value);
|
||||
}
|
||||
Reflect.set(this.proConfig, key, value);
|
||||
localSetJSON(CACHE_LOCAL_PROCONFIG, this.proConfig);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useLayoutStore;
|
||||
152
src/store/modules/router.ts
Normal file
152
src/store/modules/router.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
RouteComponent,
|
||||
RouteLocationRaw,
|
||||
RouteMeta,
|
||||
RouteRecordRaw,
|
||||
} from 'vue-router';
|
||||
import { getRouters } from '@/api/router';
|
||||
import BasicLayout from '@/layouts/BasicLayout.vue';
|
||||
import BlankLayout from '@/layouts/BlankLayout.vue';
|
||||
import LinkLayout from '@/layouts/LinkLayout.vue';
|
||||
import {
|
||||
MENU_COMPONENT_LAYOUT_BASIC,
|
||||
MENU_COMPONENT_LAYOUT_BLANK,
|
||||
MENU_COMPONENT_LAYOUT_LINK,
|
||||
} from '@/constants/menu-constants';
|
||||
|
||||
/**路由构建参数类型 */
|
||||
type RouterStore = {
|
||||
/**初始的根路由数据 */
|
||||
rootRouterData: RouteRecordRaw[];
|
||||
/**动态路由数据 */
|
||||
buildRouterData: RouteRecordRaw[];
|
||||
};
|
||||
|
||||
const useRouterStore = defineStore('router', {
|
||||
state: (): RouterStore => ({
|
||||
rootRouterData: [],
|
||||
buildRouterData: [],
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
* 记录初始根节点菜单数据
|
||||
* @param data 初始数据
|
||||
* @returns 初始数据
|
||||
*/
|
||||
setRootRouterData(data: RouteRecordRaw[]) {
|
||||
if (this.rootRouterData.length <= 0) {
|
||||
this.rootRouterData = data;
|
||||
}
|
||||
return this.rootRouterData;
|
||||
},
|
||||
/**
|
||||
* 动态路由列表数据生成
|
||||
* @returns 生成的路由菜单
|
||||
*/
|
||||
async generateRoutes() {
|
||||
const res = await getRouters();
|
||||
if (res.code === 200 && Array.isArray(res.data)) {
|
||||
const buildRoutes = buildRouters(res.data.concat());
|
||||
this.buildRouterData = buildRoutes;
|
||||
return buildRoutes;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**异步路由类型 */
|
||||
type RecordRaws = {
|
||||
path: string;
|
||||
name: string;
|
||||
meta: RouteMeta;
|
||||
redirect: RouteLocationRaw;
|
||||
component: string;
|
||||
children: RecordRaws[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建动态路由
|
||||
*
|
||||
* 遍历后台配置的路由菜单,转换为组件路由菜单
|
||||
*
|
||||
* @param recordRaws 异步路由列表
|
||||
* @returns 可添加的路由列表
|
||||
*/
|
||||
function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] {
|
||||
const routers: RouteRecordRaw[] = [];
|
||||
for (const item of recordRaws) {
|
||||
// 路由页面组件
|
||||
let component: RouteComponent = {};
|
||||
if (item.component) {
|
||||
const comp = item.component;
|
||||
if (comp === MENU_COMPONENT_LAYOUT_BASIC) {
|
||||
component = BasicLayout;
|
||||
} else if (comp === MENU_COMPONENT_LAYOUT_BLANK) {
|
||||
component = BlankLayout;
|
||||
} else if (comp === MENU_COMPONENT_LAYOUT_LINK) {
|
||||
component = LinkLayout;
|
||||
} else {
|
||||
// 指定页面视图,一般用于显示子菜单
|
||||
component = findView(comp);
|
||||
}
|
||||
}
|
||||
|
||||
// 有子菜单进行递归
|
||||
let children: RouteRecordRaw[] = [];
|
||||
if (item.children && item.children.length > 0) {
|
||||
children = buildRouters(item.children);
|
||||
}
|
||||
|
||||
// 对元数据特殊参数进行处理
|
||||
let metaIcon = (item.meta?.icon as string) || '';
|
||||
if (!metaIcon.startsWith('icon-')) {
|
||||
metaIcon = '';
|
||||
}
|
||||
item.meta = Object.assign(item.meta, {
|
||||
icon: metaIcon,
|
||||
});
|
||||
|
||||
// 构建路由
|
||||
const router: RouteRecordRaw = {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
meta: item.meta,
|
||||
redirect: item.redirect,
|
||||
component: component,
|
||||
children: children,
|
||||
};
|
||||
routers.push(router);
|
||||
}
|
||||
return routers;
|
||||
}
|
||||
|
||||
/**匹配views里面所有的.vue或.tsx文件 */
|
||||
const views = import.meta.glob('./../../views/**/*.{vue,tsx}');
|
||||
|
||||
/**
|
||||
* 查找页面模块
|
||||
*
|
||||
* 查找 `/views/system/menu/index.vue` 或 `/views/system/menu/index.tsx`
|
||||
*
|
||||
* 参数值为 `system/menu/index`
|
||||
*
|
||||
* @param dirName 组件路径
|
||||
* @returns 路由懒加载函数
|
||||
*/
|
||||
function findView(dirName: string) {
|
||||
for (const dir in views) {
|
||||
let viewDirName = '';
|
||||
const component = dir.match(/views\/(.+)\.(vue|tsx)/);
|
||||
if (component && component.length === 3) {
|
||||
viewDirName = component[1];
|
||||
}
|
||||
if (viewDirName === dirName) {
|
||||
return () => views[dir]();
|
||||
}
|
||||
}
|
||||
return () => import('@/views/error/404.vue');
|
||||
}
|
||||
|
||||
export default useRouterStore;
|
||||
189
src/store/modules/tabs.ts
Normal file
189
src/store/modules/tabs.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import type { LocationQuery, RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
/**导航标签栏类型 */
|
||||
type TabsStore = {
|
||||
/**标签列表 */
|
||||
tabs: TabType[];
|
||||
/**激活标签项 */
|
||||
activePath: string;
|
||||
/**缓存页面路由名称 */
|
||||
caches: Set<string>;
|
||||
};
|
||||
|
||||
/**标签信息类型 */
|
||||
type TabType = {
|
||||
path: string;
|
||||
query: LocationQuery;
|
||||
name: string;
|
||||
title: string;
|
||||
icon?: any;
|
||||
cache?: boolean;
|
||||
};
|
||||
|
||||
const useTabsStore = defineStore('tabs', {
|
||||
state: (): TabsStore => ({
|
||||
tabs: [],
|
||||
activePath: '',
|
||||
caches: new Set(),
|
||||
}),
|
||||
getters: {
|
||||
/**获取导航标签栏列表 */
|
||||
getTabs(state) {
|
||||
return state.tabs;
|
||||
},
|
||||
/**获取缓存页面名 */
|
||||
getCaches(state) {
|
||||
return [...state.caches];
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**清空标签项和缓存项列表 */
|
||||
clear() {
|
||||
this.tabs = [];
|
||||
this.caches.clear();
|
||||
},
|
||||
/**
|
||||
* 删除标签项
|
||||
* @param path 当期标签路由地址
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
remove(path: string) {
|
||||
if (!path) return false;
|
||||
const tabIndex = this.tabs.findIndex(tab => tab.path === path);
|
||||
if (tabIndex === -1) return false;
|
||||
// 同名称标签只剩一个时,才移除缓存
|
||||
const name = this.tabs[tabIndex].name;
|
||||
const tabs = this.tabs.filter(tab => tab.name === name);
|
||||
if (tabs.length <= 1) {
|
||||
this.cacheDelete(name);
|
||||
}
|
||||
this.tabs.splice(tabIndex, 1);
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* 添加标签项
|
||||
* @param tab 标签信息对象
|
||||
* @param index 插入指定位置,默认加到最后
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
add(tab: TabType, index?: number) {
|
||||
const { path, query, name, title, icon, cache } = tab;
|
||||
// 是否缓存
|
||||
if (cache) {
|
||||
this.cacheAdd(name);
|
||||
}
|
||||
// 获取没有才添加
|
||||
let tabIndex = this.tabs.findIndex(tab => tab.path === path);
|
||||
if (tabIndex >= 0) return false;
|
||||
const idx = index ? index : this.tabs.length;
|
||||
this.tabs.splice(idx, 0, { path, query, name, title, icon });
|
||||
return true;
|
||||
},
|
||||
/**添加缓存项
|
||||
* @param name 路由名称
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
cacheAdd(name: string) {
|
||||
if (!name) return;
|
||||
const has = this.caches.has(name);
|
||||
if (has) return;
|
||||
this.caches.add(name);
|
||||
},
|
||||
/**
|
||||
* 删除缓存项
|
||||
* @param name 路由名称
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
cacheDelete(name: string) {
|
||||
if (!name) return false;
|
||||
const has = this.caches.has(name);
|
||||
if (!has) return false;
|
||||
return this.caches.delete(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开标签
|
||||
*
|
||||
* 动态参数会开新标签,这是考虑多信息查看才没用同一个标签打开。
|
||||
* @param raw 跳转的路由信息
|
||||
* @returns 无
|
||||
*/
|
||||
tabOpen(raw: RouteLocationNormalizedLoaded) {
|
||||
// 刷新是重定向不记录
|
||||
if (raw.path.startsWith('/redirect')) return;
|
||||
// 标签缓存使用路由名称
|
||||
const name = (raw.name && raw.name.toString()) || '-';
|
||||
// 新增到当期标签后面打开,获取当期标签下标
|
||||
const tabIndex = this.tabs.findIndex(tab => tab.path === this.activePath);
|
||||
this.add(
|
||||
{
|
||||
path: raw.path,
|
||||
query: raw.query,
|
||||
name: name,
|
||||
title: raw.meta.title || '-',
|
||||
icon: raw.meta.icon || '#',
|
||||
cache: Boolean(raw.meta.cache),
|
||||
},
|
||||
tabIndex + 1
|
||||
);
|
||||
// 设置激活项
|
||||
this.activePath = raw.path;
|
||||
},
|
||||
/**
|
||||
* 关闭标签
|
||||
* @param path 当期标签路由地址
|
||||
* @returns 新跳转push路由参数
|
||||
*/
|
||||
tabClose(path: string) {
|
||||
if (!path) return null;
|
||||
// 获取当前项和最后项下标
|
||||
const tabIndex = this.tabs.findIndex(tab => tab.path === path);
|
||||
if (tabIndex === -1) return null;
|
||||
const lastIndex = this.tabs.length - 1;
|
||||
let to = null;
|
||||
// 只有一项默认跳首页
|
||||
if (lastIndex === 0) {
|
||||
to = {
|
||||
path: '/index',
|
||||
query: {},
|
||||
};
|
||||
}
|
||||
// 关闭当期标签,操作第一项跳后一项
|
||||
else if (path === this.activePath && tabIndex === 0) {
|
||||
const tab = this.tabs[tabIndex + 1];
|
||||
to = {
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
};
|
||||
}
|
||||
// 关闭当期标签,默认跳前一项
|
||||
else if (path === this.activePath && tabIndex <= lastIndex) {
|
||||
const tab = this.tabs[tabIndex - 1];
|
||||
to = {
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
};
|
||||
}
|
||||
// 移除标签
|
||||
this.remove(path);
|
||||
return to;
|
||||
},
|
||||
/**
|
||||
* 跳转标签
|
||||
* @param path 当期标签路由地址
|
||||
* @returns 新跳转push路由参数
|
||||
*/
|
||||
tabGoto(path: string) {
|
||||
if (!path) return null;
|
||||
const tab = this.tabs.find(tab => tab.path === path);
|
||||
if (!tab) return null;
|
||||
return {
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useTabsStore;
|
||||
171
src/store/modules/user.ts
Normal file
171
src/store/modules/user.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import defaultAvatar from '@/assets/images/default_avatar.png';
|
||||
import useLayoutStore from './layout';
|
||||
import { login, logout, getInfo } from '@/api/login';
|
||||
import { getToken, setToken, removeToken } from '@/plugins/auth-token';
|
||||
import { defineStore } from 'pinia';
|
||||
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
|
||||
import { validHttp } from '@/utils/regular-utils';
|
||||
|
||||
/**用户信息类型 */
|
||||
type UserInfo = {
|
||||
/**授权凭证 */
|
||||
token: string;
|
||||
/**登录账号 */
|
||||
userName: string;
|
||||
/**用户角色 字符串数组 */
|
||||
roles: string[];
|
||||
/**用户权限 字符串数组 */
|
||||
permissions: string[];
|
||||
/**用户头像 */
|
||||
avatar: string;
|
||||
/**用户昵称 */
|
||||
nickName: string;
|
||||
/**用户手机号 */
|
||||
phonenumber: string;
|
||||
/**用户邮箱 */
|
||||
email: string;
|
||||
/**用户性别 */
|
||||
sex: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式解析头像地址
|
||||
* @param avatar 头像路径
|
||||
* @returns url地址
|
||||
*/
|
||||
function parseAvatar(avatar: string): string {
|
||||
if (!avatar) {
|
||||
return defaultAvatar;
|
||||
}
|
||||
if (validHttp(avatar)) {
|
||||
return avatar;
|
||||
}
|
||||
const baseApi = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${baseApi}${avatar}`;
|
||||
}
|
||||
|
||||
const useUserStore = defineStore('user', {
|
||||
state: (): UserInfo => ({
|
||||
token: getToken(),
|
||||
userName: '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
avatar: '',
|
||||
nickName: '',
|
||||
phonenumber: '',
|
||||
email: '',
|
||||
sex: undefined,
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
* 获取正确头像地址
|
||||
* @param state 内部属性不用传入
|
||||
* @returns 头像地址url
|
||||
*/
|
||||
getAvatar(state) {
|
||||
return parseAvatar(state.avatar);
|
||||
},
|
||||
/**
|
||||
* 获取基础信息属性
|
||||
* @param state 内部属性不用传入
|
||||
* @returns 基础信息
|
||||
*/
|
||||
getBaseInfo(state) {
|
||||
return {
|
||||
nickName: state.nickName,
|
||||
phonenumber: state.phonenumber,
|
||||
email: state.email,
|
||||
sex: state.sex,
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* 更新基础信息属性
|
||||
* @param data 变更信息
|
||||
*/
|
||||
setBaseInfo(data: Record<string, any>) {
|
||||
this.nickName = data.nickName;
|
||||
this.phonenumber = data.phonenumber;
|
||||
this.email = data.email;
|
||||
this.sex = data.sex;
|
||||
},
|
||||
/**
|
||||
* 更新头像
|
||||
* @param avatar 上传后的地址
|
||||
*/
|
||||
setAvatar(avatar: string) {
|
||||
this.avatar = avatar;
|
||||
},
|
||||
/**
|
||||
* 获取正确头像地址
|
||||
* @param avatar
|
||||
*/
|
||||
fnAvatar(avatar: string) {
|
||||
return parseAvatar(avatar);
|
||||
},
|
||||
// 登录
|
||||
async fnLogin(loginBody: Record<string, string>) {
|
||||
const res = await login(loginBody);
|
||||
if (res.code === 200 && res.data) {
|
||||
const token = res.data[TOKEN_RESPONSE_FIELD];
|
||||
setToken(token);
|
||||
this.token = token;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
// 获取用户信息
|
||||
async fnGetInfo() {
|
||||
const res = await getInfo();
|
||||
if (res.code === 200 && res.data) {
|
||||
const { user, roles, permissions } = res.data;
|
||||
// 登录账号
|
||||
this.userName = user.userName;
|
||||
// 用户头像
|
||||
this.avatar = user.avatar;
|
||||
// 基础信息
|
||||
this.nickName = user.nickName;
|
||||
this.phonenumber = user.phonenumber;
|
||||
this.email = user.email;
|
||||
this.sex = user.sex;
|
||||
|
||||
// 验证返回的roles是否是一个非空数组
|
||||
if (Array.isArray(roles) && roles.length > 0) {
|
||||
this.roles = roles;
|
||||
this.permissions = permissions;
|
||||
} else {
|
||||
this.roles = ['ROLE_DEFAULT'];
|
||||
this.permissions = [];
|
||||
}
|
||||
|
||||
// 水印文字信息=用户昵称 手机号
|
||||
let waterMarkContent = this.nickName;
|
||||
if (this.phonenumber) {
|
||||
waterMarkContent = `${this.nickName} ${this.phonenumber}`;
|
||||
}
|
||||
useLayoutStore().changeWaterMark(waterMarkContent);
|
||||
}
|
||||
// 网络错误时退出登录状态
|
||||
if (res.code === 500) {
|
||||
removeToken();
|
||||
window.location.reload();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
// 退出系统
|
||||
async fnLogOut() {
|
||||
try {
|
||||
await logout();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.token = '';
|
||||
this.roles = [];
|
||||
this.permissions = [];
|
||||
removeToken();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useUserStore;
|
||||
54
src/typings/components.d.ts
vendored
Normal file
54
src/typings/components.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AAvatar: typeof import('ant-design-vue/lib')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/lib')['Badge']
|
||||
AButton: typeof import('ant-design-vue/lib')['Button']
|
||||
ACol: typeof import('ant-design-vue/lib')['Col']
|
||||
ADropdown: typeof import('ant-design-vue/lib')['Dropdown']
|
||||
AForm: typeof import('ant-design-vue/lib')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/lib')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/lib')['Image']
|
||||
AInput: typeof import('ant-design-vue/lib')['Input']
|
||||
AInputPassword: typeof import('ant-design-vue/lib')['InputPassword']
|
||||
AMenu: typeof import('ant-design-vue/lib')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/lib')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/lib')['MenuItem']
|
||||
APopover: typeof import('ant-design-vue/lib')['Popover']
|
||||
ARow: typeof import('ant-design-vue/lib')['Row']
|
||||
ASpace: typeof import('ant-design-vue/lib')['Space']
|
||||
ATabPane: typeof import('ant-design-vue/lib')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/lib')['Tabs']
|
||||
ATooltip: typeof import('ant-design-vue/lib')['Tooltip']
|
||||
BellOutlined: typeof import('@ant-design/icons-vue')['BellOutlined']
|
||||
CronModal: typeof import('./../components/CronModal/index.vue')['default']
|
||||
Day: typeof import('./../components/CronModal/components/Day.vue')['default']
|
||||
DictTag: typeof import('./../components/DictTag/index.vue')['default']
|
||||
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
|
||||
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
||||
Hour: typeof import('./../components/CronModal/components/Hour.vue')['default']
|
||||
IconFont: typeof import('./../components/IconFont/index.vue')['default']
|
||||
LinkiFrame: typeof import('./../components/LinkiFrame/index.vue')['default']
|
||||
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
|
||||
LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined']
|
||||
Minute: typeof import('./../components/CronModal/components/Minute.vue')['default']
|
||||
MobileOutlined: typeof import('@ant-design/icons-vue')['MobileOutlined']
|
||||
Month: typeof import('./../components/CronModal/components/Month.vue')['default']
|
||||
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
||||
QuestionCircleOutlined: typeof import('@ant-design/icons-vue')['QuestionCircleOutlined']
|
||||
ReloadOutlined: typeof import('@ant-design/icons-vue')['ReloadOutlined']
|
||||
RobotOutlined: typeof import('@ant-design/icons-vue')['RobotOutlined']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Second: typeof import('./../components/CronModal/components/Second.vue')['default']
|
||||
SettingOutlined: typeof import('@ant-design/icons-vue')['SettingOutlined']
|
||||
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
|
||||
}
|
||||
}
|
||||
7
src/typings/dict.d.ts
vendored
Normal file
7
src/typings/dict.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/**字段类型 */
|
||||
type DictType = {
|
||||
label: string;
|
||||
value: string;
|
||||
elTagType: string;
|
||||
elTagClass: string;
|
||||
};
|
||||
13
src/typings/router.d.ts
vendored
Normal file
13
src/typings/router.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'vue-router';
|
||||
import { MetaRecord, MenuDataItem } from '@ant-design-vue/pro-layout';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta extends MetaRecord {
|
||||
/**请求授权 */
|
||||
requiresAuth?: boolean;
|
||||
/**权限 */
|
||||
permissions?: string[];
|
||||
/**角色 */
|
||||
roles?: string[];
|
||||
}
|
||||
}
|
||||
8
src/typings/vite-env.d.ts
vendored
Normal file
8
src/typings/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
40
src/utils/cache-local-utils.ts
Normal file
40
src/utils/cache-local-utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**长期级缓存设置 */
|
||||
export function localSet(key: string, value: string) {
|
||||
if (!localStorage || key == null || value == null) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
/**长期级缓存获取 */
|
||||
export function localGet(key: string) {
|
||||
if (!localStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
/**长期级缓存移除 */
|
||||
export function localRemove(key: string) {
|
||||
if (!localStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**长期级缓存设置JSON */
|
||||
export function localSetJSON(key: string, jsonValue: object) {
|
||||
if (key == null || jsonValue == null) {
|
||||
return null;
|
||||
}
|
||||
localSet(key, JSON.stringify(jsonValue));
|
||||
}
|
||||
|
||||
/**长期级缓存获取JSON */
|
||||
export function localGetJSON(key: string) {
|
||||
const value = localGet(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
40
src/utils/cache-session-utils.ts
Normal file
40
src/utils/cache-session-utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**会话级缓存设置 */
|
||||
export function sessionSet(key: string, value: string) {
|
||||
if (!sessionStorage || key == null || value == null) {
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
/**会话级缓存获取 */
|
||||
export function sessionGet(key: string) {
|
||||
if (!sessionStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return sessionStorage.getItem(key);
|
||||
}
|
||||
|
||||
/**会话级缓存移除 */
|
||||
export function sessionRemove(key: string) {
|
||||
if (!sessionStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**会话级缓存设置JSON */
|
||||
export function sessionSetJSON(key: string, jsonValue: object) {
|
||||
if (key == null || jsonValue == null) {
|
||||
return null;
|
||||
}
|
||||
sessionSet(key, JSON.stringify(jsonValue));
|
||||
}
|
||||
|
||||
/**会话级缓存获取JSON */
|
||||
export function sessionGetJSON(key: string) {
|
||||
const value = sessionGet(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
72
src/utils/date-utils.ts
Normal file
72
src/utils/date-utils.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// 依赖来源 https://github.com/iamkun/dayjs
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 导入本地化语言并设为默认使用
|
||||
import('dayjs/locale/zh-cn');
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
/**年 列如:2022 */
|
||||
export const YYYY = 'YYYY';
|
||||
|
||||
/**年-月 列如:2022-12 */
|
||||
export const YYYY_MM = 'YYYY-MM';
|
||||
|
||||
/**年-月-日 列如:2022-12-30 */
|
||||
export const YYYY_MM_DD = 'YYYY-MM-DD';
|
||||
|
||||
/**年月日时分秒 列如:20221230010159 */
|
||||
export const YYYYMMDDHHMMSS = 'YYYYMMDDHHmmss';
|
||||
|
||||
/**年-月-日 时:分:秒 列如:2022-12-30 01:01:59 */
|
||||
export const YYYY_MM_DD_HH_MM_SS = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
/**
|
||||
* 格式时间字符串
|
||||
* @param dateStr 时间字符串
|
||||
* @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
* @returns Date对象
|
||||
*/
|
||||
export function parseStrToDate(
|
||||
dateStr: string,
|
||||
formatStr: string = YYYY_MM_DD_HH_MM_SS
|
||||
): Date {
|
||||
return dayjs(dateStr, formatStr).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式时间
|
||||
* @param date 可转的Date对象
|
||||
* @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
* @returns 时间格式字符串
|
||||
*/
|
||||
export function parseDateToStr(
|
||||
date: string | number | Date,
|
||||
formatStr: string = YYYY_MM_DD_HH_MM_SS
|
||||
): string {
|
||||
return dayjs(date).format(formatStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式时间成日期路径
|
||||
*
|
||||
* 年/月 列如:2022/12
|
||||
* @returns 时间格式字符串 YYYY/MM
|
||||
*/
|
||||
export function parseDatePath(date: number | Date = Date.now()): string {
|
||||
return dayjs(date).format('YYYY/MM');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两次时间差
|
||||
* @param endDate 结束时间
|
||||
* @param startDate 开始时间
|
||||
* @returns 单位秒
|
||||
*/
|
||||
export function diffSeconds(
|
||||
endDate: number | Date,
|
||||
startDate: number | Date
|
||||
): number {
|
||||
const value = Math.ceil(dayjs(endDate).diff(startDate, 'seconds'));
|
||||
if (Number.isNaN(value)) return 0;
|
||||
return value;
|
||||
}
|
||||
188
src/utils/parse-tree-utils.ts
Normal file
188
src/utils/parse-tree-utils.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 解析数据层级转树结构
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldParentId 读取节点父节点字段 默认 'parentId'
|
||||
* @param fieldChildren 设置子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseDataToTree(
|
||||
data: Record<string, any>[],
|
||||
fieldId: string = 'id',
|
||||
fieldParentId: string = 'parentId',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点分组
|
||||
let map: Map<string, Record<string, any>[]> = new Map();
|
||||
// 节点id
|
||||
let treeIds: string[] = [];
|
||||
// 树节点
|
||||
let tree: Record<string, any>[] = [];
|
||||
|
||||
for (const item of data) {
|
||||
let parentId = item[fieldParentId];
|
||||
// 分组
|
||||
let mapItem = map.get(parentId) ?? [];
|
||||
mapItem.push(item);
|
||||
map.set(parentId, mapItem);
|
||||
// 记录节点id
|
||||
treeIds.push(item[fieldId]);
|
||||
}
|
||||
|
||||
for (const [key, value] of map) {
|
||||
// 选择不是节点id的作为树节点
|
||||
if (!treeIds.includes(key)) {
|
||||
tree.push(...value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const iterator of tree) {
|
||||
componet(iterator);
|
||||
}
|
||||
|
||||
/**闭包递归函数 */
|
||||
function componet(iterator: Record<string, any>) {
|
||||
let id = iterator[fieldId];
|
||||
let item = map.get(id);
|
||||
if (item) {
|
||||
iterator[fieldChildren] = item;
|
||||
}
|
||||
if (iterator[fieldChildren]) {
|
||||
for (let i of iterator[fieldChildren]) {
|
||||
componet(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据层级转树结构-排除节点
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param excludeField 排除节点字段 默认 'type'
|
||||
* @param excludeValue 排除节点值 默认 '0'
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldParentId 读取节点父节点字段 默认 'parentId'
|
||||
* @param fieldChildren 设置子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseDataToTreeExclude(
|
||||
data: Record<string, any>[],
|
||||
excludeField = 'type',
|
||||
excludeValue = '0',
|
||||
fieldId: string = 'id',
|
||||
fieldParentId: string = 'parentId',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点分组
|
||||
let map: Map<string, Record<string, any>[]> = new Map();
|
||||
// 节点id
|
||||
let treeIds: string[] = [];
|
||||
// 树节点
|
||||
let tree: Record<string, any>[] = [];
|
||||
|
||||
for (const item of data) {
|
||||
// 排除值跳过
|
||||
let exclude = item[excludeField];
|
||||
if (exclude && exclude === excludeValue) {
|
||||
continue;
|
||||
}
|
||||
let parentId = item[fieldParentId];
|
||||
// 分组
|
||||
let mapItem = map.get(parentId) ?? [];
|
||||
mapItem.push(item);
|
||||
map.set(parentId, mapItem);
|
||||
// 记录节点id
|
||||
treeIds.push(item[fieldId]);
|
||||
}
|
||||
|
||||
for (const [key, value] of map) {
|
||||
// 选择不是节点id的作为树节点
|
||||
if (!treeIds.includes(key)) {
|
||||
tree.push(...value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const iterator of tree) {
|
||||
componet(iterator);
|
||||
}
|
||||
|
||||
/**闭包递归函数 */
|
||||
function componet(iterator: Record<string, any>) {
|
||||
let id = iterator[fieldId];
|
||||
let item = map.get(id);
|
||||
if (item) {
|
||||
iterator[fieldChildren] = item;
|
||||
}
|
||||
if (iterator[fieldChildren]) {
|
||||
for (let i of iterator[fieldChildren]) {
|
||||
componet(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析树结构数据转出一维id数组
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldChildren 读取子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseTreeKeys(
|
||||
data: Record<string, any>[],
|
||||
fieldId: string = 'id',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点id
|
||||
let treeIds: string[] | number[] = [];
|
||||
componet(data);
|
||||
/**闭包递归函数 */
|
||||
function componet(data: Record<string, any>[]) {
|
||||
if (data.length <= 0) return;
|
||||
for (const iterator of data) {
|
||||
let id = iterator[fieldId];
|
||||
if (id) {
|
||||
treeIds.push(id as never);
|
||||
}
|
||||
if (Array.isArray(iterator[fieldChildren])) {
|
||||
componet(iterator[fieldChildren]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析树结构数据转出含子节点的一维id数组
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldChildren 读取子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseTreeNodeKeys(
|
||||
data: Record<string, any>[],
|
||||
fieldId: string = 'id',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点id
|
||||
let treeIds: string[] | number[] = [];
|
||||
componet(data);
|
||||
/**闭包递归函数 */
|
||||
function componet(data: Record<string, any>[]) {
|
||||
if (data.length <= 0) return;
|
||||
for (const iterator of data) {
|
||||
let nodes = iterator[fieldChildren];
|
||||
if (Array.isArray(nodes) && nodes.length > 0) {
|
||||
treeIds.push(iterator[fieldId] as never);
|
||||
componet(iterator[fieldChildren]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIds;
|
||||
}
|
||||
67
src/utils/regular-utils.ts
Normal file
67
src/utils/regular-utils.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 有效账号格式
|
||||
*
|
||||
* 账号不能以数字开头,可包含大写小写字母,数字,且不少于5位
|
||||
*/
|
||||
export const regExpUserName = /^[a-zA-Z][a-z0-9A-Z]{5,}$/;
|
||||
|
||||
/**
|
||||
* 有效密码格式
|
||||
*
|
||||
* 密码至少包含大小写字母、数字、特殊符号,且不少于6位
|
||||
*/
|
||||
export const regExpPasswd =
|
||||
/^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$/;
|
||||
|
||||
/**
|
||||
* 有效手机号格式
|
||||
*/
|
||||
export const regExpMobile = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/;
|
||||
|
||||
/**
|
||||
* 有效邮箱格式
|
||||
*/
|
||||
export const regExpEmail =
|
||||
/^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/;
|
||||
|
||||
/**
|
||||
* 有效用户昵称格式
|
||||
*
|
||||
* 用户昵称只能包含字母、数字、中文和下划线,且不少于2位
|
||||
*/
|
||||
export const regExpNick = /^[\w\u4e00-\u9fa5-]{2,}$/;
|
||||
|
||||
/**
|
||||
* 是否为http(s)://开头
|
||||
*/
|
||||
export const regExpHttp = /^http(s)?:\/\/+/;
|
||||
|
||||
/**
|
||||
* 判断是否为http(s)://开头
|
||||
* @param link 网络链接
|
||||
* @returns true | false
|
||||
*/
|
||||
export function validHttp(link: string): boolean {
|
||||
if (!link) return false;
|
||||
return regExpHttp.test(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效手机号格式
|
||||
* @param mobile 手机号字符串
|
||||
* @returns true | false
|
||||
*/
|
||||
export function validMobile(mobile: string): boolean {
|
||||
if (!mobile) return false;
|
||||
return regExpMobile.test(mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效邮箱格式
|
||||
* @param email 邮箱字符串
|
||||
* @returns true | false
|
||||
*/
|
||||
export function validEmail(email: string): boolean {
|
||||
if (!email) return false;
|
||||
return regExpEmail.test(email);
|
||||
}
|
||||
248
src/views/account/components/base-info.vue
Normal file
248
src/views/account/components/base-info.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<script lang="ts" setup>
|
||||
import { Modal, message } from 'ant-design-vue/lib';
|
||||
import { FileType } from 'ant-design-vue/lib/upload/interface';
|
||||
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
import { onMounted, reactive, ref, toRaw } from 'vue';
|
||||
import { updateUserProfile, uploadAvatar } from '@/api/profile';
|
||||
import { regExpEmail, regExpMobile, regExpNick } from '@/utils/regular-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
const uerStore = useUserStore();
|
||||
const { getDict } = useDictStore();
|
||||
|
||||
/**用户性别字典 */
|
||||
let sysUserSex = ref<DictType[]>([
|
||||
{ label: '未知', value: '0', elTagType: '', elTagClass: '' },
|
||||
{ label: '男', value: '1', elTagType: '', elTagClass: '' },
|
||||
{ label: '女', value: '2', elTagType: '', elTagClass: '' },
|
||||
]);
|
||||
|
||||
/**表单数据状态 */
|
||||
let stateForm = reactive({
|
||||
/**表单属性 */
|
||||
form: {
|
||||
nickName: '',
|
||||
email: '',
|
||||
phonenumber: '',
|
||||
sex: undefined,
|
||||
},
|
||||
/**表单提交点击状态 */
|
||||
formClick: false,
|
||||
});
|
||||
|
||||
/**表单数据状态初始化 */
|
||||
function fnInitstateForm() {
|
||||
stateForm.form = Object.assign(stateForm.form, uerStore.getBaseInfo);
|
||||
stateForm.formClick = false;
|
||||
}
|
||||
|
||||
/**表单验证通过 */
|
||||
function fnFinish() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认要提交修改用户基本信息吗?`,
|
||||
onOk() {
|
||||
stateForm.formClick = true;
|
||||
// 发送请求
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
const form = toRaw(stateForm.form);
|
||||
updateUserProfile(form).then(res => {
|
||||
hide();
|
||||
stateForm.formClick = false;
|
||||
if (res.code === 200) {
|
||||
Modal.success({
|
||||
title: '提示',
|
||||
content: `用户基本信息修改成功!`,
|
||||
okText: '我知道了',
|
||||
onOk() {
|
||||
uerStore.setBaseInfo(form);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**上传状态 */
|
||||
let upState = ref<boolean>(false);
|
||||
|
||||
/**上传前检查或转换压缩 */
|
||||
function fnBeforeUpload(file: FileType) {
|
||||
if (upState.value) return false;
|
||||
const isJpgOrPng = ['image/jpeg', 'image/png'].includes(file.type);
|
||||
if (!isJpgOrPng) {
|
||||
message.error('只支持上传图片格式(jpg、png)', 3);
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('图片文件大小必须小于 2MB', 3);
|
||||
}
|
||||
return isJpgOrPng && isLt2M;
|
||||
}
|
||||
|
||||
/**上传变更 */
|
||||
function fnUpload(up: UploadRequestOption) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认要上传/变更用户头像吗?`,
|
||||
onOk() {
|
||||
// 发送请求
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
upState.value = true;
|
||||
let formData = new FormData();
|
||||
formData.append('file', up.file);
|
||||
uploadAvatar(formData).then(res => {
|
||||
upState.value = false;
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
message.success('头像上传/变更成功', 3);
|
||||
uerStore.setAvatar(res.data);
|
||||
} else {
|
||||
message.error(res.msg, 3);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
getDict('sys_user_sex').then(res => {
|
||||
if (res.length > 0) {
|
||||
sysUserSex.value = res;
|
||||
}
|
||||
});
|
||||
// 初始表单值
|
||||
fnInitstateForm();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form
|
||||
:model="stateForm.form"
|
||||
name="stateForm"
|
||||
layout="vertical"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
@finish="fnFinish"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24" style="margin-bottom: 30px">
|
||||
<a-form-item
|
||||
label="用户昵称"
|
||||
name="nickName"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpNick,
|
||||
message: '昵称只能包含字母、数字、中文和下划线,且不少于2位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="stateForm.form.nickName"
|
||||
allow-clear
|
||||
:maxlength="26"
|
||||
placeholder="请输入用户昵称"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="手机号码"
|
||||
name="phonenumber"
|
||||
:rules="[
|
||||
{
|
||||
required: false,
|
||||
pattern: regExpMobile,
|
||||
message: '请输入正确手机号码',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="stateForm.form.phonenumber"
|
||||
allow-clear
|
||||
:maxlength="11"
|
||||
placeholder="请输入手机号码"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="电子邮箱"
|
||||
name="email"
|
||||
:rules="[
|
||||
{
|
||||
required: false,
|
||||
pattern: regExpEmail,
|
||||
message: '请输入正确电子邮箱',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="stateForm.form.email"
|
||||
allow-clear
|
||||
:maxlength="40"
|
||||
placeholder="请输入电子邮箱"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="用户性别"
|
||||
name="sex"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择用户性别',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="stateForm.form.sex"
|
||||
placeholder="用户性别"
|
||||
:options="sysUserSex"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
block
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:loading="stateForm.formClick"
|
||||
>
|
||||
确认修改
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
@click="fnInitstateForm"
|
||||
:disabled="stateForm.formClick"
|
||||
>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-space direction="vertical" :size="16">
|
||||
<a-image :width="128" :height="128" :src="uerStore.getAvatar" />
|
||||
<span>请选择等比大小图片作为头像,如200x200、400x400</span>
|
||||
<a-upload
|
||||
name="file"
|
||||
list-type="picture"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
:before-upload="fnBeforeUpload"
|
||||
:custom-request="fnUpload"
|
||||
>
|
||||
<a-button type="default" :loading="upState">
|
||||
上传/变更图片
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
158
src/views/account/components/reset-passwd.vue
Normal file
158
src/views/account/components/reset-passwd.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script lang="ts" setup>
|
||||
import { Modal, message } from 'ant-design-vue/lib';
|
||||
import { reactive } from 'vue';
|
||||
import { updateUserPwd } from '@/api/profile';
|
||||
import { regExpPasswd } from '@/utils/regular-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
const { userName, fnLogOut } = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
let state = reactive({
|
||||
/**表单属性 */
|
||||
form: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
/**表单提交点击状态 */
|
||||
formClick: false,
|
||||
});
|
||||
|
||||
/**表单验证确认密码是否一致 */
|
||||
function fnEqualToPassword(
|
||||
rule: Record<string, any>,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (!value) {
|
||||
return Promise.reject('请输入确认新密码');
|
||||
}
|
||||
if (state.form.oldPassword === value) {
|
||||
return Promise.reject('与旧密码一致,请重新输入新密码');
|
||||
}
|
||||
if (state.form.newPassword === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject('两次输入的新密码不一致');
|
||||
}
|
||||
|
||||
/**表单验证通过 */
|
||||
function fnFinish() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认要提交修改密码吗?`,
|
||||
onOk() {
|
||||
state.formClick = true;
|
||||
// 发送请求
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
updateUserPwd(state.form.oldPassword, state.form.confirmPassword)
|
||||
.then(res => {
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
Modal.success({
|
||||
title: '提示',
|
||||
content: `恭喜您,${userName} 账号密码修改成功!`,
|
||||
okText: '重新登录',
|
||||
onOk() {
|
||||
fnLogOut().finally(() => router.push({ name: 'Login' }));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
state.formClick = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form
|
||||
:model="state.form"
|
||||
name="stateForm"
|
||||
layout="vertical"
|
||||
:wrapper-col="{ span: 9 }"
|
||||
@finish="fnFinish"
|
||||
>
|
||||
<a-form-item
|
||||
label="旧密码"
|
||||
name="oldPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 6,
|
||||
max: 26,
|
||||
message: '旧密码不能为空,且不少于6位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.oldPassword"
|
||||
placeholder="请输入旧密码"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpPasswd,
|
||||
message: '密码至少包含大小写字母、数字、特殊符号,且不少于6位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.newPassword"
|
||||
placeholder="请输入新密码"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="确认新密码"
|
||||
name="confirmPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
validator: fnEqualToPassword,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.confirmPassword"
|
||||
placeholder="请确认新密码"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ span: 3 }">
|
||||
<a-button
|
||||
block
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:loading="state.formClick"
|
||||
>
|
||||
提交修改
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
165
src/views/account/components/style-layout.vue
Normal file
165
src/views/account/components/style-layout.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { getLocalColor, changePrimaryColor } from '@/hooks/useTheme';
|
||||
import useLayoutStore from '@/store/modules/layout';
|
||||
const { proConfig, changeConf } = useLayoutStore();
|
||||
|
||||
let color = ref<string>(getLocalColor());
|
||||
|
||||
/**改变主题色 */
|
||||
function fnColorChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.nodeName === 'INPUT') {
|
||||
changePrimaryColor(target.value ?? '#1890ff');
|
||||
} else {
|
||||
changePrimaryColor();
|
||||
}
|
||||
color.value = getLocalColor();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-divider orientation="left">布局属性</a-divider>
|
||||
<a-list item-layout="vertical" size="large" row-key="title">
|
||||
<a-list-item>
|
||||
整体布局
|
||||
<template #actions> 导航模式模块设置 </template>
|
||||
<template #extra>
|
||||
<a-radio-group
|
||||
style="margin-bottom: 12px"
|
||||
:value="proConfig.layout"
|
||||
@change="(e:any) => changeConf('layout', e.target.value)"
|
||||
>
|
||||
<a-radio value="side">左侧菜单布局</a-radio>
|
||||
<a-radio value="top">顶部菜单布局</a-radio>
|
||||
<a-radio value="mix">混合菜单布局</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
风格配色
|
||||
<template #actions> 整体风格配色设置 </template>
|
||||
<template #extra>
|
||||
<a-space :size="16" align="end" direction="horizontal">
|
||||
<a-button type="primary" size="small" @click="fnColorChange">
|
||||
<BgColorsOutlined /> 随机
|
||||
</a-button>
|
||||
<input type="color" :value="color" @input="fnColorChange" />
|
||||
</a-space>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
深色菜单
|
||||
<template #actions> 只能改变导航模式的菜单 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.navTheme === 'dark'"
|
||||
@change="
|
||||
(checked:any) => changeConf('navTheme', checked ? 'dark' : 'light')
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
固定顶部导航栏
|
||||
<template #actions> 顶部导航栏是否固定,不随滚动条移动 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.fixedHeader"
|
||||
@change="(checked:any) => changeConf('fixedHeader', checked)"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
固定左侧菜单
|
||||
<template #actions> 左侧菜单是否固定,仅左侧菜单布局时有效 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.fixSiderbar"
|
||||
@change="(checked:any) => changeConf('fixSiderbar', checked)"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
自动分割菜单
|
||||
<template #actions>
|
||||
顶部有多级菜单时显示左侧菜单,仅混合菜单布局时有效
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.splitMenus"
|
||||
@change="(checked:any) => changeConf('splitMenus', checked)"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<a-divider orientation="left">内容区域</a-divider>
|
||||
<a-list item-layout="vertical" size="large" row-key="title">
|
||||
<a-list-item>
|
||||
顶栏
|
||||
<template #actions> 是否显示顶部导航栏 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.headerRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('headerRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
页脚
|
||||
<template #actions> 是否显示底部导航栏 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.footerRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('footerRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
菜单头
|
||||
<template #actions> 是否显示左侧菜单栏顶部LOGO区域 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.menuHeaderRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('menuHeaderRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
导航标签项
|
||||
<template #actions> 是否显示顶部Tab导航标签项 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.tabRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('tabRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
198
src/views/account/profile.vue
Normal file
198
src/views/account/profile.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="ts" setup>
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
import { getUserProfile } from '@/api/profile';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
|
||||
/**加载状态 */
|
||||
let loading = ref<boolean>(true);
|
||||
|
||||
/**Tab标签激活 */
|
||||
let activeKey = ref<string>('list');
|
||||
|
||||
/**个人信息数据状态 */
|
||||
let state = reactive<{
|
||||
user: Record<string, any>;
|
||||
postGroup: string[];
|
||||
roleGroup: string[];
|
||||
}>({
|
||||
user: {},
|
||||
postGroup: [],
|
||||
roleGroup: [],
|
||||
});
|
||||
|
||||
/**列表数据 */
|
||||
let listData = ref([
|
||||
{
|
||||
id: 'Vue',
|
||||
title: 'Vue.js - 渐进式 JavaScript 框架 | Vue.js',
|
||||
description:
|
||||
'基于标准 HTML、CSS 和 JavaScript 构建,提供容易上手的 API 和一流的文档。 性能出色 经过编译器优化、完全响应式的渲染系统,几乎不需要手动优化。',
|
||||
},
|
||||
{
|
||||
id: 'Vue Router',
|
||||
title: 'Vue Router | Vue.js 的官方路由',
|
||||
description:
|
||||
'为Vue.js 提供富有表现力、可配置的、方便的路由,用直观且强大的语法来定义静态或动态路由。',
|
||||
},
|
||||
{
|
||||
id: 'Pinia',
|
||||
title: 'Pinia | The intuitive store for Vue.js',
|
||||
description:
|
||||
'Pinia hooks into Vue devtools to give you an enhanced development experience in both Vue 2 and Vue 3. ',
|
||||
},
|
||||
{
|
||||
id: 'Vite',
|
||||
title: 'Vite | 下一代的前端工具链',
|
||||
description:
|
||||
'Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验',
|
||||
},
|
||||
]);
|
||||
|
||||
/**查询用户个人信息 */
|
||||
function fnGetProfile() {
|
||||
getUserProfile().then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const { user, roleGroup, postGroup } = res.data;
|
||||
state.user = user;
|
||||
state.roleGroup = roleGroup;
|
||||
state.postGroup = postGroup;
|
||||
// 头像解析
|
||||
state.user.avatar = useUserStore().fnAvatar(user.avatar);
|
||||
loading.value = false;
|
||||
} else {
|
||||
message.error(res.msg, 3);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取信息
|
||||
fnGetProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :loading="loading">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="6" :xs="24">
|
||||
<a-card :body-style="{ padding: '0px' }" style="margin-bottom: 16px">
|
||||
<template #title>
|
||||
<div class="info-top">
|
||||
<div class="info-top-no">No:{{ state.user.userId }}</div>
|
||||
<a-avatar
|
||||
shape="circle"
|
||||
:size="96"
|
||||
:src="state.user.avatar"
|
||||
:alt="state.user.userName"
|
||||
></a-avatar>
|
||||
<div class="info-top-nickname" :title="state.user.nickName">
|
||||
{{ state.user.nickName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-descriptions
|
||||
size="small"
|
||||
layout="vertical"
|
||||
:bordered="true"
|
||||
:column="1"
|
||||
>
|
||||
<a-descriptions-item label="手机号码">
|
||||
{{ state.user.phonenumber || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户邮箱">
|
||||
{{ state.user.email || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属部门">
|
||||
{{ state.user.dept?.deptName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="拥有岗位">
|
||||
<span v-if="state.postGroup.length === 0">-</span>
|
||||
<a-tag v-else v-for="v in state.postGroup" :key="v">
|
||||
{{ v }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="拥有角色">
|
||||
<span v-if="state.roleGroup.length === 0">-</span>
|
||||
<a-tag v-else v-for="v in state.roleGroup" :key="v">
|
||||
{{ v }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="登录地址">
|
||||
{{ state.user.loginIp || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="登录时间">
|
||||
<span v-if="+state.user.loginDate > 0">
|
||||
{{ parseDateToStr(+state.user.loginDate) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :lg="18" :md="18" :xs="24">
|
||||
<a-card>
|
||||
<a-tabs
|
||||
tab-position="top"
|
||||
:destroy-inactive-tab-pane="true"
|
||||
v-model:activeKey="activeKey"
|
||||
>
|
||||
<a-tab-pane key="list" tab="列表">
|
||||
<a-list
|
||||
item-layout="horizontal"
|
||||
:data-source="listData"
|
||||
row-key="id"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ item.description }}
|
||||
</template>
|
||||
<template #avatar>
|
||||
<a-avatar>{{ item.id }}</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="empty" tab="空状态">
|
||||
<a-empty>
|
||||
<template #description> 暂无数据,尝试刷新看看 </template>
|
||||
<a-button type="primary">刷新</a-button>
|
||||
</a-empty>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.info-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
&-no {
|
||||
align-self: flex-start;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
&-nickname {
|
||||
margin-top: 16px;
|
||||
font-size: 24px;
|
||||
align-self: flex-start;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
30
src/views/account/settings.vue
Normal file
30
src/views/account/settings.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import BaseInfo from './components/base-info.vue';
|
||||
import ResetPasswd from './components/reset-passwd.vue';
|
||||
import StyleLayout from './components/style-layout.vue';
|
||||
|
||||
/**Tab标签激活 */
|
||||
let activeKey = ref<string>('base-info');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card>
|
||||
<a-tabs tab-position="left" v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="base-info" tab="基础信息">
|
||||
<BaseInfo></BaseInfo>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="reset-passwd" tab="重置密码">
|
||||
<ResetPasswd></ResetPasswd>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="style-layout" tab="个性化">
|
||||
<StyleLayout></StyleLayout>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
27
src/views/dome/dome1.vue
Normal file
27
src/views/dome/dome1.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-result
|
||||
status="404"
|
||||
:style="{
|
||||
height: '100%',
|
||||
background: '#fff',
|
||||
}"
|
||||
title="Hello World"
|
||||
sub-title="Sorry, you are not authorized to access this page."
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleClick">Back Home</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
|
||||
const handleClick = () => {
|
||||
console.log('info');
|
||||
message.info('BackHome button clicked!');
|
||||
};
|
||||
</script>
|
||||
61
src/views/dome/dome2.vue
Normal file
61
src/views/dome/dome2.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<a-layout class="layout">
|
||||
<a-layout-header>
|
||||
<div class="logo" />
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
:style="{ lineHeight: '64px' }"
|
||||
>
|
||||
<a-menu-item key="1">nav 1</a-menu-item>
|
||||
<a-menu-item key="2">nav 2</a-menu-item>
|
||||
<a-menu-item key="3">nav 3</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-header>
|
||||
<a-layout-content style="padding: 0 50px">
|
||||
<a-breadcrumb style="margin: 16px 0">
|
||||
<a-breadcrumb-item>Home</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>List</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>App</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<div :style="{ background: '#fff', padding: '24px', minHeight: '280px' }">
|
||||
Content {{ selectedKeys }}
|
||||
</div>
|
||||
</a-layout-content>
|
||||
<a-layout-footer style="text-align: center">
|
||||
Ant Design ©2018 Created by Ant UED
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const selectedKeys = ref<string[]>(['2']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.site-layout-content {
|
||||
min-height: 280px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#components-layout-demo-top .logo {
|
||||
float: left;
|
||||
width: 120px;
|
||||
height: 31px;
|
||||
margin: 16px 24px 16px 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ant-row-rtl #components-layout-demo-top .logo {
|
||||
float: right;
|
||||
margin: 16px 0 16px 24px;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .site-layout-content {
|
||||
background: #141414;
|
||||
}
|
||||
</style>
|
||||
30
src/views/dome/dome3.vue
Normal file
30
src/views/dome/dome3.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<PageContainer title="Version" sub-title="show current project dependencies">
|
||||
<template #content>
|
||||
<strong>Content Area</strong>
|
||||
</template>
|
||||
<template #extra>
|
||||
<strong>Extra Area</strong>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<strong>ExtraContent Area</strong>
|
||||
</template>
|
||||
<template #tags>
|
||||
<a-tag>Tag1</a-tag>
|
||||
<a-tag color="pink">Tag2</a-tag>
|
||||
</template>
|
||||
<a-card title="Info">
|
||||
<p v-for="i in list" :key="i">
|
||||
text block...
|
||||
<a-tag>{{ i }}</a-tag>
|
||||
</p>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
|
||||
const list = ref<number>(50);
|
||||
</script>
|
||||
87
src/views/domes/dynamic-match.vue
Normal file
87
src/views/domes/dynamic-match.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<PageContainer :title="`${route.meta.title} ${route.params.id}`">
|
||||
<template #content>
|
||||
<a-descriptions size="small" :column="2">
|
||||
<a-descriptions-item label="创建人">张三</a-descriptions-item>
|
||||
<a-descriptions-item label="联系方式">
|
||||
<a>421421</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">2017-01-10</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">2017-10-10</a-descriptions-item>
|
||||
<a-descriptions-item label="备注">
|
||||
中国浙江省杭州市西湖区古翠路
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button key="3">操作</a-button>
|
||||
<a-button key="2">操作</a-button>
|
||||
<a-button key="1" type="primary">主操作</a-button>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<a-space>
|
||||
<a-statistic title="Feedback" :value="1128">
|
||||
<template #prefix>
|
||||
<LikeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<a-statistic title="Unmerged" :value="93" suffix="/ 100" />
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 主内容区 -->
|
||||
<div style="height: 300px">
|
||||
<p>路由参数联动 分页器 组件</p>
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="prev">跳转上一页</a-button>
|
||||
<a-button type="dashed" @click="next">跳转下一页</a-button>
|
||||
</a-space>
|
||||
<a-divider />
|
||||
<a-pagination
|
||||
:current="currentId"
|
||||
:total="total"
|
||||
show-less-items
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentId = computed(() => {
|
||||
let id = route.params && (route.params.id as string);
|
||||
return Number.parseInt(id as string, 10) || 1;
|
||||
});
|
||||
const total = computed(() => {
|
||||
const v = currentId.value * 20;
|
||||
if (v >= Number.MAX_SAFE_INTEGER) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
|
||||
const next = () => {
|
||||
router.push({
|
||||
name: 'DynamicMatch',
|
||||
params: { id: currentId.value + 1 },
|
||||
});
|
||||
};
|
||||
const prev = () => {
|
||||
router.push({
|
||||
name: 'DynamicMatch',
|
||||
params: { id: currentId.value > 1 ? currentId.value - 1 : 1 },
|
||||
});
|
||||
};
|
||||
const handlePageChange = (currentPage: number) => {
|
||||
router.push({
|
||||
name: 'DynamicMatch',
|
||||
params: { id: currentPage },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
58
src/views/domes/page-info.vue
Normal file
58
src/views/domes/page-info.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<PageContainer :title="title">
|
||||
<template #content>
|
||||
<a-descriptions size="small" :column="2">
|
||||
<a-descriptions-item label="创建人">张三</a-descriptions-item>
|
||||
<a-descriptions-item label="联系方式">
|
||||
<a>421421</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">2017-01-10</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">2017-10-10</a-descriptions-item>
|
||||
<a-descriptions-item label="备注">
|
||||
中国浙江省杭州市西湖区古翠路
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button key="3">操作</a-button>
|
||||
<a-button key="2">操作</a-button>
|
||||
<a-button key="1" type="primary">主操作</a-button>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<a-space>
|
||||
<a-statistic title="Feedback" :value="1128">
|
||||
<template #prefix>
|
||||
<LikeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<a-statistic title="Unmerged" :value="93" suffix="/ 100" />
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 主内容区 -->
|
||||
<div style="height: 120vh">
|
||||
<a-result
|
||||
status="404"
|
||||
:style="{
|
||||
height: '100%',
|
||||
background: '#fff',
|
||||
}"
|
||||
title="Hello World"
|
||||
sub-title="Sorry, you are not authorized to access this page."
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary">Back Home</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
</script>
|
||||
102
src/views/domes/page-typography.vue
Normal file
102
src/views/domes/page-typography.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<a-typography>
|
||||
<a-typography-title>Introduction</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
In the process of internal desktop applications development, many
|
||||
different design specs and implementations would be involved, which
|
||||
might cause designers and developers difficulties and duplication and
|
||||
reduce the efficiency of development.
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
After massive project practice and summaries, Ant Design, a design
|
||||
language for background applications, is refined by Ant UED Team, which
|
||||
aims to
|
||||
<a-typography-text strong>
|
||||
uniform the user interface specs for internal background projects,
|
||||
lower the unnecessary cost of design differences and implementation
|
||||
and liberate the resources of design and front-end development.
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-title :level="2"
|
||||
>Guidelines and Resources</a-typography-title
|
||||
>
|
||||
<a-typography-paragraph>
|
||||
We supply a series of design principles, practical patterns and high
|
||||
quality design resources (
|
||||
<a-typography-text code>Sketch</a-typography-text>
|
||||
and
|
||||
<a-typography-text code>Axure</a-typography-text>
|
||||
), to help people create their product prototypes beautifully and
|
||||
efficiently.
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<a-typography-link href="/docs/resources"
|
||||
>Resource Download</a-typography-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
Press
|
||||
<a-typography-text keyboard>Esc</a-typography-text>
|
||||
to exit...
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-typography-title>介绍</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
随着商业化的趋势,越来越多的企业级产品对更好的用户体验有了进一步的要求。带着这样的一个终极目标,我们(蚂蚁金服体验技术部)经过大量的项目实践和总结,逐步打磨出一个服务于企业级产品的设计体系
|
||||
Ant Design。基于
|
||||
<a-typography-text mark>『确定』和『自然』</a-typography-text>
|
||||
的设计价值观,通过模块化的解决方案,降低冗余的生产成本,让设计者专注于
|
||||
<a-typography-text strong>更好的用户体验</a-typography-text>
|
||||
。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-title :level="2">设计资源</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
我们提供完善的设计原则、最佳实践和设计资源文件(
|
||||
<a-typography-text code>Sketch</a-typography-text>
|
||||
和
|
||||
<a-typography-text code>Axure</a-typography-text>
|
||||
),来帮助业务快速设计出高质量的产品原型。
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<a-typography-link href="/docs/resources-cn"
|
||||
>设计资源</a-typography-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
<blockquote>{{ blockContent }}</blockquote>
|
||||
<pre>{{ blockContent }}</pre>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
按
|
||||
<a-typography-text keyboard>Esc</a-typography-text>
|
||||
键退出阅读……
|
||||
</a-typography-paragraph>
|
||||
</a-typography>
|
||||
</a-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const blockContent = ref<string>(`AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、不限可能的数据可视化最佳实践。得益于丰富的业务场景和用户需求挑战,AntV 经历多年积累与不断打磨,已支撑整个阿里集团内外 20000+ 业务系统,通过了日均千万级 UV 产品的严苛考验。
|
||||
我们正在基础图表,图分析,图编辑,地理空间可视化,智能可视化等各个可视化的领域耕耘,欢迎同路人一起前行。` );
|
||||
|
||||
</script>
|
||||
21
src/views/error/403.vue
Normal file
21
src/views/error/403.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-result
|
||||
status="403"
|
||||
title="没有访问权限"
|
||||
sub-title="请不要进行非法操作,您可以返回主页面或返回"
|
||||
>
|
||||
<template #extra>
|
||||
<RouterLink :to="{ name: 'Index' }" :replace="true">
|
||||
<a-button type="primary"> 返回首页 </a-button>
|
||||
</RouterLink>
|
||||
<a-button type="default" @click="() => router.back()"> 返回 </a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
26
src/views/error/404.vue
Normal file
26
src/views/error/404.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<a-result
|
||||
status="404"
|
||||
title="找不到匹配页面"
|
||||
sub-title="对不起,您正在寻找的页面不存在。"
|
||||
>
|
||||
<template #extra>
|
||||
<RouterLink :to="{ name: 'Index' }" :replace="true">
|
||||
<a-button type="primary"> 返回首页 </a-button>
|
||||
</RouterLink>
|
||||
</template>
|
||||
<a-typography>
|
||||
<a-typography-title> 找不到网页? </a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
1. 尝试检查URL的错误,然后按浏览器上的刷新按钮。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
2. 尝试在我们的应用程序中找到其他内容。
|
||||
</a-typography-paragraph>
|
||||
</a-typography>
|
||||
</a-result>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
123
src/views/index.vue
Normal file
123
src/views/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import donate from '@/assets/donate.jpg';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
const userStore = useUserStore();
|
||||
const { appName, appVersion } = useAppStore();
|
||||
|
||||
/**跳转 */
|
||||
function goTarget(type: string) {
|
||||
let url = '';
|
||||
if (type === 'code') {
|
||||
url = 'https://gitee.com/TsMask/';
|
||||
}
|
||||
if (type === 'issues') {
|
||||
url = 'https://gitee.com/TsMask/mask_antd_vue3/issues';
|
||||
}
|
||||
window.open(url, '__blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :breadcrumb="false" :title="appName" sub-title="by TsMask">
|
||||
<template #tags>
|
||||
<a-tag>当前版本:{{ appVersion }}</a-tag>
|
||||
<a-tag color="magenta"><PayCircleOutlined /> 免费开源</a-tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="goTarget('code')">开源仓库</a-button>
|
||||
<a-button type="default" @click="goTarget('issues')">提些建议</a-button>
|
||||
</template>
|
||||
<template #content>
|
||||
<a-space :size="16" align="center">
|
||||
<a-avatar
|
||||
shape="circle"
|
||||
:size="72"
|
||||
:src="userStore.getAvatar"
|
||||
:alt="userStore.userName"
|
||||
></a-avatar>
|
||||
<span class="nickname">
|
||||
{{ userStore.nickName }} ,想必你那里一切安好吧。
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<a-space :size="16">
|
||||
<a-statistic title="在线用户" :value="545486" />
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="16" :md="16" :xs="24">
|
||||
<a-card title="项目简介" style="margin-bottom: 16px">
|
||||
<a-typography>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text mark> Vue3 </a-typography-text>
|
||||
技术组合,支持按钮及数据权限,可自定义部门数据权限。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理等,支持在线定时任务配置。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
使用 <a-typography-text mark> Ant-Design-Vue </a-typography-text>
|
||||
组件库,搭建的前后端分离极速后台管理系统。
|
||||
</a-typography-paragraph>
|
||||
</a-typography>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :lg="8" :md="8" :xs="24">
|
||||
<a-card title="快速开始" style="margin-bottom: 16px">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-button
|
||||
type="link"
|
||||
target="_blank"
|
||||
title="开发手册"
|
||||
href="https://juejin.cn/column/7188761626017792056"
|
||||
>
|
||||
开发手册
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-button
|
||||
type="link"
|
||||
target="_blank"
|
||||
title="来自Apifox的接口文档"
|
||||
href="https://mask-api-midwayjs.apifox.cn/"
|
||||
>
|
||||
接口文档
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-button
|
||||
type="link"
|
||||
target="_blank"
|
||||
title="Middwayjs版本服务端"
|
||||
href="https://gitee.com/TsMask/mask_api_midwayjs"
|
||||
>
|
||||
Node后端
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-button type="text"> 相关待定 </a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-card title="捐赠鼓励" style="margin-top: 16px">
|
||||
<a-image width="100%" :src="donate" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.nickname {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
</style>
|
||||
469
src/views/login.vue
Normal file
469
src/views/login.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import { getCaptchaImage } from '@/api/login';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { regExpMobile, validMobile } from '@/utils/regular-utils';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
const { t, changeLocale } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const codeImgFall =
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
|
||||
/**登录后重定向页面 */
|
||||
const redirectPath =
|
||||
(route.query && (route.query.redirect as string)) || '/index';
|
||||
|
||||
/**Tab默认激活 */
|
||||
let activeKey = ref<'username' | 'phonenumber'>('username');
|
||||
|
||||
let state = reactive({
|
||||
/**表单属性 */
|
||||
from: {
|
||||
/**账号 */
|
||||
username: 'maskAdmin',
|
||||
/**密码 */
|
||||
password: 'Admin@1234',
|
||||
/**手机号 */
|
||||
phonenumber: '',
|
||||
/**验证码 */
|
||||
code: '',
|
||||
/**验证码uuid */
|
||||
uuid: '',
|
||||
},
|
||||
/**表单提交点击状态 */
|
||||
fromClick: false,
|
||||
/**验证码状态 */
|
||||
captcha: {
|
||||
/**验证码开关 */
|
||||
enabled: false,
|
||||
/**验证码图片地址 */
|
||||
codeImg: '',
|
||||
codeImgFall: codeImgFall,
|
||||
},
|
||||
/**验证码点击状态 */
|
||||
captchaClick: false,
|
||||
});
|
||||
|
||||
/**表单验证通过 */
|
||||
function fnFinish() {
|
||||
state.fromClick = true;
|
||||
let form = {};
|
||||
// 账号密码登录
|
||||
if (activeKey.value === 'username') {
|
||||
form = {
|
||||
username: state.from.username,
|
||||
password: state.from.password,
|
||||
code: state.from.code,
|
||||
uuid: state.from.uuid,
|
||||
};
|
||||
}
|
||||
// 手机号登录
|
||||
if (activeKey.value === 'phonenumber') {
|
||||
form = {
|
||||
phonenumber: state.from.phonenumber,
|
||||
code: state.from.code,
|
||||
uuid: state.from.uuid,
|
||||
};
|
||||
}
|
||||
// 发送请求
|
||||
useUserStore()
|
||||
.fnLogin(form)
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success(t('views.login.loginSuccess'), 3);
|
||||
router.push({ path: redirectPath });
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
state.fromClick = false;
|
||||
// 刷新验证码
|
||||
if (state.captcha.enabled) {
|
||||
state.from.code = '';
|
||||
fnGetCaptcha();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
function fnGetCaptcha() {
|
||||
if (state.captchaClick) return;
|
||||
state.captchaClick = true;
|
||||
getCaptchaImage().then(res => {
|
||||
state.captchaClick = false;
|
||||
if (res.code != 200) {
|
||||
message.warning(`${res.msg}`, 3);
|
||||
return;
|
||||
}
|
||||
state.captcha.enabled = Boolean(res.captchaEnabled);
|
||||
if (state.captcha.enabled) {
|
||||
state.captcha.codeImg = res.img;
|
||||
state.from.uuid = res.uuid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**短信验证码定时器 */
|
||||
let smsInterval: any = undefined;
|
||||
|
||||
/**短信验证码信息状态 */
|
||||
let smsState = reactive({
|
||||
/**点击状态 */
|
||||
click: false,
|
||||
/**发送倒计时 */
|
||||
time: 120,
|
||||
});
|
||||
|
||||
/**获取短信验证码 */
|
||||
function fnGetSmsCaptcha() {
|
||||
if (smsState.click) return;
|
||||
if (!validMobile(state.from.phonenumber)) {
|
||||
message.warning(t('valid.phoneReg'), 3);
|
||||
return;
|
||||
}
|
||||
smsState.click = true;
|
||||
|
||||
setTimeout(() => {
|
||||
// start 得到发送结果启动定时
|
||||
message.success(t('valid.codeSmsSend'), 3);
|
||||
state.from.uuid = '短信校验id';
|
||||
smsInterval = setInterval(() => {
|
||||
if (smsState.time <= 0) {
|
||||
smsState.time = 120;
|
||||
smsState.click = false;
|
||||
clearTimeout(smsInterval);
|
||||
} else {
|
||||
smsState.time--;
|
||||
}
|
||||
}, 1000);
|
||||
// end
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**改变多语言 */
|
||||
function fnChangeLocale(e: any) {
|
||||
changeLocale(e.key);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnGetCaptcha();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
smsState.time = 120;
|
||||
smsState.click = false;
|
||||
clearTimeout(smsInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="top">
|
||||
<div class="header">
|
||||
<a href="/" target="_self"
|
||||
><img src="@/assets/logo.png" class="logo" alt="logo" />
|
||||
<span class="title">{{ t('common.title') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="desc">{{ t('common.desc') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<a-form :model="state.from" name="stateFrom" @finish="fnFinish">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeKey"
|
||||
tabPosition="top"
|
||||
type="line"
|
||||
:centered="true"
|
||||
:destroy-inactive-tab-pane="true"
|
||||
>
|
||||
<a-tab-pane key="username" :tab="t('views.login.tabPane1')">
|
||||
<a-form-item
|
||||
name="username"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 2,
|
||||
max: 30,
|
||||
message: t('valid.userNamePlease'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="state.from.username"
|
||||
size="large"
|
||||
:placeholder="t('valid.userNameHit')"
|
||||
:maxlength="30"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
name="password"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 6,
|
||||
max: 26,
|
||||
message: t('valid.passwordPlease'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.from.password"
|
||||
size="large"
|
||||
:placeholder="t('valid.passwordHit')"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="8" v-if="state.captcha.enabled">
|
||||
<a-col :span="16">
|
||||
<a-form-item
|
||||
name="code"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 1,
|
||||
message: t('valid.codePlease'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="state.from.code"
|
||||
size="large"
|
||||
:placeholder="t('valid.codeHit')"
|
||||
:maxlength="6"
|
||||
>
|
||||
<template #prefix>
|
||||
<RobotOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-image
|
||||
:alt="t('valid.codeHit')"
|
||||
style="cursor: pointer; border-radius: 2px"
|
||||
width="120px"
|
||||
height="40px"
|
||||
:preview="false"
|
||||
:src="state.captcha.codeImg"
|
||||
:fallback="state.captcha.codeImgFall"
|
||||
@click="fnGetCaptcha"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row
|
||||
:gutter="8"
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<a-col :span="12">
|
||||
<a-button
|
||||
type="link"
|
||||
target="_self"
|
||||
:title="t('views.login.registerBtn')"
|
||||
@click="() => router.push({ name: 'Register' })"
|
||||
>
|
||||
{{ t('views.login.registerBtn') }}
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="phonenumber" :tab="t('views.login.tabPane2')">
|
||||
<a-form-item
|
||||
name="phonenumber"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpMobile,
|
||||
message: t('valid.phonePlease'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="state.from.phonenumber"
|
||||
size="large"
|
||||
:placeholder="t('valid.phoneHit')"
|
||||
:maxlength="11"
|
||||
>
|
||||
<template #prefix>
|
||||
<MobileOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
name="code"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 4,
|
||||
message: t('valid.codePlease'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="state.from.code"
|
||||
size="large"
|
||||
:placeholder="t('valid.codeHit')"
|
||||
:maxlength="6"
|
||||
>
|
||||
<template #prefix>
|
||||
<RobotOutlined class="prefix-icon" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
:disabled="smsState.click"
|
||||
@click="fnGetSmsCaptcha"
|
||||
>
|
||||
{{
|
||||
smsState.click
|
||||
? `${smsState.time} s`
|
||||
: t('valid.codeText')
|
||||
}}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-button
|
||||
block
|
||||
type="primary"
|
||||
size="large"
|
||||
html-type="submit"
|
||||
:loading="state.fromClick"
|
||||
>
|
||||
{{ t('views.login.loginBtn') }}
|
||||
</a-button>
|
||||
|
||||
<a-row
|
||||
:gutter="8"
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
style="margin-top: 18px"
|
||||
>
|
||||
<a-col :span="18">
|
||||
<span>{{ t('views.login.loginMethod') }}</span>
|
||||
<a-tooltip :title="t('views.login.loginMethodWX')">
|
||||
<a-button shape="circle" size="middle" type="link">
|
||||
<template #icon>
|
||||
<WechatOutlined
|
||||
:style="{ color: '#52c41a', fontSize: '18px' }"
|
||||
/>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('views.login.loginMethodQQ')">
|
||||
<a-button shape="circle" size="middle" type="link">
|
||||
<template #icon>
|
||||
<QqOutlined :style="{ color: '#40a9ff', fontSize: '18px' }" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-dropdown :trigger="['click', 'hover']">
|
||||
<a-button size="small" type="default">
|
||||
{{ t('i18n') }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="fnChangeLocale">
|
||||
<a-menu-item key="zh_CN">中文</a-menu-item>
|
||||
<a-menu-item key="en_US">English</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: 110px 0 144px;
|
||||
background-image: url(../assets/background.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 110px;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.top {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
.logo {
|
||||
height: 44px;
|
||||
margin-right: 16px;
|
||||
vertical-align: top;
|
||||
border-style: none;
|
||||
border-radius: 6.66px;
|
||||
}
|
||||
.title {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 600;
|
||||
font-size: 33px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 40px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 368px;
|
||||
min-width: 260px;
|
||||
margin: 0 auto;
|
||||
|
||||
.prefix-icon {
|
||||
color: #8c8c8c;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
498
src/views/monitor/cache/index.vue
vendored
Normal file
498
src/views/monitor/cache/index.vue
vendored
Normal file
@@ -0,0 +1,498 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import {
|
||||
listCacheName,
|
||||
listCacheKey,
|
||||
getCacheValue,
|
||||
clearCacheName,
|
||||
clearCacheKey,
|
||||
clearCacheSafe,
|
||||
} from '@/api/monitor/cache';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table/Table';
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
import { hasPermissions } from '@/plugins/auth-user';
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
|
||||
/**请求点击 */
|
||||
let isClick = ref<boolean>(false);
|
||||
|
||||
/**缓存内容信息 */
|
||||
let cacheKeyInfo = reactive({
|
||||
loading: true,
|
||||
data: {
|
||||
cacheKey: '',
|
||||
cacheName: '',
|
||||
cacheValue: '',
|
||||
remark: '',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 查询缓存内容详细
|
||||
* @param cacheKey
|
||||
*/
|
||||
function fnCacheKeyInfo(cacheKey: string) {
|
||||
if (!hasPermissions(['monitor:cache:query'])) return;
|
||||
if (isClick.value) return;
|
||||
isClick.value = true;
|
||||
cacheKeyInfo.loading = true;
|
||||
getCacheValue(cacheKeyTable.cacheName, cacheKey).then(res => {
|
||||
isClick.value = false;
|
||||
if (res.code === 200) {
|
||||
cacheKeyInfo.data = Object.assign(cacheKeyInfo.data, res.data);
|
||||
cacheKeyInfo.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**键名列表表格字段列 */
|
||||
let cacheKeyTableColumns: ColumnsType = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'num',
|
||||
width: '50px',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
return opt.index + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '缓存键名',
|
||||
dataIndex: 'cacheKey',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
// 渲染值处理
|
||||
customRender(opt) {
|
||||
return opt.text;
|
||||
},
|
||||
// 自定义过滤控件
|
||||
customFilterDropdown: true,
|
||||
onFilter: (value, record) => {
|
||||
if (typeof value === 'string') {
|
||||
const nameLower = record.cacheKey.toLowerCase();
|
||||
return nameLower.includes(value.toLowerCase());
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'option',
|
||||
align: 'center',
|
||||
width: '50px',
|
||||
},
|
||||
];
|
||||
|
||||
/**键名列表表格数据 */
|
||||
let cacheKeyTable = reactive({
|
||||
loading: true,
|
||||
data: [],
|
||||
/**当前键名列表的缓存名称 */
|
||||
cacheName: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* 清理指定缓存键名
|
||||
* @param cacheKey 键名列表中的缓存键名
|
||||
*/
|
||||
function fnCacheKeyClear(cacheKey: string) {
|
||||
if (isClick.value) return;
|
||||
isClick.value = true;
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
clearCacheKey(cacheKeyTable.cacheName, cacheKey).then(res => {
|
||||
hide();
|
||||
isClick.value = false;
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `已删除缓存键名 ${cacheKey}`,
|
||||
duration: 3,
|
||||
});
|
||||
// 缓存内容显示且是删除的缓存键名,需要进行加载显示
|
||||
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheKey === cacheKey) {
|
||||
cacheKeyInfo.loading = true;
|
||||
}
|
||||
} else {
|
||||
message.error({
|
||||
content: res.msg,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
fnCacheKeyList();
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询缓存键名列表 */
|
||||
function fnCacheKeyList(cacheName: string = 'load') {
|
||||
if (cacheName === 'load') {
|
||||
cacheName = cacheKeyTable.cacheName;
|
||||
}
|
||||
if (!cacheName) {
|
||||
message.warning('请在缓存列表中选择数据项!', 3);
|
||||
return;
|
||||
}
|
||||
if (isClick.value) return;
|
||||
isClick.value = true;
|
||||
cacheKeyTable.loading = true;
|
||||
listCacheKey(cacheName).then(res => {
|
||||
isClick.value = false;
|
||||
if (res.code === 200 && res.data) {
|
||||
cacheKeyTable.cacheName = cacheName;
|
||||
cacheKeyTable.data = res.data;
|
||||
cacheKeyTable.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**缓存列表表格数据 */
|
||||
let cacheNameTable = reactive({
|
||||
loading: true,
|
||||
data: [],
|
||||
});
|
||||
|
||||
/**缓存列表表格字段列 */
|
||||
let cacheNameTableColumns: ColumnsType = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'num',
|
||||
width: '50px',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
return opt.index + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '缓存名称',
|
||||
dataIndex: 'cacheName',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
// 渲染值处理
|
||||
customRender(opt) {
|
||||
return opt.text;
|
||||
},
|
||||
// 自定义过滤控件
|
||||
customFilterDropdown: true,
|
||||
onFilter: (value, record) => {
|
||||
if (typeof value === 'string') {
|
||||
const nameLower = record.cacheName.toLowerCase();
|
||||
return nameLower.includes(value.toLowerCase());
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'option',
|
||||
align: 'center',
|
||||
width: '50px',
|
||||
},
|
||||
];
|
||||
|
||||
/**安全清理缓存 */
|
||||
function fnClearCacheSafe() {
|
||||
if (isClick.value) return;
|
||||
isClick.value = true;
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
clearCacheSafe().then(res => {
|
||||
hide();
|
||||
isClick.value = false;
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: '已完成安全清理缓存',
|
||||
duration: 3,
|
||||
});
|
||||
cacheKeyTable.loading = true;
|
||||
cacheKeyInfo.loading = true;
|
||||
} else {
|
||||
message.error({
|
||||
content: res.msg,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定缓存名称
|
||||
* @param cacheName 缓存名称
|
||||
*/
|
||||
function fnCacheNameClear(cacheName: string) {
|
||||
if (isClick.value) return;
|
||||
isClick.value = true;
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
clearCacheName(cacheName).then(res => {
|
||||
hide();
|
||||
isClick.value = false;
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `已清理缓存名称 ${cacheName}`,
|
||||
duration: 3,
|
||||
});
|
||||
// 缓存内容显示且是删除的缓存名称,需要进行加载显示
|
||||
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheName === cacheName) {
|
||||
cacheKeyInfo.loading = true;
|
||||
}
|
||||
} else {
|
||||
message.error({
|
||||
content: res.msg,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
fnCacheKeyList(cacheName);
|
||||
});
|
||||
}
|
||||
|
||||
/**查询缓存名称列表 */
|
||||
function fnCacheNameList() {
|
||||
if (isClick.value) return;
|
||||
isClick.value = true;
|
||||
cacheNameTable.loading = true;
|
||||
listCacheName().then(res => {
|
||||
isClick.value = false;
|
||||
if (res.code === 200 && res.data) {
|
||||
cacheNameTable.data = res.data;
|
||||
cacheNameTable.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnCacheNameList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :title="title">
|
||||
<template #content>
|
||||
<a-typography-paragraph>
|
||||
系统在缓存
|
||||
<a-typography-text code>Redis</a-typography-text>
|
||||
应用程序中的可控的缓存信息
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :lg="8" :md="8" :xs="24">
|
||||
<a-card
|
||||
title="缓存列表"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>刷新</template>
|
||||
<a-button type="text" @click.prevent="fnCacheNameList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>安全清理</template>
|
||||
<a-popconfirm
|
||||
placement="bottomRight"
|
||||
title="确认要执行可安全清理的缓存下所有键名吗?`"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="fnClearCacheSafe()"
|
||||
>
|
||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
row-key="cacheName"
|
||||
size="small"
|
||||
:columns="cacheNameTableColumns"
|
||||
:data-source="cacheNameTable.data"
|
||||
:loading="cacheNameTable.loading"
|
||||
:row-selection="{
|
||||
type: 'radio',
|
||||
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyList(selectedRowKeys[0] as string),
|
||||
}"
|
||||
:pagination="false"
|
||||
>
|
||||
<template
|
||||
#customFilterDropdown="{
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
column,
|
||||
}"
|
||||
>
|
||||
<div style="padding: 8px">
|
||||
<a-input
|
||||
:placeholder="`模糊过滤 ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
style="width: 188px; margin-bottom: 8px; display: block"
|
||||
@change="
|
||||
e => setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
"
|
||||
@pressEnter="confirm()"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
@click="confirm()"
|
||||
>
|
||||
过滤
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
@click="clearFilters({ confirm: true })"
|
||||
>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'option'">
|
||||
<a-popconfirm
|
||||
placement="topRight"
|
||||
title="确认要清理该缓存名称下的所有键名吗?`"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="fnCacheNameClear(record.cacheName)"
|
||||
>
|
||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="8" :md="8" :xs="24">
|
||||
<a-card
|
||||
title="键名列表"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<template #extra>
|
||||
<a-tooltip>
|
||||
<template #title>刷新</template>
|
||||
<a-button type="text" @click.prevent="fnCacheKeyList()">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-table
|
||||
row-key="cacheKey"
|
||||
size="small"
|
||||
:columns="cacheKeyTableColumns"
|
||||
:data-source="cacheKeyTable.data"
|
||||
:loading="cacheKeyTable.loading"
|
||||
:row-selection="{
|
||||
type: 'radio',
|
||||
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyInfo(selectedRowKeys[0] as string),
|
||||
}"
|
||||
:pagination="false"
|
||||
>
|
||||
<template
|
||||
#customFilterDropdown="{
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
column,
|
||||
}"
|
||||
>
|
||||
<div style="padding: 8px">
|
||||
<a-input
|
||||
:placeholder="`模糊过滤 ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
style="width: 188px; margin-bottom: 8px; display: block"
|
||||
@change="
|
||||
e => setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
"
|
||||
@pressEnter="confirm()"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
@click="confirm()"
|
||||
>
|
||||
过滤
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
@click="clearFilters({ confirm: true })"
|
||||
>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'option'">
|
||||
<a-popconfirm
|
||||
placement="topRight"
|
||||
title="确认要删除该缓存键吗?`"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="fnCacheKeyClear(record.cacheKey)"
|
||||
>
|
||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="8" :md="8" :xs="24" v-perms:has="['monitor:cache:query']">
|
||||
<a-card
|
||||
title="缓存内容"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
:loading="cacheKeyInfo.loading"
|
||||
>
|
||||
<a-descriptions
|
||||
size="small"
|
||||
layout="vertical"
|
||||
:bordered="true"
|
||||
:column="1"
|
||||
>
|
||||
<a-descriptions-item label="缓存名称">
|
||||
{{ cacheKeyInfo.data.cacheName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="缓存键名">
|
||||
{{ cacheKeyInfo.data.cacheKey }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="缓存内容">
|
||||
<a-typography-paragraph>
|
||||
<a-textarea
|
||||
:value="cacheKeyInfo.data.cacheValue"
|
||||
:auto-size="{ minRows: 4, maxRows: 10 }"
|
||||
:maxlength="4000"
|
||||
:disabled="true"
|
||||
placeholder="显示缓存内容"
|
||||
/>
|
||||
</a-typography-paragraph>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
222
src/views/monitor/cache/info.vue
vendored
Normal file
222
src/views/monitor/cache/info.vue
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts/core';
|
||||
import {
|
||||
ToolboxComponent,
|
||||
ToolboxComponentOption,
|
||||
TooltipComponent,
|
||||
TooltipComponentOption,
|
||||
LegendComponent,
|
||||
LegendComponentOption,
|
||||
} from 'echarts/components';
|
||||
import { PieChart, PieSeriesOption } from 'echarts/charts';
|
||||
import { LabelLayout } from 'echarts/features';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { getCache } from '@/api/monitor/cache';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
echarts.use([
|
||||
ToolboxComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
PieChart,
|
||||
CanvasRenderer,
|
||||
LabelLayout,
|
||||
]);
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
|
||||
/**加载状态 */
|
||||
let loading = ref<boolean>(true);
|
||||
|
||||
/**数据参数类型 */
|
||||
type CacheType = {
|
||||
/**服务信息 */
|
||||
info: InfoType;
|
||||
/**当前连接可用键Key总数 */
|
||||
dbSize: number;
|
||||
/**命令状态 */
|
||||
commandStats: Record<string, string>[];
|
||||
};
|
||||
|
||||
/**数据参数服务信息类型 */
|
||||
type InfoType = {
|
||||
clients: Record<string, string>;
|
||||
cluster: Record<string, string>;
|
||||
cpu: Record<string, string>;
|
||||
errorstats: Record<string, string>;
|
||||
keyspace: Record<string, string>;
|
||||
memory: Record<string, string>;
|
||||
modules: Record<string, string>;
|
||||
persistence: Record<string, string>;
|
||||
replication: Record<string, string>;
|
||||
server: Record<string, string>;
|
||||
stats: Record<string, string>;
|
||||
};
|
||||
|
||||
let cache: CacheType = reactive({
|
||||
info: {
|
||||
clients: {},
|
||||
cluster: {},
|
||||
cpu: {},
|
||||
errorstats: {},
|
||||
keyspace: {},
|
||||
memory: {},
|
||||
modules: {},
|
||||
persistence: {},
|
||||
replication: {},
|
||||
server: {},
|
||||
stats: {},
|
||||
},
|
||||
dbSize: 0,
|
||||
commandStats: [],
|
||||
});
|
||||
|
||||
/**生成命令统计图 */
|
||||
function commandStatsChart() {
|
||||
const commandStatsDom = document.getElementById('commandstats');
|
||||
if (!commandStatsDom) return;
|
||||
const commandStatsEchart = echarts.init(commandStatsDom);
|
||||
const option: echarts.ComposeOption<
|
||||
| ToolboxComponentOption
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
| PieSeriesOption
|
||||
> = {
|
||||
// 鼠标悬浮提示
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||
},
|
||||
// 左侧标签
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
// 右侧工具
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
mark: { show: true },
|
||||
dataView: { show: true, readOnly: false },
|
||||
restore: { show: true },
|
||||
saveAsImage: { show: true },
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '命令',
|
||||
type: 'pie',
|
||||
radius: ['5%', '80%'],
|
||||
center: ['60%', '50%'],
|
||||
roseType: 'area',
|
||||
itemStyle: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
data: cache.commandStats,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
commandStatsEchart.setOption(option);
|
||||
window.addEventListener('resize', function () {
|
||||
commandStatsEchart.resize();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCache()
|
||||
.then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
cache.info = res.data.info;
|
||||
cache.dbSize = res.data.dbSize;
|
||||
cache.commandStats = res.data.commandStats;
|
||||
// 加载状态
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// 加载结束后生成图
|
||||
commandStatsChart();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :title="title" :loading="loading">
|
||||
<template #content>
|
||||
<a-typography-paragraph>
|
||||
缓存
|
||||
<a-typography-text code>Redis</a-typography-text>
|
||||
应用程序的信息
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
|
||||
<a-card
|
||||
title="基本信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:bordered="true"
|
||||
:column="{ lg: 4, md: 2, xs: 1 }"
|
||||
>
|
||||
<a-descriptions-item label="Redis版本">
|
||||
{{ cache.info.server.redis_version }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行模式">
|
||||
{{ cache.info.server.redis_mode == 'standalone' ? '单机' : '集群' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="端口">
|
||||
{{ cache.info.server.tcp_port }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="客户端数">
|
||||
{{ cache.info.clients.connected_clients }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行时间(天)">
|
||||
{{ cache.info.server.uptime_in_days }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用内存">
|
||||
{{ cache.info.memory.used_memory_human }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用CPU">
|
||||
{{ parseFloat(cache.info.cpu.used_cpu_user_children).toFixed(2) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="内存配置">
|
||||
{{ cache.info.memory.maxmemory_human }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="AOF是否开启">
|
||||
{{ cache.info.persistence.aof_enabled == '0' ? '否' : '是' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="RDB是否成功">
|
||||
{{ cache.info.persistence.rdb_last_bgsave_status }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Key数量">
|
||||
{{ cache.dbSize }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="网络入口/出口">
|
||||
{{ cache.info.stats.instantaneous_input_kbps }} kps /
|
||||
{{ cache.info.stats.instantaneous_output_kbps }} kps
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card title="命令统计" :bordered="false">
|
||||
<div id="commandstats" style="height: 400px; width: 100%"></div>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
1087
src/views/monitor/job/index.vue
Normal file
1087
src/views/monitor/job/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
682
src/views/monitor/job/log.vue
Normal file
682
src/views/monitor/job/log.vue
Normal file
@@ -0,0 +1,682 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { reactive, ref, onMounted, toRaw } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message, Modal } from 'ant-design-vue/lib';
|
||||
import { SizeType } from 'ant-design-vue/lib/config-provider';
|
||||
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table';
|
||||
import {
|
||||
exportJobLog,
|
||||
listJobLog,
|
||||
delJobLog,
|
||||
cleanJobLog,
|
||||
} from '@/api/monitor/jobLog';
|
||||
import { getJob } from '@/api/monitor/job';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import useTabsStore from '@/store/modules/tabs';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
const tabsStore = useTabsStore();
|
||||
const { getDict } = useDictStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 获取地址栏参数
|
||||
const jobId = route.params && (route.params.jobId as string);
|
||||
|
||||
/**字典数据 */
|
||||
let dict: {
|
||||
/**任务组名 */
|
||||
sysJobGroup: DictType[];
|
||||
/**执行状态 */
|
||||
sysCommonStatus: DictType[];
|
||||
} = reactive({
|
||||
sysJobGroup: [],
|
||||
sysCommonStatus: [],
|
||||
});
|
||||
|
||||
/**记录开始结束时间 */
|
||||
let queryRangePicker = ref<[string, string]>(['', '']);
|
||||
|
||||
/**查询参数 */
|
||||
let queryParams = reactive({
|
||||
/**任务名称 */
|
||||
jobName: '',
|
||||
/**任务组名 */
|
||||
jobGroup: undefined,
|
||||
/**执行状态 */
|
||||
status: undefined,
|
||||
/**记录开始时间 */
|
||||
beginTime: '',
|
||||
/**记录结束时间 */
|
||||
endTime: '',
|
||||
/**当前页数 */
|
||||
pageNum: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
/**查询参数重置 */
|
||||
function fnQueryReset() {
|
||||
if (jobId && jobId !== '0') {
|
||||
queryParams = Object.assign(queryParams, {
|
||||
status: undefined,
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
} else {
|
||||
queryParams = Object.assign(queryParams, {
|
||||
jobName: '',
|
||||
jobGroup: undefined,
|
||||
status: undefined,
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
}
|
||||
queryRangePicker.value = ['', ''];
|
||||
tablePagination.current = 1;
|
||||
tablePagination.pageSize = 20;
|
||||
fnGetList();
|
||||
}
|
||||
|
||||
/**表格状态类型 */
|
||||
type TabeStateType = {
|
||||
/**加载等待 */
|
||||
loading: boolean;
|
||||
/**紧凑型 */
|
||||
size: SizeType;
|
||||
/**斑马纹 */
|
||||
striped: boolean;
|
||||
/**搜索栏 */
|
||||
seached: boolean;
|
||||
/**记录数据 */
|
||||
data: object[];
|
||||
/**勾选记录 */
|
||||
selectedRowKeys: (string | number)[];
|
||||
};
|
||||
|
||||
/**表格状态 */
|
||||
let tableState: TabeStateType = reactive({
|
||||
loading: false,
|
||||
size: 'middle',
|
||||
striped: false,
|
||||
seached: true,
|
||||
data: [],
|
||||
selectedRowKeys: [],
|
||||
});
|
||||
|
||||
/**表格字段列 */
|
||||
let tableColumns: ColumnsType = [
|
||||
{
|
||||
title: '日志编号',
|
||||
dataIndex: 'jobLogId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'jobName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '任务组名',
|
||||
dataIndex: 'jobGroup',
|
||||
key: 'jobGroup',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '调用目标',
|
||||
dataIndex: 'invokeTarget',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '执行状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '记录时间',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
if (+opt.value <= 0) return '';
|
||||
return parseDateToStr(+opt.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '消耗时间',
|
||||
dataIndex: 'costTime',
|
||||
key: 'costTime',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
return `${opt.value} ms`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'jobLogId',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
/**表格分页器参数 */
|
||||
let tablePagination = reactive({
|
||||
/**当前页数 */
|
||||
current: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
/**默认的每页条数 */
|
||||
defaultPageSize: 20,
|
||||
/**指定每页可以显示多少条 */
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
/**只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage: false,
|
||||
/**是否可以快速跳转至某页 */
|
||||
showQuickJumper: true,
|
||||
/**是否可以改变 pageSize */
|
||||
showSizeChanger: true,
|
||||
/**数据总数 */
|
||||
total: 0,
|
||||
showTotal: (total: number) => `总共 ${total} 条`,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
tablePagination.current = page;
|
||||
tablePagination.pageSize = pageSize;
|
||||
queryParams.pageNum = page;
|
||||
queryParams.pageSize = pageSize;
|
||||
fnGetList();
|
||||
},
|
||||
});
|
||||
|
||||
/**表格紧凑型变更操作 */
|
||||
function fnTableSize({ key }: MenuInfo) {
|
||||
tableState.size = key as SizeType;
|
||||
}
|
||||
|
||||
/**表格斑马纹 */
|
||||
function fnTableStriped(_record: unknown, index: number) {
|
||||
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
|
||||
}
|
||||
|
||||
/**表格多选 */
|
||||
function fnTableSelectedRowKeys(keys: (string | number)[]) {
|
||||
tableState.selectedRowKeys = keys;
|
||||
}
|
||||
|
||||
/**对话框对象信息状态类型 */
|
||||
type ModalStateType = {
|
||||
/**详情框是否显示 */
|
||||
visibleByView: boolean;
|
||||
/**标题 */
|
||||
title: string;
|
||||
/**表单数据 */
|
||||
from: Record<string, any>;
|
||||
};
|
||||
|
||||
/**对话框对象信息状态 */
|
||||
let modalState: ModalStateType = reactive({
|
||||
visibleByView: false,
|
||||
title: '任务日志',
|
||||
from: {
|
||||
jobLogId: undefined,
|
||||
jobName: '',
|
||||
jobGroup: 'DEFAULT',
|
||||
invokeTarget: '',
|
||||
targetParams: '',
|
||||
status: '0',
|
||||
jobMsg: '',
|
||||
createTime: 0,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 对话框弹出显示为 查看
|
||||
* @param row 调度日志信息对象
|
||||
*/
|
||||
function fnModalVisibleByVive(row: Record<string, string>) {
|
||||
modalState.from = Object.assign(modalState.from, row);
|
||||
modalState.title = '调度日志信息';
|
||||
modalState.visibleByView = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框弹出关闭执行函数
|
||||
*/
|
||||
function fnModalCancel() {
|
||||
modalState.visibleByView = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务删除
|
||||
*/
|
||||
function fnRecordDelete() {
|
||||
const ids = tableState.selectedRowKeys.join(',');
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认删除调度日志编号为 【${ids}】 的数据项吗?`,
|
||||
onOk() {
|
||||
const key = 'delJobLog';
|
||||
message.loading({ content: '请稍等...', key });
|
||||
delJobLog(ids).then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `删除成功`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
fnGetList();
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**列表清空 */
|
||||
function fnCleanList() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认清空所有调度日志数据项吗?`,
|
||||
onOk() {
|
||||
const key = 'cleanJobLog';
|
||||
message.loading({ content: '请稍等...', key });
|
||||
cleanJobLog().then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `清空成功`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
fnGetList();
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**列表导出 */
|
||||
function fnExportList() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认根据搜索条件导出xlsx表格文件吗?`,
|
||||
onOk() {
|
||||
const key = 'exportJobLog';
|
||||
message.loading({ content: '请稍等...', key });
|
||||
exportJobLog(toRaw(queryParams)).then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `已完成导出`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
saveAs(res.data, `job_log_${Date.now()}.xlsx`);
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**关闭跳转 */
|
||||
function fnClose() {
|
||||
const to = tabsStore.tabClose(route.path);
|
||||
if (to) {
|
||||
router.push(to);
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
}
|
||||
|
||||
/**查询调度日志列表 */
|
||||
function fnGetList() {
|
||||
tableState.loading = true;
|
||||
queryParams.beginTime = queryRangePicker.value[0];
|
||||
queryParams.endTime = queryRangePicker.value[1];
|
||||
listJobLog(toRaw(queryParams)).then(res => {
|
||||
if (res.code === 200) {
|
||||
// 取消勾选
|
||||
if (tableState.selectedRowKeys.length > 0) {
|
||||
tableState.selectedRowKeys = [];
|
||||
}
|
||||
tablePagination.total = res.total;
|
||||
tableState.data = res.rows;
|
||||
tableState.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
Promise.allSettled([
|
||||
getDict('sys_job_group'),
|
||||
getDict('sys_common_status'),
|
||||
]).then(resArr => {
|
||||
if (resArr[0].status === 'fulfilled') {
|
||||
dict.sysJobGroup = resArr[0].value;
|
||||
}
|
||||
if (resArr[1].status === 'fulfilled') {
|
||||
dict.sysCommonStatus = resArr[1].value;
|
||||
}
|
||||
});
|
||||
// 指定任务id数据列表
|
||||
if (jobId && jobId !== '0') {
|
||||
getJob(jobId).then(res => {
|
||||
if (res.code === 200) {
|
||||
queryParams.jobName = res.data.jobName;
|
||||
queryParams.jobGroup = res.data.jobGroup;
|
||||
fnGetList();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 获取列表数据
|
||||
fnGetList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card
|
||||
v-show="tableState.seached"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
|
||||
>
|
||||
<!-- 表格搜索栏 -->
|
||||
<a-form :model="queryParams" name="queryParams" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="任务名称" name="jobName">
|
||||
<a-input
|
||||
v-model:value="queryParams.jobName"
|
||||
:allow-clear="jobId === '0'"
|
||||
:disabled="jobId !== '0'"
|
||||
placeholder="请输入任务名称"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="任务组名" name="jobGroup">
|
||||
<a-select
|
||||
v-model:value="queryParams.jobGroup"
|
||||
allow-clear
|
||||
placeholder="请选择菜单状态"
|
||||
:options="dict.sysJobGroup"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="执行状态" name="status">
|
||||
<a-select
|
||||
v-model:value="queryParams.status"
|
||||
allow-clear
|
||||
placeholder="请选择执行状态"
|
||||
:options="dict.sysCommonStatus"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="记录时间" name="queryRangePicker">
|
||||
<a-range-picker
|
||||
v-model:value="queryRangePicker"
|
||||
allow-clear
|
||||
bordered
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="['记录开始', '记录结束']"
|
||||
style="width: 100%"
|
||||
></a-range-picker>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item>
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" @click.prevent="fnGetList">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button type="default" @click.prevent="fnQueryReset">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
<a-button type="default" @click.prevent="fnClose()">
|
||||
<template #icon><CloseOutlined /></template>
|
||||
关闭
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
danger
|
||||
:disabled="tableState.selectedRowKeys.length <= 0"
|
||||
@click.prevent="fnRecordDelete()"
|
||||
v-perms:has="['monitor:job:remove']"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
danger
|
||||
@click.prevent="fnCleanList()"
|
||||
v-perms:has="['monitor:job:remove']"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
清空
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
@click.prevent="fnExportList()"
|
||||
v-perms:has="['monitor:job:export']"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>搜索栏</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.seached"
|
||||
checked-children="显"
|
||||
un-checked-children="隐"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>表格斑马纹</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.striped"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>刷新</template>
|
||||
<a-button type="text" @click.prevent="fnGetList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>密度</template>
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="text">
|
||||
<template #icon><ColumnHeightOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu
|
||||
:selected-keys="[tableState.size as string]"
|
||||
@click="fnTableSize"
|
||||
>
|
||||
<a-menu-item key="default">默认</a-menu-item>
|
||||
<a-menu-item key="middle">中等</a-menu-item>
|
||||
<a-menu-item key="small">紧凑</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<a-table
|
||||
class="table"
|
||||
row-key="jobLogId"
|
||||
:columns="tableColumns"
|
||||
:loading="tableState.loading"
|
||||
:data-source="tableState.data"
|
||||
:size="tableState.size"
|
||||
:row-class-name="fnTableStriped"
|
||||
:scroll="{ x: true }"
|
||||
:pagination="tablePagination"
|
||||
:row-selection="{
|
||||
type: 'checkbox',
|
||||
onChange: fnTableSelectedRowKeys,
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'jobGroup'">
|
||||
<DictTag :options="dict.sysJobGroup" :value="record.jobGroup" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="+record.status ? 'success' : 'error'">
|
||||
{{ ['失败', '正常'][+record.status] }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'jobLogId'">
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>查看详情</template>
|
||||
<a-button
|
||||
type="link"
|
||||
@click.prevent="fnModalVisibleByVive(record)"
|
||||
v-perms:has="['monitor:job:query']"
|
||||
>
|
||||
<template #icon><ProfileOutlined /></template>
|
||||
详情
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 详情框 -->
|
||||
<a-modal
|
||||
width="800px"
|
||||
:visible="modalState.visibleByView"
|
||||
:title="modalState.title"
|
||||
@cancel="fnModalCancel"
|
||||
>
|
||||
<a-form layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="日志编号" name="jobLogId">
|
||||
{{ modalState.from.jobLogId }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="执行状态" name="status">
|
||||
<a-tag :color="+modalState.from.status ? 'success' : 'error'">
|
||||
{{ ['失败', '正常'][+modalState.from.status] }}
|
||||
</a-tag>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="任务名称" name="jobName">
|
||||
{{ modalState.from.jobName }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="任务组名" name="jobGroup">
|
||||
<DictTag
|
||||
:options="dict.sysJobGroup"
|
||||
:value="modalState.from.jobGroup"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="调用目标" name="invokeTarget">
|
||||
{{ modalState.from.invokeTarget }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="记录时间" name="createTime">
|
||||
<span v-if="+modalState.from.createTime > 0">
|
||||
{{ parseDateToStr(+modalState.from.createTime) }}
|
||||
</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="传入参数" name="targetParams">
|
||||
<a-textarea
|
||||
v-model:value="modalState.from.targetParams"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
placeholder="传入参数"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="日志信息" name="jobMsg">
|
||||
<a-textarea
|
||||
v-model:value="modalState.from.jobMsg"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
placeholder="日志信息"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table :deep(.table-striped) td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.table :deep(.ant-pagination) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
546
src/views/monitor/logininfor/index.vue
Normal file
546
src/views/monitor/logininfor/index.vue
Normal file
@@ -0,0 +1,546 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref, onMounted, toRaw } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message, Modal } from 'ant-design-vue/lib';
|
||||
import { SizeType } from 'ant-design-vue/lib/config-provider';
|
||||
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table';
|
||||
import {
|
||||
exportLogininfor,
|
||||
listLogininfor,
|
||||
delLogininfor,
|
||||
cleanLogininfor,
|
||||
unlockLogininfor,
|
||||
} from '@/api/monitor/logininfor';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
const { getDict } = useDictStore();
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
|
||||
/**字典数据 */
|
||||
let dict: {
|
||||
/**登录状态 */
|
||||
sysCommonStatus: DictType[];
|
||||
} = reactive({
|
||||
sysCommonStatus: [],
|
||||
});
|
||||
|
||||
/**开始结束时间 */
|
||||
let queryRangePicker = ref<[string, string]>(['', '']);
|
||||
|
||||
/**查询参数 */
|
||||
let queryParams = reactive({
|
||||
/**登录地址 */
|
||||
ipaddr: '',
|
||||
/**登录账号 */
|
||||
userName: '',
|
||||
/**登录状态 */
|
||||
status: undefined,
|
||||
/**开始时间 */
|
||||
beginTime: '',
|
||||
/**结束时间 */
|
||||
endTime: '',
|
||||
/**当前页数 */
|
||||
pageNum: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
/**查询参数重置 */
|
||||
function fnQueryReset() {
|
||||
queryParams = Object.assign(queryParams, {
|
||||
ipaddr: '',
|
||||
userName: '',
|
||||
status: undefined,
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
queryRangePicker.value = ['', ''];
|
||||
tablePagination.current = 1;
|
||||
tablePagination.pageSize = 20;
|
||||
fnGetList();
|
||||
}
|
||||
|
||||
/**表格状态类型 */
|
||||
type TabeStateType = {
|
||||
/**加载等待 */
|
||||
loading: boolean;
|
||||
/**紧凑型 */
|
||||
size: SizeType;
|
||||
/**斑马纹 */
|
||||
striped: boolean;
|
||||
/**搜索栏 */
|
||||
seached: boolean;
|
||||
/**记录数据 */
|
||||
data: object[];
|
||||
/**勾选记录 */
|
||||
selectedRowKeys: (string | number)[];
|
||||
/**勾选单个的登录账号 */
|
||||
selectedUserName: string;
|
||||
};
|
||||
|
||||
/**表格状态 */
|
||||
let tableState: TabeStateType = reactive({
|
||||
loading: false,
|
||||
size: 'middle',
|
||||
striped: false,
|
||||
seached: false,
|
||||
data: [],
|
||||
selectedRowKeys: [],
|
||||
selectedUserName: '',
|
||||
});
|
||||
|
||||
/**表格字段列 */
|
||||
let tableColumns: ColumnsType = [
|
||||
{
|
||||
title: '日志编号',
|
||||
dataIndex: 'infoId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录账号',
|
||||
dataIndex: 'userName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录地址',
|
||||
dataIndex: 'ipaddr',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录地点',
|
||||
dataIndex: 'loginLocation',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
dataIndex: 'os',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
dataIndex: 'browser',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录信息',
|
||||
dataIndex: 'msg',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录时间',
|
||||
dataIndex: 'loginTime',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
if (+opt.value <= 0) return '';
|
||||
return parseDateToStr(+opt.value);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**表格分页器参数 */
|
||||
let tablePagination = reactive({
|
||||
/**当前页数 */
|
||||
current: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
/**默认的每页条数 */
|
||||
defaultPageSize: 20,
|
||||
/**指定每页可以显示多少条 */
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
/**只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage: false,
|
||||
/**是否可以快速跳转至某页 */
|
||||
showQuickJumper: true,
|
||||
/**是否可以改变 pageSize */
|
||||
showSizeChanger: true,
|
||||
/**数据总数 */
|
||||
total: 0,
|
||||
showTotal: (total: number) => `总共 ${total} 条`,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
tablePagination.current = page;
|
||||
tablePagination.pageSize = pageSize;
|
||||
queryParams.pageNum = page;
|
||||
queryParams.pageSize = pageSize;
|
||||
fnGetList();
|
||||
},
|
||||
});
|
||||
|
||||
/**表格紧凑型变更操作 */
|
||||
function fnTableSize({ key }: MenuInfo) {
|
||||
tableState.size = key as SizeType;
|
||||
}
|
||||
|
||||
/**表格斑马纹 */
|
||||
function fnTableStriped(_record: unknown, index: number) {
|
||||
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
|
||||
}
|
||||
|
||||
/**表格多选 */
|
||||
function fnTableSelectedRows(
|
||||
_: (string | number)[],
|
||||
rows: Record<string, string>[]
|
||||
) {
|
||||
tableState.selectedRowKeys = rows.map(item => item.infoId);
|
||||
// 针对单个登录账号解锁
|
||||
if (rows.length === 1) {
|
||||
tableState.selectedUserName = rows[0].userName;
|
||||
} else {
|
||||
tableState.selectedUserName = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**记录删除 */
|
||||
function fnRecordDelete() {
|
||||
const ids = tableState.selectedRowKeys.join(',');
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认删除访问编号为 【${ids}】 的数据项吗?`,
|
||||
onOk() {
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
delLogininfor(ids).then(res => {
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `删除成功`,
|
||||
duration: 3,
|
||||
});
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
fnGetList();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**列表清空 */
|
||||
function fnCleanList() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认清空所有登录日志数据项?`,
|
||||
onOk() {
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
cleanLogininfor().then(res => {
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `清空成功`,
|
||||
duration: 3,
|
||||
});
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
fnGetList();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**登录账号解锁 */
|
||||
function fnUnlock() {
|
||||
const username = tableState.selectedUserName;
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认解锁用户 【${username}】 数据项?`,
|
||||
onOk() {
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
unlockLogininfor(username).then(res => {
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `${username} 解锁成功`,
|
||||
duration: 3,
|
||||
});
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**列表导出 */
|
||||
function fnExportList() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认根据搜索条件导出xlsx表格文件吗?`,
|
||||
onOk() {
|
||||
const hide = message.loading('正在打开...', 0);
|
||||
exportLogininfor(toRaw(queryParams)).then(res => {
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `已完成导出`,
|
||||
duration: 2,
|
||||
});
|
||||
saveAs(res.data, `logininfor_${Date.now()}.xlsx`);
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**查询登录日志列表 */
|
||||
function fnGetList() {
|
||||
if (tableState.loading) return;
|
||||
tableState.loading = true;
|
||||
queryParams.beginTime = queryRangePicker.value[0];
|
||||
queryParams.endTime = queryRangePicker.value[1];
|
||||
listLogininfor(toRaw(queryParams)).then(res => {
|
||||
if (res.code === 200 && Array.isArray(res.rows)) {
|
||||
// 取消勾选
|
||||
if (tableState.selectedRowKeys.length > 0) {
|
||||
tableState.selectedRowKeys = [];
|
||||
}
|
||||
tablePagination.total = res.total;
|
||||
tableState.data = res.rows;
|
||||
}
|
||||
tableState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
Promise.allSettled([getDict('sys_common_status')]).then(resArr => {
|
||||
if (resArr[0].status === 'fulfilled') {
|
||||
dict.sysCommonStatus = resArr[0].value;
|
||||
}
|
||||
});
|
||||
// 获取列表数据
|
||||
fnGetList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :title="title">
|
||||
<template #content>
|
||||
<a-typography-paragraph>
|
||||
对登录进行日志收集,登录锁定的信息存入
|
||||
<a-typography-text code>Redis</a-typography-text>
|
||||
可对登录账号进行解锁。
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
|
||||
<a-card
|
||||
v-show="tableState.seached"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
|
||||
>
|
||||
<!-- 表格搜索栏 -->
|
||||
<a-form :model="queryParams" name="queryParams" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="登录地址" name="ipaddr">
|
||||
<a-input
|
||||
v-model:value="queryParams.ipaddr"
|
||||
allow-clear
|
||||
:maxlength="128"
|
||||
placeholder="请输入登录地址"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="登录账号" name="userName">
|
||||
<a-input
|
||||
v-model:value="queryParams.userName"
|
||||
allow-clear
|
||||
:maxlength="30"
|
||||
placeholder="请输入登录账号"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="登录状态" name="status">
|
||||
<a-select
|
||||
v-model:value="queryParams.status"
|
||||
allow-clear
|
||||
placeholder="请选择登录状态"
|
||||
:options="dict.sysCommonStatus"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="登录时间" name="queryRangePicker">
|
||||
<a-range-picker
|
||||
v-model:value="queryRangePicker"
|
||||
allow-clear
|
||||
bordered
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="['登录开始', '登录结束']"
|
||||
style="width: 100%"
|
||||
></a-range-picker>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item>
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" @click.prevent="fnGetList">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索</a-button
|
||||
>
|
||||
<a-button type="default" @click.prevent="fnQueryReset">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
重置</a-button
|
||||
>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!tableState.selectedUserName"
|
||||
@click.prevent="fnUnlock()"
|
||||
v-perms:has="['monitor:logininfor:unlock']"
|
||||
>
|
||||
<template #icon><UnlockOutlined /></template>
|
||||
解锁
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
danger
|
||||
:disabled="tableState.selectedRowKeys.length <= 0"
|
||||
@click.prevent="fnRecordDelete()"
|
||||
v-perms:has="['monitor:logininfor:remove']"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
danger
|
||||
@click.prevent="fnCleanList()"
|
||||
v-perms:has="['monitor:logininfor:remove']"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
清空
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
@click.prevent="fnExportList()"
|
||||
v-perms:has="['monitor:logininfor:export']"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>搜索栏</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.seached"
|
||||
checked-children="显"
|
||||
un-checked-children="隐"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>表格斑马纹</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.striped"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>刷新</template>
|
||||
<a-button type="text" @click.prevent="fnGetList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>密度</template>
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="text">
|
||||
<template #icon><ColumnHeightOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu
|
||||
:selected-keys="[tableState.size as string]"
|
||||
@click="fnTableSize"
|
||||
>
|
||||
<a-menu-item key="default">默认</a-menu-item>
|
||||
<a-menu-item key="middle">中等</a-menu-item>
|
||||
<a-menu-item key="small">紧凑</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<a-table
|
||||
class="table"
|
||||
row-key="infoId"
|
||||
:columns="tableColumns"
|
||||
:loading="tableState.loading"
|
||||
:data-source="tableState.data"
|
||||
:size="tableState.size"
|
||||
:row-class-name="fnTableStriped"
|
||||
:scroll="{ x: true }"
|
||||
:pagination="tablePagination"
|
||||
:row-selection="{
|
||||
type: 'checkbox',
|
||||
onChange: fnTableSelectedRows,
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<DictTag :options="dict.sysCommonStatus" :value="record.status" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table :deep(.table-striped) td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.table :deep(.ant-pagination) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
338
src/views/monitor/online/index.vue
Normal file
338
src/views/monitor/online/index.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message, Modal } from 'ant-design-vue/lib';
|
||||
import { forceLogout, listOnline } from '@/api/monitor/online';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import { SizeType } from 'ant-design-vue/lib/config-provider';
|
||||
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table';
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
|
||||
/**查询参数 */
|
||||
let queryParams = reactive({
|
||||
/**登录主机 */
|
||||
ipaddr: '',
|
||||
/**登录账号 */
|
||||
userName: '',
|
||||
});
|
||||
|
||||
/**表格状态类型 */
|
||||
type TabeStateType = {
|
||||
/**加载等待 */
|
||||
loading: boolean;
|
||||
/**紧凑型 */
|
||||
size: SizeType;
|
||||
/**斑马纹 */
|
||||
striped: boolean;
|
||||
/**搜索栏 */
|
||||
seached: boolean;
|
||||
/**记录数据 */
|
||||
data: object[];
|
||||
};
|
||||
|
||||
/**表格状态 */
|
||||
let tableState: TabeStateType = reactive({
|
||||
loading: false,
|
||||
size: 'middle',
|
||||
striped: false,
|
||||
seached: false,
|
||||
data: [],
|
||||
});
|
||||
|
||||
/**表格字段列 */
|
||||
let tableColumns: ColumnsType = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'num',
|
||||
width: '50px',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
const idxNum = (tablePagination.current - 1) * tablePagination.pageSize;
|
||||
return idxNum + opt.index + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '会话编号',
|
||||
dataIndex: 'tokenId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录账号',
|
||||
dataIndex: 'userName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
dataIndex: 'deptName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录主机',
|
||||
dataIndex: 'ipaddr',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录地点',
|
||||
dataIndex: 'loginLocation',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
dataIndex: 'os',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
dataIndex: 'browser',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '登录时间',
|
||||
dataIndex: 'loginTime',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
if (+opt.value <= 0) return '';
|
||||
return parseDateToStr(+opt.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'tokenId',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
/**表格分页器参数 */
|
||||
let tablePagination = {
|
||||
/**当前页数 */
|
||||
current: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
/**默认的每页条数 */
|
||||
defaultPageSize: 20,
|
||||
/**指定每页可以显示多少条 */
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
/**只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage: true,
|
||||
/**是否可以快速跳转至某页 */
|
||||
showQuickJumper: true,
|
||||
/**是否可以改变 pageSize */
|
||||
showSizeChanger: true,
|
||||
/**数据总数 */
|
||||
total: 0,
|
||||
showTotal: (total: number) => `总共 ${total} 条`,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
tablePagination.current = page;
|
||||
tablePagination.pageSize = pageSize;
|
||||
},
|
||||
};
|
||||
|
||||
/**表格紧凑型变更操作 */
|
||||
function fnTableSize({ key }: MenuInfo) {
|
||||
tableState.size = key as SizeType;
|
||||
}
|
||||
|
||||
/**表格斑马纹 */
|
||||
function fnTableStriped(_record: unknown, index: number) {
|
||||
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
|
||||
}
|
||||
|
||||
/**查询参数重置 */
|
||||
function fnQueryReset() {
|
||||
queryParams.ipaddr = '';
|
||||
queryParams.userName = '';
|
||||
tablePagination.current = 1;
|
||||
tablePagination.pageSize = 20;
|
||||
fnGetList();
|
||||
}
|
||||
|
||||
/** 查询在线用户列表 */
|
||||
function fnGetList() {
|
||||
if (tableState.loading) return;
|
||||
tableState.loading = true;
|
||||
listOnline(queryParams).then(res => {
|
||||
if (res.code === 200 && Array.isArray(res.rows)) {
|
||||
tableState.data = res.rows;
|
||||
}
|
||||
tableState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** 强退按钮操作 */
|
||||
function fnForceLogout(row: Record<string, string>) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认强退登录账号为 ${row.userName} 的用户?`,
|
||||
onOk() {
|
||||
const hide = message.loading('正在打开...', 0);
|
||||
forceLogout(row.tokenId).finally(() => {
|
||||
hide();
|
||||
message.error({
|
||||
content: `已强退用户 ${row.userName}`,
|
||||
duration: 2,
|
||||
});
|
||||
});
|
||||
fnGetList();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnGetList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :title="title">
|
||||
<template #content>
|
||||
<a-typography-paragraph>
|
||||
登录用户
|
||||
<a-typography-text code>Token</a-typography-text>
|
||||
授权标识记录,存储在
|
||||
<a-typography-text code>Redis</a-typography-text>
|
||||
中,可撤销对用户的授权,拒绝用户请求并强制退出。
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
|
||||
<a-card
|
||||
v-show="tableState.seached"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
|
||||
>
|
||||
<!-- 表格搜索栏 -->
|
||||
<a-form :model="queryParams" name="queryParams" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="登录账号" name="userName">
|
||||
<a-input
|
||||
v-model:value="queryParams.userName"
|
||||
allow-clear
|
||||
:maxlength="30"
|
||||
placeholder="请输入登录账号"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="登录主机" name="ipaddr">
|
||||
<a-input
|
||||
v-model:value="queryParams.ipaddr"
|
||||
allow-clear
|
||||
:maxlength="128"
|
||||
placeholder="请输入登录主机"
|
||||
></a-input> </a-form-item
|
||||
></a-col>
|
||||
<a-col :lg="12" :md="24" :xs="24">
|
||||
<a-form-item>
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" @click.prevent="fnGetList">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button type="default" @click.prevent="fnQueryReset">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
{{ title }}
|
||||
</template>
|
||||
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>搜索栏</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.seached"
|
||||
checked-children="显"
|
||||
un-checked-children="隐"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>表格斑马纹</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.striped"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>刷新</template>
|
||||
<a-button type="text" @click.prevent="fnGetList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>密度</template>
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="text">
|
||||
<template #icon><ColumnHeightOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu
|
||||
:selected-keys="[tableState.size as string]"
|
||||
@click="fnTableSize"
|
||||
>
|
||||
<a-menu-item key="default">默认</a-menu-item>
|
||||
<a-menu-item key="middle">中等</a-menu-item>
|
||||
<a-menu-item key="small">紧凑</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<a-table
|
||||
class="table"
|
||||
row-key="tokenId"
|
||||
:columns="tableColumns"
|
||||
:loading="tableState.loading"
|
||||
:data-source="tableState.data"
|
||||
:size="tableState.size"
|
||||
:row-class-name="fnTableStriped"
|
||||
:pagination="tablePagination"
|
||||
:scroll="{ x: true }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'tokenId'">
|
||||
<a-button
|
||||
type="link"
|
||||
@click.prevent="fnForceLogout(record)"
|
||||
v-perms:has="['monitor:online:forceLogout']"
|
||||
>
|
||||
<template #icon><LogoutOutlined /></template>
|
||||
强退
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table :deep(.table-striped) td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.table :deep(.ant-pagination) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
692
src/views/monitor/operlog/index.vue
Normal file
692
src/views/monitor/operlog/index.vue
Normal file
@@ -0,0 +1,692 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref, onMounted, toRaw } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message, Modal } from 'ant-design-vue/lib';
|
||||
import { SizeType } from 'ant-design-vue/lib/config-provider';
|
||||
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table';
|
||||
import {
|
||||
exportOperlog,
|
||||
listOperlog,
|
||||
delOperlog,
|
||||
cleanOperlog,
|
||||
} from '@/api/monitor/operlog';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
const { getDict } = useDictStore();
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
|
||||
/**字典数据 */
|
||||
let dict: {
|
||||
/**业务类型 */
|
||||
sysBusinessType: DictType[];
|
||||
/**登录状态 */
|
||||
sysCommonStatus: DictType[];
|
||||
} = reactive({
|
||||
sysBusinessType: [],
|
||||
sysCommonStatus: [],
|
||||
});
|
||||
|
||||
/**开始结束时间 */
|
||||
let queryRangePicker = ref<[string, string]>(['', '']);
|
||||
|
||||
/**查询参数 */
|
||||
let queryParams = reactive({
|
||||
/**操作模块 */
|
||||
title: '',
|
||||
/**操作人员 */
|
||||
operName: '',
|
||||
/**业务类型 */
|
||||
businessType: undefined,
|
||||
/**操作状态 */
|
||||
status: undefined,
|
||||
/**开始时间 */
|
||||
beginTime: '',
|
||||
/**结束时间 */
|
||||
endTime: '',
|
||||
/**当前页数 */
|
||||
pageNum: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
/**查询参数重置 */
|
||||
function fnQueryReset() {
|
||||
queryParams = Object.assign(queryParams, {
|
||||
title: '',
|
||||
operName: '',
|
||||
businessType: undefined,
|
||||
status: undefined,
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
queryRangePicker.value = ['', ''];
|
||||
tablePagination.current = 1;
|
||||
tablePagination.pageSize = 20;
|
||||
fnGetList();
|
||||
}
|
||||
|
||||
/**表格状态类型 */
|
||||
type TabeStateType = {
|
||||
/**加载等待 */
|
||||
loading: boolean;
|
||||
/**紧凑型 */
|
||||
size: SizeType;
|
||||
/**斑马纹 */
|
||||
striped: boolean;
|
||||
/**搜索栏 */
|
||||
seached: boolean;
|
||||
/**记录数据 */
|
||||
data: object[];
|
||||
/**勾选记录 */
|
||||
selectedRowKeys: (string | number)[];
|
||||
};
|
||||
|
||||
/**表格状态 */
|
||||
let tableState: TabeStateType = reactive({
|
||||
loading: false,
|
||||
size: 'middle',
|
||||
striped: false,
|
||||
seached: false,
|
||||
data: [],
|
||||
selectedRowKeys: [],
|
||||
});
|
||||
|
||||
/**表格字段列 */
|
||||
let tableColumns: ColumnsType = [
|
||||
{
|
||||
title: '日志编号',
|
||||
dataIndex: 'operId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '模块名称',
|
||||
dataIndex: 'title',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '业务类型',
|
||||
dataIndex: 'businessType',
|
||||
key: 'businessType',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作人员',
|
||||
dataIndex: 'operName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '请求方式',
|
||||
dataIndex: 'requestMethod',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '请求主机',
|
||||
dataIndex: 'operIp',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作日期',
|
||||
dataIndex: 'operTime',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
if (+opt.value <= 0) return '';
|
||||
return parseDateToStr(+opt.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '消耗时间',
|
||||
dataIndex: 'costTime',
|
||||
key: 'costTime',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
return `${opt.value} ms`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'operId',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
/**表格分页器参数 */
|
||||
let tablePagination = reactive({
|
||||
/**当前页数 */
|
||||
current: 1,
|
||||
/**每页条数 */
|
||||
pageSize: 20,
|
||||
/**默认的每页条数 */
|
||||
defaultPageSize: 20,
|
||||
/**指定每页可以显示多少条 */
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
/**只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage: false,
|
||||
/**是否可以快速跳转至某页 */
|
||||
showQuickJumper: true,
|
||||
/**是否可以改变 pageSize */
|
||||
showSizeChanger: true,
|
||||
/**数据总数 */
|
||||
total: 0,
|
||||
showTotal: (total: number) => `总共 ${total} 条`,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
tablePagination.current = page;
|
||||
tablePagination.pageSize = pageSize;
|
||||
queryParams.pageNum = page;
|
||||
queryParams.pageSize = pageSize;
|
||||
fnGetList();
|
||||
},
|
||||
});
|
||||
|
||||
/**表格紧凑型变更操作 */
|
||||
function fnTableSize({ key }: MenuInfo) {
|
||||
tableState.size = key as SizeType;
|
||||
}
|
||||
|
||||
/**表格斑马纹 */
|
||||
function fnTableStriped(_record: unknown, index: number) {
|
||||
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
|
||||
}
|
||||
|
||||
/**表格多选 */
|
||||
function fnTableSelectedRowKeys(keys: (string | number)[]) {
|
||||
tableState.selectedRowKeys = keys;
|
||||
}
|
||||
|
||||
/**对话框对象信息状态类型 */
|
||||
type ModalStateType = {
|
||||
/**详情框是否显示 */
|
||||
visibleByView: boolean;
|
||||
/**标题 */
|
||||
title: string;
|
||||
/**表单数据 */
|
||||
from: Record<string, any>;
|
||||
};
|
||||
|
||||
/**对话框对象信息状态 */
|
||||
let modalState: ModalStateType = reactive({
|
||||
visibleByView: false,
|
||||
title: '操作日志',
|
||||
from: {
|
||||
operId: undefined,
|
||||
businessType: 0,
|
||||
deptName: '',
|
||||
method: '',
|
||||
operIp: '',
|
||||
operLocation: '',
|
||||
operMsg: '',
|
||||
operName: '',
|
||||
operParam: '',
|
||||
operTime: 0,
|
||||
operUrl: '',
|
||||
operatorType: 1,
|
||||
requestMethod: 'PUT',
|
||||
status: 1,
|
||||
title: '',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 对话框弹出显示为 查看
|
||||
* @param row 操作日志信息对象
|
||||
*/
|
||||
function fnModalVisibleByVive(row: Record<string, string>) {
|
||||
modalState.from = Object.assign(modalState.from, row);
|
||||
modalState.title = '操作日志信息';
|
||||
modalState.visibleByView = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框弹出关闭执行函数
|
||||
*/
|
||||
function fnModalCancel() {
|
||||
modalState.visibleByView = false;
|
||||
}
|
||||
|
||||
/**记录删除 */
|
||||
function fnRecordDelete() {
|
||||
const ids = tableState.selectedRowKeys.join(',');
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认删除访问编号为 【${ids}】 的数据项吗?`,
|
||||
onOk() {
|
||||
const key = 'delOperlog';
|
||||
message.loading({ content: '请稍等...', key });
|
||||
delOperlog(ids).then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: '删除成功',
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
fnGetList();
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**列表清空 */
|
||||
function fnCleanList() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认清空所有登录日志数据项?`,
|
||||
onOk() {
|
||||
const key = 'cleanOperlog';
|
||||
message.loading({ content: '请稍等...', key });
|
||||
cleanOperlog().then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: '清空成功',
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
fnGetList();
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**列表导出 */
|
||||
function fnExportList() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认根据搜索条件导出xlsx表格文件吗?`,
|
||||
onOk() {
|
||||
const key = 'exportOperlog';
|
||||
message.loading({ content: '请稍等...', key });
|
||||
exportOperlog(toRaw(queryParams)).then(res => {
|
||||
if (res.code === 200) {
|
||||
message.success({
|
||||
content: `已完成导出`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
saveAs(res.data, `operlog_${Date.now()}.xlsx`);
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
key,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**查询登录日志列表 */
|
||||
function fnGetList() {
|
||||
if (tableState.loading) return;
|
||||
tableState.loading = true;
|
||||
queryParams.beginTime = queryRangePicker.value[0];
|
||||
queryParams.endTime = queryRangePicker.value[1];
|
||||
listOperlog(toRaw(queryParams)).then(res => {
|
||||
if (res.code === 200 && Array.isArray(res.rows)) {
|
||||
// 取消勾选
|
||||
if (tableState.selectedRowKeys.length > 0) {
|
||||
tableState.selectedRowKeys = [];
|
||||
}
|
||||
tablePagination.total = res.total;
|
||||
tableState.data = res.rows;
|
||||
}
|
||||
tableState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
Promise.allSettled([
|
||||
getDict('sys_oper_type'),
|
||||
getDict('sys_common_status'),
|
||||
]).then(resArr => {
|
||||
if (resArr[0].status === 'fulfilled') {
|
||||
dict.sysBusinessType = resArr[0].value;
|
||||
}
|
||||
if (resArr[1].status === 'fulfilled') {
|
||||
dict.sysCommonStatus = resArr[1].value;
|
||||
}
|
||||
});
|
||||
// 获取列表数据
|
||||
fnGetList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :title="title">
|
||||
<template #content>
|
||||
<a-typography-paragraph>
|
||||
对接口请求进行日志收集,统计高频接口分析优化等操作。
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
|
||||
<a-card
|
||||
v-show="tableState.seached"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
|
||||
>
|
||||
<!-- 表格搜索栏 -->
|
||||
<a-form :model="queryParams" name="queryParams" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="操作模块" name="title">
|
||||
<a-input
|
||||
v-model:value="queryParams.title"
|
||||
allow-clear
|
||||
placeholder="请输入操作模块"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="操作人员" name="operName">
|
||||
<a-input
|
||||
v-model:value="queryParams.operName"
|
||||
allow-clear
|
||||
placeholder="请输入操作人员"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="操作类型" name="businessType">
|
||||
<a-select
|
||||
v-model:value="queryParams.businessType"
|
||||
allow-clear
|
||||
placeholder="请选择操作类型"
|
||||
:options="dict.sysBusinessType"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="操作状态" name="status">
|
||||
<a-select
|
||||
v-model:value="queryParams.status"
|
||||
allow-clear
|
||||
placeholder="请选择操作状态"
|
||||
:options="dict.sysCommonStatus"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item label="操作时间" name="queryRangePicker">
|
||||
<a-range-picker
|
||||
v-model:value="queryRangePicker"
|
||||
allow-clear
|
||||
bordered
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="['操作开始', '操作结束']"
|
||||
style="width: 100%"
|
||||
></a-range-picker>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-form-item>
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" @click.prevent="fnGetList">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索</a-button
|
||||
>
|
||||
<a-button type="default" @click.prevent="fnQueryReset">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
重置</a-button
|
||||
>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||||
<!-- 插槽-卡片左侧侧 -->
|
||||
<template #title>
|
||||
<a-space :size="8" align="center">
|
||||
<a-button
|
||||
type="default"
|
||||
danger
|
||||
:disabled="tableState.selectedRowKeys.length <= 0"
|
||||
@click.prevent="fnRecordDelete()"
|
||||
v-perms:has="['monitor:operlog:remove']"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
danger
|
||||
@click.prevent="fnCleanList()"
|
||||
v-perms:has="['monitor:operlog:remove']"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
清空
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
@click.prevent="fnExportList()"
|
||||
v-perms:has="['monitor:operlog:export']"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 插槽-卡片右侧 -->
|
||||
<template #extra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>搜索栏</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.seached"
|
||||
checked-children="显"
|
||||
un-checked-children="隐"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>表格斑马纹</template>
|
||||
<a-switch
|
||||
v-model:checked="tableState.striped"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
size="small"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>刷新</template>
|
||||
<a-button type="text" @click.prevent="fnGetList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>密度</template>
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="text">
|
||||
<template #icon><ColumnHeightOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu
|
||||
:selected-keys="[tableState.size as string]"
|
||||
@click="fnTableSize"
|
||||
>
|
||||
<a-menu-item key="default">默认</a-menu-item>
|
||||
<a-menu-item key="middle">中等</a-menu-item>
|
||||
<a-menu-item key="small">紧凑</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<a-table
|
||||
class="table"
|
||||
row-key="operId"
|
||||
:columns="tableColumns"
|
||||
:loading="tableState.loading"
|
||||
:data-source="tableState.data"
|
||||
:size="tableState.size"
|
||||
:row-class-name="fnTableStriped"
|
||||
:scroll="{ x: true }"
|
||||
:pagination="tablePagination"
|
||||
:row-selection="{
|
||||
type: 'checkbox',
|
||||
onChange: fnTableSelectedRowKeys,
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'businessType'">
|
||||
<DictTag
|
||||
:options="dict.sysBusinessType"
|
||||
:value="record.businessType"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<DictTag :options="dict.sysCommonStatus" :value="record.status" />
|
||||
</template>
|
||||
<template v-if="column.key === 'operId'">
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>查看详情</template>
|
||||
<a-button
|
||||
type="link"
|
||||
@click.prevent="fnModalVisibleByVive(record)"
|
||||
v-perms:has="['monitor:operlog:query']"
|
||||
>
|
||||
<template #icon><ProfileOutlined /></template>
|
||||
详情
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 详情框 -->
|
||||
<a-modal
|
||||
width="800px"
|
||||
:visible="modalState.visibleByView"
|
||||
:title="modalState.title"
|
||||
@cancel="fnModalCancel"
|
||||
>
|
||||
<a-form layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="日志编号" name="operId">
|
||||
{{ modalState.from.operId }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="执行状态" name="status">
|
||||
<a-tag :color="+modalState.from.status ? 'success' : 'error'">
|
||||
{{ ['失败', '正常'][+modalState.from.status] }}
|
||||
</a-tag>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="业务类型" name="businessType">
|
||||
{{ modalState.from.title }} /
|
||||
<DictTag
|
||||
:options="dict.sysBusinessType"
|
||||
:value="modalState.from.businessType"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="操作人员" name="operName">
|
||||
{{ modalState.from.operName }} / {{ modalState.from.operIp }} /
|
||||
{{ modalState.from.operLocation }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="请求地址" name="operUrl">
|
||||
{{ modalState.from.requestMethod }} -
|
||||
{{ modalState.from.operUrl }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="操作时间" name="operTime">
|
||||
<span v-if="+modalState.from.operTime > 0">
|
||||
{{ parseDateToStr(+modalState.from.operTime) }}
|
||||
</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="请求耗时" name="costTime">
|
||||
{{ modalState.from.costTime }} ms
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item label="操作方法" name="method">
|
||||
{{ modalState.from.method }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="请求参数" name="operParam">
|
||||
<a-textarea
|
||||
v-model:value="modalState.from.operParam"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
placeholder="请求参数"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作信息" name="operMsg">
|
||||
<a-textarea
|
||||
v-model:value="modalState.from.operMsg"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
placeholder="操作信息"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table :deep(.table-striped) td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.table :deep(.ant-pagination) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
329
src/views/monitor/server/info.vue
Normal file
329
src/views/monitor/server/info.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table';
|
||||
import { getServer } from '@/api/monitor/server';
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
|
||||
/**加载状态 */
|
||||
let loading = ref<boolean>(true);
|
||||
|
||||
/**磁盘信息表格字段列 */
|
||||
let diskTableColumns: ColumnsType = [
|
||||
{
|
||||
title: '路径盘符',
|
||||
dataIndex: 'target',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '总大小',
|
||||
dataIndex: 'size',
|
||||
align: 'center',
|
||||
},
|
||||
|
||||
{
|
||||
title: '剩余大小',
|
||||
dataIndex: 'avail',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '已使用大小',
|
||||
dataIndex: 'used',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '空间使用率(%)',
|
||||
dataIndex: 'pcent',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
/**数据参数类型 */
|
||||
type ServerType = {
|
||||
/**CPU */
|
||||
cpu: Record<string, string | number>;
|
||||
/**磁盘 */
|
||||
disk: Record<string, string>[];
|
||||
/**内存 */
|
||||
memory: Record<string, string | number>;
|
||||
/**网络 */
|
||||
network: Record<string, string>;
|
||||
/**项目 */
|
||||
project: Record<string, string>;
|
||||
/**系统 */
|
||||
system: Record<string, string | number>;
|
||||
/**时间 */
|
||||
time: Record<string, string | number>;
|
||||
};
|
||||
|
||||
let server: ServerType = reactive({
|
||||
cpu: {},
|
||||
disk: [],
|
||||
memory: {},
|
||||
network: {},
|
||||
project: {},
|
||||
system: {},
|
||||
time: {},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getServer().then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
// CPU信息
|
||||
let cpu = res.data.cpu;
|
||||
cpu.coreUsed = cpu.coreUsed.map((item: string) => item).join(' / ');
|
||||
server.cpu = cpu;
|
||||
// 磁盘信息
|
||||
server.disk = res.data.disk;
|
||||
// 内存信息
|
||||
server.memory = res.data.memory;
|
||||
// 网络信息
|
||||
server.network = res.data.network;
|
||||
// 项目信息
|
||||
server.project = res.data.project;
|
||||
// 系统信息
|
||||
server.system = res.data.system;
|
||||
// 时间信息
|
||||
server.time = res.data.time;
|
||||
// 加载状态
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :title="title" :loading="loading">
|
||||
<template #content>
|
||||
<a-typography-paragraph> 服务器与应用程序的信息 </a-typography-paragraph>
|
||||
</template>
|
||||
|
||||
<a-card
|
||||
title="项目信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:bordered="true"
|
||||
:column="{ lg: 2, md: 2, xs: 1 }"
|
||||
>
|
||||
<a-descriptions-item label="项目名称">
|
||||
{{ server.project.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="项目版本">
|
||||
{{ server.project.version }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="项目环境">
|
||||
{{ server.project.env }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="项目路径">
|
||||
{{ server.project.appDir }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="项目依赖">
|
||||
<a-tag
|
||||
v-for="(value, name) in server.project.dependencies"
|
||||
:key="name"
|
||||
>
|
||||
{{ name }}:{{ value }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card
|
||||
title="系统信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:column="{ lg: 2, md: 2, xs: 1 }"
|
||||
:bordered="true"
|
||||
>
|
||||
<a-descriptions-item
|
||||
label="GO版本"
|
||||
:span="2"
|
||||
v-if="server.system && server.system.go"
|
||||
>
|
||||
{{ server.system.go }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label="Node版本"
|
||||
v-if="server.system && server.system.node"
|
||||
>
|
||||
{{ server.system.node }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label="V8版本"
|
||||
v-if="server.system && server.system.v8"
|
||||
>
|
||||
{{ server.system.v8 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="进程PID号">
|
||||
{{ server.system.processId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行平台">
|
||||
{{ server.system.platform }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="系统架构">
|
||||
{{ server.system.arch }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="系统平台">
|
||||
{{ server.system.uname }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="系统发行版本">
|
||||
{{ server.system.release }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="主机名称">
|
||||
{{ server.system.hostname }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="主机用户目录" :span="2">
|
||||
{{ server.system.homeDir }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="项目路径" :span="2">
|
||||
{{ server.system.cmd }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行命令" :span="2">
|
||||
{{ server.system.execCommand }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card
|
||||
title="CPU信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:column="1"
|
||||
:bordered="true"
|
||||
>
|
||||
<a-descriptions-item label="型号">
|
||||
{{ server.cpu.model }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="速率Hz">
|
||||
{{ server.cpu.speed }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="核心数">
|
||||
{{ server.cpu.core }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用率(%)">
|
||||
{{ server.cpu.coreUsed }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card
|
||||
title="内存信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:column="{ lg: 2, md: 2, xs: 1 }"
|
||||
:bordered="true"
|
||||
>
|
||||
<a-descriptions-item label="总内存">
|
||||
{{ server.memory.totalmem }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="剩余内存">
|
||||
{{ server.memory.freemem }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="使用率(%)">
|
||||
{{ server.memory.usage }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="进程总内存">
|
||||
{{ server.memory.rss }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="堆的总大小">
|
||||
{{ server.memory.heapTotal }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="堆已分配">
|
||||
{{ server.memory.heapUsed }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="链接库占用">
|
||||
{{ server.memory.external }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card
|
||||
title="时间信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:column="{ lg: 2, md: 2, xs: 1 }"
|
||||
:bordered="true"
|
||||
>
|
||||
<a-descriptions-item label="时区">
|
||||
{{ server.time.timezone }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="时间">
|
||||
{{ server.time.current }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="时区名称">
|
||||
{{ server.time.timezoneName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="程序启动时间">
|
||||
{{ server.time.uptime }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card
|
||||
title="网络信息"
|
||||
:bordered="false"
|
||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
||||
>
|
||||
<a-descriptions
|
||||
size="middle"
|
||||
layout="horizontal"
|
||||
:label-style="{ width: '140px' }"
|
||||
:column="1"
|
||||
:bordered="true"
|
||||
>
|
||||
<a-descriptions-item
|
||||
:label="name"
|
||||
v-for="(value, name) in server.network"
|
||||
:key="name"
|
||||
>
|
||||
{{ value }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card title="磁盘信息" :bordered="false" :body-style="{ padding: 0 }">
|
||||
<a-table
|
||||
class="disk"
|
||||
row-key="target"
|
||||
size="middle"
|
||||
:columns="diskTableColumns"
|
||||
:data-source="server.disk"
|
||||
:pagination="false"
|
||||
:scroll="{ x: true }"
|
||||
>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
11
src/views/redirect/index.vue
Normal file
11
src/views/redirect/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const { params, query } = router.currentRoute.value;
|
||||
|
||||
router.replace({ path: `/${params.path}`, query });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>稍等...</span>
|
||||
</template>
|
||||
313
src/views/register.vue
Normal file
313
src/views/register.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<script lang="ts" setup>
|
||||
import { Modal, message } from 'ant-design-vue/lib';
|
||||
import { reactive, onMounted, toRaw } from 'vue';
|
||||
import { getCaptchaImage, register } from '@/api/login';
|
||||
import { regExpPasswd, regExpUserName } from '@/utils/regular-utils';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const codeImgFall =
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
|
||||
let state = reactive({
|
||||
/**表单属性 */
|
||||
form: {
|
||||
/**账号 */
|
||||
username: '',
|
||||
/**密码 */
|
||||
password: '',
|
||||
/**确认密码 */
|
||||
confirmPassword: '',
|
||||
/**验证码 */
|
||||
code: '',
|
||||
/**验证码uuid */
|
||||
uuid: '',
|
||||
},
|
||||
/**表单提交点击状态 */
|
||||
formClick: false,
|
||||
/**验证码状态 */
|
||||
captcha: {
|
||||
/**验证码开关 */
|
||||
enabled: false,
|
||||
/**验证码图片地址 */
|
||||
codeImg: '',
|
||||
codeImgFall: codeImgFall,
|
||||
},
|
||||
/**验证码点击状态 */
|
||||
captchaClick: false,
|
||||
});
|
||||
|
||||
/**表单验证确认密码是否一致 */
|
||||
function fnEqualToPassword(
|
||||
rule: Record<string, any>,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (!value) {
|
||||
return Promise.reject(t('views.register.passwordErr'));
|
||||
}
|
||||
if (state.form.password === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t('views.register.passwordConfirmErr'));
|
||||
}
|
||||
|
||||
/**表单验证通过 */
|
||||
function fnFinish() {
|
||||
state.formClick = true;
|
||||
// 发送请求
|
||||
const hide = message.loading(t('common.loading'), 0);
|
||||
register(toRaw(state.form))
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
Modal.success({
|
||||
title: t('common.tipTitle'),
|
||||
content: t('views.register.tipContent', {
|
||||
username: state.form.username,
|
||||
}),
|
||||
okText: t('views.register.tipBtn'),
|
||||
onOk() {
|
||||
router.push({ name: 'Login' });
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hide();
|
||||
state.formClick = false;
|
||||
// 刷新验证码
|
||||
if (state.captcha.enabled) {
|
||||
state.form.code = '';
|
||||
fnGetCaptcha();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
function fnGetCaptcha() {
|
||||
if (state.captchaClick) return;
|
||||
state.captchaClick = true;
|
||||
getCaptchaImage().then(res => {
|
||||
state.captchaClick = false;
|
||||
if (res.code != 200) {
|
||||
message.warning(`${res.msg}`, 3);
|
||||
return;
|
||||
}
|
||||
state.captcha.enabled = Boolean(res.captchaEnabled);
|
||||
if (state.captcha.enabled) {
|
||||
state.captcha.codeImg = res.img;
|
||||
state.form.uuid = res.uuid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnGetCaptcha();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="top">
|
||||
<div class="header">
|
||||
<a href="/" target="_self"
|
||||
><img src="@/assets/logo.png" class="logo" alt="logo" />
|
||||
<span class="title">{{ t('common.title') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="desc">{{ t('common.desc') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<a-form :model="state.form" name="tabForm" @finish="fnFinish">
|
||||
<a-form-item
|
||||
name="username"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpUserName,
|
||||
message: t('valid.userNameReg'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="state.form.username"
|
||||
size="large"
|
||||
:placeholder="t('valid.userNameHit')"
|
||||
:maxlength="30"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
name="password"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpPasswd,
|
||||
message: t('valid.passwordReg'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.password"
|
||||
size="large"
|
||||
:placeholder="t('valid.passwordHit')"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
name="confirmPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 6,
|
||||
max: 26,
|
||||
validator: fnEqualToPassword,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.confirmPassword"
|
||||
size="large"
|
||||
:placeholder="t('valid.passwordConfirmHit')"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="8" v-if="state.captcha.enabled">
|
||||
<a-col :span="16">
|
||||
<a-form-item
|
||||
name="code"
|
||||
:rules="[
|
||||
{ required: true, min: 1, message: t('valid.codePlease') },
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="state.form.code"
|
||||
size="large"
|
||||
:placeholder="t('valid.codeHit')"
|
||||
:maxlength="6"
|
||||
>
|
||||
<template #prefix>
|
||||
<RobotOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-image
|
||||
:alt="t('valid.codeHit')"
|
||||
style="cursor: pointer; border-radius: 2px"
|
||||
width="120px"
|
||||
height="40px"
|
||||
:preview="false"
|
||||
:src="state.captcha.codeImg"
|
||||
:fallback="state.captcha.codeImgFall"
|
||||
@click="fnGetCaptcha"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-button
|
||||
block
|
||||
type="primary"
|
||||
size="large"
|
||||
html-type="submit"
|
||||
:loading="state.formClick"
|
||||
>
|
||||
{{ t('views.register.registerBtn') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
type="default"
|
||||
size="large"
|
||||
style="margin-top: 16px"
|
||||
@click="() => router.push({ name: 'Login' })"
|
||||
>
|
||||
{{ t('views.register.loginBtn') }}
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: 110px 0 144px;
|
||||
background-image: url(../assets/background.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 110px;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.top {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
.logo {
|
||||
height: 44px;
|
||||
margin-right: 16px;
|
||||
vertical-align: top;
|
||||
border-style: none;
|
||||
border-radius: 6.66px;
|
||||
}
|
||||
.title {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 600;
|
||||
font-size: 33px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 40px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 368px;
|
||||
min-width: 260px;
|
||||
margin: 0 auto;
|
||||
|
||||
.prefix-icon {
|
||||
color: #8c8c8c;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user