1a9cd55d66
2、调整对账
2062 lines
76 KiB
Vue
2062 lines
76 KiB
Vue
<template>
|
||
<component
|
||
:is="editorContainer"
|
||
v-bind="editorContainerProps"
|
||
@update:model-value="visible = $event"
|
||
>
|
||
<div
|
||
v-loading="loading"
|
||
class="reconciliation-editor"
|
||
:class="{ 'reconciliation-editor--page': pageMode }"
|
||
>
|
||
<section-card title="导入外部账单,与内部账单核对">
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
>
|
||
<el-row :gutter="24">
|
||
<el-col v-for="field in formFields" :key="field.prop" :span="6">
|
||
<el-form-item :label="field.label" :prop="field.prop">
|
||
<el-input
|
||
v-if="field.prop === 'formalSettlementNo' && editable"
|
||
v-model="form.formalSettlementNo"
|
||
readonly
|
||
placeholder="请选择正式结算单"
|
||
@click="openFormalDialog"
|
||
>
|
||
<template #append><el-button @click="openFormalDialog">选择</el-button></template>
|
||
</el-input>
|
||
<el-select
|
||
v-else-if="field.prop === 'reconciliationMode' && editable"
|
||
v-model="form.reconciliationMode"
|
||
>
|
||
<el-option label="整车总额对账" value="vehicle" />
|
||
<el-option label="货物明细对账" value="cargo" />
|
||
</el-select>
|
||
<el-date-picker
|
||
v-else-if="field.type === 'date' && editable"
|
||
v-model="form.reconciliationDate"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/>
|
||
<el-input-number
|
||
v-else-if="field.money"
|
||
v-model="form[field.prop]"
|
||
:controls="false"
|
||
:precision="2"
|
||
disabled
|
||
/>
|
||
<span v-else-if="field.prop === 'reconciliationMode'">
|
||
{{ reconciliationModeLabel }}
|
||
</span>
|
||
<span v-else>{{ displayValue(form[field.prop]) }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input
|
||
v-if="editable"
|
||
v-model="form.remark"
|
||
type="textarea"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入备注"
|
||
/>
|
||
<span v-else>{{ displayValue(form.remark) }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</section-card>
|
||
|
||
<section-card title="内部账单">
|
||
<div class="reconciliation-editor__stats">
|
||
<div class="stat-card">
|
||
<b>{{ form.internalBillCount || 0 }}</b
|
||
><span>我方账单数</span
|
||
><small
|
||
>对方 {{ form.externalBillCount || 0 }} 条 / 差异
|
||
{{ form.differenceCount || 0 }} 条</small
|
||
>
|
||
</div>
|
||
<div class="stat-card">
|
||
<b>{{ formatNumber(form.internalQuantity) }}</b
|
||
><span>我方货量</span
|
||
><small
|
||
>对方 {{ formatNumber(form.externalQuantity) }} / 差异
|
||
{{ formatNumber(form.differenceQuantity) }}</small
|
||
>
|
||
</div>
|
||
<div class="stat-card">
|
||
<b>{{ formatMoney(form.internalAmount) }}</b
|
||
><span>我方结算金额</span
|
||
><small
|
||
>对方 {{ formatMoney(form.externalAmount) }} / 差异
|
||
{{ formatMoney(form.differenceAmount) }}</small
|
||
>
|
||
</div>
|
||
<div class="stat-card stat-card--success">
|
||
<b>{{ form.matchedCount || 0 }}</b
|
||
><span>成功匹配(条)</span>
|
||
</div>
|
||
<div class="stat-card stat-card--danger">
|
||
<b>{{ form.unmatchedCount || 0 }}</b
|
||
><span>无法匹配(条)</span>
|
||
</div>
|
||
</div>
|
||
<div class="reconciliation-editor__filter">
|
||
<div class="reconciliation-editor__filter-item">
|
||
<span class="reconciliation-editor__filter-label">单据号</span>
|
||
<el-input v-model="internalQuery.documentNo" placeholder="请输入" clearable />
|
||
</div>
|
||
<div class="reconciliation-editor__filter-item">
|
||
<span class="reconciliation-editor__filter-label">车号</span>
|
||
<el-input v-model="internalQuery.vehicleNo" placeholder="请输入" clearable />
|
||
</div>
|
||
<div class="reconciliation-editor__filter-item">
|
||
<span class="reconciliation-editor__filter-label">批次号</span>
|
||
<el-input v-model="internalQuery.batchNo" placeholder="请输入" clearable />
|
||
</div>
|
||
<div class="reconciliation-editor__filter-item">
|
||
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||
</div>
|
||
<el-button @click="resetInternalQuery">重置</el-button>
|
||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="filteredInternalRows"
|
||
border
|
||
max-height="420"
|
||
:row-class-name="internalRowClassName"
|
||
>
|
||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||
<el-table-column
|
||
v-for="column in internalTableColumns"
|
||
:key="column.prop"
|
||
v-bind="column"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tag v-if="column.prop === 'matchResult'" :type="matchTagType(row.matchResult)" class="status-text">{{
|
||
matchName(row.matchResult)
|
||
}}</el-tag>
|
||
<el-tag
|
||
v-else-if="column.prop === 'updateResult'"
|
||
:type="updateTagType(row.updateResult)"
|
||
class="status-text"
|
||
>{{ updateName(row.updateResult) }}</el-tag
|
||
>
|
||
<span v-else-if="column.prop === 'matchedExternalLineNo'">
|
||
<el-input
|
||
:model-value="
|
||
hasMatchedExternal(row) ? formatMatchedExternalLineNo(row.matchedExternalLineNo) : ''
|
||
"
|
||
readonly
|
||
size="small"
|
||
:placeholder="hasMatchedExternal(row) ? '' : '请选择匹配账单'"
|
||
>
|
||
<template #append>
|
||
<el-button :icon="Search" :disabled="!editable" @click="openExternalMatchDialog(row)" />
|
||
</template>
|
||
</el-input>
|
||
</span>
|
||
<span v-else-if="column.prop === 'transportType'">
|
||
{{ transportTypeLabel(row.transportType) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'transportQuantity'">
|
||
{{ formatTransportQuantity(row.transportQuantity) }}
|
||
</span>
|
||
<span v-else-if="column.feeItemName">
|
||
{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||
<span
|
||
v-else-if="
|
||
column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'
|
||
"
|
||
>{{ formatDate(row[column.prop]) }}</span
|
||
>
|
||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="reconciliation-editor__links">
|
||
<el-link v-if="editable" type="primary" @click="openAdjust(row)">调整</el-link>
|
||
<el-link
|
||
v-if="editable && ['matched', 'partial'].includes(row.matchResult)"
|
||
type="primary"
|
||
@click="handleUnmatch(row)"
|
||
>取消匹配</el-link
|
||
>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card title="导入外部账单">
|
||
<template #extra>
|
||
<div class="reconciliation-editor__actions" v-if="editable">
|
||
<el-button
|
||
v-if="form.reconciliationMode === 'vehicle'"
|
||
type="primary"
|
||
plain
|
||
@click="downloadTemplate('vehicle')"
|
||
>下载整车对账模板</el-button
|
||
>
|
||
<el-button
|
||
v-if="form.reconciliationMode === 'cargo'"
|
||
type="primary"
|
||
plain
|
||
@click="downloadTemplate('cargo')"
|
||
>下载货物明细对账模板</el-button
|
||
>
|
||
<el-button type="primary" plain @click="chooseImport">导入</el-button>
|
||
<el-button type="primary" :disabled="!canStartMatch" @click="handleMatch"
|
||
>开始匹配内部账单</el-button
|
||
>
|
||
<input
|
||
ref="fileInput"
|
||
type="file"
|
||
accept=".xls,.xlsx"
|
||
class="hidden-file"
|
||
@change="handleImport"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<el-tabs v-model="externalTab">
|
||
<el-tab-pane :label="`导入明细(${externalDetails.length})`" name="all" />
|
||
<el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" />
|
||
</el-tabs>
|
||
<el-table
|
||
:data="visibleExternalRows"
|
||
border
|
||
max-height="420"
|
||
:row-class-name="externalRowClassName"
|
||
>
|
||
<el-table-column
|
||
v-for="column in externalColumns"
|
||
:key="column.prop"
|
||
v-bind="column"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)" class="status-text">{{
|
||
matchName(row.matchStatus)
|
||
}}</el-tag>
|
||
<template v-else-if="isExternalRowEditing(row) && isExternalEditableColumn(column.prop)">
|
||
<el-input-number
|
||
v-if="column.feeItemName"
|
||
v-model="row.feeItems[column.feeItemName]"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
size="small"
|
||
/>
|
||
<el-date-picker
|
||
v-else-if="isExternalDateColumn(column.prop)"
|
||
v-model="row[column.prop]"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
format="YYYY-MM-DD"
|
||
size="small"
|
||
placeholder="请选择"
|
||
/>
|
||
<el-input-number
|
||
v-else-if="isExternalNumberColumn(column.prop)"
|
||
v-model="row[column.prop]"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
size="small"
|
||
/>
|
||
<el-select
|
||
v-else-if="column.prop === 'transportType'"
|
||
v-model="row[column.prop]"
|
||
size="small"
|
||
clearable
|
||
placeholder="请选择"
|
||
>
|
||
<el-option
|
||
v-for="item in transportTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
<el-input v-else v-model="row[column.prop]" size="small" />
|
||
</template>
|
||
<span v-else-if="column.prop === 'transportType'">
|
||
{{ transportTypeLabel(row.transportType) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'transportQuantity'">
|
||
{{ formatTransportQuantity(row.transportQuantity) }}
|
||
</span>
|
||
<span v-else-if="column.feeItemName">
|
||
{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||
<span
|
||
v-else-if="
|
||
column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'
|
||
"
|
||
>{{ formatDate(row[column.prop]) }}</span
|
||
>
|
||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="reconciliation-editor__links">
|
||
<template v-if="editable && isExternalRowEditing(row)">
|
||
<el-link type="primary" @click="finishExternalAdjust(row)">完成</el-link>
|
||
<el-link type="primary" @click="cancelExternalAdjust(row)">取消</el-link>
|
||
</template>
|
||
<el-link v-else-if="editable" type="primary" @click="openExternalAdjust(row)">
|
||
调整
|
||
</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
</div>
|
||
|
||
<template v-if="!pageMode" #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<template v-if="editable">
|
||
<el-button type="primary" plain :loading="saving" @click="handleSave">保存草稿</el-button>
|
||
<el-button type="primary" plain :loading="actionLoading" @click="handleUpdate"
|
||
>按匹配结果更新账单</el-button
|
||
>
|
||
<el-button type="primary" :loading="actionLoading" @click="handleComplete"
|
||
>完成对账</el-button
|
||
>
|
||
</template>
|
||
</template>
|
||
|
||
<div v-if="pageMode" class="reconciliation-editor__page-actions">
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<template v-if="editable">
|
||
<el-button type="primary" plain :loading="actionLoading" @click="handleUpdate">
|
||
按匹配结果更新账单
|
||
</el-button>
|
||
<el-button type="primary" :loading="actionLoading" @click="handleComplete">
|
||
完成对账
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
|
||
<el-dialog v-model="formalDialog.visible" title="选择正式结算单" width="84%" append-to-body>
|
||
<el-form :model="formalDialog.query" inline label-position="right" label-width="88px">
|
||
<el-form-item label="正式结算单号"
|
||
><el-input v-model="formalDialog.query.keyword" clearable
|
||
/></el-form-item>
|
||
<el-form-item><el-button @click="loadFormalOptions">查询</el-button></el-form-item>
|
||
</el-form>
|
||
<el-table
|
||
v-loading="formalDialog.loading"
|
||
:data="formalDialog.rows"
|
||
border
|
||
@selection-change="formalDialog.selected = $event"
|
||
>
|
||
<el-table-column type="selection" width="52" :selectable="row => !row.isSelected" />
|
||
<el-table-column type="index" label="序号" width="64" />
|
||
<el-table-column prop="formalSettlementNo" label="正式结算单号" min-width="150" />
|
||
<el-table-column prop="contractNo" label="合同编号" min-width="130" />
|
||
<el-table-column prop="contractName" label="合同名称" min-width="160" />
|
||
<el-table-column prop="payerName" label="付款方" min-width="150" />
|
||
<el-table-column prop="payeeName" label="收款方" min-width="150" />
|
||
<el-table-column prop="settlementAmount" label="结算金额" min-width="120"
|
||
><template #default="{ row }">{{
|
||
formatMoney(row.settlementAmount, row.currency)
|
||
}}</template></el-table-column
|
||
>
|
||
<el-table-column label="审批状态" width="110"
|
||
><template #default="{ row }">{{
|
||
formalApprovalStatusLabel(row.approvalStatus)
|
||
}}</template></el-table-column
|
||
>
|
||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="selectFormal(row)">选择</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="reconciliation-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="formalDialog.page.current"
|
||
v-model:page-size="formalDialog.page.size"
|
||
:total="formalDialog.page.total"
|
||
layout="total, prev, pager, next"
|
||
@current-change="loadFormalOptions"
|
||
/>
|
||
</div>
|
||
<template #footer
|
||
><el-button @click="formalDialog.visible = false">取消</el-button
|
||
><el-button type="primary" @click="confirmFormal">确定</el-button></template
|
||
>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="adjustDialog.visible" title="结算明细调整" width="90%" append-to-body>
|
||
<el-table :data="adjustDialog.rows" border>
|
||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||
<el-table-column prop="cargoType" label="货物类型" min-width="120" />
|
||
<el-table-column prop="transportQuantity" label="运输量" min-width="120"
|
||
><template #default="{ row }"
|
||
><el-input-number
|
||
v-model="row.transportQuantity"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false" /></template
|
||
></el-table-column>
|
||
<el-table-column prop="unitPrice" label="运输单价" min-width="120"
|
||
><template #default="{ row }"
|
||
><el-input-number v-model="row.unitPrice" :min="0" :controls="false" /></template
|
||
></el-table-column>
|
||
<el-table-column prop="freightAmount" label="运输费" min-width="120"
|
||
><template #default="{ row }"
|
||
><el-input-number v-model="row.freightAmount" :min="0" :controls="false" /></template
|
||
></el-table-column>
|
||
<el-table-column prop="settlementAmount" label="结算金额" min-width="140"
|
||
><template #default="{ row }"
|
||
><el-input-number v-model="row.settlementAmount" :min="0" :controls="false" /></template
|
||
></el-table-column>
|
||
</el-table>
|
||
<el-form class="dialog-form" label-position="right" label-width="auto"
|
||
><el-form-item label="调整原因" required
|
||
><el-input
|
||
v-model="adjustDialog.reason"
|
||
type="textarea"
|
||
maxlength="200"
|
||
show-word-limit /></el-form-item
|
||
></el-form>
|
||
<template #footer
|
||
><el-button @click="adjustDialog.visible = false">取消</el-button
|
||
><el-button type="primary" :loading="adjustDialog.saving" @click="saveAdjust"
|
||
>保存</el-button
|
||
></template
|
||
>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="manualMatchDialog.visible"
|
||
title="选择未匹配外部账单"
|
||
width="92%"
|
||
append-to-body
|
||
>
|
||
<el-table v-loading="manualMatchDialog.loading" :data="unmatchedExternalRows" border max-height="520">
|
||
<el-table-column
|
||
v-for="column in externalColumns"
|
||
:key="`manual-${column.prop}`"
|
||
v-bind="column"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="column.prop === 'transportType'">{{ transportTypeLabel(row.transportType) }}</span>
|
||
<span v-else-if="column.prop === 'transportQuantity'">{{ formatTransportQuantity(row.transportQuantity) }}</span>
|
||
<span v-else-if="column.feeItemName">{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}</span>
|
||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||
<span v-else-if="column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'">{{ formatDate(row[column.prop]) }}</span>
|
||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="handleManualMatch(row)">选择</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="manualMatchDialog.visible = false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="completionDialog.visible"
|
||
title="汇总对账结果费用"
|
||
width="92%"
|
||
top="4vh"
|
||
append-to-body
|
||
destroy-on-close
|
||
class="completion-dialog"
|
||
>
|
||
<div class="completion-dialog__content">
|
||
<section class="completion-dialog__section">
|
||
<div class="completion-dialog__grid">
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">对账单号</span>
|
||
<span class="completion-dialog__value">{{ displayValue(form.reconciliationNo) }}</span>
|
||
</div>
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">预结算单号</span>
|
||
<span class="completion-dialog__value">{{
|
||
displayValue(form.preSettlementNo || form.formalSettlementNo)
|
||
}}</span>
|
||
</div>
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">合同编号</span>
|
||
<span class="completion-dialog__value">{{ displayValue(form.contractNo) }}</span>
|
||
</div>
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">单据周期</span>
|
||
<span class="completion-dialog__value">{{ completionPeriod }}</span>
|
||
</div>
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">客户/承运商</span>
|
||
<span class="completion-dialog__value">{{ displayValue(form.customerName) }}</span>
|
||
</div>
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">项目</span>
|
||
<span class="completion-dialog__value">{{ displayValue(form.projectName) }}</span>
|
||
</div>
|
||
<div class="completion-dialog__field">
|
||
<span class="completion-dialog__label">对账模式</span>
|
||
<span class="completion-dialog__value">{{ reconciliationModeLabel }}</span>
|
||
</div>
|
||
<div class="completion-dialog__field completion-dialog__field--remark">
|
||
<span class="completion-dialog__label">备注</span>
|
||
<span class="completion-dialog__value">{{ displayValue(form.remark) }}</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="completion-dialog__section completion-dialog__section--fees">
|
||
<div class="completion-dialog__section-title">费用合计信息</div>
|
||
<div class="completion-dialog__fee-actions">
|
||
<el-button type="primary" plain @click="openFeeAdjustment('补款')">补款</el-button>
|
||
<el-button type="primary" plain @click="openFeeAdjustment('扣款')">扣款</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="completionSummaryRows"
|
||
border
|
||
show-summary
|
||
:summary-method="getCompletionSummarySums"
|
||
>
|
||
<el-table-column type="index" label="序号" width="72" align="center" />
|
||
<el-table-column prop="name" label="费用项目" min-width="150" align="center">
|
||
<template #default="{ row }">
|
||
<span :class="{ 'completion-dialog__negative': row._feeKind === '扣款' }">{{ row.name }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="原金额(元)" min-width="160" align="right">
|
||
<template #default="{ row }">{{ formatCompletionMoney(row.originalAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整金额(元)" min-width="160" align="right">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-if="editable && row.manualFlag === 1"
|
||
v-model="row.adjustAmount"
|
||
:class="{ 'completion-dialog__negative-input': row._feeKind === '扣款' }"
|
||
:precision="2"
|
||
:step="0.01"
|
||
:controls="false"
|
||
:value-on-clear="0"
|
||
@change="recalculateCompletionFee(row)"
|
||
/>
|
||
<span v-else :class="{ 'completion-dialog__negative': Number(row.adjustAmount) < 0 }">{{
|
||
formatCompletionMoney(row.adjustAmount)
|
||
}}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整后金额(元)" min-width="180" align="right">
|
||
<template #default="{ row }">
|
||
<span :class="{ 'completion-dialog__negative': Number(row.afterAmount) < 0 }">{{
|
||
formatCompletionMoney(row.afterAmount)
|
||
}}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="220">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="editable"
|
||
v-model="row.remark"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入备注"
|
||
/>
|
||
<span v-else>{{ displayValue(row.remark) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="editable" label="操作" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-link v-if="row.manualFlag === 1" type="danger" @click="removeCompletionFee(row)">删除</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
</div>
|
||
<template #footer>
|
||
<div class="completion-dialog__footer">
|
||
<el-button type="primary" plain @click="completionDialog.visible = false"
|
||
>上一步</el-button
|
||
>
|
||
<el-button type="primary" :loading="completionDialog.loading" @click="confirmCompletion"
|
||
>确认</el-button
|
||
>
|
||
<el-button @click="completionDialog.visible = false">取消</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
</component>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex';
|
||
import * as api from '@/api/settlement/transportReconciliation';
|
||
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
|
||
import { getDictionary } from '@/api/system/dictbiz';
|
||
import { Search } from '@element-plus/icons-vue';
|
||
import { h } from 'vue';
|
||
import { downloadXls } from '@/utils/util';
|
||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||
import {
|
||
internalColumns,
|
||
externalCargoColumns,
|
||
externalVehicleColumns,
|
||
} from '@/option/settlement/transportReconciliationTable';
|
||
|
||
export default {
|
||
name: 'TransportReconciliationEditor',
|
||
props: {
|
||
modelValue: Boolean,
|
||
recordId: [String, Number],
|
||
settlementType: { type: String, default: 'payable' },
|
||
readonly: Boolean,
|
||
pageMode: Boolean,
|
||
},
|
||
emits: ['update:modelValue', 'success'],
|
||
data() {
|
||
return {
|
||
Search,
|
||
loading: false,
|
||
saving: false,
|
||
actionLoading: false,
|
||
currentId: null,
|
||
form: this.emptyForm(),
|
||
formFields: transportReconciliationFormFields,
|
||
internalColumns,
|
||
transportTypeOptions: [],
|
||
internalDetails: [],
|
||
externalDetails: [],
|
||
internalQuery: { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' },
|
||
internalFilterTick: 0,
|
||
externalTab: 'all',
|
||
formalDialog: {
|
||
visible: false,
|
||
loading: false,
|
||
rows: [],
|
||
selected: [],
|
||
query: { keyword: '' },
|
||
page: { current: 1, size: 10, total: 0 },
|
||
},
|
||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||
manualMatchDialog: { visible: false, loading: false, row: null },
|
||
completionDialog: { visible: false, loading: false },
|
||
completionSummaryRowsData: null,
|
||
externalEditing: {},
|
||
externalEditSnapshots: {},
|
||
matchingStarted: false,
|
||
rules: {
|
||
formalSettlementNo: [{ required: true, message: '请选择正式结算单', trigger: 'change' }],
|
||
reconciliationMode: [{ required: true, message: '请选择对账模式', trigger: 'change' }],
|
||
remark: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['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: 'reconciliation-editor-shell reconciliation-editor-shell--page' };
|
||
}
|
||
return {
|
||
modelValue: this.visible,
|
||
title: this.title,
|
||
width: '98%',
|
||
top: '2vh',
|
||
appendToBody: true,
|
||
destroyOnClose: true,
|
||
};
|
||
},
|
||
editable() {
|
||
return !this.readonly;
|
||
},
|
||
title() {
|
||
return this.readonly
|
||
? '查看运输对账单'
|
||
: this.currentId
|
||
? '编辑运输对账单'
|
||
: '新增运输对账单';
|
||
},
|
||
filteredInternalRows() {
|
||
void this.internalFilterTick;
|
||
const query = this.internalQuery;
|
||
return this.groupedInternalRows.filter(
|
||
row =>
|
||
(!query.documentNo || String(row.documentNo || '').includes(query.documentNo)) &&
|
||
(!query.vehicleNo || String(row.vehicleNo || '').includes(query.vehicleNo)) &&
|
||
(!query.batchNo || String(row.batchNo || '').includes(query.batchNo)) &&
|
||
(!query.cargoName || String(row.cargoName || '').includes(query.cargoName))
|
||
);
|
||
},
|
||
groupedInternalRows() {
|
||
if (this.form.reconciliationMode === 'cargo') {
|
||
return this.internalDetails.map(row => ({ ...row, _sourceRows: [row] }));
|
||
}
|
||
const groups = new Map();
|
||
this.internalDetails.forEach((row, index) => {
|
||
const key = row.documentNo ? String(row.documentNo) : `__row_${index}`;
|
||
if (!groups.has(key)) groups.set(key, []);
|
||
groups.get(key).push(row);
|
||
});
|
||
return [...groups.values()].map(rows => this.mergeInternalRows(rows));
|
||
},
|
||
internalFeeItemNames() {
|
||
const names = new Set();
|
||
this.internalDetails.forEach(row => {
|
||
Object.keys(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(name => {
|
||
if (name) names.add(name);
|
||
});
|
||
});
|
||
return [...names];
|
||
},
|
||
internalTableColumns() {
|
||
const columns = internalColumns.filter(column => column.prop !== 'freightAmount');
|
||
const settlementIndex = columns.findIndex(column => column.prop === 'settlementAmount');
|
||
const feeColumns = this.internalFeeItemNames.map((name, index) => ({
|
||
prop: `feeItem_${index}`,
|
||
label: name,
|
||
minWidth: 120,
|
||
money: true,
|
||
feeItemName: name,
|
||
}));
|
||
columns.splice(settlementIndex < 0 ? columns.length : settlementIndex, 0, ...feeColumns);
|
||
return columns;
|
||
},
|
||
externalColumns() {
|
||
const columns = [
|
||
...(this.form.reconciliationMode === 'cargo'
|
||
? externalCargoColumns
|
||
: externalVehicleColumns),
|
||
];
|
||
const settlementIndex = columns.findIndex(column => column.prop === 'settlementAmount');
|
||
const feeColumns = this.externalFeeItemNames.map((name, index) => ({
|
||
prop: `externalFeeItem_${index}`,
|
||
label: name,
|
||
minWidth: 120,
|
||
money: true,
|
||
feeItemName: name,
|
||
}));
|
||
columns.splice(settlementIndex < 0 ? columns.length : settlementIndex, 0, ...feeColumns);
|
||
return columns;
|
||
},
|
||
externalFeeItemNames() {
|
||
const names = new Set();
|
||
this.externalDetails.forEach(row => {
|
||
Object.keys(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(name => {
|
||
if (name) names.add(name);
|
||
});
|
||
});
|
||
return [...names];
|
||
},
|
||
duplicateRows() {
|
||
const duplicateKeys = new Set();
|
||
const duplicateRows = [];
|
||
this.externalDetails.forEach(row => {
|
||
const key = this.externalDuplicateKey(row);
|
||
if (duplicateKeys.has(key)) duplicateRows.push(row);
|
||
else duplicateKeys.add(key);
|
||
});
|
||
const duplicateKeySet = new Set(
|
||
duplicateRows.map(row => this.externalDuplicateKey(row))
|
||
);
|
||
return this.externalDetails.filter(row => duplicateKeySet.has(this.externalDuplicateKey(row)));
|
||
},
|
||
canStartMatch() {
|
||
return Boolean(this.form.formalSettlementId && this.externalDetails.length);
|
||
},
|
||
visibleExternalRows() {
|
||
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
||
},
|
||
unmatchedExternalRows() {
|
||
return this.externalDetails.filter(row => row.matchStatus !== 'matched');
|
||
},
|
||
completionPeriod() {
|
||
const period = this.form.period || this.form.documentPeriod || this.form.settlementPeriod;
|
||
if (Array.isArray(period) && period.length) return period.filter(Boolean).join(' 至 ');
|
||
if (typeof period === 'string' && period.trim()) return period;
|
||
const start =
|
||
this.form.periodStart ||
|
||
this.form.periodStartDate ||
|
||
this.form.startDate ||
|
||
this.form.billStartDate ||
|
||
this.form.reconciliationStartDate ||
|
||
this.form.settlementStartDate;
|
||
const end =
|
||
this.form.periodEnd ||
|
||
this.form.periodEndDate ||
|
||
this.form.endDate ||
|
||
this.form.billEndDate ||
|
||
this.form.reconciliationEndDate ||
|
||
this.form.settlementEndDate;
|
||
if (start || end) return [start, end].filter(Boolean).join(' 至 ');
|
||
return '-';
|
||
},
|
||
reconciliationModeLabel() {
|
||
const labels = { vehicle: '整车总额对账', cargo: '货物明细对账' };
|
||
return this.form.reconciliationModeName || labels[this.form.reconciliationMode] || '-';
|
||
},
|
||
completionSummaryRows() {
|
||
if (Array.isArray(this.completionSummaryRowsData)) return this.completionSummaryRowsData;
|
||
return this.buildCompletionSummaryRows;
|
||
},
|
||
|
||
buildCompletionSummaryRows() {
|
||
const source =
|
||
this.form.feeSummary ||
|
||
this.form.feeSummaries ||
|
||
this.form.feeDetails ||
|
||
this.form.summaryFees;
|
||
if (Array.isArray(source) && source.length) {
|
||
return source.map((item, index) => this.normalizeCompletionFee(item, index));
|
||
}
|
||
if (source && typeof source === 'object') {
|
||
return Object.entries(source).map(([name, item], index) =>
|
||
this.normalizeCompletionFee(
|
||
typeof item === 'object' ? { ...item, name: item.name || name } : { name, afterAmount: item },
|
||
index
|
||
)
|
||
);
|
||
}
|
||
|
||
const names = new Set([...this.internalFeeItemNames, ...this.externalFeeItemNames]);
|
||
if (!names.size) names.add('运费');
|
||
return [...names].map((name, index) => {
|
||
const originalAmount = this.sumFeeAmount(this.internalDetails, name);
|
||
const externalAmount = this.sumFeeAmount(this.externalDetails, name);
|
||
const afterAmount = this.externalDetails.length
|
||
? externalAmount
|
||
: name === '运费'
|
||
? Number(this.form.internalAmount || 0)
|
||
: originalAmount;
|
||
return {
|
||
name,
|
||
feeType: '',
|
||
feeItem: name,
|
||
originalAmount,
|
||
adjustAmount: afterAmount - originalAmount,
|
||
afterAmount,
|
||
remark: '',
|
||
manualFlag: 0,
|
||
_feeKind: '',
|
||
_index: index,
|
||
};
|
||
});
|
||
},
|
||
},
|
||
watch: {
|
||
modelValue: {
|
||
immediate: true,
|
||
handler(value) {
|
||
if (value) this.initialize();
|
||
},
|
||
},
|
||
},
|
||
methods: {
|
||
emptyForm() {
|
||
return {
|
||
reconciliationNo: '',
|
||
reconciliationMode: 'vehicle',
|
||
formalSettlementId: null,
|
||
formalSettlementNo: '',
|
||
customerName: '',
|
||
payerName: '',
|
||
payeeName: '',
|
||
projectName: '',
|
||
deptName: '',
|
||
contractName: '',
|
||
paidAmount: 0,
|
||
reconcilerName: '',
|
||
reconciliationDate: '',
|
||
remark: '',
|
||
internalBillCount: 0,
|
||
externalBillCount: 0,
|
||
differenceCount: 0,
|
||
internalQuantity: 0,
|
||
externalQuantity: 0,
|
||
differenceQuantity: 0,
|
||
internalAmount: 0,
|
||
externalAmount: 0,
|
||
differenceAmount: 0,
|
||
matchedCount: 0,
|
||
unmatchedCount: 0,
|
||
};
|
||
},
|
||
async initialize() {
|
||
this.loadTransportTypeOptions();
|
||
this.currentId = this.recordId;
|
||
this.form = this.emptyForm();
|
||
this.internalDetails = [];
|
||
this.externalDetails = [];
|
||
this.externalEditing = {};
|
||
this.externalEditSnapshots = {};
|
||
this.completionSummaryRowsData = null;
|
||
this.matchingStarted = false;
|
||
this.externalTab = 'all';
|
||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||
if (!this.currentId) {
|
||
this.form.reconciliationMode = 'vehicle';
|
||
this.form.reconcilerName =
|
||
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '当前用户';
|
||
this.form.reconciliationDate = this.$dayjs().format('YYYY-MM-DD');
|
||
return;
|
||
}
|
||
this.loading = true;
|
||
try {
|
||
await this.loadDetail();
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
async loadDetail() {
|
||
const data = this.unwrapData(await api.getDetail(this.currentId)) || {};
|
||
this.form = {
|
||
...this.emptyForm(),
|
||
...data,
|
||
customerName:
|
||
data.customerName ||
|
||
((data.settlementType || this.settlementType) === 'receivable'
|
||
? data.payerName
|
||
: data.payeeName) ||
|
||
'',
|
||
};
|
||
this.completionSummaryRowsData = null;
|
||
const internalRows = data.internalDetails || data.internalBillDetails || data.internals || [];
|
||
this.internalDetails = internalRows.map(row => this.normalizeInternalRow(row));
|
||
const externalRows = data.externalDetails || data.externalBillDetails || data.externals || [];
|
||
this.externalDetails = externalRows.map((row, index) =>
|
||
this.normalizeExternalRow(row, index)
|
||
);
|
||
this.matchingStarted =
|
||
this.matchingStarted ||
|
||
data.matchStatus === 'partial' ||
|
||
this.internalDetails.some(row => row.matchResult === 'matched') ||
|
||
this.externalDetails.some(row => row.matchStatus === 'matched');
|
||
},
|
||
openFormalDialog() {
|
||
if (!this.editable) return;
|
||
this.formalDialog.visible = true;
|
||
this.formalDialog.page.current = 1;
|
||
this.loadFormalOptions();
|
||
},
|
||
parseFeeItems(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
},
|
||
externalDuplicateKey(row) {
|
||
const feeItems = row.feeItems || this.parseFeeItems(row.feeItemsJson);
|
||
const normalized = value => {
|
||
if (value === null || value === undefined || value === '') return '';
|
||
if (typeof value === 'number') return Number(value).toString();
|
||
if (typeof value === 'object') {
|
||
return JSON.stringify(
|
||
Object.keys(value)
|
||
.sort()
|
||
.reduce((result, key) => {
|
||
result[key] = normalized(value[key]);
|
||
return result;
|
||
}, {})
|
||
);
|
||
}
|
||
const text = String(value).trim().replace(/\s+/g, '').toLowerCase();
|
||
return /^-?\d+(\.\d+)?$/.test(text) ? Number(text).toString() : text;
|
||
};
|
||
const normalizeDate = value => {
|
||
if (!value) return '';
|
||
const date = this.$dayjs(value);
|
||
return date.isValid() ? date.format('YYYY-MM-DDTHH:mm:ss') : value;
|
||
};
|
||
return [
|
||
row.vehicleNo,
|
||
row.departureAddress,
|
||
row.arrivalAddress,
|
||
normalizeDate(row.actualDepartureTime),
|
||
normalizeDate(row.actualCompletionTime),
|
||
row.transportType,
|
||
row.cargoName,
|
||
row.cargoType,
|
||
row.specification,
|
||
row.model,
|
||
row.transportQuantity,
|
||
row.quantityUnit,
|
||
row.mileage,
|
||
row.batchNo,
|
||
row.unitPrice,
|
||
row.freightAmount,
|
||
feeItems,
|
||
row.settlementAmount,
|
||
]
|
||
.map(normalized)
|
||
.join('|');
|
||
},
|
||
normalizeInternalRow(row) {
|
||
const feeItems =
|
||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||
? row.feeItems
|
||
: this.parseFeeItems(row.feeItemsJson);
|
||
return { ...row, feeItems };
|
||
},
|
||
normalizeExternalRow(row, index) {
|
||
const feeItems =
|
||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||
? row.feeItems
|
||
: this.parseFeeItems(row.feeItemsJson);
|
||
return {
|
||
...row,
|
||
feeItems,
|
||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||
};
|
||
},
|
||
mergeInternalRows(rows) {
|
||
const firstRow = rows[0] || {};
|
||
const feeItems = {};
|
||
rows.forEach(row => {
|
||
Object.entries(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(
|
||
([name, amount]) => {
|
||
feeItems[name] = Number(feeItems[name] || 0) + Number(amount || 0);
|
||
}
|
||
);
|
||
});
|
||
const matchResults = rows.map(row => row.matchResult);
|
||
const updateResults = rows.map(row => row.updateResult);
|
||
return {
|
||
...firstRow,
|
||
cargoName: this.joinInternalValues(rows, 'cargoName'),
|
||
cargoType: this.joinInternalValues(rows, 'cargoType'),
|
||
transportQuantity: this.sumInternalValues(rows, 'transportQuantity'),
|
||
settlementAmount: this.sumInternalValues(rows, 'settlementAmount'),
|
||
feeItems,
|
||
matchedExternalLineNo: this.joinInternalValues(rows, 'matchedExternalLineNo'),
|
||
matchResult: matchResults.every(value => value === 'matched')
|
||
? 'matched'
|
||
: matchResults.some(value => value === 'matched')
|
||
? 'partial'
|
||
: 'unmatched',
|
||
updateResult: updateResults.every(value => value === updateResults[0])
|
||
? updateResults[0]
|
||
: 'partial_updated',
|
||
_sourceRows: rows,
|
||
};
|
||
},
|
||
joinInternalValues(rows, prop) {
|
||
return [
|
||
...new Set(
|
||
rows
|
||
.map(row => row[prop])
|
||
.filter(value => value !== null && value !== undefined && value !== '')
|
||
),
|
||
].join('、');
|
||
},
|
||
sumInternalValues(rows, prop) {
|
||
return rows.reduce((total, row) => total + Number(row[prop] || 0), 0);
|
||
},
|
||
getFeeItemAmount(row, name) {
|
||
return (row.feeItems || this.parseFeeItems(row.feeItemsJson))[name] ?? 0;
|
||
},
|
||
formalApprovalStatusLabel(value) {
|
||
return (
|
||
{
|
||
draft: '草稿',
|
||
reviewing: '审批中',
|
||
approved: '审批通过',
|
||
returned: '已驳回',
|
||
voided: '已作废',
|
||
}[value] ||
|
||
value ||
|
||
'-'
|
||
);
|
||
},
|
||
async loadFormalOptions() {
|
||
this.formalDialog.loading = true;
|
||
try {
|
||
const params = {
|
||
settlementType: this.settlementType,
|
||
keyword: this.formalDialog.query.keyword,
|
||
approvalStatus: 'draft',
|
||
};
|
||
const data = this.unwrapData(
|
||
await api.getFormalOptions(
|
||
this.formalDialog.page.current,
|
||
this.formalDialog.page.size,
|
||
params
|
||
)
|
||
);
|
||
this.formalDialog.rows = data.records || [];
|
||
this.formalDialog.page.total = data.total || 0;
|
||
} finally {
|
||
this.formalDialog.loading = false;
|
||
}
|
||
},
|
||
async confirmFormal() {
|
||
if (this.formalDialog.selected.length !== 1)
|
||
return this.$message.warning('请选择一张正式结算单');
|
||
await this.selectFormal(this.formalDialog.selected[0]);
|
||
},
|
||
async selectFormal(selected) {
|
||
if (selected?.approvalStatus && selected.approvalStatus !== 'draft') {
|
||
return this.$message.warning('只能选择草稿状态的正式结算单');
|
||
}
|
||
const formalSettlementChanged = this.form.formalSettlementId !== selected.id;
|
||
this.form = {
|
||
...this.form,
|
||
...selected,
|
||
formalSettlementId: selected.id,
|
||
formalSettlementNo: selected.formalSettlementNo,
|
||
customerName:
|
||
selected.customerName ||
|
||
((selected.settlementType || this.settlementType) === 'receivable'
|
||
? selected.payerName
|
||
: selected.payeeName) ||
|
||
'',
|
||
reconciliationNo: this.form.reconciliationNo,
|
||
};
|
||
if (formalSettlementChanged) {
|
||
this.externalDetails = [];
|
||
this.matchingStarted = false;
|
||
this.completionSummaryRowsData = null;
|
||
}
|
||
await this.loadFormalInternalPreview(selected.id);
|
||
this.formalDialog.visible = false;
|
||
},
|
||
async loadFormalInternalPreview(formalSettlementId) {
|
||
const formal = this.unwrapData(await formalSettlementApi.getDetail(formalSettlementId));
|
||
const details = formal.details || [];
|
||
const buildInternalRow = (detail, overrides = {}, index = 0) =>
|
||
this.normalizeInternalRow({
|
||
...detail,
|
||
...overrides,
|
||
id: null,
|
||
lineNo: index + 1,
|
||
formalSettlementDetailId: detail.id,
|
||
settlementAmount: overrides.settlementAmount ?? detail.settlementAmountTax ?? 0,
|
||
matchResult: 'unmatched',
|
||
updateResult: 'not_updated',
|
||
});
|
||
if (this.form.reconciliationMode === 'cargo') {
|
||
const feeGroups = await Promise.all(
|
||
details.map(async detail => ({
|
||
detail,
|
||
fees: this.unwrapData(await formalSettlementApi.getDetailFees(detail.id)) || [],
|
||
}))
|
||
);
|
||
this.internalDetails = feeGroups.flatMap(({ detail, fees }) => {
|
||
if (!fees.length) return [buildInternalRow(detail, {}, 0)];
|
||
return fees.map((fee, index) =>
|
||
buildInternalRow(
|
||
detail,
|
||
{
|
||
...fee,
|
||
settlementAmount: fee.settlementAmountTax ?? 0,
|
||
},
|
||
index
|
||
)
|
||
);
|
||
});
|
||
} else {
|
||
this.internalDetails = details.map((detail, index) => buildInternalRow(detail, {}, index));
|
||
}
|
||
const internalQuantity = this.internalDetails.reduce(
|
||
(total, row) => total + Number(row.transportQuantity || 0),
|
||
0
|
||
);
|
||
const internalAmount = this.internalDetails.reduce(
|
||
(total, row) => total + Number(row.settlementAmount || 0),
|
||
0
|
||
);
|
||
this.form = {
|
||
...this.form,
|
||
internalBillCount: this.internalDetails.length,
|
||
internalQuantity,
|
||
internalAmount,
|
||
externalBillCount: 0,
|
||
externalQuantity: 0,
|
||
externalAmount: 0,
|
||
differenceCount: this.internalDetails.length,
|
||
differenceQuantity: internalQuantity,
|
||
differenceAmount: internalAmount,
|
||
matchedCount: 0,
|
||
unmatchedCount: this.internalDetails.length,
|
||
};
|
||
},
|
||
unwrapData(response) {
|
||
const body = response?.data || response || {};
|
||
return body?.data || body;
|
||
},
|
||
async loadTransportTypeOptions() {
|
||
try {
|
||
const data = this.unwrapData(await getDictionary({ code: 'transport_type' }));
|
||
const records = Array.isArray(data) ? data : data?.records || [];
|
||
this.transportTypeOptions = records.map(item => ({
|
||
label: item.dictValue || item.label || item.name,
|
||
value: item.dictKey || item.value || item.dictValue || item.name,
|
||
}));
|
||
} catch {
|
||
this.transportTypeOptions = [];
|
||
}
|
||
},
|
||
async handleSave(silent = false) {
|
||
const valid = await this.$refs.formRef.validate().catch(() => false);
|
||
if (!valid || !this.form.formalSettlementId) {
|
||
this.$message.warning('请选择正式结算单');
|
||
return null;
|
||
}
|
||
this.saving = true;
|
||
try {
|
||
const data = this.unwrapData(
|
||
await api.save({
|
||
id: this.currentId,
|
||
formalSettlementId: this.form.formalSettlementId,
|
||
reconciliationMode: this.form.reconciliationMode,
|
||
reconciliationDate: this.form.reconciliationDate,
|
||
remark: this.form.remark,
|
||
})
|
||
);
|
||
this.currentId = data;
|
||
await this.loadDetail();
|
||
if (!silent) {
|
||
this.$message.success('草稿保存成功');
|
||
this.$emit('success', this.currentId);
|
||
}
|
||
return data;
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
serializeInternalRows() {
|
||
return this.internalDetails.map(row => {
|
||
const snapshot = { ...row };
|
||
delete snapshot._sourceRows;
|
||
delete snapshot._externalKey;
|
||
delete snapshot.feeItems;
|
||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||
return snapshot;
|
||
});
|
||
},
|
||
serializeExternalRows() {
|
||
return this.externalDetails.map(row => {
|
||
const snapshot = { ...row };
|
||
delete snapshot._sourceRows;
|
||
delete snapshot._externalKey;
|
||
delete snapshot.feeItems;
|
||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||
return snapshot;
|
||
});
|
||
},
|
||
async handleMatch() {
|
||
if (!this.currentId) return this.$message.warning('请先保存草稿后再匹配');
|
||
this.actionLoading = true;
|
||
try {
|
||
const data = this.unwrapData(
|
||
await api.matchPreview({
|
||
id: this.currentId,
|
||
reconciliationMode: this.form.reconciliationMode,
|
||
internalDetails: this.serializeInternalRows(),
|
||
externalDetails: this.serializeExternalRows(),
|
||
})
|
||
);
|
||
this.form = { ...this.form, ...data };
|
||
this.internalDetails = (data.internalDetails || []).map(row => this.normalizeInternalRow(row));
|
||
this.externalDetails = (data.externalDetails || []).map((row, index) =>
|
||
this.normalizeExternalRow(row, index)
|
||
);
|
||
this.completionSummaryRowsData = null;
|
||
this.matchingStarted = true;
|
||
this.$message.success('匹配完成');
|
||
} finally {
|
||
this.actionLoading = false;
|
||
}
|
||
},
|
||
async handleUpdate() {
|
||
await this.$confirm('将以外部账单数据更新运单及内部结算明细,是否继续?', '更新账单', {
|
||
type: 'warning',
|
||
});
|
||
this.actionLoading = true;
|
||
try {
|
||
await api.updateByMatch(this.currentId, {
|
||
id: this.currentId,
|
||
reconciliationMode: this.form.reconciliationMode,
|
||
internalDetails: this.serializeInternalRows(),
|
||
externalDetails: this.serializeExternalRows(),
|
||
});
|
||
await this.loadDetail();
|
||
this.$message.success('账单更新完成');
|
||
} finally {
|
||
this.actionLoading = false;
|
||
}
|
||
},
|
||
async handleComplete() {
|
||
if (
|
||
Number(this.form.differenceCount || 0) !== 0 ||
|
||
Number(this.form.differenceQuantity || 0) !== 0 ||
|
||
Number(this.form.differenceAmount || 0) !== 0
|
||
) {
|
||
return this.$message.warning('差异单数、差异货量和差异金额必须全部为0才可完成对账');
|
||
}
|
||
this.completionSummaryRowsData = this.buildCompletionSummaryRows.map(row => ({ ...row }));
|
||
this.completionDialog.visible = true;
|
||
},
|
||
async confirmCompletion() {
|
||
if (this.completionDialog.loading) return;
|
||
for (const row of this.completionSummaryRows) {
|
||
const adjustAmount = Number(row.adjustAmount || 0);
|
||
if (row.manualFlag === 1 && row._feeKind === '补款' && adjustAmount <= 0) {
|
||
return this.$message.warning('补款调整金额必须大于0');
|
||
}
|
||
if (row.manualFlag === 1 && row._feeKind === '扣款' && adjustAmount >= 0) {
|
||
return this.$message.warning('扣款调整金额必须小于0');
|
||
}
|
||
if (String(row.remark || '').length > 200) return this.$message.warning('备注不能超过200个字');
|
||
this.recalculateCompletionFee(row);
|
||
}
|
||
this.completionDialog.loading = true;
|
||
try {
|
||
this.actionLoading = true;
|
||
const data = this.unwrapData(
|
||
await api.completeWithData({
|
||
id: this.currentId,
|
||
reconciliationDate: this.form.reconciliationDate,
|
||
remark: this.form.remark,
|
||
internalDetails: this.serializeInternalRows(),
|
||
externalDetails: this.serializeExternalRows(),
|
||
feeSummary: this.completionSummaryRows.map(row => ({
|
||
id: row.id || undefined,
|
||
feeType: row.feeType,
|
||
feeItem: row.feeItem || row.name,
|
||
originalAmount: Number(row.originalAmount || 0),
|
||
adjustAmount: Number(row.adjustAmount || 0),
|
||
settlementAmount: Number(row.afterAmount || 0),
|
||
remark: row.remark || '',
|
||
manualFlag: Number(row.manualFlag || 0),
|
||
})),
|
||
})
|
||
);
|
||
this.form = { ...this.form, ...data };
|
||
this.internalDetails = (data.internalDetails || []).map(row => this.normalizeInternalRow(row));
|
||
this.externalDetails = (data.externalDetails || []).map((row, index) =>
|
||
this.normalizeExternalRow(row, index)
|
||
);
|
||
this.$message.success('对账完成');
|
||
this.completionDialog.visible = false;
|
||
this.visible = false;
|
||
this.$emit('success', this.currentId);
|
||
} finally {
|
||
this.completionDialog.loading = false;
|
||
this.actionLoading = false;
|
||
}
|
||
},
|
||
openFeeAdjustment(type) {
|
||
if (!Array.isArray(this.completionSummaryRowsData)) {
|
||
this.completionSummaryRowsData = this.buildCompletionSummaryRows.map(row => ({ ...row }));
|
||
}
|
||
this.completionSummaryRowsData.push({
|
||
id: null,
|
||
name: type,
|
||
feeType: '其他费用',
|
||
feeItem: type,
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
afterAmount: 0,
|
||
remark: '',
|
||
manualFlag: 1,
|
||
_feeKind: type,
|
||
_index: this.completionSummaryRowsData.length,
|
||
});
|
||
},
|
||
removeCompletionFee(row) {
|
||
if (row.manualFlag !== 1 || !Array.isArray(this.completionSummaryRowsData)) return;
|
||
const index = this.completionSummaryRowsData.indexOf(row);
|
||
if (index >= 0) this.completionSummaryRowsData.splice(index, 1);
|
||
},
|
||
recalculateCompletionFee(row) {
|
||
row.originalAmount = row.manualFlag === 1 ? 0 : Number(row.originalAmount || 0);
|
||
row.afterAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
|
||
},
|
||
getCompletionSummarySums({ columns, data }) {
|
||
return columns.map((column, index) => {
|
||
if (index === 0) return '合计';
|
||
const amountPropertyByIndex = {
|
||
2: 'originalAmount',
|
||
3: 'adjustAmount',
|
||
4: 'afterAmount',
|
||
};
|
||
const property = column.property || amountPropertyByIndex[index];
|
||
if (property === 'originalAmount' || property === 'adjustAmount' || property === 'afterAmount') {
|
||
const total = data.reduce((sum, row) => {
|
||
const value = property === 'afterAmount' ? row.afterAmount : row[property];
|
||
return sum + Number(value || 0);
|
||
}, 0);
|
||
const text = this.formatCompletionMoney(total);
|
||
const negativeAmount =
|
||
(property === 'adjustAmount' || property === 'afterAmount') && total < 0;
|
||
return negativeAmount
|
||
? h('span', { class: 'completion-dialog__negative', style: { color: '#f56c6c' } }, text)
|
||
: text;
|
||
}
|
||
return '';
|
||
});
|
||
},
|
||
sumFeeAmount(rows, name) {
|
||
return rows.reduce((sum, row) => {
|
||
const feeItems = row.feeItems || this.parseFeeItems(row.feeItemsJson);
|
||
const fallback = name === '运费' ? row.freightAmount ?? row.settlementAmount : 0;
|
||
return sum + Number(feeItems[name] ?? fallback ?? 0);
|
||
}, 0);
|
||
},
|
||
normalizeCompletionFee(item, index) {
|
||
const originalAmount = Number(
|
||
item.originalAmount ?? item.original ?? item.originalSettlementAmount ?? 0
|
||
);
|
||
const afterAmount = Number(
|
||
item.afterAmount ?? item.adjustedAmount ?? item.settlementAmount ?? item.totalAmount ?? originalAmount
|
||
);
|
||
return {
|
||
id: item.id,
|
||
feeType: item.feeType || '',
|
||
feeItem: item.feeItem || item.name || item.feeItemName || '',
|
||
name: item.name || item.feeItem || item.feeItemName || `费用${index + 1}`,
|
||
originalAmount,
|
||
adjustAmount: Number(item.adjustAmount ?? item.adjustmentAmount ?? afterAmount - originalAmount),
|
||
afterAmount,
|
||
remark: item.remark || item.adjustRemark || '',
|
||
manualFlag: Number(item.manualFlag || 0),
|
||
_feeKind: item.feeItem === '补款' || item.feeItem === '扣款' ? item.feeItem : '',
|
||
_index: index,
|
||
};
|
||
},
|
||
formatCompletionMoney(value) {
|
||
return Number(value || 0).toLocaleString('en-US', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
});
|
||
},
|
||
chooseImport() {
|
||
this.$refs.fileInput?.click();
|
||
},
|
||
async handleImport(event) {
|
||
const file = event.target.files?.[0];
|
||
event.target.value = '';
|
||
if (!file) return;
|
||
try {
|
||
const savedId = await this.handleSave(true);
|
||
if (!savedId) return;
|
||
const response =
|
||
this.form.reconciliationMode === 'cargo'
|
||
? await api.importCargo(this.currentId, file)
|
||
: await api.importVehicle(this.currentId, file);
|
||
const contentType = response.headers?.['content-type'] || response.data?.type || '';
|
||
if (contentType.includes('spreadsheetml') || contentType.includes('ms-excel')) {
|
||
downloadXls(
|
||
response.data,
|
||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||
);
|
||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||
} else {
|
||
const result = JSON.parse(await response.data.text());
|
||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||
this.$message.success('外部账单导入成功');
|
||
}
|
||
this.matchingStarted = false;
|
||
await this.loadDetail();
|
||
this.matchingStarted = false;
|
||
} catch (error) {
|
||
this.$message.error(error.message || '外部账单导入失败');
|
||
}
|
||
},
|
||
async downloadTemplate(mode) {
|
||
if (!this.form.formalSettlementId) {
|
||
return this.$message.warning('请先选择正式结算单');
|
||
}
|
||
const response = await api.template(mode, {
|
||
id: this.currentId,
|
||
formalSettlementId: this.form.formalSettlementId,
|
||
feeItems: this.internalFeeItemNames.join(','),
|
||
});
|
||
downloadXls(
|
||
response.data,
|
||
`${mode === 'cargo' ? '货物明细对账模板' : '整车总额对账模板'}.xlsx`
|
||
);
|
||
},
|
||
resetInternalQuery() {
|
||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||
this.internalFilterTick++;
|
||
},
|
||
openAdjust(row) {
|
||
this.adjustDialog = {
|
||
visible: true,
|
||
saving: false,
|
||
reason: '',
|
||
rows: (row._sourceRows || this.internalDetails.filter(
|
||
item => item.formalSettlementDetailId === row.formalSettlementDetailId
|
||
)).map(item => ({ ...item })),
|
||
};
|
||
},
|
||
async saveAdjust() {
|
||
if (!this.adjustDialog.reason.trim()) return this.$message.warning('请输入调整原因');
|
||
this.adjustDialog.saving = true;
|
||
try {
|
||
const rowsById = new Map(this.adjustDialog.rows.map(row => [row.id, row]));
|
||
this.internalDetails = this.internalDetails.map(row => {
|
||
const adjusted = rowsById.get(row.id);
|
||
if (!adjusted) return row;
|
||
return {
|
||
...row,
|
||
transportQuantity: adjusted.transportQuantity,
|
||
unitPrice: adjusted.unitPrice,
|
||
mileage: adjusted.mileage,
|
||
freightAmount: adjusted.freightAmount,
|
||
feeItems: adjusted.feeItems || this.parseFeeItems(adjusted.feeItemsJson),
|
||
feeItemsJson: JSON.stringify(adjusted.feeItems || this.parseFeeItems(adjusted.feeItemsJson)),
|
||
settlementAmount: adjusted.settlementAmount,
|
||
updateResult: 'manually_adjusted',
|
||
updateMessage: this.adjustDialog.reason.trim(),
|
||
};
|
||
});
|
||
this.adjustDialog.visible = false;
|
||
this.refreshLocalStats();
|
||
this.$message.success('调整已暂存');
|
||
} finally {
|
||
this.adjustDialog.saving = false;
|
||
}
|
||
},
|
||
async handleUnmatch(row) {
|
||
const matchedRows = (row._sourceRows || [row]).filter(item => item.matchResult === 'matched');
|
||
matchedRows.forEach(item => {
|
||
const external = this.externalDetails.find(candidate => candidate.id === item.matchedExternalId);
|
||
if (external) {
|
||
external.matchedInternalId = null;
|
||
external.matchStatus = 'unmatched';
|
||
external.suspectedDuplicate = false;
|
||
}
|
||
item.matchedExternalId = null;
|
||
item.matchedExternalLineNo = null;
|
||
item.matchResult = 'unmatched';
|
||
});
|
||
this.refreshLocalStats();
|
||
this.$message.success('取消匹配已暂存');
|
||
},
|
||
hasMatchedExternal(row) {
|
||
return row.matchedExternalLineNo !== null && row.matchedExternalLineNo !== undefined && row.matchedExternalLineNo !== '';
|
||
},
|
||
getInternalMatchTarget(row) {
|
||
const sourceRows = row._sourceRows || [row];
|
||
return sourceRows.find(item => item.id && item.matchResult !== 'matched') || sourceRows.find(item => item.id);
|
||
},
|
||
openExternalMatchDialog(row) {
|
||
if (!this.editable) return;
|
||
if (!this.getInternalMatchTarget(row)) return this.$message.warning('当前内部账单明细无法匹配');
|
||
this.manualMatchDialog.row = row;
|
||
this.manualMatchDialog.visible = true;
|
||
},
|
||
async handleManualMatch(externalRow) {
|
||
const internalRow = this.getInternalMatchTarget(this.manualMatchDialog.row);
|
||
if (!internalRow) return this.$message.warning('当前内部账单明细无法匹配');
|
||
this.manualMatchDialog.loading = true;
|
||
try {
|
||
const internal = this.internalDetails.find(item => item.id === internalRow.id);
|
||
const external = this.externalDetails.find(item => item.id === externalRow.id);
|
||
if (!internal || !external) return this.$message.warning('匹配明细不存在');
|
||
if (external.matchedInternalId && external.matchedInternalId !== internal.id) {
|
||
const oldInternal = this.internalDetails.find(item => item.id === external.matchedInternalId);
|
||
if (oldInternal) {
|
||
oldInternal.matchedExternalId = null;
|
||
oldInternal.matchedExternalLineNo = null;
|
||
oldInternal.matchResult = 'unmatched';
|
||
}
|
||
}
|
||
if (internal.matchedExternalId && internal.matchedExternalId !== external.id) {
|
||
const oldExternal = this.externalDetails.find(item => item.id === internal.matchedExternalId);
|
||
if (oldExternal) {
|
||
oldExternal.matchedInternalId = null;
|
||
oldExternal.matchStatus = 'unmatched';
|
||
oldExternal.suspectedDuplicate = false;
|
||
}
|
||
}
|
||
internal.matchedExternalId = external.id;
|
||
internal.matchedExternalLineNo = external.externalLineNo;
|
||
internal.matchResult = 'matched';
|
||
external.matchedInternalId = internal.id;
|
||
external.matchStatus = 'matched';
|
||
external.suspectedDuplicate = false;
|
||
this.manualMatchDialog.visible = false;
|
||
this.matchingStarted = true;
|
||
this.refreshLocalStats();
|
||
this.$message.success('匹配成功');
|
||
} finally {
|
||
this.manualMatchDialog.loading = false;
|
||
}
|
||
},
|
||
internalRowClassName({ row }) {
|
||
return this.matchingStarted && row.matchResult !== 'matched'
|
||
? 'reconciliation-editor__row--unmatched'
|
||
: '';
|
||
},
|
||
externalRowClassName({ row }) {
|
||
return this.matchingStarted && row.matchStatus !== 'matched'
|
||
? 'reconciliation-editor__row--unmatched'
|
||
: '';
|
||
},
|
||
externalRowKey(row) {
|
||
return String(row.id || row.externalLineNo || row._externalKey || '');
|
||
},
|
||
isExternalRowEditing(row) {
|
||
return Boolean(this.externalEditing[this.externalRowKey(row)]);
|
||
},
|
||
isExternalEditableColumn(prop) {
|
||
return !['externalLineNo', 'matchStatus'].includes(prop);
|
||
},
|
||
isExternalDateColumn(prop) {
|
||
return ['actualDepartureTime', 'actualCompletionTime'].includes(prop);
|
||
},
|
||
isExternalNumberColumn(prop) {
|
||
return ['transportQuantity', 'mileage', 'unitPrice', 'freightAmount', 'settlementAmount'].includes(prop);
|
||
},
|
||
openExternalAdjust(row) {
|
||
const key = this.externalRowKey(row);
|
||
this.externalEditSnapshots = {
|
||
...this.externalEditSnapshots,
|
||
[key]: { ...row },
|
||
};
|
||
this.externalEditing = { ...this.externalEditing, [key]: true };
|
||
},
|
||
cancelExternalAdjust(row) {
|
||
const key = this.externalRowKey(row);
|
||
const snapshot = this.externalEditSnapshots[key];
|
||
if (snapshot) Object.assign(row, snapshot);
|
||
const editing = { ...this.externalEditing };
|
||
const snapshots = { ...this.externalEditSnapshots };
|
||
delete editing[key];
|
||
delete snapshots[key];
|
||
this.externalEditing = editing;
|
||
this.externalEditSnapshots = snapshots;
|
||
},
|
||
async finishExternalAdjust(row) {
|
||
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||
const key = this.externalRowKey(row);
|
||
const editing = { ...this.externalEditing };
|
||
const snapshots = { ...this.externalEditSnapshots };
|
||
delete editing[key];
|
||
delete snapshots[key];
|
||
this.externalEditing = editing;
|
||
this.externalEditSnapshots = snapshots;
|
||
this.refreshLocalStats();
|
||
this.$message.success('调整已暂存');
|
||
},
|
||
normalizeExternalDateTime(value) {
|
||
if (!value) return null;
|
||
const text = String(value);
|
||
return text.length === 10 ? `${text}T00:00:00` : text;
|
||
},
|
||
refreshLocalStats() {
|
||
const internalQuantity = this.internalDetails.reduce(
|
||
(total, item) => total + Number(item.transportQuantity || 0),
|
||
0
|
||
);
|
||
const internalAmount = this.internalDetails.reduce(
|
||
(total, item) => total + Number(item.settlementAmount || 0),
|
||
0
|
||
);
|
||
const externalQuantity = this.externalDetails.reduce(
|
||
(total, item) => total + Number(item.transportQuantity || 0),
|
||
0
|
||
);
|
||
const externalAmount = this.externalDetails.reduce(
|
||
(total, item) => total + Number(item.settlementAmount || 0),
|
||
0
|
||
);
|
||
const matchedCount = this.internalDetails.filter(item => item.matchResult === 'matched').length;
|
||
const internalUnmatched = this.internalDetails.filter(item => item.matchResult !== 'matched').length;
|
||
const externalUnmatched = this.externalDetails.filter(item => item.matchStatus !== 'matched').length;
|
||
this.form = {
|
||
...this.form,
|
||
internalBillCount: this.internalDetails.length,
|
||
externalBillCount: this.externalDetails.length,
|
||
internalQuantity,
|
||
internalAmount,
|
||
externalQuantity,
|
||
externalAmount,
|
||
differenceCount:
|
||
Math.abs(this.internalDetails.length - this.externalDetails.length) +
|
||
Math.min(internalUnmatched, externalUnmatched),
|
||
differenceQuantity: Math.abs(internalQuantity - externalQuantity),
|
||
differenceAmount: Math.abs(internalAmount - externalAmount),
|
||
matchedCount,
|
||
unmatchedCount: internalUnmatched + externalUnmatched,
|
||
matchStatus:
|
||
this.internalDetails.length > 0 &&
|
||
this.internalDetails.length === this.externalDetails.length &&
|
||
matchedCount === this.internalDetails.length
|
||
? 'matched'
|
||
: matchedCount > 0
|
||
? 'partial'
|
||
: 'unmatched',
|
||
};
|
||
},
|
||
refreshExternalStats() {
|
||
this.refreshLocalStats();
|
||
},
|
||
matchName(value) {
|
||
return (
|
||
{
|
||
matched: '已匹配',
|
||
suspected_duplicate: '疑似重复',
|
||
partial: '部分匹配',
|
||
unmatched: '未匹配',
|
||
}[value] || '未匹配'
|
||
);
|
||
},
|
||
matchTagType(value) {
|
||
return value === 'matched'
|
||
? 'success'
|
||
: value === 'suspected_duplicate'
|
||
? 'danger'
|
||
: value === 'partial'
|
||
? 'warning'
|
||
: 'info';
|
||
},
|
||
updateName(value) {
|
||
return (
|
||
{
|
||
updated: '已更新',
|
||
skipped_multi_cargo: '跳过更新',
|
||
manually_adjusted: '手工调整',
|
||
partial_updated: '部分更新',
|
||
not_updated: '未更新',
|
||
}[value] ||
|
||
value ||
|
||
'未更新'
|
||
);
|
||
},
|
||
updateTagType(value) {
|
||
return value === 'updated' || value === 'manually_adjusted'
|
||
? 'success'
|
||
: value === 'skipped_multi_cargo' || value === 'partial_updated'
|
||
? 'warning'
|
||
: 'info';
|
||
},
|
||
displayValue(value) {
|
||
return value === null || value === undefined || value === '' ? '-' : value;
|
||
},
|
||
formatMatchedExternalLineNo(value) {
|
||
return Number(value) < 0 ? '' : this.displayValue(value);
|
||
},
|
||
formatTransportQuantity(value) {
|
||
return value === null || value === undefined || value === ''
|
||
? '-'
|
||
: Number(value || 0).toFixed(2);
|
||
},
|
||
formatMileage(value) {
|
||
return Number(value) === -1 ? '' : this.displayValue(value);
|
||
},
|
||
formatDate(value) {
|
||
if (value === null || value === undefined || value === '') return '-';
|
||
const date = this.$dayjs(value);
|
||
return date.isValid() ? date.format('YYYY-MM-DD') : this.displayValue(value);
|
||
},
|
||
transportTypeLabel(value) {
|
||
if (value === null || value === undefined || value === '') return '-';
|
||
return (
|
||
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
|
||
);
|
||
},
|
||
formatMoney(value, currency = 'RMB') {
|
||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||
},
|
||
formatNumber(value) {
|
||
return Number(value || 0).toFixed(2);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.reconciliation-editor__actions,
|
||
.reconciliation-editor__links {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.reconciliation-editor__stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.stat-card {
|
||
min-height: 104px;
|
||
border: 1px solid #eff1f7;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
background: #fff;
|
||
}
|
||
.stat-card b {
|
||
font-size: 20px;
|
||
color: #303133;
|
||
}
|
||
.stat-card span {
|
||
color: #606266;
|
||
}
|
||
.stat-card small {
|
||
color: #909399;
|
||
}
|
||
.stat-card--success b {
|
||
color: #67c23a;
|
||
}
|
||
.stat-card--danger b {
|
||
color: #f56c6c;
|
||
}
|
||
.reconciliation-editor__filter {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.reconciliation-editor__filter-item {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
.reconciliation-editor__filter-label {
|
||
flex: 0 0 auto;
|
||
margin-right: 8px;
|
||
color: #606266;
|
||
white-space: nowrap;
|
||
}
|
||
.reconciliation-editor__filter-item :deep(.el-input) {
|
||
min-width: 0;
|
||
}
|
||
.reconciliation-editor__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
.reconciliation-editor-shell--page {
|
||
display: block;
|
||
}
|
||
.reconciliation-editor--page {
|
||
padding-bottom: 12px;
|
||
}
|
||
.reconciliation-editor__page-actions {
|
||
position: fixed;
|
||
z-index: 20;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 230px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
min-height: 64px;
|
||
box-sizing: border-box;
|
||
padding: 12px 24px;
|
||
background: #fff;
|
||
border-top: 1px solid #eff1f7;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
.reconciliation-editor__page-actions .el-button + .el-button {
|
||
margin-left: 12px;
|
||
}
|
||
.reconciliation-editor__help {
|
||
color: #606266;
|
||
line-height: 1.8;
|
||
}
|
||
.dialog-form {
|
||
margin-top: 16px;
|
||
}
|
||
.completion-dialog__content {
|
||
padding: 4px 8px 0;
|
||
}
|
||
.completion-dialog__section {
|
||
padding: 20px 24px;
|
||
border: 1px solid #eff1f7;
|
||
background: #fff;
|
||
}
|
||
.completion-dialog__section + .completion-dialog__section {
|
||
margin-top: 16px;
|
||
}
|
||
.completion-dialog__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 18px 32px;
|
||
}
|
||
.completion-dialog__field {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
.completion-dialog__field--remark {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.completion-dialog__label {
|
||
flex: 0 0 auto;
|
||
min-width: 88px;
|
||
margin-right: 12px;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
.completion-dialog__value {
|
||
min-width: 0;
|
||
flex: 1;
|
||
box-sizing: border-box;
|
||
min-height: 36px;
|
||
padding: 8px 12px;
|
||
overflow: hidden;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
background: #f5f7fa;
|
||
color: #606266;
|
||
line-height: 18px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.completion-dialog__field--remark .completion-dialog__value {
|
||
white-space: normal;
|
||
}
|
||
.completion-dialog__section--fees {
|
||
position: relative;
|
||
padding-top: 48px;
|
||
}
|
||
.completion-dialog__section-title {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
.completion-dialog__section-title::before {
|
||
width: 4px;
|
||
height: 22px;
|
||
margin-right: 8px;
|
||
background: #409eff;
|
||
content: '';
|
||
}
|
||
.completion-dialog__fee-actions {
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 24px;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
.completion-dialog__fee-actions .el-button + .el-button {
|
||
margin-left: 0;
|
||
}
|
||
.completion-dialog__negative {
|
||
color: #f56c6c;
|
||
}
|
||
.completion-dialog__negative-input :deep(input) {
|
||
color: #f56c6c;
|
||
}
|
||
.completion-dialog__footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
}
|
||
.completion-dialog__footer .el-button + .el-button {
|
||
margin-left: 0;
|
||
}
|
||
.hidden-file {
|
||
display: none;
|
||
}
|
||
.reconciliation-editor :deep(.el-form-item) {
|
||
margin-bottom: 14px;
|
||
}
|
||
.reconciliation-editor :deep(.el-table) {
|
||
--el-table-border-color: #eff1f7;
|
||
}
|
||
.reconciliation-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||
background: #fafafa;
|
||
}
|
||
.reconciliation-editor :deep(.el-table__body tr.reconciliation-editor__row--unmatched > td.el-table__cell) {
|
||
color: #f56c6c;
|
||
background: #fff1f0 !important;
|
||
}
|
||
.reconciliation-editor
|
||
:deep(.el-table__body tr.reconciliation-editor__row--unmatched > td.el-table__cell .cell) {
|
||
color: #f56c6c;
|
||
}
|
||
.reconciliation-editor
|
||
:deep(.el-table__body tr.reconciliation-editor__row--unmatched .status-text) {
|
||
color: #f56c6c;
|
||
background: #fde2e2;
|
||
border-color: #fbc4c4;
|
||
}
|
||
@media (max-width: 1200px) {
|
||
.reconciliation-editor__stats {
|
||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||
}
|
||
.reconciliation-editor__filter {
|
||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||
}
|
||
.completion-dialog__grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
@media (max-width: 680px) {
|
||
.completion-dialog__grid {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
}
|
||
.completion-dialog__field--remark {
|
||
grid-column: auto;
|
||
}
|
||
.completion-dialog__section {
|
||
padding-right: 12px;
|
||
padding-left: 12px;
|
||
}
|
||
.completion-dialog__section-title {
|
||
left: 12px;
|
||
}
|
||
.completion-dialog__fee-actions {
|
||
right: 12px;
|
||
}
|
||
}
|
||
</style>
|