Files
HertzAdmin-SpringBoot/ui/src/views/admin/Dashboard.vue
2026-01-22 17:33:28 +08:00

382 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>