445 lines
11 KiB
Vue
445 lines
11 KiB
Vue
<template>
|
|
<div class="dashboard-container">
|
|
<!-- 统计卡片 -->
|
|
<a-row :gutter="16" class="stats-cards">
|
|
<a-col :xs="24" :sm="12" :lg="6">
|
|
<a-card :loading="loading" hoverable>
|
|
<a-statistic
|
|
title="用户总数"
|
|
:value="stats.userCount"
|
|
:prefix="UserOutlined"
|
|
:value-style="{ color: '#1890ff' }"
|
|
>
|
|
<template #suffix>
|
|
<a-tag color="blue">活跃</a-tag>
|
|
</template>
|
|
</a-statistic>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :sm="12" :lg="6">
|
|
<a-card :loading="loading" hoverable>
|
|
<a-statistic
|
|
title="角色数量"
|
|
:value="stats.roleCount"
|
|
:prefix="TeamOutlined"
|
|
:value-style="{ color: '#52c41a' }"
|
|
>
|
|
<template #suffix>
|
|
<a-tag color="green">配置</a-tag>
|
|
</template>
|
|
</a-statistic>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :sm="12" :lg="6">
|
|
<a-card :loading="loading" hoverable>
|
|
<a-statistic
|
|
title="权限节点"
|
|
:value="stats.permissionCount"
|
|
:prefix="KeyOutlined"
|
|
:value-style="{ color: '#722ed1' }"
|
|
>
|
|
<template #suffix>
|
|
<a-tag color="purple">权限</a-tag>
|
|
</template>
|
|
</a-statistic>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :sm="12" :lg="6">
|
|
<a-card :loading="loading" hoverable>
|
|
<a-statistic
|
|
title="在线用户"
|
|
:value="stats.onlineUserCount"
|
|
:prefix="ClockCircleOutlined"
|
|
:value-style="{ color: '#fa8c16' }"
|
|
>
|
|
<template #suffix>
|
|
<a-tag color="orange">在线</a-tag>
|
|
</template>
|
|
</a-statistic>
|
|
</a-card>
|
|
</a-col>
|
|
</a-row>
|
|
|
|
<!-- 图表区域 -->
|
|
<a-row :gutter="16" class="chart-row">
|
|
<a-col :xs="24" :lg="12">
|
|
<a-card title="用户分布" :loading="loading">
|
|
<div class="chart-container">
|
|
<div class="pie-chart">
|
|
<div class="chart-item" v-for="(item, index) in userDistribution" :key="index">
|
|
<div class="chart-bar" :style="{ width: item.percent + '%' }"></div>
|
|
<div class="chart-label">{{ item.label }}: {{ item.value }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :lg="12">
|
|
<a-card title="系统状态" :loading="loading">
|
|
<a-descriptions :column="1" bordered>
|
|
<a-descriptions-item label="系统版本">{{ systemInfo.version }}</a-descriptions-item>
|
|
<a-descriptions-item label="PHP 版本">{{ systemInfo.phpVersion }}</a-descriptions-item>
|
|
<a-descriptions-item label="Laravel 版本">{{ systemInfo.laravelVersion }}</a-descriptions-item>
|
|
<a-descriptions-item label="服务器时间">{{ systemInfo.serverTime }}</a-descriptions-item>
|
|
<a-descriptions-item label="运行环境">{{ systemInfo.environment }}</a-descriptions-item>
|
|
</a-descriptions>
|
|
</a-card>
|
|
</a-col>
|
|
</a-row>
|
|
|
|
<!-- 快速操作和最近操作 -->
|
|
<a-row :gutter="16" class="action-row">
|
|
<a-col :xs="24" :lg="12">
|
|
<a-card title="快速操作">
|
|
<a-row :gutter="[8, 8]">
|
|
<a-col :xs="12" :sm="8">
|
|
<a-button type="primary" block @click="goToUser">
|
|
<UserOutlined />
|
|
用户管理
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :xs="12" :sm="8">
|
|
<a-button type="primary" block @click="goToRole">
|
|
<TeamOutlined />
|
|
角色管理
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :xs="12" :sm="8">
|
|
<a-button type="primary" block @click="goToPermission">
|
|
<KeyOutlined />
|
|
权限管理
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :xs="12" :sm="8">
|
|
<a-button type="primary" block @click="goToDepartment">
|
|
<ApartmentOutlined />
|
|
部门管理
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :xs="12" :sm="8">
|
|
<a-button type="primary" block @click="goToOnlineUsers">
|
|
<WifiOutlined />
|
|
在线用户
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :xs="12" :sm="8">
|
|
<a-button type="primary" block @click="goToConfig">
|
|
<SettingOutlined />
|
|
系统配置
|
|
</a-button>
|
|
</a-col>
|
|
</a-row>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :lg="12">
|
|
<a-card title="最近日志" :loading="loading">
|
|
<a-list :data-source="recentLogs" size="small">
|
|
<template #renderItem="{ item }">
|
|
<a-list-item>
|
|
<a-list-item-meta>
|
|
<template #title>
|
|
<a-tag :color="getLogColor(item.level)">{{ item.level }}</a-tag>
|
|
{{ item.message }}
|
|
</template>
|
|
<template #description>
|
|
{{ item.created_at }}
|
|
</template>
|
|
</a-list-item-meta>
|
|
</a-list-item>
|
|
</template>
|
|
</a-list>
|
|
</a-card>
|
|
</a-col>
|
|
</a-row>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useUserStore } from '@/stores/modules/user'
|
|
import authApi from '@/api/auth'
|
|
import { message } from 'ant-design-vue'
|
|
|
|
// 定义组件名称
|
|
defineOptions({
|
|
name: 'HomePage',
|
|
})
|
|
|
|
const router = useRouter()
|
|
const userStore = useUserStore()
|
|
|
|
const loading = ref(false)
|
|
const stats = ref({
|
|
userCount: 0,
|
|
roleCount: 0,
|
|
permissionCount: 0,
|
|
onlineUserCount: 0
|
|
})
|
|
|
|
const userDistribution = ref([
|
|
{ label: '已激活', value: 0, percent: 0 },
|
|
{ label: '未激活', value: 0, percent: 0 },
|
|
{ label: '已禁用', value: 0, percent: 0 }
|
|
])
|
|
|
|
const systemInfo = ref({
|
|
version: '1.0.0',
|
|
phpVersion: '8.1.0',
|
|
laravelVersion: '10.0.0',
|
|
serverTime: '',
|
|
environment: 'production'
|
|
})
|
|
|
|
const recentLogs = ref([])
|
|
|
|
// 获取统计数据
|
|
const fetchStats = async () => {
|
|
try {
|
|
loading.value = true
|
|
|
|
// 获取用户列表统计
|
|
const userRes = await authApi.users.list.get({ page_size: 1 })
|
|
if (userRes.code === 200) {
|
|
stats.value.userCount = userRes.data.total || 0
|
|
}
|
|
|
|
// 获取角色列表统计
|
|
const roleRes = await authApi.roles.list.get({ page_size: 1 })
|
|
if (roleRes.code === 200) {
|
|
stats.value.roleCount = roleRes.data.total || 0
|
|
}
|
|
|
|
// 获取权限列表统计
|
|
const permRes = await authApi.permissions.list.get({ page_size: 1 })
|
|
if (permRes.code === 200) {
|
|
stats.value.permissionCount = permRes.data.total || 0
|
|
}
|
|
|
|
// 获取在线用户统计
|
|
const onlineRes = await authApi.onlineUsers.count.get()
|
|
if (onlineRes.code === 200) {
|
|
stats.value.onlineUserCount = onlineRes.data.count || 0
|
|
}
|
|
|
|
// 获取用户分布数据(这里使用模拟数据,实际项目中应从后端获取)
|
|
const activeCount = Math.floor(stats.value.userCount * 0.7)
|
|
const inactiveCount = Math.floor(stats.value.userCount * 0.2)
|
|
const disabledCount = stats.value.userCount - activeCount - inactiveCount
|
|
|
|
userDistribution.value = [
|
|
{ label: '已激活', value: activeCount, percent: stats.value.userCount > 0 ? Math.round((activeCount / stats.value.userCount) * 100) : 0 },
|
|
{ label: '未激活', value: inactiveCount, percent: stats.value.userCount > 0 ? Math.round((inactiveCount / stats.value.userCount) * 100) : 0 },
|
|
{ label: '已禁用', value: disabledCount, percent: stats.value.userCount > 0 ? Math.round((disabledCount / stats.value.userCount) * 100) : 0 }
|
|
]
|
|
|
|
// 更新系统信息
|
|
systemInfo.value.serverTime = new Date().toLocaleString('zh-CN')
|
|
systemInfo.value.environment = import.meta.env.VITE_APP_ENV || 'production'
|
|
|
|
// 模拟最近日志数据(实际项目中应从后端获取)
|
|
recentLogs.value = [
|
|
{ level: 'info', message: '用户登录成功', created_at: formatTime(new Date()) },
|
|
{ level: 'info', message: '系统配置更新', created_at: formatTime(new Date(Date.now() - 300000)) },
|
|
{ level: 'warning', message: '检测到异常访问', created_at: formatTime(new Date(Date.now() - 600000)) },
|
|
{ level: 'error', message: '数据库连接失败', created_at: formatTime(new Date(Date.now() - 900000)) },
|
|
{ level: 'info', message: '定时任务执行完成', created_at: formatTime(new Date(Date.now() - 1200000)) }
|
|
]
|
|
} catch (error) {
|
|
message.error('获取统计数据失败')
|
|
console.error(error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 格式化时间
|
|
const formatTime = (date) => {
|
|
const now = new Date()
|
|
const diff = now - date
|
|
const minutes = Math.floor(diff / 60000)
|
|
|
|
if (minutes < 1) {
|
|
return '刚刚'
|
|
} else if (minutes < 60) {
|
|
return `${minutes}分钟前`
|
|
} else if (minutes < 1440) {
|
|
return `${Math.floor(minutes / 60)}小时前`
|
|
} else {
|
|
return `${Math.floor(minutes / 1440)}天前`
|
|
}
|
|
}
|
|
|
|
// 获取日志颜色
|
|
const getLogColor = (level) => {
|
|
const colors = {
|
|
info: 'blue',
|
|
warning: 'orange',
|
|
error: 'red',
|
|
success: 'green'
|
|
}
|
|
return colors[level] || 'default'
|
|
}
|
|
|
|
// 路由跳转方法
|
|
const goToUser = () => {
|
|
router.push('/system/users')
|
|
}
|
|
|
|
const goToRole = () => {
|
|
router.push('/system/roles')
|
|
}
|
|
|
|
const goToPermission = () => {
|
|
router.push('/system/permissions')
|
|
}
|
|
|
|
const goToDepartment = () => {
|
|
router.push('/system/departments')
|
|
}
|
|
|
|
const goToOnlineUsers = () => {
|
|
router.push('/system/online-users')
|
|
}
|
|
|
|
const goToConfig = () => {
|
|
router.push('/system/config')
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
fetchStats()
|
|
|
|
// 每分钟更新一次服务器时间
|
|
setInterval(() => {
|
|
systemInfo.value.serverTime = new Date().toLocaleString('zh-CN')
|
|
}, 60000)
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.dashboard-container {
|
|
padding: 20px;
|
|
background: #f0f2f5;
|
|
min-height: calc(100vh - 64px);
|
|
|
|
.stats-cards {
|
|
margin-bottom: 16px;
|
|
|
|
:deep(.ant-card) {
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
.ant-statistic-title {
|
|
font-size: 14px;
|
|
color: rgba(0, 0, 0, 0.65);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.ant-statistic-content {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ant-statistic-content-prefix {
|
|
margin-right: 8px;
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.chart-row,
|
|
.action-row {
|
|
margin-bottom: 16px;
|
|
|
|
:deep(.ant-card) {
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
height: 100%;
|
|
|
|
.ant-card-head {
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.chart-container {
|
|
padding: 20px 0;
|
|
|
|
.pie-chart {
|
|
.chart-item {
|
|
margin-bottom: 16px;
|
|
position: relative;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.chart-bar {
|
|
height: 8px;
|
|
background: linear-gradient(90deg, #1890ff 0%, #36cfc9 100%);
|
|
border-radius: 4px;
|
|
margin-bottom: 8px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.chart-label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 14px;
|
|
color: rgba(0, 0, 0, 0.65);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
:deep(.ant-btn) {
|
|
height: 80px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
|
|
.anticon {
|
|
font-size: 24px;
|
|
}
|
|
}
|
|
|
|
:deep(.ant-list-item) {
|
|
padding: 12px 0;
|
|
}
|
|
|
|
:deep(.ant-descriptions-item-label) {
|
|
width: 100px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
// 响应式调整
|
|
@media (max-width: 768px) {
|
|
padding: 10px;
|
|
|
|
.stats-cards {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.chart-row,
|
|
.action-row {
|
|
margin-bottom: 10px;
|
|
|
|
.ant-col {
|
|
margin-bottom: 10px;
|
|
}
|
|
}
|
|
|
|
:deep(.ant-btn) {
|
|
height: 60px;
|
|
|
|
.anticon {
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|