382 lines
9.7 KiB
Vue
382 lines
9.7 KiB
Vue
<template>
|
||
<div class="dashboard-container">
|
||
<!-- Welcome Section -->
|
||
<el-card class="welcome-card mb-4" shadow="hover">
|
||
<div class="welcome-content">
|
||
<div class="welcome-left">
|
||
<el-avatar :size="64" :src="avatarSrc" class="welcome-avatar">
|
||
<template #default>
|
||
{{ auth.nickname?.charAt(0) || auth.username?.charAt(0) }}
|
||
</template>
|
||
</el-avatar>
|
||
<div class="welcome-text">
|
||
<h2 class="greeting">{{ greeting }},{{ auth.nickname || auth.username }}</h2>
|
||
<p class="subtitle">欢迎回到 Hertz Admin,今天也是充满活力的一天!</p>
|
||
</div>
|
||
</div>
|
||
<!-- <div class="welcome-right">
|
||
<div class="stat-item">
|
||
<span class="label">我的角色</span>
|
||
<span class="value">{{ auth.roles }}</span>
|
||
</div>
|
||
</div> -->
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- Statistics Cards -->
|
||
<el-row :gutter="20" class="mb-4">
|
||
<el-col :span="8" :xs="24" class="mb-xs-4">
|
||
<el-card shadow="hover" class="stat-card" :body-style="{ padding: '20px' }">
|
||
<div class="stat-icon user-bg">
|
||
<el-icon><User /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value">{{ stats.userCount }}</div>
|
||
<div class="stat-label">总用户数</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="8" :xs="24" class="mb-xs-4">
|
||
<el-card shadow="hover" class="stat-card" :body-style="{ padding: '20px' }">
|
||
<div class="stat-icon role-bg">
|
||
<el-icon><Stamp /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value">{{ stats.roleCount }}</div>
|
||
<div class="stat-label">角色数量</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="8" :xs="24">
|
||
<el-card shadow="hover" class="stat-card" :body-style="{ padding: '20px' }">
|
||
<div class="stat-icon menu-bg">
|
||
<el-icon><IconMenu /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value">{{ stats.menuCount }}</div>
|
||
<div class="stat-label">菜单项</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- Content Grid -->
|
||
<el-row :gutter="20">
|
||
<!-- Recent Users -->
|
||
<el-col :span="16" :xs="24" class="mb-xs-4">
|
||
<el-card shadow="hover" class="h-100">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span><el-icon class="mr-1"><Timer /></el-icon> 最新加入用户</span>
|
||
<el-button text type="primary" @click="router.push('/admin/system/user')">
|
||
查看全部 <el-icon class="el-icon--right"><ArrowRight /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
<el-table :data="recentUsers" style="width: 100%" v-loading="loading">
|
||
<el-table-column prop="username" label="用户名" min-width="120">
|
||
<template #default="{ row }">
|
||
<div class="user-cell">
|
||
<el-avatar :size="24" :src="resolveFileUrl(row.avatarPath)" class="mr-2">
|
||
{{ row.nickname?.charAt(0) || row.username.charAt(0) }}
|
||
</el-avatar>
|
||
<span>{{ row.username }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="nickname" label="昵称" min-width="120" />
|
||
<el-table-column prop="createdAt" label="注册时间" min-width="180">
|
||
<template #default="{ row }">
|
||
{{ new Date(row.createdAt).toLocaleString() }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status" label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- Quick Actions -->
|
||
<el-col :span="8" :xs="24">
|
||
<el-card shadow="hover" class="h-100">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>快捷导航</span>
|
||
</div>
|
||
</template>
|
||
<div class="quick-actions">
|
||
<div
|
||
v-for="action in quickActions"
|
||
:key="action.path"
|
||
class="action-item"
|
||
@click="router.push(action.path)"
|
||
>
|
||
<div class="action-icon" :style="{ backgroundColor: action.color + '20', color: action.color }">
|
||
<el-icon><component :is="action.icon" /></el-icon>
|
||
</div>
|
||
<span class="action-name">{{ action.name }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useAuthStore } from '../../stores/auth'
|
||
import { fetchUsers, pageRoles, pageMenus } from '../../api/system'
|
||
import { User, Stamp, Menu as IconMenu, Timer, ArrowRight, DataLine, Setting } from '@element-plus/icons-vue'
|
||
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const apiBase = import.meta.env.VITE_API_BASE || 'http://localhost:8080'
|
||
|
||
const stats = ref({
|
||
userCount: 0,
|
||
roleCount: 0,
|
||
menuCount: 0
|
||
})
|
||
|
||
const recentUsers = ref([])
|
||
const loading = ref(true)
|
||
|
||
const greeting = computed(() => {
|
||
const hour = new Date().getHours()
|
||
if (hour < 6) return '夜深了'
|
||
if (hour < 9) return '早上好'
|
||
if (hour < 12) return '上午好'
|
||
if (hour < 14) return '中午好'
|
||
if (hour < 17) return '下午好'
|
||
if (hour < 19) return '傍晚好'
|
||
return '晚上好'
|
||
})
|
||
|
||
function resolveFileUrl(path) {
|
||
if (!path) return undefined
|
||
if (/^https?:\/\//i.test(path)) return path
|
||
const normalizedBase = apiBase.endsWith('/') ? apiBase.slice(0, -1) : apiBase
|
||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||
return `${normalizedBase}${normalizedPath}`
|
||
}
|
||
|
||
const avatarSrc = computed(() => resolveFileUrl(auth.avatarPath))
|
||
|
||
const quickActions = [
|
||
{ name: '用户管理', path: '/admin/system/user', icon: User, color: '#409EFF' },
|
||
{ name: '角色管理', path: '/admin/system/role', icon: Stamp, color: '#67C23A' },
|
||
{ name: '菜单管理', path: '/admin/system/menu', icon: IconMenu, color: '#E6A23C' },
|
||
{ name: '个人资料', path: '/admin/profile', icon: Setting, color: '#909399' }
|
||
]
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
// Parallel requests for stats
|
||
const [usersRes, rolesRes, menusRes] = await Promise.all([
|
||
fetchUsers({ page: 1, size: 5 }), // Get first page to show recent users too
|
||
pageRoles({ page: 1, size: 1 }),
|
||
pageMenus({ page: 1, size: 1 })
|
||
])
|
||
|
||
stats.value = {
|
||
userCount: usersRes.total,
|
||
roleCount: rolesRes.total,
|
||
menuCount: menusRes.total
|
||
}
|
||
|
||
recentUsers.value = usersRes.records
|
||
} catch (error) {
|
||
console.error('Failed to load dashboard data:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.mb-4 {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.mr-1 {
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.mr-2 {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.h-100 {
|
||
height: 100%;
|
||
}
|
||
|
||
/* Welcome Card */
|
||
.welcome-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.welcome-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.welcome-text .greeting {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin: 0 0 8px;
|
||
color: #303133;
|
||
}
|
||
|
||
.welcome-text .subtitle {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
margin: 0;
|
||
}
|
||
|
||
.welcome-right {
|
||
display: flex;
|
||
gap: 32px;
|
||
padding-right: 20px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.stat-item .label {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-item .value {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
/* Stat Cards */
|
||
.stat-card {
|
||
border: none;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.stat-card :deep(.el-card__body) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 16px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 28px;
|
||
}
|
||
|
||
.user-bg { background-color: #ecf5ff; color: #409eff; }
|
||
.role-bg { background-color: #f0f9eb; color: #67c23a; }
|
||
.menu-bg { background-color: #fdf6ec; color: #e6a23c; }
|
||
|
||
.stat-info .stat-value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
color: #303133;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-info .stat-label {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
|
||
/* Card Header */
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Recent Users */
|
||
.user-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Quick Actions */
|
||
.quick-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.action-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
border-radius: 12px;
|
||
background-color: #ecf5ff;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.action-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.action-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #606266;
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.welcome-content {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 20px;
|
||
}
|
||
|
||
.welcome-right {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
padding-right: 0;
|
||
border-top: 1px solid #EBEEF5;
|
||
padding-top: 16px;
|
||
}
|
||
|
||
.mb-xs-4 {
|
||
margin-bottom: 24px;
|
||
}
|
||
}
|
||
</style>
|