✨ use websocket
This commit is contained in:
@@ -16,4 +16,4 @@ VITE_TITLE_NAME='五味子云相册'
|
||||
VITE_APP_TOKEN_KEY='Bearer'
|
||||
|
||||
# the websocket url
|
||||
VITE_WEB_SOCKET_URL='ws://127.0.0.1:3010/wx/socket'
|
||||
VITE_WEB_SOCKET_URL='ws://127.0.0.1:8080/api/ws/socket'
|
||||
|
@@ -15,4 +15,4 @@ VITE_TITLE_NAME='五味子云相册'
|
||||
VITE_APP_TOKEN_KEY='Bearer'
|
||||
|
||||
# the websocket url
|
||||
VITE_WEB_SOCKET_URL='ws://127.0.0.1:3010/wx/socket'
|
||||
VITE_WEB_SOCKET_URL='ws://127.0.0.1:8080/api/ws/socket'
|
||||
|
@@ -34,6 +34,7 @@
|
||||
"vue": "^3.4.37",
|
||||
"vue-i18n": "^10.0.0-beta.5",
|
||||
"vue-router": "^4.4.3",
|
||||
"ws": "^8.18.0",
|
||||
"zipson": "^0.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -8,6 +8,10 @@ export const generateClientId = () => {
|
||||
{
|
||||
meta: {
|
||||
ignoreToken: true,
|
||||
},
|
||||
cacheFor: {
|
||||
mode: "restore",
|
||||
expire: 1000 * 60 * 60 * 24 * 30 // 30天
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -25,13 +29,34 @@ export const generateQrCode = (clientId: string) => {
|
||||
meta: {
|
||||
ignoreToken: true,
|
||||
},
|
||||
cacheFor: {
|
||||
// 设置缓存模式为持久化模式
|
||||
mode: 'restore',
|
||||
// 缓存时间
|
||||
expire: 30 * 24 * 60 * 60 * 1000,
|
||||
tag: 'v1'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
/**
|
||||
* 关闭websocket
|
||||
* @param clientId
|
||||
*/
|
||||
export const closeWebsocket = (clientId: string) => {
|
||||
return service.Get('/api/ws/delete',
|
||||
{
|
||||
params: {
|
||||
client_id: clientId
|
||||
},
|
||||
meta: {
|
||||
ignoreToken: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
export const sendSocketMessage = (clientId: string) => {
|
||||
return service.Get('/api/ws/send',
|
||||
{
|
||||
params: {
|
||||
client_id: clientId
|
||||
},
|
||||
meta: {
|
||||
ignoreToken: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@@ -16,6 +16,6 @@ export default function useStore() {
|
||||
return {
|
||||
user: isAutoLogin() ? useAuthStore() : useAuthSessionStore(), // 自动登录时使用 useAuthStore,否则使用 useAuthSessionStore
|
||||
theme: useThemeStore(),
|
||||
lang: langStore()
|
||||
lang: langStore(),
|
||||
};
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ export const useAuthSessionStore = defineStore(
|
||||
() => {
|
||||
const user: any = reactive({
|
||||
accessToken: '',
|
||||
userId: '',
|
||||
uid: '',
|
||||
refreshToken: '',
|
||||
expiresAt: 0,
|
||||
});
|
||||
|
@@ -7,7 +7,7 @@ export const useAuthStore = defineStore(
|
||||
() => {
|
||||
const user: any = reactive({
|
||||
accessToken: '',
|
||||
userId: '',
|
||||
uid: '',
|
||||
refreshToken: '',
|
||||
expiresAt: 0,
|
||||
});
|
||||
|
@@ -46,7 +46,12 @@ export const service = createAlova({
|
||||
requestAdapter: axiosRequestAdapter(),
|
||||
l2Cache: localforageStorageAdapter,
|
||||
cacheLogger: import.meta.env.VITE_NODE_ENV === 'development',
|
||||
cacheFor: {},
|
||||
cacheFor: {
|
||||
// GET: {
|
||||
// mode: "restore",
|
||||
// expire: 1000 * 60 * 60 * 24 * 7 // 7天过期
|
||||
// }
|
||||
},
|
||||
// 设置全局的请求拦截器
|
||||
beforeRequest: onAuthRequired(async (method: any) => {
|
||||
if (!method.meta?.ignoreToken) {
|
||||
|
112
src/utils/websocket/websocket.ts
Normal file
112
src/utils/websocket/websocket.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// src/utils/websocket.ts
|
||||
import {onUnmounted} from 'vue';
|
||||
|
||||
interface WebSocketOptions {
|
||||
url: string;
|
||||
protocols?: string | string[];
|
||||
reconnectTimeout?: number;
|
||||
}
|
||||
|
||||
type MessageCallback = (data: any) => void;
|
||||
type EventCallback = () => void;
|
||||
|
||||
class WebSocketService {
|
||||
private ws: WebSocket | null = null;
|
||||
private callbacks: { [key: string]: (MessageCallback | EventCallback)[] } = {};
|
||||
private reconnectTimeoutMs: number = 5000; // 默认5秒重连间隔
|
||||
private heartbeatIntervalMs: number = 5000; // 默认5秒心跳间隔
|
||||
|
||||
constructor(private options: WebSocketOptions) {
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
this.ws = new WebSocket(this.options.url, this.options.protocols);
|
||||
this.ws.addEventListener('open', this.handleOpen);
|
||||
this.ws.addEventListener('message', this.handleMessage);
|
||||
this.ws.addEventListener('error', this.handleError);
|
||||
this.ws.addEventListener('close', this.handleClose);
|
||||
setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.send('ping');
|
||||
}
|
||||
}, this.heartbeatIntervalMs);
|
||||
}
|
||||
|
||||
public close(isActiveClose = false): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
if (!isActiveClose) {
|
||||
setTimeout(() => this.reconnect(), this.reconnectTimeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public reconnect(): void {
|
||||
this.open();
|
||||
}
|
||||
|
||||
public on(event: 'message', callback: MessageCallback): void;
|
||||
public on(event: 'open' | 'error' | 'close', callback: EventCallback): void;
|
||||
public on(event: string, callback: (...args: any[]) => void): void {
|
||||
if (!this.callbacks[event]) {
|
||||
this.callbacks[event] = [];
|
||||
}
|
||||
this.callbacks[event].push(callback);
|
||||
}
|
||||
|
||||
private handleOpen = (): void => {
|
||||
console.log('WebSocket连接已建立');
|
||||
if (this.callbacks.open) {
|
||||
this.callbacks.open.forEach((cb) => (cb as EventCallback)());
|
||||
}
|
||||
};
|
||||
|
||||
private handleMessage = (event: MessageEvent): void => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket接收到消息:', data);
|
||||
if (this.callbacks.message) {
|
||||
this.callbacks.message.forEach((cb) => (cb as MessageCallback)(data));
|
||||
}
|
||||
};
|
||||
|
||||
private handleError = (error: Event): void => {
|
||||
console.error('WebSocket错误:', error);
|
||||
if (this.callbacks.error) {
|
||||
this.callbacks.error.forEach((cb) => (cb as EventCallback)());
|
||||
}
|
||||
};
|
||||
|
||||
private handleClose = (): void => {
|
||||
console.log('WebSocket连接已关闭');
|
||||
if (this.callbacks.close) {
|
||||
this.callbacks.close.forEach((cb) => (cb as EventCallback)());
|
||||
if (!this.options.reconnectTimeout) {
|
||||
this.reconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public send(data: any): void {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(data));
|
||||
} else {
|
||||
console.warn('尝试发送消息时WebSocket未连接');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function useWebSocket(options: WebSocketOptions) {
|
||||
const wsService = new WebSocketService(options);
|
||||
|
||||
onUnmounted(() => {
|
||||
wsService.close(true);
|
||||
});
|
||||
|
||||
return {
|
||||
open: wsService.open.bind(wsService),
|
||||
close: wsService.close.bind(wsService),
|
||||
reconnect: wsService.reconnect.bind(wsService),
|
||||
on: wsService.on.bind(wsService),
|
||||
send: wsService.send.bind(wsService)
|
||||
};
|
||||
}
|
@@ -330,7 +330,7 @@ async function phoneLoginSubmit() {
|
||||
if (res.code === 0 && res.success) {
|
||||
const userStore = useStore().user;
|
||||
const {uid, access_token, refresh_token, expires_at} = res.data;
|
||||
userStore.user.userId = uid;
|
||||
userStore.user.uid = uid;
|
||||
userStore.user.accessToken = access_token;
|
||||
userStore.user.refreshToken = refresh_token;
|
||||
userStore.user.expiresAt = expires_at;
|
||||
@@ -408,7 +408,7 @@ async function checkAccountLoginCaptcha(angle: number) {
|
||||
if (res.code === 0 && res.success) {
|
||||
const userStore = useStore().user;
|
||||
const {uid, access_token, refresh_token, expires_at} = res.data;
|
||||
userStore.user.userId = uid;
|
||||
userStore.user.uid = uid;
|
||||
userStore.user.accessToken = access_token;
|
||||
userStore.user.refreshToken = refresh_token;
|
||||
userStore.user.expiresAt = expires_at;
|
||||
|
@@ -18,7 +18,10 @@
|
||||
:size="230"
|
||||
:error-level="'H'"
|
||||
:status="status"
|
||||
@refresh="async () => await getQrCode()"
|
||||
@refresh="() => {
|
||||
getClientId();
|
||||
getQrCode();
|
||||
}"
|
||||
:value=qrcode
|
||||
:icon="logo"
|
||||
/>
|
||||
@@ -42,9 +45,12 @@ import {useI18n} from "vue-i18n";
|
||||
import BoxDog from "@/components/BoxDog/BoxDog.vue";
|
||||
import QRLoginFooter from "@/views/QRLogin/QRLoginFooter.vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {generateClientId, generateQrCode} from "@/api/oauth";
|
||||
import {ref} from "vue";
|
||||
import {closeWebsocket, generateClientId, generateQrCode} from "@/api/oauth";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import logo from "@/assets/svgs/logo-schisandra.svg";
|
||||
import useWebSocket from "@/utils/websocket/websocket.ts";
|
||||
import useStore from "@/store";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
@@ -66,15 +72,17 @@ async function getClientId() {
|
||||
}
|
||||
}
|
||||
|
||||
getClientId();
|
||||
|
||||
/**
|
||||
* 获取二维码
|
||||
*/
|
||||
async function getQrCode() {
|
||||
const clientId: any = localStorage.getItem('client_id');
|
||||
if (!clientId) {
|
||||
status.value = 'expired';
|
||||
return;
|
||||
}
|
||||
const res: any = await generateQrCode(clientId);
|
||||
console.log(res);
|
||||
if (res.code === 0 && res.data) {
|
||||
status.value = 'active';
|
||||
qrcode.value = res.data;
|
||||
@@ -84,7 +92,53 @@ async function getQrCode() {
|
||||
}
|
||||
}
|
||||
|
||||
getQrCode();
|
||||
/**
|
||||
* 获取本地client_id
|
||||
*/
|
||||
function getLocalClientId(): string {
|
||||
const clientID: string | null = localStorage.getItem('client_id');
|
||||
if (clientID) {
|
||||
return clientID;
|
||||
} else {
|
||||
getClientId();
|
||||
return getLocalClientId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const wsOptions = {
|
||||
url: import.meta.env.VITE_WEB_SOCKET_URL as string + "?client_id=" + getLocalClientId(),
|
||||
};
|
||||
|
||||
const {open, close, on} = useWebSocket(wsOptions);
|
||||
|
||||
onMounted(async () => {
|
||||
await getClientId();
|
||||
await getQrCode();
|
||||
open();
|
||||
|
||||
// 注册消息接收处理函数
|
||||
on('message', (data: any) => {
|
||||
console.log(data);
|
||||
if (data) {
|
||||
const user = useStore().user;
|
||||
user.user.accessToken = data.access_token;
|
||||
user.user.refreshToken = data.refresh_token;
|
||||
user.user.uid = data.uid;
|
||||
user.user.expiresAt = data.expires_at;
|
||||
status.value = 'scanned';
|
||||
message.success(t('login.loginSuccess'));
|
||||
} else {
|
||||
message.error(t('login.loginError'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
// await closeWebsocket(getLocalClientId());
|
||||
close(true);
|
||||
|
||||
});
|
||||
</script>
|
||||
<style src="./index.scss" scoped>
|
||||
@import "@/assets/styles/global.scss";
|
||||
|
@@ -3429,6 +3429,11 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
ws@^8.18.0:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
||||
|
||||
xml-name-validator@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
||||
|
Reference in New Issue
Block a user