feat(frontend): Phase 5c-ii — live status cards on the dashboard

Adds the CPU / memory / swap / disk dashboard cards to IndexPage,
backed by a useStatus() composable that polls /panel/api/server/status
every 2 s and a Status / CurTotal model ported from the legacy inline
classes in index.html.

- models/status.js — Status & CurTotal classes (CurTotal exposes
  reactive .percent and .color computed-style getters; Status maps
  the API payload + xray state to color/message strings)
- composables/useStatus.js — 2s polling with shallowRef so each fetch
  swaps the whole Status object atomically. WebSocket integration
  intentionally deferred — the legacy panel falls back to this same
  2s polling when its websocket drops, so we ship the proven path
  first and add WS on top in a later sub-phase.
- pages/index/StatusCard.vue — four a-progress dashboard widgets in
  a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a
  history button; the modal it opens is part of 5c-iv.
- IndexPage now consumes both, plus useMediaQuery so the layout
  responds to viewport changes.

AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in
favor of explicit AreaChartOutlined / HistoryOutlined imports.
<a-tooltip slot="title"> → <template #title>.

i18n strings still hardcoded English (Phase 7 wires up vue-i18n).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-08 12:31:55 +02:00
parent e24e70dde2
commit c2fd5bc1da
4 changed files with 252 additions and 28 deletions
+76
View File
@@ -0,0 +1,76 @@
import { NumberFormatter } from '@/utils';
export class CurTotal {
constructor(current, total) {
this.current = current;
this.total = total;
}
get percent() {
if (this.total === 0) return 0;
return NumberFormatter.toFixed((this.current / this.total) * 100, 2);
}
get color() {
const p = this.percent;
if (p < 80) return '#008771'; // green
if (p < 90) return '#f37b24'; // orange
return '#cf3c3c'; // red
}
}
const XRAY_STATE_COLORS = {
running: 'green',
stop: 'orange',
error: 'red',
};
const XRAY_STATE_MESSAGES = {
running: 'Xray is running',
stop: 'Xray is stopped',
error: 'Xray error',
};
export class Status {
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.logicalPro = 0;
this.cpuSpeedMhz = 0;
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
this.netIO = { up: 0, down: 0 };
this.netTraffic = { sent: 0, recv: 0 };
this.publicIP = { ipv4: 0, ipv6: 0 };
this.swap = new CurTotal(0, 0);
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.appUptime = 0;
this.appStats = { threads: 0, mem: 0, uptime: 0 };
this.xray = { state: 'stop', stateMsg: '', errorMsg: '', version: '', color: '' };
if (data == null) return;
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.logicalPro = data.logicalPro;
this.cpuSpeedMhz = data.cpuSpeedMhz;
this.disk = new CurTotal(data.disk?.current ?? 0, data.disk?.total ?? 0);
this.loads = (data.loads || [0, 0, 0]).map((v) => NumberFormatter.toFixed(v, 2));
this.mem = new CurTotal(data.mem?.current ?? 0, data.mem?.total ?? 0);
this.netIO = data.netIO ?? this.netIO;
this.netTraffic = data.netTraffic ?? this.netTraffic;
this.publicIP = data.publicIP ?? this.publicIP;
this.swap = new CurTotal(data.swap?.current ?? 0, data.swap?.total ?? 0);
this.tcpCount = data.tcpCount ?? 0;
this.udpCount = data.udpCount ?? 0;
this.uptime = data.uptime ?? 0;
this.appUptime = data.appUptime ?? 0;
this.appStats = data.appStats ?? this.appStats;
this.xray = { ...this.xray, ...(data.xray || {}) };
this.xray.color = XRAY_STATE_COLORS[this.xray.state] ?? 'gray';
this.xray.stateMsg = XRAY_STATE_MESSAGES[this.xray.state] ?? 'Unknown';
}
}