✨ use websocket
This commit is contained in:
@@ -16,4 +16,4 @@ VITE_TITLE_NAME='五味子云相册'
|
|||||||
VITE_APP_TOKEN_KEY='Bearer'
|
VITE_APP_TOKEN_KEY='Bearer'
|
||||||
|
|
||||||
# the websocket url
|
# 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'
|
VITE_APP_TOKEN_KEY='Bearer'
|
||||||
|
|
||||||
# the websocket url
|
# 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": "^3.4.37",
|
||||||
"vue-i18n": "^10.0.0-beta.5",
|
"vue-i18n": "^10.0.0-beta.5",
|
||||||
"vue-router": "^4.4.3",
|
"vue-router": "^4.4.3",
|
||||||
|
"ws": "^8.18.0",
|
||||||
"zipson": "^0.2.12"
|
"zipson": "^0.2.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -8,6 +8,10 @@ export const generateClientId = () => {
|
|||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
ignoreToken: true,
|
ignoreToken: true,
|
||||||
|
},
|
||||||
|
cacheFor: {
|
||||||
|
mode: "restore",
|
||||||
|
expire: 1000 * 60 * 60 * 24 * 30 // 30天
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -25,13 +29,34 @@ export const generateQrCode = (clientId: string) => {
|
|||||||
meta: {
|
meta: {
|
||||||
ignoreToken: true,
|
ignoreToken: true,
|
||||||
},
|
},
|
||||||
cacheFor: {
|
}
|
||||||
// 设置缓存模式为持久化模式
|
);
|
||||||
mode: 'restore',
|
};
|
||||||
// 缓存时间
|
/**
|
||||||
expire: 30 * 24 * 60 * 60 * 1000,
|
* 关闭websocket
|
||||||
tag: 'v1'
|
* @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 {
|
return {
|
||||||
user: isAutoLogin() ? useAuthStore() : useAuthSessionStore(), // 自动登录时使用 useAuthStore,否则使用 useAuthSessionStore
|
user: isAutoLogin() ? useAuthStore() : useAuthSessionStore(), // 自动登录时使用 useAuthStore,否则使用 useAuthSessionStore
|
||||||
theme: useThemeStore(),
|
theme: useThemeStore(),
|
||||||
lang: langStore()
|
lang: langStore(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ export const useAuthSessionStore = defineStore(
|
|||||||
() => {
|
() => {
|
||||||
const user: any = reactive({
|
const user: any = reactive({
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
userId: '',
|
uid: '',
|
||||||
refreshToken: '',
|
refreshToken: '',
|
||||||
expiresAt: 0,
|
expiresAt: 0,
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,7 @@ export const useAuthStore = defineStore(
|
|||||||
() => {
|
() => {
|
||||||
const user: any = reactive({
|
const user: any = reactive({
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
userId: '',
|
uid: '',
|
||||||
refreshToken: '',
|
refreshToken: '',
|
||||||
expiresAt: 0,
|
expiresAt: 0,
|
||||||
});
|
});
|
||||||
|
@@ -46,7 +46,12 @@ export const service = createAlova({
|
|||||||
requestAdapter: axiosRequestAdapter(),
|
requestAdapter: axiosRequestAdapter(),
|
||||||
l2Cache: localforageStorageAdapter,
|
l2Cache: localforageStorageAdapter,
|
||||||
cacheLogger: import.meta.env.VITE_NODE_ENV === 'development',
|
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) => {
|
beforeRequest: onAuthRequired(async (method: any) => {
|
||||||
if (!method.meta?.ignoreToken) {
|
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) {
|
if (res.code === 0 && res.success) {
|
||||||
const userStore = useStore().user;
|
const userStore = useStore().user;
|
||||||
const {uid, access_token, refresh_token, expires_at} = res.data;
|
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.accessToken = access_token;
|
||||||
userStore.user.refreshToken = refresh_token;
|
userStore.user.refreshToken = refresh_token;
|
||||||
userStore.user.expiresAt = expires_at;
|
userStore.user.expiresAt = expires_at;
|
||||||
@@ -408,7 +408,7 @@ async function checkAccountLoginCaptcha(angle: number) {
|
|||||||
if (res.code === 0 && res.success) {
|
if (res.code === 0 && res.success) {
|
||||||
const userStore = useStore().user;
|
const userStore = useStore().user;
|
||||||
const {uid, access_token, refresh_token, expires_at} = res.data;
|
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.accessToken = access_token;
|
||||||
userStore.user.refreshToken = refresh_token;
|
userStore.user.refreshToken = refresh_token;
|
||||||
userStore.user.expiresAt = expires_at;
|
userStore.user.expiresAt = expires_at;
|
||||||
|
@@ -18,7 +18,10 @@
|
|||||||
:size="230"
|
:size="230"
|
||||||
:error-level="'H'"
|
:error-level="'H'"
|
||||||
:status="status"
|
:status="status"
|
||||||
@refresh="async () => await getQrCode()"
|
@refresh="() => {
|
||||||
|
getClientId();
|
||||||
|
getQrCode();
|
||||||
|
}"
|
||||||
:value=qrcode
|
:value=qrcode
|
||||||
:icon="logo"
|
:icon="logo"
|
||||||
/>
|
/>
|
||||||
@@ -42,9 +45,12 @@ import {useI18n} from "vue-i18n";
|
|||||||
import BoxDog from "@/components/BoxDog/BoxDog.vue";
|
import BoxDog from "@/components/BoxDog/BoxDog.vue";
|
||||||
import QRLoginFooter from "@/views/QRLogin/QRLoginFooter.vue";
|
import QRLoginFooter from "@/views/QRLogin/QRLoginFooter.vue";
|
||||||
import {useRouter} from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
import {generateClientId, generateQrCode} from "@/api/oauth";
|
import {closeWebsocket, generateClientId, generateQrCode} from "@/api/oauth";
|
||||||
import {ref} from "vue";
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
import logo from "@/assets/svgs/logo-schisandra.svg";
|
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();
|
const {t} = useI18n();
|
||||||
|
|
||||||
@@ -66,15 +72,17 @@ async function getClientId() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientId();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取二维码
|
* 获取二维码
|
||||||
*/
|
*/
|
||||||
async function getQrCode() {
|
async function getQrCode() {
|
||||||
const clientId: any = localStorage.getItem('client_id');
|
const clientId: any = localStorage.getItem('client_id');
|
||||||
|
if (!clientId) {
|
||||||
|
status.value = 'expired';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const res: any = await generateQrCode(clientId);
|
const res: any = await generateQrCode(clientId);
|
||||||
console.log(res);
|
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
status.value = 'active';
|
status.value = 'active';
|
||||||
qrcode.value = res.data;
|
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>
|
</script>
|
||||||
<style src="./index.scss" scoped>
|
<style src="./index.scss" scoped>
|
||||||
@import "@/assets/styles/global.scss";
|
@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"
|
resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
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:
|
xml-name-validator@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
||||||
|
Reference in New Issue
Block a user