新增资金出款查询与财务统计
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
<script setup lang="ts">
|
||||
import { Search, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import {
|
||||
fetchFinanceDisbursements,
|
||||
type FinanceDisbursementItem,
|
||||
type FinanceDisbursementListSummary,
|
||||
} from '@/features/admin/api/adminFinance'
|
||||
import { fetchAdminWithdrawal, type WithdrawalDetail } from '@/features/admin/api/adminWithdrawal'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import { formatDateTime, formatInputDate } from '@/shared/utils/time'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
import WithdrawalDetailDialog from '../components/WithdrawalDetailDialog.vue'
|
||||
|
||||
const adminSession = useAdminSessionStore()
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const items = ref<FinanceDisbursementItem[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const showWithdrawalDetail = ref(false)
|
||||
const selectedWithdrawal = ref<WithdrawalDetail | null>(null)
|
||||
const summary = ref<FinanceDisbursementListSummary>(emptySummary())
|
||||
const filters = reactive({
|
||||
date_type: 'created',
|
||||
start_date: defaultStartDate(),
|
||||
end_date: formatInputDate(new Date()),
|
||||
source_type: '',
|
||||
status: '',
|
||||
business_no: '',
|
||||
keyword: '',
|
||||
user_id: '',
|
||||
})
|
||||
|
||||
onMounted(loadItems)
|
||||
|
||||
async function loadItems() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await fetchFinanceDisbursements({
|
||||
...filters,
|
||||
page: currentPage.value,
|
||||
page_size: currentPageSize.value,
|
||||
})
|
||||
items.value = result.items
|
||||
total.value = result.total
|
||||
summary.value = result.summary
|
||||
} catch (error) {
|
||||
items.value = []
|
||||
total.value = 0
|
||||
summary.value = emptySummary()
|
||||
ElMessage.error(readError(error, '资金出款明细加载失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
currentPage.value = 1
|
||||
void loadItems()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.date_type = 'created'
|
||||
filters.start_date = defaultStartDate()
|
||||
filters.end_date = formatInputDate(new Date())
|
||||
filters.source_type = ''
|
||||
filters.status = ''
|
||||
filters.business_no = ''
|
||||
filters.keyword = ''
|
||||
filters.user_id = ''
|
||||
currentPage.value = 1
|
||||
void loadItems()
|
||||
}
|
||||
|
||||
async function openWithdrawalDetail(row: FinanceDisbursementItem) {
|
||||
if (!row.withdrawal_id || detailLoading.value) return
|
||||
detailLoading.value = true
|
||||
try {
|
||||
selectedWithdrawal.value = await fetchAdminWithdrawal(row.withdrawal_id)
|
||||
showWithdrawalDetail.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '提现详情加载失败'))
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onWithdrawalSaved() {
|
||||
showWithdrawalDetail.value = false
|
||||
selectedWithdrawal.value = null
|
||||
void loadItems()
|
||||
}
|
||||
|
||||
function emptySummary(): FinanceDisbursementListSummary {
|
||||
return {
|
||||
record_count: 0,
|
||||
paid_amount_cent: 0,
|
||||
paid_count: 0,
|
||||
payment_pending_amount_cent: 0,
|
||||
payment_pending_count: 0,
|
||||
review_pending_amount_cent: 0,
|
||||
review_pending_count: 0,
|
||||
withdrawal_fee_amount_cent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function defaultStartDate() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 29)
|
||||
return formatInputDate(date)
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return formatCentWithSymbol(Number(value || 0))
|
||||
}
|
||||
|
||||
function sourceLabel(source: string) {
|
||||
return source === 'platform_managed' ? '平台代管' : '用户提现'
|
||||
}
|
||||
|
||||
function sourceType(source: string) {
|
||||
return source === 'platform_managed' ? 'primary' : 'info'
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
review_pending: '待审核',
|
||||
payment_pending: '待打款',
|
||||
paid: '已打款',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
function statusType(status: string) {
|
||||
if (status === 'paid') return 'success'
|
||||
if (status === 'review_pending' || status === 'payment_pending') return 'warning'
|
||||
if (status === 'rejected') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function accountTypeLabel(type: string) {
|
||||
const labels: Record<string, string> = {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信',
|
||||
bank: '银行卡',
|
||||
}
|
||||
return labels[type] || type || '线下约定'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page disbursement-page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Disbursements</p>
|
||||
<h1>资金出款</h1>
|
||||
<p>查询平台代管线下结算与用户提现的应付、待办和实际打款记录。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="search">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid disbursement-metrics">
|
||||
<div class="metric-card">
|
||||
<span>筛选记录</span>
|
||||
<strong>{{ summary.record_count }} 笔</strong>
|
||||
<small>当前筛选条件</small>
|
||||
</div>
|
||||
<div class="metric-card paid-metric">
|
||||
<span>已打款</span>
|
||||
<strong>{{ money(summary.paid_amount_cent) }}</strong>
|
||||
<small>{{ summary.paid_count }} 笔实际出款</small>
|
||||
</div>
|
||||
<div class="metric-card pending-metric">
|
||||
<span>待打款</span>
|
||||
<strong>{{ money(summary.payment_pending_amount_cent) }}</strong>
|
||||
<small>{{ summary.payment_pending_count }} 笔已进入打款阶段</small>
|
||||
</div>
|
||||
<div class="metric-card review-metric">
|
||||
<span>提现待审核</span>
|
||||
<strong>{{ money(summary.review_pending_amount_cent) }}</strong>
|
||||
<small>{{ summary.review_pending_count }} 笔待审核</small>
|
||||
</div>
|
||||
<div class="metric-card fee-metric">
|
||||
<span>已完成提现手续费</span>
|
||||
<strong>{{ money(summary.withdrawal_fee_amount_cent) }}</strong>
|
||||
<small>随筛选结果统计</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form class="filter-panel disbursement-filters" label-position="top" @keyup.enter="search">
|
||||
<el-form-item label="日期类型">
|
||||
<el-select v-model="filters.date_type" class="full-control">
|
||||
<el-option label="业务形成日期" value="created" />
|
||||
<el-option label="实际打款日期" value="paid" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期">
|
||||
<el-date-picker
|
||||
v-model="filters.start_date"
|
||||
class="full-control"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束日期">
|
||||
<el-date-picker
|
||||
v-model="filters.end_date"
|
||||
class="full-control"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="出款类型">
|
||||
<el-select
|
||||
v-model="filters.source_type"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="平台代管" value="platform_managed" />
|
||||
<el-option label="用户提现" value="withdrawal" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="出款状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部状态" class="full-control">
|
||||
<el-option label="待审核" value="review_pending" />
|
||||
<el-option label="待打款" value="payment_pending" />
|
||||
<el-option label="已打款" value="paid" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务单号">
|
||||
<el-input v-model="filters.business_no" clearable placeholder="订单号或提现单号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款人">
|
||||
<el-input v-model="filters.keyword" clearable placeholder="姓名、手机号或账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input v-model="filters.user_id" clearable placeholder="按用户 ID" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="items" empty-text="暂无出款记录">
|
||||
<el-table-column label="业务" min-width="205" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="business-cell">
|
||||
<el-tag size="small" :type="sourceType(row.source_type)">
|
||||
{{ sourceLabel(row.source_type) }}
|
||||
</el-tag>
|
||||
<RouterLink
|
||||
v-if="row.order_id"
|
||||
class="business-no"
|
||||
:to="adminPath(`orders/${row.order_id}`)"
|
||||
>
|
||||
{{ row.business_no }}
|
||||
</RouterLink>
|
||||
<strong v-else class="business-no">{{ row.business_no }}</strong>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款人" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<div class="stack-cell">
|
||||
<strong>{{ row.payee_name || '-' }}</strong>
|
||||
<small v-if="row.payee_phone">{{ row.payee_phone }}</small>
|
||||
<small v-else-if="row.source_type === 'withdrawal'">用户 ID {{ row.user_id }}</small>
|
||||
<small v-else>联系方式未登记</small>
|
||||
<small v-if="row.uploader_name">上传:{{ row.uploader_name }}</small>
|
||||
<small v-if="row.source_channel">渠道:{{ row.source_channel }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款账户" min-width="205">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.source_type === 'withdrawal'" class="stack-cell">
|
||||
<span>{{ accountTypeLabel(row.account_type) }} · {{ row.account_name || '-' }}</span>
|
||||
<small>{{ row.account_no || '-' }}</small>
|
||||
<small v-if="row.bank_name">{{ row.bank_name }}</small>
|
||||
</div>
|
||||
<span v-else class="muted-text">线下约定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="155" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="amount-stack">
|
||||
<strong>{{ money(row.actual_amount_cent) }}</strong>
|
||||
<small v-if="row.source_type === 'withdrawal'">
|
||||
申请 {{ money(row.amount_cent) }} / 手续费 {{ money(row.fee_cent) }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="105" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="形成时间" min-width="175">
|
||||
<template #default="{ row }">{{ formatDateTime(row.business_created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="打款时间" min-width="175">
|
||||
<template #default="{ row }">{{ formatDateTime(row.paid_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理人" min-width="120">
|
||||
<template #default="{ row }">{{ row.operator_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.withdrawal_id && adminSession.hasPermission('withdrawal:detail')"
|
||||
link
|
||||
type="primary"
|
||||
:icon="View"
|
||||
:loading="detailLoading"
|
||||
@click="openWithdrawalDetail(row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<RouterLink v-else-if="row.order_id" :to="adminPath(`orders/${row.order_id}`)">
|
||||
<el-button link type="primary" :icon="View">详情</el-button>
|
||||
</RouterLink>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@page-change="loadItems"
|
||||
/>
|
||||
|
||||
<WithdrawalDetailDialog
|
||||
v-model="showWithdrawalDetail"
|
||||
:withdrawal="selectedWithdrawal"
|
||||
@saved="onWithdrawalSaved"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.disbursement-metrics {
|
||||
grid-template-columns: repeat(5, minmax(170px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.paid-metric {
|
||||
border-left: 3px solid #0f766e;
|
||||
}
|
||||
|
||||
.pending-metric {
|
||||
border-left: 3px solid #d97706;
|
||||
}
|
||||
|
||||
.review-metric {
|
||||
border-left: 3px solid #64748b;
|
||||
}
|
||||
|
||||
.fee-metric {
|
||||
border-left: 3px solid #2563eb;
|
||||
}
|
||||
|
||||
.disbursement-filters {
|
||||
grid-template-columns: repeat(8, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.business-cell,
|
||||
.stack-cell,
|
||||
.amount-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.business-cell {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.business-no {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stack-cell strong,
|
||||
.stack-cell span,
|
||||
.amount-stack strong {
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.stack-cell small,
|
||||
.amount-stack small,
|
||||
.muted-text {
|
||||
color: #7f8aa3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.amount-stack {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.amount-stack strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.disbursement-metrics {
|
||||
grid-template-columns: repeat(3, minmax(170px, 1fr));
|
||||
}
|
||||
|
||||
.disbursement-filters {
|
||||
grid-template-columns: repeat(4, minmax(130px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.disbursement-metrics,
|
||||
.disbursement-filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user