This commit is contained in:
zlwu committed 2026-06-04 12:57:59 +08:00
1 parent afd517ce10
commit eec61cb5dd
82 files changed
+16585 -9026

No files matched your search

@@ -758,7 +758,7 @@
step="any"
:min="1"
style="width: 100%"
@change="tryGenerateInstallmentDetails"
@change="handleFqParamsChange"
/>
</el-form-item>
<el-form-item label="每期间隔(月)">
@@ -766,9 +766,9 @@
v-model="formData.installmentIntervalMonths"
type="number"
step="any"
:min="1"
:min="0.1"
style="width: 100%"
@change="tryGenerateInstallmentDetails"
@change="handleFqParamsChange"
/>
</el-form-item>
<el-form-item label="分期开始时间">
@@ -780,7 +780,7 @@
placeholder="选择开始时间"
clearable
style="width: 100%"
@change="tryGenerateInstallmentDetails"
@change="handleFqParamsChange"
/>
</el-form-item>
</template>
@@ -790,7 +790,11 @@
<!-- 缴费明细 -->
<div class="section-subtitle" style="margin-top: 20px">缴费明细</div>
<div style="margin-bottom: 14px">
<el-button type="primary" @click="addPaymentDetail">
<el-button
type="primary"
:disabled="!canAddPaymentDetail(formData.payMode, formData.paymentDetails)"
@click="addPaymentDetail"
>
<el-icon><Plus /></el-icon>
添加明细
</el-button>
@@ -834,6 +838,7 @@
{{ paymentDetailSummary(detail) }}
</span>
<el-button
v-if="!isPayDetailLocked(detail)"
link
type="danger"
size="small"
@@ -872,6 +877,7 @@
<AmountInput
v-model="detail.transferAmount"
:min="0"
:disabled="isPayDetailLocked(detail)"
placeholder="请输入转账金额"
/>
</el-form-item>
@@ -882,6 +888,7 @@
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
clearable
:disabled="isPayDetailLocked(detail)"
style="width: 100%"
/>
</el-form-item>
@@ -889,6 +896,7 @@
<el-select
v-model="detail.payStatus"
placeholder="状态"
:disabled="isPayDetailLocked(detail)"
style="width: 100%"
>
<el-option label="待缴费" value="dfj" />
@@ -906,12 +914,13 @@
format="YYYY-MM-DD"
placeholder="选择应缴费时间"
clearable
:disabled="isPayDetailLocked(detail)"
style="width: 100%"
/>
</el-form-item>
</div>
<!-- 每条明细的附件上传区 -->
<div class="payment-detail-card__attachments">
<div v-if="!isPayDetailLocked(detail)" class="payment-detail-card__attachments">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
@@ -1175,10 +1184,24 @@
} from '@/api/admin'
import { fetchEthnicList, type EthnicOption } from '@/api/ethnic'
import { useUserStore } from '@/store/modules/user'
import { syncFqPaymentDetailRowCount } from '@/utils/protocol-payment-installment'
import {
calcFqInstallmentDueDate,
syncFqPaymentDetailRowCount
} from '@/utils/protocol-payment-installment'
applyPayModeSwitch,
canAddPaymentDetail,
evaluatePayModeSwitch,
isPayDetailLocked,
markPayDetailLocks,
sumPaidAmount,
type PayMode
} from '@/utils/protocol-pay-mode-switch'
import {
applyFqParamsChange,
evaluateFqParamsChange,
fqParamsSnapshot,
generateInitialFqDetails,
isSameFqParams,
type FqParams
} from '@/utils/protocol-fq-params-change'
import AmountInput from '@/components/common/amount-input/index.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import {
@@ -1199,6 +1222,16 @@
const userStore = useUserStore()
/** 上次确认生效的缴费模式(用于切换取消/拦截时回滚) */
const lastConfirmedPayMode = ref<PayMode>('dj')
/** 上次确认生效的分期参数(用于编辑时修改期数/间隔/开始时间回滚) */
const lastConfirmedFqParams = ref<FqParams>({
installmentCount: null,
installmentIntervalMonths: null,
installmentStartDate: ''
})
const customerId = ref<string | null>(null)
const customerOptions = ref<
Array<{ id: string; customerName: string; phoneNo: string; idCardNo: string }>
@@ -1544,6 +1577,12 @@
batchTransportVisible.value = false
Object.assign(batchTransport, createEmptyTransport())
activeFormTab.value = 'basic'
lastConfirmedPayMode.value = 'dj'
lastConfirmedFqParams.value = {
installmentCount: null,
installmentIntervalMonths: null,
installmentStartDate: ''
}
}
function scrollToSection(key: string) {
@@ -1627,6 +1666,7 @@
}
function addPaymentDetail() {
if (!canAddPaymentDetail(formData.payMode, formData.paymentDetails)) return
const now = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
const defaultTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
@@ -1702,77 +1742,180 @@
return Number.isFinite(n) ? Math.trunc(n) : undefined
}
function toFloatOrUndefined(val: unknown): number | undefined {
if (val == null || val === '') return undefined
const n = Number(val)
return Number.isFinite(n) ? n : undefined
}
function fqPaymentContext() {
return {
payMode: formData.payMode,
installmentCount: toIntOrUndefined(formData.installmentCount) ?? null,
contractAmount: formData.contractAmount,
installmentStartDate: formData.installmentStartDate,
installmentIntervalMonths: toIntOrUndefined(formData.installmentIntervalMonths) ?? null
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths) ?? null
}
}
/** 分期模式:三个参数都填好后生成/补齐缴费明细行(已有明细时仅补齐期次,不整表覆盖) */
function tryGenerateInstallmentDetails() {
if (formData.payMode !== 'fq') return
const count = formData.installmentCount
const interval = formData.installmentIntervalMonths
const startDate = formData.installmentStartDate
if (!count || !interval || !startDate) return
function payModeSwitchContext() {
return {
contractAmount: formData.contractAmount,
lumpSumDeadline: formData.lumpSumDeadline,
installmentCount: toIntOrUndefined(formData.installmentCount) ?? null,
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths) ?? null,
installmentStartDate: formData.installmentStartDate
}
}
if (formData.paymentDetails.length) {
syncFqPaymentDetailRowCount(fqPaymentContext(), formData.paymentDetails)
syncPaymentDueAmounts()
formData.paymentDetails.forEach((d, i) => {
d.installmentNo = i + 1
d.installmentDueDate = calcFqInstallmentDueDate(startDate, i, interval)
})
function getFqParamsFromForm(): FqParams {
return {
installmentCount: toIntOrUndefined(formData.installmentCount) ?? null,
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths) ?? null,
installmentStartDate: formData.installmentStartDate || ''
}
}
function restoreFqParams(params: FqParams) {
formData.installmentCount = params.installmentCount
formData.installmentIntervalMonths = params.installmentIntervalMonths
formData.installmentStartDate = params.installmentStartDate || ''
}
function syncLastConfirmedFqParams() {
lastConfirmedFqParams.value = fqParamsSnapshot(getFqParamsFromForm())
}
/**
* 分期参数变更(期数 / 间隔 / 开始时间):校验 → 确认 → 重建未缴期次
* 与缴费模式切换独立;保留 yjf 已缴行,作废原 dfj 后按剩余金额重建。
*/
async function handleFqParamsChange() {
if (formData.payMode !== 'fq') return
const newParams = getFqParamsFromForm()
const oldParams = lastConfirmedFqParams.value
if (isSameFqParams(oldParams, newParams)) return
if (
!newParams.installmentCount ||
!newParams.installmentIntervalMonths ||
!newParams.installmentStartDate
) {
return
}
const dueAmounts = calcInstallmentDueAmounts(formData.contractAmount, count)
const details = []
for (let i = 0; i < count; i++) {
details.push({
transferType: '',
dueAmount: dueAmounts[i] ?? null,
transferAmount: null,
transferTime: '',
payStatus: 'dfj',
installmentNo: i + 1,
installmentDueDate: calcFqInstallmentDueDate(startDate, i, interval),
attachments: [] as Array<{
id?: string
fileName: string
fileUrl: string
fileSize: number
fileSizeDisplay?: string
fileType?: string
_uploading?: boolean
}>
})
if (formData.paymentDetails.length === 0) {
formData.paymentDetails = generateInitialFqDetails(newParams, formData.contractAmount)
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
syncLastConfirmedFqParams()
return
}
initPaymentDetailExpandFlags(details)
formData.paymentDetails = details
const evaluation = evaluateFqParamsChange(
oldParams,
newParams,
formData.paymentDetails,
formData.contractAmount
)
if (evaluation.action === 'noop') {
syncLastConfirmedFqParams()
return
}
if (evaluation.action === 'blocked') {
restoreFqParams(oldParams)
await ElMessageBox.alert(evaluation.reason, '无法修改分期参数', { type: 'warning' })
return
}
if (evaluation.action === 'confirm') {
try {
await ElMessageBox.confirm(evaluation.summary, '修改分期参数', {
confirmButtonText: '确认修改',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
restoreFqParams(oldParams)
return
}
}
formData.paymentDetails = applyFqParamsChange(
newParams,
formData.paymentDetails,
formData.contractAmount
)
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
syncLastConfirmedFqParams()
}
/** 缴费模式切换回调 */
function onPayModeChange(val: string) {
if (val === 'fq') {
applyFqPaymentDefaults({ resetCountInterval: true })
tryGenerateInstallmentDetails()
} else {
// 趸交模式:将趸交截止时间同步到所有明细的应缴费时间
syncPaymentDueAmounts()
/** 缴费模式切换:校验 → 确认 → 变换明细 */
async function onPayModeChange(val: string) {
const toMode = val as PayMode
const fromMode = lastConfirmedPayMode.value
if (toMode === fromMode) return
const evaluation = evaluatePayModeSwitch(
fromMode,
toMode,
formData.paymentDetails,
payModeSwitchContext()
)
if (evaluation.action === 'blocked') {
formData.payMode = fromMode
await ElMessageBox.alert(evaluation.reason, '无法切换缴费模式', { type: 'warning' })
return
}
if (evaluation.action === 'confirm') {
try {
await ElMessageBox.confirm(evaluation.summary, '切换缴费模式', {
confirmButtonText: '确认切换',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
formData.payMode = fromMode
return
}
}
if (toMode === 'fq') {
applyFqPaymentDefaults({ resetCountInterval: false })
}
formData.paymentDetails = applyPayModeSwitch(
fromMode,
toMode,
formData.paymentDetails,
payModeSwitchContext()
)
initPaymentDetailExpandFlags(formData.paymentDetails)
markPayDetailLocks(formData.paymentDetails)
if (toMode === 'dj') {
syncLumpSumDueDate()
}
lastConfirmedPayMode.value = toMode
if (toMode === 'fq') {
syncLastConfirmedFqParams()
}
}
/** 趸交模式:将趸交截止时间同步到所有缴费明细的应缴费时间 */
/** 趸交模式:将趸交截止时间同步到待缴明细的应缴费时间 */
function syncLumpSumDueDate() {
if (formData.payMode !== 'dj') return
const deadline = formData.lumpSumDeadline || ''
formData.paymentDetails.forEach((d) => {
if (isPayDetailLocked(d)) return
d.installmentDueDate = deadline
})
}
@@ -1885,6 +2028,7 @@
formData.installmentStartDate = String(pay.installmentStartDate || '')
if (formData.payMode === 'fq') {
applyFqPaymentDefaults()
syncLastConfirmedFqParams()
}
}
@@ -1896,8 +2040,10 @@
}))
initPaymentDetailExpandFlags(details)
formData.paymentDetails = details
markPayDetailLocks(formData.paymentDetails)
}
syncFqPaymentDetailRowCount(fqPaymentContext(), formData.paymentDetails)
lastConfirmedPayMode.value = (formData.payMode === 'fq' ? 'fq' : 'dj') as PayMode
const pa =
(row as any).protocolAttachments || (bundle.protocol as any)?.protocolAttachments || ''
@@ -2057,11 +2203,13 @@
contractAmount: formData.contractAmount ?? undefined,
lumpSumDeadline: formData.lumpSumDeadline || undefined,
installmentCount: toIntOrUndefined(formData.installmentCount),
installmentIntervalMonths: toIntOrUndefined(formData.installmentIntervalMonths),
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths),
installmentStartDate: formData.installmentStartDate || undefined
},
paymentDetails: formData.paymentDetails.map(({ _expanded, ...d }) => ({
transferType: d.transferType,
paymentDetails: formData.paymentDetails.map(
({ _expanded, _payModeLocked, _voided, ...d }) => ({
id: d.id,
transferType: d.transferType,
dueAmount: d.dueAmount,
transferAmount: d.transferAmount,
transferTime: d.transferTime,
@@ -2087,7 +2235,7 @@
fileSizeDisplay: a.fileSizeDisplay,
fileType: a.fileType
}))
}))
}))
}
if (isEdit.value && editingId.value) {
@@ -2527,21 +2675,29 @@
return amounts
}
/** 按缴费模式同步各明细应缴费金额 */
/** 按缴费模式同步各明细应缴费金额(跳过已锁定明细) */
function syncPaymentDueAmounts() {
const contract = formData.contractAmount
if (contract == null) return
if (formData.payMode === 'dj') {
formData.paymentDetails.forEach((d) => {
d.dueAmount = contract
const paidSum = sumPaidAmount(formData.paymentDetails)
const remaining = Math.round((Number(contract) - paidSum) * 100) / 100
const dfjRows = formData.paymentDetails.filter(
(d) => !isPayDetailLocked(d) && String(d.payStatus || 'dfj') === 'dfj'
)
const targetAmount = paidSum > 0 ? remaining : contract
dfjRows.forEach((d, i) => {
if (i === 0) d.dueAmount = targetAmount
})
return
}
if (formData.payMode === 'fq') {
if (formData.paymentDetails.some((d) => isPayDetailLocked(d))) return
const count = formData.installmentCount || formData.paymentDetails.length
if (!count) return
const amounts = calcInstallmentDueAmounts(Number(contract), count)
const amounts = calcInstallmentDueAmounts(Number(contract), Number(count))
formData.paymentDetails.forEach((d, i) => {
if (isPayDetailLocked(d)) return
if (amounts[i] !== undefined) d.dueAmount = amounts[i]
})
}