diff --git a/.env.development b/.env.development index 3300079..7c281ac 100644 --- a/.env.development +++ b/.env.development @@ -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' diff --git a/.env.production b/.env.production index 5e6a961..fc46c40 100644 --- a/.env.production +++ b/.env.production @@ -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' diff --git a/package.json b/package.json index 80fb778..1f90899 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/api/oauth/index.ts b/src/api/oauth/index.ts index ee38cd0..3c07cf6 100644 --- a/src/api/oauth/index.ts +++ b/src/api/oauth/index.ts @@ -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, + }, } ); }; diff --git a/src/store/index.ts b/src/store/index.ts index 3e91832..32de4f1 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -16,6 +16,6 @@ export default function useStore() { return { user: isAutoLogin() ? useAuthStore() : useAuthSessionStore(), // 自动登录时使用 useAuthStore,否则使用 useAuthSessionStore theme: useThemeStore(), - lang: langStore() + lang: langStore(), }; } diff --git a/src/store/modules/userSessionStore.ts b/src/store/modules/userSessionStore.ts index 036dcd6..c729581 100644 --- a/src/store/modules/userSessionStore.ts +++ b/src/store/modules/userSessionStore.ts @@ -7,7 +7,7 @@ export const useAuthSessionStore = defineStore( () => { const user: any = reactive({ accessToken: '', - userId: '', + uid: '', refreshToken: '', expiresAt: 0, }); diff --git a/src/store/modules/userStore.ts b/src/store/modules/userStore.ts index da95c55..408ceea 100644 --- a/src/store/modules/userStore.ts +++ b/src/store/modules/userStore.ts @@ -7,7 +7,7 @@ export const useAuthStore = defineStore( () => { const user: any = reactive({ accessToken: '', - userId: '', + uid: '', refreshToken: '', expiresAt: 0, }); diff --git a/src/utils/alova/service.ts b/src/utils/alova/service.ts index b6d72fc..ec081cd 100644 --- a/src/utils/alova/service.ts +++ b/src/utils/alova/service.ts @@ -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) { diff --git a/src/utils/websocket/websocket.ts b/src/utils/websocket/websocket.ts new file mode 100644 index 0000000..9885314 --- /dev/null +++ b/src/utils/websocket/websocket.ts @@ -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) + }; +} diff --git a/src/views/Login/LoginPage.vue b/src/views/Login/LoginPage.vue index ea3d4a4..744cf81 100644 --- a/src/views/Login/LoginPage.vue +++ b/src/views/Login/LoginPage.vue @@ -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; diff --git a/src/views/QRLogin/QRLogin.vue b/src/views/QRLogin/QRLogin.vue index ff192b5..6ac9ba3 100644 --- a/src/views/QRLogin/QRLogin.vue +++ b/src/views/QRLogin/QRLogin.vue @@ -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); + +});