diff --git a/src/App.vue b/src/App.vue
index c455798..982baa7 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,7 +1,15 @@
diff --git a/src/components/DefaultLayout.vue b/src/components/DefaultLayout.vue
index 154fc9f..a6eeaa9 100644
--- a/src/components/DefaultLayout.vue
+++ b/src/components/DefaultLayout.vue
@@ -34,7 +34,7 @@ import {
SidebarRail,
} from '~/components/ui/sidebar'
-import { authCode, useBackendOnline } from '../composables'
+import { authCode } from '../composables'
const router = useRouter()
@@ -99,8 +99,6 @@ onMounted(() => {
router.push('/auth')
}
})
-
-const online = useBackendOnline()
@@ -113,7 +111,7 @@ const online = useBackendOnline()
- 服务状态正常 {{ online.isOnline }}
+ 服务状态正常
diff --git a/src/composables/fetch.ts b/src/composables/fetch.ts
index 055fa64..7dacc4d 100644
--- a/src/composables/fetch.ts
+++ b/src/composables/fetch.ts
@@ -25,50 +25,50 @@ export const baseFetch = createFetch({
})
-export async function doCheckOnline() {
- let resp = null
- try {
- resp = await baseFetch('/ping', {
- timeout: 2000,
- })
- const { error } = resp
- if (error.value)
- throw new Error(error.value)
- const status = resp.response.value?.status
- if (!status) {
- return false
- }
- if (status === 200) {
- return true
- }
- else if (status >= 400 && status < 500) {
- return true
- }
- return false
- }
- catch (err) {
- if (resp?.response.value?.status === 401) {
- return true
- }
- console.error(err)
- return false
- }
-}
+// export async function doCheckOnline() {
+// let resp = null
+// try {
+// resp = await baseFetch('/ping', {
+// timeout: 2000,
+// })
+// const { error } = resp
+// if (error.value)
+// throw new Error(error.value)
+// const status = resp.response.value?.status
+// if (!status) {
+// return false
+// }
+// if (status === 200) {
+// return true
+// }
+// else if (status >= 400 && status < 500) {
+// return true
+// }
+// return false
+// }
+// catch (err) {
+// if (resp?.response.value?.status === 401) {
+// return true
+// }
+// console.error(err)
+// return false
+// }
+// }
-export const useBackendOnline = createSharedComposable(() => {
- const isOnline = ref(true)
+// export const useBackendOnline = createSharedComposable(() => {
+// const isOnline = ref(true)
- const useCheckInterval = useIntervalFn(() => {
- doCheckOnline().then((online) => {
- isOnline.value = online
- })
- }, 5000, {
- immediate: true,
- immediateCallback: true,
- })
- useCheckInterval.resume()
+// const useCheckInterval = useIntervalFn(() => {
+// doCheckOnline().then((online) => {
+// isOnline.value = online
+// })
+// }, 5000, {
+// immediate: true,
+// immediateCallback: true,
+// })
+// useCheckInterval.resume()
- return {
- isOnline,
- }
-})
+// return {
+// isOnline,
+// }
+// })
diff --git a/src/composables/online.ts b/src/composables/online.ts
index 519844d..507a426 100644
--- a/src/composables/online.ts
+++ b/src/composables/online.ts
@@ -1,3 +1,147 @@
-export const hasConnection = ref(false)
+import { useIntervalFn } from '@vueuse/core'
+import { API_BASE_URL } from '~/CONSTANT'
-// get `https://`
+interface UseAPIOnlineOptions {
+ /**
+ * WebSocket endpoint URL
+ */
+ url?: string
+ /**
+ * 心跳间隔(ms)
+ * @default 30000
+ */
+ heartbeatInterval?: number
+ /**
+ * 重连间隔(ms)
+ * @default 5000
+ */
+ reconnectDelay?: number
+ /**
+ * 连接成功回调
+ */
+ onConnected?: () => void
+ /**
+ * 断开连接回调
+ */
+ onDisconnected?: () => void
+ /**
+ * 连接错误回调
+ */
+ onError?: (error: any) => void
+}
+
+export function useAPIOnline(options: UseAPIOnlineOptions = {}) {
+ const {
+ url = `ws://${API_BASE_URL.replace('https://', '').replace('http://', '')}/ping/ws`,
+ heartbeatInterval = 30000,
+ reconnectDelay = 5000,
+ onConnected,
+ onDisconnected,
+ onError,
+ } = options
+
+ const ws = ref(null)
+ const isOnline = ref(false)
+ const isConnecting = ref(false)
+
+ // 最后一次收到pong的时间戳
+ const lastPongTime = ref(0)
+
+ // 检查是否需要重连的定时器
+ const { pause: pauseHealthCheck, resume: resumeHealthCheck } = useIntervalFn(() => {
+ const now = Date.now()
+ // 如果45秒没有收到pong,认为连接断开
+ if (now - lastPongTime.value > 45000) {
+ isOnline.value = false
+ reconnect()
+ }
+ }, 15000)
+
+ // 心跳定时器
+ const { pause: pauseHeartbeat, resume: resumeHeartbeat } = useIntervalFn(() => {
+ if (ws.value?.readyState === WebSocket.OPEN) {
+ ws.value.send(JSON.stringify({ type: 'ping' }))
+ }
+ }, heartbeatInterval)
+
+ const connect = () => {
+ if (isConnecting.value || (ws.value?.readyState === WebSocket.CONNECTING)) {
+ return
+ }
+
+ isConnecting.value = true
+
+ // 清理旧的连接
+ if (ws.value) {
+ ws.value.close()
+ ws.value = null
+ }
+
+ const socket = new WebSocket(url)
+
+ socket.onopen = () => {
+ isOnline.value = true
+ isConnecting.value = false
+ lastPongTime.value = Date.now()
+ resumeHeartbeat()
+ resumeHealthCheck()
+ onConnected?.()
+ }
+
+ socket.onclose = () => {
+ isOnline.value = false
+ isConnecting.value = false
+ pauseHeartbeat()
+ pauseHealthCheck()
+ onDisconnected?.()
+ setTimeout(reconnect, reconnectDelay)
+ }
+
+ socket.onerror = (error) => {
+ isOnline.value = false
+ isConnecting.value = false
+ pauseHeartbeat()
+ pauseHealthCheck()
+ onError?.(error)
+ }
+
+ socket.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ if (data.type === 'pong') {
+ lastPongTime.value = Date.now()
+ }
+ }
+ catch (e) {
+ console.error('Failed to parse websocket message:', e)
+ }
+ }
+
+ ws.value = socket
+ }
+
+ function reconnect() {
+ if (!isOnline.value && !isConnecting.value) {
+ connect()
+ }
+ }
+
+ const disconnect = () => {
+ if (ws.value) {
+ ws.value.close()
+ ws.value = null
+ }
+ isOnline.value = false
+ isConnecting.value = false
+ pauseHeartbeat()
+ pauseHealthCheck()
+ }
+
+ return {
+ isOnline,
+ isConnecting,
+ connect,
+ disconnect,
+ reconnect,
+ }
+}