a1da644c49
地图选点回填国家省市区;合同选择显示编号/名称;常用数据组织筛选修正;运单详情将-1费用字段显示为空。 Co-authored-by: Cursor <cursoragent@cursor.com>
2966 lines
113 KiB
Vue
2966 lines
113 KiB
Vue
<template>
|
||
<component
|
||
:is="editorContainer"
|
||
v-bind="editorContainerProps"
|
||
@update:model-value="visible = $event"
|
||
>
|
||
<div v-loading="loading" class="formal-editor" :class="{ 'formal-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 fields" :key="field.prop" :span="6">
|
||
<el-form-item :label="field.label" :prop="field.prop">
|
||
<el-date-picker
|
||
v-if="field.type === 'date' && editable"
|
||
v-model="form[field.prop]"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/>
|
||
<el-select
|
||
v-else-if="field.type === 'project' && editable"
|
||
v-model="form.projectId"
|
||
filterable
|
||
clearable
|
||
placeholder="请选择项目"
|
||
:disabled="details.length > 0 || sources.length > 0"
|
||
@change="handleProjectChange"
|
||
>
|
||
<el-option
|
||
v-for="item in projects"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
<el-select
|
||
v-else-if="field.type === 'contract' && editable"
|
||
v-model="form.contractId"
|
||
filterable
|
||
remote
|
||
:remote-method="loadContracts"
|
||
:disabled="!form.projectId || details.length > 0 || sources.length > 0"
|
||
placeholder="请选择合同"
|
||
@change="handleContractChange"
|
||
>
|
||
<el-option
|
||
v-for="item in contracts"
|
||
:key="item.id"
|
||
:label="formatContractOptionLabel(item)"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
<el-input-number
|
||
v-else-if="field.type === 'number' && editable"
|
||
v-model="form[field.prop]"
|
||
:min="0.000001"
|
||
:precision="2"
|
||
:controls="false"
|
||
/>
|
||
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
|
||
<span v-else-if="field.type === 'contract'">{{
|
||
displayValue(form.contractName)
|
||
}}</span>
|
||
<span v-else-if="field.type === 'project'">{{
|
||
displayValue(form.projectName)
|
||
}}</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
|
||
/>
|
||
<span v-else>{{ displayValue(form.remark) }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</section-card>
|
||
|
||
<section-card title="结算合计">
|
||
<template #extra>
|
||
<el-button v-if="editable" type="primary" plain @click="addSummaryFee">
|
||
添加费用
|
||
</el-button>
|
||
</template>
|
||
<el-table :data="summaryFees" border show-summary :summary-method="getSummarySums">
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column
|
||
v-for="column in summaryTableColumns"
|
||
:key="column.prop"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-if="editable && column.prop === 'feeType' && row.manualFlag === 1"
|
||
v-model="row.feeType"
|
||
filterable
|
||
placeholder="请选择"
|
||
@change="handleManualFeeTypeChange(row)"
|
||
>
|
||
<el-option
|
||
v-for="item in feeOptions"
|
||
:key="item.feeType"
|
||
:label="feeCategoryName(item.feeType)"
|
||
:value="item.feeType"
|
||
/>
|
||
</el-select>
|
||
<el-select
|
||
v-else-if="editable && column.prop === 'feeItem' && row.manualFlag === 1"
|
||
v-model="row.feeItem"
|
||
filterable
|
||
placeholder="请选择"
|
||
>
|
||
<el-option
|
||
v-for="name in manualFeeItems(row.feeType)"
|
||
:key="name"
|
||
:label="name"
|
||
:value="name"
|
||
/>
|
||
</el-select>
|
||
<el-input-number
|
||
v-else-if="editable && column.prop === 'adjustAmount'"
|
||
v-model="row.adjustAmount"
|
||
:class="{
|
||
'formal-editor__negative-amount': Number(row.adjustAmount || 0) < 0,
|
||
}"
|
||
:precision="2"
|
||
:step="0.01"
|
||
:controls="false"
|
||
@change="recalculateSummaryRow(row)"
|
||
/>
|
||
<el-input
|
||
v-else-if="editable && column.prop === 'remark'"
|
||
v-model="row.remark"
|
||
maxlength="50"
|
||
show-word-limit
|
||
/>
|
||
<span
|
||
v-else-if="isSummaryMoney(column.prop)"
|
||
:class="{
|
||
'formal-editor__negative-amount': isNegativeAdjustedAmount(row, column.prop),
|
||
}"
|
||
>
|
||
{{ formatMoney(row[column.prop]) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'feeType'">{{ feeCategoryName(row.feeType) }}</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="editable" label="操作" width="100" align="center">
|
||
<template #default="{ row, $index }">
|
||
<el-link v-if="row.manualFlag === 1" type="danger" @click="removeSummaryFee($index)">
|
||
删除
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card title="结算明细">
|
||
<template #extra>
|
||
<div class="formal-editor__actions">
|
||
<el-button type="primary" text @click="detailCollapsed = !detailCollapsed">
|
||
{{ detailCollapsed ? '展开明细' : '收起明细' }}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
<el-form
|
||
:model="detailQuery"
|
||
inline
|
||
label-position="right"
|
||
label-width="88px"
|
||
class="formal-editor__detail-filter"
|
||
@submit.prevent
|
||
>
|
||
<el-form-item label="单据号">
|
||
<el-input v-model="detailQuery.documentNo" clearable @keyup.enter="handleDetailQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="运单号">
|
||
<el-input v-model="detailQuery.waybillNo" clearable @keyup.enter="handleDetailQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="批次号">
|
||
<el-input v-model="detailQuery.batchNo" clearable @keyup.enter="handleDetailQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="货物名称">
|
||
<el-input v-model="detailQuery.cargoName" clearable @keyup.enter="handleDetailQuery" />
|
||
</el-form-item>
|
||
<el-form-item class="formal-editor__detail-filter-actions">
|
||
<el-button type="primary" @click="handleDetailQuery">查询</el-button>
|
||
<el-button @click="resetDetailQuery">重置</el-button>
|
||
<el-button type="primary" :disabled="!details.length" @click="exportDetails">
|
||
导出
|
||
</el-button>
|
||
<el-button v-if="editable" type="primary" @click="openDetailDialog">
|
||
选择结算明细
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table v-show="!detailCollapsed" :data="pagedDetails" border>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="64"
|
||
fixed="left"
|
||
align="center"
|
||
:index="detailIndexMethod"
|
||
/>
|
||
<el-table-column
|
||
v-for="column in detailTableColumns"
|
||
:key="column.prop"
|
||
v-bind="column"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||
<span v-else-if="column.prop === 'transportQuantity'">
|
||
{{ formatDetailTransportQuantity(row[column.prop]) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'mileage'">
|
||
{{ formatDetailMileage(row[column.prop]) }}
|
||
</span>
|
||
<span v-else-if="column.prop === 'transportType'">
|
||
{{ transportTypeName(row[column.prop]) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="formal-editor__links">
|
||
<el-link type="primary" @click="openAdjustDialog(row)">调整</el-link>
|
||
<el-link v-if="!row.sourcePreSettlementId" type="danger" @click="removeDetail(row)"
|
||
>踢出</el-link
|
||
>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div v-show="!detailCollapsed" class="formal-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="detailPage.current"
|
||
v-model:page-size="detailPage.size"
|
||
:total="filteredDetails.length"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleDetailPageSizeChange"
|
||
/>
|
||
</div>
|
||
</section-card>
|
||
|
||
<section-card>
|
||
<template #title>
|
||
<span>附件</span>
|
||
<span v-if="missingAttachmentTypeText" class="formal-editor__attachment-missing">
|
||
{{ missingAttachmentTypeText }}
|
||
</span>
|
||
</template>
|
||
<template #extra>
|
||
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
|
||
批量下载
|
||
</el-button>
|
||
</template>
|
||
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column label="类型" min-width="180" align="center">
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-if="editable"
|
||
v-model="row.type"
|
||
filterable
|
||
placeholder="请选择类型"
|
||
@change="sortAttachments"
|
||
>
|
||
<el-option
|
||
v-for="item in attachmentTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
<span v-else>{{ attachmentTypeName(row.type) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件名" min-width="220" align="left" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-link
|
||
:disabled="!getAttachmentUrl(row)"
|
||
type="primary"
|
||
@click="previewAttachment(row)"
|
||
>
|
||
{{ row.originalName || row.name || '-' }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
{{ displayValue(row.uploadUserName) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadTime" label="上传时间" min-width="170" align="center">
|
||
<template #default="{ row }">
|
||
{{ displayValue(row.uploadTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="size" label="文件大小" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
{{ displayValue(row.size) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="editable" label="操作" width="90" align="center">
|
||
<template #default="{ $index }">
|
||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div v-if="editable" class="formal-editor__attachment-upload">
|
||
<vehicle-attachment-upload
|
||
v-model="attachmentUploadFiles"
|
||
:multiple="true"
|
||
:limit="20"
|
||
:max-size="500"
|
||
:file-types="attachmentFileTypes"
|
||
button-text="上传附件"
|
||
:show-file-list="false"
|
||
:show-uploading="true"
|
||
@success="handleAttachmentUploadSuccess"
|
||
/>
|
||
</div>
|
||
</section-card>
|
||
|
||
<section-card title="发票信息">
|
||
<div class="formal-editor__invoice-claim">
|
||
<el-form inline label-position="right" label-width="auto" @submit.prevent>
|
||
<el-form-item label="发票号码">
|
||
<el-input
|
||
v-model="invoiceClaimNo"
|
||
clearable
|
||
maxlength="32"
|
||
placeholder="请输入发票号码"
|
||
@keyup.enter="openInvoiceClaimDialog"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button
|
||
type="primary"
|
||
:loading="invoiceClaimLoading"
|
||
@click="openInvoiceClaimDialog"
|
||
>
|
||
认领查询
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<el-table :data="invoices" border>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column prop="invoiceNo" label="发票号" min-width="150" align="center" />
|
||
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.invoiceDate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.invoiceType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="taxRate" label="税率" min-width="100" align="center">
|
||
<template #default="{ row }">
|
||
{{ row.taxRate === null || row.taxRate === undefined ? '-' : `${row.taxRate}%` }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="invoiceAmount"
|
||
label="发票金额(含税)"
|
||
min-width="150"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="availableInvoiceAmount"
|
||
label="可匹配发票金额(含税)"
|
||
min-width="180"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="matchedAmount"
|
||
label="匹配结算单金额(含税)"
|
||
min-width="190"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-if="editable"
|
||
v-model="row.matchedAmount"
|
||
:min="0"
|
||
:max="Number(row.availableInvoiceAmount || 0)"
|
||
:precision="2"
|
||
:controls="false"
|
||
/>
|
||
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="220" align="center">
|
||
<template #default="{ row, $index }">
|
||
<div class="formal-editor__links">
|
||
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
|
||
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
|
||
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<el-dialog
|
||
v-model="invoiceClaimDialog.visible"
|
||
title="查询可认领发票"
|
||
width="92%"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<el-table
|
||
ref="invoiceClaimTable"
|
||
v-loading="invoiceClaimDialog.loading"
|
||
:data="invoiceClaimPagedRows"
|
||
row-key="invoiceNo"
|
||
border
|
||
height="520"
|
||
@selection-change="invoiceClaimDialog.selected = $event"
|
||
>
|
||
<el-table-column
|
||
type="selection"
|
||
width="52"
|
||
fixed="left"
|
||
align="center"
|
||
reserve-selection
|
||
/>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column prop="invoiceNo" label="发票号" min-width="180" align="center" />
|
||
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center" />
|
||
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center" />
|
||
<el-table-column prop="issuerName" label="开票单位" min-width="180" align="center" />
|
||
<el-table-column prop="receiverName" label="受票单位" min-width="180" align="center" />
|
||
<el-table-column
|
||
prop="invoiceAmount"
|
||
label="发票金额(含税)"
|
||
min-width="160"
|
||
align="right"
|
||
>
|
||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="matchedAmount" label="认领金额" min-width="140" align="right">
|
||
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="formal-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="invoiceClaimDialog.page.current"
|
||
v-model:page-size="invoiceClaimDialog.page.size"
|
||
:total="invoiceClaimDialog.page.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="invoiceClaimDialog.visible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="invoiceClaimDialog.confirming"
|
||
@click="confirmInvoiceClaims"
|
||
>确认认领</el-button
|
||
>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<section-card
|
||
v-if="readonly"
|
||
:title="form.settlementType === 'receivable' ? '收款信息' : '付款信息'"
|
||
>
|
||
<el-table
|
||
v-if="form.settlementType === 'receivable'"
|
||
:data="receiptClaims"
|
||
border
|
||
empty-text="暂无收款认领数据"
|
||
>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column
|
||
prop="receiptNoticeNo"
|
||
label="认领通知单"
|
||
min-width="160"
|
||
align="center"
|
||
/>
|
||
<el-table-column prop="payerName" label="付款人" min-width="150" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.payerName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="receiptAmount" label="收款金额" min-width="130" align="center">
|
||
<template #default="{ row }">{{ formatMoney(row.receiptAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="allocatedReceiptAmount"
|
||
label="本次认领金额"
|
||
min-width="145"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ formatMoney(row.allocatedReceiptAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="transactionTime" label="交易时间" min-width="170" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.transactionTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="claimerName" label="认领人" min-width="120" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.claimerName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="claimDate" label="认领日期" min-width="130" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.claimDate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="claimStatusName" label="认领状态" min-width="120" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.claimStatusName) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-table v-else :data="paymentApplications" border>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column prop="paymentTypeName" label="付款类型" min-width="120" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.paymentTypeName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="付款方式" min-width="140" align="center">
|
||
<template #default="{ row }">{{ paymentMethodName(row.paymentMethod) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="paymentNo" label="单据号" min-width="160" align="center" />
|
||
<el-table-column prop="appliedAmount" label="申请付款金额" min-width="145" align="center">
|
||
<template #default="{ row }">{{ formatMoney(row.appliedAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="paidAmount" label="已付款金额" min-width="130" align="center">
|
||
<template #default="{ row }">{{ formatMoney(row.paidAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="approvalStatusName"
|
||
label="单据状态"
|
||
min-width="120"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ displayValue(row.approvalStatusName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.kingdeeBillNo) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createUserName" label="创建人" min-width="120" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.createUserName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.createTime) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card v-if="readonly" title="结算调整单">
|
||
<el-table :data="adjustments" border>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column prop="adjustmentNo" label="调整单号" min-width="170" align="center" />
|
||
<el-table-column prop="adjustmentAmount" label="调整金额" min-width="130" align="center">
|
||
<template #default="{ row }">{{ formatMoney(row.adjustmentAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="approvalStatusName"
|
||
label="单据状态"
|
||
min-width="120"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ displayValue(row.approvalStatusName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="220" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.remark) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.kingdeeBillNo) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createUserName" label="创建人" min-width="120" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.createUserName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.createTime) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card v-if="readonly" title="变更记录">
|
||
<el-table :data="changeRecords" border>
|
||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||
<el-table-column prop="changeType" label="变更类型" min-width="150" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.changeType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="lineNo" label="行号" min-width="90" align="center">
|
||
<template #default="{ row }">{{ changeRecordLineNo(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="operationType" label="类型" min-width="110" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.operationType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="changeContent" label="变更内容" min-width="360" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.changeContent) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="changeReason" label="变更原因" min-width="220" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.changeReason) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="operatorName" label="操作人" min-width="120" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.operatorName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="changeTime" label="变更时间" min-width="170" align="center">
|
||
<template #default="{ row }">{{ displayValue(row.changeTime) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
</div>
|
||
<template v-if="!pageMode" #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button
|
||
v-if="editable && !recordId"
|
||
type="primary"
|
||
plain
|
||
:loading="draftSaving"
|
||
@click="handleDraftSave"
|
||
>
|
||
保存
|
||
</el-button>
|
||
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
|
||
>提交</el-button
|
||
>
|
||
</template>
|
||
<div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button
|
||
v-if="editable && !recordId"
|
||
type="primary"
|
||
plain
|
||
:loading="draftSaving"
|
||
@click="handleDraftSave"
|
||
>
|
||
保存
|
||
</el-button>
|
||
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave">
|
||
提交
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
|
||
<el-form
|
||
:model="candidate.query"
|
||
inline
|
||
label-position="right"
|
||
label-width="88px"
|
||
class="formal-editor__candidate-search"
|
||
>
|
||
<el-form-item label="预结算单号"
|
||
><el-input v-model="candidate.query.preSettlementNo" clearable
|
||
/></el-form-item>
|
||
<el-form-item label="合同编号"
|
||
><el-input v-model="candidate.query.contractNo" clearable
|
||
/></el-form-item>
|
||
<el-form-item
|
||
><el-button @click="resetCandidates">重置</el-button
|
||
><el-button type="primary" @click="loadCandidates">查询</el-button></el-form-item
|
||
>
|
||
</el-form>
|
||
<el-table
|
||
ref="candidateTable"
|
||
v-loading="candidate.loading"
|
||
:data="candidate.rows"
|
||
border
|
||
@selection-change="candidate.selected = $event"
|
||
>
|
||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||
<el-table-column
|
||
v-for="column in candidateTableColumns"
|
||
:key="column.prop"
|
||
v-bind="column"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||
<span v-else-if="column.prop === 'transportType'">
|
||
{{ transportTypeName(row[column.prop]) }}
|
||
</span>
|
||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="formal-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="candidate.page.current"
|
||
v-model:page-size="candidate.page.size"
|
||
:total="candidate.page.total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="loadCandidates"
|
||
@size-change="loadCandidates"
|
||
/>
|
||
</div>
|
||
<template #footer
|
||
><el-button @click="candidate.visible = false">取消</el-button
|
||
><el-button type="primary" @click="confirmCandidates">确定</el-button></template
|
||
>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="detailCandidate.visible" title="选择结算明细" width="92%" append-to-body>
|
||
<el-form
|
||
:model="detailCandidate.query"
|
||
inline
|
||
label-position="right"
|
||
label-width="88px"
|
||
class="formal-editor__detail-search"
|
||
>
|
||
<el-form-item label="批次号"
|
||
><el-input v-model="detailCandidate.query.batchNo" clearable
|
||
/></el-form-item>
|
||
<el-form-item label="创建时间"
|
||
><el-date-picker
|
||
v-model="detailCandidate.query.createTimeRange"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
format="YYYY-MM-DD"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/></el-form-item>
|
||
<el-form-item
|
||
><el-button @click="resetDetailCandidates">重置</el-button
|
||
><el-button type="primary" @click="loadDetailCandidates">查询</el-button></el-form-item
|
||
>
|
||
</el-form>
|
||
<el-table
|
||
v-loading="detailCandidate.loading"
|
||
:data="detailCandidate.rows"
|
||
border
|
||
@selection-change="detailCandidate.selected = $event"
|
||
>
|
||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||
<el-table-column
|
||
v-for="column in candidateDetailTableColumns"
|
||
:key="column.prop"
|
||
v-bind="column"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }"
|
||
><span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||
>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="formal-editor__pagination">
|
||
<el-pagination
|
||
v-model:current-page="detailCandidate.page.current"
|
||
v-model:page-size="detailCandidate.page.size"
|
||
:total="detailCandidate.page.total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="loadDetailCandidates"
|
||
@size-change="loadDetailCandidates"
|
||
/>
|
||
</div>
|
||
<template #footer
|
||
><el-button @click="detailCandidate.visible = false">取消</el-button
|
||
><el-button type="primary" @click="confirmDetailCandidates">确定</el-button></template
|
||
>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="adjust.visible"
|
||
title="结算明细调整"
|
||
width="96%"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<el-table v-loading="adjust.loading" :data="adjust.rows" border>
|
||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
|
||
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
|
||
<el-table-column label="计费规则" min-width="190" align="center">
|
||
<template #default="{ row }">
|
||
<div class="formal-editor__billing-rule-cell">
|
||
<span>{{ displayValue(billingRuleName(row)) }}</span>
|
||
<el-icon
|
||
class="formal-editor__billing-rule-icon"
|
||
title="查看计费规则详情"
|
||
@click="openBillingRuleInfo(row)"
|
||
>
|
||
<InfoFilled />
|
||
</el-icon>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="运输总量" min-width="150" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.transportQuantity"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateAdjustRow(row, 'transportQuantity')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="里程(KM)" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.mileage"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateAdjustRow(row, 'mileage')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="运输单价" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.unitPrice"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateAdjustRow(row, 'unitPrice')"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-for="feeItem in adjustFeeItemNames"
|
||
:key="feeItem"
|
||
:label="feeItem"
|
||
min-width="140"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.feeItems[feeItem]"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
@change="recalculateAdjustRow(row, feeItem)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结算金额(含税)" min-width="170" align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number v-model="row.settlementAmountTax" :precision="2" :controls="false" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="200" align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.remark" maxlength="200" />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-form
|
||
:model="adjust"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="formal-editor__adjust-reason"
|
||
>
|
||
<el-form-item label="调整原因" required>
|
||
<el-input v-model="adjust.reason" maxlength="200" show-word-limit />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="adjust.visible = false">取消</el-button>
|
||
<el-button type="primary" :loading="adjust.saving" @click="saveAdjustment">
|
||
保存
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="billingRuleDialog.visible"
|
||
title="计费规则详情"
|
||
width="720px"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<template v-if="billingRuleDialog.rule">
|
||
<div class="dialog-section-title">计费规则</div>
|
||
<el-descriptions :column="2" border class="formal-editor__billing-rule-detail">
|
||
<el-descriptions-item label="规则名称">{{
|
||
displayValue(billingRuleDialog.rule.name)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="费用类型">{{
|
||
displayValue(billingRuleDialog.rule.feeType)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="费用项">{{
|
||
displayValue(billingRuleDialog.rule.feeItem)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="计费要素">{{
|
||
displayValue(billingRuleDialog.rule.billingElement)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="计费类型">{{
|
||
displayValue(billingRuleDialog.rule.billingType)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="计费单位">{{
|
||
displayValue(billingRuleDialog.rule.billingUnit)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="单价">{{
|
||
displayValue(billingRuleDialog.rule.unitPrice)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="最低计费量">{{
|
||
displayValue(billingRuleDialog.rule.minimumBillingWeight)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="计费区间">{{
|
||
billingRuleRanges(billingRuleDialog.rule)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="2">{{
|
||
displayValue(billingRuleDialog.rule.remark)
|
||
}}</el-descriptions-item>
|
||
</el-descriptions>
|
||
<div class="dialog-section-title formal-editor__billing-rule-match-title">匹配条件</div>
|
||
<el-descriptions :column="2" border class="formal-editor__billing-rule-detail">
|
||
<el-descriptions-item label="起运地">{{
|
||
displayValue(billingRuleDialog.rule.matchCondition.origin)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="目的地">{{
|
||
displayValue(billingRuleDialog.rule.matchCondition.destination)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="运输方式">{{
|
||
displayValue(billingRuleDialog.rule.matchCondition.transportMode)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="货物类型">{{
|
||
displayValue(billingRuleDialog.rule.matchCondition.cargoType)
|
||
}}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</template>
|
||
<el-empty v-else description="暂无计费规则详情" />
|
||
<template #footer>
|
||
<el-button type="primary" @click="billingRuleDialog.visible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</component>
|
||
</template>
|
||
|
||
<script>
|
||
import { getMkPublicDetail } from '@/api/mk-process';
|
||
import {
|
||
adjustDetail,
|
||
getCandidateDetails,
|
||
getCandidates,
|
||
getContractOptions,
|
||
getDetail,
|
||
getDetailFees,
|
||
getFeeOptions,
|
||
getNextNo,
|
||
getReceiptClaims,
|
||
claimInvoices as claimInvoicesApi,
|
||
save,
|
||
} from '@/api/settlement/formalSettlement';
|
||
import {
|
||
getDetail as getPreSettlementDetail,
|
||
getDetailFees as getPreSettlementDetailFees,
|
||
} from '@/api/settlement/preSettlement';
|
||
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||
import {
|
||
createFormalSettlementForm,
|
||
formalSettlementFormFields,
|
||
} from '@/option/settlement/formalSettlementForm';
|
||
import {
|
||
candidateColumns,
|
||
candidateDetailColumns,
|
||
detailColumns,
|
||
sourceColumns,
|
||
summaryColumns,
|
||
} from '@/option/settlement/formalSettlementTable';
|
||
import { getDictionary } from '@/api/system/dictbiz';
|
||
import { h } from 'vue';
|
||
import { InfoFilled } from '@element-plus/icons-vue';
|
||
import { mapGetters } from 'vuex';
|
||
import { formatContractOptionLabel } from '@/utils/contract';
|
||
import { downloadFileByUrl } from '@/utils/util';
|
||
import * as XLSX from 'xlsx';
|
||
|
||
export default {
|
||
name: 'FormalSettlementEditor',
|
||
components: { InfoFilled },
|
||
props: {
|
||
modelValue: Boolean,
|
||
recordId: [String, Number],
|
||
readonly: Boolean,
|
||
pageMode: Boolean,
|
||
initialData: {
|
||
type: Object,
|
||
default: null,
|
||
},
|
||
publicMode: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
emits: ['update:modelValue', 'success'],
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
draftSaving: false,
|
||
saving: false,
|
||
form: createFormalSettlementForm(),
|
||
sources: [],
|
||
details: [],
|
||
summaryFees: [],
|
||
paymentApplications: [],
|
||
receiptClaims: [],
|
||
adjustments: [],
|
||
changeRecords: [],
|
||
attachments: [],
|
||
selectedAttachmentRows: [],
|
||
invoices: [],
|
||
invoiceClaimNo: '',
|
||
invoiceClaimLoading: false,
|
||
invoiceClaimDialog: {
|
||
visible: false,
|
||
loading: false,
|
||
confirming: false,
|
||
rows: [],
|
||
selected: [],
|
||
page: { current: 1, size: 10, total: 0 },
|
||
},
|
||
attachmentUploadFiles: [],
|
||
attachmentTypeOptions: [],
|
||
attachmentFileTypes: [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
'rar',
|
||
],
|
||
contracts: [],
|
||
allContracts: [],
|
||
projects: [],
|
||
feeOptions: [],
|
||
feeCategoryOptions: [],
|
||
transportTypeOptions: [],
|
||
fields: formalSettlementFormFields,
|
||
sourceTableColumns: sourceColumns,
|
||
summaryTableColumns: summaryColumns,
|
||
detailTableColumns: detailColumns,
|
||
candidateTableColumns: candidateColumns,
|
||
candidateDetailTableColumns: candidateDetailColumns,
|
||
rules: {
|
||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||
contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
|
||
exchangeRateDate: [{ required: true, message: '请选择汇率日期', trigger: 'change' }],
|
||
exchangeRate: [{ required: true, message: '请输入结算汇率', trigger: 'blur' }],
|
||
},
|
||
candidate: {
|
||
visible: false,
|
||
loading: false,
|
||
query: {},
|
||
rows: [],
|
||
selected: [],
|
||
page: { current: 1, size: 10, total: 0 },
|
||
},
|
||
detailCandidate: {
|
||
visible: false,
|
||
loading: false,
|
||
query: { batchNo: '', createTimeRange: [] },
|
||
rows: [],
|
||
selected: [],
|
||
page: { current: 1, size: 10, total: 0 },
|
||
},
|
||
adjust: {
|
||
visible: false,
|
||
loading: false,
|
||
saving: false,
|
||
detailId: null,
|
||
sourceDetailId: null,
|
||
feeKey: '',
|
||
reason: '',
|
||
rows: [],
|
||
targetRow: null,
|
||
},
|
||
detailFeeSnapshots: {},
|
||
pendingAdjustments: {},
|
||
billingRuleDialog: {
|
||
visible: false,
|
||
rule: null,
|
||
},
|
||
detailCollapsed: false,
|
||
detailQuery: {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
},
|
||
appliedDetailQuery: {
|
||
documentNo: '',
|
||
waybillNo: '',
|
||
batchNo: '',
|
||
cargoName: '',
|
||
},
|
||
detailPage: {
|
||
current: 1,
|
||
size: 10,
|
||
},
|
||
};
|
||
},
|
||
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: 'formal-editor-shell formal-editor-shell--page' };
|
||
}
|
||
return {
|
||
modelValue: this.visible,
|
||
title: this.title,
|
||
width: '96%',
|
||
top: '2vh',
|
||
appendToBody: true,
|
||
destroyOnClose: true,
|
||
};
|
||
},
|
||
editable() {
|
||
return !this.readonly;
|
||
},
|
||
title() {
|
||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||
},
|
||
summaryTotal() {
|
||
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
|
||
},
|
||
filteredDetails() {
|
||
return this.details.filter(row =>
|
||
Object.entries(this.appliedDetailQuery).every(([field, keyword]) => {
|
||
if (!keyword?.trim()) return true;
|
||
return String(row[field] || '')
|
||
.toLocaleLowerCase()
|
||
.includes(keyword.trim().toLocaleLowerCase());
|
||
})
|
||
);
|
||
},
|
||
pagedDetails() {
|
||
const list = this.filteredDetails;
|
||
const size = Number(this.detailPage.size) || 10;
|
||
const current = Math.max(Number(this.detailPage.current) || 1, 1);
|
||
const start = (current - 1) * size;
|
||
return list.slice(start, start + size);
|
||
},
|
||
missingAttachmentTypeText() {
|
||
const missing = this.getMissingAttachmentTypes();
|
||
return missing.length ? `未上传:${missing.join('、')}` : '';
|
||
},
|
||
adjustFeeItemNames() {
|
||
const names = new Set();
|
||
this.adjust.rows.forEach(row => {
|
||
Object.keys(row.feeItems || {}).forEach(name => names.add(name));
|
||
});
|
||
return Array.from(names);
|
||
},
|
||
invoiceClaimPagedRows() {
|
||
const { current, size } = this.invoiceClaimDialog.page;
|
||
const start = (current - 1) * size;
|
||
return this.invoiceClaimDialog.rows.slice(start, start + size);
|
||
},
|
||
},
|
||
watch: {
|
||
modelValue: {
|
||
immediate: true,
|
||
handler(value) {
|
||
if (value) this.initialize();
|
||
},
|
||
},
|
||
filteredDetails: {
|
||
handler(list) {
|
||
const size = Number(this.detailPage.size) || 10;
|
||
const maxPage = Math.max(1, Math.ceil((list?.length || 0) / size) || 1);
|
||
if (this.detailPage.current > maxPage) {
|
||
this.detailPage.current = maxPage;
|
||
}
|
||
},
|
||
immediate: true,
|
||
},
|
||
summaryTotal(value) {
|
||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||
this.form.localSettlementAmount = (
|
||
Number(value || 0) * Number(this.form.exchangeRate || 1)
|
||
).toFixed(2);
|
||
},
|
||
'form.exchangeRate'(value) {
|
||
this.form.localSettlementAmount = (
|
||
Number(this.summaryTotal || 0) * Number(value || 1)
|
||
).toFixed(2);
|
||
},
|
||
},
|
||
methods: {
|
||
formatContractOptionLabel,
|
||
async loadFormalDetail(id) {
|
||
const response = this.publicMode
|
||
? await getMkPublicDetail('formal-settlement', id)
|
||
: await getDetail(id);
|
||
const data = this.unwrapData(response);
|
||
this.form = {
|
||
...createFormalSettlementForm(),
|
||
...data,
|
||
sourcePreSettlementIds: (data.sources || []).map(item => item.preSettlementId),
|
||
sourceDetailIds: (data.details || [])
|
||
.filter(item => !item.sourcePreSettlementId)
|
||
.map(item => item.sourceDetailId),
|
||
};
|
||
this.sources = data.sources || [];
|
||
this.details = data.details || [];
|
||
this.detailPage.current = 1;
|
||
this.summaryFees = data.summaryFees || [];
|
||
// 服务端数据已刷新,费用快照与暂存调整全部作废。
|
||
this.detailFeeSnapshots = {};
|
||
this.pendingAdjustments = {};
|
||
this.paymentApplications = data.paymentApplications || [];
|
||
if (this.readonly && this.form.settlementType === 'receivable' && !this.publicMode) {
|
||
this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || [];
|
||
}
|
||
this.adjustments = data.adjustments || [];
|
||
this.changeRecords = data.changeRecords || [];
|
||
this.attachments = this.parseAttachments(data.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
this.invoices = data.invoices || [];
|
||
this.sortAttachments();
|
||
this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
|
||
},
|
||
async initialize() {
|
||
const newRecordAudit = this.recordId
|
||
? null
|
||
: {
|
||
createUserName: this.userInfo?.realName || this.userInfo?.userName || '',
|
||
createTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||
};
|
||
this.form = createFormalSettlementForm();
|
||
if (['receivable', 'payable'].includes(this.initialData?.settlementType)) {
|
||
this.form.settlementType = this.initialData.settlementType;
|
||
this.form.settlementTypeName =
|
||
this.initialData.settlementType === 'receivable' ? '应收' : '应付';
|
||
}
|
||
if (newRecordAudit) Object.assign(this.form, newRecordAudit);
|
||
this.sources = [];
|
||
this.details = [];
|
||
this.summaryFees = [];
|
||
this.detailFeeSnapshots = {};
|
||
this.pendingAdjustments = {};
|
||
this.paymentApplications = [];
|
||
this.receiptClaims = [];
|
||
this.adjustments = [];
|
||
this.changeRecords = [];
|
||
this.attachments = [];
|
||
this.selectedAttachmentRows = [];
|
||
this.invoices = [];
|
||
this.invoiceClaimNo = '';
|
||
this.invoiceClaimDialog.visible = false;
|
||
this.invoiceClaimDialog.rows = [];
|
||
this.invoiceClaimDialog.selected = [];
|
||
this.attachmentUploadFiles = [];
|
||
this.billingRuleDialog = {
|
||
visible: false,
|
||
rule: null,
|
||
};
|
||
this.detailCollapsed = false;
|
||
this.detailPage = { current: 1, size: 10 };
|
||
this.resetDetailQuery(false);
|
||
if (!this.publicMode) {
|
||
await Promise.all([
|
||
this.loadAllContracts(),
|
||
this.loadFeeOptions(),
|
||
this.loadFeeCategoryOptions(),
|
||
this.loadAttachmentTypeOptions(),
|
||
this.loadTransportTypeOptions(),
|
||
]);
|
||
}
|
||
if (!this.recordId) {
|
||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||
if (this.initialData) await this.applyInitialData();
|
||
Object.assign(this.form, newRecordAudit);
|
||
await this.refreshFormalSettlementNo();
|
||
return;
|
||
}
|
||
this.loading = true;
|
||
try {
|
||
await this.loadFormalDetail(this.recordId);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
async refreshFormalSettlementNo() {
|
||
if (this.form.id || !this.form.settlementType) {
|
||
if (!this.form.id) this.form.formalSettlementNo = '';
|
||
return;
|
||
}
|
||
const response = await getNextNo(this.form.settlementType);
|
||
this.form.formalSettlementNo = this.unwrapData(response) || '';
|
||
},
|
||
async applyInitialData() {
|
||
const sourcePreSettlements = Array.isArray(this.initialData?.sourcePreSettlements)
|
||
? this.initialData.sourcePreSettlements
|
||
: [];
|
||
if (sourcePreSettlements.length) {
|
||
await this.applyInitialSources(sourcePreSettlements);
|
||
return;
|
||
}
|
||
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
||
if (!rows.length) return;
|
||
const first = rows[0];
|
||
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
|
||
const matchedContract = this.filterContracts(this.allContracts, null, settlementType).find(
|
||
item =>
|
||
(first.contractId && String(item.id) === String(first.contractId)) ||
|
||
(first.contractNo && String(item.contractNo) === String(first.contractNo))
|
||
);
|
||
const contractId = first.contractId || matchedContract?.id || null;
|
||
const projectId = first.projectId || matchedContract?.projectId || null;
|
||
const projectName = first.projectName || matchedContract?.projectName || '';
|
||
this.contracts = this.filterContracts(this.allContracts, projectId, settlementType);
|
||
if (projectId && !this.projects.some(item => String(item.id) === String(projectId))) {
|
||
this.projects.push({ id: projectId, name: projectName });
|
||
}
|
||
if (!this.contracts.some(item => String(item.id) === String(contractId))) {
|
||
this.contracts.push({
|
||
id: contractId,
|
||
contractNo: first.contractNo,
|
||
contractName: first.contractName,
|
||
projectId,
|
||
projectName,
|
||
deptId: first.deptId || matchedContract?.deptId,
|
||
deptName: first.deptName || matchedContract?.deptName,
|
||
payerName: first.payerName,
|
||
payeeName: first.payeeName,
|
||
partyA: matchedContract?.partyA,
|
||
partyB: matchedContract?.partyB,
|
||
settlementType,
|
||
});
|
||
}
|
||
this.handleContractChange(contractId, false);
|
||
Object.assign(this.form, {
|
||
contractId,
|
||
contractNo: first.contractNo || this.form.contractNo,
|
||
contractName: first.contractName || this.form.contractName,
|
||
projectId: projectId || this.form.projectId,
|
||
projectName: projectName || this.form.projectName,
|
||
deptId: first.deptId || this.form.deptId,
|
||
deptName: first.deptName || this.form.deptName,
|
||
payerName: first.payerName || this.form.payerName,
|
||
payeeName: first.payeeName || this.form.payeeName,
|
||
settlementType,
|
||
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
||
currency: first.currency || 'RMB',
|
||
});
|
||
this.details = rows.map(row => ({
|
||
...row,
|
||
sourceDetailId: row.sourceDetailId || row.id,
|
||
settlementAmountTax:
|
||
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
|
||
}));
|
||
this.detailPage.current = 1;
|
||
this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId);
|
||
await this.refreshSummaryFees();
|
||
},
|
||
async applyInitialSources(rows) {
|
||
const first = rows[0];
|
||
const contractId = first.contractId || null;
|
||
const settlementType = first.settlementType || 'payable';
|
||
if (!this.contracts.some(item => String(item.id) === String(contractId))) {
|
||
this.contracts.push({
|
||
id: contractId,
|
||
contractNo: first.contractNo,
|
||
contractName: first.contractName,
|
||
projectId: first.projectId,
|
||
projectName: first.projectName,
|
||
deptId: first.deptId,
|
||
deptName: first.deptName,
|
||
payerName: first.payerName,
|
||
payeeName: first.payeeName,
|
||
settlementType,
|
||
});
|
||
}
|
||
this.handleContractChange(contractId, false);
|
||
this.sources = rows.map(row => ({
|
||
...row,
|
||
preSettlementId: row.preSettlementId || row.id,
|
||
}));
|
||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId);
|
||
Object.assign(this.form, first, {
|
||
id: null,
|
||
formalSettlementNo: this.form.formalSettlementNo,
|
||
contractId,
|
||
settlementType,
|
||
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
||
sourcePreSettlementIds: this.form.sourcePreSettlementIds,
|
||
});
|
||
const responses = await Promise.all(
|
||
this.sources.map(item => getPreSettlementDetail(item.preSettlementId))
|
||
);
|
||
const settlements = responses.map(response => this.unwrapData(response) || {});
|
||
this.details = settlements.flatMap(settlement =>
|
||
(settlement.details || []).map(detail => ({
|
||
...detail,
|
||
sourcePreSettlementId: settlement.id,
|
||
}))
|
||
);
|
||
this.detailPage.current = 1;
|
||
const summaryMap = new Map();
|
||
settlements
|
||
.flatMap(settlement => settlement.summaryFees || [])
|
||
.forEach(row => {
|
||
const key = `${row.feeType}\u0000${row.feeItem}`;
|
||
const current = summaryMap.get(key) || {
|
||
feeType: row.feeType,
|
||
feeItem: row.feeItem,
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
settlementAmount: 0,
|
||
remark: '',
|
||
manualFlag: 0,
|
||
};
|
||
current.originalAmount += Number(row.settlementAmount || 0);
|
||
current.settlementAmount = current.originalAmount;
|
||
summaryMap.set(key, current);
|
||
});
|
||
this.summaryFees = [...summaryMap.values()];
|
||
if (!this.summaryFees.length) await this.refreshSummaryFees();
|
||
},
|
||
async loadAllContracts() {
|
||
const response = await getContractOptions('');
|
||
this.allContracts = this.unwrapData(response) || [];
|
||
const projectMap = new Map();
|
||
this.allContracts.forEach(item => {
|
||
if (item.projectId)
|
||
projectMap.set(String(item.projectId), { id: item.projectId, name: item.projectName });
|
||
});
|
||
this.projects = [...projectMap.values()];
|
||
this.contracts = [];
|
||
},
|
||
contractCategory(settlementType = this.form.settlementType) {
|
||
if (settlementType === 'receivable') return '客户合同';
|
||
if (settlementType === 'payable') return '承运商合同';
|
||
return '';
|
||
},
|
||
filterContracts(items, projectId, settlementType = this.form.settlementType) {
|
||
const category = this.contractCategory(settlementType);
|
||
return (items || []).filter(item => {
|
||
if (projectId && String(item.projectId) !== String(projectId)) return false;
|
||
if (!category) return true;
|
||
const itemCategory = String(item.contractCategory || '').trim();
|
||
if (itemCategory) return itemCategory === category;
|
||
const itemSettlementType = String(item.settlementType || '').trim();
|
||
return (
|
||
(category === '客户合同' && itemSettlementType === 'receivable') ||
|
||
(category === '承运商合同' && itemSettlementType === 'payable')
|
||
);
|
||
});
|
||
},
|
||
async loadContracts(keyword = '') {
|
||
const projectId = this.form.projectId;
|
||
if (!projectId) {
|
||
this.contracts = [];
|
||
return;
|
||
}
|
||
const response = await getContractOptions(keyword, projectId);
|
||
if (String(this.form.projectId) === String(projectId)) {
|
||
this.contracts = this.filterContracts(this.unwrapData(response), projectId);
|
||
}
|
||
},
|
||
async loadFeeOptions() {
|
||
const response = await getFeeOptions();
|
||
this.feeOptions = this.unwrapData(response) || [];
|
||
},
|
||
async loadFeeCategoryOptions() {
|
||
const { data } = await getDictionary({ code: 'fee_category' });
|
||
this.feeCategoryOptions = data?.data || [];
|
||
},
|
||
handleProjectChange(id) {
|
||
const project = this.projects.find(item => String(item.id) === String(id));
|
||
this.form.projectName = project?.name || '';
|
||
this.form.contractId = null;
|
||
this.form.contractNo = '';
|
||
this.form.contractName = '';
|
||
this.form.deptId = null;
|
||
this.form.deptName = '';
|
||
this.form.payerName = '';
|
||
this.form.payeeName = '';
|
||
this.sources = [];
|
||
this.details = [];
|
||
this.summaryFees = [];
|
||
this.form.sourcePreSettlementIds = [];
|
||
this.form.sourceDetailIds = [];
|
||
this.loadContracts();
|
||
},
|
||
handleContractChange(id, refreshSettlementNo = true) {
|
||
const contract = this.contracts.find(item => String(item.id) === String(id));
|
||
if (!contract) return;
|
||
const formalSettlementId = this.form.id;
|
||
const settlementType =
|
||
contract.settlementType ||
|
||
(contract.contractCategory === '客户合同'
|
||
? 'receivable'
|
||
: contract.contractCategory === '承运商合同'
|
||
? 'payable'
|
||
: this.form.settlementType);
|
||
Object.assign(this.form, contract, {
|
||
id: formalSettlementId,
|
||
contractId: contract.id,
|
||
payerName:
|
||
contract.payerName ||
|
||
(settlementType === 'receivable' ? contract.partyB : contract.partyA),
|
||
payeeName:
|
||
contract.payeeName ||
|
||
(settlementType === 'receivable' ? contract.partyA : contract.partyB),
|
||
settlementType,
|
||
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
||
});
|
||
this.sources = [];
|
||
this.details = [];
|
||
this.summaryFees = [];
|
||
this.form.sourcePreSettlementIds = [];
|
||
this.form.sourceDetailIds = [];
|
||
if (refreshSettlementNo) this.refreshFormalSettlementNo();
|
||
},
|
||
openCandidateDialog() {
|
||
this.candidate.visible = true;
|
||
this.candidate.page.current = 1;
|
||
this.loadCandidates();
|
||
},
|
||
async loadCandidates() {
|
||
this.candidate.loading = true;
|
||
try {
|
||
const response = await getCandidates(
|
||
this.candidate.page.current,
|
||
this.candidate.page.size,
|
||
{
|
||
...this.candidate.query,
|
||
contractId: this.form.contractId || undefined,
|
||
}
|
||
);
|
||
const data = this.unwrapData(response);
|
||
this.candidate.rows = data.records || [];
|
||
this.candidate.page.total = data.total || 0;
|
||
} finally {
|
||
this.candidate.loading = false;
|
||
}
|
||
},
|
||
resetCandidates() {
|
||
this.candidate.query = {};
|
||
this.candidate.page.current = 1;
|
||
this.loadCandidates();
|
||
},
|
||
confirmCandidates() {
|
||
const merged = [...this.sources, ...this.candidate.selected];
|
||
const map = new Map(
|
||
merged.map(item => [
|
||
String(item.preSettlementId || item.id),
|
||
{ ...item, preSettlementId: item.preSettlementId || item.id },
|
||
])
|
||
);
|
||
this.sources = [...map.values()];
|
||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId);
|
||
if (this.sources.length) {
|
||
Object.assign(this.form, this.sources[0], {
|
||
contractId: this.sources[0].contractId,
|
||
formalSettlementNo: this.form.formalSettlementNo,
|
||
});
|
||
this.refreshFormalSettlementNo();
|
||
}
|
||
this.form.settlementAmount = this.sources.reduce(
|
||
(sum, item) => sum + Number(item.settlementAmount || 0),
|
||
0
|
||
);
|
||
this.form.localSettlementAmount =
|
||
this.form.settlementAmount * Number(this.form.exchangeRate || 1);
|
||
this.candidate.visible = false;
|
||
},
|
||
removeSource(index) {
|
||
this.sources.splice(index, 1);
|
||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId || item.id);
|
||
},
|
||
openDetailDialog() {
|
||
if (!this.form.contractId) return this.$message.warning('请先选择合同');
|
||
this.detailCandidate.visible = true;
|
||
this.detailCandidate.page.current = 1;
|
||
this.detailCandidate.selected = [];
|
||
this.loadDetailCandidates();
|
||
},
|
||
async loadDetailCandidates() {
|
||
this.detailCandidate.loading = true;
|
||
try {
|
||
const range = this.detailCandidate.query.createTimeRange || [];
|
||
const params = {
|
||
contractId: this.form.contractId,
|
||
batchNo: this.detailCandidate.query.batchNo,
|
||
createStartDate: range[0],
|
||
createEndDate: range[1],
|
||
};
|
||
const response = await getCandidateDetails(
|
||
this.detailCandidate.page.current,
|
||
this.detailCandidate.page.size,
|
||
params
|
||
);
|
||
const data = this.unwrapData(response);
|
||
this.detailCandidate.rows = data.records || [];
|
||
this.detailCandidate.page.total = data.total || 0;
|
||
} finally {
|
||
this.detailCandidate.loading = false;
|
||
}
|
||
},
|
||
resetDetailCandidates() {
|
||
this.detailCandidate.query = { batchNo: '', createTimeRange: [] };
|
||
this.detailCandidate.page.current = 1;
|
||
this.loadDetailCandidates();
|
||
},
|
||
async confirmDetailCandidates() {
|
||
if (!this.detailCandidate.selected.length) {
|
||
return this.$message.warning('请选择结算明细');
|
||
}
|
||
const existing = new Map(
|
||
this.details
|
||
.filter(item => !item.formalSettlementId)
|
||
.map(item => [String(item.sourceDetailId || item.id), item])
|
||
);
|
||
this.detailCandidate.selected.forEach(item =>
|
||
existing.set(String(item.id), {
|
||
...item,
|
||
sourceDetailId: item.id,
|
||
settlementAmountTax: item.totalAmount,
|
||
})
|
||
);
|
||
this.details = [
|
||
...this.details.filter(item => item.formalSettlementId),
|
||
...existing.values(),
|
||
];
|
||
this.detailPage.current = 1;
|
||
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
|
||
this.detailCandidate.loading = true;
|
||
try {
|
||
await this.refreshSummaryFees();
|
||
} finally {
|
||
this.detailCandidate.loading = false;
|
||
}
|
||
this.detailCandidate.visible = false;
|
||
},
|
||
async removeDetail(row) {
|
||
await this.$confirm('确认将该明细踢出正式结算单?', '提示', { type: 'warning' });
|
||
const index = this.details.indexOf(row);
|
||
if (index < 0) return;
|
||
this.details.splice(index, 1);
|
||
this.form.sourceDetailIds = this.details
|
||
.filter(item => !item.sourcePreSettlementId)
|
||
.map(item => item.sourceDetailId);
|
||
await this.refreshSummaryFees();
|
||
},
|
||
addSummaryFee() {
|
||
this.summaryFees.push({
|
||
feeType: '',
|
||
feeItem: '',
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
settlementAmount: 0,
|
||
remark: '',
|
||
manualFlag: 1,
|
||
});
|
||
},
|
||
removeSummaryFee(index) {
|
||
this.summaryFees.splice(index, 1);
|
||
},
|
||
handleManualFeeTypeChange(row) {
|
||
if (!this.manualFeeItems(row.feeType).includes(row.feeItem)) row.feeItem = '';
|
||
},
|
||
manualFeeItems(feeType) {
|
||
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
|
||
},
|
||
// fee_category 业务字典:dictKey 为字典键值(落库值),dictValue 为字典名称(展示值)。
|
||
feeCategoryName(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
const option = this.feeCategoryOptions.find(
|
||
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
||
);
|
||
if (option) return option.dictValue ?? value;
|
||
return (
|
||
this.feeOptions.find(item => String(item.feeType) === String(value))?.feeTypeName || value
|
||
);
|
||
},
|
||
isSummaryMoney(prop) {
|
||
return ['originalAmount', 'adjustAmount', 'settlementAmount'].includes(prop);
|
||
},
|
||
isNegativeAdjustedAmount(row, prop) {
|
||
return (
|
||
Number(row.adjustAmount || 0) < 0 && ['adjustAmount', 'settlementAmount'].includes(prop)
|
||
);
|
||
},
|
||
recalculateSummaryRow(row) {
|
||
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
|
||
},
|
||
getSummarySums({ columns, data }) {
|
||
const sumProps = ['originalAmount', 'adjustAmount', 'settlementAmount'];
|
||
const hasNegativeAdjustment = data.some(row => Number(row.adjustAmount || 0) < 0);
|
||
return columns.map((column, index) => {
|
||
if (index === 0) return '合计';
|
||
if (!sumProps.includes(column.property)) return '';
|
||
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
|
||
const totalText = this.formatMoney(total);
|
||
if (column.property === 'settlementAmount' && hasNegativeAdjustment) {
|
||
return h('span', { style: { color: 'var(--el-color-danger)' } }, totalText);
|
||
}
|
||
return totalText;
|
||
});
|
||
},
|
||
async refreshSummaryFees() {
|
||
await this.ensureDetailFeeSnapshots();
|
||
this.buildSummaryFeesFromDetails();
|
||
},
|
||
// 结算合计按费用项归集:原金额取明细进入结算单时的费用金额,结算金额取调整后的金额,调整金额为两者之差。
|
||
buildSummaryFeesFromDetails() {
|
||
const generatedMap = new Map();
|
||
const existingGenerated = new Map(
|
||
this.summaryFees
|
||
.filter(row => Number(row.manualFlag) !== 1)
|
||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||
);
|
||
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||
const originalAmount = Number(originalValue || 0);
|
||
const settlementAmount = Number(settlementValue || 0);
|
||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||
return;
|
||
}
|
||
if (Math.abs(originalAmount) < 0.005 && Math.abs(settlementAmount) < 0.005) return;
|
||
const option = this.feeOptions.find(item =>
|
||
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
||
);
|
||
const feeType =
|
||
option?.feeType ||
|
||
fallbackFeeType ||
|
||
(this.isFreightFeeItem(feeItem) ? '物流配送' : '其他费用');
|
||
const key = `${feeType}\u0000${feeItem}`;
|
||
const old = existingGenerated.get(key);
|
||
const current = generatedMap.get(key) || {
|
||
id: old?.id,
|
||
feeType,
|
||
feeItem,
|
||
originalAmount: 0,
|
||
adjustAmount: 0,
|
||
settlementAmount: 0,
|
||
remark: old?.remark || '',
|
||
manualFlag: 0,
|
||
};
|
||
current.originalAmount = Number((current.originalAmount + originalAmount).toFixed(2));
|
||
current.settlementAmount = Number((current.settlementAmount + settlementAmount).toFixed(2));
|
||
current.adjustAmount = Number(
|
||
(current.settlementAmount - current.originalAmount).toFixed(2)
|
||
);
|
||
generatedMap.set(key, current);
|
||
};
|
||
this.details.forEach(detail => {
|
||
this.resolveDetailFeePairs(detail).forEach(([baseRow, currentRow]) =>
|
||
this.appendFeeRowSummary(baseRow, currentRow, detail, append)
|
||
);
|
||
});
|
||
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
||
},
|
||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
|
||
resolveDetailFeePairs(detail) {
|
||
const feeKey = this.detailFeeKey(detail);
|
||
const baseRows = this.detailFeeSnapshots[feeKey] || [];
|
||
if (!baseRows.length) return [this.buildDetailFeePair(detail)];
|
||
const pendingRows = this.pendingAdjustments[feeKey]?.rows || [];
|
||
return baseRows.map((baseRow, index) => {
|
||
const matched =
|
||
pendingRows.find(row => baseRow.id && String(row.id) === String(baseRow.id)) ||
|
||
pendingRows[index];
|
||
return [baseRow, matched || baseRow];
|
||
});
|
||
},
|
||
buildDetailFeePair(detail) {
|
||
const baseRow = {
|
||
feeItems: this.parseFeeItems(detail.feeItemsJson || detail.feeItems),
|
||
feeType: detail.feeType,
|
||
freightAmount: Number(detail.freightAmount || 0),
|
||
settlementAmountTax: Number(
|
||
detail.originalAmount ?? this.detailSettlementAmount(detail)
|
||
),
|
||
};
|
||
return [baseRow, { ...baseRow, settlementAmountTax: this.detailSettlementAmount(detail) }];
|
||
},
|
||
detailSettlementAmount(detail) {
|
||
return Number(
|
||
detail?.settlementAmountTax ??
|
||
detail?.totalAmount ??
|
||
detail?.afterAmount ??
|
||
detail?.settlementAmount ??
|
||
0
|
||
);
|
||
},
|
||
appendFeeRowSummary(baseRow, currentRow, detail, append) {
|
||
const fallbackFeeType = currentRow.feeType || detail.feeType || '';
|
||
const feeItemNames = Array.from(
|
||
new Set([...Object.keys(baseRow.feeItems || {}), ...Object.keys(currentRow.feeItems || {})])
|
||
);
|
||
feeItemNames.forEach(name =>
|
||
append(name, baseRow.feeItems?.[name], currentRow.feeItems?.[name], fallbackFeeType)
|
||
);
|
||
const hasFreightItem = feeItemNames.some(name => this.isFreightFeeItem(name));
|
||
if (!hasFreightItem) {
|
||
append('运输费', baseRow.freightAmount, currentRow.freightAmount, '物流配送');
|
||
}
|
||
const knownAmount = row =>
|
||
this.sumFeeItems(row.feeItems) + (hasFreightItem ? 0 : Number(row.freightAmount || 0));
|
||
const baseResidual = Number(
|
||
(Number(baseRow.settlementAmountTax || 0) - knownAmount(baseRow)).toFixed(2)
|
||
);
|
||
const currentResidual = Number(
|
||
(Number(currentRow.settlementAmountTax || 0) - knownAmount(currentRow)).toFixed(2)
|
||
);
|
||
if (Math.abs(baseResidual) < 0.005 && Math.abs(currentResidual) < 0.005) return;
|
||
// 结算金额被手工改写且无法拆分到具体费用项时,计入金额占比最大的费用项,否则归入其他费用。
|
||
append(
|
||
this.residualFeeItem(baseRow, currentRow),
|
||
baseResidual,
|
||
currentResidual,
|
||
fallbackFeeType
|
||
);
|
||
},
|
||
residualFeeItem(baseRow, currentRow) {
|
||
const feeItems = { ...(baseRow.feeItems || {}), ...(currentRow.feeItems || {}) };
|
||
const dominant = Object.entries(feeItems)
|
||
.filter(([, amount]) => Math.abs(Number(amount || 0)) >= 0.005)
|
||
.sort((a, b) => Math.abs(Number(b[1] || 0)) - Math.abs(Number(a[1] || 0)))[0];
|
||
if (dominant) return dominant[0];
|
||
return (
|
||
currentRow.billingRule?.feeItem ||
|
||
currentRow.feeItem ||
|
||
baseRow.billingRule?.feeItem ||
|
||
baseRow.feeItem ||
|
||
'其他费用'
|
||
);
|
||
},
|
||
sumFeeItems(feeItems) {
|
||
return Number(
|
||
Object.values(feeItems || {})
|
||
.reduce((total, value) => total + Number(value || 0), 0)
|
||
.toFixed(2)
|
||
);
|
||
},
|
||
detailFeeKey(detail) {
|
||
if (detail.formalSettlementId && detail.id) return `formal:${detail.id}`;
|
||
if (detail.sourcePreSettlementId && detail.id) return `pre:${detail.id}`;
|
||
const sourceDetailId = detail.sourceDetailId || detail.id;
|
||
return sourceDetailId ? `src:${sourceDetailId}` : '';
|
||
},
|
||
async loadDetailFeeRows(detail) {
|
||
if (this.publicMode) {
|
||
return [this.normalizeAdjustRow(detail)];
|
||
}
|
||
if (detail.formalSettlementId && detail.id) {
|
||
const rows = this.unwrapData(await getDetailFees(detail.id)) || [];
|
||
return rows.map(item => this.normalizeAdjustRow(item));
|
||
}
|
||
if (detail.sourcePreSettlementId && detail.id) {
|
||
const rows = this.unwrapData(await getPreSettlementDetailFees(detail.id)) || [];
|
||
return rows.map(item => this.normalizeAdjustRow(item));
|
||
}
|
||
const sourceDetailId = detail.sourceDetailId || detail.id;
|
||
if (!sourceDetailId) return [];
|
||
const data = this.unwrapData(await getFeeDetail(sourceDetailId)) || {};
|
||
return (data.records || []).map(item =>
|
||
this.normalizeAdjustRow(item, data.feeItemNames || [])
|
||
);
|
||
},
|
||
// 明细列表只有汇总金额,费用项级别的原始金额需要按明细单独拉取并缓存为调整基准。
|
||
async ensureDetailFeeSnapshots() {
|
||
const targets = this.details.filter(detail => {
|
||
const feeKey = this.detailFeeKey(detail);
|
||
return feeKey && !this.detailFeeSnapshots[feeKey];
|
||
});
|
||
if (!targets.length) return;
|
||
await Promise.all(
|
||
targets.map(async detail => {
|
||
const feeKey = this.detailFeeKey(detail);
|
||
try {
|
||
const feeRows = await this.loadDetailFeeRows(detail);
|
||
if (!feeRows.length) return;
|
||
this.detailFeeSnapshots[feeKey] = feeRows;
|
||
this.applySnapshotToDetail(detail, feeRows);
|
||
} catch (error) {
|
||
// 取不到费用明细时退回按明细汇总金额归集,后续操作会再次尝试拉取。
|
||
delete this.detailFeeSnapshots[feeKey];
|
||
}
|
||
})
|
||
);
|
||
},
|
||
applySnapshotToDetail(detail, feeRows) {
|
||
if (!feeRows.length || detail.pendingDetailAdjustment) return;
|
||
const feeItems = {};
|
||
feeRows.forEach(row =>
|
||
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
|
||
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
|
||
})
|
||
);
|
||
const settlementAmount = feeRows.reduce(
|
||
(total, row) => total + Number(row.settlementAmountTax || 0),
|
||
0
|
||
);
|
||
const freightAmount = feeRows.reduce(
|
||
(total, row) => total + Number(row.freightAmount || 0),
|
||
0
|
||
);
|
||
Object.assign(detail, {
|
||
feeItemsJson: JSON.stringify(feeItems),
|
||
freightAmount: Number(freightAmount.toFixed(2)),
|
||
originalAmount: Number(settlementAmount.toFixed(2)),
|
||
adjustAmount: 0,
|
||
settlementAmountTax: Number(settlementAmount.toFixed(2)),
|
||
});
|
||
},
|
||
normalizeAdjustRow(item, feeItemNames = []) {
|
||
const rawFeeItems =
|
||
item.feeItems && typeof item.feeItems === 'object'
|
||
? item.feeItems
|
||
: this.parseFeeItems(item.feeItemsJson);
|
||
const names = feeItemNames.length ? feeItemNames : Object.keys(rawFeeItems);
|
||
return {
|
||
...item,
|
||
transportQuantity: Number(item.transportQuantity || 0),
|
||
mileage:
|
||
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
|
||
? null
|
||
: Number(item.mileage),
|
||
freightAmount: Number(item.freightAmount || 0),
|
||
feeItems: Object.fromEntries(names.map(name => [name, Number(rawFeeItems[name] || 0)])),
|
||
settlementAmountTax: Number(
|
||
item.settlementAmountTax ?? item.totalAmount ?? item.afterAmount ?? item.settlementAmount ?? 0
|
||
),
|
||
billingRule: this.normalizeBillingRule(item),
|
||
calculating: false,
|
||
calculateError: '',
|
||
adjustCalculateTimer: null,
|
||
adjustCalculateVersion: 0,
|
||
};
|
||
},
|
||
cloneAdjustRows(rows) {
|
||
return (rows || []).map(row => ({
|
||
...row,
|
||
feeItems: { ...row.feeItems },
|
||
calculating: false,
|
||
calculateError: '',
|
||
adjustCalculateTimer: null,
|
||
adjustCalculateVersion: 0,
|
||
}));
|
||
},
|
||
isFreightFeeItem(name) {
|
||
return name && (name.includes('运费') || name.includes('运输费'));
|
||
},
|
||
handleDetailQuery() {
|
||
this.appliedDetailQuery = { ...this.detailQuery };
|
||
this.detailPage.current = 1;
|
||
},
|
||
resetDetailQuery(apply = true) {
|
||
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
|
||
if (apply) this.handleDetailQuery();
|
||
else this.appliedDetailQuery = { ...this.detailQuery };
|
||
},
|
||
handleDetailPageSizeChange() {
|
||
this.detailPage.current = 1;
|
||
},
|
||
detailIndexMethod(index) {
|
||
return (this.detailPage.current - 1) * this.detailPage.size + index + 1;
|
||
},
|
||
exportDetails() {
|
||
if (!this.details.length) return this.$message.warning('暂无可导出的结算明细');
|
||
const rows = this.details.map((row, index) => {
|
||
const result = { 序号: index + 1 };
|
||
this.detailTableColumns.forEach(column => {
|
||
result[column.label] = column.money
|
||
? Number(row[column.prop] || 0).toFixed(2)
|
||
: row[column.prop] ?? '';
|
||
});
|
||
return result;
|
||
});
|
||
const workbook = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '结算明细');
|
||
XLSX.writeFile(
|
||
workbook,
|
||
`${this.form.formalSettlementNo || '正式结算单'}结算明细${this.$dayjs().format(
|
||
'YYYY-MM-DD HH-mm-ss'
|
||
)}.xlsx`
|
||
);
|
||
},
|
||
billingRuleName(row) {
|
||
return this.normalizeBillingRule(row).name;
|
||
},
|
||
openBillingRuleInfo(row) {
|
||
this.billingRuleDialog.rule = this.normalizeBillingRule(row);
|
||
this.billingRuleDialog.visible = true;
|
||
},
|
||
normalizeBillingRule(row) {
|
||
const source = row || {};
|
||
const nested = [
|
||
source.billingRule,
|
||
source.billingRuleInfo,
|
||
source.billingRuleJson,
|
||
source.billingRulesJson,
|
||
source.billingRules,
|
||
source.billingPlanRule,
|
||
source.feeRule,
|
||
source.rule,
|
||
].find(value => value !== undefined && value !== null && value !== '');
|
||
const parsedNested = this.parseJsonValue(nested);
|
||
const matchedRules = Array.isArray(parsedNested)
|
||
? parsedNested.filter(item => item && typeof item === 'object')
|
||
: [];
|
||
const rule = this.parseObject(matchedRules[0] || parsedNested || nested);
|
||
const matchCondition = this.parseObject(
|
||
source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition
|
||
);
|
||
const ruleNames = matchedRules
|
||
.map(item => item.ruleName || item.name || item.billingRuleName || item.feeItem)
|
||
.filter(Boolean);
|
||
return {
|
||
...rule,
|
||
name:
|
||
source.billingRuleName ||
|
||
source.feeRuleName ||
|
||
source.ruleName ||
|
||
source.billingPlanRuleName ||
|
||
source.billingPlanName ||
|
||
rule.name ||
|
||
rule.ruleName ||
|
||
rule.billingRuleName ||
|
||
rule.planName ||
|
||
rule.billingPlanName ||
|
||
ruleNames.join('、') ||
|
||
rule.feeItem ||
|
||
'',
|
||
feeType: source.billingFeeType || source.feeType || rule.feeType,
|
||
feeItem: source.billingFeeItem || source.feeItem || rule.feeItem,
|
||
billingElement:
|
||
source.billingElement ||
|
||
source.billingFactor ||
|
||
rule.billingElement ||
|
||
rule.billingFactor,
|
||
billingType: source.billingType || rule.billingType,
|
||
billingUnit: source.billingUnit || source.priceUnit || rule.billingUnit || rule.priceUnit,
|
||
unitPrice: source.billingUnitPrice ?? source.unitPrice ?? rule.unitPrice,
|
||
minimumBillingWeight:
|
||
source.minimumBillingWeight ?? rule.minimumBillingWeight ?? rule.lowerLimit,
|
||
remark: source.billingRuleRemark || source.ruleRemark || rule.remark,
|
||
rules: matchedRules,
|
||
matchCondition: {
|
||
...matchCondition,
|
||
origin: matchCondition.origin || matchCondition.originName,
|
||
destination: matchCondition.destination || matchCondition.destinationName,
|
||
transportMode: matchCondition.transportMode || matchCondition.transportType,
|
||
cargoType: matchCondition.cargoType || matchCondition.cargoTypeName,
|
||
},
|
||
};
|
||
},
|
||
billingRuleRanges(rule) {
|
||
const ranges = Array.isArray(rule?.limitRanges) ? rule.limitRanges : [];
|
||
if (ranges.length) {
|
||
return ranges
|
||
.map(
|
||
item => `${item.lowerLimit ?? '-'}~${item.upperLimit ?? '-'}:${item.unitPrice ?? '-'}`
|
||
)
|
||
.join(';');
|
||
}
|
||
if (rule?.lowerLimit !== undefined || rule?.upperLimit !== undefined) {
|
||
return `${rule.lowerLimit ?? '-'}~${rule.upperLimit ?? '-'}`;
|
||
}
|
||
return '-';
|
||
},
|
||
recalculateAdjustRow(row, changedField) {
|
||
if (changedField && this.isFreightFeeItem(changedField)) {
|
||
row.freightAmount = Number(row.feeItems[changedField] || 0);
|
||
}
|
||
const feeItemTotal = Object.values(row.feeItems || {}).reduce(
|
||
(total, value) => total + Number(value || 0),
|
||
0
|
||
);
|
||
const hasFreightItem = Object.keys(row.feeItems || {}).some(this.isFreightFeeItem);
|
||
row.settlementAmountTax = Number(
|
||
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
|
||
);
|
||
if (changedField && this.adjust.sourceDetailId && (row.sourceFeeId || row.id)) {
|
||
this.scheduleSourceAdjustCalculation(row);
|
||
}
|
||
},
|
||
scheduleSourceAdjustCalculation(row) {
|
||
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
|
||
row.adjustCalculateVersion = Number(row.adjustCalculateVersion || 0) + 1;
|
||
row.calculating = true;
|
||
const version = row.adjustCalculateVersion;
|
||
row.adjustCalculateTimer = setTimeout(async () => {
|
||
row.adjustCalculateTimer = null;
|
||
try {
|
||
const response = await calculateAdjustedFee({
|
||
detailId: this.adjust.sourceDetailId,
|
||
feeId: row.sourceFeeId || row.id,
|
||
transportQuantity: Number(row.transportQuantity || 0),
|
||
mileage: Number(row.mileage || 0),
|
||
freightAmount: Number(row.freightAmount || 0),
|
||
feeItems: Object.fromEntries(
|
||
Object.entries(row.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
|
||
),
|
||
});
|
||
if (version !== row.adjustCalculateVersion || !this.adjust.visible) return;
|
||
const data = response.data?.data || response.data || {};
|
||
row.freightAmount = Number(data.freightAmount || 0);
|
||
row.feeItems = Object.fromEntries(
|
||
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
|
||
);
|
||
row.adjustAmount = Number(data.adjustAmount || 0);
|
||
row.settlementAmountTax = Number(data.afterAmount || 0);
|
||
} catch (error) {
|
||
row.calculateError = error.message || '费用试算失败';
|
||
} finally {
|
||
if (version === row.adjustCalculateVersion) row.calculating = false;
|
||
}
|
||
}, 300);
|
||
},
|
||
async openAdjustDialog(row) {
|
||
const feeKey = this.detailFeeKey(row);
|
||
const pending = feeKey ? this.pendingAdjustments[feeKey] : null;
|
||
this.adjust = {
|
||
visible: true,
|
||
loading: true,
|
||
saving: false,
|
||
detailId: row.formalSettlementId ? row.id : null,
|
||
sourceDetailId: row.sourceDetailId || null,
|
||
feeKey,
|
||
reason: pending?.reason || '',
|
||
rows: [],
|
||
targetRow: row.formalSettlementId ? null : row,
|
||
};
|
||
try {
|
||
// 已暂存的调整结果优先回显,避免重新打开时又回到调整前的数据。
|
||
if (pending?.rows?.length) {
|
||
this.adjust.rows = this.cloneAdjustRows(pending.rows);
|
||
return;
|
||
}
|
||
if (feeKey && !this.detailFeeSnapshots[feeKey]) {
|
||
const feeRows = await this.loadDetailFeeRows(row);
|
||
if (feeRows.length) this.detailFeeSnapshots[feeKey] = feeRows;
|
||
}
|
||
const snapshot = feeKey ? this.detailFeeSnapshots[feeKey] : null;
|
||
this.adjust.rows = snapshot?.length
|
||
? this.cloneAdjustRows(snapshot)
|
||
: [this.normalizeAdjustRow(row)];
|
||
} finally {
|
||
this.adjust.loading = false;
|
||
}
|
||
},
|
||
async saveAdjustment() {
|
||
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
|
||
if (this.adjust.rows.some(row => row.calculating)) {
|
||
return this.$message.warning('费用正在重新计算,请稍候');
|
||
}
|
||
if (!this.adjust.detailId) {
|
||
const target = this.adjust.targetRow;
|
||
const edited = this.adjust.rows[0];
|
||
if (!target || !edited) return;
|
||
// 原金额以明细进入结算单时的费用快照为基准,调整金额 = 结算金额 - 原金额。
|
||
const baseRows = this.detailFeeSnapshots[this.adjust.feeKey] || [];
|
||
const originalAmount = baseRows.length
|
||
? Number(
|
||
baseRows
|
||
.reduce((total, row) => total + Number(row.settlementAmountTax || 0), 0)
|
||
.toFixed(2)
|
||
)
|
||
: Number(
|
||
target.originalAmount ??
|
||
target.totalAmount ??
|
||
target.afterAmount ??
|
||
target.settlementAmountTax ??
|
||
target.settlementAmount ??
|
||
0
|
||
);
|
||
const settlementAmount = Number(
|
||
this.adjust.rows
|
||
.reduce((total, item) => total + Number(item.settlementAmountTax || 0), 0)
|
||
.toFixed(2)
|
||
);
|
||
const feeItems = this.adjust.rows.reduce((result, item) => {
|
||
Object.entries(item.feeItems || {}).forEach(([name, amount]) => {
|
||
result[name] = Number((Number(result[name] || 0) + Number(amount || 0)).toFixed(2));
|
||
});
|
||
return result;
|
||
}, {});
|
||
if (!Number.isFinite(settlementAmount) || settlementAmount < 0) {
|
||
return this.$message.warning('调整后的结算金额不能小于0');
|
||
}
|
||
Object.assign(target, {
|
||
originalAmount,
|
||
adjustAmount: Number((settlementAmount - originalAmount).toFixed(2)),
|
||
settlementAmountTax: settlementAmount,
|
||
feeItemsJson: JSON.stringify(feeItems),
|
||
freightAmount: this.adjust.rows.reduce(
|
||
(total, item) => total + Number(item.freightAmount || 0),
|
||
0
|
||
),
|
||
transportQuantity: this.adjust.rows.reduce(
|
||
(total, item) => total + Number(item.transportQuantity || 0),
|
||
0
|
||
),
|
||
mileage: this.adjust.rows.reduce((total, item) => total + Number(item.mileage || 0), 0),
|
||
unitPrice: edited.unitPrice,
|
||
remark: edited.remark,
|
||
pendingDetailAdjustment: true,
|
||
});
|
||
if (this.adjust.feeKey) {
|
||
this.pendingAdjustments[this.adjust.feeKey] = {
|
||
reason: this.adjust.reason,
|
||
rows: this.cloneAdjustRows(this.adjust.rows),
|
||
};
|
||
}
|
||
await this.refreshSummaryFees();
|
||
this.adjust.visible = false;
|
||
this.$message.success('调整已暂存,保存正式结算单后生效');
|
||
return;
|
||
}
|
||
this.adjust.saving = true;
|
||
try {
|
||
await adjustDetail({
|
||
detailId: this.adjust.detailId,
|
||
changeReason: this.adjust.reason,
|
||
rows: this.adjust.rows.map(row => ({
|
||
id: row.id,
|
||
transportQuantity: row.transportQuantity,
|
||
mileage: row.mileage,
|
||
unitPrice: row.unitPrice,
|
||
freightAmount: row.freightAmount,
|
||
feeItems: row.feeItems,
|
||
settlementAmountTax: row.settlementAmountTax,
|
||
settlementAmountNoTax: row.settlementAmountNoTax,
|
||
remark: row.remark,
|
||
})),
|
||
});
|
||
this.$message.success('调整保存成功');
|
||
this.adjust.visible = false;
|
||
await this.loadFormalDetail(this.form.id);
|
||
} finally {
|
||
this.adjust.saving = false;
|
||
}
|
||
},
|
||
claimInvoices() {
|
||
this.invoiceClaimLoading = true;
|
||
try {
|
||
const keyword = String(this.invoiceClaimNo || '').trim();
|
||
const defaultNo = `MOCK${this.$dayjs().format('YYYYMMDD')}`;
|
||
const baseNo = (keyword || defaultNo).slice(0, 29);
|
||
const numbers = [baseNo, `${baseNo}-02`, `${baseNo}-03`];
|
||
const invoiceAmounts = [1000, 2000, 3000];
|
||
const availableAmounts = [1000, 1000, 3000];
|
||
const preferredMatchedAmounts = [1000, 1000, 2000];
|
||
let remainingAmount = Math.max(
|
||
Number(this.form.settlementAmount || this.summaryTotal || 0) -
|
||
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
|
||
0
|
||
);
|
||
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
|
||
const mockRows = numbers
|
||
.map((invoiceNo, index) => {
|
||
const matchedAmount = Math.min(
|
||
preferredMatchedAmounts[index],
|
||
availableAmounts[index],
|
||
remainingAmount
|
||
);
|
||
remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2));
|
||
return {
|
||
invoiceNo,
|
||
invoiceDate: this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD'),
|
||
invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票',
|
||
taxRate: [3, 6, 9][index],
|
||
invoiceAmount: invoiceAmounts[index],
|
||
availableInvoiceAmount: availableAmounts[index],
|
||
matchedAmount,
|
||
attachmentJson: '',
|
||
mockData: true,
|
||
};
|
||
})
|
||
.filter(item => !existingNumbers.has(item.invoiceNo));
|
||
if (!mockRows.length) {
|
||
this.$message.warning('当前发票号码的Mock数据已认领');
|
||
return;
|
||
}
|
||
this.invoices = [...this.invoices, ...mockRows];
|
||
this.$message.success(`已生成${mockRows.length}条Mock发票数据`);
|
||
} finally {
|
||
this.invoiceClaimLoading = false;
|
||
}
|
||
},
|
||
openInvoiceClaimDialog() {
|
||
const stamp = Date.now();
|
||
this.invoiceClaimDialog.loading = true;
|
||
this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => {
|
||
const amount = Math.floor(1000 + Math.random() * 49000);
|
||
return {
|
||
invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32),
|
||
invoiceDate: this.$dayjs()
|
||
.subtract(Math.floor(Math.random() * 90), 'day')
|
||
.format('YYYY-MM-DD'),
|
||
invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票',
|
||
issuerName: this.form.payerName || 'Mock开票单位',
|
||
receiverName: this.form.payeeName || 'Mock受票单位',
|
||
invoiceAmount: amount,
|
||
availableInvoiceAmount: amount,
|
||
matchedAmount: amount,
|
||
taxRate: [3, 6, 9, 13][index % 4],
|
||
attachmentJson: '',
|
||
mockData: true,
|
||
};
|
||
});
|
||
this.invoiceClaimDialog.selected = [];
|
||
this.invoiceClaimDialog.page.current = 1;
|
||
this.invoiceClaimDialog.page.total = 80;
|
||
this.invoiceClaimDialog.visible = true;
|
||
this.invoiceClaimDialog.loading = false;
|
||
},
|
||
async confirmInvoiceClaims() {
|
||
if (!this.invoiceClaimDialog.selected.length) {
|
||
this.$message.warning('请至少选择一条发票');
|
||
return;
|
||
}
|
||
this.invoiceClaimDialog.confirming = true;
|
||
try {
|
||
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
|
||
let remaining = Math.max(
|
||
Number(this.form.settlementAmount || 0) -
|
||
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
|
||
0
|
||
);
|
||
const invoices = this.invoiceClaimDialog.selected
|
||
.filter(row => !existingNumbers.has(String(row.invoiceNo)))
|
||
.map(row => {
|
||
const sourceAmount = Number(row.matchedAmount || 0);
|
||
const matchedAmount =
|
||
Number(this.form.settlementAmount || 0) > 0
|
||
? Math.min(sourceAmount, remaining)
|
||
: sourceAmount;
|
||
if (Number(this.form.settlementAmount || 0) > 0) {
|
||
remaining = Math.max(0, remaining - matchedAmount);
|
||
}
|
||
return { ...row, matchedAmount };
|
||
})
|
||
.filter(row => row.matchedAmount > 0);
|
||
if (!invoices.length) {
|
||
this.$message.warning('当前正式结算单没有可认领的剩余金额');
|
||
return;
|
||
}
|
||
if (!this.form.id) {
|
||
this.invoices = [...this.invoices, ...invoices];
|
||
this.invoiceClaimDialog.visible = false;
|
||
this.$message.success(`已选择${invoices.length}条发票,保存正式结算单后生效`);
|
||
return;
|
||
}
|
||
await claimInvoicesApi({ formalSettlementId: this.form.id, invoices });
|
||
await this.loadFormalDetail(this.form.id);
|
||
this.invoiceClaimDialog.visible = false;
|
||
this.$message.success('发票认领成功,已同步正式结算单及关联付款单据');
|
||
} finally {
|
||
this.invoiceClaimDialog.confirming = false;
|
||
}
|
||
},
|
||
validateInvoices() {
|
||
const invoiceNumbers = new Set();
|
||
let matchedTotal = 0;
|
||
for (const row of this.invoices) {
|
||
const invoiceNo = String(row.invoiceNo || '').trim();
|
||
const invoiceAmount = Number(row.invoiceAmount || 0);
|
||
const availableAmount = Number(row.availableInvoiceAmount || 0);
|
||
const matchedAmount = Number(row.matchedAmount || 0);
|
||
const taxRate = Number(row.taxRate || 0);
|
||
if (!invoiceNo) {
|
||
this.$message.warning('发票号不能为空');
|
||
return false;
|
||
}
|
||
if (invoiceNumbers.has(invoiceNo)) {
|
||
this.$message.warning(`发票号${invoiceNo}重复`);
|
||
return false;
|
||
}
|
||
invoiceNumbers.add(invoiceNo);
|
||
if ([invoiceAmount, availableAmount, matchedAmount].some(value => value < 0)) {
|
||
this.$message.warning(`发票${invoiceNo}的金额不能小于0`);
|
||
return false;
|
||
}
|
||
if (availableAmount > invoiceAmount) {
|
||
this.$message.warning(`发票${invoiceNo}的可匹配金额不能超过发票金额`);
|
||
return false;
|
||
}
|
||
if (matchedAmount > availableAmount) {
|
||
this.$message.warning(`发票${invoiceNo}的匹配金额不能超过可匹配金额`);
|
||
return false;
|
||
}
|
||
if (taxRate < 0 || taxRate > 100) {
|
||
this.$message.warning(`发票${invoiceNo}的税率必须在0-100之间`);
|
||
return false;
|
||
}
|
||
matchedTotal += matchedAmount;
|
||
}
|
||
if (matchedTotal > Number(this.form.settlementAmount || 0)) {
|
||
this.$message.warning('发票匹配结算单金额合计不能超过结算金额');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
removeInvoice(index) {
|
||
this.invoices.splice(index, 1);
|
||
},
|
||
invoiceAttachment(row = {}) {
|
||
if (!row.attachmentJson) return {};
|
||
if (typeof row.attachmentJson === 'object') {
|
||
return Array.isArray(row.attachmentJson) ? row.attachmentJson[0] || {} : row.attachmentJson;
|
||
}
|
||
try {
|
||
const parsed = JSON.parse(row.attachmentJson);
|
||
return Array.isArray(parsed) ? parsed[0] || {} : parsed || {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
},
|
||
invoiceAttachmentUrl(row) {
|
||
const attachment = this.invoiceAttachment(row);
|
||
return attachment.url || attachment.link || attachment.fileUrl || '';
|
||
},
|
||
invoiceAttachmentName(row) {
|
||
const attachment = this.invoiceAttachment(row);
|
||
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
|
||
},
|
||
viewInvoiceAttachment(row) {
|
||
const url = this.invoiceAttachmentUrl(row);
|
||
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
|
||
window.open(url, '_blank');
|
||
},
|
||
downloadInvoiceAttachment(row) {
|
||
const url = this.invoiceAttachmentUrl(row);
|
||
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
|
||
downloadFileByUrl(url, this.invoiceAttachmentName(row));
|
||
},
|
||
buildSavePayload() {
|
||
return {
|
||
id: this.form.id,
|
||
formalSettlementNo: this.form.formalSettlementNo,
|
||
contractId: this.form.contractId,
|
||
settlementType: this.form.settlementType,
|
||
sourcePreSettlementIds: this.form.sourcePreSettlementIds,
|
||
sourceDetailIds: this.form.sourceDetailIds,
|
||
detailAdjustments: this.details
|
||
.filter(row => row.pendingDetailAdjustment)
|
||
.map(row => ({
|
||
sourcePreSettlementDetailId:
|
||
row.sourcePreSettlementDetailId || (row.sourcePreSettlementId ? row.id : undefined),
|
||
sourceDetailId: row.sourcePreSettlementId ? undefined : row.sourceDetailId || row.id,
|
||
adjustAmount: Number(row.adjustAmount || 0),
|
||
})),
|
||
exchangeRateDate: this.form.exchangeRateDate,
|
||
exchangeRate: this.form.exchangeRate,
|
||
summaryFees: this.summaryFees
|
||
.filter(
|
||
row =>
|
||
row.manualFlag === 1 ||
|
||
row.id ||
|
||
Number(row.adjustAmount || 0) !== 0 ||
|
||
String(row.remark || '').trim()
|
||
)
|
||
.map(row => ({
|
||
id: row.id || undefined,
|
||
feeType: row.feeType,
|
||
feeItem: row.feeItem,
|
||
adjustAmount: Number(row.adjustAmount || 0),
|
||
remark: row.remark,
|
||
manualFlag: row.manualFlag || 0,
|
||
})),
|
||
attachmentsJson: this.stringifyAttachments(this.attachments),
|
||
invoices: this.invoices.map(row => ({
|
||
invoiceNo: String(row.invoiceNo || '').trim(),
|
||
invoiceDate: row.invoiceDate || null,
|
||
invoiceType: row.invoiceType || '',
|
||
taxRate: Number(row.taxRate || 0),
|
||
invoiceAmount: Number(row.invoiceAmount || 0),
|
||
availableInvoiceAmount: Number(row.availableInvoiceAmount || 0),
|
||
matchedAmount: Number(row.matchedAmount || 0),
|
||
attachmentJson: row.attachmentJson || '',
|
||
})),
|
||
remark: this.form.remark,
|
||
};
|
||
},
|
||
async handleDraftSave() {
|
||
this.draftSaving = true;
|
||
try {
|
||
await save({
|
||
...this.buildSavePayload(),
|
||
approvalStatus: 'draft',
|
||
});
|
||
this.$message.success('保存成功');
|
||
this.visible = false;
|
||
this.$emit('success');
|
||
} finally {
|
||
this.draftSaving = false;
|
||
}
|
||
},
|
||
async handleSave() {
|
||
await this.$refs.formRef.validate();
|
||
if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) {
|
||
return this.$message.warning('请选择结算明细');
|
||
}
|
||
const invalidManualFee = this.summaryFees.find(
|
||
row =>
|
||
row.manualFlag === 1 &&
|
||
(!String(row.feeType || '').trim() || !String(row.feeItem || '').trim())
|
||
);
|
||
if (invalidManualFee) {
|
||
return this.$message.warning('请完善手工费用的费用类型和费用项');
|
||
}
|
||
if (!this.validateInvoices()) return;
|
||
// 附件类型仅做提示,不阻断正式结算单新增/保存提交。
|
||
// this.validateAttachments();
|
||
this.saving = true;
|
||
try {
|
||
await save(this.buildSavePayload());
|
||
this.$message.success('保存成功');
|
||
this.visible = false;
|
||
this.$emit('success');
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
displayValue(value) {
|
||
return value === null || value === undefined || value === '' ? '-' : value;
|
||
},
|
||
changeRecordLineNo(row) {
|
||
const lineNo = row?.lineNo;
|
||
return lineNo === -1 || String(lineNo) === '-1' ? '' : this.displayValue(lineNo);
|
||
},
|
||
paymentMethodName(value) {
|
||
const labels = {
|
||
bank_transfer: '银行转账',
|
||
bank_draft: '银行承兑汇票',
|
||
commercial_draft: '商业承兑汇票',
|
||
};
|
||
return this.displayValue(labels[value] || value);
|
||
},
|
||
unwrapData(response) {
|
||
const body = response?.data || response || {};
|
||
return body?.data || body;
|
||
},
|
||
formatMoney(value) {
|
||
return `${Number(value || 0).toFixed(2)} RMB`;
|
||
},
|
||
formatQuantity(value) {
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
const quantity = Number(value);
|
||
return Number.isFinite(quantity) ? quantity.toFixed(2) : '-';
|
||
},
|
||
formatDetailTransportQuantity(value) {
|
||
return this.readonly && Number(value) === -1 ? '-' : this.formatQuantity(value);
|
||
},
|
||
formatDetailMileage(value) {
|
||
return this.readonly && Number(value) === -1 ? '-' : this.displayValue(value);
|
||
},
|
||
async loadTransportTypeOptions() {
|
||
const { data } = await getDictionary({ code: 'transport_type' });
|
||
this.transportTypeOptions = data?.data || [];
|
||
},
|
||
transportTypeName(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
const normalizedValue = String(value).trim().toLocaleLowerCase();
|
||
const option = this.transportTypeOptions.find(
|
||
item =>
|
||
String(item.dictKey ?? '')
|
||
.trim()
|
||
.toLocaleLowerCase() === normalizedValue ||
|
||
String(item.dictValue ?? '')
|
||
.trim()
|
||
.toLocaleLowerCase() === normalizedValue
|
||
);
|
||
return option?.dictValue || value;
|
||
},
|
||
async loadAttachmentTypeOptions() {
|
||
const response = await getDictionary({ code: 'settle_attachment_types' });
|
||
const data = response?.data?.data || [];
|
||
this.attachmentTypeOptions = data.map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictValue,
|
||
}));
|
||
this.sortAttachments();
|
||
},
|
||
resolveAttachmentType(fileName) {
|
||
const normalizedName = String(fileName || '').toLocaleLowerCase();
|
||
const matchedType = [...this.attachmentTypeOptions]
|
||
.filter(item => item?.label || item?.value)
|
||
.sort(
|
||
(a, b) =>
|
||
String(b.label || b.value || '').length - String(a.label || a.value || '').length
|
||
)
|
||
.find(item => {
|
||
const typeText = String(item.label || item.value || '').toLocaleLowerCase();
|
||
return typeText && normalizedName.includes(typeText);
|
||
});
|
||
if (matchedType?.value) return matchedType.value;
|
||
const otherType = this.attachmentTypeOptions.find(item => this.isOtherAttachmentType(item));
|
||
if (otherType?.value) return otherType.value;
|
||
this.attachmentTypeOptions.push({ label: '其他附件', value: '其他附件' });
|
||
return '其他附件';
|
||
},
|
||
isOtherAttachmentType(item) {
|
||
const typeText = String(item?.label || item?.value || '').trim();
|
||
return typeText === '其他' || typeText === '其他附件';
|
||
},
|
||
getAttachmentTypeOrder(type) {
|
||
const normalizedType = String(type || '').trim();
|
||
const index = this.attachmentTypeOptions.findIndex(
|
||
item =>
|
||
String(item.value || '').trim() === normalizedType ||
|
||
String(item.label || '').trim() === normalizedType
|
||
);
|
||
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
||
},
|
||
sortAttachments() {
|
||
this.attachments = (this.attachments || [])
|
||
.map((file, index) => ({ file, index }))
|
||
.sort((a, b) => {
|
||
const orderDifference =
|
||
this.getAttachmentTypeOrder(a.file.type) - this.getAttachmentTypeOrder(b.file.type);
|
||
return orderDifference || a.index - b.index;
|
||
})
|
||
.map(item => item.file);
|
||
},
|
||
getMissingAttachmentTypes(files = this.attachments) {
|
||
const uploadedTypes = new Set(
|
||
(files || [])
|
||
.filter(item => this.getAttachmentUrl(item))
|
||
.map(item => String(item.type || '').trim())
|
||
.filter(Boolean)
|
||
);
|
||
return this.attachmentTypeOptions
|
||
.filter(item => !this.isOtherAttachmentType(item))
|
||
.filter(item => !uploadedTypes.has(String(item.value || item.label || '').trim()))
|
||
.map(item => item.label || item.value);
|
||
},
|
||
validateAttachments(files = this.attachments) {
|
||
const missing = this.getMissingAttachmentTypes(files);
|
||
if (!missing.length) return true;
|
||
this.$message.warning(`请先上传附件:${missing.join('、')}`);
|
||
return false;
|
||
},
|
||
handleAttachmentUploadSuccess(file) {
|
||
const originalName = file.originalName || file.name || '附件';
|
||
this.attachments.push({
|
||
type: this.resolveAttachmentType(originalName),
|
||
originalName,
|
||
name: originalName,
|
||
uploadUserName: this.userInfo.realName || this.userInfo.userName || '',
|
||
uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||
size: this.formatAttachmentSize(file.size),
|
||
url: file.url || file.link || '',
|
||
});
|
||
this.sortAttachments();
|
||
},
|
||
handleAttachmentSelectionChange(rows) {
|
||
this.selectedAttachmentRows = rows || [];
|
||
},
|
||
removeAttachment(index) {
|
||
this.attachments.splice(index, 1);
|
||
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(file =>
|
||
this.attachments.includes(file)
|
||
);
|
||
},
|
||
getAttachmentUrl(file = {}) {
|
||
return file.url || file.link || file.src || file.domain || '';
|
||
},
|
||
getAttachmentName(file = {}) {
|
||
return (
|
||
file.originalName || file.name || this.getAttachmentFileName(this.getAttachmentUrl(file))
|
||
);
|
||
},
|
||
downloadAttachment(file) {
|
||
const url = this.getAttachmentUrl(file);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法下载');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, this.getAttachmentName(file) || '附件');
|
||
},
|
||
downloadAttachments() {
|
||
const files = this.selectedAttachmentRows.length
|
||
? this.selectedAttachmentRows
|
||
: this.attachments;
|
||
const downloadableFiles = files.filter(file => this.getAttachmentUrl(file));
|
||
if (!downloadableFiles.length) {
|
||
this.$message.warning(
|
||
this.selectedAttachmentRows.length ? '所选附件无法下载' : '暂无可下载附件'
|
||
);
|
||
return;
|
||
}
|
||
downloadableFiles.forEach((file, index) => {
|
||
window.setTimeout(() => this.downloadAttachment(file), index * 200);
|
||
});
|
||
},
|
||
previewAttachment(file) {
|
||
const url = this.getAttachmentUrl(file);
|
||
if (!url) return this.$message.warning('附件地址为空,无法预览');
|
||
window.open(url, '_blank');
|
||
},
|
||
attachmentTypeName(type) {
|
||
const option = this.attachmentTypeOptions.find(
|
||
item => String(item.value) === String(type) || String(item.label) === String(type)
|
||
);
|
||
return this.displayValue(option?.label || type);
|
||
},
|
||
formatAttachmentSize(size) {
|
||
if (!size) return '';
|
||
const byteSize = Number(size);
|
||
if (!Number.isFinite(byteSize)) return String(size);
|
||
const mb = byteSize / 1024 / 1024;
|
||
if (mb >= 1) return `${mb.toFixed(2)} MB`;
|
||
return `${(byteSize / 1024).toFixed(2)} KB`;
|
||
},
|
||
stringifyAttachments(list) {
|
||
const files = (list || [])
|
||
.filter(item => this.getAttachmentUrl(item))
|
||
.map(item => ({
|
||
type: String(item.type || '').trim(),
|
||
originalName: String(item.originalName || item.name || '').trim(),
|
||
name: String(item.originalName || item.name || '').trim(),
|
||
uploadUserName: String(item.uploadUserName || '').trim(),
|
||
uploadTime: String(item.uploadTime || '').trim(),
|
||
size: String(item.size || '').trim(),
|
||
url: String(this.getAttachmentUrl(item)).trim(),
|
||
}));
|
||
return files.length ? JSON.stringify(files) : '';
|
||
},
|
||
parseFeeItems(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object') return value;
|
||
try {
|
||
return JSON.parse(value);
|
||
} catch {
|
||
return {};
|
||
}
|
||
},
|
||
parseJsonValue(value) {
|
||
if (value === undefined || value === null || value === '') return null;
|
||
if (typeof value === 'object') return value;
|
||
try {
|
||
return JSON.parse(value);
|
||
} catch {
|
||
return value;
|
||
}
|
||
},
|
||
parseObject(value) {
|
||
const parsed = this.parseJsonValue(value);
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||
},
|
||
parseAttachments(value) {
|
||
if (!value) return [];
|
||
let attachments = value;
|
||
if (!Array.isArray(value)) {
|
||
try {
|
||
attachments = JSON.parse(value);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
if (!Array.isArray(attachments)) return [];
|
||
return attachments.map(item => {
|
||
const url = this.getAttachmentUrl(item);
|
||
const originalName =
|
||
item.originalName || item.name || this.getAttachmentFileName(url) || '附件';
|
||
return {
|
||
...item,
|
||
type: item.type || this.resolveAttachmentType(originalName),
|
||
originalName,
|
||
name: originalName,
|
||
uploadUserName: item.uploadUserName || item.createUserName || item.uploadUser || '',
|
||
uploadTime: item.uploadTime || item.createTime || '',
|
||
size: this.formatAttachmentSize(item.size || item.attachSize),
|
||
url,
|
||
};
|
||
});
|
||
},
|
||
getAttachmentFileName(url) {
|
||
if (!url) return '';
|
||
const path = String(url).split('?')[0];
|
||
try {
|
||
return decodeURIComponent(path.substring(path.lastIndexOf('/') + 1));
|
||
} catch {
|
||
return path.substring(path.lastIndexOf('/') + 1);
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.formal-editor--page {
|
||
padding-bottom: 72px;
|
||
}
|
||
.formal-editor__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.formal-editor__detail-filter {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 8px 16px;
|
||
margin-bottom: 8px;
|
||
padding: 12px 12px 4px;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
.formal-editor__detail-filter-actions {
|
||
grid-column: 1 / -1;
|
||
justify-self: end;
|
||
}
|
||
.formal-editor__page-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 24px;
|
||
border-top: 1px solid #eff1f7;
|
||
position: fixed;
|
||
right: 0;
|
||
left: 230px;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
margin: 0;
|
||
background: #fff;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
:global(.avue--collapse) .formal-editor__page-actions {
|
||
left: 60px;
|
||
}
|
||
:global(.avue-layout--horizontal) .formal-editor__page-actions {
|
||
left: 0;
|
||
}
|
||
.formal-editor__links {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
.formal-editor :deep(.el-form-item) {
|
||
margin-bottom: 14px;
|
||
}
|
||
.formal-editor :deep(.el-form-item__content) {
|
||
line-height: normal;
|
||
}
|
||
.formal-editor :deep(.el-table) {
|
||
--el-table-border-color: #eff1f7;
|
||
}
|
||
.formal-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||
background: #fafafa;
|
||
}
|
||
.formal-editor__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
.formal-editor__adjust-reason {
|
||
margin-top: 16px;
|
||
}
|
||
.formal-editor__billing-rule-cell {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
max-width: 100%;
|
||
}
|
||
.formal-editor__billing-rule-icon {
|
||
flex: 0 0 auto;
|
||
color: var(--el-color-primary);
|
||
cursor: pointer;
|
||
}
|
||
.formal-editor__billing-rule-detail {
|
||
margin-top: 12px;
|
||
}
|
||
.formal-editor__billing-rule-match-title {
|
||
margin-top: 20px;
|
||
}
|
||
.dialog-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 22px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
.dialog-section-title::before {
|
||
width: 4px;
|
||
height: 18px;
|
||
margin-right: 8px;
|
||
background: #409eff;
|
||
content: '';
|
||
}
|
||
.formal-editor__attachment-missing {
|
||
margin-left: 12px;
|
||
color: var(--el-color-danger);
|
||
font-size: 13px;
|
||
font-weight: 400;
|
||
}
|
||
.formal-editor__attachment-upload {
|
||
margin-top: 12px;
|
||
}
|
||
.formal-editor__invoice-claim {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.formal-editor__invoice-claim-hint {
|
||
color: var(--el-color-danger);
|
||
font-size: 13px;
|
||
}
|
||
.formal-editor__invoice-claim :deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
.formal-editor__invoice-claim :deep(.el-input) {
|
||
width: 360px;
|
||
}
|
||
.formal-editor__negative-amount {
|
||
color: var(--el-color-danger);
|
||
}
|
||
.formal-editor :deep(.formal-editor__negative-amount .el-input__inner) {
|
||
color: var(--el-color-danger);
|
||
}
|
||
/* 选择预结算单 / 选择结算明细 弹窗搜索区白底 */
|
||
.formal-editor__candidate-search,
|
||
.formal-editor__detail-search {
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
margin-bottom: 16px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.formal-editor__detail-filter {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|