From 489cbb5d0f97aafd21531d58aff552e7eab407b6 Mon Sep 17 00:00:00 2001 From: Timi Date: Fri, 10 Apr 2026 00:01:38 +0800 Subject: [PATCH] add Dashboard --- package.json | 4 +- pnpm-lock.yaml | 47 +- src/api/system.ts | 194 ++++++ .../DockerDashboard/DockerDashboard.vue | 57 ++ .../ServerDashboard/ServerDashboard.vue | 653 ++++++++++++++++++ .../dashboard/UPSDashboard/UPSDashboard.vue | 57 ++ src/pages/detail/ServerLogPage.vue | 6 - src/pages/file/FileExplorerPage.vue | 8 +- src/pages/tabs/DashboardTab.vue | 111 +++ src/pages/tabs/{FilePage.vue => FileTab.vue} | 0 src/pages/tabs/ServerStatusPage.vue | 25 - .../{SettingsPage.vue => SettingsTab.vue} | 0 src/router/index.ts | 6 +- src/router/tabs.ts | 20 +- 14 files changed, 1134 insertions(+), 54 deletions(-) create mode 100644 src/api/system.ts create mode 100644 src/pages/dashboard/DockerDashboard/DockerDashboard.vue create mode 100644 src/pages/dashboard/ServerDashboard/ServerDashboard.vue create mode 100644 src/pages/dashboard/UPSDashboard/UPSDashboard.vue delete mode 100644 src/pages/detail/ServerLogPage.vue create mode 100644 src/pages/tabs/DashboardTab.vue rename src/pages/tabs/{FilePage.vue => FileTab.vue} (100%) delete mode 100644 src/pages/tabs/ServerStatusPage.vue rename src/pages/tabs/{SettingsPage.vue => SettingsTab.vue} (100%) diff --git a/package.json b/package.json index 99e1cd1..5970253 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "dependencies": { "axios": "^1.8.4", "less": "^4.3.0", + "echarts": "^6.0.0", + "vue-echarts": "^8.0.1", "music-metadata-browser": "^2.5.11", "pinia": "^3.0.2", "tdesign-mobile-vue": "^1.13.2", "timi-tdesign-mobile": "0.0.2", - "timi-web": "0.0.1", + "timi-web": "0.0.3", "ts-node": "^10.9.2", "vue": "^3.5.16", "vue-router": "4.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62015b5..3ca4ea7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: axios: specifier: ^1.8.4 version: 1.14.0 + echarts: + specifier: ^6.0.0 + version: 6.0.0 less: specifier: ^4.3.0 version: 4.6.4 @@ -27,14 +30,17 @@ importers: specifier: 0.0.2 version: 0.0.2(typescript@5.8.3) timi-web: - specifier: 0.0.1 - version: 0.0.1 + specifier: 0.0.3 + version: 0.0.3 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@24.12.0)(typescript@5.8.3) vue: specifier: ^3.5.16 version: 3.5.31(typescript@5.8.3) + vue-echarts: + specifier: ^8.0.1 + version: 8.0.1(echarts@6.0.0)(vue@3.5.31(typescript@5.8.3)) vue-router: specifier: 4.5.1 version: 4.5.1(vue@3.5.31(typescript@5.8.3)) @@ -946,6 +952,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1823,8 +1832,8 @@ packages: resolution: {integrity: sha512-R24yS4ovWTadCmpqJz3ajikMgk8YkKiUFbOIHuw3DBaJc2NwUcvVtY7aPu6kqnT/4j4AN3DgUFY+WkZbxqEhgQ==} engines: {node: '>=16.0.0'} - timi-web@0.0.1: - resolution: {integrity: sha512-xYGnbkEh4y9ZHEOLDU/MetXBxPyoO8vQCsPvdGNMd6x/YeWFM0YRsMNC1zXzoaE8riCnMW436VooICfEbstfOQ==} + timi-web@0.0.3: + resolution: {integrity: sha512-uCJ+XQf1DydvnZHyI5NmQxKmZWvkVNPRrhqEszhMEh4CKGMPG7wrNiTl6jAPDcY3x228AaRP5MPu6TzIs9GfFQ==} engines: {node: '>=16.0.0'} tinycolor2@1.6.0: @@ -1866,6 +1875,9 @@ packages: '@swc/wasm': optional: true + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2011,6 +2023,12 @@ packages: '@vue/composition-api': optional: true + vue-echarts@8.0.1: + resolution: {integrity: sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==} + peerDependencies: + echarts: ^6.0.0 + vue: ^3.3.0 + vue-eslint-parser@10.4.0: resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2097,6 +2115,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + snapshots: '@babel/helper-string-parser@7.27.1': {} @@ -2888,6 +2909,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + ee-first@1.1.1: {} encodeurl@2.0.0: {} @@ -3849,7 +3875,7 @@ snapshots: transitivePeerDependencies: - typescript - timi-web@0.0.1: + timi-web@0.0.3: dependencies: axios: 1.13.5 less: 4.5.1 @@ -3902,6 +3928,8 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tslib@2.3.0: {} + tslib@2.8.1: {} type-check@0.4.0: @@ -4023,6 +4051,11 @@ snapshots: dependencies: vue: 3.5.31(typescript@5.8.3) + vue-echarts@8.0.1(echarts@6.0.0)(vue@3.5.31(typescript@5.8.3)): + dependencies: + echarts: 6.0.0 + vue: 3.5.31(typescript@5.8.3) + vue-eslint-parser@10.4.0(eslint@9.26.0): dependencies: debug: 4.4.3 @@ -4110,3 +4143,7 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/src/api/system.ts b/src/api/system.ts new file mode 100644 index 0000000..bc59a79 --- /dev/null +++ b/src/api/system.ts @@ -0,0 +1,194 @@ +import axios, { AxiosError } from "axios"; +import { useSettingStore } from "@/store/settingStore"; + +interface ApiResponse { + code?: number | string; + data?: T; + msg?: string; + message?: string; +} + +export interface SystemStatusSnapshotView { + serverTime: number; + sampleRateMs: number; + snapshot: SystemStatusSnapshot; +} + +export interface SystemStatusHistoryView { + serverTime: number; + sampleRateMs: number; + from: number; + to: number; + points: SystemStatusHistoryPoint[]; +} + +export interface SystemStatusSnapshot { + os?: { + name?: string; + bootAt?: number; + uptimeMs?: number; + }; + cpu?: { + model?: string; + physicalCores?: number; + logicalCores?: number; + usagePercent?: number | null; + systemPercent?: number | null; + temperatureCelsius?: number; + }; + memory?: { + totalBytes?: number; + usedBytes?: number | null; + usagePercent?: number | null; + swapTotalBytes?: number; + swapUsedBytes?: number | null; + }; + jvm?: { + name?: string; + version?: string; + bootAt?: number; + heapInitBytes?: number; + heapMaxBytes?: number; + heapUsedBytes?: number | null; + heapCommittedBytes?: number | null; + gc?: { + collector?: string; + cycleCount?: number; + pauseCount?: number; + lastPauseAt?: number; + lastRecoveredBytes?: number; + }; + }; + network?: { + interfaceName?: string; + mac?: string; + rxBytesPerSecond?: number; + txBytesPerSecond?: number; + rxTotalBytes?: number; + txTotalBytes?: number; + rxPacketsTotal?: number; + txPacketsTotal?: number; + inErrors?: number; + outErrors?: number; + inDrops?: number; + collisions?: number; + }; +} + +export interface SystemStatusHistoryPoint { + at: number; + cpuUsagePercent?: number | null; + cpuSystemPercent?: number | null; + memoryUsedBytes?: number | null; + swapUsedBytes?: number | null; + heapUsedBytes?: number | null; + heapCommittedBytes?: number | null; + gcCycleTimeMs?: number | null; + gcPauseTimeMs?: number | null; + rxBytesPerSecond?: number | null; + txBytesPerSecond?: number | null; +} + +export async function getSystemStatus(metrics?: string): Promise { + const response = await axios.get | SystemStatusSnapshotView>(`${resolveBaseURL()}/system/server/status`, { + params: { + ...buildQueryParams(), + ...(metrics ? { metrics } : {}) + }, + timeout: 15000 + }); + + return unwrapResponse(response.data); +} + +export async function getSystemStatusHistory(params?: { + window?: string; + metrics?: string; +}): Promise { + const response = await axios.get | SystemStatusHistoryView>(`${resolveBaseURL()}/system/server/status/history`, { + params: { + ...buildQueryParams(), + ...(params?.window ? { window: params.window } : {}), + ...(params?.metrics ? { metrics: params.metrics } : {}) + }, + timeout: 15000 + }); + + return unwrapResponse(response.data); +} + +export function resolveSystemRequestErrorMessage(error: unknown): string { + if (error instanceof AxiosError) { + const message = resolveApiMessage(error.response?.data); + if (message) { + return message; + } + + if (error.message) { + return error.message; + } + } + + if (error instanceof Error && error.message) { + return error.message; + } + + return "请求失败,请稍后重试"; +} + +function unwrapResponse(payload: ApiResponse | T): T { + if (payload && typeof payload === "object" && "data" in payload) { + const data = (payload as ApiResponse).data; + if (data !== undefined) { + return data; + } + } + + if (payload && typeof payload === "object" && ("msg" in payload || "message" in payload)) { + throw new Error(resolveApiMessage(payload) || "接口返回为空"); + } + + return payload as T; +} + +function resolveBaseURL(): string { + const settingStore = useSettingStore(); + const connect = settingStore.connect; + const envBaseURL = typeof import.meta.env.VITE_API === "string" ? import.meta.env.VITE_API.trim() : ""; + + if (connect.host && connect.port) { + return `${connect.protocol}://${connect.host}:${connect.port}`; + } + + return envBaseURL.replace(/\/+$/, ""); +} + +function buildQueryParams(): Record { + const settingStore = useSettingStore(); + const token = settingStore.connect.token.trim(); + + if (!token) { + return {}; + } + + return { + token + }; +} + +function resolveApiMessage(payload: unknown): string { + if (!payload || typeof payload !== "object") { + return ""; + } + + const { msg, message } = payload as ApiResponse; + if (typeof msg === "string" && msg.trim()) { + return msg.trim(); + } + + if (typeof message === "string" && message.trim()) { + return message.trim(); + } + + return ""; +} diff --git a/src/pages/dashboard/DockerDashboard/DockerDashboard.vue b/src/pages/dashboard/DockerDashboard/DockerDashboard.vue new file mode 100644 index 0000000..937efeb --- /dev/null +++ b/src/pages/dashboard/DockerDashboard/DockerDashboard.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/src/pages/dashboard/ServerDashboard/ServerDashboard.vue b/src/pages/dashboard/ServerDashboard/ServerDashboard.vue new file mode 100644 index 0000000..df9a5f9 --- /dev/null +++ b/src/pages/dashboard/ServerDashboard/ServerDashboard.vue @@ -0,0 +1,653 @@ + + + + + diff --git a/src/pages/dashboard/UPSDashboard/UPSDashboard.vue b/src/pages/dashboard/UPSDashboard/UPSDashboard.vue new file mode 100644 index 0000000..6c206c8 --- /dev/null +++ b/src/pages/dashboard/UPSDashboard/UPSDashboard.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/src/pages/detail/ServerLogPage.vue b/src/pages/detail/ServerLogPage.vue deleted file mode 100644 index d670ba7..0000000 --- a/src/pages/detail/ServerLogPage.vue +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/src/pages/file/FileExplorerPage.vue b/src/pages/file/FileExplorerPage.vue index 3860cbf..e575f50 100644 --- a/src/pages/file/FileExplorerPage.vue +++ b/src/pages/file/FileExplorerPage.vue @@ -17,9 +17,9 @@ />
- +
- + @@ -72,7 +72,7 @@ const shouldUseRoutePath = computed(() => { return false; } - return route.name === "FilePage" || route.name === "FileExplorerPage"; + return route.name === "FileTab" || route.name === "FileExplorerPage"; }); const routeSegments = computed(() => normalizePath(route.params.pathMatch)); @@ -399,4 +399,4 @@ function getDirectoryKey(pathSegments: string[]): string { box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22); } } - + \ No newline at end of file diff --git a/src/pages/tabs/DashboardTab.vue b/src/pages/tabs/DashboardTab.vue new file mode 100644 index 0000000..a515ed5 --- /dev/null +++ b/src/pages/tabs/DashboardTab.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/pages/tabs/FilePage.vue b/src/pages/tabs/FileTab.vue similarity index 100% rename from src/pages/tabs/FilePage.vue rename to src/pages/tabs/FileTab.vue diff --git a/src/pages/tabs/ServerStatusPage.vue b/src/pages/tabs/ServerStatusPage.vue deleted file mode 100644 index 695a9e7..0000000 --- a/src/pages/tabs/ServerStatusPage.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/src/pages/tabs/SettingsPage.vue b/src/pages/tabs/SettingsTab.vue similarity index 100% rename from src/pages/tabs/SettingsPage.vue rename to src/pages/tabs/SettingsTab.vue diff --git a/src/router/index.ts b/src/router/index.ts index 1587726..4bd7e8f 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,7 +6,7 @@ import { DEFAULT_BODY_BACKGROUND, useGlobalUIStore } from "@/store/globalUIStore import { useSettingStore } from "@/store/settingStore"; import NotFoundPage from "@/pages/system/NotFoundPage.vue"; import ServerIndexPage from "@/pages/system/ServerIndexPage.vue"; -import FilePage from "@/pages/tabs/FilePage.vue"; +import FileTab from "@/pages/tabs/FileTab.vue"; import ServerLogPage from "@/pages/detail/ServerLogPage.vue"; const router = createRouter({ @@ -63,7 +63,7 @@ const router = createRouter({ contentFixedHeight: true, bodyBackground: "#F4F4F4" }, - component: FilePage + component: FileTab }, { path: "/server/logs", @@ -120,4 +120,4 @@ router.afterEach((to: RouteLocationNormalized) => { globalUIStore.setBodyBackground(targetBackground); }); -export default router; +export default router; \ No newline at end of file diff --git a/src/router/tabs.ts b/src/router/tabs.ts index fc6215a..3e521dc 100644 --- a/src/router/tabs.ts +++ b/src/router/tabs.ts @@ -1,12 +1,12 @@ import type { RouteRecordRaw } from "vue-router"; -import FilePage from "@/pages/tabs/FilePage.vue"; -import ServerStatusPage from "@/pages/tabs/ServerStatusPage.vue"; -import SettingsPage from "@/pages/tabs/SettingsPage.vue"; +import FileTab from "@/pages/tabs/FileTab.vue"; +import DashboardTab from "@/pages/tabs/DashboardTab.vue"; +import SettingsTab from "@/pages/tabs/SettingsTab.vue"; const tabs: RouteRecordRaw[] = [ { path: "/", - name: "FilePage", + name: "FileTab", meta: { depth: 2, navBarVisible: true, @@ -17,11 +17,11 @@ const tabs: RouteRecordRaw[] = [ contentFixedHeight: true, bodyBackground: "#F4F4F4" }, - component: FilePage + component: FileTab }, { path: "/server", - name: "ServerStatusPage", + name: "DashboardTab", meta: { depth: 2, navBarVisible: true, @@ -29,13 +29,13 @@ const tabs: RouteRecordRaw[] = [ tabBarVisible: true, tabBarRoot: true, tabBarPadding: true, - bodyBackground: "#FFF" + bodyBackground: "#F4F4F4" }, - component: ServerStatusPage + component: DashboardTab }, { path: "/settings", - name: "SettingsPage", + name: "SettingsTab", meta: { depth: 2, navBarVisible: true, @@ -45,7 +45,7 @@ const tabs: RouteRecordRaw[] = [ tabBarPadding: true, bodyBackground: "#FFF" }, - component: SettingsPage + component: SettingsTab } ];