This commit is contained in:
zlwu committed 2026-07-06 19:41:24 +08:00
1 parent cdaa0ac273
commit b7cd657f32
32 files changed
+2885 -1936

No files matched your search

@@ -529,10 +529,19 @@
<div class="section-subtitle">缴费主表</div>
<div class="form-grid form-grid--col6">
<el-form-item label="缴费模式">
<el-radio-group v-model="formData.payMode" @change="onPayModeChange">
<el-radio label="dj">趸交</el-radio>
<el-radio label="fq">分期</el-radio>
</el-radio-group>
<el-select
v-model="formData.payMode"
placeholder="请选择缴费模式"
style="width: 100%"
@change="onPayModeChange"
>
<el-option
v-for="opt in payModeOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="合同金额(元)">
<AmountInput
@@ -556,7 +565,7 @@
/>
</el-form-item>
</template>
<template v-if="formData.payMode === 'fq'">
<template v-if="isInstallmentPayMode(formData.payMode)">
<el-form-item label="分期期数">
<el-input
v-model="formData.installmentCount"
@@ -602,39 +611,6 @@
</el-form-item>
</template>
</div>
<div v-if="formData.payMode === 'fq'" class="installment-amounts-row">
<el-form-item label="分期金额(元)" class="installment-amounts-field">
<div class="installment-amounts-cells">
<div
v-for="(_amount, rIdx) in formData.installmentRatios"
:key="rIdx"
class="installment-amount-cell"
>
<span class="installment-amount-cell-no">{{ rIdx + 1 }}期</span>
<AmountInput
v-model="formData.installmentRatios[rIdx]"
:min="0"
placeholder="金额"
@change="onInstallmentAmountChange(rIdx)"
/>
</div>
</div>
</el-form-item>
<div class="installment-amounts-summary-row">
<span
class="installment-amounts-sum"
:class="{ 'is-error': !!installmentAmountsError }"
>
合计 {{ fmtMoney(installmentAmountsSum) }}
<template v-if="formData.contractAmount != null">
/ 合同 {{ fmtMoney(formData.contractAmount) }}
</template>
</span>
<div v-if="installmentAmountsError" class="form-tip form-tip--error">
{{ installmentAmountsError }}
</div>
</div>
</div>
</div>
<!-- 缴费明细 -->
@@ -693,7 +669,7 @@
type="danger"
size="small"
class="detail-delete"
@click.stop="formData.paymentDetails.splice(dIdx, 1)"
@click.stop="removePaymentDetail(dIdx)"
>
<el-icon><Delete /></el-icon>
</el-button>
@@ -963,22 +939,28 @@
import { useUserStore } from '@/store/modules/user'
import {
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
isInstallmentAmountsValid,
normalizeInstallmentAmounts,
resizeInstallmentAmounts,
scaleInstallmentAmountsToContract,
sumInstallmentAmounts,
syncFqPaymentDetailRowCount
} from '@/utils/protocol-payment-installment'
import {
applyPayModeSwitch,
canAddPaymentDetail,
createPaymentDetailRow,
evaluatePayModeSwitch,
isPayDetailLocked,
markPayDetailLocks,
sumPaidAmount,
type PayMode
} from '@/utils/protocol-pay-mode-switch'
import {
availablePayModes,
getActiveSampleCount,
isInstallmentPayMode,
isPayModeValidForSampleCount,
resolvePaymentPreset,
type PayMode as PresetPayMode
} from '@/utils/protocol-payment-presets'
import {
applyFqParamsChange,
evaluateFqParamsChange,
@@ -1019,6 +1001,21 @@
/** 上次确认生效的缴费模式(用于切换取消/拦截时回滚) */
const lastConfirmedPayMode = ref<PayMode>('dj')
const activeSampleCount = computed(() => getActiveSampleCount(formData.samples))
const payModeOptions = computed(() => {
const modes = [...availablePayModes(activeSampleCount.value)]
const current = String(formData.payMode || '') as PresetPayMode
if (current && !modes.includes(current)) {
modes.push(current)
}
return modes.map((value) => ({ value, label: payModeLabel(value) }))
})
function hasLockedPaymentDetails() {
return formData.paymentDetails.some((d) => isPayDetailLocked(d))
}
/** 上次确认生效的分期参数(用于编辑时修改期数/间隔/开始时间回滚) */
const lastConfirmedFqParams = ref<FqParams>({
installmentCount: null,
@@ -1357,6 +1354,9 @@
function scrollToSection(key: string) {
activeFormTab.value = key
if (key === 'payment') {
maybeApplyPaymentPresetOnTabEnter()
}
const el = document.getElementById(`form-section-${key}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
@@ -1386,6 +1386,13 @@
}
}
function removePaymentDetail(dIdx: number) {
const detail = formData.paymentDetails[dIdx]
if (detail && isPayDetailLocked(detail)) return
formData.paymentDetails.splice(dIdx, 1)
syncContractAmountFromPaymentDetails()
}
function addPaymentDetail() {
if (!canAddPaymentDetail(formData.payMode, formData.paymentDetails)) return
const now = new Date()
@@ -1446,8 +1453,13 @@
*/
function applyFqPaymentDefaults(options?: { resetCountInterval?: boolean }) {
const reset = options?.resetCountInterval ?? false
const sampleCount = getActiveSampleCount(formData.samples)
const preset = resolvePaymentPreset(sampleCount, formData.payMode as PresetPayMode)
if (preset?.contractAmount != null) {
formData.contractAmount = preset.contractAmount
}
if (reset || isFqNumericFieldEmpty(formData.installmentCount)) {
formData.installmentCount = 6
formData.installmentCount = preset?.installmentCount ?? 6
}
if (reset || isFqNumericFieldEmpty(formData.installmentIntervalMonths)) {
formData.installmentIntervalMonths = 6
@@ -1459,11 +1471,58 @@
formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
}
const count = toIntOrUndefined(formData.installmentCount) || 6
formData.installmentRatios = normalizeInstallmentAmounts(
formData.installmentRatios || [],
formData.contractAmount,
count
)
if (preset?.installmentAmounts?.length) {
formData.installmentRatios = [...preset.installmentAmounts]
} else {
formData.installmentRatios = normalizeInstallmentAmounts(
formData.installmentRatios || [],
formData.contractAmount,
count
)
}
}
function applyPaymentPresetForMode(payMode: PresetPayMode) {
const sampleCount = getActiveSampleCount(formData.samples)
const preset = resolvePaymentPreset(sampleCount, payMode)
if (!preset) return
formData.contractAmount = preset.contractAmount
if (isInstallmentPayMode(payMode) && preset.installmentCount) {
formData.installmentCount = preset.installmentCount
if (isFqNumericFieldEmpty(formData.installmentIntervalMonths)) {
formData.installmentIntervalMonths = 6
}
if (!formData.installmentStartDate) {
formData.installmentStartDate = defaultInstallmentStartDateYmd()
}
if (formData.installmentAdvanceDays == null) {
formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
}
formData.installmentRatios = [...(preset.installmentAmounts || [])]
formData.paymentDetails = generateInitialFqDetails(
getFqParamsFromForm(),
preset.contractAmount
)
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
syncLastConfirmedFqParams()
} else {
formData.paymentDetails = [
createPaymentDetailRow({
dueAmount: preset.contractAmount,
installmentDueDate: formData.lumpSumDeadline || '',
expanded: true
})
]
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
}
}
function maybeApplyPaymentPresetOnTabEnter() {
if (hasLockedPaymentDetails()) return
if (formData.paymentDetails.length > 0) return
applyPaymentPresetForMode(formData.payMode as PresetPayMode)
}
function toIntOrUndefined(val: unknown): number | undefined {
@@ -1529,7 +1588,7 @@
* 与缴费模式切换独立;保留 yjf 已缴行,作废原 dfj 后按剩余金额重建。
*/
async function handleFqParamsChange() {
if (formData.payMode !== 'fq') return
if (formData.payMode !== 'fq' && formData.payMode !== 'djfq') return
const newParams = getFqParamsFromForm()
const oldParams = lastConfirmedFqParams.value
@@ -1625,25 +1684,35 @@
}
}
if (toMode === 'fq') {
applyFqPaymentDefaults({ resetCountInterval: false })
if (!hasLockedPaymentDetails()) {
applyPaymentPresetForMode(toMode as PresetPayMode)
} else {
if (isInstallmentPayMode(toMode)) {
applyFqPaymentDefaults({ resetCountInterval: fromMode === 'dj' })
}
formData.paymentDetails = applyPayModeSwitch(
fromMode,
toMode,
formData.paymentDetails,
payModeSwitchContext()
)
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
const preset = resolvePaymentPreset(
getActiveSampleCount(formData.samples),
toMode as PresetPayMode
)
if (preset?.contractAmount != null) {
formData.contractAmount = preset.contractAmount
}
}
formData.paymentDetails = applyPayModeSwitch(
fromMode,
toMode,
formData.paymentDetails,
payModeSwitchContext()
)
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
if (toMode === 'dj') {
syncLumpSumDueDate()
}
lastConfirmedPayMode.value = toMode
if (toMode === 'fq') {
if (isInstallmentPayMode(toMode)) {
syncLastConfirmedFqParams()
}
}
@@ -1770,7 +1839,7 @@
if (loadedAmounts.length) {
formData.installmentRatios = loadedAmounts.map((v: unknown) => Number(v))
}
if (formData.payMode === 'fq') {
if (isInstallmentPayMode(formData.payMode)) {
applyFqPaymentDefaults()
const count = toIntOrUndefined(formData.installmentCount) || 6
formData.installmentRatios = normalizeInstallmentAmounts(
@@ -1793,7 +1862,9 @@
markPayDetailLocks(formData.paymentDetails)
}
syncFqPaymentDetailRowCount(fqPaymentContext(), formData.paymentDetails)
lastConfirmedPayMode.value = (formData.payMode === 'fq' ? 'fq' : 'dj') as PayMode
lastConfirmedPayMode.value = (
['fq', 'djfq'].includes(formData.payMode) ? formData.payMode : 'dj'
) as PayMode
const pa =
(row as any).protocolAttachments || (bundle.protocol as any)?.protocolAttachments || ''
@@ -1880,16 +1951,16 @@
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
if (formData.payMode === 'fq') {
if (installmentAmountsError.value) {
await ElMessageBox.alert(installmentAmountsError.value, '分期金额无效', { type: 'warning' })
return
}
const amounts = (formData.installmentRatios || []).map((v) => Number(v || 0))
if (!isInstallmentAmountsValid(amounts, formData.contractAmount)) {
await ElMessageBox.alert('分期金额合计须等于合同金额', '分期金额无效', { type: 'warning' })
return
}
const sampleCount = getActiveSampleCount(formData.samples)
if (!isPayModeValidForSampleCount(formData.payMode as PresetPayMode, sampleCount)) {
await ElMessageBox.alert(
formData.payMode === 'djfq'
? '「趸交+分期」仅适用于 2 个样本,请切换缴费模式'
: '当前缴费模式与样本数量不匹配,请切换缴费模式',
'无法保存',
{ type: 'warning' }
)
return
}
if (!isEdit.value) {
for (const sampleRow of formData.samples) {
@@ -1969,8 +2040,7 @@
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths),
installmentStartDate: formData.installmentStartDate || undefined,
installmentAdvanceDays: toIntOrUndefined(formData.installmentAdvanceDays),
installmentRatios:
formData.payMode === 'fq'
installmentRatios: isInstallmentPayMode(formData.payMode)
? (formData.installmentRatios || []).map((v) => Number(v || 0))
: undefined
},
@@ -2596,7 +2666,7 @@
formData.payMode = parsed.payMode || 'dj'
lastConfirmedPayMode.value = formData.payMode as PayMode
if (parsed.payMode === 'fq') {
if (isInstallmentPayMode(parsed.payMode || 'dj')) {
applyFqPaymentDefaults()
}
@@ -3111,38 +3181,32 @@
handleFqParamsChange()
}
function onInstallmentAmountChange(_changedIdx: number) {
syncPaymentDueAmounts()
handleFqParamsChange()
/** 缴费明细应缴金额之和回写合同金额 */
function syncContractAmountFromPaymentDetails() {
if (!formData.paymentDetails.length) return
const total = formData.paymentDetails.reduce(
(sum, d) => sum + (Number(d.dueAmount) || 0),
0
)
formData.contractAmount = Math.round(total * 100) / 100
}
/** 缴费明细应缴金额变更:分期模式下回写分期金额,保持与主表一致 */
/** 缴费明细应缴金额变更:分期回写 installmentRatios,并同步合同金额 */
function onPaymentDetailDueAmountChange(dIdx: number) {
if (formData.payMode !== 'fq') return
const detail = formData.paymentDetails[dIdx]
if (!detail || isPayDetailLocked(detail)) return
const installmentNo = Number(detail.installmentNo)
const ratioIdx = installmentNo >= 1 ? installmentNo - 1 : dIdx
if (!formData.installmentRatios) formData.installmentRatios = []
while (formData.installmentRatios.length <= ratioIdx) {
formData.installmentRatios.push(0)
if (formData.payMode === 'fq' || formData.payMode === 'djfq') {
const installmentNo = Number(detail.installmentNo)
const ratioIdx = installmentNo >= 1 ? installmentNo - 1 : dIdx
if (!formData.installmentRatios) formData.installmentRatios = []
while (formData.installmentRatios.length <= ratioIdx) {
formData.installmentRatios.push(0)
}
formData.installmentRatios[ratioIdx] = Number(detail.dueAmount) || 0
}
formData.installmentRatios[ratioIdx] = Number(detail.dueAmount) || 0
syncContractAmountFromPaymentDetails()
}
const installmentAmountsError = computed(() => {
const amounts = formData.installmentRatios || []
if (!amounts.length || formData.contractAmount == null) return ''
if (amounts.some((v) => Number(v) < 0)) return '分期金额不能为负数'
const sum = sumInstallmentAmounts(amounts)
if (Math.abs(sum - Number(formData.contractAmount)) > 0.01) {
return `分期金额合计须等于合同金额(${fmtMoney(formData.contractAmount)})`
}
return ''
})
const installmentAmountsSum = computed(() => sumInstallmentAmounts(formData.installmentRatios || []))
/** 按缴费模式同步各明细应缴费金额(跳过已锁定明细) */
function syncPaymentDueAmounts() {
const contract = formData.contractAmount
@@ -3159,14 +3223,14 @@
})
return
}
if (formData.payMode === 'fq') {
const count = formData.installmentCount || formData.paymentDetails.length
if (formData.payMode === 'fq' || formData.payMode === 'djfq') {
const count = Number(formData.installmentCount || formData.paymentDetails.length)
if (!count) return
const amounts = normalizeInstallmentAmounts(
formData.installmentRatios || [],
contract,
Number(count)
)
const ratios = formData.installmentRatios || []
const amounts =
ratios.length === count
? ratios.map((v) => Number(v || 0))
: normalizeInstallmentAmounts(ratios, null, count)
const paidRows = formData.paymentDetails.filter((d) => String(d.payStatus) === 'yjf')
paidRows.forEach((d) => {
const no = Number(d.installmentNo)
@@ -3179,37 +3243,16 @@
}
}
/** 合同金额手动变更:等比缩放分期金额并同步应缴费金额 */
/** 合同金额手动变更:趸交时同步待缴明细应缴金额 */
function onContractAmountChange() {
if (formData.payMode === 'fq' && (formData.installmentRatios || []).length) {
formData.installmentRatios = scaleInstallmentAmountsToContract(
formData.installmentRatios,
formData.contractAmount
)
if (formData.payMode === 'dj') {
syncPaymentDueAmounts()
}
syncPaymentDueAmounts()
}
/** 根据非作废样本价格之和同步合同金额,并联动应缴费金额 */
function syncContractAmount() {
const total = formData.samples.reduce((sum, s) => {
if (s.sampleStatus === 'yzf') return sum
return sum + (Number(s.price) || 0)
}, 0)
const nextContract = Math.round(total * 100) / 100
if (formData.payMode === 'fq' && (formData.installmentRatios || []).length) {
formData.installmentRatios = scaleInstallmentAmountsToContract(
formData.installmentRatios,
nextContract
)
}
formData.contractAmount = nextContract
syncPaymentDueAmounts()
}
/** 样本价格或状态变更 */
/** 样本价格或状态变更(不自动改合同金额) */
function onSamplePriceChange() {
syncContractAmount()
// 保留样本价格字段,不与合同金额联动
}
/* ====== 母亲身份证号 ↔ 出生年月联动 ====== */
@@ -3232,6 +3275,22 @@
() => calcBirthFromIdCard()
)
/** 样本数变化时按当前缴费模式刷新默认定价(新增单;无已缴锁定明细时整单重填) */
watch(activeSampleCount, () => {
if (isEdit.value) return
if (hasLockedPaymentDetails()) {
const preset = resolvePaymentPreset(
getActiveSampleCount(formData.samples),
formData.payMode as PresetPayMode
)
if (preset?.contractAmount != null) {
formData.contractAmount = preset.contractAmount
}
return
}
applyPaymentPresetForMode(formData.payMode as PresetPayMode)
})
/* ====== 趸交截止时间联动 ====== */
watch(
() => formData.lumpSumDeadline,