Files
hfb_sys/frontend/src/features/admin/views/AdminDashboardView.vue
T

527 lines
13 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.
<script setup lang="ts">
import {
Bell,
ChatDotRound,
Document,
Grid,
Loading,
Operation,
Refresh,
ScaleToOriginal,
ShoppingBag,
Shop,
Tickets,
User,
Wallet,
Warning,
} from '@element-plus/icons-vue'
import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { adminPath } from '@/shared/utils/adminPath'
import { disputeStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
const {
loading,
error,
data: dashboard,
load: loadDashboard,
} = useAdminTable<AdminDashboard>({
fetchFn: fetchAdminDashboard,
})
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Dashboard</p>
<h1>后台仪表盘</h1>
<p>实时监控平台运营数据,快速处理待办事项。</p>
</div>
<el-button @click="loadDashboard" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新数据
</el-button>
</div>
<!-- 加载状态 -->
<div v-if="loading && !dashboard" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="error && !dashboard" class="error-state">
<el-icon><Warning /></el-icon>
<span>{{ error }}</span>
<el-button @click="loadDashboard" type="primary">重试</el-button>
</div>
<!-- 核心指标卡片 -->
<div v-if="dashboard" class="metric-grid dashboard-metrics">
<div class="metric-card metric-card--primary">
<div class="metric-icon">
<el-icon><User /></el-icon>
</div>
<div class="metric-content">
<span>用户总数</span>
<strong>{{ dashboard.metrics.total_users }}</strong>
<small>实名 {{ dashboard.metrics.verified_users }} 人</small>
</div>
</div>
<div class="metric-card metric-card--success">
<div class="metric-icon">
<el-icon><ShoppingBag /></el-icon>
</div>
<div class="metric-content">
<span>发布商品</span>
<strong>{{ dashboard.metrics.total_listings }}</strong>
<small>已上架 {{ dashboard.metrics.published_listings }} 个</small>
</div>
</div>
<div class="metric-card metric-card--warning">
<div class="metric-icon">
<el-icon><Document /></el-icon>
</div>
<div class="metric-content">
<span>订单总数</span>
<strong>{{ dashboard.metrics.total_orders }}</strong>
<small>使用中 {{ dashboard.metrics.renting_orders }} 个</small>
</div>
</div>
<div class="metric-card metric-card--danger">
<div class="metric-icon">
<el-icon><Wallet /></el-icon>
</div>
<div class="metric-content">
<span>今日流水</span>
<strong>{{ formatCentWithSymbol(dashboard.metrics.today_ledger_amount_cent) }}</strong>
<small>今日订单 {{ dashboard.metrics.today_orders }} 个</small>
</div>
</div>
</div>
<!-- 待处理事项 & 快捷入口 -->
<div v-if="dashboard" class="dashboard-panels">
<div class="dashboard-panel">
<div class="panel-header">
<h2>
<el-icon><Bell /></el-icon>
待处理事项
</h2>
<el-tag type="warning" effect="dark" round>
{{
dashboard.pending.disputes +
dashboard.pending.listing_reviews +
dashboard.pending.pending_handoffs +
dashboard.pending.pending_return_confirms
}}
项
</el-tag>
</div>
<div class="pending-list">
<RouterLink class="pending-row" :to="adminPath('disputes')">
<div class="pending-info">
<span class="pending-label">待仲裁申诉</span>
<span class="pending-desc">需要处理的用户争议</span>
</div>
<strong :class="{ 'has-pending': dashboard.pending.disputes > 0 }">{{
dashboard.pending.disputes
}}</strong>
</RouterLink>
<RouterLink class="pending-row" :to="adminPath('listings/review')">
<div class="pending-info">
<span class="pending-label">待审核商品</span>
<span class="pending-desc">新提交的商品审核</span>
</div>
<strong :class="{ 'has-pending': dashboard.pending.listing_reviews > 0 }">{{
dashboard.pending.listing_reviews
}}</strong>
</RouterLink>
<div class="pending-row">
<div class="pending-info">
<span class="pending-label">待交接订单</span>
<span class="pending-desc">等待卖家交接</span>
</div>
<strong :class="{ 'has-pending': dashboard.pending.pending_handoffs > 0 }">{{
dashboard.pending.pending_handoffs
}}</strong>
</div>
<div class="pending-row">
<div class="pending-info">
<span class="pending-label">待结账确认</span>
<span class="pending-desc">等待双方确认</span>
</div>
<strong :class="{ 'has-pending': dashboard.pending.pending_return_confirms > 0 }">{{
dashboard.pending.pending_return_confirms
}}</strong>
</div>
</div>
</div>
<div class="dashboard-panel">
<div class="panel-header">
<h2>
<el-icon><Grid /></el-icon>
快捷入口
</h2>
</div>
<div class="quick-links">
<RouterLink class="quick-link" :to="adminPath('users')">
<el-icon><User /></el-icon>
<span>用户管理</span>
</RouterLink>
<RouterLink class="quick-link" :to="adminPath('orders')">
<el-icon><Tickets /></el-icon>
<span>订单管理</span>
</RouterLink>
<RouterLink class="quick-link" :to="adminPath('disputes')">
<el-icon><ScaleToOriginal /></el-icon>
<span>仲裁中心</span>
</RouterLink>
<RouterLink class="quick-link" :to="adminPath('listings')">
<el-icon><Shop /></el-icon>
<span>商品管理</span>
</RouterLink>
<RouterLink class="quick-link" :to="adminPath('chats')">
<el-icon><ChatDotRound /></el-icon>
<span>客服群聊</span>
</RouterLink>
<RouterLink class="quick-link" :to="adminPath('system-configs')">
<el-icon><Operation /></el-icon>
<span>系统配置</span>
</RouterLink>
</div>
</div>
</div>
<!-- 最近订单 -->
<div v-if="dashboard" class="dashboard-section">
<h2 class="section-title">
<el-icon><Tickets /></el-icon>
最近订单
</h2>
<el-table class="table-panel" :data="dashboard.recent_orders">
<el-table-column prop="order_no" label="订单号" min-width="210" />
<el-table-column prop="title" label="商品名称" min-width="170" />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag
:type="
row.status === 'completed'
? 'success'
: row.status === 'renting'
? 'primary'
: 'warning'
"
size="small"
effect="plain"
>
{{ orderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="rent_amount_cent" label="金额" width="100">
<template #default="{ row }">
<span class="amount">{{ formatCentWithSymbol(row.rent_amount_cent) }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
</el-table>
</div>
<!-- 最近申诉 -->
<div v-if="dashboard && dashboard.recent_disputes.length > 0" class="dashboard-section">
<h2 class="section-title">
<el-icon><Warning /></el-icon>
最近申诉
</h2>
<el-table class="table-panel" :data="dashboard.recent_disputes">
<el-table-column prop="order_no" label="订单号" min-width="210" />
<el-table-column prop="title" label="商品名称" min-width="170" />
<el-table-column prop="type" label="申诉类型" width="140" />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag
:type="row.status === 'resolved' ? 'success' : 'danger'"
size="small"
effect="plain"
>
{{ disputeStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default>
<RouterLink :to="adminPath('disputes')">
<el-button size="small" type="primary" link>处理</el-button>
</RouterLink>
</template>
</el-table-column>
</el-table>
</div>
</section>
</template>
<style scoped>
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
min-height: 300px;
color: #8f9bba;
}
.loading-state .el-icon {
font-size: 40px;
color: #4f7cff;
}
.loading-state span {
font-size: 14px;
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
min-height: 300px;
color: #ef4444;
}
.error-state .el-icon {
font-size: 40px;
}
.error-state span {
font-size: 14px;
color: #8f9bba;
}
.metric-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
}
.metric-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.metric-card--primary .metric-icon {
background: rgba(79, 124, 255, 0.1);
color: #4f7cff;
}
.metric-card--success .metric-icon {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.metric-card--warning .metric-icon {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.metric-card--danger .metric-icon {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.metric-content {
flex: 1;
min-width: 0;
}
.metric-content span {
font-size: 13px;
color: #8f9bba;
}
.metric-content strong {
display: block;
margin-top: 4px;
font-size: 28px;
font-weight: 800;
color: #1b2559;
line-height: 1;
}
.metric-content small {
display: block;
margin-top: 8px;
font-size: 12px;
color: #a3aed0;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.panel-header h2 {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 16px;
font-weight: 700;
color: #1b2559;
}
.panel-header h2 .el-icon {
color: #4f7cff;
}
.pending-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.pending-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-radius: 10px;
text-decoration: none;
transition: background 0.2s;
}
.pending-row:hover {
background: #f8f9fe;
}
.pending-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.pending-label {
font-size: 14px;
font-weight: 600;
color: #1b2559;
}
.pending-desc {
font-size: 12px;
color: #a3aed0;
}
.pending-row strong {
font-size: 20px;
font-weight: 800;
color: #10b981;
min-width: 32px;
text-align: center;
}
.pending-row strong.has-pending {
color: #ef4444;
}
.quick-links {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.quick-link {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 20px 16px;
border-radius: 12px;
background: #f8f9fe;
text-decoration: none;
transition: all 0.2s;
}
.quick-link:hover {
background: #eef2ff;
transform: translateY(-2px);
}
.quick-link .el-icon {
font-size: 24px;
color: #4f7cff;
}
.quick-link span {
font-size: 13px;
font-weight: 600;
color: #1b2559;
}
.dashboard-section {
margin-top: 28px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 16px;
font-size: 16px;
font-weight: 700;
color: #1b2559;
}
.section-title .el-icon {
color: #4f7cff;
}
.amount {
font-weight: 700;
color: #1b2559;
}
@media (max-width: 768px) {
.quick-links {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.metric-card {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.metric-icon {
width: 40px;
height: 40px;
font-size: 18px;
}
.metric-content strong {
font-size: 24px;
}
}
</style>