Update mengyastore
This commit is contained in:
@@ -1,254 +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>
|
||||
<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>
|
||||
|
||||
@@ -1,143 +1,143 @@
|
||||
# 萌芽小店 · 前端(mengyastore-frontend)
|
||||
|
||||
基于 **Vue 3 + Vite** + **Tailwind CSS(Vite 插件)** 的数字商品前端:商店、结算(含扫码收款演示)、管理后台、收藏、订单、聊天、PWA。
|
||||
|
||||
## 技术依赖
|
||||
|
||||
| 包 | 用途 |
|
||||
|----|------|
|
||||
| vue ^3.x | 框架 |
|
||||
| vite ^5.x | 构建 |
|
||||
| @tailwindcss/vite | 工具类样式 |
|
||||
| vue-router ^4.x | 路由 |
|
||||
| pinia ^2.x | 状态(与 reactive 并存) |
|
||||
| axios ^1.6 | HTTP |
|
||||
| markdown-it ^14 | 商品描述 |
|
||||
| vite-plugin-pwa | PWA |
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── assets/
|
||||
│ ├── styles.css # 全局样式、.ghost / .page-card / . form-field 等
|
||||
│ └── payment/ # 支付宝/微信收款码 PNG(import 进 CheckoutPage)
|
||||
├── router/index.js # 路由;维护模式 beforeEach
|
||||
├── modules/
|
||||
│ ├── shared/
|
||||
│ │ ├── api.js # API 封装(含 fetchSystemStatus)
|
||||
│ │ ├── auth.js # 登录态、getLoginUrl
|
||||
│ │ ├── fulfillment.js # isFixedFulfillment、stockLabel、isSoldOutProduct
|
||||
│ │ └── useWishlist.js
|
||||
│ ├── store/
|
||||
│ │ ├── StorePage.vue
|
||||
│ │ ├── ProductDetail.vue
|
||||
│ │ ├── CheckoutPage.vue # 响应式布局、支付渠道 Tab、订单号复制、收款图
|
||||
│ │ └── components/ProductCard.vue
|
||||
│ ├── admin/
|
||||
│ │ ├── AdminPage.vue # 侧边栏:商品/订单/聊天/设置/系统状态
|
||||
│ │ └── components/
|
||||
│ │ ├── AdminProductModal.vue # 发货方式:卡密 | 固定内容
|
||||
│ │ ├── AdminProductTable.vue
|
||||
│ │ ├── AdminOrderTable.vue
|
||||
│ │ ├── AdminChatPanel.vue
|
||||
│ │ ├── AdminMaintenanceRow.vue
|
||||
│ │ ├── AdminSMTPRow.vue
|
||||
│ │ └── AdminSystemStatusPanel.vue
|
||||
│ ├── user/MyOrdersPage.vue
|
||||
│ ├── wishlist/WishlistPage.vue
|
||||
│ ├── maintenance/MaintenancePage.vue
|
||||
│ ├── auth/AuthCallback.vue
|
||||
│ └── chat/ChatWidget.vue
|
||||
└── App.vue # 顶栏:桌面横排导航;移动端汉堡菜单 + 下拉面板
|
||||
```
|
||||
|
||||
## 路由
|
||||
|
||||
| 路径 | 组件 | 说明 |
|
||||
|------|------|------|
|
||||
| `/` | StorePage | 商品列表 |
|
||||
| `/product/:id` | ProductDetail | 详情 |
|
||||
| `/checkout/:id` | CheckoutPage | 结算(路径参数为商品 id) |
|
||||
| `/my/orders` | MyOrdersPage | 我的订单(建议登录) |
|
||||
| `/wishlist` | WishlistPage | 收藏 |
|
||||
| `/admin` | AdminPage | 管理后台(token) |
|
||||
| `/auth/callback` | AuthCallback | OAuth 回调 |
|
||||
| `/maintenance` | MaintenancePage | 维护中 |
|
||||
|
||||
### 路由守卫
|
||||
|
||||
- 请求 `GET /api/site/maintenance`;维护中时跳转 `/maintenance`。
|
||||
- 豁免:`/maintenance`、`/wishlist`、`/admin`、`/auth/callback`(与 `router/index.js` 保持一致)。
|
||||
|
||||
## 认证
|
||||
|
||||
1. 「萌芽账号登录」→ SproutGate 登录页,`callback` 回指本站。
|
||||
2. Token 存 `localStorage`,`authState` 共享用户信息。
|
||||
3. 需登录接口带 `Authorization: Bearer <token>`。
|
||||
|
||||
## 功能要点
|
||||
|
||||
### 商品与库存展示
|
||||
|
||||
- API 字段 **`fulfillmentType`**:`card` | `fixed`。
|
||||
- **`fixed`**:列表/详情/卡片显示库存为 **「不限」**,不按 `quantity===0` 售罄。
|
||||
- 工具函数:`modules/shared/fulfillment.js`。
|
||||
|
||||
### 管理后台 · 商品
|
||||
|
||||
- **卡密**:多行输入,逐条对应 `codes`。
|
||||
- **固定内容**:大文本 `fixedContent`;保存后无需维护条数。
|
||||
|
||||
### 结算页(CheckoutPage)
|
||||
|
||||
- 栅格与表单单列适配窄屏。
|
||||
- 下单成功后:应付金额、支付宝/微信/赞赏 **本地图片** 切换、`订单号` **一键复制**、可选展开服务端 `qrCodeUrl`。
|
||||
- 数量上限:卡密=min(库存, 每账号限购);固定内容=仅每账号限购(若设置)。
|
||||
|
||||
### 顶栏(App.vue)
|
||||
|
||||
- `md` 及以上:横向按钮。
|
||||
- 以下:汉堡按钮 + 全宽大标题项 + 遮罩;Escape 关闭;打开时锁 body 滚动。
|
||||
|
||||
### 系统状态
|
||||
|
||||
- `AdminSystemStatusPanel` 调用 `GET /api/admin/system-status`,展示后端/API、MySQL、Redis、RabbitMQ(需管理 Token)。
|
||||
|
||||
### PWA
|
||||
|
||||
- `vite-plugin-pwa`:`registerType: 'autoUpdate'`,构建后生成 SW;根组件有更新 Toast 与 `SplashScreen`。
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # :5173
|
||||
npm run build # dist/
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
开发示例(`.env.development`):
|
||||
|
||||
```env
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
生产部署设置实际 API 基地址(或同源反代可不设)。
|
||||
|
||||
## 部署(静态资源)
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
字体与色板以 `src/assets/styles.css` 及 Tailwind 主题为准;文档若与代码不一致,以仓库当前实现为准。
|
||||
# 萌芽小店 · 前端(mengyastore-frontend)
|
||||
|
||||
基于 **Vue 3 + Vite** + **Tailwind CSS(Vite 插件)** 的数字商品前端:商店、结算(含扫码收款演示)、管理后台、收藏、订单、聊天、PWA。
|
||||
|
||||
## 技术依赖
|
||||
|
||||
| 包 | 用途 |
|
||||
|----|------|
|
||||
| vue ^3.x | 框架 |
|
||||
| vite ^5.x | 构建 |
|
||||
| @tailwindcss/vite | 工具类样式 |
|
||||
| vue-router ^4.x | 路由 |
|
||||
| pinia ^2.x | 状态(与 reactive 并存) |
|
||||
| axios ^1.6 | HTTP |
|
||||
| markdown-it ^14 | 商品描述 |
|
||||
| vite-plugin-pwa | PWA |
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── assets/
|
||||
│ ├── styles.css # 全局样式、.ghost / .page-card / . form-field 等
|
||||
│ └── payment/ # 支付宝/微信收款码 PNG(import 进 CheckoutPage)
|
||||
├── router/index.js # 路由;维护模式 beforeEach
|
||||
├── modules/
|
||||
│ ├── shared/
|
||||
│ │ ├── api.js # API 封装(含 fetchSystemStatus)
|
||||
│ │ ├── auth.js # 登录态、getLoginUrl
|
||||
│ │ ├── fulfillment.js # isFixedFulfillment、stockLabel、isSoldOutProduct
|
||||
│ │ └── useWishlist.js
|
||||
│ ├── store/
|
||||
│ │ ├── StorePage.vue
|
||||
│ │ ├── ProductDetail.vue
|
||||
│ │ ├── CheckoutPage.vue # 响应式布局、支付渠道 Tab、订单号复制、收款图
|
||||
│ │ └── components/ProductCard.vue
|
||||
│ ├── admin/
|
||||
│ │ ├── AdminPage.vue # 侧边栏:商品/订单/聊天/设置/系统状态
|
||||
│ │ └── components/
|
||||
│ │ ├── AdminProductModal.vue # 发货方式:卡密 | 固定内容
|
||||
│ │ ├── AdminProductTable.vue
|
||||
│ │ ├── AdminOrderTable.vue
|
||||
│ │ ├── AdminChatPanel.vue
|
||||
│ │ ├── AdminMaintenanceRow.vue
|
||||
│ │ ├── AdminSMTPRow.vue
|
||||
│ │ └── AdminSystemStatusPanel.vue
|
||||
│ ├── user/MyOrdersPage.vue
|
||||
│ ├── wishlist/WishlistPage.vue
|
||||
│ ├── maintenance/MaintenancePage.vue
|
||||
│ ├── auth/AuthCallback.vue
|
||||
│ └── chat/ChatWidget.vue
|
||||
└── App.vue # 顶栏:桌面横排导航;移动端汉堡菜单 + 下拉面板
|
||||
```
|
||||
|
||||
## 路由
|
||||
|
||||
| 路径 | 组件 | 说明 |
|
||||
|------|------|------|
|
||||
| `/` | StorePage | 商品列表 |
|
||||
| `/product/:id` | ProductDetail | 详情 |
|
||||
| `/checkout/:id` | CheckoutPage | 结算(路径参数为商品 id) |
|
||||
| `/my/orders` | MyOrdersPage | 我的订单(建议登录) |
|
||||
| `/wishlist` | WishlistPage | 收藏 |
|
||||
| `/admin` | AdminPage | 管理后台(token) |
|
||||
| `/auth/callback` | AuthCallback | OAuth 回调 |
|
||||
| `/maintenance` | MaintenancePage | 维护中 |
|
||||
|
||||
### 路由守卫
|
||||
|
||||
- 请求 `GET /api/site/maintenance`;维护中时跳转 `/maintenance`。
|
||||
- 豁免:`/maintenance`、`/wishlist`、`/admin`、`/auth/callback`(与 `router/index.js` 保持一致)。
|
||||
|
||||
## 认证
|
||||
|
||||
1. 「萌芽账号登录」→ SproutGate 登录页,`callback` 回指本站。
|
||||
2. Token 存 `localStorage`,`authState` 共享用户信息。
|
||||
3. 需登录接口带 `Authorization: Bearer <token>`。
|
||||
|
||||
## 功能要点
|
||||
|
||||
### 商品与库存展示
|
||||
|
||||
- API 字段 **`fulfillmentType`**:`card` | `fixed`。
|
||||
- **`fixed`**:列表/详情/卡片显示库存为 **「不限」**,不按 `quantity===0` 售罄。
|
||||
- 工具函数:`modules/shared/fulfillment.js`。
|
||||
|
||||
### 管理后台 · 商品
|
||||
|
||||
- **卡密**:多行输入,逐条对应 `codes`。
|
||||
- **固定内容**:大文本 `fixedContent`;保存后无需维护条数。
|
||||
|
||||
### 结算页(CheckoutPage)
|
||||
|
||||
- 栅格与表单单列适配窄屏。
|
||||
- 下单成功后:应付金额、支付宝/微信/赞赏 **本地图片** 切换、`订单号` **一键复制**、可选展开服务端 `qrCodeUrl`。
|
||||
- 数量上限:卡密=min(库存, 每账号限购);固定内容=仅每账号限购(若设置)。
|
||||
|
||||
### 顶栏(App.vue)
|
||||
|
||||
- `md` 及以上:横向按钮。
|
||||
- 以下:汉堡按钮 + 全宽大标题项 + 遮罩;Escape 关闭;打开时锁 body 滚动。
|
||||
|
||||
### 系统状态
|
||||
|
||||
- `AdminSystemStatusPanel` 调用 `GET /api/admin/system-status`,展示后端/API、MySQL、Redis、RabbitMQ(需管理 Token)。
|
||||
|
||||
### PWA
|
||||
|
||||
- `vite-plugin-pwa`:`registerType: 'autoUpdate'`,构建后生成 SW;根组件有更新 Toast 与 `SplashScreen`。
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # :5173
|
||||
npm run build # dist/
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
开发示例(`.env.development`):
|
||||
|
||||
```env
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
生产部署设置实际 API 基地址(或同源反代可不设)。
|
||||
|
||||
## 部署(静态资源)
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
字体与色板以 `src/assets/styles.css` 及 Tailwind 主题为准;文档若与代码不一致,以仓库当前实现为准。
|
||||
|
||||
Reference in New Issue
Block a user