mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 17:46:02 +00:00
feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c2fd5bc1da
commit
c3293bca82
2 changed files with 199 additions and 8 deletions
|
|
@ -1,29 +1,72 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { theme as antdTheme } from 'ant-design-vue';
|
import { theme as antdTheme } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { HttpUtil } from '@/utils';
|
||||||
import { theme as themeState } from '@/composables/useTheme.js';
|
import { theme as themeState } from '@/composables/useTheme.js';
|
||||||
import { useStatus } from '@/composables/useStatus.js';
|
import { useStatus } from '@/composables/useStatus.js';
|
||||||
import { useMediaQuery } from '@/composables/useMediaQuery.js';
|
import { useMediaQuery } from '@/composables/useMediaQuery.js';
|
||||||
import AppSidebar from '@/components/AppSidebar.vue';
|
import AppSidebar from '@/components/AppSidebar.vue';
|
||||||
import StatusCard from './StatusCard.vue';
|
import StatusCard from './StatusCard.vue';
|
||||||
|
import XrayStatusCard from './XrayStatusCard.vue';
|
||||||
|
|
||||||
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
|
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
|
||||||
const antdThemeConfig = computed(() => ({
|
const antdThemeConfig = computed(() => ({
|
||||||
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { status, fetched } = useStatus();
|
const { status, fetched, refresh } = useStatus();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
|
|
||||||
|
// `/panel/setting/defaultSettings` returns ipLimitEnable; the xray
|
||||||
|
// card hides its log button when access logs are off.
|
||||||
|
const ipLimitEnable = ref(false);
|
||||||
|
HttpUtil.post('/panel/setting/defaultSettings').then((msg) => {
|
||||||
|
if (msg?.success && msg.obj) ipLimitEnable.value = !!msg.obj.ipLimitEnable;
|
||||||
|
});
|
||||||
|
|
||||||
// In production the Go panel injects basePath + requestUri into the
|
// In production the Go panel injects basePath + requestUri into the
|
||||||
// served HTML; during `npm run dev` we infer them from window.location.
|
// served HTML; during `npm run dev` we infer them from window.location.
|
||||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||||
const requestUri = window.location.pathname;
|
const requestUri = window.location.pathname;
|
||||||
|
|
||||||
|
const busy = ref(false);
|
||||||
|
|
||||||
|
async function stopXray() {
|
||||||
|
busy.value = true;
|
||||||
|
try {
|
||||||
|
await HttpUtil.post('/panel/api/server/stopXrayService');
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
busy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartXray() {
|
||||||
|
busy.value = true;
|
||||||
|
try {
|
||||||
|
await HttpUtil.post('/panel/api/server/restartXrayService');
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
busy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onOpenCpuHistory() {
|
function onOpenCpuHistory() {
|
||||||
// CPU-history modal is part of Phase 5c-iv. Leaving the emit wired
|
// CPU-history modal is part of Phase 5c-iv. Wired emit so the
|
||||||
// so the button isn't dead-clickable; no-op until then.
|
// button isn't dead-clickable; no-op until that phase ships.
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenLogs() {
|
||||||
|
// Panel-logs modal — Phase 5c-iv.
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenXrayLogs() {
|
||||||
|
// Xray-logs modal — Phase 5c-iv.
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenVersionSwitch() {
|
||||||
|
// Xray version-picker modal — Phase 5c-iv.
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -41,14 +84,26 @@ function onOpenCpuHistory() {
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<StatusCard :status="status" :is-mobile="isMobile" @open-cpu-history="onOpenCpuHistory" />
|
<StatusCard :status="status" :is-mobile="isMobile" @open-cpu-history="onOpenCpuHistory" />
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24">
|
<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="onOpenXrayLogs"
|
||||||
|
@open-logs="onOpenLogs"
|
||||||
|
@open-version-switch="onOpenVersionSwitch"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :sm="24" :lg="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable>
|
||||||
<a-space direction="vertical" :size="8" style="width: 100%">
|
<a-space direction="vertical" :size="8" style="width: 100%">
|
||||||
<h3 style="margin: 0">Dashboard scaffold</h3>
|
<h3 style="margin: 0">Dashboard scaffold</h3>
|
||||||
<p style="margin: 0; opacity: 0.7">
|
<p style="margin: 0; opacity: 0.7">
|
||||||
Phase 5c-ii adds the live status cards above (CPU / memory / swap / disk).
|
Phase 5c-iii wires the xray status card on the left.
|
||||||
Xray status, panel update modal, logs, and the custom-geo section
|
Panel update modal, logs / xray-logs / backup, and the
|
||||||
arrive in 5c-iii through 5c-v.
|
custom-geo section arrive in 5c-iv and 5c-v.
|
||||||
</p>
|
</p>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
|
||||||
136
frontend/src/pages/index/XrayStatusCard.vue
Normal file
136
frontend/src/pages/index/XrayStatusCard.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
BarsOutlined,
|
||||||
|
PoweroffOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
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>Xray status</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>Xray error</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">Logs</span>
|
||||||
|
</a-space>
|
||||||
|
<a-space direction="horizontal" class="action" @click="$emit('stop-xray')">
|
||||||
|
<PoweroffOutlined />
|
||||||
|
<span v-if="!isMobile">Stop xray</span>
|
||||||
|
</a-space>
|
||||||
|
<a-space direction="horizontal" class="action" @click="$emit('restart-xray')">
|
||||||
|
<ReloadOutlined />
|
||||||
|
<span v-if="!isMobile">Restart xray</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}`
|
||||||
|
: 'Switch xray' }}
|
||||||
|
</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>
|
||||||
Loading…
Reference in a new issue