Files
mengyastore/mengyastore-frontend/src/modules/admin/components/AdminSystemStatusPanel.vue
2026-04-03 21:18:28 +08:00

255 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="flex items-center justify-between mb-4">
<p class="text-[13px] text-muted">后端 APIMySQLRedisRabbitMQ 探活与配置摘要需管理 Token</p>
<button
class="ghost inline-flex items-center gap-1.5 text-sm px-3.5 py-1.5"
:class="loading ? 'opacity-50 pointer-events-none' : ''"
type="button"
@click="load"
>
<svg
width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
:class="loading ? 'animate-spin' : ''"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
<path d="M8 16H3v5"/>
</svg>
{{ loading ? '检测中…' : '刷新' }}
</button>
</div>
<div
v-if="error"
class="mb-4 px-4 py-3 rounded-lg text-sm text-[#c95a6a] bg-[rgba(201,90,106,0.08)] border border-[rgba(201,90,106,0.2)]"
>
{{ error }}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- 后端 API -->
<div class="px-[18px] py-4 bg-white/50 border border-white/35 rounded-xl max-md:px-3">
<div class="flex items-center justify-between flex-wrap gap-2 mb-3">
<div class="flex items-center gap-2.5">
<svg class="text-accent opacity-80" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
<span class="text-[15px] font-semibold text-apptext">后端 API</span>
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" :class="dotClass(be?.status, loading)" :title="labelText(be?.status, loading)" />
<span class="text-[13px] font-medium" :class="textClass(be?.status)">{{ labelText(be?.status, loading) }}</span>
</div>
</div>
<div class="border-t border-white/35 mb-3" />
<template v-if="be">
<div class="grid gap-2 text-[13px]">
<div class="flex justify-between gap-2"><span class="text-muted">应用环境</span><span class="font-medium text-apptext text-right">{{ be.appEnv === 'production' ? '生产' : '开发' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">Gin 模式</span><span class="font-medium text-apptext text-right">{{ be.ginMode || '—' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">当前请求 Host</span><span class="font-medium text-apptext text-right break-all">{{ be.requestHost || '—' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">进程监听</span><span class="font-medium text-apptext text-right break-all">{{ be.listenAddr || '—' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">对外基址</span><span class="font-medium text-apptext text-right break-all">{{ be.publicBaseUrl || '—' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">运行时长</span><span class="font-medium text-apptext text-right">{{ formatUptime(be.uptimeSeconds) }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">认证 API</span><span class="font-medium text-apptext text-right">{{ be.authApiConfigured ? '已配置' : '未配置' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">RabbitMQ 功能</span><span class="font-medium text-apptext text-right">{{ be.rabbitmqEnabled ? '开' : '关' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">Redis 功能</span><span class="font-medium text-apptext text-right">{{ be.redisEnabled ? '开' : '关' }}</span></div>
</div>
</template>
</div>
<!-- MySQL -->
<div class="px-[18px] py-4 bg-white/50 border border-white/35 rounded-xl max-md:px-3">
<div class="flex items-center justify-between flex-wrap gap-2 mb-3">
<div class="flex items-center gap-2.5">
<svg class="text-accent opacity-80" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<ellipse cx="12" cy="5" rx="9" ry="3"/>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
</svg>
<span class="text-[15px] font-semibold text-apptext">MySQL</span>
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" :class="dotClass(mysql?.status, loading)" :title="labelText(mysql?.status, loading)" />
<span class="text-[13px] font-medium" :class="textClass(mysql?.status)">{{ labelText(mysql?.status, loading) }}</span>
</div>
</div>
<div class="border-t border-white/35 mb-3" />
<template v-if="mysql">
<div class="grid gap-2 text-[13px]">
<div v-if="mysql.host" class="flex justify-between gap-2"><span class="text-muted">地址</span><span class="font-medium text-apptext text-right break-all">{{ mysql.host }}{{ mysql.port ? ':' + mysql.port : '' }}</span></div>
<div v-if="mysql.socket" class="flex justify-between gap-2"><span class="text-muted">Socket</span><span class="font-medium text-apptext text-right break-all">{{ mysql.socket }}</span></div>
<div v-if="mysql.database" class="flex justify-between gap-2"><span class="text-muted">库名</span><span class="font-medium text-apptext text-right break-all">{{ mysql.database }}</span></div>
<div v-if="mysql.user" class="flex justify-between gap-2"><span class="text-muted">用户</span><span class="font-medium text-apptext text-right">{{ mysql.user }}</span></div>
<div v-if="mysql.status === 'ok' && mysql.version" class="flex justify-between gap-2"><span class="text-muted">版本</span><span class="font-medium text-apptext text-right break-all">{{ mysql.version }}</span></div>
<div v-if="mysql.status === 'ok'" class="flex justify-between gap-2"><span class="text-muted">连接池</span><span class="font-medium text-apptext text-right"> {{ mysql.openConnections }} / {{ mysql.inUse }} / {{ mysql.idle }}</span></div>
</div>
<p v-if="mysql.message" class="mt-3 text-[12px] text-[#c95a6a] leading-relaxed break-all">{{ mysql.message }}</p>
<p v-if="mysql.dsnParseError" class="mt-2 text-[12px] text-[#b8860b] break-all">DSN 解析{{ mysql.dsnParseError }}</p>
</template>
</div>
<!-- Redis -->
<div class="px-[18px] py-4 bg-white/50 border border-white/35 rounded-xl max-md:px-3">
<div class="flex items-center justify-between flex-wrap gap-2 mb-3">
<div class="flex items-center gap-2.5">
<svg class="text-accent opacity-80" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20"/>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
<span class="text-[15px] font-semibold text-apptext">Redis</span>
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" :class="dotClass(rd?.status, loading)" :title="labelText(rd?.status, loading)" />
<span class="text-[13px] font-medium" :class="textClass(rd?.status)">{{ labelText(rd?.status, loading) }}</span>
</div>
</div>
<div class="border-t border-white/35 mb-3" />
<template v-if="rd">
<div class="grid gap-2 text-[13px]">
<div class="flex justify-between gap-2"><span class="text-muted">功能开关</span><span class="font-medium text-apptext text-right">{{ rd.enabled ? '已启用' : '未启用' }}</span></div>
<div v-if="rd.enabled" class="flex justify-between gap-2"><span class="text-muted">逻辑环境</span><span class="font-medium text-apptext text-right">{{ rd.env === 'prod' ? '生产 (prod)' : '开发 (dev)' }}</span></div>
<div v-if="rd.host" class="flex justify-between gap-2"><span class="text-muted">地址</span><span class="font-medium text-apptext text-right break-all">{{ rd.host }}:{{ rd.port || '6379' }}</span></div>
<div v-if="rd.enabled" class="flex justify-between gap-2"><span class="text-muted">DB 编号</span><span class="font-medium text-apptext text-right">{{ rd.dbIndex ?? '' }}</span></div>
<div v-if="rd.status === 'ok' && rd.redisVersion" class="flex justify-between gap-2"><span class="text-muted">版本</span><span class="font-medium text-apptext text-right">{{ rd.redisVersion }}</span></div>
<div v-if="rd.status === 'ok' && rd.usedMemoryHuman" class="flex justify-between gap-2"><span class="text-muted">占用内存</span><span class="font-medium text-apptext text-right">{{ rd.usedMemoryHuman }}</span></div>
<div v-if="rd.status === 'ok'" class="flex justify-between gap-2"><span class="text-muted">Key 约数</span><span class="font-medium text-apptext text-right">{{ rd.keysApprox ?? '' }}</span></div>
</div>
<p v-if="rd.message" class="mt-3 text-[12px] text-[#c95a6a] leading-relaxed break-all">{{ rd.message }}</p>
</template>
</div>
<!-- RabbitMQ -->
<div class="px-[18px] py-4 bg-white/50 border border-white/35 rounded-xl max-md:px-3">
<div class="flex items-center justify-between flex-wrap gap-2 mb-3">
<div class="flex items-center gap-2.5">
<svg class="text-accent opacity-80" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 12h16"/>
<path d="M4 8h12"/>
<path d="M4 16h8"/>
<circle cx="18" cy="8" r="2"/>
<circle cx="14" cy="16" r="2"/>
</svg>
<span class="text-[15px] font-semibold text-apptext">RabbitMQ</span>
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" :class="dotClass(rb?.status, loading)" :title="labelText(rb?.status, loading)" />
<span class="text-[13px] font-medium" :class="textClass(rb?.status)">{{ labelText(rb?.status, loading) }}</span>
</div>
</div>
<div class="border-t border-white/35 mb-3" />
<template v-if="rb">
<div class="grid gap-2 text-[13px]">
<div class="flex justify-between gap-2"><span class="text-muted">功能开关</span><span class="font-medium text-apptext text-right">{{ rb.enabled ? '已启用' : '未启用' }}</span></div>
<div v-if="rb.enabled" class="flex justify-between gap-2"><span class="text-muted">逻辑环境</span><span class="font-medium text-apptext text-right">{{ rb.env === 'prod' ? '生产 (prod)' : '开发 (dev)' }}</span></div>
<div v-if="rb.brokerHost" class="flex justify-between gap-2"><span class="text-muted">Broker 地址</span><span class="font-medium text-apptext text-right break-all">{{ rb.brokerHost }}:{{ rb.brokerPort }}</span></div>
<div v-if="rb.vhost != null && rb.vhost !== ''" class="flex justify-between gap-2"><span class="text-muted">vhost</span><span class="font-medium text-apptext text-right break-all">{{ rb.vhost }}</span></div>
<div v-if="rb.brokerUser" class="flex justify-between gap-2"><span class="text-muted">连接用户</span><span class="font-medium text-apptext text-right">{{ rb.brokerUser }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">交换机</span><span class="font-medium text-apptext text-right break-all">{{ rb.exchange || '—' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">队列</span><span class="font-medium text-apptext text-right break-all">{{ rb.queue || '—' }}</span></div>
<div class="flex justify-between gap-2"><span class="text-muted">路由键</span><span class="font-medium text-apptext text-right break-all">{{ rb.routingKey || '—' }}</span></div>
<div v-if="rb.status === 'ok'" class="flex justify-between gap-2"><span class="text-muted">待投递消息数</span><span class="font-medium text-apptext text-right">{{ rb.messagesReady ?? '' }}</span></div>
<div v-if="rb.status === 'ok'" class="flex justify-between gap-2"><span class="text-muted">消费者数</span><span class="font-medium text-apptext text-right">{{ rb.consumers ?? '' }}</span></div>
</div>
<p v-if="rb.message" class="mt-3 text-[12px] text-[#c95a6a] leading-relaxed break-all">{{ rb.message }}</p>
</template>
</div>
</div>
<p v-if="updatedAt" class="mt-3 text-[12px] text-muted text-right">最后检测{{ updatedAt }}</p>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { fetchSystemStatus } from '../../shared/api'
const props = defineProps({
adminToken: { type: String, default: '' }
})
const data = ref(null)
const loading = ref(false)
const error = ref('')
const updatedAt = ref('')
const be = computed(() => data.value?.backend || null)
const mysql = computed(() => data.value?.mysql || null)
const rd = computed(() => data.value?.redis || null)
const rb = computed(() => data.value?.rabbitmq || null)
function formatUptime(sec) {
if (sec == null || sec < 0) return '—'
const s = Math.floor(sec)
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
const r = s % 60
if (h > 0) return `${h} 小时 ${m}`
if (m > 0) return `${m}${r}`
return `${r}`
}
function labelText(status, isLoading) {
if (status == null && isLoading) return '检测中…'
if (status == null) return '未加载'
if (status === 'ok') return '正常'
if (status === 'disabled') return '未启用'
if (status === 'misconfigured') return '配置不全'
if (status === 'degraded') return '部分可用'
if (status === 'error') return '异常'
return '未知'
}
function dotClass(status, isLoading) {
if (status === 'ok') return 'bg-[#3a9a68]'
if (status === 'disabled') return 'bg-[#c0bcc4]'
if (status === 'degraded' || status === 'misconfigured') return 'bg-[#c9a227]'
if (status == null && isLoading) return 'bg-[#b49acb] animate-pulse'
return 'bg-[#c95a6a]'
}
function textClass(status) {
if (status === 'ok') return 'text-[#3a9a68]'
if (status === 'disabled') return 'text-muted'
if (status === 'degraded' || status === 'misconfigured') return 'text-[#b8860b]'
return 'text-[#c95a6a]'
}
const load = async () => {
if (!props.adminToken) {
error.value = '请先输入管理 Token'
return
}
loading.value = true
error.value = ''
try {
data.value = await fetchSystemStatus(props.adminToken)
updatedAt.value = new Date().toLocaleTimeString('zh-CN')
} catch (e) {
error.value = e?.response?.status === 401
? 'Token 无效,无权查看'
: '获取失败:' + (e?.message || '网络错误')
} finally {
loading.value = false
}
}
onMounted(() => {
if (props.adminToken) load()
})
watch(
() => props.adminToken,
(v) => {
if (v) load()
}
)
defineExpose({ load })
</script>