mirror of
https://github.com/snowykami/server-status-web.git
synced 2025-06-07 15:45:26 +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
|
uptime: number; // uptime in seconds
|
||||||
link: string | null; // 链接或是nil
|
link: string | null; // 链接或是nil
|
||||||
observed_at: number; // unix timestamp
|
observed_at: number; // unix timestamp
|
||||||
|
start_time: number; // unix timestamp
|
||||||
};
|
};
|
||||||
hardware: {
|
hardware: {
|
||||||
mem: {
|
mem: {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
export const onlineTimeout = 30
|
||||||
|
|
||||||
export function getLinuxReleaseIcon(name: string, release: string): { name: string, icon: string } {
|
export function getLinuxReleaseIcon(name: string, release: string): { name: string, icon: string } {
|
||||||
if (name.toLowerCase() == 'windows') {
|
if (name.toLowerCase() == 'windows') {
|
||||||
return {name: 'Windows', icon: '/svg/system-windows.svg'}
|
return {name: 'Windows', icon: '/svg/system-windows.svg'}
|
||||||
@ -57,3 +59,50 @@ export function formatDate(timestamp: number, timeOnly: boolean = false){
|
|||||||
const time = d.toLocaleTimeString()
|
const time = d.toLocaleTimeString()
|
||||||
return timeOnly ? time : `${date} ${time}`
|
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 {Status} from "../api";
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
import * as echarts from "echarts";
|
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<{
|
const props = defineProps<{
|
||||||
status: Status
|
status: Status
|
||||||
@ -12,6 +23,8 @@ const status = computed(
|
|||||||
() => props.status
|
() => props.status
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const uptime = ref(formatUptime(status.value.meta.uptime))
|
||||||
|
console.log(uptime.value)
|
||||||
const cpuChartRef = ref(null);
|
const cpuChartRef = ref(null);
|
||||||
const memoryChartRef = ref(null);
|
const memoryChartRef = ref(null);
|
||||||
const swapChartRef = ref(null);
|
const swapChartRef = ref(null);
|
||||||
@ -19,11 +32,14 @@ const swapChartRef = ref(null);
|
|||||||
// 网络
|
// 网络
|
||||||
const netChartRef = ref(null);
|
const netChartRef = ref(null);
|
||||||
let netStats: [number, number, number][] = []
|
let netStats: [number, number, number][] = []
|
||||||
|
const isOnline = ref(true)
|
||||||
const dotColor = ref('#22c55e')
|
const dotColor = computed(
|
||||||
|
() => isOnline.value ? '#22c55e' : '#ff4d4f'
|
||||||
|
)
|
||||||
|
const spreadColor = computed(
|
||||||
|
() => isOnline.value ? '#80ffb0' : '#fd8182'
|
||||||
|
)
|
||||||
const deltaTime = ref('0')
|
const deltaTime = ref('0')
|
||||||
// const isDiskCollapsed = ref(status.value.hardware.disks)
|
|
||||||
// const isDiskOpen = ref(false)
|
|
||||||
const os = computed(() => {
|
const os = computed(() => {
|
||||||
return getLinuxReleaseIcon(status.value.meta.os.name, status.value.meta.os.version)
|
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'
|
return status.value.hardware.swap.total > 0 ? format2Size(status.value.hardware.swap.used, status.value.hardware.swap.total) : 'N/A'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function onMountedFunc() {
|
function onMountedFunc() {
|
||||||
const cpuChart = echarts.init(cpuChartRef.value);
|
const cpuChart = echarts.init(cpuChartRef.value);
|
||||||
const memoryChart = echarts.init(memoryChartRef.value);
|
const memoryChart = echarts.init(memoryChartRef.value);
|
||||||
@ -48,8 +63,13 @@ function onMountedFunc() {
|
|||||||
}
|
}
|
||||||
const radius = ['65%', '90%']
|
const radius = ['65%', '90%']
|
||||||
|
|
||||||
const hwColor = ['#4c4c4c', '#e3e3e3']
|
const netColor = ['#a2d8f4', '#10a0ed']
|
||||||
const netColor = ['#4c4c4c', '#bababa']
|
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() {
|
function update() {
|
||||||
const timeDiff = (Date.now()) / 1000 - status.value.meta.observed_at
|
const timeDiff = (Date.now()) / 1000 - status.value.meta.observed_at
|
||||||
@ -63,12 +83,13 @@ function onMountedFunc() {
|
|||||||
netStats.shift()
|
netStats.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeDiff > 30) {
|
isOnline.value = timeDiff <= onlineTimeout;
|
||||||
dotColor.value = '#ff4d4f'
|
|
||||||
}
|
|
||||||
cpuChart.setOption(
|
cpuChart.setOption(
|
||||||
{
|
{
|
||||||
color: hwColor,
|
color: [
|
||||||
|
getBaseColor(status.value.hardware.cpu.percent),
|
||||||
|
getBlankColor(status.value.hardware.cpu.percent)
|
||||||
|
],
|
||||||
title: {
|
title: {
|
||||||
text: status.value.hardware.cpu.percent + '%',
|
text: status.value.hardware.cpu.percent + '%',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@ -106,7 +127,10 @@ function onMountedFunc() {
|
|||||||
)
|
)
|
||||||
memoryChart.setOption(
|
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: {
|
title: {
|
||||||
text: `${(status.value.hardware.mem.used / status.value.hardware.mem.total * 100).toFixed(1)}%`,
|
text: `${(status.value.hardware.mem.used / status.value.hardware.mem.total * 100).toFixed(1)}%`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@ -142,7 +166,10 @@ function onMountedFunc() {
|
|||||||
)
|
)
|
||||||
swapChart.setOption(
|
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: {
|
title: {
|
||||||
text: status.value.hardware.swap.total > 0 ? `${(status.value.hardware.swap.used / status.value.hardware.swap.total * 100).toFixed(1)}%` : 'N/A',
|
text: status.value.hardware.swap.total > 0 ? `${(status.value.hardware.swap.used / status.value.hardware.swap.total * 100).toFixed(1)}%` : 'N/A',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@ -276,29 +303,24 @@ onMounted(
|
|||||||
<template>
|
<template>
|
||||||
<div class="host">
|
<div class="host">
|
||||||
<div class="meta-1" style="display: flex; justify-content: space-between">
|
<div class="meta-1" style="display: flex; justify-content: space-between">
|
||||||
<div class="meta1-left" style="display: flex; justify-content: flex-start">
|
<div class="meta1-left" style="display: flex; justify-content: flex-start; align-items: center">
|
||||||
<span>{{ props.status.meta.name }}</span>
|
<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>
|
||||||
<div class="meta1-right" style="display: flex; justify-content: flex-end; align-items: center">
|
<div class="meta1-right" style="display: flex; justify-content: flex-end; align-items: center">
|
||||||
<div style="margin-right: 5px">{{ deltaTime }}s ago</div>
|
<div style="margin-right: 5px">{{ uptime }}</div>
|
||||||
<div class="dot" :style="{backgroundColor: dotColor}"
|
|
||||||
style="height: 15px; width: 15px; border-radius: 50%"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-2" style="display: flex">
|
<div class="meta-2" style="display: flex">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<img class="icon" :src="os.icon" alt="system">
|
<img class="icon" :src="os.icon" alt="system">
|
||||||
<span>{{ os.name }}</span>
|
<span class="meta2-text">{{ os.name }} · {{ props.status.meta.location }}</span>
|
||||||
</div>
|
|
||||||
<div class="section">
|
|
||||||
<img class="icon" src="/svg/location.svg" alt="system">
|
|
||||||
<span>{{ props.status.meta.location }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="labels" style="display: flex; justify-content: flex-start">-->
|
<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>-->
|
<span class="label" v-for="label in props.status.meta.labels" :key="label">{{ label }}</span>
|
||||||
<!-- </div>-->
|
</div>
|
||||||
<div class="charts-container" style="display: flex; justify-content: space-between">
|
<div class="charts-container" style="display: flex; justify-content: space-between">
|
||||||
<div class="cpu-info hw-info">
|
<div class="cpu-info hw-info">
|
||||||
<div class="chart" ref="cpuChartRef"></div>
|
<div class="chart" ref="cpuChartRef"></div>
|
||||||
@ -324,19 +346,50 @@ onMounted(
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.meta-2 {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
.meta2-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.labels {
|
.labels {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
padding: 2px 5px;
|
padding: 0.05rem 0.5rem;
|
||||||
border-radius: 5px;
|
border: 2px dashed;
|
||||||
|
border-color: var(--text-color-1);
|
||||||
|
border-radius: 50px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
background-color: black;
|
background-color: #dfdfdf;
|
||||||
color: white;
|
color: var(--text-color-1);
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,7 +397,6 @@ onMounted(
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
margin: 0.5em;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@ -387,12 +439,7 @@ onMounted(
|
|||||||
|
|
||||||
.net {
|
.net {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
.net-title {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.net-detail {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
.net-chart {
|
.net-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 2;
|
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">
|
<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 {getStatuses, Status} from "../api";
|
||||||
import Host from "../components/Host.vue";
|
import Host from "../components/Host.vue";
|
||||||
|
import {onlineTimeout} from "../api/utils.ts";
|
||||||
|
|
||||||
const statuses: Ref<Record<string, Status>> = ref({})
|
const statuses: Ref<Record<string, Status>> = ref({})
|
||||||
onMounted(async () => {
|
|
||||||
statuses.value = await getStatuses()
|
const offline = ref(0)
|
||||||
console.log("mounted")
|
|
||||||
// 创建定时器,每隔 5 秒刷新一次数据
|
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 () => {
|
const timer = setInterval(async () => {
|
||||||
statuses.value = await getStatuses()
|
statuses.value = await getStatuses()
|
||||||
console.log("refresh")
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
statuses.value = await getStatuses()
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
console.log("unmounted")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="overview" style="">
|
||||||
|
<h2>Overview: {{onlineNum}} Online {{offline}} Offline</h2>
|
||||||
|
</div>
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<Host class="grid-item" v-for="(status, id) in statuses" :key="id" :status="status"/>
|
<Host class="grid-item" v-for="(status, id) in statuses" :key="id" :status="status"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.overview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user