新彩色面板!

This commit is contained in:
远野千束 2024-10-04 10:18:38 +08:00
parent 5b2fd20c79
commit 87091f454a
6 changed files with 231 additions and 56 deletions

View File

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 787 B

View File

@ -12,6 +12,7 @@ export interface Status {
uptime: number; // uptime in seconds
link: string | null; // 链接或是nil
observed_at: number; // unix timestamp
start_time: number; // unix timestamp
};
hardware: {
mem: {

View File

@ -1,3 +1,5 @@
export const onlineTimeout = 30
export function getLinuxReleaseIcon(name: string, release: string): { name: string, icon: string } {
if (name.toLowerCase() == 'windows') {
return {name: 'Windows', icon: '/svg/system-windows.svg'}
@ -19,10 +21,10 @@ export function getLinuxReleaseIcon(name: string, release: string): { name: stri
}
}
export function formatSizeByUnit(bytes: number, unit: string | null = null, suffix: string|null = null): string {
export function formatSizeByUnit(bytes: number, unit: string | null = null, suffix: string | null = null): string {
// 若指定单位,则格式化为指定单位对应的大小字符串加上单位
// 若未指定单位则选择1-1024之间的最大单位格式化为该单位对应的大小字符串加上单位
if( bytes == 0 ){
if (bytes == 0) {
return '0'
}
if (bytes < 1024) {
@ -51,9 +53,56 @@ export function format2Size(num1: number, num2: number, suffix: string | null =
return `${n1}/${r2.num.toFixed(1)}${r2.unit}${suffix}`
}
export function formatDate(timestamp: number, timeOnly: boolean = false){
export function formatDate(timestamp: number, timeOnly: boolean = false) {
const d = new Date(timestamp * 1000)
const date = d.toLocaleDateString()
const time = d.toLocaleTimeString()
return timeOnly ? time : `${date} ${time}`
}
export function getBaseColor(percent: number, disable: boolean = false) {
// 获取基础颜色
// 0~60: green, 60~80: yellow, 80~90: orange, 90~100: red
if (disable) {
return '#9ca3af'
}
if (percent < 60) {
return '#22c55e'
} else if (percent < 80) {
return '#eab308'
} else if (percent < 90) {
return '#f97316'
} else {
return '#ef4444'
}
}
export function getBlankColor(percent: number, disable: boolean = false) {
if (disable) {
return '#e5e7eb'
}
//相比base更浅的颜色
if (percent < 60) {
return '#bbf7d0'
} else if (percent < 80) {
return '#fef08a'
} else if (percent < 90) {
return '#fed7aa'
} else {
return '#fecaca'
}
}
// 1727998501
export function formatUptime(uptime: number ): string {
const seconds = uptime
const d = Math.floor(seconds / 86400)
const h = Math.floor((seconds % 86400) / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = Math.floor(seconds % 60)
return `${d}:${h}:${m}:${s}`
}

View File

@ -2,7 +2,18 @@
import {Status} from "../api";
import {computed, onMounted, ref, watch} from "vue";
import * as echarts from "echarts";
import {format2Size, formatDate, formatSizeByUnit, getLinuxReleaseIcon} from "../api/utils.ts";
import {
format2Size,
formatDate,
formatSizeByUnit,
formatUptime,
getBaseColor,
getBlankColor,
getLinuxReleaseIcon,
onlineTimeout
} from "../api/utils.ts";
import OutlineAnime from "./OutlineAnime.vue";
const props = defineProps<{
status: Status
@ -12,6 +23,8 @@ const status = computed(
() => props.status
)
const uptime = ref(formatUptime(status.value.meta.uptime))
console.log(uptime.value)
const cpuChartRef = ref(null);
const memoryChartRef = ref(null);
const swapChartRef = ref(null);
@ -19,11 +32,14 @@ const swapChartRef = ref(null);
//
const netChartRef = ref(null);
let netStats: [number, number, number][] = []
const dotColor = ref('#22c55e')
const isOnline = ref(true)
const dotColor = computed(
() => isOnline.value ? '#22c55e' : '#ff4d4f'
)
const spreadColor = computed(
() => isOnline.value ? '#80ffb0' : '#fd8182'
)
const deltaTime = ref('0')
// const isDiskCollapsed = ref(status.value.hardware.disks)
// const isDiskOpen = ref(false)
const os = computed(() => {
return getLinuxReleaseIcon(status.value.meta.os.name, status.value.meta.os.version)
})
@ -36,7 +52,6 @@ const swapDetail = computed(() => {
return status.value.hardware.swap.total > 0 ? format2Size(status.value.hardware.swap.used, status.value.hardware.swap.total) : 'N/A'
})
function onMountedFunc() {
const cpuChart = echarts.init(cpuChartRef.value);
const memoryChart = echarts.init(memoryChartRef.value);
@ -48,14 +63,19 @@ function onMountedFunc() {
}
const radius = ['65%', '90%']
const hwColor = ['#4c4c4c', '#e3e3e3']
const netColor = ['#4c4c4c', '#bababa']
const netColor = ['#a2d8f4', '#10a0ed']
setInterval(() => {
if (isOnline.value) {
const deltaTime = (Date.now()) / 1000 - status.value.meta.observed_at
uptime.value = formatUptime(status.value.meta.uptime + deltaTime)
}
}, 1000)
function update() {
const timeDiff = (Date.now()) / 1000 - status.value.meta.observed_at
deltaTime.value = timeDiff.toFixed(1)
// push
if (netStats.length === 0 || netStats[netStats.length - 1][0] !== status.value.meta.observed_at){
if (netStats.length === 0 || netStats[netStats.length - 1][0] !== status.value.meta.observed_at) {
netStats.push([status.value.meta.observed_at, status.value.hardware.net.up, status.value.hardware.net.down])
}
@ -63,12 +83,13 @@ function onMountedFunc() {
netStats.shift()
}
if (timeDiff > 30) {
dotColor.value = '#ff4d4f'
}
isOnline.value = timeDiff <= onlineTimeout;
cpuChart.setOption(
{
color: hwColor,
color: [
getBaseColor(status.value.hardware.cpu.percent),
getBlankColor(status.value.hardware.cpu.percent)
],
title: {
text: status.value.hardware.cpu.percent + '%',
left: 'center',
@ -106,7 +127,10 @@ function onMountedFunc() {
)
memoryChart.setOption(
{
color: hwColor,
color: [
getBaseColor(status.value.hardware.mem.used / status.value.hardware.mem.total * 100),
getBlankColor(status.value.hardware.mem.used / status.value.hardware.mem.total * 100)
],
title: {
text: `${(status.value.hardware.mem.used / status.value.hardware.mem.total * 100).toFixed(1)}%`,
left: 'center',
@ -142,7 +166,10 @@ function onMountedFunc() {
)
swapChart.setOption(
{
color: hwColor,
color: [
getBaseColor(status.value.hardware.swap.used / status.value.hardware.swap.total * 100, status.value.hardware.swap.total <= 0),
getBlankColor(status.value.hardware.swap.used / status.value.hardware.swap.total * 100, status.value.hardware.swap.total <= 0)
],
title: {
text: status.value.hardware.swap.total > 0 ? `${(status.value.hardware.swap.used / status.value.hardware.swap.total * 100).toFixed(1)}%` : 'N/A',
left: 'center',
@ -223,7 +250,7 @@ function onMountedFunc() {
type: 'value',
axisLabel: {
formatter: function (value: number) {
return formatSizeByUnit(value*8, null, 'b')
return formatSizeByUnit(value * 8, null, 'b')
}
}
}
@ -276,29 +303,24 @@ onMounted(
<template>
<div class="host">
<div class="meta-1" style="display: flex; justify-content: space-between">
<div class="meta1-left" style="display: flex; justify-content: flex-start">
<span>{{ props.status.meta.name }}</span>
<div class="meta1-left" style="display: flex; justify-content: flex-start; align-items: center">
<OutlineAnime class="outline-anime" :color="dotColor" :spreadColor="spreadColor" :is-online="isOnline"/>
<span class="host-name" style="display: flex">{{ props.status.meta.name }}</span>
</div>
<div class="meta1-right" style="display: flex; justify-content: flex-end; align-items: center">
<div style="margin-right: 5px">{{ deltaTime }}s ago</div>
<div class="dot" :style="{backgroundColor: dotColor}"
style="height: 15px; width: 15px; border-radius: 50%"></div>
<div style="margin-right: 5px">{{ uptime }}</div>
</div>
</div>
<div class="meta-2" style="display: flex">
<div class="section">
<img class="icon" :src="os.icon" alt="system">
<span>{{ os.name }}</span>
</div>
<div class="section">
<img class="icon" src="/svg/location.svg" alt="system">
<span>{{ props.status.meta.location }}</span>
<span class="meta2-text">{{ os.name }} · {{ props.status.meta.location }}</span>
</div>
</div>
<!-- <div class="labels" style="display: flex; justify-content: flex-start">-->
<!-- <span class="label" v-for="label in props.status.meta.labels" :key="label">{{ label }}</span>-->
<!-- </div>-->
<div class="labels" style="display: flex; justify-content: flex-start">
<span class="label" v-for="label in props.status.meta.labels" :key="label">{{ label }}</span>
</div>
<div class="charts-container" style="display: flex; justify-content: space-between">
<div class="cpu-info hw-info">
<div class="chart" ref="cpuChartRef"></div>
@ -324,19 +346,50 @@ onMounted(
</template>
<style scoped>
:root {
--text-color-1: #000;
--text-color-2: #383838;
--liteyuki-color-1: #d0e9ff;
--liteyuki-color-2: #a2d8f4;
}
.meta-1 {
.outline-anime {
margin-right: 0.5em;
}
.meta1-left {
.host-name {
text-align: center;
font-size: 1.2rem;
font-weight: bold;
}
}
}
.meta-2 {
margin-top: 0.5em;
.meta2-text {
font-size: 0.9rem;
color: var(--text-color-2);
}
}
.labels {
margin-top: 0.5em;
.label {
padding: 2px 5px;
border-radius: 5px;
padding: 0.05rem 0.5rem;
border: 2px dashed;
border-color: var(--text-color-1);
border-radius: 50px;
margin-right: 10px;
background-color: black;
color: white;
background-color: #dfdfdf;
color: var(--text-color-1);
font-size: 0.8rem;
}
}
@ -344,7 +397,6 @@ onMounted(
padding: 1em;
border: 1px solid #ccc;
border-radius: 20px;
margin: 0.5em;
flex-direction: column;
justify-content: space-between;
}
@ -385,15 +437,10 @@ onMounted(
}
}
.net{
.net {
margin-top: 0.5rem;
.net-title {
font-size: 0.9rem;
}
.net-detail {
font-size: 0.7rem;
}
.net-chart{
.net-chart {
width: 100%;
aspect-ratio: 2;
}

View File

@ -0,0 +1,53 @@
<template>
<div class="online-status">
<div class="dot" :style="{ backgroundColor: props.color }"></div>
<div class="pulse" v-if="isOnline" :style="{ backgroundColor: props.spreadColor }"></div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
const props = defineProps<{
color: string;
spreadColor: string;
isOnline: boolean;
}>();
</script>
<style scoped>
.online-status {
position: relative;
width: 15px;
height: 15px;
}
.dot {
width: 100%;
height: 100%;
border-radius: 50%;
z-index: 1; /* Ensure the base color layer is on top */
}
.pulse {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
animation: pulse 1.5s infinite;
z-index: 0; /* Ensure the radar layer is below the base color layer */
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(2);
opacity: 0;
}
}
</style>

View File

@ -1,33 +1,58 @@
<script setup lang="ts">
import {onMounted, onUnmounted, Ref, ref} from "vue";
import {computed, onMounted, onUnmounted, Ref, ref} from "vue";
import {getStatuses, Status} from "../api";
import Host from "../components/Host.vue";
import {onlineTimeout} from "../api/utils.ts";
const statuses: Ref<Record<string, Status>> = ref({})
const offline = ref(0)
const onlineNum = computed(
() => {
const nowTimestamp = Date.now() / 1000
let online = 0
for (const status of Object.values(statuses.value)) {
if (nowTimestamp - status.meta.observed_at < onlineTimeout) {
online++
}
}
offline.value = Object.values(statuses.value).length - online
return online
}
)
const timer = setInterval(async () => {
statuses.value = await getStatuses()
}, 1000)
onMounted(async () => {
statuses.value = await getStatuses()
console.log("mounted")
// 5
const timer = setInterval(async () => {
statuses.value = await getStatuses()
console.log("refresh")
}, 1000)
onUnmounted(() => {
clearInterval(timer)
console.log("unmounted")
})
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<template>
<div class="overview" style="">
<h2>Overview: {{onlineNum}} Online {{offline}} Offline</h2>
</div>
<div class="grid-container">
<Host class="grid-item" v-for="(status, id) in statuses" :key="id" :status="status"/>
</div>
</template>
<style scoped>
.overview {
display: flex;
justify-content: space-between;
padding: 10px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));