初始化

This commit is contained in:
zg
2026-07-15 16:13:27 +08:00
parent 6d6f559793
commit c4023c1623
276 changed files with 42622 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
<template>
<div class="dashboard">
<!-- 页面标题 -->
<div class="page-title">
<h2>概览</h2>
<p>网关平台运行状态一览</p>
</div>
<!-- 统计卡片 -->
<div class="stat-grid">
<div class="stat-card" v-for="card in statCards" :key="card.label">
<div class="stat-icon-box" :style="{ background: card.bg, color: card.color }">
<el-icon :size="22"><component :is="card.icon" /></el-icon>
</div>
<div class="stat-body">
<div class="stat-label">{{ card.label }}</div>
<div class="stat-value">
<el-skeleton v-if="loading" animated style="width:40px;height:28px" />
<span v-else>{{ card.value }}</span>
</div>
</div>
</div>
</div>
<!-- 图表 + 服务器信息 -->
<div class="info-grid">
<!-- 登录趋势 -->
<div class="chart-panel">
<div class="panel-header">
<div class="panel-title">
<el-icon :size="16" color="var(--color-primary)"><DataLine /></el-icon>
<span>近30天登录趋势</span>
</div>
</div>
<div class="panel-body">
<el-skeleton v-if="chartLoading" animated :rows="8" />
<template v-else>
<div ref="chartRef" class="chart-box"></div>
<el-empty v-if="!hasData" description="暂无登录数据" :image-size="80" />
</template>
</div>
</div>
<!-- 服务器状态 -->
<div class="server-panel">
<div class="panel-header">
<div class="panel-title">
<el-icon :size="16" color="var(--color-success)"><Monitor /></el-icon>
<span>服务器状态</span>
</div>
<el-tag size="small" type="success" effect="plain" round>运行中</el-tag>
</div>
<div class="panel-body">
<el-skeleton v-if="loading" animated :rows="6" />
<div v-else class="server-metrics">
<div class="metric-item">
<span class="metric-label">CPU 核心</span>
<span class="metric-value">{{ stats.serverInfo?.availableProcessors || '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">已用内存</span>
<span class="metric-value">{{ stats.serverInfo?.totalMemory || '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">可用内存</span>
<span class="metric-value highlight">{{ stats.serverInfo?.freeMemory || '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">最大内存</span>
<span class="metric-value">{{ stats.serverInfo?.maxMemory || '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">Java 版本</span>
<span class="metric-value">{{ stats.serverInfo?.javaVersion || '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">操作系统</span>
<span class="metric-value">{{ stats.serverInfo?.osName || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, onBeforeUnmount } from 'vue'
import { Monitor, User, DataLine, Connection } from '@element-plus/icons-vue'
import request from '@/utils/request'
const stats = reactive<any>({ serverInfo: {} })
const chartRef = ref()
const hasData = ref(false)
const loading = ref(true)
const chartLoading = ref(true)
let chartInstance: any = null
const statCards = ref([
{ label: '系统总数', value: 0, icon: Monitor, bg: '#eef2ff', color: '#6366f1' },
{ label: '用户总数', value: 0, icon: User, bg: '#ecfdf5', color: '#10b981' },
{ label: '今日登录', value: 0, icon: DataLine, bg: '#fffbeb', color: '#f59e0b' },
{ label: '当前在线', value: 0, icon: Connection, bg: '#fef2f2', color: '#ef4444' },
])
onMounted(async () => {
try {
const res = await request.get('/zgapi/v1/admin/dashboard/stats')
Object.assign(stats, res.data)
statCards.value[0].value = stats.systemCount || 0
statCards.value[1].value = stats.userCount || 0
statCards.value[2].value = stats.todayLoginCount || 0
statCards.value[3].value = stats.onlineCount || 0
await nextTick()
if (stats.loginTrend?.length) {
hasData.value = true
renderChart()
}
} catch {} finally {
loading.value = false
chartLoading.value = false
}
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (chartInstance) chartInstance.dispose()
})
function handleResize() {
chartInstance?.resize()
}
async function renderChart() {
if (!chartRef.value) return
const echartsModule: any = await import('echarts')
const echarts = echartsModule.default || echartsModule
chartInstance = echarts.init(chartRef.value)
chartInstance.setOption({
tooltip: {
trigger: 'axis',
backgroundColor: '#fff',
borderColor: '#e2e8f0',
textStyle: { color: '#1e293b', fontSize: 13 },
boxShadow: '0 4px 16px rgba(0,0,0,0.08)',
padding: [12, 16],
},
grid: { left: 40, right: 20, top: 16, bottom: 48 },
xAxis: {
type: 'category',
data: stats.loginTrend.map((d: any) => d.date),
axisLabel: { rotate: 45, fontSize: 11, color: '#94a3b8' },
axisLine: { lineStyle: { color: '#e2e8f0' } },
axisTick: { show: false },
},
yAxis: {
type: 'value',
minInterval: 1,
axisLabel: { fontSize: 11, color: '#94a3b8' },
splitLine: { lineStyle: { color: '#f1f5f9', type: 'dashed' } },
},
series: [{
data: stats.loginTrend.map((d: any) => d.count),
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
emphasis: { showSymbol: true, symbolSize: 8 },
lineStyle: { width: 2.5, color: '#6366f1' },
itemStyle: { color: '#6366f1' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(99,102,241,0.12)' },
{ offset: 1, color: 'rgba(99,102,241,0.0)' },
]),
},
}],
})
}
</script>
<style scoped>
.dashboard {
/* 占满宽度 */
}
.page-title {
margin-bottom: 24px;
}
.page-title h2 {
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.3px;
margin-bottom: 4px;
}
.page-title p {
font-size: 14px;
color: var(--text-muted);
}
/* ===== 统计卡片 ===== */
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #fff;
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-xs);
transition: all var(--transition-base);
}
.stat-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.stat-icon-box {
width: 44px;
height: 44px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-body {
flex: 1;
}
.stat-label {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
font-weight: 500;
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
letter-spacing: -0.5px;
}
/* ===== 信息网格 ===== */
.info-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 16px;
}
.chart-panel,
.server-panel {
background: #fff;
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-xs);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
}
.panel-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
color: var(--text-primary);
}
.panel-body {
padding: 20px;
}
.chart-box {
height: 300px;
}
/* ===== 服务器指标 ===== */
.server-metrics {
display: flex;
flex-direction: column;
gap: 0;
}
.metric-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--border-light);
}
.metric-item:last-child {
border-bottom: none;
}
.metric-label {
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
}
.metric-value {
font-size: 13px;
color: var(--text-secondary);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.metric-value.highlight {
color: var(--color-success);
}
/* ===== 响应式 ===== */
@media (max-width: 1024px) {
.stat-grid {
grid-template-columns: repeat(2, 1fr);
}
.info-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.stat-grid { grid-template-columns: 1fr; }
.chart-box { height: 220px; }
.panel-body { padding: 12px; }
}
</style>