将项目筛选并入搜索框下方的 van-dropdown 行,与协议状态样式一致;科研捐赠 Tab 同步展示两项筛选,并修复捐赠列表按 protocolStatus 过滤 donation_status。 Co-authored-by: Cursor <cursoragent@cursor.com>
842 lines
37 KiB
JavaScript
842 lines
37 KiB
JavaScript
import { groupLegacyPaymentPeriods } from '../protocol-v1-payment.js'
|
||
import {
|
||
formatProductionReceptionDate,
|
||
productionReceptionFieldValue,
|
||
resolveProductionReceptionFields
|
||
} from '../protocol-production-reception.js'
|
||
import {
|
||
resolveStatusTag,
|
||
protocolStatus,
|
||
donationStatus,
|
||
payMode,
|
||
payStatus,
|
||
paymentScheduleStatus,
|
||
auditStatus
|
||
} from '../status-tag-registry.js'
|
||
|
||
const SEX_LABELS = Object.freeze({ '1': '男', '0': '女' })
|
||
/** 科研捐赠客户信息只展示母亲姓名;这些父母字段仍参与提交,避免编辑时清空已有数据 */
|
||
const DONATION_HIDDEN_PARENT_KEYS = new Set([
|
||
'motherPhone', 'motherIdCard', 'motherEmail', 'motherBirthYm', 'motherEthnic',
|
||
'motherAddress', 'fatherName', 'fatherPhone', 'fatherIdCard'
|
||
])
|
||
const DONATION_PARENT_FIELD_HIDE = Object.freeze({ hiddenSources: ['research_donation'] })
|
||
const DELIVERY_LABELS = Object.freeze({ sc: '顺产', pgc: '剖宫产' })
|
||
const SAMPLE_STATUS_LABELS = Object.freeze({
|
||
ycj: '已采集', jcz: '检测中', yrk: '已入库', yzf: '已作废'
|
||
})
|
||
const VOID_TYPE_LABELS = Object.freeze({
|
||
khhc: '客户毁存', jcbhg: '检测不合格'
|
||
})
|
||
const STATUS_CHANGE_TYPE_LABELS = Object.freeze({
|
||
activate: '生效',
|
||
expire: '过期',
|
||
void: '作废',
|
||
to_research: '转科研',
|
||
to_storage: '转细胞存储'
|
||
})
|
||
const CELL_PROTOCOL_STATUS_OPTIONS = Object.freeze([
|
||
{ label: '待生效', value: 'dsx' },
|
||
{ label: '生效中', value: 'sxz' },
|
||
{ label: '已过期', value: 'ygq' },
|
||
{ label: '已作废', value: 'yzf' },
|
||
{ label: '属地废弃', value: 'sdfq' },
|
||
{ label: '转科研', value: 'zky' }
|
||
])
|
||
const DONATION_PROTOCOL_STATUS_OPTIONS = Object.freeze([
|
||
{ label: '待生效', value: 'dsx' },
|
||
{ label: '生效中', value: 'sxz' },
|
||
{ label: '已过期', value: 'ygq' },
|
||
{ label: '已作废', value: 'yzf' },
|
||
{ label: '属地废弃', value: 'sdfq' },
|
||
{ label: '转细胞存储', value: 'zckc' }
|
||
])
|
||
const TRANSFER_TYPE_LABELS = Object.freeze({
|
||
wxzz: '微信转账', wx: '微信转账', wechat: '微信转账',
|
||
zzbzz: '支付宝转账', zfb: '支付宝转账', alipay: '支付宝转账',
|
||
yhkzz: '银行卡转账', bank: '银行卡转账',
|
||
xj: '现金', cash: '现金', lszz: '乐刷'
|
||
})
|
||
|
||
function value(source, key) {
|
||
const result = source?.[key]
|
||
return result == null || result === '' ? '—' : String(result)
|
||
}
|
||
|
||
function label(map, source) {
|
||
const key = String(source || '').trim()
|
||
return map[key] || (key ? (/[㐀-鿿]/u.test(key) ? key : '未知') : '—')
|
||
}
|
||
|
||
/** 细胞存储走 protocolStatus;捐赠侧码(如 zckc)走 donationStatus */
|
||
function bizStatusTag(code, source) {
|
||
const key = String(code || '').trim()
|
||
if (!key) return { label: '—', tagType: 'info' }
|
||
if (source === 'research_donation') {
|
||
if (donationStatus[key]) return resolveStatusTag(donationStatus, key)
|
||
if (protocolStatus[key]) return resolveStatusTag(protocolStatus, key)
|
||
return resolveStatusTag(donationStatus, key)
|
||
}
|
||
if (protocolStatus[key]) return resolveStatusTag(protocolStatus, key)
|
||
if (donationStatus[key]) return resolveStatusTag(donationStatus, key)
|
||
return resolveStatusTag(protocolStatus, key)
|
||
}
|
||
|
||
function payModeTag(code) {
|
||
return resolveStatusTag(payMode, code)
|
||
}
|
||
|
||
function transferPayTag(code) {
|
||
return resolveStatusTag(payStatus, code)
|
||
}
|
||
|
||
function scheduleTag(code) {
|
||
return resolveStatusTag(paymentScheduleStatus, code)
|
||
}
|
||
|
||
function auditTag(code) {
|
||
return resolveStatusTag(auditStatus, code)
|
||
}
|
||
|
||
function statusChangeTypeTone(changeType) {
|
||
if (changeType === 'void') return 'danger'
|
||
if (changeType === 'expire') return 'warning'
|
||
if (changeType === 'activate') return 'success'
|
||
return 'primary'
|
||
}
|
||
|
||
function statusChangeFlowText(row, statusRegistry) {
|
||
const fromLabel = row?.fromStatus
|
||
? resolveStatusTag(statusRegistry, row.fromStatus).label
|
||
: ''
|
||
const toLabel = resolveStatusTag(statusRegistry, row?.toStatus).label
|
||
if (fromLabel) return `${fromLabel} → ${toLabel}`
|
||
return toLabel || '—'
|
||
}
|
||
|
||
function statusChangeDescText(row) {
|
||
const changeType = String(row?.changeType || '')
|
||
const desc = String(row?.changeDesc || '').trim()
|
||
if (changeType === 'void') {
|
||
const voidLabel =
|
||
VOID_TYPE_LABELS[String(row?.voidType || '')] ||
|
||
String(row?.voidTypeLabel || row?.voidType || '').trim()
|
||
if (voidLabel && desc) return `作废类型:${voidLabel};${desc}`
|
||
if (voidLabel) return `作废类型:${voidLabel}`
|
||
}
|
||
return desc || '—'
|
||
}
|
||
|
||
function operationActionLabel(action) {
|
||
const map = {
|
||
create: '创建',
|
||
update: '修改',
|
||
delete: '删除',
|
||
void: '作废',
|
||
activate: '生效',
|
||
expire: '过期',
|
||
to_research: '转科研',
|
||
to_storage: '转细胞存储',
|
||
audit: '审核'
|
||
}
|
||
return map[action] || action || '—'
|
||
}
|
||
|
||
function operationChannelLabel(channel) {
|
||
if (channel === 'mini') return '小程序'
|
||
if (channel === 'web') return 'Web'
|
||
return channel || '—'
|
||
}
|
||
|
||
function buildOperationLogCards(rows) {
|
||
return (Array.isArray(rows) ? rows : []).map((row, index) => {
|
||
const action = String(row?.action || '')
|
||
const before = row?.before || {}
|
||
const after = row?.after || {}
|
||
const beforeProto = before.protocol || before.donation || {}
|
||
const afterProto = after.protocol || after.donation || {}
|
||
const beforeParty = before.partyCustomer || before.party || {}
|
||
const afterParty = after.partyCustomer || after.party || {}
|
||
const beforeStatus = String(beforeProto.protocolStatus || beforeProto.donationStatus || '')
|
||
const afterStatus = String(afterProto.protocolStatus || afterProto.donationStatus || '')
|
||
let summary = ''
|
||
if (beforeStatus && afterStatus && beforeStatus !== afterStatus) summary = `状态 ${beforeStatus}→${afterStatus}`
|
||
else if (afterStatus) summary = `状态 ${afterStatus}`
|
||
for (const key of ['motherName', 'customerName', 'donorName', 'name']) {
|
||
const b = String(beforeParty[key] || '').trim()
|
||
const a = String(afterParty[key] || '').trim()
|
||
if (b || a) {
|
||
const namePart = b && a && b !== a ? `姓名 ${b}→${a}` : a ? `姓名 ${a}` : ''
|
||
summary = summary ? `${summary};${namePart}` : namePart
|
||
break
|
||
}
|
||
}
|
||
if (!summary) summary = String(row?.remark || '').trim() || '见前后快照'
|
||
return {
|
||
key: `operation-log-${row?.id || index}`,
|
||
title: `${index + 1}. ${operationActionLabel(action)}`,
|
||
fields: [
|
||
{ label: '来源端', value: operationChannelLabel(String(row?.clientChannel || '')) },
|
||
{ label: '操作时间', value: row?.operateTime || '—' },
|
||
{ label: '操作人', value: row?.operatorUserName || '—' },
|
||
{ label: '变更摘要', value: summary }
|
||
]
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
|
||
function buildStatusChangeCards(rows, statusRegistry) {
|
||
return (rows || []).map((row, index) => {
|
||
const changeType = String(row?.changeType || '')
|
||
return {
|
||
key: String(row?.id || `status-${index}`),
|
||
title: `变更 ${index + 1}`,
|
||
statusText: STATUS_CHANGE_TYPE_LABELS[changeType] || changeType || '变更',
|
||
statusTone: statusChangeTypeTone(changeType),
|
||
fields: [
|
||
{ label: '变更时间', value: value(row, 'changeTime') },
|
||
{ label: '状态流转', value: statusChangeFlowText(row, statusRegistry) },
|
||
{ label: '变更说明', value: statusChangeDescText(row) },
|
||
{ label: '操作人', value: person(row?.operatorUserName, row?.operatorUserId) }
|
||
]
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 无 statusChanges 时,从主表快照字段合成卡片(对齐方案 A 单行展示) */
|
||
function synthesizeStatusChangesFromBasic(basic, source) {
|
||
const rows = []
|
||
if (basic?.effectiveTime) {
|
||
rows.push({
|
||
id: 'snap-activate',
|
||
changeType: 'activate',
|
||
toStatus: 'sxz',
|
||
changeTime: basic.effectiveTime,
|
||
changeDesc: basic.effectiveDesc || ''
|
||
})
|
||
}
|
||
if (basic?.expireTime) {
|
||
rows.push({
|
||
id: 'snap-expire',
|
||
changeType: 'expire',
|
||
toStatus: 'ygq',
|
||
changeTime: basic.expireTime,
|
||
changeDesc: basic.expireDesc || ''
|
||
})
|
||
}
|
||
if (basic?.voidTime) {
|
||
rows.push({
|
||
id: 'snap-void',
|
||
changeType: 'void',
|
||
toStatus: 'yzf',
|
||
changeTime: basic.voidTime,
|
||
changeDesc: basic.voidDesc || '',
|
||
voidType: basic.voidType || '',
|
||
voidTypeLabel: basic.voidTypeLabel || ''
|
||
})
|
||
}
|
||
if (source === 'research_donation') {
|
||
const convertTime = basic?.convertTime || basic?.toStorageTime
|
||
if (convertTime) {
|
||
rows.push({
|
||
id: 'snap-to-storage',
|
||
changeType: 'to_storage',
|
||
toStatus: 'zckc',
|
||
changeTime: convertTime,
|
||
changeDesc: basic?.convertDesc || basic?.toStorageDesc || ''
|
||
})
|
||
}
|
||
} else if (basic?.toResearchTime) {
|
||
rows.push({
|
||
id: 'snap-to-research',
|
||
changeType: 'to_research',
|
||
toStatus: 'zky',
|
||
changeTime: basic.toResearchTime,
|
||
changeDesc: basic.toResearchDesc || ''
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
function person(name, id) {
|
||
const displayName = String(name || '').trim()
|
||
const referenceId = String(id || '').trim()
|
||
if (displayName && displayName !== referenceId) return displayName
|
||
return referenceId ? '未知人员' : '—'
|
||
}
|
||
|
||
function referenceName(name, id) {
|
||
const displayName = String(name || '').trim()
|
||
const referenceId = String(id || '').trim()
|
||
if (displayName && displayName !== referenceId) return displayName
|
||
return referenceId ? '未知' : '—'
|
||
}
|
||
|
||
function fields(source, definitions) {
|
||
return definitions.map(([key, text, options = {}]) => ({
|
||
label: text,
|
||
value: options.map ? options.map(source?.[key]) : value(source, key),
|
||
...options,
|
||
map: undefined
|
||
}))
|
||
}
|
||
|
||
function checkedItems(items) {
|
||
const names = (items || [])
|
||
.filter((item) => item?.itemName)
|
||
.map((item) => `${item.itemName}:${item.checked ? '是' : '否'}`)
|
||
return names.length ? names.join('、') : '—'
|
||
}
|
||
|
||
function transferAttachmentValue(transfer) {
|
||
return transfer?.attachments || transfer?.transferAttachmentFiles || transfer?.transferAttachments || ''
|
||
}
|
||
|
||
function refundRecordAttachmentValue(row) {
|
||
if (Array.isArray(row?.refundAttachmentFiles) && row.refundAttachmentFiles.length) {
|
||
return row.refundAttachmentFiles
|
||
}
|
||
return row?.refundAttachments || ''
|
||
}
|
||
|
||
function refundRecordCards(records) {
|
||
return (records || []).map((row, index) => ({
|
||
key: String(row?.id || `refund-${index}`),
|
||
title: `退费登记 ${index + 1}`,
|
||
fields: [
|
||
{ label: '退费金额', value: value(row, 'refundAmount') },
|
||
{ label: '退费时间', value: value(row, 'refundTime') },
|
||
{ label: '备注', value: value(row, 'refundRemark') },
|
||
{ label: '附件', type: 'attachments', value: refundRecordAttachmentValue(row) }
|
||
]
|
||
}))
|
||
}
|
||
|
||
function transferTypeText(transfer) {
|
||
const backendLabel = String(transfer?.transferTypeLabel || '').trim()
|
||
if (backendLabel) return backendLabel
|
||
const code = String(transfer?.transferType || '').trim()
|
||
if (!code) return '—'
|
||
return TRANSFER_TYPE_LABELS[code] || (/[㐀-鿿]/u.test(code) ? code : code)
|
||
}
|
||
|
||
function sampleLifecycleCards(item, source) {
|
||
const cards = []
|
||
const reception = item?.sampleReception
|
||
if (reception) {
|
||
cards.push({
|
||
key: `${item?.id || item?.sampleCode}-reception`,
|
||
title: '质检接收',
|
||
statusText: value(reception, 'acceptConclusionLabel'),
|
||
fields: [
|
||
{ label: '接收时间', value: value(reception, 'receptionTime') },
|
||
{ label: '经手人', value: value(reception, 'handlerName') },
|
||
{ label: '接收温度(℃)', value: value(reception, 'receiveTemperature') },
|
||
{ label: '结论', value: value(reception, 'acceptConclusionLabel'), tag: true },
|
||
{ label: '实验室入库编码', value: value(reception, 'labStorageCode') },
|
||
{ label: '检查项', value: checkedItems(reception?.checkResults) },
|
||
{ label: '备注', value: value(reception, 'remark') },
|
||
{ label: '接收附件', value: reception?.receiveAttachmentFiles || reception?.receiveAttachments || '', type: 'attachments' }
|
||
]
|
||
})
|
||
}
|
||
const productionReception = item?.productionReception || reception?.productionReception
|
||
if (productionReception) {
|
||
const productionFields = resolveProductionReceptionFields(
|
||
source,
|
||
item?.sampleTypeName || ''
|
||
)
|
||
cards.push({
|
||
key: `${item?.id || item?.sampleCode}-production-reception`,
|
||
title: '生产接收',
|
||
fields: [
|
||
{
|
||
label: '处理时间',
|
||
value: formatProductionReceptionDate(productionReception?.processTime)
|
||
},
|
||
{ label: '处理人', value: value(productionReception, 'handlerName') },
|
||
...productionFields.map((field) => ({
|
||
label: field.label,
|
||
value: productionReceptionFieldValue(field, productionReception)
|
||
})),
|
||
{ label: '备注', value: value(productionReception, 'remark') }
|
||
]
|
||
})
|
||
}
|
||
const release = item?.storageReleaseInfo
|
||
if (release) {
|
||
cards.push({
|
||
key: `${item?.id || item?.sampleCode}-storage-release`,
|
||
title: '入库放行',
|
||
statusText: value(release, 'storageStatusLabel'),
|
||
fields: [
|
||
{ label: '生产结论', value: value(release, 'prodReviewConclusionLabel'), tag: true },
|
||
{ label: '质检结论', value: value(release, 'qcReviewConclusionLabel'), tag: true },
|
||
{ label: '入库状态', value: value(release, 'storageStatusLabel'), tag: true },
|
||
{ label: '生产审核人', value: value(release, 'prodReviewByName') },
|
||
{ label: '生产审核时间', value: value(release, 'prodReviewTime') },
|
||
{ label: '生产审核备注', value: value(release, 'prodReviewRemark') },
|
||
{ label: '质检审核人', value: value(release, 'qcReviewByName') },
|
||
{ label: '质检审核时间', value: value(release, 'qcReviewTime') },
|
||
{ label: '质检审核备注', value: value(release, 'qcReviewRemark') },
|
||
{ label: '质检附件', value: release?.qcReviewAttachments || '', type: 'attachments' },
|
||
{ label: '生产附件', value: release?.prodReviewAttachments || '', type: 'attachments' }
|
||
]
|
||
})
|
||
}
|
||
return cards
|
||
}
|
||
|
||
function paymentTransferCard(transfer, transferIndex) {
|
||
const pay = transferPayTag(transfer?.payStatus)
|
||
const transferType = transferTypeText(transfer)
|
||
const transferTime = value(transfer, 'transferTime')
|
||
return {
|
||
key: String(transfer?.id || `transfer-${transferIndex}`),
|
||
title: `转账 ${transferIndex + 1}`,
|
||
variant: 'transfer',
|
||
transferIndex: transferIndex + 1,
|
||
amount: value(transfer, 'transferAmount'),
|
||
meta: `${transferType} · ${transferTime}`,
|
||
statusText: pay.label,
|
||
statusTone: pay.tagType,
|
||
fields: [
|
||
{ label: '备注', value: value(transfer, 'remark') },
|
||
{ label: '财务备注', value: value(transfer, 'financeRemark') },
|
||
{ label: '转账凭证', value: transferAttachmentValue(transfer), type: 'attachments' }
|
||
]
|
||
}
|
||
}
|
||
|
||
function paymentScheduleCards(pay) {
|
||
return (pay?.schedules || []).map((schedule, scheduleIndex) => {
|
||
const status = scheduleTag(schedule?.scheduleStatus)
|
||
return {
|
||
key: String(schedule?.id || `schedule-${scheduleIndex}`),
|
||
title: `第 ${schedule?.scheduleNo || scheduleIndex + 1} 期`,
|
||
variant: 'schedule',
|
||
cardsLabel: '转账记录',
|
||
statusText: status.label,
|
||
statusTone: status.tagType,
|
||
fields: [
|
||
{ label: '应缴金额', value: value(schedule, 'dueAmount'), money: true },
|
||
{ label: '应缴日期', value: value(schedule, 'dueDate') },
|
||
{ label: '实缴金额', value: value(schedule, 'paidAmount'), money: true },
|
||
{ label: '欠缴金额', value: value(schedule, 'arrearsAmount'), money: true }
|
||
],
|
||
cards: (schedule?.transfers || []).map(paymentTransferCard)
|
||
}
|
||
})
|
||
}
|
||
|
||
function legacyPaymentDetailCards(payment, details) {
|
||
return groupLegacyPaymentPeriods({ payment, details }).map((period, index) => {
|
||
const status = scheduleTag(period.scheduleStatus)
|
||
return {
|
||
key: `legacy-payment-${period.scheduleNo || index + 1}`,
|
||
title: `第 ${period.scheduleNo || index + 1} 期`,
|
||
variant: 'schedule',
|
||
cardsLabel: '转账记录',
|
||
statusText: status.label,
|
||
statusTone: status.tagType,
|
||
fields: [
|
||
{ label: '应缴金额', value: period.dueAmount == null ? '—' : String(period.dueAmount), money: true },
|
||
{ label: '应缴日期', value: period.dueDate || '—' },
|
||
{ label: '确认实缴金额', value: String(period.paidAmount), money: true },
|
||
{ label: '欠缴金额', value: period.arrearsAmount == null ? '—' : String(period.arrearsAmount), money: true }
|
||
],
|
||
cards: period.transfers.map((detail, transferIndex) =>
|
||
paymentTransferCard(
|
||
{ ...detail, id: detail?.id || `legacy-transfer-${period.scheduleNo}-${transferIndex}` },
|
||
transferIndex
|
||
)
|
||
)
|
||
}
|
||
})
|
||
}
|
||
|
||
export default {
|
||
key: 'protocol',
|
||
resource: 'protocols',
|
||
actions: { specialist: ['view', 'create', 'edit'], manager: ['view', 'create', 'edit'] },
|
||
listParams: ({ status } = {}) => ({
|
||
source: ['cell_storage', 'research_donation'].includes(String(status || '').trim())
|
||
? String(status).trim()
|
||
: 'cell_storage'
|
||
}),
|
||
statusOptions: [
|
||
{ text: '细胞存储', value: 'cell_storage' },
|
||
{ text: '科研捐赠', value: 'research_donation' }
|
||
],
|
||
// 协议列表用搜索下方 van-dropdown-menu(对齐预签约),不走筛选弹层
|
||
filterFields: [],
|
||
inlineDropdownFilters: [
|
||
{
|
||
key: 'protocolStatus',
|
||
allText: '全部协议状态',
|
||
source: 'cell_storage',
|
||
options: CELL_PROTOCOL_STATUS_OPTIONS.map((item) => ({ text: item.label, value: item.value }))
|
||
},
|
||
{
|
||
key: 'protocolStatus',
|
||
allText: '全部协议状态',
|
||
source: 'research_donation',
|
||
options: DONATION_PROTOCOL_STATUS_OPTIONS.map((item) => ({ text: item.label, value: item.value }))
|
||
}
|
||
],
|
||
statusLabel(detail) {
|
||
const basic = detail?.protocol || detail?.basic || detail || {}
|
||
const status = detail?.source === 'research_donation'
|
||
? basic?.donationStatus
|
||
: basic?.protocolStatus
|
||
return bizStatusTag(status, detail?.source).label
|
||
},
|
||
statusTone(detail) {
|
||
const basic = detail?.protocol || detail?.basic || detail || {}
|
||
const status = detail?.source === 'research_donation'
|
||
? basic?.donationStatus
|
||
: basic?.protocolStatus
|
||
return bizStatusTag(status, detail?.source).tagType
|
||
},
|
||
detailSections(detail) {
|
||
if (Array.isArray(detail?.sections)) {
|
||
return detail.sections.map((item) => ({ ...item, fields: item?.fields || [], cards: item?.cards || [] }))
|
||
}
|
||
const source = detail?.source
|
||
const basic = detail?.protocol || detail?.basic || detail || {}
|
||
const statusKey = source === 'research_donation' ? 'donationStatus' : 'protocolStatus'
|
||
const dateKey = source === 'research_donation' ? 'donationDate' : 'registerDate'
|
||
const bizStatus = bizStatusTag(basic?.[statusKey], source)
|
||
const isDonation = source === 'research_donation'
|
||
const values = [
|
||
{
|
||
key: 'protocol',
|
||
title: isDonation ? '基本信息' : '协议信息',
|
||
fields: isDonation
|
||
? [
|
||
{ label: '状态', value: bizStatus.label, tag: true, tagType: bizStatus.tagType },
|
||
{ label: '母亲姓名', value: value(detail?.customer, 'motherName') },
|
||
{ label: '所属项目', value: basic.projectName || (basic.projectId ? '历史项目' : '未归属') },
|
||
{ label: '项目组', value: basic.teamName || '—' },
|
||
{ label: '捐赠日期', value: value(basic, dateKey) },
|
||
{ label: '创建时间', value: value(basic, 'createTime') },
|
||
{ label: '备注', value: value(basic, 'remark') }
|
||
]
|
||
: [
|
||
{ label: '协议状态', value: bizStatus.label, tag: true, tagType: bizStatus.tagType },
|
||
{ label: '母亲姓名', value: value(detail?.customer, 'motherName') },
|
||
{ label: '所属项目', value: basic.projectName || (basic.projectId ? '历史项目' : '未归属') },
|
||
{ label: '项目组', value: basic.teamName || '—' },
|
||
{ label: '登记日期', value: value(basic, dateKey) },
|
||
{ label: '协议总金额', value: value(basic, 'protocolTotalAmount') },
|
||
{ label: '备注', value: value(basic, 'remark') },
|
||
{ label: '创建时间', value: value(basic, 'createTime') },
|
||
{ label: '更新时间', value: value(basic, 'updateTime') }
|
||
],
|
||
cards: []
|
||
},
|
||
{
|
||
key: 'collection',
|
||
title: '采集信息',
|
||
fields: fields(detail?.collection, [
|
||
['salesUserName', '市场专员', { map: () => person(detail?.collection?.salesUserName, detail?.collection?.salesUserId) }],
|
||
['regionName', '采集地区', { map: () => referenceName(detail?.collection?.regionName, detail?.collection?.regionId) }],
|
||
['hospitalName', '采集医院', { map: () => referenceName(detail?.collection?.hospitalName, detail?.collection?.hospitalId) }],
|
||
['regionManagerUserName', '地区经理', { map: () => person(detail?.collection?.regionManagerUserName, detail?.collection?.regionManagerUserId) }],
|
||
['medicalStaff', '采集医护'],
|
||
['sampleCollectTime', '样本采集时间'],
|
||
['maternalBloodCollectTime', '母血采集时间']
|
||
]),
|
||
cards: []
|
||
},
|
||
{
|
||
key: 'customer',
|
||
title: '客户信息',
|
||
fields: fields(detail?.customer, [
|
||
['motherName', '母亲姓名'], ['motherPhone', '母亲联系电话'], ['motherIdCard', '母亲身份证号'],
|
||
['motherEmail', '母亲邮箱'], ['motherBirthYm', '母亲出生年月'], ['motherEthnic', '母亲民族'],
|
||
['motherAddress', '母亲家庭住址'], ['fatherName', '父亲姓名'], ['fatherPhone', '父亲电话'],
|
||
['fatherIdCard', '父亲身份证号'], ['newbornName', '新生儿姓名'],
|
||
['newbornSex', '新生儿性别', { map: (item) => label(SEX_LABELS, item) }],
|
||
['expectedDeliveryDate', '预产期'], ['newbornDeliveryTime', '新生儿分娩时间'],
|
||
['newbornIdCard', '新生儿身份证'],
|
||
['deliveryMode', '分娩方式', { map: (item) => label(DELIVERY_LABELS, item) }],
|
||
['birthOrder', '胎次'], ['newbornWeightGram', '新生儿体重(克)'],
|
||
['apgar1min', '新生儿评分(1分钟)']
|
||
].filter(([key]) => !isDonation || !DONATION_HIDDEN_PARENT_KEYS.has(key))),
|
||
cards: []
|
||
},
|
||
{
|
||
key: 'samples',
|
||
title: isDonation ? '样本及运输信息' : '样本信息',
|
||
fields: [],
|
||
cards: (detail?.samples || []).map((item, index) => ({
|
||
key: String(item?.id || `sample-${index}`),
|
||
title: item?.sampleCode || `样本 ${index + 1}`,
|
||
statusText: label(SAMPLE_STATUS_LABELS, item?.sampleStatusLabel || item?.sampleStatus),
|
||
fields: fields(item, [
|
||
['sampleTypeName', '样本类型', { map: () => referenceName(item?.sampleTypeName, item?.sampleTypeId) }],
|
||
['sampleCode', '样本编码'],
|
||
['sampleStatus', '样本状态', { map: () => label(SAMPLE_STATUS_LABELS, item?.sampleStatusLabel || item?.sampleStatus) }],
|
||
...(source === 'cell_storage'
|
||
? [['price', '优惠价'], ['originalPrice', '原价'], ['storageYears', '存储年限']]
|
||
: []),
|
||
[
|
||
'transportMode',
|
||
'运输方式',
|
||
{
|
||
map: () =>
|
||
item?.freight?.transportModeLabel ||
|
||
item?.freight?.transportMode ||
|
||
'—'
|
||
}
|
||
],
|
||
[
|
||
'freightAmount',
|
||
'运费金额',
|
||
{
|
||
map: () => {
|
||
const freight = item?.freight || {}
|
||
const shared = Number(freight.linkedSampleCount || 0) > 1
|
||
const amount = freight.freightAmount
|
||
if (String(freight.transportMode || '') === 'sfkd' && (amount == null || amount === '')) {
|
||
return `月结${shared ? '(共用)' : ''}`
|
||
}
|
||
if (amount == null) return '—'
|
||
return `${amount}${shared ? '(共用)' : ''}`
|
||
}
|
||
}
|
||
],
|
||
['etaTime', '预计到达时间', { map: () => item?.etaTime || item?.transport?.etaTime || '' }],
|
||
['isSpecial', '是否特殊样本', { map: (itemValue) => Number(itemValue) === 1 ? '是' : '否' }],
|
||
['specialRemark', '特殊说明']
|
||
]),
|
||
cards: sampleLifecycleCards(item, source)
|
||
}))
|
||
}
|
||
]
|
||
const depositTransfers = Array.isArray(detail?.depositTransfers) ? detail.depositTransfers : []
|
||
const depositHint = detail?.presignDepositHint
|
||
if (detail?.source === 'cell_storage' && (depositHint || depositTransfers.length)) {
|
||
const amt = Number(depositHint?.confirmedDepositAmount || 0)
|
||
const cnt = Number(depositHint?.confirmedTransferCount || 0)
|
||
values.push({
|
||
key: 'deposit',
|
||
title: '意向金(关联预签约)',
|
||
fields: depositHint
|
||
? [{
|
||
label: '已确认意向金',
|
||
value: `¥${amt.toFixed(2)}(${cnt} 笔)`
|
||
}]
|
||
: [],
|
||
cards: depositTransfers.map((tr, index) => {
|
||
const pay = transferPayTag(tr?.payStatus)
|
||
return {
|
||
key: String(tr?.id || `deposit-${index}`),
|
||
title: `意向金转账 ${index + 1}`,
|
||
statusText: pay.label,
|
||
statusTone: pay.tagType,
|
||
fields: [
|
||
{ label: '转账方式', value: transferTypeText(tr) },
|
||
{ label: '转账金额', value: value(tr, 'transferAmount') },
|
||
{ label: '转账时间', value: value(tr, 'transferTime') },
|
||
{ label: '转账凭证', value: transferAttachmentValue(tr), type: 'attachments' },
|
||
{ label: '备注', value: value(tr, 'remark') },
|
||
{ label: '财务备注', value: value(tr, 'financeRemark') }
|
||
]
|
||
}
|
||
})
|
||
})
|
||
}
|
||
if (detail?.source === 'cell_storage') {
|
||
const samplePays = Array.isArray(detail?.samplePayments) ? detail.samplePayments : []
|
||
if (samplePays.length) {
|
||
values.push({
|
||
key: 'payment',
|
||
title: '缴费信息',
|
||
fields: [],
|
||
cards: samplePays.map((pay, index) => {
|
||
const mode = payModeTag(pay?.payMode)
|
||
return {
|
||
key: String(pay?.id || pay?.sampleCode || `pay-${index}`),
|
||
title: pay?.sampleCode || `样本缴费 ${index + 1}`,
|
||
fields: [
|
||
{ label: '缴费模式', value: mode.label, tag: true, tagType: mode.tagType },
|
||
{ label: '合同金额', value: value(pay, 'contractAmount') },
|
||
...(String(pay?.payMode || '') === 'dj'
|
||
? [{ label: '趸交截止时间', value: value(pay, 'lumpSumDeadline') }]
|
||
: [
|
||
{ label: '分期期数', value: value(pay, 'installmentCount') },
|
||
{ label: '每期间隔(月)', value: value(pay, 'installmentIntervalMonths') },
|
||
{ label: '分期开始时间', value: value(pay, 'installmentStartDate') },
|
||
{ label: '提前催缴预警(天)', value: value(pay, 'installmentAdvanceDays') }
|
||
]),
|
||
...((pay?.schedules || []).map((sch, sIdx) => ({
|
||
label: `第 ${sch?.scheduleNo || sIdx + 1} 期`,
|
||
value: `应缴 ${sch?.dueAmount ?? '—'} / 实缴 ${sch?.paidAmount ?? '—'} / 欠缴 ${sch?.arrearsAmount ?? '—'} / ${sch?.dueDate || '—'}`
|
||
})))
|
||
],
|
||
cards: paymentScheduleCards(pay)
|
||
}
|
||
})
|
||
})
|
||
} else {
|
||
const legacyDetails = Array.isArray(detail?.paymentDetails) ? detail.paymentDetails : []
|
||
const legacyMode = payModeTag(detail?.payment?.payMode)
|
||
values.push({
|
||
key: 'payment',
|
||
title: '缴费信息',
|
||
fields: [
|
||
{ label: '缴费模式', value: legacyMode.label, tag: true, tagType: legacyMode.tagType },
|
||
...fields(detail?.payment, [
|
||
['contractAmount', '合同金额'], ['installmentCount', '分期期数'],
|
||
['installmentIntervalMonths', '每期间隔(月)'], ['lumpSumDeadline', '趸交截止时间'],
|
||
['installmentStartDate', '分期开始时间'], ['installmentAdvanceDays', '提前催缴预警(天)'],
|
||
['remark', '缴费备注']
|
||
])
|
||
],
|
||
cards: legacyPaymentDetailCards(detail?.payment || {}, legacyDetails)
|
||
})
|
||
}
|
||
const refundRecords = Array.isArray(detail?.refundRecords) ? detail.refundRecords : []
|
||
values.push({
|
||
key: 'refundRecords',
|
||
title: '退费登记',
|
||
fields: refundRecords.length ? [] : [{ label: '记录', value: '暂无退费登记' }],
|
||
cards: refundRecordCards(refundRecords)
|
||
})
|
||
}
|
||
const audits = Array.isArray(detail?.audits) ? detail.audits : []
|
||
values.push({
|
||
key: 'audits',
|
||
title: '审核记录',
|
||
fields: audits.length ? [] : [{ label: '记录', value: '暂无审核记录' }],
|
||
cards: audits.map((row, index) => {
|
||
const audit = auditTag(row?.auditStatus)
|
||
return {
|
||
key: String(row?.id || `audit-${index}`),
|
||
title: `审核 ${index + 1}`,
|
||
statusText: audit.label,
|
||
statusTone: audit.tagType,
|
||
fields: [
|
||
{ label: '审核人', value: person(row?.auditorUserName, row?.auditorUserId) },
|
||
{ label: '审核意见', value: value(row, 'auditRemark') },
|
||
{ label: '时间', value: value(row, 'createTime') }
|
||
]
|
||
}
|
||
})
|
||
})
|
||
values.push({
|
||
key: 'attachments',
|
||
title: '附件信息',
|
||
fields: isDonation
|
||
? [
|
||
{ label: '知情同意书', type: 'attachments', value: detail?.attachments?.consentFormUrls || '' },
|
||
{ label: '传染病检测报告', type: 'attachments', value: detail?.attachments?.infectiousDiseaseReportUrls || '' },
|
||
{ label: '其他附件', type: 'attachments', value: detail?.attachments?.attachmentUrls || '' }
|
||
]
|
||
: [
|
||
{ label: '协议附件', type: 'attachments', value: detail?.attachments?.protocolAttachments || '' },
|
||
{ label: '知情同意书', type: 'attachments', value: detail?.attachments?.consentFormUrls || '' },
|
||
{ label: '孕妇健康调查表', type: 'attachments', value: detail?.attachments?.healthSurveyFormUrls || '' }
|
||
],
|
||
cards: []
|
||
})
|
||
const statusRegistry = source === 'research_donation' ? donationStatus : protocolStatus
|
||
const statusChangeRows = Array.isArray(detail?.statusChanges) && detail.statusChanges.length
|
||
? detail.statusChanges
|
||
: synthesizeStatusChangesFromBasic(basic, source)
|
||
const statusChangeCards = buildStatusChangeCards(statusChangeRows, statusRegistry)
|
||
const operationLogRows = Array.isArray(detail?.operationLogs) ? detail.operationLogs : []
|
||
const operationLogCards = buildOperationLogCards(operationLogRows)
|
||
values.push({
|
||
key: 'statusChange',
|
||
title: '状态变更记录',
|
||
fields: statusChangeCards.length
|
||
? []
|
||
: [{ label: '记录', value: '暂无状态变更记录' }],
|
||
cards: statusChangeCards
|
||
},
|
||
{
|
||
key: 'operationLog',
|
||
title: '操作日志',
|
||
fields: operationLogCards.length
|
||
? []
|
||
: [{ label: '记录', value: '暂无操作日志' }],
|
||
cards: operationLogCards
|
||
})
|
||
if (isDonation) {
|
||
const order = ['protocol', 'collection', 'samples', 'customer', 'audits', 'attachments', 'statusChange', 'operationLog']
|
||
const byKey = new Map(values.map((section) => [section.key, section]))
|
||
return order.map((key) => byKey.get(key)).filter(Boolean)
|
||
}
|
||
return values
|
||
},
|
||
formSections: [
|
||
{ key: 'protocol', title: '协议信息', fields: [
|
||
{ key: 'source', label: '业务来源', type: 'tabs', required: true, createOnly: true, options: [
|
||
{ text: '细胞存储', value: 'cell_storage' }, { text: '科研捐赠', value: 'research_donation' }
|
||
] },
|
||
{ key: 'protocolStatus', label: '协议状态', type: 'select', required: true, source: 'cell_storage', options: CELL_PROTOCOL_STATUS_OPTIONS },
|
||
{ key: 'donationStatus', label: '协议状态', type: 'select', required: true, source: 'research_donation', options: DONATION_PROTOCOL_STATUS_OPTIONS },
|
||
// 服务端发号:表单不展示,仍参与 payload
|
||
{ key: 'protocolNo', label: '协议编号', source: 'cell_storage', hidden: true },
|
||
{ key: 'donationNo', label: '捐赠编号', source: 'research_donation', hidden: true },
|
||
{ key: 'registerDate', label: '登记日期', type: 'date', valueFormat: 'YYYYMMDD', source: 'cell_storage' },
|
||
{ key: 'donationDate', label: '捐赠日期', type: 'date', valueFormat: 'YYYYMMDD', source: 'research_donation' },
|
||
{ key: 'collectorUserId', label: '采集专员', type: 'select', optionKey: 'collectorStaff', hidden: true },
|
||
// 展示在缴费信息面板(ProtocolSamplePaymentPanel / ProtocolV1PaymentPanel)
|
||
{ key: 'protocolTotalAmount', label: '协议总金额', type: 'number', source: 'cell_storage', hidden: true },
|
||
{ key: 'remark', label: '备注', type: 'textarea' }
|
||
] },
|
||
{ key: 'collection', title: '采集信息', nested: true, fields: [
|
||
{ key: 'salesUserId', label: '市场专员', type: 'select', optionKey: 'salesStaff' },
|
||
{ key: 'regionId', label: '采集地区', type: 'select', optionKey: 'regions' },
|
||
{ key: 'hospitalId', label: '采集医院', type: 'select', optionKey: 'hospitals' },
|
||
{ key: 'regionManagerUserId', label: '地区经理', type: 'select', optionKey: 'managerStaff' },
|
||
{ key: 'medicalStaff', label: '采集医护' },
|
||
{ key: 'sampleCollectTime', label: '样本采集时间', type: 'datetime' },
|
||
{ key: 'maternalBloodCollectTime', label: '母血采集时间', type: 'datetime' }
|
||
] },
|
||
{ key: 'customer', title: '客户信息', nested: true, fields: [
|
||
{ key: 'motherName', label: '母亲姓名', type: 'customerSearch', role: 'mother', required: true },
|
||
{ key: 'motherPhone', label: '联系电话', type: 'tel', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'motherIdCard', label: '母亲身份证号', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'motherEmail', label: '母亲邮箱', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'motherBirthYm', label: '母亲出生年月', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'motherEthnic', label: '母亲民族', type: 'select', optionKey: 'ethnicities', searchable: true, ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'motherAddress', label: '母亲家庭住址', type: 'textarea', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'fatherName', label: '父亲姓名', type: 'customerSearch', role: 'father', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'fatherPhone', label: '父亲电话', type: 'tel', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'fatherIdCard', label: '父亲身份证号', ...DONATION_PARENT_FIELD_HIDE },
|
||
{ key: 'newbornName', label: '新生儿姓名' },
|
||
{ key: 'newbornSex', label: '新生儿性别', type: 'tabs', options: [
|
||
{ label: '男', value: '1' }, { label: '女', value: '0' }
|
||
] },
|
||
{ key: 'expectedDeliveryDate', label: '预产期', type: 'date', valueFormat: 'YYYYMMDD' },
|
||
{ key: 'newbornDeliveryTime', label: '新生儿分娩时间', type: 'datetime' },
|
||
{ key: 'newbornIdCard', label: '新生儿身份证' },
|
||
{ key: 'deliveryMode', label: '分娩方式', type: 'select', options: [
|
||
{ label: '顺产', value: 'sc' }, { label: '剖宫产', value: 'pgc' }
|
||
] },
|
||
{ key: 'birthOrder', label: '胎次', type: 'number', integer: true },
|
||
{ key: 'newbornWeightGram', label: '新生儿体重(克)', type: 'number', integer: true },
|
||
{ key: 'apgar1min', label: '新生儿评分(1分钟)', type: 'number' }
|
||
] },
|
||
// 样本及运输改由 ProtocolSampleTransportPanel 托管(对齐 Web)
|
||
{ key: 'attachments', title: '附件', nested: true, fields: [
|
||
{ key: 'protocolAttachments', label: '协议附件', type: 'upload', attachmentPolicy: 'protocol', source: 'cell_storage' },
|
||
{ key: 'consentFormUrls', label: '知情同意书', type: 'upload', attachmentPolicy: 'protocol' },
|
||
{ key: 'healthSurveyFormUrls', label: '健康调查表', type: 'upload', attachmentPolicy: 'protocol', source: 'cell_storage' },
|
||
{ key: 'infectiousDiseaseReportUrls', label: '传染病报告', type: 'upload', attachmentPolicy: 'protocol', source: 'research_donation' },
|
||
{ key: 'attachmentUrls', label: '其他附件', type: 'upload', attachmentPolicy: 'protocol', source: 'research_donation' }
|
||
] },
|
||
// 渲染时须置于样本运输 / 缴费面板之后(见 MarketModuleForm)
|
||
{ key: 'confirm', title: '提交确认', nested: true, uiOnly: true, renderLast: true, fields: [
|
||
{ key: 'confirmed', label: '我已核对以上信息', type: 'checkbox', required: true }
|
||
] }
|
||
]
|
||
}
|