From e406e14c3b4fd237e3dc072122f1bd92feefb020 Mon Sep 17 00:00:00 2001 From: Zephyruso <176294927+Zephyruso@users.noreply.github.com> Date: Sun, 8 Dec 2024 13:13:59 +0800 Subject: [PATCH] feat: charts in settings & fix websocket reactive --- package.json | 2 + pnpm-lock.yaml | 16 +++ src/api/index.ts | 21 +++- src/components/charts/MemoryCharts.vue | 116 ++++++++++++++++++ .../{sidebar => charts}/SpeedCharts.vue | 30 +++-- src/components/sidebar/CommonCtrl.vue | 14 +-- src/components/sidebar/SideBar.vue | 23 +--- src/components/sidebar/StatisticsInfo.vue | 16 +++ src/config/index.ts | 16 +++ src/i18n/en.ts | 3 +- src/i18n/zh.ts | 1 + src/store/connections.ts | 22 ++-- src/store/logs.ts | 6 +- src/store/statistics.ts | 75 ++++++----- src/views/HomePage.vue | 21 +--- src/views/SettingsPage.vue | 22 ++++ 16 files changed, 293 insertions(+), 111 deletions(-) create mode 100644 src/components/charts/MemoryCharts.vue rename src/components/{sidebar => charts}/SpeedCharts.vue (80%) create mode 100644 src/components/sidebar/StatisticsInfo.vue diff --git a/package.json b/package.json index fd760f5e..a531ad0e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@heroicons/vue": "^2.2.0", "@tanstack/vue-table": "^8.20.5", "@types/echarts": "^4.9.22", + "@types/reconnectingwebsocket": "^1.0.10", "@vueuse/core": "^12.0.0", "axios": "^1.7.8", "dayjs": "^1.11.13", @@ -28,6 +29,7 @@ "prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-tailwindcss": "^0.6.9", "pretty-bytes": "^6.1.1", + "reconnectingwebsocket": "^1.0.0", "subsetted-fonts": "^1.0.4", "theme-change": "^2.5.0", "uuid": "^11.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28f74901..37a86223 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@types/echarts': specifier: ^4.9.22 version: 4.9.22 + '@types/reconnectingwebsocket': + specifier: ^1.0.10 + version: 1.0.10 '@vueuse/core': specifier: ^12.0.0 version: 12.0.0(typescript@5.6.3) @@ -53,6 +56,9 @@ importers: pretty-bytes: specifier: ^6.1.1 version: 6.1.1 + reconnectingwebsocket: + specifier: ^1.0.0 + version: 1.0.0 subsetted-fonts: specifier: ^1.0.4 version: 1.0.4 @@ -1108,6 +1114,9 @@ packages: '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} + '@types/reconnectingwebsocket@1.0.10': + resolution: {integrity: sha512-30Pq4D3o8BKcdY53dzr0elGFyB/ChYpGrHiRH/GuaZKXXGWq/CsD1QBEu1b8IgdHReOKpo9tjk80UaxSbuXoTQ==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -2490,6 +2499,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + reconnectingwebsocket@1.0.0: + resolution: {integrity: sha512-r7H/dwkkfBu9x5eMGIt8td5WLqNbqy675x8Xg0+SoXaUS3xzniVlmfO7t7HSYmN/ZGzYjOKa9G2W4xCgCo7Zlg==} + reflect.getprototypeof@1.0.7: resolution: {integrity: sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==} engines: {node: '>= 0.4'} @@ -4074,6 +4086,8 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/reconnectingwebsocket@1.0.10': {} + '@types/resolve@1.20.2': {} '@types/sortablejs@1.15.8': {} @@ -5489,6 +5503,8 @@ snapshots: dependencies: picomatch: 2.3.1 + reconnectingwebsocket@1.0.0: {} + reflect.getprototypeof@1.0.7: dependencies: call-bind: 1.0.7 diff --git a/src/api/index.ts b/src/api/index.ts index 1acf0a09..ba5810b1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,7 @@ import { activeBackend } from '@/store/setup' import type { Config, Proxy, ProxyProvider, Rule, RuleProvider } from '@/types' -import { useWebSocket } from '@vueuse/core' import axios from 'axios' +import ReconnectingWebSocket from 'reconnectingwebsocket' import { computed, ref, watch } from 'vue' axios.interceptors.request.use((config) => { @@ -136,10 +136,21 @@ const createWebSocket = (url: string, searchParams?: Record) }) } - return useWebSocket(resurl, { - autoClose: false, - autoReconnect: true, - }) + const data = ref() + const websocket = new ReconnectingWebSocket(resurl.toString()) + + const close = () => { + websocket.close() + } + + websocket.onmessage = ({ data: message }) => { + data.value = JSON.parse(message) + } + + return { + data, + close, + } } export const fetchConnectionsAPI = () => { diff --git a/src/components/charts/MemoryCharts.vue b/src/components/charts/MemoryCharts.vue new file mode 100644 index 00000000..80e088a5 --- /dev/null +++ b/src/components/charts/MemoryCharts.vue @@ -0,0 +1,116 @@ + + + diff --git a/src/components/sidebar/SpeedCharts.vue b/src/components/charts/SpeedCharts.vue similarity index 80% rename from src/components/sidebar/SpeedCharts.vue rename to src/components/charts/SpeedCharts.vue index 795dfdd4..e1ab26b2 100644 --- a/src/components/sidebar/SpeedCharts.vue +++ b/src/components/charts/SpeedCharts.vue @@ -19,6 +19,7 @@ diff --git a/src/components/sidebar/SideBar.vue b/src/components/sidebar/SideBar.vue index 9e7b4677..a510abbb 100644 --- a/src/components/sidebar/SideBar.vue +++ b/src/components/sidebar/SideBar.vue @@ -23,7 +23,7 @@ @click="() => router.push({ name: r })" > diff --git a/src/config/index.ts b/src/config/index.ts index 9dddd4e8..a34f7a31 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,3 +1,11 @@ +import { + ArrowsRightLeftIcon, + Cog6ToothIcon, + DocumentTextIcon, + GlobeAltIcon, + WrenchScrewdriverIcon, +} from '@heroicons/vue/24/outline' + export const NOT_CONNECTED = 0 export enum LANG { EN_US = 'en-US', @@ -87,3 +95,11 @@ export enum ROUTE_NAME { rules = 'rules', settings = 'settings', } + +export const ROUTE_ICON_MAP = { + [ROUTE_NAME.proxies]: GlobeAltIcon, + [ROUTE_NAME.connections]: ArrowsRightLeftIcon, + [ROUTE_NAME.rules]: WrenchScrewdriverIcon, + [ROUTE_NAME.logs]: DocumentTextIcon, + [ROUTE_NAME.settings]: Cog6ToothIcon, +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 45ee45b1..ac4d6efc 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -15,7 +15,7 @@ export default { upload: 'Upload', downloadSpeed: 'Download Speed', uploadSpeed: 'Upload Speed', - memoryUsage: 'Memory Usage', + memoryUsage: 'Memory', version: 'Version', quickFilter: 'Quick Filter', noContent: 'No Content', @@ -91,4 +91,5 @@ export default { lowLatencyDesc: 'Yellow threshold', mediumLatencyDesc: 'Red threshold', fonts: 'Fonts', + statistics: 'Statistics', } diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index a4237b90..adf0537a 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -90,4 +90,5 @@ export default { lowLatencyDesc: '黄色的阈值', mediumLatencyDesc: '红色的阈值', fonts: '字体', + statistics: '统计', } diff --git a/src/store/connections.ts b/src/store/connections.ts index 03d863ee..e6a2054a 100644 --- a/src/store/connections.ts +++ b/src/store/connections.ts @@ -22,19 +22,17 @@ export const initConnections = () => { downloadTotal.value = 0 uploadTotal.value = 0 - const ws = fetchConnectionsAPI() + const ws = fetchConnectionsAPI<{ + connections: ConnectionRawMessage[] + downloadTotal: number + uploadTotal: number + memory: number + }>() const unwatch = watch(ws.data, (data) => { if (!data) return - const parsedData = JSON.parse(data) as { - connections: ConnectionRawMessage[] - downloadTotal: number - uploadTotal: number - memory: number - } - - downloadTotal.value = parsedData.downloadTotal - uploadTotal.value = parsedData.uploadTotal + downloadTotal.value = data.downloadTotal + uploadTotal.value = data.uploadTotal if (isPaused.value) { return @@ -42,10 +40,10 @@ export const initConnections = () => { closedConnections.value = [ ...closedConnections.value, - ...differenceWith(activeConnections.value, parsedData.connections, (a, b) => a.id === b.id), + ...differenceWith(activeConnections.value, data.connections, (a, b) => a.id === b.id), ] activeConnections.value = - parsedData.connections?.map((connection) => { + data.connections?.map((connection) => { const preConnection = activeConnections.value.find((c) => c.id === connection.id) if (!preConnection) { diff --git a/src/store/logs.ts b/src/store/logs.ts index 093de424..ba01d69d 100644 --- a/src/store/logs.ts +++ b/src/store/logs.ts @@ -16,21 +16,19 @@ export const initLogs = () => { logs.value = [] let idx = 1 - const ws = fetchLogsAPI({ + const ws = fetchLogsAPI({ level: logLevel.value, }) const unwatch = watch(ws.data, (data) => { if (!data) return - const parsedData = JSON.parse(data) as Log - if (isPaused.value) { idx++ return } logs.value.unshift({ - ...parsedData, + ...data, time: new Date().valueOf(), seq: idx++, }) diff --git a/src/store/statistics.ts b/src/store/statistics.ts index f877df80..abb79a28 100644 --- a/src/store/statistics.ts +++ b/src/store/statistics.ts @@ -4,6 +4,7 @@ import { ref, watch } from 'vue' const initValue = new Array(60).fill(0).map((v, i) => ({ name: i, value: v })) export const memory = ref(0) +export const memoryHistory = ref([...initValue]) export const downloadSpeed = ref(0) export const uploadSpeed = ref(0) export const downloadSpeedHistory = ref([...initValue]) @@ -16,48 +17,60 @@ export const initSatistic = () => { downloadSpeedHistory.value = [...initValue] uploadSpeedHistory.value = [...initValue] + memoryHistory.value = [...initValue] - const memoryWs = fetchMemoryAPI() - const unwatchMemory = watch(memoryWs.data, (data) => { - if (!data) return + const { data: memoryWsData, close: memoryWsClose } = fetchMemoryAPI<{ + inuse: number + }>() + const unwatchMemory = watch( + () => memoryWsData.value, + (data) => { + console.log(data, 'statistics') + if (!data) return - const parsedData = JSON.parse(data) as { - inuse: number - } + memory.value = data.inuse - memory.value = parsedData.inuse - }) + memoryHistory.value.push({ + value: data.inuse, + name: Date.now().valueOf(), + }) - const trafficWs = fetchTrafficAPI() - const unwatchTraffic = watch(trafficWs.data, (data) => { - if (!data) return + memoryHistory.value = memoryHistory.value.slice(-60) + }, + ) - const parsedData = JSON.parse(data) as { - up: number - down: number - } - const timestamp = Date.now().valueOf() + const { data: trafficWsData, close: trafficWsClose } = fetchTrafficAPI<{ + down: number + up: number + }>() + const unwatchTraffic = watch( + () => trafficWsData.value, + (data) => { + if (!data) return - downloadSpeed.value = parsedData.down - uploadSpeed.value = parsedData.up + const timestamp = Date.now().valueOf() - downloadSpeedHistory.value.push({ - value: parsedData.down, - name: timestamp, - }) - uploadSpeedHistory.value.push({ - value: parsedData.up, - name: timestamp, - }) + downloadSpeed.value = data.down + uploadSpeed.value = data.up - downloadSpeedHistory.value = downloadSpeedHistory.value.slice(-60) - uploadSpeedHistory.value = uploadSpeedHistory.value.slice(-60) - }) + downloadSpeedHistory.value.push({ + value: data.down, + name: timestamp, + }) + uploadSpeedHistory.value.push({ + value: data.up, + name: timestamp, + }) + + downloadSpeedHistory.value = downloadSpeedHistory.value.slice(-60) + uploadSpeedHistory.value = uploadSpeedHistory.value.slice(-60) + }, + ) cancel = () => { - memoryWs.close() + memoryWsClose() + trafficWsClose() unwatchMemory() - trafficWs.close() unwatchTraffic() } } diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index a892fb53..d808a89b 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -34,7 +34,7 @@ :href="`#${r}`" > @@ -61,7 +61,7 @@ import RulesCtrl from '@/components/sidebar/RulesCtrl.vue' import SideBar from '@/components/sidebar/SideBar.vue' import { useProxies } from '@/composables/proxies' import { rulesTabShow } from '@/composables/rules' -import { PROXY_TAB_TYPE, ROUTE_NAME, RULE_TAB_TYPE } from '@/config' +import { PROXY_TAB_TYPE, ROUTE_ICON_MAP, ROUTE_NAME, RULE_TAB_TYPE } from '@/config' import { fetchConfigs } from '@/store/config' import { initConnections } from '@/store/connections' import { initLogs } from '@/store/logs' @@ -70,14 +70,7 @@ import { fetchRules } from '@/store/rules' import { isSiderbarCollapsed } from '@/store/settings' import { activeUuid } from '@/store/setup' import { initSatistic } from '@/store/statistics' -import { - ArrowsRightLeftIcon, - Bars3Icon, - Cog6ToothIcon, - DocumentTextIcon, - GlobeAltIcon, - WrenchScrewdriverIcon, -} from '@heroicons/vue/24/outline' +import { Bars3Icon } from '@heroicons/vue/24/outline' import { computed, watch } from 'vue' import { RouterView, useRoute } from 'vue-router' @@ -85,14 +78,6 @@ const isPWA = (() => { return window.matchMedia('(display-mode: standalone)').matches || navigator.standalone })() -const routeIconMap = { - [ROUTE_NAME.proxies]: GlobeAltIcon, - [ROUTE_NAME.connections]: ArrowsRightLeftIcon, - [ROUTE_NAME.rules]: WrenchScrewdriverIcon, - [ROUTE_NAME.logs]: DocumentTextIcon, - [ROUTE_NAME.settings]: Cog6ToothIcon, -} - const ctrlsMap = { [ROUTE_NAME.connections]: ConnectionCtrl, [ROUTE_NAME.logs]: LogsCtrl, diff --git a/src/views/SettingsPage.vue b/src/views/SettingsPage.vue index 0bda6dd7..23e5f9a1 100644 --- a/src/views/SettingsPage.vue +++ b/src/views/SettingsPage.vue @@ -1,5 +1,6 @@