This commit is contained in:
2026-01-22 17:33:28 +08:00
parent 1bbf177b2c
commit b46759dc73
105 changed files with 2929 additions and 433 deletions

View File

@@ -0,0 +1,381 @@
<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>