mirror of
https://github.com/snowykami/server-status-web.git
synced 2025-06-06 07:05:23 +00:00
✨ 新彩色面板!
This commit is contained in:
parent
5b2fd20c79
commit
87091f454a
Before Width: | Height: | Size: 787 B After Width: | Height: | Size: 787 B |
@ -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: {
|
||||
|
@ -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}`
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
53
src/components/OutlineAnime.vue
Normal file
53
src/components/OutlineAnime.vue
Normal 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>
|
@ -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));
|
||||
|
Loading…
x
Reference in New Issue
Block a user