mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-14 23:31:00 +00:00
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:
@@ -1,26 +1,30 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { theme as antdTheme } from 'ant-design-vue';
|
||||
|
||||
import { theme as themeState } from '@/composables/useTheme.js';
|
||||
import { useStatus } from '@/composables/useStatus.js';
|
||||
import { useMediaQuery } from '@/composables/useMediaQuery.js';
|
||||
import AppSidebar from '@/components/AppSidebar.vue';
|
||||
import StatusCard from './StatusCard.vue';
|
||||
|
||||
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
|
||||
const antdThemeConfig = computed(() => ({
|
||||
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||
}));
|
||||
|
||||
// Phase 5c-i ships the page shell only — sidebar, layout, theme.
|
||||
// Real content (CPU/mem/swap/disk cards, Xray status card, panel
|
||||
// update modal, logs, custom-geo section) follows in 5c-ii through
|
||||
// 5c-iv. Loading state is currently a placeholder true so the shell
|
||||
// renders; it will be wired to the real /server/status fetch later.
|
||||
const fetched = ref(true);
|
||||
const { status, fetched } = useStatus();
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
// In production the Go panel injects basePath + requestUri into the
|
||||
// served HTML; during `npm run dev` we infer them from window.location.
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
function onOpenCpuHistory() {
|
||||
// CPU-history modal is part of Phase 5c-iv. Leaving the emit wired
|
||||
// so the button isn't dead-clickable; no-op until then.
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -33,18 +37,23 @@ const requestUri = window.location.pathname;
|
||||
<a-spin :spinning="!fetched" :delay="200" size="large">
|
||||
<div v-if="!fetched" class="loading-spacer" />
|
||||
|
||||
<div v-else class="page-body">
|
||||
<a-card hoverable>
|
||||
<a-space direction="vertical" :size="12" style="width: 100%">
|
||||
<h2 style="margin: 0">Dashboard (vue3-migration shell)</h2>
|
||||
<p style="margin: 0; opacity: 0.7">
|
||||
Phase 5c-i: layout, sidebar, and theme switching wired up.
|
||||
Status cards, xray controls, and custom-geo arrive in
|
||||
follow-up commits.
|
||||
</p>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</div>
|
||||
<a-row v-else :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]">
|
||||
<a-col :span="24">
|
||||
<StatusCard :status="status" :is-mobile="isMobile" @open-cpu-history="onOpenCpuHistory" />
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-card hoverable>
|
||||
<a-space direction="vertical" :size="8" style="width: 100%">
|
||||
<h3 style="margin: 0">Dashboard scaffold</h3>
|
||||
<p style="margin: 0; opacity: 0.7">
|
||||
Phase 5c-ii adds the live status cards above (CPU / memory / swap / disk).
|
||||
Xray status, panel update modal, logs, and the custom-geo section
|
||||
arrive in 5c-iii through 5c-v.
|
||||
</p>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
@@ -54,7 +63,6 @@ const requestUri = window.location.pathname;
|
||||
|
||||
<style scoped>
|
||||
.index-page {
|
||||
/* Same legacy palette source as the login page. */
|
||||
--bg-page: #f0f2f5;
|
||||
--bg-card: #ffffff;
|
||||
|
||||
@@ -63,13 +71,13 @@ const requestUri = window.location.pathname;
|
||||
}
|
||||
|
||||
.index-page.is-dark {
|
||||
--bg-page: #0a1222; /* legacy --dark-color-background */
|
||||
--bg-card: #151f31; /* legacy --dark-color-surface-100 */
|
||||
--bg-page: #0a1222;
|
||||
--bg-card: #151f31;
|
||||
}
|
||||
|
||||
.index-page.is-dark.is-ultra {
|
||||
--bg-page: #21242a; /* legacy ultra --dark-color-background */
|
||||
--bg-card: #0c0e12; /* legacy ultra surface-100 */
|
||||
--bg-page: #21242a;
|
||||
--bg-card: #0c0e12;
|
||||
}
|
||||
|
||||
.index-page :deep(.ant-layout),
|
||||
@@ -88,8 +96,4 @@ const requestUri = window.location.pathname;
|
||||
.loading-spacer {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.page-body :deep(.ant-card) {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
import { AreaChartOutlined, HistoryOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { CPUFormatter, SizeFormatter } from '@/utils';
|
||||
|
||||
defineProps({
|
||||
status: { type: Object, required: true },
|
||||
isMobile: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
defineEmits(['open-cpu-history']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card hoverable>
|
||||
<a-row :gutter="[0, isMobile ? 16 : 0]">
|
||||
<!-- CPU + Memory -->
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress
|
||||
type="dashboard"
|
||||
status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:percent="status.cpu.percent"
|
||||
/>
|
||||
<div>
|
||||
<b>CPU:</b> {{ CPUFormatter.cpuCoreFormat(status.cpuCores) }}
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<div><b>Logical processors:</b> {{ status.logicalPro }}</div>
|
||||
<div><b>Frequency:</b> {{ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) }}</div>
|
||||
</template>
|
||||
<AreaChartOutlined />
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>CPU history</template>
|
||||
<a-button size="small" shape="circle" class="ml-8" @click="$emit('open-cpu-history')">
|
||||
<template #icon><HistoryOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress
|
||||
type="dashboard"
|
||||
status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:percent="status.mem.percent"
|
||||
/>
|
||||
<div>
|
||||
<b>Memory:</b> {{ SizeFormatter.sizeFormat(status.mem.current) }} /
|
||||
{{ SizeFormatter.sizeFormat(status.mem.total) }}
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
|
||||
<!-- Swap + Disk -->
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress
|
||||
type="dashboard"
|
||||
status="normal"
|
||||
:stroke-color="status.swap.color"
|
||||
:percent="status.swap.percent"
|
||||
/>
|
||||
<div>
|
||||
<b>Swap:</b> {{ SizeFormatter.sizeFormat(status.swap.current) }} /
|
||||
{{ SizeFormatter.sizeFormat(status.swap.total) }}
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress
|
||||
type="dashboard"
|
||||
status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:percent="status.disk.percent"
|
||||
/>
|
||||
<div>
|
||||
<b>Storage:</b> {{ SizeFormatter.sizeFormat(status.disk.current) }} /
|
||||
{{ SizeFormatter.sizeFormat(status.disk.total) }}
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.ml-8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user