mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-23 23:39:47 +08:00
feat: 整合 sse 推送
This commit is contained in:
@ -1,15 +1,16 @@
|
||||
# backend service base url, test environment
|
||||
VITE_SERVICE_BASE_URL=http://127.0.0.1:8080
|
||||
VITE_SERVICE_BASE_URL=http://154.44.10.176:8080
|
||||
|
||||
VITE_APP_BASE_API=/dev-api
|
||||
|
||||
VITE_APP_WEBSOCKET=true
|
||||
VITE_APP_WEBSOCKET=N
|
||||
VITE_APP_SSE=Y
|
||||
|
||||
# app client id
|
||||
VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
|
||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||
VITE_APP_ENCRYPT=true
|
||||
VITE_APP_ENCRYPT=Y
|
||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
@ -38,7 +38,10 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
apiParams,
|
||||
columns: config.columns,
|
||||
transformer: res => {
|
||||
const { rows: records = [], pageNum: current = 1, pageSize: size = 10, total = 0 } = res.data || {};
|
||||
const { rows: records = [], total = 0 } = res.data || {};
|
||||
|
||||
const current = searchParams.pageNum as number;
|
||||
const size = (searchParams.pageSize || 0) as number;
|
||||
|
||||
// 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;
|
||||
@ -124,8 +127,8 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
pagination.page = page;
|
||||
|
||||
updateSearchParams({
|
||||
current: page,
|
||||
size: pagination.pageSize!
|
||||
pageNum: page,
|
||||
pageSize: pagination.pageSize!
|
||||
});
|
||||
|
||||
getData();
|
||||
@ -135,8 +138,8 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
pagination.page = 1;
|
||||
|
||||
updateSearchParams({
|
||||
current: pagination.page,
|
||||
size: pageSize
|
||||
pageNum: pagination.page,
|
||||
pageSize
|
||||
});
|
||||
|
||||
getData();
|
||||
@ -174,8 +177,8 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
});
|
||||
|
||||
updateSearchParams({
|
||||
current: pageNum,
|
||||
size: pagination.pageSize!
|
||||
pageNum,
|
||||
pageSize: pagination.pageSize!
|
||||
});
|
||||
|
||||
await getData();
|
||||
|
@ -5,6 +5,7 @@ import type { LayoutMode } from '@sa/materials';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { initWebSocket } from '@/utils/websocket';
|
||||
import { initSSE } from '@/utils/sse';
|
||||
import GlobalHeader from '../modules/global-header/index.vue';
|
||||
import GlobalSider from '../modules/global-sider/index.vue';
|
||||
import GlobalTab from '../modules/global-tab/index.vue';
|
||||
@ -105,6 +106,7 @@ function getSiderCollapsedWidth() {
|
||||
onMounted(() => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
initWebSocket(`${protocol + window.location.host + import.meta.env.VITE_APP_BASE_API}/resource/websocket`);
|
||||
initSSE(`${import.meta.env.VITE_APP_BASE_API}/resource/sse`);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -50,7 +50,7 @@ function logout() {
|
||||
positiveText: $t('common.confirm'),
|
||||
negativeText: $t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
authStore.resetStore();
|
||||
authStore.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -39,3 +39,17 @@ export function fetchLogin(data: Api.Auth.LoginData) {
|
||||
export function fetchGetUserInfo() {
|
||||
return request<Api.Auth.UserInfo>({ url: '/system/user/getInfo' });
|
||||
}
|
||||
|
||||
/** Logout */
|
||||
export function fetchLogout() {
|
||||
if (import.meta.env.VITE_APP_SSE === 'Y') {
|
||||
request({
|
||||
url: '/resource/sse/close',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
return request({
|
||||
url: '/auth/logout',
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
const responseCode = String(response.data.code);
|
||||
|
||||
function handleLogout() {
|
||||
authStore.resetStore();
|
||||
authStore.logout();
|
||||
}
|
||||
|
||||
function logoutAndCleanup() {
|
||||
@ -118,7 +118,7 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
return null;
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
|
||||
if (import.meta.env.VITE_APP_ENCRYPT === 'Y') {
|
||||
// 加密后的 AES 秘钥
|
||||
const keyStr = response.headers[encryptHeader];
|
||||
// 加密
|
||||
@ -194,6 +194,7 @@ function handleRepeatSubmit(config: InternalAxiosRequestConfig) {
|
||||
const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
|
||||
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
|
||||
const message = '数据正在处理,请勿重复提交';
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[${s_url}]: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
@ -206,7 +207,7 @@ function handleEncrypt(config: InternalAxiosRequestConfig) {
|
||||
// 是否需要加密
|
||||
const isEncrypt = config.headers?.isEncrypt === 'true';
|
||||
|
||||
if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
|
||||
if (import.meta.env.VITE_APP_ENCRYPT === 'Y') {
|
||||
// 当开启参数加密
|
||||
if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
|
||||
// 生成一个 AES 密钥
|
||||
|
@ -4,7 +4,7 @@ import { defineStore } from 'pinia';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { fetchGetUserInfo, fetchLogin } from '@/service/api';
|
||||
import { fetchGetUserInfo, fetchLogin, fetchLogout } from '@/service/api';
|
||||
import { localStg } from '@/utils/storage';
|
||||
// import { $t } from '@/locales';
|
||||
import { useRouteStore } from '../route';
|
||||
@ -52,6 +52,11 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
routeStore.resetStore();
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetchLogout();
|
||||
resetStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
@ -147,6 +152,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
loginLoading,
|
||||
resetStore,
|
||||
login,
|
||||
logout,
|
||||
initUserInfo
|
||||
};
|
||||
});
|
||||
|
@ -264,7 +264,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
setIsInitAuthRoute(true);
|
||||
} else {
|
||||
// if fetch user routes failed, reset store
|
||||
authStore.resetStore();
|
||||
authStore.logout();
|
||||
}
|
||||
}
|
||||
|
||||
|
2
src/typings/api/api.d.ts
vendored
2
src/typings/api/api.d.ts
vendored
@ -21,7 +21,7 @@ declare namespace Api {
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'pageNum' | 'pageSize'>;
|
||||
|
||||
/**
|
||||
* enable status
|
||||
|
5
src/typings/env.d.ts
vendored
5
src/typings/env.d.ts
vendored
@ -104,10 +104,11 @@ declare namespace Env {
|
||||
/** 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_ENCRYPT?: CommonType.YesOrNo;
|
||||
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
|
||||
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
|
||||
readonly VITE_APP_WEBSOCKET: string;
|
||||
readonly VITE_APP_WEBSOCKET: CommonType.YesOrNo;
|
||||
readonly VITE_APP_SSE: CommonType.YesOrNo;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
|
||||
const config: App.Service.ServiceConfig = {
|
||||
baseURL: httpConfig.baseURL,
|
||||
ws: VITE_APP_WEBSOCKET === 'true',
|
||||
ws: VITE_APP_WEBSOCKET === 'Y',
|
||||
proxyPattern: VITE_APP_BASE_API,
|
||||
other: otherConfig
|
||||
};
|
||||
|
45
src/utils/sse.ts
Normal file
45
src/utils/sse.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { useEventSource } from '@vueuse/core';
|
||||
import { watch } from 'vue';
|
||||
import useNoticeStore from '@/store/modules/notice';
|
||||
import { localStg } from './storage';
|
||||
|
||||
// 初始化
|
||||
export const initSSE = (url: any) => {
|
||||
if (import.meta.env.VITE_APP_SSE === 'N') {
|
||||
return;
|
||||
}
|
||||
const token = localStg.get('token');
|
||||
const sseUrl = `${url}?Authorization=Bearer ${token}&clientid=${import.meta.env.VITE_APP_CLIENT_ID}`;
|
||||
const { data, error } = useEventSource(sseUrl, [], {
|
||||
autoReconnect: {
|
||||
retries: 10,
|
||||
delay: 3000,
|
||||
onFailed() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to connect after 10 retries');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(error, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('SSE connection error:', error.value);
|
||||
error.value = null;
|
||||
});
|
||||
|
||||
watch(data, () => {
|
||||
if (!data.value) return;
|
||||
useNoticeStore().addNotice({
|
||||
message: data.value,
|
||||
read: false,
|
||||
time: new Date().toLocaleString()
|
||||
});
|
||||
window.$notification?.create({
|
||||
title: '消息',
|
||||
content: data.value,
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
});
|
||||
data.value = null;
|
||||
});
|
||||
};
|
@ -32,7 +32,7 @@ let socketError = 0; // 错误次数
|
||||
|
||||
// 初始化socket
|
||||
export function initWebSocket(url: any) {
|
||||
if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
|
||||
if (import.meta.env.VITE_APP_WEBSOCKET === 'N') {
|
||||
return null;
|
||||
}
|
||||
socketUrl = url;
|
||||
@ -50,6 +50,7 @@ export function initWebSocket(url: any) {
|
||||
// socket 连接成功
|
||||
export function websocketonopen() {
|
||||
websocket.onopen = () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('连接 websocket 成功');
|
||||
resetHeart();
|
||||
};
|
||||
@ -58,14 +59,16 @@ export function websocketonopen() {
|
||||
// socket 连接失败
|
||||
export function websocketonerror() {
|
||||
websocket.onerror = (e: any) => {
|
||||
console.log('连接 websocket 失败', e);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('连接 websocket 失败', e);
|
||||
};
|
||||
}
|
||||
|
||||
// socket 断开链接
|
||||
export function websocketclose() {
|
||||
websocket.onclose = (e: any) => {
|
||||
console.log('断开连接', e);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('断开连接', e);
|
||||
};
|
||||
}
|
||||
|
||||
@ -102,9 +105,11 @@ export function reconnect() {
|
||||
clearInterval(heartTime);
|
||||
initWebSocket(socketUrl);
|
||||
socketError += 1;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('socket重连', socketError);
|
||||
} else {
|
||||
console.log('重试次数已用完');
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('重试次数已用完');
|
||||
clearInterval(heartTime);
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,12 @@ interface Props {
|
||||
defineProps<Props>();
|
||||
|
||||
onMounted(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('mounted');
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('activated');
|
||||
});
|
||||
</script>
|
||||
|
@ -92,7 +92,7 @@ handleFetchCaptchaCode();
|
||||
<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')" />
|
||||
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="password">
|
||||
<NInput
|
||||
|
Reference in New Issue
Block a user