cba2ad3b3b
2、调整运单
2892 lines
99 KiB
Vue
2892 lines
99 KiB
Vue
<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>
|