chore: 脚手架
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
# 端口号
|
||||
VITE_PORT=5555
|
||||
|
||||
# base路径
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||
VITE_NITRO_MOCK=true
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 后台请求路径 具体在vite.config.mts配置代理
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
|
||||
VITE_GLOB_ENABLE_ENCRYPT=true
|
||||
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
|
||||
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
|
||||
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
|
||||
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
# 客户端id
|
||||
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
|
||||
# 开启WEBSOCKET
|
||||
VITE_GLOB_WEBSOCKET_ENABLE=false
|
||||
|
@@ -1,8 +1,5 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||
|
||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||
VITE_COMPRESS=none
|
||||
|
||||
@@ -10,7 +7,22 @@ VITE_COMPRESS=none
|
||||
VITE_PWA=false
|
||||
|
||||
# vue-router 的模式
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
VITE_ROUTER_HISTORY=history
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 后台请求路径 具体在vite.config.mts配置代理
|
||||
VITE_GLOB_API_URL=/prod-api
|
||||
|
||||
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
|
||||
VITE_GLOB_ENABLE_ENCRYPT=true
|
||||
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
|
||||
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
|
||||
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
|
||||
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
# 客户端id
|
||||
VITE_GLOB_APP_CLIENT_ID=6afcaa29272b14c1c87264950c726ef4
|
||||
|
||||
# 开启WEBSOCKET
|
||||
VITE_GLOB_WEBSOCKET_ENABLE=false
|
||||
|
9
apps/web-antd/.vscode/settings.json
vendored
Normal file
9
apps/web-antd/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
}
|
||||
}
|
@@ -14,19 +14,6 @@
|
||||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||
<title><%= VITE_APP_TITLE %></title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script>
|
||||
// 生产环境下注入百度统计
|
||||
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement('script');
|
||||
hm.src =
|
||||
'https://hm.baidu.com/hm.js?d20a01273820422b6aa2ee41b6c9414d';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@@ -42,9 +42,17 @@
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"ant-design-vue": "^4.2.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.12",
|
||||
"echarts": "^5.5.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "2.2.0",
|
||||
"vue": "^3.4.35",
|
||||
"vue-router": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash-es": "^4.17.12"
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,25 @@
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
code?: string;
|
||||
grantType: string;
|
||||
password: string;
|
||||
tenantId: string;
|
||||
username: string;
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
refreshToken: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
access_token: string;
|
||||
client_id: string;
|
||||
expire_in: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +27,46 @@ export namespace AuthApi {
|
||||
* 登录
|
||||
*/
|
||||
export async function login(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||
return requestClient.post<AuthApi.LoginResult>(
|
||||
'/auth/login',
|
||||
{ ...data, clientId },
|
||||
{
|
||||
encrypt: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
* 用户登出
|
||||
* @returns void
|
||||
*/
|
||||
export async function getAccessCodes() {
|
||||
return requestClient.get<string[]>('/auth/codes');
|
||||
export function doLogout() {
|
||||
return requestClient.post<void>('/auth/logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param companyName 租户/公司名称
|
||||
* @param domain 绑定域名(不带http(s)://) 可选
|
||||
* @param tenantId 租户id
|
||||
*/
|
||||
export interface TenantOption {
|
||||
companyName: string;
|
||||
domain?: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tenantEnabled 是否启用租户
|
||||
* @param voList 租户列表
|
||||
*/
|
||||
export interface TenantResp {
|
||||
tenantEnabled: boolean;
|
||||
voList: TenantOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户列表 下拉框使用
|
||||
*/
|
||||
export function tenantList() {
|
||||
return requestClient.get<TenantResp>('/auth/tenant/list');
|
||||
}
|
||||
|
42
apps/web-antd/src/api/core/captcha.ts
Normal file
42
apps/web-antd/src/api/core/captcha.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param phonenumber 手机号
|
||||
* @returns void
|
||||
*/
|
||||
export function sendSmsCode(phonenumber: string) {
|
||||
return requestClient.get<void>('/resource/sms/code', {
|
||||
params: { phonenumber },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件验证码
|
||||
* @param email 邮箱
|
||||
* @returns void
|
||||
*/
|
||||
export function sendEmailCode(email: string) {
|
||||
return requestClient.get<void>('/resource/email/code', {
|
||||
params: { email },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param img 图片验证码 需要和base64拼接
|
||||
* @param captchaEnabled 是否开启
|
||||
* @param uuid 验证码ID
|
||||
*/
|
||||
export interface CaptchaResponse {
|
||||
captchaEnabled: boolean;
|
||||
img: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片验证码
|
||||
* @returns resp
|
||||
*/
|
||||
export function captchaImage() {
|
||||
return requestClient.get<CaptchaResponse>('/auth/code');
|
||||
}
|
@@ -1,10 +1,45 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* @description: 菜单meta
|
||||
* @param title 菜单名
|
||||
* @param icon 菜单图标
|
||||
* @param noCache 是否不缓存
|
||||
* @param link 外链链接
|
||||
*/
|
||||
export interface MenuMeta {
|
||||
icon: string;
|
||||
link?: string;
|
||||
noCache: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 菜单
|
||||
* @param name 菜单名
|
||||
* @param path 菜单路径
|
||||
* @param hidden 是否隐藏
|
||||
* @param component 组件名称 Laout
|
||||
* @param alwaysShow 总是显示
|
||||
* @param query 路由参数(json形式)
|
||||
* @param meta 路由信息
|
||||
* @param children 子路由信息
|
||||
*/
|
||||
export interface Menu {
|
||||
alwaysShow?: boolean;
|
||||
children: Menu[];
|
||||
component: string;
|
||||
hidden: boolean;
|
||||
meta: MenuMeta;
|
||||
name: string;
|
||||
path: string;
|
||||
query?: string;
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenus() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
return requestClient.get<Menu[]>('/system/menu/getRouters');
|
||||
}
|
||||
|
@@ -1,10 +1,45 @@
|
||||
import type { UserInfo } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export interface Role {
|
||||
dataScope: string;
|
||||
flag: boolean;
|
||||
roleId: number;
|
||||
roleKey: string;
|
||||
roleName: string;
|
||||
roleSort: number;
|
||||
status: string;
|
||||
superAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
avatar: string;
|
||||
createTime: string;
|
||||
deptId: number;
|
||||
deptName: string;
|
||||
email: string;
|
||||
loginDate: string;
|
||||
loginIp: string;
|
||||
nickName: string;
|
||||
phonenumber: string;
|
||||
remark: string;
|
||||
roles: Role[];
|
||||
sex: string;
|
||||
status: string;
|
||||
tenantId: string;
|
||||
userId: number;
|
||||
userName: string;
|
||||
userType: string;
|
||||
}
|
||||
|
||||
export interface UserInfoResp {
|
||||
permissions: string[];
|
||||
roles: string[];
|
||||
user: User;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfo() {
|
||||
return requestClient.get<UserInfo>('/user/info');
|
||||
return requestClient.get<UserInfoResp>('/system/user/getInfo');
|
||||
}
|
||||
|
69
apps/web-antd/src/api/helper.ts
Normal file
69
apps/web-antd/src/api/helper.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { isObject, isString } from '@vben/utils';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
export function joinTimestamp<T extends boolean>(
|
||||
join: boolean,
|
||||
restful: T,
|
||||
): T extends true ? string : object;
|
||||
|
||||
export function joinTimestamp(join: boolean, restful = false): object | string {
|
||||
if (!join) {
|
||||
return restful ? '' : {};
|
||||
}
|
||||
const now = Date.now();
|
||||
if (restful) {
|
||||
return `?_t=${now}`;
|
||||
}
|
||||
return { _t: now };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Format request parameter time
|
||||
*/
|
||||
export function formatRequestDate(params: Record<string, any>) {
|
||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in params) {
|
||||
const format = params[key]?.format ?? null;
|
||||
if (format && typeof format === 'function') {
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT);
|
||||
}
|
||||
if (isString(key)) {
|
||||
const value = params[key];
|
||||
if (value) {
|
||||
try {
|
||||
params[key] = isString(value) ? value.trim() : value;
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isObject(params[key])) {
|
||||
formatRequestDate(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the object as a parameter to the URL
|
||||
* @param baseUrl url
|
||||
* @param obj
|
||||
* @returns {string}
|
||||
* eg:
|
||||
* let obj = {a: '3', b: '4'}
|
||||
* setObjToUrlParams('www.baidu.com', obj)
|
||||
* ==>www.baidu.com?a=3&b=4
|
||||
*/
|
||||
export function setObjToUrlParams(baseUrl: string, obj: any): string {
|
||||
let parameters = '';
|
||||
for (const key in obj) {
|
||||
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
|
||||
}
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
return /\?$/.test(baseUrl)
|
||||
? baseUrl + parameters
|
||||
: baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
24
apps/web-antd/src/api/monitor/cache/index.ts
vendored
Normal file
24
apps/web-antd/src/api/monitor/cache/index.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export interface CommandStats {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface RedisInfo {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface CacheInfo {
|
||||
commandStats: CommandStats[];
|
||||
dbSize: number;
|
||||
info: RedisInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns redis信息
|
||||
*/
|
||||
export function redisCacheInfo() {
|
||||
return requestClient.get<CacheInfo>('/monitor/cache');
|
||||
}
|
@@ -4,19 +4,51 @@
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { RequestClient } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { isString } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
import {
|
||||
decryptBase64,
|
||||
decryptWithAes,
|
||||
encryptBase64,
|
||||
encryptWithAes,
|
||||
generateAesKey,
|
||||
} from '#/utils/encryption/crypto';
|
||||
import * as encryptUtil from '#/utils/encryption/jsencrypt';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
import { formatRequestDate, joinTimestamp, setObjToUrlParams } from './helper';
|
||||
|
||||
const { apiURL, clientId, enableEncrypt } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
);
|
||||
|
||||
/** 控制是否弹窗 防止登录超时请求多个api会弹窗多次 */
|
||||
let showTimeoutToast = true;
|
||||
|
||||
function createRequestClient(baseURL: string) {
|
||||
const client = new RequestClient({
|
||||
// 后端地址
|
||||
baseURL,
|
||||
// 消息提示类型
|
||||
errorMessageMode: 'message',
|
||||
// 格式化提交参数时间
|
||||
formatDate: true,
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
isReturnNativeResponse: false,
|
||||
// 需要对返回数据进行处理
|
||||
isTransformResponse: true,
|
||||
// post请求的时候添加参数到url
|
||||
joinParamsToUrl: false,
|
||||
// 是否加入时间戳
|
||||
joinTime: false,
|
||||
// 为每个请求携带 Authorization
|
||||
makeAuthorization: () => {
|
||||
return {
|
||||
@@ -43,23 +75,192 @@ function createRequestClient(baseURL: string) {
|
||||
},
|
||||
};
|
||||
},
|
||||
/**
|
||||
* http状态码不为200会走到这里
|
||||
* 其他会走到addResponseInterceptor
|
||||
* @param msg
|
||||
* @returns void
|
||||
*/
|
||||
makeErrorMessage: (msg) => message.error(msg),
|
||||
|
||||
makeRequestHeaders: () => {
|
||||
/**
|
||||
* locale跟后台不一致 需要转换
|
||||
*/
|
||||
const language = preferences.app.locale.replace('-', '_');
|
||||
return {
|
||||
// 为每个请求携带 Accept-Language
|
||||
'Accept-Language': preferences.app.locale,
|
||||
'Accept-Language': language,
|
||||
clientId,
|
||||
};
|
||||
},
|
||||
});
|
||||
client.addResponseInterceptor<HttpResponse>((response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
const { code, data, message: msg } = responseData;
|
||||
if (status >= 200 && status < 400 && code === 0) {
|
||||
return data;
|
||||
client.addRequestInterceptor((config) => {
|
||||
const { encrypt, formatDate, joinParamsToUrl, joinTime = true } = config;
|
||||
const params = config.params || {};
|
||||
const data = config.data || false;
|
||||
formatDate && data && !isString(data) && formatRequestDate(data);
|
||||
if (config.method?.toUpperCase() === 'GET') {
|
||||
if (isString(params)) {
|
||||
// 兼容restful风格
|
||||
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`;
|
||||
config.params = undefined;
|
||||
} else {
|
||||
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||
config.params = Object.assign(
|
||||
params || {},
|
||||
joinTimestamp(joinTime, false),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isString(params)) {
|
||||
// 兼容restful风格
|
||||
config.url = config.url + params;
|
||||
config.params = undefined;
|
||||
} else {
|
||||
formatDate && formatRequestDate(params);
|
||||
if (
|
||||
Reflect.has(config, 'data') &&
|
||||
config.data &&
|
||||
(Object.keys(config.data).length > 0 ||
|
||||
config.data instanceof FormData)
|
||||
) {
|
||||
config.data = data;
|
||||
config.params = params;
|
||||
} else {
|
||||
// 非GET请求如果没有提供data,则将params视为data
|
||||
config.data = params;
|
||||
config.params = undefined;
|
||||
}
|
||||
if (joinParamsToUrl) {
|
||||
config.url = setObjToUrlParams(
|
||||
config.url as string,
|
||||
Object.assign({}, config.params, config.data),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
console.log('请求参数', config);
|
||||
// 全局开启 && 该请求开启 && 是post/put请求
|
||||
if (
|
||||
enableEncrypt &&
|
||||
encrypt &&
|
||||
['POST', 'PUT'].includes(config.method?.toUpperCase() || '')
|
||||
) {
|
||||
const aesKey = generateAesKey();
|
||||
config.headers['encrypt-key'] = encryptUtil.encrypt(
|
||||
encryptBase64(aesKey),
|
||||
);
|
||||
|
||||
config.data =
|
||||
typeof config.data === 'object'
|
||||
? encryptWithAes(JSON.stringify(config.data), aesKey)
|
||||
: encryptWithAes(config.data, aesKey);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
client.addResponseInterceptor<HttpResponse>((response) => {
|
||||
const encryptKey = (response.headers || {})['encrypt-key'];
|
||||
if (encryptKey) {
|
||||
/** RSA私钥解密 拿到解密秘钥的base64 */
|
||||
const base64Str = encryptUtil.decrypt(encryptKey);
|
||||
/** base64 解码 得到请求头的 AES 秘钥 */
|
||||
const aesSecret = decryptBase64(base64Str.toString());
|
||||
/** 使用aesKey解密 responseData */
|
||||
const decryptData = decryptWithAes(
|
||||
response.data as unknown as string,
|
||||
aesSecret,
|
||||
);
|
||||
/** 赋值 需要转为对象 */
|
||||
response.data = JSON.parse(decryptData);
|
||||
}
|
||||
|
||||
const { isReturnNativeResponse, isTransformResponse } = response.config;
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
if (isReturnNativeResponse) {
|
||||
return response;
|
||||
}
|
||||
// 不进行任何处理,直接返回
|
||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||
if (!isTransformResponse) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
const axiosResponseData = response.data;
|
||||
if (!axiosResponseData) {
|
||||
throw new Error($t('fallback.http.apiRequestFailed'));
|
||||
}
|
||||
|
||||
// ruoyi-plus没有采用严格的{code, msg, data}模式
|
||||
const { code, data, msg, ...other } = axiosResponseData;
|
||||
|
||||
// 这里逻辑可以根据项目进行修改
|
||||
const hasSuccess = Reflect.has(axiosResponseData, 'code') && code === 200;
|
||||
if (hasSuccess) {
|
||||
let successMsg = msg;
|
||||
|
||||
if (isNull(successMsg) || isEmpty(successMsg)) {
|
||||
successMsg = $t(`fallback.http.operationSuccess`);
|
||||
}
|
||||
|
||||
if (response.config.successMessageMode === 'modal') {
|
||||
Modal.success({
|
||||
content: successMsg,
|
||||
title: $t('fallback.http.successTip'),
|
||||
});
|
||||
} else if (response.config.successMessageMode === 'message') {
|
||||
message.success(successMsg);
|
||||
}
|
||||
// ruoyi-plus没有采用严格的{code, msg, data}模式
|
||||
// 如果有data 直接返回data 没有data将剩余参数(...other)封装为data返回
|
||||
// 需要考虑data为null的情况(比如查询为空)
|
||||
if (data !== undefined) {
|
||||
return data;
|
||||
}
|
||||
return other;
|
||||
}
|
||||
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
|
||||
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
||||
let timeoutMsg = '';
|
||||
switch (code) {
|
||||
case 401: {
|
||||
const _msg = '登录超时, 请重新登录';
|
||||
const userStore = useAuthStore();
|
||||
userStore.logout().then(() => {
|
||||
/** 只弹窗一次 */
|
||||
if (showTimeoutToast) {
|
||||
showTimeoutToast = false;
|
||||
message.error(_msg);
|
||||
/** 定时器 3s后再开启弹窗 */
|
||||
setTimeout(() => {
|
||||
showTimeoutToast = true;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
// 不再执行下面逻辑
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
if (msg) {
|
||||
timeoutMsg = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorMessageMode='modal'的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
||||
if (response.config.errorMessageMode === 'modal') {
|
||||
Modal.error({
|
||||
content: timeoutMsg,
|
||||
title: $t('fallback.http.errorTip'),
|
||||
});
|
||||
} else if (response.config.errorMessageMode === 'message') {
|
||||
message.error(timeoutMsg);
|
||||
}
|
||||
|
||||
throw new Error(timeoutMsg || $t('fallback.http.apiRequestFailed'));
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
@@ -14,55 +11,18 @@ import {
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
resetAllStores,
|
||||
storeToRefs,
|
||||
useAccessStore,
|
||||
useUserStore,
|
||||
} from '@vben/stores';
|
||||
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { resetRoutes } from '#/router';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
date: '3小时前',
|
||||
isRead: true,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '收到了 14 份新周报',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '刚刚',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '朱偏右 回复了你',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '2024-01-01',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '曲丽丽 评论了你',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '代办提醒',
|
||||
},
|
||||
]);
|
||||
import { useAuthStore, useNotifyStore } from '#/store';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const showDot = computed(() =>
|
||||
notifications.value.some((item) => !item.isRead),
|
||||
);
|
||||
|
||||
const menus = computed(() => [
|
||||
{
|
||||
@@ -100,20 +60,18 @@ const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function handleLogout() {
|
||||
resetAllStores();
|
||||
// resetAllStores();
|
||||
resetRoutes();
|
||||
await router.replace(LOGIN_PATH);
|
||||
// await router.replace(LOGIN_PATH);
|
||||
authStore.logout();
|
||||
}
|
||||
|
||||
function handleNoticeClear() {
|
||||
notifications.value = [];
|
||||
}
|
||||
const notifyStore = useNotifyStore();
|
||||
onMounted(() => notifyStore.startListeningMessage());
|
||||
|
||||
function handleMakeAll() {
|
||||
notifications.value.forEach((item) => (item.isRead = true));
|
||||
function handleViewAll() {
|
||||
message.warning('暂未开放');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -131,10 +89,12 @@ function handleMakeAll() {
|
||||
</template>
|
||||
<template #notification>
|
||||
<Notification
|
||||
:dot="showDot"
|
||||
:notifications="notifications"
|
||||
@clear="handleNoticeClear"
|
||||
@make-all="handleMakeAll"
|
||||
:dot="notifyStore.showDot"
|
||||
:notifications="notifyStore.notificationList"
|
||||
@clear="notifyStore.clearAllMessage"
|
||||
@make-all="notifyStore.setAllRead"
|
||||
@read="notifyStore.setRead"
|
||||
@view-all="handleViewAll"
|
||||
/>
|
||||
</template>
|
||||
<template #extra>
|
||||
|
@@ -24,10 +24,10 @@ const localesMap = loadLocalesMap(modules);
|
||||
*/
|
||||
async function loadMessages(lang: SupportedLanguagesType) {
|
||||
const [appLocaleMessages] = await Promise.all([
|
||||
localesMap[lang]?.(),
|
||||
localesMap[lang](),
|
||||
loadThirdPartyMessage(lang),
|
||||
]);
|
||||
return appLocaleMessages?.default;
|
||||
return appLocaleMessages.default;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -7,6 +7,18 @@ import { defineOverridesPreferences } from '@vben/preferences';
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
/**
|
||||
* 不要动这里 后端路由模式
|
||||
*/
|
||||
accessMode: 'backend',
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
tabbar: {
|
||||
/**
|
||||
* 标签tab 持久化 关闭
|
||||
*/
|
||||
persist: false,
|
||||
styleType: 'card',
|
||||
},
|
||||
theme: {},
|
||||
});
|
||||
|
@@ -1,18 +1,88 @@
|
||||
import type {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
RouteRecordStringComponent,
|
||||
} from '@vben/types';
|
||||
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { getAllMenus } from '#/api';
|
||||
import { getAllMenus, type Menu } from '#/api';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
const NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');
|
||||
|
||||
/**
|
||||
* 这里放本地路由
|
||||
*/
|
||||
const localMenuList: RouteRecordStringComponent[] = [
|
||||
{
|
||||
component: 'BasicLayout',
|
||||
meta: {
|
||||
order: -1,
|
||||
title: 'page.dashboard.title',
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
redirect: '/analytics',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
path: '/analytics',
|
||||
component: '/dashboard/analytics/index',
|
||||
meta: {
|
||||
affixTab: true,
|
||||
title: 'page.dashboard.analytics',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
path: '/workspace',
|
||||
component: '/dashboard/workspace/index',
|
||||
meta: {
|
||||
title: 'page.dashboard.workspace',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
component: 'IFrameView',
|
||||
meta: {
|
||||
icon: 'lucide:book-open-text',
|
||||
iframeSrc: 'https://dapdap.top',
|
||||
keepAlive: true,
|
||||
title: $t('page.vben.document'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'BasicLayout',
|
||||
meta: {
|
||||
hideChildrenInMenu: true,
|
||||
icon: 'lucide:copyright',
|
||||
order: 9999,
|
||||
title: $t('page.vben.about'),
|
||||
},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
children: [
|
||||
{
|
||||
component: '/_core/vben/about/index',
|
||||
meta: {
|
||||
title: $t('page.vben.about'),
|
||||
},
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
@@ -20,16 +90,164 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
NotFoundComponent,
|
||||
};
|
||||
|
||||
/**
|
||||
* 后台路由转vben路由
|
||||
*
|
||||
* todo 需要重构
|
||||
* @param menuList 后台菜单
|
||||
* @param parentPath 上级目录
|
||||
* @returns vben路由
|
||||
*/
|
||||
function backMenuToVbenMenu(
|
||||
menuList: Menu[],
|
||||
parentPath = '',
|
||||
): RouteRecordStringComponent[] {
|
||||
const resultList: RouteRecordStringComponent[] = [];
|
||||
menuList.forEach((menu) => {
|
||||
// 根目录为菜单形式
|
||||
// 固定有一个children children为当前菜单
|
||||
if (menu.path === '/' && menu.children && menu.children.length === 1) {
|
||||
menu.meta = menu.children[0].meta;
|
||||
/**
|
||||
* todo 先写死 后续再优化
|
||||
*/
|
||||
menu.path = '/root_menu';
|
||||
menu.component = 'RootMenu';
|
||||
}
|
||||
// 外链: http开头 & 组件为Layout || ParentView
|
||||
// 正则判断是否为http://或者https://开头
|
||||
if (
|
||||
/^http(s)?:\/\//.test(menu.path) &&
|
||||
(menu.component === 'Layout' || menu.component === 'ParentView')
|
||||
) {
|
||||
menu.component = 'Link';
|
||||
}
|
||||
// 内嵌iframe 组件为InnerLink
|
||||
if (menu.meta?.link && menu.component === 'InnerLink') {
|
||||
menu.component = 'IFrameView';
|
||||
}
|
||||
|
||||
// path
|
||||
if (parentPath) {
|
||||
menu.path = `${parentPath}/${menu.path}`;
|
||||
}
|
||||
|
||||
const vbenRoute: RouteRecordStringComponent = {
|
||||
component: menu.component,
|
||||
meta: {
|
||||
// 当前路由不在菜单显示 但是可以通过链接访问
|
||||
// 不可访问的路由由后端控制隐藏(不返回对应路由)
|
||||
hideMenu: menu.hidden,
|
||||
icon: menu.meta?.icon,
|
||||
keepAlive: !menu.meta?.noCache,
|
||||
title: menu.meta?.title,
|
||||
},
|
||||
name: menu.name,
|
||||
path: menu.path,
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理不同组件
|
||||
*/
|
||||
switch (menu.component) {
|
||||
case 'Layout': {
|
||||
vbenRoute.component = 'BasicLayout';
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* iframe内嵌
|
||||
*/
|
||||
case 'IFrameView': {
|
||||
vbenRoute.component = 'IFrameView';
|
||||
if (vbenRoute.meta) {
|
||||
vbenRoute.meta.iframeSrc = menu.meta.link;
|
||||
}
|
||||
/**
|
||||
* 需要判断特殊情况 比如vue的hash是带#的
|
||||
* 比如链接 aaa.com/#/bbb path会转换为 aaa/com/#/bbb
|
||||
* 比如链接 aaa.com/?bbb=xxx
|
||||
* 需要去除# 否则无法被添加到路由
|
||||
*/
|
||||
/**
|
||||
* todo 不优雅 考虑别的方案
|
||||
*/
|
||||
if (vbenRoute.path.includes('/#/')) {
|
||||
vbenRoute.path = vbenRoute.path.replace('/#/', '');
|
||||
}
|
||||
if (vbenRoute.path.includes('#')) {
|
||||
vbenRoute.path = vbenRoute.path.replace('#', '');
|
||||
}
|
||||
if (vbenRoute.path.includes('?') || vbenRoute.path.includes('&')) {
|
||||
vbenRoute.path = vbenRoute.path.replace('?', '');
|
||||
vbenRoute.path = vbenRoute.path.replace('&', '');
|
||||
}
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* 外链 新窗口打开
|
||||
*/
|
||||
case 'Link': {
|
||||
if (vbenRoute.meta) {
|
||||
vbenRoute.meta.link = menu.meta.link;
|
||||
}
|
||||
vbenRoute.component = 'BasicLayout';
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* 根目录菜单
|
||||
*/
|
||||
case 'RootMenu': {
|
||||
if (vbenRoute.meta) {
|
||||
vbenRoute.meta.hideChildrenInMenu = true;
|
||||
}
|
||||
vbenRoute.component = 'BasicLayout';
|
||||
console.log('RootMenu', vbenRoute);
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* 不能为layout 会套两层BasicLayout
|
||||
*/
|
||||
case 'ParentView': {
|
||||
vbenRoute.component = '';
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* 其他自定义组件 如system/user/index 拼接/
|
||||
*/
|
||||
default: {
|
||||
vbenRoute.component = `/${menu.component}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// children处理
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
vbenRoute.children = backMenuToVbenMenu(menu.children, menu.path);
|
||||
}
|
||||
|
||||
resultList.push(vbenRoute);
|
||||
});
|
||||
return resultList;
|
||||
}
|
||||
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
message.loading({
|
||||
content: `${$t('common.loadingMenu')}...`,
|
||||
duration: 1.5,
|
||||
duration: 1,
|
||||
});
|
||||
return await getAllMenus();
|
||||
// 后台返回路由/菜单
|
||||
const backMenuList = await getAllMenus();
|
||||
// 转换为vben能用的路由
|
||||
const vbenMenuList = backMenuToVbenMenu(backMenuList);
|
||||
// 特别注意 这里要深拷贝
|
||||
const menuList = [...cloneDeep(localMenuList), ...vbenMenuList];
|
||||
console.log('menuList', menuList);
|
||||
return menuList;
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent,
|
||||
|
@@ -62,20 +62,14 @@ function setupAccessGuard(router: Router) {
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// accessToken 检查
|
||||
if (!accessStore.accessToken) {
|
||||
// 明确声明忽略权限访问权限,则可以访问
|
||||
if (to.meta.ignoreAccess) {
|
||||
if (
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
coreRouteNames.includes(to.name as string) ||
|
||||
// 明确声明忽略权限访问权限,则可以访问
|
||||
to.meta.ignoreAccess
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -93,6 +87,15 @@ function setupAccessGuard(router: Router) {
|
||||
}
|
||||
|
||||
const accessRoutes = accessStore.accessRoutes;
|
||||
/**
|
||||
* 已经登录 前往登录页 跳转到首页
|
||||
*/
|
||||
if (to.path === LOGIN_PATH) {
|
||||
return {
|
||||
path: DEFAULT_HOME_PATH,
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessRoutes && accessRoutes.length > 0) {
|
||||
|
@@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getAccessCodes, getUserInfo, login } from '#/api';
|
||||
import { doLogout, getUserInfo, login } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
@@ -33,40 +33,35 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { accessToken, refreshToken } = await login(params);
|
||||
const { access_token } = await login(params);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(accessToken);
|
||||
accessStore.setRefreshToken(refreshToken);
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(access_token);
|
||||
accessStore.setRefreshToken(access_token);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
fetchUserInfo(),
|
||||
getAccessCodes(),
|
||||
]);
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
userInfo = await fetchUserInfo();
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
userStore.setUserInfo(userInfo);
|
||||
/**
|
||||
* 在这里设置权限
|
||||
*/
|
||||
accessStore.setAccessCodes(userInfo.permissions);
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess ? await onSuccess?.() : await router.push(DEFAULT_HOME_PATH);
|
||||
}
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
notification.success({
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
}
|
||||
if (userInfo?.realName) {
|
||||
notification.success({
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
@@ -78,21 +73,38 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
try {
|
||||
await doLogout();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登陆页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
},
|
||||
});
|
||||
// 回登陆页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
let userInfo: null | UserInfo = null;
|
||||
userInfo = await getUserInfo();
|
||||
const { permissions = [], roles = [], user } = await getUserInfo();
|
||||
|
||||
/**
|
||||
* 从后台user -> vben user转换
|
||||
*/
|
||||
const userInfo: UserInfo = {
|
||||
avatar: user.avatar ?? '',
|
||||
permissions,
|
||||
realName: user.nickName,
|
||||
roles,
|
||||
userId: user.userId,
|
||||
username: user.userName,
|
||||
};
|
||||
userStore.setUserInfo(userInfo);
|
||||
return userInfo;
|
||||
}
|
||||
|
@@ -1 +1,2 @@
|
||||
export * from './auth';
|
||||
export * from './notify';
|
||||
|
119
apps/web-antd/src/store/notify.ts
Normal file
119
apps/web-antd/src/store/notify.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { useEventSource } from '@vueuse/core';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { random } from 'lodash-es';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
const { apiURL, clientId } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
);
|
||||
|
||||
export const useNotifyStore = defineStore(
|
||||
'app-notify',
|
||||
() => {
|
||||
const notificationList = ref<NotificationItem[]>([]);
|
||||
|
||||
/**
|
||||
* 开始监听sse消息
|
||||
*/
|
||||
function startListeningMessage() {
|
||||
const accessStore = useAccessStore();
|
||||
const token = accessStore.accessToken;
|
||||
|
||||
const sseAddr = `${apiURL}/resource/sse?clientid=${clientId}&Authorization=Bearer ${token}`;
|
||||
|
||||
const { data } = useEventSource(sseAddr, [], {
|
||||
autoReconnect: {
|
||||
delay: 1000,
|
||||
onFailed() {
|
||||
console.log('sse重连失败.');
|
||||
},
|
||||
retries: 3,
|
||||
},
|
||||
});
|
||||
|
||||
watch(data, (message) => {
|
||||
if (!message) return;
|
||||
console.log(`接收到消息: ${message}`);
|
||||
|
||||
notification.success({
|
||||
description: message,
|
||||
duration: 3,
|
||||
message: '收到新消息',
|
||||
});
|
||||
|
||||
notificationList.value.push({
|
||||
// 随机头像
|
||||
avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`,
|
||||
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
isRead: false,
|
||||
message,
|
||||
title: '消息',
|
||||
});
|
||||
|
||||
data.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全部已读
|
||||
*/
|
||||
function setAllRead() {
|
||||
notificationList.value.forEach((item) => {
|
||||
item.isRead = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单条消息已读
|
||||
* @param item 通知
|
||||
*/
|
||||
function setRead(item: NotificationItem) {
|
||||
!item.isRead && (item.isRead = true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空全部消息
|
||||
*/
|
||||
function clearAllMessage() {
|
||||
notificationList.value = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 只需要空实现即可
|
||||
* 否则会在退出登录清空所有
|
||||
*/
|
||||
function $reset() {
|
||||
// notificationList.value = [];
|
||||
}
|
||||
/**
|
||||
* 显示小圆点
|
||||
*/
|
||||
const showDot = computed(() =>
|
||||
notificationList.value.some((item) => !item.isRead),
|
||||
);
|
||||
|
||||
return {
|
||||
$reset,
|
||||
clearAllMessage,
|
||||
notificationList,
|
||||
setAllRead,
|
||||
setRead,
|
||||
showDot,
|
||||
startListeningMessage,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
paths: ['notificationList'],
|
||||
},
|
||||
},
|
||||
);
|
80
apps/web-antd/src/utils/encryption/crypto.ts
Normal file
80
apps/web-antd/src/utils/encryption/crypto.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
function randomUUID() {
|
||||
const chars = [
|
||||
...'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
];
|
||||
const uuid = Array.from({ length: 36 });
|
||||
let rnd = 0;
|
||||
let r: number;
|
||||
for (let i = 0; i < 36; i++) {
|
||||
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
||||
uuid[i] = '-';
|
||||
} else if (i === 14) {
|
||||
uuid[i] = '4';
|
||||
} else {
|
||||
if (rnd <= 0x02)
|
||||
rnd = Math.trunc(0x2_00_00_00 + Math.random() * 0x1_00_00_00);
|
||||
r = rnd & 16;
|
||||
rnd = rnd >> 4;
|
||||
uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r];
|
||||
}
|
||||
}
|
||||
return uuid.join('').replaceAll('-', '').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机生成aes 密钥
|
||||
*
|
||||
* @returns aes 密钥
|
||||
*/
|
||||
export function generateAesKey() {
|
||||
return CryptoJS.enc.Utf8.parse(randomUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* base64编码
|
||||
* @param str
|
||||
* @returns base64编码
|
||||
*/
|
||||
export function encryptBase64(str: CryptoJS.lib.WordArray) {
|
||||
return CryptoJS.enc.Base64.stringify(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥加密
|
||||
* @param message 加密内容
|
||||
* @param aesKey aesKey
|
||||
* @returns 使用公钥加密
|
||||
*/
|
||||
export function encryptWithAes(
|
||||
message: string,
|
||||
aesKey: CryptoJS.lib.WordArray,
|
||||
) {
|
||||
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密base64
|
||||
*/
|
||||
export function decryptBase64(str: string) {
|
||||
return CryptoJS.enc.Base64.parse(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行解密
|
||||
*/
|
||||
export function decryptWithAes(
|
||||
message: string,
|
||||
aesKey: CryptoJS.lib.WordArray,
|
||||
) {
|
||||
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
}
|
31
apps/web-antd/src/utils/encryption/jsencrypt.ts
Normal file
31
apps/web-antd/src/utils/encryption/jsencrypt.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// 密钥对生成 http://web.chacuo.net/netrsakeypair
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
|
||||
const { rsaPrivateKey, rsaPublicKey } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
);
|
||||
|
||||
/**
|
||||
* 加密
|
||||
* @param txt 需要加密的数据
|
||||
* @returns 加密后的数据
|
||||
*/
|
||||
export function encrypt(txt: string) {
|
||||
const instance = new JSEncrypt();
|
||||
instance.setPublicKey(rsaPublicKey);
|
||||
return instance.encrypt(txt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
* @param txt 需要解密的数据
|
||||
* @returns 解密后的数据
|
||||
*/
|
||||
export function decrypt(txt: string) {
|
||||
const instance = new JSEncrypt();
|
||||
instance.setPrivateKey(rsaPrivateKey);
|
||||
return instance.decrypt(txt);
|
||||
}
|
@@ -16,7 +16,6 @@ const loading = ref(false);
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: LoginCodeParams) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
@@ -9,7 +9,6 @@ defineOptions({ name: 'ForgetPassword' });
|
||||
const loading = ref(false);
|
||||
|
||||
function handleSubmit(value: string) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
@@ -1,18 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { AuthenticationLogin } from '@vben/common-ui';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { tenantList, type TenantResp } from '#/api';
|
||||
import { captchaImage, type CaptchaResponse } from '#/api/core/captcha';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const captchaInfo = ref<CaptchaResponse>({
|
||||
captchaEnabled: false,
|
||||
img: '',
|
||||
uuid: '',
|
||||
});
|
||||
|
||||
async function loadCaptcha() {
|
||||
const resp = await captchaImage();
|
||||
if (resp.captchaEnabled) {
|
||||
resp.img = `data:image/png;base64,${resp.img}`;
|
||||
}
|
||||
captchaInfo.value = resp;
|
||||
}
|
||||
|
||||
const tenantInfo = ref<TenantResp>({
|
||||
tenantEnabled: false,
|
||||
voList: [],
|
||||
});
|
||||
|
||||
async function loadTenant() {
|
||||
const resp = await tenantList();
|
||||
tenantInfo.value = resp;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCaptcha();
|
||||
loadTenant();
|
||||
});
|
||||
|
||||
interface LoginForm {
|
||||
code?: string;
|
||||
grantType: string;
|
||||
password: string;
|
||||
tenantId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const loginRef = ref<InstanceType<typeof AuthenticationLogin>>();
|
||||
async function handleAccountLogin(values: LoginForm) {
|
||||
try {
|
||||
const requestParam: any = omit(values, ['code']);
|
||||
// 验证码
|
||||
if (captchaInfo.value.captchaEnabled) {
|
||||
requestParam.code = values.code;
|
||||
requestParam.uuid = captchaInfo.value.uuid;
|
||||
}
|
||||
// 登录
|
||||
await authStore.authLogin(requestParam);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// 处理验证码错误
|
||||
if (error instanceof Error) {
|
||||
const message = error.message;
|
||||
if (message.includes('captcha') || message.includes('验证码')) {
|
||||
// 刷新验证码
|
||||
loginRef.value?.resetCaptcha();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:captcha-base64="captchaInfo.img"
|
||||
:loading="authStore.loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
:tenant-options="tenantInfo.voList"
|
||||
:use-captcha="captchaInfo.captchaEnabled"
|
||||
:use-tenant="tenantInfo.tenantEnabled"
|
||||
password-placeholder="密码"
|
||||
username-placeholder="用户名"
|
||||
@captcha-click="loadCaptcha"
|
||||
@submit="handleAccountLogin"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -11,7 +11,6 @@ defineOptions({ name: 'Register' });
|
||||
const loading = ref(false);
|
||||
|
||||
function handleSubmit(value: LoginAndRegisterParams) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
18
apps/web-antd/src/views/common.tsx
Normal file
18
apps/web-antd/src/views/common.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CommonSkeleton',
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="flex h-[600px] w-full items-center justify-center">
|
||||
<Fallback
|
||||
description="等待官方组件中"
|
||||
status="coming-soon"
|
||||
title="Coming Soon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
9
apps/web-antd/src/views/demo/demo/index.vue
Normal file
9
apps/web-antd/src/views/demo/demo/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/demo/tree/index.vue
Normal file
9
apps/web-antd/src/views/demo/tree/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
@@ -41,11 +41,9 @@ async function changeAccount(role: string) {
|
||||
|
||||
const account = accounts[role];
|
||||
resetAllStores();
|
||||
if (account) {
|
||||
await authStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
await authStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@@ -41,17 +41,12 @@ async function changeAccount(role: string) {
|
||||
|
||||
const account = accounts[role];
|
||||
resetAllStores();
|
||||
if (account) {
|
||||
await accessStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
await accessStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleToggleAccessMode() {
|
||||
if (!accounts.super) {
|
||||
return;
|
||||
}
|
||||
await toggleAccessMode();
|
||||
resetAllStores();
|
||||
|
||||
|
6
apps/web-antd/src/views/monitor/admin/index.vue
Normal file
6
apps/web-antd/src/views/monitor/admin/index.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<iframe
|
||||
class="size-full"
|
||||
src="http://localhost:9090/admin/applications"
|
||||
></iframe>
|
||||
</template>
|
83
apps/web-antd/src/views/monitor/cache/components/CommandChart.vue
vendored
Normal file
83
apps/web-antd/src/views/monitor/cache/components/CommandChart.vue
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import type { EChartsOption } from 'echarts';
|
||||
|
||||
import { defineComponent, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CommandChart',
|
||||
props: {
|
||||
data: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
setup(props, { expose }) {
|
||||
expose({});
|
||||
|
||||
const commandHtmlRef = ref<HTMLDivElement>();
|
||||
const echartsInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!commandHtmlRef.value) return;
|
||||
setEchartsOption(props.data);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
echartsInstance.value = echarts.init(
|
||||
commandHtmlRef.value,
|
||||
preferences.theme.mode,
|
||||
);
|
||||
setEchartsOption(props.data);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => preferences.theme.mode,
|
||||
(mode) => {
|
||||
echartsInstance.value?.dispose();
|
||||
echartsInstance.value = echarts.init(commandHtmlRef.value, mode);
|
||||
setEchartsOption(props.data);
|
||||
},
|
||||
);
|
||||
|
||||
function setEchartsOption(data: any[]) {
|
||||
const option: EChartsOption = {
|
||||
series: [
|
||||
{
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicInOut',
|
||||
center: ['50%', '38%'],
|
||||
data,
|
||||
name: '命令',
|
||||
radius: [15, 95],
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||
trigger: 'item',
|
||||
},
|
||||
};
|
||||
echartsInstance.value?.setOption(option);
|
||||
}
|
||||
|
||||
return {
|
||||
commandHtmlRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="commandHtmlRef" class="h-[400px] w-full"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
102
apps/web-antd/src/views/monitor/cache/components/MemoryChart.vue
vendored
Normal file
102
apps/web-antd/src/views/monitor/cache/components/MemoryChart.vue
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import type { EChartsOption } from 'echarts';
|
||||
|
||||
import { defineComponent, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MemoryChart',
|
||||
props: {
|
||||
data: {
|
||||
default: '0',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props, { expose }) {
|
||||
expose({});
|
||||
|
||||
const memoryHtmlRef = ref<HTMLDivElement>();
|
||||
const echartsInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!memoryHtmlRef.value) return;
|
||||
setEchartsOption(props.data);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
echartsInstance.value = echarts.init(
|
||||
memoryHtmlRef.value,
|
||||
preferences.theme.mode,
|
||||
);
|
||||
setEchartsOption(props.data);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => preferences.theme.mode,
|
||||
(mode) => {
|
||||
echartsInstance.value?.dispose();
|
||||
echartsInstance.value = echarts.init(memoryHtmlRef.value, mode);
|
||||
setEchartsOption(props.data);
|
||||
},
|
||||
);
|
||||
|
||||
function getNearestPowerOfTen(num: number) {
|
||||
let power = 10;
|
||||
while (power <= num) {
|
||||
power *= 10;
|
||||
}
|
||||
return power;
|
||||
}
|
||||
|
||||
function setEchartsOption(value: string) {
|
||||
// x10
|
||||
const formattedValue = Math.floor(Number.parseFloat(value));
|
||||
// 最大值 10以内取10 100以内取100 以此类推
|
||||
const max = getNearestPowerOfTen(formattedValue);
|
||||
const options: EChartsOption = {
|
||||
series: [
|
||||
{
|
||||
animation: true,
|
||||
animationDuration: 1000,
|
||||
data: [
|
||||
{
|
||||
name: '内存消耗',
|
||||
value: Number.parseFloat(value),
|
||||
},
|
||||
],
|
||||
detail: {
|
||||
formatter: `${value}M`,
|
||||
valueAnimation: true,
|
||||
},
|
||||
max,
|
||||
min: 0,
|
||||
name: '峰值',
|
||||
type: 'gauge',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
formatter: `{b} <br/>{a} : ${value}M`,
|
||||
},
|
||||
};
|
||||
echartsInstance.value?.setOption(options);
|
||||
}
|
||||
|
||||
return {
|
||||
memoryHtmlRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="memoryHtmlRef" class="h-[400px] w-full"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
65
apps/web-antd/src/views/monitor/cache/components/RedisDescription.vue
vendored
Normal file
65
apps/web-antd/src/views/monitor/cache/components/RedisDescription.vue
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { RedisInfo } from '#/api/monitor/cache';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
|
||||
interface IRedisInfo extends RedisInfo {
|
||||
dbSize: string;
|
||||
}
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
required: true,
|
||||
type: Object as PropType<IRedisInfo>,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Descriptions
|
||||
:column="{ xs: 1, sm: 1, md: 3, lg: 4, xl: 4 }"
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<DescriptionsItem label="redis版本">
|
||||
{{ data.redis_version }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="redis模式">
|
||||
{{ data.redis_mode === 'standalone' ? '单机模式' : '集群模式' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="tcp端口">
|
||||
{{ data.tcp_port }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="客户端数">
|
||||
{{ data.connected_clients }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="运行时间">
|
||||
{{ `${data.uptime_in_days}天` }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="使用内存">
|
||||
{{ data.used_memory_human }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="使用CPU">
|
||||
{{ parseFloat(data.used_cpu_user_children!).toFixed(2) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="内存配置">
|
||||
{{ data.maxmemory_human }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="AOF是否开启">
|
||||
{{ data.aof_enabled === '0' ? '否' : '是' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="RDB是否成功">
|
||||
{{ data.rdb_last_bgsave_status }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="Key数量">
|
||||
{{ data.dbSize }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="网络入口/出口">
|
||||
{{
|
||||
`${data.instantaneous_input_kbps}kps/${data.instantaneous_output_kbps}kps`
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</template>
|
94
apps/web-antd/src/views/monitor/cache/index.vue
vendored
Normal file
94
apps/web-antd/src/views/monitor/cache/index.vue
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Button, Card, Col, Row } from 'ant-design-vue';
|
||||
|
||||
import { redisCacheInfo, type RedisInfo } from '#/api/monitor/cache';
|
||||
|
||||
import CommandChart from './components/CommandChart.vue';
|
||||
import MemoryChart from './components/MemoryChart.vue';
|
||||
import RedisDescription from './components/RedisDescription.vue';
|
||||
|
||||
const baseSpan = { lg: 12, md: 24, sm: 24, xl: 12, xs: 24 };
|
||||
|
||||
const chartData = reactive<{ command: any[]; memory: string }>({
|
||||
command: [],
|
||||
memory: '0',
|
||||
});
|
||||
|
||||
interface IRedisInfo extends RedisInfo {
|
||||
dbSize: string;
|
||||
}
|
||||
const redisInfo = ref<IRedisInfo>();
|
||||
|
||||
onMounted(async () => {
|
||||
await loadInfo();
|
||||
});
|
||||
|
||||
async function loadInfo() {
|
||||
try {
|
||||
const ret = await redisCacheInfo();
|
||||
|
||||
// 单位MB 保留两位小数
|
||||
const usedMemory = (
|
||||
Number.parseInt(ret.info.used_memory!) /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2);
|
||||
chartData.memory = usedMemory;
|
||||
// 命令统计
|
||||
chartData.command = ret.commandStats;
|
||||
// redis信息
|
||||
redisInfo.value = { ...ret.info, dbSize: String(ret.dbSize) };
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-[16px]">
|
||||
<Row :gutter="[15, 15]">
|
||||
<Col :span="24">
|
||||
<Card size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-start gap-[6px]">
|
||||
<span class="icon-[logos--redis]"></span>
|
||||
<span>redis信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<Button size="small" @click="loadInfo">
|
||||
<div class="flex">
|
||||
<span class="icon-[charm--refresh]"></span>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
<RedisDescription v-if="redisInfo" :data="redisInfo" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col v-bind="baseSpan">
|
||||
<Card size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-[6px]">
|
||||
<span class="icon-[flat-color-icons--command-line]"></span>
|
||||
<span>命令统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<CommandChart :data="chartData.command" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col v-bind="baseSpan">
|
||||
<Card size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-start gap-[6px]">
|
||||
<span class="icon-[la--memory]"></span>
|
||||
<span>内存占用</span>
|
||||
</div>
|
||||
</template>
|
||||
<MemoryChart :data="chartData.memory" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/monitor/logininfor/index.vue
Normal file
9
apps/web-antd/src/views/monitor/logininfor/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/monitor/online/index.vue
Normal file
9
apps/web-antd/src/views/monitor/online/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/monitor/operlog/index.vue
Normal file
9
apps/web-antd/src/views/monitor/operlog/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
3
apps/web-antd/src/views/monitor/snailjob/index.vue
Normal file
3
apps/web-antd/src/views/monitor/snailjob/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<iframe class="size-full" src="http://localhost:8800/snail-job"></iframe>
|
||||
</template>
|
9
apps/web-antd/src/views/system/client/index.vue
Normal file
9
apps/web-antd/src/views/system/client/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/config/index.vue
Normal file
9
apps/web-antd/src/views/system/config/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/dept/index.vue
Normal file
9
apps/web-antd/src/views/system/dept/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/dict/index.vue
Normal file
9
apps/web-antd/src/views/system/dict/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/menu/index.vue
Normal file
9
apps/web-antd/src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/notice/index.vue
Normal file
9
apps/web-antd/src/views/system/notice/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/oss/index.vue
Normal file
9
apps/web-antd/src/views/system/oss/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/post/index.vue
Normal file
9
apps/web-antd/src/views/system/post/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/role/index.vue
Normal file
9
apps/web-antd/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/tenant/index.vue
Normal file
9
apps/web-antd/src/views/system/tenant/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/system/tenantPackage/index.vue
Normal file
9
apps/web-antd/src/views/system/tenantPackage/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
17
apps/web-antd/src/views/system/user/index.vue
Normal file
17
apps/web-antd/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
onMounted(() => {
|
||||
console.log('keepAlive测试 -> 挂载了');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-[8px]">
|
||||
<Button type="primary" v-access:code="['system:user:list']">
|
||||
测试按钮可见
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/tool/gen/index.vue
Normal file
9
apps/web-antd/src/views/tool/gen/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/category/index.vue
Normal file
9
apps/web-antd/src/views/workflow/category/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/formManage/index.vue
Normal file
9
apps/web-antd/src/views/workflow/formManage/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/leave/index.vue
Normal file
9
apps/web-antd/src/views/workflow/leave/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/model/index.vue
Normal file
9
apps/web-antd/src/views/workflow/model/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/task/allTaskWaiting.vue
Normal file
9
apps/web-antd/src/views/workflow/task/allTaskWaiting.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/task/myDocument.vue
Normal file
9
apps/web-antd/src/views/workflow/task/myDocument.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/task/taskCopyList.vue
Normal file
9
apps/web-antd/src/views/workflow/task/taskCopyList.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/task/taskFinish.vue
Normal file
9
apps/web-antd/src/views/workflow/task/taskFinish.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
9
apps/web-antd/src/views/workflow/task/taskWaiting.vue
Normal file
9
apps/web-antd/src/views/workflow/task/taskWaiting.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
@@ -10,7 +10,7 @@ export default defineConfig(async () => {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// mock代理目标地址
|
||||
target: 'http://localhost:5320/api',
|
||||
target: 'http://localhost:8080',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user