update ServerDashboard

This commit is contained in:
Timi
2026-04-12 00:15:54 +08:00
parent 489cbb5d0f
commit 611830f393
30 changed files with 2078 additions and 892 deletions

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

View File

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

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