653 lines
14 KiB
Vue
653 lines
14 KiB
Vue
<template>
|
||
<div class="admin-dashboard">
|
||
<!-- 页面头部 -->
|
||
<div class="dashboard-header">
|
||
<div class="header-left">
|
||
<h2>{{ t('admin.dashboard.title') }}</h2>
|
||
<p class="subtitle">{{ t('admin.dashboard.subtitle') }}</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button type="primary" @click="refreshData">
|
||
<el-icon><Refresh /></el-icon>
|
||
{{ t('admin.dashboard.refresh') }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-grid">
|
||
<el-card class="stat-card" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon users">
|
||
<el-icon><UserFilled /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-number">{{ stats.totalUsers }}</div>
|
||
<div class="stat-label">{{ t('admin.dashboard.stats.totalUsers') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card class="stat-card" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon orders">
|
||
<el-icon><ShoppingCart /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-number">{{ stats.totalOrders }}</div>
|
||
<div class="stat-label">{{ t('admin.dashboard.stats.totalOrders') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card class="stat-card" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon reviews">
|
||
<el-icon><Document /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-number">{{ stats.pendingReviews }}</div>
|
||
<div class="stat-label">{{ t('admin.dashboard.stats.pendingReviews') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card class="stat-card" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon revenue">
|
||
<el-icon><Money /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-number">${{ formatNumber(stats.revenue) }}</div>
|
||
<div class="stat-label">{{ t('admin.dashboard.stats.revenue') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<div class="charts-section">
|
||
<el-row :gutter="24">
|
||
<!-- 销售趋势图表 -->
|
||
<el-col :xs="24" :lg="12">
|
||
<el-card class="chart-card" shadow="hover">
|
||
<template #header>
|
||
<div class="chart-header">
|
||
<span>{{ t('admin.dashboard.charts.salesTrend') }}</span>
|
||
<el-select v-model="chartPeriod" size="small" style="width: 120px">
|
||
<el-option label="最近7天" value="7d" />
|
||
<el-option label="最近30天" value="30d" />
|
||
<el-option label="最近90天" value="90d" />
|
||
</el-select>
|
||
</div>
|
||
</template>
|
||
<div class="chart-container">
|
||
<div class="simple-chart">
|
||
<div
|
||
v-for="(point, index) in salesData"
|
||
:key="index"
|
||
class="chart-bar"
|
||
:style="{
|
||
height: `${(point.value / maxSalesValue) * 200}px`,
|
||
background: `linear-gradient(to top, #6b46c1, #a78bfa)`
|
||
}"
|
||
:title="`${point.date}: $${formatNumber(point.value)}`"
|
||
></div>
|
||
</div>
|
||
<div class="chart-labels">
|
||
<span
|
||
v-for="(point, index) in salesData"
|
||
:key="index"
|
||
class="chart-label"
|
||
>
|
||
{{ point.date }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 订单状态分布 -->
|
||
<el-col :xs="24" :lg="12">
|
||
<el-card class="chart-card" shadow="hover">
|
||
<template #header>
|
||
<div class="chart-header">
|
||
<span>{{ t('admin.dashboard.charts.orderStatus') }}</span>
|
||
</div>
|
||
</template>
|
||
<div class="chart-container">
|
||
<div class="pie-chart">
|
||
<div class="pie-visual"></div>
|
||
<div class="pie-center"></div>
|
||
</div>
|
||
<div class="pie-legend">
|
||
<div class="legend-item">
|
||
<div class="legend-color completed"></div>
|
||
<span>{{ t('admin.orders.statusOptions.completed') }} ({{ orderStats.completed }}%)</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color pending"></div>
|
||
<span>{{ t('admin.orders.statusOptions.pending') }} ({{ orderStats.pending }}%)</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color cancelled"></div>
|
||
<span>{{ t('admin.orders.statusOptions.cancelled') }} ({{ orderStats.cancelled }}%)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 最近活动 -->
|
||
<div class="recent-activity">
|
||
<el-card class="activity-card" shadow="hover">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>{{ t('admin.dashboard.charts.recentActivity') }}</span>
|
||
<el-button text type="primary" size="small">{{ t('common.viewAll') }}</el-button>
|
||
</div>
|
||
</template>
|
||
<div class="activity-list">
|
||
<div
|
||
v-for="activity in recentActivities"
|
||
:key="activity.id"
|
||
class="activity-item"
|
||
>
|
||
<div class="activity-icon" :class="activity.type">
|
||
<el-icon><component :is="getActivityIcon(activity.type)" /></el-icon>
|
||
</div>
|
||
<div class="activity-content">
|
||
<div class="activity-title">{{ activity.title }}</div>
|
||
<div class="activity-time">{{ formatTime(activity.time) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import {
|
||
UserFilled,
|
||
ShoppingCart,
|
||
Document,
|
||
Money,
|
||
User,
|
||
Bell,
|
||
Setting
|
||
} from '@element-plus/icons-vue'
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 响应式数据
|
||
const chartPeriod = ref('7d')
|
||
|
||
const stats = ref({
|
||
totalUsers: 12568,
|
||
totalOrders: 3421,
|
||
pendingReviews: 89,
|
||
revenue: 125678
|
||
})
|
||
|
||
const salesData = ref([
|
||
{ date: '11/12', value: 12000 },
|
||
{ date: '11/13', value: 18000 },
|
||
{ date: '11/14', value: 15000 },
|
||
{ date: '11/15', value: 22000 },
|
||
{ date: '11/16', value: 19000 },
|
||
{ date: '11/17', value: 25000 },
|
||
{ date: '11/18', value: 28000 }
|
||
])
|
||
|
||
const orderStats = ref({
|
||
completed: 75,
|
||
pending: 20,
|
||
cancelled: 5
|
||
})
|
||
|
||
const recentActivities = ref([
|
||
{
|
||
id: 1,
|
||
type: 'user',
|
||
title: '新用户注册:zhangsan@example.com',
|
||
time: new Date(Date.now() - 1000 * 60 * 5)
|
||
},
|
||
{
|
||
id: 2,
|
||
type: 'order',
|
||
title: '新订单:#ORD-2025-001',
|
||
time: new Date(Date.now() - 1000 * 60 * 15)
|
||
},
|
||
{
|
||
id: 3,
|
||
type: 'review',
|
||
title: '内容审核:用户反馈',
|
||
time: new Date(Date.now() - 1000 * 60 * 30)
|
||
},
|
||
{
|
||
id: 4,
|
||
type: 'system',
|
||
title: '系统更新完成',
|
||
time: new Date(Date.now() - 1000 * 60 * 60)
|
||
}
|
||
])
|
||
|
||
// 计算属性
|
||
const maxSalesValue = computed(() => {
|
||
return Math.max(...salesData.value.map(item => item.value))
|
||
})
|
||
|
||
// 方法
|
||
const formatNumber = (num) => {
|
||
return new Intl.NumberFormat('zh-CN').format(num)
|
||
}
|
||
|
||
const formatTime = (time) => {
|
||
const now = new Date()
|
||
const diff = Math.floor((now - time) / 1000)
|
||
|
||
if (diff < 60) return '刚刚'
|
||
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
|
||
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
|
||
return `${Math.floor(diff / 86400)}天前`
|
||
}
|
||
|
||
const getActivityIcon = (type) => {
|
||
const icons = {
|
||
user: User,
|
||
order: ShoppingCart,
|
||
review: Document,
|
||
system: Setting
|
||
}
|
||
return icons[type] || Bell
|
||
}
|
||
|
||
// 刷新数据方法
|
||
const refreshData = () => {
|
||
// 模拟数据刷新
|
||
stats.value.totalUsers += Math.floor(Math.random() * 10)
|
||
stats.value.totalOrders += Math.floor(Math.random() * 5)
|
||
stats.value.pendingReviews = Math.floor(Math.random() * 100)
|
||
stats.value.revenue += Math.floor(Math.random() * 1000)
|
||
|
||
// 刷新销售数据
|
||
salesData.value = salesData.value.map(item => ({
|
||
...item,
|
||
value: item.value + Math.floor(Math.random() * 2000) - 1000
|
||
}))
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 模拟数据刷新
|
||
setInterval(() => {
|
||
stats.value.totalUsers += Math.floor(Math.random() * 3)
|
||
stats.value.totalOrders += Math.floor(Math.random() * 2)
|
||
stats.value.pendingReviews = Math.floor(Math.random() * 100)
|
||
}, 30000)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.admin-dashboard {
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 页面头部样式 */
|
||
.dashboard-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 24px;
|
||
padding: 24px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.header-left h2 {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.subtitle {
|
||
color: #6b7280;
|
||
font-size: 16px;
|
||
margin: 0;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.stat-card {
|
||
border: none;
|
||
border-radius: 12px;
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.stat-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.stat-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.stat-icon.users {
|
||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||
}
|
||
|
||
.stat-icon.orders {
|
||
background: linear-gradient(135deg, #10b981, #059669);
|
||
}
|
||
|
||
.stat-icon.reviews {
|
||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||
}
|
||
|
||
.stat-icon.revenue {
|
||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* 图表区域 */
|
||
.charts-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.chart-card {
|
||
border: none;
|
||
border-radius: 12px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.chart-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.chart-container {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
/* 销售趋势图表 */
|
||
.simple-chart {
|
||
display: flex;
|
||
align-items: end;
|
||
justify-content: space-between;
|
||
height: 200px;
|
||
padding: 0 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.chart-bar {
|
||
width: 20px;
|
||
border-radius: 4px 4px 0 0;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.chart-bar:hover {
|
||
opacity: 1;
|
||
transform: scaleY(1.05);
|
||
}
|
||
|
||
.chart-labels {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.chart-label {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* 饼图 */
|
||
.pie-chart {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto 16px;
|
||
position: relative;
|
||
}
|
||
|
||
.pie-visual {
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 50%;
|
||
background: conic-gradient(
|
||
#10b981 0deg 270deg,
|
||
#f59e0b 270deg 342deg,
|
||
#ef4444 342deg 360deg
|
||
);
|
||
position: relative;
|
||
}
|
||
|
||
.pie-center {
|
||
position: absolute;
|
||
width: 60px;
|
||
height: 60px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.pie-legend {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
.legend-color {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.legend-color.completed {
|
||
background: #10b981;
|
||
}
|
||
|
||
.legend-color.pending {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
.legend-color.cancelled {
|
||
background: #ef4444;
|
||
}
|
||
|
||
/* 最近活动 */
|
||
.activity-card {
|
||
border: none;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.activity-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.activity-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.activity-item:hover {
|
||
background: #f9fafb;
|
||
}
|
||
|
||
.activity-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.activity-icon.user {
|
||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||
}
|
||
|
||
.activity-icon.order {
|
||
background: linear-gradient(135deg, #10b981, #059669);
|
||
}
|
||
|
||
.activity-icon.review {
|
||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||
}
|
||
|
||
.activity-icon.system {
|
||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||
}
|
||
|
||
.activity-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.activity-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.activity-time {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.stats-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.stat-content {
|
||
gap: 12px;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.simple-chart {
|
||
height: 150px;
|
||
}
|
||
|
||
.chart-bar {
|
||
width: 16px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.admin-dashboard {
|
||
padding: 0;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.stat-content {
|
||
gap: 8px;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.simple-chart {
|
||
height: 120px;
|
||
}
|
||
}
|
||
</style> |