From 8cbbd3548bac2d59e17ed6e442e721ca6ffeb2f4 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sat, 10 Oct 2026 12:19:46 +0800 Subject: [PATCH] feat(web-admin): transfer remark columns and jfqrz withdraw UI. Co-authored-by: Cursor --- web-admin/src/api/presign-order.ts | 18 + web-admin/src/api/protocol-sample-payment.ts | 21 + .../cell-storage/payment-confirm/index.vue | 424 ++- .../payment-info/PaymentExcelList.vue | 743 +++++ .../presign-order/PresignOrderFormDialog.vue | 724 ++++- .../protocol/ProtocolFormDialog.vue | 2842 ++++++++--------- .../protocol/ProtocolViewDialog.vue | 1692 +++++----- .../protocol/SamplePaymentPanel.vue | 960 +++++- 8 files changed, 4774 insertions(+), 2650 deletions(-) create mode 100644 web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue diff --git a/web-admin/src/api/presign-order.ts b/web-admin/src/api/presign-order.ts index b4a99804..26684a8b 100644 --- a/web-admin/src/api/presign-order.ts +++ b/web-admin/src/api/presign-order.ts @@ -78,6 +78,13 @@ export function fetchSubmitPresignTransferFinance(transferId: string) { }) } +export function fetchWithdrawPresignTransfer(transferId: string) { + return request.post({ + url: `/web/admin/presign-transfers/${transferId}/withdraw`, + showSuccessMessage: true + }) +} + export function fetchPresignTransferReceiptConfirmList(params?: { page?: number pageSize?: number @@ -89,6 +96,17 @@ export function fetchPresignTransferReceiptConfirmList(params?: { }) } +export function fetchPresignTransferReceiptConfirmedList(params?: { + page?: number + pageSize?: number + keyword?: string +}) { + return request.get>>({ + url: '/web/admin/presign-transfers/receipt-confirmed', + params + }) +} + export function fetchConfirmPresignTransfer( transferId: string, data?: { financeConfirmAttachments?: string; financeRemark?: string } diff --git a/web-admin/src/api/protocol-sample-payment.ts b/web-admin/src/api/protocol-sample-payment.ts index 0c2b0b16..e2bc31dc 100644 --- a/web-admin/src/api/protocol-sample-payment.ts +++ b/web-admin/src/api/protocol-sample-payment.ts @@ -48,6 +48,13 @@ export function fetchSubmitPaymentTransferFinance(transferId: string) { }) } +export function fetchWithdrawPaymentTransfer(transferId: string) { + return request.post({ + url: `/web/admin/payment-transfers/${transferId}/withdraw`, + showSuccessMessage: true + }) +} + export function fetchPaymentTransferList(params?: { page?: number pageSize?: number @@ -156,3 +163,17 @@ export function fetchSamplePaymentDetail(samplePaymentId: string) { url: `/web/admin/sample-payments/${samplePaymentId}` }) } + +export function fetchPaymentChaseList(params?: { + page?: number + pageSize?: number + keyword?: string + chaseType?: string + payMode?: string + salesUserId?: string +}) { + return request.get>>({ + url: '/web/admin/payment-chase', + params + }) +} diff --git a/web-admin/src/views/cell-storage/payment-confirm/index.vue b/web-admin/src/views/cell-storage/payment-confirm/index.vue index fb92b029..ba123562 100644 --- a/web-admin/src/views/cell-storage/payment-confirm/index.vue +++ b/web-admin/src/views/cell-storage/payment-confirm/index.vue @@ -9,13 +9,26 @@ type="button" role="tab" class="panel-pill-tabs__btn" - :class="{ 'is-active': mainTab === 'payment' }" - :aria-selected="mainTab === 'payment'" - @click="mainTab = 'payment'" + :class="{ 'is-active': mainTab === 'presign' }" + :aria-selected="mainTab === 'presign'" + @click="mainTab = 'presign'" > - 缴费确认 - {{ - formatTabBadgeCount(paymentPendingBadge) + 意向金 + {{ + formatTabBadgeCount(presignPendingBadge) + }} + + @@ -35,7 +48,7 @@ -
+

待确认 @@ -49,19 +62,20 @@
- +
{{ row.motherName || '-' }}
-
- {{ row.protocolNo || '-' }} - - -
+
{{ cardSubtitle(row) }}
{{ formatPaymentAmount(row.transferAmount) }}
-
转账金额
+
+ {{ isPresignRow(row) ? '待确认意向金' : '待确认转账' }} +
-
-
- - 第 {{ row.installmentNo ?? '-' }} 期 -
-
- - {{ formatPaymentDueDate(row.installmentDueDate) }} -
-
- - {{ formatPaymentAmount(row.dueAmount) }} -
-
- - {{ payModeLabel(String(row.payMode || '')) }} -
-
- - {{ transferTypeLabel(String(row.transferType || '')) }} -
-
- - {{ formatDisplayOrDash(row.transferTime) }} -
-
- - {{ row.source === 'presign' ? '预签约意向金' : '正式协议' }} -
+
+ {{ + transferTypeLabel(String(row.transferType || '')) || '-' + }} + {{ + formatDisplayOrDash(row.transferTime) + }} +
+
备注:{{ row.remark }}
+
+ + +
+
+ +

{{ row.sampleTypeNames }}

@@ -183,19 +217,20 @@
- + - + {{ row.motherName || '-' }} {{ maskIdCard(row.motherIdCard) }} - - - {{ row.installmentNo ?? '-' }} - + @@ -375,6 +418,16 @@ > + + + ( + String(route.query.tab || '') === 'invoice' + ? 'invoice' + : String(route.query.tab || '') === 'formal' || String(route.query.tab || '') === 'payment' + ? 'formal' + : 'presign' + ) + const isPaymentTab = computed(() => mainTab.value === 'presign' || mainTab.value === 'formal') + const isPresignTab = computed(() => mainTab.value === 'presign') + const presignPendingBadge = ref(0) + const formalPendingBadge = ref(0) const invoicePendingBadge = ref(0) function formatTabBadgeCount(count: number): string { @@ -570,6 +643,23 @@ return '¥' + formatMoneyDisplay(v) } + function isPresignRow(row: Record | null | undefined) { + return String(row?.source || '') === 'presign' + } + + function cardSubtitle(row: Record) { + const parts: string[] = [] + const protocolNo = String(row.protocolNo || '').trim() + if (protocolNo) parts.push(protocolNo) + if (!isPresignRow(row)) { + const sampleCode = String(row.sampleCode || '').trim() + if (sampleCode) parts.push(sampleCode) + const idCard = String(row.motherIdCard || '').trim() + if (idCard) parts.push(maskIdCard(idCard)) + } + return parts.join(' · ') + } + const pendingSearchForm = reactive({ keyword: '' }) const confirmedSearchForm = reactive({ keyword: '' }) const pendingInstallmentDueDateRange = ref<[string, string] | null>(null) @@ -631,7 +721,8 @@ fetchPresignTransferReceiptConfirmList({ page: 1, pageSize: 1 }), fetchProtocolInvoicePendingCount() ]) - paymentPendingBadge.value = (paymentRes.total ?? 0) + (presignRes.total ?? 0) + formalPendingBadge.value = paymentRes.total ?? 0 + presignPendingBadge.value = presignRes.total ?? 0 invoicePendingBadge.value = Number(invRes?.count ?? 0) } catch { // 角标加载失败时保留上次数值,避免干扰主流程 @@ -641,18 +732,19 @@ async function loadPending() { pendingLoading.value = true try { - // 正式 + 预签约意向金待办合并(前端 merge,分页近似:各取一页后合并) - const [formal, presign] = await Promise.all([ - fetchPaymentTransferReceiptConfirmList(buildPendingParams()), - fetchPresignTransferReceiptConfirmList({ + if (isPresignTab.value) { + const res = await fetchPresignTransferReceiptConfirmList({ page: pendingPagination.page, pageSize: pendingPagination.pageSize, keyword: pendingSearchForm.keyword || undefined }) - ]) - const merged = [...(formal.list || []), ...(presign.list || [])] - pendingData.value = merged - pendingPagination.total = (formal.total ?? 0) + (presign.total ?? 0) + pendingData.value = res.list || [] + pendingPagination.total = res.total ?? 0 + } else { + const res = await fetchPaymentTransferReceiptConfirmList(buildPendingParams()) + pendingData.value = res.list || [] + pendingPagination.total = res.total ?? 0 + } } finally { pendingLoading.value = false } @@ -661,9 +753,19 @@ async function loadConfirmed() { confirmedLoading.value = true try { - const res = await fetchPaymentTransferReceiptConfirmedList(buildConfirmedParams()) - confirmedData.value = res.list || [] - confirmedPagination.total = res.total ?? 0 + if (isPresignTab.value) { + const res = await fetchPresignTransferReceiptConfirmedList({ + page: confirmedPagination.page, + pageSize: confirmedPagination.pageSize, + keyword: confirmedSearchForm.keyword || undefined + }) + confirmedData.value = res.list || [] + confirmedPagination.total = res.total ?? 0 + } else { + const res = await fetchPaymentTransferReceiptConfirmedList(buildConfirmedParams()) + confirmedData.value = res.list || [] + confirmedPagination.total = res.total ?? 0 + } } finally { confirmedLoading.value = false } @@ -738,7 +840,7 @@ async function submitReject() { if (!currentDetailId.value) return try { - await ElMessageBox.confirm('确定退回?退回后状态将变为「待缴费」。', '退回确认', { + await ElMessageBox.confirm('确定退回?退回后状态将变为「已缴费」。', '退回确认', { type: 'warning' }) } catch { @@ -779,20 +881,18 @@ onMounted(async () => { await loadTransferTypeDict() - const tab = route.query.tab - if (tab === 'invoice') { - mainTab.value = 'invoice' - } else { - mainTab.value = 'payment' - } loadTabPendingCounts() - if (mainTab.value === 'payment') { + if (isPaymentTab.value) { loadAll() } }) - watch(mainTab, () => { + watch(mainTab, (tab) => { loadTabPendingCounts() + if (tab === 'invoice') return + pendingPagination.page = 1 + confirmedPagination.page = 1 + loadAll() }) @@ -818,8 +918,7 @@ font-size: 13px; font-weight: 500; color: var(--mint-text-muted, #64748b); - padding: 0 0 4px; - line-height: 1.2; + padding-right: 12px; } .pending-card-section { @@ -829,23 +928,26 @@ .pending-card-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); - gap: 12px; - padding: 12px 24px 0; + grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); + gap: 16px; + padding: 16px 24px 8px; } .pending-card { + display: flex; + flex-direction: column; + gap: 14px; border: 1px solid var(--mint-border, #e2e8f0); - border-radius: 10px; + border-radius: 12px; background: #fff; - padding: 16px 18px; + padding: 20px 22px 16px; transition: border-color 0.15s, box-shadow 0.15s; &:hover { - border-color: #c5e0d4; - box-shadow: 0 2px 8px rgb(15 23 42 / 5%); + border-color: #a7f3d0; + box-shadow: 0 4px 14px rgb(15 23 42 / 6%); } } @@ -853,19 +955,18 @@ display: flex; align-items: flex-start; justify-content: space-between; - gap: 12px; - margin-bottom: 12px; + gap: 16px; } .pending-card__name { - font-size: 15px; + font-size: 18px; font-weight: 600; color: var(--text, #1e293b); - line-height: 1.4; + line-height: 1.3; } .pending-card__sub { - margin-top: 2px; + margin-top: 4px; font-size: 12px; color: var(--mint-text-muted, #64748b); line-height: 1.4; @@ -877,37 +978,93 @@ } .pending-card__amount-val { - font-size: 18px; + font-size: 22px; font-weight: 700; color: var(--primary-dark, #059669); font-variant-numeric: tabular-nums; - line-height: 1.3; + line-height: 1.2; } .pending-card__amount-label { - margin-top: 2px; - font-size: 11px; + margin-top: 4px; + font-size: 12px; color: var(--mint-text-muted, #64748b); } - .pending-card__grid { - display: grid; - grid-template-columns: repeat(3, 1fr); + .pending-card__transfer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; gap: 8px 16px; - margin-bottom: 14px; + padding: 10px 12px; + background: var(--primary-bg, #ecfdf5); + border-radius: 8px; + } + + .pending-card__transfer-way { + font-size: 13px; + font-weight: 600; + color: var(--primary-dark, #059669); + } + + .pending-card__transfer-time { + font-size: 13px; + color: var(--mint-text, #334155); + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + .pending-card__remark { + font-size: 12px; + color: var(--mint-text-muted, #64748b); + word-break: break-all; + } + + .pending-card__facts { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px 24px; } .pending-card__field label { display: block; - margin-bottom: 2px; - font-size: 11px; + margin-bottom: 4px; + font-size: 12px; color: var(--mint-text-muted, #64748b); } .pending-card__field span { - font-size: 13px; + font-size: 14px; color: var(--text, #1e293b); line-height: 1.4; + word-break: break-word; + } + + .pending-card__field em { + display: inline-block; + margin-left: 6px; + font-size: 12px; + font-style: normal; + color: var(--mint-text-muted, #64748b); + } + + .pending-card__samples { + padding-top: 2px; + + label { + display: block; + margin-bottom: 4px; + font-size: 12px; + color: var(--mint-text-muted, #64748b); + } + + p { + margin: 0; + font-size: 13px; + color: var(--mint-text, #334155); + line-height: 1.55; + } } .pending-card__foot { @@ -915,6 +1072,7 @@ align-items: center; justify-content: space-between; gap: 12px; + margin-top: 2px; padding-top: 12px; border-top: 1px solid #f1f5f9; } @@ -984,8 +1142,8 @@ grid-template-columns: 1fr; } - .pending-card__grid { - grid-template-columns: repeat(2, 1fr); + .pending-card__facts { + grid-template-columns: 1fr; } } diff --git a/web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue b/web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue new file mode 100644 index 00000000..7cb6a307 --- /dev/null +++ b/web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue @@ -0,0 +1,743 @@ + + + + + + diff --git a/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue b/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue index 29dbad45..5b79c65f 100644 --- a/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue +++ b/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue @@ -4,17 +4,19 @@ -
+
+ + + + + + + + + + + - + - +
意向金转账
-
+
添加转账笔 @@ -143,70 +192,176 @@ 已确认意向金合计 ¥{{ formatMoneyDisplay(confirmedDeposit) }}
-
- - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -231,7 +386,7 @@ diff --git a/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue b/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue index 9ab94916..4c6183f2 100644 --- a/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue +++ b/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue @@ -34,42 +34,13 @@ scroll-to-error class="protocol-form" > - -
-
-
-

一键导入

-
- - - -
- - - 一键导入 - -
-
+ +

基本信息

-
+
+
+
+ + +
+
+
+

采集信息

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+
+

样本及运输信息

+
+ +
+
+ + +
+
+
+

客户信息

+
+
+ +
+
+ 🤱 +

母亲信息

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + +
+
+ 👨 +

父亲信息

+
+
+ + + + + + + + + + + + +
+
+ + +
+
+ 👶 +

新生儿信息

+
+
+ + + + + + 男 + 女 + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+
+
+

缴费信息

+
+ + + + + + +
+
+ + +
+
+
+

附件信息

+
- - -
-
-
-

采集信息

-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
-
-
-

样本及运输信息

-
- -
-
- - -
-
-
-

客户信息

-
-
- -
-
- 🤱 -

母亲信息

- 关联客户 ▸ -
- - - - - -
- - - - - - - - - - - - - - - - - -
- - - - - - -
- - -
-
- 👨 -

父亲信息

-
-
- - - - - - - - - -
-
- - -
-
- 👶 -

新生儿信息

-
-
- - - - - - 男 - 女 - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - -
-
-
-

缴费信息

-
- - - - - - -
-
@@ -1081,8 +1178,7 @@ Loading, Download, ArrowDown, - ArrowRight, - Upload + ArrowRight } from '@element-plus/icons-vue' import { fetchCollectionProtocolDetail, @@ -1097,17 +1193,6 @@ useIndexedAttachmentDropZone } from '@/composables/useAttachmentDropZone' import { formatDatetimeToMinute } from '@/utils/chinese-datetime-parse' - import { - matchExpressCompany, - matchSampleTypeByExactName - } from '@/utils/donation-standard-import-parse' - import { - parseProtocolStandardImportText, - resolveTransferTypeCode, - isBlankString, - matchCellSampleType, - type ProtocolStandardImportParsed - } from '@/utils/protocol-standard-import-parse' import { fetchCustomerList, fetchUserList, @@ -1116,7 +1201,8 @@ fetchItemUnitList, fetchCollectionRegionList, fetchHospitalList, - fetchSearchCollectionHospitals, + fetchRegionStaffList, + fetchRegionsByStaffUser, fetchSampleCodeList, fetchExpressCompanyList, fetchSiteList @@ -1145,7 +1231,6 @@ getActiveSampleCount, isInstallmentPayMode, isPayModeValidForSampleCount, - resolvePaymentPreset, type PayMode as PresetPayMode } from '@/utils/protocol-payment-presets' import { @@ -1170,10 +1255,13 @@ payStatusTagType } from '@/utils/protocol-payment-labels' import SamplePaymentPanel from './SamplePaymentPanel.vue' - import ProtocolRefundRecordBlock from './ProtocolRefundRecordBlock.vue' + import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue' import { parseInstallmentRatios, + parsePreferentialPrice, protocolTotalMismatchHint, + sampleScheduleDuesMismatchHint, + sampleScheduleRequiredFieldsHint, sumSampleContractAmounts } from '@/utils/protocol-sample-payment' @@ -1219,18 +1307,6 @@ installmentRatios: [] as number[] }) - const customerId = ref(null) - const customerOptions = ref< - Array<{ id: string; customerName: string; phoneNo: string; idCardNo: string }> - >([]) - const customerLoading = ref(false) - const customerSnapshot = ref<{ - id: string - customerName: string - phoneNo: string - idCardNo: string - } | null>(null) - const regionOptions = ref([]) const regionLoading = ref(false) const hospitalOptions = ref([]) @@ -1246,6 +1322,16 @@ const salesLoading = ref(false) const salesSnapshot = ref(null) + const regionManagerUserId = ref(null) + const managerUserOptions = ref([]) + const managerLoading = ref(false) + const managerSnapshot = ref(null) + const linkedRegionMode = ref(false) + const linkedMotherCustomerId = ref('') + const linkedMotherCustomerName = ref('') + const linkedFatherCustomerId = ref('') + const linkedFatherCustomerName = ref('') + const sampleTypeOptions = ref< Array<{ id: string @@ -1390,15 +1476,14 @@ { key: 'collection', label: '采集信息' }, { key: 'sample', label: '样本及运输信息' }, { key: 'party', label: '客户信息' }, - { key: 'payment', label: '缴费信息' } + { key: 'payment', label: '缴费信息' }, + { key: 'attachment', label: '附件信息' } ] as const /* ====== 新增/编辑弹窗 ====== */ const dialogFormLoading = ref(false) const dialogTitle = ref('新增协议单') const isEdit = ref(false) - const standardImportText = ref('') - const standardImportLoading = ref(false) const submitLoading = ref(false) const editingId = ref(null) const formRef = ref() @@ -1444,6 +1529,7 @@ }>, // Party Customer customerId: '' as string, + fatherCustomerId: '' as string, motherName: '', motherPhone: '', motherIdCard: '', @@ -1469,6 +1555,7 @@ hospitalId: '', medicalStaff: '', salesUserId: '', + regionManagerUserId: '', sampleCollectTime: '', maternalBloodCollectTime: '', // Samples @@ -1493,23 +1580,40 @@ const paymentModelVersion = computed(() => Number(formData.paymentModelVersion || 2)) const presignDepositHint = ref | null>(null) + const depositTransfers = ref>>([]) const sampleContractSum = computed(() => sumSampleContractAmounts(formData.samplePayments)) const protocolTotalHint = computed(() => protocolTotalMismatchHint(formData.protocolTotalAmount, sampleContractSum.value) ) + /** 未手改时,协议总金额跟随样本合同合计 */ + const protocolTotalFollowSamples = ref(true) 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} 笔),仅提示不自动写入样本缴费` + return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),下方列表与预签同一数据,不自动写入样本缴费` }) - watch(sampleContractSum, (sum) => { + function roundMoney2(n: number) { + return Math.round(n * 100) / 100 + } + + function syncProtocolTotalFromSamples() { if (paymentModelVersion.value < 2) return - if (formData.protocolTotalAmount == null && sum > 0) { - formData.protocolTotalAmount = Math.round(sum * 100) / 100 - } + if (!protocolTotalFollowSamples.value) return + const sum = sampleContractSum.value + formData.protocolTotalAmount = sum > 0 ? roundMoney2(sum) : null + } + + function onProtocolTotalUserEdit(val: number | null) { + const sum = sampleContractSum.value + protocolTotalFollowSamples.value = + val != null && sum > 0 && Math.abs(Number(val) - sum) <= 0.015 + } + + watch(sampleContractSum, () => { + syncProtocolTotalFromSamples() }) const formRules = { @@ -1531,6 +1635,7 @@ healthSurveyAttachments: [], remark: '', customerId: '', + fatherCustomerId: '', motherName: '', motherPhone: '', motherIdCard: '', @@ -1555,6 +1660,7 @@ hospitalId: '', medicalStaff: '', salesUserId: '', + regionManagerUserId: '', sampleCollectTime: '', maternalBloodCollectTime: '', samples: [], @@ -1572,16 +1678,23 @@ sourcePresignOrderId: '', samplePayments: [] }) + protocolTotalFollowSamples.value = true presignDepositHint.value = null - customerId.value = null - customerOptions.value = [] - customerSnapshot.value = null + depositTransfers.value = [] collectorUserId.value = null collectorUserOptions.value = [] collectorSnapshot.value = null salesUserId.value = null salesUserOptions.value = [] salesSnapshot.value = null + regionManagerUserId.value = null + managerUserOptions.value = [] + managerSnapshot.value = null + linkedRegionMode.value = false + linkedMotherCustomerId.value = '' + linkedMotherCustomerName.value = '' + linkedFatherCustomerId.value = '' + linkedFatherCustomerName.value = '' regionOptions.value = [] hospitalOptions.value = [] sampleTypeOptions.value = [] @@ -1591,7 +1704,6 @@ expressCompanyOptions.value = [] activeFormTab.value = 'basic' lastConfirmedPayMode.value = 'dj' - standardImportText.value = '' lastConfirmedFqParams.value = { installmentCount: null, installmentIntervalMonths: null, @@ -1603,8 +1715,8 @@ function scrollToSection(key: string) { activeFormTab.value = key - if (key === 'payment') { - maybeApplyPaymentPresetOnTabEnter() + if (key === 'payment' && paymentModelVersion.value < 2) { + maybeApplyPreferentialContractOnTabEnter() } const el = document.getElementById(`form-section-${key}`) if (el) { @@ -1612,6 +1724,20 @@ } } + /** 有效样本优惠价之和(v1 整单合同默认) */ + function sumActiveSamplePreferentialPrices(): number | null { + let sum = 0 + let any = false + for (const s of formData.samples || []) { + if (String(s.sampleStatus || '') === 'yzf') continue + const p = parsePreferentialPrice(s) + if (p == null) continue + sum += p + any = true + } + return any ? roundMoney2(sum) : null + } + function normalizeSampleRow(raw: Record, _index: number) { const transportRaw = raw.transport && typeof raw.transport === 'object' ? raw.transport : {} const statusRaw = raw.sampleStatus @@ -1697,18 +1823,16 @@ } /** - * 分期默认:期数、每期间隔均为 6;开始时间为空时按分娩/预产期/今日填充。 - * resetCountInterval:切换到分期时强制重置期数与间隔(与新增一致)。 + * 分期结构默认:期数/间隔可重置;合同取优惠价之和;各期金额不预填。 */ 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 + const prefSum = sumActiveSamplePreferentialPrices() + if (prefSum != null) { + formData.contractAmount = prefSum } if (reset || isFqNumericFieldEmpty(formData.installmentCount)) { - formData.installmentCount = preset?.installmentCount ?? 6 + formData.installmentCount = 6 } if (reset || isFqNumericFieldEmpty(formData.installmentIntervalMonths)) { formData.installmentIntervalMonths = 6 @@ -1719,25 +1843,19 @@ if (reset || formData.installmentAdvanceDays == null) { formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS } - const count = toIntOrUndefined(formData.installmentCount) || 6 - if (preset?.installmentAmounts?.length) { - formData.installmentRatios = [...preset.installmentAmounts] - } else { - formData.installmentRatios = normalizeInstallmentAmounts( - formData.installmentRatios || [], - formData.contractAmount, - count - ) - } + formData.installmentRatios = [] } - 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 + /** v1:按优惠价之和填合同;分期明细金额留空 */ + function applyPreferentialContractForV1Mode(payMode: PresetPayMode) { + const prefSum = sumActiveSamplePreferentialPrices() + if (prefSum != null) { + formData.contractAmount = prefSum + } + if (isInstallmentPayMode(payMode)) { + if (isFqNumericFieldEmpty(formData.installmentCount)) { + formData.installmentCount = 6 + } if (isFqNumericFieldEmpty(formData.installmentIntervalMonths)) { formData.installmentIntervalMonths = 6 } @@ -1747,18 +1865,16 @@ if (formData.installmentAdvanceDays == null) { formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS } - formData.installmentRatios = [...(preset.installmentAmounts || [])] - formData.paymentDetails = generateInitialFqDetails( - getFqParamsFromForm(), - preset.contractAmount - ) + formData.installmentRatios = [] + const details = generateInitialFqDetails(getFqParamsFromForm(), formData.contractAmount) + formData.paymentDetails = details.map((d) => ({ ...d, dueAmount: null })) initPaymentDetailExpandFlags(formData.paymentDetails) markPayDetailLocks(formData.paymentDetails) syncLastConfirmedFqParams() } else { formData.paymentDetails = [ createPaymentDetailRow({ - dueAmount: preset.contractAmount, + dueAmount: formData.contractAmount, installmentDueDate: formData.lumpSumDeadline || '', expanded: true }) @@ -1768,10 +1884,10 @@ } } - function maybeApplyPaymentPresetOnTabEnter() { + function maybeApplyPreferentialContractOnTabEnter() { if (hasLockedPaymentDetails()) return if (formData.paymentDetails.length > 0) return - applyPaymentPresetForMode(formData.payMode as PresetPayMode) + applyPreferentialContractForV1Mode(formData.payMode as PresetPayMode) } function toIntOrUndefined(val: unknown): number | undefined { @@ -1934,7 +2050,7 @@ } if (!hasLockedPaymentDetails()) { - applyPaymentPresetForMode(toMode as PresetPayMode) + applyPreferentialContractForV1Mode(toMode as PresetPayMode) } else { if (isInstallmentPayMode(toMode)) { applyFqPaymentDefaults({ resetCountInterval: fromMode === 'dj' }) @@ -1947,12 +2063,9 @@ ) initPaymentDetailExpandFlags(formData.paymentDetails) markPayDetailLocks(formData.paymentDetails) - const preset = resolvePaymentPreset( - getActiveSampleCount(formData.samples), - toMode as PresetPayMode - ) - if (preset?.contractAmount != null) { - formData.contractAmount = preset.contractAmount + const prefSum = sumActiveSamplePreferentialPrices() + if (prefSum != null) { + formData.contractAmount = prefSum } } @@ -1984,9 +2097,8 @@ formData.motherEthnic = DEFAULT_MOTHER_ETHNIC formData.registerDate = new Date().toISOString().slice(0, 10).replace(/-/g, '') void loadEthnicOptions() - void remoteSearchCustomers('') - // 默认采集专员、销售人员为当前登录人 + // 默认采集专员、市场专员为当前登录人 const currentUser = userStore.getUserInfo const uid = String(currentUser?.id || currentUser?.userId || '') if (uid) { @@ -2015,11 +2127,12 @@ collectorSnapshot.value = { ...userStub } collectorUserOptions.value = [{ ...userStub }] - // 销售人员默认当前用户 + // 市场专员默认当前用户,并按关联地区带出采集地区 / 地区经理 salesUserId.value = uid formData.salesUserId = uid salesSnapshot.value = { ...userStub } salesUserOptions.value = [{ ...userStub }] + void applyRegionsForSalesUser(uid) } } @@ -2037,13 +2150,32 @@ formData.sourcePresignOrderId = String(prot.sourcePresignOrderId || '') } presignDepositHint.value = (bundle.presignDepositHint as Record) || null + depositTransfers.value = Array.isArray(bundle.depositTransfers) + ? (bundle.depositTransfers as Array>) + : [] const samplePays = bundle.samplePayments as Array> | null if (samplePays && samplePays.length) { formData.samplePayments = samplePays.map((p) => ({ ...p, - installmentRatios: parseInstallmentRatios(p.installmentRatios) + installmentRatios: parseInstallmentRatios(p.installmentRatios), + schedules: (p.schedules || []).map((sch: Record) => ({ + ...sch, + transfers: (sch.transfers || []).map((t: Record) => ({ + ...t, + attachments: Array.isArray(t.attachments) + ? t.attachments.map((a: Record) => ({ ...a })) + : [] + })) + })) })) } + { + const sum = sampleContractSum.value + const total = formData.protocolTotalAmount + protocolTotalFollowSamples.value = + total == null || (sum > 0 && Math.abs(Number(total) - sum) <= 0.015) + syncProtocolTotalFromSamples() + } const pc = bundle.partyCustomer as Record | null if (pc) { @@ -2069,6 +2201,11 @@ formData.apgar1min = pc.apgar1min ?? null formData.apgar5min = pc.apgar5min ?? null formData.customerId = String(pc.customerId || '') + formData.fatherCustomerId = String(pc.fatherCustomerId || '') + linkedMotherCustomerId.value = formData.customerId + linkedMotherCustomerName.value = formData.motherName + linkedFatherCustomerId.value = formData.fatherCustomerId + linkedFatherCustomerName.value = formData.fatherName } const ci = bundle.collectionInfo as Record | null @@ -2077,6 +2214,7 @@ formData.hospitalId = String(ci.hospitalId || '') formData.medicalStaff = String(ci.medicalStaff || '') formData.salesUserId = String(ci.salesUserId || '') + formData.regionManagerUserId = String(ci.regionManagerUserId || '') formData.sampleCollectTime = formatDatetimeToMinute(String(ci.sampleCollectTime || '')) formData.maternalBloodCollectTime = formatDatetimeToMinute(String(ci.maternalBloodCollectTime || '')) } @@ -2177,17 +2315,16 @@ applyProtocolBundleToForm(bundle as Record, row) } + const ciBundle = (bundle as any)?.collectionInfo as Record | null const hydrateTasks: Promise[] = [ hydrateCollectorUserForEdit( formData.collectorUserId, String((row as any).collectorUserName || '') ), hydrateSalesUserForEdit(formData.salesUserId, String((row as any).salesUserName || '')), - hydrateCustomerForEdit( - formData.customerId, - formData.motherName, - formData.motherPhone, - formData.motherIdCard + hydrateManagerUserForEdit( + formData.regionManagerUserId, + String(ciBundle?.regionManagerUserName || (row as any).regionManagerUserName || '') ) ] if (formData.regionId) { @@ -2205,6 +2342,12 @@ ) } await Promise.all(hydrateTasks) + if (formData.salesUserId) { + await applyRegionsForSalesUser(formData.salesUserId, { + preferKeepRegion: true, + skipManagerDefault: true + }) + } if (formData.samples.length) { await Promise.all([ remoteSearchSampleTypes(''), @@ -2253,6 +2396,20 @@ ElMessage.warning('请先为样本分配样本编码,再保存按样本缴费方案') return } + for (const pay of formData.samplePayments) { + const requiredHint = sampleScheduleRequiredFieldsHint(pay) + if (requiredHint) { + submitLoading.value = false + await ElMessageBox.alert(requiredHint, '无法保存', { type: 'warning' }) + return + } + const duesHint = sampleScheduleDuesMismatchHint(pay) + if (duesHint) { + submitLoading.value = false + await ElMessageBox.alert(duesHint, '无法保存', { type: 'warning' }) + return + } + } const mismatch = protocolTotalMismatchHint( formData.protocolTotalAmount, sumSampleContractAmounts(formData.samplePayments) @@ -2327,6 +2484,7 @@ remark: formData.remark || undefined, partyCustomer: { customerId: formData.customerId || undefined, + fatherCustomerId: formData.fatherCustomerId || undefined, motherName: formData.motherName || undefined, motherPhone: formData.motherPhone || undefined, motherIdCard: formData.motherIdCard || undefined, @@ -2353,6 +2511,7 @@ hospitalId: formData.hospitalId || undefined, medicalStaff: formData.medicalStaff || undefined, salesUserId: formData.salesUserId || undefined, + regionManagerUserId: formData.regionManagerUserId || undefined, sampleCollectTime: formData.sampleCollectTime || undefined, maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined }, @@ -2392,15 +2551,23 @@ 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 - })) + transfers: (sch.transfers || []).map((t: any) => { + const fromFiles = Array.isArray(t.attachments) + ? t.attachments + .filter((f: any) => f?.fileUrl && !f._uploading) + .map((f: any) => String(f.fileUrl)) + .join(',') + : '' + return { + id: t.id, + transferType: t.transferType, + transferAmount: t.transferAmount, + transferTime: t.transferTime, + transferAttachments: fromFiles || t.transferAttachments || undefined, + payStatus: t.payStatus || 'dfj', + remark: t.remark + } + }) })) })) } else { @@ -2539,47 +2706,42 @@ ) } - async function hydrateCustomerForEdit( - customerIdVal: string, - motherName: string, - motherPhone: string, - motherIdCard: string - ) { - const cid = (customerIdVal || '').trim() - if (!cid) { - customerId.value = null - customerSnapshot.value = null - customerOptions.value = [] - formData.customerId = '' - return - } - let snapshot = { - id: cid, - customerName: motherName, - phoneNo: motherPhone, - idCardNo: motherIdCard - } - const keyword = motherPhone || motherName || motherIdCard - if (keyword) { - try { - const res = await fetchCustomerList({ page: 1, pageSize: 50, keyword }) - const match = (res.list || []).find((c) => c.id === cid) - if (match) { - snapshot = { - id: match.id, - customerName: match.customerName || motherName, - phoneNo: match.phoneNo || motherPhone, - idCardNo: match.idCardNo || motherIdCard - } - } - } catch { - /* 使用母亲信息占位 */ + async function hydrateManagerUserForEdit(userId: string, displayName?: string) { + await hydrateRemoteUserForEdit( + userId, + displayName, + (user) => { + managerSnapshot.value = { ...user } + regionManagerUserId.value = user.id + managerUserOptions.value = [{ ...user }] + formData.regionManagerUserId = user.id + }, + () => { + regionManagerUserId.value = null + managerSnapshot.value = null + managerUserOptions.value = [] + formData.regionManagerUserId = '' } - } - customerSnapshot.value = snapshot - customerId.value = cid - customerOptions.value = [snapshot] - formData.customerId = cid + ) + } + + function makeManagerUserStub(id: string, name: string): Api.User.UserItem { + return { + id, + nickName: name || id, + phoneNo: '', + deptId: '', + userNo: '', + email: '', + sex: '', + avatar: '', + status: '1', + tenantNo: '', + orderNum: 0, + remark: '', + createTime: '', + updateTime: '' + } as Api.User.UserItem } async function ensureRegionOptionForEdit(regionId: string, regionName?: string) { @@ -2729,570 +2891,6 @@ ensureSitesInOptions(formData.samples) } - function formatNowDatetime(): string { - const now = new Date() - const pad = (n: number) => String(n).padStart(2, '0') - return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` - } - - /** 一键导入前清空上次导入/手填的可覆盖字段(保留协议状态、登记日期、附件等) */ - function clearFormBeforeStandardImport() { - customerId.value = null - customerSnapshot.value = null - customerOptions.value = [] - formData.customerId = '' - formData.motherName = '' - formData.motherPhone = '' - formData.motherIdCard = '' - formData.motherBirthYm = '' - formData.motherEmail = '' - formData.motherAddress = '' - - formData.fatherName = '' - formData.fatherPhone = '' - formData.fatherIdCard = '' - formData.newbornName = '' - formData.newbornIdCard = '' - formData.expectedDeliveryDate = '' - - formData.newbornSex = '' - formData.newbornWeightGram = null - formData.apgar1min = null - formData.apgar5min = null - formData.newbornDeliveryTime = '' - formData.deliveryMode = '' - formData.birthOrder = null - - formData.regionId = '' - formData.hospitalId = '' - formData.medicalStaff = '' - formData.sampleCollectTime = '' - formData.maternalBloodCollectTime = '' - regionOptions.value = [] - hospitalOptions.value = [] - - collectorUserId.value = null - collectorSnapshot.value = null - collectorUserOptions.value = [] - formData.collectorUserId = '' - - salesUserId.value = null - salesSnapshot.value = null - salesUserOptions.value = [] - formData.salesUserId = '' - - formData.samples = [] - sampleCodeOptions.value = [] - siteOptions.value = [] - expressCompanyOptions.value = [] - - formData.payMode = 'dj' - formData.contractAmount = null - formData.lumpSumDeadline = '' - formData.installmentCount = null - formData.installmentIntervalMonths = null - formData.installmentStartDate = '' - formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS - formData.installmentRatios = [] - formData.paymentDetails = [] - lastConfirmedPayMode.value = 'dj' - lastConfirmedFqParams.value = { - installmentCount: null, - installmentIntervalMonths: null, - installmentStartDate: '', - installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS, - installmentRatios: [] as number[] - } - - formData.remark = '' - } - - async function applyMotherByName(name: string): Promise<'linked' | 'name-only' | 'skipped'> { - const keyword = (name || '').trim() - if (!keyword) return 'skipped' - if (!isBlankString(formData.customerId)) return 'skipped' - if (!isBlankString(formData.motherName)) return 'skipped' - - try { - const res = await fetchCustomerList({ page: 1, pageSize: 50, keyword }) - const list = res.list || [] - const match = - list.find((c) => c.customerName === keyword) || - list.find((c) => (c.customerName || '').includes(keyword)) - if (match) { - customerOptions.value = [match, ...list.filter((c) => c.id !== match.id)] - onCustomerChange(match.id) - return 'linked' - } - formData.motherName = keyword - return 'name-only' - } catch { - formData.motherName = keyword - return 'name-only' - } - } - - async function applyHospitalByName(name: string): Promise { - const keyword = (name || '').trim() - if (!keyword || !isBlankString(formData.hospitalId)) return false - try { - const res = await fetchSearchCollectionHospitals({ keyword, limit: 20 }) - const list = res.list || [] - const match = - list.find((h) => h.hospitalName === keyword) || - list.find((h) => (h.hospitalName || '').includes(keyword)) || - list[0] - if (!match) return false - - const regionStub: Api.BizCollectionRegion.RegionItem = { - id: match.regionId, - regionName: match.regionName || match.regionId, - hospitalNames: [], - tenantNo: match.tenantNo || '', - orderNum: 0, - remark: '', - createTime: '', - updateTime: '' - } - if (!regionOptions.value.some((r) => r.id === match.regionId)) { - regionOptions.value = [regionStub, ...regionOptions.value] - } - formData.regionId = match.regionId - await loadHospitalsByRegion(match.regionId, keyword) - if (!hospitalOptions.value.some((h) => h.id === match.id)) { - hospitalOptions.value = [match, ...hospitalOptions.value] - } - formData.hospitalId = match.id - return true - } catch { - return false - } - } - - async function applySalesUserByName(name: string): Promise { - const keyword = (name || '').trim() - if (!keyword) return false - try { - const res = await fetchUserList({ - page: 1, - pageSize: 50, - keyword, - status: '1' - }) - const list = res.list || [] - const match = - list.find((u) => u.nickName === keyword) || - list.find((u) => (u.nickName || '').includes(keyword)) - if (!match) return false - applyUserToCollector(match) - applyUserToSales(match) - return true - } catch { - return false - } - } - - function applyUserToCollector(u: Api.User.UserItem) { - collectorSnapshot.value = { ...u } - collectorUserId.value = u.id - formData.collectorUserId = u.id - if (!collectorUserOptions.value.some((x) => x.id === u.id)) { - collectorUserOptions.value = [u, ...collectorUserOptions.value] - } - } - - function applyUserToSales(u: Api.User.UserItem) { - salesSnapshot.value = { ...u } - salesUserId.value = u.id - formData.salesUserId = u.id - if (!salesUserOptions.value.some((x) => x.id === u.id)) { - salesUserOptions.value = [u, ...salesUserOptions.value] - } - } - - async function resolveSampleCodeByCode(code: string) { - const keyword = (code || '').trim() - if (!keyword) return null - const res = await fetchSampleCodeList({ page: 1, pageSize: 50, keyword }) - const list = (res.list || []) as Api.BizSampleCode.SampleCodeItem[] - return ( - list.find((sc) => sc.sampleCode === keyword) || - list.find((sc) => String(sc.sampleCode || '').includes(keyword)) || - null - ) - } - - function applyParsedTransportToSample( - sample: Record, - parsed: ProtocolStandardImportParsed, - warnings: string[], - filledLabels: string[] - ) { - if (!parsed.transportType) return - - const transport = { ...(sample.transport || createEmptyTransport()) } - let transportFilled = false - - if (isBlankString(transport.transportType)) { - transport.transportType = parsed.transportType - transportFilled = true - } - if (isBlankString(transport.destSiteId) && siteOptions.value.length) { - const site = siteOptions.value[0] - transport.destSiteId = site.id - transportFilled = true - } - if (parsed.etaTime && isBlankString(transport.etaTime)) { - transport.etaTime = parsed.etaTime - transportFilled = true - } else if (isBlankString(transport.etaTime)) { - transport.etaTime = formatNowDatetime() - transportFilled = true - } - if (parsed.transportType === 'kdys' && isBlankString(transport.expressCompanyId)) { - const ec = matchExpressCompany( - parsed.transportModeText || '顺丰快递', - expressCompanyOptions.value - ) - if (ec) { - transport.expressCompanyId = ec.id - if (!expressCompanyOptions.value.some((x) => x.id === ec.id)) { - expressCompanyOptions.value = [ec, ...expressCompanyOptions.value] - } - transportFilled = true - } else { - warnings.push(`未找到快递公司「${parsed.transportModeText || ''}」`) - } - } - if (parsed.transportType === 'ryxd' && parsed.carrierName && isBlankString(transport.carrierName)) { - transport.carrierName = parsed.carrierName - transportFilled = true - } - sample.transport = transport - if (transportFilled) filledLabels.push('运输信息') - } - - async function applySampleHintsFromImport( - parsed: ProtocolStandardImportParsed, - warnings: string[], - filledLabels: string[] - ) { - let hints = parsed.sampleHints - if (!hints.length) { - const matched = matchCellSampleType(parsed.sampleTypeHeader, sampleTypeOptions.value) - if (matched) { - hints = [{ typeKey: 'hsc', typeName: matched.typeName }] - } - } - - for (const hint of hints) { - const matchedType = matchSampleTypeByExactName(hint.typeName, sampleTypeOptions.value) - const sample = normalizeSampleRow({ sampleTypeId: '', sampleStatus: 'ycj' }, formData.samples.length) - formData.samples.push(sample) - - if (matchedType) { - sample.sampleTypeId = matchedType.id - sample.storageYears = matchedType.storageYears ?? null - sample.originalPrice = matchedType.storagePrice ?? null - sample.price = matchedType.storagePrice ?? null - if (!sampleTypeOptions.value.some((x) => x.id === matchedType.id)) { - sampleTypeOptions.value = [matchedType, ...sampleTypeOptions.value] - } - filledLabels.push(hint.typeName) - } else { - warnings.push(`无法匹配样本类型「${hint.typeName}」`) - } - - if (hint.sampleCode) { - const sc = await resolveSampleCodeByCode(hint.sampleCode) - if (sc) { - sample.sampleCodeId = sc.id - sample.sampleCode = sc.sampleCode - if (!sampleCodeOptions.value.some((x) => x.id === sc.id)) { - sampleCodeOptions.value = [sc, ...sampleCodeOptions.value] - } - filledLabels.push('样本编码') - } else { - warnings.push(`未找到样本编码「${hint.sampleCode}」`) - } - } - - applyParsedTransportToSample(sample, parsed, warnings, filledLabels) - } - } - - function syncFormSamplesAfterImport() { - formData.samples = formData.samples.map((s, i) => - normalizeSampleRow( - { - ...s, - transport: s.transport ? { ...s.transport } : undefined - }, - i - ) - ) - hydrateSampleLookupOptions() - } - - function applyPaymentFromImport(parsed: ProtocolStandardImportParsed, warnings: string[]) { - formData.payMode = parsed.payMode || 'dj' - lastConfirmedPayMode.value = formData.payMode as PayMode - - if (isInstallmentPayMode(parsed.payMode || 'dj')) { - applyFqPaymentDefaults() - } - - if (parsed.contractAmount != null) { - formData.contractAmount = parsed.contractAmount - } - - const transferTypeCode = resolveTransferTypeCode( - parsed.transferTypeLabel || '', - transferTypeOptions.value - ) - if (parsed.transferTypeLabel && !transferTypeCode) { - warnings.push(`无法匹配转账方式「${parsed.transferTypeLabel}」`) - } - - 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())}` - - formData.paymentDetails = [] - const transfers = parsed.paymentTransfers.filter((t) => t.transferAmount > 0) - - if (transfers.length) { - transfers.forEach((t, i) => { - formData.paymentDetails.push({ - transferType: transferTypeCode, - dueAmount: t.transferAmount, - transferAmount: t.transferAmount, - transferTime: t.transferTime || defaultTime, - payStatus: 'dfj', - installmentDueDate: formData.payMode === 'dj' ? formData.lumpSumDeadline : '', - _expanded: i === 0, - attachments: [] - }) - }) - } else if (formData.payMode === 'dj' && formData.contractAmount != null) { - addPaymentDetail() - const detail = formData.paymentDetails[0] - if (detail) { - detail.transferType = transferTypeCode - detail.dueAmount = formData.contractAmount - detail.transferAmount = formData.contractAmount - } - } - - initPaymentDetailExpandFlags(formData.paymentDetails) - markPayDetailLocks(formData.paymentDetails) - if (!transfers.length && !(formData.payMode === 'dj' && formData.contractAmount != null)) { - syncPaymentDueAmounts() - } - - if (parsed.contractAmount != null && transfers.length) { - const sum = transfers.reduce((acc, t) => acc + Number(t.transferAmount || 0), 0) - if (Math.abs(sum - Number(parsed.contractAmount)) > 0.01) { - warnings.push( - `转账合计 ${sum} 与合同金额 ${parsed.contractAmount} 不一致,请核对缴费明细` - ) - } - } - } - - async function handleStandardImport() { - const parsed = parseProtocolStandardImportText(standardImportText.value) - if (!parsed) { - ElMessage.warning('请粘贴有效的标准信息') - return - } - - standardImportLoading.value = true - const warnings: string[] = [] - const filledLabels: string[] = [] - - try { - clearFormBeforeStandardImport() - await ensureTransferTypeOptions() - await remoteSearchSampleTypes('') - - const tasks: Promise[] = [] - - if (parsed.motherName) { - tasks.push( - applyMotherByName(parsed.motherName).then((result) => { - if (result === 'linked') filledLabels.push('母亲信息(已关联客户)') - else if (result === 'name-only') filledLabels.push('母亲姓名') - }) - ) - } - - if (parsed.hospitalName) { - tasks.push( - applyHospitalByName(parsed.hospitalName).then((ok) => { - if (ok) filledLabels.push('采集地区/采集医院') - else if (isBlankString(formData.hospitalId)) { - warnings.push(`未找到采集医院「${parsed.hospitalName}」`) - } - }) - ) - } - - if (parsed.salesUserName) { - tasks.push( - applySalesUserByName(parsed.salesUserName).then((ok) => { - if (ok) filledLabels.push('采集专员/销售人员') - else warnings.push(`未找到销售人员「${parsed.salesUserName}」`) - }) - ) - } - - const needsTransport = !!parsed.transportType - if (needsTransport) { - tasks.push(remoteSearchSites('').then(() => undefined)) - if (parsed.transportType === 'kdys') { - tasks.push( - remoteSearchExpressCompanies( - (parsed.transportModeText || '').replace(/快递$/, '') || '顺丰' - ).then(() => undefined) - ) - } - } - - await Promise.all(tasks) - - if (parsed.motherPhone) { - formData.motherPhone = parsed.motherPhone - filledLabels.push('孕妇电话') - } - if (parsed.newbornSex) { - formData.newbornSex = parsed.newbornSex - filledLabels.push('新生儿性别') - } - if (parsed.newbornWeightGram != null) { - formData.newbornWeightGram = parsed.newbornWeightGram - filledLabels.push('新生儿体重') - } - if (parsed.apgarScore != null) { - formData.apgar1min = parsed.apgarScore - filledLabels.push('新生儿评分') - } - if (parsed.newbornDeliveryTime) { - formData.newbornDeliveryTime = parsed.newbornDeliveryTime - filledLabels.push('新生儿分娩时间') - } - if (parsed.sampleCollectTime) { - formData.sampleCollectTime = formatDatetimeToMinute(parsed.sampleCollectTime) - filledLabels.push('样本采集时间') - } - if (parsed.maternalBloodExplicitEmpty) { - formData.maternalBloodCollectTime = '' - filledLabels.push('母血采集时间') - } else if (parsed.maternalBloodCollectTime) { - formData.maternalBloodCollectTime = formatDatetimeToMinute(parsed.maternalBloodCollectTime) - filledLabels.push('母血采集时间') - } - if (parsed.deliveryMode) { - formData.deliveryMode = parsed.deliveryMode - filledLabels.push('分娩方式') - } - if (parsed.birthOrder != null) { - formData.birthOrder = parsed.birthOrder - filledLabels.push('胎次') - } - if (parsed.medicalStaff) { - formData.medicalStaff = parsed.medicalStaff - filledLabels.push('采集医护') - } - if (parsed.remark) { - formData.remark = parsed.remark - filledLabels.push('备注') - } - - await applySampleHintsFromImport(parsed, warnings, filledLabels) - - if (formData.samples.length) { - syncFormSamplesAfterImport() - } - - applyPaymentFromImport(parsed, warnings) - - if (parsed.contractAmount != null) { - filledLabels.push('合同金额') - } - - if (parsed.transportModeText && !parsed.transportType) { - warnings.push(`无法识别运输方式「${parsed.transportModeText}」`) - } - - const uniqueFilled = [...new Set(filledLabels)] - if (uniqueFilled.length) { - ElMessage.success(`已填充:${uniqueFilled.join('、')}`) - } else if (!warnings.length) { - ElMessage.info('没有可填充的字段') - } - if (warnings.length) { - ElMessage.warning(warnings.join(';')) - } - } catch { - ElMessage.error('导入失败,请检查标准信息格式') - } finally { - standardImportLoading.value = false - } - } - - /* ====== 客户检索(母亲信息) ====== */ - function formatCustomerLabel(c: { customerName: string; phoneNo: string; idCardNo: string }) { - const parts = [c.customerName || ''] - if (c.phoneNo) parts.push(c.phoneNo) - if (c.idCardNo) parts.push(c.idCardNo) - return parts.filter(Boolean).join(' · ') - } - - async function remoteSearchCustomers(query: string) { - customerLoading.value = true - try { - const res = await fetchCustomerList({ page: 1, pageSize: 50, keyword: query || undefined }) - let list = [...(res.list || [])] - // Keep current selection visible - if ( - customerId.value && - customerSnapshot.value?.id === customerId.value && - !list.some((x) => x.id === customerId.value) - ) { - list = [customerSnapshot.value, ...list] - } - customerOptions.value = list - } catch { - customerOptions.value = [] - } finally { - customerLoading.value = false - } - } - - function onCustomerChange(val: string | null | undefined) { - if (!val) { - customerId.value = null - customerSnapshot.value = null - // Clear auto-filled fields only - formData.motherName = '' - formData.motherPhone = '' - formData.motherIdCard = '' - formData.motherBirthYm = '' - formData.motherEmail = '' - return - } - const c = customerOptions.value.find((x) => x.id === val) || customerSnapshot.value - if (!c) return - customerSnapshot.value = { ...c } as any - customerId.value = c.id - formData.customerId = c.id - formData.motherName = c.customerName?.trim() || '' - formData.motherPhone = c.phoneNo?.trim() || '' - formData.motherIdCard = c.idCardNo?.trim() || '' - calcBirthFromIdCard() - } - /* ====== 采集专员检索 ====== */ async function remoteSearchCollectorUsers(query: string) { collectorLoading.value = true @@ -3333,7 +2931,7 @@ formData.collectorUserId = u.id } - /* ====== 销售人员检索 ====== */ + /* ====== 市场专员检索 ====== */ async function remoteSearchSalesUsers(query: string) { salesLoading.value = true try { @@ -3359,11 +2957,38 @@ } } - function onSalesUserChange(val: string | null | undefined) { + async function remoteSearchManagerUsers(query: string) { + managerLoading.value = true + try { + const res = await fetchUserList({ + page: 1, + pageSize: 50, + keyword: (query || '').trim() || undefined, + status: '1' + }) + let list = [...(res.list || [])] + const cur = regionManagerUserId.value + if (cur && !list.some((x) => x.id === cur)) { + const keep = + managerSnapshot.value?.id === cur + ? managerSnapshot.value + : managerUserOptions.value.find((x) => x.id === cur) + if (keep) list = [keep, ...list] + } + managerUserOptions.value = list + } catch { + managerUserOptions.value = [] + } finally { + managerLoading.value = false + } + } + + async function onSalesUserChange(val: string | null | undefined) { if (!val) { salesUserId.value = null salesSnapshot.value = null formData.salesUserId = '' + await applyRegionsForSalesUser(null) return } const u = salesUserOptions.value.find((x) => x.id === val) || salesSnapshot.value @@ -3371,6 +2996,131 @@ salesSnapshot.value = { ...u } salesUserId.value = u.id formData.salesUserId = u.id + await applyRegionsForSalesUser(u.id) + } + + function onManagerUserChange(val: string | null | undefined) { + if (!val) { + regionManagerUserId.value = null + managerSnapshot.value = null + formData.regionManagerUserId = '' + return + } + const u = managerUserOptions.value.find((x) => x.id === val) || managerSnapshot.value + if (!u) { + regionManagerUserId.value = val + formData.regionManagerUserId = val + return + } + managerSnapshot.value = { ...u } + regionManagerUserId.value = u.id + formData.regionManagerUserId = u.id + } + + async function applyDefaultRegionManager(regionId: string) { + if (!regionId) { + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + managerSnapshot.value = null + return + } + try { + const res: any = await fetchRegionStaffList(regionId, { page: 1, pageSize: 100 }) + const list = (res?.list || []) as Array> + const leader = list.find((x) => x.isLeader) || list[0] + if (!leader?.userId) { + regionManagerUserId.value = null + formData.regionManagerUserId = '' + return + } + const stub = makeManagerUserStub( + String(leader.userId), + String(leader.nickName || leader.userName || '') + ) + regionManagerUserId.value = stub.id + formData.regionManagerUserId = stub.id + managerSnapshot.value = { ...stub } + if (!managerUserOptions.value.some((x) => x.id === stub.id)) { + managerUserOptions.value = [stub, ...managerUserOptions.value] + } + } catch { + /* 地区无人员配置时留空,可手工选择 */ + } + } + + async function applyRegionsForSalesUser( + userId: string | null, + opts?: { preferKeepRegion?: boolean; skipManagerDefault?: boolean } + ) { + const uid = (userId || '').trim() + if (!uid) { + linkedRegionMode.value = false + formData.regionId = '' + formData.hospitalId = '' + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + managerSnapshot.value = null + regionOptions.value = [] + hospitalOptions.value = [] + return + } + try { + const res: any = await fetchRegionsByStaffUser(uid) + const list = (res?.list || []) as Array<{ id: string; regionName: string }> + if (list.length > 0) { + linkedRegionMode.value = true + // 编辑回填:以库中已存 regionId 为准,即使不在专员关联列表中也保留 + const keep = Boolean(opts?.preferKeepRegion && formData.regionId) + const inLinked = Boolean(formData.regionId && list.some((x) => x.id === formData.regionId)) + const prevRegionName = + regionOptions.value.find((r) => r.id === formData.regionId)?.regionName || '' + regionOptions.value = list as any + if (keep && !inLinked) { + const stub: Api.BizCollectionRegion.RegionItem = { + id: formData.regionId, + regionName: prevRegionName || formData.regionId, + hospitalNames: [], + tenantNo: '', + orderNum: 0, + remark: '', + createTime: '', + updateTime: '' + } + regionOptions.value = [stub, ...regionOptions.value] + } + const nextId = keep ? formData.regionId : inLinked ? formData.regionId : list[0].id + const regionChanged = nextId !== formData.regionId + formData.regionId = nextId + if (regionChanged) { + formData.hospitalId = '' + hospitalOptions.value = [] + await loadHospitalsByRegion(nextId, '') + if (!opts?.skipManagerDefault) { + await applyDefaultRegionManager(nextId) + } + } else if (!opts?.skipManagerDefault && !formData.regionManagerUserId) { + await applyDefaultRegionManager(nextId) + } + } else { + linkedRegionMode.value = false + // 编辑回填:专员无关联地区时保留库中已存值,仅退出关联模式(恢复全量检索) + if (opts?.preferKeepRegion) { + return + } + formData.regionId = '' + formData.hospitalId = '' + hospitalOptions.value = [] + regionOptions.value = [] + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + managerSnapshot.value = null + } + } catch { + linkedRegionMode.value = false + } } async function remoteSearchSampleCodes(query: string, codeFilter?: string) { @@ -3479,24 +3229,32 @@ } } - function onRegionChange(val: string | null | undefined) { + async function onRegionChange(val: string | null | undefined) { if (!val) { formData.regionId = '' hospitalOptions.value = [] formData.hospitalId = '' + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + managerSnapshot.value = null return } formData.regionId = val formData.hospitalId = '' hospitalOptions.value = [] - // 联动加载该地区下的医院 - loadHospitalsByRegion(val) + await loadHospitalsByRegion(val) + await applyDefaultRegionManager(val) } function onRegionClear() { formData.regionId = '' formData.hospitalId = '' hospitalOptions.value = [] + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + managerSnapshot.value = null } async function loadHospitalsByRegion(regionId: string, keyword?: string) { @@ -3613,9 +3371,87 @@ } } - /** 样本价格或状态变更(不自动改合同金额) */ + /** 样本优惠价变更:SamplePaymentPanel 按 sample.price 同步合同金额 */ function onSamplePriceChange() { - // 保留样本价格字段,不与合同金额联动 + // 联动在 SamplePaymentPanel 的 samples watch 中完成 + } + + /* ====== 母亲/父亲姓名检索客户档案 ====== */ + async function queryPartyCustomers( + query: string, + cb: (list: Api.BizCustomer.CustomerItem[]) => void + ) { + const q = (query || '').trim() + if (!q) { + cb([]) + return + } + try { + const res = await fetchCustomerList({ page: 1, pageSize: 20, keyword: q }) + cb(res.list || []) + } catch { + cb([]) + } + } + + function onMotherCustomerSelect(item: Api.BizCustomer.CustomerItem) { + const name = String(item.customerName || '').trim() + formData.customerId = item.id + formData.motherName = name + formData.motherPhone = String(item.phoneNo || '').trim() + formData.motherIdCard = String(item.idCardNo || '').trim() + linkedMotherCustomerId.value = item.id + linkedMotherCustomerName.value = name + calcBirthFromIdCard() + const birthYm = String(item.birthYm || '').trim() + const ethnic = String(item.ethnic || '').trim() + const email = String(item.email || '').trim() + const address = String(item.address || '').trim() + if (!(formData.motherBirthYm || '').trim() && birthYm) formData.motherBirthYm = birthYm + if (!(formData.motherEthnic || '').trim() && ethnic) { + formData.motherEthnic = ethnic + ensureEthnicOptionInList(ethnic) + } + if (!(formData.motherEmail || '').trim() && email) formData.motherEmail = email + if (!(formData.motherAddress || '').trim() && address) formData.motherAddress = address + } + + function onMotherCustomerClear() { + formData.customerId = '' + linkedMotherCustomerId.value = '' + linkedMotherCustomerName.value = '' + } + + function onMotherNameInput(val: string) { + if (linkedMotherCustomerId.value && val !== linkedMotherCustomerName.value) { + formData.customerId = '' + linkedMotherCustomerId.value = '' + linkedMotherCustomerName.value = '' + } + } + + function onFatherCustomerSelect(item: Api.BizCustomer.CustomerItem) { + const name = String(item.customerName || '').trim() + formData.fatherCustomerId = item.id + formData.fatherName = name + formData.fatherPhone = String(item.phoneNo || '').trim() + formData.fatherIdCard = String(item.idCardNo || '').trim() + linkedFatherCustomerId.value = item.id + linkedFatherCustomerName.value = name + } + + function onFatherCustomerClear() { + formData.fatherCustomerId = '' + linkedFatherCustomerId.value = '' + linkedFatherCustomerName.value = '' + } + + function onFatherNameInput(val: string) { + if (linkedFatherCustomerId.value && val !== linkedFatherCustomerName.value) { + formData.fatherCustomerId = '' + linkedFatherCustomerId.value = '' + linkedFatherCustomerName.value = '' + } } /* ====== 母亲身份证号 ↔ 出生年月联动 ====== */ @@ -3638,20 +3474,18 @@ () => calcBirthFromIdCard() ) - /** 样本数变化时按当前缴费模式刷新默认定价(新增单;无已缴锁定明细时整单重填) */ + /** 样本数/优惠价变化:新增单 v1 整单合同跟随优惠价之和(无已缴锁定时) */ watch(activeSampleCount, () => { if (isEdit.value) return + if (paymentModelVersion.value >= 2) return if (hasLockedPaymentDetails()) { - const preset = resolvePaymentPreset( - getActiveSampleCount(formData.samples), - formData.payMode as PresetPayMode - ) - if (preset?.contractAmount != null) { - formData.contractAmount = preset.contractAmount + const prefSum = sumActiveSamplePreferentialPrices() + if (prefSum != null) { + formData.contractAmount = prefSum } return } - applyPaymentPresetForMode(formData.payMode as PresetPayMode) + applyPreferentialContractForV1Mode(formData.payMode as PresetPayMode) }) /* ====== 趸交截止时间联动 ====== */ @@ -3907,78 +3741,44 @@ /* ═══════════════════════════════════════════ DIALOG BODY ═══════════════════════════════════════════ */ - .protocol-dialog-body { + :deep(.protocol-dialog-body) { overflow: visible; - background: #f5f7fa; + background: #fff; + } + + .detail-tabs { + margin: 0 20px 12px; } .protocol-form { + padding: 0 20px 16px; + :deep(.el-form-item) { margin-bottom: 4px; } } /* ═══════════════════════════════════════════ - SECTION CARDS + SECTIONS(单区域连续排布,无分块圆角卡片) ═══════════════════════════════════════════ */ .form-tab-pane { - padding: 0 28px 4px; + padding: 0; & + .form-tab-pane { - margin-top: 0; - padding-top: 0; + margin-top: 14px; } } .section-card { - border-radius: 12px; - padding: 22px 24px; - margin-bottom: 20px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); - transition: - transform 0.2s, - box-shadow 0.2s; + border-radius: 0; + padding: 0; + margin-bottom: 0; + box-shadow: none; position: relative; - background: #fff; - - &:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - } + background: transparent; &--green { - background: #f0fdf4; - } - - &--import { - margin-bottom: 0; - - .import-textarea-item { - margin-bottom: 12px; - - :deep(.el-textarea__inner) { - min-height: 560px; - } - } - } - } - - .import-actions { - display: flex; - flex-shrink: 0; - justify-content: flex-end; - padding: 0 0 8px; - } - - .form-tab-pane--import-basic { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 16px; - align-items: stretch; - margin-bottom: 28px; - - .section-card { - margin-bottom: 0; - height: 100%; + background: transparent; } } @@ -3986,9 +3786,9 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 18px; - padding-bottom: 12px; - border-bottom: 1px solid #e2e8f0; + margin-bottom: 10px; + padding-bottom: 0; + border-bottom: none; } .section-title-left { @@ -3998,16 +3798,16 @@ &::before { content: ''; width: 3px; - height: 18px; + height: 14px; background: #047857; border-radius: 2px; - margin-right: 10px; + margin-right: 8px; flex-shrink: 0; } } .section-title h3 { - font-size: 15px; + font-size: 14px; font-weight: 600; color: #1e293b; } @@ -4019,7 +3819,7 @@ font-size: 13px; font-weight: 600; color: #374151; - margin: 18px 0 14px; + margin: 12px 0 10px; &::before { content: ''; @@ -4040,6 +3840,10 @@ color: #94a3b8; } + .muted-dash { + color: var(--mint-text-muted, #94a3b8); + } + .warning-hint { color: #d97706; } @@ -4050,7 +3854,7 @@ .form-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 16px 20px; + gap: 12px 16px; &--col2 { grid-template-columns: repeat(2, 1fr); @@ -4069,8 +3873,12 @@ } &--attachment { - margin-top: 16px; + margin-top: 10px; grid-template-columns: 1fr; + + &:first-of-type { + margin-top: 0; + } } :deep(.el-form-item) { @@ -4125,18 +3933,6 @@ font-weight: 600; color: #1e293b; } - - .search-link { - margin-left: auto; - font-size: 12px; - color: #047857; - cursor: pointer; - font-weight: 500; - - &:hover { - text-decoration: underline; - } - } } /* ═══════════════════════════════════════════ @@ -4151,6 +3947,37 @@ border: 1px solid rgba(0, 0, 0, 0.03); } + .protocol-total-card { + padding-top: 14px; + padding-bottom: 14px; + } + + .protocol-total-inline { + .protocol-total-item { + margin-bottom: 0; + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + + :deep(.el-form-item__label) { + margin: 0 !important; + padding: 0 !important; + line-height: 32px; + height: auto; + width: auto !important; + justify-content: flex-start; + white-space: nowrap; + } + + :deep(.el-form-item__content) { + flex: 1; + min-width: 0; + margin-left: 0 !important; + } + } + } + .payment-details { display: flex; flex-direction: column; @@ -4450,10 +4277,6 @@ RESPONSIVE ═══════════════════════════════════════════ */ @media (max-width: 900px) { - .form-tab-pane--import-basic { - grid-template-columns: 1fr; - } - .form-grid { grid-template-columns: repeat(2, 1fr); @@ -4523,4 +4346,35 @@ color: #dc2626; font-size: 12px; } + + :global(.mother-customer-suggest-popper) { + min-width: 520px !important; + width: max-content !important; + } + + :global(.mother-customer-suggest-popper .mother-customer-suggest-item) { + display: flex; + align-items: center; + gap: 16px; + white-space: nowrap; + line-height: 1.4; + } + + :global(.mother-customer-suggest-popper .mother-customer-suggest-item__name) { + flex: 0 0 72px; + font-weight: 500; + color: var(--mint-text, #0f172a); + } + + :global(.mother-customer-suggest-popper .mother-customer-suggest-item__phone) { + flex: 0 0 120px; + color: var(--mint-text-muted, #64748b); + } + + :global(.mother-customer-suggest-popper .mother-customer-suggest-item__idcard) { + flex: 0 0 auto; + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; + color: var(--mint-text, #0f172a); + } diff --git a/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue b/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue index 894c1885..71c4d5eb 100644 --- a/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue +++ b/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue @@ -65,211 +65,18 @@ >
{{ dash(protocol?.createTime) }}
- -
-
状态变更记录
-
- - - - -
-
- -
-
-
协议附件
-
- -
-
-
-
知情同意书
-
- -
-
-
-
孕妇健康调查表
-
- -
-
-
+
+
备注
{{ dash(protocol?.remark) }}
- -
-
-
储存证附件
-
- -
-
-
-
制作状态
-
- - {{ storageCertMakeStatusLabel(String(storageCertInfo?.makeStatus || '0')) }} - -
-
-
-
制作日期
-
{{ formatYmd(storageCertInfo?.makeDate) }}
-
-
-
- - -
-

状态变更记录

- -
- - - - -

采集信息

-
+
采集地区
{{ regionDisplay }}
{{ dash(collectionInfo?.medicalStaff) }}
销售人员
市场专员
{{ salesDisplay }}
+
地区经理
{{ regionManagerDisplay }}
样本采集时间
{{ dash(collectionInfo?.sampleCollectTime) }}
运费(元)
-
{{ - row.freight?.freightAmount != null - ? '¥' + formatMoneyDisplay(row.freight.freightAmount) - : '—' - }}{{ - Number(row.freight?.linkedSampleCount || 0) > 1 ? '(共用)' : '' - }}
+
{{ + row.freight?.freightAmount != null + ? '¥' + formatMoneyDisplay(row.freight.freightAmount) + : '—' + }}{{ + Number(row.freight?.linkedSampleCount || 0) > 1 ? '(共用)' : '' + }}
@@ -415,6 +228,7 @@ :gate-qc-info="row.qcInfo" :process-qc-info-list="row.processQcInfoList" :sample-reception="row.sampleReception" + :show-section-label="false" /> @@ -489,11 +303,11 @@ >
母亲民族
{{ dash(partyCustomer?.motherEthnic) }}
+
母亲邮箱
{{ dash(partyCustomer?.motherEmail) }}
-
母亲邮箱
{{ dash(partyCustomer?.motherEmail) }}
母亲家庭住址
{{ @@ -576,116 +390,80 @@
- +
-

新生儿保险信息

-
-
满月日期
{{ dash(fullmoonInsuranceInfo?.fullmoonDate) }}
-
-
待办状态
-
- - {{ fullmoonTodoStatusLabel(String(fullmoonInsuranceInfo?.todoStatus || '0')) }} - +

储存证与新生儿保险

+
+
+

储存证

+
+
+
办理时间
+
{{ dash(storageCertInfo?.handleTime) }}
+
+
+
办理人员
+
{{ dash(storageCertInfo?.handlerUserName) }}
+
+
+
+
储存证附件
+ +
+
+
+
备注
+
{{ + dash(storageCertInfo?.remark) + }}
+
-
-
是否已订购保险
-
- - {{ - insuranceSubscribedLabel( - String(fullmoonInsuranceInfo?.insuranceSubscribed || '0') - ) - }} - +
+

新生儿保险

+
+
+
办理时间
+
{{ dash(fullmoonInsuranceInfo?.handleTime) }}
+
+
+
办理人员
+
{{ dash(fullmoonInsuranceInfo?.handlerUserName) }}
+
+
+
+
保险附件
+ +
+
+
+
备注
+
{{ + dash(fullmoonInsuranceInfo?.handleRemark) + }}
+
-
保险公司
{{ - dash(fullmoonInsuranceInfo?.insuranceCompanyName) - }}
-
保险险种
{{ dash(fullmoonInsuranceInfo?.insuranceType) }}
-
保险单号
{{ - dash(fullmoonInsuranceInfo?.insurancePolicyNo) - }}
-
保单费用(元)
{{ - fmtMoney(fullmoonInsuranceInfo?.insuranceFee) - }}
-
保险单生效时间
{{ - formatYmd(fullmoonInsuranceInfo?.insuranceEffectiveDate) - }}
-
保险单失效时间
{{ - formatYmd(fullmoonInsuranceInfo?.insuranceExpiryDate) - }}
-
办理日期
{{ - formatYmd(fullmoonInsuranceInfo?.handleDate) - }}
-
-
-
-
办理备注
-
{{ - dash(fullmoonInsuranceInfo?.handleRemark) - }}
-
-
-
-
保险单附件
- -
—
-
+

缴费信息

- + - +
@@ -1212,6 +1065,105 @@
+ + +
+

附件信息

+
+
+
协议附件
+
+ +
+
+
+
知情同意书
+
+ +
+
+
+
孕妇健康调查表
+
+ +
+
+
+
+ + +
+

状态变更记录

+ +
+ + + + +
+
@@ -1302,11 +1254,11 @@ listRow?: CollectionProtocolRow | null /** 展示审核记录 Tab(采集协议单审核页) */ showAuditHistory?: boolean - /** 储存证管理查看:展示储存证附件,状态变更记录独立区域 */ + /** 只读摘要:展示储存证办理字段 */ showStorageCert?: boolean - /** 满月待办查看:客户信息下方独立展示新生儿保险信息 */ + /** 只读摘要:与储存证并排展示新生儿保险办理字段 */ showFullmoonInsurance?: boolean - /** 样本信息管理:储存证/保险轻量办理 */ + /** 样本信息管理:储存证/保险轻量办理(可编辑,与只读摘要互斥) */ showHandlePanels?: boolean highlightSampleCode?: string }>(), @@ -1331,6 +1283,11 @@ { key: 'payment', label: '缴费信息' } ] as const + /** 协议审核等页:只读展示储存证/保险(与样本信息录入字段对齐);办理编辑用 showHandlePanels */ + const showHandleSummary = computed( + () => !props.showHandlePanels && (props.showStorageCert || props.showFullmoonInsurance) + ) + const formTabs = computed(() => { const tabs: Array<{ key: string; label: string }> = [...baseFormTabs] if (props.showHandlePanels) { @@ -1338,13 +1295,15 @@ tabs.splice(sampleIdx + 1, 0, { key: 'storage-cert-handle', label: '储存证办理' }) tabs.splice(sampleIdx + 2, 0, { key: 'insurance-handle', label: '保险办理' }) } - if (props.showFullmoonInsurance) { + if (showHandleSummary.value) { const partyIdx = tabs.findIndex((t) => t.key === 'party') - tabs.splice(partyIdx + 1, 0, { key: 'fullmoon-insurance', label: '新生儿保险' }) + tabs.splice(partyIdx + 1, 0, { key: 'storage-insurance', label: '储存证与保险' }) } if (props.showAuditHistory) { tabs.push({ key: 'audit', label: '审核记录' }) } + tabs.push({ key: 'attachment', label: '附件信息' }) + tabs.push({ key: 'status-change', label: '状态变更记录' }) return tabs }) @@ -1379,6 +1338,9 @@ const refundRecords = computed( () => (bundle.value?.refundRecords as Array>) || [] ) + const depositTransfers = computed( + () => (bundle.value?.depositTransfers as Array>) || [] + ) const presignDepositHint = computed( () => (bundle.value?.presignDepositHint as Record) || null ) @@ -1388,7 +1350,7 @@ if (!h) return '' const amt = Number(h.confirmedDepositAmount || 0) const cnt = Number(h.confirmedTransferCount || 0) - return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),仅提示不自动计入样本缴费` + return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),下方列表与预签同一数据,不自动计入样本缴费` }) const v2ReceivedAmount = computed(() => { let sum = 0 @@ -1417,6 +1379,64 @@ () => (bundle.value?.paymentDetails as Array>) || [] ) + /** + * v1 明细按期归组,结构对齐 v2 schedules(期汇总 + 多笔转账) + * - 趸交:整单视为第 1 期 + * - 分期:按 installmentNo 归组 + */ + const v1PayPeriods = computed(() => { + const details = paymentDetails.value + if (!details.length) return [] as Array<{ + scheduleNo: number + dueAmount: number | null + dueDate: string + paidAmount: number + transfers: Array> + }> + + const paidOf = (rows: Array>) => + rows + .filter((d) => String(d.payStatus || '') === 'yjf') + .reduce((sum, d) => sum + (Number(d.transferAmount) || 0), 0) + + const isFq = isInstallmentPayMode(String(payment.value?.payMode || '')) + if (!isFq) { + return [ + { + scheduleNo: 1, + dueAmount: + details[0]?.dueAmount != null + ? Number(details[0].dueAmount) + : payment.value?.contractAmount != null + ? Number(payment.value.contractAmount) + : null, + dueDate: String( + payment.value?.lumpSumDeadline || details[0]?.installmentDueDate || '' + ), + paidAmount: paidOf(details), + transfers: details + } + ] + } + + const groups = new Map>>() + for (const d of details) { + const no = Number(d.installmentNo) || 0 + const list = groups.get(no) || [] + list.push(d) + groups.set(no, list) + } + return [...groups.entries()] + .sort((a, b) => a[0] - b[0]) + .map(([no, rows]) => ({ + scheduleNo: no || 1, + dueAmount: rows[0]?.dueAmount != null ? Number(rows[0].dueAmount) : null, + dueDate: String(rows[0]?.installmentDueDate || ''), + paidAmount: paidOf(rows), + transfers: rows + })) + }) + /** 已收金额 = 已缴费明细的转账金额之和(排除待缴费、退费确认中、已退费) */ const receivedAmount = computed(() => { return paymentDetails.value @@ -1448,11 +1468,15 @@ const sc = bundle.value?.storageCert as Record | undefined | null if (sc) return sc if (!props.showStorageCert || !props.listRow) return null + const row = props.listRow as Record return { - makeStatus: props.listRow.makeStatus ?? '0', - certImageUrls: props.listRow.certImageUrls ?? '', - makeDate: props.listRow.makeDate ?? '', - remark: props.listRow.remark ?? '' + makeStatus: row.makeStatus ?? '0', + certImageUrls: row.certImageUrls ?? '', + makeDate: row.makeDate ?? '', + handleTime: row.handleTime ?? '', + handlerUserId: row.handlerUserId ?? '', + handlerUserName: row.handlerUserName ?? '', + remark: row.remark ?? '' } }) @@ -1476,16 +1500,9 @@ if (!props.showFullmoonInsurance || !props.listRow) return null const row = props.listRow as Record return { - fullmoonDate: row.fullmoonDate ?? '', - todoStatus: row.todoStatus ?? '0', - insuranceSubscribed: row.insuranceSubscribed ?? '0', - insuranceCompanyName: row.insuranceCompanyName ?? '', - insuranceType: row.insuranceType ?? '', - insurancePolicyNo: row.insurancePolicyNo ?? '', - insuranceEffectiveDate: row.insuranceEffectiveDate ?? row.policyEffectiveDate ?? '', - insuranceExpiryDate: row.insuranceExpiryDate ?? row.policyExpiryDate ?? '', - insuranceFee: row.insuranceFee, - handleDate: row.handleDate ?? row.processDate ?? '', + handleTime: row.handleTime ?? '', + handlerUserId: row.handlerUserId ?? '', + handlerUserName: row.handlerUserName ?? '', handleRemark: row.handleRemark ?? row.remark ?? '', insuranceAttachments: row.insuranceAttachments ?? '' } @@ -1505,36 +1522,12 @@ }) }) - function fullmoonTodoStatusLabel(status: string) { - return status === '1' ? '已办保险' : '未办保险' - } - - function fullmoonTodoStatusTagType(status: string) { - return status === '1' ? 'success' : 'warning' - } - - function insuranceSubscribedLabel(val: string) { - return val === '1' ? '是' : '否' - } - - function insuranceSubscribedTagType(val: string) { - return val === '1' ? 'success' : 'info' - } - /** 金额只读展示:千分位 + ¥ 前缀,null 显示 — */ function fmtMoney(v: unknown): string { if (v == null || v === '') return '—' return '¥' + formatMoneyDisplay(v) } - function storageCertMakeStatusLabel(makeStatus: string) { - return makeStatus === '1' ? '已制作' : '未制作' - } - - function storageCertMakeStatusTagType(makeStatus: string) { - return makeStatus === '1' ? 'success' : 'warning' - } - const dialogTitle = computed(() => { const name = partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName @@ -1571,6 +1564,15 @@ return u?.nickName || dash(id) }) + const regionManagerDisplay = computed(() => { + const fromList = (props.listRow as any)?.regionManagerUserName + if (fromList) return String(fromList) + const fromBundle = (collectionInfo.value as any)?.regionManagerUserName + if (fromBundle) return String(fromBundle) + const id = (collectionInfo.value as any)?.regionManagerUserId + return dash(id) + }) + const regionDisplay = computed(() => { const fromList = props.listRow?.regionName if (fromList) return String(fromList) @@ -1808,6 +1810,14 @@ return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id) } + function samplePayTitle(sp: Record) { + const code = String(sp.sampleCode || '').trim() + const sample = samples.value.find((s) => String(s.sampleCode || '') === code) + if (sample?.sampleTypeName) return String(sample.sampleTypeName) + if (sample?.sampleTypeId) return sampleTypeName(String(sample.sampleTypeId)) + return code || '样本' + } + function ensureUnitsInOptions(samples: Array>) { const merged = [...unitOptions.value] for (const s of samples) { @@ -1875,6 +1885,11 @@ return Array.isArray(detail.attachments) ? detail.attachments : [] } + function transferAttachments(tr: Record) { + if (Array.isArray(tr.attachments) && tr.attachments.length) return tr.attachments + return urlsToAttachmentFiles(String(tr.transferAttachments || '')) + } + function financeConfirmAttachmentFiles(detail: Record) { if (Array.isArray(detail.financeConfirmAttachmentFiles)) { return detail.financeConfirmAttachmentFiles @@ -2256,6 +2271,18 @@ &--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } + + &--6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + &--7 { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } + } + + .data-grid--collection-row { + gap: 12px 14px; } .data-item { @@ -2284,30 +2311,37 @@ } } - /* 储存证:储存证附件 / 制作状态 / 制作日期同一行(在协议附件行下方) */ - .storage-cert-info-row { - margin-top: 14px; - padding-top: 14px; - border-top: 1px dashed #e2e8f0; - } - - .storage-cert-attachments-item { - .storage-cert-attachments-value { - font-weight: 400; - } - - :deep(.protocol-attachment-empty) { - font-size: 14px; - color: var(--mint-text-muted, #334155); - } - } - - /* 协议附件 / 知情同意书 / 孕妇健康调查表 / 备注(三行堆叠) */ + /* 协议附件 / 知情同意书 / 孕妇健康调查表(三行堆叠) */ .protocol-basic-attachments-block { margin-top: 14px; display: flex; flex-direction: column; gap: 14px; + + &--flush { + margin-top: 0; + } + } + + .handle-summary-cards { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } + + .handle-summary-card { + background: #fff; + border-radius: 10px; + padding: 18px 20px; + border: 1px solid rgba(0, 0, 0, 0.03); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + + .attachment-row, + .remark-row { + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed #e2e8f0; + } } .protocol-attachments-item { @@ -2573,11 +2607,26 @@ } .sample-card__section-label { - font-size: 11px; + margin: 0 0 10px; + padding-left: 10px; + font-size: 13px; font-weight: 600; - color: var(--mint-text-muted, #334155); - margin-bottom: 10px; - letter-spacing: 0.3px; + line-height: 1.4; + color: #334155; + border-left: 3px solid #10b981; + } + + /* 样本卡内子组件小节标题:与 sample-card__section-label 统一 */ + :deep(.sample-card__section-label), + :deep(.sample-qc-grouped-panel__subtitle), + :deep(.detail-section-title) { + margin: 0 0 10px; + padding-left: 10px; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + color: #334155; + border-left: 3px solid #10b981; } .data-value--code { @@ -2597,6 +2646,10 @@ grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; + + &--3 { + grid-template-columns: repeat(3, 1fr); + } } .payment-metric { @@ -2767,37 +2820,147 @@ .sample-pay-readonly__head { display: flex; + align-items: center; justify-content: space-between; - margin-bottom: 8px; + gap: 12px; + margin-bottom: 12px; font-size: 13px; font-weight: 600; } + .sample-pay-readonly__title { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + } + .sample-pay-readonly__amt { + flex-shrink: 0; color: #047857; } - .sample-pay-readonly__sch { - margin-top: 8px; - padding: 8px 10px; - border-radius: 8px; - background: #f8fafc; + .sample-pay-readonly__batch-title { + margin: 14px 0 8px; + padding-left: 10px; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + color: #334155; + border-left: 3px solid #10b981; + + &--mt { + margin-top: 20px; + } } - .sample-pay-readonly__sch-hd { - margin-bottom: 6px; + .sample-pay-readonly__empty { + margin: 4px 0 8px; font-size: 12px; + color: var(--mint-text-muted, #94a3b8); + + &--sm { + margin: 0; + padding: 2px 0 0; + } + } + + .sample-pay-readonly__sch { + margin-top: 10px; + padding: 12px 12px 10px; + border-radius: 8px; + background: #f8fafc; + border: 1px solid #eef2f7; + } + + .sample-pay-readonly__sch-title { + margin-bottom: 10px; + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + .sample-pay-readonly__tr-title { + margin: 12px 0 8px; + font-size: 12px; + font-weight: 600; color: #475569; } - .sample-pay-readonly__tr { + .sample-pay-readonly__tr-count { + font-weight: 400; + color: #94a3b8; + } + + .sample-pay-readonly__sch .view-inner-table-wrap { + margin-top: 0; + } + + .muted-dash { + color: var(--mint-text-muted, #94a3b8); + } + + /* ---- v1 缴费明细(简洁平铺) ---- */ + .pay-detail-simple { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 16px; + } + + .pay-detail-simple__card { + padding: 12px 14px; + border: 1px solid var(--mint-border, #e2e8f0); + border-radius: 10px; + background: #fff; + } + + .pay-detail-simple__head { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + } + + .pay-detail-simple__title { display: flex; flex-wrap: wrap; - gap: 8px 12px; + gap: 8px; align-items: center; - padding: 4px 0; + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + .pay-detail-simple__amt { + flex-shrink: 0; + font-size: 14px; + font-weight: 700; + color: #047857; + } + + .pay-detail-simple__attach { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-start; + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed #e2e8f0; + } + + .pay-detail-simple__attach-label { + flex-shrink: 0; + margin-top: 4px; font-size: 12px; - color: #334155; + color: #64748b; + } + + .pay-detail-simple__extra { + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed #e2e8f0; } /* ---- Mini Flow ---- */ @@ -3320,18 +3483,11 @@ } } - .client-sub-cards { + .client-sub-cards, + .handle-summary-cards { grid-template-columns: 1fr; } - .storage-cert-info-row.data-grid--3 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - - .storage-cert-attachments-item { - grid-column: 1 / -1; - } - } - .protocol-attach-remark-row.data-grid--2 { grid-template-columns: 1fr; } @@ -3354,19 +3510,19 @@ border-bottom: none; } - .client-sub-cards { + .client-sub-cards, + .handle-summary-cards { grid-template-columns: 1fr; } - .data-grid--4 { + .data-grid--4, + .data-grid--6, + .data-grid--7 { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .storage-cert-info-row.data-grid--3 { - grid-template-columns: 1fr; - } - - .payment-summary { + .payment-summary, + .payment-summary--3 { grid-template-columns: repeat(2, 1fr); } diff --git a/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue b/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue index e131cbcb..2f09c57d 100644 --- a/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue +++ b/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue @@ -18,23 +18,30 @@ {{ pay.sampleCode }}
- - {{ pay.payMode === 'fq' ? '分期' : '趸交' }} + + {{ payModeLabel(String(pay.payMode || '')) || '—' }}
-
+
- - + - + @@ -106,11 +116,7 @@ >
- 第 {{ sch.scheduleNo || sIdx + 1 }} 期 · 应缴 - {{ fmtMoney(sch.dueAmount) }} - - + 第 {{ sch.scheduleNo || sIdx + 1 }} 期 已缴 {{ @@ -121,80 +127,219 @@ fmtMoney(schedulePaidArrears(Number(sch.dueAmount) || 0, sch.transfers || []).arrears) }} - + 添加转账
- -
- - - - - - +
+ - + - - - {{ payStatusLabel(String(tr.payStatus || 'dfj')) }} - - - 提交财务确认 - - - 删除 - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -202,7 +347,7 @@