Files
tms-erp-web/src/views/settlement/receivable-payable-detail.vue
T
gxwebsoft c9f673342a style(ui): 统一底部操作栏按钮间距,取消所有gap样式
- 移除各底部操作栏(footer/__actions/page-actions)内所有gap属性
- 按钮间距统一采用 Element Plus 全局默认的相邻按钮12px margin-left规则
- 修改 .el-dialog__footer 内.el-button间距由8px调整为12px,保持全站一致
- 确认 Avue 表单底部按钮无自带间距,去gap后间距由 Element Plus 提供
- 涉及模块覆盖 vehicle、payment、business、settlement,移除了相关组件样式中gap属性
- MEMORY.md规范更新,补充底栏按钮间距统一为无gap纯文字规则
2026-08-27 11:39:13 +08:00

2002 lines
68 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 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="changeDialog.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-tab-pane>
</el-tabs>
</div>
<footer class="settlement-detail-page__detail-panel-footer">
<el-button @click="closeDetailPanel">关闭</el-button>
</footer>
</section>
</teleport>
<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"
>
<div v-if="isReceivable" class="settlement-detail-page__adjust-toolbar">
<el-button type="primary" :disabled="adjustDialog.loading" @click="addAdjustFee">
新增费用
</el-button>
</div>
<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 }">
<span
v-if="row.manualFee && ['billingFactor', 'billingType'].includes(column.prop)"
>
-
</span>
<el-input
v-else-if="column.prop === 'cargoName' && row.manualFee"
v-model="row.cargoName"
clearable
maxlength="100"
placeholder="请输入"
/>
<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 === '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 === 'remark'"
v-model="row.remark"
clearable
maxlength="200"
placeholder="请输入备注"
/>
<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" @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-select
v-model="updateFeeForm.contractId"
clearable
filterable
placeholder="请选择合同"
@change="handleUpdateContractChange"
>
<el-option
v-for="item in updateContractOptions"
:key="item.id"
:label="item.contractName"
:value="item.id"
/>
</el-select>
</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 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-select
v-model="generateQuery.contractId"
class="settlement-detail-page__generate-select"
filterable
clearable
:loading="contractLoading"
placeholder="请选择合同"
@change="handleGenerateContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName"
:value="item.id"
/>
</el-select>
</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>
</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 { 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';
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'],
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 },
changeDialog: { loading: false },
updateFeeDialog: { visible: false, submitting: false, row: null },
updateFeeForm: { contractId: '', billingPlanId: '' },
updateFeeRules: {
contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }],
},
contractOptions: [],
contractLoading: false,
updateContractOptions: [],
transportTypeOptions: [],
billingPlanOptions: [],
transferDialog: { visible: false, loading: false, submitting: false },
transferQuery: {},
transferForm: { settlementBillType: 'formal' },
transferRows: [],
transferSelection: [],
transferPage: { current: 1, size: 10, total: 0 },
generateDialog: { visible: false, loading: false },
generateQuery: {},
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: {
settlementTypeLabel() {
if (this.settlementType === 'receivable') return '应收';
if (this.settlementType === 'payable') return '应付';
return '应收应付';
},
isReceivable() {
return this.settlementType === 'receivable';
},
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
feeDetailColumns() {
return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
},
displayTableColumns() {
const columns = [...this.tableColumns];
const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText');
const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({
label: name,
prop: `tableFeeItem${index}`,
feeItemName: name,
dynamic: true,
minWidth: 130,
align: 'right',
}));
if (totalIndex < 0) return [...columns, ...dynamicColumns];
columns.splice(totalIndex, 0, ...dynamicColumns);
return columns;
},
previewColumns() {
return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
},
adjustFeeColumns() {
return [
...feeDetailBaseColumns,
...this.adjustDynamicFeeColumns,
...feeDetailTailColumns,
{ label: '变更原因', prop: 'changeReason', minWidth: 220 },
];
},
transportCargoTypeOptions() {
return this.cargoTypeOptions.filter(item => item.children?.length);
},
},
mounted() {
this.loadTransportTypeOptions();
this.loadCargoTypeOptions();
this.loadPriceUnitOptions();
this.loadTable();
},
methods: {
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;
},
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 = '';
},
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.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.collectFeeItemNames(this.rows);
this.page.total = data.total || 0;
} finally {
this.loading = false;
}
},
async loadContracts() {
const contractCategory = this.settlementType === 'payable' ? '承运商合同' : '客户合同';
this.contractLoading = true;
try {
const res = await getContractList(1, 999, {
contractCategory,
});
const data = this.unwrapPage(res);
this.contractOptions = data.records || [];
} finally {
this.contractLoading = false;
}
},
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;
},
closeAdjustPanel() {
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 } });
},
openWaybillDetail(row) {
if (!row.waybillId) {
this.$message.info('当前记录未关联运单详情');
return;
}
this.$router.push({
path: '/business/waybill-manage',
query: { detailId: 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.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,
dataSource:
item.dataSource || (item.billingFactor === '手工调整' ? '手工录入' : '自动生成'),
manualFee: item.dataSource === '手工录入' || item.billingFactor === '手工调整',
cargoTypePath: this.resolveCargoTypePath(item.cargoType),
};
this.recalculateAdjustRow(adjusted);
return adjusted;
});
} finally {
this.adjustDialog.loading = false;
}
},
addAdjustFee() {
if (!this.isReceivable) return;
const feeItems = this.adjustDynamicFeeColumns.reduce((items, column) => {
items[column.feeItemName] = 0;
return items;
}, {});
this.adjustRows.push({
dataSource: '手工录入',
cargoName: '',
cargoType: '',
cargoTypePath: [],
specification: '',
model: '',
billingFactor: '-',
billingType: '-',
transportQuantity: '',
priceUnit: '',
unitPrice: '',
mileage: null,
freightAmount: '',
originalAmount: 0,
originalAmountText: '-',
adjustAmount: 0,
afterAmount: 0,
feeItems,
manualFee: true,
remark: '',
changeReason: '',
});
},
feeSourceLabel(value) {
return value === '手工录入' ? '手工录入' : '自动生成';
},
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);
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));
},
isFreightFeeItem(name) {
return String(name || '').includes('运费') || String(name || '').includes('运输费');
},
async saveAdjustFee() {
if (!this.adjustRows.length || !this.adjustDialog.row) {
this.$message.warning('没有可调整的费用明细');
return;
}
this.adjustDialog.submitting = true;
try {
await api.adjustFee({
detailId: this.adjustDialog.row.id,
rows: this.adjustRows.map(row => ({
id: row.id,
cargoName: row.cargoName,
cargoType: row.cargoType,
specification: row.specification,
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;
}
},
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.loadChangeRecords();
},
async loadChangeRecords() {
if (!this.detailDialog.row) return;
this.changeDialog.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.changeDialog.loading = false;
}
},
handleChangeSizeChange() {
this.changePage.current = 1;
this.loadChangeRecords();
},
async openUpdateFeeDialog(row) {
await this.loadUpdateFeeContracts();
this.updateFeeDialog = { ...this.updateFeeDialog, visible: true, row: row || null };
this.updateFeeForm = {
settlementType: this.settlementType || undefined,
contractId: row?.contractId || '',
billingPlanId: '',
};
this.handleUpdateContractChange(this.updateFeeForm.contractId);
},
async loadUpdateFeeContracts() {
const res = await api.getUpdateFeeContracts({
settlementType: this.settlementType || undefined,
});
this.updateContractOptions = res.data?.data || [];
},
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: 'formal' };
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.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.billingPlanOptions = [];
this.generateRows = [];
this.generateSelection = [];
this.generatePage = { current: 1, size: 10, total: 0 };
this.loadContracts();
},
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 ||
'',
...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.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 ||
'-',
};
},
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 {};
}
},
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;
}
.settlement-detail-page__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.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-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 12px;
background: transparent;
}
.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__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>