1663 lines
55 KiB
Vue
1663 lines
55 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 formFields" :key="field.prop" :span="field.span || 6">
|
||
<el-form-item :label="field.label" :prop="field.prop">
|
||
<el-select
|
||
v-if="field.type === 'contract'"
|
||
v-model="form.contractId"
|
||
filterable
|
||
remote
|
||
clearable
|
||
:remote-method="loadContractOptions"
|
||
:loading="contractLoading"
|
||
:disabled="!editable || details.length > 0"
|
||
placeholder="请选择合同"
|
||
@change="handleContractChange"
|
||
>
|
||
<el-option
|
||
v-for="item in contractOptions"
|
||
:key="item.id"
|
||
:label="`${item.contractName}(${item.contractNo || '-'})`"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
<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 || form.currency === 'RMB'"
|
||
placeholder="请选择"
|
||
@change="recalculateLocalAmount"
|
||
/>
|
||
<el-input-number
|
||
v-else-if="field.type === 'number'"
|
||
v-model="form[field.prop]"
|
||
:min="0"
|
||
:precision="6"
|
||
:controls="false"
|
||
:disabled="!editable || form.currency === 'RMB'"
|
||
@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.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>
|
||
<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 feeOptions"
|
||
:key="item.feeType"
|
||
:label="item.feeTypeName || item.feeType"
|
||
:value="item.feeType"
|
||
/>
|
||
</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'"
|
||
v-model="row.adjustAmount"
|
||
: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)">
|
||
{{ 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>
|
||
<div class="pre-settlement-editor__summary-total">
|
||
合计:{{ formatMoney(summaryTotal, form.currency) }}
|
||
</div>
|
||
</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"
|
||
inline
|
||
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="filteredDetails" border class="pre-settlement-editor__detail-table">
|
||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||
<el-table-column
|
||
v-for="column in visibleDetailColumns"
|
||
:key="column.prop"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<el-link
|
||
v-if="column.link && row[column.prop]"
|
||
type="primary"
|
||
@click="openAdjustDialog(row, true)"
|
||
>
|
||
{{ row[column.prop] }}
|
||
</el-link>
|
||
<span v-else-if="column.money">
|
||
{{ formatMoney(row[column.prop], row.currency || form.currency) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'transportQuantityText'">
|
||
{{ formatQuantity(row) }}
|
||
</span>
|
||
<span v-else-if="pageMode && column.prop === 'mileage'">
|
||
{{ formatDetailMileage(row[column.prop]) }}
|
||
</span>
|
||
<span v-else-if="pageMode && column.prop === 'transportType'">
|
||
{{ transportTypeName(row[column.prop]) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-for="feeItem in detailFeeItemNames"
|
||
:key="`fee-${feeItem}`"
|
||
:label="feeItem"
|
||
min-width="130"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
{{
|
||
formatMoney(
|
||
parseFeeItems(row.feeItemsJson)[feeItem],
|
||
row.currency || form.currency
|
||
)
|
||
}}
|
||
</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>
|
||
</section-card>
|
||
|
||
<section-card title="附件">
|
||
<div class="pre-settlement-editor__attachment">
|
||
<div class="pre-settlement-editor__attachment-actions">
|
||
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
|
||
批量下载
|
||
</el-button>
|
||
</div>
|
||
<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="260" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="previewAttachment(row)">
|
||
{{ attachmentName(row) }}
|
||
</el-link>
|
||
</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>
|
||
|
||
<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>{{ 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" 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="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"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="里程(KM)" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.mileage"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="adjustDialog.readonly"
|
||
/>
|
||
</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"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="运费" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.freightAmount"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
:disabled="adjustDialog.readonly"
|
||
@change="recalculateAdjustRow(row, 'freight')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-for="feeItem in adjustFeeItemNames"
|
||
:key="feeItem"
|
||
:label="feeItem"
|
||
min-width="140"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
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-column label="备注" min-width="200" align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.remark" maxlength="200" :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" maxlength="200" show-word-limit />
|
||
</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>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex';
|
||
import {
|
||
adjustDetail,
|
||
getCandidateDetails,
|
||
getContractOptions,
|
||
getDetail,
|
||
getDetailFees,
|
||
getFeeOptions,
|
||
removeDetail,
|
||
save,
|
||
submit,
|
||
} from '@/api/settlement/preSettlement';
|
||
import { getDictionary } from '@/api/system/dictbiz';
|
||
import {
|
||
emptyPreSettlementForm,
|
||
preSettlementFormFields,
|
||
preSettlementFormRules,
|
||
} from '@/option/settlement/preSettlementForm';
|
||
import {
|
||
advanceColumns,
|
||
candidateDetailColumns,
|
||
changeRecordColumns,
|
||
settlementDetailColumns,
|
||
settlementSummaryColumns,
|
||
} from '@/option/settlement/preSettlementTable';
|
||
import { downloadFileByUrl } from '@/utils/util';
|
||
import * as XLSX from 'xlsx';
|
||
|
||
export default {
|
||
name: 'PreSettlementEditor',
|
||
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,
|
||
},
|
||
},
|
||
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,
|
||
contractLoading: false,
|
||
contractOptions: [],
|
||
feeOptions: [],
|
||
feeCategoryOptions: [],
|
||
transportTypeOptions: [],
|
||
summaryFees: [],
|
||
details: [],
|
||
detailCollapsed: false,
|
||
detailQuery: {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
},
|
||
appliedDetailQuery: {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
},
|
||
advances: [],
|
||
changeRecords: [],
|
||
attachments: [],
|
||
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: '',
|
||
reason: '',
|
||
},
|
||
adjustRows: [],
|
||
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() {
|
||
const authority = this.userInfo?.authority;
|
||
return Array.isArray(authority)
|
||
? authority.includes('admin')
|
||
: String(authority || '').includes('admin');
|
||
},
|
||
canAdjustDetail() {
|
||
return this.editable && this.hasPermission('pre_settlement_adjust');
|
||
},
|
||
dialogTitle() {
|
||
if (this.readonly) return '查看预结算单';
|
||
return this.form.id ? '编辑预结算单' : '新增预结算单';
|
||
},
|
||
settlementTypeName() {
|
||
return this.form.settlementType === 'receivable' ? '应收' : '应付';
|
||
},
|
||
summaryTotal() {
|
||
if (this.summaryFees.length) {
|
||
return this.summaryFees.reduce(
|
||
(total, row) => total + Number(row.settlementAmount || 0),
|
||
0
|
||
);
|
||
}
|
||
return this.details.reduce(
|
||
(total, row) =>
|
||
total +
|
||
Number(
|
||
row.settlementAmountTax ??
|
||
row.totalAmount ??
|
||
row.afterAmount ??
|
||
row.settlementAmount ??
|
||
0
|
||
),
|
||
0
|
||
);
|
||
},
|
||
visibleDetailColumns() {
|
||
if (!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);
|
||
},
|
||
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());
|
||
})
|
||
);
|
||
},
|
||
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();
|
||
},
|
||
},
|
||
methods: {
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||
},
|
||
async initialize() {
|
||
this.resetEditor();
|
||
await this.loadContractOptions('');
|
||
await this.loadFeeOptions();
|
||
await this.loadFeeCategoryOptions();
|
||
if (this.pageMode) await this.loadTransportTypeOptions();
|
||
if (this.recordId) await this.loadDetail();
|
||
else if (this.initialData) this.applyInitialData();
|
||
},
|
||
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 || '';
|
||
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: settlementType === 'receivable' ? first.payeeName : first.payerName,
|
||
partyB: settlementType === 'receivable' ? first.payerName : first.payeeName,
|
||
});
|
||
}
|
||
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: first.payerName || this.form.payerName,
|
||
payeeName: first.payeeName || this.form.payeeName,
|
||
settlementType,
|
||
currency: first.currency || 'RMB',
|
||
localCurrency: first.localCurrency || 'RMB',
|
||
});
|
||
this.details = rows.map(row => ({
|
||
...row,
|
||
sourceDetailId: row.sourceDetailId || row.id,
|
||
settlementAmountTax:
|
||
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
|
||
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
|
||
}));
|
||
this.buildSummaryFeesFromDetails();
|
||
},
|
||
resetEditor() {
|
||
this.form = emptyPreSettlementForm();
|
||
this.summaryFees = [];
|
||
this.details = [];
|
||
this.detailCollapsed = false;
|
||
this.detailQuery = {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
};
|
||
this.appliedDetailQuery = { ...this.detailQuery };
|
||
this.advances = [];
|
||
this.changeRecords = [];
|
||
this.attachments = [];
|
||
this.selectedAttachmentRows = [];
|
||
this.$nextTick(() => this.$refs.formRef?.clearValidate());
|
||
},
|
||
async loadDetail() {
|
||
this.loading = true;
|
||
try {
|
||
const { data } = await getDetail(this.form.id || this.recordId);
|
||
const detail = data?.data || {};
|
||
this.form = { ...emptyPreSettlementForm(), ...detail };
|
||
this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
|
||
this.details = (detail.details || []).map(row => ({ ...row }));
|
||
this.advances = detail.advances || [];
|
||
this.changeRecords = detail.changeRecords || [];
|
||
this.attachments = this.parseAttachments(detail.attachmentsJson);
|
||
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,
|
||
});
|
||
}
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
async loadContractOptions(keyword = '') {
|
||
this.contractLoading = true;
|
||
try {
|
||
const { data } = await getContractOptions(keyword);
|
||
this.contractOptions = data?.data || [];
|
||
} finally {
|
||
this.contractLoading = false;
|
||
}
|
||
},
|
||
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' });
|
||
this.transportTypeOptions = data?.data || [];
|
||
},
|
||
handleContractChange(id) {
|
||
const contract = this.contractOptions.find(item => String(item.id) === String(id));
|
||
if (!contract) {
|
||
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 = [];
|
||
return;
|
||
}
|
||
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';
|
||
if (this.form.settlementType === 'receivable') {
|
||
this.form.payerName = contract.partyB;
|
||
this.form.payeeName = contract.partyA;
|
||
} else {
|
||
this.form.payerName = contract.partyA;
|
||
this.form.payeeName = contract.partyB;
|
||
}
|
||
this.summaryFees = [];
|
||
},
|
||
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;
|
||
}
|
||
const stateKey = shouldSubmit ? 'submitting' : 'saving';
|
||
this[stateKey] = true;
|
||
try {
|
||
const { data } = await save(this.buildSavePayload());
|
||
this.form.id = data?.data || this.form.id;
|
||
if (shouldSubmit) {
|
||
await submit({ id: this.form.id });
|
||
this.$message.success('审批流程已发起');
|
||
this.visible = false;
|
||
this.$emit('success', this.form.id);
|
||
return;
|
||
}
|
||
this.$message.success('保存成功');
|
||
await this.loadDetail();
|
||
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: row.feeType || '',
|
||
feeItem: row.feeItem,
|
||
originalAmount: Number(row.originalAmount || 0),
|
||
adjustAmount: Number(row.adjustAmount || 0),
|
||
remark: row.remark,
|
||
manualFlag: row.manualFlag || 0,
|
||
})),
|
||
};
|
||
},
|
||
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 => item.feeType === feeType)?.feeItems || [];
|
||
},
|
||
feeCategoryName(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
const option = this.feeCategoryOptions.find(
|
||
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
||
);
|
||
return option?.dictValue || 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) {
|
||
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
|
||
},
|
||
buildSummaryFeesFromDetails() {
|
||
const summaryMap = new Map();
|
||
const defaultFreightItem =
|
||
this.feeOptions
|
||
.flatMap(item => item.feeItems || [])
|
||
.find(name => this.isFreightFeeItem(name)) || '运输费';
|
||
const appendSummary = (feeItem, value, fallbackFeeType = '') => {
|
||
const amount = Number(value || 0);
|
||
if (!feeItem || !Number.isFinite(amount) || Math.abs(amount) < 0.005) return;
|
||
const feeOption = this.feeOptions.find(item =>
|
||
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
||
);
|
||
const feeType = feeOption?.feeType || fallbackFeeType || '';
|
||
const key = `${feeType}\u0000${feeItem}`;
|
||
const current = summaryMap.get(key) || {
|
||
id: '',
|
||
feeType,
|
||
feeItem,
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
settlementAmount: 0,
|
||
remark: '',
|
||
manualFlag: 0,
|
||
};
|
||
current.originalAmount = Number((current.originalAmount + amount).toFixed(2));
|
||
current.settlementAmount = current.originalAmount;
|
||
summaryMap.set(key, current);
|
||
};
|
||
|
||
this.details.forEach(row => {
|
||
const feeItems = this.parseFeeItems(row.feeItemsJson || row.feeItems);
|
||
const feeItemEntries = Object.entries(feeItems);
|
||
feeItemEntries.forEach(([feeItem, amount]) =>
|
||
appendSummary(feeItem, amount, row.feeType)
|
||
);
|
||
|
||
let knownAmount = feeItemEntries.reduce(
|
||
(total, [, amount]) => total + Number(amount || 0),
|
||
0
|
||
);
|
||
if (!feeItemEntries.some(([feeItem]) => this.isFreightFeeItem(feeItem))) {
|
||
const freightAmount = Number(row.freightAmount || 0);
|
||
appendSummary(defaultFreightItem, freightAmount, row.feeType);
|
||
knownAmount += freightAmount;
|
||
}
|
||
if (!feeItemEntries.some(([feeItem]) => !this.isFreightFeeItem(feeItem))) {
|
||
const otherFeeAmount = Number(row.otherFeeAmount || 0);
|
||
appendSummary('其他费用', otherFeeAmount, row.feeType);
|
||
knownAmount += otherFeeAmount;
|
||
}
|
||
|
||
const totalAmount = Number(
|
||
row.settlementAmountTax ??
|
||
row.totalAmount ??
|
||
row.afterAmount ??
|
||
row.settlementAmount ??
|
||
0
|
||
);
|
||
const residualAmount = Number((totalAmount - knownAmount).toFixed(2));
|
||
if (Math.abs(residualAmount) >= 0.005) {
|
||
appendSummary('其他费用', residualAmount, row.feeType);
|
||
}
|
||
});
|
||
|
||
const manualFees = this.summaryFees.filter(row => row.manualFlag === 1);
|
||
this.summaryFees = [...summaryMap.values(), ...manualFees];
|
||
this.form.settlementAmount = this.summaryFees
|
||
.reduce((total, row) => total + Number(row.settlementAmount || 0), 0)
|
||
.toFixed(2);
|
||
this.recalculateLocalAmount();
|
||
},
|
||
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({ ...row, sourceDetailId: row.id });
|
||
}
|
||
});
|
||
this.candidateDialog.confirming = true;
|
||
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 };
|
||
},
|
||
resetDetailQuery() {
|
||
this.detailQuery = {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
};
|
||
this.appliedDetailQuery = { ...this.detailQuery };
|
||
},
|
||
async openAdjustDialog(row, readonly) {
|
||
if (!row.id || row.id === row.sourceDetailId) {
|
||
this.$message.warning('请先保存预结算单');
|
||
return;
|
||
}
|
||
this.adjustDialog.visible = true;
|
||
this.adjustDialog.loading = true;
|
||
this.adjustDialog.readonly = readonly;
|
||
this.adjustDialog.detailId = row.id;
|
||
this.adjustDialog.reason = '';
|
||
try {
|
||
const { data } = await getDetailFees(row.id);
|
||
this.adjustRows = (data?.data || []).map(item => ({
|
||
...item,
|
||
feeItems: this.parseFeeItems(item.feeItemsJson),
|
||
}));
|
||
} finally {
|
||
this.adjustDialog.loading = false;
|
||
}
|
||
},
|
||
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)
|
||
);
|
||
},
|
||
isFreightFeeItem(name) {
|
||
return String(name || '').includes('运费') || String(name || '').includes('运输费');
|
||
},
|
||
async saveAdjustment() {
|
||
if (!this.adjustDialog.reason.trim()) {
|
||
this.$message.warning('请输入调整原因');
|
||
return;
|
||
}
|
||
this.adjustDialog.saving = true;
|
||
try {
|
||
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,
|
||
remark: row.remark,
|
||
})),
|
||
});
|
||
this.adjustDialog.visible = false;
|
||
this.$message.success('结算明细调整成功');
|
||
await this.loadDetail();
|
||
this.$emit('success', this.form.id);
|
||
} finally {
|
||
this.adjustDialog.saving = false;
|
||
}
|
||
},
|
||
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');
|
||
this.attachments = (list || []).map(file => ({
|
||
...file,
|
||
uploadUserName: file.uploadUserName || uploadUserName,
|
||
uploadTime: file.uploadTime || uploadTime,
|
||
}));
|
||
this.selectedAttachmentRows = [];
|
||
},
|
||
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 [];
|
||
if (Array.isArray(value)) return value;
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return Array.isArray(parsed) ? parsed : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
},
|
||
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 {};
|
||
}
|
||
},
|
||
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'}`;
|
||
},
|
||
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}` : ''}`;
|
||
},
|
||
displayValue(value) {
|
||
return value === undefined || value === null || value === '' ? '-' : value;
|
||
},
|
||
advanceStatusName(status) {
|
||
return (
|
||
{
|
||
reviewing: '审批中',
|
||
approved: '审批通过',
|
||
paid: '已付款',
|
||
returned: '已驳回',
|
||
voided: '已作废',
|
||
}[status] || this.displayValue(status)
|
||
);
|
||
},
|
||
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;
|
||
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: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0 24px;
|
||
margin-bottom: 12px;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-input) {
|
||
width: 220px;
|
||
}
|
||
}
|
||
|
||
&__detail-filter-actions {
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__attachment-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
&__attachment-upload {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
margin-top: 12px;
|
||
|
||
:deep(.vehicle-attachment-upload) {
|
||
width: auto;
|
||
}
|
||
|
||
:deep(.el-upload) {
|
||
display: inline-flex;
|
||
}
|
||
}
|
||
|
||
&__summary-total {
|
||
padding: 12px 16px 0;
|
||
text-align: right;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
&__adjust-reason {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.form-readonly {
|
||
min-height: 32px;
|
||
line-height: 32px;
|
||
color: #303133;
|
||
}
|
||
}
|
||
|
||
.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: 14px;
|
||
}
|
||
|
||
: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>
|