冲抵后的余额不再自动记成发放,发放和冲抵分开提交,未结清单据继续留在待处理。 Co-authored-by: Cursor <cursoragent@cursor.com>
1761 lines
67 KiB
Vue
1761 lines
67 KiB
Vue
<!-- 支付管理(发放/冲抵) -->
|
||
<!-- eslint-disable vue/no-v-model-argument -->
|
||
<template>
|
||
<div class="page-container">
|
||
<section class="panel">
|
||
<el-tabs
|
||
v-model="activeTab"
|
||
class="panel-tabs"
|
||
@tab-change="handleTabChange"
|
||
>
|
||
<!-- 待处理 -->
|
||
<el-tab-pane label="待处理" name="pending">
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<ListQuickSearchInput
|
||
v-model="quickSearchKeyword"
|
||
placeholder="单号/报销标题"
|
||
@search="handleQuickSearch"
|
||
@clear="handleQuickSearchClear"
|
||
/>
|
||
<el-button class="btn-main" type="primary" @click="handleToolbarIssue">
|
||
<el-icon><Money /></el-icon>发放
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="openBatchIssueDialog">
|
||
<el-icon><Document /></el-icon>批量发放
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="handleToolbarOffset">
|
||
<el-icon><Switch /></el-icon>冲抵备用金
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasActiveFilters"
|
||
class="btn-ghost"
|
||
@click="clearAllFilters"
|
||
>
|
||
<el-icon><Filter /></el-icon>清空列过滤
|
||
</el-button>
|
||
<ListExportDropdown
|
||
:export-fn="paymentApi.exportPending"
|
||
:get-filter-params="getPaymentPendingExportFilterParams"
|
||
:has-selection="true"
|
||
:selected-ids="selectedRows.map((r) => String(r.id))"
|
||
:visible-columns="paymentExportColumns"
|
||
filename="报销待付款.xlsx"
|
||
/>
|
||
|
||
<ListToolbarEndActions
|
||
:loading="loading"
|
||
:column-defs="listColumnDefs"
|
||
v-model:visible-columns="visibleColumns"
|
||
v-model:check-all="checkAll"
|
||
:is-indeterminate="isIndeterminate"
|
||
@refresh="runPendingSearch"
|
||
@reset-columns="handleResetColumns"
|
||
/>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list-body list-wrap">
|
||
<div ref="pendingPaymentTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||
<ListTable ref="pendingPaymentTableRef" :height="pendingPaymentTableHeight" table-key="bx-payment"
|
||
class="list-table"
|
||
:data="rows"
|
||
v-loading="loading"
|
||
stripe
|
||
@selection-change="onSelectionChange"
|
||
@row-dblclick="openView"
|
||
@header-dragend="() => pendingPaymentStatsRowRef?.resync?.()"
|
||
>
|
||
<el-table-column type="selection" width="48" align="center" header-align="center" />
|
||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||
<el-table-column
|
||
v-if="isColumnVisible('applyTime')"
|
||
prop="applyTime"
|
||
label="申请时间"
|
||
min-width="160"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">申请时间</span>
|
||
<ColumnHeaderFilter
|
||
prop="applyTime"
|
||
label="申请时间"
|
||
type="datetime"
|
||
:is-active="isFilterActive('applyTime')"
|
||
:model-value="filters.applyTime || null"
|
||
@update:model-value="(v) => setFilterState('applyTime', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="applyCode"
|
||
label="单号"
|
||
class-name="col-apply-code"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">单号</span>
|
||
<ColumnHeaderFilter
|
||
prop="applyCode"
|
||
label="单号"
|
||
type="enum"
|
||
:options="facetOptionsMap.applyCode"
|
||
:options-loading="facetLoadingMap.applyCode"
|
||
:is-active="isFilterActive('applyCode')"
|
||
:model-value="filters.applyCode || null"
|
||
@update:model-value="(v) => setFilterState('applyCode', v)"
|
||
@open="onFilterOpen('applyCode')"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('applicantName')"
|
||
prop="applicantName"
|
||
label="申请人"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">申请人</span>
|
||
<ColumnHeaderFilter
|
||
prop="applicantName"
|
||
label="申请人"
|
||
type="enum"
|
||
:options="facetOptionsMap.applicantName"
|
||
:options-loading="facetLoadingMap.applicantName"
|
||
:is-active="isFilterActive('applicantName')"
|
||
:model-value="filters.applicantName || null"
|
||
@update:model-value="(v) => setFilterState('applicantName', v)"
|
||
@open="onFilterOpen('applicantName')"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('title')"
|
||
prop="title"
|
||
label="报销标题"
|
||
min-width="180"
|
||
align="left"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">报销标题</span>
|
||
<ColumnHeaderFilter
|
||
prop="title"
|
||
label="报销标题"
|
||
type="enum"
|
||
:options="facetOptionsMap.title"
|
||
:options-loading="facetLoadingMap.title"
|
||
:is-active="isFilterActive('title')"
|
||
:model-value="filters.title || null"
|
||
@update:model-value="(v) => setFilterState('title', v)"
|
||
@open="onFilterOpen('title')"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('amount')"
|
||
prop="amount"
|
||
label="报销金额(元)"
|
||
:formatter="moneyTableFormatter"
|
||
class-name="col-money"
|
||
min-width="120"
|
||
align="right"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label is-right">报销金额(元)</span>
|
||
<ColumnHeaderFilter
|
||
prop="amount"
|
||
label="报销金额(元)"
|
||
type="number"
|
||
:is-active="isFilterActive('amount')"
|
||
:model-value="filters.amount || null"
|
||
@update:model-value="(v) => setFilterState('amount', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('offsetAmount')"
|
||
prop="offsetAmount"
|
||
label="冲抵金额(元)"
|
||
class-name="col-money"
|
||
min-width="120"
|
||
align="right"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label is-right">冲抵金额(元)</span>
|
||
<ColumnHeaderFilter
|
||
prop="offsetAmount"
|
||
label="冲抵金额(元)"
|
||
type="number"
|
||
:is-active="isFilterActive('offsetAmount')"
|
||
:model-value="filters.offsetAmount || null"
|
||
@update:model-value="(v) => setFilterState('offsetAmount', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
{{ formatSumMoney(Number(row.offsetAmount || 0)) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('payAmount')"
|
||
prop="actualAmount"
|
||
label="发放金额(元)"
|
||
class-name="col-money"
|
||
min-width="120"
|
||
align="right"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label is-right">发放金额(元)</span>
|
||
<ColumnHeaderFilter
|
||
prop="payAmount"
|
||
label="发放金额(元)"
|
||
type="number"
|
||
:is-active="isFilterActive('payAmount')"
|
||
:model-value="filters.payAmount || null"
|
||
@update:model-value="(v) => setFilterState('payAmount', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
{{
|
||
row.actualAmount != null && row.actualAmount !== ''
|
||
? formatSumMoney(Number(row.actualAmount))
|
||
: '—'
|
||
}}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="未结清余额(元)"
|
||
min-width="130"
|
||
align="right"
|
||
header-align="center"
|
||
class-name="col-money"
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatSumMoney(unpaidBalance(row)) }}
|
||
</template>
|
||
</el-table-column>
|
||
</ListTable>
|
||
</div>
|
||
<div ref="pendingPaymentBelowRef">
|
||
<ListColumnStatsRow
|
||
ref="pendingPaymentStatsRowRef"
|
||
storage-key="bx-payment-pending-column-stats"
|
||
:columns="pendingPaymentStatsRowColumns"
|
||
:data="rows"
|
||
:table-wrap-el="pendingPaymentTableWrapRef"
|
||
:get-cell-value="getPaymentStatCellValue"
|
||
/>
|
||
|
||
<div class="pagination list-pager-bar">
|
||
<div class="list-pager-selected">
|
||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||
</div>
|
||
|
||
<span class="list-range">{{ rangeText }}</span>
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:total="total"
|
||
layout="total, sizes, prev, pager, next"
|
||
:page-sizes="[10, 20, 100, 500]"
|
||
@size-change="onSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 已处理 -->
|
||
<el-tab-pane label="已处理" name="processed">
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<ListQuickSearchInput
|
||
v-model="processedQuickSearchKeyword"
|
||
placeholder="单号/报销标题"
|
||
@search="handleProcessedQuickSearch"
|
||
@clear="handleProcessedQuickSearchClear"
|
||
/>
|
||
<el-button
|
||
v-if="hasActiveFilters"
|
||
class="btn-ghost"
|
||
@click="clearAllFilters"
|
||
>
|
||
<el-icon><Filter /></el-icon>清空列过滤
|
||
</el-button>
|
||
<ListExportDropdown
|
||
:export-fn="paymentApi.exportProcessed"
|
||
:get-filter-params="getPaymentProcessedExportFilterParams"
|
||
:has-selection="false"
|
||
:selected-ids="[]"
|
||
:visible-columns="paymentExportColumns"
|
||
filename="报销已付款.xlsx"
|
||
/>
|
||
|
||
<ListToolbarEndActions
|
||
:loading="processedLoading"
|
||
:column-defs="listColumnDefs"
|
||
v-model:visible-columns="visibleColumns"
|
||
v-model:check-all="checkAll"
|
||
:is-indeterminate="isIndeterminate"
|
||
@refresh="runProcessedSearch"
|
||
@reset-columns="handleResetColumns"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list-body list-wrap">
|
||
<div ref="processedPaymentTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||
<ListTable ref="processedPaymentTableRef" :height="processedPaymentTableHeight" table-key="bx-payment-2"
|
||
class="list-table"
|
||
:data="processedRows"
|
||
v-loading="processedLoading"
|
||
stripe
|
||
@row-dblclick="openProcessedView"
|
||
@header-dragend="() => processedPaymentStatsRowRef?.resync?.()"
|
||
>
|
||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||
<el-table-column
|
||
v-if="isColumnVisible('applyTime')"
|
||
prop="applyTime"
|
||
label="申请时间"
|
||
min-width="160"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">申请时间</span>
|
||
<ColumnHeaderFilter
|
||
prop="applyTime"
|
||
label="申请时间"
|
||
type="datetime"
|
||
:is-active="isFilterActive('applyTime')"
|
||
:model-value="filters.applyTime || null"
|
||
@update:model-value="(v) => setFilterState('applyTime', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="applyCode"
|
||
label="单号"
|
||
class-name="col-apply-code"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">单号</span>
|
||
<ColumnHeaderFilter
|
||
prop="applyCode"
|
||
label="单号"
|
||
type="enum"
|
||
:options="facetOptionsMap.applyCode"
|
||
:options-loading="facetLoadingMap.applyCode"
|
||
:is-active="isFilterActive('applyCode')"
|
||
:model-value="filters.applyCode || null"
|
||
@update:model-value="(v) => setFilterState('applyCode', v)"
|
||
@open="onFilterOpen('applyCode')"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="openProcessedView(row)">{{ row.applyCode }}</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('applicantName')"
|
||
prop="applicantName"
|
||
label="申请人"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">申请人</span>
|
||
<ColumnHeaderFilter
|
||
prop="applicantName"
|
||
label="申请人"
|
||
type="enum"
|
||
:options="facetOptionsMap.applicantName"
|
||
:options-loading="facetLoadingMap.applicantName"
|
||
:is-active="isFilterActive('applicantName')"
|
||
:model-value="filters.applicantName || null"
|
||
@update:model-value="(v) => setFilterState('applicantName', v)"
|
||
@open="onFilterOpen('applicantName')"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('title')"
|
||
prop="title"
|
||
label="标题"
|
||
min-width="180"
|
||
align="left"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">标题</span>
|
||
<ColumnHeaderFilter
|
||
prop="title"
|
||
label="标题"
|
||
type="text"
|
||
:is-active="isFilterActive('title')"
|
||
:model-value="filters.title || null"
|
||
@update:model-value="(v) => setFilterState('title', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('amount')"
|
||
prop="amount"
|
||
label="报销金额(元)"
|
||
:formatter="moneyTableFormatter"
|
||
class-name="col-money"
|
||
min-width="120"
|
||
align="right"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label is-right">报销金额(元)</span>
|
||
<ColumnHeaderFilter
|
||
prop="amount"
|
||
label="报销金额(元)"
|
||
type="number"
|
||
:is-active="isFilterActive('amount')"
|
||
:model-value="filters.amount || null"
|
||
@update:model-value="(v) => setFilterState('amount', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('offsetAmount')"
|
||
prop="offsetAmount"
|
||
label="冲抵金额(元)"
|
||
class-name="col-money"
|
||
min-width="120"
|
||
align="right"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label is-right">冲抵金额(元)</span>
|
||
<ColumnHeaderFilter
|
||
prop="offsetAmount"
|
||
label="冲抵金额(元)"
|
||
type="number"
|
||
:is-active="isFilterActive('offsetAmount')"
|
||
:model-value="filters.offsetAmount || null"
|
||
@update:model-value="(v) => setFilterState('offsetAmount', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
{{ formatSumMoney(Number(row.offsetAmount || 0)) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('payAmount')"
|
||
prop="actualAmount"
|
||
label="发放金额(元)"
|
||
class-name="col-money"
|
||
min-width="120"
|
||
align="right"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label is-right">发放金额(元)</span>
|
||
<ColumnHeaderFilter
|
||
prop="payAmount"
|
||
label="发放金额(元)"
|
||
type="number"
|
||
:is-active="isFilterActive('payAmount')"
|
||
:model-value="filters.payAmount || null"
|
||
@update:model-value="(v) => setFilterState('payAmount', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
{{
|
||
row.actualAmount != null && row.actualAmount !== ''
|
||
? formatSumMoney(Number(row.actualAmount))
|
||
: '—'
|
||
}}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('processStatus')"
|
||
label="处理状态"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">处理状态</span>
|
||
<ColumnHeaderFilter
|
||
prop="processStatus"
|
||
label="处理状态"
|
||
type="enum"
|
||
:enum-options="enumOptionsFor('processStatus')"
|
||
:is-active="isFilterActive('processStatus')"
|
||
:model-value="filters.processStatus || null"
|
||
@update:model-value="(v) => setFilterState('processStatus', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-tag :type="processedStatusTagType(row.status, row.payStatus)" size="small">
|
||
{{ processedStatusLabel(row.status, row.payStatus) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('payTime')"
|
||
prop="payTime"
|
||
label="处理时间"
|
||
min-width="160"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #header>
|
||
<div class="col-header-with-filter">
|
||
<span class="col-header-with-filter__label">处理时间</span>
|
||
<ColumnHeaderFilter
|
||
prop="payTime"
|
||
label="处理时间"
|
||
type="datetime"
|
||
:is-active="isFilterActive('payTime')"
|
||
:model-value="filters.payTime || null"
|
||
@update:model-value="(v) => setFilterState('payTime', v)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #default="{ row }">
|
||
{{ row.payTime || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="未结清余额(元)"
|
||
min-width="130"
|
||
align="right"
|
||
header-align="center"
|
||
class-name="col-money"
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatSumMoney(unpaidBalance(row)) }}
|
||
</template>
|
||
</el-table-column>
|
||
</ListTable>
|
||
</div>
|
||
<div ref="processedPaymentBelowRef">
|
||
<ListColumnStatsRow
|
||
ref="processedPaymentStatsRowRef"
|
||
storage-key="bx-payment-processed-column-stats"
|
||
:columns="processedPaymentStatsRowColumns"
|
||
:data="processedRows"
|
||
:table-wrap-el="processedPaymentTableWrapRef"
|
||
:get-cell-value="getPaymentStatCellValue"
|
||
/>
|
||
|
||
<div class="pagination list-pager-bar">
|
||
<span class="list-range">{{ processedRangeText }}</span>
|
||
<el-pagination
|
||
v-model:current-page="processedCurrentPage"
|
||
v-model:page-size="processedPageSize"
|
||
:total="processedTotal"
|
||
layout="total, sizes, prev, pager, next"
|
||
:page-sizes="[10, 20, 100, 500]"
|
||
@size-change="processedOnSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</section>
|
||
|
||
<!-- 单据详情 -->
|
||
<el-dialog
|
||
v-model="dialogs.viewDetail"
|
||
title="单据详情"
|
||
width="1040px"
|
||
class="dialog-form-layout dialog-view-xl"
|
||
align-center
|
||
destroy-on-close
|
||
>
|
||
<div class="dlg-view">
|
||
<div class="dlg-view-hero">
|
||
<div class="dlg-view-hero-top">
|
||
<h2 class="dlg-view-title">{{ viewData.applyCode || '-' }}</h2>
|
||
<div class="dlg-view-tags">
|
||
<el-tag :type="statusTagType(viewData.status)" size="small">
|
||
{{ statusLabel(viewData.status) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<p class="dlg-view-meta">
|
||
金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
|
||
</p>
|
||
<div v-if="activeTab === 'pending'" class="dlg-view-hero-actions">
|
||
<el-button class="btn-main" type="primary" @click="openIssueDialog(viewData)">
|
||
<el-icon><Money /></el-icon>发放
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="openOffsetDialog(viewData)">
|
||
<el-icon><Switch /></el-icon>冲抵备用金
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="dlg-view-section">
|
||
<h4 class="dlg-view-section-title">基础信息</h4>
|
||
<BxViewKvTable :fields="viewBasicKvFields" mode="triple" />
|
||
</div>
|
||
<div class="dlg-view-section">
|
||
<h4 class="dlg-view-section-title">发放信息</h4>
|
||
<BxViewKvTable :fields="viewPayKvFields" mode="triple" />
|
||
</div>
|
||
<div v-if="viewAttachments.length" class="dlg-view-section">
|
||
<h4 class="dlg-view-section-title">相关附件</h4>
|
||
<BxAttachmentReadonly :files="viewAttachments" />
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 批量发放 -->
|
||
<el-dialog
|
||
v-model="dialogs.batchIssue"
|
||
title="批量发放"
|
||
width="960px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form label-position="top" class="bx-dialog-form">
|
||
<h4 class="dialog-section-title">已选报销单</h4>
|
||
<el-table :data="batchLines" class="list-table" stripe max-height="320">
|
||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||
<el-table-column prop="applyCode" label="单号" min-width="150" align="center" header-align="center" />
|
||
<el-table-column prop="applicantName" label="申请人" min-width="90" align="center" header-align="center" />
|
||
<el-table-column
|
||
prop="title"
|
||
label="报销标题"
|
||
min-width="160"
|
||
align="left"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column label="发放金额(元)" min-width="130" align="right" header-align="center" class-name="col-money">
|
||
<template #default="{ row }">
|
||
{{ formatMoney(row.payAmount) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="实际发放金额(元)" min-width="160" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.actualAmount" placeholder="请输入实际发放金额" />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-form-item label="备注" class="batch-issue-remark">
|
||
<el-input v-model="batchIssueRemark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
<h4 class="dialog-section-title">发放附件</h4>
|
||
<BxAttachmentUpload
|
||
ref="batchAttachmentRef"
|
||
v-model="batchFiles"
|
||
biz-type="reimburse_pay"
|
||
biz-id="0"
|
||
hint="同一批报销单共用这些发放凭证。"
|
||
/>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="dialogs.batchIssue = false">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" :loading="loadingMap.batchIssueSubmit" @click="submitBatchIssue">
|
||
<el-icon><Check /></el-icon>确认发放
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 发放 / 冲抵,按入口只展示对应表单 -->
|
||
<el-dialog
|
||
v-model="dialogs.offset"
|
||
:title="paymentActionTab === 'pay' ? '发放' : '冲抵备用金'"
|
||
width="960px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form label-position="top" class="bx-dialog-form">
|
||
<h4 class="dialog-section-title">报销单信息</h4>
|
||
<div class="form-grid-2">
|
||
<el-form-item label="报销单号">
|
||
<el-input :model-value="currentRow.applyCode || ''" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="报销金额(元)" class="field-amount">
|
||
<el-input :model-value="String(currentRow.amount || '')" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="已冲抵金额(元)" class="field-amount">
|
||
<el-input :model-value="formatMoneyYuan(currentRow.offsetAmount || 0)" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="已发放金额(元)" class="field-amount">
|
||
<el-input :model-value="formatMoneyYuan(currentRow.actualAmount || 0)" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="未结清余额(元)" class="field-amount">
|
||
<el-input :model-value="formatMoneyYuan(unpaidBalance(currentRow))" disabled />
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<template v-if="paymentActionTab === 'offset'">
|
||
<div class="form-grid-2">
|
||
<el-form-item label="本次冲抵金额(元)" class="field-amount">
|
||
<el-input v-model="offsetAmount" placeholder="请输入本次冲抵金额" />
|
||
</el-form-item>
|
||
</div>
|
||
<h4 class="dialog-section-title">已发放备用金列表</h4>
|
||
<div class="inline-row">
|
||
<el-input v-model="offsetAdvanceFilter.keyword" placeholder="搜索单号/申请人" class="inline-grow" clearable />
|
||
<el-button class="btn-main" type="primary" :loading="offsetAdvanceLoading" @click="handleOffsetAdvanceSearch">
|
||
<el-icon><Search /></el-icon>搜索
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="handleOffsetAdvanceReset">
|
||
<el-icon><RefreshLeft /></el-icon>重置
|
||
</el-button>
|
||
</div>
|
||
<ListTable table-key="bx-payment-3"
|
||
class="list-table"
|
||
:data="offsetAdvances"
|
||
v-loading="offsetAdvanceLoading"
|
||
max-height="300"
|
||
stripe
|
||
@selection-change="handleOffsetSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="48" align="center" header-align="center" />
|
||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||
<el-table-column prop="applyCode" label="申请单号" min-width="130" align="center" header-align="center" />
|
||
<el-table-column prop="applicantName" label="申请人" min-width="80" align="center" header-align="center" />
|
||
<el-table-column prop="reason" label="申请原因" min-width="120" align="left" header-align="center" show-overflow-tooltip />
|
||
<el-table-column label="已发放金额(元)" min-width="100" class-name="col-money" align="right" header-align="center">
|
||
<template #default="{ row }">
|
||
{{ formatMoney(row.actualAmount || row.amount || 0) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="已冲抵金额(元)" min-width="100" class-name="col-money" align="right" header-align="center">
|
||
<template #default="{ row }">
|
||
{{ formatMoney(row.offsetAmount || 0) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="剩余可冲抵(元)" min-width="100" class-name="col-money" align="right" header-align="center">
|
||
<template #default="{ row }">
|
||
{{ formatMoney(Number(row.actualAmount || row.amount || 0) - Number(row.offsetAmount || 0)) }}
|
||
</template>
|
||
</el-table-column>
|
||
</ListTable>
|
||
<div class="offset-pager">
|
||
<el-pagination
|
||
v-model:current-page="offsetAdvancePage"
|
||
v-model:page-size="offsetAdvancePageSize"
|
||
:total="offsetAdvanceTotal"
|
||
layout="total, sizes, prev, pager, next"
|
||
:page-sizes="[10, 20, 100, 500]"
|
||
@current-change="fetchOffsetAdvances"
|
||
@size-change="handleOffsetAdvanceSizeChange"
|
||
/>
|
||
</div>
|
||
|
||
<el-form-item label="备注">
|
||
<el-input v-model="offsetRemark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
|
||
<h4 class="dialog-section-title">冲抵附件(必填)</h4>
|
||
<BxAttachmentUpload
|
||
ref="offsetAttachmentRef"
|
||
v-model="uploadedFiles"
|
||
biz-type="reimburse_offset"
|
||
:biz-id="currentRow.id"
|
||
hint="请至少上传 1 个冲抵附件。"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<div class="form-grid-2">
|
||
<el-form-item label="本次发放金额(元)" class="field-amount">
|
||
<el-input v-model="actualAmount" placeholder="请输入本次发放金额" />
|
||
</el-form-item>
|
||
<el-form-item label="发放备注" style="grid-column: 1 / -1">
|
||
<el-input v-model="issueRemark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
</div>
|
||
<h4 class="dialog-section-title">发放附件</h4>
|
||
<BxAttachmentUpload
|
||
ref="issueAttachmentRef"
|
||
v-model="payFiles"
|
||
biz-type="reimburse_pay"
|
||
:biz-id="currentRow.id"
|
||
hint="支持上传本次发放截图等凭证。"
|
||
/>
|
||
</template>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="dialogs.offset = false">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button
|
||
v-if="paymentActionTab === 'pay'"
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="loadingMap.issueSubmit"
|
||
@click="submitIssue"
|
||
>
|
||
<el-icon><Check /></el-icon>确认发放
|
||
</el-button>
|
||
<el-button
|
||
v-else
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="loadingMap.offsetSubmit"
|
||
@click="submitOffset"
|
||
>
|
||
<el-icon><Check /></el-icon>确认冲抵
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 处理详情 -->
|
||
<el-dialog
|
||
v-model="dialogs.processedDetail"
|
||
title="处理详情"
|
||
width="1040px"
|
||
class="dialog-form-layout dialog-view-xl"
|
||
align-center
|
||
destroy-on-close
|
||
>
|
||
<div class="dlg-view">
|
||
<div class="dlg-view-hero">
|
||
<div class="dlg-view-hero-top">
|
||
<h2 class="dlg-view-title">{{ processedViewData.applyCode || '-' }}</h2>
|
||
<div class="dlg-view-tags">
|
||
<el-tag :type="processedStatusTagType(processedViewData.status, processedViewData.payStatus)" size="small">
|
||
{{ processedStatusLabel(processedViewData.status, processedViewData.payStatus) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<p class="dlg-view-meta">
|
||
金额(元):<b class="money-em">{{ formatMoneyYuan(processedViewData.amount || 0) }}</b>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="dlg-view-section">
|
||
<h4 class="dlg-view-section-title">基础信息</h4>
|
||
<BxViewKvTable :fields="processedBasicKvFields" mode="triple" />
|
||
<div v-if="processedAttachments['申请附件']?.length" class="dlg-view-section-attach">
|
||
<h4 class="dlg-view-section-title">申请附件</h4>
|
||
<BxAttachmentReadonly :files="processedAttachments['申请附件']" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dlg-view-section">
|
||
<h4 class="dlg-view-section-title">发放信息</h4>
|
||
<BxViewKvTable :fields="processedPayKvFields" mode="triple" />
|
||
<div v-if="processedAttachments['发放附件']?.length" class="dlg-view-section-attach">
|
||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||
<BxAttachmentReadonly :files="processedAttachments['发放附件']" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="Number(processedViewData.offsetAmount || 0) > 0" class="dlg-view-section">
|
||
<h4 class="dlg-view-section-title">冲抵信息</h4>
|
||
<BxViewKvGrid :fields="processedOffsetKvFields" />
|
||
<div v-if="processedAttachments['冲抵附件']?.length" class="dlg-view-section-attach">
|
||
<h4 class="dlg-view-section-title">冲抵附件</h4>
|
||
<BxAttachmentReadonly :files="processedAttachments['冲抵附件']" />
|
||
</div>
|
||
|
||
<h4 class="dlg-view-section-title section-gap-tight">冲抵明细</h4>
|
||
<BaseDetailTableSection embedded title="冲抵明细" :data="processedOffsetRecords" empty-text="暂无冲抵明细">
|
||
<ListTable table-key="bx-payment-4" class="list-table" :data="processedOffsetRecords" stripe size="small">
|
||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||
<el-table-column prop="advanceApplyCode" label="备用金单号" min-width="130" align="center" header-align="center" />
|
||
<el-table-column prop="advanceApplicantName" label="备用金申请人" min-width="110" align="center" header-align="center" />
|
||
<el-table-column prop="advanceReason" label="申请原因" min-width="160" align="left" header-align="center" show-overflow-tooltip />
|
||
<el-table-column label="冲抵金额(元)" min-width="120" class-name="col-money" align="right" header-align="center">
|
||
<template #default="{ row: offsetRow }">
|
||
{{ formatMoneyYuan(offsetRow.offsetAmount || 0) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createTime" label="冲抵时间" min-width="160" align="center" header-align="center" />
|
||
<el-table-column prop="operator" label="操作人" min-width="100" align="center" header-align="center" />
|
||
</ListTable>
|
||
</BaseDetailTableSection>
|
||
</div>
|
||
|
||
<BxApprovalFlowTabs
|
||
:nodes="processedApprovals"
|
||
:history="processedApprovalHistory"
|
||
:loading="processedApprovalLoading"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
|
||
import { useColumnFacetOptions } from '@/composables/useColumnFacetOptions'
|
||
import { useColumnSort } from '@/composables/useColumnSort'
|
||
import { reactive, ref, computed, onMounted, nextTick, provide } from 'vue'
|
||
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
Money,
|
||
Document,
|
||
Switch,
|
||
Close,
|
||
Check,
|
||
Search,
|
||
RefreshLeft,
|
||
Filter
|
||
} from '@element-plus/icons-vue'
|
||
import { paymentApi, reimburseApi,
|
||
fetchReimburseColumnFacetValues
|
||
} from '@/api/bx'
|
||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||
import { statusLabel, statusTagType, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter } from '@/utils/bx/format'
|
||
import { useServerPaged } from '@/hooks/bx/useServerPaged'
|
||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||
import BxViewKvGrid from '@/components/bx/base/BxViewKvGrid.vue'
|
||
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||
import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.vue'
|
||
import BxAttachmentUpload from '@/components/bx/base/BxAttachmentUpload.vue'
|
||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||
import { paymentColumnFilterDefs } from './columnFilterDefs'
|
||
|
||
type DialogKey = 'viewDetail' | 'batchIssue' | 'offset' | 'processedDetail'
|
||
|
||
const listColumnDefs = [
|
||
{ prop: 'applyTime', label: '申请时间' },
|
||
{ prop: 'applicantName', label: '申请人' },
|
||
{ prop: 'title', label: '报销标题' },
|
||
{ prop: 'amount', label: '报销金额(元)' },
|
||
{ prop: 'offsetAmount', label: '冲抵金额(元)', defaultVisible: false },
|
||
{ prop: 'payAmount', label: '发放金额(元)' },
|
||
{ prop: 'processStatus', label: '处理状态', defaultVisible: false },
|
||
{ prop: 'payTime', label: '处理时间', defaultVisible: false }
|
||
]
|
||
|
||
const {
|
||
visibleColumns,
|
||
checkAll,
|
||
isIndeterminate,
|
||
isColumnVisible,
|
||
resetToDefault
|
||
} = usePersistedListColumns('bx-payment-list-columns', listColumnDefs)
|
||
|
||
const PAYMENT_MONEY_PROPS = new Set(['amount', 'offsetAmount', 'payAmount'])
|
||
|
||
const activeTab = ref('pending')
|
||
const pendingPaymentTableWrapRef = ref<HTMLElement | null>(null)
|
||
const pendingPaymentBelowRef = ref<HTMLElement | null>(null)
|
||
const pendingPaymentTableRef = ref<any>(null)
|
||
const { tableHeight: pendingPaymentTableHeight, recalc: recalcPendingPaymentHeight } = useListTableViewportHeight({
|
||
tableEl: pendingPaymentTableWrapRef,
|
||
pagerEl: pendingPaymentBelowRef,
|
||
tableRef: pendingPaymentTableRef,
|
||
enabled: computed(() => activeTab.value === 'pending')
|
||
})
|
||
|
||
const processedPaymentTableWrapRef = ref<HTMLElement | null>(null)
|
||
const processedPaymentBelowRef = ref<HTMLElement | null>(null)
|
||
const processedPaymentTableRef = ref<any>(null)
|
||
const { tableHeight: processedPaymentTableHeight, recalc: recalcProcessedPaymentHeight } = useListTableViewportHeight({
|
||
tableEl: processedPaymentTableWrapRef,
|
||
pagerEl: processedPaymentBelowRef,
|
||
tableRef: processedPaymentTableRef,
|
||
enabled: computed(() => activeTab.value === 'processed')
|
||
})
|
||
|
||
const pendingPaymentStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||
const processedPaymentStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||
|
||
function buildPaymentStatsColumns(opts: {
|
||
withSelection: boolean
|
||
mode: 'pending' | 'processed'
|
||
}): StatsRowColumn[] {
|
||
const cols: StatsRowColumn[] = []
|
||
if (opts.withSelection) cols.push({ key: '__selection', interactive: false })
|
||
cols.push({ key: '__index', interactive: false })
|
||
if (isColumnVisible('applyTime')) {
|
||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||
}
|
||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||
for (const def of listColumnDefs) {
|
||
if (def.prop === 'applyTime') continue
|
||
if (opts.mode === 'pending' && (def.prop === 'processStatus' || def.prop === 'payTime')) continue
|
||
if (!isColumnVisible(def.prop)) continue
|
||
cols.push({
|
||
key: def.prop,
|
||
interactive: true,
|
||
prop: def.prop,
|
||
money: PAYMENT_MONEY_PROPS.has(def.prop)
|
||
})
|
||
}
|
||
cols.push({ key: 'unpaidBalance', interactive: false, prop: 'unpaidBalance', money: true })
|
||
return cols
|
||
}
|
||
|
||
function unpaidBalance(row: any) {
|
||
const value =
|
||
Number(row?.amount || 0) - Number(row?.offsetAmount || 0) - Number(row?.actualAmount || 0)
|
||
return Math.max(0, Math.round(value * 100) / 100)
|
||
}
|
||
|
||
const pendingPaymentStatsRowColumns = computed(() =>
|
||
buildPaymentStatsColumns({ withSelection: true, mode: 'pending' })
|
||
)
|
||
const processedPaymentStatsRowColumns = computed(() =>
|
||
buildPaymentStatsColumns({ withSelection: false, mode: 'processed' })
|
||
)
|
||
|
||
function getPaymentStatCellValue(row: any, prop: string): unknown {
|
||
if (prop === 'payAmount') return row?.actualAmount
|
||
if (prop === 'unpaidBalance') return unpaidBalance(row)
|
||
if (prop === 'processStatus') return processedStatusLabel(row?.status, row?.payStatus)
|
||
return row?.[prop]
|
||
}
|
||
|
||
const loadingMap = reactive<Record<string, boolean>>({})
|
||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||
viewDetail: false,
|
||
batchIssue: false,
|
||
offset: false,
|
||
processedDetail: false
|
||
})
|
||
|
||
const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search, onSizeChange } = useServerPaged(
|
||
async (params) => {
|
||
const res: any = await paymentApi.pending(params)
|
||
const raw = res.data
|
||
if (!raw) return { rows: [], total: 0 }
|
||
const flatRows = (raw.rows || []).map((item: any) => ({
|
||
...item.data,
|
||
title: item.data.title || ''
|
||
}))
|
||
return { rows: flatRows, total: raw.total || flatRows.length }
|
||
}
|
||
)
|
||
|
||
const quickSearchKeyword = ref('')
|
||
const processedQuickSearchKeyword = ref('')
|
||
|
||
const {
|
||
filters,
|
||
hasActiveFilters,
|
||
setFilterState,
|
||
clearAllFilters,
|
||
isFilterActive,
|
||
toQueryParams,
|
||
resetToInitial,
|
||
watchVisibleColumns
|
||
} = useServerColumnFilters(paymentColumnFilterDefs, {
|
||
initialFilters: {},
|
||
onChange: () => {
|
||
if (activeTab.value === 'pending') {
|
||
runPendingSearch()
|
||
} else {
|
||
runProcessedSearch()
|
||
}
|
||
}
|
||
})
|
||
|
||
const { facetOptionsMap, facetLoadingMap, onFilterOpen } = useColumnFacetOptions({
|
||
facetFields: ['applyCode', 'applicantName', 'title'],
|
||
columnFilterDefs: paymentColumnFilterDefs,
|
||
getContextParams: () => ({
|
||
...(activeTab.value === 'pending' ? buildPendingQuery() : buildProcessedQuery()),
|
||
scope: activeTab.value === 'pending' ? 'payment_pending' : 'payment_processed'
|
||
}),
|
||
fetchFacet: (payload) => fetchReimburseColumnFacetValues(payload as any)
|
||
})
|
||
|
||
const {
|
||
sortField,
|
||
sortOrder,
|
||
setSort,
|
||
clearSort,
|
||
isSortActive,
|
||
toQueryParams: toSortQueryParams,
|
||
watchVisibleColumns: watchSortVisibleColumns
|
||
} = useColumnSort({
|
||
onChange: () => {
|
||
if (activeTab.value === 'pending') {
|
||
runPendingSearch()
|
||
} else {
|
||
runProcessedSearch()
|
||
}
|
||
}
|
||
})
|
||
|
||
provide('crmColumnSort', {
|
||
setSort,
|
||
clearSort,
|
||
isSortActive,
|
||
sortField,
|
||
sortOrder
|
||
})
|
||
|
||
/** 待处理 Tab 不计处理状态/处理时间;切回待处理时自动清掉对应条件 */
|
||
const visibleFilterProps = computed(() => {
|
||
const props: string[] = ['applyCode']
|
||
for (const def of listColumnDefs) {
|
||
if (
|
||
activeTab.value === 'pending' &&
|
||
(def.prop === 'processStatus' || def.prop === 'payTime')
|
||
) {
|
||
continue
|
||
}
|
||
if (isColumnVisible(def.prop)) props.push(def.prop)
|
||
}
|
||
return props
|
||
})
|
||
watchVisibleColumns(visibleFilterProps)
|
||
watchSortVisibleColumns(visibleFilterProps)
|
||
|
||
function handleResetColumns() {
|
||
resetToDefault()
|
||
resetToInitial()
|
||
}
|
||
|
||
function enumOptionsFor(prop: string) {
|
||
const def = paymentColumnFilterDefs.find((d) => d.prop === prop)
|
||
return (def?.enumOptions || []).map((o) => ({ ...o, count: 0 }))
|
||
}
|
||
|
||
const currentRow = ref<Record<string, any>>({})
|
||
const selectedRows = ref<any[]>([])
|
||
const viewData = ref<Record<string, any>>({})
|
||
|
||
const viewBasicKvFields = computed<ViewField[]>(() => {
|
||
const d = viewData.value
|
||
return [
|
||
bxViewKv('单号', d.applyCode, { long: false }),
|
||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||
bxViewKv('报销标题', d.title, { long: false }),
|
||
bxViewKv('审批完成时间', d.applyTime, { long: false }),
|
||
bxViewKvMoney('金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||
bxViewKv('状态', statusLabel(d.status), { long: false, tagType: statusTagType(d.status) })
|
||
]
|
||
})
|
||
|
||
const viewPayKvFields = computed<ViewField[]>(() => {
|
||
const d = viewData.value
|
||
return [
|
||
bxViewKvMoney('已冲抵金额(元)', formatMoneyYuan(d.offsetAmount || 0), { long: false }),
|
||
bxViewKvMoney('已发放金额(元)', formatMoneyYuan(d.actualAmount || 0), { long: false }),
|
||
bxViewKvMoney('未结清余额(元)', formatMoneyYuan(unpaidBalance(d)), { long: false }),
|
||
bxViewKv('发放时间', d.payTime, { long: false }),
|
||
bxViewKv('发放操作人', d.payUserName, { long: false }),
|
||
bxViewKv('发放备注', d.payRemark, { long: false })
|
||
]
|
||
})
|
||
const issueRemark = ref('')
|
||
const actualAmount = ref('')
|
||
const batchIssueRemark = ref('')
|
||
const batchFiles = ref<BxAttachmentItem[]>([])
|
||
const batchAttachmentRef = ref<InstanceType<typeof BxAttachmentUpload> | null>(null)
|
||
const batchLines = ref<
|
||
Array<{
|
||
id: string
|
||
applyCode: string
|
||
applicantName: string
|
||
title: string
|
||
payAmount: string
|
||
actualAmount: string
|
||
}>
|
||
>([])
|
||
const offsetRemark = ref('')
|
||
const uploadedFiles = ref<BxAttachmentItem[]>([])
|
||
const payFiles = ref<BxAttachmentItem[]>([])
|
||
const issueAttachmentRef = ref<InstanceType<typeof BxAttachmentUpload> | null>(null)
|
||
const offsetAttachmentRef = ref<InstanceType<typeof BxAttachmentUpload> | null>(null)
|
||
const viewAttachments = ref<BxAttachmentItem[]>([])
|
||
|
||
const offsetAdvanceFilter = reactive({ keyword: '' })
|
||
const offsetAdvances = ref<any[]>([])
|
||
const offsetAdvanceTotal = ref(0)
|
||
const offsetAdvancePage = ref(1)
|
||
const offsetAdvancePageSize = ref(5)
|
||
const offsetAdvanceLoading = ref(false)
|
||
const offsetSelectedAdvances = ref<any[]>([])
|
||
const offsetAmount = ref('')
|
||
const paymentActionTab = ref<'offset' | 'pay'>('offset')
|
||
|
||
|
||
function getPaymentPendingExportFilterParams() {
|
||
return { ...buildPendingQuery() }
|
||
}
|
||
|
||
function getPaymentProcessedExportFilterParams() {
|
||
return { ...buildProcessedQuery() }
|
||
}
|
||
|
||
const paymentExportColumns = computed(() => {
|
||
const cols = [...visibleColumns.value]
|
||
if (!cols.includes('applyCode')) cols.unshift('applyCode')
|
||
return cols
|
||
})
|
||
|
||
const tabLoaded = reactive({ pending: true, processed: false })
|
||
|
||
const {
|
||
rows: processedRows,
|
||
total: processedTotal,
|
||
currentPage: processedCurrentPage,
|
||
pageSize: processedPageSize,
|
||
rangeText: processedRangeText,
|
||
loading: processedLoading,
|
||
search: processedSearch,
|
||
onSizeChange: processedOnSizeChange
|
||
} = useServerPaged(async (params) => {
|
||
const res: any = await paymentApi.processed(params)
|
||
const raw = res.data
|
||
if (!raw) return { rows: [], total: 0 }
|
||
const flatRows = (raw.rows || []).map((item: any) => ({
|
||
...item.data,
|
||
title: item.data.title || ''
|
||
}))
|
||
return { rows: flatRows, total: raw.total || flatRows.length }
|
||
})
|
||
|
||
const processedViewData = ref<Record<string, any>>({})
|
||
|
||
const processedBasicKvFields = computed<ViewField[]>(() => {
|
||
const d = processedViewData.value
|
||
return [
|
||
bxViewKv('单号', d.applyCode, { long: false }),
|
||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||
bxViewKv('报销标题', d.title || d.reason, { long: false }),
|
||
bxViewKvMoney('金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||
bxViewKv('处理状态', processedStatusLabel(d.status, d.payStatus), {
|
||
long: false,
|
||
tagType: processedStatusTagType(d.status, d.payStatus)
|
||
}),
|
||
bxViewKv('处理时间', d.payTime, { long: false })
|
||
]
|
||
})
|
||
|
||
const processedPayKvFields = computed<ViewField[]>(() => {
|
||
const d = processedViewData.value
|
||
return [
|
||
bxViewKvMoney('已冲抵金额(元)', formatMoneyYuan(d.offsetAmount || 0), { long: false }),
|
||
bxViewKvMoney('已发放金额(元)', formatMoneyYuan(d.actualAmount || 0), { long: false }),
|
||
bxViewKvMoney('未结清余额(元)', formatMoneyYuan(unpaidBalance(d)), { long: false }),
|
||
bxViewKv('发放时间', d.payTime, { long: false }),
|
||
bxViewKv('发放操作人', d.payUserName, { long: false }),
|
||
bxViewKv('发放备注', d.payRemark, { long: false })
|
||
]
|
||
})
|
||
|
||
const processedOffsetKvFields = computed<ViewField[]>(() => {
|
||
const d = processedViewData.value
|
||
const fields: ViewField[] = [
|
||
bxViewKvMoney('冲抵金额(元)', formatMoneyYuan(d.offsetAmount || 0))
|
||
]
|
||
if (d.payTime) fields.push(bxViewKv('处理时间', d.payTime))
|
||
if (d.payUserName) fields.push(bxViewKv('处理操作人', d.payUserName))
|
||
return fields
|
||
})
|
||
const processedApprovals = ref<any[]>([])
|
||
const processedApprovalHistory = ref<any[]>([])
|
||
const processedApprovalLoading = ref(false)
|
||
const processedAttachments = ref<Record<string, BxAttachmentItem[]>>({})
|
||
const processedOffsetRecords = ref<any[]>([])
|
||
|
||
onMounted(() => {
|
||
runPendingSearch()
|
||
})
|
||
|
||
function onSelectionChange(rows: any[]) {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
function requireSinglePendingRow() {
|
||
if (selectedRows.value.length === 0) {
|
||
ElMessage.warning('请先勾选一条待处理记录')
|
||
return null
|
||
}
|
||
if (selectedRows.value.length > 1) {
|
||
ElMessage.warning('只能选择一条待处理记录')
|
||
return null
|
||
}
|
||
return selectedRows.value[0]
|
||
}
|
||
|
||
function handleToolbarIssue() {
|
||
const row = requireSinglePendingRow()
|
||
if (!row) return
|
||
openIssueDialog(row)
|
||
}
|
||
|
||
function handleToolbarOffset() {
|
||
const row = requireSinglePendingRow()
|
||
if (!row) return
|
||
openOffsetDialog(row)
|
||
}
|
||
|
||
function buildPendingQuery() {
|
||
const q: Record<string, string> = { ...toQueryParams(), ...toSortQueryParams() }
|
||
delete q.statusIn
|
||
delete q.status
|
||
const kw = quickSearchKeyword.value.trim()
|
||
if (kw) q.keyword = kw
|
||
return q
|
||
}
|
||
|
||
function buildProcessedQuery() {
|
||
const q: Record<string, string> = { ...toQueryParams(), ...toSortQueryParams() }
|
||
const kw = processedQuickSearchKeyword.value.trim()
|
||
if (kw) q.keyword = kw
|
||
return q
|
||
}
|
||
|
||
function runPendingSearch() {
|
||
search(buildPendingQuery())
|
||
nextTick(() => recalcPendingPaymentHeight())
|
||
}
|
||
|
||
function runProcessedSearch() {
|
||
processedSearch(buildProcessedQuery())
|
||
nextTick(() => recalcProcessedPaymentHeight())
|
||
}
|
||
|
||
function handleQuickSearch() {
|
||
runPendingSearch()
|
||
}
|
||
|
||
function handleQuickSearchClear() {
|
||
quickSearchKeyword.value = ''
|
||
runPendingSearch()
|
||
}
|
||
|
||
function handleTabChange(tab: string | number) {
|
||
if (tab === 'processed' && !tabLoaded.processed) {
|
||
runProcessedSearch()
|
||
tabLoaded.processed = true
|
||
} else if (tab === 'pending') {
|
||
runPendingSearch()
|
||
} else if (tab === 'processed') {
|
||
runProcessedSearch()
|
||
}
|
||
}
|
||
|
||
function handleProcessedQuickSearch() {
|
||
runProcessedSearch()
|
||
}
|
||
|
||
function handleProcessedQuickSearchClear() {
|
||
processedQuickSearchKeyword.value = ''
|
||
runProcessedSearch()
|
||
}
|
||
|
||
async function openView(row: any) {
|
||
viewData.value = row
|
||
viewAttachments.value = []
|
||
try {
|
||
if (row.id) {
|
||
const [payFiles, offsetFiles] = await Promise.all([
|
||
loadBxAttachments('reimburse_pay', row.id).catch(() => []),
|
||
loadBxAttachments('reimburse_offset', row.id).catch(() => [])
|
||
])
|
||
viewAttachments.value = [...payFiles, ...offsetFiles]
|
||
}
|
||
} catch {
|
||
/* skip */
|
||
}
|
||
dialogs.viewDetail = true
|
||
}
|
||
|
||
function openIssueDialog(row: any) {
|
||
openOffsetDialog(row, 'pay')
|
||
}
|
||
|
||
function openBatchIssueDialog() {
|
||
if (selectedRows.value.length === 0) {
|
||
ElMessage.warning('请先勾选待处理记录')
|
||
return
|
||
}
|
||
batchLines.value = selectedRows.value.map((row) => {
|
||
return {
|
||
id: String(row.id),
|
||
applyCode: row.applyCode || '',
|
||
applicantName: row.applicantName || '',
|
||
title: row.title || '',
|
||
payAmount: String(row.amount ?? ''),
|
||
actualAmount: String(unpaidBalance(row))
|
||
}
|
||
})
|
||
batchIssueRemark.value = ''
|
||
batchFiles.value = []
|
||
dialogs.batchIssue = true
|
||
}
|
||
|
||
async function submitBatchIssue() {
|
||
if (batchAttachmentRef.value?.hasUploading()) {
|
||
ElMessage.warning('附件上传中,请稍候')
|
||
return
|
||
}
|
||
const items = batchLines.value.map((line) => ({
|
||
id: line.id,
|
||
actualAmount: Number(line.actualAmount)
|
||
}))
|
||
if (items.some((item) => !Number.isFinite(item.actualAmount) || item.actualAmount < 0)) {
|
||
ElMessage.warning('请填写有效的实际发放金额')
|
||
return
|
||
}
|
||
loadingMap.batchIssueSubmit = true
|
||
try {
|
||
await paymentApi.batchPay({
|
||
payRemark: batchIssueRemark.value,
|
||
items,
|
||
attachmentIds: batchFiles.value.map((file) => file.id)
|
||
})
|
||
ElMessage.success('发放成功')
|
||
dialogs.batchIssue = false
|
||
await refresh()
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message || '发放失败')
|
||
} finally {
|
||
loadingMap.batchIssueSubmit = false
|
||
}
|
||
}
|
||
|
||
async function keepOrCloseBill() {
|
||
await refresh()
|
||
const latest = rows.value.find((item) => item.id === currentRow.value.id)
|
||
if (!latest || unpaidBalance(latest) <= 0) {
|
||
dialogs.offset = false
|
||
return
|
||
}
|
||
currentRow.value = { ...latest }
|
||
}
|
||
|
||
async function submitIssue() {
|
||
if (issueAttachmentRef.value?.hasUploading()) {
|
||
ElMessage.warning('附件上传中,请稍候')
|
||
return
|
||
}
|
||
const amount = Number(actualAmount.value)
|
||
if (!Number.isFinite(amount) || amount <= 0) {
|
||
ElMessage.warning('请填写本次发放金额')
|
||
return
|
||
}
|
||
if (amount > unpaidBalance(currentRow.value) + 0.001) {
|
||
ElMessage.warning('发放金额超过未结清余额')
|
||
return
|
||
}
|
||
loadingMap.issueSubmit = true
|
||
try {
|
||
await paymentApi.pay(currentRow.value.id, {
|
||
payRemark: issueRemark.value,
|
||
actualAmount: amount
|
||
})
|
||
ElMessage.success('发放成功')
|
||
actualAmount.value = ''
|
||
issueRemark.value = ''
|
||
payFiles.value = []
|
||
await keepOrCloseBill()
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message || '发放失败')
|
||
} finally {
|
||
loadingMap.issueSubmit = false
|
||
}
|
||
}
|
||
|
||
async function fetchOffsetAdvances() {
|
||
offsetAdvanceLoading.value = true
|
||
try {
|
||
const res: any = await paymentApi.advanceIssues({
|
||
page: offsetAdvancePage.value,
|
||
pageSize: offsetAdvancePageSize.value,
|
||
issueStatus: 'issued',
|
||
keyword: offsetAdvanceFilter.keyword
|
||
})
|
||
const data = res.data
|
||
if (Array.isArray(data)) {
|
||
offsetAdvances.value = data
|
||
offsetAdvanceTotal.value = data.length
|
||
} else {
|
||
offsetAdvances.value = data?.rows ?? []
|
||
offsetAdvanceTotal.value = data?.total ?? 0
|
||
}
|
||
} finally {
|
||
offsetAdvanceLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleOffsetAdvanceSearch() {
|
||
offsetAdvancePage.value = 1
|
||
fetchOffsetAdvances()
|
||
}
|
||
|
||
function handleOffsetAdvanceReset() {
|
||
offsetAdvanceFilter.keyword = ''
|
||
offsetAdvancePage.value = 1
|
||
fetchOffsetAdvances()
|
||
}
|
||
|
||
function handleOffsetAdvanceSizeChange() {
|
||
offsetAdvancePage.value = 1
|
||
fetchOffsetAdvances()
|
||
}
|
||
|
||
function getAdvanceAvailable(adv: any): number {
|
||
return Math.max(0, Number(adv.actualAmount || adv.amount || 0) - Number(adv.offsetAmount || 0))
|
||
}
|
||
|
||
function handleOffsetSelectionChange(rows: any[]) {
|
||
offsetSelectedAdvances.value = rows
|
||
}
|
||
|
||
function openOffsetDialog(row: any, tab: 'offset' | 'pay' = 'offset') {
|
||
currentRow.value = row
|
||
paymentActionTab.value = tab
|
||
offsetAdvanceFilter.keyword = ''
|
||
offsetAdvancePage.value = 1
|
||
offsetSelectedAdvances.value = []
|
||
offsetAmount.value = ''
|
||
actualAmount.value = ''
|
||
issueRemark.value = ''
|
||
offsetRemark.value = ''
|
||
uploadedFiles.value = []
|
||
payFiles.value = []
|
||
dialogs.offset = true
|
||
if (tab === 'offset') fetchOffsetAdvances()
|
||
}
|
||
|
||
function buildOffsetItems(totalOffset: number) {
|
||
const items: Array<{ advanceId: string; offsetAmount: number; applyCode: string }> = []
|
||
let remaining = totalOffset
|
||
for (const adv of offsetSelectedAdvances.value) {
|
||
if (remaining <= 0.001) break
|
||
const available = getAdvanceAvailable(adv)
|
||
if (available <= 0) continue
|
||
const toOffset = Math.min(remaining, available)
|
||
items.push({
|
||
advanceId: adv.id,
|
||
offsetAmount: Math.round(toOffset * 100) / 100,
|
||
applyCode: adv.applyCode || String(adv.id)
|
||
})
|
||
remaining -= toOffset
|
||
}
|
||
return { items, leftover: remaining }
|
||
}
|
||
|
||
async function submitOffset() {
|
||
if (offsetSelectedAdvances.value.length === 0) {
|
||
ElMessage.warning('请选择要冲抵的备用金申请单')
|
||
return
|
||
}
|
||
const totalOffset = Number(offsetAmount.value) || 0
|
||
if (totalOffset <= 0) {
|
||
ElMessage.warning('请填写冲抵金额')
|
||
return
|
||
}
|
||
if (totalOffset > unpaidBalance(currentRow.value) + 0.001) {
|
||
ElMessage.warning('冲抵金额超过未结清余额')
|
||
return
|
||
}
|
||
if (uploadedFiles.value.length === 0) {
|
||
ElMessage.warning('请至少上传 1 个冲抵附件')
|
||
return
|
||
}
|
||
if (offsetAttachmentRef.value?.hasUploading()) {
|
||
ElMessage.warning('附件上传中,请稍候')
|
||
return
|
||
}
|
||
|
||
const { items, leftover } = buildOffsetItems(totalOffset)
|
||
if (!items.length || leftover > 0.001) {
|
||
ElMessage.warning('冲抵金额超过所选备用金剩余可冲抵')
|
||
return
|
||
}
|
||
const advanceLines = items.map((it) => `${it.applyCode}(冲抵 ${formatMoneyYuan(it.offsetAmount)})`).join('\n')
|
||
const confirmMsg = [
|
||
'请确认冲抵信息:',
|
||
'',
|
||
`本次冲抵金额(元):${formatMoneyYuan(totalOffset)}`,
|
||
'冲抵备用金:',
|
||
advanceLines,
|
||
'',
|
||
'确定提交冲抵吗?'
|
||
].join('\n')
|
||
|
||
try {
|
||
await ElMessageBox.confirm(confirmMsg, '确认冲抵', {
|
||
confirmButtonText: '确定冲抵',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
loadingMap.offsetSubmit = true
|
||
try {
|
||
await paymentApi.batchOffset(currentRow.value.id, {
|
||
items: items.map(({ advanceId, offsetAmount: amt }) => ({ advanceId, offsetAmount: amt })),
|
||
payAmount: 0,
|
||
remark: offsetRemark.value
|
||
})
|
||
ElMessage.success('冲抵成功')
|
||
offsetAmount.value = ''
|
||
offsetRemark.value = ''
|
||
offsetSelectedAdvances.value = []
|
||
uploadedFiles.value = []
|
||
await keepOrCloseBill()
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message || '冲抵失败')
|
||
} finally {
|
||
loadingMap.offsetSubmit = false
|
||
}
|
||
}
|
||
|
||
async function openProcessedView(row: any) {
|
||
processedViewData.value = row
|
||
processedApprovals.value = []
|
||
processedApprovalHistory.value = []
|
||
processedAttachments.value = {}
|
||
processedOffsetRecords.value = []
|
||
processedApprovalLoading.value = true
|
||
dialogs.processedDetail = true
|
||
|
||
const fetchApprovalAndAttachments = async () => {
|
||
const bizId = row.id
|
||
if (!bizId) {
|
||
processedApprovalLoading.value = false
|
||
return
|
||
}
|
||
|
||
const tasks: Promise<any>[] = []
|
||
|
||
tasks.push(
|
||
Promise.all([
|
||
reimburseApi.getApprovals(bizId),
|
||
reimburseApi.getApprovalHistory(bizId)
|
||
])
|
||
.then(([flowRes, historyRes]: any[]) => {
|
||
processedApprovals.value = (flowRes.data || []).sort(
|
||
(a: any, b: any) => a.nodeOrder - b.nodeOrder
|
||
)
|
||
processedApprovalHistory.value = historyRes.data || []
|
||
})
|
||
.catch(() => {})
|
||
)
|
||
tasks.push(
|
||
paymentApi.reimburseOffsetRecords(bizId).then((res: any) => {
|
||
processedOffsetRecords.value = res.data || []
|
||
}).catch(() => {
|
||
processedOffsetRecords.value = []
|
||
})
|
||
)
|
||
|
||
const attachTypes = [
|
||
{ key: '申请附件', bizType: 'reimburse_apply' },
|
||
{ key: '发放附件', bizType: 'reimburse_pay' },
|
||
{ key: '冲抵附件', bizType: 'reimburse_offset' }
|
||
]
|
||
|
||
for (const at of attachTypes) {
|
||
tasks.push(
|
||
loadBxAttachments(at.bizType, bizId)
|
||
.then((files) => {
|
||
processedAttachments.value[at.key] = files
|
||
})
|
||
.catch(() => {
|
||
processedAttachments.value[at.key] = []
|
||
})
|
||
)
|
||
}
|
||
|
||
await Promise.all(tasks)
|
||
processedApprovalLoading.value = false
|
||
}
|
||
|
||
fetchApprovalAndAttachments()
|
||
}
|
||
|
||
function processedStatusLabel(status: string, payStatus?: string): string {
|
||
if (status === 'processed' || status === 'paid' || status === 'offset' || payStatus === 'paid' || payStatus === 'issued')
|
||
return '已处理'
|
||
return status || '-'
|
||
}
|
||
|
||
function processedStatusTagType(
|
||
status: string,
|
||
payStatus?: string
|
||
): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||
if (processedStatusLabel(status, payStatus) === '已处理') return 'success'
|
||
return 'info'
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.bx-list-table-wrap {
|
||
min-width: 0;
|
||
}
|
||
|
||
.batch-issue-remark {
|
||
margin-top: 16px;
|
||
}
|
||
</style>
|