优化财务管理整个ui

This commit is contained in:
yml2213
2026-08-20 16:32:57 +08:00
parent 6e4689edc4
commit 47ba202517
7 changed files with 4387 additions and 1569 deletions
@@ -1,4 +1,16 @@
<script setup lang="ts">
import {
Check,
Clock,
DocumentChecked,
Money,
Refresh,
Search,
Tickets,
User,
View,
Wallet,
} from '@element-plus/icons-vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -9,9 +21,10 @@ import {
confirmPayment,
type WithdrawalDetail,
} from '@/features/admin/api/adminWithdrawal'
import { formatCent } from '@/shared/utils/money'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { adminPath } from '@/shared/utils/adminPath'
import AdminTablePagination from '../components/AdminTablePagination.vue'
import WithdrawalDetailDialog from '../components/WithdrawalDetailDialog.vue'
const route = useRoute()
@@ -32,9 +45,9 @@ const showDetailDialog = ref(false)
const selectedWithdrawal = ref<WithdrawalDetail | null>(null)
const statusOptions = [
{ label: '全部', value: '' },
{ label: '全部状态', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '处理中 (待打款)', value: 'processing' },
{ label: '已完成', value: 'completed' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已取消', value: 'cancelled' },
@@ -51,6 +64,16 @@ const withdrawalReturnQuery = computed<Record<string, string>>(() => {
return query
})
const pendingCount = computed(() => withdrawals.value.filter(w => w.status === 'pending').length)
const processingCount = computed(
() => withdrawals.value.filter(w => w.status === 'processing').length
)
const completedAmount = computed(() =>
withdrawals.value
.filter(w => w.status === 'completed')
.reduce((sum, w) => sum + Number(w.actual_amount_cent || w.amount_cent || 0), 0)
)
onMounted(() => {
restoreListState()
void loadWithdrawals()
@@ -94,7 +117,7 @@ async function loadWithdrawals() {
withdrawals.value = result.items
total.value = result.total
} catch (error: any) {
ElMessage.error(error.response?.data?.message || '加载失败')
ElMessage.error(error.response?.data?.message || '提现列表加载失败')
} finally {
loading.value = false
}
@@ -105,12 +128,18 @@ function handleFilter() {
loadWithdrawals()
}
function resetFilters() {
filters.value.status = ''
filters.value.user_id = undefined
currentPage.value = 1
void loadWithdrawals()
}
function openDetail(withdrawal: WithdrawalDetail) {
selectedWithdrawal.value = withdrawal
showDetailDialog.value = true
}
/** 跳转订单管理页并预填号主搜索,快速查看该号主相关订单 */
function goToOwnerOrders(withdrawal: WithdrawalDetail) {
const keyword = withdrawal.user_phone?.trim() || String(withdrawal.user_id || '')
void router.push({
@@ -153,7 +182,7 @@ async function handleReview(withdrawal: WithdrawalDetail, approved: boolean) {
async function handleConfirmPayment(withdrawal: WithdrawalDetail) {
try {
const { value: remark } = await ElMessageBox.prompt(
'请输入打款备注(如:已通过支付宝转账)',
'请输入打款备注(如:已通过支付宝/微信转账)',
'确认打款',
{
confirmButtonText: '确认完成',
@@ -161,7 +190,7 @@ async function handleConfirmPayment(withdrawal: WithdrawalDetail) {
inputType: 'textarea',
inputPlaceholder: '输入打款备注...',
inputValidator: value => {
return value && value.trim().length > 0
return !!(value && value.trim().length > 0)
},
inputErrorMessage: '请输入打款备注',
}
@@ -183,15 +212,15 @@ async function handleConfirmPayment(withdrawal: WithdrawalDetail) {
function statusLabel(status: string) {
const labels: Record<string, string> = {
pending: '待审核',
processing: '处理中',
completed: '已完成',
processing: '待打款',
completed: '已打款',
rejected: '已拒绝',
cancelled: '已取消',
}
return labels[status] || status
}
function statusType(status: string) {
function statusType(status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
const types: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
pending: 'warning',
processing: 'primary',
@@ -215,29 +244,90 @@ function onDetailDialogSaved() {
showDetailDialog.value = false
loadWithdrawals()
}
function money(cent: number | undefined | null) {
return formatCentWithSymbol(cent || 0)
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Withdrawals</p>
<h1>提现管理</h1>
<p>审核用户提现申请并确认打款</p>
</div>
<div class="toolbar-actions">
<el-button @click="loadWithdrawals">刷新</el-button>
</div>
</div>
<div class="finance-page-container">
<!-- 头部卡片 -->
<header class="page-header-card">
<div class="header-main">
<div class="title-group">
<div class="eyebrow-tag">
<el-icon><Money /></el-icon>
<span>WITHDRAWALS</span>
</div>
<h1 class="page-title">提现审核</h1>
<p class="page-subtitle">审核号主用户钱包提现申请,核验转账账户并执行打款放款处理。</p>
</div>
<!-- 筛选器 -->
<el-card shadow="never" style="margin-bottom: 16px">
<el-form :model="filters" inline>
<el-form-item label="状态">
<div class="toolbar-actions">
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
<el-button type="primary" :icon="Search" :loading="loading" @click="handleFilter">
查询
</el-button>
</div>
</div>
</header>
<!-- 汇总指标卡片 -->
<section class="summary-metric-grid">
<div class="metric-card metric-card--review">
<div class="metric-icon-wrap amber">
<el-icon><Clock /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">本页待初审</span>
<strong class="metric-val amber-val">{{ pendingCount }} 笔</strong>
<span class="metric-sub-pill warning">待审核</span>
</div>
</div>
<div class="metric-card metric-card--pay">
<div class="metric-icon-wrap indigo">
<el-icon><Wallet /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">本页待打款</span>
<strong class="metric-val indigo-val">{{ processingCount }} 笔</strong>
<span class="metric-sub-pill info">已过审待放款</span>
</div>
</div>
<div class="metric-card metric-card--paid">
<div class="metric-icon-wrap emerald">
<el-icon><Check /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">本页已打款总额</span>
<strong class="metric-val emerald-val">{{ money(completedAmount) }}</strong>
<span class="metric-sub-pill success">已完成出款</span>
</div>
</div>
<div class="metric-card metric-card--total">
<div class="metric-icon-wrap slate">
<el-icon><Tickets /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">总申请记录</span>
<strong class="metric-val">{{ total }} 笔</strong>
<span class="metric-sub-text">所有提现记录</span>
</div>
</div>
</section>
<!-- 筛选卡片 -->
<section class="section-card filter-card">
<el-form class="filter-grid" label-position="top" @keyup.enter="handleFilter">
<el-form-item label="提现状态">
<el-select
v-model="filters.status"
placeholder="全部"
style="width: 150px"
placeholder="全部状态"
class="full-control"
@change="handleFilter"
>
<el-option
@@ -248,106 +338,140 @@ function onDetailDialogSaved() {
/>
</el-select>
</el-form-item>
<el-form-item label="用户ID">
<el-form-item label="用户 ID">
<el-input
v-model.number="filters.user_id"
placeholder="输入用户ID"
style="width: 150px"
placeholder="输入用户 ID 检索"
clearable
@clear="handleFilter"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFilter"> 查询 </el-button>
</el-form-item>
</el-form>
</el-card>
</section>
<!-- 提现列表 -->
<el-table v-loading="loading" class="table-panel" :data="withdrawals">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="withdraw_no" label="提现单号" min-width="180" />
<el-table-column label="用户" min-width="140">
<template #default="{ row }">
<div>{{ row.user_nickname }}</div>
<div style="font-size: 12px; color: #999">{{ row.user_phone }}</div>
</template>
</el-table-column>
<el-table-column label="提现金额" width="120" align="right">
<template #default="{ row }">
<span style="color: #f56c6c; font-weight: 600"> ¥{{ formatCent(row.amount_cent) }} </span>
</template>
</el-table-column>
<el-table-column label="收款方式" min-width="180">
<template #default="{ row }">
<div>
<el-tag size="small" style="margin-right: 4px">
{{ accountTypeLabel(row.account_type) }}
</el-tag>
{{ row.account_name }}
</div>
<div style="font-size: 12px; color: #666; margin-top: 4px">
{{ row.account_no }}
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusType(row.status)">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" width="160">
<template #default="{ row }">
{{ new Date(row.created_at).toLocaleString('zh-CN') }}
</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openDetail(row)"> 详情 </el-button>
<el-button size="small" type="primary" plain @click="goToOwnerOrders(row)">
号主订单
</el-button>
<el-button
v-if="row.status === 'pending'"
size="small"
type="success"
@click="handleReview(row, true)"
>
通过
</el-button>
<el-button
v-if="row.status === 'pending'"
size="small"
type="danger"
@click="handleReview(row, false)"
>
拒绝
</el-button>
<el-button
v-if="row.status === 'processing'"
size="small"
type="primary"
@click="handleConfirmPayment(row)"
>
确认打款
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 提现列表表格 -->
<section class="section-card table-section-card">
<div class="table-card-header">
<div class="table-header-title">
<el-icon class="header-icon"><DocumentChecked /></el-icon>
<span>提现申请明细</span>
</div>
<span class="count-badge">共 {{ total }} 条申请</span>
</div>
<el-pagination
v-if="total > pageSize"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: center"
@size-change="loadWithdrawals"
@current-change="loadWithdrawals"
/>
<div class="table-container">
<el-table
v-loading="loading"
class="custom-enterprise-table"
:data="withdrawals"
stripe
empty-text="暂无提现记录"
>
<el-table-column label="提现单号" min-width="190" fixed="left">
<template #default="{ row }">
<div class="withdraw-no-cell">
<span class="mono-code">{{ row.withdraw_no }}</span>
<small class="id-tag">#ID: {{ row.id }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="申请用户" min-width="160">
<template #default="{ row }">
<div class="user-cell">
<div class="user-name-line">
<el-icon class="user-icon"><User /></el-icon>
<span class="user-name">{{ row.user_nickname || '-' }}</span>
</div>
<small class="user-sub">{{ row.user_phone || `UID: ${row.user_id}` }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="提现金额" min-width="140" align="right">
<template #default="{ row }">
<div class="amount-stack">
<strong class="withdraw-amount">{{ money(row.amount_cent) }}</strong>
<small v-if="row.fee_cent" class="fee-text">扣费 {{ money(row.fee_cent) }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="收款账户信息" min-width="210">
<template #default="{ row }">
<div class="account-cell">
<div class="account-header">
<el-tag size="small" effect="plain" class="acc-tag">
{{ accountTypeLabel(row.account_type) }}
</el-tag>
<span class="acc-name">{{ row.account_name || '-' }}</span>
</div>
<small class="acc-no">{{ row.account_no }}</small>
<small v-if="row.bank_name" class="bank-name">{{ row.bank_name }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" class="status-pill">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" min-width="165">
<template #default="{ row }">
<span class="time-cell">{{ formatDateTime(row.created_at) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<template #default="{ row }">
<div class="action-cell">
<el-button link type="primary" :icon="View" @click="openDetail(row)">
详情
</el-button>
<el-button link type="primary" @click="goToOwnerOrders(row)"> 号主订单 </el-button>
<el-button
v-if="row.status === 'pending'"
link
type="success"
@click="handleReview(row, true)"
>
通过
</el-button>
<el-button
v-if="row.status === 'pending'"
link
type="danger"
@click="handleReview(row, false)"
>
拒绝
</el-button>
<el-button
v-if="row.status === 'processing'"
link
type="primary"
@click="handleConfirmPayment(row)"
>
确认打款
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<AdminTablePagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
@change="loadWithdrawals"
/>
</section>
<!-- 详情对话框 -->
<WithdrawalDetailDialog
@@ -356,52 +480,421 @@ function onDetailDialogSaved() {
:return-query="withdrawalReturnQuery"
@saved="onDetailDialogSaved"
/>
</section>
</div>
</template>
<style scoped>
.page {
padding: 24px;
.finance-page-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 4px 32px;
}
.page-header-row {
/* ================= 头部卡片 ================= */
.page-header-card {
background: #ffffff;
border-radius: 16px;
padding: 24px 28px;
box-shadow:
0 1px 3px rgba(15, 23, 42, 0.04),
0 4px 12px rgba(15, 23, 42, 0.02);
border: 1px solid rgba(226, 232, 240, 0.8);
}
.header-main {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.page-header {
flex: 1;
.title-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.eyebrow {
margin: 0 0 8px;
color: #909399;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
.eyebrow-tag {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.8px;
color: #7c3aed;
background: #f5f3ff;
padding: 3px 10px;
border-radius: 20px;
width: fit-content;
}
.page-header h1 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
}
.page-header p {
.page-title {
margin: 0;
color: #666;
font-size: 26px;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.5px;
}
.page-subtitle {
margin: 0;
font-size: 13.5px;
color: #64748b;
line-height: 1.5;
}
.toolbar-actions {
display: flex;
gap: 8px;
align-items: center;
gap: 10px;
}
.table-panel {
background: #fff;
border-radius: 8px;
/* ================= 核心指标卡片 ================= */
.summary-metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.metric-card {
background: #ffffff;
border-radius: 16px;
padding: 18px 20px;
border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow:
0 1px 3px rgba(15, 23, 42, 0.04),
0 4px 12px rgba(15, 23, 42, 0.02);
display: flex;
align-items: center;
gap: 16px;
position: relative;
overflow: hidden;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
.metric-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.metric-card--review::before {
background: linear-gradient(90deg, #f59e0b, #fbbf24);
}
.metric-card--pay::before {
background: linear-gradient(90deg, #6366f1, #818cf8);
}
.metric-card--paid::before {
background: linear-gradient(90deg, #10b981, #34d399);
}
.metric-card--total::before {
background: linear-gradient(90deg, #64748b, #94a3b8);
}
.metric-icon-wrap {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.metric-icon-wrap.amber {
background: #fffbeb;
color: #f59e0b;
}
.metric-icon-wrap.indigo {
background: #eef2ff;
color: #6366f1;
}
.metric-icon-wrap.emerald {
background: #ecfdf5;
color: #10b981;
}
.metric-icon-wrap.slate {
background: #f1f5f9;
color: #64748b;
}
.metric-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric-label {
font-size: 12.5px;
font-weight: 500;
color: #64748b;
}
.metric-val {
font-size: 22px;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.3px;
line-height: 1.2;
}
.amber-val {
color: #d97706;
}
.indigo-val {
color: #4f46e5;
}
.emerald-val {
color: #059669;
}
.metric-sub-pill {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
padding: 1px 6px;
border-radius: 4px;
width: fit-content;
}
.metric-sub-pill.warning {
background: #fffbeb;
color: #d97706;
}
.metric-sub-pill.info {
background: #eef2ff;
color: #4f46e5;
}
.metric-sub-pill.success {
background: #f0fdf4;
color: #16a34a;
}
.metric-sub-text {
font-size: 11.5px;
color: #94a3b8;
}
/* ================= 筛选面板 ================= */
.section-card {
background: #ffffff;
border-radius: 16px;
padding: 22px 24px;
border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow:
0 1px 3px rgba(15, 23, 42, 0.04),
0 4px 12px rgba(15, 23, 42, 0.02);
}
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px 16px;
}
.filter-grid :deep(.el-form-item) {
margin-bottom: 0;
}
.filter-grid :deep(.el-form-item__label) {
padding-bottom: 4px;
font-size: 12px;
font-weight: 600;
color: #64748b;
}
.full-control {
width: 100%;
}
/* ================= 表格卡片 ================= */
.table-section-card {
padding-bottom: 16px;
}
.table-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.table-header-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 700;
color: #1e293b;
}
.header-icon {
color: #7c3aed;
font-size: 18px;
}
.count-badge {
font-size: 12px;
font-weight: 600;
color: #7c3aed;
background: #f5f3ff;
padding: 3px 10px;
border-radius: 20px;
}
.table-container {
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.custom-enterprise-table :deep(.el-table__header-wrapper th) {
background-color: #f8fafc !important;
color: #475569;
font-size: 13px;
font-weight: 600;
padding: 12px 0;
border-bottom: 1px solid #e2e8f0;
}
.custom-enterprise-table :deep(.el-table__body-wrapper td) {
padding: 12px 0;
font-size: 13px;
color: #334155;
}
.withdraw-no-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.mono-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12.5px;
font-weight: 600;
color: #1e293b;
}
.id-tag {
font-size: 11px;
color: #94a3b8;
}
.user-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.user-name-line {
display: flex;
align-items: center;
gap: 4px;
font-weight: 600;
color: #0f172a;
}
.user-icon {
color: #94a3b8;
font-size: 13px;
}
.user-sub {
font-size: 11.5px;
color: #64748b;
}
.amount-stack {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.withdraw-amount {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 14px;
font-weight: 700;
color: #0f172a;
}
.fee-text {
font-size: 11px;
color: #94a3b8;
}
.account-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.account-header {
display: flex;
align-items: center;
gap: 6px;
}
.acc-tag {
font-weight: 600;
}
.acc-name {
font-size: 12.5px;
font-weight: 500;
color: #1e293b;
}
.acc-no {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11.5px;
color: #64748b;
}
.bank-name {
font-size: 11px;
color: #94a3b8;
}
.status-pill {
font-weight: 600;
}
.time-cell {
font-size: 12px;
color: #475569;
}
.action-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
flex-wrap: wrap;
}
</style>