feat: 整合 sse 推送

This commit is contained in:
xlsea
2024-09-02 09:34:34 +08:00
parent 6afd5cc36a
commit 89f5e8577e
15 changed files with 105 additions and 25 deletions

View File

@ -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 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

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

View File

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

View File

@ -50,7 +50,7 @@ function logout() {
positiveText: $t('common.confirm'),
negativeText: $t('common.cancel'),
onPositiveClick: () => {
authStore.resetStore();
authStore.logout();
}
});
}

View File

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

View File

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

View File

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

View File

@ -264,7 +264,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
setIsInitAuthRoute(true);
} else {
// if fetch user routes failed, reset store
authStore.resetStore();
authStore.logout();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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