75 lines
9.4 KiB
Vue
75 lines
9.4 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, watch } from 'vue'
|
||
import type { ProcurementCommand } from '../api/procurementCommands'
|
||
import type { PlatformProcurement, ProcurementFile } from '../procurementTypes'
|
||
import { chargeDraft, feeRetryPermission, refundAvailable, targetPreview, replacementCandidates, replacementPreview, type FinanceForm } from '../procurementFinanceForm'
|
||
import { money, decimal, feePurposes } from '../procurementPresentation'
|
||
import { commandKey } from '../procurementPending'
|
||
import ProcurementChargeFields from './ProcurementChargeFields.vue'
|
||
const props = defineProps<{ command: ProcurementCommand; detail: PlatformProcurement; files: ProcurementFile[]; can: (permission: string) => boolean }>()
|
||
const form = defineModel<FinanceForm>({ required: true })
|
||
const candidates = computed(() => props.detail.fees.filter(fee => {
|
||
if (props.command === 'fees/replace-failed') return replacementCandidates(props.detail).some(row => row.id === fee.id)
|
||
if (props.command === 'fees/retry') return fee.active && fee.walletStatus.endsWith('_FAILED') && props.can(feeRetryPermission(fee))
|
||
if (props.command === 'fees/terminate') return fee.active && fee.walletStatus.endsWith('_FAILED')
|
||
if (props.command === 'fees/release-estimate') return fee.active && fee.purpose === 'INTERNATIONAL_ESTIMATE' && ['FROZEN', 'ZERO'].includes(fee.walletStatus)
|
||
if (props.command === 'fees/correct-target') return fee.active && !['INTERNATIONAL_ESTIMATE', 'ADJUSTMENT', 'REFUND'].includes(fee.purpose)
|
||
return fee.walletStatus === 'SETTLED' && !!fee.walletLedgerId && fee.purpose !== 'INTERNATIONAL_ESTIMATE'
|
||
}))
|
||
watch(candidates, rows => { if (form.value.feeId && !rows.some(row => row.id === form.value.feeId)) form.value.feeId = '' })
|
||
const selected = computed(() => props.detail.fees.find(row => row.id === form.value.feeId))
|
||
const preview = computed(() => {
|
||
if (!selected.value) return null
|
||
try {
|
||
if (props.command === 'fees/replace-failed' && form.value.targetOriginalAmount) return { ...replacementPreview(props.detail, selected.value, form.value.targetOriginalAmount), error: '' }
|
||
if (props.command === 'fees/correct-target' && form.value.targetOriginalAmount) return { ...targetPreview(props.detail, selected.value, form.value.targetOriginalAmount), error: '' }
|
||
if (props.command === 'fees/refund') return { refundable: decimal(refundAvailable(props.detail, selected.value)), error: '' }
|
||
} catch (error) { return { error: error instanceof Error ? error.message : '请核对原费用' } }
|
||
return null
|
||
})
|
||
function addOther() { form.value.otherDetermination = 'ITEMIZED'; const row = chargeDraft('OTHER', undefined, `OTHER_${commandKey().slice(0, 20).toUpperCase()}`); row.name = ''; form.value.others.push(row) }
|
||
</script>
|
||
<template>
|
||
<div class="admin-density">
|
||
<template v-if="command === 'charges/confirm'">
|
||
<el-alert v-if="detail.operations.some(row => row.action === 'CONFIRM_CHARGES')" title="本单已完成过核费,本次只引用已有费用版本;新增费用或重新核定请使用财务操作。" type="info" :closable="false" />
|
||
<el-alert title="一次确认实际国际运费、关税、国内运费和其他费用。正额确认后发起资金处理;确认成功不代表已扣款或可交寄。" type="info" :closable="false" />
|
||
<section v-for="(row, index) in form.fixed" :key="row.purpose" class="charge-section"><h3>{{ row.name }}</h3><ProcurementChargeFields v-model="form.fixed[index]" :detail="detail" :files="files" /></section>
|
||
<section class="charge-section"><h3>其他费用</h3><el-form-item label="其他费用适用情况" required><el-radio-group v-model="form.otherDetermination"><el-radio value="NOT_APPLICABLE" :disabled="detail.fees.some(row => row.active && row.purpose === 'OTHER')">不适用</el-radio><el-radio value="ITEMIZED">逐项核定</el-radio></el-radio-group></el-form-item>
|
||
<template v-if="form.otherDetermination === 'ITEMIZED'"><section v-for="(row, index) in form.others" :key="row.code" class="other-charge"><div class="charge-heading"><el-form-item label="费用名称" required><el-input v-model="row.name" :readonly="row.mode === 'EXISTING'" maxlength="128" placeholder="填写独立发生的费用名称" /></el-form-item><el-button v-if="row.mode === 'NEW'" class="density-action" data-tone="danger" @click="form.others.splice(index, 1)">移除新费用</el-button></div><ProcurementChargeFields v-model="form.others[index]" :detail="detail" :files="files" /></section><el-button v-if="!detail.operations.some(row => row.action === 'CONFIRM_CHARGES')" class="density-action" @click="addOther">增加其他费用</el-button></template>
|
||
</section>
|
||
</template>
|
||
<template v-else-if="command === 'fees/additional'">
|
||
<el-alert title="登记独立新发生的正额费用,使用该业务日期的汇率。与原费用金额更正分开处理;已完成订单保留原履约状态。" type="info" :closable="false" />
|
||
<el-form-item label="独立费用名称" required><el-input v-model="form.additional.name" maxlength="128" placeholder="填写独立发生的费用名称" /></el-form-item>
|
||
<el-form-item label="新增费用原币金额" required><el-input v-model="form.additional.amount" placeholder="填写正额金额,最多两位小数" /></el-form-item>
|
||
<el-form-item label="新增费用币种" required><el-select v-model="form.additional.currency"><el-option value="CNY" label="人民币 CNY" /><el-option value="USD" label="美元 USD" /></el-select></el-form-item>
|
||
<el-form-item label="新增费用业务日期" required><el-date-picker v-model="form.additional.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||
<el-form-item label="新增费用凭证" required><el-select v-model="form.additional.voucherFileId" placeholder="选择本单已上传的费用凭证"><el-option v-for="file in files.filter(row => row.category === 'VOUCHER')" :key="file.fileId" :value="file.fileId" :label="file.fileName" /></el-select></el-form-item>
|
||
</template>
|
||
<template v-else>
|
||
<el-alert v-if="command === 'fees/replace-failed'" title="原费用须已明确失败并完成终止及冻结释放。本次按核定总额创建新的实际费用版本,沿用原汇率和业务日期;货款仍受客户预算限制。" type="warning" :closable="false" />
|
||
<el-alert v-if="command === 'fees/correct-target'" title="输入这条原费用链正确的原币总额。沿用原汇率,按目标与净实扣差额生成追加或逐笔退款;历史扣款不覆盖。" type="info" :closable="false" />
|
||
<el-alert v-if="command === 'fees/refund'" title="退款回到客户资金账户,关联选中的原成功扣款;不以普通充值替代,不自动退款其他费用。" type="warning" :closable="false" />
|
||
<el-alert v-if="command === 'fees/terminate'" title="仅终止明确失败的费用版本;仍有有效冻结时先释放,释放成功后才可重新核定,不能将未决结果当作失败。" type="warning" :closable="false" />
|
||
<el-alert v-if="command === 'fees/release-estimate'" title="释放有效预估冻结不产生退款或实际收费,后续交寄须重新满足预估核对条件。" type="warning" :closable="false" />
|
||
<el-alert v-if="command === 'fees/retry'" title="仅重试已明确失败的原资金请求,金额与费用版本不改变;处理中请刷新结果。" type="info" :closable="false" />
|
||
<el-form-item label="原费用" required><el-select v-model="form.feeId" placeholder="选择本单符合操作条件的费用"><el-option v-for="fee in candidates" :key="fee.id" :value="fee.id" :label="`${fee.feeNo} · ${feePurposes[fee.purpose] || fee.feeItemName} · ${money(fee.amount, fee.currency)}`" /></el-select></el-form-item>
|
||
<dl v-if="selected" class="density-fields"><div><dt>原币金额</dt><dd>{{ money(selected.originalAmount, selected.originalCurrency) }}</dd></div><div><dt>原汇率及月份</dt><dd>{{ selected.exchangeRate }} / {{ selected.exchangeRateMonth }}</dd></div></dl>
|
||
<el-form-item v-if="['fees/correct-target', 'fees/replace-failed'].includes(command)" :label="`正确原币总额(${selected?.originalCurrency || '先选择费用'})`" required><el-input v-model="form.targetOriginalAmount" placeholder="填写原费用链正确总额,最多两位小数" /></el-form-item>
|
||
<el-form-item v-if="command === 'fees/refund'" label="本次退款金额(CNY)" required><el-input v-model="form.refundAmount" placeholder="填写本次正额退款金额" /></el-form-item>
|
||
<el-alert v-if="preview?.error" :title="preview.error" type="warning" :closable="false" />
|
||
<dl v-else-if="preview" class="density-fields"><template v-if="'target' in preview"><div><dt>目标人民币金额</dt><dd>{{ money(preview.target, 'CNY') }}</dd></div><div><dt>当前净实扣</dt><dd>{{ money(preview.net, 'CNY') }}</dd></div><div><dt>本次差额</dt><dd>{{ money(preview.difference, 'CNY') }}</dd></div></template><div v-if="'refundable' in preview"><dt>该笔剩余可退</dt><dd>{{ money(preview.refundable, 'CNY') }}</dd></div></dl>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
<style scoped>
|
||
.charge-section { padding-block: var(--admin-space-4); border-bottom: 1px solid var(--cc-border); }
|
||
.charge-section h3 { font-size: var(--admin-font-section); margin: 0 0 var(--admin-space-3); }
|
||
.charge-heading { display: flex; align-items: center; gap: var(--admin-space-3); }
|
||
.charge-heading :deep(.el-form-item) { flex: 1; }
|
||
.other-charge { padding-bottom: var(--admin-space-3); }
|
||
:deep(.el-select) { width: 100%; }
|
||
</style>
|