Просмотр исходного кода

fix(mobile-oa-h5): finish storage-service tag tone parity for market details

Wire detail header statusTone for protocol/payment/freight/chase from the
registry mirror, stop hardcoding primary on nested tags, and map 已过期 to
warning so title and field colors stay consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 недель назад
Родитель
Сommit
48dcf991dc

+ 2 - 1
mobile-oa-h5/src/market/components/MarketDetailNestedCard.vue

@@ -124,7 +124,7 @@
         <van-tag
           v-else-if="field.tag"
           class="market-tag"
-          :type="field.tagType || 'primary'"
+          :type="field.tagType || resolveStatusTone(field.value)"
           round
           :title="field.value"
         >
@@ -145,6 +145,7 @@
 
 <script setup>
 import { computed } from 'vue'
+import { resolveStatusTone } from '../status-tone.js'
 import MarketAttachmentCards from './MarketAttachmentCards.vue'
 
 defineOptions({ name: 'MarketDetailNestedCard' })

+ 1 - 1
mobile-oa-h5/src/market/components/MarketDetailSection.vue

@@ -10,7 +10,7 @@
             :model-value="field.value"
             :editable="false"
           />
-          <van-tag v-else-if="field.tag" class="market-tag" :type="field.tagType || 'primary'" round :title="field.value">
+          <van-tag v-else-if="field.tag" class="market-tag" :type="field.tagType || resolveStatusTone(field.value)" round :title="field.value">
             {{ field.value }}
           </van-tag>
           <span v-else>{{ field.value }}</span>

+ 5 - 12
mobile-oa-h5/src/market/components/ProtocolV1PaymentPanel.vue

@@ -287,6 +287,7 @@ import {
   PROTOCOL_ATTACHMENT_MAX_BYTES
 } from '../attachment-upload-policy.js'
 import MarketAttachmentCards from './MarketAttachmentCards.vue'
+import { resolveStatusTag, payStatus } from '../status-tag-registry.js'
 
 const props = defineProps({
   payment: { type: Object, default: () => ({}) },
@@ -360,21 +361,11 @@ function payModeLabel(value) {
 }
 
 function statusLabel(value) {
-  return {
-    dfj: '已付款',
-    jfqrz: '付款确认中',
-    yjf: '付款完成',
-    tfqrz: '退费确认中',
-    ytf: '已退费'
-  }[String(value ?? '')] || '状态异常'
+  return resolveStatusTag(payStatus, value).label
 }
 
 function statusTone(value) {
-  const status = String(value ?? '')
-  if (status === 'yjf') return 'success'
-  if (status === 'jfqrz') return 'warning'
-  if (status === 'dfj') return 'primary'
-  return 'danger'
+  return resolveStatusTag(payStatus, value).tagType
 }
 
 function transferTypeLabel(value) {
@@ -576,6 +567,8 @@ function removeAttachment(detailIndex, attachmentIndex) {
 .protocol-v1-pay__details-head .van-button,
 .protocol-v1-pay__delete {
   min-height: 44px;
+  border-radius: 12px !important;
+  font-weight: 600 !important;
 }
 
 .protocol-v1-pay__detail.is-locked {

+ 1 - 1
mobile-oa-h5/src/market/mobile-parity-ui.test.js

@@ -539,7 +539,7 @@ test('nested market detail cards use explicit labels instead of API property nam
   const detailSection = source('./components/MarketDetailSection.vue')
 
   assert.match(detailSection, /cards:/)
-  assert.match(detailSection, /field\.tagType \|\| 'primary'/)
+  assert.match(detailSection, /field\.tagType \|\| resolveStatusTone\(field\.value\)/)
   assert.doesNotMatch(detailSection, /function flattenFields/)
 })
 

+ 31 - 0
mobile-oa-h5/src/market/module-contracts.test.js

@@ -93,6 +93,9 @@ test('protocol summary status resolves real codes without exposing unknown codes
   assert.equal(protocol.statusLabel({ source: 'research_donation', basic: { donationStatus: 'yzf' } }), '已作废')
   assert.equal(protocol.statusLabel({ source: 'cell_storage', basic: { protocolStatus: 'raw-status' } }), '未知')
   assert.equal(protocol.statusLabel({ source: 'cell_storage', basic: { protocolStatus: '' } }), '—')
+  assert.equal(protocol.statusTone({ source: 'cell_storage', basic: { protocolStatus: 'ygq' } }), 'warning')
+  assert.equal(protocol.statusTone({ source: 'research_donation', basic: { donationStatus: 'ygq' } }), 'warning')
+  assert.equal(protocol.statusTone({ source: 'cell_storage', protocol: { protocolStatus: 'sxz' } }), 'primary')
 })
 
 test('protocol adapters preserve nested detail sections', () => {
@@ -233,6 +236,34 @@ test('payment detail presents real backend progress and combined pay mode in sum
   assert.equal(summary.fields.find((field) => field.label === '缴费进度').value, '部分已缴')
 })
 
+test('payment and freight detail headers use registry tagType instead of Chinese heuristics', () => {
+  const payment = getMarketModule('payment')
+  assert.equal(payment.statusLabel({ progressStatus: 'dfj' }), '待缴费')
+  assert.equal(payment.statusTone({ progressStatus: 'dfj' }), 'info')
+  assert.equal(payment.statusTone({ progressStatus: 'yjq' }), 'success')
+
+  const freight = getMarketModule('freight')
+  assert.equal(
+    freight.statusTone({ items: [{ sampleSource: 'research_donation' }] }),
+    'warning'
+  )
+  assert.equal(
+    freight.statusTone({ items: [{ sampleSource: 'cell_storage' }] }),
+    'primary'
+  )
+
+  const chase = getMarketModule('payment-chase')
+  assert.equal(chase.statusTone({ progressStatus: 'dfj' }), 'info')
+  assert.equal(
+    chase.statusTone({
+      chaseView: 'protocol',
+      source: 'cell_storage',
+      basic: { protocolStatus: 'ygq' }
+    }),
+    'warning'
+  )
+})
+
 test('payment progress dfj aligns with web-admin samplePaymentProgress 待缴费', () => {
   const payment = getMarketModule('payment')
   const byCode = payment.detailSections({ progressStatus: 'dfj' })

+ 11 - 1
mobile-oa-h5/src/market/modules/freight.js

@@ -15,6 +15,11 @@ function person(name, id) {
   return referenceId ? '未知人员' : '—'
 }
 
+function freightHeaderTag(detail) {
+  const first = (detail?.items || [])[0] || {}
+  return resolveStatusTag(receptionSource, first.sampleSource || detail?.sampleSource || detail?.source)
+}
+
 function money(value) {
   const amount = Number(value)
   if (!Number.isFinite(amount)) return '—'
@@ -80,7 +85,6 @@ export default {
             statusTone: source.tagType,
             fields: [
               { label: '来源', value: source.label, tag: true, tagType: source.tagType },
-              { label: '协议单号', value: item?.protocolNo || '—' },
               { label: '样本编码', value: item?.sampleCode || '—' },
               { label: '样本类型', value: item?.sampleTypeName || '—' },
               { label: '母亲姓名', value: item?.motherName || '—' },
@@ -91,6 +95,12 @@ export default {
       }
     ]
   },
+  statusLabel(detail) {
+    return freightHeaderTag(detail).label
+  },
+  statusTone(detail) {
+    return freightHeaderTag(detail).tagType
+  },
   usesPagedSampleOptions(action) {
     return ['create', 'link'].includes(action)
   },

+ 12 - 1
mobile-oa-h5/src/market/modules/payment-chase.js

@@ -32,7 +32,8 @@ export default {
       options: [
         { text: '趸交', value: 'dj' },
         { text: '分期', value: 'fq' },
-        { text: '分改趸', value: 'fgd' }
+        { text: '分改趸', value: 'fgd' },
+        { text: '趸交+分期', value: 'djfq' }
       ]
     }
   ],
@@ -46,5 +47,15 @@ export default {
     return detail?.chaseView === 'protocol'
       ? protocolModule.detailSections(detail)
       : paymentModule.detailSections(detail)
+  },
+  statusLabel(detail) {
+    return detail?.chaseView === 'protocol'
+      ? protocolModule.statusLabel(detail)
+      : paymentModule.statusLabel(detail)
+  },
+  statusTone(detail) {
+    return detail?.chaseView === 'protocol'
+      ? protocolModule.statusTone(detail)
+      : paymentModule.statusTone(detail)
   }
 }

+ 6 - 0
mobile-oa-h5/src/market/modules/payment.js

@@ -82,6 +82,12 @@ export default {
       }
     ]
   },
+  statusLabel(detail) {
+    return progressTag(detail?.progressStatus || detail?.paymentProgress).label
+  },
+  statusTone(detail) {
+    return progressTag(detail?.progressStatus || detail?.paymentProgress).tagType
+  },
   initialForm(detail, target = {}) {
     const transfer = (detail?.schedules || [])
       .flatMap((row) => row.transfers || [])

+ 194 - 241
mobile-oa-h5/src/market/modules/protocol.js

@@ -22,6 +22,13 @@ const SAMPLE_STATUS_LABELS = Object.freeze({
 const VOID_TYPE_LABELS = Object.freeze({
   khhc: '客户毁存', jcbhg: '检测不合格'
 })
+const STATUS_CHANGE_TYPE_LABELS = Object.freeze({
+  activate: '生效',
+  expire: '过期',
+  void: '作废',
+  to_research: '转科研',
+  to_storage: '转细胞存储'
+})
 const TRANSFER_TYPE_LABELS = Object.freeze({
   wxzz: '微信转账', wx: '微信转账', wechat: '微信转账',
   zzbzz: '支付宝转账', zfb: '支付宝转账', alipay: '支付宝转账',
@@ -69,11 +76,106 @@ function auditTag(code) {
   return resolveStatusTag(auditStatus, code)
 }
 
-function voidTypeText(source) {
-  const backendLabel = String(source?.voidTypeLabel || '').trim()
-  if (backendLabel) return backendLabel
-  const code = String(source?.voidType || '').trim()
-  return VOID_TYPE_LABELS[code] || 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 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) {
@@ -110,121 +212,32 @@ function transferAttachmentValue(transfer) {
   return transfer?.attachments || transfer?.transferAttachmentFiles || transfer?.transferAttachments || ''
 }
 
-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 namedItems(items) {
-  return (items || [])
-    .map((item) => typeof item === 'string'
-      ? item
-      : item?.itemName || item?.name || item?.itemCode || item?.id)
-    .filter(Boolean)
-    .join('、') || '—'
-}
-
-function qcResultCard(result, index, prefix) {
-  const unit = String(result?.paramUnit || '').trim()
-  return {
-    key: String(result?.id || `${prefix}-result-${index}`),
-    title: `检测结果 ${index + 1}`,
-    fields: [
-      { label: '参数名称', value: value(result, 'paramName') },
-      { label: '参数单位', value: unit || '—' },
-      { label: '参数值', value: value(result, 'paramValue') }
-    ]
+function refundRecordAttachmentValue(row) {
+  if (Array.isArray(row?.refundAttachmentFiles) && row.refundAttachmentFiles.length) {
+    return row.refundAttachmentFiles
   }
+  return row?.refundAttachments || ''
 }
 
-function qcTaskCard(task, index, prefix) {
-  return {
-    key: String(task?.id || `${prefix}-task-${index}`),
-    title: task?.itemName || task?.taskName || task?.taskNo || `任务 ${index + 1}`,
-    statusText: value(task, 'taskStatusLabel'),
-    fields: [
-      { label: '任务号', value: value(task, 'taskNo') },
-      { label: '任务类型', value: value(task, 'taskTypeLabel') },
-      { label: '检测项目', value: value(task, 'itemName') },
-      { label: '检测机构', value: value(task, 'companyName') },
-      { label: '任务状态', value: value(task, 'taskStatusLabel'), tag: true },
-      { label: '结论', value: value(task, 'conclusionLabel'), tag: true },
-      { label: '完成时间', value: value(task, 'completedTime') },
-      { label: '备注', value: value(task, 'remark') },
-      { label: '任务附件', value: task?.attachmentFiles || task?.thirdPartyAttachments || '', type: 'attachments' }
-    ],
-    cards: (task?.results || []).map((result, resultIndex) =>
-      qcResultCard(result, resultIndex, `${prefix}-task-${index}`)
-    )
-  }
-}
-
-function qcBatchCard(batch, index, prefix) {
-  return {
-    key: String(batch?.id || `${prefix}-batch-${index}`),
-    title: `检测批次 ${index + 1}`,
-    fields: [
-      { label: '批次编号', value: value(batch, 'batchNo') },
-      { label: '批次名称', value: value(batch, 'batchName') },
-      { label: '负责人', value: value(batch, 'assigneeUserNames') },
-      { label: '实验室检测', value: batch?.hasLab ? '是' : '否' },
-      { label: '第三方检测', value: batch?.hasThirdParty ? '是' : '否' },
-      { label: '实验室项目', value: namedItems(batch?.labItems || batch?.labItemIds) },
-      { label: '第三方项目', value: namedItems(batch?.thirdPartyItems || batch?.thirdPartyItemIds) },
-      { label: '任务总数', value: value(batch, 'taskCount') },
-      { label: '已完成任务数', value: value(batch, 'completedTaskCount') }
-    ],
-    cards: (batch?.tasks || []).map((task, taskIndex) =>
-      qcTaskCard(task, taskIndex, `${prefix}-batch-${index}`)
-    )
-  }
-}
-
-function qcCard(info, title, index = 0) {
-  return {
-    key: String(info?.id || `${title}-${index}`),
-    title,
-    statusText: value(info, 'workOrderStatusLabel'),
+function refundRecordCards(records) {
+  return (records || []).map((row, index) => ({
+    key: String(row?.id || `refund-${index}`),
+    title: `退费登记 ${index + 1}`,
     fields: [
-      { label: '工单号', value: value(info, 'workOrderNo') },
-      { label: '业务类型', value: value(info, 'qcBizTypeLabel') },
-      { label: '工单状态', value: value(info, 'workOrderStatusLabel'), tag: true },
-      { label: '触发来源', value: value(info, 'triggerSourceLabel') },
-      { label: '质检对象编码', value: value(info, 'cellProductCode') },
-      { label: '质检对象名称', value: value(info, 'cellProductName') },
-      { label: '生产工单号', value: value(info, 'productionWorkOrderNo') },
-      { label: '包含实验室检测', value: info?.hasLab ? '是' : '否' },
-      { label: '包含第三方检测', value: info?.hasThirdParty ? '是' : '否' },
-      { label: '实验室检测项目', value: namedItems(info?.labItems) },
-      { label: '第三方检测项目', value: namedItems(info?.thirdPartyItems) },
-      { label: '配置时间', value: value(info, 'configuredTime') },
-      { label: '检测批次', value: (info?.inspectionBatches || []).map((row) => row?.batchNo || row?.batchName).filter(Boolean).join('、') || '—' }
-    ],
-    cards: [
-      ...(info?.inspectionBatches || []).map((batch, batchIndex) =>
-        qcBatchCard(batch, batchIndex, `${title}-${index}`)
-      ),
-      ...(info?.tasks || []).map((task, taskIndex) =>
-        qcTaskCard(task, taskIndex, `${title}-${index}`)
-      )
+      { label: '退费金额', value: value(row, 'refundAmount') },
+      { label: '退费时间', value: value(row, 'refundTime') },
+      { label: '备注', value: value(row, 'refundRemark') },
+      { label: '附件', type: 'attachments', value: refundRecordAttachmentValue(row) }
     ]
-  }
+  }))
 }
 
-function productionMaterialCard(item, index, kind, prefix) {
-  const input = kind === 'input'
-  return {
-    key: String(item?.id || `${prefix}-${kind}-${index}`),
-    title: `${input ? '投入物' : '产出物'} ${index + 1}`,
-    fields: [
-      { label: input ? '投入物类型' : '产出物类型', value: value(item, input ? 'inputType' : 'outputType') },
-      { label: input ? '投入物编码' : '产出物编码', value: value(item, input ? 'inputCode' : 'outputCode') },
-      { label: input ? '投入物名称' : '产出物名称', value: value(item, input ? 'inputName' : 'outputName') }
-    ]
-  }
+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) {
@@ -270,41 +283,6 @@ function sampleLifecycleCards(item, source) {
       ]
     })
   }
-  if (item?.qcInfo) cards.push(qcCard(item.qcInfo, '门控质检'))
-  ;(item?.processQcInfoList || []).forEach((info, index) => cards.push(qcCard(info, `过程质检 ${index + 1}`, index)))
-  const production = item?.productionInfo
-  if (production) {
-    cards.push({
-      key: `${item?.id || item?.sampleCode}-production`,
-      title: '生产信息',
-      statusText: value(production, 'workOrderStatusLabel'),
-      fields: [
-        { label: '工单号', value: value(production, 'workOrderNo') },
-        { label: '工单状态', value: value(production, 'workOrderStatusLabel'), tag: true }
-      ],
-      cards: (production?.tasks || []).map((task, index) => ({
-        key: String(task?.id || `production-task-${index}`),
-        title: `生产任务 ${index + 1}`,
-        statusText: value(task, 'taskStatusLabel'),
-        fields: [
-          { label: '任务名称', value: value(task, 'taskName') },
-          { label: '任务号', value: value(task, 'taskNo') },
-          { label: '任务状态', value: value(task, 'taskStatusLabel'), tag: true },
-          { label: '开始时间', value: value(task, 'startedTime') },
-          { label: '完成时间', value: value(task, 'completedTime') },
-          { label: '生产结论', value: value(task, 'conclusionLabel'), tag: true }
-        ],
-        cards: [
-          ...(task?.inputs || []).map((input, inputIndex) =>
-            productionMaterialCard(input, inputIndex, 'input', `production-task-${index}`)
-          ),
-          ...(task?.outputs || []).map((output, outputIndex) =>
-            productionMaterialCard(output, outputIndex, 'output', `production-task-${index}`)
-          )
-        ]
-      }))
-    })
-  }
   const release = item?.storageReleaseInfo
   if (release) {
     cards.push({
@@ -434,6 +412,13 @@ export default {
       : 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 || [] }))
@@ -442,26 +427,29 @@ export default {
     const basic = detail?.protocol || detail?.basic || detail || {}
     const statusKey = source === 'research_donation' ? 'donationStatus' : 'protocolStatus'
     const dateKey = source === 'research_donation' ? 'donationDate' : 'registerDate'
-    const numberKey = source === 'research_donation' ? 'donationNo' : 'protocolNo'
     const bizStatus = bizStatusTag(basic?.[statusKey], source)
+    const isDonation = source === 'research_donation'
     const values = [
       {
         key: 'protocol',
-        title: source === 'research_donation' ? '捐赠信息' : '协议信息',
-        fields: [
-          { label: source === 'research_donation' ? '捐赠编号' : '协议编号', value: value(basic, numberKey) },
-          {
-            label: source === 'research_donation' ? '捐赠状态' : '协议状态',
-            value: bizStatus.label,
-            tag: true,
-            tagType: bizStatus.tagType
-          },
-          { label: source === 'research_donation' ? '捐赠日期' : '登记日期', value: value(basic, dateKey) },
-          ...(source === 'cell_storage' ? [{ label: '协议总金额', value: value(basic, 'protocolTotalAmount') }] : []),
-          { label: '备注', value: value(basic, 'remark') },
-          { label: '创建时间', value: value(basic, 'createTime') },
-          { label: '更新时间', value: value(basic, 'updateTime') }
-        ],
+        title: isDonation ? '基本信息' : '协议信息',
+        fields: isDonation
+          ? [
+              { label: '状态', value: bizStatus.label, tag: true, tagType: bizStatus.tagType },
+              { label: '母亲姓名', value: value(detail?.customer, 'motherName') },
+              { 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: value(basic, dateKey) },
+              { label: '协议总金额', value: value(basic, 'protocolTotalAmount') },
+              { label: '备注', value: value(basic, 'remark') },
+              { label: '创建时间', value: value(basic, 'createTime') },
+              { label: '更新时间', value: value(basic, 'updateTime') }
+            ],
         cards: []
       },
       {
@@ -492,13 +480,13 @@ export default {
           ['newbornIdCard', '新生儿身份证'],
           ['deliveryMode', '分娩方式', { map: (item) => label(DELIVERY_LABELS, item) }],
           ['birthOrder', '胎次'], ['newbornWeightGram', '新生儿体重(克)'],
-          ['apgar1min', '新生儿评分(1分钟)'], ['apgar5min', '新生儿评分(5分钟)'], ['remark', '备注']
+          ['apgar1min', '新生儿评分(1分钟)'], ['remark', '备注']
         ]),
         cards: []
       },
       {
         key: 'samples',
-        title: '样本信息',
+        title: isDonation ? '样本及运输信息' : '样本信息',
         fields: [],
         cards: (detail?.samples || []).map((item, index) => ({
           key: String(item?.id || `sample-${index}`),
@@ -508,7 +496,9 @@ export default {
             ['sampleTypeName', '样本类型', { map: () => referenceName(item?.sampleTypeName, item?.sampleTypeId) }],
             ['sampleCode', '样本编码'],
             ['sampleStatus', '样本状态', { map: () => label(SAMPLE_STATUS_LABELS, item?.sampleStatusLabel || item?.sampleStatus) }],
-            ['price', '优惠价'], ['originalPrice', '原价'], ['storageYears', '存储年限'],
+            ...(source === 'cell_storage'
+              ? [['price', '优惠价'], ['originalPrice', '原价'], ['storageYears', '存储年限']]
+              : []),
             [
               'transportMode',
               '运输方式',
@@ -573,7 +563,7 @@ export default {
         })
       })
     }
-    if (detail?.source === 'cell_storage' || detail?.payment || detail?.samplePayments) {
+    if (detail?.source === 'cell_storage') {
       const samplePays = Array.isArray(detail?.samplePayments) ? detail.samplePayments : []
       if (samplePays.length) {
         values.push({
@@ -623,50 +613,12 @@ export default {
           cards: legacyPaymentDetailCards(detail?.payment || {}, legacyDetails)
         })
       }
-    }
-    if (detail?.source === 'cell_storage') {
-      const cert = detail?.storageCert
+      const refundRecords = Array.isArray(detail?.refundRecords) ? detail.refundRecords : []
       values.push({
-        key: 'storageCert',
-        title: '储存证',
-        fields: cert
-          ? [
-              { label: '办理状态', value: Number(cert.makeStatus) === 1 || cert.handled ? '已办理' : '未办理', tag: true },
-              { label: '办理日期', value: value(cert, 'makeDate') },
-              { label: '办理人', value: person(cert.handlerUserName, cert.handlerUserId) },
-              { label: '办理时间', value: value(cert, 'handleTime') },
-              { label: '备注', value: value(cert, 'remark') },
-              {
-                label: '储存证图片',
-                value: cert.certImageUrls || '',
-                type: 'attachments'
-              }
-            ]
-          : [{ label: '办理状态', value: '暂无储存证记录' }],
-        cards: []
-      })
-      const insurance = detail?.fullmoonTodo || {}
-      values.push({
-        key: 'insurance',
-        title: '保险 / 满月待办',
-        fields: [
-          { label: '办理状态', value: insurance.handled ? '已办理' : '未办理', tag: true },
-          { label: '满月日期', value: value(insurance, 'fullmoonDate') },
-          { label: '保险公司', value: value(insurance, 'insuranceCompanyName') },
-          { label: '保险类型', value: value(insurance, 'insuranceType') },
-          { label: '保单号', value: value(insurance, 'insurancePolicyNo') },
-          { label: '生效日', value: value(insurance, 'insuranceEffectiveDate') },
-          { label: '到期日', value: value(insurance, 'insuranceExpiryDate') },
-          { label: '办理人', value: person(insurance.handlerUserName, insurance.handlerUserId) },
-          { label: '办理时间', value: value(insurance, 'handleTime') },
-          { label: '办理备注', value: value(insurance, 'handleRemark') },
-          {
-            label: '保险附件',
-            value: insurance.insuranceAttachments || '',
-            type: 'attachments'
-          }
-        ],
-        cards: []
+        key: 'refundRecords',
+        title: '退费登记',
+        fields: refundRecords.length ? [] : [{ label: '记录', value: '暂无退费登记' }],
+        cards: refundRecordCards(refundRecords)
       })
     }
     const audits = Array.isArray(detail?.audits) ? detail.audits : []
@@ -682,7 +634,6 @@ export default {
           statusText: audit.label,
           statusTone: audit.tagType,
           fields: [
-            { label: '审核状态', value: audit.label, tag: true, tagType: audit.tagType },
             { label: '审核人', value: person(row?.auditorUserName, row?.auditorUserId) },
             { label: '审核意见', value: value(row, 'auditRemark') },
             { label: '时间', value: value(row, 'createTime') }
@@ -690,40 +641,42 @@ export default {
         }
       })
     })
-    const commonStatusFields = [
-      ['effectiveTime', '协议生效时间'],
-      ['effectiveDesc', '协议生效描述'],
-      ['expireTime', '协议过期时间'],
-      ['expireDesc', '协议过期描述'],
-      ['voidTime', '协议作废时间'],
-      ['voidDesc', '协议作废描述'],
-      ['voidType', '作废类型']
-    ]
-    const sourceStatusFields = source === 'research_donation'
-      ? [['convertTime', '转细胞存储时间'], ['convertDesc', '转细胞存储描述']]
-      : [['toResearchTime', '转科研时间'], ['toResearchDesc', '转科研描述']]
-    const statusFields = [...commonStatusFields, ...sourceStatusFields].map(([key, textLabel]) => ({
-      label: textLabel,
-      value: key === 'voidType' ? voidTypeText(basic) : value(basic, key)
-    })).filter((item) => item.value !== '—')
-    values.push({
-      key: 'statusChange',
-      title: '状态变更记录',
-      fields: statusFields.length ? statusFields : [{ label: '记录', value: '暂无状态变更记录' }],
-      cards: []
-    })
     values.push({
       key: 'attachments',
       title: '附件信息',
-      fields: [
-        { label: '协议附件', type: 'attachments', value: detail?.attachments?.protocolAttachments || '' },
-        { label: '知情同意书', type: 'attachments', value: detail?.attachments?.consentFormUrls || '' },
-        { label: '孕妇健康调查表', type: 'attachments', value: detail?.attachments?.healthSurveyFormUrls || '' },
-        { label: '传染病检测报告', type: 'attachments', value: detail?.attachments?.infectiousDiseaseReportUrls || '' },
-        { label: '其他附件', type: 'attachments', value: detail?.attachments?.attachmentUrls || '' }
-      ],
+      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 || '' },
+            { label: '传染病检测报告', type: 'attachments', value: detail?.attachments?.infectiousDiseaseReportUrls || '' },
+            { label: '其他附件', type: 'attachments', value: detail?.attachments?.attachmentUrls || '' }
+          ],
       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)
+    values.push({
+      key: 'statusChange',
+      title: '状态变更记录',
+      fields: statusChangeCards.length
+        ? []
+        : [{ label: '记录', value: '暂无状态变更记录' }],
+      cards: statusChangeCards
+    })
+    if (isDonation) {
+      const order = ['protocol', 'collection', 'samples', 'customer', 'audits', 'attachments', 'statusChange']
+      const byKey = new Map(values.map((section) => [section.key, section]))
+      return order.map((key) => byKey.get(key)).filter(Boolean)
+    }
     return values
   },
   formSections: [

+ 1 - 1
mobile-oa-h5/src/market/status-tone.js

@@ -5,7 +5,7 @@ export function resolveStatusTone(statusText, backendTone) {
   if (VALID_TONES.has(preferred)) return preferred
   const status = String(statusText || '').trim().toLowerCase()
   if (/不通过|异常|拒绝|失败|逾期|作废|danger|error|reject/.test(status)) return 'danger'
-  if (/待|未|部分|到期|进行|warning|pending|processing/.test(status)) return 'warning'
+  if (/待|未|部分|到期|过期|进行|warning|pending|processing/.test(status)) return 'warning'
   if (/完成|通过|正常|启用|生效|已缴清|已入库|已采集|已接收|已办理|success|completed/.test(status)) return 'success'
   if (/停用|默认|info/.test(status)) return 'info'
   return 'primary'

+ 5 - 0
mobile-oa-h5/src/market/status-tone.test.js

@@ -12,3 +12,8 @@ test('matches negative status before positive substrings', () => {
   assert.equal(resolveStatusTone('不通过'), 'danger')
   assert.equal(resolveStatusTone('通过'), 'success')
 })
+
+test('expired status is warning instead of primary green', () => {
+  assert.equal(resolveStatusTone('已过期'), 'warning')
+  assert.equal(resolveStatusTone('已过期', 'warning'), 'warning')
+})