Files
tms-erp-web/src/views/settlement/receivable-payable-detail.vue
T
b2894lxlx cba2ad3b3b 1、调整凭证
2、调整运单
2026-09-08 17:50:34 +08:00

2892 lines
99 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="settlement-detail-page">
<section class="settlement-detail-page__search">
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
<div class="settlement-detail-page__search-grid">
<template v-for="field in visibleSearchFields" :key="field.prop">
<el-form-item :label="field.label">
<el-date-picker
v-if="field.type === 'daterange'"
v-model="query[field.prop]"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="//"
end-placeholder="//"
/>
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
</el-form-item>
</template>
<div class="settlement-detail-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
{{ searchExpanded ? '收起' : '展开' }}
</el-button>
</div>
</div>
</el-form>
</section>
<section class="settlement-detail-page__table-panel">
<div class="settlement-detail-page__toolbar">
<div class="settlement-detail-page__toolbar-left">
<el-button type="primary" @click="openGenerateDialog">生成费用</el-button>
<el-button type="primary" plain @click="openUpdateFeeDialog">更新费用</el-button>
<el-button
v-if="isPayable || isReceivable"
type="primary"
plain
@click="openTransferDialog"
>
批量转结算
</el-button>
<el-button type="primary" plain @click="handleExport">导出</el-button>
</div>
<div class="settlement-detail-page__toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button :icon="Refresh" text @click="loadTable" />
</el-tooltip>
<el-tooltip content="列设置" placement="top">
<el-button :icon="Setting" text @click="noop" />
</el-tooltip>
</div>
</div>
<el-table
v-loading="loading"
:data="rows"
border
class="settlement-detail-page__table"
@selection-change="selectionChange"
>
<el-table-column type="selection" width="52" fixed="left" align="center" />
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column
v-for="column in displayTableColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
:align="column.align || 'center'"
:fixed="column.fixed"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link
v-if="column.link && row[column.prop]"
type="primary"
@click="handleTableLink(row, column)"
>
{{ row[column.prop] }}
</el-link>
<span v-else>{{ formatColumnValue(row, column) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<div class="settlement-detail-page__actions">
<el-link
v-if="row.settlementStatus === 'pending'"
type="primary"
@click="openAdjustDialog(row)"
>
调整
</el-link>
<el-link
v-if="row.settlementStatus === 'pending'"
type="danger"
@click="closeRow(row)"
>
关闭
</el-link>
<el-link v-else type="primary" disabled>-</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadTable"
@size-change="handleSizeChange"
/>
</div>
</section>
<teleport to="body">
<section
v-if="detailDialog.visible && !adjustDialog.visible"
class="settlement-detail-page__detail-panel"
aria-label="单据详情"
>
<header class="settlement-detail-page__detail-panel-header">
<h3>单据详情{{ detailDialog.title }}</h3>
<el-tooltip content="关闭" placement="top">
<el-button :icon="Close" text aria-label="关闭单据详情" @click="closeDetailPanel" />
</el-tooltip>
</header>
<div class="settlement-detail-page__detail-panel-body">
<el-tabs v-model="detailDialog.activeTab" @tab-change="handleDetailTabChange">
<el-tab-pane label="费用明细" name="fee">
<el-table v-loading="detailDialog.loading" :data="feeRows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in feeDetailColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
:align="column.align || 'center'"
show-overflow-tooltip
>
<template #default="{ row }">{{ formatDetailCell(row, column.prop) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="变更记录" name="change">
<el-table v-loading="latestChangeLoading" :data="latestChangeRows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in changeRecordColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default>
<el-link type="primary" @click="openChangeRecordsDialog">查看详情</el-link>
</template>
</el-table-column>
</el-table>
<div
v-if="!latestChangeLoading && !latestChangeRows.length"
class="settlement-detail-page__empty"
>
暂无变更记录
</div>
</el-tab-pane>
</el-tabs>
</div>
<footer class="settlement-detail-page__detail-panel-footer">
<el-button @click="closeDetailPanel">关闭</el-button>
</footer>
</section>
</teleport>
<el-dialog
v-model="changeRecordsDialog.visible"
title="变更记录详情"
width="88%"
append-to-body
>
<el-table v-loading="changeRecordsDialog.loading" :data="changeRows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in changeRecordColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
align="center"
show-overflow-tooltip
/>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="changePage.current"
v-model:page-size="changePage.size"
:total="changePage.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadChangeRecords"
@size-change="handleChangeSizeChange"
/>
</div>
</el-dialog>
<teleport to="body">
<section
v-if="adjustDialog.visible"
class="settlement-detail-page__detail-panel"
aria-label="调整费用"
>
<header class="settlement-detail-page__detail-panel-header">
<h3>调整费用{{ adjustDialog.row?.documentNo || '-' }}</h3>
<el-tooltip content="关闭" placement="top">
<el-button :icon="Close" text aria-label="关闭调整费用" @click="closeAdjustPanel" />
</el-tooltip>
</header>
<div
class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body"
>
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="来源" width="110" align="center">
<template #default="{ row }">{{ feeSourceLabel(row.dataSource) }}</template>
</el-table-column>
<el-table-column
v-for="column in adjustFeeColumns"
:key="column.prop || column.feeItemName"
:label="column.label"
:min-width="column.minWidth"
:align="column.align || 'center'"
show-overflow-tooltip
>
<template #default="{ row }">
<el-select
v-if="column.prop === 'cargoName' && row.manualFee"
v-model="row.cargoName"
class="settlement-detail-page__adjust-control"
:loading="commonCargoLoading"
clearable
filterable
allow-create
default-first-option
placeholder="请选择或输入"
@visible-change="visible => visible && loadCommonCargoOptions()"
@change="value => handleAdjustCargoNameChange(row, value)"
>
<el-option
v-for="item in manualCargoNameOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span v-else-if="column.prop === 'cargoName'">
{{ formatDetailCell(row, column.prop) }}
</span>
<el-cascader
v-else-if="column.prop === 'cargoType' && row.manualFee"
v-model="row.cargoTypePath"
class="settlement-detail-page__adjust-control"
:options="transportCargoTypeOptions"
:props="cargoTypeCascaderProps"
:loading="cargoTypeLoading"
clearable
filterable
:filter-method="filterCargoType"
placeholder="请选择到二级"
@visible-change="visible => visible && loadCargoTypeOptions()"
@change="value => handleAdjustCargoTypeChange(row, value)"
/>
<span v-else-if="column.prop === 'cargoType'">
{{ formatDetailCell(row, column.prop) }}
</span>
<el-select
v-else-if="column.prop === 'specification' && row.manualFee"
v-model="row.specification"
class="settlement-detail-page__adjust-control"
:loading="commonCargoLoading"
clearable
filterable
allow-create
default-first-option
placeholder="请选择或输入"
@visible-change="visible => visible && loadCommonCargoOptions()"
@change="value => handleAdjustSpecificationChange(row, value)"
>
<el-option
v-for="item in manualSpecificationOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-else-if="column.prop === 'billingFactor' && row.manualFee"
v-model="row.billingFactor"
class="settlement-detail-page__adjust-control"
clearable
placeholder="请选择"
@change="handleAdjustBillingFactorChange(row)"
>
<el-option
v-for="item in adjustBillingElements"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select
v-else-if="column.prop === 'billingType' && row.manualFee"
v-model="row.billingType"
class="settlement-detail-page__adjust-control"
:disabled="!row.billingFactor"
clearable
placeholder="请选择"
>
<el-option
v-for="item in adjustBillingTypes(row)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select
v-else-if="column.prop === 'priceUnit'"
v-model="row.priceUnit"
class="settlement-detail-page__adjust-control"
:loading="priceUnitLoading"
clearable
filterable
placeholder="请选择"
@visible-change="visible => visible && loadPriceUnitOptions()"
>
<el-option
v-for="item in priceUnitOptions"
:key="item.id || item.dictKey || item.dictValue"
:label="item.dictValue"
:value="item.dictValue"
/>
</el-select>
<el-input
v-else-if="adjustTextProps.includes(column.prop)"
v-model="row[column.prop]"
clearable
:maxlength="adjustTextMaxlength(column.prop)"
placeholder="请输入"
/>
<el-input
v-else-if="column.prop === 'transportQuantityText'"
class="settlement-detail-page__adjust-input"
:model-value="row.transportQuantity"
inputmode="decimal"
@input="value => handleAdjustDecimalInput(row, 'transportQuantity', value)"
/>
<el-input
v-else-if="column.prop === 'unitPrice'"
class="settlement-detail-page__adjust-input"
:model-value="row.unitPrice"
inputmode="decimal"
@input="value => handleAdjustDecimalInput(row, 'unitPrice', value)"
/>
<el-input
v-else-if="column.prop === 'mileage'"
class="settlement-detail-page__adjust-input"
:model-value="row.mileage"
inputmode="decimal"
@input="value => handleAdjustDecimalInput(row, 'mileage', value)"
/>
<el-input
v-else-if="column.prop === 'freightAmount'"
class="settlement-detail-page__adjust-input"
:model-value="row.freightAmount"
inputmode="decimal"
@input="value => handleAdjustDecimalInput(row, 'freightAmount', value, 'freight')"
/>
<el-input
v-else-if="column.dynamic"
class="settlement-detail-page__adjust-input"
:model-value="row.feeItems[column.feeItemName]"
inputmode="decimal"
@input="value => handleAdjustFeeItemInput(row, column.feeItemName, value)"
/>
<el-input
v-else-if="column.prop === 'changeReason'"
v-model="row.changeReason"
clearable
maxlength="300"
placeholder="请输入调整原因"
/>
<span v-else-if="column.prop === 'adjustAmountText'">
{{ fixedTwoDecimals(row.adjustAmount) }}
</span>
<span v-else-if="column.prop === 'afterAmountText'">
{{ fixedTwoDecimals(row.afterAmount) }}
</span>
<span v-else>{{ formatDetailCell(row, column.prop) }}</span>
</template>
</el-table-column>
</el-table>
</div>
<footer class="settlement-detail-page__detail-panel-footer">
<el-button @click="closeAdjustPanel">取消</el-button>
<el-button
type="primary"
:loading="adjustDialog.submitting"
:disabled="hasAdjustCalculationPending"
@click="saveAdjustFee"
>
保存
</el-button>
</footer>
</section>
</teleport>
<el-dialog v-model="updateFeeDialog.visible" title="更新费用" width="620px" append-to-body>
<el-form
ref="updateFeeFormRef"
:model="updateFeeForm"
:rules="updateFeeRules"
label-position="right"
label-width="auto"
class="settlement-detail-page__dialog-form"
>
<el-form-item label="更新范围" prop="contractId">
<el-input
:model-value="updateContractName"
class="settlement-detail-page__generate-select"
readonly
placeholder="请选择合同"
>
<template #append>
<el-button @click="openUpdateContractDialog">选择</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="合同计费方案" prop="billingPlanId">
<el-select
v-model="updateFeeForm.billingPlanId"
clearable
filterable
:disabled="!updateFeeForm.contractId"
placeholder="请选择"
>
<el-option
v-for="item in billingPlanOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="updateFeeDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="updateFeeDialog.submitting" @click="confirmUpdateFee">
提交
</el-button>
</template>
</el-dialog>
<el-dialog v-model="transferDialog.visible" title="批量转结算" width="86%" append-to-body>
<div class="settlement-detail-page__transfer-form">
<el-form :model="transferQuery" inline label-position="right" label-width="120px">
<el-form-item label="转结算类型">
<el-radio-group
v-model="transferForm.settlementBillType"
@change="handleTransferTypeChange"
>
<el-radio v-if="isPayable" label="pre">预结算单</el-radio>
<el-radio label="formal">正式结算单</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<el-form :model="transferQuery" inline label-position="right" label-width="120px">
<el-form-item
v-for="field in transferSearchFields"
:key="field.prop"
:label="field.label"
>
<el-date-picker
v-if="field.type === 'daterange'"
v-model="transferQuery[field.prop]"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-"
start-placeholder="请选择"
end-placeholder="请选择"
/>
<el-input v-else v-model="transferQuery[field.prop]" clearable placeholder="请输入" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleTransferSearch">查询</el-button>
</el-form-item>
</el-form>
</div>
<el-table
ref="transferTableRef"
v-loading="transferDialog.loading"
:data="transferRows"
row-key="id"
border
@selection-change="transferSelectionChange"
>
<el-table-column type="selection" width="52" align="center" reserve-selection />
<el-table-column
type="index"
label="序号"
width="70"
align="center"
:index="transferIndexMethod"
/>
<el-table-column
v-for="column in tableColumns.slice(0, 13)"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ formatColumnValue(row, column) }}</template>
</el-table-column>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="transferPage.current"
v-model:page-size="transferPage.size"
:total="transferPage.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadTransferCandidates"
@size-change="handleTransferSizeChange"
/>
</div>
<template #footer>
<el-button @click="transferDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
提交
</el-button>
</template>
</el-dialog>
<el-dialog v-model="generateDialog.visible" title="生成费用" width="88%" append-to-body>
<el-form
:model="generateQuery"
class="settlement-detail-page__generate-search"
inline
label-position="right"
label-width="120px"
>
<el-form-item label="运单合同" required>
<el-input
:model-value="generateContractName"
class="settlement-detail-page__generate-select"
readonly
placeholder="请选择合同"
>
<template #append>
<el-button @click="openGenerateContractDialog">选择</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="计费方案" required>
<el-select
v-model="generateQuery.billingPlanId"
class="settlement-detail-page__generate-select"
filterable
clearable
:disabled="!generateQuery.contractId"
:placeholder="generateQuery.contractId ? '请选择' : '请先选择合同'"
>
<el-option
v-for="item in billingPlanOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="批次号">
<el-input v-model="generateQuery.batchNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="运单完成日期">
<el-date-picker
v-model="generateQuery.finishDateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-"
start-placeholder="请选择"
end-placeholder="请选择"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadGenerateWaybills">查询</el-button>
<el-button @click="resetGenerateQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="generateDialog.loading"
:data="generateRows"
border
@selection-change="generateSelectionChange"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
v-for="column in generateWaybillColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link v-if="column.link && row[column.prop]" type="primary">
{{ row[column.prop] }}
</el-link>
<span v-else>{{ formatColumnValue(row, column) }}</span>
</template>
</el-table-column>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="generatePage.current"
v-model:page-size="generatePage.size"
:total="generatePage.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadGenerateWaybills"
@size-change="handleGenerateSizeChange"
/>
</div>
<template #footer>
<el-button @click="generateDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="previewDialog.loading" @click="openGeneratePreview">
生成费用
</el-button>
</template>
</el-dialog>
<el-dialog v-model="previewDialog.visible" title="生成费用明细" width="88%" append-to-body>
<el-table v-loading="previewDialog.loading" :data="previewRows" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
v-for="column in previewColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
align="center"
show-overflow-tooltip
/>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="previewPage.current"
v-model:page-size="previewPage.size"
:total="previewPage.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadGeneratePreview"
@size-change="handlePreviewSizeChange"
/>
</div>
<template #footer>
<el-button @click="previewDialog.visible = false">取消</el-button>
<el-button type="primary" plain @click="backToGenerateDialog">上一步</el-button>
<el-button type="primary" :loading="previewDialog.submitting" @click="submitGenerateFee">
提交
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="generateContractDialog.visible"
title="选择合同"
width="92%"
append-to-body
destroy-on-close
class="settlement-detail-page__contract-dialog"
@opened="layoutGenerateContractTable"
>
<el-form
:model="generateContractDialog.query"
label-position="right"
label-width="auto"
class="settlement-detail-page__contract-filter"
@submit.prevent
>
<el-form-item label="合同编号">
<el-input
v-model="generateContractDialog.query.contractNo"
clearable
placeholder="请输入"
@keyup.enter="loadGenerateContracts"
/>
</el-form-item>
<el-form-item label="合同名称">
<el-input
v-model="generateContractDialog.query.contractName"
clearable
placeholder="请输入"
@keyup.enter="loadGenerateContracts"
/>
</el-form-item>
<el-form-item label="所属项目">
<el-input
v-model="generateContractDialog.query.projectName"
clearable
placeholder="请输入"
@keyup.enter="loadGenerateContracts"
/>
</el-form-item>
<el-form-item label="所属组织">
<el-input
v-model="generateContractDialog.query.organizationName"
clearable
placeholder="请输入"
@keyup.enter="loadGenerateContracts"
/>
</el-form-item>
<el-form-item label="合同类别">
<el-select :model-value="generateContractCategory" disabled placeholder="请选择">
<el-option
v-for="item in contractCategoryOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="签约类型">
<el-select v-model="generateContractDialog.query.signType" clearable placeholder="请选择">
<el-option
v-for="item in signTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="生效类型">
<el-select
v-model="generateContractDialog.query.effectiveType"
clearable
placeholder="请选择"
>
<el-option
v-for="item in effectiveTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="合同阶段">
<el-select
v-model="generateContractDialog.query.contractStage"
clearable
placeholder="请选择"
>
<el-option
v-for="item in contractStageOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="generateContractDialog.expanded" label="审核状态">
<el-select
v-model="generateContractDialog.query.approvalStatus"
clearable
placeholder="请选择"
>
<el-option
v-for="item in contractApprovalStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item class="settlement-detail-page__contract-filter-actions">
<el-button type="primary" @click="searchGenerateContracts">查询</el-button>
<el-button @click="resetGenerateContractQuery">重置</el-button>
<el-button
text
@click="generateContractDialog.expanded = !generateContractDialog.expanded"
>
{{ generateContractDialog.expanded ? '收起' : '展开' }}
</el-button>
</el-form-item>
</el-form>
<el-table
ref="generateContractTable"
v-loading="generateContractDialog.loading"
:data="generateContractDialog.rows"
style="width: 100%"
border
highlight-current-row
@current-change="generateContractDialog.current = $event"
@row-dblclick="selectContract"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
prop="contractNo"
label="合同编号"
min-width="180"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="contractName"
label="合同名称"
min-width="220"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="projectName"
label="所属项目"
min-width="170"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="organizationName"
label="所属组织"
min-width="170"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="contractCategory"
label="合同类别"
min-width="130"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="signType"
label="签约类型"
min-width="120"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="partyA"
label="甲方"
min-width="170"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="partyB"
label="乙方"
min-width="170"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="startDate"
label="开始日期"
min-width="130"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="endDate"
label="结束日期"
min-width="130"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="temporaryStartDate"
label="临时效力起"
min-width="130"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column
prop="temporaryEndDate"
label="临时效力止"
min-width="130"
align="center"
show-overflow-tooltip
:formatter="formatContractSelectionCell"
/>
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectContract(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="generateContractDialog.page.current"
v-model:page-size="generateContractDialog.page.size"
:total="generateContractDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadGenerateContracts"
@size-change="handleGenerateContractSizeChange"
/>
</div>
<template #footer>
<el-button @click="generateContractDialog.visible = false">取消</el-button>
<el-button type="primary" @click="selectContract(generateContractDialog.current)">
确定
</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { ArrowDown, ArrowUp, Close, Refresh, Setting } from '@element-plus/icons-vue';
import {
changeRecordColumns,
feeDetailBaseColumns,
feeDetailTailColumns,
generatePreviewColumns,
generateWaybillColumns,
searchFields,
settlementStatusOptions,
tableColumns,
transferSearchFields,
} from '@/option/settlement/receivable-payable-detail';
import * as api from '@/api/settlement/receivable-payable-detail';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { getContractOptions as getSettlementContractOptions } from '@/api/settlement/preSettlement';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
import { getDictionary } from '@/api/system/dictbiz';
import { downloadXls } from '@/utils/util';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
import {
contractApprovalStatusOptions,
contractCategoryOptions,
contractStageOptions,
effectiveTypeOptions,
signTypeOptions,
} from '@/option/business/common';
const ADJUST_BILLING_ELEMENTS = [
'按重量',
'按体积',
'按车辆',
'按里程',
'按吨·公里',
'固定金额(整单一口价)',
'按数量',
];
const ADJUST_BILLING_TYPE_MAP = {
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按车辆: ['固定单价'],
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'固定金额(整单一口价)': ['固定一口价'],
按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
};
export default {
props: {
settlementType: {
type: String,
default: '',
},
},
data() {
return {
ArrowDown,
ArrowUp,
Close,
Refresh,
Setting,
searchFields,
tableColumns,
tableFeeItemNames: [],
changeRecordColumns,
transferSearchFields,
generateWaybillColumns,
loading: false,
searchExpanded: false,
query: {},
rows: [],
selection: [],
page: { current: 1, size: 10, total: 0 },
detailDialog: {
visible: false,
title: '费用明细',
activeTab: 'fee',
row: null,
loading: false,
},
feeRows: [],
dynamicFeeColumns: [],
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
adjustRows: [],
adjustDynamicFeeColumns: [],
adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'],
adjustBillingElements: ADJUST_BILLING_ELEMENTS,
commonCargoOptions: [],
commonCargoLoading: false,
commonCargoRequest: null,
cargoTypeOptions: [],
cargoTypeFlatOptions: [],
cargoTypeLoading: false,
cargoTypeRequest: null,
cargoTypeCascaderProps: {
label: 'cargoName',
value: 'id',
children: 'children',
emitPath: true,
},
priceUnitOptions: [],
priceUnitLoading: false,
priceUnitRequest: null,
changeRows: [],
changePage: { current: 1, size: 10, total: 0 },
latestChangeRows: [],
latestChangeLoading: false,
changeRecordsDialog: { visible: false, loading: false },
updateFeeDialog: { visible: false, submitting: false, row: null },
updateFeeForm: { contractId: '', billingPlanId: '' },
updateFeeRules: {
contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }],
},
contractOptions: [],
updateContractOptions: [],
contractCategoryOptions,
signTypeOptions,
effectiveTypeOptions,
contractStageOptions,
contractApprovalStatusOptions,
transportTypeOptions: [],
billingPlanOptions: [],
transferDialog: { visible: false, loading: false, submitting: false },
transferQuery: {},
transferForm: { settlementBillType: 'pre' },
transferRows: [],
transferSelection: [],
transferPage: { current: 1, size: 10, total: 0 },
generateDialog: { visible: false, loading: false },
generateQuery: {},
contractDialogMode: 'generate',
generateContractDialog: {
visible: false,
loading: false,
expanded: false,
rows: [],
current: null,
query: {
contractNo: '',
contractName: '',
projectName: '',
organizationName: '',
contractCategory: '',
signType: '',
effectiveType: '',
contractStage: '',
approvalStatus: '',
},
page: { current: 1, size: 10, total: 0 },
},
generateRows: [],
generateSelection: [],
generatePage: { current: 1, size: 10, total: 0 },
previewDialog: { visible: false, loading: false, submitting: false },
previewRows: [],
previewPage: { current: 1, size: 10, total: 0 },
};
},
computed: {
generateContractName() {
const contract = this.contractOptions.find(
item => String(item.id) === String(this.generateQuery.contractId)
);
return contract?.contractName || '';
},
updateContractName() {
const contractId = this.updateFeeForm.contractId;
const contract = [...this.updateContractOptions, ...this.contractOptions].find(
item => String(item.id) === String(contractId)
);
return contract?.contractName || '';
},
generateContractCategory() {
return this.settlementType === 'payable' ? '承运商合同' : '客户合同';
},
settlementTypeLabel() {
if (this.settlementType === 'receivable') return '应收';
if (this.settlementType === 'payable') return '应付';
return '应收应付';
},
isReceivable() {
return this.settlementType === 'receivable';
},
isPayable() {
return this.settlementType === 'payable';
},
hasAdjustCalculationPending() {
return this.adjustRows.some(row => row.calculating);
},
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
feeDetailColumns() {
const baseColumns = feeDetailBaseColumns.filter(column => column.prop !== 'freightAmount');
return [...baseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
},
displayTableColumns() {
const columns = this.tableColumns.filter(
column => this.settlementType !== 'receivable' || column.prop !== 'preSettlementNo'
);
const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText');
const otherFeeColumn = {
label: '其他费用',
prop: 'tableOtherFeeAmount',
feeSummaryType: 'other',
minWidth: 130,
align: 'right',
};
const dynamicColumns = this.isPayable
? [
{
label: '运输费',
prop: 'tableFreightAmount',
feeSummaryType: 'freight',
minWidth: 130,
align: 'right',
},
otherFeeColumn,
]
: [
...this.tableFeeItemNames.map((name, index) => ({
label: name,
prop: `tableFeeItem${index}`,
feeItemName: name,
dynamic: true,
minWidth: 130,
align: 'right',
})),
otherFeeColumn,
];
if (totalIndex < 0) return [...columns, ...dynamicColumns];
columns.splice(totalIndex, 0, ...dynamicColumns);
return columns;
},
previewColumns() {
return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
},
adjustFeeColumns() {
const baseColumns = feeDetailBaseColumns.filter(column => column.prop !== 'freightAmount');
const tailColumns = feeDetailTailColumns.filter(column => column.prop !== 'remark');
const afterAmountIndex = tailColumns.findIndex(column => column.prop === 'afterAmountText');
tailColumns.splice(afterAmountIndex + 1, 0, {
label: '调整原因',
prop: 'changeReason',
minWidth: 220,
});
return [...baseColumns, ...this.adjustDynamicFeeColumns, ...tailColumns];
},
transportCargoTypeOptions() {
return this.cargoTypeOptions.filter(item => item.children?.length);
},
},
watch: {
'$route.fullPath'() {
this.openRouteDetail();
},
},
async mounted() {
this.loadTransportTypeOptions();
this.loadCargoTypeOptions();
this.loadPriceUnitOptions();
await this.loadTable();
await this.openRouteDetail();
},
beforeUnmount() {
this.clearAdjustCalculations();
},
methods: {
contractCategoryName(value) {
return (
this.contractCategoryOptions.find(item => String(item.value) === String(value))?.label ||
this.formatCell(value)
);
},
signTypeName(value) {
return (
this.signTypeOptions.find(item => String(item.value) === String(value))?.label ||
this.formatCell(value)
);
},
displayValue(value) {
return this.formatCell(value);
},
async loadTransportTypeOptions() {
const res = await getDictionary({ code: 'transport_type' });
const records = res.data?.data || [];
this.transportTypeOptions = records.map(item => ({
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
},
transportTypeLabel(value) {
if (value === null || value === undefined || value === '') return '-';
return (
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
);
},
loadCargoTypeOptions() {
if (this.cargoTypeOptions.length) return Promise.resolve(this.cargoTypeOptions);
if (this.cargoTypeRequest) return this.cargoTypeRequest;
this.cargoTypeLoading = true;
this.cargoTypeRequest = getCargoTypeList(1, 9999)
.then(res => {
this.cargoTypeOptions = this.buildCargoTypeTree(this.extractRecords(res));
this.cargoTypeFlatOptions = this.flattenCargoTypeOptions(this.cargoTypeOptions);
return this.cargoTypeOptions;
})
.finally(() => {
this.cargoTypeLoading = false;
this.cargoTypeRequest = null;
});
return this.cargoTypeRequest;
},
loadPriceUnitOptions() {
if (this.priceUnitOptions.length) return Promise.resolve(this.priceUnitOptions);
if (this.priceUnitRequest) return this.priceUnitRequest;
this.priceUnitLoading = true;
this.priceUnitRequest = getDictionary({ code: 'unit_fee' })
.then(res => {
this.priceUnitOptions = this.extractRecords(res);
return this.priceUnitOptions;
})
.finally(() => {
this.priceUnitLoading = false;
this.priceUnitRequest = null;
});
return this.priceUnitRequest;
},
loadCommonCargoOptions() {
if (this.commonCargoOptions.length) return Promise.resolve(this.commonCargoOptions);
if (this.commonCargoRequest) return this.commonCargoRequest;
this.commonCargoLoading = true;
this.commonCargoRequest = getCommonCargoList(1, 9999)
.then(res => {
this.commonCargoOptions = this.extractRecords(res);
return this.commonCargoOptions;
})
.finally(() => {
this.commonCargoLoading = false;
this.commonCargoRequest = null;
});
return this.commonCargoRequest;
},
commonCargoMatches(row) {
const cargoType = String(row.cargoType || '').trim();
if (!cargoType) return this.commonCargoOptions;
const matched = this.commonCargoOptions.filter(item =>
[item.secondCargoTypeName, item.firstCargoTypeName, item.cargoType].some(
value => String(value || '').trim() === cargoType
)
);
return matched.length ? matched : this.commonCargoOptions;
},
manualCargoNameOptions(row) {
const options = new Map();
this.commonCargoMatches(row).forEach(item => {
const value = String(item.cargoName || '').trim();
if (value && !options.has(value)) options.set(value, { label: value, value });
});
return Array.from(options.values());
},
manualSpecificationOptions(row) {
const cargoName = String(row.cargoName || '').trim();
const records = cargoName
? this.commonCargoMatches(row).filter(
item => String(item.cargoName || '').trim() === cargoName
)
: this.commonCargoMatches(row);
const options = new Map();
records.forEach(item => {
const value = String(item.specification || item.spec || '').trim();
if (value && !options.has(value)) options.set(value, { label: value, value });
});
return Array.from(options.values());
},
handleAdjustCargoNameChange(row, value) {
row.cargoName = String(value || '').trim();
const cargo = this.commonCargoMatches(row).find(
item => String(item.cargoName || '').trim() === row.cargoName
);
if (cargo) this.applyCommonCargoToAdjustRow(row, cargo);
},
handleAdjustSpecificationChange(row, value) {
row.specification = String(value || '').trim();
const cargo = this.commonCargoMatches(row).find(
item =>
String(item.cargoName || '').trim() === String(row.cargoName || '').trim() &&
String(item.specification || item.spec || '').trim() === row.specification
);
if (cargo) this.applyCommonCargoToAdjustRow(row, cargo);
},
applyCommonCargoToAdjustRow(row, cargo) {
row.cargoName = cargo.cargoName || row.cargoName || '';
row.specification = cargo.specification || cargo.spec || row.specification || '';
row.model = cargo.model || row.model || '';
row.priceUnit = cargo.priceUnit || row.priceUnit || '';
row.cargoType =
cargo.secondCargoTypeName ||
cargo.cargoType ||
cargo.firstCargoTypeName ||
row.cargoType ||
'';
row.cargoTypePath = this.resolveCargoTypePath(row.cargoType);
},
buildCargoTypeTree(cargoTypes = []) {
const flatCargoTypes = [];
const collectCargoTypes = list => {
(list || []).forEach(item => {
flatCargoTypes.push({ ...item, children: undefined });
if (item.children?.length) collectCargoTypes(item.children);
});
};
collectCargoTypes(cargoTypes);
const nodeMap = new Map();
const codeMap = new Map();
flatCargoTypes.forEach(item => {
const id = item.id ?? item.cargoCode ?? item.code;
if (id === undefined || id === null) return;
const node = {
...item,
id: String(id),
cargoName: this.formatCargoTypeLabel(item),
cargoCode: item.cargoCode || item.code || '',
parentId:
item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
parentCargoCode: item.parentCargoCode || item.parentCode || '',
children: [],
};
nodeMap.set(node.id, node);
if (node.cargoCode) codeMap.set(String(node.cargoCode), node);
});
const rootNodes = [];
nodeMap.forEach(node => {
const parent =
nodeMap.get(node.parentId) ||
codeMap.get(String(node.parentCargoCode || '')) ||
codeMap.get(String(node.parentId || ''));
if (parent && parent !== node && Number(node.typeLevel) !== 1) {
parent.children.push(node);
} else {
rootNodes.push(node);
}
});
return rootNodes.map(item => this.normalizeCargoTypeNode(item, 1)).filter(Boolean);
},
normalizeCargoTypeNode(cargoType, level = 1, parentPath = []) {
if (level > 2) return null;
const id = String(cargoType.id);
const path = [...parentPath, id];
const children =
level === 1
? (cargoType.children || [])
.map(item => this.normalizeCargoTypeNode(item, level + 1, path))
.filter(Boolean)
: [];
return {
...cargoType,
id,
cargoName: this.formatCargoTypeLabel(cargoType),
path,
leaf: level === 2,
children: children.length ? children : undefined,
};
},
flattenCargoTypeOptions(options = []) {
const result = [];
const collectOptions = list => {
(list || []).forEach(item => {
result.push(item);
if (item.children?.length) collectOptions(item.children);
});
};
collectOptions(options);
return result;
},
formatCargoTypeLabel(item = {}) {
return item.cargoName || item.name || item.typeName || item.label || '';
},
filterCargoType(node, keyword) {
const text = String(keyword || '').trim();
if (!text) return true;
const labels = node.pathLabels?.length ? node.pathLabels : [node.label];
return (
labels.some(label => String(label || '').includes(text)) ||
String(node.text || '').includes(text)
);
},
getCargoTypePathLabels(path = []) {
let options = this.cargoTypeOptions;
const labels = [];
(path || []).forEach(value => {
const option = (options || []).find(item => String(item.id) === String(value));
if (!option) return;
labels.push(option.cargoName || '');
options = option.children || [];
});
return labels.filter(Boolean);
},
resolveCargoTypePath(cargoTypeName) {
const name = String(cargoTypeName || '').trim();
if (!name) return [];
const cargoType = this.cargoTypeFlatOptions.find(item => {
const label = this.formatCargoTypeLabel(item);
return item.path?.length >= 2 && (label === name || name.endsWith(label));
});
return cargoType?.path || [];
},
handleAdjustCargoTypeChange(row, value) {
const path =
Array.isArray(value) && value.length >= 2
? value.slice(0, 2).map(item => String(item))
: [];
const labels = this.getCargoTypePathLabels(path);
row.cargoTypePath = path;
row.cargoType = labels.length ? labels[labels.length - 1] : '';
row.cargoName = '';
row.specification = '';
row.model = '';
},
formatColumnValue(row, column) {
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
if (column.prop === 'transportQuantity') return this.fixedTwoDecimals(row[column.prop]);
if (column.feeSummaryType) {
return this.money(
this.summarizeTableFee(row, column.feeSummaryType),
row.currency || 'RMB'
);
}
if (column.dynamic) {
return this.money(
this.normalizeFeeItems(row.feeItems)[column.feeItemName],
row.currency || 'RMB'
);
}
return this.formatDetailCell(row, column.prop);
},
async loadTable() {
this.loading = true;
this.tableFeeItemNames = [];
try {
const params = this.buildRequestParams(
this.normalizeQuery(
this.query,
'generateDateRange',
'generateStartDate',
'generateEndDate'
)
);
const res = await api.getList(this.page.current, this.page.size, params);
const data = this.unwrapPage(res);
this.rows = (data.records || []).map(this.decorateRow);
this.tableFeeItemNames = this.isPayable ? [] : this.collectFeeItemNames(this.rows);
this.page.total = data.total || 0;
} finally {
this.loading = false;
}
},
openGenerateContractDialog() {
this.contractDialogMode = 'generate';
this.generateContractDialog.visible = true;
this.generateContractDialog.expanded = false;
this.generateContractDialog.current = null;
this.generateContractDialog.page.current = 1;
this.loadGenerateContracts();
},
openUpdateContractDialog() {
this.contractDialogMode = 'update';
this.generateContractDialog.query = {
contractNo: '',
contractName: '',
projectName: '',
organizationName: '',
contractCategory: '',
signType: '',
effectiveType: '',
contractStage: '',
approvalStatus: '',
};
this.generateContractDialog.visible = true;
this.generateContractDialog.expanded = false;
this.generateContractDialog.current = null;
this.generateContractDialog.page.current = 1;
this.loadGenerateContracts();
},
async loadGenerateContracts() {
this.generateContractDialog.loading = true;
try {
const res = await getContractList(
this.generateContractDialog.page.current,
this.generateContractDialog.page.size,
{
...this.generateContractDialog.query,
contractCategory: this.generateContractCategory,
approvalStatuses: 'approved,change_approved',
}
);
const data = this.unwrapPage(res);
this.generateContractDialog.rows = data.records || [];
this.generateContractDialog.page.total = Number(data.total || 0);
} finally {
this.generateContractDialog.loading = false;
this.layoutGenerateContractTable();
}
},
layoutGenerateContractTable() {
this.$nextTick(() => {
window.requestAnimationFrame(() => {
this.$refs.generateContractTable?.doLayout?.();
});
});
},
formatContractSelectionCell(row, column, cellValue) {
if (column.property === 'organizationName') {
return this.displayValue(row.organizationName || row.deptName);
}
if (column.property === 'contractCategory') {
return this.contractCategoryName(row.contractCategory);
}
if (column.property === 'signType') {
return this.signTypeName(row.signType);
}
return this.displayValue(cellValue);
},
searchGenerateContracts() {
this.generateContractDialog.page.current = 1;
this.loadGenerateContracts();
},
resetGenerateContractQuery() {
this.generateContractDialog.query = {
contractNo: '',
contractName: '',
projectName: '',
organizationName: '',
contractCategory: '',
signType: '',
effectiveType: '',
contractStage: '',
approvalStatus: '',
};
this.searchGenerateContracts();
},
handleGenerateContractSizeChange() {
this.generateContractDialog.page.current = 1;
this.loadGenerateContracts();
},
selectContract(row) {
if (!row) {
this.$message.warning('请选择合同');
return;
}
const contract = {
...row,
deptId: row.deptId || row.organizationId,
deptName: row.deptName || row.organizationName,
partyA: row.partyA || row.payerName,
partyB: row.partyB || row.payeeName,
};
const targetOptions =
this.contractDialogMode === 'update' ? this.updateContractOptions : this.contractOptions;
const index = targetOptions.findIndex(item => String(item.id) === String(contract.id));
if (index >= 0) targetOptions.splice(index, 1, contract);
else targetOptions.push(contract);
if (this.contractDialogMode === 'update') {
this.updateFeeForm.contractId = contract.id;
this.handleUpdateContractChange(contract.id);
} else {
this.generateQuery.contractId = contract.id;
this.handleGenerateContractChange(contract.id);
}
this.generateContractDialog.visible = false;
this.generateContractDialog.current = null;
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
handleReset() {
this.query = {};
this.handleSearch();
},
toggleSearch() {
this.searchExpanded = !this.searchExpanded;
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
selectionChange(selection) {
this.selection = selection;
},
closeDetailPanel() {
this.detailDialog.visible = false;
this.detailDialog.row = null;
this.changeRecordsDialog.visible = false;
},
closeAdjustPanel() {
this.clearAdjustCalculations();
this.adjustDialog.visible = false;
this.adjustDialog.row = null;
},
handleTableLink(row, column) {
if (column.prop === 'waybillNo') {
this.openWaybillDetail(row);
return;
}
if (this.settlementType === 'payable' && column.prop === 'preSettlementNo') {
this.openSettlementDetail('/settlement/pre-settlement', row.preSettlementNo);
return;
}
if (this.settlementType === 'payable' && column.prop === 'formalSettlementNo') {
this.openSettlementDetail('/settlement/formal-settlement', row.formalSettlementNo);
return;
}
this.openDetailDialog(row);
},
openSettlementDetail(path, detailNo) {
if (!detailNo) {
this.$message.info('当前记录未关联结算单详情');
return;
}
this.$router.push({ path, query: { detailNo } });
},
async openRouteDetail(detailId = this.$route.query.detailId) {
if (
!detailId ||
this.settlementType !== 'payable' ||
this.$route.path !== '/settlement/payable-detail'
) {
return;
}
let row = this.rows.find(item => String(item.id) === String(detailId));
if (!row) {
const res = await api.getList(1, 1, {
ids: detailId,
settlementType: this.settlementType,
});
const data = this.unwrapPage(res);
if (data.records?.[0]) row = this.decorateRow(data.records[0]);
}
if (!row) {
this.$message.info('当前记录未找到对应的应付明细');
return;
}
await this.openDetailDialog(row);
},
openWaybillDetail(row) {
if (!row.waybillId) {
this.$message.info('当前记录未关联运单详情');
return;
}
this.$router.push({
path: '/business/waybill-manage/detail',
query: { id: row.waybillId },
});
},
async openDetailDialog(row) {
this.closeAdjustPanel();
this.detailDialog = {
...this.detailDialog,
visible: true,
row,
title: row.documentNo,
activeTab: 'fee',
};
await this.loadFeeDetail();
},
async openAdjustDialog(row) {
this.closeDetailPanel();
this.adjustDialog = { ...this.adjustDialog, visible: true, row, loading: true };
try {
const [res] = await Promise.all([
api.getFeeDetail(row.id),
this.loadCommonCargoOptions(),
this.loadCargoTypeOptions(),
this.loadPriceUnitOptions(),
]);
const data = res.data?.data || res.data || res || {};
this.adjustDynamicFeeColumns = (data.feeItemNames || []).map(name => ({
label: name,
feeItemName: name,
dynamic: true,
minWidth: 150,
align: 'right',
}));
this.adjustRows = (data.records || []).map(item => {
const feeItems = {};
(data.feeItemNames || []).forEach(name => {
feeItems[name] = Number(item.feeItems?.[name] || 0);
});
const adjusted = {
...item,
transportQuantity: Number(item.transportQuantity || 0),
mileage:
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
? null
: Number(item.mileage),
freightAmount: Number(item.freightAmount || 0),
originalAmount: Number(item.originalAmount || 0),
feeItems,
changeReason: '',
dataSource:
item.dataSource || (item.billingFactor === '手工调整' ? '手动录入' : '自动生成'),
manualFee:
['手动录入', '手动添加', '手工录入'].includes(item.dataSource) ||
item.billingFactor === '手工调整',
cargoTypePath: this.resolveCargoTypePath(item.cargoType),
calculating: false,
adjustCalculateTimer: null,
adjustCalculateVersion: 0,
};
this.recalculateAdjustRow(adjusted);
return adjusted;
});
} finally {
this.adjustDialog.loading = false;
}
},
feeSourceLabel(value) {
return ['手动录入', '手动添加', '手工录入'].includes(value) ? '手动录入' : '自动生成';
},
adjustBillingTypes(row) {
return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || [];
},
handleAdjustBillingFactorChange(row) {
if (!this.adjustBillingTypes(row).includes(row.billingType)) row.billingType = '';
},
adjustTextMaxlength(prop) {
return ['billingFactor', 'billingType'].includes(prop) ? 100 : 255;
},
fixedTwoDecimals(value) {
return Number(value || 0).toFixed(2);
},
normalizeAdjustDecimal(value) {
const text = String(value ?? '').replace(/[^\d.]/g, '');
const decimalIndex = text.indexOf('.');
if (decimalIndex < 0) return text;
const integer = text.slice(0, decimalIndex).replace(/\D/g, '') || '0';
const decimal = text
.slice(decimalIndex + 1)
.replace(/\D/g, '')
.slice(0, 2);
return `${integer}.${decimal}`;
},
handleAdjustDecimalInput(row, prop, value, changedField) {
row[prop] = this.normalizeAdjustDecimal(value);
if (!row.manualFee && row.id && ['transportQuantity', 'mileage'].includes(prop)) {
this.scheduleAdjustFeeCalculation(row);
return;
}
this.recalculateAdjustRow(row, changedField);
},
handleAdjustFeeItemInput(row, feeItemName, value) {
row.feeItems[feeItemName] = this.normalizeAdjustDecimal(value);
this.recalculateAdjustRow(row, feeItemName);
},
recalculateAdjustRow(row, changedField) {
if (changedField && changedField !== 'freight' && this.isFreightFeeItem(changedField)) {
row.freightAmount = Number(row.feeItems[changedField] || 0);
}
if (changedField === 'freight') {
const freightItem = Object.keys(row.feeItems).find(this.isFreightFeeItem);
if (freightItem) row.feeItems[freightItem] = Number(row.freightAmount || 0);
}
const feeItemTotal = Object.values(row.feeItems).reduce(
(total, value) => total + Number(value || 0),
0
);
const hasFreightItem = Object.keys(row.feeItems).some(this.isFreightFeeItem);
row.afterAmount = Number(
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
);
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
},
scheduleAdjustFeeCalculation(row) {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
row.adjustCalculateVersion += 1;
row.calculating = true;
const version = row.adjustCalculateVersion;
row.adjustCalculateTimer = setTimeout(() => {
row.adjustCalculateTimer = null;
this.calculateAdjustRow(row, version);
}, 300);
},
async calculateAdjustRow(row, version) {
try {
const res = await api.calculateAdjustedFee({
detailId: this.adjustDialog.row?.id,
feeId: row.id,
transportQuantity: Number(row.transportQuantity || 0),
mileage: Number(row.mileage || 0),
freightAmount: Number(row.freightAmount || 0),
feeItems: Object.fromEntries(
Object.entries(row.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
),
});
if (version !== row.adjustCalculateVersion || !this.adjustDialog.visible) return;
const data = res.data?.data || res.data || res || {};
row.freightAmount = Number(data.freightAmount || 0);
row.feeItems = Object.fromEntries(
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
);
row.adjustAmount = Number(data.adjustAmount || 0);
row.afterAmount = Number(data.afterAmount || 0);
} catch (error) {
row.calculateError = error.message;
} finally {
if (version === row.adjustCalculateVersion) row.calculating = false;
}
},
clearAdjustCalculations() {
this.adjustRows.forEach(row => {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
row.adjustCalculateTimer = null;
row.adjustCalculateVersion += 1;
row.calculating = false;
});
},
isFreightFeeItem(name) {
return String(name || '').includes('运费') || String(name || '').includes('运输费');
},
async saveAdjustFee() {
if (!this.adjustRows.length || !this.adjustDialog.row) {
this.$message.warning('没有可调整的费用明细');
return;
}
if (this.hasAdjustCalculationPending) {
this.$message.warning('费用正在重新计算,请稍候');
return;
}
if (!this.validateManualAdjustRows()) return;
this.adjustDialog.submitting = true;
try {
await api.adjustFee({
detailId: this.adjustDialog.row.id,
adjustReason: [
...new Set(
this.adjustRows.map(row => String(row.changeReason || '').trim()).filter(Boolean)
),
].join(''),
rows: this.adjustRows.map(row => ({
id: row.id,
cargoName: String(row.cargoName || '').trim(),
cargoType: row.cargoType,
specification: String(row.specification || '').trim(),
model: row.model,
billingFactor: row.billingFactor,
billingType: row.billingType,
transportQuantity: row.transportQuantity,
priceUnit: row.priceUnit,
unitPrice: row.unitPrice,
mileage: row.mileage,
freightAmount: row.freightAmount,
feeItems: row.feeItems,
manualFee: row.manualFee === true,
remark: row.remark,
changeReason: row.changeReason,
})),
});
this.$message.success('保存成功');
this.closeAdjustPanel();
await this.loadTable();
} finally {
this.adjustDialog.submitting = false;
}
},
validateManualAdjustRows() {
for (const [index, row] of this.adjustRows.entries()) {
if (!row.manualFee) continue;
if (!String(row.cargoName || '').trim()) {
this.$message.warning(`第${index + 1}行货物名称不能为空`);
return false;
}
if (String(row.cargoName).trim().length > 100) {
this.$message.warning(`第${index + 1}行货物名称不能超过100个字`);
return false;
}
if (String(row.specification || '').trim().length > 255) {
this.$message.warning(`第${index + 1}行规格不能超过255个字`);
return false;
}
if (!ADJUST_BILLING_ELEMENTS.includes(row.billingFactor)) {
this.$message.warning(`第${index + 1}行请选择计费要素`);
return false;
}
if (!this.adjustBillingTypes(row).includes(row.billingType)) {
this.$message.warning(`第${index + 1}行请选择计费要素对应的计费类型`);
return false;
}
}
return true;
},
async loadFeeDetail() {
if (!this.detailDialog.row) return;
this.detailDialog.loading = true;
try {
const res = await api.getFeeDetail(this.detailDialog.row.id);
const data = res.data?.data || res.data || res || {};
this.dynamicFeeColumns = (data.feeItemNames || []).map((name, index) => ({
label: name,
prop: `feeItem${index}`,
minWidth: 130,
align: 'right',
}));
this.feeRows = (data.records || []).map(row => {
const dynamic = {};
(data.feeItemNames || []).forEach((name, index) => {
dynamic[`feeItem${index}`] = row.feeItems?.[name] || '';
});
return { ...row, ...dynamic };
});
} finally {
this.detailDialog.loading = false;
}
},
handleDetailTabChange(name) {
if (name === 'change') this.loadLatestChangeRecord();
},
async loadLatestChangeRecord() {
if (!this.detailDialog.row) return;
this.latestChangeLoading = true;
this.latestChangeRows = [];
try {
const res = await api.getChangeRecords(1, 1, {
detailId: this.detailDialog.row.id,
});
const data = this.unwrapPage(res);
this.latestChangeRows = (data.records || []).slice(0, 1);
} finally {
this.latestChangeLoading = false;
}
},
openChangeRecordsDialog() {
if (!this.detailDialog.row) return;
this.changeRecordsDialog.visible = true;
this.changePage.current = 1;
this.loadChangeRecords();
},
async loadChangeRecords() {
if (!this.detailDialog.row) return;
this.changeRecordsDialog.loading = true;
try {
const res = await api.getChangeRecords(this.changePage.current, this.changePage.size, {
detailId: this.detailDialog.row.id,
});
const data = this.unwrapPage(res);
this.changeRows = data.records || [];
this.changePage.total = data.total || 0;
} finally {
this.changeRecordsDialog.loading = false;
}
},
handleChangeSizeChange() {
this.changePage.current = 1;
this.loadChangeRecords();
},
openUpdateFeeDialog(row) {
this.updateFeeDialog = { ...this.updateFeeDialog, visible: true, row: row || null };
this.updateFeeForm = {
settlementType: this.settlementType || undefined,
contractId: row?.contractId || '',
billingPlanId: '',
};
if (row?.contractId) {
const contract = {
...row,
id: row.contractId,
deptId: row.deptId || row.organizationId,
deptName: row.deptName || row.organizationName,
partyA: row.partyA || row.payerName,
partyB: row.partyB || row.payeeName,
};
const index = this.updateContractOptions.findIndex(
item => String(item.id) === String(contract.id)
);
if (index >= 0) this.updateContractOptions.splice(index, 1, contract);
else this.updateContractOptions.push(contract);
}
this.handleUpdateContractChange(this.updateFeeForm.contractId);
},
handleUpdateContractChange(contractId) {
this.updateFeeForm.billingPlanId = '';
if (!contractId) {
this.billingPlanOptions = [];
return;
}
const options = this.syncBillingPlanOptions(contractId);
const selected = options.find(item => item.defaultPlan) || options[0];
this.updateFeeForm.billingPlanId = selected?.id || '';
},
async confirmUpdateFee() {
await this.$refs.updateFeeFormRef.validate();
await this.$confirm(
'将使用选定计费方案,更新该合同下所有待结算明细的费用,确认继续?',
'确认提示',
{ type: 'warning' }
);
this.updateFeeDialog.submitting = true;
try {
await api.updateFee(this.updateFeeForm);
this.$message.success('更新成功');
this.updateFeeDialog.visible = false;
this.loadTable();
} finally {
this.updateFeeDialog.submitting = false;
}
},
async closeRow(row) {
await this.$confirm('确认关闭该费用明细?', '提示', { type: 'warning' });
await api.updateFee({
ids: [row.id],
closeOnly: true,
settlementType: this.settlementType || undefined,
});
this.$message.success('关闭成功');
this.loadTable();
},
openTransferDialog() {
this.transferDialog.visible = true;
this.transferForm = { settlementBillType: this.isReceivable ? 'formal' : 'pre' };
this.transferQuery = {};
this.transferRows = [];
this.transferSelection = [];
this.transferPage = { current: 1, size: 10, total: 0 };
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
this.loadTransferCandidates();
},
async loadTransferCandidates() {
this.transferDialog.loading = true;
try {
const params = this.buildRequestParams(
this.normalizeQuery(
this.transferQuery,
'generateDateRange',
'generateStartDate',
'generateEndDate'
)
);
const res = await api.getTransferCandidates(
this.transferPage.current,
this.transferPage.size,
{
...params,
settlementStatus: 'pending',
settlementType: this.settlementType || undefined,
settlementBillType: this.transferForm.settlementBillType,
}
);
const data = this.unwrapPage(res);
this.transferRows = (data.records || [])
.filter(row => this.isTransferCandidate(row))
.map(this.decorateRow);
this.transferPage.total = data.total || 0;
} finally {
this.transferDialog.loading = false;
}
},
resetTransferSelection() {
this.transferSelection = [];
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
},
handleTransferSearch() {
this.transferPage.current = 1;
this.resetTransferSelection();
this.loadTransferCandidates();
},
handleTransferTypeChange() {
this.transferPage.current = 1;
this.resetTransferSelection();
this.loadTransferCandidates();
},
handleTransferSizeChange() {
this.transferPage.current = 1;
this.loadTransferCandidates();
},
transferIndexMethod(index) {
return (this.transferPage.current - 1) * this.transferPage.size + index + 1;
},
isTransferCandidate(row) {
const hasValue = value => {
if (Array.isArray(value)) return value.length > 0;
return value !== null && value !== undefined && String(value).trim() !== '';
};
const normalizeSettlementType = value => {
const normalized = String(value || '')
.trim()
.toLowerCase();
if (normalized === '应收') return 'receivable';
if (normalized === '应付') return 'payable';
return normalized;
};
const rowSettlementType = normalizeSettlementType(
row.settlementType || row.settlementTypeName
);
const expectedSettlementType = normalizeSettlementType(this.settlementType);
const matchesSettlementType =
!expectedSettlementType || rowSettlementType === expectedSettlementType;
const status = String(row.settlementStatus || '')
.trim()
.toLowerCase();
const isClosed =
[row.closed, row.isClosed, row.closeFlag, row.closedFlag].some(
value => value === true || String(value).trim().toLowerCase() === 'true'
) || ['closed', 'close', '已关闭'].includes(status);
const hasPreSettlement = [
row.preSettlementId,
row.preSettlementIds,
row.preSettlementNo,
row.preSettlementNos,
].some(hasValue);
const hasFormalSettlement = [
row.formalSettlementId,
row.formalSettlementIds,
row.formalSettlementNo,
row.formalSettlementNos,
].some(hasValue);
return (
matchesSettlementType &&
['pending', '待结算'].includes(status) &&
!isClosed &&
!hasPreSettlement &&
!hasFormalSettlement
);
},
transferSelectionChange(selection) {
this.transferSelection = selection;
},
async submitTransfer() {
if (!this.transferSelection.length) {
this.$message.warning('请选择需要转结算的明细');
return;
}
if (this.transferSelection.some(item => !item.contractId && !item.contractNo)) {
this.$message.warning('所选明细缺少合同信息,无法转结算');
return;
}
const contractKeys = new Set(
this.transferSelection.map(item =>
item.contractNo ? `no:${item.contractNo}` : `id:${item.contractId}`
)
);
if (contractKeys.size > 1) {
this.$message.warning('请选择同一合同下的明细进行转结算');
return;
}
this.transferDialog.submitting = true;
try {
const transferRows = await this.resolveTransferContractIds(this.transferSelection);
if (!transferRows) return;
const settlementBillType = this.isReceivable
? 'formal'
: this.transferForm.settlementBillType;
const transferToken = createSettlementTransfer({
settlementBillType,
settlementType: this.settlementType || transferRows[0]?.settlementType,
rows: transferRows,
});
const isPreSettlement = settlementBillType === 'pre';
this.transferDialog.visible = false;
await this.$router.push({
path: isPreSettlement
? '/settlement/pre-settlement/form'
: '/settlement/formal-settlement/form',
query: {
mode: 'add',
transferToken,
name: isPreSettlement ? '新增预结算' : '新增正式结算',
},
});
} finally {
this.transferDialog.submitting = false;
}
},
async resolveTransferContractIds(rows) {
if (rows.every(item => item.contractId && item.projectId)) return rows;
const contractId = rows.find(item => item.contractId)?.contractId;
const contractNo = rows.find(item => item.contractNo)?.contractNo;
const { data } = await getSettlementContractOptions(contractNo);
const contracts = data?.data || [];
const contract = contracts.find(
item =>
(contractId && String(item.id) === String(contractId)) ||
(contractNo && String(item.contractNo) === String(contractNo))
);
if (!contract?.id || !contract.projectId) {
this.$message.warning('未找到所选明细对应的合同或项目信息,无法转结算');
return null;
}
return rows.map(item => {
const settlementType =
item.settlementType || this.settlementType || contract.settlementType;
return {
...item,
contractId: item.contractId || contract.id,
contractNo: item.contractNo || contract.contractNo,
contractName: item.contractName || contract.contractName,
projectId: item.projectId || contract.projectId,
projectName: item.projectName || contract.projectName,
deptId: item.deptId || contract.deptId,
deptName: item.deptName || contract.deptName,
payerName:
item.payerName || (settlementType === 'receivable' ? contract.partyB : contract.partyA),
payeeName:
item.payeeName || (settlementType === 'receivable' ? contract.partyA : contract.partyB),
};
});
},
openGenerateDialog() {
this.generateDialog.visible = true;
this.generateQuery = {};
this.contractOptions = [];
this.generateContractDialog.query = {
contractNo: '',
contractName: '',
projectName: '',
organizationName: '',
contractCategory: '',
signType: '',
effectiveType: '',
contractStage: '',
approvalStatus: '',
};
this.generateContractDialog.visible = false;
this.generateContractDialog.expanded = false;
this.generateContractDialog.rows = [];
this.generateContractDialog.current = null;
this.generateContractDialog.page = { current: 1, size: 10, total: 0 };
this.billingPlanOptions = [];
this.generateRows = [];
this.generateSelection = [];
this.generatePage = { current: 1, size: 10, total: 0 };
},
handleGenerateContractChange(contractId) {
const options = this.syncBillingPlanOptions(contractId);
const selected = options.find(item => item.defaultPlan) || options[0];
this.generateQuery.billingPlanId = selected?.id || '';
},
async loadGenerateWaybills() {
if (!this.generateQuery.contractId || !this.generateQuery.billingPlanId) {
this.$message.warning('请选择运单合同和计费方案');
return;
}
this.generateDialog.loading = true;
try {
const params = this.buildRequestParams(
this.normalizeQuery(
this.generateQuery,
'finishDateRange',
'finishStartDate',
'finishEndDate'
)
);
const res = await api.getGenerateWaybills(
this.generatePage.current,
this.generatePage.size,
params
);
const data = this.unwrapPage(res);
this.generateRows = data.records || [];
this.generatePage.total = data.total || 0;
} finally {
this.generateDialog.loading = false;
}
},
handleGenerateSizeChange() {
this.generatePage.current = 1;
this.loadGenerateWaybills();
},
resetGenerateQuery() {
this.generateQuery = {};
this.generateRows = [];
this.generateSelection = [];
this.generatePage = { current: 1, size: 10, total: 0 };
},
generateSelectionChange(selection) {
this.generateSelection = selection;
},
async openGeneratePreview() {
if (!this.generateSelection.length) {
this.$message.warning('请选择需要生成费用的运单');
return;
}
this.previewPage.current = 1;
const loaded = await this.loadGeneratePreview();
if (!loaded) return;
this.previewDialog.visible = true;
this.generateDialog.visible = false;
},
async loadGeneratePreview() {
this.previewDialog.loading = true;
try {
const res = await api.getGeneratePreview(this.previewPage.current, this.previewPage.size, {
settlementType: this.settlementType || undefined,
contractId: this.generateQuery.contractId,
billingPlanId: this.generateQuery.billingPlanId,
waybillIds: this.generateSelection.map(item => item.id).join(','),
});
const data = res.data?.data || res.data || res || {};
this.dynamicFeeColumns = (data.feeItemNames || []).map((name, index) => ({
label: name,
prop: `feeItem${index}`,
minWidth: 130,
align: 'right',
}));
const selectedWaybillsById = new Map();
const selectedWaybillsByNo = new Map();
this.generateSelection.forEach(item => {
[item.id, item.waybillId, item.sourceWaybillId].forEach(id => {
if (id !== null && id !== undefined && id !== '') {
selectedWaybillsById.set(String(id), item);
}
});
if (item.waybillNo) selectedWaybillsByNo.set(String(item.waybillNo), item);
});
this.previewRows = (data.records || []).map(row => {
const dynamic = {};
(data.feeItemNames || []).forEach((name, index) => {
dynamic[`feeItem${index}`] = row.feeItems?.[name] || '';
});
const sourceWaybillId =
row.waybillId || row.sourceWaybillId || row.sourceId || row.businessId || row.id;
const sourceWaybill =
selectedWaybillsById.get(String(sourceWaybillId || '')) ||
selectedWaybillsByNo.get(String(row.waybillNo || row.waybillNumber || ''));
return {
...row,
customerName:
row.customerName ||
row.customerUnitName ||
row.clientName ||
sourceWaybill?.customerName ||
'',
waybillNo:
row.waybillNo ||
row.waybillNumber ||
row.sourceWaybillNo ||
sourceWaybill?.waybillNo ||
'',
vehicleNo:
row.vehicleNo ||
row.plateNo ||
row.carNo ||
row.vehicleNumber ||
sourceWaybill?.vehicleNo ||
'',
transportQuantityText: row.transportQuantityText ?? row.transportQuantity ?? '',
...dynamic,
};
});
this.previewPage.total = data.total || 0;
return true;
} catch (error) {
return false;
} finally {
this.previewDialog.loading = false;
}
},
handlePreviewSizeChange() {
this.previewPage.current = 1;
this.loadGeneratePreview();
},
backToGenerateDialog() {
this.previewDialog.visible = false;
this.$nextTick(() => {
this.generateDialog.visible = true;
});
},
async submitGenerateFee() {
this.previewDialog.submitting = true;
try {
await api.generateFee({
settlementType: this.settlementType || undefined,
contractId: this.generateQuery.contractId,
billingPlanId: this.generateQuery.billingPlanId,
waybillIds: this.generateSelection.map(item => item.id),
});
this.$message.success('生成成功');
this.previewDialog.visible = false;
this.loadTable();
} finally {
this.previewDialog.submitting = false;
}
},
handleExport() {
const params = this.buildRequestParams(
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
);
if (this.selection.length) {
params.ids = this.selection.map(item => item.id).join(',');
}
exportBlob(
'/blade-transport/receivable-payable-detail/export-receivable-payable-detail',
params,
{ feedback: true }
).then(res => {
downloadXls(
res.data,
`${this.settlementTypeLabel}明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
});
},
buildRequestParams(params = {}) {
return this.settlementType ? { ...params, settlementType: this.settlementType } : params;
},
syncBillingPlanOptions(contractId) {
const contracts = [...this.updateContractOptions, ...this.contractOptions];
const contract = contracts.find(item => String(item.id) === String(contractId));
const plans = this.parseJson(contract?.billingPlanJson);
this.billingPlanOptions = plans.map((item, index) => ({
id:
item.id ||
item.planId ||
item.name ||
item.planName ||
item.billingPlanName ||
`plan-${index}`,
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
defaultPlan:
item.defaultPlan === true ||
item.defaultPlan === 1 ||
['true', '1'].includes(String(item.defaultPlan).toLowerCase()),
}));
return this.billingPlanOptions;
},
normalizeQuery(source, rangeProp, startProp, endProp) {
const params = { ...source };
const range = params[rangeProp];
delete params[rangeProp];
if (Array.isArray(range)) {
params[startProp] = range[0];
params[endProp] = range[1];
}
return params;
},
decorateRow(row) {
const currency = row.currency || 'RMB';
return {
...row,
unitPriceText: this.hasMultipleCargo(row) ? '-' : this.money(row.unitPrice, currency),
freightAmountText: this.money(row.freightAmount, currency),
otherFeeAmountText: this.money(row.otherFeeAmount, currency),
totalAmountText: this.money(row.totalAmount, currency),
settlementStatusName:
settlementStatusOptions.find(item => item.value === row.settlementStatus)?.label ||
row.settlementStatus ||
'-',
};
},
parseCargoRows(value) {
if (value === null || value === undefined || value === '') return [];
if (Array.isArray(value)) return value;
if (typeof value === 'object') return [value];
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) return parsed;
return parsed && typeof parsed === 'object' ? [parsed] : [];
} catch (error) {
return [];
}
},
hasMultipleCargo(row = {}) {
const countFields = [
'cargoCount',
'goodsCount',
'cargoNum',
'goodsNum',
'cargoNumber',
'goodsNumber',
'cargoSize',
'goodsSize',
'cargoItemCount',
'goodsItemCount',
];
if (
countFields.some(field => {
const count = Number(row[field]);
return Number.isFinite(count) && count > 1;
})
) {
return true;
}
const cargoSources = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
row.cargoDetails,
row.cargoItems,
row.cargoNames,
row.goodsNames,
];
if (cargoSources.some(source => this.parseCargoRows(source).length > 1)) {
return true;
}
const summaryValues = [row.cargoName, row.cargoInfo, row.goodsInfo];
if (summaryValues.some(value => Array.isArray(value) && value.length > 1)) return true;
const summaryText = summaryValues
.filter(value => value !== null && value !== undefined)
.map(value => String(value))
.join('');
return (
/等\s*\d+\s*[种个]\s*货物/.test(summaryText) ||
summaryText.split(/[、,,;]/).filter(Boolean).length > 1
);
},
collectFeeItemNames(rows) {
const names = [];
(rows || []).forEach(row => {
Object.keys(this.normalizeFeeItems(row.feeItems)).forEach(name => {
if (!names.includes(name)) names.push(name);
});
});
return names;
},
normalizeFeeItems(value) {
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
if (typeof value !== 'string' || !value.trim()) return {};
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch (error) {
return {};
}
},
summarizeTableFee(row, summaryType) {
const feeItemEntries = Object.entries(this.normalizeFeeItems(row.feeItems));
const matchedEntries = feeItemEntries.filter(([name]) =>
summaryType === 'freight' ? this.isFreightFeeItem(name) : !this.isFreightFeeItem(name)
);
if (matchedEntries.length) {
return matchedEntries.reduce((total, [, amount]) => total + Number(amount || 0), 0);
}
return summaryType === 'freight' ? row.freightAmount : row.otherFeeAmount;
},
money(value, currency) {
if (value === null || value === undefined || value === '') return '-';
return `${Number(value).toFixed(2)} ${currency}`;
},
formatDetailCell(row, prop) {
if (
prop === 'mileage' &&
(row?.[prop] === null ||
row?.[prop] === undefined ||
row?.[prop] === '' ||
Number(row[prop]) === -1)
) {
return '';
}
return this.formatCell(row?.[prop]);
},
formatCell(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
parseJson(value) {
if (!value) return [];
try {
const data = JSON.parse(value);
return Array.isArray(data) ? data : [];
} catch (error) {
return [];
}
},
unwrapPage(res) {
const data = res.data?.data || res.data || res || {};
return data.records ? data : { records: data.records || [], total: data.total || 0 };
},
extractRecords(res) {
const data = res?.data?.data || res?.data || res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
},
noop() {},
},
};
</script>
<style scoped lang="scss">
.settlement-detail-page {
color: #303133;
}
.settlement-detail-page__search {
margin-bottom: 8px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
:deep(.el-form-item__label) {
white-space: nowrap;
}
}
.settlement-detail-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr));
gap: 8px 24px;
align-items: start;
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor) {
width: 100%;
}
}
.settlement-detail-page__search-actions {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
.settlement-detail-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
background: transparent;
}
.settlement-detail-page__toolbar-left,
.settlement-detail-page__toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.settlement-detail-page__table,
.settlement-detail-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
background: #fff;
:deep(th.el-table__cell) {
background: #f5f7fa;
}
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
background: #fafafa;
}
}
.settlement-detail-page__actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.settlement-detail-page__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.settlement-detail-page__empty {
padding: 24px 0;
color: #909399;
text-align: center;
}
.settlement-detail-page__dialog-form {
padding: 16px 20px;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
:deep(.el-form-item) {
margin-bottom: 14px;
&:last-child {
margin-bottom: 0;
}
}
:deep(.el-select) {
width: 100%;
}
}
.settlement-detail-page__adjust-input {
width: 100%;
min-width: 0;
:deep(.el-input__inner) {
text-align: right;
}
}
.settlement-detail-page__adjust-control {
width: 100%;
}
.settlement-detail-page__generate-search {
margin-bottom: 16px;
padding: 12px 12px 4px;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.settlement-detail-page__generate-select {
width: 220px;
}
.settlement-detail-page__contract-filter {
display: grid !important;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
margin-bottom: 16px;
:deep(.el-form-item) {
display: flex;
min-width: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
}
.settlement-detail-page__contract-filter-actions {
grid-column: 1 / -1;
width: 100%;
justify-content: flex-end;
:deep(.el-form-item__content) {
justify-content: flex-end;
}
}
.settlement-detail-page__transfer-form {
margin-bottom: 16px;
padding: 12px 12px 4px;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
@media screen and (max-width: 1200px) {
.settlement-detail-page__search-grid {
grid-template-columns: repeat(2, minmax(220px, 1fr));
}
}
@media screen and (max-width: 760px) {
.settlement-detail-page__search-grid {
grid-template-columns: 1fr;
}
}
.settlement-detail-page__detail-panel {
position: fixed;
z-index: 100;
bottom: 0;
left: $sidebar_width;
width: calc(100% - #{$sidebar_width});
max-height: min(60vh, 560px);
border-top: 1px solid #eff1f7;
background: #fff;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
}
:global(.avue--collapse) .settlement-detail-page__detail-panel {
left: $sidebar_collapse;
width: calc(100% - #{$sidebar_collapse});
}
:global(.avue-layout--horizontal) .settlement-detail-page__detail-panel {
left: 0;
width: 100%;
}
.settlement-detail-page__detail-panel-header {
display: flex;
min-height: 48px;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid #eff1f7;
h3 {
margin: 0;
padding-left: 12px;
border-left: 4px solid #409eff;
font-size: 16px;
font-weight: 600;
}
}
.settlement-detail-page__detail-panel-body {
max-height: calc(min(60vh, 560px) - 106px);
padding: 0 20px 16px;
overflow: auto;
:deep(.el-tabs__content) {
overflow: visible;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
background: #fafafa;
}
}
.settlement-detail-page__detail-panel-footer {
display: flex;
min-height: 57px;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 0 20px;
border-top: 1px solid #eff1f7;
}
.settlement-detail-page__adjust-panel-body {
padding-top: 16px;
}
@media screen and (max-width: 992px) {
.settlement-detail-page__detail-panel {
left: 0;
width: 100%;
}
}
</style>