Files
nl_cms-view/src/views/admin/Dashboard.vue
2025-07-29 12:43:09 +08:00

286 lines
9.9 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="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>