862 lines
29 KiB
Vue
862 lines
29 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
Check,
|
||
CircleClose,
|
||
Clock,
|
||
Coin,
|
||
CreditCard,
|
||
Document,
|
||
Money,
|
||
Plus,
|
||
Refresh,
|
||
Search,
|
||
UploadFilled,
|
||
View,
|
||
Wallet,
|
||
WarningFilled,
|
||
} from '@element-plus/icons-vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
|
||
import {
|
||
createManualDisbursement,
|
||
fetchFinanceDisbursements,
|
||
voidManualDisbursement,
|
||
type FinanceDisbursementItem,
|
||
type FinanceDisbursementListSummary,
|
||
} from '@/features/admin/api/adminFinance'
|
||
import { fetchAdminWithdrawal, type WithdrawalDetail } from '@/features/admin/api/adminWithdrawal'
|
||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||
import { uploadAdminFile } from '@/shared/api/files'
|
||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||
import { adminPath } from '@/shared/utils/adminPath'
|
||
import { readError } from '@/shared/utils/error'
|
||
import { formatCentWithSymbol, yuanToCent } 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 createVisible = ref(false)
|
||
const createSaving = ref(false)
|
||
const voucherUploading = ref(false)
|
||
const manualActionID = ref(0)
|
||
const selectedManual = ref<FinanceDisbursementItem | null>(null)
|
||
const summary = ref<FinanceDisbursementListSummary>(emptySummary())
|
||
const canManageManual = computed(() => adminSession.hasPermission('finance:manual_disbursement'))
|
||
|
||
const manualCategories = [
|
||
{ value: 'user_compensation', label: '用户补偿' },
|
||
{ value: 'seller_supplement', label: '卖家补款' },
|
||
{ value: 'operating_expense', label: '运营支出' },
|
||
{ value: 'channel_fee', label: '渠道费用' },
|
||
{ value: 'duoduo_deposit_refund', label: '多多退押金' },
|
||
{ value: 'other', label: '其他' },
|
||
{ value: 'custom', label: '自定义分类' },
|
||
]
|
||
|
||
const createForm = reactive({
|
||
category: '',
|
||
custom_category_name: '',
|
||
payee_name: '',
|
||
amount_yuan: 0,
|
||
paid_at: new Date(),
|
||
remark: '',
|
||
voucher_url: '',
|
||
})
|
||
|
||
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 openCreate() {
|
||
resetCreateForm()
|
||
createVisible.value = true
|
||
}
|
||
|
||
function resetCreateForm() {
|
||
createForm.category = ''
|
||
createForm.custom_category_name = ''
|
||
createForm.payee_name = ''
|
||
createForm.amount_yuan = 0
|
||
createForm.paid_at = new Date()
|
||
createForm.remark = ''
|
||
createForm.voucher_url = ''
|
||
}
|
||
|
||
async function uploadVoucher(options: { file: File }) {
|
||
if (!options.file.type.startsWith('image/')) {
|
||
ElMessage.warning('付款凭证仅支持图片')
|
||
return
|
||
}
|
||
voucherUploading.value = true
|
||
try {
|
||
const uploaded = await uploadAdminFile(options.file, 'manual-disbursement')
|
||
createForm.voucher_url = uploaded.url
|
||
ElMessage.success('凭证已上传')
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '凭证上传失败'))
|
||
} finally {
|
||
voucherUploading.value = false
|
||
}
|
||
}
|
||
|
||
async function submitManualDisbursement() {
|
||
const amountCent = yuanToCent(createForm.amount_yuan)
|
||
if (
|
||
!createForm.category ||
|
||
!createForm.payee_name.trim() ||
|
||
amountCent <= 0 ||
|
||
!createForm.paid_at
|
||
) {
|
||
ElMessage.warning('请填写完整的出款信息')
|
||
return
|
||
}
|
||
if (!createForm.remark.trim()) {
|
||
ElMessage.warning('请填写出款用途')
|
||
return
|
||
}
|
||
if (createForm.category === 'custom' && !createForm.custom_category_name.trim()) {
|
||
ElMessage.warning('请填写自定义分类名称')
|
||
return
|
||
}
|
||
createSaving.value = true
|
||
try {
|
||
await createManualDisbursement({
|
||
category: createForm.category,
|
||
custom_category_name:
|
||
createForm.category === 'custom' ? createForm.custom_category_name.trim() : undefined,
|
||
payee_name: createForm.payee_name.trim(),
|
||
amount_cent: amountCent,
|
||
paid_at: createForm.paid_at.toISOString(),
|
||
remark: createForm.remark.trim(),
|
||
voucher_url: createForm.voucher_url || undefined,
|
||
})
|
||
createVisible.value = false
|
||
ElMessage.success('线下出款已记账')
|
||
await loadItems()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '线下出款记账失败'))
|
||
} finally {
|
||
createSaving.value = false
|
||
}
|
||
}
|
||
|
||
function openManualDetail(row: FinanceDisbursementItem) {
|
||
selectedManual.value = row
|
||
}
|
||
|
||
async function handleVoidManual(row: FinanceDisbursementItem) {
|
||
if (manualActionID.value) return
|
||
try {
|
||
const { value } = await ElMessageBox.prompt(
|
||
`确认作废出款单 ${row.business_no}?作废后将从财务统计中排除。`,
|
||
'作废线下出款',
|
||
{
|
||
confirmButtonText: '确认作废',
|
||
cancelButtonText: '取消',
|
||
inputPlaceholder: '请输入作废原因',
|
||
inputValidator: value => !!value.trim() || '请输入作废原因',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
manualActionID.value = row.source_id
|
||
await voidManualDisbursement(row.source_id, value.trim())
|
||
selectedManual.value = null
|
||
ElMessage.success('线下出款已作废')
|
||
await loadItems()
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return
|
||
ElMessage.error(readError(error, '作废线下出款失败'))
|
||
} finally {
|
||
manualActionID.value = 0
|
||
}
|
||
}
|
||
|
||
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(cents: number | undefined | null) {
|
||
return formatCentWithSymbol(cents || 0)
|
||
}
|
||
|
||
function sourceLabel(sourceType: string) {
|
||
const labels: Record<string, string> = {
|
||
platform_managed: '平台代管',
|
||
pickup: '线下提号',
|
||
pickup_adjustment: '提号调整',
|
||
withdrawal: '用户提现',
|
||
manual_offline: '线下出款',
|
||
}
|
||
return labels[sourceType] || sourceType
|
||
}
|
||
|
||
function sourceType(sourceType: string) {
|
||
const types: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
|
||
platform_managed: 'primary',
|
||
pickup: 'success',
|
||
pickup_adjustment: 'warning',
|
||
withdrawal: 'success',
|
||
manual_offline: 'warning',
|
||
}
|
||
return types[sourceType] || 'info'
|
||
}
|
||
|
||
function statusType(status: string) {
|
||
const types: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
|
||
review_pending: 'info',
|
||
payment_pending: 'warning',
|
||
paid: 'success',
|
||
rejected: 'danger',
|
||
cancelled: 'info',
|
||
voided: 'danger',
|
||
}
|
||
return types[status] || 'info'
|
||
}
|
||
|
||
function itemStatusLabel(row: FinanceDisbursementItem) {
|
||
if (row.source_type === 'withdrawal') {
|
||
const map: Record<string, string> = {
|
||
pending: '待审核',
|
||
processing: '待打款',
|
||
completed: '已打款',
|
||
rejected: '已拒绝',
|
||
cancelled: '已取消',
|
||
}
|
||
return map[row.raw_status] || row.status
|
||
}
|
||
if (row.source_type === 'platform_managed' || row.source_type === 'pickup') {
|
||
return row.raw_status === 'settled' ? '已打款' : '待打款'
|
||
}
|
||
if (row.source_type === 'pickup_adjustment') {
|
||
return row.raw_status === 'settled' ? '已打款' : '待打款'
|
||
}
|
||
return row.status === 'paid' ? '已记账' : '已作废'
|
||
}
|
||
|
||
function categoryLabel(category?: string, customCategoryName?: string) {
|
||
if (category === 'custom') {
|
||
return customCategoryName?.trim() || '自定义分类'
|
||
}
|
||
const item = manualCategories.find(c => c.value === category)
|
||
return item?.label || category || '其他'
|
||
}
|
||
|
||
function accountTypeLabel(type: string) {
|
||
const labels: Record<string, string> = {
|
||
alipay: '支付宝',
|
||
wechat: '微信',
|
||
bank: '银行卡',
|
||
}
|
||
return labels[type] || type || '线下约定'
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="finance-page-container">
|
||
<!-- 头部卡片 -->
|
||
<header class="page-header-card">
|
||
<div class="header-main">
|
||
<div class="title-group">
|
||
<div class="eyebrow-tag">
|
||
<el-icon><CreditCard /></el-icon>
|
||
<span>DISBURSEMENTS</span>
|
||
</div>
|
||
<h1 class="page-title">资金出款</h1>
|
||
<p class="page-subtitle">
|
||
统一查询平台代管出款、用户钱包提现及手工记账的线下出款全生命周期记录。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="toolbar-actions">
|
||
<el-button
|
||
v-if="canManageManual"
|
||
type="primary"
|
||
:icon="Plus"
|
||
class="action-btn-primary"
|
||
@click="openCreate"
|
||
>
|
||
新增线下出款
|
||
</el-button>
|
||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||
<el-button type="primary" :icon="Search" :loading="loading" @click="search">
|
||
查询
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 汇总核心指标卡片 -->
|
||
<section class="summary-metric-grid">
|
||
<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(summary.paid_amount_cent) }}</strong>
|
||
<span class="metric-sub-pill success">{{ summary.paid_count }} 笔已完成</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card metric-card--pending">
|
||
<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">{{
|
||
money(summary.payment_pending_amount_cent)
|
||
}}</strong>
|
||
<span class="metric-sub-pill warning">{{ summary.payment_pending_count }} 笔待打款</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card metric-card--review">
|
||
<div class="metric-icon-wrap indigo">
|
||
<el-icon><Document /></el-icon>
|
||
</div>
|
||
<div class="metric-info">
|
||
<span class="metric-label">提现待初审</span>
|
||
<strong class="metric-val indigo-val">{{
|
||
money(summary.review_pending_amount_cent)
|
||
}}</strong>
|
||
<span class="metric-sub-pill info">{{ summary.review_pending_count }} 笔待审核</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card metric-card--fee">
|
||
<div class="metric-icon-wrap teal">
|
||
<el-icon><Coin /></el-icon>
|
||
</div>
|
||
<div class="metric-info">
|
||
<span class="metric-label">已核销提现手续费</span>
|
||
<strong class="metric-val teal-val">{{
|
||
money(summary.withdrawal_fee_amount_cent)
|
||
}}</strong>
|
||
<span class="metric-sub-text">随筛选区间统计</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card metric-card--total">
|
||
<div class="metric-icon-wrap slate">
|
||
<el-icon><Wallet /></el-icon>
|
||
</div>
|
||
<div class="metric-info">
|
||
<span class="metric-label">当前筛选总计</span>
|
||
<strong class="metric-val">{{ summary.record_count }} 笔</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="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"
|
||
:clearable="false"
|
||
/>
|
||
</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"
|
||
:clearable="false"
|
||
/>
|
||
</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="pickup" />
|
||
<el-option label="提号调整" value="pickup_adjustment" />
|
||
<el-option label="用户提现" value="withdrawal" />
|
||
<el-option label="其他线下出款" value="manual_offline" />
|
||
</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-option label="已作废" value="voided" />
|
||
</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>
|
||
</section>
|
||
|
||
<!-- 出款明细数据表格 -->
|
||
<section class="section-card table-section-card">
|
||
<div class="table-card-header">
|
||
<div class="table-header-title">
|
||
<el-icon class="header-icon"><Money /></el-icon>
|
||
<span>出款记录明细</span>
|
||
</div>
|
||
<span class="count-badge">共 {{ total }} 条出款记录</span>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<el-table
|
||
v-loading="loading"
|
||
class="custom-enterprise-table"
|
||
:data="items"
|
||
stripe
|
||
empty-text="暂无出款记录"
|
||
>
|
||
<el-table-column label="业务类型 / 单号" min-width="210" fixed="left">
|
||
<template #default="{ row }">
|
||
<div class="business-cell">
|
||
<el-tag size="small" :type="sourceType(row.source_type)" class="source-tag">
|
||
{{ sourceLabel(row.source_type) }}
|
||
</el-tag>
|
||
<RouterLink
|
||
v-if="row.order_id"
|
||
class="business-no link-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="180">
|
||
<template #default="{ row }">
|
||
<div class="stack-cell">
|
||
<span class="payee-name">{{ row.payee_name || '-' }}</span>
|
||
<small v-if="row.payee_phone" class="phone-text">{{ row.payee_phone }}</small>
|
||
<small v-else-if="row.source_type === 'withdrawal'" class="muted-note">
|
||
用户 ID: {{ row.user_id }}
|
||
</small>
|
||
<small v-else-if="row.source_type === 'manual_offline'" class="muted-note">
|
||
{{ categoryLabel(row.category, row.custom_category_name) }}
|
||
</small>
|
||
<small v-if="row.uploader_name" class="channel-note"
|
||
>上传:{{ row.uploader_name }}</small
|
||
>
|
||
<small v-if="row.source_channel" class="channel-note"
|
||
>渠道:{{ row.source_channel }}</small
|
||
>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="出款账户信息" min-width="210">
|
||
<template #default="{ row }">
|
||
<div v-if="row.source_type === 'withdrawal'" class="stack-cell">
|
||
<span class="account-line">
|
||
<span class="account-type-badge">{{ accountTypeLabel(row.account_type) }}</span>
|
||
{{ row.account_name || '-' }}
|
||
</span>
|
||
<small class="account-num">{{ row.account_no || '-' }}</small>
|
||
<small v-if="row.bank_name" class="bank-name">{{ row.bank_name }}</small>
|
||
</div>
|
||
<div v-else-if="row.source_type === 'manual_offline'" class="stack-cell">
|
||
<el-tag size="small" effect="plain" class="category-tag">
|
||
{{ categoryLabel(row.category, row.custom_category_name) }}
|
||
</el-tag>
|
||
<small :class="row.voucher_url ? 'voucher-uploaded' : 'voucher-missing'">
|
||
{{ row.voucher_url ? '✓ 已上传付款凭证' : '未附凭证' }}
|
||
</small>
|
||
</div>
|
||
<span v-else class="muted-text">平台代管线下结算</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="实际出款金额" min-width="160" align="right">
|
||
<template #default="{ row }">
|
||
<div class="amount-stack">
|
||
<strong class="actual-amount">{{ money(row.actual_amount_cent) }}</strong>
|
||
<small v-if="row.source_type === 'withdrawal'" class="fee-detail">
|
||
提现 {{ money(row.amount_cent) }} (扣费 {{ money(row.fee_cent) }})
|
||
</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">
|
||
{{ itemStatusLabel(row) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="业务形成时间" min-width="165">
|
||
<template #default="{ row }">
|
||
<span class="time-cell">{{ formatDateTime(row.business_created_at) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="实际打款时间" min-width="165">
|
||
<template #default="{ row }">
|
||
<span class="time-cell" :class="{ 'empty-time': !row.paid_at }">
|
||
{{ formatDateTime(row.paid_at) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="操作人" min-width="110">
|
||
<template #default="{ row }">
|
||
<span class="operator-name">{{ row.operator_name || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="备注" min-width="160" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span class="remark-text">{{ row.remark || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="action-cell">
|
||
<el-button
|
||
v-if="row.withdrawal_id && adminSession.hasPermission('withdrawal:detail')"
|
||
link
|
||
type="primary"
|
||
:icon="View"
|
||
@click="openWithdrawalDetail(row)"
|
||
>
|
||
详情
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.source_type === 'manual_offline'"
|
||
link
|
||
type="primary"
|
||
:icon="View"
|
||
@click="openManualDetail(row)"
|
||
>
|
||
查看
|
||
</el-button>
|
||
<el-button
|
||
v-if="
|
||
row.source_type === 'manual_offline' && row.status === 'paid' && canManageManual
|
||
"
|
||
link
|
||
type="danger"
|
||
:icon="CircleClose"
|
||
:loading="manualActionID === row.source_id"
|
||
@click="handleVoidManual(row)"
|
||
>
|
||
作废
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<AdminTablePagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="currentPageSize"
|
||
:total="total"
|
||
@page-change="loadItems"
|
||
/>
|
||
</section>
|
||
|
||
<!-- 新增线下出款抽屉 / 弹窗 -->
|
||
<el-dialog
|
||
v-model="createVisible"
|
||
title="新增线下出款记账"
|
||
width="560px"
|
||
destroy-on-close
|
||
class="custom-modal"
|
||
>
|
||
<el-form label-position="top" class="modal-form">
|
||
<el-form-item label="出款分类" required>
|
||
<el-select
|
||
v-model="createForm.category"
|
||
placeholder="请选择出款类别"
|
||
class="full-control"
|
||
>
|
||
<el-option
|
||
v-for="item in manualCategories"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="createForm.category === 'custom'" label="自定义分类名称" required>
|
||
<el-input
|
||
v-model="createForm.custom_category_name"
|
||
placeholder="请输入分类名称(不超过 50 字)"
|
||
maxlength="50"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="收款人姓名 / 单位" required>
|
||
<el-input v-model="createForm.payee_name" placeholder="请输入实际收款方姓名或单位" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="出款金额(元)" required>
|
||
<el-input-number
|
||
v-model="createForm.amount_yuan"
|
||
:min="0.01"
|
||
:precision="2"
|
||
:step="10"
|
||
class="full-control"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="打款时间" required>
|
||
<el-date-picker
|
||
v-model="createForm.paid_at"
|
||
type="datetime"
|
||
placeholder="选择打款时间"
|
||
class="full-control"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="出款用途 / 详细备注" required>
|
||
<el-input
|
||
v-model="createForm.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="详细记录出款原因与对账依据"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="打款凭证截图(可选)">
|
||
<div class="voucher-upload-box">
|
||
<el-upload
|
||
action="#"
|
||
:http-request="uploadVoucher"
|
||
:show-file-list="false"
|
||
accept="image/*"
|
||
>
|
||
<el-button :loading="voucherUploading" :icon="UploadFilled">
|
||
{{ createForm.voucher_url ? '重新上传凭证' : '上传支付凭证' }}
|
||
</el-button>
|
||
</el-upload>
|
||
<div v-if="createForm.voucher_url" class="voucher-preview-wrap">
|
||
<AuthImage
|
||
:source="createForm.voucher_url"
|
||
alt="凭证预览"
|
||
class="voucher-img-preview"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="modal-footer">
|
||
<el-button @click="createVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="createSaving" @click="submitManualDisbursement">
|
||
确认记账
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 线下出款详情弹窗 -->
|
||
<el-dialog
|
||
:model-value="!!selectedManual"
|
||
title="线下出款详情"
|
||
width="540px"
|
||
@update:model-value="(val: boolean) => (!val ? (selectedManual = null) : null)"
|
||
>
|
||
<div v-if="selectedManual" class="detail-content-wrap">
|
||
<div class="detail-row">
|
||
<span class="detail-label">出款单号</span>
|
||
<span class="detail-val font-mono">{{ selectedManual.business_no }}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">分类</span>
|
||
<span class="detail-val">{{
|
||
categoryLabel(selectedManual.category, selectedManual.custom_category_name)
|
||
}}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">收款人</span>
|
||
<span class="detail-val">{{ selectedManual.payee_name }}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">出款金额</span>
|
||
<span class="detail-val text-primary font-bold text-lg">
|
||
{{ money(selectedManual.amount_cent) }}
|
||
</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">状态</span>
|
||
<el-tag :type="statusType(selectedManual.status)">
|
||
{{ itemStatusLabel(selectedManual) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">打款时间</span>
|
||
<span class="detail-val">{{ formatDateTime(selectedManual.paid_at) }}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">录入人</span>
|
||
<span class="detail-val">{{ selectedManual.operator_name || '-' }}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">用途备注</span>
|
||
<span class="detail-val">{{ selectedManual.remark || '-' }}</span>
|
||
</div>
|
||
<div v-if="selectedManual.voucher_url" class="detail-row column-row">
|
||
<span class="detail-label">打款凭证</span>
|
||
<div class="detail-img-container">
|
||
<AuthImage :source="selectedManual.voucher_url" alt="凭证" class="detail-voucher-img" />
|
||
</div>
|
||
</div>
|
||
<div v-if="selectedManual.status === 'voided'" class="void-info-box">
|
||
<div class="void-title">
|
||
<el-icon><WarningFilled /></el-icon>
|
||
<span>作废信息</span>
|
||
</div>
|
||
<p>作废时间:{{ formatDateTime(selectedManual.voided_at) }}</p>
|
||
<p>作废人:{{ selectedManual.voided_by_name || '-' }}</p>
|
||
<p>作废原因:{{ selectedManual.void_reason || '-' }}</p>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<WithdrawalDetailDialog
|
||
v-model="showWithdrawalDetail"
|
||
:withdrawal="selectedWithdrawal"
|
||
@saved="onWithdrawalSaved"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped src="./AdminFinanceDisbursementsView.css"></style>
|