527 lines
13 KiB
Vue
527 lines
13 KiB
Vue
<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>
|