合作页面的内容

This commit is contained in:
李琦
2026-01-16 09:32:46 +08:00
parent a23b718865
commit 3bcef22487
15 changed files with 819 additions and 432 deletions

View File

@@ -10,11 +10,13 @@
},
"dependencies": {
"@types/markdown-it": "^14.1.2",
"echarts": "^6.0.0",
"highlight": "^0.2.4",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"md-editor-v3": "^6.3.1",
"vue": "^3.5.13",
"vue-echarts": "^8.0.1",
"vue-router": "^4.4.5"
},
"devDependencies": {

37
client/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
echarts:
specifier: ^6.0.0
version: 6.0.0
highlight:
specifier: ^0.2.4
version: 0.2.4
@@ -26,6 +29,9 @@ importers:
vue:
specifier: ^3.5.13
version: 3.5.26(typescript@5.6.3)
vue-echarts:
specifier: ^8.0.1
version: 8.0.1(echarts@6.0.0)(vue@3.5.26(typescript@5.6.3))
vue-router:
specifier: ^4.4.5
version: 4.6.4(vue@3.5.26(typescript@5.6.3))
@@ -692,6 +698,9 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
echarts@6.0.0:
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
electron-to-chromium@1.5.267:
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
@@ -1023,6 +1032,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
typescript@5.6.3:
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
engines: {node: '>=14.17'}
@@ -1083,6 +1095,12 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-echarts@8.0.1:
resolution: {integrity: sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==}
peerDependencies:
echarts: ^6.0.0
vue: ^3.3.0
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
@@ -1110,6 +1128,9 @@ packages:
engines: {node: '>= 0.10.0'}
hasBin: true
zrender@6.0.0:
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -1843,6 +1864,11 @@ snapshots:
dlv@1.1.3: {}
echarts@6.0.0:
dependencies:
tslib: 2.3.0
zrender: 6.0.0
electron-to-chromium@1.5.267: {}
entities@4.5.0: {}
@@ -2207,6 +2233,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
tslib@2.3.0: {}
typescript@5.6.3: {}
uc.micro@2.1.0: {}
@@ -2233,6 +2261,11 @@ snapshots:
vscode-uri@3.1.0: {}
vue-echarts@8.0.1(echarts@6.0.0)(vue@3.5.26(typescript@5.6.3)):
dependencies:
echarts: 6.0.0
vue: 3.5.26(typescript@5.6.3)
vue-router@4.6.4(vue@3.5.26(typescript@5.6.3)):
dependencies:
'@vue/devtools-api': 6.6.4
@@ -2260,3 +2293,7 @@ snapshots:
dependencies:
commander: 2.20.3
cssfilter: 0.0.10
zrender@6.0.0:
dependencies:
tslib: 2.3.0

View File

@@ -45,12 +45,7 @@
</li>
</ul>
</nav>
<div class="sidebar-footer">
<button @click="handleLogout" class="logout-btn">
<span class="nav-icon">🚪</span>
<span class="nav-text">退出登录</span>
</button>
</div>
<!-- Sidebar Footer removed as per request -->
</aside>
<!-- Main Content Area -->
@@ -63,9 +58,33 @@
</button>
</div>
<div class="header-right">
<div class="user-info">
<span class="username">{{ currentUser.username }}</span>
<span class="role">({{ currentUser.role }})</span>
<div class="user-dropdown relative" ref="dropdownRef">
<button class="user-info-btn" @click="toggleUserDropdown">
<span class="username">{{ currentUser.username }}</span>
<span class="role">({{ currentUser.role }})</span>
<span class="arrow" :class="{ 'rotated': isUserDropdownOpen }"></span>
</button>
<div v-if="isUserDropdownOpen" class="dropdown-menu">
<div class="dropdown-header">
<span class="user-email">管理员</span>
</div>
<ul class="dropdown-list">
<li>
<button @click="handleChangePassword" class="dropdown-item">
<span class="icon">🔒</span>
修改密码
</button>
</li>
<li class="divider"></li>
<li>
<button @click="handleLogout" class="dropdown-item text-red-400">
<span class="icon">🚪</span>
退出登录
</button>
</li>
</ul>
</div>
</div>
</div>
</header>
@@ -79,7 +98,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from '../../composables/useToast'
@@ -88,6 +107,26 @@ const toast = useToast()
const isSidebarOpen = ref(true)
const currentUser = ref(JSON.parse(localStorage.getItem('user') || '{}'))
// Dropdown logic
const isUserDropdownOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null)
const toggleUserDropdown = () => {
isUserDropdownOpen.value = !isUserDropdownOpen.value
}
const closeDropdown = (e: MouseEvent) => {
if (dropdownRef.value && !dropdownRef.value.contains(e.target as Node)) {
isUserDropdownOpen.value = false
}
}
const handleChangePassword = () => {
// Implement change password logic or navigate to page
toast.showToast('功能开发中...', 'info')
isUserDropdownOpen.value = false
}
interface MenuItem {
title: string
icon: string
@@ -173,6 +212,11 @@ onMounted(() => {
if (!token) {
router.push('/login')
}
document.addEventListener('click', closeDropdown)
})
onUnmounted(() => {
document.removeEventListener('click', closeDropdown)
})
</script>
@@ -184,6 +228,105 @@ onMounted(() => {
color: #ececec;
}
/* User Dropdown Styles */
.user-dropdown {
position: relative;
}
.user-info-btn {
display: flex;
align-items: center;
gap: 0.75rem;
background: transparent;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.375rem;
transition: background-color 0.2s;
}
.user-info-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.arrow {
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.5);
transition: transform 0.2s;
}
.arrow.rotated {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
width: 200px;
background: #1a1a1a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 100;
overflow: hidden;
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.dropdown-header {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.02);
}
.user-email {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.5);
display: block;
}
.dropdown-list {
list-style: none;
padding: 0.5rem 0;
margin: 0;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
cursor: pointer;
text-align: left;
transition: background-color 0.2s;
}
.dropdown-item:hover {
background-color: rgba(212, 179, 131, 0.1);
color: #d4b383;
}
.dropdown-item .icon {
font-size: 1.1rem;
}
.divider {
height: 1px;
background-color: rgba(255, 255, 255, 0.05);
margin: 0.5rem 0;
}
/* Sidebar Styles */
.admin-sidebar {
width: 250px;

View File

@@ -1,362 +0,0 @@
<template>
<div class="dashboard-container">
<h1 class="page-title">仪表盘</h1>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon users">👥</div>
<div class="stat-content">
<h3 class="stat-title">用户总数</h3>
<p class="stat-value">{{ stats.users }}</p>
<span class="stat-change positive">+2.5%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon posts">📝</div>
<div class="stat-content">
<h3 class="stat-title">文章总数</h3>
<p class="stat-value">{{ stats.posts }}</p>
<span class="stat-change positive">+5.2%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon works">🎨</div>
<div class="stat-content">
<h3 class="stat-title">作品总数</h3>
<p class="stat-value">{{ stats.works }}</p>
<span class="stat-change positive">+3.1%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon snippets">💻</div>
<div class="stat-content">
<h3 class="stat-title">代码片段</h3>
<p class="stat-value">{{ stats.snippets }}</p>
<span class="stat-change negative">-1.2%</span>
</div>
</div>
</div>
<!-- Recent Activities -->
<div class="dashboard-grid">
<div class="panel">
<h2 class="panel-title">最近操作</h2>
<div class="activity-list">
<div class="activity-item" v-for="activity in recentActivities" :key="activity.id">
<div class="activity-icon">
{{ activity.icon }}
</div>
<div class="activity-content">
<p class="activity-text">{{ activity.text }}</p>
<span class="activity-time">{{ activity.time }}</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="panel">
<h2 class="panel-title">快速操作</h2>
<div class="quick-actions">
<button class="action-btn" @click="router.push('/admin/posts/create')">
<span class="action-icon">📝</span>
<span class="action-text">新建文章</span>
</button>
<button class="action-btn" @click="router.push('/admin/works/create')">
<span class="action-icon">🎨</span>
<span class="action-text">新建作品</span>
</button>
<button class="action-btn" @click="router.push('/admin/snippets/create')">
<span class="action-icon">💻</span>
<span class="action-text">新建代码片段</span>
</button>
<button class="action-btn" @click="router.push('/admin/users/create')">
<span class="action-icon">👥</span>
<span class="action-text">新建用户</span>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getDashboardStats, getRecentActivities } from '../../services/api'
const router = useRouter()
// Stats data
const stats = ref({
users: 0,
posts: 0,
works: 0,
snippets: 0
})
// Recent activities data
const recentActivities = ref([])
// Fetch dashboard data
const fetchDashboardData = async () => {
try {
// Get stats
const statsData = await getDashboardStats()
stats.value = statsData
// Get recent activities
const activitiesData = await getRecentActivities()
recentActivities.value = activitiesData
} catch (error) {
console.error('Failed to fetch dashboard data:', error)
}
}
onMounted(() => {
fetchDashboardData()
})
</script>
<style scoped>
.dashboard-container {
width: 100%;
}
.page-title {
font-size: 1.75rem;
font-weight: 600;
color: #d4b383;
margin-bottom: 1.5rem;
font-family: 'Inter', sans-serif;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 1.5rem;
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(212, 179, 131, 0.3);
}
.stat-icon {
font-size: 2.5rem;
margin-right: 1rem;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
background-color: rgba(212, 179, 131, 0.1);
color: #d4b383;
}
.stat-icon.users {
background-color: rgba(59, 130, 246, 0.1);
color: rgba(59, 130, 246, 0.8);
}
.stat-icon.posts {
background-color: rgba(16, 185, 129, 0.1);
color: rgba(16, 185, 129, 0.8);
}
.stat-icon.works {
background-color: rgba(251, 191, 36, 0.1);
color: rgba(251, 191, 36, 0.8);
}
.stat-icon.snippets {
background-color: rgba(139, 92, 246, 0.1);
color: rgba(139, 92, 246, 0.8);
}
.stat-content {
flex: 1;
}
.stat-title {
font-size: 0.875rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
margin: 0 0 0.25rem 0;
font-family: 'Inter', sans-serif;
}
.stat-value {
font-size: 1.75rem;
font-weight: 600;
color: white;
margin: 0 0 0.25rem 0;
font-family: 'Inter', sans-serif;
}
.stat-change {
font-size: 0.75rem;
font-weight: 500;
}
.stat-change.positive {
color: #10b981;
}
.stat-change.negative {
color: #ef4444;
}
/* Dashboard Grid */
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1.5rem;
}
/* Panel Styles */
.panel {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.panel-title {
font-size: 1.25rem;
font-weight: 600;
color: #d4b383;
margin: 0 0 1rem 0;
font-family: 'Playfair Display', serif;
}
/* Activity List */
.activity-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.activity-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.activity-icon {
font-size: 1.25rem;
margin-top: 0.25rem;
width: auto;
height: auto;
background: transparent;
color: #d4b383;
}
.activity-content {
flex: 1;
}
.activity-text {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 0.25rem 0;
font-family: 'Inter', sans-serif;
}
.activity-time {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.4);
}
/* Quick Actions */
.quick-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.action-btn {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
text-align: left;
color: rgba(255, 255, 255, 0.8);
font-family: 'Inter', sans-serif;
}
.action-btn:hover {
background: rgba(212, 179, 131, 0.1);
border-color: #d4b383;
transform: translateX(4px);
box-shadow: 0 5px 15px rgba(212, 179, 131, 0.1);
}
.action-icon {
font-size: 1.25rem;
background: transparent;
color: #d4b383;
}
.action-text {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
/* Responsive Design */
@media (max-width: 1024px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<div class="dashboard-overview">
<!-- Top Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="stat-card">
<div class="stat-icon bg-blue-500/10 text-blue-400">📝</div>
<div class="stat-info">
<h3>总文章数</h3>
<p>{{ stats.posts || 0 }}</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon bg-green-500/10 text-green-400">👥</div>
<div class="stat-info">
<h3>今日访客 (UV)</h3>
<!-- 这里应该展示今日UV目前API返回的是列表需要处理 -->
<p>{{ todayUV }}</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon bg-purple-500/10 text-purple-400">📩</div>
<div class="stat-info">
<h3>合作咨询</h3>
<p>{{ stats.inquiryCount || 0 }}</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon bg-yellow-500/10 text-yellow-400">🎨</div>
<div class="stat-info">
<h3>作品展示</h3>
<!-- 暂时没有返回作品数先占位 -->
<p>-</p>
</div>
</div>
</div>
<!-- Charts Row 1 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- New Posts Trend -->
<div class="chart-card">
<h3 class="chart-title">近7天新增博文</h3>
<div class="h-80">
<v-chart class="chart" :option="postsTrendOption" autoresize />
</div>
</div>
<!-- UV Trend -->
<div class="chart-card">
<h3 class="chart-title">近7天访客趋势</h3>
<div class="h-80">
<v-chart class="chart" :option="uvTrendOption" autoresize />
</div>
</div>
</div>
<!-- Charts Row 2 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- User Regions -->
<div class="chart-card">
<h3 class="chart-title">用户地域分布</h3>
<div class="h-80">
<v-chart class="chart" :option="regionOption" autoresize />
</div>
</div>
<!-- Top Posts -->
<div class="chart-card">
<h3 class="chart-title">热门文章 Top 5</h3>
<div class="h-80">
<v-chart class="chart" :option="topPostsOption" autoresize />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import {
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
} from 'echarts/components'
import VChart from 'vue-echarts'
import { getDashboardStats } from '../../../services/api'
use([
CanvasRenderer,
BarChart,
LineChart,
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
])
const stats = ref<any>({})
const todayUV = computed(() => {
if (!stats.value.uvTrend || stats.value.uvTrend.length === 0) return 0
return stats.value.uvTrend[stats.value.uvTrend.length - 1].Count
})
// Chart Options
const postsTrendOption = computed(() => ({
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: stats.value.postsTrend?.map((i: any) => i.Date) || [],
axisLabel: { color: '#ccc' }
},
yAxis: {
type: 'value',
axisLabel: { color: '#ccc' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
series: [{
data: stats.value.postsTrend?.map((i: any) => i.Count) || [],
type: 'line',
smooth: true,
itemStyle: { color: '#3b82f6' },
areaStyle: { color: 'rgba(59, 130, 246, 0.1)' }
}]
}))
const uvTrendOption = computed(() => ({
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: stats.value.uvTrend?.map((i: any) => i.Date) || [],
axisLabel: { color: '#ccc' }
},
yAxis: {
type: 'value',
axisLabel: { color: '#ccc' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
series: [{
data: stats.value.uvTrend?.map((i: any) => i.Count) || [],
type: 'bar',
itemStyle: { color: '#10b981' },
barWidth: '40%'
}]
}))
const regionOption = computed(() => ({
tooltip: { trigger: 'item' },
legend: {
orient: 'vertical',
left: 'left',
textStyle: { color: '#ccc' }
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#050505',
borderWidth: 2
},
label: { show: false, position: 'center' },
emphasis: {
label: { show: true, fontSize: 20, fontWeight: 'bold', color: '#fff' }
},
labelLine: { show: false },
data: stats.value.userRegions?.map((i: any) => ({ value: i.Count, name: i.Region })) || []
}
]
}))
const topPostsOption = computed(() => ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'value',
axisLabel: { color: '#ccc' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
yAxis: {
type: 'category',
data: stats.value.topPosts?.map((i: any) => i.title.substring(0, 10) + '...') || [],
axisLabel: { color: '#ccc' }
},
series: [
{
name: '阅读量',
type: 'bar',
data: stats.value.topPosts?.map((i: any) => i.readCount) || [],
itemStyle: { color: '#d4b383' }
}
]
}))
const loadData = async () => {
try {
stats.value = await getDashboardStats()
} catch (error) {
console.error(error)
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.dashboard-overview {
width: 100%;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
.stat-icon {
width: 3rem;
height: 3rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.stat-info h3 {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.6);
margin: 0 0 0.25rem 0;
}
.stat-info p {
font-size: 1.5rem;
font-weight: 600;
color: #fff;
margin: 0;
}
.chart-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
}
.chart-title {
font-size: 1.1rem;
color: #d4b383;
margin: 0 0 1rem 0;
font-weight: 500;
}
.chart {
height: 100%;
width: 100%;
}
</style>

View File

@@ -21,7 +21,7 @@ const routes = [
children: [
// 仪表盘
{ path: '', redirect: '/admin/dashboard' },
{ path: 'dashboard', name: 'admin-dashboard', component: () => import('./pages/admin/Dashboard.vue') },
{ path: 'dashboard', name: 'admin-dashboard', component: () => import('./pages/admin/dashboard/Overview.vue') },
// 用户管理
{ path: 'users', name: 'admin-users', component: () => import('./pages/admin/Users.vue') },