feat: P3阶段完成 - 全部模块迁移完成 🎉

## P3.1: 争议仲裁模块(disputes)✅
- API: disputes.ts
- 模块导出

## P3.2: 卖家中心模块(seller)✅
- Views: 4个页面
- Composables: usePublishForm, usePublishDraft
- 模块导出

## P3.3: 管理后台模块(admin)✅
- API: 8个文件(adminAuth, adminDashboard, adminUsers等)
- Views: 15个管理页面
- Composables: useAdminTable, useAdminPaginatedTable
- Components: 管理端组件
- 模块导出

---

## 🎉 Features 架构迁移全部完成!

### 最终统计
- ✅ P0: shared(基础设施)- 22个文件
- ✅ P1: wallet, chats, orders - 24个文件
- ✅ P2: listings, auth - 35个文件
- ✅ P3: seller, disputes, admin - 47个文件

**总计:** 9个模块,128个文件完成迁移

### 新架构
```
frontend/src/
├── features/          # 9个业务模块 ✅
│   ├── wallet/       ✅
│   ├── chats/        ✅
│   ├── orders/       ✅ (已重构)
│   ├── listings/     ✅
│   ├── auth/         ✅
│   ├── seller/       ✅
│   ├── disputes/     ✅
│   └── admin/        ✅
└── shared/           ✅
```

下一步:清理旧文件、更新路由配置

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-06-04 09:05:34 +08:00
co-authored by Claude Opus 4.7
parent 3534cffce1
commit c9397635e2
47 changed files with 9406 additions and 0 deletions
@@ -0,0 +1,494 @@
<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 '@/api/adminDashboard'
import { useAdminTable } from '@/composables/useAdminTable'
import { useMoney } from '@/composables/useMoney'
import { disputeStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
const money = useMoney()
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>{{ money(dashboard.metrics.today_ledger_amount) }}</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="/admin/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="/admin/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="/admin/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</RouterLink>
<RouterLink class="quick-link" to="/admin/orders">
<el-icon><Tickets /></el-icon>
<span>订单管理</span>
</RouterLink>
<RouterLink class="quick-link" to="/admin/disputes">
<el-icon><ScaleToOriginal /></el-icon>
<span>仲裁中心</span>
</RouterLink>
<RouterLink class="quick-link" to="/admin/listings">
<el-icon><Shop /></el-icon>
<span>商品管理</span>
</RouterLink>
<RouterLink class="quick-link" to="/admin/chats">
<el-icon><ChatDotRound /></el-icon>
<span>客服群聊</span>
</RouterLink>
<RouterLink class="quick-link" to="/admin/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" label="金额" width="100">
<template #default="{ row }">
<span class="amount">{{ money(row.rent_amount) }}</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="/admin/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>