初始化
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user