样式更改
This commit is contained in:
@@ -74,11 +74,6 @@
|
||||
<template #title>代理管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/withdrawals">
|
||||
<el-icon><CreditCard /></el-icon>
|
||||
<template #title>提现管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/announcements">
|
||||
<el-icon><Bell /></el-icon>
|
||||
<template #title>通知公告</template>
|
||||
|
||||
@@ -142,16 +142,6 @@ const routes = [
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: 'matching-management',
|
||||
// name: 'MatchingManagement',
|
||||
// component: () => import('@/views/MatchingManagement.vue'),
|
||||
// meta: {
|
||||
// title: '匹配管理 - 炬融圈',
|
||||
// icon: 'Connection',
|
||||
// requiresAdmin: true
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: 'agents',
|
||||
name: 'Agents',
|
||||
@@ -162,16 +152,6 @@ const routes = [
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'withdrawals',
|
||||
name: 'Withdrawals',
|
||||
component: () => import('@/views/Withdrawals.vue'),
|
||||
meta: {
|
||||
title: '提现管理 - 炬融圈',
|
||||
icon: 'CreditCard',
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'announcements',
|
||||
name: 'Announcements',
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
<template>
|
||||
<div class="matching-management">
|
||||
<div class="page-header">
|
||||
<h1>匹配管理</h1>
|
||||
<p class="description">管理和修复不合理的用户余额匹配</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-icon">
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.unreasonable_matches || 0 }}</div>
|
||||
<div class="stat-label">不合理匹配</div>
|
||||
<div class="stat-amount">¥{{ formatAmount(stats.unreasonable_amount || 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<el-icon><SuccessFilled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.reasonable_matches || 0 }}</div>
|
||||
<div class="stat-label">合理匹配</div>
|
||||
<div class="stat-amount">¥{{ formatAmount(stats.reasonable_amount || 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card info">
|
||||
<div class="stat-icon">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.system_matches || 0 }}</div>
|
||||
<div class="stat-label">系统匹配</div>
|
||||
<div class="stat-amount">昨日出款: ¥{{ formatAmount(yesterdayStats.total_outbound || 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Tools"
|
||||
@click="batchFixAll"
|
||||
:loading="batchFixing"
|
||||
:disabled="!stats.unreasonable_matches"
|
||||
>
|
||||
批量修复所有不合理匹配
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
@click="refreshData"
|
||||
:loading="loading"
|
||||
>
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 不合理匹配列表 -->
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<h2>不合理匹配记录</h2>
|
||||
<span class="subtitle">正余额用户被匹配的情况(造成公司直接亏损)</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="unreasonableMatches"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="allocation_id" label="分配ID" width="100" />
|
||||
|
||||
<el-table-column label="发起用户" width="150">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<div class="username">{{ scope.row.from_username }}</div>
|
||||
<div class="balance" :class="scope.row.from_user_balance < 0 ? 'negative' : 'positive'">
|
||||
余额: ¥{{ formatAmount(scope.row.from_user_balance) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="被匹配用户" width="150">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<div class="username">{{ scope.row.to_username }}</div>
|
||||
<div class="balance positive">
|
||||
余额: ¥{{ formatAmount(scope.row.to_user_balance) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="匹配金额" width="120">
|
||||
<template #default="scope">
|
||||
<div class="amount danger">¥{{ formatAmount(scope.row.amount) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="亏损金额" width="120">
|
||||
<template #default="scope">
|
||||
<div class="loss-amount">
|
||||
¥{{ formatAmount(Math.min(scope.row.amount, scope.row.to_user_balance)) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="outbound_date" label="出款日期" width="120" >
|
||||
<template #default="scope">
|
||||
{{ scope.row.outbound_date ? formatDateTime(scope.row.outbound_date) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="fixMatch(scope.row, 'reassign')"
|
||||
:loading="scope.row.fixing"
|
||||
>
|
||||
重新分配
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="fixMatch(scope.row, 'cancel')"
|
||||
:loading="scope.row.fixing"
|
||||
>
|
||||
取消匹配
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { WarningFilled, SuccessFilled, InfoFilled, Tools, Refresh } from '@element-plus/icons-vue';
|
||||
import api from '../utils/api';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const batchFixing = ref(false);
|
||||
const unreasonableMatches = ref([]);
|
||||
const stats = ref({});
|
||||
const yesterdayStats = ref({});
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 获取不合理匹配记录
|
||||
const fetchUnreasonableMatches = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await api.matching.getUnreasonableMatches({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
unreasonableMatches.value = response.data.data.matches.map(match => ({
|
||||
...match,
|
||||
fixing: false
|
||||
}));
|
||||
pagination.total = response.data.data.pagination.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取不合理匹配记录失败:', error);
|
||||
ElMessage.error('获取不合理匹配记录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取统计信息
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await api.matching.getMatchingStats();
|
||||
if (response.data.success) {
|
||||
stats.value = response.data.data.currentStats;
|
||||
yesterdayStats.value = response.data.data.yesterdayStats;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 修复单个匹配
|
||||
const fixMatch = async (match, action) => {
|
||||
try {
|
||||
const actionText = action === 'reassign' ? '重新分配给负余额用户' : '取消匹配';
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
`确定要${actionText}吗?这将立即生效。`,
|
||||
'确认操作',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
match.fixing = true;
|
||||
|
||||
const response = await api.matching.fixUnreasonableMatch(match.allocation_id, {
|
||||
action
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success(response.data.message);
|
||||
await refreshData();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('修复匹配失败:', error);
|
||||
ElMessage.error(error.response?.data?.message || '修复匹配失败');
|
||||
}
|
||||
} finally {
|
||||
match.fixing = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 批量修复所有不合理匹配
|
||||
const batchFixAll = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要批量修复所有 ${stats.value.unreasonable_matches} 条不合理匹配吗?\n\n这将:\n1. 优先重新分配给负余额用户\n2. 如无可用负余额用户则取消匹配\n3. 立即生效,无法撤销`,
|
||||
'批量修复确认',
|
||||
{
|
||||
confirmButtonText: '确定修复',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
);
|
||||
|
||||
batchFixing.value = true;
|
||||
|
||||
const response = await api.matching.fixAllUnreasonable();
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success(response.data.message);
|
||||
if (response.data.data.errors.length > 0) {
|
||||
console.warn('部分修复失败:', response.data.data.errors);
|
||||
}
|
||||
await refreshData();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('批量修复失败:', error);
|
||||
ElMessage.error(error.response?.data?.message || '批量修复失败');
|
||||
}
|
||||
} finally {
|
||||
batchFixing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = async () => {
|
||||
await Promise.all([
|
||||
fetchUnreasonableMatches(),
|
||||
fetchStats()
|
||||
]);
|
||||
};
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val;
|
||||
pagination.page = 1;
|
||||
fetchUnreasonableMatches();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val;
|
||||
fetchUnreasonableMatches();
|
||||
};
|
||||
|
||||
// 工具函数
|
||||
const formatAmount = (amount) => {
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTime) => {
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const types = {
|
||||
pending: 'warning',
|
||||
confirmed: 'danger',
|
||||
completed: 'success',
|
||||
cancelled: 'info'
|
||||
};
|
||||
return types[status] || 'info';
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待处理',
|
||||
confirmed: '已确认',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
refreshData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.matching-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
.stat-card.info {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.danger .stat-icon {
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.success .stat-icon {
|
||||
background: #f0fdf4;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.info .stat-icon {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-amount {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.table-header h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.balance.positive {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.balance.negative {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.amount.danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.loss-amount {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
<!-- 转账统计卡片 -->
|
||||
<el-card class="stats-card">
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-row :gutter="24" class="stats-row">
|
||||
<el-col :span="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.totalTransfers }}</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
<el-icon class="stat-icon"><Money /></el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-col :span="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.confirmedTransfers }}</div>
|
||||
@@ -26,16 +26,7 @@
|
||||
<el-icon class="stat-icon"><Check /></el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card bad-debt">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.badDebtTransfers }}</div>
|
||||
<div class="stat-label">坏账数量</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon"><Warning /></el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-col :span="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">¥{{ stats.totalAmount }}</div>
|
||||
@@ -66,11 +57,29 @@
|
||||
<el-input v-model="filters.search" placeholder="搜索用户名或姓名" clearable />
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-button type="primary" @click="fetchTransfers">搜索</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="success" @click="showCreateDialog">分配转账</el-button>
|
||||
<el-col :span="4">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-col>
|
||||
|
||||
<el-row style="margin-left: 150px;">
|
||||
<el-col :span="8">
|
||||
<el-button type="primary" @click="fetchTransfers">搜索</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="success" @click="showCreateDialog">分配转账</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
|
||||
</el-card>
|
||||
@@ -422,6 +431,9 @@ const currentUser = computed(() => userStore.user)
|
||||
const loading = ref(false)
|
||||
const transfers = ref([])
|
||||
const users = ref([])
|
||||
|
||||
const dateRange = ref([])
|
||||
|
||||
const stats = ref({
|
||||
totalTransfers: 0,
|
||||
pendingTransfers: 0,
|
||||
@@ -437,7 +449,9 @@ const stats = ref({
|
||||
|
||||
const filters = ref({
|
||||
status: '',
|
||||
search: ''
|
||||
search: '',
|
||||
start_date: dateRange.value[0] || '',
|
||||
end_date: dateRange.value[1] || '',
|
||||
});
|
||||
|
||||
const pagination = ref({
|
||||
@@ -525,6 +539,12 @@ const fetchTransfers = async () => {
|
||||
limit: pagination.value.limit,
|
||||
...filters.value
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (dateRange.value.length === 2) {
|
||||
params.start_date = dateRange.value[0] + ' 00:00:00'
|
||||
params.end_date = dateRange.value[1] + ' 23:59:59'
|
||||
}
|
||||
|
||||
const response = await api.transfers.getTransfers(params)
|
||||
transfers.value = response.data.data.transfers
|
||||
@@ -747,8 +767,11 @@ const resetFilters = () => {
|
||||
filters.value = {
|
||||
status: '',
|
||||
transfer_type: '',
|
||||
search: ''
|
||||
search: '',
|
||||
start_date: '',
|
||||
end_date: ''
|
||||
}
|
||||
dateRange.value = []
|
||||
pagination.value.page = 1
|
||||
fetchTransfers()
|
||||
}
|
||||
@@ -999,18 +1022,6 @@ onMounted(async () => {
|
||||
color: #e4e7ed;
|
||||
}
|
||||
|
||||
.stat-card.bad-debt {
|
||||
border-left: 4px solid #f56c6c;
|
||||
}
|
||||
|
||||
.stat-card.bad-debt .stat-icon {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.stat-card.bad-debt .stat-number {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.stat-card.overdue {
|
||||
border-left: 4px solid #e6a23c;
|
||||
}
|
||||
|
||||
@@ -1,687 +0,0 @@
|
||||
<template>
|
||||
<div class="withdrawals-container">
|
||||
<div class="page-header">
|
||||
<h2>提现管理</h2>
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_applications }}</div>
|
||||
<div class="stat-label">总申请数</div>
|
||||
</div>
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-value">{{ stats.pending_count }}</div>
|
||||
<div class="stat-label">待审核</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-value">{{ stats.completed_count }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
<div class="stat-card amount">
|
||||
<div class="stat-value">¥{{ stats.pending_amount }}</div>
|
||||
<div class="stat-label">待审核金额</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="状态" style="width: 200px;">
|
||||
<el-select v-model="filters.status" placeholder="全部状态" clearable @change="loadWithdrawals">
|
||||
<el-option label="待审核" value="pending" />
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="代理ID">
|
||||
<el-input v-model="filters.agent_id" placeholder="输入代理ID" clearable @change="loadWithdrawals" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="withdrawals"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="申请ID" width="80" />
|
||||
<el-table-column label="代理信息" width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">{{ row.agent_name }}</div>
|
||||
<div class="agent-details">
|
||||
<span class="agent-code">{{ row.agent_code }}</span>
|
||||
<span class="agent-phone">{{ row.agent_phone }}</span>
|
||||
</div>
|
||||
<div class="agent-region">{{ row.city_name }}{{ row.district_name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="提现金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="银行信息" width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="bank-info">
|
||||
<div>{{ row.bank_name }}</div>
|
||||
<div class="account">{{ maskBankAccount(row.bank_account) }}</div>
|
||||
<div class="holder">{{ row.account_holder }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="申请时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processed_at" label="处理时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.processed_at ? formatDateTime(row.processed_at) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="reviewWithdrawal(row, 'approve')"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="reviewWithdrawal(row, 'reject')"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'approved'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="completeWithdrawal(row)"
|
||||
>
|
||||
标记完成
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
size="small"
|
||||
@click="viewDetails(row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadWithdrawals"
|
||||
@size-change="loadWithdrawals"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核对话框 -->
|
||||
<el-dialog
|
||||
v-model="reviewDialog.visible"
|
||||
:title="reviewDialog.action === 'approve' ? '通过提现申请' : '拒绝提现申请'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="reviewDialog.form" label-width="80px">
|
||||
<el-form-item label="申请信息">
|
||||
<div class="review-info">
|
||||
<p><strong>代理:</strong> {{ reviewDialog.withdrawal?.agent_name }}</p>
|
||||
<p><strong>金额:</strong> ¥{{ reviewDialog.withdrawal?.amount }}</p>
|
||||
<p><strong>银行:</strong> {{ reviewDialog.withdrawal?.bank_name }}</p>
|
||||
<p><strong>账号:</strong> {{ reviewDialog.withdrawal?.bank_account }}</p>
|
||||
<p><strong>户名:</strong> {{ reviewDialog.withdrawal?.account_holder }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="reviewDialog.form.admin_note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="reviewDialog.action === 'approve' ? '审核通过备注(可选)' : '拒绝原因'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="reviewDialog.visible = false">取消</el-button>
|
||||
<el-button
|
||||
:type="reviewDialog.action === 'approve' ? 'success' : 'danger'"
|
||||
@click="confirmReview"
|
||||
:loading="reviewDialog.loading"
|
||||
>
|
||||
{{ reviewDialog.action === 'approve' ? '通过' : '拒绝' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailDialog.visible" title="提现申请详情" width="600px">
|
||||
<div class="detail-content" v-if="detailDialog.withdrawal">
|
||||
<div class="detail-section">
|
||||
<h4>申请信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<label>申请ID:</label>
|
||||
<span>{{ detailDialog.withdrawal.id }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>提现金额:</label>
|
||||
<span class="amount">¥{{ detailDialog.withdrawal.amount }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>申请时间:</label>
|
||||
<span>{{ formatDateTime(detailDialog.withdrawal.created_at) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>状态:</label>
|
||||
<el-tag :type="getStatusType(detailDialog.withdrawal.status)">
|
||||
{{ getStatusText(detailDialog.withdrawal.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>代理信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<label>代理姓名:</label>
|
||||
<span>{{ detailDialog.withdrawal.agent_name }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>代理编号:</label>
|
||||
<span>{{ detailDialog.withdrawal.agent_code }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>联系电话:</label>
|
||||
<span>{{ detailDialog.withdrawal.agent_phone }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>代理区域:</label>
|
||||
<span>{{ detailDialog.withdrawal.city_name }}{{ detailDialog.withdrawal.district_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>银行信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<label>银行名称:</label>
|
||||
<span>{{ detailDialog.withdrawal.bank_name }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>银行账号:</label>
|
||||
<span>{{ detailDialog.withdrawal.bank_account }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>开户人:</label>
|
||||
<span>{{ detailDialog.withdrawal.account_holder }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.withdrawal.apply_note">
|
||||
<h4>申请备注</h4>
|
||||
<p>{{ detailDialog.withdrawal.apply_note }}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.withdrawal.admin_note">
|
||||
<h4>管理员备注</h4>
|
||||
<p>{{ detailDialog.withdrawal.admin_note }}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.withdrawal.processed_at">
|
||||
<h4>处理信息</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<label>处理人:</label>
|
||||
<span>{{ detailDialog.withdrawal.processed_by_name || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>处理时间:</label>
|
||||
<span>{{ formatDateTime(detailDialog.withdrawal.processed_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Withdrawals',
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
const withdrawals = ref([])
|
||||
const total = ref(0)
|
||||
const stats = ref({
|
||||
total_applications: 0,
|
||||
pending_count: 0,
|
||||
approved_count: 0,
|
||||
completed_count: 0,
|
||||
rejected_count: 0,
|
||||
pending_amount: 0,
|
||||
completed_amount: 0
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
status: '',
|
||||
agent_id: ''
|
||||
})
|
||||
|
||||
const reviewDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
action: '',
|
||||
withdrawal: null,
|
||||
form: {
|
||||
admin_note: ''
|
||||
}
|
||||
})
|
||||
|
||||
const detailDialog = reactive({
|
||||
visible: false,
|
||||
withdrawal: null
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载提现申请列表
|
||||
*/
|
||||
const loadWithdrawals = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await api.get('/admin/withdrawals', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
withdrawals.value = response.data.data.withdrawals
|
||||
total.value = response.data.data.total
|
||||
stats.value = response.data.data.stats
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提现申请列表失败:', error)
|
||||
ElMessage.error('加载提现申请列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核提现申请
|
||||
*/
|
||||
const reviewWithdrawal = (withdrawal, action) => {
|
||||
reviewDialog.withdrawal = withdrawal
|
||||
reviewDialog.action = action
|
||||
reviewDialog.form.admin_note = ''
|
||||
reviewDialog.visible = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认审核
|
||||
*/
|
||||
const confirmReview = async () => {
|
||||
try {
|
||||
reviewDialog.loading = true
|
||||
|
||||
const response = await api.put(`/admin/withdrawals/${reviewDialog.withdrawal.id}/review`, {
|
||||
action: reviewDialog.action,
|
||||
admin_note: reviewDialog.form.admin_note
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success(response.data.message)
|
||||
reviewDialog.visible = false
|
||||
loadWithdrawals()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('审核失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '审核失败')
|
||||
} finally {
|
||||
reviewDialog.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记提现完成
|
||||
*/
|
||||
const completeWithdrawal = async (withdrawal) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认标记提现申请 #${withdrawal.id} 为已完成?`,
|
||||
'确认操作',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const response = await api.put(`/admin/withdrawals/${withdrawal.id}/complete`)
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success(response.data.message)
|
||||
loadWithdrawals()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('标记完成失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '标记完成失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看详情
|
||||
*/
|
||||
const viewDetails = async (withdrawal) => {
|
||||
try {
|
||||
const response = await api.get(`/admin/withdrawals/${withdrawal.id}`)
|
||||
|
||||
if (response.data.success) {
|
||||
detailDialog.withdrawal = response.data.data
|
||||
detailDialog.visible = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
ElMessage.error('获取详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态类型
|
||||
*/
|
||||
const getStatusType = (status) => {
|
||||
const types = {
|
||||
pending: 'warning',
|
||||
approved: 'info',
|
||||
rejected: 'danger',
|
||||
completed: 'success'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成'
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
const formatDateTime = (dateTime) => {
|
||||
if (!dateTime) return '-'
|
||||
return new Date(dateTime).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
/**
|
||||
* 银行账号脱敏
|
||||
*/
|
||||
const maskBankAccount = (account) => {
|
||||
if (!account || account.length < 8) return account
|
||||
return account.substring(0, 4) + '****' + account.substring(account.length - 4)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadWithdrawals()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
withdrawals,
|
||||
total,
|
||||
stats,
|
||||
pagination,
|
||||
filters,
|
||||
reviewDialog,
|
||||
detailDialog,
|
||||
loadWithdrawals,
|
||||
reviewWithdrawal,
|
||||
confirmReview,
|
||||
completeWithdrawal,
|
||||
viewDetails,
|
||||
getStatusType,
|
||||
getStatusText,
|
||||
formatDateTime,
|
||||
maskBankAccount
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.withdrawals-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
|
||||
.stat-card.pending {
|
||||
border-left-color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
border-left-color: #67c23a;
|
||||
}
|
||||
|
||||
.stat-card.amount {
|
||||
border-left-color: #f56c6c;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.agent-details {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.agent-code {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.agent-region {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.bank-info {
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.account {
|
||||
color: #909399;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.holder {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-weight: bold;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.review-info {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.review-info p {
|
||||
margin: 5px 0;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #303133;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.detail-item span {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user