提交
This commit is contained in:
1 parent
427bcb1551
commit
31b04575e2
247 files changed
+39284
-20367
No files matched your search
@@ -3,9 +3,11 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="1380px"
|
||||
width="1600px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
@@ -82,12 +84,11 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-grid form-grid--col2 basic-remark-attach-row">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-grid form-grid--attachment">
|
||||
<el-form-item label="协议附件">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
@@ -169,7 +170,7 @@
|
||||
<div class="section-title">
|
||||
<div class="section-title-left"><h3>采集信息</h3></div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-grid form-grid--col6">
|
||||
<el-form-item label="采集地区">
|
||||
<el-select
|
||||
v-model="formData.regionId"
|
||||
@@ -271,12 +272,22 @@
|
||||
<SampleTransportFormSection
|
||||
v-model:samples="formData.samples"
|
||||
:sample-type-options="sampleTypeOptions"
|
||||
:sample-type-options-loading="sampleTypeLoading"
|
||||
:sample-code-options="sampleCodeOptions"
|
||||
:sample-code-options-loading="sampleCodeLoading"
|
||||
:unit-options="unitOptions"
|
||||
:unit-options-loading="unitLoading"
|
||||
:site-options="siteOptions"
|
||||
:site-options-loading="siteLoading"
|
||||
:express-company-options="expressCompanyOptions"
|
||||
:express-company-options-loading="expressCompanyLoading"
|
||||
:show-price="true"
|
||||
@price-change="onSamplePriceChange"
|
||||
@search-sample-code="remoteSearchSampleCodes"
|
||||
@search-sample-type="remoteSearchSampleTypes"
|
||||
@search-unit="remoteSearchUnits"
|
||||
@search-site="remoteSearchSites"
|
||||
@search-express-company="remoteSearchExpressCompanies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -486,7 +497,7 @@
|
||||
<!-- 缴费主表 -->
|
||||
<div class="payment-main-card">
|
||||
<div class="section-subtitle">缴费主表</div>
|
||||
<div class="form-grid form-grid--col5">
|
||||
<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>
|
||||
@@ -547,68 +558,52 @@
|
||||
@change="handleFqParamsChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="installment-advance-ratios-row form-col-span-2">
|
||||
<el-form-item label="应缴费提前(天)" class="installment-advance-field">
|
||||
<el-input
|
||||
v-model="formData.installmentAdvanceDays"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="365"
|
||||
step="1"
|
||||
style="width: 120px"
|
||||
@change="handleFqParamsChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费比例(%)" class="installment-ratios-field">
|
||||
<div class="installment-ratios-panel">
|
||||
<div class="installment-ratios-bar" aria-hidden="true">
|
||||
<div
|
||||
v-for="(ratio, rIdx) in formData.installmentRatios"
|
||||
:key="'bar-' + rIdx"
|
||||
class="installment-ratios-bar-seg"
|
||||
:class="{ 'is-auto': rIdx === formData.installmentRatios.length - 1 }"
|
||||
:style="{ flex: Math.max(Number(ratio) || 0, 0.001) }"
|
||||
:title="`第${rIdx + 1}期 ${ratio}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="installment-ratios-cells">
|
||||
<div
|
||||
v-for="(_ratio, rIdx) in formData.installmentRatios"
|
||||
:key="rIdx"
|
||||
class="installment-ratio-cell"
|
||||
:class="{ 'is-auto': rIdx === formData.installmentRatios.length - 1 }"
|
||||
>
|
||||
<span class="installment-ratio-cell-no">{{ rIdx + 1 }}期</span>
|
||||
<el-input
|
||||
v-model="formData.installmentRatios[rIdx]"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="100"
|
||||
step="any"
|
||||
:disabled="rIdx === formData.installmentRatios.length - 1"
|
||||
@change="onInstallmentRatioChange(rIdx)"
|
||||
>
|
||||
<template #suffix>%</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="installment-ratios-foot">
|
||||
<span class="form-label-tip">末期为自动补齐</span>
|
||||
<span
|
||||
class="installment-ratios-sum"
|
||||
:class="{ 'is-error': !!installmentRatiosError }"
|
||||
>
|
||||
合计 {{ installmentRatiosSum }}%
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="installmentRatiosError" class="form-tip form-tip--error">
|
||||
{{ installmentRatiosError }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="应缴费提前(天)">
|
||||
<el-input
|
||||
v-model="formData.installmentAdvanceDays"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="365"
|
||||
step="1"
|
||||
style="width: 100%"
|
||||
@change="handleFqParamsChange"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 缴费明细 -->
|
||||
@@ -844,7 +839,14 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== 文件预览弹窗 ========== -->
|
||||
<el-dialog v-model="previewVisible" title="文件预览" width="800px" align-center destroy-on-close>
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
@@ -900,16 +902,14 @@
|
||||
import { fetchEthnicList, type EthnicOption } from '@/api/ethnic'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
calcInstallmentDueAmountsByRatios,
|
||||
calcUnpaidAmountsByRatios,
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
DEFAULT_INSTALLMENT_RATIOS_6,
|
||||
defaultInstallmentRatios,
|
||||
finalizeInstallmentRatios,
|
||||
isInstallmentRatiosValid,
|
||||
resizeInstallmentRatios,
|
||||
syncFqPaymentDetailRowCount
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
isInstallmentAmountsValid,
|
||||
normalizeInstallmentAmounts,
|
||||
resizeInstallmentAmounts,
|
||||
scaleInstallmentAmountsToContract,
|
||||
sumInstallmentAmounts,
|
||||
syncFqPaymentDetailRowCount
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
import {
|
||||
applyPayModeSwitch,
|
||||
canAddPaymentDetail,
|
||||
@@ -955,7 +955,7 @@
|
||||
installmentIntervalMonths: null,
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6]
|
||||
installmentRatios: [] as number[]
|
||||
})
|
||||
|
||||
const customerId = ref<string | null>(null)
|
||||
@@ -997,6 +997,10 @@
|
||||
const unitOptions = ref<Array<{ id: string; unitName: string; unitCode: string }>>([])
|
||||
const sampleCodeOptions = ref<Api.BizSampleCode.SampleCodeItem[]>([])
|
||||
const sampleCodeLoading = ref(false)
|
||||
const sampleTypeLoading = ref(false)
|
||||
const unitLoading = ref(false)
|
||||
const siteLoading = ref(false)
|
||||
const expressCompanyLoading = ref(false)
|
||||
const expressCompanyOptions = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
|
||||
const siteOptions = ref<Api.Site.SiteItem[]>([])
|
||||
let sampleRowSeq = 0
|
||||
@@ -1194,7 +1198,7 @@
|
||||
lumpSumDeadline: '',
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS as number | null,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6] as number[],
|
||||
installmentRatios: [] as number[] as number[],
|
||||
// Payment Details
|
||||
paymentDetails: [] as Array<Record<string, any>>
|
||||
})
|
||||
@@ -1248,7 +1252,7 @@
|
||||
lumpSumDeadline: '',
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6],
|
||||
installmentRatios: [] as number[],
|
||||
paymentDetails: []
|
||||
})
|
||||
customerId.value = null
|
||||
@@ -1262,6 +1266,11 @@
|
||||
salesSnapshot.value = null
|
||||
regionOptions.value = []
|
||||
hospitalOptions.value = []
|
||||
sampleTypeOptions.value = []
|
||||
unitOptions.value = []
|
||||
sampleCodeOptions.value = []
|
||||
siteOptions.value = []
|
||||
expressCompanyOptions.value = []
|
||||
activeFormTab.value = 'basic'
|
||||
lastConfirmedPayMode.value = 'dj'
|
||||
lastConfirmedFqParams.value = {
|
||||
@@ -1269,7 +1278,7 @@
|
||||
installmentIntervalMonths: null,
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6]
|
||||
installmentRatios: [] as number[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1377,13 +1386,11 @@
|
||||
formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||||
}
|
||||
const count = toIntOrUndefined(formData.installmentCount) || 6
|
||||
if (reset || !formData.installmentRatios?.length) {
|
||||
formData.installmentRatios = defaultInstallmentRatios(count)
|
||||
} else {
|
||||
formData.installmentRatios = finalizeInstallmentRatios(
|
||||
resizeInstallmentRatios(formData.installmentRatios, count)
|
||||
)
|
||||
}
|
||||
formData.installmentRatios = normalizeInstallmentAmounts(
|
||||
formData.installmentRatios || [],
|
||||
formData.contractAmount,
|
||||
count
|
||||
)
|
||||
}
|
||||
|
||||
function toIntOrUndefined(val: unknown): number | undefined {
|
||||
@@ -1428,7 +1435,7 @@
|
||||
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths) ?? null,
|
||||
installmentStartDate: formData.installmentStartDate || '',
|
||||
installmentAdvanceDays: toIntOrUndefined(formData.installmentAdvanceDays) ?? null,
|
||||
installmentRatios: finalizeInstallmentRatios([...(formData.installmentRatios || [])])
|
||||
installmentRatios: [...(formData.installmentRatios || [])].map((v) => Number(v || 0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1584,7 +1591,6 @@
|
||||
resetForm()
|
||||
formData.motherEthnic = DEFAULT_MOTHER_ETHNIC
|
||||
formData.registerDate = new Date().toISOString().slice(0, 10).replace(/-/g, '')
|
||||
loadSampleOptions()
|
||||
void loadEthnicOptions()
|
||||
void remoteSearchCustomers('')
|
||||
|
||||
@@ -1686,12 +1692,18 @@
|
||||
formData.installmentStartDate = String(pay.installmentStartDate || '')
|
||||
formData.installmentAdvanceDays =
|
||||
pay.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||||
const loadedRatios = Array.isArray(pay.installmentRatios) ? pay.installmentRatios : []
|
||||
if (loadedRatios.length) {
|
||||
formData.installmentRatios = loadedRatios.map((v: unknown) => Number(v))
|
||||
const loadedAmounts = Array.isArray(pay.installmentRatios) ? pay.installmentRatios : []
|
||||
if (loadedAmounts.length) {
|
||||
formData.installmentRatios = loadedAmounts.map((v: unknown) => Number(v))
|
||||
}
|
||||
if (formData.payMode === 'fq') {
|
||||
applyFqPaymentDefaults()
|
||||
const count = toIntOrUndefined(formData.installmentCount) || 6
|
||||
formData.installmentRatios = normalizeInstallmentAmounts(
|
||||
formData.installmentRatios,
|
||||
formData.contractAmount,
|
||||
count
|
||||
)
|
||||
syncLastConfirmedFqParams()
|
||||
}
|
||||
}
|
||||
@@ -1741,8 +1753,7 @@
|
||||
dialogFormLoading.value = true
|
||||
|
||||
try {
|
||||
const [, , bundle] = await Promise.all([
|
||||
loadSampleOptions(),
|
||||
const [, bundle] = await Promise.all([
|
||||
loadEthnicOptions(),
|
||||
fetchCollectionProtocolDetail(String(row.id)).catch(() => null)
|
||||
])
|
||||
@@ -1779,7 +1790,13 @@
|
||||
}
|
||||
await Promise.all(hydrateTasks)
|
||||
if (formData.samples.length) {
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
await Promise.all([
|
||||
remoteSearchSampleTypes(''),
|
||||
remoteSearchSites(''),
|
||||
remoteSearchUnits(''),
|
||||
remoteSearchSampleCodes('')
|
||||
])
|
||||
hydrateSampleLookupOptions()
|
||||
}
|
||||
} catch {
|
||||
/* 保留列表行已填基础字段 */
|
||||
@@ -1792,13 +1809,13 @@
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
if (formData.payMode === 'fq') {
|
||||
if (installmentRatiosError.value) {
|
||||
await ElMessageBox.alert(installmentRatiosError.value, '缴费比例无效', { type: 'warning' })
|
||||
if (installmentAmountsError.value) {
|
||||
await ElMessageBox.alert(installmentAmountsError.value, '分期金额无效', { type: 'warning' })
|
||||
return
|
||||
}
|
||||
const ratios = finalizeInstallmentRatios([...(formData.installmentRatios || [])])
|
||||
if (!isInstallmentRatiosValid(ratios)) {
|
||||
await ElMessageBox.alert('缴费比例之和须等于 100%', '缴费比例无效', { type: 'warning' })
|
||||
const amounts = (formData.installmentRatios || []).map((v) => Number(v || 0))
|
||||
if (!isInstallmentAmountsValid(amounts, formData.contractAmount)) {
|
||||
await ElMessageBox.alert('分期金额合计须等于合同金额', '分期金额无效', { type: 'warning' })
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1883,7 +1900,7 @@
|
||||
installmentAdvanceDays: toIntOrUndefined(formData.installmentAdvanceDays),
|
||||
installmentRatios:
|
||||
formData.payMode === 'fq'
|
||||
? finalizeInstallmentRatios([...(formData.installmentRatios || [])])
|
||||
? (formData.installmentRatios || []).map((v) => Number(v || 0))
|
||||
: undefined
|
||||
},
|
||||
paymentDetails: formData.paymentDetails.map(
|
||||
@@ -2116,6 +2133,89 @@
|
||||
sampleCodeOptions.value = merged
|
||||
}
|
||||
|
||||
function ensureSampleTypesInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...sampleTypeOptions.value]
|
||||
for (const s of samples) {
|
||||
const id = String(s.sampleTypeId || '')
|
||||
if (!id) continue
|
||||
const name = String(s.sampleTypeName || '').trim()
|
||||
const existing = merged.find((x) => x.id === id)
|
||||
if (existing) {
|
||||
if (name && (!existing.typeName || existing.typeName === existing.id)) {
|
||||
existing.typeName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({
|
||||
id,
|
||||
typeCode: '',
|
||||
typeName: name || id,
|
||||
storageYears: s.storageYears ?? null,
|
||||
storagePrice: s.originalPrice ?? null
|
||||
})
|
||||
}
|
||||
sampleTypeOptions.value = merged
|
||||
}
|
||||
|
||||
function ensureUnitsInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...unitOptions.value]
|
||||
for (const s of samples) {
|
||||
const id = String(s.unitId || '')
|
||||
if (!id) continue
|
||||
const name = String(s.unitName || '').trim()
|
||||
const existing = merged.find((x) => x.id === id)
|
||||
if (existing) {
|
||||
if (name && (!existing.unitName || existing.unitName === existing.id)) {
|
||||
existing.unitName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({ id, unitName: name || id, unitCode: String(s.unitCode || '') })
|
||||
}
|
||||
unitOptions.value = merged
|
||||
}
|
||||
|
||||
function ensureSitesInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...siteOptions.value]
|
||||
for (const s of samples) {
|
||||
const id = String(s.transport?.destSiteId || '')
|
||||
if (!id) continue
|
||||
const name = String(s.transport?.destSiteName || '').trim()
|
||||
const existing = merged.find((x) => x.id === id)
|
||||
if (existing) {
|
||||
if (name && (!existing.siteName || existing.siteName === existing.id)) {
|
||||
existing.siteName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({
|
||||
id,
|
||||
siteCode: '',
|
||||
siteName: name || id,
|
||||
siteType: '',
|
||||
status: '',
|
||||
address: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
businessScope: '',
|
||||
siteImage: '',
|
||||
tenantNo: '',
|
||||
orderNum: 0,
|
||||
remark: '',
|
||||
createTime: '',
|
||||
updateTime: ''
|
||||
})
|
||||
}
|
||||
siteOptions.value = merged
|
||||
}
|
||||
|
||||
function hydrateSampleLookupOptions() {
|
||||
ensureSampleTypesInOptions(formData.samples)
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
ensureUnitsInOptions(formData.samples)
|
||||
ensureSitesInOptions(formData.samples)
|
||||
}
|
||||
|
||||
/* ====== 客户检索(母亲信息) ====== */
|
||||
function formatCustomerLabel(c: { customerName: string; phoneNo: string; idCardNo: string }) {
|
||||
const parts = [c.customerName || '']
|
||||
@@ -2248,23 +2348,91 @@
|
||||
formData.salesUserId = u.id
|
||||
}
|
||||
|
||||
/* ====== 样本类型/单位选项 ====== */
|
||||
async function loadSampleOptions() {
|
||||
async function remoteSearchSampleCodes(query: string) {
|
||||
sampleCodeLoading.value = true
|
||||
try {
|
||||
const [stRes, uRes, scRes, ecRes, siteRes] = await Promise.all([
|
||||
fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
|
||||
fetchItemUnitList({ page: 1, pageSize: 100 }),
|
||||
fetchSampleCodeList({ page: 1, pageSize: 100 }),
|
||||
fetchExpressCompanyList({ page: 1, pageSize: 100 }),
|
||||
fetchSiteList({ page: 1, pageSize: 100 })
|
||||
])
|
||||
sampleTypeOptions.value = (stRes.list || []) as any
|
||||
unitOptions.value = (uRes.list || []) as any
|
||||
sampleCodeOptions.value = (scRes.list || []) as any
|
||||
expressCompanyOptions.value = ecRes.list || []
|
||||
siteOptions.value = siteRes.list || []
|
||||
const res = await fetchSampleCodeList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
sampleCodeOptions.value = (res.list || []) as Api.BizSampleCode.SampleCodeItem[]
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
} catch {
|
||||
/* ignore */
|
||||
sampleCodeOptions.value = []
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
} finally {
|
||||
sampleCodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchSampleTypes(query: string) {
|
||||
sampleTypeLoading.value = true
|
||||
try {
|
||||
const res = await fetchCellSampleTypeList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
sampleTypeOptions.value = (res.list || []) as any
|
||||
ensureSampleTypesInOptions(formData.samples)
|
||||
} catch {
|
||||
sampleTypeOptions.value = []
|
||||
ensureSampleTypesInOptions(formData.samples)
|
||||
} finally {
|
||||
sampleTypeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchUnits(query: string) {
|
||||
unitLoading.value = true
|
||||
try {
|
||||
const res = await fetchItemUnitList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
unitOptions.value = (res.list || []) as any
|
||||
ensureUnitsInOptions(formData.samples)
|
||||
} catch {
|
||||
unitOptions.value = []
|
||||
ensureUnitsInOptions(formData.samples)
|
||||
} finally {
|
||||
unitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchSites(query: string) {
|
||||
siteLoading.value = true
|
||||
try {
|
||||
const res = await fetchSiteList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
siteOptions.value = res.list || []
|
||||
ensureSitesInOptions(formData.samples)
|
||||
} catch {
|
||||
siteOptions.value = []
|
||||
ensureSitesInOptions(formData.samples)
|
||||
} finally {
|
||||
siteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchExpressCompanies(query: string) {
|
||||
expressCompanyLoading.value = true
|
||||
try {
|
||||
const res = await fetchExpressCompanyList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
expressCompanyOptions.value = res.list || []
|
||||
} catch {
|
||||
expressCompanyOptions.value = []
|
||||
} finally {
|
||||
expressCompanyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2341,33 +2509,32 @@
|
||||
function onInstallmentCountChange() {
|
||||
const count = toIntOrUndefined(formData.installmentCount) || 0
|
||||
if (count > 0) {
|
||||
formData.installmentRatios = finalizeInstallmentRatios(
|
||||
resizeInstallmentRatios(formData.installmentRatios || [], count)
|
||||
formData.installmentRatios = resizeInstallmentAmounts(
|
||||
formData.installmentRatios || [],
|
||||
count,
|
||||
formData.contractAmount
|
||||
)
|
||||
}
|
||||
handleFqParamsChange()
|
||||
}
|
||||
|
||||
function onInstallmentRatioChange(changedIdx: number) {
|
||||
if (changedIdx === formData.installmentRatios.length - 1) return
|
||||
formData.installmentRatios = finalizeInstallmentRatios([...formData.installmentRatios])
|
||||
function onInstallmentAmountChange(_changedIdx: number) {
|
||||
syncPaymentDueAmounts()
|
||||
handleFqParamsChange()
|
||||
}
|
||||
|
||||
const installmentRatiosError = computed(() => {
|
||||
const ratios = formData.installmentRatios || []
|
||||
if (!ratios.length) return ''
|
||||
const head = ratios.slice(0, -1)
|
||||
const sum = head.reduce((s, v) => s + Number(v || 0), 0)
|
||||
if (sum > 100 + 0.001) return '前 N−1 期比例之和不能超过 100%'
|
||||
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 installmentRatiosSum = computed(() => {
|
||||
const total = (formData.installmentRatios || []).reduce((s, v) => s + Number(v || 0), 0)
|
||||
return Math.round(total * 100) / 100
|
||||
})
|
||||
const installmentAmountsSum = computed(() => sumInstallmentAmounts(formData.installmentRatios || []))
|
||||
|
||||
/** 按缴费模式同步各明细应缴费金额(跳过已锁定明细) */
|
||||
function syncPaymentDueAmounts() {
|
||||
@@ -2388,27 +2555,16 @@
|
||||
if (formData.payMode === 'fq') {
|
||||
const count = formData.installmentCount || formData.paymentDetails.length
|
||||
if (!count) return
|
||||
const ratios = finalizeInstallmentRatios(formData.installmentRatios || [])
|
||||
const amounts = normalizeInstallmentAmounts(
|
||||
formData.installmentRatios || [],
|
||||
contract,
|
||||
Number(count)
|
||||
)
|
||||
const paidRows = formData.paymentDetails.filter((d) => String(d.payStatus) === 'yjf')
|
||||
let amounts: number[]
|
||||
if (paidRows.length > 0 && formData.paymentDetails.some((d) => isPayDetailLocked(d))) {
|
||||
const paidSum = sumPaidAmount(formData.paymentDetails)
|
||||
const remaining = Math.round((Number(contract) - paidSum) * 100) / 100
|
||||
const unpaidIndices = Array.from({ length: Number(count) }, (_, i) => i).filter(
|
||||
(i) => !paidRows.some((d) => Number(d.installmentNo) === i + 1)
|
||||
)
|
||||
const unpaidAmounts = calcUnpaidAmountsByRatios(remaining, ratios, unpaidIndices)
|
||||
amounts = Array(Number(count)).fill(0)
|
||||
unpaidIndices.forEach((idx, j) => {
|
||||
amounts[idx] = unpaidAmounts[j] ?? 0
|
||||
})
|
||||
paidRows.forEach((d) => {
|
||||
const no = Number(d.installmentNo)
|
||||
if (no >= 1 && no <= Number(count)) amounts[no - 1] = Number(d.dueAmount) || 0
|
||||
})
|
||||
} else {
|
||||
amounts = calcInstallmentDueAmountsByRatios(Number(contract), ratios)
|
||||
}
|
||||
paidRows.forEach((d) => {
|
||||
const no = Number(d.installmentNo)
|
||||
if (no >= 1 && no <= Number(count)) amounts[no - 1] = Number(d.dueAmount) || 0
|
||||
})
|
||||
formData.paymentDetails.forEach((d, i) => {
|
||||
if (isPayDetailLocked(d)) return
|
||||
if (amounts[i] !== undefined) d.dueAmount = amounts[i]
|
||||
@@ -2422,7 +2578,14 @@
|
||||
if (s.sampleStatus === 'yzf') return sum
|
||||
return sum + (Number(s.price) || 0)
|
||||
}, 0)
|
||||
formData.contractAmount = Math.round(total * 100) / 100
|
||||
const nextContract = Math.round(total * 100) / 100
|
||||
if (formData.payMode === 'fq' && (formData.installmentRatios || []).length) {
|
||||
formData.installmentRatios = scaleInstallmentAmountsToContract(
|
||||
formData.installmentRatios,
|
||||
nextContract
|
||||
)
|
||||
}
|
||||
formData.contractAmount = nextContract
|
||||
syncPaymentDueAmounts()
|
||||
}
|
||||
|
||||
@@ -2756,15 +2919,20 @@
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--col6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--attachment {
|
||||
margin-top: 16px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.basic-remark-attach-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.form-col-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -3143,13 +3311,10 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--col5 {
|
||||
&--col5,
|
||||
&--col6 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--col2.basic-remark-attach-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.client-sub-cards {
|
||||
@@ -3157,106 +3322,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.installment-advance-ratios-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
.installment-amounts-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.installment-advance-field {
|
||||
flex: 0 0 auto;
|
||||
.installment-amounts-field.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.installment-ratios-field {
|
||||
flex: 1 1 360px;
|
||||
min-width: 0;
|
||||
.installment-amounts-summary-row {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.installment-ratios-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #c5e0d4;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.installment-ratios-bar {
|
||||
display: flex;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.installment-ratios-bar-seg {
|
||||
background: #047857;
|
||||
min-width: 2px;
|
||||
transition: flex 0.2s ease;
|
||||
|
||||
&.is-auto {
|
||||
background: #6bb99a;
|
||||
}
|
||||
|
||||
& + & {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.installment-ratios-cells {
|
||||
.installment-amounts-cells {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
.installment-ratio-cell {
|
||||
.installment-amount-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1 1 72px;
|
||||
min-width: 72px;
|
||||
max-width: 96px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding-left: 8px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.is-auto :deep(.el-input__wrapper) {
|
||||
background: #e8f4ee;
|
||||
}
|
||||
gap: 6px;
|
||||
min-width: 120px;
|
||||
flex: 1 1 120px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.installment-ratio-cell-no {
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.installment-ratios-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.installment-ratios-sum {
|
||||
.installment-amount-cell-no {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.installment-amounts-sum {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
|
||||
&.is-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip--error {
|
||||
margin-top: 6px;
|
||||
color: #dc2626;
|
||||
|
||||
Reference in new issue
Block a user