mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +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>
|
||||
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>
|
||||
|
|
|
|||
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