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 @@
+
+
+
+
+
+
+
+ {{props.mountpoint}}
+
+
+ {{ format2Size(props.used, props.total) }} [{{ props.fstype }}]
+
+
+
+
+
+
\ 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(
-