use websocket

This commit is contained in:
landaiqing
2024-08-17 20:01:23 +08:00
parent 3140eaca99
commit 0c3edc360c
12 changed files with 223 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,6 @@ export default function useStore() {
return {
user: isAutoLogin() ? useAuthStore() : useAuthSessionStore(), // 自动登录时使用 useAuthStore否则使用 useAuthSessionStore
theme: useThemeStore(),
lang: langStore()
lang: langStore(),
};
}

View File

@@ -7,7 +7,7 @@ export const useAuthSessionStore = defineStore(
() => {
const user: any = reactive({
accessToken: '',
userId: '',
uid: '',
refreshToken: '',
expiresAt: 0,
});

View File

@@ -7,7 +7,7 @@ export const useAuthStore = defineStore(
() => {
const user: any = reactive({
accessToken: '',
userId: '',
uid: '',
refreshToken: '',
expiresAt: 0,
});

View File

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

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

View File

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

View File

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

View File

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