mirror of
https://github.com/snowykami/server-status-web.git
synced 2025-09-06 12:46:24 +00:00
✨ first
This commit is contained in:
24
src/App.vue
Normal file
24
src/App.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import Nav from './components/Nav.vue'
|
||||
console.log('App.vue')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Nav/>
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
48
src/api/index.ts
Normal file
48
src/api/index.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/// <reference types="node" />
|
||||
export interface Status {
|
||||
meta: {
|
||||
id: string; // 服务器ID,用于标识服务器
|
||||
name: string;
|
||||
os: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
labels: string[]; // 服务器标签
|
||||
location: string; // Chongqing, China
|
||||
uptime: number; // uptime in seconds
|
||||
link: string | null; // 链接或是nil
|
||||
observed_at: number; // unix timestamp
|
||||
};
|
||||
hardware: {
|
||||
mem: {
|
||||
total: number;
|
||||
used: number;
|
||||
};
|
||||
swap: {
|
||||
total: number;
|
||||
used: number;
|
||||
};
|
||||
cpu: {
|
||||
cores: number;
|
||||
logics: number;
|
||||
percent: number; // 0-100
|
||||
};
|
||||
disks: {
|
||||
[key: string]: {
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
net: {
|
||||
up: number;
|
||||
down: number;
|
||||
type: string; // IPv4 or IPv6 or IPv4/6
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const apiRoot = import.meta.env.VITE_API_ROOT
|
||||
|
||||
export async function getStatuses(): Promise<Record<string, Status>> {
|
||||
const response = await fetch(`${apiRoot}/api/status`)
|
||||
return response.json()
|
||||
}
|
0
src/api/node.ts
Normal file
0
src/api/node.ts
Normal file
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
41
src/components/HelloWorld.vue
Normal file
41
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
238
src/components/Host.vue
Normal file
238
src/components/Host.vue
Normal file
@ -0,0 +1,238 @@
|
||||
<script setup lang="ts">
|
||||
import {Status} from "../api";
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps<{
|
||||
status: Status
|
||||
}>()
|
||||
|
||||
const status = computed(
|
||||
() => props.status
|
||||
)
|
||||
|
||||
const cpuChartRef = ref(null);
|
||||
const memoryChartRef = ref(null);
|
||||
const swapChartRef = ref(null);
|
||||
const diskChartRef = ref(null);
|
||||
|
||||
onMounted(
|
||||
() => {
|
||||
setOptions()
|
||||
}
|
||||
)
|
||||
|
||||
function setOptions() {
|
||||
const cpuChart = echarts.init(cpuChartRef.value);
|
||||
const memoryChart = echarts.init(memoryChartRef.value);
|
||||
const swapChart = echarts.init(swapChartRef.value);
|
||||
const diskChart = echarts.init(diskChartRef.value);
|
||||
|
||||
function setOption() {
|
||||
cpuChart.setOption(
|
||||
{
|
||||
title: {
|
||||
text: 'CPU',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontSize: 14,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
// data: [
|
||||
// {value: status.value.hardware.cpu.percent, name: 'CPU'},
|
||||
// {value: 100 - status.value.hardware.cpu.percent, name: '空闲'}
|
||||
// ]
|
||||
data: computed(
|
||||
() => [
|
||||
{value: status.value.hardware.cpu.percent, name: 'CPU'},
|
||||
{value: 100 - status.value.hardware.cpu.percent, name: '空闲'}
|
||||
]
|
||||
).value
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
memoryChart.setOption(
|
||||
{
|
||||
title: {
|
||||
text: 'Memory',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontSize: 14,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{value: props.status.hardware.mem.used, name: 'Memory'},
|
||||
{value: props.status.hardware.mem.total - props.status.hardware.mem.used, name: '空闲'}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
swapChart.setOption(
|
||||
{
|
||||
title: {
|
||||
text: 'Swap',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontSize: 14,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{value: props.status.hardware.swap.used, name: 'Swap'},
|
||||
{value: props.status.hardware.swap.total - props.status.hardware.swap.used, name: '空闲'}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
diskChart.setOption(
|
||||
{
|
||||
title: {
|
||||
text: 'Disk',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontSize: 14,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setOption()
|
||||
watch(
|
||||
() => status.value,
|
||||
() => {
|
||||
setOption()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="host">
|
||||
<div class="meta-1" style="display: flex; justify-content: flex-start">
|
||||
<span>{{ props.status.meta.name }}</span><span>{{ props.status.meta.id }}</span>
|
||||
</div>
|
||||
<div class="meta-2" style="display: flex">
|
||||
<div class="section">
|
||||
<img class="icon" src="/svg/location.svg" alt="system">
|
||||
<span>{{ props.status.meta.location }}</span>
|
||||
</div>
|
||||
<div class="section">
|
||||
<img class="icon" src="/svg/system.svg" alt="system">
|
||||
<span>{{ props.status.meta.os.name }}</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="charts-container" style="display: flex">
|
||||
<div class="chart" ref="cpuChartRef"></div>
|
||||
<div class="chart" ref="memoryChartRef"></div>
|
||||
<div class="chart" ref="swapChartRef"></div>
|
||||
<div class="chart" ref="diskChartRef"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.host {
|
||||
padding: 1em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px;
|
||||
margin: 0.5em;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5em;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
background-color: #535bf2;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
</style>
|
48
src/components/Nav.vue
Normal file
48
src/components/Nav.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import router from "../router/index.ts"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 三部分 左 点击侧栏, 中 logo, 右 语言-->
|
||||
<div class="navbar">
|
||||
<div class="navbar-left">
|
||||
</div>
|
||||
<div class="navbar-center">
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div v-for="route in router.getRoutes()">
|
||||
<router-link :to="route.path" :key="route.name">
|
||||
{{ route.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 80px;
|
||||
background-color: #d0e9ff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-left img {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.navbar-left,
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-center img {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
</style>
|
10
src/main.ts
Normal file
10
src/main.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from "./router";
|
||||
|
||||
// createApp(App).use(router).mount('#app')
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
23
src/router/index.ts
Normal file
23
src/router/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {createRouter, createWebHistory} from "vue-router"
|
||||
import Home from "../views/Home.vue"
|
||||
import Test from "../views/Test.vue"
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: '主页',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: '测试',
|
||||
component: Test
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory("/"),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
85
src/style.css
Normal file
85
src/style.css
Normal file
@ -0,0 +1,85 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
/*body {*/
|
||||
/* margin: 0;*/
|
||||
/* display: flex;*/
|
||||
/* place-items: center;*/
|
||||
/* min-width: 320px;*/
|
||||
/* min-height: 100vh;*/
|
||||
/*}*/
|
||||
|
||||
/*h1 {*/
|
||||
/* font-size: 3.2em;*/
|
||||
/* line-height: 1.1;*/
|
||||
/*}*/
|
||||
|
||||
/*button {*/
|
||||
/* border-radius: 8px;*/
|
||||
/* border: 1px solid transparent;*/
|
||||
/* padding: 0.6em 1.2em;*/
|
||||
/* font-size: 1em;*/
|
||||
/* font-weight: 500;*/
|
||||
/* font-family: inherit;*/
|
||||
/* background-color: #1a1a1a;*/
|
||||
/* cursor: pointer;*/
|
||||
/* transition: border-color 0.25s;*/
|
||||
/*}*/
|
||||
/*button:hover {*/
|
||||
/* border-color: #646cff;*/
|
||||
/*}*/
|
||||
/*button:focus,*/
|
||||
/*button:focus-visible {*/
|
||||
/* outline: 4px auto -webkit-focus-ring-color;*/
|
||||
/*}*/
|
||||
|
||||
/*.card {*/
|
||||
/* padding: 2em;*/
|
||||
/*}*/
|
||||
|
||||
/*#app {*/
|
||||
/* max-width: 1280px;*/
|
||||
/* margin: 0 auto;*/
|
||||
/* padding: 2rem;*/
|
||||
/* text-align: center;*/
|
||||
/*}*/
|
||||
|
||||
/*@media (prefers-color-scheme: light) {*/
|
||||
/* :root {*/
|
||||
/* color: #213547;*/
|
||||
/* background-color: #ffffff;*/
|
||||
/* }*/
|
||||
/* a:hover {*/
|
||||
/* color: #747bff;*/
|
||||
/* }*/
|
||||
/* button {*/
|
||||
/* background-color: #f9f9f9;*/
|
||||
/* }*/
|
||||
/*}*/
|
45
src/views/Home.vue
Normal file
45
src/views/Home.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted, Ref, ref} from "vue";
|
||||
import {getStatuses, Status} from "../api";
|
||||
import Host from "../components/Host.vue";
|
||||
|
||||
const statuses: Ref<Record<string, Status>> = ref({})
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid-container">
|
||||
<Host class="grid-item" v-for="(status, id) in statuses" :key="id" :status="status"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
background-color: #3bb7c3;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid #3bb7c3;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
11
src/views/Test.vue
Normal file
11
src/views/Test.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Test
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
Reference in New Issue
Block a user