update ServerDashboard
This commit is contained in:
@@ -18,8 +18,8 @@
|
|||||||
"music-metadata-browser": "^2.5.11",
|
"music-metadata-browser": "^2.5.11",
|
||||||
"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.2",
|
"timi-tdesign-mobile": "0.0.9",
|
||||||
"timi-web": "0.0.3",
|
"timi-web": "0.0.15",
|
||||||
"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",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -27,11 +27,11 @@ importers:
|
|||||||
specifier: ^1.13.2
|
specifier: ^1.13.2
|
||||||
version: 1.13.2(vue@3.5.31(typescript@5.8.3))
|
version: 1.13.2(vue@3.5.31(typescript@5.8.3))
|
||||||
timi-tdesign-mobile:
|
timi-tdesign-mobile:
|
||||||
specifier: 0.0.2
|
specifier: 0.0.9
|
||||||
version: 0.0.2(typescript@5.8.3)
|
version: 0.0.9(typescript@5.8.3)
|
||||||
timi-web:
|
timi-web:
|
||||||
specifier: 0.0.3
|
specifier: 0.0.15
|
||||||
version: 0.0.3
|
version: 0.0.15
|
||||||
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)
|
||||||
@@ -1828,12 +1828,12 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
timi-tdesign-mobile@0.0.2:
|
timi-tdesign-mobile@0.0.9:
|
||||||
resolution: {integrity: sha512-R24yS4ovWTadCmpqJz3ajikMgk8YkKiUFbOIHuw3DBaJc2NwUcvVtY7aPu6kqnT/4j4AN3DgUFY+WkZbxqEhgQ==}
|
resolution: {integrity: sha512-SV8m/FsLsEabSmJ/NvI650fLyb/JCLz7JUNQrkWeoo5xICElsN4x39L3n4lTzu6ARidjpT6uA+XEmA4wnyQmuA==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
timi-web@0.0.3:
|
timi-web@0.0.15:
|
||||||
resolution: {integrity: sha512-uCJ+XQf1DydvnZHyI5NmQxKmZWvkVNPRrhqEszhMEh4CKGMPG7wrNiTl6jAPDcY3x228AaRP5MPu6TzIs9GfFQ==}
|
resolution: {integrity: sha512-j5CU8Byd9qdK7AWUGM5vgQ5Tix20m/WrD5Sv4h77aFV0G/rqdKi8uoRg1uUXUZLqsIy1zjI7kbIV3DnRplBYog==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
tinycolor2@1.6.0:
|
tinycolor2@1.6.0:
|
||||||
@@ -3868,14 +3868,14 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
timi-tdesign-mobile@0.0.2(typescript@5.8.3):
|
timi-tdesign-mobile@0.0.9(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.31(typescript@5.8.3)
|
vue: 3.5.31(typescript@5.8.3)
|
||||||
vue-router: 4.5.0(vue@3.5.31(typescript@5.8.3))
|
vue-router: 4.5.0(vue@3.5.31(typescript@5.8.3))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
timi-web@0.0.3:
|
timi-web@0.0.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.13.5
|
axios: 1.13.5
|
||||||
less: 4.5.1
|
less: 4.5.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
|
import type { ServerFile } from "@/types/File";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { ServerFile } from "@/pages/file/fileExplorer.shared";
|
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
code?: number | string;
|
code?: number | string;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
import type { SystemStatusHistoryView, SystemStatusSnapshotView } from "@/types/System";
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
code?: number | string;
|
code?: number | string;
|
||||||
@@ -8,87 +9,6 @@ interface ApiResponse<T> {
|
|||||||
message?: 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> {
|
export async function getSystemStatus(metrics?: string): Promise<SystemStatusSnapshotView> {
|
||||||
const response = await axios.get<ApiResponse<SystemStatusSnapshotView> | SystemStatusSnapshotView>(`${resolveBaseURL()}/system/server/status`, {
|
const response = await axios.get<ApiResponse<SystemStatusSnapshotView> | SystemStatusSnapshotView>(`${resolveBaseURL()}/system/server/status`, {
|
||||||
params: {
|
params: {
|
||||||
193
src/components/ProgressGroup.vue
Normal file
193
src/components/ProgressGroup.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<div class="progress-group">
|
||||||
|
<div
|
||||||
|
class="bar"
|
||||||
|
:class="{
|
||||||
|
heap: mode === 'heap',
|
||||||
|
splice: mode === 'splice'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-if="mode === 'heap'">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in heapProgressList"
|
||||||
|
:key="`heap-${index}`"
|
||||||
|
class="layer"
|
||||||
|
:style="{
|
||||||
|
width: `${item.value}%`,
|
||||||
|
background: item.color || 'var(--td-brand-color)'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in spliceProgressList"
|
||||||
|
:key="`splice-${index}`"
|
||||||
|
class="segment"
|
||||||
|
:style="{
|
||||||
|
width: `${item.value}%`,
|
||||||
|
background: item.color || 'var(--td-brand-color)'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div v-if="Toolkit.isNotEmpty(note)" class="note" v-text="note" />
|
||||||
|
</div>
|
||||||
|
<div v-if="Toolkit.isNotEmpty(legendList)" class="legends">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in legendList"
|
||||||
|
:key="`legend-${index}`"
|
||||||
|
class="legend"
|
||||||
|
>
|
||||||
|
<div class="block" :style="{ background: item.color }" />
|
||||||
|
<div v-text="item.legend" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Toolkit } from "timi-web";
|
||||||
|
|
||||||
|
export interface ProgressItem {
|
||||||
|
color?: string;
|
||||||
|
value: number;
|
||||||
|
legend?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressMode = "heap" | "splice";
|
||||||
|
|
||||||
|
interface RenderProgressItem {
|
||||||
|
color?: string;
|
||||||
|
value: number;
|
||||||
|
legend?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ProgressGroup"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
mode?: ProgressMode;
|
||||||
|
maxValue?: number;
|
||||||
|
progress?: ProgressItem[];
|
||||||
|
note?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
mode: "heap",
|
||||||
|
maxValue: 1,
|
||||||
|
progress: () => [],
|
||||||
|
note: ""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalizedProgressList = computed<RenderProgressItem[]>(() => {
|
||||||
|
const maxValue = 0 < props.maxValue ? props.maxValue : 100;
|
||||||
|
return props.progress.map((item) => {
|
||||||
|
const value = Math.min(Math.max(item.value / maxValue * 100, 0), 100);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const heapProgressList = computed<RenderProgressItem[]>(() => {
|
||||||
|
return normalizedProgressList.value.filter((item) => 0 < item.value)
|
||||||
|
.slice()
|
||||||
|
.sort((left, right) => right.value - left.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const spliceProgressList = computed<RenderProgressItem[]>(() => {
|
||||||
|
let rest = 100;
|
||||||
|
return normalizedProgressList.value.filter((item) => 0 < item.value).map((item) => {
|
||||||
|
if (rest < 1) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const width = Math.min(item.value, rest);
|
||||||
|
rest -= width;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: width
|
||||||
|
};
|
||||||
|
}).filter((item) => 0 < item.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const legendList = computed<RenderProgressItem[]>(() => {
|
||||||
|
return normalizedProgressList.value.filter(item => Toolkit.isNotEmpty(item.legend));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.progress-group {
|
||||||
|
gap: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 1.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: var(--td-progress-track-bg-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
|
||||||
|
border-radius: .1875rem;
|
||||||
|
|
||||||
|
&.heap {
|
||||||
|
.layer {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.splice {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.segment {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.layer,
|
||||||
|
.segment {
|
||||||
|
transition: width 320ms var(--tui-bezier);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
top: 50%;
|
||||||
|
right: .25rem;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
font-size: .8rem;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legends {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
gap: .375rem;
|
||||||
|
display: flex;
|
||||||
|
font-size: .75rem;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--td-text-color-secondary, var(--app-sub, #8899a8));
|
||||||
|
|
||||||
|
.block {
|
||||||
|
width: .8rem;
|
||||||
|
height: .8rem;
|
||||||
|
border-radius: .125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="placeholder">
|
|
||||||
<div class="box">
|
|
||||||
<p class="tag">当前组件</p>
|
|
||||||
<h1 class="title" v-text="title"></h1>
|
|
||||||
<p class="desc" v-text="description"></p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.placeholder {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 1.2rem;
|
|
||||||
|
|
||||||
.box {
|
|
||||||
gap: .9rem;
|
|
||||||
display: flex;
|
|
||||||
min-height: 12rem;
|
|
||||||
padding: 1.2rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--app-line);
|
|
||||||
background: linear-gradient(160deg, rgba(255, 255, 255, .92), rgba(255, 255, 255, .72));
|
|
||||||
box-shadow: 0 .4rem 1.2rem rgba(17, 32, 56, .08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--app-primary);
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--app-sub);
|
|
||||||
font-size: .95rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .placeholder {
|
|
||||||
.box {
|
|
||||||
background: linear-gradient(160deg, rgba(29, 37, 47, .96), rgba(22, 28, 36, .88));
|
|
||||||
box-shadow: 0 .4rem 1.2rem rgba(0, 0, 0, .28);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
90
src/components/TCellInfo.vue
Normal file
90
src/components/TCellInfo.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<t-cell class="t-cell-info" :class="{ 'has-description': hasDescription }">
|
||||||
|
<template #title>
|
||||||
|
<div class="content">
|
||||||
|
<slot name="label">
|
||||||
|
<div class="label" v-text="label"></div>
|
||||||
|
</slot>
|
||||||
|
<slot name="value">
|
||||||
|
<div class="value clip-text light-gray" v-text="value"></div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<slot name="description">
|
||||||
|
<slot>
|
||||||
|
<div v-if="description" class="description" v-text="description"></div>
|
||||||
|
</slot>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
description?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
const { label, value, description } = toRefs(props);
|
||||||
|
const hasDescription = computed(() => {
|
||||||
|
return Boolean(props.description?.trim() || slots.description || slots.default);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default?: () => unknown;
|
||||||
|
label?: () => unknown;
|
||||||
|
value?: () => unknown;
|
||||||
|
description?: () => unknown;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.t-cell-info {
|
||||||
|
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__description) {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-description {
|
||||||
|
:deep(.t-cell__description) {
|
||||||
|
margin-top: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: .8rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -228,6 +228,12 @@ const contentHeight = computed(() => {
|
|||||||
transition: opacity 520ms, transform 520ms var(--tui-bezier);
|
transition: opacity 520ms, transform 520ms var(--tui-bezier);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&.glass-white {
|
||||||
|
// 不明原因失效,重新配置
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
&.is-hidden {
|
&.is-hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="root-layout">
|
<div class="root-layout diselect">
|
||||||
<page-transition />
|
<page-transition />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="docker-dashboard">
|
<div class="docker-dashboard">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="tag">Docker</p>
|
<p class="tag">Docker</p>
|
||||||
<h2 class="title">容器状态</h2>
|
<h2 class="header">容器状态</h2>
|
||||||
<p class="desc">这里用于承载容器列表、运行状态、镜像占用和资源使用情况。</p>
|
<p class="desc">这里用于承载容器列表、运行状态、镜像占用和资源使用情况。</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.tag,
|
||||||
.title,
|
.header,
|
||||||
.desc {
|
.desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.header {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,4 +54,4 @@
|
|||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
91
src/pages/dashboard/ServerDashboard/ServerDetail.vue
Normal file
91
src/pages/dashboard/ServerDashboard/ServerDetail.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="server-detail">
|
||||||
|
<div v-if="isLoading && !snapshotView" class="loading-wrap">
|
||||||
|
<t-loading text="加载系统详情..." />
|
||||||
|
</div>
|
||||||
|
<template v-else-if="snapshotView">
|
||||||
|
<t-cell-group title="系统" theme="card">
|
||||||
|
<t-cell title="服务器时间" :note="Time.toDateTime(snapshotView.serverTime)" />
|
||||||
|
<t-cell title="采样周期" :note="`${snapshotView.sampleRateMs} ms`" />
|
||||||
|
<t-cell title="操作系统" :note="os?.name" />
|
||||||
|
<t-cell title="启动时间" :note="Time.toPassedDateTime(os?.bootAt)" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group title="硬件信息" theme="card">
|
||||||
|
<t-cell title="主板厂商" :note="hardware?.baseboard?.manufacturer" />
|
||||||
|
<t-cell title="主板型号" :note="hardware?.baseboard?.model" />
|
||||||
|
<t-cell title="主板版本" :note="hardware?.baseboard?.version" />
|
||||||
|
<t-cell title="主板序列号" :note="hardware?.baseboard?.serialNumber" />
|
||||||
|
<t-cell title="固件厂商" :note="hardware?.firmware?.manufacturer" />
|
||||||
|
<t-cell title="固件名称" :note="hardware?.firmware?.name" />
|
||||||
|
<t-cell title="固件描述" :note="hardware?.firmware?.description" />
|
||||||
|
<t-cell title="固件版本" :note="hardware?.firmware?.version" />
|
||||||
|
<t-cell title="固件发布日期" :note="hardware?.firmware?.releaseDate" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else description="暂无系统详情数据" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
import { getSystemStatus, resolveSystemRequestErrorMessage } from "@/api/SystemAPI";
|
||||||
|
import type { SystemStatusSnapshotView } from "@/types/System";
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ServerDetail"
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const snapshotView = ref<SystemStatusSnapshotView | null>(null);
|
||||||
|
const refreshIntervalMs = 3000;
|
||||||
|
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const os = computed(() => snapshotView.value?.snapshot?.os);
|
||||||
|
const hardware = computed(() => snapshotView.value?.snapshot?.hardware);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshDetail();
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
void refreshDetail();
|
||||||
|
}, refreshIntervalMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (!refreshTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshDetail(): Promise<void> {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
snapshotView.value = await getSystemStatus("os,hardware");
|
||||||
|
} catch (error) {
|
||||||
|
Toast({
|
||||||
|
theme: "error",
|
||||||
|
message: resolveSystemRequestErrorMessage(error)
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.server-detail {
|
||||||
|
padding: var(--app-nav-offset) 0 1rem 0;
|
||||||
|
|
||||||
|
.loading-wrap {
|
||||||
|
display: flex;
|
||||||
|
min-height: 4rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
122
src/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue
Normal file
122
src/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div class="server-performance-detail">
|
||||||
|
<div v-if="isLoading && !snapshotView" class="loading-wrap">
|
||||||
|
<t-loading text="加载资源详情..." />
|
||||||
|
</div>
|
||||||
|
<template v-else-if="snapshotView">
|
||||||
|
<t-cell-group v-if="cpu" title="CPU" theme="card">
|
||||||
|
<t-cell-info label="型号" :value="cpu.model" />
|
||||||
|
<t-cell title="物理核心" :note="String(cpu.physicalCores)" />
|
||||||
|
<t-cell title="逻辑核心" :note="String(cpu.logicalCores)" />
|
||||||
|
<t-cell title="使用率" :note="Text.unit(cpu.usageTotal * 100, '%')" />
|
||||||
|
<t-cell title="系统使用率" :note="Text.unit(cpu.usageSystem * 100, '%')" />
|
||||||
|
<t-cell title="温度" :note="Text.unit(cpu.temperatureCelsius || 0, '℃')"></t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group v-if="memory" title="内存" theme="card">
|
||||||
|
<t-cell title="物理内存" :note="IOSize.format(memory.totalBytes)" />
|
||||||
|
<t-cell title="已使用" :note="IOSize.format(memory.usedBytes)" />
|
||||||
|
<t-cell title="使用率" :note="Text.unit(memory.usedBytes / memory.totalBytes * 100, '%')" />
|
||||||
|
<t-cell title="交换区内存" :note="IOSize.format(memory.swapTotalBytes)" />
|
||||||
|
<t-cell title="交换区已使用" :note="IOSize.format(memory.swapUsedBytes)" />
|
||||||
|
<t-cell title="交换区使用率" :note="Text.unit(memory.swapUsedBytes / memory.swapTotalBytes * 100, '%')" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group v-if="jvm" title="JVM" theme="card">
|
||||||
|
<t-cell title="名称" :note="jvm.name" />
|
||||||
|
<t-cell title="版本" :note="jvm.version" />
|
||||||
|
<t-cell title="启动于" :note="Time.toPassedDateTime(jvm.bootAt)" />
|
||||||
|
<t-cell title="堆内存最大大小" :note="IOSize.format(jvm.heapMaxBytes)" />
|
||||||
|
<t-cell title="堆内存初始大小" :note="IOSize.format(jvm.heapInitBytes)" />
|
||||||
|
<t-cell title="堆内存已提交" :note="IOSize.format(jvm.heapCommittedBytes)" />
|
||||||
|
<t-cell title="堆内存申请率" :note="Text.unit(jvm.heapCommittedBytes / jvm.heapMaxBytes * 100, '%')" />
|
||||||
|
<t-cell title="堆内存已使用" :note="IOSize.format(jvm.heapUsedBytes)" />
|
||||||
|
<t-cell title="堆内存使用率" :note="Text.unit(jvm.heapUsedBytes / jvm.heapCommittedBytes * 100, '%')" />
|
||||||
|
<t-cell-info label="GC 收集器" :value="jvm.gc.collector" />
|
||||||
|
<t-cell title="GC 周期次数" :note="String(jvm.gc.cycleCount)" />
|
||||||
|
<t-cell title="GC 暂停次数" :note="String(jvm.gc.pauseCount)" />
|
||||||
|
<t-cell title="最近暂停时间" :note="Time.toPassedDateTime(jvm.gc.lastPauseAt)" />
|
||||||
|
<t-cell title="最近回收内存" :note="IOSize.format(jvm.gc.lastRecoveredBytes)" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group v-if="network" title="网络" theme="card">
|
||||||
|
<t-cell-info label="网卡" :value="network.interfaceName" />
|
||||||
|
<t-cell title="MAC" :note="network.mac" />
|
||||||
|
<t-cell title="接收速率" :note="IOSize.speed(network.rxBytesPerSecond)" />
|
||||||
|
<t-cell title="发送速率" :note="IOSize.speed(network.txBytesPerSecond)" />
|
||||||
|
<t-cell title="累计接收" :note="IOSize.format(network.rxTotalBytes)" />
|
||||||
|
<t-cell title="累计发送" :note="IOSize.format(network.txTotalBytes)" />
|
||||||
|
<t-cell title="累计接收包" :note="network.rxPacketsTotal.toLocaleString()" />
|
||||||
|
<t-cell title="累计发送包" :note="network.txPacketsTotal.toLocaleString()" />
|
||||||
|
<t-cell title="输入错误包" :note="String(network.inErrors)" />
|
||||||
|
<t-cell title="输出错误包" :note="String(network.outErrors)" />
|
||||||
|
<t-cell title="输入丢包" :note="String(network.inDrops)" />
|
||||||
|
<t-cell title="累计冲突" :note="String(network.collisions)" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else description="暂无资源详情数据" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
import { getSystemStatus, resolveSystemRequestErrorMessage } from "@/api/SystemAPI";
|
||||||
|
import type { SystemStatusSnapshotView } from "@/types/System";
|
||||||
|
import { IOSize, Text, Time } from "timi-web";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ServerPerformanceDetail"
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const snapshotView = ref<SystemStatusSnapshotView | null>(null);
|
||||||
|
const refreshIntervalMs = 3000;
|
||||||
|
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const cpu = computed(() => snapshotView.value?.snapshot?.cpu);
|
||||||
|
const memory = computed(() => snapshotView.value?.snapshot?.memory);
|
||||||
|
const jvm = computed(() => snapshotView.value?.snapshot?.jvm);
|
||||||
|
const network = computed(() => snapshotView.value?.snapshot?.network);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshDetail();
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
void refreshDetail();
|
||||||
|
}, refreshIntervalMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (!refreshTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshDetail(): Promise<void> {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
snapshotView.value = await getSystemStatus("cpu,memory,jvm,network");
|
||||||
|
} catch (error) {
|
||||||
|
Toast({
|
||||||
|
theme: "error",
|
||||||
|
message: resolveSystemRequestErrorMessage(error)
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.server-performance-detail {
|
||||||
|
padding: var(--app-nav-offset) 0 1rem 0;
|
||||||
|
|
||||||
|
.loading-wrap {
|
||||||
|
display: flex;
|
||||||
|
min-height: 4rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="ups-dashboard">
|
<div class="ups-dashboard">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="tag">UPS</p>
|
<p class="tag">UPS</p>
|
||||||
<h2 class="title">供电监控</h2>
|
<h2 class="header">供电监控</h2>
|
||||||
<p class="desc">这里用于展示 UPS 电池状态、负载功率、剩余续航和告警信息。</p>
|
<p class="desc">这里用于展示 UPS 电池状态、负载功率、剩余续航和告警信息。</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.tag,
|
||||||
.title,
|
.header,
|
||||||
.desc {
|
.desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.header {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,4 +54,4 @@
|
|||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<recycle-scroller
|
<recycle-scroller
|
||||||
class="file-explorer-list"
|
class="file-explorer-list"
|
||||||
:items="items"
|
:items="items"
|
||||||
:item-size="56"
|
:item-size="44"
|
||||||
key-field="path"
|
key-field="path"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #before>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { listServerFiles, resolveRequestErrorMessage } from "@/api/file";
|
import { listServerFiles, resolveRequestErrorMessage } from "@/api/FileAPI";
|
||||||
import FileExplorerGrid from "./FileExplorerGrid.vue";
|
import FileExplorerGrid from "./FileExplorerGrid.vue";
|
||||||
import FileExplorerList from "./FileExplorerList.vue";
|
import FileExplorerList from "./FileExplorerList.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
@@ -399,4 +399,4 @@ function getDirectoryKey(pathSegments: string[]): string {
|
|||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
|
import type { ServerFile } from "@/types/File";
|
||||||
|
|
||||||
export type DisplayMode = "list" | "grid";
|
export type DisplayMode = "list" | "grid";
|
||||||
export type FileItemType = "dir" | "file";
|
export type FileItemType = "dir" | "file";
|
||||||
|
|
||||||
export interface ServerFile {
|
|
||||||
name: string;
|
|
||||||
extension?: string;
|
|
||||||
absolutePath?: string;
|
|
||||||
size?: number;
|
|
||||||
modifiedAt?: number;
|
|
||||||
type?: string;
|
|
||||||
isFile?: boolean;
|
|
||||||
isDirectory?: boolean;
|
|
||||||
canPreview?: boolean;
|
|
||||||
previewURI?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExplorerItem {
|
export interface ExplorerItem {
|
||||||
name: string;
|
name: string;
|
||||||
type: FileItemType;
|
type: FileItemType;
|
||||||
|
|||||||
180
src/pages/setting/ConnectSetting.vue
Normal file
180
src/pages/setting/ConnectSetting.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<section class="card">
|
||||||
|
<div class="head">
|
||||||
|
<p class="tag">连接配置</p>
|
||||||
|
<h2 class="header">服务器连接</h2>
|
||||||
|
<p class="desc">这里的配置会持久化保存,缺失时应用会强制回到连接引导页。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<p class="label">协议</p>
|
||||||
|
<div class="protocols">
|
||||||
|
<t-button
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
:variant="form.protocol === 'http' ? 'base' : 'outline'"
|
||||||
|
@click="setProtocol('http')"
|
||||||
|
>
|
||||||
|
HTTP
|
||||||
|
</t-button>
|
||||||
|
<t-button
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
:variant="form.protocol === 'https' ? 'base' : 'outline'"
|
||||||
|
@click="setProtocol('https')"
|
||||||
|
>
|
||||||
|
HTTPS
|
||||||
|
</t-button>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
import type { ConnectProtocol, ConnectSetting } from "@/store/settingStore";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ConnectSetting"
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
|
const form = reactive<ConnectSetting>({
|
||||||
|
protocol: "http",
|
||||||
|
host: "",
|
||||||
|
port: "",
|
||||||
|
token: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => settingStore.connect,
|
||||||
|
(connect) => {
|
||||||
|
Object.assign(form, connect);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
function setProtocol(protocol: ConnectProtocol): void {
|
||||||
|
form.protocol = protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConnect(): boolean {
|
||||||
|
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 saveConnect(): void {
|
||||||
|
if (!validateConnect()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingStore.setConnect(form);
|
||||||
|
Toast({
|
||||||
|
theme: "success",
|
||||||
|
message: "连接配置已保存"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConnect(): void {
|
||||||
|
settingStore.resetConnect();
|
||||||
|
Object.assign(form, settingStore.connect);
|
||||||
|
Toast({
|
||||||
|
theme: "success",
|
||||||
|
message: "连接配置已清空"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.page {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocols {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.theme-dark) .page {
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
265
src/pages/setting/DashboardSetting.vue
Normal file
265
src/pages/setting/DashboardSetting.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<section class="card">
|
||||||
|
<div class="head">
|
||||||
|
<p class="tag">服务器</p>
|
||||||
|
<h2 class="header">数据刷新与采集</h2>
|
||||||
|
<p class="desc">用于配置服务器仪表板的请求频率和 metrics 参数。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
import {
|
||||||
|
useSettingStore,
|
||||||
|
type DashboardHistoryMetric,
|
||||||
|
type DashboardSnapshotMetric,
|
||||||
|
type ServerDashboardSetting
|
||||||
|
} from "@/store/settingStore";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "DashboardSetting"
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
|
const snapshotMetricOptions: Array<{ label: string; value: DashboardSnapshotMetric }> = [
|
||||||
|
{ label: "系统", value: "os" },
|
||||||
|
{ label: "CPU", value: "cpu" },
|
||||||
|
{ 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({
|
||||||
|
snapshotRefreshSeconds: 3,
|
||||||
|
historyRefreshSeconds: 10,
|
||||||
|
snapshotMetrics: ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"],
|
||||||
|
historyMetrics: ["cpu", "memory", "jvm", "network"]
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast({
|
||||||
|
theme: "success",
|
||||||
|
message: "已恢复默认配置"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSecondValue(value: string, fallback: number): number {
|
||||||
|
const numberValue = Number(value);
|
||||||
|
if (Number.isNaN(numberValue)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(Math.max(Math.floor(numberValue), 1), 120);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.page {
|
||||||
|
gap: 1rem;
|
||||||
|
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>
|
||||||
93
src/pages/setting/ThemeSetting.vue
Normal file
93
src/pages/setting/ThemeSetting.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<section class="card">
|
||||||
|
<div class="head">
|
||||||
|
<p class="tag">主题</p>
|
||||||
|
<h2 class="header">界面模式</h2>
|
||||||
|
<p class="desc">当前提供浅色、深色和跟随系统三种模式。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modes">
|
||||||
|
<t-button
|
||||||
|
v-for="item in themeModeList"
|
||||||
|
:key="item.value"
|
||||||
|
theme="primary"
|
||||||
|
:variant="globalUIStore.themeMode === item.value ? 'base' : 'outline'"
|
||||||
|
@click="globalUIStore.setThemeMode(item.value)"
|
||||||
|
>
|
||||||
|
<span v-text="item.label" />
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGlobalUIStore, type ThemeMode } from "@/store/globalUIStore";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ThemeSetting"
|
||||||
|
});
|
||||||
|
|
||||||
|
const globalUIStore = useGlobalUIStore();
|
||||||
|
|
||||||
|
const themeModeList: Array<{ label: string; value: ThemeMode }> = [
|
||||||
|
{ label: "浅色", value: "light" },
|
||||||
|
{ label: "深色", value: "dark" },
|
||||||
|
{ label: "跟随系统", value: "system" }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.page {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
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>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<t-icon v-else name="music" />
|
<t-icon v-else name="music" />
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<p class="title" v-text="currentSong.title || currentFile?.name || '未选择歌曲'"></p>
|
<p class="header" v-text="currentSong.title || currentFile?.name || '未选择歌曲'"></p>
|
||||||
<p class="artist" v-text="currentSong.artist || '未知艺术家'"></p>
|
<p class="artist" v-text="currentSong.artist || '未知艺术家'"></p>
|
||||||
</div>
|
</div>
|
||||||
<t-button class="close-btn" variant="text" shape="square" @click="closePopup">
|
<t-button class="close-btn" variant="text" shape="square" @click="closePopup">
|
||||||
@@ -164,7 +164,7 @@ function playItem(index: number): void {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title,
|
.header,
|
||||||
.artist,
|
.artist,
|
||||||
.queue-title,
|
.queue-title,
|
||||||
.queue-count,
|
.queue-count,
|
||||||
@@ -174,7 +174,7 @@ function playItem(index: number): void {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title,
|
.header,
|
||||||
.queue-name {
|
.queue-name {
|
||||||
color: var(--app-text);
|
color: var(--app-text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -244,7 +244,7 @@ function playItem(index: number): void {
|
|||||||
|
|
||||||
.queue-name,
|
.queue-name,
|
||||||
.queue-artist,
|
.queue-artist,
|
||||||
.title,
|
.header,
|
||||||
.artist {
|
.artist {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<route-placeholder title="NotFoundPage" description="未匹配到路由时显示的占位页,避免空白页面。" />
|
<span>找不到页面</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<p class="tag">NAS 连接</p>
|
<p class="tag">NAS 连接</p>
|
||||||
<h1 class="title">先配置服务器连接</h1>
|
<h1 class="header">先配置服务器连接</h1>
|
||||||
<p class="desc">缺少连接配置时,应用不会进入文件、状态和设置页面。</p>
|
<p class="desc">缺少连接配置时,应用不会进入文件、状态和设置页面。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ async function submitConnect(): Promise<void> {
|
|||||||
letter-spacing: .08em;
|
letter-spacing: .08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-tab">
|
<div class="dashboard-tab">
|
||||||
<div class="tabs-wrap">
|
<div ref="tabsWrapRef" class="tabs-wrap">
|
||||||
<t-tabs class="tabs" :value="activeTab" @change="onChangeTab">
|
<t-tabs class="tabs" :value="activeTab" @change="onChangeTab">
|
||||||
<t-tab-panel value="server" label="服务器" />
|
<t-tab-panel value="server" label="服务器" />
|
||||||
<t-tab-panel value="docker" label="Docker" />
|
<t-tab-panel value="docker" label="Docker" />
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</t-tabs>
|
</t-tabs>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-wrap">
|
<div class="content-wrap">
|
||||||
<transition :name="transitionName" mode="out-in">
|
<transition :name="panelTransitionName" mode="out-in">
|
||||||
<component :is="activeComponent" :key="activeTab" class="dashboard-panel" />
|
<component :is="activeComponent" :key="activeTab" class="dashboard-panel" />
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,17 +22,47 @@ import DockerDashboard from "@/pages/dashboard/DockerDashboard/DockerDashboard.v
|
|||||||
import UPSDashboard from "@/pages/dashboard/UPSDashboard/UPSDashboard.vue";
|
import UPSDashboard from "@/pages/dashboard/UPSDashboard/UPSDashboard.vue";
|
||||||
|
|
||||||
type DashboardTabValue = "server" | "docker" | "ups";
|
type DashboardTabValue = "server" | "docker" | "ups";
|
||||||
|
type PanelTransitionName = "panel-slide-next" | "panel-slide-prev";
|
||||||
|
|
||||||
const tabOrder: DashboardTabValue[] = ["server", "docker", "ups"];
|
|
||||||
const dashboardMap: Record<DashboardTabValue, Component> = {
|
const dashboardMap: Record<DashboardTabValue, Component> = {
|
||||||
server: ServerDashboard,
|
server: ServerDashboard,
|
||||||
docker: DockerDashboard,
|
docker: DockerDashboard,
|
||||||
ups: UPSDashboard
|
ups: UPSDashboard
|
||||||
};
|
};
|
||||||
|
const tabOrder: DashboardTabValue[] = ["server", "docker", "ups"];
|
||||||
|
|
||||||
const activeTab = ref<DashboardTabValue>("server");
|
const activeTab = ref<DashboardTabValue>("server");
|
||||||
const transitionName = ref<"slide-left" | "slide-right">("slide-left");
|
const panelTransitionName = ref<PanelTransitionName>("panel-slide-next");
|
||||||
const activeComponent = computed(() => dashboardMap[activeTab.value]);
|
const activeComponent = computed(() => dashboardMap[activeTab.value]);
|
||||||
|
const tabsWrapRef = ref<HTMLElement>();
|
||||||
|
const tabsHeight = ref("0px");
|
||||||
|
let tabsResizeObs: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
function updateTabsHeight(): void {
|
||||||
|
if (!tabsWrapRef.value) {
|
||||||
|
tabsHeight.value = "0px";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tabsHeight.value = `${tabsWrapRef.value.offsetHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateTabsHeight();
|
||||||
|
if (!tabsWrapRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabsResizeObs = new ResizeObserver(() => {
|
||||||
|
updateTabsHeight();
|
||||||
|
});
|
||||||
|
tabsResizeObs.observe(tabsWrapRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (tabsResizeObs) {
|
||||||
|
tabsResizeObs.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function onChangeTab(value: string): void {
|
function onChangeTab(value: string): void {
|
||||||
const nextTab = value as DashboardTabValue;
|
const nextTab = value as DashboardTabValue;
|
||||||
@@ -40,28 +70,35 @@ function onChangeTab(value: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextIndex = tabOrder.indexOf(nextTab);
|
|
||||||
const currentIndex = tabOrder.indexOf(activeTab.value);
|
const currentIndex = tabOrder.indexOf(activeTab.value);
|
||||||
transitionName.value = currentIndex < nextIndex ? "slide-left" : "slide-right";
|
const nextIndex = tabOrder.indexOf(nextTab);
|
||||||
|
if (currentIndex <= -1 || nextIndex <= -1) {
|
||||||
|
panelTransitionName.value = "panel-slide-next";
|
||||||
|
} else {
|
||||||
|
panelTransitionName.value = currentIndex < nextIndex ? "panel-slide-next" : "panel-slide-prev";
|
||||||
|
}
|
||||||
activeTab.value = nextTab;
|
activeTab.value = nextTab;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.dashboard-tab {
|
.dashboard-tab {
|
||||||
height: 100%;
|
--dashboard-tabs-height: v-bind(tabsHeight);
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.tabs-wrap {
|
.tabs-wrap {
|
||||||
top: 0;
|
top: var(--app-nav-offset, 0px);
|
||||||
z-index: 9;
|
left: 0;
|
||||||
position: sticky;
|
right: 0;
|
||||||
|
z-index: 1090;
|
||||||
|
position: fixed;
|
||||||
background: var(--app-bg, #fff);
|
background: var(--app-bg, #fff);
|
||||||
border-bottom: 1px solid var(--app-line);
|
border-bottom: 1px solid var(--app-line);
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
|
--td-tab-track-width: 4rem;
|
||||||
|
|
||||||
:deep(.t-tabs__nav) {
|
:deep(.t-tabs__nav) {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
@@ -76,36 +113,26 @@ function onChangeTab(value: string): void {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding-top: calc(var(--dashboard-tabs-height) + var(--app-nav-offset));
|
||||||
.dashboard-panel {
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-left-enter-active,
|
.panel-slide-next-enter-active,
|
||||||
.slide-left-leave-active,
|
.panel-slide-next-leave-active,
|
||||||
.slide-right-enter-active,
|
.panel-slide-prev-enter-active,
|
||||||
.slide-right-leave-active {
|
.panel-slide-prev-leave-active {
|
||||||
transition: transform .28s ease, opacity .28s ease;
|
transition: opacity 260ms var(--tui-bezier), transform 260ms var(--tui-bezier);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-left-enter-from,
|
.panel-slide-next-enter-from,
|
||||||
.slide-right-leave-to {
|
.panel-slide-prev-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20%);
|
transform: translateX(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-left-leave-to,
|
.panel-slide-next-leave-to,
|
||||||
.slide-right-enter-from {
|
.panel-slide-prev-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-20%);
|
transform: translateX(-50%);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .dashboard-tab {
|
|
||||||
.tabs-wrap {
|
|
||||||
background: var(--app-bg, #11161d);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,72 +2,19 @@
|
|||||||
<div class="page">
|
<div class="page">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<p class="tag">连接配置</p>
|
<p class="tag">系统设置</p>
|
||||||
<h2 class="title">服务器连接</h2>
|
<h2 class="header">配置入口</h2>
|
||||||
<p class="desc">这里的配置会持久化保存,缺失时应用会强制回到连接引导页。</p>
|
<p class="desc">设置页只保留入口,具体配置在独立页面中维护。</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="entries">
|
||||||
<div class="group">
|
<t-button block variant="outline" @click="openConnectSetting">
|
||||||
<p class="label">协议</p>
|
连接配置
|
||||||
<div class="protocols">
|
</t-button>
|
||||||
<t-button
|
<t-button block variant="outline" @click="openDashboardSetting">
|
||||||
size="small"
|
仪表板
|
||||||
theme="primary"
|
</t-button>
|
||||||
:variant="form.protocol === 'http' ? 'base' : 'outline'"
|
<t-button block variant="outline" @click="openThemeSetting">
|
||||||
@click="setProtocol('http')"
|
主题
|
||||||
>
|
|
||||||
HTTP
|
|
||||||
</t-button>
|
|
||||||
<t-button
|
|
||||||
size="small"
|
|
||||||
theme="primary"
|
|
||||||
:variant="form.protocol === 'https' ? 'base' : 'outline'"
|
|
||||||
@click="setProtocol('https')"
|
|
||||||
>
|
|
||||||
HTTPS
|
|
||||||
</t-button>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<div class="head">
|
|
||||||
<p class="tag">主题</p>
|
|
||||||
<h2 class="title">界面模式</h2>
|
|
||||||
<p class="desc">当前仅提供浅色、深色和跟随系统三种模式。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modes">
|
|
||||||
<t-button
|
|
||||||
v-for="item in themeModeList"
|
|
||||||
:key="item.value"
|
|
||||||
theme="primary"
|
|
||||||
:variant="globalUIStore.themeMode === item.value ? 'base' : 'outline'"
|
|
||||||
@click="globalUIStore.setThemeMode(item.value)"
|
|
||||||
>
|
|
||||||
<span v-text="item.label"></span>
|
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -75,74 +22,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
defineOptions({
|
||||||
import { useGlobalUIStore, type ThemeMode } from "@/store/globalUIStore";
|
name: "SettingsTab"
|
||||||
import type { ConnectProtocol, ConnectSetting } from "@/store/settingStore";
|
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
|
||||||
|
|
||||||
const globalUIStore = useGlobalUIStore();
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
const themeModeList: Array<{ label: string; value: ThemeMode }> = [
|
|
||||||
{ label: "浅色", value: "light" },
|
|
||||||
{ label: "深色", value: "dark" },
|
|
||||||
{ label: "跟随系统", value: "system" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const form = reactive<ConnectSetting>({
|
|
||||||
protocol: "http",
|
|
||||||
host: "",
|
|
||||||
port: "",
|
|
||||||
token: ""
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
const router = useRouter();
|
||||||
() => settingStore.connect,
|
|
||||||
(connect) => {
|
|
||||||
Object.assign(form, connect);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function setProtocol(protocol: ConnectProtocol): void {
|
function openConnectSetting(): void {
|
||||||
form.protocol = protocol;
|
void router.push("/settings/connect");
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateConnect(): boolean {
|
function openDashboardSetting(): void {
|
||||||
const host = form.host.trim();
|
void router.push("/settings/dashboard");
|
||||||
const port = form.port.trim();
|
|
||||||
const token = form.token.trim();
|
|
||||||
|
|
||||||
if (!host || !port || !token) {
|
|
||||||
Toast({
|
|
||||||
theme: "warning",
|
|
||||||
message: "请完整填写连接配置"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConnect(): void {
|
function openThemeSetting(): void {
|
||||||
if (!validateConnect()) {
|
void router.push("/settings/theme");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
settingStore.setConnect(form);
|
|
||||||
Toast({
|
|
||||||
theme: "success",
|
|
||||||
message: "连接配置已保存"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetConnect(): void {
|
|
||||||
settingStore.resetConnect();
|
|
||||||
Object.assign(form, settingStore.connect);
|
|
||||||
Toast({
|
|
||||||
theme: "success",
|
|
||||||
message: "连接配置已清空"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -164,26 +59,23 @@ function resetConnect(): void {
|
|||||||
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.head,
|
.head {
|
||||||
.group {
|
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.tag,
|
||||||
.label,
|
|
||||||
.desc {
|
.desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--app-sub);
|
color: var(--app-sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.tag {
|
||||||
.label {
|
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
@@ -192,11 +84,10 @@ function resetConnect(): void {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.protocols,
|
.entries {
|
||||||
.modes {
|
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { useSettingStore } from "@/store/settingStore";
|
|||||||
import NotFoundPage from "@/pages/system/NotFoundPage.vue";
|
import NotFoundPage from "@/pages/system/NotFoundPage.vue";
|
||||||
import ServerIndexPage from "@/pages/system/ServerIndexPage.vue";
|
import ServerIndexPage from "@/pages/system/ServerIndexPage.vue";
|
||||||
import FileTab from "@/pages/tabs/FileTab.vue";
|
import FileTab from "@/pages/tabs/FileTab.vue";
|
||||||
import ServerLogPage from "@/pages/detail/ServerLogPage.vue";
|
import ConnectSetting from "@/pages/setting/ConnectSetting.vue";
|
||||||
|
import DashboardSetting from "@/pages/setting/DashboardSetting.vue";
|
||||||
|
import ThemeSetting from "@/pages/setting/ThemeSetting.vue";
|
||||||
|
import ServerDetail from "@/pages/dashboard/ServerDashboard/ServerDetail.vue";
|
||||||
|
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory("/"),
|
history: createWebHistory("/"),
|
||||||
@@ -66,16 +70,66 @@ const router = createRouter({
|
|||||||
component: FileTab
|
component: FileTab
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/server/logs",
|
path: "/server/system-detail",
|
||||||
name: "ServerLogPage",
|
name: "ServerDetail",
|
||||||
meta: {
|
meta: {
|
||||||
depth: 3,
|
depth: 3,
|
||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "服务日志",
|
navBarTitle: "系统详情",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F4F4F4"
|
||||||
|
},
|
||||||
|
component: ServerDetail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/server/performance-detail",
|
||||||
|
name: "ServerPerformanceDetail",
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "资源详情",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F4F4F4"
|
||||||
|
},
|
||||||
|
component: ServerPerformanceDetail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/settings/connect",
|
||||||
|
name: "ConnectSetting",
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "连接配置",
|
||||||
tabBarVisible: false
|
tabBarVisible: false
|
||||||
},
|
},
|
||||||
component: ServerLogPage
|
component: ConnectSetting
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/settings/dashboard",
|
||||||
|
name: "DashboardSetting",
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "仪表板设置",
|
||||||
|
tabBarVisible: false
|
||||||
|
},
|
||||||
|
component: DashboardSetting
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/settings/theme",
|
||||||
|
name: "ThemeSetting",
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "主题设置",
|
||||||
|
tabBarVisible: false
|
||||||
|
},
|
||||||
|
component: ThemeSetting
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -120,4 +174,4 @@ router.afterEach((to: RouteLocationNormalized) => {
|
|||||||
globalUIStore.setBodyBackground(targetBackground);
|
globalUIStore.setBodyBackground(targetBackground);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { parseBlob } from "music-metadata-browser";
|
import { parseBlob } from "music-metadata-browser";
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { buildServerFileURL } from "@/api/file";
|
import { buildServerFileURL } from "@/api/FileAPI";
|
||||||
import { isBrowserSupportedAudio } from "@/pages/file/fileAudio.shared";
|
import { isBrowserSupportedAudio } from "@/pages/file/fileAudio.shared";
|
||||||
import type { ExplorerItem } from "@/pages/file/fileExplorer.shared";
|
import type { ExplorerItem } from "@/pages/file/fileExplorer.shared";
|
||||||
import Storage from "@/utils/Storage";
|
import Storage from "@/utils/Storage";
|
||||||
|
|||||||
@@ -12,8 +12,23 @@ 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 {
|
||||||
|
snapshotRefreshSeconds: number;
|
||||||
|
historyRefreshSeconds: number;
|
||||||
|
snapshotMetrics: DashboardSnapshotMetric[];
|
||||||
|
historyMetrics: DashboardHistoryMetric[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardSetting {
|
||||||
|
server: ServerDashboardSetting;
|
||||||
|
}
|
||||||
|
|
||||||
interface SettingState {
|
interface SettingState {
|
||||||
connect: ConnectSetting;
|
connect: ConnectSetting;
|
||||||
|
dashboard: DashboardSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConnectSetting = (): ConnectSetting => ({
|
const defaultConnectSetting = (): ConnectSetting => ({
|
||||||
@@ -23,8 +38,20 @@ const defaultConnectSetting = (): ConnectSetting => ({
|
|||||||
token: ""
|
token: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultServerDashboardSetting = (): ServerDashboardSetting => ({
|
||||||
|
snapshotRefreshSeconds: 3,
|
||||||
|
historyRefreshSeconds: 10,
|
||||||
|
snapshotMetrics: ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"],
|
||||||
|
historyMetrics: ["cpu", "memory", "jvm", "network"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultDashboardSetting = (): DashboardSetting => ({
|
||||||
|
server: defaultServerDashboardSetting()
|
||||||
|
});
|
||||||
|
|
||||||
const defaultSettingState = (): SettingState => ({
|
const defaultSettingState = (): SettingState => ({
|
||||||
connect: defaultConnectSetting()
|
connect: defaultConnectSetting(),
|
||||||
|
dashboard: defaultDashboardSetting()
|
||||||
});
|
});
|
||||||
|
|
||||||
function normalizeConnectSetting(connect?: Partial<ConnectSetting>): ConnectSetting {
|
function normalizeConnectSetting(connect?: Partial<ConnectSetting>): ConnectSetting {
|
||||||
@@ -38,10 +65,52 @@ 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 {
|
||||||
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(Math.max(Math.floor(value), 1), 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeServerDashboardSetting(setting?: Partial<ServerDashboardSetting>): ServerDashboardSetting {
|
||||||
|
const fallback = defaultServerDashboardSetting();
|
||||||
|
return {
|
||||||
|
snapshotRefreshSeconds: normalizeRefreshSeconds(setting?.snapshotRefreshSeconds, fallback.snapshotRefreshSeconds),
|
||||||
|
historyRefreshSeconds: normalizeRefreshSeconds(setting?.historyRefreshSeconds, fallback.historyRefreshSeconds),
|
||||||
|
snapshotMetrics: normalizeSnapshotMetrics(setting?.snapshotMetrics),
|
||||||
|
historyMetrics: normalizeHistoryMetrics(setting?.historyMetrics)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDashboardSetting(setting?: Partial<DashboardSetting>): DashboardSetting {
|
||||||
|
return {
|
||||||
|
server: normalizeServerDashboardSetting(setting?.server)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const useSettingStore = defineStore("setting", () => {
|
export const useSettingStore = defineStore("setting", () => {
|
||||||
const state = ref<SettingState>(Storage.getDefault<SettingState>(SETTING_STORAGE_KEY, defaultSettingState()));
|
const state = ref<SettingState>(Storage.getDefault<SettingState>(SETTING_STORAGE_KEY, defaultSettingState()));
|
||||||
|
state.value.connect = normalizeConnectSetting(state.value.connect);
|
||||||
|
state.value.dashboard = normalizeDashboardSetting(state.value.dashboard);
|
||||||
|
persist();
|
||||||
|
|
||||||
const connect = computed(() => state.value.connect);
|
const connect = computed(() => state.value.connect);
|
||||||
|
const dashboard = computed(() => state.value.dashboard);
|
||||||
const hasConnectConfig = computed(() => {
|
const hasConnectConfig = computed(() => {
|
||||||
const currentConnect = state.value.connect;
|
const currentConnect = state.value.connect;
|
||||||
return !!currentConnect.host && !!currentConnect.port && !!currentConnect.token;
|
return !!currentConnect.host && !!currentConnect.port && !!currentConnect.token;
|
||||||
@@ -64,10 +133,20 @@ export const useSettingStore = defineStore("setting", () => {
|
|||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setServerDashboard(serverSetting: Partial<ServerDashboardSetting>): void {
|
||||||
|
state.value.dashboard.server = normalizeServerDashboardSetting({
|
||||||
|
...state.value.dashboard.server,
|
||||||
|
...serverSetting
|
||||||
|
});
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connect,
|
connect,
|
||||||
|
dashboard,
|
||||||
hasConnectConfig,
|
hasConnectConfig,
|
||||||
setConnect,
|
setConnect,
|
||||||
resetConnect
|
resetConnect,
|
||||||
|
setServerDashboard
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
12
src/types/File.ts
Normal file
12
src/types/File.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface ServerFile {
|
||||||
|
name: string;
|
||||||
|
extension?: string;
|
||||||
|
absolutePath?: string;
|
||||||
|
size?: number;
|
||||||
|
modifiedAt?: number;
|
||||||
|
type?: string;
|
||||||
|
isFile?: boolean;
|
||||||
|
isDirectory?: boolean;
|
||||||
|
canPreview?: boolean;
|
||||||
|
previewURI?: string;
|
||||||
|
}
|
||||||
108
src/types/System.ts
Normal file
108
src/types/System.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
cpu?: {
|
||||||
|
model: string;
|
||||||
|
physicalCores: number;
|
||||||
|
logicalCores: number;
|
||||||
|
usageTotal: number;
|
||||||
|
usageSystem: number;
|
||||||
|
temperatureCelsius?: number;
|
||||||
|
};
|
||||||
|
memory?: {
|
||||||
|
totalBytes: number;
|
||||||
|
usedBytes: number;
|
||||||
|
swapTotalBytes: number;
|
||||||
|
swapUsedBytes: number;
|
||||||
|
};
|
||||||
|
jvm?: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
bootAt: number;
|
||||||
|
heapInitBytes: number;
|
||||||
|
heapMaxBytes: number;
|
||||||
|
heapUsedBytes: number;
|
||||||
|
heapCommittedBytes: number;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
hardware?: {
|
||||||
|
fanSpeeds?: Array<number>;
|
||||||
|
baseboard?: {
|
||||||
|
manufacturer?: string;
|
||||||
|
model?: string;
|
||||||
|
version?: string;
|
||||||
|
serialNumber?: string;
|
||||||
|
};
|
||||||
|
firmware?: {
|
||||||
|
manufacturer?: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
version?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
storagePartitions?: Array<{
|
||||||
|
diskName?: string;
|
||||||
|
diskModel?: string;
|
||||||
|
diskSerial?: string;
|
||||||
|
partitionId: string;
|
||||||
|
partitionName?: string;
|
||||||
|
partitionType?: string;
|
||||||
|
uuid: string;
|
||||||
|
mountPoint: string;
|
||||||
|
total: number;
|
||||||
|
used: number;
|
||||||
|
transferTimeMs?: number;
|
||||||
|
healthStatus?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatusHistoryPoint {
|
||||||
|
at: number;
|
||||||
|
cpuUsagePercent: number;
|
||||||
|
cpuSystemPercent: number;
|
||||||
|
memoryUsedBytes: number;
|
||||||
|
swapUsedBytes: number;
|
||||||
|
heapUsedBytes: number;
|
||||||
|
heapCommittedBytes: number;
|
||||||
|
gcCycleTimeMs: number;
|
||||||
|
gcPauseTimeMs: number;
|
||||||
|
rxBytesPerSecond: number;
|
||||||
|
txBytesPerSecond: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user