feat: 服务器资源趋势图表(Chart.js本地化+CPU/内存/磁盘趋势)
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
<script src="/static/js/tailwind.js"></script>
|
<script src="/static/js/tailwind.js"></script>
|
||||||
<script>tailwind.config = { darkMode: 'class' };</script>
|
<script>tailwind.config = { darkMode: 'class' };</script>
|
||||||
<script src="/static/js/vue.global.prod.js"></script>
|
<script src="/static/js/vue.global.prod.js"></script>
|
||||||
|
<script src="/static/js/chart.umd.js"></script>
|
||||||
<script>window.APP_CONFIG = { appName: "{{ app_name }}" };</script>
|
<script>window.APP_CONFIG = { appName: "{{ app_name }}" };</script>
|
||||||
<style>
|
<style>
|
||||||
html { -webkit-tap-highlight-color: transparent; }
|
html { -webkit-tap-highlight-color: transparent; }
|
||||||
|
|||||||
+34
-2
@@ -1,5 +1,5 @@
|
|||||||
/* vps-manager 前端应用(Vue 3 组件化) */
|
/* vps-manager 前端应用(Vue 3 组件化) */
|
||||||
const { createApp, reactive, ref, computed, onMounted, watch } = Vue;
|
const { createApp, reactive, ref, computed, onMounted, watch, nextTick } = Vue;
|
||||||
const Api = window.VpsApi;
|
const Api = window.VpsApi;
|
||||||
const Fmt = window.VpsFmt;
|
const Fmt = window.VpsFmt;
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ async function viewServer(a) {
|
|||||||
store.view = 'servers';
|
store.view = 'servers';
|
||||||
try {
|
try {
|
||||||
const [metrics, info, checks] = await Promise.all([
|
const [metrics, info, checks] = await Promise.all([
|
||||||
Api.get('/monitor/' + a.id + '/metrics'),
|
Api.get('/monitor/' + a.id + '/metrics?limit=200'),
|
||||||
Api.get('/monitor/' + a.id + '/info'),
|
Api.get('/monitor/' + a.id + '/info'),
|
||||||
Api.get('/monitor/' + a.id + '/security'),
|
Api.get('/monitor/' + a.id + '/security'),
|
||||||
]);
|
]);
|
||||||
@@ -494,6 +494,10 @@ const ServersView = {
|
|||||||
<div>公网 IP:{{ m.info.public_ip || '—' }}</div><div>状态:{{ m.info.status }}</div>
|
<div>公网 IP:{{ m.info.public_ip || '—' }}</div><div>状态:{{ m.info.status }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="m.metrics && m.metrics.length > 1" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
||||||
|
<h3 class="font-semibold mb-2 text-sm">资源趋势(最近 {{ m.metrics.length }} 次采样)</h3>
|
||||||
|
<canvas id="metricsChart" height="90"></canvas>
|
||||||
|
</div>
|
||||||
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
||||||
<h3 class="font-semibold mb-2 text-sm">安全检查</h3>
|
<h3 class="font-semibold mb-2 text-sm">安全检查</h3>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -513,6 +517,34 @@ const ServersView = {
|
|||||||
setup() {
|
setup() {
|
||||||
const m = computed(() => store.serverMetrics);
|
const m = computed(() => store.serverMetrics);
|
||||||
const latest = computed(() => (m.value.metrics && m.value.metrics.length) ? m.value.metrics[0] : null);
|
const latest = computed(() => (m.value.metrics && m.value.metrics.length) ? m.value.metrics[0] : null);
|
||||||
|
let chartInstance = null;
|
||||||
|
function renderChart() {
|
||||||
|
const canvas = document.getElementById('metricsChart');
|
||||||
|
if (!canvas || !window.Chart) return;
|
||||||
|
if (!m.value.metrics || m.value.metrics.length < 2) return;
|
||||||
|
if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
|
||||||
|
const data = [...m.value.metrics].reverse();
|
||||||
|
const labels = data.map(p => new Date(p.ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }));
|
||||||
|
chartInstance = new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{ label: 'CPU %', data: data.map(p => p.cpu_pct), borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.1)', tension: 0.3, pointRadius: 0 },
|
||||||
|
{ label: '内存 %', data: data.map(p => p.mem_pct), borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,0.1)', tension: 0.3, pointRadius: 0 },
|
||||||
|
{ label: '磁盘 %', data: data.map(p => p.disk_pct), borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.1)', tension: 0.3, pointRadius: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
interaction: { mode: 'index', intersect: false },
|
||||||
|
scales: { y: { min: 0, max: 100, ticks: { callback: v => v + '%' } } },
|
||||||
|
plugins: { legend: { position: 'top' } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
watch(m, () => { nextTick(renderChart); }, { deep: true });
|
||||||
|
onMounted(() => { nextTick(renderChart); });
|
||||||
return { store, Fmt, m, latest };
|
return { store, Fmt, m, latest };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user