样式更改

This commit is contained in:
dzl
2025-09-17 10:44:11 +08:00
parent 6aedba23bc
commit 7e070fb821
5 changed files with 42 additions and 1275 deletions

View File

@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>