chore: 脚手架
This commit is contained in:
parent
4bd4f7490b
commit
c31259598b
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -23,7 +23,7 @@
|
||||
"editor.suggestSelection": "recentlyUsedByPrefix",
|
||||
"editor.acceptSuggestionOnEnter": "smart",
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.stickyScroll.enabled": true,
|
||||
"editor.stickyScroll.enabled": false,
|
||||
"editor.hover.sticky": true,
|
||||
"editor.suggest.insertMode": "replace",
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
@ -46,7 +46,7 @@
|
||||
"terminal.integrated.persistentSessionReviveProcess": "never",
|
||||
"terminal.integrated.tabs.enabled": true,
|
||||
"terminal.integrated.scrollback": 10000,
|
||||
"terminal.integrated.stickyScroll.enabled": true,
|
||||
"terminal.integrated.stickyScroll.enabled": false,
|
||||
|
||||
// files
|
||||
"files.eol": "\n",
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
@ -19,6 +19,10 @@ const customConfig: Linter.Config[] = [
|
||||
files: ['apps/**/**'],
|
||||
ignores: restrictedImportIgnores,
|
||||
rules: {
|
||||
// 允许使用void类型
|
||||
'@typescript-eslint/no-invalid-void-type': 'off',
|
||||
// 关闭 不允许使用console
|
||||
'no-console': 'off',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
@ -46,6 +50,7 @@ const customConfig: Linter.Config[] = [
|
||||
],
|
||||
},
|
||||
],
|
||||
'regexp/no-unused-capturing-group': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -86,6 +91,13 @@ const customConfig: Linter.Config[] = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/effects/access/**/**'],
|
||||
ignores: restrictedImportIgnores,
|
||||
rules: {
|
||||
'regexp/no-unused-capturing-group': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// 不能引入@vben/*里面的包
|
||||
files: [
|
||||
|
8
packages/@core/base/typings/src/basic.d.ts
vendored
8
packages/@core/base/typings/src/basic.d.ts
vendored
@ -12,6 +12,10 @@ interface BasicUserInfo {
|
||||
* 头像
|
||||
*/
|
||||
avatar: string;
|
||||
/**
|
||||
* 用户权限
|
||||
*/
|
||||
permissions: string[];
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@ -19,11 +23,11 @@ interface BasicUserInfo {
|
||||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
roles?: string[];
|
||||
roles: string[];
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
userId: string;
|
||||
userId: number | string;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
|
@ -88,7 +88,7 @@ const defaultPreferences: Preferences = {
|
||||
colorPrimary: 'hsl(231 98% 65%)',
|
||||
colorSuccess: 'hsl(144 57% 58%)',
|
||||
colorWarning: 'hsl(42 84% 61%)',
|
||||
mode: 'dark',
|
||||
mode: 'auto',
|
||||
radius: '0.5',
|
||||
semiDarkMenu: true,
|
||||
},
|
||||
|
@ -24,6 +24,12 @@ async function generateAccessible(
|
||||
|
||||
// 动态添加到router实例内
|
||||
accessibleRoutes.forEach((route) => {
|
||||
/**
|
||||
* 外链不应该被添加到路由 由menu处理
|
||||
*/
|
||||
if (/^http(s)?:\/\//.test(route.path)) {
|
||||
return;
|
||||
}
|
||||
router.addRoute(route);
|
||||
});
|
||||
|
||||
|
@ -28,7 +28,13 @@ function useAccess() {
|
||||
*/
|
||||
function hasAccessByCodes(codes: string[]) {
|
||||
const userCodesSet = new Set(accessStore.accessCodes);
|
||||
|
||||
/**
|
||||
* 管理员权限
|
||||
*/
|
||||
if (userCodesSet.has('*:*:*')) {
|
||||
return true;
|
||||
}
|
||||
// 其他 判断是否存在
|
||||
const intersection = codes.filter((item) => userCodesSet.has(item));
|
||||
return intersection.length > 0;
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthenticationProps, LoginEmits } from './typings';
|
||||
|
||||
import { computed, reactive } from 'vue';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
VbenButton,
|
||||
VbenCheckbox,
|
||||
VbenInput,
|
||||
@ -15,13 +21,31 @@ import {
|
||||
import Title from './auth-title.vue';
|
||||
import ThirdPartyLogin from './third-party-login.vue';
|
||||
|
||||
interface Props extends AuthenticationProps {}
|
||||
interface Props extends AuthenticationProps {
|
||||
/**
|
||||
* @zh_CN 验证码图片base64
|
||||
*/
|
||||
captchaBase64?: string;
|
||||
/**
|
||||
* 租户信息options
|
||||
*/
|
||||
tenantOptions?: { companyName: string; domain?: string; tenantId: string }[];
|
||||
/**
|
||||
* @zh_CN 是否启用验证码
|
||||
*/
|
||||
useCaptcha?: boolean;
|
||||
/**
|
||||
* @zh_CN 是否启用租户
|
||||
*/
|
||||
useTenant?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationLogin',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
captchaBase64: '',
|
||||
codeLoginPath: '/auth/code-login',
|
||||
forgetPasswordPath: '/auth/forget-password',
|
||||
loading: false,
|
||||
@ -35,11 +59,16 @@ withDefaults(defineProps<Props>(), {
|
||||
showRememberMe: true,
|
||||
showThirdPartyLogin: true,
|
||||
subTitle: '',
|
||||
tenantOptions: () => [],
|
||||
title: '',
|
||||
usernamePlaceholder: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 验证码点击
|
||||
*/
|
||||
captchaClick: [];
|
||||
submit: LoginEmits['submit'];
|
||||
}>();
|
||||
|
||||
@ -47,15 +76,28 @@ const router = useRouter();
|
||||
|
||||
const REMEMBER_ME_KEY = `REMEMBER_ME_USERNAME_${location.hostname}`;
|
||||
|
||||
const localUsername = localStorage.getItem(REMEMBER_ME_KEY) || '';
|
||||
const localUsername = localStorage.getItem(REMEMBER_ME_KEY) || 'admin';
|
||||
|
||||
const formState = reactive({
|
||||
password: '',
|
||||
code: '',
|
||||
password: 'admin123',
|
||||
rememberMe: !!localUsername,
|
||||
submitted: false,
|
||||
// 默认租户
|
||||
tenantId: '000000',
|
||||
username: localUsername,
|
||||
});
|
||||
|
||||
/**
|
||||
* 默认选中第一项租户
|
||||
*/
|
||||
const stop = watch(props.tenantOptions, (options) => {
|
||||
if (options.length > 0) {
|
||||
formState.tenantId = options[0]!.tenantId;
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
const usernameStatus = computed(() => {
|
||||
return formState.submitted && !formState.username ? 'error' : 'default';
|
||||
});
|
||||
@ -64,6 +106,10 @@ const passwordStatus = computed(() => {
|
||||
return formState.submitted && !formState.password ? 'error' : 'default';
|
||||
});
|
||||
|
||||
const captchaStatus = computed(() => {
|
||||
return formState.submitted && !formState.code ? 'error' : 'default';
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
formState.submitted = true;
|
||||
|
||||
@ -74,13 +120,21 @@ function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证码
|
||||
if (props.useCaptcha && captchaStatus.value !== 'default') {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
REMEMBER_ME_KEY,
|
||||
formState.rememberMe ? formState.username : '',
|
||||
);
|
||||
|
||||
emit('submit', {
|
||||
code: formState.code,
|
||||
grantType: 'password',
|
||||
password: formState.password,
|
||||
tenantId: formState.tenantId,
|
||||
username: formState.username,
|
||||
});
|
||||
}
|
||||
@ -88,6 +142,18 @@ function handleSubmit() {
|
||||
function handleGo(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置验证码
|
||||
*/
|
||||
function resetCaptcha() {
|
||||
emit('captchaClick');
|
||||
formState.code = '';
|
||||
// todo 获取焦点
|
||||
// VbenInput并没有提供focus方法
|
||||
}
|
||||
|
||||
defineExpose({ resetCaptcha });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -101,6 +167,26 @@ function handleGo(path: string) {
|
||||
</template>
|
||||
</Title>
|
||||
|
||||
<!-- 租户 -->
|
||||
<div v-if="useTenant" class="mb-6">
|
||||
<Select v-model="formState.tenantId">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择公司" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="item in tenantOptions"
|
||||
:key="item.tenantId"
|
||||
:value="item.tenantId"
|
||||
>
|
||||
{{ item.companyName }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<VbenInput
|
||||
v-model="formState.username"
|
||||
:autofocus="false"
|
||||
@ -123,6 +209,27 @@ function handleGo(path: string) {
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<!-- 图片验证码 -->
|
||||
<div v-if="useCaptcha" class="flex">
|
||||
<div class="flex-1">
|
||||
<VbenInput
|
||||
v-model="formState.code"
|
||||
:error-tip="$t('authentication.captchaTip')"
|
||||
:label="$t('authentication.captcha')"
|
||||
:placeholder="$t('authentication.captcha')"
|
||||
:status="captchaStatus"
|
||||
name="code"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
:src="captchaBase64"
|
||||
class="h-[38px] w-[115px] rounded-r-md"
|
||||
@click="emit('captchaClick')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 mt-4 flex justify-between">
|
||||
<div v-if="showRememberMe" class="flex-center">
|
||||
<VbenCheckbox v-model:checked="formState.rememberMe" name="rememberMe">
|
||||
|
@ -3,6 +3,7 @@ interface AuthenticationProps {
|
||||
* @zh_CN 验证码登录路径
|
||||
*/
|
||||
codeLoginPath?: string;
|
||||
|
||||
/**
|
||||
* @zh_CN 忘记密码路径
|
||||
*/
|
||||
@ -32,6 +33,7 @@ interface AuthenticationProps {
|
||||
* @zh_CN 是否显示验证码登录
|
||||
*/
|
||||
showCodeLogin?: boolean;
|
||||
|
||||
/**
|
||||
* @zh_CN 是否显示忘记密码
|
||||
*/
|
||||
@ -66,7 +68,6 @@ interface AuthenticationProps {
|
||||
* @zh_CN 登录框标题
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* @zh_CN 用户名占位符
|
||||
*/
|
||||
@ -74,8 +75,12 @@ interface AuthenticationProps {
|
||||
}
|
||||
|
||||
interface LoginAndRegisterParams {
|
||||
code?: string;
|
||||
grantType: string;
|
||||
password: string;
|
||||
tenantId: string;
|
||||
username: string;
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
interface LoginCodeParams {
|
||||
|
@ -15,9 +15,26 @@ export function useAppConfig(
|
||||
? window._VBEN_ADMIN_PRO_APP_CONF_
|
||||
: (env as VbenAdminProAppConfigRaw);
|
||||
|
||||
const { VITE_GLOB_API_URL } = config;
|
||||
const {
|
||||
VITE_GLOB_API_URL,
|
||||
VITE_GLOB_APP_CLIENT_ID,
|
||||
VITE_GLOB_ENABLE_ENCRYPT,
|
||||
VITE_GLOB_RSA_PRIVATE_KEY,
|
||||
VITE_GLOB_RSA_PUBLIC_KEY,
|
||||
VITE_GLOB_WEBSOCKET_ENABLE,
|
||||
} = config;
|
||||
|
||||
return {
|
||||
// 后端地址
|
||||
apiURL: VITE_GLOB_API_URL,
|
||||
// 客户端key
|
||||
clientId: VITE_GLOB_APP_CLIENT_ID,
|
||||
enableEncrypt: VITE_GLOB_ENABLE_ENCRYPT === 'true',
|
||||
// RSA私钥
|
||||
rsaPrivateKey: VITE_GLOB_RSA_PRIVATE_KEY,
|
||||
// RSA公钥
|
||||
rsaPublicKey: VITE_GLOB_RSA_PUBLIC_KEY,
|
||||
// 是否开启websocket
|
||||
websocketEnable: VITE_GLOB_WEBSOCKET_ENABLE === 'true',
|
||||
};
|
||||
}
|
||||
|
@ -99,7 +99,8 @@ class RequestClient {
|
||||
const authorization = this.makeAuthorization?.(config);
|
||||
if (authorization) {
|
||||
const { token } = authorization.tokenHandler?.() ?? {};
|
||||
config.headers[authorization.key || 'Authorization'] = token;
|
||||
config.headers[authorization.key || 'Authorization'] =
|
||||
`Bearer ${token}`;
|
||||
}
|
||||
|
||||
const requestHeader = this.makeRequestHeaders?.(config);
|
||||
|
@ -43,13 +43,9 @@ interface RequestClientOptions extends CreateAxiosDefaults {
|
||||
}
|
||||
|
||||
interface HttpResponse<T = any> {
|
||||
/**
|
||||
* 0 表示成功 其他表示失败
|
||||
* 0 means success, others means fail
|
||||
*/
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
@ -60,3 +56,44 @@ export type {
|
||||
RequestClientOptions,
|
||||
RequestContentType,
|
||||
};
|
||||
|
||||
export type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined;
|
||||
export type SuccessMessageMode = ErrorMessageMode;
|
||||
|
||||
/**
|
||||
* 拓展axios的请求配置
|
||||
*/
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
/** 是否加密请求参数 */
|
||||
encrypt?: boolean;
|
||||
/**
|
||||
* 错误弹窗类型
|
||||
*/
|
||||
errorMessageMode?: ErrorMessageMode;
|
||||
/**
|
||||
* 是否格式化日期
|
||||
*/
|
||||
formatDate?: boolean;
|
||||
/**
|
||||
* 是否返回原生axios响应
|
||||
*/
|
||||
isReturnNativeResponse?: boolean;
|
||||
/**
|
||||
* 是否需要转换响应 即只获取{code, msg, data}中的data
|
||||
*/
|
||||
isTransformResponse?: boolean;
|
||||
/**
|
||||
* param添加到url后
|
||||
*/
|
||||
joinParamsToUrl?: boolean;
|
||||
/**
|
||||
* 加入时间戳
|
||||
*/
|
||||
joinTime?: boolean;
|
||||
/**
|
||||
* 成功弹窗类型
|
||||
*/
|
||||
successMessageMode?: SuccessMessageMode;
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,12 @@
|
||||
"unauthorized": "Unauthorized. Please log in to continue.",
|
||||
"forbidden": "Forbidden. You do not have permission to access this resource.",
|
||||
"notFound": "Not Found. The requested resource could not be found.",
|
||||
"internalServerError": "Internal Server Error. Something went wrong on our end. Please try again later."
|
||||
"internalServerError": "Internal Server Error. Something went wrong on our end. Please try again later.",
|
||||
"apiRequestFailed": "The interface request failed, please try again later!",
|
||||
"operationSuccess": "Operation Success",
|
||||
"operationFailed": "Operation failed",
|
||||
"errorTip": "Error Tip",
|
||||
"successTip": "Success Tip"
|
||||
}
|
||||
},
|
||||
"widgets": {
|
||||
@ -95,8 +100,10 @@
|
||||
"loginSubtitle": "Enter your account details to manage your projects",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"captcha": "Captcha",
|
||||
"usernameTip": "Please enter username",
|
||||
"passwordTip": "Please enter password",
|
||||
"captchaTip": "Please enter captcha",
|
||||
"rememberMe": "Remember Me",
|
||||
"createAnAccount": "Create an Account",
|
||||
"createAccount": "Create Account",
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"page": {
|
||||
"core": {
|
||||
"login": "登陆",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登陆",
|
||||
"qrcodeLogin": "二维码登陆",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码"
|
||||
},
|
||||
"dashboard": {
|
||||
@ -51,7 +51,12 @@
|
||||
"unauthorized": "登录认证过期。请重新登录后继续。",
|
||||
"forbidden": "禁止访问, 您没有权限访问此资源。",
|
||||
"notFound": "未找到, 请求的资源不存在。",
|
||||
"internalServerError": "内部服务器错误,请稍后再试。"
|
||||
"internalServerError": "内部服务器错误,请稍后再试。",
|
||||
"apiRequestFailed": "请求出错,请稍候重试",
|
||||
"operationSuccess": "操作成功",
|
||||
"operationFailed": "操作失败",
|
||||
"errorTip": "错误提示",
|
||||
"successTip": "成功提示"
|
||||
}
|
||||
},
|
||||
"widgets": {
|
||||
@ -95,8 +100,10 @@
|
||||
"loginSubtitle": "请输入您的帐户信息以开始管理您的项目",
|
||||
"username": "账号",
|
||||
"password": "密码",
|
||||
"captcha": "验证码",
|
||||
"usernameTip": "请输入用户名",
|
||||
"passwordTip": "请输入密码",
|
||||
"captchaTip": "请输入验证码",
|
||||
"rememberMe": "记住账号",
|
||||
"createAnAccount": "创建一个账号",
|
||||
"createAccount": "创建账号",
|
||||
|
@ -5,6 +5,10 @@ interface BasicUserInfo {
|
||||
* 头像
|
||||
*/
|
||||
avatar: string;
|
||||
/**
|
||||
* 用户权限
|
||||
*/
|
||||
permissions: string[];
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@ -12,11 +16,11 @@ interface BasicUserInfo {
|
||||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
roles?: string[];
|
||||
roles: string[];
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
userId: string;
|
||||
userId: number | string;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
|
22
packages/types/global.d.ts
vendored
22
packages/types/global.d.ts
vendored
@ -8,11 +8,33 @@ declare module 'vue-router' {
|
||||
}
|
||||
|
||||
export interface VbenAdminProAppConfigRaw {
|
||||
// 后端接口地址
|
||||
VITE_GLOB_API_URL: string;
|
||||
// 客户端ID
|
||||
VITE_GLOB_APP_CLIENT_ID: string;
|
||||
// # 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
|
||||
VITE_GLOB_ENABLE_ENCRYPT: string;
|
||||
// RSA请求解密私钥
|
||||
VITE_GLOB_RSA_PRIVATE_KEY: string;
|
||||
// RSA请求加密公钥
|
||||
VITE_GLOB_RSA_PUBLIC_KEY: string;
|
||||
// 是否开启websocket 注意从配置文件获取的类型为string
|
||||
VITE_GLOB_WEBSOCKET_ENABLE: string;
|
||||
}
|
||||
|
||||
export interface ApplicationConfig {
|
||||
// 后端接口地址
|
||||
apiURL: string;
|
||||
// 客户端key
|
||||
clientId: string;
|
||||
// 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
|
||||
enableEncrypt: boolean;
|
||||
// RSA响应解密私钥
|
||||
rsaPrivateKey: string;
|
||||
// RSA请求加密公钥
|
||||
rsaPublicKey: string;
|
||||
// 是否开启websocket
|
||||
websocketEnable: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
13
packages/types/src/user.d.ts
vendored
13
packages/types/src/user.d.ts
vendored
@ -3,18 +3,9 @@ import type { BasicUserInfo } from '@vben-core/typings';
|
||||
/** 用户信息 */
|
||||
interface UserInfo extends BasicUserInfo {
|
||||
/**
|
||||
* 用户描述
|
||||
* 拓展使用
|
||||
*/
|
||||
desc: string;
|
||||
/**
|
||||
* 首页地址
|
||||
*/
|
||||
homePath: string;
|
||||
|
||||
/**
|
||||
* accessToken
|
||||
*/
|
||||
token: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type { UserInfo };
|
||||
|
@ -61,6 +61,11 @@ function convertRoutes(
|
||||
? normalizePath
|
||||
: `${normalizePath}.vue`
|
||||
];
|
||||
if (!route.component) {
|
||||
console.error(`未找到对应组件: ${component}`);
|
||||
// 默认为404页面
|
||||
route.component = layoutMap.NotFoundComponent;
|
||||
}
|
||||
}
|
||||
|
||||
return route;
|
||||
|
@ -160,9 +160,21 @@ importers:
|
||||
ant-design-vue:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3(vue@3.4.35(typescript@5.5.4))
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
dayjs:
|
||||
specifier: ^1.11.12
|
||||
version: 1.11.12
|
||||
echarts:
|
||||
specifier: ^5.5.1
|
||||
version: 5.5.1
|
||||
jsencrypt:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
pinia:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0(typescript@5.5.4)(vue@3.4.35(typescript@5.5.4))
|
||||
@ -172,6 +184,13 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.4.2
|
||||
version: 4.4.2(vue@3.4.35(typescript@5.5.4))
|
||||
devDependencies:
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
|
||||
apps/web-ele:
|
||||
dependencies:
|
||||
@ -1219,36 +1238,42 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-arm64-gnu@0.22.6':
|
||||
resolution: {integrity: sha512-9PAqNJlAQfFm1RW0DVCM/S4gFHdppxUTWacB3qEeJZXgdLnoH0KGQa4z3Xo559SPYDKZy0VnY02mZ3XJ+v6/Vw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-x64-gnu@0.21.4':
|
||||
resolution: {integrity: sha512-U7jl8RGpxKV+pjFstY0y5qD+D+wm9dXNO7NBbIOnETgTMizTFiUuQWT7SOlIklhcxxuXqWzfwhNN1qwI0tGNWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-x64-gnu@0.22.6':
|
||||
resolution: {integrity: sha512-nZf+gxXVrZqvP1LN6HwzOMA4brF3umBXfMequQzv8S6HeJ4c34P23F0Tw8mHtQpVYP9PQWJUvt3LJQ8Xvd5Hiw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-x64-musl@0.21.4':
|
||||
resolution: {integrity: sha512-SOGR93kGomRR+Vh87+jXI3pJLR+J+dekCI8a4S22kGX9iAen8/+Ew++lFouDueKLyszmmhCrIk1WnJvYPuSFBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/napi-linux-x64-musl@0.22.6':
|
||||
resolution: {integrity: sha512-gcJeBMgJQf2pZZo0lgH0Vg4ycyujM7Am8VlomXhavC/dPpkddA1tiHSIC4fCNneLU1EqHITy3ALSmM4GLdsjBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/napi-win32-arm64-msvc@0.21.4':
|
||||
resolution: {integrity: sha512-ciGaTbkPjbCGqUyLwIPvcNeftNXjSG3cXE+5NiLThRbDhh2yUOE8YJkElUQcu0xQCdSlXnb4l/imEED/65jGfw==}
|
||||
@ -3281,7 +3306,6 @@ packages:
|
||||
|
||||
'@ls-lint/ls-lint@2.2.3':
|
||||
resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==}
|
||||
cpu: [x64, arm64, s390x]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@ -3384,30 +3408,35 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.4.1':
|
||||
resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.4.1':
|
||||
resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.4.1':
|
||||
resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.4.1':
|
||||
resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-wasm@2.4.1':
|
||||
resolution: {integrity: sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA==}
|
||||
@ -3607,91 +3636,109 @@ packages:
|
||||
resolution: {integrity: sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.20.0':
|
||||
resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.19.2':
|
||||
resolution: {integrity: sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.20.0':
|
||||
resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.19.2':
|
||||
resolution: {integrity: sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.19.2':
|
||||
resolution: {integrity: sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.20.0':
|
||||
resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.19.2':
|
||||
resolution: {integrity: sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.19.2':
|
||||
resolution: {integrity: sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.19.2':
|
||||
resolution: {integrity: sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.19.2':
|
||||
resolution: {integrity: sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.19.2':
|
||||
resolution: {integrity: sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.20.0':
|
||||
resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.19.2':
|
||||
resolution: {integrity: sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==}
|
||||
@ -3819,6 +3866,9 @@ packages:
|
||||
'@types/conventional-commits-parser@5.0.0':
|
||||
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/eslint@8.56.11':
|
||||
resolution: {integrity: sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==}
|
||||
|
||||
@ -4905,6 +4955,9 @@ packages:
|
||||
uWebSockets.js:
|
||||
optional: true
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
@ -6558,6 +6611,9 @@ packages:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsencrypt@3.3.2:
|
||||
resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
|
||||
|
||||
jsesc@0.5.0:
|
||||
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
|
||||
hasBin: true
|
||||
@ -12489,6 +12545,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.1.0
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/eslint@8.56.11':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
@ -13775,6 +13833,8 @@ snapshots:
|
||||
|
||||
crossws@0.2.4: {}
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
|
||||
cspell-config-lib@8.13.1:
|
||||
@ -15689,6 +15749,8 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
jsencrypt@3.3.2: {}
|
||||
|
||||
jsesc@0.5.0: {}
|
||||
|
||||
jsesc@2.5.2: {}
|
||||
|
Loading…
Reference in New Issue
Block a user