3172 lines
114 KiB
Vue
3172 lines
114 KiB
Vue
<template>
|
||
<component
|
||
:is="editorContainer"
|
||
v-bind="editorContainerProps"
|
||
@update:model-value="visible = $event"
|
||
@closed="handleClosed"
|
||
>
|
||
<div v-loading="loading" class="pre-settlement-editor__content">
|
||
<section-card title="结算基本信息">
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="formRules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="pre-settlement-editor__form"
|
||
>
|
||
<el-row :gutter="24">
|
||
<el-col v-for="field in visibleFormFields" :key="field.prop" :span="field.span || 6">
|
||
<el-form-item :label="field.label" :prop="field.prop">
|
||
<el-input
|
||
v-if="field.type === 'contract'"
|
||
:model-value="formatContractOptionLabel(form)"
|
||
readonly
|
||
:disabled="!editable || details.length > 0"
|
||
placeholder="请选择合同"
|
||
>
|
||
<template #append>
|
||
<el-button
|
||
:disabled="!editable || details.length > 0"
|
||
@click="openContractDialog"
|
||
>
|
||
选择
|
||
</el-button>
|
||
</template>
|
||
</el-input>
|
||
<el-date-picker
|
||
v-else-if="field.type === 'date'"
|
||
v-model="form[field.prop]"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
format="YYYY-MM-DD"
|
||
:disabled="!editable"
|
||
placeholder="请选择"
|
||
@change="handleExchangeRateDateChange"
|
||
/>
|
||
<el-input-number
|
||
v-else-if="field.type === 'number'"
|
||
v-model="form[field.prop]"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="!editable"
|
||
@change="recalculateLocalAmount"
|
||
/>
|
||
<el-input
|
||
v-else-if="field.type === 'textarea'"
|
||
v-model="form[field.prop]"
|
||
type="textarea"
|
||
:rows="2"
|
||
:maxlength="field.maxlength"
|
||
show-word-limit
|
||
:disabled="!editable"
|
||
placeholder="请输入"
|
||
/>
|
||
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
|
||
{{ settlementTypeName }}
|
||
</span>
|
||
<span
|
||
v-else-if="field.prop === 'preSettlementNo'"
|
||
class="form-readonly"
|
||
:class="{ 'form-readonly--code': form.preSettlementNo }"
|
||
>
|
||
{{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }}
|
||
</span>
|
||
<span v-else-if="field.prop === 'createTime'" class="form-readonly">
|
||
{{ formatCreateDate(form.createTime) }}
|
||
</span>
|
||
<span v-else-if="field.money" class="form-readonly">
|
||
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
|
||
</span>
|
||
<span v-else class="form-readonly">{{ displayValue(form[field.prop]) }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</section-card>
|
||
|
||
<section-card title="结算合计">
|
||
<template #extra>
|
||
<el-button v-if="editable" type="primary" plain @click="addSummaryFee">
|
||
添加费用
|
||
</el-button>
|
||
</template>
|
||
<el-table :data="summaryFees" border show-summary :summary-method="getSummarySums">
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column
|
||
v-for="column in summaryColumns"
|
||
:key="column.prop"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-if="editable && column.prop === 'feeType' && row.manualFlag === 1"
|
||
v-model="row.feeType"
|
||
filterable
|
||
placeholder="请选择"
|
||
@change="handleManualFeeTypeChange(row)"
|
||
>
|
||
<el-option
|
||
v-for="item in feeCategoryOptions"
|
||
:key="item.id || item.dictKey"
|
||
:label="item.dictValue || item.dictKey"
|
||
:value="item.dictKey"
|
||
/>
|
||
</el-select>
|
||
<el-select
|
||
v-else-if="editable && column.prop === 'feeItem' && row.manualFlag === 1"
|
||
v-model="row.feeItem"
|
||
filterable
|
||
placeholder="请选择"
|
||
>
|
||
<el-option
|
||
v-for="name in manualFeeItems(row.feeType)"
|
||
:key="name"
|
||
:label="name"
|
||
:value="name"
|
||
/>
|
||
</el-select>
|
||
<el-input-number
|
||
v-else-if="
|
||
editable && column.prop === 'adjustAmount' && Number(row.manualFlag) === 1
|
||
"
|
||
v-model="row.adjustAmount"
|
||
:class="{
|
||
'pre-settlement-editor__negative-amount': Number(row.adjustAmount || 0) < 0,
|
||
}"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateSummaryRow(row)"
|
||
/>
|
||
<el-input
|
||
v-else-if="editable && column.prop === 'remark'"
|
||
v-model="row.remark"
|
||
maxlength="50"
|
||
show-word-limit
|
||
/>
|
||
<span
|
||
v-else-if="isSummaryMoney(column.prop)"
|
||
:class="{
|
||
'pre-settlement-editor__negative-amount':
|
||
column.prop === 'adjustAmount' && Number(row.adjustAmount || 0) < 0,
|
||
}"
|
||
>
|
||
{{ formatMoney(row[column.prop], form.currency) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'feeType'">
|
||
{{ feeCategoryName(row.feeType) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="editable" label="操作" width="100" align="center">
|
||
<template #default="{ row, $index }">
|
||
<el-link v-if="row.manualFlag === 1" type="danger" @click="removeSummaryFee($index)">
|
||
删除
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card title="结算明细">
|
||
<template #extra>
|
||
<div class="pre-settlement-editor__section-actions">
|
||
<el-button
|
||
v-if="hasPermission('pre_settlement_export')"
|
||
type="primary"
|
||
plain
|
||
@click="exportDetails"
|
||
>
|
||
导出
|
||
</el-button>
|
||
<el-button v-if="editable" type="primary" plain @click="openCandidateDialog">
|
||
选择结算明细
|
||
</el-button>
|
||
<el-button type="primary" text @click="detailCollapsed = !detailCollapsed">
|
||
{{ detailCollapsed ? '展开明细' : '收起明细' }}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
<el-form
|
||
:model="detailQuery"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="pre-settlement-editor__detail-filter"
|
||
@submit.prevent
|
||
>
|
||
<el-form-item label="单据号">
|
||
<el-input
|
||
v-model="detailQuery.documentNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="handleDetailQuery"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="运单号">
|
||
<el-input
|
||
v-model="detailQuery.waybillNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="handleDetailQuery"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="批次号">
|
||
<el-input
|
||
v-model="detailQuery.batchNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="handleDetailQuery"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="货物名称">
|
||
<el-input
|
||
v-model="detailQuery.cargoName"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="handleDetailQuery"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item class="pre-settlement-editor__detail-filter-actions">
|
||
<el-button type="primary" @click="handleDetailQuery">查询</el-button>
|
||
<el-button @click="resetDetailQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div v-show="!detailCollapsed">
|
||
<el-table :data="pagedDetails" border class="pre-settlement-editor__detail-table">
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="64"
|
||
fixed="left"
|
||
align="center"
|
||
:index="detailIndexMethod"
|
||
/>
|
||
<el-table-column
|
||
v-for="item in detailDisplayColumns"
|
||
:key="item.key"
|
||
:label="item.kind === 'fee' ? item.name : item.column.label"
|
||
:prop="item.kind === 'fee' ? undefined : item.column.prop"
|
||
:min-width="item.kind === 'fee' ? 130 : item.column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="item.kind === 'fee'">
|
||
{{
|
||
formatMoney(
|
||
parseFeeItems(row.feeItemsJson)[item.name],
|
||
row.currency || form.currency
|
||
)
|
||
}}
|
||
</span>
|
||
<el-link
|
||
v-else-if="item.column.link && row[item.column.prop] && !publicMode"
|
||
type="primary"
|
||
@click.stop="handleDetailLink(row, item.column)"
|
||
>
|
||
{{ row[item.column.prop] }}
|
||
</el-link>
|
||
<span v-else-if="item.column.money && item.column.prop === 'unitPrice'">
|
||
{{ formatUnitPrice(row) }}
|
||
</span>
|
||
<span v-else-if="item.column.money">
|
||
{{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
|
||
</span>
|
||
<span v-else-if="item.column.prop === 'transportQuantityText'">
|
||
{{ formatQuantity(row) }}
|
||
</span>
|
||
<span v-else-if="item.column.prop === 'mileage'">
|
||
{{ formatDetailMileage(row[item.column.prop]) }}
|
||
</span>
|
||
<span v-else-if="item.column.prop === 'transportType'">
|
||
{{ transportTypeName(row[item.column.prop]) }}
|
||
</span>
|
||
<span v-else-if="item.column.prop === 'cargoName'">
|
||
{{ formatCargoNames(row) }}
|
||
</span>
|
||
<span v-else-if="item.column.prop === 'cargoType'">
|
||
{{ formatCargoTypes(row) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[item.column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" :width="editable ? 150 : 90" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="pre-settlement-editor__links">
|
||
<el-link type="primary" @click="openAdjustDialog(row, !canAdjustDetail)">
|
||
{{ canAdjustDetail ? '调整' : '查看' }}
|
||
</el-link>
|
||
<el-link v-if="editable" type="danger" @click="removeDetailRow(row)">
|
||
踢出
|
||
</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pre-settlement-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="detailPage.current"
|
||
v-model:page-size="detailPage.size"
|
||
:total="filteredDetails.length"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleDetailPageSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
|
||
<section-card title="附件">
|
||
<template #extra>
|
||
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
|
||
批量下载
|
||
</el-button>
|
||
</template>
|
||
<div class="pre-settlement-editor__attachment">
|
||
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column label="附件类型" min-width="180" align="center">
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-if="editable"
|
||
v-model="row.type"
|
||
filterable
|
||
placeholder="请选择"
|
||
@change="sortAttachments"
|
||
>
|
||
<el-option
|
||
v-for="item in attachmentTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
<span v-else>{{ attachmentTypeName(row.type) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="文件名称"
|
||
min-width="260"
|
||
show-overflow-tooltip
|
||
class-name="is-ellipsis-link"
|
||
>
|
||
<template #default="{ row }">
|
||
<span
|
||
class="attachment-file-name"
|
||
role="button"
|
||
tabindex="0"
|
||
@click="previewAttachment(row)"
|
||
@keydown.enter="previewAttachment(row)"
|
||
@keydown.space.prevent="previewAttachment(row)"
|
||
>{{ attachmentName(row) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件描述" min-width="220">
|
||
<template #default="{ row }">
|
||
<span v-if="!editable">{{ row.description || '-' }}</span>
|
||
<el-input
|
||
v-else
|
||
v-model="row.description"
|
||
maxlength="200"
|
||
placeholder="请输入"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件大小" width="120" align="center">
|
||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||
<el-table-column
|
||
prop="uploadTime"
|
||
label="上传时间"
|
||
width="170"
|
||
align="center"
|
||
sortable
|
||
/>
|
||
<el-table-column label="操作" :width="editable ? 150 : 80" fixed="right" align="center">
|
||
<template #default="{ row, $index }">
|
||
<div class="pre-settlement-editor__links">
|
||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||
<el-link v-if="editable" type="danger" @click="removeAttachment($index)">
|
||
删除
|
||
</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pre-settlement-editor__attachment-upload">
|
||
<vehicle-attachment-upload
|
||
ref="attachmentUploadRef"
|
||
v-model="attachments"
|
||
:readonly="!editable"
|
||
:multiple="true"
|
||
:limit="20"
|
||
:max-size="500"
|
||
:file-types="attachmentFileTypes"
|
||
:show-file-list="false"
|
||
button-text="上传附件"
|
||
@change="handleAttachmentChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
|
||
<project-advance-write-off
|
||
ref="projectAdvanceWriteOffRef"
|
||
v-model="projectAdvanceWriteOffs"
|
||
:editable="editable"
|
||
:contract-id="form.contractId"
|
||
:settlement-amount="form.settlementAmount"
|
||
settlement-type="pre"
|
||
:settlement-id="form.id"
|
||
/>
|
||
|
||
<section-card v-if="form.id && readonly" title="预付信息">
|
||
<el-table :data="advances" border>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column
|
||
v-for="column in advanceTableColumns"
|
||
:key="column.prop"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="['appliedAmount', 'paidAmount'].includes(column.prop)">
|
||
{{ formatMoney(row[column.prop], form.currency) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'billStatus'">
|
||
{{ advanceStatusName(row.billStatus) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'createUserName'">
|
||
{{ advanceCreateUserName(row) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'createTime'">
|
||
{{ advanceCreateTime(row) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card v-if="form.id && readonly" title="变更记录">
|
||
<el-table :data="changeRecords" border>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column
|
||
v-for="column in changeTableColumns"
|
||
:key="column.prop"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="column.prop === 'lineNo'">{{ changeRecordLineNo(row) }}</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
</div>
|
||
|
||
<template v-if="!pageMode" #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button v-if="editable" type="primary" plain :loading="saving" @click="saveDraft(false)"
|
||
>保存</el-button
|
||
>
|
||
<el-button
|
||
v-if="editable && hasPermission('pre_settlement_submit')"
|
||
type="primary"
|
||
:loading="submitting"
|
||
@click="saveDraft(true)"
|
||
>
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
<div v-if="pageMode && !publicMode" class="pre-settlement-editor__page-actions">
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button v-if="editable" type="primary" plain :loading="saving" @click="saveDraft(false)"
|
||
>保存</el-button
|
||
>
|
||
<el-button
|
||
v-if="editable && hasPermission('pre_settlement_submit')"
|
||
type="primary"
|
||
:loading="submitting"
|
||
@click="saveDraft(true)"
|
||
>
|
||
提交
|
||
</el-button>
|
||
</div>
|
||
</component>
|
||
|
||
<el-dialog
|
||
v-model="candidateDialog.visible"
|
||
title="选择结算明细"
|
||
width="92%"
|
||
append-to-body
|
||
destroy-on-close
|
||
class="pre-settlement-candidate-dialog"
|
||
>
|
||
<section-card title="选择结算明细">
|
||
<el-form :model="candidateQuery" inline label-position="right" label-width="auto">
|
||
<el-form-item label="批次号">
|
||
<el-input v-model="candidateQuery.batchNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="费用日期">
|
||
<el-date-picker
|
||
v-model="candidateQuery.feeDateRange"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
format="YYYY-MM-DD"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="loadCandidates">查询</el-button>
|
||
<el-button @click="resetCandidateQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</section-card>
|
||
<el-table
|
||
ref="candidateTableRef"
|
||
v-loading="candidateDialog.loading"
|
||
:data="candidateRows"
|
||
border
|
||
@selection-change="handleCandidateSelection"
|
||
>
|
||
<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 candidateColumns"
|
||
:key="column.prop"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
|
||
<span v-else-if="column.prop === 'transportType'">
|
||
{{ transportTypeName(row[column.prop]) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pre-settlement-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="candidatePage.current"
|
||
v-model:page-size="candidatePage.size"
|
||
:total="candidatePage.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="loadCandidates"
|
||
@size-change="handleCandidateSizeChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="candidateDialog.visible = false">取消</el-button>
|
||
<el-button type="primary" :loading="candidateDialog.confirming" @click="confirmCandidates">
|
||
确认
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="contractDialog.visible"
|
||
title="选择合同"
|
||
width="92%"
|
||
append-to-body
|
||
destroy-on-close
|
||
class="pre-settlement-contract-dialog"
|
||
>
|
||
<el-form
|
||
:model="contractDialog.query"
|
||
inline
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="pre-settlement-editor__contract-filter"
|
||
@submit.prevent
|
||
>
|
||
<el-form-item label="合同编号">
|
||
<el-input
|
||
v-model="contractDialog.query.contractNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="loadContractRows"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同名称">
|
||
<el-input
|
||
v-model="contractDialog.query.contractName"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="loadContractRows"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="所属项目">
|
||
<el-input
|
||
v-model="contractDialog.query.projectName"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="loadContractRows"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="所属组织">
|
||
<el-input
|
||
v-model="contractDialog.query.organizationName"
|
||
clearable
|
||
placeholder="请输入"
|
||
@keyup.enter="loadContractRows"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同类别">
|
||
<el-select :model-value="contractDialogCategory" 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="contractDialog.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="contractDialog.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="contractDialog.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="contractDialog.expanded" label="审核状态">
|
||
<el-select v-model="contractDialog.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="pre-settlement-editor__contract-filter-actions">
|
||
<el-button type="primary" @click="searchContractRows">查询</el-button>
|
||
<el-button @click="resetContractQuery">重置</el-button>
|
||
<el-button text @click="contractDialog.expanded = !contractDialog.expanded">
|
||
{{ contractDialog.expanded ? '收起' : '展开' }}
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table
|
||
v-loading="contractDialog.loading"
|
||
:data="contractDialog.rows"
|
||
border
|
||
highlight-current-row
|
||
@current-change="contractDialog.current = $event"
|
||
@row-dblclick="selectContract"
|
||
>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column
|
||
v-for="column in contractSelectionColumns"
|
||
:key="column.prop"
|
||
:prop="column.prop"
|
||
:label="column.label"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="column.prop === 'organizationName'">{{
|
||
row.organizationName || row.deptName || '-'
|
||
}}</span>
|
||
<span v-else-if="column.prop === 'contractCategory'">{{
|
||
contractCategoryName(row.contractCategory)
|
||
}}</span>
|
||
<span v-else-if="column.prop === 'signType'">{{ signTypeName(row.signType) }}</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<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="pre-settlement-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="contractDialog.page.current"
|
||
v-model:page-size="contractDialog.page.size"
|
||
:total="contractDialog.page.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="loadContractRows"
|
||
@size-change="handleContractSizeChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="contractDialog.visible = false">取消</el-button>
|
||
<el-button type="primary" @click="selectContract(contractDialog.current)">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="adjustDialog.visible"
|
||
title="结算明细调整"
|
||
width="96%"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
|
||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
|
||
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
|
||
<el-table-column label="运输总量" min-width="150" align="center">
|
||
<template #default="{ row }">
|
||
<span v-if="adjustDialog.readonly">{{ formatQuantity(row) }}</span>
|
||
<el-input-number
|
||
v-else
|
||
v-model="row.transportQuantity"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateAdjustRow(row, 'transportQuantity')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="里程(KM)" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<span v-if="adjustDialog.readonly">{{ formatDetailMileage(row.mileage) }}</span>
|
||
<el-input-number
|
||
v-else
|
||
v-model="row.mileage"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateAdjustRow(row, 'mileage')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="运输单价" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.unitPrice"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="adjustDialog.readonly"
|
||
@change="recalculateAdjustRow(row, 'unitPrice')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-for="feeItem in adjustFeeItemNames"
|
||
:key="feeItem"
|
||
:label="feeItem"
|
||
min-width="160"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<div
|
||
v-if="isFreightFeeItem(feeItem)"
|
||
class="pre-settlement-editor__billing-rule-cell"
|
||
>
|
||
<el-input-number
|
||
v-model="row.feeItems[feeItem]"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="adjustDialog.readonly"
|
||
@change="recalculateAdjustRow(row, feeItem)"
|
||
/>
|
||
<el-icon
|
||
v-if="hasBillingRule(row)"
|
||
class="pre-settlement-editor__billing-rule-icon"
|
||
title="查看计费规则详情"
|
||
@click="openBillingRuleInfo(row)"
|
||
>
|
||
<InfoFilled />
|
||
</el-icon>
|
||
</div>
|
||
<el-input-number
|
||
v-else
|
||
v-model="row.feeItems[feeItem]"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="adjustDialog.readonly"
|
||
@change="recalculateAdjustRow(row, feeItem)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结算金额(含税)" min-width="170" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.settlementAmountTax"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="adjustDialog.readonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-form
|
||
v-if="!adjustDialog.readonly"
|
||
:model="adjustDialog"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="pre-settlement-editor__adjust-reason"
|
||
>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="adjustDialog.reason"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="adjustDialog.visible = false">取消</el-button>
|
||
<el-button
|
||
v-if="!adjustDialog.readonly"
|
||
type="primary"
|
||
:loading="adjustDialog.saving"
|
||
@click="saveAdjustment"
|
||
>
|
||
保存
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<change-record-detail-dialog
|
||
v-model="adjustChangeRecordVisible"
|
||
:rows="adjustChangeRecordDetailRows"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="billingRuleDialog.visible"
|
||
title="计费规则详情"
|
||
width="720px"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<template v-if="billingRuleDialog.rule">
|
||
<div class="dialog-section-title">计费规则</div>
|
||
<el-descriptions :column="2" border class="pre-settlement-editor__billing-rule-detail">
|
||
<el-descriptions-item label="费用项">
|
||
{{ displayValue(billingRuleDialog.rule.feeItem) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="计费要素">
|
||
{{ displayValue(billingRuleDialog.rule.billingElement) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="计费类型">
|
||
{{ displayValue(billingRuleDialog.rule.billingType) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="计费单位">
|
||
{{ displayValue(billingRuleDialog.rule.billingUnit) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="单价">
|
||
{{ displayValue(billingRuleDialog.rule.unitPrice) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="最低计费量">
|
||
{{ displayValue(billingRuleDialog.rule.minimumBillingWeight) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="计费区间">
|
||
{{ billingRuleRanges(billingRuleDialog.rule) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="2">
|
||
{{ displayValue(billingRuleDialog.rule.remark) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
<div class="dialog-section-title pre-settlement-editor__billing-rule-match-title">
|
||
匹配条件
|
||
</div>
|
||
<el-descriptions :column="2" border class="pre-settlement-editor__billing-rule-detail">
|
||
<el-descriptions-item label="起运地">
|
||
{{ displayValue(billingRuleDialog.rule.matchCondition.origin) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="目的地">
|
||
{{ displayValue(billingRuleDialog.rule.matchCondition.destination) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="货物名称">
|
||
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoName) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="货物类型">
|
||
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoType) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</template>
|
||
<el-empty v-else description="暂无计费规则详情" />
|
||
<template #footer>
|
||
<el-button type="primary" @click="billingRuleDialog.visible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script>
|
||
import { h } from 'vue';
|
||
import { mapGetters } from 'vuex';
|
||
import { InfoFilled } from '@element-plus/icons-vue';
|
||
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
||
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
|
||
import { getMkPublicDetail } from '@/api/mk-process';
|
||
import {
|
||
adjustDetail,
|
||
getCandidateDetails,
|
||
getContractList,
|
||
getContractOptions,
|
||
getDetail,
|
||
getDetailFees,
|
||
getFeeOptions,
|
||
getNextNo,
|
||
removeDetail,
|
||
save,
|
||
submit,
|
||
} from '@/api/settlement/preSettlement';
|
||
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||
import {
|
||
resolveReceivablePayableDetailId,
|
||
resolveWaybillId,
|
||
settlementDetailPath,
|
||
} from '@/utils/settlement-detail-link';
|
||
import { getBfiExchangeRate } from '@/api/base/currency';
|
||
import { getDictionary } from '@/api/system/dictbiz';
|
||
import {
|
||
emptyPreSettlementForm,
|
||
preSettlementFormFields,
|
||
preSettlementFormRules,
|
||
} from '@/option/settlement/preSettlementForm';
|
||
import {
|
||
contractApprovalStatusOptions,
|
||
contractCategoryOptions,
|
||
contractStageOptions,
|
||
effectiveTypeOptions,
|
||
signTypeOptions,
|
||
} from '@/option/business/common';
|
||
import {
|
||
advanceColumns,
|
||
candidateDetailColumns,
|
||
changeRecordColumns,
|
||
settlementDetailColumns,
|
||
settlementSummaryColumns,
|
||
} from '@/option/settlement/preSettlementTable';
|
||
import { formatContractOptionLabel } from '@/utils/contract';
|
||
import { normalizeCurrencyCode } from '@/utils/currency';
|
||
import { settlementStatusName } from '@/utils/settlementStatus';
|
||
import { downloadFileByUrl } from '@/utils/util';
|
||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||
import * as XLSX from 'xlsx';
|
||
import { isAdministrator } from '@/utils/auth';
|
||
|
||
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
|
||
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
|
||
|
||
export default {
|
||
name: 'PreSettlementEditor',
|
||
components: { InfoFilled, ChangeRecordDetailDialog, ProjectAdvanceWriteOff },
|
||
props: {
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
recordId: {
|
||
type: [String, Number],
|
||
default: '',
|
||
},
|
||
readonly: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
pageMode: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
initialData: {
|
||
type: Object,
|
||
default: null,
|
||
},
|
||
publicMode: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
emits: ['update:modelValue', 'success'],
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
saving: false,
|
||
submitting: false,
|
||
form: emptyPreSettlementForm(),
|
||
formFields: preSettlementFormFields,
|
||
formRules: preSettlementFormRules,
|
||
summaryColumns: settlementSummaryColumns,
|
||
detailColumns: settlementDetailColumns,
|
||
advanceTableColumns: advanceColumns,
|
||
changeTableColumns: changeRecordColumns,
|
||
candidateColumns: candidateDetailColumns,
|
||
contractOptions: [],
|
||
contractCategoryOptions,
|
||
signTypeOptions,
|
||
effectiveTypeOptions,
|
||
contractStageOptions,
|
||
contractApprovalStatusOptions,
|
||
contractSelectionColumns: [
|
||
{ prop: 'contractNo', label: '合同编号', minWidth: 180 },
|
||
{ prop: 'contractName', label: '合同名称', minWidth: 220 },
|
||
{ prop: 'projectName', label: '所属项目', minWidth: 170 },
|
||
{ prop: 'organizationName', label: '所属组织', minWidth: 170 },
|
||
{ prop: 'contractCategory', label: '合同类别', minWidth: 130 },
|
||
{ prop: 'signType', label: '签约类型', minWidth: 120 },
|
||
{ prop: 'partyA', label: '甲方', minWidth: 170 },
|
||
{ prop: 'partyB', label: '乙方', minWidth: 170 },
|
||
{ prop: 'startDate', label: '开始日期', minWidth: 130 },
|
||
{ prop: 'endDate', label: '结束日期', minWidth: 130 },
|
||
{ prop: 'temporaryStartDate', label: '临时效力起', minWidth: 130 },
|
||
{ prop: 'temporaryEndDate', label: '临时效力止', minWidth: 130 },
|
||
],
|
||
contractDialog: {
|
||
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 },
|
||
},
|
||
feeOptions: [],
|
||
feeCategoryOptions: [],
|
||
transportTypeOptions: [],
|
||
summaryFees: [],
|
||
details: [],
|
||
detailCollapsed: false,
|
||
detailQuery: {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
},
|
||
appliedDetailQuery: {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
},
|
||
detailPage: {
|
||
current: 1,
|
||
size: 10,
|
||
},
|
||
advances: [],
|
||
projectAdvanceWriteOffs: [],
|
||
changeRecords: [],
|
||
attachments: [],
|
||
attachmentTypeOptions: [],
|
||
selectedAttachmentRows: [],
|
||
candidateDialog: {
|
||
visible: false,
|
||
loading: false,
|
||
confirming: false,
|
||
},
|
||
candidateQuery: {
|
||
batchNo: '',
|
||
feeDateRange: [],
|
||
},
|
||
candidateRows: [],
|
||
candidateSelection: [],
|
||
candidatePage: {
|
||
current: 1,
|
||
size: 10,
|
||
total: 0,
|
||
},
|
||
adjustDialog: {
|
||
visible: false,
|
||
loading: false,
|
||
saving: false,
|
||
readonly: false,
|
||
detailId: '',
|
||
sourceDetailId: '',
|
||
detailLineNo: '',
|
||
reason: '',
|
||
},
|
||
adjustRows: [],
|
||
pendingAdjustments: {},
|
||
sourceFeeSnapshots: {},
|
||
detailFeeCache: {}, // 缓存每个明细的费用明细数据(用于拼接货物名称和类型)
|
||
adjustChangeRecordVisible: false,
|
||
adjustChangeRecord: null,
|
||
adjustChangeRecordDetailRows: [],
|
||
billingRuleDialog: {
|
||
visible: false,
|
||
rule: null,
|
||
},
|
||
attachmentFileTypes: [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
],
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission', 'userInfo']),
|
||
visible: {
|
||
get() {
|
||
return this.modelValue;
|
||
},
|
||
set(value) {
|
||
this.$emit('update:modelValue', value);
|
||
},
|
||
},
|
||
editorContainer() {
|
||
return this.pageMode ? 'div' : 'el-dialog';
|
||
},
|
||
editorContainerProps() {
|
||
if (this.pageMode) {
|
||
return {
|
||
class: ['pre-settlement-editor', 'pre-settlement-editor--page'],
|
||
};
|
||
}
|
||
return {
|
||
modelValue: this.visible,
|
||
title: this.dialogTitle,
|
||
width: '96%',
|
||
top: '2vh',
|
||
appendToBody: true,
|
||
destroyOnClose: true,
|
||
class: 'pre-settlement-editor',
|
||
};
|
||
},
|
||
editable() {
|
||
return !this.readonly && ['draft', 'returned'].includes(this.form.approvalStatus);
|
||
},
|
||
isAdmin() {
|
||
return isAdministrator(this.userInfo);
|
||
},
|
||
canAdjustDetail() {
|
||
return this.editable && this.hasPermission('pre_settlement_adjust');
|
||
},
|
||
dialogTitle() {
|
||
if (this.readonly) return '查看预付结算单';
|
||
return this.form.id ? '编辑预付结算单' : '新增预付结算单';
|
||
},
|
||
settlementTypeName() {
|
||
return this.form.settlementType === 'receivable' ? '应收' : '应付';
|
||
},
|
||
// 币种为空按人民币处理,与后端 normalizeRate 的取默认值一致
|
||
isLocalCurrencySettlement() {
|
||
return normalizeCurrencyCode(this.form.currency) === 'RMB';
|
||
},
|
||
visibleFormFields() {
|
||
if (!this.isLocalCurrencySettlement) return this.formFields;
|
||
return this.formFields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
|
||
},
|
||
contractDialogCategory() {
|
||
return '承运商合同';
|
||
},
|
||
summaryTotal() {
|
||
// 与结算合计表格的合计行保持一致:包含手工添加的费用与明细调整后的金额。
|
||
if (this.summaryFees.length) {
|
||
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
|
||
}
|
||
return this.details.reduce((total, row) => total + this.detailSettlementAmount(row), 0);
|
||
},
|
||
visibleDetailColumns() {
|
||
if (!this.readonly && !this.pageMode) return this.detailColumns;
|
||
return this.detailColumns.filter(column => column.prop !== 'settlementAmountNoTax');
|
||
},
|
||
detailFeeItemNames() {
|
||
const names = new Set();
|
||
this.details.forEach(row => {
|
||
Object.keys(this.parseFeeItems(row.feeItemsJson)).forEach(name => names.add(name));
|
||
});
|
||
return Array.from(names);
|
||
},
|
||
detailDisplayColumns() {
|
||
const columns = this.visibleDetailColumns;
|
||
const unitPriceIndex = columns.findIndex(column => column.prop === 'unitPrice');
|
||
const beforeColumns = unitPriceIndex < 0 ? columns : columns.slice(0, unitPriceIndex + 1);
|
||
const afterColumns = unitPriceIndex < 0 ? [] : columns.slice(unitPriceIndex + 1);
|
||
return [
|
||
...beforeColumns.map(column => ({ kind: 'field', key: column.prop, column })),
|
||
...this.detailFeeItemNames.map(name => ({ kind: 'fee', key: `fee-${name}`, name })),
|
||
...afterColumns.map(column => ({ kind: 'field', key: column.prop, column })),
|
||
];
|
||
},
|
||
// 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型)
|
||
groupedDetails() {
|
||
const groups = {};
|
||
this.details.forEach(detail => {
|
||
// 优先使用 sourceDetailId,如果没有则使用 preSettlementDetailId
|
||
const groupKey = detail.sourceDetailId || detail.preSettlementDetailId || detail.id;
|
||
if (!groups[groupKey]) {
|
||
groups[groupKey] = [];
|
||
}
|
||
groups[groupKey].push(detail);
|
||
});
|
||
return groups;
|
||
},
|
||
filteredDetails() {
|
||
return this.details.filter(row =>
|
||
Object.entries(this.appliedDetailQuery).every(([field, keyword]) => {
|
||
if (!keyword?.trim()) return true;
|
||
return String(row[field] || '')
|
||
.toLocaleLowerCase()
|
||
.includes(keyword.trim().toLocaleLowerCase());
|
||
})
|
||
);
|
||
},
|
||
pagedDetails() {
|
||
const list = this.filteredDetails;
|
||
const size = Number(this.detailPage.size) || 10;
|
||
const current = Math.max(Number(this.detailPage.current) || 1, 1);
|
||
const start = (current - 1) * size;
|
||
return list.slice(start, start + size);
|
||
},
|
||
adjustFeeItemNames() {
|
||
const names = new Set();
|
||
this.adjustRows.forEach(row => {
|
||
Object.keys(row.feeItems || {}).forEach(name => names.add(name));
|
||
});
|
||
return Array.from(names);
|
||
},
|
||
},
|
||
watch: {
|
||
modelValue: {
|
||
immediate: true,
|
||
handler(value) {
|
||
if (value) this.initialize();
|
||
},
|
||
},
|
||
summaryTotal(value) {
|
||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||
this.recalculateLocalAmount();
|
||
},
|
||
filteredDetails: {
|
||
handler(list) {
|
||
const size = Number(this.detailPage.size) || 10;
|
||
const maxPage = Math.max(1, Math.ceil((list?.length || 0) / size) || 1);
|
||
if (this.detailPage.current > maxPage) {
|
||
this.detailPage.current = maxPage;
|
||
}
|
||
},
|
||
immediate: true,
|
||
},
|
||
},
|
||
methods: {
|
||
formatContractOptionLabel,
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||
},
|
||
async initialize() {
|
||
this.resetEditor();
|
||
// 新增页在保存前没有创建人/创建日期,先按当前登录用户与当前时间展示将要落库的值
|
||
if (!this.recordId) {
|
||
this.form.createUserName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||
this.form.createTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
}
|
||
if (!this.publicMode) {
|
||
await this.loadFeeOptions();
|
||
await this.loadFeeCategoryOptions();
|
||
await this.loadTransportTypeOptions();
|
||
await this.loadAttachmentTypeOptions();
|
||
}
|
||
if (this.recordId) await this.loadDetail();
|
||
else if (this.initialData) {
|
||
await this.applyInitialData();
|
||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||
await this.refreshPreSettlementNo();
|
||
await this.fetchBfiExchangeRate();
|
||
} else if (!this.publicMode) {
|
||
await this.refreshPreSettlementNo();
|
||
}
|
||
},
|
||
async refreshPreSettlementNo() {
|
||
if (this.form.id) return;
|
||
const response = await getNextNo();
|
||
this.form.preSettlementNo = response.data?.data || '';
|
||
},
|
||
async applyInitialData() {
|
||
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
||
if (!rows.length) return;
|
||
const first = rows[0];
|
||
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
|
||
const contractId = first.contractId || '';
|
||
let contractPartyA = first.payerName || '';
|
||
let contractPartyB = first.payeeName || '';
|
||
if (contractId && (!contractPartyA || !contractPartyB)) {
|
||
try {
|
||
const { data } = await getContractOptions(first.contractNo || '');
|
||
const matched = (data?.data || []).find(item => String(item.id) === String(contractId));
|
||
contractPartyA = contractPartyA || matched?.partyA || '';
|
||
contractPartyB = contractPartyB || matched?.partyB || '';
|
||
} catch (error) {
|
||
contractPartyA = '';
|
||
contractPartyB = '';
|
||
}
|
||
}
|
||
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
|
||
this.contractOptions.push({
|
||
id: contractId,
|
||
contractNo: first.contractNo,
|
||
contractName: first.contractName,
|
||
projectId: first.projectId,
|
||
projectName: first.projectName,
|
||
deptId: first.deptId,
|
||
deptName: first.deptName,
|
||
settlementType,
|
||
partyA: contractPartyA,
|
||
partyB: contractPartyB,
|
||
});
|
||
}
|
||
this.handleContractChange(contractId);
|
||
Object.assign(this.form, {
|
||
contractId,
|
||
contractNo: first.contractNo || this.form.contractNo,
|
||
contractName: first.contractName || this.form.contractName,
|
||
projectId: first.projectId || this.form.projectId,
|
||
projectName: first.projectName || this.form.projectName,
|
||
deptId: first.deptId || this.form.deptId,
|
||
deptName: first.deptName || this.form.deptName,
|
||
payerName: this.form.payerName || contractPartyA,
|
||
payeeName: this.form.payeeName || contractPartyB,
|
||
settlementType,
|
||
currency: first.currency || 'RMB',
|
||
localCurrency: first.localCurrency || 'RMB',
|
||
});
|
||
this.details = rows.map(row => this.normalizeSourceDetail(row));
|
||
this.detailPage.current = 1;
|
||
this.loading = true;
|
||
try {
|
||
await this.ensureSourceFeeSnapshots();
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
this.buildSummaryFeesFromDetails();
|
||
},
|
||
normalizeSourceDetail(row) {
|
||
const settlementAmount = this.detailSettlementAmount(row);
|
||
const originalValue =
|
||
row.originalAmount ?? row.originalSettlementAmount ?? row.totalAmount ?? settlementAmount;
|
||
const originalAmount = Number(originalValue || 0);
|
||
const adjustValue = row.adjustAmount ?? row.adjustmentAmount;
|
||
const adjustAmount =
|
||
adjustValue === undefined || adjustValue === null
|
||
? settlementAmount - originalAmount
|
||
: Number(adjustValue || 0);
|
||
return {
|
||
...row,
|
||
sourceDetailId: row.sourceDetailId || row.id,
|
||
originalAmount: Number(originalAmount.toFixed(2)),
|
||
adjustAmount: Number(adjustAmount.toFixed(2)),
|
||
settlementAmountTax: Number(settlementAmount.toFixed(2)),
|
||
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
|
||
};
|
||
},
|
||
detailOriginalAmount(row) {
|
||
return Number(row?.originalAmount ?? 0);
|
||
},
|
||
detailAdjustAmount(row) {
|
||
const value = row?.adjustAmount ?? row?.adjustmentAmount;
|
||
return Number(
|
||
Number(
|
||
value === undefined || value === null
|
||
? this.detailSettlementAmount(row) - this.detailOriginalAmount(row)
|
||
: value
|
||
).toFixed(2)
|
||
);
|
||
},
|
||
detailSettlementAmount(row) {
|
||
return Number(
|
||
row?.settlementAmountTax ??
|
||
row?.totalAmount ??
|
||
row?.afterAmount ??
|
||
row?.settlementAmount ??
|
||
0
|
||
);
|
||
},
|
||
resetEditor() {
|
||
this.form = emptyPreSettlementForm();
|
||
this.summaryFees = [];
|
||
this.details = [];
|
||
this.detailCollapsed = false;
|
||
this.detailQuery = {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
};
|
||
this.appliedDetailQuery = { ...this.detailQuery };
|
||
this.detailPage = { current: 1, size: 10 };
|
||
this.advances = [];
|
||
this.projectAdvanceWriteOffs = [];
|
||
this.changeRecords = [];
|
||
this.attachments = [];
|
||
this.selectedAttachmentRows = [];
|
||
this.adjustRows = [];
|
||
this.pendingAdjustments = {};
|
||
this.sourceFeeSnapshots = {};
|
||
this.detailFeeCache = {};
|
||
this.adjustDialog.detailId = '';
|
||
this.adjustDialog.sourceDetailId = '';
|
||
this.adjustDialog.detailLineNo = '';
|
||
this.adjustChangeRecordVisible = false;
|
||
this.adjustChangeRecord = null;
|
||
this.adjustChangeRecordDetailRows = [];
|
||
this.billingRuleDialog.visible = false;
|
||
this.billingRuleDialog.rule = null;
|
||
this.contractOptions = [];
|
||
this.contractDialog.visible = false;
|
||
this.contractDialog.expanded = false;
|
||
this.contractDialog.rows = [];
|
||
this.contractDialog.current = null;
|
||
this.contractDialog.page = { current: 1, size: 10, total: 0 };
|
||
this.$nextTick(() => this.$refs.formRef?.clearValidate());
|
||
},
|
||
async loadDetail() {
|
||
this.loading = true;
|
||
try {
|
||
const response = this.publicMode
|
||
? await getMkPublicDetail('pre-settlement', this.form.id || this.recordId)
|
||
: await getDetail(this.form.id || this.recordId);
|
||
const detail = response.data?.data || {};
|
||
this.form = { ...emptyPreSettlementForm(), ...detail };
|
||
this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
|
||
this.details = (detail.details || []).map(row => this.normalizeSourceDetail(row));
|
||
this.detailPage.current = 1;
|
||
this.advances = (detail.advances || []).map(row => ({
|
||
...row,
|
||
createUserName: row.createUserName || detail.createUserName,
|
||
createTime: row.createTime || detail.createTime,
|
||
}));
|
||
this.projectAdvanceWriteOffs = (detail.projectAdvanceWriteOffs || []).map(row => ({
|
||
...row,
|
||
writeOffAmount: Number(row.writeOffAmount || 0),
|
||
projectAdvanceAmount: Number(row.projectAdvanceAmount || 0),
|
||
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
|
||
createTime: row.paymentCreateTime || row.createTime,
|
||
}));
|
||
this.changeRecords = detail.changeRecords || [];
|
||
this.attachments = this.parseAttachments(detail.attachmentsJson);
|
||
this.sortAttachments();
|
||
this.selectedAttachmentRows = [];
|
||
if (
|
||
detail.contractId &&
|
||
!this.contractOptions.some(item => item.id === detail.contractId)
|
||
) {
|
||
this.contractOptions.push({
|
||
id: detail.contractId,
|
||
contractNo: detail.contractNo,
|
||
contractName: detail.contractName,
|
||
projectId: detail.projectId,
|
||
projectName: detail.projectName,
|
||
deptId: detail.deptId,
|
||
deptName: detail.deptName,
|
||
partyA: detail.payerName,
|
||
partyB: detail.payeeName,
|
||
});
|
||
}
|
||
if (this.publicMode && detail.detailFees) {
|
||
Object.entries(detail.detailFees).forEach(([detailId, feeRows]) => {
|
||
if (Array.isArray(feeRows) && feeRows.length) {
|
||
this.detailFeeCache[detailId] = feeRows;
|
||
}
|
||
});
|
||
} else {
|
||
await this.loadAllDetailFees();
|
||
}
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
openContractDialog() {
|
||
if (!this.editable || this.details.length > 0) return;
|
||
this.contractDialog.visible = true;
|
||
this.contractDialog.expanded = false;
|
||
this.contractDialog.current = null;
|
||
this.contractDialog.page.current = 1;
|
||
this.loadContractRows();
|
||
},
|
||
async loadContractRows() {
|
||
this.contractDialog.loading = true;
|
||
try {
|
||
const { data } = await getContractList(
|
||
this.contractDialog.page.current,
|
||
this.contractDialog.page.size,
|
||
{
|
||
...this.contractDialog.query,
|
||
contractCategory: this.contractDialogCategory,
|
||
}
|
||
);
|
||
const page = data?.data || {};
|
||
this.contractDialog.rows = page.records || [];
|
||
this.contractDialog.page.total = Number(page.total || 0);
|
||
} finally {
|
||
this.contractDialog.loading = false;
|
||
}
|
||
},
|
||
searchContractRows() {
|
||
this.contractDialog.page.current = 1;
|
||
this.loadContractRows();
|
||
},
|
||
resetContractQuery() {
|
||
this.contractDialog.query = {
|
||
contractNo: '',
|
||
contractName: '',
|
||
projectName: '',
|
||
organizationName: '',
|
||
contractCategory: this.contractDialogCategory,
|
||
signType: '',
|
||
effectiveType: '',
|
||
contractStage: '',
|
||
approvalStatus: '',
|
||
};
|
||
this.searchContractRows();
|
||
},
|
||
handleContractSizeChange() {
|
||
this.contractDialog.page.current = 1;
|
||
this.loadContractRows();
|
||
},
|
||
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,
|
||
settlementType:
|
||
row.settlementType || (row.contractCategory === '客户合同' ? 'receivable' : 'payable'),
|
||
};
|
||
const index = this.contractOptions.findIndex(item => String(item.id) === String(contract.id));
|
||
if (index >= 0) this.contractOptions.splice(index, 1, contract);
|
||
else this.contractOptions.push(contract);
|
||
this.handleContractChange(contract.id);
|
||
this.contractDialog.visible = false;
|
||
this.contractDialog.current = null;
|
||
this.$refs.formRef?.clearValidate('contractId');
|
||
},
|
||
contractCategoryName(value) {
|
||
return (
|
||
this.contractCategoryOptions.find(item => String(item.value) === String(value))?.label ||
|
||
this.displayValue(value)
|
||
);
|
||
},
|
||
signTypeName(value) {
|
||
return (
|
||
this.signTypeOptions.find(item => String(item.value) === String(value))?.label ||
|
||
this.displayValue(value)
|
||
);
|
||
},
|
||
async loadFeeOptions() {
|
||
const { data } = await getFeeOptions();
|
||
this.feeOptions = data?.data || [];
|
||
},
|
||
async loadFeeCategoryOptions() {
|
||
const { data } = await getDictionary({ code: 'fee_category' });
|
||
this.feeCategoryOptions = data?.data || [];
|
||
},
|
||
async loadTransportTypeOptions() {
|
||
const { data } = await getDictionary({ code: 'transport_type' });
|
||
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
|
||
this.transportTypeOptions = records.map(item => ({
|
||
dictKey: item.dictKey ?? item.value,
|
||
dictValue: item.dictValue ?? item.label ?? item.name,
|
||
}));
|
||
},
|
||
handleContractChange(id) {
|
||
const contract = this.contractOptions.find(item => String(item.id) === String(id));
|
||
if (!contract) {
|
||
this.form.contractId = '';
|
||
this.form.contractNo = '';
|
||
this.form.contractName = '';
|
||
this.form.projectId = '';
|
||
this.form.projectName = '';
|
||
this.form.deptId = '';
|
||
this.form.deptName = '';
|
||
this.form.payerName = '';
|
||
this.form.payeeName = '';
|
||
this.summaryFees = [];
|
||
this.projectAdvanceWriteOffs = [];
|
||
return;
|
||
}
|
||
this.form.contractId = contract.id;
|
||
this.form.contractNo = contract.contractNo;
|
||
this.form.contractName = contract.contractName;
|
||
this.form.projectId = contract.projectId;
|
||
this.form.projectName = contract.projectName;
|
||
this.form.deptId = contract.deptId;
|
||
this.form.deptName = contract.deptName;
|
||
this.form.settlementType = contract.settlementType || 'payable';
|
||
// 部分候选合同只带 id/名称,缺甲方乙方;此时保留已带出的收付方,不要清成 undefined
|
||
if (contract.partyA) this.form.payerName = contract.partyA;
|
||
if (contract.partyB) this.form.payeeName = contract.partyB;
|
||
this.form.currency = contract.settlementCurrency || 'RMB';
|
||
this.summaryFees = [];
|
||
this.projectAdvanceWriteOffs = [];
|
||
if (this.form.currency !== 'RMB') {
|
||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||
this.fetchBfiExchangeRate();
|
||
}
|
||
},
|
||
async saveDraft(shouldSubmit) {
|
||
await this.$refs.formRef?.validate();
|
||
if (shouldSubmit && !this.details.length) {
|
||
this.$message.warning('请至少选择一条结算明细');
|
||
return;
|
||
}
|
||
const invalidManualFeeIndex = this.summaryFees.findIndex(
|
||
row =>
|
||
Number(row.manualFlag) === 1 &&
|
||
(!String(row.feeType || '').trim() || !String(row.feeItem || '').trim())
|
||
);
|
||
if (invalidManualFeeIndex >= 0) {
|
||
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
|
||
return;
|
||
}
|
||
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
|
||
return;
|
||
}
|
||
const stateKey = shouldSubmit ? 'submitting' : 'saving';
|
||
this[stateKey] = true;
|
||
try {
|
||
const { data } = await save(this.buildSavePayload());
|
||
this.form.id = data?.data || this.form.id;
|
||
await this.loadDetail();
|
||
await this.persistPendingAdjustments();
|
||
if (shouldSubmit) {
|
||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||
await submit({ id: this.form.id });
|
||
const mkStarted = await submitMkApprovalFlow({
|
||
bizType: 'pre-settlement',
|
||
bizId: this.form.id,
|
||
subjectName: this.form.preSettlementNo || this.form.contractName || '',
|
||
approvalStatus: previousApprovalStatus,
|
||
mkEnabled: await loadMkEnabled().catch(() => false),
|
||
});
|
||
this.$message.success(mkStarted ? '审批流程已发起' : '提交成功');
|
||
this.visible = false;
|
||
this.$emit('success', this.form.id);
|
||
return;
|
||
}
|
||
this.$message.success('保存成功');
|
||
this.visible = false;
|
||
this.$emit('success', this.form.id);
|
||
} finally {
|
||
this[stateKey] = false;
|
||
}
|
||
},
|
||
buildSavePayload() {
|
||
return {
|
||
id: this.form.id || undefined,
|
||
contractId: this.form.contractId,
|
||
exchangeRateDate: this.form.exchangeRateDate || undefined,
|
||
exchangeRate: this.form.exchangeRate,
|
||
attachmentsJson: JSON.stringify(this.attachments || []),
|
||
remark: this.form.remark,
|
||
sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id),
|
||
summaryFees: this.summaryFees.map(row => ({
|
||
id: row.id || undefined,
|
||
feeType: this.feeCategoryValue(row.feeType),
|
||
feeItem: row.feeItem,
|
||
originalAmount: Number(row.originalAmount || 0),
|
||
adjustAmount: Number(row.adjustAmount || 0),
|
||
remark: row.remark,
|
||
manualFlag: row.manualFlag || 0,
|
||
})),
|
||
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
|
||
};
|
||
},
|
||
addSummaryFee() {
|
||
this.summaryFees.push({
|
||
id: '',
|
||
feeType: '',
|
||
feeItem: '',
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
settlementAmount: 0,
|
||
remark: '',
|
||
manualFlag: 1,
|
||
});
|
||
},
|
||
handleManualFeeTypeChange(row) {
|
||
if (!this.manualFeeItems(row.feeType).includes(row.feeItem)) row.feeItem = '';
|
||
},
|
||
manualFeeItems(feeType) {
|
||
return (
|
||
this.feeOptions.find(
|
||
item => this.feeCategoryValue(item.feeType) === this.feeCategoryValue(feeType)
|
||
)?.feeItems || []
|
||
);
|
||
},
|
||
findFeeCategory(value) {
|
||
return this.feeCategoryOptions.find(
|
||
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
||
);
|
||
},
|
||
// fee_category 业务字典:dictKey 为字典键值(落库值),dictValue 为字典名称(展示值)。
|
||
feeCategoryValue(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
return this.findFeeCategory(value)?.dictKey ?? value;
|
||
},
|
||
feeCategoryName(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
const option = this.findFeeCategory(value);
|
||
if (option) return option.dictValue ?? value;
|
||
const feeOption = this.feeOptions.find(item => String(item.feeType) === String(value));
|
||
return feeOption?.feeTypeName || value;
|
||
},
|
||
transportTypeName(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
const normalizedValue = String(value).trim().toLocaleLowerCase();
|
||
const option = this.transportTypeOptions.find(
|
||
item =>
|
||
String(item.dictKey ?? '')
|
||
.trim()
|
||
.toLocaleLowerCase() === normalizedValue ||
|
||
String(item.dictValue ?? '')
|
||
.trim()
|
||
.toLocaleLowerCase() === normalizedValue
|
||
);
|
||
return option?.dictValue || value;
|
||
},
|
||
formatDetailMileage(value) {
|
||
if (value === undefined || value === null || value === '' || Number(value) === -1) return '-';
|
||
return value;
|
||
},
|
||
removeSummaryFee(index) {
|
||
this.summaryFees.splice(index, 1);
|
||
},
|
||
recalculateSummaryRow(row) {
|
||
if (Number(row.manualFlag) !== 1) return;
|
||
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
|
||
},
|
||
getSummarySums({ columns, data }) {
|
||
const sumProps = ['originalAmount', 'adjustAmount', 'settlementAmount'];
|
||
return columns.map((column, index) => {
|
||
if (index === 0) return '合计';
|
||
if (!sumProps.includes(column.property)) return '';
|
||
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
|
||
const totalText = this.formatMoney(total, this.form.currency);
|
||
if (column.property === 'adjustAmount' && total < 0) {
|
||
return h('span', { style: { color: 'var(--el-color-danger)' } }, totalText);
|
||
}
|
||
return totalText;
|
||
});
|
||
},
|
||
// 结算合计按费用项归集:原金额取来源费用明细的初始金额,结算金额取调整后的金额,调整金额为两者之差。
|
||
buildSummaryFeesFromDetails() {
|
||
const summaryMap = new Map();
|
||
const generatedFees = new Map(
|
||
this.summaryFees
|
||
.filter(row => Number(row.manualFlag) !== 1)
|
||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||
);
|
||
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||
const originalAmount = Number(originalValue || 0);
|
||
const settlementAmount = Number(settlementValue || 0);
|
||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||
return;
|
||
}
|
||
if (Math.abs(originalAmount) < 0.005 && Math.abs(settlementAmount) < 0.005) return;
|
||
const feeOption = this.feeOptions.find(item =>
|
||
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
||
);
|
||
const feeType = this.feeCategoryValue(feeOption?.feeType || fallbackFeeType);
|
||
const key = `${feeType}\u0000${feeItem}`;
|
||
const generated = generatedFees.get(key);
|
||
const current = summaryMap.get(key) || {
|
||
id: generated?.id || '',
|
||
feeType,
|
||
feeItem,
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
settlementAmount: 0,
|
||
remark: generated?.remark || '',
|
||
manualFlag: 0,
|
||
};
|
||
current.originalAmount = Number((current.originalAmount + originalAmount).toFixed(2));
|
||
current.settlementAmount = Number((current.settlementAmount + settlementAmount).toFixed(2));
|
||
current.adjustAmount = Number(
|
||
(current.settlementAmount - current.originalAmount).toFixed(2)
|
||
);
|
||
summaryMap.set(key, current);
|
||
};
|
||
|
||
this.details.forEach(detail => {
|
||
this.resolveDetailFeePairs(detail).forEach(([baseRow, currentRow]) =>
|
||
this.appendFeeRowSummary(baseRow, currentRow, detail, appendSummary)
|
||
);
|
||
});
|
||
|
||
const manualFees = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||
const nextFeeMap = new Map();
|
||
summaryMap.forEach((row, key) => nextFeeMap.set(`0\u0000${key}`, row));
|
||
manualFees.forEach(row => {
|
||
const key = `1\u0000${row.feeType}\u0000${row.feeItem}`;
|
||
nextFeeMap.set(key, row);
|
||
});
|
||
// 按编辑前顺序回填,新增费用项追加到末尾,避免手工费用被统一挤到最后。
|
||
const orderedFees = [];
|
||
const previousKeys = this.summaryFees.map(
|
||
row => `${Number(row.manualFlag) === 1 ? 1 : 0}\u0000${row.feeType}\u0000${row.feeItem}`
|
||
);
|
||
previousKeys.forEach(key => {
|
||
const row = nextFeeMap.get(key);
|
||
if (!row) return;
|
||
orderedFees.push(row);
|
||
nextFeeMap.delete(key);
|
||
});
|
||
nextFeeMap.forEach(row => orderedFees.push(row));
|
||
this.summaryFees = orderedFees;
|
||
this.recalculateLocalAmount();
|
||
},
|
||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
|
||
resolveDetailFeePairs(detail) {
|
||
const sourceDetailId = String(detail.sourceDetailId || detail.id || '');
|
||
const baseRows = this.sourceFeeSnapshots[sourceDetailId] || [];
|
||
if (!baseRows.length) return [this.buildDetailFeePair(detail)];
|
||
const pendingRows = this.pendingAdjustments[sourceDetailId]?.rows || [];
|
||
return baseRows.map((baseRow, index) => {
|
||
const matched =
|
||
pendingRows.find(row => baseRow.id && String(row.id) === String(baseRow.id)) ||
|
||
pendingRows[index];
|
||
return [baseRow, matched || baseRow];
|
||
});
|
||
},
|
||
buildDetailFeePair(detail) {
|
||
const baseRow = {
|
||
feeItems: this.parseFeeItems(detail.feeItemsJson || detail.feeItems),
|
||
feeType: detail.feeType,
|
||
freightAmount: Number(detail.freightAmount || 0),
|
||
settlementAmountTax: this.detailOriginalAmount(detail),
|
||
};
|
||
return [baseRow, { ...baseRow, settlementAmountTax: this.detailSettlementAmount(detail) }];
|
||
},
|
||
appendFeeRowSummary(baseRow, currentRow, detail, appendSummary) {
|
||
const fallbackFeeType = currentRow.feeType || detail.feeType || '';
|
||
const feeItemNames = Array.from(
|
||
new Set([...Object.keys(baseRow.feeItems || {}), ...Object.keys(currentRow.feeItems || {})])
|
||
);
|
||
feeItemNames.forEach(name =>
|
||
appendSummary(name, baseRow.feeItems?.[name], currentRow.feeItems?.[name], fallbackFeeType)
|
||
);
|
||
const hasFreightItem = feeItemNames.some(name => this.isFreightFeeItem(name));
|
||
if (!hasFreightItem) {
|
||
appendSummary(
|
||
this.defaultFreightFeeItem(),
|
||
baseRow.freightAmount,
|
||
currentRow.freightAmount,
|
||
fallbackFeeType
|
||
);
|
||
}
|
||
const knownAmount = row =>
|
||
this.sumFeeItems(row.feeItems) + (hasFreightItem ? 0 : Number(row.freightAmount || 0));
|
||
const baseResidual = Number(
|
||
(Number(baseRow.settlementAmountTax || 0) - knownAmount(baseRow)).toFixed(2)
|
||
);
|
||
const currentResidual = Number(
|
||
(Number(currentRow.settlementAmountTax || 0) - knownAmount(currentRow)).toFixed(2)
|
||
);
|
||
if (Math.abs(baseResidual) < 0.005 && Math.abs(currentResidual) < 0.005) return;
|
||
// 结算金额被手工改写且无法拆分到具体费用项时,计入金额占比最大的费用项,否则归入其他费用。
|
||
appendSummary(
|
||
this.residualFeeItem(baseRow, currentRow),
|
||
baseResidual,
|
||
currentResidual,
|
||
fallbackFeeType
|
||
);
|
||
},
|
||
defaultFreightFeeItem() {
|
||
return (
|
||
this.feeOptions
|
||
.flatMap(item => item.feeItems || [])
|
||
.find(name => this.isFreightFeeItem(name)) || '运输费'
|
||
);
|
||
},
|
||
residualFeeItem(baseRow, currentRow) {
|
||
const feeItems = { ...(baseRow.feeItems || {}), ...(currentRow.feeItems || {}) };
|
||
const dominant = Object.entries(feeItems)
|
||
.filter(([, amount]) => Math.abs(Number(amount || 0)) >= 0.005)
|
||
.sort((a, b) => Math.abs(Number(b[1] || 0)) - Math.abs(Number(a[1] || 0)))[0];
|
||
if (dominant) return dominant[0];
|
||
return (
|
||
currentRow.billingRule?.feeItem ||
|
||
currentRow.feeItem ||
|
||
baseRow.billingRule?.feeItem ||
|
||
baseRow.feeItem ||
|
||
'其他费用'
|
||
);
|
||
},
|
||
sumFeeItems(feeItems) {
|
||
return Number(
|
||
Object.values(feeItems || {})
|
||
.reduce((total, value) => total + Number(value || 0), 0)
|
||
.toFixed(2)
|
||
);
|
||
},
|
||
handleExchangeRateDateChange() {
|
||
this.fetchBfiExchangeRate();
|
||
this.recalculateLocalAmount();
|
||
},
|
||
async fetchBfiExchangeRate() {
|
||
if (!this.form.currency || this.form.currency === 'RMB') return;
|
||
if (!this.form.exchangeRateDate) return;
|
||
try {
|
||
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
|
||
const rateData = data?.data;
|
||
if (rateData?.excval != null) {
|
||
this.form.exchangeRate = Number(rateData.excval);
|
||
this.recalculateLocalAmount();
|
||
} else {
|
||
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
|
||
}
|
||
} catch (e) {
|
||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||
}
|
||
},
|
||
recalculateLocalAmount() {
|
||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||
},
|
||
openCandidateDialog() {
|
||
if (!this.form.contractId) {
|
||
this.$message.warning('请先选择合同名称');
|
||
return;
|
||
}
|
||
this.candidateDialog.visible = true;
|
||
this.candidatePage.current = 1;
|
||
this.candidateSelection = [];
|
||
this.loadCandidates();
|
||
},
|
||
async loadCandidates() {
|
||
this.candidateDialog.loading = true;
|
||
try {
|
||
const dateRange = this.candidateQuery.feeDateRange || [];
|
||
const { data } = await getCandidateDetails(
|
||
this.candidatePage.current,
|
||
this.candidatePage.size,
|
||
{
|
||
contractId: this.form.contractId,
|
||
settlementType: this.form.settlementType,
|
||
batchNo: this.candidateQuery.batchNo,
|
||
feeStartDate: dateRange[0],
|
||
feeEndDate: dateRange[1],
|
||
}
|
||
);
|
||
const page = data?.data || {};
|
||
this.candidateRows = page.records || [];
|
||
this.candidatePage.total = Number(page.total || 0);
|
||
} finally {
|
||
this.candidateDialog.loading = false;
|
||
}
|
||
},
|
||
resetCandidateQuery() {
|
||
this.candidateQuery = { batchNo: '', feeDateRange: [] };
|
||
this.candidatePage.current = 1;
|
||
this.loadCandidates();
|
||
},
|
||
handleCandidateSelection(rows) {
|
||
this.candidateSelection = rows;
|
||
},
|
||
handleCandidateSizeChange() {
|
||
this.candidatePage.current = 1;
|
||
this.loadCandidates();
|
||
},
|
||
async confirmCandidates() {
|
||
if (!this.candidateSelection.length) {
|
||
this.$message.warning('请选择结算明细');
|
||
return;
|
||
}
|
||
const existingIds = new Set(this.details.map(row => String(row.sourceDetailId || row.id)));
|
||
this.candidateSelection.forEach(row => {
|
||
if (!existingIds.has(String(row.id))) {
|
||
this.details.push(this.normalizeSourceDetail(row));
|
||
}
|
||
});
|
||
this.candidateDialog.confirming = true;
|
||
await this.ensureSourceFeeSnapshots();
|
||
this.buildSummaryFeesFromDetails();
|
||
// 页面模式选择明细先更新本地数据,保存/提交时再统一落库。
|
||
if (this.pageMode) {
|
||
this.candidateDialog.visible = false;
|
||
this.candidateDialog.confirming = false;
|
||
this.$message.success('结算明细添加成功,请点击保存提交');
|
||
return;
|
||
}
|
||
try {
|
||
const { data } = await save(this.buildSavePayload());
|
||
this.form.id = data?.data || this.form.id;
|
||
await this.loadDetail();
|
||
this.candidateDialog.visible = false;
|
||
this.$message.success('结算明细添加成功');
|
||
this.$emit('success');
|
||
} finally {
|
||
this.candidateDialog.confirming = false;
|
||
}
|
||
},
|
||
async removeDetailRow(row) {
|
||
await this.$confirm('确认将该明细踢出预付结算单?', '提示', { type: 'warning' });
|
||
if (!this.form.id || !row.id || row.id === row.sourceDetailId) {
|
||
this.details = this.details.filter(item => item !== row);
|
||
this.buildSummaryFeesFromDetails();
|
||
return;
|
||
}
|
||
await removeDetail(this.form.id, row.id);
|
||
this.$message.success('已移除');
|
||
await this.loadDetail();
|
||
this.$emit('success', this.form.id);
|
||
},
|
||
handleDetailQuery() {
|
||
this.appliedDetailQuery = { ...this.detailQuery };
|
||
this.detailPage.current = 1;
|
||
},
|
||
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
|
||
async handleDetailLink(row, column) {
|
||
if (column.prop === 'documentNo') {
|
||
const settlementType = row.settlementType || this.form.settlementType || 'payable';
|
||
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
|
||
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
|
||
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
|
||
return;
|
||
}
|
||
if (column.prop === 'waybillNo') {
|
||
const waybillId = await resolveWaybillId(row);
|
||
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
|
||
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
|
||
}
|
||
},
|
||
resetDetailQuery() {
|
||
this.detailQuery = {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
};
|
||
this.appliedDetailQuery = { ...this.detailQuery };
|
||
this.detailPage.current = 1;
|
||
},
|
||
handleDetailPageSizeChange() {
|
||
this.detailPage.current = 1;
|
||
},
|
||
detailIndexMethod(index) {
|
||
return (this.detailPage.current - 1) * this.detailPage.size + index + 1;
|
||
},
|
||
normalizeAdjustRow(item, feeItemNames = []) {
|
||
const rawFeeItems =
|
||
item.feeItems && typeof item.feeItems === 'object'
|
||
? item.feeItems
|
||
: this.parseFeeItems(item.feeItemsJson);
|
||
const names = feeItemNames.length ? feeItemNames : Object.keys(rawFeeItems);
|
||
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: Object.fromEntries(names.map(name => [name, Number(rawFeeItems[name] || 0)])),
|
||
settlementAmountTax: Number(item.settlementAmountTax ?? item.afterAmount ?? 0),
|
||
settlementAmountNoTax: item.settlementAmountNoTax,
|
||
billingRule: this.normalizeBillingRule(item),
|
||
calculating: false,
|
||
calculateError: '',
|
||
adjustCalculateTimer: null,
|
||
adjustCalculateVersion: 0,
|
||
};
|
||
if (item.settlementAmountTax === undefined && item.afterAmount === undefined) {
|
||
this.recalculateAdjustRow(adjusted);
|
||
}
|
||
return adjusted;
|
||
},
|
||
cloneAdjustRows(rows) {
|
||
return (rows || []).map(row => ({
|
||
...row,
|
||
feeItems: { ...row.feeItems },
|
||
calculating: false,
|
||
calculateError: '',
|
||
adjustCalculateTimer: null,
|
||
adjustCalculateVersion: 0,
|
||
}));
|
||
},
|
||
async loadSourceFeeRows(sourceDetailId) {
|
||
const response = await getFeeDetail(sourceDetailId);
|
||
const data = response.data?.data || response.data || {};
|
||
return (data.records || []).map(item =>
|
||
this.normalizeAdjustRow(item, data.feeItemNames || [])
|
||
);
|
||
},
|
||
// 批量加载所有明细的费用明细数据,用于拼接货物名称和类型
|
||
async loadAllDetailFees() {
|
||
if (!this.details.length) return;
|
||
|
||
// 并发加载所有明细的费用数据
|
||
await Promise.all(
|
||
this.details.map(async detail => {
|
||
if (!detail.id) return;
|
||
|
||
try {
|
||
const response = await getDetailFees(detail.id);
|
||
const feeRows = response.data?.data || response.data || [];
|
||
|
||
// 缓存费用明细数据
|
||
if (Array.isArray(feeRows) && feeRows.length > 0) {
|
||
this.detailFeeCache[detail.id] = feeRows;
|
||
}
|
||
} catch (error) {
|
||
// 加载失败时使用明细本身的数据
|
||
console.warn(`加载明细 ${detail.id} 的费用数据失败:`, error);
|
||
}
|
||
})
|
||
);
|
||
},
|
||
// 明细列表只有汇总金额,费用项级别的原始金额需要按来源明细单独拉取并缓存为调整基准。
|
||
async ensureSourceFeeSnapshots() {
|
||
const targets = this.details.filter(row => {
|
||
const sourceDetailId = String(row.sourceDetailId || row.id || '');
|
||
return sourceDetailId && !this.sourceFeeSnapshots[sourceDetailId];
|
||
});
|
||
if (!targets.length) return;
|
||
await Promise.all(
|
||
targets.map(async row => {
|
||
const sourceDetailId = String(row.sourceDetailId || row.id || '');
|
||
try {
|
||
const feeRows = await this.loadSourceFeeRows(sourceDetailId);
|
||
if (!feeRows.length) return;
|
||
this.sourceFeeSnapshots[sourceDetailId] = feeRows;
|
||
this.applySnapshotToDetail(row, feeRows);
|
||
} catch (error) {
|
||
// 取不到费用明细时退回按明细汇总金额归集,后续操作会再次尝试拉取。
|
||
delete this.sourceFeeSnapshots[sourceDetailId];
|
||
}
|
||
})
|
||
);
|
||
},
|
||
applySnapshotToDetail(detail, feeRows) {
|
||
if (!feeRows.length) return;
|
||
const feeItems = {};
|
||
feeRows.forEach(row =>
|
||
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
|
||
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
|
||
})
|
||
);
|
||
const settlementAmount = feeRows.reduce(
|
||
(total, row) => total + Number(row.settlementAmountTax || 0),
|
||
0
|
||
);
|
||
const freightAmount = feeRows.reduce(
|
||
(total, row) => total + Number(row.freightAmount || 0),
|
||
0
|
||
);
|
||
Object.assign(detail, {
|
||
feeItemsJson: JSON.stringify(feeItems),
|
||
freightAmount: Number(freightAmount.toFixed(2)),
|
||
originalAmount: Number(settlementAmount.toFixed(2)),
|
||
adjustAmount: 0,
|
||
settlementAmountTax: Number(settlementAmount.toFixed(2)),
|
||
});
|
||
},
|
||
async openAdjustDialog(row, readonly) {
|
||
const detailRow = row;
|
||
this.adjustDialog.visible = true;
|
||
this.adjustDialog.loading = true;
|
||
this.adjustDialog.readonly = readonly;
|
||
this.adjustDialog.detailId = detailRow.id;
|
||
this.adjustDialog.detailLineNo = detailRow.lineNo;
|
||
this.adjustDialog.sourceDetailId =
|
||
detailRow.id && String(detailRow.id) === String(detailRow.sourceDetailId)
|
||
? detailRow.sourceDetailId
|
||
: '';
|
||
const sourceDetailId = String(this.adjustDialog.sourceDetailId || '');
|
||
const pending = sourceDetailId ? this.pendingAdjustments[sourceDetailId] : null;
|
||
// 底部备注回显结算明细备注;暂存过的调整优先取暂存备注
|
||
this.adjustDialog.reason =
|
||
pending?.reason !== undefined && pending?.reason !== null
|
||
? pending.reason
|
||
: detailRow.remark || '';
|
||
try {
|
||
// 已暂存的调整结果优先回显,避免重新打开时又回到调整前的数据。
|
||
if (pending?.rows?.length) {
|
||
this.adjustRows = this.cloneAdjustRows(pending.rows);
|
||
return;
|
||
}
|
||
if (this.publicMode) {
|
||
const cachedFees = this.detailFeeCache[detailRow.id] || [];
|
||
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
|
||
return;
|
||
}
|
||
if (sourceDetailId) {
|
||
if (!this.sourceFeeSnapshots[sourceDetailId]) {
|
||
this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId);
|
||
}
|
||
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]);
|
||
return;
|
||
}
|
||
const cachedFees = this.detailFeeCache[detailRow.id];
|
||
if (this.publicMode && Array.isArray(cachedFees)) {
|
||
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
|
||
return;
|
||
}
|
||
const response = await getDetailFees(detailRow.id);
|
||
const rows = response.data?.data || response.data || [];
|
||
this.adjustRows = rows.map(item => this.normalizeAdjustRow(item));
|
||
} finally {
|
||
this.adjustDialog.loading = false;
|
||
}
|
||
},
|
||
parseChangeRecordData(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object') return value;
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
},
|
||
formatAdjustChangeValue(value) {
|
||
if (value === undefined || value === null || value === '') return '空';
|
||
if (typeof value === 'object') return JSON.stringify(value);
|
||
return String(value);
|
||
},
|
||
buildAdjustChangeRecordRows(row = {}) {
|
||
const before = this.parseChangeRecordData(row.beforeData);
|
||
const after = this.parseChangeRecordData(row.afterData);
|
||
const fields = [...new Set([...Object.keys(before), ...Object.keys(after)])];
|
||
const rows = fields
|
||
.filter(field => JSON.stringify(before[field]) !== JSON.stringify(after[field]))
|
||
.map(field => ({
|
||
field,
|
||
before: this.formatAdjustChangeValue(before[field]),
|
||
after: this.formatAdjustChangeValue(after[field]),
|
||
}));
|
||
if (row.changeContent)
|
||
rows.unshift({ field: '变更内容', before: '空', after: row.changeContent });
|
||
if (row.changeReason) rows.push({ field: '变更原因', before: '空', after: row.changeReason });
|
||
return rows;
|
||
},
|
||
openAdjustChangeRecord(row) {
|
||
this.adjustChangeRecord = row;
|
||
this.adjustChangeRecordDetailRows = this.buildAdjustChangeRecordRows(row);
|
||
this.adjustChangeRecordVisible = true;
|
||
},
|
||
hasBillingRule(row) {
|
||
const rule = this.normalizeBillingRule(row);
|
||
return Boolean(
|
||
rule.feeItem ||
|
||
rule.billingElement ||
|
||
rule.billingType ||
|
||
rule.unitPrice ||
|
||
rule.rules?.length ||
|
||
rule.matchCondition?.origin ||
|
||
rule.matchCondition?.destination ||
|
||
rule.matchCondition?.cargoName ||
|
||
rule.matchCondition?.cargoType
|
||
);
|
||
},
|
||
openBillingRuleInfo(row) {
|
||
this.billingRuleDialog.rule = this.normalizeBillingRule(row);
|
||
this.billingRuleDialog.visible = true;
|
||
},
|
||
normalizeBillingRule(row) {
|
||
const source = row || {};
|
||
const nested = [
|
||
source.billingRule,
|
||
source.billingRuleInfo,
|
||
source.billingRuleJson,
|
||
source.billingRulesJson,
|
||
source.billingRules,
|
||
source.billingPlanRule,
|
||
source.feeRule,
|
||
source.rule,
|
||
].find(value => value !== undefined && value !== null && value !== '');
|
||
const parsedNested = this.parseJsonValue(nested);
|
||
const matchedRules = Array.isArray(parsedNested)
|
||
? parsedNested.filter(item => item && typeof item === 'object')
|
||
: [];
|
||
let rule = this.parseObject(matchedRules[0] || parsedNested || nested);
|
||
if (!Object.keys(rule).length && typeof nested === 'string') rule = { name: nested };
|
||
const matchCondition = this.parseObject(
|
||
source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition
|
||
);
|
||
return {
|
||
...rule,
|
||
feeItem: source.billingFeeItem || source.feeItem || rule.feeItem,
|
||
billingElement:
|
||
source.billingElement ||
|
||
source.billingFactor ||
|
||
rule.billingElement ||
|
||
rule.billingFactor,
|
||
billingType: source.billingType || rule.billingType,
|
||
billingUnit: source.billingUnit || source.priceUnit || rule.billingUnit || rule.priceUnit,
|
||
unitPrice: source.billingUnitPrice ?? source.unitPrice ?? rule.unitPrice,
|
||
minimumBillingWeight:
|
||
source.minimumBillingWeight ?? rule.minimumBillingWeight ?? rule.lowerLimit,
|
||
remark: source.billingRuleRemark || source.ruleRemark || rule.remark,
|
||
rules: matchedRules,
|
||
matchCondition: {
|
||
...matchCondition,
|
||
origin: matchCondition.origin || matchCondition.originName,
|
||
destination: matchCondition.destination || matchCondition.destinationName,
|
||
cargoName:
|
||
matchCondition.cargoName ||
|
||
matchCondition.cargoNameLabel ||
|
||
source.cargoName ||
|
||
rule.cargoName,
|
||
cargoType: matchCondition.cargoType || matchCondition.cargoTypeName,
|
||
},
|
||
};
|
||
},
|
||
billingRuleRanges(rule) {
|
||
const ranges = Array.isArray(rule?.limitRanges) ? rule.limitRanges : [];
|
||
if (ranges.length) {
|
||
return ranges
|
||
.map(
|
||
item => `${item.lowerLimit ?? '-'}~${item.upperLimit ?? '-'}:${item.unitPrice ?? '-'}`
|
||
)
|
||
.join(';');
|
||
}
|
||
if (rule?.lowerLimit !== undefined || rule?.upperLimit !== undefined) {
|
||
return `${rule.lowerLimit ?? '-'}~${rule.upperLimit ?? '-'}`;
|
||
}
|
||
return '-';
|
||
},
|
||
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.settlementAmountTax = Number(
|
||
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
|
||
);
|
||
if (changedField && this.adjustDialog.sourceDetailId && row.id) {
|
||
this.scheduleSourceAdjustCalculation(row);
|
||
}
|
||
},
|
||
scheduleSourceAdjustCalculation(row) {
|
||
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
|
||
row.adjustCalculateVersion += 1;
|
||
row.calculating = true;
|
||
const version = row.adjustCalculateVersion;
|
||
row.adjustCalculateTimer = setTimeout(async () => {
|
||
row.adjustCalculateTimer = null;
|
||
try {
|
||
const response = await calculateAdjustedFee({
|
||
detailId: this.adjustDialog.sourceDetailId,
|
||
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 = response.data?.data || response.data || {};
|
||
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.settlementAmountTax = Number(data.afterAmount || 0);
|
||
} catch (error) {
|
||
row.calculateError = error.message || '费用试算失败';
|
||
} finally {
|
||
if (version === row.adjustCalculateVersion) row.calculating = false;
|
||
}
|
||
}, 300);
|
||
},
|
||
isFreightFeeItem(name) {
|
||
return String(name || '').includes('运费') || String(name || '').includes('运输费');
|
||
},
|
||
async saveAdjustment() {
|
||
if (this.adjustRows.some(row => row.calculating)) {
|
||
this.$message.warning('费用正在重新计算,请稍候');
|
||
return;
|
||
}
|
||
this.adjustDialog.saving = true;
|
||
try {
|
||
if (this.adjustDialog.sourceDetailId) {
|
||
this.pendingAdjustments[this.adjustDialog.sourceDetailId] = {
|
||
reason: this.adjustDialog.reason,
|
||
rows: this.adjustRows.map(row => ({ ...row, feeItems: { ...row.feeItems } })),
|
||
};
|
||
this.applyLocalAdjustment(
|
||
this.adjustDialog.sourceDetailId,
|
||
this.adjustRows,
|
||
this.adjustDialog.reason
|
||
);
|
||
this.adjustDialog.visible = false;
|
||
this.$message.success('结算明细调整已暂存,保存预付结算单后生效');
|
||
return;
|
||
}
|
||
await adjustDetail({
|
||
detailId: this.adjustDialog.detailId,
|
||
changeReason: this.adjustDialog.reason,
|
||
rows: this.adjustRows.map(row => ({
|
||
id: row.id,
|
||
transportQuantity: row.transportQuantity,
|
||
mileage: row.mileage,
|
||
unitPrice: row.unitPrice,
|
||
freightAmount: row.freightAmount,
|
||
feeItems: row.feeItems,
|
||
settlementAmountTax: row.settlementAmountTax,
|
||
settlementAmountNoTax: row.settlementAmountNoTax,
|
||
})),
|
||
});
|
||
this.adjustDialog.visible = false;
|
||
this.$message.success('结算明细调整成功');
|
||
await this.loadDetail();
|
||
this.$emit('success', this.form.id);
|
||
} finally {
|
||
this.adjustDialog.saving = false;
|
||
}
|
||
},
|
||
applyLocalAdjustment(sourceDetailId, rows, detailRemark) {
|
||
const detail = this.details.find(
|
||
item => String(item.sourceDetailId || item.id) === String(sourceDetailId)
|
||
);
|
||
if (!detail) return;
|
||
// 原金额以进入预付结算单时的费用快照为基准,调整金额 = 结算金额 - 原金额。
|
||
const baseRows = this.sourceFeeSnapshots[String(sourceDetailId)] || [];
|
||
const originalAmount = baseRows.length
|
||
? baseRows.reduce((sum, row) => sum + Number(row.settlementAmountTax || 0), 0)
|
||
: rows.reduce((sum, row) => sum + Number(row.originalAmount || 0), 0);
|
||
const settlementAmountTax = rows.reduce(
|
||
(sum, row) => sum + Number(row.settlementAmountTax || 0),
|
||
0
|
||
);
|
||
const settlementAmountNoTax = rows.reduce(
|
||
(sum, row) => sum + Number(row.settlementAmountNoTax || 0),
|
||
0
|
||
);
|
||
const hasSettlementAmountNoTax = rows.some(
|
||
row => row.settlementAmountNoTax !== undefined && row.settlementAmountNoTax !== null
|
||
);
|
||
const freightAmount = rows.reduce((sum, row) => sum + Number(row.freightAmount || 0), 0);
|
||
const transportQuantity = rows.reduce(
|
||
(sum, row) => sum + Number(row.transportQuantity || 0),
|
||
0
|
||
);
|
||
const mileage = rows.reduce((sum, row) => sum + Number(row.mileage || 0), 0);
|
||
const feeItems = {};
|
||
rows.forEach(row =>
|
||
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
|
||
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
|
||
})
|
||
);
|
||
const firstRow = rows[0] || {};
|
||
Object.assign(detail, {
|
||
transportQuantity: Number(transportQuantity.toFixed(2)),
|
||
mileage: Number(mileage.toFixed(2)),
|
||
unitPrice: Number(firstRow.unitPrice || 0),
|
||
originalAmount: Number(originalAmount.toFixed(2)),
|
||
adjustAmount: Number((settlementAmountTax - originalAmount).toFixed(2)),
|
||
settlementAmountTax: Number(settlementAmountTax.toFixed(2)),
|
||
settlementAmountNoTax: hasSettlementAmountNoTax
|
||
? Number(settlementAmountNoTax.toFixed(2))
|
||
: detail.settlementAmountNoTax,
|
||
freightAmount: Number(freightAmount.toFixed(2)),
|
||
feeItemsJson: JSON.stringify(feeItems),
|
||
// 底部备注写入结算明细备注
|
||
remark: detailRemark ?? '',
|
||
});
|
||
// 暂存调整后同步费用缓存,保证货物名称/类型拼接与列表展示一致。
|
||
if (detail.id) {
|
||
this.detailFeeCache[detail.id] = rows.map(row => ({
|
||
...row,
|
||
feeItems: { ...row.feeItems },
|
||
}));
|
||
}
|
||
this.buildSummaryFeesFromDetails();
|
||
},
|
||
async persistPendingAdjustments() {
|
||
const pendingEntries = Object.entries(this.pendingAdjustments);
|
||
if (!pendingEntries.length || !this.form.id) return;
|
||
for (const [sourceDetailId, pending] of pendingEntries) {
|
||
const detail = this.details.find(
|
||
item => String(item.sourceDetailId || '') === String(sourceDetailId)
|
||
);
|
||
if (!detail) continue;
|
||
const persistedFees = await getDetailFees(detail.id);
|
||
const feeRows = persistedFees.data?.data || persistedFees.data || [];
|
||
const pendingRows = pending.rows || [];
|
||
const rows = feeRows.map(fee => {
|
||
const pendingRow = this.findPendingAdjustmentRow(fee, pendingRows);
|
||
if (!pendingRow) {
|
||
throw new Error(`费用行“${fee.cargoName || fee.lineNo || fee.id}”保存匹配失败`);
|
||
}
|
||
return {
|
||
id: fee.id,
|
||
transportQuantity: pendingRow.transportQuantity,
|
||
mileage: pendingRow.mileage,
|
||
unitPrice: pendingRow.unitPrice,
|
||
freightAmount: pendingRow.freightAmount,
|
||
feeItems: pendingRow.feeItems,
|
||
settlementAmountTax: pendingRow.settlementAmountTax,
|
||
settlementAmountNoTax: pendingRow.settlementAmountNoTax,
|
||
};
|
||
});
|
||
if (rows.length) {
|
||
await adjustDetail({
|
||
detailId: detail.id,
|
||
changeReason: pending.reason,
|
||
rows,
|
||
});
|
||
}
|
||
}
|
||
this.pendingAdjustments = {};
|
||
await this.loadDetail();
|
||
},
|
||
findPendingAdjustmentRow(fee, pendingRows) {
|
||
const exactIdMatch = pendingRows.find(
|
||
row => row.id && fee.sourceFeeId && String(row.id) === String(fee.sourceFeeId)
|
||
);
|
||
if (exactIdMatch) return exactIdMatch;
|
||
const exactLineMatch = pendingRows.find(
|
||
row =>
|
||
row.lineNo &&
|
||
fee.lineNo &&
|
||
String(row.lineNo) === String(fee.lineNo) &&
|
||
String(row.cargoName || '') === String(fee.cargoName || '') &&
|
||
String(row.cargoType || '') === String(fee.cargoType || '')
|
||
);
|
||
if (exactLineMatch) return exactLineMatch;
|
||
const cargoMatches = pendingRows.filter(
|
||
row =>
|
||
String(row.cargoName || '') === String(fee.cargoName || '') &&
|
||
String(row.cargoType || '') === String(fee.cargoType || '')
|
||
);
|
||
return cargoMatches.length === 1 ? cargoMatches[0] : null;
|
||
},
|
||
exportDetails() {
|
||
if (!this.filteredDetails.length) {
|
||
this.$message.warning('暂无可导出的结算明细');
|
||
return;
|
||
}
|
||
const dynamicFeeItems = this.detailFeeItemNames;
|
||
const rows = this.filteredDetails.map((row, index) => {
|
||
const feeItems = this.parseFeeItems(row.feeItemsJson);
|
||
const result = {
|
||
序号: index + 1,
|
||
单据号: row.documentNo,
|
||
运单号: row.waybillNo,
|
||
车号: row.vehicleNo,
|
||
发货地址: row.departureAddress,
|
||
到货地址: row.arrivalAddress,
|
||
实际发货时间: row.actualDepartureTime,
|
||
实际完成时间: row.actualCompletionTime,
|
||
运输类型: row.transportType,
|
||
货物名称: row.cargoName,
|
||
货物类型: row.cargoType,
|
||
运输总量: this.formatQuantity(row),
|
||
'里程(KM)': row.mileage,
|
||
批次号: row.batchNo,
|
||
运输单价: this.formatMoney(row.unitPrice, row.currency || this.form.currency),
|
||
运费: this.formatMoney(row.freightAmount, row.currency || this.form.currency),
|
||
};
|
||
dynamicFeeItems.forEach(name => {
|
||
result[name] = this.formatMoney(feeItems[name], row.currency || this.form.currency);
|
||
});
|
||
result['结算金额(含税)'] = this.formatMoney(
|
||
row.settlementAmountTax,
|
||
row.currency || this.form.currency
|
||
);
|
||
result['结算金额(不含税)'] = this.formatMoney(
|
||
row.settlementAmountNoTax,
|
||
row.currency || this.form.currency
|
||
);
|
||
result.备注 = row.remark;
|
||
return result;
|
||
});
|
||
const sheet = XLSX.utils.json_to_sheet(rows);
|
||
const workbook = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(workbook, sheet, '结算明细');
|
||
XLSX.writeFile(
|
||
workbook,
|
||
`${this.form.preSettlementNo || '预付结算单'}结算明细${this.$dayjs().format(
|
||
'YYYY-MM-DD HH-mm-ss'
|
||
)}.xlsx`
|
||
);
|
||
},
|
||
handleAttachmentChange(list) {
|
||
const uploadUserName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
const existingRows = this.attachments || [];
|
||
this.attachments = (list || []).map(file => {
|
||
const fileUrl = this.attachmentUrl(file);
|
||
const existing = existingRows.find(item => {
|
||
const sameUid = file.uid && item.uid && String(file.uid) === String(item.uid);
|
||
const sameUrl = fileUrl && this.attachmentUrl(item) === fileUrl;
|
||
return sameUid || sameUrl;
|
||
});
|
||
const originalName = this.attachmentName(file);
|
||
return {
|
||
...file,
|
||
type: existing?.type || file.type || this.resolveAttachmentType(originalName),
|
||
description: existing?.description || file.description || '',
|
||
uploadUserName: existing?.uploadUserName || file.uploadUserName || uploadUserName,
|
||
uploadTime: existing?.uploadTime || file.uploadTime || uploadTime,
|
||
};
|
||
});
|
||
this.sortAttachments();
|
||
this.selectedAttachmentRows = [];
|
||
},
|
||
async loadAttachmentTypeOptions() {
|
||
try {
|
||
const response = await getDictionary({ code: 'pre_settlement_attachment_type' });
|
||
const data = response?.data?.data || [];
|
||
this.attachmentTypeOptions = data
|
||
.map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictValue,
|
||
}))
|
||
.filter(item => item.label && item.value);
|
||
} catch (error) {
|
||
this.attachmentTypeOptions = [];
|
||
}
|
||
this.sortAttachments();
|
||
},
|
||
resolveAttachmentType(fileName) {
|
||
const normalizedName = String(fileName || '').toLocaleLowerCase();
|
||
const matchedType = [...this.attachmentTypeOptions]
|
||
.filter(item => item?.label || item?.value)
|
||
.sort(
|
||
(a, b) =>
|
||
String(b.label || b.value || '').length - String(a.label || a.value || '').length
|
||
)
|
||
.find(item => {
|
||
const typeText = String(item.label || item.value || '').toLocaleLowerCase();
|
||
return typeText && normalizedName.includes(typeText);
|
||
});
|
||
if (matchedType?.value) return matchedType.value;
|
||
const otherType = this.attachmentTypeOptions.find(item => this.isOtherAttachmentType(item));
|
||
if (otherType?.value) return otherType.value;
|
||
return '';
|
||
},
|
||
isOtherAttachmentType(item) {
|
||
const typeText = String(item?.label || item?.value || '').trim();
|
||
return (
|
||
typeText === '其他' ||
|
||
typeText === '其它' ||
|
||
typeText === '其他附件' ||
|
||
typeText === '其它附件'
|
||
);
|
||
},
|
||
getAttachmentTypeOrder(type) {
|
||
const normalizedType = String(type || '').trim();
|
||
const index = this.attachmentTypeOptions.findIndex(
|
||
item =>
|
||
String(item.value || '').trim() === normalizedType ||
|
||
String(item.label || '').trim() === normalizedType
|
||
);
|
||
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
||
},
|
||
sortAttachments() {
|
||
this.attachments = (this.attachments || [])
|
||
.map((file, index) => ({ file, index }))
|
||
.sort((a, b) => {
|
||
const orderDifference =
|
||
this.getAttachmentTypeOrder(a.file.type) - this.getAttachmentTypeOrder(b.file.type);
|
||
return orderDifference || a.index - b.index;
|
||
})
|
||
.map(item => item.file);
|
||
},
|
||
attachmentTypeName(type) {
|
||
const option = this.attachmentTypeOptions.find(
|
||
item => String(item.value) === String(type) || String(item.label) === String(type)
|
||
);
|
||
return option?.label || type || '-';
|
||
},
|
||
handleAttachmentSelectionChange(rows) {
|
||
this.selectedAttachmentRows = rows || [];
|
||
},
|
||
attachmentUrl(file = {}) {
|
||
return file.url || file.link || file.fileUrl || file.domain || '';
|
||
},
|
||
attachmentName(file = {}) {
|
||
return file.originalName || file.name || file.fileName || '附件';
|
||
},
|
||
formatFileSize(size) {
|
||
const bytes = Number(size);
|
||
if (!bytes) return '-';
|
||
if (bytes < 1024) return `${bytes}B`;
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
||
},
|
||
previewAttachment(file) {
|
||
this.$refs.attachmentUploadRef?.handlePreview(file);
|
||
},
|
||
downloadAttachment(file) {
|
||
const url = this.attachmentUrl(file);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, this.attachmentName(file));
|
||
},
|
||
downloadAttachments() {
|
||
const files = this.selectedAttachmentRows.length
|
||
? this.selectedAttachmentRows
|
||
: this.attachments;
|
||
files.forEach((file, index) => {
|
||
window.setTimeout(() => this.downloadAttachment(file), index * 200);
|
||
});
|
||
},
|
||
removeAttachment(index) {
|
||
this.attachments.splice(index, 1);
|
||
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(file =>
|
||
this.attachments.includes(file)
|
||
);
|
||
},
|
||
parseAttachments(value) {
|
||
if (!value) return [];
|
||
let list = [];
|
||
if (Array.isArray(value)) {
|
||
list = value;
|
||
} else {
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
list = Array.isArray(parsed) ? parsed : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
}
|
||
return list.map(file => {
|
||
const originalName = this.attachmentName(file);
|
||
return {
|
||
...file,
|
||
type: file?.type || this.resolveAttachmentType(originalName),
|
||
description: file?.description || '',
|
||
};
|
||
});
|
||
},
|
||
parseFeeItems(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object') return { ...value };
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
},
|
||
parseObject(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
|
||
if (typeof value !== 'string') return {};
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
},
|
||
parseJsonValue(value) {
|
||
if (typeof value !== 'string') return value;
|
||
try {
|
||
return JSON.parse(value);
|
||
} catch (error) {
|
||
return value;
|
||
}
|
||
},
|
||
formatMoney(value, currency = 'RMB') {
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
const amount = Number(value);
|
||
if (!Number.isFinite(amount)) return '-';
|
||
return `${amount.toFixed(2)} ${currency || 'RMB'}`;
|
||
},
|
||
formatCreateDate(value) {
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
const parsed = this.$dayjs(value);
|
||
return parsed.isValid() ? parsed.format('YYYY-MM-DD') : this.displayValue(value);
|
||
},
|
||
formatQuantity(row) {
|
||
const value = row.transportQuantity;
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
const quantity = Number(value);
|
||
if (!Number.isFinite(quantity)) return '-';
|
||
return `${quantity.toFixed(2)}${row.quantityUnit ? ` ${row.quantityUnit}` : ''}`;
|
||
},
|
||
// 拼接同一运单的货物名称
|
||
formatCargoNames(row) {
|
||
const detailId = row.id;
|
||
const feeRows = this.detailFeeCache[detailId] || [];
|
||
|
||
// 如果没有缓存数据或只有一条,返回原始值
|
||
if (feeRows.length <= 1) {
|
||
return this.displayValue(row.cargoName);
|
||
}
|
||
|
||
// 收集所有货物名称并去重
|
||
const names = feeRows
|
||
.map(item => item.cargoName)
|
||
.filter(name => name && String(name).trim())
|
||
.filter((name, index, arr) => arr.indexOf(name) === index);
|
||
|
||
return names.length > 0 ? names.join('、') : this.displayValue(row.cargoName);
|
||
},
|
||
// 拼接同一运单的货物类型
|
||
formatCargoTypes(row) {
|
||
const detailId = row.id;
|
||
const feeRows = this.detailFeeCache[detailId] || [];
|
||
|
||
// 如果没有缓存数据或只有一条,返回原始值
|
||
if (feeRows.length <= 1) {
|
||
return this.displayValue(row.cargoType);
|
||
}
|
||
|
||
// 收集所有货物类型并去重
|
||
const types = feeRows
|
||
.map(item => item.cargoType)
|
||
.filter(type => type && String(type).trim())
|
||
.filter((type, index, arr) => arr.indexOf(type) === index);
|
||
|
||
return types.length > 0 ? types.join('、') : this.displayValue(row.cargoType);
|
||
},
|
||
// 格式化运输单价,如果多个货物的单价不一样则显示'-'
|
||
formatUnitPrice(row) {
|
||
const detailId = row.id;
|
||
const feeRows = this.detailFeeCache[detailId] || [];
|
||
|
||
// 如果没有缓存数据或只有一条,返回原始值
|
||
if (feeRows.length <= 1) {
|
||
return this.formatMoney(row.unitPrice, row.currency || this.form.currency);
|
||
}
|
||
|
||
// 检查所有货物的运输单价是否一致
|
||
const prices = feeRows
|
||
.map(item => Number(item.unitPrice || 0))
|
||
.filter((price, index, arr) => arr.indexOf(price) === index);
|
||
|
||
// 如果单价不一致,返回'-'
|
||
if (prices.length > 1) {
|
||
return '-';
|
||
}
|
||
|
||
// 单价一致,返回该单价
|
||
return this.formatMoney(prices[0], row.currency || this.form.currency);
|
||
},
|
||
displayValue(value) {
|
||
return value === undefined || value === null || value === '' ? '-' : value;
|
||
},
|
||
advanceStatusName(status) {
|
||
return settlementStatusName(status, this.displayValue);
|
||
},
|
||
advanceCreateUserName(row) {
|
||
return this.displayValue(
|
||
row?.createUserName || row?.advanceCreateUserName || row?.applyUserName
|
||
);
|
||
},
|
||
advanceCreateTime(row) {
|
||
return this.displayValue(row?.createTime || row?.advanceCreateTime || row?.applyTime);
|
||
},
|
||
changeRecordLineNo(row) {
|
||
if (row.changeType !== '结算明细项') return '';
|
||
if (row.lineNo !== undefined && row.lineNo !== null && row.lineNo !== '') return row.lineNo;
|
||
return '';
|
||
},
|
||
isSummaryMoney(prop) {
|
||
return ['originalAmount', 'adjustAmount', 'settlementAmount'].includes(prop);
|
||
},
|
||
moneyCurrency(prop) {
|
||
return prop === 'localSettlementAmount' ? this.form.localCurrency : this.form.currency;
|
||
},
|
||
handleClosed() {
|
||
this.resetEditor();
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.pre-settlement-editor {
|
||
&__content {
|
||
max-height: 82vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
&--page &__content {
|
||
max-height: none;
|
||
padding-bottom: 72px;
|
||
overflow: visible;
|
||
}
|
||
|
||
&__page-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 24px;
|
||
border-top: 1px solid #eff1f7;
|
||
position: fixed;
|
||
right: 0;
|
||
left: 230px;
|
||
bottom: 0;
|
||
margin: 0;
|
||
z-index: 10;
|
||
background: #fff;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
&__form {
|
||
:deep(.el-select),
|
||
:deep(.el-date-editor),
|
||
:deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__section-actions,
|
||
&__links {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__detail-filter {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||
align-items: center;
|
||
gap: 8px 16px;
|
||
margin-bottom: 12px;
|
||
padding: 12px 12px 4px;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
|
||
:deep(.el-form-item) {
|
||
display: flex;
|
||
min-width: 0;
|
||
margin-right: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-form-item__content) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.el-input) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__detail-filter-actions {
|
||
width: auto;
|
||
margin-left: 0;
|
||
white-space: nowrap;
|
||
justify-self: end;
|
||
|
||
:deep(.el-form-item__content) {
|
||
flex: none;
|
||
justify-content: flex-end;
|
||
flex-wrap: nowrap;
|
||
}
|
||
}
|
||
|
||
&__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%;
|
||
}
|
||
}
|
||
|
||
&__contract-filter-actions {
|
||
grid-column: 1 / -1;
|
||
width: 100%;
|
||
justify-content: flex-end;
|
||
|
||
:deep(.el-form-item__content) {
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
&__attachment-upload {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
margin-top: 12px;
|
||
|
||
:deep(.vehicle-attachment-upload) {
|
||
width: auto;
|
||
}
|
||
|
||
:deep(.el-upload) {
|
||
display: inline-flex;
|
||
}
|
||
}
|
||
|
||
&__negative-amount {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
:deep(.pre-settlement-editor__negative-amount .el-input__inner) {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
&__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
&__adjust-reason {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
&__billing-rule-cell {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
max-width: 100%;
|
||
}
|
||
|
||
&__billing-rule-icon {
|
||
flex: 0 0 auto;
|
||
color: var(--el-color-primary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
&__billing-rule-detail {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
&__billing-rule-match-title {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.form-readonly {
|
||
min-height: 32px;
|
||
line-height: 32px;
|
||
color: #303133;
|
||
}
|
||
|
||
.form-readonly--code {
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
.dialog-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 22px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
|
||
&::before {
|
||
width: 4px;
|
||
height: 18px;
|
||
margin-right: 8px;
|
||
background: #409eff;
|
||
content: '';
|
||
}
|
||
}
|
||
|
||
:deep(.pre-settlement-editor .el-dialog__body) {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
:deep(.pre-settlement-editor .el-form-item) {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
:deep(.pre-settlement-editor .el-table),
|
||
:deep(.pre-settlement-candidate-dialog .el-table) {
|
||
--el-table-border-color: #eff1f7;
|
||
}
|
||
|
||
:deep(.pre-settlement-editor .el-table__body tr:nth-child(even) > td.el-table__cell),
|
||
:deep(.pre-settlement-candidate-dialog .el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||
background: #fafafa;
|
||
}
|
||
:global(.avue--collapse .pre-settlement-editor__page-actions) {
|
||
left: 60px;
|
||
}
|
||
:global(.avue-layout--horizontal .pre-settlement-editor__page-actions) {
|
||
left: 0;
|
||
}
|
||
</style>
|