Files
tms-erp-web/src/views/settlement/components/transport-reconciliation-editor.vue
T
2026-08-28 15:30:24 +08:00

992 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog v-model="visible" :title="title" width="98%" top="2vh" append-to-body destroy-on-close>
<div v-loading="loading" class="reconciliation-editor">
<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>{{ 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">
<el-input v-model="internalQuery.documentNo" placeholder="单据号" clearable />
<el-input v-model="internalQuery.vehicleNo" placeholder="车号" clearable />
<el-input v-model="internalQuery.batchNo" placeholder="批次号" clearable />
<el-input v-model="internalQuery.cargoName" placeholder="货物名称" clearable />
<el-button @click="resetInternalQuery">重置</el-button>
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
</div>
<el-table :data="filteredInternalRows" border max-height="420">
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column
v-for="column in internalColumns"
: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'">
{{ formatMatchedExternalLineNo(row.matchedExternalLineNo) }}
</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.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 && row.matchResult === 'matched'"
type="primary"
@click="handleUnmatch(row)"
>取消匹配</el-link
>
<el-link
v-if="editable && row.matchResult !== 'matched'"
type="primary"
@click="openManualMatch(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 type="primary" plain @click="downloadTemplate('vehicle')"
>下载整车对账模板</el-button
>
<el-button 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">
<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>
<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.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="90" fixed="right" align="center">
<template #default="{ row }"
><el-link
v-if="editable && row.matchStatus !== 'matched'"
type="primary"
@click="openManualMatchByExternal(row)"
>匹配</el-link
></template
>
</el-table-column>
</el-table>
</section-card>
</div>
<template #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>
<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="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="manualDialog.visible" title="选择外部账单明细" width="86%" append-to-body>
<el-table :data="unmatchedExternalRows" border @row-click="manualDialog.selected = $event">
<el-table-column type="index" label="序号" width="64" />
<el-table-column prop="externalLineNo" label="外部行号" width="90" />
<el-table-column prop="vehicleNo" label="车牌号" width="110" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
<el-table-column prop="transportQuantity" label="运输量" width="110"
><template #default="{ row }">{{
formatTransportQuantity(row.transportQuantity)
}}</template></el-table-column
>
<el-table-column prop="settlementAmount" label="结算金额" width="130"
><template #default="{ row }">{{
formatMoney(row.settlementAmount)
}}</template></el-table-column
>
<el-table-column label="选择" width="80"
><template #default="{ row }"
><el-radio v-model="manualDialog.selected" :label="row">&nbsp;</el-radio></template
></el-table-column
>
</el-table>
<template #footer
><el-button @click="manualDialog.visible = false">取消</el-button
><el-button type="primary" @click="confirmManualMatch">确定</el-button></template
>
</el-dialog>
</el-dialog>
</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 { 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,
},
emits: ['update:modelValue', 'success'],
data() {
return {
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: '' },
manualDialog: { visible: false, internal: null, selected: null },
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);
},
},
editable() {
return !this.readonly;
},
title() {
return this.readonly
? '查看运输对账单'
: this.currentId
? '编辑运输对账单'
: '新增运输对账单';
},
filteredInternalRows() {
void this.internalFilterTick;
const query = this.internalQuery;
return this.internalDetails.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))
);
},
externalColumns() {
return this.form.reconciliationMode === 'cargo'
? externalCargoColumns
: externalVehicleColumns;
},
duplicateRows() {
return this.externalDetails.filter(
row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate'
);
},
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' && !row.suspectedDuplicate
);
},
},
watch: {
modelValue(value) {
if (value) this.initialize();
},
},
methods: {
emptyForm() {
return {
reconciliationNo: '',
reconciliationMode: 'vehicle',
formalSettlementId: null,
formalSettlementNo: '',
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.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 };
this.internalDetails =
data.internalDetails || data.internalBillDetails || data.internals || [];
this.externalDetails =
data.externalDetails || data.externalBillDetails || data.externals || [];
},
openFormalDialog() {
if (!this.editable) return;
this.formalDialog.visible = true;
this.formalDialog.page.current = 1;
this.loadFormalOptions();
},
async loadFormalOptions() {
this.formalDialog.loading = true;
try {
const params = {
settlementType: this.settlementType,
keyword: this.formalDialog.query.keyword,
};
const data = this.unwrapData(
await api.getFormalOptions(
this.formalDialog.page.current,
this.formalDialog.page.size,
params
)
);
let rows = data.records || [];
let total = data.total || 0;
if (!rows.length) {
const fallback = await formalSettlementApi.getList(
this.formalDialog.page.current,
this.formalDialog.page.size,
{ ...params, approvalStatus: 'approved' }
);
const fallbackData = this.unwrapData(fallback);
rows = fallbackData.records || [];
total = fallbackData.total || 0;
}
this.formalDialog.rows = rows;
this.formalDialog.page.total = total;
} 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) {
const formalSettlementChanged = this.form.formalSettlementId !== selected.id;
this.form = {
...this.form,
...selected,
formalSettlementId: selected.id,
formalSettlementNo: selected.formalSettlementNo,
reconciliationNo: this.form.reconciliationNo,
};
if (formalSettlementChanged) this.externalDetails = [];
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) => ({
...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');
}
return data;
} finally {
this.saving = false;
}
},
async handleMatch() {
if (!this.currentId) return this.$message.warning('请先保存草稿后再匹配');
this.actionLoading = true;
try {
await api.match(this.currentId);
await this.loadDetail();
this.$message.success('匹配完成');
} finally {
this.actionLoading = false;
}
},
async handleUpdate() {
await this.$confirm('将以外部账单匹配金额更新内部结算明细,是否继续?', '更新账单', {
type: 'warning',
});
const savedId = await this.handleSave(true);
if (!savedId) return;
this.actionLoading = true;
try {
await api.updateByMatch(this.currentId);
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才可完成对账');
}
await this.$confirm('完成后对账单不可修改和删除,是否继续?', '完成对账', {
type: 'warning',
});
const savedId = await this.handleSave(true);
if (!savedId) return;
this.actionLoading = true;
try {
await api.complete(this.currentId);
await this.loadDetail();
this.$message.success('对账完成');
this.visible = false;
this.$emit('success');
} finally {
this.actionLoading = false;
}
},
chooseImport() {
this.$refs.fileInput?.click();
},
async handleImport(event) {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
try {
if (!this.currentId) {
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('外部账单导入成功');
}
await this.loadDetail();
} catch (error) {
this.$message.error(error.message || '外部账单导入失败');
}
},
async downloadTemplate(mode) {
const response = await api.template(mode);
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: 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 {
for (const row of this.adjustDialog.rows)
await api.adjust({
...row,
reconciliationId: this.currentId,
updateMessage: this.adjustDialog.reason,
});
this.adjustDialog.visible = false;
await this.loadDetail();
this.$message.success('调整保存成功');
} finally {
this.adjustDialog.saving = false;
}
},
async handleUnmatch(row) {
await api.unmatch(row.id);
await this.loadDetail();
},
openManualMatch(row) {
this.manualDialog = { visible: true, internal: row, selected: null };
},
openManualMatchByExternal(row) {
this.manualDialog = { visible: true, internal: null, selected: row };
},
async confirmManualMatch() {
if (!this.manualDialog.selected) return this.$message.warning('请选择外部账单明细');
if (!this.manualDialog.internal) return this.$message.warning('请从内部账单行发起人工匹配');
await api.manualMatch({
reconciliationId: this.currentId,
internalId: this.manualDialog.internal.id,
externalId: this.manualDialog.selected.id,
});
this.manualDialog.visible = false;
await this.loadDetail();
this.$message.success('人工匹配成功');
},
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: '手工调整',
not_updated: '未更新',
}[value] ||
value ||
'未更新'
);
},
updateTagType(value) {
return value === 'updated' || value === 'manually_adjusted'
? 'success'
: value === 'skipped_multi_cargo'
? '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__pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.reconciliation-editor__help {
color: #606266;
line-height: 1.8;
}
.dialog-form {
margin-top: 16px;
}
.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;
}
@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));
}
}
</style>