mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat: 整合登录
This commit is contained in:
BIN
src/assets/imgs/logo.png
Normal file
BIN
src/assets/imgs/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="currentColor"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="currentColor"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -3,7 +3,7 @@ defineOptions({ name: 'SystemLogo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<icon-local-logo />
|
||||
<img src="@/assets/imgs/logo.png" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -9,10 +9,10 @@ export function useAuth() {
|
||||
}
|
||||
|
||||
if (typeof codes === 'string') {
|
||||
return authStore.userInfo.buttons.includes(codes);
|
||||
return authStore.userInfo.permissions.includes(codes);
|
||||
}
|
||||
|
||||
return codes.some(code => authStore.userInfo.buttons.includes(code));
|
||||
return codes.some(code => authStore.userInfo.permissions.includes(code));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -38,7 +38,8 @@ export function useFormRules() {
|
||||
phone: [createRequiredRule($t('form.phone.required')), patternRules.phone],
|
||||
pwd: [createRequiredRule($t('form.pwd.required')), patternRules.pwd],
|
||||
code: [createRequiredRule($t('form.code.required')), patternRules.code],
|
||||
email: [createRequiredRule($t('form.email.required')), patternRules.email]
|
||||
email: [createRequiredRule($t('form.email.required')), patternRules.email],
|
||||
tenantId: [createRequiredRule('请选择/输入公司名称')]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
/** the default required rule */
|
||||
|
@ -38,7 +38,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
apiParams,
|
||||
columns: config.columns,
|
||||
transformer: res => {
|
||||
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
|
||||
const { rows: records = [], pageNum: current = 1, pageSize: size = 10, total = 0 } = res.data || {};
|
||||
|
||||
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
|
||||
const pageSize = size <= 0 ? 10 : size;
|
||||
@ -225,9 +225,9 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
||||
/** the editing row data */
|
||||
const editingData: Ref<T | null> = ref(null);
|
||||
|
||||
function handleEdit(id: T['id']) {
|
||||
function handleEdit(field: keyof T, id: string) {
|
||||
operateType.value = 'edit';
|
||||
const findItem = data.value.find(item => item.id === id) || null;
|
||||
const findItem = data.value.find(item => item[field] === id) || null;
|
||||
editingData.value = jsonClone(findItem);
|
||||
|
||||
openDrawer();
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { AdminLayout, LAYOUT_SCROLL_EL_ID } from '@sa/materials';
|
||||
import type { LayoutMode } from '@sa/materials';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { initWebSocket } from '@/utils/websocket';
|
||||
import GlobalHeader from '../modules/global-header/index.vue';
|
||||
import GlobalSider from '../modules/global-sider/index.vue';
|
||||
import GlobalTab from '../modules/global-tab/index.vue';
|
||||
@ -100,6 +101,11 @@ function getSiderCollapsedWidth() {
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
initWebSocket(`${protocol + window.location.host + import.meta.env.VITE_APP_BASE_API}/resource/websocket`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -73,7 +73,7 @@ function handleDropdown(key: DropdownKey) {
|
||||
<div>
|
||||
<ButtonIcon>
|
||||
<SvgIcon icon="ph:user-circle" class="text-icon-large" />
|
||||
<span class="text-16px font-medium">{{ authStore.userInfo.userName }}</span>
|
||||
<span class="text-16px font-medium">{{ authStore.userInfo.user?.userName }}</span>
|
||||
</ButtonIcon>
|
||||
</div>
|
||||
</NDropdown>
|
||||
|
@ -17,7 +17,7 @@ withDefaults(defineProps<Props>(), {
|
||||
|
||||
<template>
|
||||
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
|
||||
<SystemLogo class="text-32px text-primary" />
|
||||
<SystemLogo class="w-32px text-primary" />
|
||||
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
|
||||
{{ $t('system.title') }}
|
||||
</h2>
|
||||
|
@ -1,6 +1,6 @@
|
||||
const local: App.I18n.Schema = {
|
||||
system: {
|
||||
title: 'SoybeanAdmin',
|
||||
title: 'RuoYi Vue Plus',
|
||||
updateTitle: 'System Version Update Notification',
|
||||
updateContent: 'A new version of the system has been detected. Do you want to refresh the page immediately?',
|
||||
updateConfirm: 'Refresh immediately',
|
||||
|
@ -1,6 +1,6 @@
|
||||
const local: App.I18n.Schema = {
|
||||
system: {
|
||||
title: 'Soybean 管理系统',
|
||||
title: 'RuoYi Vue Plus',
|
||||
updateTitle: '系统版本更新通知',
|
||||
updateContent: '检测到系统有新版本发布,是否立即刷新页面?',
|
||||
updateConfirm: '立即刷新',
|
||||
|
@ -2,36 +2,20 @@
|
||||
import { getRgb } from '@sa/color';
|
||||
import { $t } from '@/locales';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import systemLogo from '@/assets/svg-icon/logo.svg?raw';
|
||||
import SystemLogo from '@/assets/imgs/logo.png';
|
||||
|
||||
export function setupLoading() {
|
||||
const themeColor = localStg.get('themeColor') || '#646cff';
|
||||
const themeColor = localStg.get('themeColor') || '#2080f0';
|
||||
|
||||
const { r, g, b } = getRgb(themeColor);
|
||||
|
||||
const primaryColor = `--primary-color: ${r} ${g} ${b}`;
|
||||
|
||||
const loadingClasses = [
|
||||
'left-0 top-0',
|
||||
'left-0 bottom-0 animate-delay-500',
|
||||
'right-0 top-0 animate-delay-1000',
|
||||
'right-0 bottom-0 animate-delay-1500'
|
||||
];
|
||||
|
||||
const logoWithClass = systemLogo.replace('<svg', `<svg class="size-128px text-primary"`);
|
||||
|
||||
const dot = loadingClasses
|
||||
.map(item => {
|
||||
return `<div class="absolute w-16px h-16px bg-primary rounded-8px animate-pulse ${item}"></div>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const loading = `
|
||||
<div class="fixed-center flex-col" style="${primaryColor}">
|
||||
${logoWithClass}
|
||||
<div class="w-56px h-56px my-36px">
|
||||
<div class="w-120px h-120px my-36px">
|
||||
<div class="relative h-full animate-spin">
|
||||
${dot}
|
||||
<img src="${SystemLogo}" width="120" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-28px font-500 text-#646464">${$t('system.title')}</h2>
|
||||
|
@ -30,6 +30,99 @@ export function createStaticRoutes() {
|
||||
};
|
||||
}
|
||||
|
||||
const dynamicConstantRoutes: ElegantRoute[] = [
|
||||
{
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
component: 'layout.base$view.home',
|
||||
meta: {
|
||||
title: 'home',
|
||||
i18nKey: 'route.home',
|
||||
icon: 'mdi:monitor-dashboard',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '403',
|
||||
path: '/403',
|
||||
component: 'layout.blank$view.403',
|
||||
meta: {
|
||||
title: '403',
|
||||
i18nKey: 'route.403',
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '404',
|
||||
path: '/404',
|
||||
component: 'layout.blank$view.404',
|
||||
meta: {
|
||||
title: '404',
|
||||
i18nKey: 'route.404',
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '500',
|
||||
path: '/500',
|
||||
component: 'layout.blank$view.500',
|
||||
meta: {
|
||||
title: '500',
|
||||
i18nKey: 'route.500',
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
|
||||
component: 'layout.blank$view.login',
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'login',
|
||||
i18nKey: 'route.login',
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'iframe-page',
|
||||
path: '/iframe-page/:url',
|
||||
component: 'layout.base$view.iframe-page',
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'iframe-page',
|
||||
i18nKey: 'route.iframe-page',
|
||||
constant: true,
|
||||
hideInMenu: true,
|
||||
keepAlive: true,
|
||||
icon: 'material-symbols:iframe-outline'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/** create routes when the auth route mode is static */
|
||||
export function createDynamicRoutes() {
|
||||
const constantRoutes: ElegantConstRoute[] = [];
|
||||
|
||||
const authRoutes: ElegantConstRoute[] = [];
|
||||
|
||||
[...customRoutes, ...dynamicConstantRoutes].forEach(item => {
|
||||
if (item.meta?.constant) {
|
||||
constantRoutes.push(item);
|
||||
} else {
|
||||
authRoutes.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
constantRoutes,
|
||||
authRoutes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auth vue routes
|
||||
*
|
||||
|
@ -1,48 +1,41 @@
|
||||
import { request } from '../request';
|
||||
|
||||
/** Get tenant list */
|
||||
export function fetchTenantList() {
|
||||
return request<Api.Auth.TenantList>({
|
||||
url: '/auth/tenant/list',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/** Get image code */
|
||||
export function fetchCaptchaCode() {
|
||||
return request<Api.Auth.CaptchaCode>({
|
||||
url: '/auth/code',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* @param userName User name
|
||||
* @param username User name
|
||||
* @param password Password
|
||||
*/
|
||||
export function fetchLogin(userName: string, password: string) {
|
||||
export function fetchLogin(data: Api.Auth.LoginData) {
|
||||
return request<Api.Auth.LoginToken>({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data: {
|
||||
userName,
|
||||
password
|
||||
}
|
||||
headers: {
|
||||
isToken: false,
|
||||
isEncrypt: true,
|
||||
repeatSubmit: false
|
||||
},
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/** Get user info */
|
||||
export function fetchGetUserInfo() {
|
||||
return request<Api.Auth.UserInfo>({ url: '/auth/getUserInfo' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh token
|
||||
*
|
||||
* @param refreshToken Refresh token
|
||||
*/
|
||||
export function fetchRefreshToken(refreshToken: string) {
|
||||
return request<Api.Auth.LoginToken>({
|
||||
url: '/auth/refreshToken',
|
||||
method: 'post',
|
||||
data: {
|
||||
refreshToken
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* return custom backend error
|
||||
*
|
||||
* @param code error code
|
||||
* @param msg error message
|
||||
*/
|
||||
export function fetchCustomBackendError(code: string, msg: string) {
|
||||
return request({ url: '/auth/error', params: { code, msg } });
|
||||
return request<Api.Auth.UserInfo>({ url: '/system/user/getInfo' });
|
||||
}
|
||||
|
@ -1,20 +1,7 @@
|
||||
import type { ElegantConstRoute } from '@elegant-router/types';
|
||||
import { request } from '../request';
|
||||
|
||||
/** get constant routes */
|
||||
export function fetchGetConstantRoutes() {
|
||||
return request<Api.Route.MenuRoute[]>({ url: '/route/getConstantRoutes' });
|
||||
}
|
||||
|
||||
/** get user routes */
|
||||
export function fetchGetUserRoutes() {
|
||||
return request<Api.Route.UserRoute>({ url: '/route/getUserRoutes' });
|
||||
}
|
||||
|
||||
/**
|
||||
* whether the route is exist
|
||||
*
|
||||
* @param routeName route name
|
||||
*/
|
||||
export function fetchIsRouteExist(routeName: string) {
|
||||
return request<boolean>({ url: '/route/isRouteExist', params: { routeName } });
|
||||
/** get routes */
|
||||
export function fetchGetRoutes() {
|
||||
return request<ElegantConstRoute[]>({ url: '/system/menu/getRouters' });
|
||||
}
|
||||
|
10
src/service/api/system/menu.ts
Normal file
10
src/service/api/system/menu.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { request } from '@/service/request';
|
||||
|
||||
/** get menu list */
|
||||
export function fetchGetMenuList(params?: Api.System.MenuSearchParams) {
|
||||
return request<Api.System.MenuList>({
|
||||
url: '/system/menu/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
@ -1,30 +1,50 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
|
||||
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { BACKEND_ERROR_CODE, createFlatRequest } from '@sa/axios';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { localStg, sessionStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { decryptBase64, decryptWithAes, encryptBase64, encryptWithAes, generateAesKey } from '@/utils/crypto';
|
||||
import { decrypt, encrypt } from '@/utils/jsencrypt';
|
||||
import { handleRefreshToken, showErrorMsg } from './shared';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
const encryptHeader = 'encrypt-key';
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
|
||||
export const request = createFlatRequest<App.Service.Response, RequestInstanceState>(
|
||||
{
|
||||
baseURL,
|
||||
headers: {
|
||||
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
||||
'axios-retry': {
|
||||
retries: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
|
||||
// 对应国际化资源文件后缀
|
||||
config.headers['Content-Language'] = 'zh_CN';
|
||||
|
||||
const isToken = config.headers?.isToken === false;
|
||||
|
||||
// set token
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
Object.assign(headers, { Authorization });
|
||||
if (token && !isToken) {
|
||||
config.headers.Clientid = import.meta.env.VITE_APP_CLIENT_ID;
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
Object.assign(headers, { Authorization });
|
||||
}
|
||||
|
||||
handleRepeatSubmit(config);
|
||||
|
||||
handleEncrypt(config);
|
||||
|
||||
// FormData数据去请求头Content-Type
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type'];
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
@ -98,6 +118,31 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
return null;
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
|
||||
// 加密后的 AES 秘钥
|
||||
const keyStr = response.headers[encryptHeader];
|
||||
// 加密
|
||||
if (keyStr && keyStr !== '') {
|
||||
const data = String(response.data);
|
||||
// 请求体 AES 解密
|
||||
const base64Str = decrypt(keyStr);
|
||||
// base64 解码 得到请求头的 AES 秘钥
|
||||
const aesKey = decryptBase64(base64Str.toString());
|
||||
// aesKey 解码 data
|
||||
const decryptData = decryptWithAes(data, aesKey);
|
||||
// 将结果 (得到的是 JSON 字符串) 转为 JSON
|
||||
response.data = JSON.parse(decryptData);
|
||||
}
|
||||
}
|
||||
|
||||
// 二进制数据则直接返回
|
||||
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
if (response.data.rows) {
|
||||
return response.data;
|
||||
}
|
||||
return response.data.data;
|
||||
},
|
||||
onError(error) {
|
||||
@ -129,44 +174,48 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
}
|
||||
);
|
||||
|
||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
|
||||
{
|
||||
baseURL: otherBaseURL.demo
|
||||
},
|
||||
{
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
function handleRepeatSubmit(config: InternalAxiosRequestConfig) {
|
||||
// 是否需要防止数据重复提交
|
||||
const isRepeatSubmit = config.headers?.repeatSubmit === false;
|
||||
|
||||
// set token
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
Object.assign(headers, { Authorization });
|
||||
|
||||
return config;
|
||||
},
|
||||
isBackendSuccess(response) {
|
||||
// when the backend response code is "200", it means the request is success
|
||||
// you can change this logic by yourself
|
||||
return response.data.status === '200';
|
||||
},
|
||||
async onBackendFail(_response) {
|
||||
// when the backend response code is not "200", it means the request is fail
|
||||
// for example: the token is expired, refresh token and retry request
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
return response.data.result;
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
|
||||
let message = error.message;
|
||||
|
||||
// show backend error message
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.message || message;
|
||||
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
||||
const requestObj = {
|
||||
url: config.url!,
|
||||
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
||||
time: new Date().getTime()
|
||||
};
|
||||
const sessionObj = sessionStg.get('sessionObj');
|
||||
if (!sessionObj) {
|
||||
sessionStg.set('sessionObj', requestObj);
|
||||
} else {
|
||||
const s_url = sessionObj.url; // 请求地址
|
||||
const s_data = sessionObj.data; // 请求数据
|
||||
const s_time = sessionObj.time; // 请求时间
|
||||
const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
|
||||
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
|
||||
const message = '数据正在处理,请勿重复提交';
|
||||
console.warn(`[${s_url}]: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
window.$message?.error(message);
|
||||
sessionStg.set('sessionObj', requestObj);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleEncrypt(config: InternalAxiosRequestConfig) {
|
||||
// 是否需要加密
|
||||
const isEncrypt = config.headers?.isEncrypt === 'true';
|
||||
|
||||
if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
|
||||
// 当开启参数加密
|
||||
if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
|
||||
// 生成一个 AES 密钥
|
||||
const aesKey = generateAesKey();
|
||||
config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
|
||||
config.data =
|
||||
typeof config.data === 'object'
|
||||
? encryptWithAes(JSON.stringify(config.data), aesKey)
|
||||
: encryptWithAes(config.data, aesKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { fetchRefreshToken } from '../api';
|
||||
// import { localStg } from '@/utils/storage';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
/**
|
||||
@ -9,22 +8,11 @@ import type { RequestInstanceState } from './type';
|
||||
*
|
||||
* @param axiosConfig - request config when the token is expired
|
||||
*/
|
||||
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
|
||||
export async function handleRefreshToken(_: AxiosRequestConfig) {
|
||||
const { resetStore } = useAuthStore();
|
||||
|
||||
const refreshToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(refreshToken);
|
||||
if (!error) {
|
||||
localStg.set('token', data.token);
|
||||
localStg.set('refreshToken', data.refreshToken);
|
||||
|
||||
const config = { ...axiosConfig };
|
||||
if (config.headers) {
|
||||
config.headers.Authorization = data.token;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
// request
|
||||
// const refreshToken = localStg.get('refreshToken') || '';
|
||||
|
||||
resetStore();
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { SetupStoreId } from '@/enum';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { fetchGetUserInfo, fetchLogin } from '@/service/api';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { $t } from '@/locales';
|
||||
// import { $t } from '@/locales';
|
||||
import { useRouteStore } from '../route';
|
||||
import { useTabStore } from '../tab';
|
||||
import { clearAuthStorage, getToken } from './shared';
|
||||
@ -21,10 +21,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const token = ref(getToken());
|
||||
|
||||
const userInfo: Api.Auth.UserInfo = reactive({
|
||||
userId: '',
|
||||
userName: '',
|
||||
user: undefined,
|
||||
roles: [],
|
||||
buttons: []
|
||||
permissions: []
|
||||
});
|
||||
|
||||
/** is super role in static route */
|
||||
@ -56,14 +55,20 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* @param userName User name
|
||||
* @param password Password
|
||||
* @param [redirect=true] Whether to redirect after login. Default is `true`
|
||||
*/
|
||||
async function login(userName: string, password: string, redirect = true) {
|
||||
async function login(loginForm: Api.Auth.LoginForm, redirect = true) {
|
||||
startLoading();
|
||||
|
||||
const { data: loginToken, error } = await fetchLogin(userName, password);
|
||||
const { VITE_APP_CLIENT_ID } = import.meta.env;
|
||||
|
||||
const loginData: Api.Auth.LoginData = {
|
||||
...loginForm,
|
||||
clientId: VITE_APP_CLIENT_ID!,
|
||||
grantType: 'password'
|
||||
};
|
||||
|
||||
const { data: loginToken, error } = await fetchLogin(loginData);
|
||||
|
||||
if (!error) {
|
||||
const pass = await loginByToken(loginToken);
|
||||
@ -76,11 +81,11 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
}
|
||||
|
||||
if (routeStore.isInitAuthRoute) {
|
||||
window.$notification?.success({
|
||||
title: $t('page.login.common.loginSuccess'),
|
||||
content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }),
|
||||
duration: 4500
|
||||
});
|
||||
// window.$notification?.success({
|
||||
// title: $t('page.login.common.loginSuccess'),
|
||||
// content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }),
|
||||
// duration: 4500
|
||||
// });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -88,18 +93,20 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
}
|
||||
|
||||
endLoading();
|
||||
|
||||
return error ? Promise.reject(error) : Promise.resolve();
|
||||
}
|
||||
|
||||
async function loginByToken(loginToken: Api.Auth.LoginToken) {
|
||||
// 1. stored in the localStorage, the later requests need it in headers
|
||||
localStg.set('token', loginToken.token);
|
||||
localStg.set('refreshToken', loginToken.refreshToken);
|
||||
localStg.set('token', loginToken.access_token);
|
||||
localStg.set('refreshToken', loginToken.refresh_token);
|
||||
|
||||
// 2. get user info
|
||||
const pass = await getUserInfo();
|
||||
|
||||
if (pass) {
|
||||
token.value = loginToken.token;
|
||||
token.value = loginToken.access_token;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
43
src/store/modules/notice/index.ts
Normal file
43
src/store/modules/notice/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
interface NoticeItem {
|
||||
title?: string;
|
||||
read: boolean;
|
||||
message: any;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export const useNoticeStore = defineStore('notice', () => {
|
||||
const state = reactive({
|
||||
notices: [] as NoticeItem[]
|
||||
});
|
||||
|
||||
const addNotice = (notice: NoticeItem) => {
|
||||
state.notices.push(notice);
|
||||
};
|
||||
|
||||
const removeNotice = (notice: NoticeItem) => {
|
||||
state.notices.splice(state.notices.indexOf(notice), 1);
|
||||
};
|
||||
|
||||
// 实现全部已读
|
||||
const readAll = () => {
|
||||
state.notices.forEach((item: any) => {
|
||||
item.read = true;
|
||||
});
|
||||
};
|
||||
|
||||
const clearNotice = () => {
|
||||
state.notices = [];
|
||||
};
|
||||
return {
|
||||
state,
|
||||
addNotice,
|
||||
removeNotice,
|
||||
readAll,
|
||||
clearNotice
|
||||
};
|
||||
});
|
||||
|
||||
export default useNoticeStore;
|
@ -5,10 +5,10 @@ import { useBoolean } from '@sa/hooks';
|
||||
import type { CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey, RouteMap } from '@elegant-router/types';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { router } from '@/router';
|
||||
import { createStaticRoutes, getAuthVueRoutes } from '@/router/routes';
|
||||
import { createDynamicRoutes, createStaticRoutes, getAuthVueRoutes } from '@/router/routes';
|
||||
import { ROOT_ROUTE } from '@/router/routes/builtin';
|
||||
import { getRouteName, getRoutePath } from '@/router/elegant/transform';
|
||||
import { fetchGetConstantRoutes, fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
|
||||
import { fetchGetRoutes } from '@/service/api';
|
||||
import { useAppStore } from '../app';
|
||||
import { useAuthStore } from '../auth';
|
||||
import { useTabStore } from '../tab';
|
||||
@ -75,6 +75,17 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
authRoutesMap.set(route.name, route);
|
||||
});
|
||||
|
||||
const dynamicRoutes = createDynamicRoutes();
|
||||
|
||||
dynamicRoutes.authRoutes.forEach(route => {
|
||||
const parent = authRoutesMap.get(route.name);
|
||||
if (parent && route.children) {
|
||||
parent.children?.push(...route.children);
|
||||
return;
|
||||
}
|
||||
authRoutesMap.set(route.name, route);
|
||||
});
|
||||
|
||||
authRoutes.value = Array.from(authRoutesMap.values());
|
||||
}
|
||||
|
||||
@ -201,14 +212,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
if (authRouteMode.value === 'static') {
|
||||
addConstantRoutes(staticRoute.constantRoutes);
|
||||
} else {
|
||||
const { data, error } = await fetchGetConstantRoutes();
|
||||
|
||||
if (!error) {
|
||||
addConstantRoutes(data);
|
||||
} else {
|
||||
// if fetch constant routes failed, use static constant routes
|
||||
addConstantRoutes(staticRoute.constantRoutes);
|
||||
}
|
||||
addConstantRoutes(staticRoute.constantRoutes);
|
||||
}
|
||||
|
||||
handleConstantAndAuthRoutes();
|
||||
@ -246,18 +250,16 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
/** Init dynamic auth route */
|
||||
async function initDynamicAuthRoute() {
|
||||
const { data, error } = await fetchGetUserRoutes();
|
||||
const { data, error } = await fetchGetRoutes();
|
||||
|
||||
if (!error) {
|
||||
const { routes, home } = data;
|
||||
|
||||
addAuthRoutes(routes);
|
||||
addAuthRoutes(data);
|
||||
|
||||
handleConstantAndAuthRoutes();
|
||||
|
||||
setRouteHome(home);
|
||||
setRouteHome('home');
|
||||
|
||||
handleUpdateRootRouteRedirect(home);
|
||||
handleUpdateRootRouteRedirect('home');
|
||||
|
||||
setIsInitAuthRoute(true);
|
||||
} else {
|
||||
@ -335,14 +337,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authRouteMode.value === 'static') {
|
||||
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
|
||||
return isRouteExistByRouteName(routeName, staticAuthRoutes);
|
||||
}
|
||||
|
||||
const { data } = await fetchIsRouteExist(routeName);
|
||||
|
||||
return data;
|
||||
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
|
||||
return isRouteExistByRouteName(routeName, staticAuthRoutes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
grayscale: false,
|
||||
colourWeakness: false,
|
||||
recommendColor: false,
|
||||
themeColor: '#646cff',
|
||||
themeColor: '#2080f0',
|
||||
otherColor: {
|
||||
info: '#2080f0',
|
||||
success: '#52c41a',
|
||||
|
65
src/typings/api.d.ts → src/typings/api/api.d.ts
vendored
65
src/typings/api.d.ts → src/typings/api/api.d.ts
vendored
@ -8,40 +8,38 @@ declare namespace Api {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** current page number */
|
||||
current: number;
|
||||
pageNum: number;
|
||||
/** page size */
|
||||
size: number;
|
||||
pageSize: number;
|
||||
/** total count */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
records: T[];
|
||||
rows: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
* - "0": enabled
|
||||
* - "1": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2';
|
||||
type EnableStatus = '0' | '1';
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** record id */
|
||||
id: number;
|
||||
/** record creator */
|
||||
createBy: string;
|
||||
/** record dept */
|
||||
createDept?: any;
|
||||
/** record create time */
|
||||
createTime: string;
|
||||
/** record updater */
|
||||
updateBy: string;
|
||||
/** record update time */
|
||||
updateTime: string;
|
||||
/** record status */
|
||||
status: EnableStatus | null;
|
||||
} & T;
|
||||
}
|
||||
|
||||
@ -51,16 +49,53 @@ declare namespace Api {
|
||||
* backend api module: "auth"
|
||||
*/
|
||||
namespace Auth {
|
||||
interface LoginData {
|
||||
tenantId?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
rememberMe?: boolean;
|
||||
socialCode?: string;
|
||||
socialState?: string;
|
||||
source?: string;
|
||||
code?: string;
|
||||
uuid?: string;
|
||||
clientId: string;
|
||||
grantType: string;
|
||||
}
|
||||
|
||||
type LoginForm = Pick<LoginData, 'tenantId' | 'username' | 'password' | 'rememberMe' | 'code' | 'uuid'>;
|
||||
|
||||
interface LoginToken {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
access_token: string;
|
||||
client_id: string;
|
||||
expire_in: number;
|
||||
openid: string;
|
||||
refresh_expire_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
userName: string;
|
||||
user?: Api.System.User;
|
||||
roles: string[];
|
||||
buttons: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface Tenant {
|
||||
companyName: string;
|
||||
domain: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
interface TenantList {
|
||||
tenantEnabled: boolean;
|
||||
voList: Tenant[];
|
||||
}
|
||||
|
||||
interface CaptchaCode {
|
||||
captchaEnabled: boolean;
|
||||
uuid?: string;
|
||||
img?: string;
|
||||
}
|
||||
}
|
||||
|
123
src/typings/api/system.api.d.ts
vendored
Normal file
123
src/typings/api/system.api.d.ts
vendored
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Namespace Api
|
||||
*
|
||||
* All backend api type
|
||||
*/
|
||||
declare namespace Api {
|
||||
/**
|
||||
* namespace System
|
||||
*
|
||||
* backend api module: "system"
|
||||
*/
|
||||
namespace System {
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'pageNum' | 'pageSize'>;
|
||||
|
||||
/** role */
|
||||
type Role = Common.CommonRecord<{
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort: number;
|
||||
dataScope: string;
|
||||
menuCheckStrictly: boolean;
|
||||
deptCheckStrictly: boolean;
|
||||
status: string;
|
||||
delFlag: string;
|
||||
remark?: any;
|
||||
flag: boolean;
|
||||
menuIds?: Array<string | number>;
|
||||
deptIds?: Array<string | number>;
|
||||
admin: boolean;
|
||||
}>;
|
||||
|
||||
/** role search params */
|
||||
type RoleSearchParams = CommonType.RecordNullable<
|
||||
Pick<Role, 'roleName' | 'roleKey' | 'status'> & CommonSearchParams
|
||||
>;
|
||||
|
||||
/** role list */
|
||||
type RoleList = Common.PaginatingQueryRecord<Role>;
|
||||
|
||||
/** all role */
|
||||
type AllRole = Pick<Role, 'roleId' | 'roleName' | 'roleKey'>;
|
||||
|
||||
/**
|
||||
* user gender
|
||||
*
|
||||
* - "1": "male"
|
||||
* - "2": "female"
|
||||
*/
|
||||
type UserGender = '1' | '2';
|
||||
|
||||
/** user */
|
||||
type User = Common.CommonRecord<{
|
||||
userId: string | number;
|
||||
deptId: number;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
userType: string;
|
||||
email: string;
|
||||
phonenumber: string;
|
||||
sex: string;
|
||||
avatar: string;
|
||||
status: string;
|
||||
delFlag: string;
|
||||
loginIp: string;
|
||||
loginDate: string;
|
||||
remark: string;
|
||||
deptName: string;
|
||||
roles: Role[];
|
||||
roleIds: any;
|
||||
postIds: any;
|
||||
roleId: any;
|
||||
admin: boolean;
|
||||
}>;
|
||||
|
||||
/** user search params */
|
||||
type UserSearchParams = CommonType.RecordNullable<
|
||||
Pick<User, 'userName' | 'sex' | 'nickName' | 'phonenumber' | 'email' | 'status'> & CommonSearchParams
|
||||
>;
|
||||
|
||||
/** user list */
|
||||
type UserList = Common.PaginatingQueryRecord<User>;
|
||||
|
||||
/**
|
||||
* menu type
|
||||
*
|
||||
* - "M": "目录"
|
||||
* - "C": "菜单"
|
||||
* - "F": "按钮"
|
||||
*/
|
||||
type MenuType = 'M' | 'C' | 'F';
|
||||
|
||||
type Menu = Common.CommonRecord<
|
||||
{
|
||||
parentName: string;
|
||||
parentId: string | number;
|
||||
children: Menu[];
|
||||
menuId: string | number;
|
||||
menuName: string;
|
||||
orderNum: number;
|
||||
path: string;
|
||||
component: string;
|
||||
queryParam: string;
|
||||
isFrame: string;
|
||||
isCache: string;
|
||||
menuType: MenuType;
|
||||
visible: string;
|
||||
status: Common.EnableStatus;
|
||||
perms: string;
|
||||
icon: string;
|
||||
componentInfo: string;
|
||||
remark: string;
|
||||
keywords?: string;
|
||||
} & Pick<import('vue-router').RouteMeta, 'i18nKey'>
|
||||
>;
|
||||
|
||||
/** menu list */
|
||||
type MenuList = Common.PaginatingQueryRecord<Menu>;
|
||||
|
||||
type MenuSearchParams = CommonType.RecordNullable<Pick<Api.System.Menu, 'menuName' | 'status' | 'keywords'>> &
|
||||
CommonSearchParams;
|
||||
}
|
||||
}
|
3
src/typings/app.d.ts
vendored
3
src/typings/app.d.ts
vendored
@ -532,6 +532,7 @@ declare namespace App {
|
||||
baseURL: string;
|
||||
/** The proxy pattern of the backend service base url */
|
||||
proxyPattern: string;
|
||||
ws?: boolean;
|
||||
}
|
||||
|
||||
interface OtherServiceConfigItem extends ServiceConfigItem {
|
||||
@ -556,6 +557,8 @@ declare namespace App {
|
||||
msg: string;
|
||||
/** The backend service response data */
|
||||
data: T;
|
||||
rows?: any[];
|
||||
total?: number;
|
||||
};
|
||||
|
||||
/** The demo backend service response data */
|
||||
|
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@ -76,6 +76,7 @@ declare module 'vue' {
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
|
5
src/typings/env.d.ts
vendored
5
src/typings/env.d.ts
vendored
@ -103,6 +103,11 @@ declare namespace Env {
|
||||
readonly VITE_ICONIFY_URL?: string;
|
||||
/** Used to differentiate storage across different domains */
|
||||
readonly VITE_STORAGE_PREFIX?: string;
|
||||
readonly VITE_APP_CLIENT_ID?: string;
|
||||
readonly VITE_APP_ENCRYPT?: string;
|
||||
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
|
||||
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
|
||||
readonly VITE_APP_WEBSOCKET: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
5
src/typings/storage.d.ts
vendored
5
src/typings/storage.d.ts
vendored
@ -7,6 +7,11 @@ declare namespace StorageType {
|
||||
// * the theme settings
|
||||
// */
|
||||
// themeSettings: App.Theme.ThemeSetting;
|
||||
sessionObj: {
|
||||
url: string;
|
||||
data: any;
|
||||
time: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Local {
|
||||
|
70
src/utils/crypto.ts
Normal file
70
src/utils/crypto.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
/**
|
||||
* 随机生成32位的字符串
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
const generateRandomString = () => {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < 32; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机生成aes 密钥
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export const generateAesKey = () => {
|
||||
return CryptoJS.enc.Utf8.parse(generateRandomString());
|
||||
};
|
||||
|
||||
/**
|
||||
* 加密base64
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export const encryptBase64 = (str: CryptoJS.lib.WordArray) => {
|
||||
return CryptoJS.enc.Base64.stringify(str);
|
||||
};
|
||||
|
||||
/** 解密base64 */
|
||||
export const decryptBase64 = (str: string) => {
|
||||
return CryptoJS.enc.Base64.parse(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行加密
|
||||
*
|
||||
* @param message
|
||||
* @param aesKey
|
||||
* @returns {string}
|
||||
*/
|
||||
export const 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();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行解密
|
||||
*
|
||||
* @param message
|
||||
* @param aesKey
|
||||
* @returns {string}
|
||||
*/
|
||||
export const 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);
|
||||
};
|
21
src/utils/jsencrypt.ts
Normal file
21
src/utils/jsencrypt.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
// 密钥对生成 http://web.chacuo.net/netrsakeypair
|
||||
|
||||
const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY;
|
||||
|
||||
// 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大
|
||||
const privateKey = import.meta.env.VITE_APP_RSA_PRIVATE_KEY;
|
||||
|
||||
// 加密
|
||||
export const encrypt = (txt: string) => {
|
||||
const encryptor = new JSEncrypt();
|
||||
encryptor.setPublicKey(publicKey!); // 设置公钥
|
||||
return encryptor.encrypt(txt); // 对数据进行加密
|
||||
};
|
||||
|
||||
// 解密
|
||||
export const decrypt = (txt: string) => {
|
||||
const encryptor = new JSEncrypt();
|
||||
encryptor.setPrivateKey(privateKey!); // 设置私钥
|
||||
return encryptor.decrypt(txt); // 对数据进行解密
|
||||
};
|
@ -4,11 +4,11 @@
|
||||
* @param env The current env
|
||||
*/
|
||||
export function createServiceConfig(env: Env.ImportMeta) {
|
||||
const { VITE_SERVICE_BASE_URL, VITE_OTHER_SERVICE_BASE_URL } = env;
|
||||
const { VITE_SERVICE_BASE_URL, VITE_OTHER_SERVICE_BASE_URL, VITE_APP_BASE_API, VITE_APP_WEBSOCKET } = env;
|
||||
|
||||
let other = {} as Record<App.Service.OtherBaseURLKey, string>;
|
||||
try {
|
||||
other = JSON.parse(VITE_OTHER_SERVICE_BASE_URL);
|
||||
other = JSON.parse(VITE_OTHER_SERVICE_BASE_URL || '{}');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid JSON string');
|
||||
@ -24,6 +24,7 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
const otherConfig: App.Service.OtherServiceConfigItem[] = otherHttpKeys.map(key => {
|
||||
return {
|
||||
key,
|
||||
ws: false,
|
||||
baseURL: httpConfig.other[key],
|
||||
proxyPattern: createProxyPattern(key)
|
||||
};
|
||||
@ -31,7 +32,8 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
|
||||
const config: App.Service.ServiceConfig = {
|
||||
baseURL: httpConfig.baseURL,
|
||||
proxyPattern: createProxyPattern(),
|
||||
ws: VITE_APP_WEBSOCKET === 'true',
|
||||
proxyPattern: VITE_APP_BASE_API,
|
||||
other: otherConfig
|
||||
};
|
||||
|
||||
@ -45,7 +47,7 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
* @param isProxy - if use proxy
|
||||
*/
|
||||
export function getServiceBaseURL(env: Env.ImportMeta, isProxy: boolean) {
|
||||
const { baseURL, other } = createServiceConfig(env);
|
||||
const { baseURL, other, proxyPattern } = createServiceConfig(env);
|
||||
|
||||
const otherBaseURL = {} as Record<App.Service.OtherBaseURLKey, string>;
|
||||
|
||||
@ -54,7 +56,7 @@ export function getServiceBaseURL(env: Env.ImportMeta, isProxy: boolean) {
|
||||
});
|
||||
|
||||
return {
|
||||
baseURL: isProxy ? createProxyPattern() : baseURL,
|
||||
baseURL: isProxy ? proxyPattern : baseURL,
|
||||
otherBaseURL
|
||||
};
|
||||
}
|
||||
@ -64,10 +66,6 @@ export function getServiceBaseURL(env: Env.ImportMeta, isProxy: boolean) {
|
||||
*
|
||||
* @param key If not set, will use the default key
|
||||
*/
|
||||
function createProxyPattern(key?: App.Service.OtherBaseURLKey) {
|
||||
if (!key) {
|
||||
return '/proxy-default';
|
||||
}
|
||||
|
||||
function createProxyPattern(key: App.Service.OtherBaseURLKey) {
|
||||
return `/proxy-${key}`;
|
||||
}
|
||||
|
139
src/utils/websocket.ts
Normal file
139
src/utils/websocket.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* socket 通信
|
||||
*
|
||||
* @module initWebSocket 初始化
|
||||
* @module websocketonopen 连接成功
|
||||
* @module websocketonerror 连接失败
|
||||
* @module websocketclose 断开连接
|
||||
* @module resetHeart 重置心跳
|
||||
* @module sendSocketHeart 心跳发送
|
||||
* @module reconnect 重连
|
||||
* @module sendMsg 发送数据
|
||||
* @module websocketonmessage 接收数据
|
||||
* @module test 测试收到消息传递
|
||||
* @param {any} url socket地址
|
||||
* @param {any} websocket websocket 实例
|
||||
* @param {any} heartTime 心跳定时器实例
|
||||
* @param {number} socketHeart 心跳次数
|
||||
* @param {number} HeartTimeOut 心跳超时时间
|
||||
* @param {number} socketError 错误次数
|
||||
*/
|
||||
|
||||
import useNoticeStore from '@/store/modules/notice';
|
||||
import { localStg } from './storage';
|
||||
|
||||
let socketUrl: any = ''; // socket地址
|
||||
let websocket: any = null; // websocket 实例
|
||||
let heartTime: any = null; // 心跳定时器实例
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let socketHeart = 0; // 心跳次数
|
||||
const HeartTimeOut = 10000; // 心跳超时时间 10000 = 10s
|
||||
let socketError = 0; // 错误次数
|
||||
|
||||
// 初始化socket
|
||||
export function initWebSocket(url: any) {
|
||||
if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
|
||||
return null;
|
||||
}
|
||||
socketUrl = url;
|
||||
// 初始化 websocket
|
||||
const token = localStg.get('token');
|
||||
websocket = new WebSocket(`${url}?Authorization=Bearer ${token}&clientid=${import.meta.env.VITE_APP_CLIENT_ID}`);
|
||||
websocketonopen();
|
||||
websocketonmessage();
|
||||
websocketonerror();
|
||||
websocketclose();
|
||||
sendSocketHeart();
|
||||
return websocket;
|
||||
}
|
||||
|
||||
// socket 连接成功
|
||||
export function websocketonopen() {
|
||||
websocket.onopen = () => {
|
||||
console.log('连接 websocket 成功');
|
||||
resetHeart();
|
||||
};
|
||||
}
|
||||
|
||||
// socket 连接失败
|
||||
export function websocketonerror() {
|
||||
websocket.onerror = (e: any) => {
|
||||
console.log('连接 websocket 失败', e);
|
||||
};
|
||||
}
|
||||
|
||||
// socket 断开链接
|
||||
export function websocketclose() {
|
||||
websocket.onclose = (e: any) => {
|
||||
console.log('断开连接', e);
|
||||
};
|
||||
}
|
||||
|
||||
// socket 重置心跳
|
||||
export function resetHeart() {
|
||||
socketHeart = 0;
|
||||
socketError = 0;
|
||||
clearInterval(heartTime);
|
||||
sendSocketHeart();
|
||||
}
|
||||
|
||||
// socket心跳发送
|
||||
export function sendSocketHeart() {
|
||||
heartTime = setInterval(() => {
|
||||
// 如果连接正常则发送心跳
|
||||
if (websocket.readyState === 1) {
|
||||
// if (socketHeart <= 30) {
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
type: 'ping'
|
||||
})
|
||||
);
|
||||
socketHeart += 1;
|
||||
} else {
|
||||
// 重连
|
||||
reconnect();
|
||||
}
|
||||
}, HeartTimeOut);
|
||||
}
|
||||
|
||||
// socket重连
|
||||
export function reconnect() {
|
||||
if (socketError <= 2) {
|
||||
clearInterval(heartTime);
|
||||
initWebSocket(socketUrl);
|
||||
socketError += 1;
|
||||
console.log('socket重连', socketError);
|
||||
} else {
|
||||
console.log('重试次数已用完');
|
||||
clearInterval(heartTime);
|
||||
}
|
||||
}
|
||||
|
||||
// socket 发送数据
|
||||
export function sendMsg(data: any) {
|
||||
websocket.send(data);
|
||||
}
|
||||
|
||||
// socket 接收数据
|
||||
export function websocketonmessage() {
|
||||
websocket.onmessage = (e: any) => {
|
||||
if (e.data.indexOf('heartbeat') > 0) {
|
||||
resetHeart();
|
||||
}
|
||||
if (e.data.indexOf('ping') > 0) {
|
||||
return null;
|
||||
}
|
||||
useNoticeStore().addNotice({
|
||||
message: e.data,
|
||||
read: false,
|
||||
time: new Date().toLocaleString()
|
||||
});
|
||||
window.$notification?.create({
|
||||
title: '消息',
|
||||
content: e.data,
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
});
|
||||
return e.data;
|
||||
};
|
||||
}
|
@ -56,7 +56,7 @@ const bgColor = computed(() => {
|
||||
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
|
||||
<div class="w-400px lt-sm:w-300px">
|
||||
<header class="flex-y-center justify-between">
|
||||
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
|
||||
<SystemLogo class="w-64px text-primary lt-sm:text-48px" />
|
||||
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
|
||||
<div class="i-flex-col">
|
||||
<ThemeSchemaSwitch
|
||||
|
@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { $t } from '@/locales';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { fetchCaptchaCode, fetchTenantList } from '@/service/api';
|
||||
|
||||
defineOptions({
|
||||
name: 'PwdLogin'
|
||||
@ -13,71 +16,83 @@ defineOptions({
|
||||
const authStore = useAuthStore();
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
const { loading: codeLoading, startLoading: startCodeLoading, endLoading: endCodeLoading } = useLoading();
|
||||
|
||||
interface FormModel {
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
const codeUrl = ref<string>();
|
||||
const captchaEnabled = ref<boolean>(false);
|
||||
const tenantEnabled = ref<boolean>(false);
|
||||
const tenantOption = ref<SelectOption[]>([]);
|
||||
|
||||
const model: FormModel = reactive({
|
||||
userName: 'Soybean',
|
||||
password: '123456'
|
||||
const model: Api.Auth.LoginForm = reactive({
|
||||
tenantId: '000000',
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
remberMe: false
|
||||
});
|
||||
|
||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||
const rules = computed<Record<keyof Api.Auth.LoginForm, App.Global.FormRule[]>>(() => {
|
||||
// inside computed to make locale reactive, if not apply i18n, you can define it without computed
|
||||
const { formRules } = useFormRules();
|
||||
const { formRules, createRequiredRule } = useFormRules();
|
||||
|
||||
return {
|
||||
userName: formRules.userName,
|
||||
password: formRules.pwd
|
||||
const loginRules: Record<keyof Api.Auth.LoginForm, App.Global.FormRule[]> = {
|
||||
username: formRules.userName,
|
||||
password: formRules.pwd,
|
||||
code: captchaEnabled.value ? [createRequiredRule($t('form.code.required'))] : [],
|
||||
tenantId: tenantEnabled.value ? formRules.tenantId : [],
|
||||
rememberMe: [],
|
||||
uuid: []
|
||||
};
|
||||
|
||||
return loginRules;
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
await authStore.login(model.userName, model.password);
|
||||
}
|
||||
|
||||
type AccountKey = 'super' | 'admin' | 'user';
|
||||
|
||||
interface Account {
|
||||
key: AccountKey;
|
||||
label: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
{
|
||||
key: 'super',
|
||||
label: $t('page.login.pwdLogin.superAdmin'),
|
||||
userName: 'Super',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: $t('page.login.pwdLogin.admin'),
|
||||
userName: 'Admin',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: $t('page.login.pwdLogin.user'),
|
||||
userName: 'User',
|
||||
password: '123456'
|
||||
try {
|
||||
await authStore.login(model);
|
||||
} catch (error) {
|
||||
handleFetchCaptchaCode();
|
||||
}
|
||||
]);
|
||||
|
||||
async function handleAccountLogin(account: Account) {
|
||||
await authStore.login(account.userName, account.password);
|
||||
}
|
||||
|
||||
async function handleFetchTenantList() {
|
||||
const { data, error } = await fetchTenantList();
|
||||
if (!error) {
|
||||
tenantEnabled.value = data.tenantEnabled;
|
||||
tenantOption.value = data.voList.map(tenant => {
|
||||
return {
|
||||
label: tenant.companyName,
|
||||
value: tenant.tenantId
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleFetchTenantList();
|
||||
|
||||
async function handleFetchCaptchaCode() {
|
||||
startCodeLoading();
|
||||
const { data, error } = await fetchCaptchaCode();
|
||||
if (!error) {
|
||||
captchaEnabled.value = data.captchaEnabled;
|
||||
if (data.captchaEnabled) {
|
||||
model.uuid = data.uuid;
|
||||
codeUrl.value = `data:image/gif;base64,${data.img}`;
|
||||
}
|
||||
}
|
||||
endCodeLoading();
|
||||
}
|
||||
|
||||
handleFetchCaptchaCode();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<NFormItem path="userName">
|
||||
<NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
|
||||
<NFormItem v-if="tenantEnabled" path="tenantId">
|
||||
<NSelect v-model:value="model.tenantId" placeholder="请选择/输入公司名称" :options="tenantOption" />
|
||||
</NFormItem>
|
||||
<NFormItem path="username">
|
||||
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.usernamePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="password">
|
||||
<NInput
|
||||
@ -87,6 +102,17 @@ async function handleAccountLogin(account: Account) {
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="captchaEnabled" path="code">
|
||||
<div class="w-full flex-y-center gap-16px">
|
||||
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<NSpin :show="codeLoading" :size="28" class="h-42px">
|
||||
<NButton :focusable="false" class="login-code h-42px w-116px" @click="handleFetchCaptchaCode">
|
||||
<img v-if="codeUrl" :src="codeUrl" />
|
||||
<NEmpty v-else :show-icon="false" description="暂无验证码" />
|
||||
</NButton>
|
||||
</NSpin>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="24">
|
||||
<div class="flex-y-center justify-between">
|
||||
<NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
|
||||
@ -105,14 +131,19 @@ async function handleAccountLogin(account: Account) {
|
||||
{{ $t(loginModuleRecord.register) }}
|
||||
</NButton>
|
||||
</div>
|
||||
<NDivider class="text-14px text-#666 !m-0">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</NDivider>
|
||||
<div class="flex-center gap-12px">
|
||||
<NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">
|
||||
{{ item.label }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.login-code {
|
||||
&.n-button {
|
||||
--n-padding: 0 8px !important;
|
||||
background-color: #c0c0c0;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -48,7 +48,7 @@ const statisticData = computed<StatisticData[]>(() => [
|
||||
</div>
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">
|
||||
{{ $t('page.home.greeting', { userName: authStore.userInfo.userName }) }}
|
||||
{{ $t('page.home.greeting', { userName: authStore.userInfo.user?.userName }) }}
|
||||
</h3>
|
||||
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user