From 637559ff0a6f3e6f0586d8c8f861d576648260f0 Mon Sep 17 00:00:00 2001 From: snowykami Date: Sat, 5 Oct 2024 04:42:03 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96UI?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 34 ++++++ public/svg/label.svg | 1 + public/svg/screenshots.svg | 1 + public/svg/timezone.svg | 1 + src/App.vue | 6 +- src/api/index.ts | 4 + src/components/Disk.vue | 84 +++++++++++++++ src/components/Host.vue | 214 ++++++++++++++++++++++++++----------- src/style.css | 2 + src/views/Home.vue | 2 +- 11 files changed, 283 insertions(+), 67 deletions(-) create mode 100644 public/svg/label.svg create mode 100644 public/svg/screenshots.svg create mode 100644 public/svg/timezone.svg create mode 100644 src/components/Disk.vue diff --git a/package.json b/package.json index dae5c94..59a40ce 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "echarts": "^5.5.1", + "html2canvas": "^1.4.1", "vue": "^3.4.37", "vue-router": "^4.4.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2f22fc..a9bcab3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: echarts: specifier: ^5.5.1 version: 5.5.1 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 vue: specifier: ^3.4.37 version: 3.5.10(typescript@5.6.2) @@ -545,6 +548,11 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: @@ -555,6 +563,12 @@ packages: resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} dev: true + /css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + dependencies: + utrie: 1.0.2 + dev: false + /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -620,6 +634,14 @@ packages: hasBin: true dev: true + /html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + dev: false + /magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} dependencies: @@ -696,6 +718,12 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + /text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + dependencies: + utrie: 1.0.2 + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -713,6 +741,12 @@ packages: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} dev: true + /utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + dependencies: + base64-arraybuffer: 1.0.2 + dev: false + /vite@5.4.8(@types/node@22.7.4): resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/public/svg/label.svg b/public/svg/label.svg new file mode 100644 index 0000000..8e235b7 --- /dev/null +++ b/public/svg/label.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svg/screenshots.svg b/public/svg/screenshots.svg new file mode 100644 index 0000000..1b2fd3e --- /dev/null +++ b/public/svg/screenshots.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svg/timezone.svg b/public/svg/timezone.svg new file mode 100644 index 0000000..9a71935 --- /dev/null +++ b/public/svg/timezone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 74ead23..25eb9fa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,8 @@ const year = new Date().getFullYear()

Server Status

@@ -30,4 +31,7 @@ footer { padding: 1em; color: #666; } +a { + color: #36a7ec; +} diff --git a/src/api/index.ts b/src/api/index.ts index bb06c1f..21d6a97 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,6 +13,7 @@ export interface Status { link: string | null; // 链接或是nil observed_at: number; // unix timestamp start_time: number; // unix timestamp + timezone: string; // Asia/Shanghai }; hardware: { mem: { @@ -32,6 +33,9 @@ export interface Status { [key: string]: { used: number; total: number; + mountpoint: string; + fstype: string; + device: string; }; }; net: { diff --git a/src/components/Disk.vue b/src/components/Disk.vue new file mode 100644 index 0000000..caed723 --- /dev/null +++ b/src/components/Disk.vue @@ -0,0 +1,84 @@ + + + + + \ No newline at end of file diff --git a/src/components/Host.vue b/src/components/Host.vue index 51ff58e..1d55429 100644 --- a/src/components/Host.vue +++ b/src/components/Host.vue @@ -14,6 +14,8 @@ import { } from "../api/utils.ts"; import OutlineAnime from "./OutlineAnime.vue"; +import Disk from "./Disk.vue"; +import html2canvas from "html2canvas"; const props = defineProps<{ status: Status @@ -24,7 +26,6 @@ const status = computed( ) const uptime = ref(formatUptime(status.value.meta.uptime)) -console.log(uptime.value) const cpuChartRef = ref(null); const memoryChartRef = ref(null); const swapChartRef = ref(null); @@ -33,10 +34,10 @@ const swapChartRef = ref(null); const netChartRef = ref(null); let netStats: [number, number, number][] = [] const isOnline = ref(true) -const dotColor = computed( +const statusColor = computed( () => isOnline.value ? '#22c55e' : '#ff4d4f' ) -const spreadColor = computed( +const statusColor2 = computed( () => isOnline.value ? '#80ffb0' : '#fd8182' ) const deltaTime = ref('0') @@ -52,18 +53,40 @@ const swapDetail = computed(() => { return status.value.hardware.swap.total > 0 ? format2Size(status.value.hardware.swap.used, status.value.hardware.swap.total) : 'N/A' }) +const gradientStyle = computed(() => { + return { + borderColor: `linear-gradient(90deg, ${statusColor.value}, ${statusColor2.value})` + } +}) + +const hoverBorderColor = computed(() => { + return statusColor.value +}) + + function onMountedFunc() { const cpuChart = echarts.init(cpuChartRef.value); const memoryChart = echarts.init(memoryChartRef.value); const swapChart = echarts.init(swapChartRef.value); const netChart = echarts.init(netChartRef.value); + + // style const titleStyle = { color: 'rgba(0, 0, 0, 0.8)', - fontSize: 13, + fontSize: 15, } const radius = ['65%', '90%'] + const netColor = ['#a2d8f4', '#0194e3'] // Tx Rx + const pieLabelPosition = 'center' + const emphasis = { + label: { + show: true, + fontSize: 15, + position: ['50%', '20%'] // 设置标签位置为圆环外部 + }, + } - const netColor = ['#a2d8f4', '#10a0ed'] + // 更新时间 setInterval(() => { if (isOnline.value) { const deltaTime = (Date.now()) / 1000 - status.value.meta.observed_at @@ -76,7 +99,7 @@ function onMountedFunc() { deltaTime.value = timeDiff.toFixed(1) // 判断该时间与上一个时间不同才push 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]) + netStats.push([status.value.meta.observed_at, status.value.hardware.net.up, status.value.hardware.net.down]) // 时间 上行 下行 } if (netStats.length > 20) { @@ -94,7 +117,7 @@ function onMountedFunc() { text: status.value.hardware.cpu.percent + '%', left: 'center', top: 'center', - textStyle: titleStyle + textStyle: titleStyle, }, series: [ { @@ -103,22 +126,16 @@ function onMountedFunc() { avoidLabelOverlap: false, label: { show: false, - position: 'center' - }, - emphasis: { - label: { - show: true, - fontSize: '20', - fontWeight: 'bold' - } + position: pieLabelPosition }, + emphasis: emphasis, labelLine: { show: false }, data: computed( () => [ - {value: status.value.hardware.cpu.percent}, - {value: 100 - status.value.hardware.cpu.percent} + {value: status.value.hardware.cpu.percent, name: 'Used'}, + {value: 100 - status.value.hardware.cpu.percent, name: 'Free'} ] ).value } @@ -144,21 +161,15 @@ function onMountedFunc() { avoidLabelOverlap: false, label: { show: false, - position: 'center' - }, - emphasis: { - label: { - show: true, - fontSize: '20', - fontWeight: 'bold' - } + position: pieLabelPosition }, + emphasis: emphasis, labelLine: { show: false }, data: [ - {value: status.value.hardware.mem.used}, - {value: status.value.hardware.mem.total - status.value.hardware.mem.used} + {value: status.value.hardware.mem.used, name: 'Used'}, + {value: status.value.hardware.mem.total - status.value.hardware.mem.used, name: 'Free'} ] } ] @@ -183,22 +194,17 @@ function onMountedFunc() { avoidLabelOverlap: false, label: { show: false, - position: 'center' - }, - emphasis: { - label: { - show: true, - fontSize: '20', - fontWeight: 'bold' - } + position: pieLabelPosition }, + emphasis: emphasis, labelLine: { show: false }, data: [ - {value: status.value.hardware.swap.total > 0 ? status.value.hardware.swap.used : 0}, + {value: status.value.hardware.swap.total > 0 ? status.value.hardware.swap.used : 0, name: 'Used'}, { - value: status.value.hardware.swap.total > 0 ? status.value.hardware.swap.total - status.value.hardware.swap.used : 100 + value: status.value.hardware.swap.total > 0 ? status.value.hardware.swap.total - status.value.hardware.swap.used : 100, + name: 'Free' } ] } @@ -209,16 +215,28 @@ function onMountedFunc() { netChart.setOption( { color: netColor, - title: { - text: 'Network', - }, tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { - backgroundColor: '#6a7985' + backgroundColor: '#0f7bc5', + borderRadius: 50, + formatter: function (params: any) { + if (params.axisDimension === 'y') { + return formatSizeByUnit(params.value * 8, null, 'bps'); + } else { + return formatDate(params.value, true); + } + } } + }, + formatter: function (params: any) { + let result = formatDate(params[0].name, true) + '
'; + params.forEach(function (item: any) { + result += item.marker + (item.seriesName == 'Tx' ? '↑' : '↓') + ': ' + formatSizeByUnit(item.value * 8, null, 'bps') + '
'; + }); + return result; } }, toolbox: { @@ -257,24 +275,29 @@ function onMountedFunc() { ], series: [ { - name: 'Up', + name: 'Tx', type: 'line', stack: 'Total', areaStyle: {}, emphasis: { focus: 'series' }, - data: netStats.map(item => item[1]) + data: netStats.map(item => item[1]), + showSymbol: false, + lineStyle: { + type: 'dashed' + } }, { - name: 'Down', + name: 'Rx', type: 'line', stack: 'Total', areaStyle: {}, emphasis: { focus: 'series' }, - data: netStats.map(item => item[2]) + data: netStats.map(item => item[2]), + showSymbol: false } ] } @@ -292,6 +315,21 @@ function onMountedFunc() { } +// link.download = `screenshot-${status.value.meta.id}-${formatDate(Date.now(), false)}.svg`; + +function downloadScreenshot() { + const hostElement = document.querySelector(".host#"+status.value.meta.id); + if (hostElement) { + html2canvas(hostElement, { scale: 2 }).then((canvas) => { + const dataURL = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.href = dataURL; + link.download = `screenshot-${status.value.meta.id}-${formatDate(Date.now(), false)}.png`; + link.click(); + }); + } +} + onMounted( () => { onMountedFunc() @@ -301,25 +339,36 @@ onMounted( @@ -348,12 +407,24 @@ onMounted( \ No newline at end of file diff --git a/src/style.css b/src/style.css index 2fa6c6b..793399b 100644 --- a/src/style.css +++ b/src/style.css @@ -11,6 +11,8 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + --text-color-1: #000; + --text-color-2: #383838; } .dark { diff --git a/src/views/Home.vue b/src/views/Home.vue index c50665f..b2aa849 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -56,7 +56,7 @@ onUnmounted(() => { .grid-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 10px; + gap: 20px; padding: 10px; } \ No newline at end of file