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:
MHSanaei 2026-05-08 12:39:38 +02:00
parent c2fd5bc1da
commit c3293bca82
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 199 additions and 8 deletions

View file

@ -1,29 +1,72 @@
<script setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { theme as antdTheme } from 'ant-design-vue';
import { HttpUtil } 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 StatusCard from './StatusCard.vue';
import XrayStatusCard from './XrayStatusCard.vue';
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
const { status, fetched } = useStatus();
const { status, fetched, refresh } = useStatus();
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
// served HTML; during `npm run dev` we infer them from window.location.
const basePath = window.__X_UI_BASE_PATH__ || '';
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() {
// CPU-history modal is part of Phase 5c-iv. Leaving the emit wired
// so the button isn't dead-clickable; no-op until then.
// CPU-history modal is part of Phase 5c-iv. Wired emit so the
// 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>
@ -41,14 +84,26 @@ function onOpenCpuHistory() {
<a-col :span="24">
<StatusCard :status="status" :is-mobile="isMobile" @open-cpu-history="onOpenCpuHistory" />
</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-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.
Phase 5c-iii wires the xray status card on the left.
Panel update modal, logs / xray-logs / backup, and the
custom-geo section arrive in 5c-iv and 5c-v.
</p>
</a-space>
</a-card>

View 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>