add Dashboard

This commit is contained in:
Timi
2026-04-10 00:01:38 +08:00
parent d9e32c4dbe
commit 489cbb5d0f
14 changed files with 1134 additions and 54 deletions

194
src/api/system.ts Normal file
View 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 "";
}

View 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>

View 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>

View 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>

View File

@@ -1,6 +0,0 @@
<template>
<route-placeholder title="服务日志页" description="这里保留为二级详情占位,用于验证从状态页进入日志页时的动效和返回行为。" />
</template>
<script setup lang="ts">
</script>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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
}
];