feat: fulfillment modes, MQ, admin status, docs; scrub compose secrets
Made-with: Cursor
This commit is contained in:
@@ -42,19 +42,45 @@
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<label>商品库存</label>
|
||||
<span class="tag">共 {{ form.inventoryItems.length }} 条</span>
|
||||
<button class="ghost px-3.5 py-1.5 text-[13px] rounded-full" type="button" @click="addInventoryItem">添加商品库存</button>
|
||||
<label>发货方式</label>
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-white/35 bg-white/70 p-3">
|
||||
<label class="flex items-start gap-2.5 cursor-pointer font-semibold text-apptext m-0">
|
||||
<input v-model="form.fulfillmentType" class="mt-1 w-auto accent-accent" type="radio" value="card" />
|
||||
<span>
|
||||
卡密 / 逐条库存
|
||||
<span class="block text-[13px] font-normal text-muted">每条库存单独添加,下单按条扣减;售完不可再买。</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-start gap-2.5 cursor-pointer font-semibold text-apptext m-0">
|
||||
<input v-model="form.fulfillmentType" class="mt-1 w-auto accent-accent" type="radio" value="fixed" />
|
||||
<span>
|
||||
固定内容(不限库存)
|
||||
<span class="block text-[13px] font-normal text-muted">同一篇内容反复发货,如网盘链接、统一说明;无需维护条数。</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="tag">每个输入框保存一条可发放内容,购买后会直接展示给用户。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="form.fulfillmentType === 'fixed'" class="form-field">
|
||||
<label>固定发货内容</label>
|
||||
<textarea v-model="form.fixedContent" rows="5" placeholder="例如:https://… 网盘链接,或一段固定发给买家的文字"></textarea>
|
||||
<p class="tag">保存后顾客每笔订单会收到这份内容(若一次买多件,会复制多行相同内容便于核对)。</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="form-field">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<label>卡密 / 库存条目</label>
|
||||
<span class="tag">共 {{ form.inventoryItems.length }} 条</span>
|
||||
<button class="ghost px-3.5 py-1.5 text-[13px] rounded-full" type="button" @click="addInventoryItem">添加一行</button>
|
||||
</div>
|
||||
<p class="tag">每条一行,手动逐个添加;购买后按顺序发放并扣减库存。</p>
|
||||
<div class="flex flex-col gap-2 mt-1">
|
||||
<div v-for="(_, index) in form.inventoryItems" :key="index" class="flex items-center gap-2 max-md:flex-col max-md:items-stretch">
|
||||
<input
|
||||
:ref="(el) => setInventoryInputRef(el, index)"
|
||||
v-model="form.inventoryItems[index]"
|
||||
class="flex-1"
|
||||
placeholder="库存内容,可填卡密、下载链接等"
|
||||
placeholder="单条卡密或可发放内容"
|
||||
/>
|
||||
<button
|
||||
v-if="form.inventoryItems.length > 1"
|
||||
@@ -176,6 +202,7 @@ const normalizeScreenshotUrls = (values = []) =>
|
||||
const makeEmptyForm = () => ({
|
||||
id: '', name: '', price: 0, discountPrice: 0, tagsText: '', coverUrl: '',
|
||||
screenshotUrls: createScreenshotSlots(), inventoryItems: createInventoryItems(),
|
||||
fulfillmentType: 'card', fixedContent: '',
|
||||
description: '', active: true, requireLogin: false, maxPerAccount: 0,
|
||||
deliveryMode: 'auto', showNote: true, showContact: true
|
||||
})
|
||||
@@ -183,14 +210,18 @@ const makeEmptyForm = () => ({
|
||||
const form = reactive(makeEmptyForm())
|
||||
|
||||
const fillForm = (item) => {
|
||||
const ft = (item?.fulfillmentType || 'card').toLowerCase() === 'fixed' ? 'fixed' : 'card'
|
||||
Object.assign(form, {
|
||||
id: item?.id || '', name: item?.name || '', price: item?.price || 0, discountPrice: item?.discountPrice || 0,
|
||||
tagsText: (item?.tags || []).join(','), coverUrl: item?.coverUrl || '',
|
||||
screenshotUrls: createScreenshotSlots(item?.screenshotUrls || []),
|
||||
inventoryItems: createInventoryItems(item?.codes || []),
|
||||
inventoryItems: ft === 'fixed' ? [''] : createInventoryItems(item?.codes || []),
|
||||
fulfillmentType: ft,
|
||||
fixedContent: item?.fixedContent || '',
|
||||
description: item?.description || '', active: item?.active ?? true,
|
||||
requireLogin: item?.requireLogin ?? false, maxPerAccount: item?.maxPerAccount ?? 0,
|
||||
deliveryMode: 'auto', showNote: item?.showNote ?? true, showContact: item?.showContact ?? true
|
||||
deliveryMode: item?.deliveryMode || 'auto',
|
||||
showNote: item?.showNote ?? true, showContact: item?.showContact ?? true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -220,13 +251,16 @@ const removeInventoryItem = async (index) => {
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
const ft = form.fulfillmentType === 'fixed' ? 'fixed' : 'card'
|
||||
emit('submit', {
|
||||
id: form.id, name: form.name, price: form.price, discountPrice: form.discountPrice || 0,
|
||||
tags: form.tagsText, coverUrl: form.coverUrl,
|
||||
codes: normalizeInventoryItems(form.inventoryItems),
|
||||
codes: ft === 'fixed' ? [] : normalizeInventoryItems(form.inventoryItems),
|
||||
fulfillmentType: ft,
|
||||
fixedContent: ft === 'fixed' ? (form.fixedContent || '').trim() : '',
|
||||
screenshotUrls: normalizeScreenshotUrls(form.screenshotUrls),
|
||||
description: form.description, active: form.active, requireLogin: form.requireLogin,
|
||||
maxPerAccount: form.maxPerAccount || 0, deliveryMode: 'auto',
|
||||
maxPerAccount: form.maxPerAccount || 0, deliveryMode: form.deliveryMode || 'auto',
|
||||
showNote: form.showNote, showContact: form.showContact
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<tr class="bg-white/70 border-b-2 border-white/35">
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted max-md:px-2.5 max-md:py-2 max-md:text-xs">商品</th>
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted max-md:px-2.5 max-md:py-2 max-md:text-xs">价格</th>
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted text-center max-md:px-2.5 max-md:py-2 max-md:text-xs">库存</th>
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted text-center max-md:px-2.5 max-md:py-2 max-md:text-xs">库存 / 方式</th>
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted text-center max-md:px-2.5 max-md:py-2 max-md:text-xs">浏览量</th>
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted max-md:px-2.5 max-md:py-2 max-md:text-xs">状态</th>
|
||||
<th class="px-4 py-3 text-sm font-semibold uppercase tracking-[0.6px] text-muted max-md:px-2.5 max-md:py-2 max-md:text-xs">操作</th>
|
||||
@@ -40,9 +40,10 @@
|
||||
</td>
|
||||
<td class="px-4 py-3.5 text-center align-middle max-md:px-2.5 max-md:py-2">
|
||||
<span
|
||||
class="inline-block px-2.5 py-0.5 rounded-full text-[15px] font-semibold"
|
||||
:class="item.quantity === 0 ? 'bg-[rgba(201,90,106,0.12)] text-[#c95a6a]' : 'bg-[rgba(100,185,140,0.15)] text-[#3a9a68]'"
|
||||
>{{ item.quantity }}</span>
|
||||
class="inline-block px-2.5 py-0.5 rounded-full text-[13px] font-semibold"
|
||||
:class="stockBadgeClass(item)"
|
||||
>{{ stockLabel(item) }}</span>
|
||||
<div class="text-[11px] text-muted mt-1">{{ fulfillmentHint(item) }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3.5 text-center text-[15px] text-muted align-middle max-md:px-2.5 max-md:py-2">{{ item.viewCount || 0 }}</td>
|
||||
<td class="px-4 py-3.5 align-middle max-md:px-2.5 max-md:py-2">
|
||||
@@ -77,6 +78,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isFixedFulfillment, stockLabel as stockLabelText } from '../../shared/fulfillment'
|
||||
|
||||
defineProps({ products: { type: Array, default: () => [] } })
|
||||
defineEmits(['edit', 'toggle', 'remove'])
|
||||
|
||||
const stockLabel = (item) => stockLabelText(item)
|
||||
|
||||
const stockBadgeClass = (item) => {
|
||||
if (isFixedFulfillment(item)) return 'bg-[rgba(100,185,140,0.15)] text-[#3a9a68]'
|
||||
return (item.quantity ?? 0) === 0
|
||||
? 'bg-[rgba(201,90,106,0.12)] text-[#c95a6a]'
|
||||
: 'bg-[rgba(100,185,140,0.15)] text-[#3a9a68]'
|
||||
}
|
||||
|
||||
const fulfillmentHint = (item) => (isFixedFulfillment(item) ? '固定内容' : '卡密')
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-[13px] text-muted">后端 API、MySQL、Redis、RabbitMQ 探活与配置摘要(需管理 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>
|
||||
Reference in New Issue
Block a user