mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-03 17:07:15 +00:00
fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,29 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { theme as antdTheme } from 'ant-design-vue';
|
||||
import { BarsOutlined, CloudServerOutlined, CloudDownloadOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
BarsOutlined,
|
||||
ControlOutlined,
|
||||
CloudServerOutlined,
|
||||
CloudDownloadOutlined,
|
||||
CloudUploadOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
GlobalOutlined,
|
||||
SwapOutlined,
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { HttpUtil, SizeFormatter, TimeFormatter } from '@/utils';
|
||||
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 CustomStatistic from '@/components/CustomStatistic.vue';
|
||||
import TextModal from '@/components/TextModal.vue';
|
||||
import StatusCard from './StatusCard.vue';
|
||||
import XrayStatusCard from './XrayStatusCard.vue';
|
||||
import PanelUpdateModal from './PanelUpdateModal.vue';
|
||||
@@ -47,6 +61,16 @@ onMounted(() => {
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
// In production, dist.go injects window.__X_UI_CUR_VER__ at serve time.
|
||||
// In dev, Vite serves the HTML directly so the global is missing — fall
|
||||
// back to currentVersion from the panel-update API once it answers.
|
||||
const displayVersion = computed(
|
||||
() => panelUpdateInfo.value?.currentVersion || window.__X_UI_CUR_VER__ || '?',
|
||||
);
|
||||
|
||||
// Hide/reveal the public IPv4/IPv6 — same pattern as legacy.
|
||||
const showIp = ref(false);
|
||||
|
||||
// Modal open state.
|
||||
const logsOpen = ref(false);
|
||||
const backupOpen = ref(false);
|
||||
@@ -54,6 +78,8 @@ const panelUpdateOpen = ref(false);
|
||||
const cpuHistoryOpen = ref(false);
|
||||
const xrayLogsOpen = ref(false);
|
||||
const versionOpen = ref(false);
|
||||
const configTextOpen = ref(false);
|
||||
const configText = ref('');
|
||||
|
||||
// Page-level loading overlay; modals can request it via @busy.
|
||||
const loading = ref(false);
|
||||
@@ -76,6 +102,20 @@ async function restartXray() {
|
||||
function openCpuHistory() { cpuHistoryOpen.value = true; }
|
||||
function openXrayLogs() { xrayLogsOpen.value = true; }
|
||||
function openVersionSwitch() { versionOpen.value = true; }
|
||||
|
||||
// Legacy "Config" action — fetch the rendered xray config and show
|
||||
// it as JSON in the shared TextModal (same UX as main).
|
||||
async function openConfig() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
||||
if (!msg?.success) return;
|
||||
configText.value = JSON.stringify(msg.obj, null, 2);
|
||||
configTextOpen.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -94,68 +134,191 @@ function openVersionSwitch() { versionOpen.value = true; }
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<XrayStatusCard
|
||||
:status="status"
|
||||
:is-mobile="isMobile"
|
||||
:ip-limit-enable="ipLimitEnable"
|
||||
@stop-xray="stopXray"
|
||||
@restart-xray="restartXray"
|
||||
@open-xray-logs="openXrayLogs"
|
||||
@open-logs="logsOpen = true"
|
||||
@open-version-switch="openVersionSwitch"
|
||||
/>
|
||||
<XrayStatusCard :status="status" :is-mobile="isMobile" :ip-limit-enable="ipLimitEnable"
|
||||
@stop-xray="stopXray" @restart-xray="restartXray" @open-xray-logs="openXrayLogs"
|
||||
@open-logs="logsOpen = true" @open-version-switch="openVersionSwitch" />
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('menu.link')" hoverable>
|
||||
<template v-if="panelUpdateInfo.updateAvailable" #extra>
|
||||
<a-tooltip :title="`${t('pages.index.updatePanel')}: ${panelUpdateInfo.latestVersion}`">
|
||||
<a-tag color="orange" class="update-tag" @click="panelUpdateOpen = true">
|
||||
<CloudDownloadOutlined />
|
||||
{{ panelUpdateInfo.latestVersion }}
|
||||
<span v-if="!isMobile">{{ t('update') }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-space class="action" @click="logsOpen = true">
|
||||
<BarsOutlined />
|
||||
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
|
||||
</a-space>
|
||||
<a-space class="action" @click="openConfig">
|
||||
<ControlOutlined />
|
||||
<span v-if="!isMobile">{{ t('pages.index.config') }}</span>
|
||||
</a-space>
|
||||
<a-space class="action" @click="backupOpen = true">
|
||||
<CloudServerOutlined />
|
||||
<span v-if="!isMobile">{{ t('pages.index.backupTitle') }}</span>
|
||||
</a-space>
|
||||
<a-space class="action" @click="panelUpdateOpen = true">
|
||||
<CloudDownloadOutlined />
|
||||
<span v-if="!isMobile">
|
||||
{{ panelUpdateInfo.updateAvailable
|
||||
? `${t('update')} → ${panelUpdateInfo.latestVersion}`
|
||||
: t('pages.index.panelUpToDate') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card title="3X-UI" hoverable>
|
||||
<template v-if="panelUpdateInfo.updateAvailable" #extra>
|
||||
<a-tooltip :title="`${t('pages.index.updatePanel')}: ${panelUpdateInfo.latestVersion}`">
|
||||
<a-tag color="orange" class="update-tag" @click="panelUpdateOpen = true">
|
||||
<CloudDownloadOutlined />
|
||||
{{ panelUpdateInfo.latestVersion }}
|
||||
<span v-if="!isMobile">{{ t('pages.index.updatePanel') }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank" rel="noopener noreferrer">
|
||||
<a-tag color="green">v{{ displayVersion }}</a-tag>
|
||||
</a>
|
||||
<a href="https://t.me/XrayUI" target="_blank" rel="noopener noreferrer">
|
||||
<a-tag color="green">@XrayUI</a-tag>
|
||||
</a>
|
||||
<a href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank" rel="noopener noreferrer">
|
||||
<a-tag color="purple">{{ t('pages.index.documentation') }}</a-tag>
|
||||
</a>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('pages.index.operationHours')" hoverable>
|
||||
<a-tag :color="status.xray.color">
|
||||
Xray: {{ TimeFormatter.formatSecond(status.appStats.uptime) }}
|
||||
</a-tag>
|
||||
<a-tag color="green">OS: {{ TimeFormatter.formatSecond(status.uptime) }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('pages.index.systemLoad')" hoverable>
|
||||
<a-tooltip :title="t('pages.index.systemLoadDesc')">
|
||||
<a-tag color="green">
|
||||
{{ status.loads[0] }} | {{ status.loads[1] }} | {{ status.loads[2] }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('usage')" hoverable>
|
||||
<a-tag color="green">
|
||||
{{ t('pages.index.memory') }}: {{ SizeFormatter.sizeFormat(status.appStats.mem) }}
|
||||
</a-tag>
|
||||
<a-tag color="green">
|
||||
{{ t('pages.index.threads') }}: {{ status.appStats.threads }}
|
||||
</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('pages.index.overallSpeed')" hoverable>
|
||||
<a-row :gutter="isMobile ? [8, 8] : 0">
|
||||
<a-col :span="12">
|
||||
<CustomStatistic :title="t('pages.index.upload')"
|
||||
:value="SizeFormatter.sizeFormat(status.netIO.up)">
|
||||
<template #prefix>
|
||||
<ArrowUpOutlined />
|
||||
</template>
|
||||
<template #suffix>/s</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<CustomStatistic :title="t('pages.index.download')"
|
||||
:value="SizeFormatter.sizeFormat(status.netIO.down)">
|
||||
<template #prefix>
|
||||
<ArrowDownOutlined />
|
||||
</template>
|
||||
<template #suffix>/s</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('pages.index.totalData')" hoverable>
|
||||
<a-row :gutter="isMobile ? [8, 8] : 0">
|
||||
<a-col :span="12">
|
||||
<CustomStatistic :title="t('pages.index.sent')"
|
||||
:value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
|
||||
<template #prefix>
|
||||
<CloudUploadOutlined />
|
||||
</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<CustomStatistic :title="t('pages.index.received')"
|
||||
:value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
|
||||
<template #prefix>
|
||||
<CloudDownloadOutlined />
|
||||
</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('pages.index.ipAddresses')" hoverable>
|
||||
<template #extra>
|
||||
<a-tooltip :title="t('pages.index.toggleIpVisibility')" :placement="isMobile ? 'topRight' : 'top'">
|
||||
<component :is="showIp ? EyeOutlined : EyeInvisibleOutlined" class="ip-toggle-icon"
|
||||
@click="showIp = !showIp" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8, 8] : 0">
|
||||
<a-col :span="isMobile ? 24 : 12">
|
||||
<CustomStatistic title="IPv4" :value="status.publicIP.ipv4">
|
||||
<template #prefix>
|
||||
<GlobalOutlined />
|
||||
</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
<a-col :span="isMobile ? 24 : 12">
|
||||
<CustomStatistic title="IPv6" :value="status.publicIP.ipv6">
|
||||
<template #prefix>
|
||||
<GlobalOutlined />
|
||||
</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card :title="t('pages.index.connectionCount')" hoverable>
|
||||
<a-row :gutter="isMobile ? [8, 8] : 0">
|
||||
<a-col :span="12">
|
||||
<CustomStatistic title="TCP" :value="status.tcpCount">
|
||||
<template #prefix>
|
||||
<SwapOutlined />
|
||||
</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<CustomStatistic title="UDP" :value="status.udpCount">
|
||||
<template #prefix>
|
||||
<SwapOutlined />
|
||||
</template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<PanelUpdateModal
|
||||
v-model:open="panelUpdateOpen"
|
||||
:info="panelUpdateInfo"
|
||||
@busy="setBusy"
|
||||
/>
|
||||
<PanelUpdateModal v-model:open="panelUpdateOpen" :info="panelUpdateInfo" @busy="setBusy" />
|
||||
<LogModal v-model:open="logsOpen" />
|
||||
<BackupModal
|
||||
v-model:open="backupOpen"
|
||||
:base-path="basePath"
|
||||
@busy="setBusy"
|
||||
/>
|
||||
<BackupModal v-model:open="backupOpen" :base-path="basePath" @busy="setBusy" />
|
||||
<CpuHistoryModal v-model:open="cpuHistoryOpen" :status="status" />
|
||||
<XrayLogModal v-model:open="xrayLogsOpen" />
|
||||
<VersionModal v-model:open="versionOpen" :status="status" @busy="setBusy" />
|
||||
<TextModal v-model:open="configTextOpen" :title="t('pages.index.config')" :content="configText"
|
||||
file-name="config.json" />
|
||||
</a-layout>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
@@ -184,8 +347,13 @@ function openVersionSwitch() { versionOpen.value = true; }
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-shell { background: transparent; }
|
||||
.content-area { padding: 24px; }
|
||||
.content-shell {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.loading-spacer {
|
||||
min-height: calc(100vh - 120px);
|
||||
@@ -203,4 +371,18 @@ function openVersionSwitch() { versionOpen.value = true; }
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ip-toggle-icon {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ip-hidden :deep(.ant-statistic-content-value) {
|
||||
filter: blur(6px);
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.ip-visible :deep(.ant-statistic-content-value) {
|
||||
filter: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user