add Dashboard
This commit is contained in:
@@ -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",
|
||||
|
||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
194
src/api/system.ts
Normal file
194
src/api/system.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useSettingStore } from "@/store/settingStore";
|
||||
|
||||
interface ApiResponse<T> {
|
||||
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<SystemStatusSnapshotView> {
|
||||
const response = await axios.get<ApiResponse<SystemStatusSnapshotView> | 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<SystemStatusHistoryView> {
|
||||
const response = await axios.get<ApiResponse<SystemStatusHistoryView> | 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<T>(payload: ApiResponse<T> | T): T {
|
||||
if (payload && typeof payload === "object" && "data" in payload) {
|
||||
const data = (payload as ApiResponse<T>).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<string, string> {
|
||||
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<unknown>;
|
||||
if (typeof msg === "string" && msg.trim()) {
|
||||
return msg.trim();
|
||||
}
|
||||
|
||||
if (typeof message === "string" && message.trim()) {
|
||||
return message.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
57
src/pages/dashboard/DockerDashboard/DockerDashboard.vue
Normal file
57
src/pages/dashboard/DockerDashboard/DockerDashboard.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="docker-dashboard">
|
||||
<section class="card">
|
||||
<p class="tag">Docker</p>
|
||||
<h2 class="title">容器状态</h2>
|
||||
<p class="desc">这里用于承载容器列表、运行状态、镜像占用和资源使用情况。</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.docker-dashboard {
|
||||
padding: 1rem;
|
||||
|
||||
.card {
|
||||
gap: .6rem;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--app-line);
|
||||
background: var(--app-card);
|
||||
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||
}
|
||||
|
||||
.tag,
|
||||
.title,
|
||||
.desc {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag,
|
||||
.desc {
|
||||
color: var(--app-sub);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .docker-dashboard {
|
||||
.card {
|
||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
653
src/pages/dashboard/ServerDashboard/ServerDashboard.vue
Normal file
653
src/pages/dashboard/ServerDashboard/ServerDashboard.vue
Normal file
@@ -0,0 +1,653 @@
|
||||
<template>
|
||||
<div class="server-dashboard">
|
||||
<section class="card">
|
||||
<div class="head">
|
||||
<h3 class="sub-title">当前状态</h3>
|
||||
<t-button size="small" variant="outline" :loading="isLoading" @click="refreshDashboard">
|
||||
刷新
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading && !snapshotView" class="loading-wrap">
|
||||
<t-loading text="加载系统状态..." />
|
||||
</div>
|
||||
|
||||
<div v-else-if="snapshotView" class="metrics">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">操作系统</span>
|
||||
<span class="metric-value" v-text="osNameText" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">CPU 使用率</span>
|
||||
<span class="metric-value" v-text="cpuUsageText" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">CPU 温度</span>
|
||||
<span class="metric-value" v-text="cpuTemperatureText" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">内存使用</span>
|
||||
<span class="metric-value" v-text="memoryUsageText" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">JVM 堆内存</span>
|
||||
<span class="metric-value" v-text="heapUsageText" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">网络吞吐</span>
|
||||
<span class="metric-value" v-text="networkThroughputText" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">系统运行时长</span>
|
||||
<span class="metric-value" v-text="uptimeText" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t-empty v-else description="暂无系统状态数据" />
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="head">
|
||||
<h3 class="sub-title">历史采样(30 分钟)</h3>
|
||||
<t-tabs class="metric-tabs" :value="historyMetric" @change="handleHistoryMetricChange">
|
||||
<t-tab-panel value="cpu" label="CPU" />
|
||||
<t-tab-panel value="memory" label="内存" />
|
||||
<t-tab-panel value="network" label="网络" />
|
||||
</t-tabs>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading && !historyItems.length" class="loading-wrap">
|
||||
<t-loading text="加载历史数据..." />
|
||||
</div>
|
||||
|
||||
<v-chart
|
||||
v-else-if="historyItems.length"
|
||||
ref="historyChartRef"
|
||||
class="history-chart"
|
||||
:option="historyChartOption"
|
||||
:autoresize="true"
|
||||
/>
|
||||
|
||||
<t-empty v-else description="暂无历史采样数据" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { use } from "echarts/core";
|
||||
import { SVGRenderer } from "echarts/renderers";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components";
|
||||
import VChart from "vue-echarts";
|
||||
import {
|
||||
getSystemStatus,
|
||||
getSystemStatusHistory,
|
||||
resolveSystemRequestErrorMessage,
|
||||
type SystemStatusHistoryPoint,
|
||||
type SystemStatusSnapshotView
|
||||
} from "@/api/system";
|
||||
import type { EChartsOption } from "echarts";
|
||||
|
||||
interface RenderHistoryItem {
|
||||
at: number;
|
||||
atText: string;
|
||||
cpuUsage: number | null;
|
||||
memoryUsed: number | null;
|
||||
rxSpeed: number | null;
|
||||
txSpeed: number | null;
|
||||
}
|
||||
|
||||
type HistoryMetric = "cpu" | "memory" | "network";
|
||||
|
||||
use([
|
||||
SVGRenderer,
|
||||
LineChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent
|
||||
]);
|
||||
|
||||
const isLoading = ref(false);
|
||||
const snapshotView = ref<SystemStatusSnapshotView | null>(null);
|
||||
const historyPoints = ref<SystemStatusHistoryPoint[]>([]);
|
||||
const historyMetric = ref<HistoryMetric>("cpu");
|
||||
const historyChartRef = ref<InstanceType<typeof VChart> | null>(null);
|
||||
const isDarkTheme = ref(false);
|
||||
let themeObserver: MutationObserver | null = null;
|
||||
|
||||
const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false
|
||||
});
|
||||
|
||||
const osNameText = computed(() => snapshotView.value?.snapshot?.os?.name || "--");
|
||||
|
||||
const cpuUsageText = computed(() => {
|
||||
const usage = snapshotView.value?.snapshot?.cpu?.usagePercent;
|
||||
return formatPercentText(usage);
|
||||
});
|
||||
|
||||
const cpuTemperatureText = computed(() => {
|
||||
const temperature = snapshotView.value?.snapshot?.cpu?.temperatureCelsius;
|
||||
if (typeof temperature !== "number" || Number.isNaN(temperature)) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return `${temperature.toFixed(1)} °C`;
|
||||
});
|
||||
|
||||
const memoryUsageText = computed(() => {
|
||||
const memory = snapshotView.value?.snapshot?.memory;
|
||||
const usedBytes = memory?.usedBytes;
|
||||
const totalBytes = memory?.totalBytes;
|
||||
if (typeof usedBytes !== "number" || typeof totalBytes !== "number" || totalBytes < 1) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return `${formatBytes(usedBytes)} / ${formatBytes(totalBytes)} (${formatPercentText(memory.usagePercent)})`;
|
||||
});
|
||||
|
||||
const heapUsageText = computed(() => {
|
||||
const jvm = snapshotView.value?.snapshot?.jvm;
|
||||
const usedBytes = jvm?.heapUsedBytes;
|
||||
const maxBytes = jvm?.heapMaxBytes;
|
||||
if (typeof usedBytes !== "number" || typeof maxBytes !== "number" || maxBytes < 1) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
const usagePercent = usedBytes / maxBytes * 100;
|
||||
return `${formatBytes(usedBytes)} / ${formatBytes(maxBytes)} (${formatPercentText(usagePercent)})`;
|
||||
});
|
||||
|
||||
const networkThroughputText = computed(() => {
|
||||
const network = snapshotView.value?.snapshot?.network;
|
||||
const rx = network?.rxBytesPerSecond;
|
||||
const tx = network?.txBytesPerSecond;
|
||||
if (typeof rx !== "number" || typeof tx !== "number") {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return `↓ ${formatSpeed(rx)} ↑ ${formatSpeed(tx)}`;
|
||||
});
|
||||
|
||||
const uptimeText = computed(() => {
|
||||
const uptime = snapshotView.value?.snapshot?.os?.uptimeMs;
|
||||
if (typeof uptime !== "number" || uptime < 0) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return formatDuration(uptime);
|
||||
});
|
||||
|
||||
const historyItems = computed<RenderHistoryItem[]>(() => {
|
||||
return historyPoints.value
|
||||
.slice()
|
||||
.sort((left, right) => right.at - left.at)
|
||||
.slice(0, 12)
|
||||
.map((point) => ({
|
||||
at: point.at,
|
||||
atText: formatDateTime(point.at),
|
||||
cpuUsage: normalizeNumber(point.cpuUsagePercent),
|
||||
memoryUsed: normalizeNumber(point.memoryUsedBytes),
|
||||
rxSpeed: normalizeNumber(point.rxBytesPerSecond),
|
||||
txSpeed: normalizeNumber(point.txBytesPerSecond)
|
||||
}));
|
||||
});
|
||||
|
||||
const historyChartOption = computed<EChartsOption>(() => {
|
||||
const axisColor = isDarkTheme.value ? "#9aa4b2" : "#7e8a9a";
|
||||
const lineColor = isDarkTheme.value ? "rgba(255, 255, 255, .12)" : "rgba(18, 42, 66, .08)";
|
||||
const legendColor = isDarkTheme.value ? "#d7dee8" : "#334155";
|
||||
|
||||
if (historyMetric.value === "cpu") {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: "axis"
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: historyItems.value.map((item) => item.atText),
|
||||
axisLabel: {
|
||||
color: axisColor
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: lineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
color: axisColor,
|
||||
formatter: "{value}%"
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: lineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "line",
|
||||
name: "CPU 使用率",
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: .18
|
||||
},
|
||||
data: historyItems.value.map((item) => item.cpuUsage)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
if (historyMetric.value === "memory") {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: "axis"
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: historyItems.value.map((item) => item.atText),
|
||||
axisLabel: {
|
||||
color: axisColor
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: lineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
color: axisColor,
|
||||
formatter: (value: number) => formatBytes(value)
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: lineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "line",
|
||||
name: "内存使用",
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: .18
|
||||
},
|
||||
data: historyItems.value.map((item) => item.memoryUsed)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: "axis"
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
textStyle: {
|
||||
color: legendColor
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: historyItems.value.map((item) => item.atText),
|
||||
axisLabel: {
|
||||
color: axisColor
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: lineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
color: axisColor,
|
||||
formatter: (value: number) => `${formatBytes(value)}/s`
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: lineColor
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "line",
|
||||
name: "接收",
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
data: historyItems.value.map((item) => item.rxSpeed)
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
name: "发送",
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
data: historyItems.value.map((item) => item.txSpeed)
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
syncTheme();
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
themeObserver = new MutationObserver(() => {
|
||||
syncTheme();
|
||||
});
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"]
|
||||
});
|
||||
void refreshDashboard();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
themeObserver?.disconnect();
|
||||
themeObserver = null;
|
||||
});
|
||||
|
||||
async function refreshDashboard(): Promise<void> {
|
||||
if (isLoading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const [statusView, historyView] = await Promise.all([
|
||||
getSystemStatus("os,cpu,memory,network,jvm"),
|
||||
getSystemStatusHistory({
|
||||
window: "30m",
|
||||
metrics: "cpu,memory,network"
|
||||
})
|
||||
]);
|
||||
|
||||
snapshotView.value = statusView;
|
||||
historyPoints.value = historyView.points || [];
|
||||
} catch (error) {
|
||||
Toast({
|
||||
theme: "error",
|
||||
message: resolveSystemRequestErrorMessage(error)
|
||||
});
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleHistoryMetricChange(value: string): void {
|
||||
const nextValue = value as HistoryMetric;
|
||||
if (nextValue === "cpu" || nextValue === "memory" || nextValue === "network") {
|
||||
historyMetric.value = nextValue;
|
||||
}
|
||||
}
|
||||
|
||||
function syncTheme(): void {
|
||||
const htmlDark = document.documentElement.classList.contains("theme-dark");
|
||||
const bodyDark = document.body.classList.contains("theme-dark");
|
||||
isDarkTheme.value = htmlDark || bodyDark;
|
||||
}
|
||||
|
||||
function handleWindowResize(): void {
|
||||
historyChartRef.value?.resize();
|
||||
}
|
||||
|
||||
function formatDateTime(value?: number): string {
|
||||
if (!value) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return dateTimeFormatter.format(value);
|
||||
}
|
||||
|
||||
function formatPercentText(value?: number | null): string {
|
||||
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function formatSpeed(value?: number | null): string {
|
||||
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return `${formatBytes(value)}/s`;
|
||||
}
|
||||
|
||||
function formatBytes(value?: number | null): string {
|
||||
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (value < 1024) {
|
||||
return `${value} B`;
|
||||
}
|
||||
|
||||
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||
let nextValue = value / 1024;
|
||||
let index = 0;
|
||||
|
||||
while (1024 <= nextValue && index < units.length - 1) {
|
||||
nextValue /= 1024;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (10 <= nextValue) {
|
||||
return `${nextValue.toFixed(0)} ${units[index]}`;
|
||||
}
|
||||
|
||||
if (1 <= nextValue) {
|
||||
return `${nextValue.toFixed(1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
return `${nextValue.toFixed(2)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const secondsInDay = 24 * 3600;
|
||||
const secondsInHour = 3600;
|
||||
const secondsInMinute = 60;
|
||||
const days = Math.floor(totalSeconds / secondsInDay);
|
||||
const hours = Math.floor(totalSeconds % secondsInDay / secondsInHour);
|
||||
const minutes = Math.floor(totalSeconds % secondsInHour / secondsInMinute);
|
||||
const seconds = totalSeconds % secondsInMinute;
|
||||
const parts: string[] = [];
|
||||
|
||||
if (0 < days) {
|
||||
parts.push(`${days} 天`);
|
||||
}
|
||||
|
||||
if (0 < hours || parts.length) {
|
||||
parts.push(`${hours} 小时`);
|
||||
}
|
||||
|
||||
if (0 < minutes || parts.length) {
|
||||
parts.push(`${minutes} 分钟`);
|
||||
}
|
||||
|
||||
parts.push(`${seconds} 秒`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function normalizeNumber(value?: number | null): number | null {
|
||||
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.server-dashboard {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.card {
|
||||
gap: .75rem;
|
||||
border: 1px solid var(--app-line);
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
background: #FFF;
|
||||
border-radius: 1rem;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||
}
|
||||
|
||||
.intro {
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.tag,
|
||||
.title,
|
||||
.desc,
|
||||
.time,
|
||||
.sub-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag,
|
||||
.desc,
|
||||
.time {
|
||||
color: var(--app-sub);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
||||
.head {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 1rem;
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.metric-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
display: flex;
|
||||
min-height: 4rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.history-chart {
|
||||
width: 100%;
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: .875rem;
|
||||
color: var(--app-sub);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: 600;
|
||||
font-size: .875rem;
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
:deep(.metric-tabs .t-tabs__nav) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .server-dashboard {
|
||||
.card {
|
||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/pages/dashboard/UPSDashboard/UPSDashboard.vue
Normal file
57
src/pages/dashboard/UPSDashboard/UPSDashboard.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="ups-dashboard">
|
||||
<section class="card">
|
||||
<p class="tag">UPS</p>
|
||||
<h2 class="title">供电监控</h2>
|
||||
<p class="desc">这里用于展示 UPS 电池状态、负载功率、剩余续航和告警信息。</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ups-dashboard {
|
||||
padding: 1rem;
|
||||
|
||||
.card {
|
||||
gap: .6rem;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--app-line);
|
||||
background: var(--app-card);
|
||||
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||
}
|
||||
|
||||
.tag,
|
||||
.title,
|
||||
.desc {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag,
|
||||
.desc {
|
||||
color: var(--app-sub);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .ups-dashboard {
|
||||
.card {
|
||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<route-placeholder title="服务日志页" description="这里保留为二级详情占位,用于验证从状态页进入日志页时的动效和返回行为。" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -17,9 +17,9 @@
|
||||
/>
|
||||
</section>
|
||||
<div v-else-if="pageLoading" class="loading-wrap">
|
||||
<t-loading text="加载目录中" />
|
||||
<t-loading text="Loading directory" />
|
||||
</div>
|
||||
<t-empty v-else description="当前目录为空" />
|
||||
<t-empty v-else description="Directory is empty" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
111
src/pages/tabs/DashboardTab.vue
Normal file
111
src/pages/tabs/DashboardTab.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="dashboard-tab">
|
||||
<div class="tabs-wrap">
|
||||
<t-tabs class="tabs" :value="activeTab" @change="onChangeTab">
|
||||
<t-tab-panel value="server" label="服务器" />
|
||||
<t-tab-panel value="docker" label="Docker" />
|
||||
<t-tab-panel value="ups" label="UPS" />
|
||||
</t-tabs>
|
||||
</div>
|
||||
<div class="content-wrap">
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<component :is="activeComponent" :key="activeTab" class="dashboard-panel" />
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
import ServerDashboard from "@/pages/dashboard/ServerDashboard/ServerDashboard.vue";
|
||||
import DockerDashboard from "@/pages/dashboard/DockerDashboard/DockerDashboard.vue";
|
||||
import UPSDashboard from "@/pages/dashboard/UPSDashboard/UPSDashboard.vue";
|
||||
|
||||
type DashboardTabValue = "server" | "docker" | "ups";
|
||||
|
||||
const tabOrder: DashboardTabValue[] = ["server", "docker", "ups"];
|
||||
const dashboardMap: Record<DashboardTabValue, Component> = {
|
||||
server: ServerDashboard,
|
||||
docker: DockerDashboard,
|
||||
ups: UPSDashboard
|
||||
};
|
||||
|
||||
const activeTab = ref<DashboardTabValue>("server");
|
||||
const transitionName = ref<"slide-left" | "slide-right">("slide-left");
|
||||
const activeComponent = computed(() => dashboardMap[activeTab.value]);
|
||||
|
||||
function onChangeTab(value: string): void {
|
||||
const nextTab = value as DashboardTabValue;
|
||||
if (nextTab === activeTab.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = tabOrder.indexOf(nextTab);
|
||||
const currentIndex = tabOrder.indexOf(activeTab.value);
|
||||
transitionName.value = currentIndex < nextIndex ? "slide-left" : "slide-right";
|
||||
activeTab.value = nextTab;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dashboard-tab {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
|
||||
.tabs-wrap {
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
position: sticky;
|
||||
background: var(--app-bg, #fff);
|
||||
border-bottom: 1px solid var(--app-line);
|
||||
|
||||
.tabs {
|
||||
:deep(.t-tabs__nav) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
:deep(.t-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.dashboard-panel {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition: transform .28s ease, opacity .28s ease;
|
||||
}
|
||||
|
||||
.slide-left-enter-from,
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20%);
|
||||
}
|
||||
|
||||
.slide-left-leave-to,
|
||||
.slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20%);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .dashboard-tab {
|
||||
.tabs-wrap {
|
||||
background: var(--app-bg, #11161d);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<route-placeholder title="状态页" description="这里保留为服务器状态入口,用于验证底部标签切换和服务日志页前进返回动画。" />
|
||||
<t-button block variant="outline" @click="openServerLogs">
|
||||
查看服务日志页
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const router = useRouter();
|
||||
|
||||
async function openServerLogs(): Promise<void> {
|
||||
await router.push("/server/logs");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: 1.2rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user