chore: 脚手架

This commit is contained in:
dap 2024-08-07 08:57:56 +08:00
parent 4bd4f7490b
commit c31259598b
83 changed files with 2127 additions and 225 deletions

View File

@ -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",

View File

@ -1,16 +1,25 @@
# 端口号
VITE_PORT=5555
# base路径
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=/api
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=true
# 是否打开 devtoolstrue 为打开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

View File

@ -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
View 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"
}
}

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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');
}

View 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');
}

View File

@ -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');
}

View File

@ -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');
}

View 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;
}

View 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');
}

View File

@ -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;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
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;
}

View File

@ -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>

View File

@ -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;
}
/**

View File

@ -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: {},
});

View File

@ -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,

View File

@ -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) {

View File

@ -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;
}

View File

@ -1 +1,2 @@
export * from './auth';
export * from './notify';

View 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'],
},
},
);

View 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);
}

View 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);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>
);
},
});

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -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>

View File

@ -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();

View File

@ -0,0 +1,6 @@
<template>
<iframe
class="size-full"
src="http://localhost:9090/admin/applications"
></iframe>
</template>

View 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>

View 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));
// 1010 100100
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>

View 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>

View 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>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<iframe class="size-full" src="http://localhost:8800/snail-job"></iframe>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View 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>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

View File

@ -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,
},
},

View File

@ -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: [

View File

@ -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;
/**
*
*/

View File

@ -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,
},

View File

@ -24,6 +24,12 @@ async function generateAccessible(
// 动态添加到router实例内
accessibleRoutes.forEach((route) => {
/**
* menu处理
*/
if (/^http(s)?:\/\//.test(route.path)) {
return;
}
router.addRoute(route);
});

View File

@ -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;
}

View File

@ -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
// VbenInputfocus
}
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">

View File

@ -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 {

View File

@ -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',
};
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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": "创建账号",

View File

@ -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;
/**
*
*/

View File

@ -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 {

View File

@ -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 };

View File

@ -61,6 +61,11 @@ function convertRoutes(
? normalizePath
: `${normalizePath}.vue`
];
if (!route.component) {
console.error(`未找到对应组件: ${component}`);
// 默认为404页面
route.component = layoutMap.NotFoundComponent;
}
}
return route;

View File

@ -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: {}