Compare commits
8 Commits
a6c89717a6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e192a90e1 | ||
|
|
6f067477f0 | ||
|
|
11c1199449 | ||
|
|
42efa2b370 | ||
|
|
631122c79b | ||
|
|
9942afafa7 | ||
|
|
2f45330ebd | ||
|
|
753ab56e06 |
@@ -3,7 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="application-name" content="Timi Server" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Timi Server" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<title>Timi Server</title>
|
<title>Timi Server</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"tdesign-mobile-vue": "^1.13.2",
|
"tdesign-mobile-vue": "^1.13.2",
|
||||||
"timi-tdesign-mobile": "0.0.9",
|
"timi-tdesign-mobile": "0.0.9",
|
||||||
"timi-web": "0.0.17",
|
"timi-web": "0.0.18",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"vue": "^3.5.16",
|
"vue": "^3.5.16",
|
||||||
"vue-router": "4.5.1",
|
"vue-router": "4.5.1",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -30,8 +30,8 @@ importers:
|
|||||||
specifier: 0.0.9
|
specifier: 0.0.9
|
||||||
version: 0.0.9(typescript@5.8.3)
|
version: 0.0.9(typescript@5.8.3)
|
||||||
timi-web:
|
timi-web:
|
||||||
specifier: 0.0.17
|
specifier: 0.0.18
|
||||||
version: 0.0.17
|
version: 0.0.18
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@24.12.0)(typescript@5.8.3)
|
version: 10.9.2(@types/node@24.12.0)(typescript@5.8.3)
|
||||||
@@ -1832,8 +1832,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-SV8m/FsLsEabSmJ/NvI650fLyb/JCLz7JUNQrkWeoo5xICElsN4x39L3n4lTzu6ARidjpT6uA+XEmA4wnyQmuA==}
|
resolution: {integrity: sha512-SV8m/FsLsEabSmJ/NvI650fLyb/JCLz7JUNQrkWeoo5xICElsN4x39L3n4lTzu6ARidjpT6uA+XEmA4wnyQmuA==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
timi-web@0.0.17:
|
timi-web@0.0.18:
|
||||||
resolution: {integrity: sha512-6ycsqfnl+zeQkgOICjhhJYHoo3CtCxnhaNFFirNLVfX/VrrTDjW7DiR+/W8bouZzx6AR19fZKyG+UE0BKKBURg==}
|
resolution: {integrity: sha512-e7QDPZbt7ZVvghtYwgpZrWJ0onIB4WNMhUaODCYyOdeUmi6PGQk4Aax/Gyo1M12MflrjaBUmlCF+9kPAqKGdtQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
tinycolor2@1.6.0:
|
tinycolor2@1.6.0:
|
||||||
@@ -3875,7 +3875,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
timi-web@0.0.17:
|
timi-web@0.0.18:
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.13.5
|
axios: 1.13.5
|
||||||
less: 4.5.1
|
less: 4.5.1
|
||||||
|
|||||||
10
public/manifest.webmanifest
Normal file
10
public/manifest.webmanifest
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Timi Server",
|
||||||
|
"short_name": "Timi",
|
||||||
|
"lang": "zh-CN",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#ffffff"
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
import { viewDepthKey } from "vue-router";
|
import { viewDepthKey } from "vue-router";
|
||||||
import { useGlobalUIStore } from "@/store/globalUIStore";
|
import { useGlobalUIStore } from "@/store/globalUIStore";
|
||||||
|
import { hasProgrammaticBackNavigation } from "@/utils/backNavigationSignal";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PageTransition"
|
name: "PageTransition"
|
||||||
@@ -26,6 +27,8 @@ const viewDepth = inject(viewDepthKey, 0);
|
|||||||
|
|
||||||
const transitionName = ref("");
|
const transitionName = ref("");
|
||||||
const hasTransition = ref(false);
|
const hasTransition = ref(false);
|
||||||
|
// iOS 左滑返回由系统接管,页面侧不应叠加离场动画。
|
||||||
|
const isIOS = /iP(ad|hone|od)/i.test(window.navigator.userAgent);
|
||||||
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
||||||
const currentDepth = computed(() => Number(unref(viewDepth)));
|
const currentDepth = computed(() => Number(unref(viewDepth)));
|
||||||
const pageKey = computed(() => {
|
const pageKey = computed(() => {
|
||||||
@@ -72,6 +75,17 @@ const unregisterGuard = router.beforeEach((to, from) => {
|
|||||||
|
|
||||||
const toDepth = calcDepth(to);
|
const toDepth = calcDepth(to);
|
||||||
const fromDepth = calcDepth(from);
|
const fromDepth = calcDepth(from);
|
||||||
|
const isBackwardNavigation = toDepth < fromDepth;
|
||||||
|
// 主动触发的返回(如导航栏返回)保留动画。
|
||||||
|
const isProgrammaticBack = hasProgrammaticBackNavigation();
|
||||||
|
// 仅在 iOS 系统返回手势下关闭动画。
|
||||||
|
const shouldDisableTransition = isIOS && isBackwardNavigation && !isProgrammaticBack;
|
||||||
|
|
||||||
|
if (shouldDisableTransition) {
|
||||||
|
transitionName.value = "";
|
||||||
|
hasTransition.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (fromDepth < toDepth) {
|
if (fromDepth < toDepth) {
|
||||||
transitionName.value = "push-left";
|
transitionName.value = "push-left";
|
||||||
@@ -102,8 +116,8 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
.pages {
|
.pages {
|
||||||
|
|||||||
@@ -5,9 +5,14 @@
|
|||||||
v-if="navBarStore.isShowing"
|
v-if="navBarStore.isShowing"
|
||||||
class="nav-bar"
|
class="nav-bar"
|
||||||
:title="navBarStore.title"
|
:title="navBarStore.title"
|
||||||
:left-arrow="!!navBarStore.canBack"
|
:left-arrow="!hasCustomLeft && !!navBarStore.canBack"
|
||||||
@left-click="doBack"
|
@left-click="onLeftClick"
|
||||||
>
|
>
|
||||||
|
<template v-if="hasCustomLeft" #left>
|
||||||
|
<div class="nav-extra nav-extra-left">
|
||||||
|
<component :is="navBarStore.leftRenderer" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #right>
|
<template #right>
|
||||||
<div class="nav-extra">
|
<div class="nav-extra">
|
||||||
<component :is="navBarStore.rightRenderer" v-if="navBarStore.rightRenderer" />
|
<component :is="navBarStore.rightRenderer" v-if="navBarStore.rightRenderer" />
|
||||||
@@ -17,8 +22,9 @@
|
|||||||
<div :class="{ 'is-fixed-height': isContentFixedHeight }" class="router-view">
|
<div :class="{ 'is-fixed-height': isContentFixedHeight }" class="router-view">
|
||||||
<page-transition />
|
<page-transition />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- iOS 手势返回场景下临时关闭 tabbar 过渡,避免系统回退与页面动画叠加 -->
|
||||||
<t-tab-bar
|
<t-tab-bar
|
||||||
:class="{ 'is-hidden': !tabBarStore.isShowing }"
|
:class="{ 'is-hidden': !tabBarStore.isShowing, 'skip-transition': tabBarStore.shouldSkipTransition }"
|
||||||
class="tab-bar glass-white"
|
class="tab-bar glass-white"
|
||||||
v-model="tabVal"
|
v-model="tabVal"
|
||||||
shape="round"
|
shape="round"
|
||||||
@@ -80,6 +86,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// ---------- 导航返回 ----------
|
// ---------- 导航返回 ----------
|
||||||
|
|
||||||
|
const hasCustomLeft = computed(() => !!navBarStore.leftRenderer);
|
||||||
|
|
||||||
function doBack(): void {
|
function doBack(): void {
|
||||||
if (navBarStore.backTo) {
|
if (navBarStore.backTo) {
|
||||||
router.push(navBarStore.backTo);
|
router.push(navBarStore.backTo);
|
||||||
@@ -89,6 +97,14 @@ function doBack(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLeftClick(): void {
|
||||||
|
if (hasCustomLeft.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doBack();
|
||||||
|
}
|
||||||
|
|
||||||
const navBarPadding = computed(() => {
|
const navBarPadding = computed(() => {
|
||||||
if (navBarStore.isShowing) {
|
if (navBarStore.isShowing) {
|
||||||
return `${navBarStore.height || 48}px`;
|
return `${navBarStore.height || 48}px`;
|
||||||
@@ -114,33 +130,29 @@ function resolveTabValue(path: string): string {
|
|||||||
if (path === "/" || path.startsWith("/files/")) {
|
if (path === "/" || path.startsWith("/files/")) {
|
||||||
return "/";
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(() => route.path, (newPath: string) => {
|
||||||
() => route.path,
|
|
||||||
(newPath: string) => {
|
|
||||||
tabVal.value = resolveTabValue(newPath);
|
tabVal.value = resolveTabValue(newPath);
|
||||||
},
|
}, { immediate: true });
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function onChangeTab(value: string): void {
|
async function onChangeTab(value: string): Promise<void> {
|
||||||
if (value === "__music__") {
|
if (value === "__music__") {
|
||||||
musicPlayerStore.setPopupVisible(true);
|
musicPlayerStore.setPopupVisible(true);
|
||||||
tabVal.value = resolveTabValue(route.path);
|
tabVal.value = resolveTabValue(route.path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (resolveTabValue(route.path) === value) {
|
||||||
router.push(value);
|
return;
|
||||||
|
}
|
||||||
|
await router.replace(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTabBarPaddingNeeded = computed(() => {
|
const isTabBarPaddingNeeded = computed(() => {
|
||||||
if (!tabBarStore.isShowing) {
|
if (!tabBarStore.isShowing) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return route.meta.tabBarPadding !== false;
|
return route.meta.tabBarPadding !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,7 +160,6 @@ const tabBarPadding = computed(() => {
|
|||||||
if (isTabBarPaddingNeeded.value) {
|
if (isTabBarPaddingNeeded.value) {
|
||||||
return "6rem";
|
return "6rem";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "0rem";
|
return "0rem";
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,6 +180,7 @@ const contentHeight = computed(() => {
|
|||||||
.main-layout {
|
.main-layout {
|
||||||
--app-nav-offset: v-bind(topPadding);
|
--app-nav-offset: v-bind(topPadding);
|
||||||
--safe-top: env(safe-area-inset-top, 0px);
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -177,6 +189,8 @@ const contentHeight = computed(() => {
|
|||||||
transition: padding-bottom .24s ease;
|
transition: padding-bottom .24s ease;
|
||||||
|
|
||||||
.nav-bar {
|
.nav-bar {
|
||||||
|
--td-navbar-padding-top: var(--safe-top);
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -184,11 +198,8 @@ const contentHeight = computed(() => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
:deep(.t-navbar) {
|
:deep(.t-navbar) {
|
||||||
z-index: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-navbar__content) {
|
&__content {
|
||||||
color: var(--tui-black, #000);
|
color: var(--tui-black, #000);
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||||
background: rgba(250, 250, 250, .8);
|
background: rgba(250, 250, 250, .8);
|
||||||
@@ -196,12 +207,23 @@ const contentHeight = computed(() => {
|
|||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__left,
|
||||||
|
&__right {
|
||||||
|
top: var(----td-navbar-padding-top);
|
||||||
|
height: var(--td-navbar-height, 48px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nav-extra {
|
.nav-extra {
|
||||||
gap: .35rem;
|
gap: .35rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-extra-left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-view {
|
.router-view {
|
||||||
@@ -218,9 +240,9 @@ const contentHeight = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
bottom: 1rem;
|
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -240,6 +262,11 @@ const contentHeight = computed(() => {
|
|||||||
transform: translateY(calc(100% + 1rem));
|
transform: translateY(calc(100% + 1rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.skip-transition {
|
||||||
|
// 配合 tabBarStore.shouldSkipTransition:仅本次导航禁用过渡
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
50
src/main.ts
50
src/main.ts
@@ -11,6 +11,53 @@ import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
|||||||
import { axios } from "timi-web";
|
import { axios } from "timi-web";
|
||||||
import { useSettingStore } from "@/store/settingStore.ts";
|
import { useSettingStore } from "@/store/settingStore.ts";
|
||||||
|
|
||||||
|
type FullscreenElement = HTMLElement & {
|
||||||
|
webkitRequestFullscreen?: () => Promise<void> | void;
|
||||||
|
msRequestFullscreen?: () => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StandaloneNavigator = Navigator & {
|
||||||
|
standalone?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestLaunchFullscreen = async (): Promise<void> => {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigatorInfo = window.navigator as StandaloneNavigator;
|
||||||
|
const isStandaloneMode = window.matchMedia("(display-mode: standalone)").matches || navigatorInfo.standalone === true;
|
||||||
|
if (isStandaloneMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootElement = document.documentElement as FullscreenElement;
|
||||||
|
if (rootElement.requestFullscreen) {
|
||||||
|
await rootElement.requestFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rootElement.webkitRequestFullscreen) {
|
||||||
|
await rootElement.webkitRequestFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rootElement.msRequestFullscreen) {
|
||||||
|
await rootElement.msRequestFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindLaunchFullscreenRequest = (): void => {
|
||||||
|
const onFirstInteraction = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await requestLaunchFullscreen();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("\u5168\u5c4f\u8bf7\u6c42\u88ab\u6d4f\u89c8\u5668\u62d2\u7edd\u6216\u4e0d\u652f\u6301", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointerdown", onFirstInteraction, { once: true, passive: true });
|
||||||
|
window.addEventListener("keydown", onFirstInteraction, { once: true });
|
||||||
|
};
|
||||||
|
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use((config) => {
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const token = settingStore.connect.token.trim();
|
const token = settingStore.connect.token.trim();
|
||||||
@@ -35,9 +82,10 @@ app.config.errorHandler = (error) => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "页面发生异常,请稍后重试"
|
message: "\u9875\u9762\u53d1\u751f\u5f02\u5e38\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.mount("#root");
|
app.mount("#root");
|
||||||
|
bindLaunchFullscreenRequest();
|
||||||
|
|||||||
@@ -731,6 +731,12 @@ function clearAutoRefreshTimer(): void {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(canvas) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -140,14 +140,16 @@ import { useRouter } from "vue-router";
|
|||||||
import type { EChartsOption, SeriesOption } from "echarts";
|
import type { EChartsOption, SeriesOption } from "echarts";
|
||||||
import SystemAPI from "@/api/SystemAPI";
|
import SystemAPI from "@/api/SystemAPI";
|
||||||
import type { SystemStatusHistoryPoint, SystemStatusSnapshotView } from "@/types/System";
|
import type { SystemStatusHistoryPoint, SystemStatusSnapshotView } from "@/types/System";
|
||||||
import type { DashboardHistoryMetric } from "@/store/settingStore";
|
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import { IOSize, type LabelValue, Text, Time } from "timi-web";
|
import { IOSize, type LabelValue, Text, Time } from "timi-web";
|
||||||
import TCellInfo from "@/components/TCellInfo.vue";
|
import TCellInfo from "@/components/TCellInfo.vue";
|
||||||
import type { ProgressItem } from "@/components/ProgressGroup.vue";
|
import type { ProgressItem } from "@/components/ProgressGroup.vue";
|
||||||
import ProgressGroup from "@/components/ProgressGroup.vue";
|
import ProgressGroup from "@/components/ProgressGroup.vue";
|
||||||
|
|
||||||
type HistoryMetric = DashboardHistoryMetric;
|
type HistoryMetric = "cpu" | "memory" | "jvm" | "network";
|
||||||
|
|
||||||
|
const SERVER_SNAPSHOT_METRICS = ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"] as const;
|
||||||
|
const SERVER_HISTORY_METRICS = ["cpu", "memory", "jvm", "network"] as const;
|
||||||
|
|
||||||
use([
|
use([
|
||||||
SVGRenderer,
|
SVGRenderer,
|
||||||
@@ -262,7 +264,7 @@ const historyMetricTabs = computed(() => {
|
|||||||
network: "网络"
|
network: "网络"
|
||||||
};
|
};
|
||||||
|
|
||||||
return settingStore.dashboard.server.historyMetrics.map((metric) => ({
|
return SERVER_HISTORY_METRICS.map((metric) => ({
|
||||||
value: metric,
|
value: metric,
|
||||||
label: labelMap[metric]
|
label: labelMap[metric]
|
||||||
}));
|
}));
|
||||||
@@ -635,7 +637,7 @@ async function refreshSnapshot(): Promise<void> {
|
|||||||
}
|
}
|
||||||
isSnapshotLoading.value = true;
|
isSnapshotLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const metrics = settingStore.dashboard.server.snapshotMetrics.join(",");
|
const metrics = SERVER_SNAPSHOT_METRICS.join(",");
|
||||||
snapshotView.value = await SystemAPI.getStatus(metrics);
|
snapshotView.value = await SystemAPI.getStatus(metrics);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
@@ -657,7 +659,7 @@ async function refreshHistory(): Promise<void> {
|
|||||||
}
|
}
|
||||||
isHistoryLoading.value = true;
|
isHistoryLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const metrics = settingStore.dashboard.server.historyMetrics.join(",");
|
const metrics = SERVER_HISTORY_METRICS.join(",");
|
||||||
const historyView = await SystemAPI.getStatusHistory({
|
const historyView = await SystemAPI.getStatusHistory({
|
||||||
window: "1h",
|
window: "1h",
|
||||||
metrics
|
metrics
|
||||||
@@ -770,6 +772,12 @@ onMounted(restartAutoRefresh);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(canvas) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -615,6 +615,12 @@ function clearAutoRefreshTimer(): void {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(canvas) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -393,10 +393,4 @@ function getDirectoryKey(pathSegments: string[]): string {
|
|||||||
font-size: .8125rem;
|
font-size: .8125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.theme-dark) .page {
|
|
||||||
.go-up {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,103 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="connect-setting">
|
||||||
<section class="card">
|
<t-cell-group theme="card">
|
||||||
<div class="head">
|
<t-cell title="使用 HTTPS">
|
||||||
<p class="tag">连接配置</p>
|
<template #rightIcon>
|
||||||
<h2 class="header">服务器连接</h2>
|
<t-switch v-model="httpsEnabled" />
|
||||||
<p class="desc">这里的配置会持久化保存,缺失时应用会强制回到连接引导页。</p>
|
</template>
|
||||||
</div>
|
</t-cell>
|
||||||
|
<t-input
|
||||||
<div class="group">
|
v-model.trim="form.host"
|
||||||
<p class="label">协议</p>
|
clearable
|
||||||
<div class="protocols">
|
label="地址"
|
||||||
<t-button
|
placeholder="192.168.1.10 或 nas.local"
|
||||||
size="small"
|
/>
|
||||||
theme="primary"
|
<t-input
|
||||||
:variant="form.protocol === 'http' ? 'base' : 'outline'"
|
v-model.trim="form.port"
|
||||||
@click="setProtocol('http')"
|
clearable
|
||||||
>
|
label="端口"
|
||||||
HTTP
|
placeholder="8080"
|
||||||
</t-button>
|
type="number"
|
||||||
<t-button
|
/>
|
||||||
size="small"
|
<t-input
|
||||||
theme="primary"
|
v-model.trim="form.token"
|
||||||
:variant="form.protocol === 'https' ? 'base' : 'outline'"
|
clearable
|
||||||
@click="setProtocol('https')"
|
label="密钥"
|
||||||
>
|
placeholder="请输入访问密钥"
|
||||||
HTTPS
|
/>
|
||||||
</t-button>
|
</t-cell-group>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="group">
|
|
||||||
<p class="label">主机地址</p>
|
|
||||||
<t-input v-model="form.host" clearable placeholder="例如 192.168.1.100 或 nas.local" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="group">
|
|
||||||
<p class="label">端口</p>
|
|
||||||
<t-input v-model="form.port" clearable type="number" placeholder="例如 8080" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="group">
|
|
||||||
<p class="label">访问令牌</p>
|
|
||||||
<t-input v-model="form.token" clearable placeholder="请输入 token" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<t-button block theme="primary" @click="saveConnect">
|
|
||||||
保存连接配置
|
|
||||||
</t-button>
|
|
||||||
<t-button block variant="outline" @click="resetConnect">
|
|
||||||
清空连接配置
|
|
||||||
</t-button>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import type { ConnectProtocol, ConnectSetting } from "@/store/settingStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import type { ConnectSetting } from "@/store/settingStore";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "ConnectSetting"
|
name: "ConnectSetting"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
const form = reactive<ConnectSetting>({
|
// ---------- 顶部导航 ----------
|
||||||
protocol: "http",
|
|
||||||
host: "",
|
const navBarOwner = `connect-setting-${Math.random().toString(36).slice(2)}`;
|
||||||
port: "",
|
|
||||||
token: ""
|
const navBarLeftRenderer = defineComponent({
|
||||||
|
name: "connect-setting-nav-left",
|
||||||
|
setup() {
|
||||||
|
const buttonComponent = resolveComponent("t-button");
|
||||||
|
|
||||||
|
return () => h(buttonComponent, {
|
||||||
|
variant: "text",
|
||||||
|
theme: "default",
|
||||||
|
size: "small",
|
||||||
|
onClick: cancelConnect
|
||||||
|
}, () => "取消");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
const navBarRightRenderer = defineComponent({
|
||||||
() => settingStore.connect,
|
name: "connect-setting-nav-right",
|
||||||
(connect) => {
|
setup() {
|
||||||
Object.assign(form, connect);
|
const buttonComponent = resolveComponent("t-button");
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function setProtocol(protocol: ConnectProtocol): void {
|
return () => h(buttonComponent, {
|
||||||
form.protocol = protocol;
|
variant: "text",
|
||||||
}
|
theme: "primary",
|
||||||
|
size: "small",
|
||||||
function validateConnect(): boolean {
|
onClick: saveConnect
|
||||||
const host = form.host.trim();
|
}, () => "保存");
|
||||||
const port = form.port.trim();
|
|
||||||
const token = form.token.trim();
|
|
||||||
|
|
||||||
if (!host || !port || !token) {
|
|
||||||
Toast({
|
|
||||||
theme: "warning",
|
|
||||||
message: "请完整填写连接配置"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
function cancelConnect(): void {
|
||||||
|
Object.assign(form, settingStore.connect);
|
||||||
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConnect(): void {
|
function saveConnect(): void {
|
||||||
@@ -110,71 +91,60 @@ function saveConnect(): void {
|
|||||||
theme: "success",
|
theme: "success",
|
||||||
message: "连接配置已保存"
|
message: "连接配置已保存"
|
||||||
});
|
});
|
||||||
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetConnect(): void {
|
onMounted(() => {
|
||||||
settingStore.resetConnect();
|
navBarStore.setLeftRenderer(navBarLeftRenderer, navBarOwner);
|
||||||
Object.assign(form, settingStore.connect);
|
navBarStore.setRightRenderer(navBarRightRenderer, navBarOwner);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
navBarStore.clearLeft(navBarOwner);
|
||||||
|
navBarStore.clearRight(navBarOwner);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
|
const form = reactive<ConnectSetting>({
|
||||||
|
protocol: "http",
|
||||||
|
host: "",
|
||||||
|
port: "",
|
||||||
|
token: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => settingStore.connect, (connect) => {
|
||||||
|
Object.assign(form, connect);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const httpsEnabled = computed({
|
||||||
|
get: () => form.protocol === "https",
|
||||||
|
set: (value: boolean) => {
|
||||||
|
form.protocol = value ? "https" : "http";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateConnect(): boolean {
|
||||||
|
const host = form.host.trim();
|
||||||
|
const port = form.port.trim();
|
||||||
|
const token = form.token.trim();
|
||||||
|
if (!host || !port || !token) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "success",
|
theme: "warning",
|
||||||
message: "连接配置已清空"
|
message: "请完整填写连接配置"
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.page {
|
.connect-setting {
|
||||||
padding: 1rem;
|
padding-top: calc(var(--app-nav-offset) + 1rem);
|
||||||
|
|
||||||
.card {
|
:deep(.t-input:last-child:after) {
|
||||||
gap: 1rem;
|
background: transparent;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.head,
|
|
||||||
.group {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag,
|
|
||||||
.label,
|
|
||||||
.desc {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--app-sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag,
|
|
||||||
.label {
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.protocols {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .page {
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,88 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="dashboard-setting">
|
||||||
<section class="card">
|
<t-cell-group title="刷新频率" theme="card">
|
||||||
<div class="head">
|
<t-cell-info label="当前状态" :value="`${snapshotRefreshValue} 秒`">
|
||||||
<p class="tag">服务器</p>
|
<t-slider v-model="snapshotRefreshValue" :max="10" :min="1" :step="1" />
|
||||||
<h2 class="header">数据刷新与采集</h2>
|
</t-cell-info>
|
||||||
<p class="desc">用于配置服务器仪表板的请求频率和 metrics 参数。</p>
|
<t-cell-info label="历史采样" :value="`${historyRefreshValue} 秒`">
|
||||||
</div>
|
<t-slider v-model="historyRefreshValue" :max="10" :min="3" :step="1" />
|
||||||
|
</t-cell-info>
|
||||||
<div class="group">
|
</t-cell-group>
|
||||||
<p class="label">当前状态刷新频率(秒)</p>
|
|
||||||
<t-input v-model="snapshotRefreshText" type="number" clearable placeholder="默认 3 秒" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="group">
|
|
||||||
<p class="label">历史采样刷新频率(秒)</p>
|
|
||||||
<t-input v-model="historyRefreshText" type="number" clearable placeholder="默认 10 秒" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="group">
|
|
||||||
<p class="label">当前状态采集指标</p>
|
|
||||||
<div class="metrics">
|
|
||||||
<t-button
|
|
||||||
v-for="metric in snapshotMetricOptions"
|
|
||||||
:key="metric.value"
|
|
||||||
size="small"
|
|
||||||
theme="primary"
|
|
||||||
:variant="isSnapshotMetricChecked(metric.value) ? 'base' : 'outline'"
|
|
||||||
@click="toggleSnapshotMetric(metric.value)"
|
|
||||||
>
|
|
||||||
<span v-text="metric.label" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="group">
|
|
||||||
<p class="label">历史采样采集指标</p>
|
|
||||||
<div class="metrics">
|
|
||||||
<t-button
|
|
||||||
v-for="metric in historyMetricOptions"
|
|
||||||
:key="metric.value"
|
|
||||||
size="small"
|
|
||||||
theme="primary"
|
|
||||||
:variant="isHistoryMetricChecked(metric.value) ? 'base' : 'outline'"
|
|
||||||
@click="toggleHistoryMetric(metric.value)"
|
|
||||||
>
|
|
||||||
<span v-text="metric.label" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<t-button block theme="primary" @click="saveServerDashboardSetting">
|
|
||||||
保存服务器配置
|
|
||||||
</t-button>
|
|
||||||
<t-button block variant="outline" @click="resetServerDashboardSetting">
|
|
||||||
恢复默认
|
|
||||||
</t-button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<div class="head">
|
|
||||||
<p class="tag">Docker</p>
|
|
||||||
<h2 class="header">配置待定</h2>
|
|
||||||
<p class="desc">后续将支持 Docker 仪表板采集项和展示策略配置。</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<div class="head">
|
|
||||||
<p class="tag">UPS</p>
|
|
||||||
<h2 class="header">配置待定</h2>
|
|
||||||
<p class="desc">后续将支持 UPS 仪表板采集项和告警策略配置。</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import {
|
|
||||||
useSettingStore,
|
|
||||||
type DashboardHistoryMetric,
|
|
||||||
type DashboardSnapshotMetric,
|
|
||||||
type ServerDashboardSetting
|
|
||||||
} from "@/store/settingStore";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "DashboardSetting"
|
name: "DashboardSetting"
|
||||||
@@ -90,176 +20,35 @@ defineOptions({
|
|||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
const snapshotMetricOptions: Array<{ label: string; value: DashboardSnapshotMetric }> = [
|
const snapshotRefreshValue = computed({
|
||||||
{ label: "系统", value: "os" },
|
get: (): number => normalizeRefreshValue(settingStore.dashboard.server.snapshotRefreshSeconds, 1, 10, 3),
|
||||||
{ label: "CPU", value: "cpu" },
|
set: (value: number): void => {
|
||||||
{ label: "内存", value: "memory" },
|
|
||||||
{ label: "JVM", value: "jvm" },
|
|
||||||
{ label: "网络", value: "network" },
|
|
||||||
{ label: "硬件", value: "hardware" },
|
|
||||||
{ label: "磁盘", value: "storage" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const historyMetricOptions: Array<{ label: string; value: DashboardHistoryMetric }> = [
|
|
||||||
{ label: "CPU", value: "cpu" },
|
|
||||||
{ label: "内存", value: "memory" },
|
|
||||||
{ label: "JVM", value: "jvm" },
|
|
||||||
{ label: "网络", value: "network" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const snapshotRefreshText = ref("");
|
|
||||||
const historyRefreshText = ref("");
|
|
||||||
const selectedSnapshotMetrics = ref<DashboardSnapshotMetric[]>([]);
|
|
||||||
const selectedHistoryMetrics = ref<DashboardHistoryMetric[]>([]);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => settingStore.dashboard.server,
|
|
||||||
(setting) => {
|
|
||||||
snapshotRefreshText.value = String(setting.snapshotRefreshSeconds);
|
|
||||||
historyRefreshText.value = String(setting.historyRefreshSeconds);
|
|
||||||
selectedSnapshotMetrics.value = [...setting.snapshotMetrics];
|
|
||||||
selectedHistoryMetrics.value = [...setting.historyMetrics];
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function isSnapshotMetricChecked(metric: DashboardSnapshotMetric): boolean {
|
|
||||||
return selectedSnapshotMetrics.value.includes(metric);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHistoryMetricChecked(metric: DashboardHistoryMetric): boolean {
|
|
||||||
return selectedHistoryMetrics.value.includes(metric);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSnapshotMetric(metric: DashboardSnapshotMetric): void {
|
|
||||||
if (isSnapshotMetricChecked(metric)) {
|
|
||||||
if (selectedSnapshotMetrics.value.length <= 1) {
|
|
||||||
Toast({
|
|
||||||
theme: "warning",
|
|
||||||
message: "当前状态至少保留一个采集指标"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedSnapshotMetrics.value = selectedSnapshotMetrics.value.filter((item) => item !== metric);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedSnapshotMetrics.value = [...selectedSnapshotMetrics.value, metric];
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleHistoryMetric(metric: DashboardHistoryMetric): void {
|
|
||||||
if (isHistoryMetricChecked(metric)) {
|
|
||||||
if (selectedHistoryMetrics.value.length <= 1) {
|
|
||||||
Toast({
|
|
||||||
theme: "warning",
|
|
||||||
message: "历史采样至少保留一个采集指标"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedHistoryMetrics.value = selectedHistoryMetrics.value.filter((item) => item !== metric);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedHistoryMetrics.value = [...selectedHistoryMetrics.value, metric];
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveServerDashboardSetting(): void {
|
|
||||||
const nextSetting: Partial<ServerDashboardSetting> = {
|
|
||||||
snapshotRefreshSeconds: normalizeSecondValue(snapshotRefreshText.value, 3),
|
|
||||||
historyRefreshSeconds: normalizeSecondValue(historyRefreshText.value, 10),
|
|
||||||
snapshotMetrics: selectedSnapshotMetrics.value,
|
|
||||||
historyMetrics: selectedHistoryMetrics.value
|
|
||||||
};
|
|
||||||
|
|
||||||
settingStore.setServerDashboard(nextSetting);
|
|
||||||
Toast({
|
|
||||||
theme: "success",
|
|
||||||
message: "服务器仪表板配置已保存"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetServerDashboardSetting(): void {
|
|
||||||
settingStore.setServerDashboard({
|
settingStore.setServerDashboard({
|
||||||
snapshotRefreshSeconds: 3,
|
snapshotRefreshSeconds: normalizeRefreshValue(value, 1, 10, 3)
|
||||||
historyRefreshSeconds: 10,
|
|
||||||
snapshotMetrics: ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"],
|
|
||||||
historyMetrics: ["cpu", "memory", "jvm", "network"]
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Toast({
|
const historyRefreshValue = computed({
|
||||||
theme: "success",
|
get: (): number => normalizeRefreshValue(settingStore.dashboard.server.historyRefreshSeconds, 3, 10, 10),
|
||||||
message: "已恢复默认配置"
|
set: (value: number): void => {
|
||||||
|
settingStore.setServerDashboard({
|
||||||
|
historyRefreshSeconds: normalizeRefreshValue(value, 3, 10, 10)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function normalizeSecondValue(value: string, fallback: number): number {
|
function normalizeRefreshValue(value: number, min: number, max: number, fallback: number): number {
|
||||||
const numberValue = Number(value);
|
if (Number.isNaN(value)) {
|
||||||
if (Number.isNaN(numberValue)) {
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
const normalizedValue = Math.floor(value);
|
||||||
return Math.min(Math.max(Math.floor(numberValue), 1), 120);
|
return Math.min(Math.max(normalizedValue, min), max);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.page {
|
.dashboard-setting {
|
||||||
gap: 1rem;
|
padding-top: var(--app-nav-offset);
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.card {
|
|
||||||
gap: 1rem;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.head,
|
|
||||||
.group {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag,
|
|
||||||
.label,
|
|
||||||
.desc {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--app-sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag,
|
|
||||||
.label {
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metrics {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .page {
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="theme-setting">
|
||||||
<section class="card">
|
<t-cell-group theme="card">
|
||||||
<div class="head">
|
<t-radio
|
||||||
<p class="tag">主题</p>
|
|
||||||
<h2 class="header">界面模式</h2>
|
|
||||||
<p class="desc">当前提供浅色、深色和跟随系统三种模式。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modes">
|
|
||||||
<t-button
|
|
||||||
v-for="item in themeModeList"
|
v-for="item in themeModeList"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
theme="primary"
|
class="block"
|
||||||
:variant="globalUIStore.themeMode === item.value ? 'base' : 'outline'"
|
default-checked
|
||||||
|
allow-uncheck
|
||||||
|
:label="item.label"
|
||||||
|
icon="line"
|
||||||
|
:checked="globalUIStore.themeMode === item.value"
|
||||||
@click="globalUIStore.setThemeMode(item.value)"
|
@click="globalUIStore.setThemeMode(item.value)"
|
||||||
>
|
/>
|
||||||
<span v-text="item.label" />
|
</t-cell-group>
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -39,55 +33,14 @@ const themeModeList: Array<{ label: string; value: ThemeMode }> = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.page {
|
.theme-setting {
|
||||||
padding: 1rem;
|
padding-top: calc(var(--app-nav-offset) + 1rem);
|
||||||
|
|
||||||
.card {
|
:deep(.t-radio:last-child) {
|
||||||
gap: 1rem;
|
|
||||||
display: flex;
|
.t-radio__border {
|
||||||
padding: 1rem;
|
display: none;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag,
|
|
||||||
.desc {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--app-sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modes {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .page {
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,99 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="settings-tab">
|
||||||
<section class="card">
|
<h2 class="title">Timi Server</h2>
|
||||||
<div class="head">
|
<t-cell-group theme="card">
|
||||||
<p class="tag">系统设置</p>
|
<t-cell title="连接配置" arrow @click="router.push('/settings/connect')" />
|
||||||
<h2 class="header">配置入口</h2>
|
<t-cell title="仪表板" arrow @click="router.push('/settings/dashboard')" />
|
||||||
<p class="desc">设置页只保留入口,具体配置在独立页面中维护。</p>
|
<t-cell title="主题" arrow @click="router.push('/settings/theme')" />
|
||||||
</div>
|
</t-cell-group>
|
||||||
<div class="entries">
|
<copyright icp="粤ICP备2025368555号-1" domain="imyeyu.com" author="夜雨" />
|
||||||
<t-button block variant="outline" @click="openConnectSetting">
|
|
||||||
连接配置
|
|
||||||
</t-button>
|
|
||||||
<t-button block variant="outline" @click="openDashboardSetting">
|
|
||||||
仪表板
|
|
||||||
</t-button>
|
|
||||||
<t-button block variant="outline" @click="openThemeSetting">
|
|
||||||
主题
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Copyright } from "timi-web";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "SettingsTab"
|
name: "SettingsTab"
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function openConnectSetting(): void {
|
|
||||||
void router.push("/settings/connect");
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDashboardSetting(): void {
|
|
||||||
void router.push("/settings/dashboard");
|
|
||||||
}
|
|
||||||
|
|
||||||
function openThemeSetting(): void {
|
|
||||||
void router.push("/settings/theme");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.page {
|
.settings-tab {
|
||||||
gap: 1rem;
|
padding-top: calc(var(--app-nav-offset) + 1rem);
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.card {
|
.title {
|
||||||
gap: 1rem;
|
text-align: center;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.head {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag,
|
|
||||||
.desc {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--app-sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entries {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .page {
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import ServerDetail from "@/pages/dashboard/ServerDashboard/ServerDetail.vue";
|
|||||||
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
||||||
import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue";
|
import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue";
|
||||||
import DockerContainerDetail from "@/pages/dashboard/DockerDashboard/DockerContainerDetail.vue";
|
import DockerContainerDetail from "@/pages/dashboard/DockerDashboard/DockerContainerDetail.vue";
|
||||||
|
import {
|
||||||
|
hasProgrammaticBackNavigation,
|
||||||
|
clearProgrammaticBackNavigation,
|
||||||
|
markProgrammaticBackNavigation
|
||||||
|
} from "@/utils/backNavigationSignal";
|
||||||
|
import { useTabBarStore } from "@/store/tabBarStore";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory("/"),
|
history: createWebHistory("/"),
|
||||||
@@ -131,7 +137,8 @@ const router = createRouter({
|
|||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "连接配置",
|
navBarTitle: "连接配置",
|
||||||
tabBarVisible: false
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F4F4F4"
|
||||||
},
|
},
|
||||||
component: ConnectSetting
|
component: ConnectSetting
|
||||||
},
|
},
|
||||||
@@ -143,7 +150,8 @@ const router = createRouter({
|
|||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "仪表板设置",
|
navBarTitle: "仪表板设置",
|
||||||
tabBarVisible: false
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F4F4F4"
|
||||||
},
|
},
|
||||||
component: DashboardSetting
|
component: DashboardSetting
|
||||||
},
|
},
|
||||||
@@ -155,7 +163,8 @@ const router = createRouter({
|
|||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "主题设置",
|
navBarTitle: "主题设置",
|
||||||
tabBarVisible: false
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F4F4F4"
|
||||||
},
|
},
|
||||||
component: ThemeSetting
|
component: ThemeSetting
|
||||||
}
|
}
|
||||||
@@ -177,8 +186,60 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to: RouteLocationNormalized) => {
|
const rawBack = router.back.bind(router);
|
||||||
|
const rawGo = router.go.bind(router);
|
||||||
|
const isIOS = /iP(ad|hone|od)/i.test(window.navigator.userAgent);
|
||||||
|
// 与 MainLayout 的 tabbar 过渡时长保持一致,额外预留一点缓冲时间
|
||||||
|
const TAB_BAR_TRANSITION_MS = 520;
|
||||||
|
const TAB_BAR_SKIP_EXTRA_MS = 180;
|
||||||
|
const depthCache = new Map<string, number>();
|
||||||
|
|
||||||
|
function calcDepth(sourceRoute: RouteLocationNormalized): number {
|
||||||
|
if (!sourceRoute.meta.dynamicDepth) {
|
||||||
|
return Number(sourceRoute.meta.depth ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = sourceRoute.fullPath;
|
||||||
|
const cachedDepth = depthCache.get(fullPath);
|
||||||
|
if (cachedDepth !== undefined) {
|
||||||
|
return cachedDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseDepth = Number(sourceRoute.meta.baseDepth ?? 0);
|
||||||
|
const pathSegments = fullPath.split("?")[0].split("/").filter((pathSegment) => pathSegment !== "");
|
||||||
|
const routeDepth = baseDepth + (pathSegments.length - 1);
|
||||||
|
depthCache.set(fullPath, routeDepth);
|
||||||
|
return routeDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局代理:所有代码触发的后退都自动标记为“主动返回”。
|
||||||
|
router.back = () => {
|
||||||
|
markProgrammaticBackNavigation();
|
||||||
|
rawBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 覆盖 go(-1 / -n) 场景,保证与 back() 行为一致。
|
||||||
|
router.go = (delta) => {
|
||||||
|
if (delta < 0) {
|
||||||
|
markProgrammaticBackNavigation();
|
||||||
|
}
|
||||||
|
rawGo(delta);
|
||||||
|
};
|
||||||
|
|
||||||
|
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized) => {
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
const toDepth = calcDepth(to);
|
||||||
|
const fromDepth = calcDepth(from);
|
||||||
|
const isBackwardNavigation = toDepth < fromDepth;
|
||||||
|
// 代码主动调用 back/go(-1) 的返回,仍保留 tabbar 进入动画
|
||||||
|
const isProgrammaticBack = hasProgrammaticBackNavigation();
|
||||||
|
const isTabBarEntering = !from.meta.tabBarVisible && !!to.meta.tabBarVisible;
|
||||||
|
// 仅 iOS 手势返回(非代码触发)且无 tabbar -> 有 tabbar 时,禁用本次 tabbar 进入动画
|
||||||
|
const shouldSkipTabBarEnterTransition = isIOS && isBackwardNavigation && !isProgrammaticBack && isTabBarEntering;
|
||||||
|
if (shouldSkipTabBarEnterTransition) {
|
||||||
|
tabBarStore.skipTransitionOnce();
|
||||||
|
}
|
||||||
|
|
||||||
if (to.meta.ignoreConnectCheck) {
|
if (to.meta.ignoreConnectCheck) {
|
||||||
return true;
|
return true;
|
||||||
@@ -197,6 +258,15 @@ router.beforeEach((to: RouteLocationNormalized) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to: RouteLocationNormalized) => {
|
router.afterEach((to: RouteLocationNormalized) => {
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
|
if (tabBarStore.shouldSkipTransition) {
|
||||||
|
// 本次导航稳定后再恢复过渡,避免手势返回期间出现二次进入动画
|
||||||
|
window.setTimeout(() => {
|
||||||
|
tabBarStore.restoreTransition();
|
||||||
|
}, TAB_BAR_TRANSITION_MS + TAB_BAR_SKIP_EXTRA_MS);
|
||||||
|
}
|
||||||
|
// 一次导航结束后清理信号,防止影响后续动画判定。
|
||||||
|
clearProgrammaticBackNavigation();
|
||||||
const globalUIStore = useGlobalUIStore();
|
const globalUIStore = useGlobalUIStore();
|
||||||
const targetBackground = typeof to.meta.bodyBackground === "string" ? to.meta.bodyBackground : DEFAULT_BODY_BACKGROUND;
|
const targetBackground = typeof to.meta.bodyBackground === "string" ? to.meta.bodyBackground : DEFAULT_BODY_BACKGROUND;
|
||||||
globalUIStore.setBodyBackground(targetBackground);
|
globalUIStore.setBodyBackground(targetBackground);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const tabs: RouteRecordRaw[] = [
|
|||||||
tabBarVisible: true,
|
tabBarVisible: true,
|
||||||
tabBarRoot: true,
|
tabBarRoot: true,
|
||||||
tabBarPadding: true,
|
tabBarPadding: true,
|
||||||
bodyBackground: "#FFF"
|
bodyBackground: "#F4F4F4"
|
||||||
},
|
},
|
||||||
component: SettingsTab
|
component: SettingsTab
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const useNavBarStore = defineStore("nav-bar", () => {
|
|||||||
const height = ref(0);
|
const height = ref(0);
|
||||||
const title = ref("");
|
const title = ref("");
|
||||||
const backTo = ref<string>();
|
const backTo = ref<string>();
|
||||||
|
const leftRenderer = shallowRef<Component>();
|
||||||
|
const leftOwner = ref<string>();
|
||||||
const rightRenderer = shallowRef<Component>();
|
const rightRenderer = shallowRef<Component>();
|
||||||
const rightOwner = ref<string>();
|
const rightOwner = ref<string>();
|
||||||
|
|
||||||
@@ -34,6 +36,20 @@ export const useNavBarStore = defineStore("nav-bar", () => {
|
|||||||
title.value = value || "";
|
title.value = value || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setLeftRenderer(renderer?: Component, owner?: string): void {
|
||||||
|
leftRenderer.value = renderer;
|
||||||
|
leftOwner.value = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLeft(owner?: string): void {
|
||||||
|
if (owner && leftOwner.value !== owner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftRenderer.value = undefined;
|
||||||
|
leftOwner.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function setRightRenderer(renderer?: Component, owner?: string): void {
|
function setRightRenderer(renderer?: Component, owner?: string): void {
|
||||||
rightRenderer.value = renderer;
|
rightRenderer.value = renderer;
|
||||||
rightOwner.value = owner;
|
rightOwner.value = owner;
|
||||||
@@ -52,12 +68,15 @@ export const useNavBarStore = defineStore("nav-bar", () => {
|
|||||||
height,
|
height,
|
||||||
title,
|
title,
|
||||||
backTo,
|
backTo,
|
||||||
|
leftRenderer,
|
||||||
rightRenderer,
|
rightRenderer,
|
||||||
isShowing,
|
isShowing,
|
||||||
canBack,
|
canBack,
|
||||||
setHeight,
|
setHeight,
|
||||||
setBackTo,
|
setBackTo,
|
||||||
setTitle,
|
setTitle,
|
||||||
|
setLeftRenderer,
|
||||||
|
clearLeft,
|
||||||
setRightRenderer,
|
setRightRenderer,
|
||||||
clearRight
|
clearRight
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,14 +12,9 @@ export interface ConnectSetting {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DashboardSnapshotMetric = "os" | "cpu" | "memory" | "jvm" | "network" | "hardware" | "storage";
|
|
||||||
export type DashboardHistoryMetric = "cpu" | "memory" | "jvm" | "network";
|
|
||||||
|
|
||||||
export interface ServerDashboardSetting {
|
export interface ServerDashboardSetting {
|
||||||
snapshotRefreshSeconds: number;
|
snapshotRefreshSeconds: number;
|
||||||
historyRefreshSeconds: number;
|
historyRefreshSeconds: number;
|
||||||
snapshotMetrics: DashboardSnapshotMetric[];
|
|
||||||
historyMetrics: DashboardHistoryMetric[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardSetting {
|
export interface DashboardSetting {
|
||||||
@@ -40,9 +35,7 @@ const defaultConnectSetting = (): ConnectSetting => ({
|
|||||||
|
|
||||||
const defaultServerDashboardSetting = (): ServerDashboardSetting => ({
|
const defaultServerDashboardSetting = (): ServerDashboardSetting => ({
|
||||||
snapshotRefreshSeconds: 3,
|
snapshotRefreshSeconds: 3,
|
||||||
historyRefreshSeconds: 10,
|
historyRefreshSeconds: 10
|
||||||
snapshotMetrics: ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"],
|
|
||||||
historyMetrics: ["cpu", "memory", "jvm", "network"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultDashboardSetting = (): DashboardSetting => ({
|
const defaultDashboardSetting = (): DashboardSetting => ({
|
||||||
@@ -65,20 +58,6 @@ function normalizeConnectSetting(connect?: Partial<ConnectSetting>): ConnectSett
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSnapshotMetrics(metrics?: DashboardSnapshotMetric[]): DashboardSnapshotMetric[] {
|
|
||||||
const validMetrics: DashboardSnapshotMetric[] = ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"];
|
|
||||||
const metricList = Array.isArray(metrics) ? metrics : [];
|
|
||||||
const normalizedMetrics = validMetrics.filter((metric) => metricList.includes(metric));
|
|
||||||
return normalizedMetrics.length ? normalizedMetrics : defaultServerDashboardSetting().snapshotMetrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHistoryMetrics(metrics?: DashboardHistoryMetric[]): DashboardHistoryMetric[] {
|
|
||||||
const validMetrics: DashboardHistoryMetric[] = ["cpu", "memory", "jvm", "network"];
|
|
||||||
const metricList = Array.isArray(metrics) ? metrics : [];
|
|
||||||
const normalizedMetrics = validMetrics.filter((metric) => metricList.includes(metric));
|
|
||||||
return normalizedMetrics.length ? normalizedMetrics : defaultServerDashboardSetting().historyMetrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRefreshSeconds(value?: number, fallback = 3): number {
|
function normalizeRefreshSeconds(value?: number, fallback = 3): number {
|
||||||
if (typeof value !== "number" || Number.isNaN(value)) {
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -91,9 +70,7 @@ function normalizeServerDashboardSetting(setting?: Partial<ServerDashboardSettin
|
|||||||
const fallback = defaultServerDashboardSetting();
|
const fallback = defaultServerDashboardSetting();
|
||||||
return {
|
return {
|
||||||
snapshotRefreshSeconds: normalizeRefreshSeconds(setting?.snapshotRefreshSeconds, fallback.snapshotRefreshSeconds),
|
snapshotRefreshSeconds: normalizeRefreshSeconds(setting?.snapshotRefreshSeconds, fallback.snapshotRefreshSeconds),
|
||||||
historyRefreshSeconds: normalizeRefreshSeconds(setting?.historyRefreshSeconds, fallback.historyRefreshSeconds),
|
historyRefreshSeconds: normalizeRefreshSeconds(setting?.historyRefreshSeconds, fallback.historyRefreshSeconds)
|
||||||
snapshotMetrics: normalizeSnapshotMetrics(setting?.snapshotMetrics),
|
|
||||||
historyMetrics: normalizeHistoryMetrics(setting?.historyMetrics)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,23 @@ import { defineStore } from "pinia";
|
|||||||
|
|
||||||
export const useTabBarStore = defineStore("tab-bar", () => {
|
export const useTabBarStore = defineStore("tab-bar", () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// 仅用于“当前这一次”禁用 tabbar 过渡,导航结束后会恢复
|
||||||
|
const shouldSkipTransition = ref(false);
|
||||||
|
|
||||||
const isShowing = computed(() => !!router.currentRoute.value.meta.tabBarVisible);
|
const isShowing = computed(() => !!router.currentRoute.value.meta.tabBarVisible);
|
||||||
|
|
||||||
|
function skipTransitionOnce(): void {
|
||||||
|
shouldSkipTransition.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreTransition(): void {
|
||||||
|
shouldSkipTransition.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isShowing
|
isShowing,
|
||||||
|
shouldSkipTransition,
|
||||||
|
skipTransitionOnce,
|
||||||
|
restoreTransition
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
28
src/utils/backNavigationSignal.ts
Normal file
28
src/utils/backNavigationSignal.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
let programmaticBackAt = 0;
|
||||||
|
|
||||||
|
// 主动返回信号有效期,避免历史信号污染后续导航
|
||||||
|
const PROGRAMMATIC_BACK_SIGNAL_TTL_MS = 1200;
|
||||||
|
|
||||||
|
// 由代码触发 router.back / router.go(-1) 时打标
|
||||||
|
export function markProgrammaticBackNavigation(): void {
|
||||||
|
programmaticBackAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取主动返回信号,超时后自动清理
|
||||||
|
export function hasProgrammaticBackNavigation(): boolean {
|
||||||
|
if (programmaticBackAt < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSignal = Date.now() - programmaticBackAt <= PROGRAMMATIC_BACK_SIGNAL_TTL_MS;
|
||||||
|
if (!hasSignal) {
|
||||||
|
programmaticBackAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在导航结束后统一清理信号
|
||||||
|
export function clearProgrammaticBackNavigation(): void {
|
||||||
|
programmaticBackAt = 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user