3x-ui/frontend/src/pages/index/XrayStatusCard.vue
MHSanaei 36e75143fa
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>
2026-05-08 17:21:03 +02:00

144 lines
4.2 KiB
Vue

<script setup>
import { useI18n } from 'vue-i18n';
import {
BarsOutlined,
PoweroffOutlined,
ReloadOutlined,
ToolOutlined,
} from '@ant-design/icons-vue';
const { t } = useI18n();
defineProps({
status: { type: Object, required: true },
isMobile: { type: Boolean, default: false },
ipLimitEnable: { type: Boolean, default: false },
});
defineEmits(['stop-xray', 'restart-xray', 'open-xray-logs', 'open-version-switch']);
// Map xray.color → which animation class to apply on the badge dot.
// The legacy .xray-*-animation classes only override the badge ring
// color; the actual pulsing comes from .xray-processing-animation
// (which animates .ant-badge-status-dot via @keyframes runningAnimation).
function badgeAnimationClass(color) {
if (color === 'green') return 'xray-running-animation';
if (color === 'orange') return 'xray-stop-animation';
if (color === 'red') return 'xray-error-animation';
return 'xray-processing-animation';
}
</script>
<template>
<a-card hoverable>
<template #title>
<a-space direction="horizontal">
<span>{{ t('pages.index.xrayStatus') }}</span>
<a-tag v-if="isMobile && status.xray.version && status.xray.version !== 'Unknown'" color="green">
v{{ status.xray.version }}
</a-tag>
</a-space>
</template>
<template #extra>
<template v-if="status.xray.state !== 'error'">
<a-badge status="processing" :class="['xray-processing-animation', badgeAnimationClass(status.xray.color)]"
:text="status.xray.stateMsg" :color="status.xray.color" />
</template>
<template v-else>
<a-popover>
<template #title>
<a-row type="flex" align="middle" justify="space-between">
<a-col><span>{{ t('pages.index.xrayStatusError') }}</span></a-col>
<a-col>
<BarsOutlined class="cursor-pointer" @click="$emit('open-logs')" />
</a-col>
</a-row>
</template>
<template #content>
<span v-for="(line, i) in (status.xray.errorMsg || '').split('\n')" :key="i" class="error-line">
{{ line }}
</span>
</template>
<a-badge status="processing" :text="status.xray.stateMsg" :color="status.xray.color"
:class="['xray-processing-animation', 'xray-error-animation']" />
</a-popover>
</template>
</template>
<template #actions>
<a-space v-if="ipLimitEnable" direction="horizontal" class="action" @click="$emit('open-xray-logs')">
<BarsOutlined />
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
</a-space>
<a-space direction="horizontal" class="action" @click="$emit('stop-xray')">
<PoweroffOutlined />
<span v-if="!isMobile">{{ t('pages.index.stopXray') }}</span>
</a-space>
<a-space direction="horizontal" class="action" @click="$emit('restart-xray')">
<ReloadOutlined />
<span v-if="!isMobile">{{ t('pages.index.restartXray') }}</span>
</a-space>
<a-space direction="horizontal" class="action" @click="$emit('open-version-switch')">
<ToolOutlined />
<span v-if="!isMobile">
{{ status.xray.version && status.xray.version !== 'Unknown'
? `v${status.xray.version}`
: t('pages.index.xraySwitch') }}
</span>
</a-space>
</template>
</a-card>
</template>
<style scoped>
.action {
cursor: pointer;
justify-content: center;
}
.error-line {
display: block;
max-width: 400px;
white-space: pre-wrap;
}
.cursor-pointer {
cursor: pointer;
}
</style>
<style>
/* Legacy xray-*-animation classes — they need to be global so they
* pierce the AD-Vue badge's internal DOM (.ant-badge-status-*). */
.xray-processing-animation .ant-badge-status-dot {
animation: xray-pulse 1.2s linear infinite;
}
.xray-running-animation .ant-badge-status-processing::after {
border-color: var(--color-primary-100, #008771);
}
.xray-stop-animation .ant-badge-status-processing::after {
border-color: #fa8c16;
}
.xray-error-animation .ant-badge-status-processing::after {
border-color: #f5222d;
}
@keyframes xray-pulse {
0%,
50%,
100% {
transform: scale(1);
opacity: 1;
}
10% {
transform: scale(1.5);
opacity: 0.2;
}
}
</style>