first

This commit is contained in:
2024-10-02 13:48:21 +08:00
commit d3ceff0fee
31 changed files with 1512 additions and 0 deletions

24
src/App.vue Normal file
View 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
View 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
View File

1
src/assets/vue.svg Normal file
View 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

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

@ -0,0 +1 @@
/// <reference types="vite/client" />