286 lines
9.9 KiB
Vue
286 lines
9.9 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- 页面标题 -->
|
||
<div class="flex justify-between items-center">
|
||
<div>
|
||
<h1 class="text-2xl font-bold text-gray-900">仪表板</h1>
|
||
<div class="text-sm text-gray-500">
|
||
<span>今天是 {{ currentTime }}</span>
|
||
<span class="ml-4">欢迎回来!</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
<div v-for="stat in stats" :key="stat.title" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div :class="['w-8 h-8 rounded-md flex items-center justify-center', stat.iconBg]">
|
||
<component :is="stat.icon" class="w-5 h-5 text-white" />
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">{{ stat.title }}</dt>
|
||
<dd class="flex items-baseline">
|
||
<div class="text-2xl font-semibold text-gray-900">{{ stat.value }}</div>
|
||
<div :class="['ml-2 flex items-baseline text-sm font-semibold', stat.changeType === 'increase' ? 'text-green-600' : 'text-red-600']">
|
||
<ArrowUpIcon v-if="stat.changeType === 'increase'" class="self-center flex-shrink-0 h-4 w-4" />
|
||
<ArrowDownIcon v-else class="self-center flex-shrink-0 h-4 w-4" />
|
||
<span class="sr-only">{{ stat.changeType === 'increase' ? '增加' : '减少' }}</span>
|
||
{{ stat.change }}
|
||
</div>
|
||
</dd>
|
||
</dl>
|
||
<p class="mt-1 text-xs text-gray-500">
|
||
<span>{{ stat.changeText }}</span>
|
||
<span class="ml-1">较上月</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表和数据 -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<!-- 访问量趋势 -->
|
||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-semibold text-gray-900">访问量趋势</h3>
|
||
<select class="text-sm border border-gray-300 rounded-md px-3 py-1">
|
||
<option>最近7天</option>
|
||
<option>最近30天</option>
|
||
<option>最近90天</option>
|
||
</select>
|
||
</div>
|
||
<div class="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||
<p class="text-gray-500">图表区域 (可集成Chart.js 或其他图表库)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 最新活动 -->
|
||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||
<h3 class="text-lg font-semibold text-gray-900 mb-4">最新活动</h3>
|
||
<div class="space-y-4">
|
||
<div v-for="activity in activities" :key="activity.id" class="flex items-start space-x-3">
|
||
<div :class="['w-2 h-2 rounded-full mt-2', activity.color]"></div>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="text-sm text-gray-900">{{ activity.title }}</p>
|
||
<p class="text-xs text-gray-500">{{ activity.time }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 快速操作 -->
|
||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||
<h3 class="text-lg font-semibold text-gray-900 mb-4">快速操作</h3>
|
||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<router-link
|
||
v-for="action in quickActions"
|
||
:key="action.name"
|
||
:to="action.href"
|
||
class="flex flex-col items-center p-4 border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-md transition-all"
|
||
>
|
||
<component :is="action.icon" class="w-8 h-8 text-blue-600 mb-2" />
|
||
<span class="text-sm font-medium text-gray-900">{{ action.name }}</span>
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
|
||
// 图标组件
|
||
const UsersIcon = {
|
||
template: `<svg 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"></path></svg>`
|
||
}
|
||
|
||
const DocumentTextIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>`
|
||
}
|
||
|
||
const EyeIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>`
|
||
}
|
||
|
||
const ChatIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>`
|
||
}
|
||
|
||
const ArrowUpIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11l5-5m0 0l5 5m-5-5v12"></path></svg>`
|
||
}
|
||
|
||
const ArrowDownIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 13l-5 5m0 0l-5-5m5 5V6"></path></svg>`
|
||
}
|
||
|
||
const PlusIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>`
|
||
}
|
||
|
||
const NewspaperIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path></svg>`
|
||
}
|
||
|
||
const CogIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
|
||
}
|
||
|
||
// 响应式数据
|
||
const currentTime = ref('')
|
||
|
||
// 统计数据
|
||
const stats = ref([
|
||
{
|
||
title: '总用户数',
|
||
value: '2,651',
|
||
change: '+12%',
|
||
changeType: 'increase',
|
||
changeText: '新增 156 人',
|
||
icon: 'UsersIcon',
|
||
iconBg: 'bg-blue-500'
|
||
},
|
||
{
|
||
title: '文章总数',
|
||
value: '1,423',
|
||
change: '+8%',
|
||
changeType: 'increase',
|
||
changeText: '新增 32 篇',
|
||
icon: 'DocumentTextIcon',
|
||
iconBg: 'bg-green-500'
|
||
},
|
||
{
|
||
title: '总浏览量',
|
||
value: '45,210',
|
||
change: '+15%',
|
||
changeType: 'increase',
|
||
changeText: '增长 5,832',
|
||
icon: 'EyeIcon',
|
||
iconBg: 'bg-yellow-500'
|
||
},
|
||
{
|
||
title: '评论总数',
|
||
value: '892',
|
||
change: '-3%',
|
||
changeType: 'decrease',
|
||
changeText: '减少 28 条',
|
||
icon: 'ChatIcon',
|
||
iconBg: 'bg-purple-500'
|
||
}
|
||
])
|
||
|
||
// 最新活动
|
||
const activities = ref([
|
||
{
|
||
id: 1,
|
||
title: '用户 张三 发表了新文章《Vue3 开发指南》',
|
||
time: '2分钟前',
|
||
color: 'bg-blue-500'
|
||
},
|
||
{
|
||
id: 2,
|
||
title: '管理员更新了系统配置',
|
||
time: '15分钟前',
|
||
color: 'bg-green-500'
|
||
},
|
||
{
|
||
id: 3,
|
||
title: '新用户 李四 注册成功',
|
||
time: '1小时前',
|
||
color: 'bg-yellow-500'
|
||
},
|
||
{
|
||
id: 4,
|
||
title: '文章《React vs Vue》获得100个赞',
|
||
time: '2小时前',
|
||
color: 'bg-purple-500'
|
||
},
|
||
{
|
||
id: 5,
|
||
title: '系统备份完成',
|
||
time: '3小时前',
|
||
color: 'bg-gray-500'
|
||
}
|
||
])
|
||
|
||
// 快速操作
|
||
const quickActions = ref([
|
||
{
|
||
name: '新建文章',
|
||
href: '/admin/articles/create',
|
||
icon: 'PlusIcon'
|
||
},
|
||
{
|
||
name: '文章管理',
|
||
href: '/admin/articles',
|
||
icon: 'DocumentTextIcon'
|
||
},
|
||
{
|
||
name: '新闻管理',
|
||
href: '/admin/news',
|
||
icon: 'NewspaperIcon'
|
||
},
|
||
{
|
||
name: '系统设置',
|
||
href: '/admin/settings',
|
||
icon: 'CogIcon'
|
||
}
|
||
])
|
||
|
||
// 更新当前时间
|
||
const updateTime = () => {
|
||
const now = new Date()
|
||
currentTime.value = now.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
weekday: 'long'
|
||
})
|
||
}
|
||
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
updateTime()
|
||
// 每分钟更新一次时间
|
||
setInterval(updateTime, 60000)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 响应式优化 */
|
||
@media (max-width: 768px) {
|
||
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.grid-cols-1.lg\:grid-cols-2 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.grid-cols-2.md\:grid-cols-4 {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
/* 卡片悬停效果 */
|
||
.hover\:border-blue-300:hover {
|
||
border-color: #93c5fd;
|
||
}
|
||
|
||
.hover\:shadow-md:hover {
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
/* 过渡动画 */
|
||
.transition-all {
|
||
transition-property: all;
|
||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||
transition-duration: 150ms;
|
||
}
|
||
</style> |