feat(web-admin): per-sample payment UI and refund registration on protocol form

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-03 11:13:32 +08:00
1 parent a328b9ff2d
commit 4e71a13165
5 files changed
+1280 -19

No files matched your search

@@ -683,6 +683,43 @@
<div class="section-title-left"><h3>缴费信息</h3></div>
</div>
<!-- v2: 按样本缴费 -->
<template v-if="paymentModelVersion >= 2">
<el-alert
v-if="presignDepositHint"
type="info"
:closable="false"
show-icon
class="presign-deposit-hint"
:title="presignDepositHintText"
/>
<div class="payment-main-card" style="margin-bottom: 16px">
<div class="section-subtitle">协议总金额</div>
<div class="form-grid form-grid--col4">
<el-form-item label="协议总金额(元)">
<AmountInput
v-model="formData.protocolTotalAmount"
:min="0"
placeholder="手填协议总金额"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="样本合同合计(元)">
<el-input :model-value="fmtMoney(sampleContractSum)" disabled />
</el-form-item>
</div>
<p v-if="protocolTotalHint" class="muted-hint warning-hint">{{ protocolTotalHint }}</p>
</div>
<SamplePaymentPanel
v-model="formData.samplePayments"
:samples="formData.samples"
:protocol-id="editingId || undefined"
/>
<ProtocolRefundRecordBlock :protocol-id="editingId || undefined" />
</template>
<!-- v1: 旧整单缴费主表(历史协议) -->
<template v-else>
<!-- 缴费主表 -->
<div class="payment-main-card">
<div class="section-subtitle">缴费主表</div>
@@ -998,6 +1035,7 @@
:image-size="48"
/>
</div>
</template>
</div>
</div>
</el-form>
@@ -1142,6 +1180,13 @@
payStatusLabel,
payStatusTagType
} from '@/utils/protocol-payment-labels'
import SamplePaymentPanel from './SamplePaymentPanel.vue'
import ProtocolRefundRecordBlock from './ProtocolRefundRecordBlock.vue'
import {
parseInstallmentRatios,
protocolTotalMismatchHint,
sumSampleContractAmounts
} from '@/utils/protocol-sample-payment'
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
@@ -1449,7 +1494,33 @@
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS as number | null,
installmentRatios: [] as number[] as number[],
// Payment Details
paymentDetails: [] as Array<Record<string, any>>
paymentDetails: [] as Array<Record<string, any>>,
// v2 per-sample payment
paymentModelVersion: 2,
protocolTotalAmount: null as number | null,
sourcePresignOrderId: '',
samplePayments: [] as Array<Record<string, any>>
})
const paymentModelVersion = computed(() => Number(formData.paymentModelVersion || 2))
const presignDepositHint = ref<Record<string, any> | null>(null)
const sampleContractSum = computed(() => sumSampleContractAmounts(formData.samplePayments))
const protocolTotalHint = computed(() =>
protocolTotalMismatchHint(formData.protocolTotalAmount, sampleContractSum.value)
)
const presignDepositHintText = computed(() => {
const h = presignDepositHint.value
if (!h) return ''
const amt = Number(h.confirmedDepositAmount || 0)
const cnt = Number(h.confirmedTransferCount || 0)
return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),仅提示不自动写入样本缴费`
})
watch(sampleContractSum, (sum) => {
if (paymentModelVersion.value < 2) return
if (formData.protocolTotalAmount == null && sum > 0) {
formData.protocolTotalAmount = Math.round(sum * 100) / 100
}
})
const formRules = {
@@ -1506,8 +1577,13 @@
installmentStartDate: '',
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
installmentRatios: [] as number[],
paymentDetails: []
paymentDetails: [],
paymentModelVersion: 2,
protocolTotalAmount: null,
sourcePresignOrderId: '',
samplePayments: []
})
presignDepositHint.value = null
customerId.value = null
customerOptions.value = []
customerSnapshot.value = null
@@ -1966,6 +2042,18 @@
formData.protocolStatus = String(prot.protocolStatus || formData.protocolStatus || 'dsx')
formData.registerDate = String(prot.registerDate || formData.registerDate || '')
formData.remark = String(prot.remark ?? formData.remark ?? '')
formData.paymentModelVersion = Number(prot.paymentModelVersion || 1)
formData.protocolTotalAmount =
prot.protocolTotalAmount != null ? Number(prot.protocolTotalAmount) : null
formData.sourcePresignOrderId = String(prot.sourcePresignOrderId || '')
}
presignDepositHint.value = (bundle.presignDepositHint as Record<string, any>) || null
const samplePays = bundle.samplePayments as Array<Record<string, any>> | null
if (samplePays && samplePays.length) {
formData.samplePayments = samplePays.map((p) => ({
...p,
installmentRatios: parseInstallmentRatios(p.installmentRatios)
}))
}
const pc = bundle.partyCustomer as Record<string, any> | null
@@ -2154,16 +2242,33 @@
return
}
const sampleCount = getActiveSampleCount(formData.samples)
if (!isPayModeValidForSampleCount(formData.payMode as PresetPayMode, sampleCount)) {
submitLoading.value = false
await ElMessageBox.alert(
formData.payMode === 'djfq'
? '「趸交+分期」仅适用于 2 个样本,请切换缴费模式'
: '当前缴费模式与样本数量不匹配,请切换缴费模式',
'无法保存',
{ type: 'warning' }
const isV2 = paymentModelVersion.value >= 2
if (!isV2) {
if (!isPayModeValidForSampleCount(formData.payMode as PresetPayMode, sampleCount)) {
submitLoading.value = false
await ElMessageBox.alert(
formData.payMode === 'djfq'
? '「趸交+分期」仅适用于 2 个样本,请切换缴费模式'
: '当前缴费模式与样本数量不匹配,请切换缴费模式',
'无法保存',
{ type: 'warning' }
)
return
}
} else {
const missingCode = formData.samples.some(
(s) => String(s.sampleStatus || '') !== 'yzf' && !String(s.sampleCode || '').trim()
)
return
if (missingCode && formData.samplePayments.length) {
submitLoading.value = false
ElMessage.warning('请先为样本分配样本编码,再保存按样本缴费方案')
return
}
const mismatch = protocolTotalMismatchHint(
formData.protocolTotalAmount,
sumSampleContractAmounts(formData.samplePayments)
)
if (mismatch) ElMessage.warning(mismatch)
}
if (!isEdit.value) {
for (const sampleRow of formData.samples) {
@@ -2176,14 +2281,16 @@
}
}
}
if (formData.payMode === 'dj') {
if (!isV2 && formData.payMode === 'dj') {
const dueHint = djDueAmountMismatchHint(formData.contractAmount, formData.paymentDetails)
if (dueHint) {
ElMessage.warning(dueHint)
}
}
try {
syncFqPaymentDetailRowCount(fqPaymentContext(), formData.paymentDetails)
if (!isV2) {
syncFqPaymentDetailRowCount(fqPaymentContext(), formData.paymentDetails)
}
const protocolAttachmentUrls = formData.attachments
.map((a) => a.fileUrl)
.filter(Boolean)
@@ -2267,8 +2374,47 @@
? String(s.sampleStatus).trim()
: null,
transport: transport?.transportType ? { ...transport } : undefined
})),
payment: {
}))
}
if (isV2) {
payload.paymentModelVersion = 2
payload.protocolTotalAmount = formData.protocolTotalAmount
payload.sourcePresignOrderId = formData.sourcePresignOrderId || undefined
payload.samplePayments = formData.samplePayments.map((p) => ({
id: p.id,
sampleCode: p.sampleCode,
protocolSampleId: p.protocolSampleId || undefined,
payMode: p.payMode,
contractAmount: p.contractAmount ?? undefined,
lumpSumDeadline: p.lumpSumDeadline || undefined,
installmentCount: toIntOrUndefined(p.installmentCount),
installmentIntervalMonths: toFloatOrUndefined(p.installmentIntervalMonths),
installmentStartDate: p.installmentStartDate || undefined,
installmentAdvanceDays: toIntOrUndefined(p.installmentAdvanceDays),
installmentRatios: Array.isArray(p.installmentRatios)
? p.installmentRatios.map((v: any) => Number(v || 0))
: parseInstallmentRatios(p.installmentRatios),
remark: p.remark || undefined,
schedules: (p.schedules || []).map((sch: any) => ({
scheduleNo: sch.scheduleNo,
dueAmount: sch.dueAmount,
dueDate: sch.dueDate || undefined,
scheduleStatus: sch.scheduleStatus || 'open',
remark: sch.remark,
transfers: (sch.transfers || []).map((t: any) => ({
id: t.id,
transferType: t.transferType,
transferAmount: t.transferAmount,
transferTime: t.transferTime,
transferAttachments: t.transferAttachments,
payStatus: t.payStatus || 'dfj',
remark: t.remark
}))
}))
}))
} else {
payload.payment = {
payMode: formData.payMode,
contractAmount: formData.contractAmount ?? undefined,
lumpSumDeadline: formData.lumpSumDeadline || undefined,
@@ -2277,10 +2423,10 @@
installmentStartDate: formData.installmentStartDate || undefined,
installmentAdvanceDays: toIntOrUndefined(formData.installmentAdvanceDays),
installmentRatios: isInstallmentPayMode(formData.payMode)
? (formData.installmentRatios || []).map((v) => Number(v || 0))
: undefined
},
paymentDetails: formData.paymentDetails.map(
? (formData.installmentRatios || []).map((v) => Number(v || 0))
: undefined
}
payload.paymentDetails = formData.paymentDetails.map(
({ _expanded, _payModeLocked, _voided, ...d }) => ({
id: d.id,
transferType: d.transferType,
@@ -3894,6 +4040,20 @@
}
}
.presign-deposit-hint {
margin-bottom: 14px;
}
.muted-hint {
margin: 4px 0 0;
font-size: 12px;
color: #94a3b8;
}
.warning-hint {
color: #d97706;
}
/* ═══════════════════════════════════════════
FORM GRID
═══════════════════════════════════════════ */