Files
ai/frontend/src/views/Admin.vue
2025-07-21 23:53:44 +08:00

431 lines
19 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="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- 导航栏 -->
<nav class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
AI 管理后台
</h1>
</div>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-600 dark:text-gray-300">
管理员{{ adminUser?.username || '未知用户' }}
</span>
<button class="btn-outline" @click="handleLogout">
退出登录
</button>
</div>
</div>
</div>
</nav>
<!-- 主要内容 -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 页面标题 -->
<div class="mb-8">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
系统管理
</h2>
<p class="text-gray-600 dark:text-gray-300">
管理用户权限和系统配置
</p>
</div>
<!-- 管理菜单 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div
v-for="menu in adminMenus"
:key="menu.id"
class="card p-6 cursor-pointer hover:shadow-lg transition-shadow"
@click="navigateToMenu(menu)"
>
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-10 h-10 bg-primary-100 dark:bg-primary-900 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="menu.icon === 'UserIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
<path v-else-if="menu.icon === 'KeyIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
<path v-else-if="menu.icon === 'ShieldIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
<path v-else-if="menu.icon === 'ChartIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
{{ menu.name }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ menu.description }}
</p>
</div>
</div>
</div>
</div>
<!-- 系统统计 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="card p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">总用户数</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.totalUsers }}</p>
</div>
</div>
</div>
<div class="card p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">总账号数</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.totalAccounts }}</p>
</div>
</div>
</div>
<div class="card p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">今日访问</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.todayVisits }}</p>
</div>
</div>
</div>
<div class="card p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">系统告警</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.alerts }}</p>
</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="mb-8">
<!-- 快速操作卡片 -->
<div class="card">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
快速操作
</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<button
@click="router.push('/admin/users')"
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center mr-3">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<div class="text-left">
<p class="text-sm font-medium text-gray-900 dark:text-white">添加用户</p>
<p class="text-xs text-gray-500 dark:text-gray-400">创建新用户账号</p>
</div>
</button>
<button
@click="router.push('/admin/accounts')"
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center mr-3">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div class="text-left">
<p class="text-sm font-medium text-gray-900 dark:text-white">添加账号</p>
<p class="text-xs text-gray-500 dark:text-gray-400">管理网站账号</p>
</div>
</button>
<button
@click="router.push('/admin/permissions')"
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center mr-3">
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div class="text-left">
<p class="text-sm font-medium text-gray-900 dark:text-white">权限设置</p>
<p class="text-xs text-gray-500 dark:text-gray-400">配置访问权限</p>
</div>
</button>
<button
@click="router.push('/admin/monitor')"
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center mr-3">
<svg class="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div class="text-left">
<p class="text-sm font-medium text-gray-900 dark:text-white">系统监控</p>
<p class="text-xs text-gray-500 dark:text-gray-400">查看运行状态</p>
</div>
</button>
</div>
</div>
</div>
</div>
<!-- 最近活动 -->
<div class="card">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
最近活动
</h3>
</div>
<div class="p-6">
<div v-if="recentActivities.length === 0" class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">暂无活动</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
系统运行正常暂无异常活动
</p>
</div>
<div v-else class="space-y-4">
<div
v-for="activity in recentActivities"
:key="activity.id"
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg"
:class="{
'border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/20':
activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED'
}"
>
<div class="flex-shrink-0">
<div
class="w-8 h-8 rounded-full flex items-center justify-center"
:class="{
'bg-red-100 dark:bg-red-900': activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED',
'bg-gray-100 dark:bg-gray-800': !(activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED')
}"
>
<svg
class="w-4 h-4"
:class="{
'text-red-600 dark:text-red-400': activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED',
'text-gray-600 dark:text-gray-400': !(activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED')
}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
v-if="activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED'"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
</div>
<div class="ml-4 flex-1">
<p
class="text-sm"
:class="{
'text-red-900 dark:text-red-100': activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED',
'text-gray-900 dark:text-white': !(activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED')
}"
>
{{ activity.description }}
</p>
<div class="flex items-center space-x-4 mt-1">
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ formatTime(activity.time) }}
</p>
<p v-if="activity.ipAddress" class="text-xs text-blue-600 dark:text-blue-400">
IP: {{ activity.ipAddress }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAdminStore } from '@/stores/admin'
import type { AdminMenu, AdminUser, SystemStats } from '@/types'
import { adminAuth } from '@/utils/auth'
const router = useRouter()
const adminStore = useAdminStore()
const adminUser = ref<AdminUser | null>(null)
const stats = ref<SystemStats>({
totalUsers: 0,
totalAccounts: 0,
todayVisits: 0,
alerts: 0
})
const adminMenus: AdminMenu[] = [
{
id: 'users',
name: '用户管理',
description: '管理系统用户',
icon: 'UserIcon',
path: '/admin/users'
},
{
id: 'accounts',
name: '账号管理',
description: '管理网站账号',
icon: 'KeyIcon',
path: '/admin/accounts'
},
{
id: 'permissions',
name: '权限管理',
description: '配置访问权限',
icon: 'ShieldIcon',
path: '/admin/permissions'
},
{
id: 'monitor',
name: '系统监控',
description: '监控系统状态',
icon: 'ChartIcon',
path: '/admin/monitor'
}
]
// 导航到菜单
const navigateToMenu = (menu: AdminMenu) => {
router.push(menu.path)
}
// 退出登录
const handleLogout = async () => {
try {
await adminStore.logout()
router.push('/admin/login')
} catch (error) {
console.error('退出登录失败:', error)
}
}
// 加载管理员信息
const loadAdminInfo = async () => {
try {
// 从adminAuth获取管理员信息
const adminInfo = adminAuth.getAdminInfo()
if (adminInfo) {
adminUser.value = adminInfo
}
} catch (error) {
console.error('加载管理员信息失败:', error)
}
}
// 加载系统统计数据
const loadStats = async () => {
try {
const response = await adminStore.getStats()
if (response && response.data) {
stats.value = response.data
}
} catch (error) {
console.error('加载系统统计失败:', error)
}
}
// 最近活动
const recentActivities = ref<any[]>([])
// 格式化时间
const formatTime = (time: string) => {
const date = new Date(time)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return date.toLocaleDateString('zh-CN')
}
}
// 加载最近活动
const loadRecentActivities = async () => {
try {
const response = await adminStore.getRecentActivities()
if (response && response.data.activities) {
recentActivities.value = response.data.activities
console.log('最近活动加载成功:', recentActivities.value)
} else {
console.error('最近活动数据格式错误:', response)
recentActivities.value = []
}
} catch (error) {
console.error('加载最近活动失败:', error)
recentActivities.value = []
}
}
onMounted(async () => {
await loadAdminInfo()
await loadStats()
await loadRecentActivities()
})
</script>