diff --git a/mobile-oa-h5/src/market/components/ProtocolSamplePaymentPanel.vue b/mobile-oa-h5/src/market/components/ProtocolSamplePaymentPanel.vue index ed2f27cb..1b1c405a 100644 --- a/mobile-oa-h5/src/market/components/ProtocolSamplePaymentPanel.vue +++ b/mobile-oa-h5/src/market/components/ProtocolSamplePaymentPanel.vue @@ -231,140 +231,15 @@ ¥{{ schedulePaid(sch).arrears }} -
-
- 转账 {{ tIdx + 1 }} - - {{ payStatusLabel(tr.payStatus) }} - -
-
-
- -
- -
-
-
- - -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
-
- - 保存 - - - 提交财务 - - - 撤回 - - - 删除 - -
-
- - 添加转账 - + @@ -379,13 +254,6 @@ close-on-click-action @select="onPayModeSelect" /> - import { computed, ref, watch } from 'vue' -import { showConfirmDialog, showSuccessToast, showToast } from 'vant' -import OaDatePicker from '../../oa/components/OaDatePicker.vue' -import { performMarketAction, uploadMarketAttachment } from '../../api/market.js' -import PresignAttachmentUploader from './PresignAttachmentUploader.vue' +import { showConfirmDialog, showToast } from 'vant' +import ScheduleTransferEditor from './ScheduleTransferEditor.vue' import { applyConsolidateRemainingToPayment, applyExpandRemainingToInstallment, @@ -421,32 +287,11 @@ import { shouldAutoFillLastPeriodDue } from '../protocol-sample-payment.js' import { - applySavedTransfer, - assertCanSaveTransfer, - buildTransferSavePayload, - canSubmitFinance, canUseProtocolScheduleAction, - canUseProtocolTransferAction, - canWithdrawFinance, - emptyTransferRow, - ensureTransferAttachments, - PAY_STATUS_LABELS, - patchTransfersAt, - validateTransferRow + emptyTransferRow } from '../protocol-sample-transfer.js' -import { marketErrorMessage } from '../market-error.js' import { - createPendingMarketAttachment, - dropPendingMarketAttachment, - removeMarketAttachmentAt, - resolvePendingMarketAttachment, - withPendingMarketAttachment -} from '../attachment-upload-state.js' -import { - applyTransferDeleteResult, - clearTransferDirty, hasDirtyTransfers, - isTransferDirty, markTransferDirty } from '../protocol-transfer-dirty.js' import { compareMoney, normalizeMoney } from '../money.js' @@ -472,16 +317,11 @@ const lastPeriodDueOverrideByKey = ref({}) const modeSheetVisible = ref(false) const modeSheetPayIdx = ref(-1) const modeSheetActions = ref([]) -const transferTypeSheetVisible = ref(false) -const transferTypeSheetActions = ref([]) -const transferTypeTarget = ref(null) const datePickerVisible = ref(false) const datePickerValue = ref(['2026', '10', '13']) const dateTarget = ref(null) const minDate = new Date(2020, 0, 1) const maxDate = new Date(2040, 11, 31) -const busyKey = ref('') -const uploadKeys = ref(new Set()) const syncedPayments = computed(() => props.modelValue || []) const expandedSchedules = ref(new Set()) @@ -592,105 +432,23 @@ function isScheduleLocked(sch) { return (sch.transfers || []).some((t) => t.payStatus === 'yjf') } -function paymentContext() { - return { module: 'payment', marketRole: props.marketRole || 'specialist' } -} - function schedulePaid(sch) { return schedulePaidArrears(sch?.dueAmount, sch?.transfers || []) } -function payStatusLabel(status) { - return PAY_STATUS_LABELS[String(status || 'dfj')] || '已付款' +function replaceSchedule(pIdx, sIdx, nextSchedule) { + const list = [...(props.modelValue || [])] + const pay = { ...list[pIdx] } + const schedules = [...(pay.schedules || [])] + schedules[sIdx] = nextSchedule + pay.schedules = schedules + list[pIdx] = pay + emitList(list) + emit('transfer-dirty-change', hasDirtyTransfers(list)) } -function payStatusTone(status) { - const code = String(status || 'dfj') - if (code === 'yjf') return 'success' - if (code === 'jfqrz') return 'warning' - return 'primary' -} - -function transferTypeLabel(value) { - const key = String(value || '').trim() - const hit = (props.transferTypes || []).find((opt) => String(opt.value) === key) - return hit?.label || key || '请选择' -} - -function parseAmount(raw) { - if (raw === '' || raw == null) return null - return String(raw) -} - -function transferBusyKey(pIdx, sIdx, tIdx, tr) { - return `save:${tr?.id || `${pIdx}-${sIdx}-${tIdx}`}` -} - -function isTransferUploading(pIdx, sIdx, tIdx) { - return uploadKeys.value.has(`${pIdx}-${sIdx}-${tIdx}`) -} - -function getTransferList(pIdx, sIdx) { - return [...(props.modelValue?.[pIdx]?.schedules?.[sIdx]?.transfers || [])] -} - -function setTransferList(pIdx, sIdx, transfers) { - const next = patchTransfersAt(props.modelValue, pIdx, sIdx, transfers) - emitList(next) - emit('transfer-dirty-change', hasDirtyTransfers(next)) -} - -function patchTransfer(pIdx, sIdx, tIdx, patch) { - const transfers = getTransferList(pIdx, sIdx) - transfers[tIdx] = markTransferDirty({ ...transfers[tIdx], ...patch }) - setTransferList(pIdx, sIdx, transfers) -} - -function addTransfer(pIdx, sIdx) { - const schedule = props.modelValue?.[pIdx]?.schedules?.[sIdx] - if (!canAddTransfer(schedule)) return - const transfers = getTransferList(pIdx, sIdx) - transfers.push(markTransferDirty(emptyTransferRow())) - setTransferList(pIdx, sIdx, transfers) - const key = scheduleKey(pIdx, sIdx) - if (!expandedSchedules.value.has(key)) { - const next = new Set(expandedSchedules.value) - next.add(key) - expandedSchedules.value = next - } -} - -async function removeTransfer(pIdx, sIdx, tIdx) { - const schedule = props.modelValue?.[pIdx]?.schedules?.[sIdx] - const transfer = schedule?.transfers?.[tIdx] - if (!transfer || !canDeleteTransfer(schedule, transfer)) return - try { - await showConfirmDialog({ title: '删除转账', message: '确认删除该笔转账记录吗?' }) - } catch { - return - } - if (!transfer.id) { - const transfers = getTransferList(pIdx, sIdx) - transfers.splice(tIdx, 1) - setTransferList(pIdx, sIdx, transfers) - return - } - busyKey.value = `delete:${transfer.id}` - try { - await performMarketAction(paymentContext(), 'delete-payment-transfer', { - id: transfer.id - }) - const next = applyTransferDeleteResult( - { samplePayments: props.modelValue }, pIdx, sIdx, tIdx, true - ).samplePayments - emitList(next) - emit('transfer-dirty-change', hasDirtyTransfers(next)) - showSuccessToast('转账记录已删除') - } catch (error) { - showToast(marketErrorMessage(error, '删除转账失败')) - } finally { - busyKey.value = '' - } +function onTransferChanged() { + emit('transfer-dirty-change', hasDirtyTransfers(props.modelValue)) } function canAddTransfer(schedule) { @@ -701,197 +459,17 @@ function canAddTransfer(schedule) { return canUseProtocolScheduleAction(schedule, 'register-transfer') } -function canEditTransfer(schedule, transfer) { - if (!transfer?.id) return canAddTransfer(schedule) - return canUseProtocolTransferAction(transfer, 'edit-transfer') -} - -function canDeleteTransfer(schedule, transfer) { - if (!transfer?.id) return canAddTransfer(schedule) - return canUseProtocolTransferAction(transfer, 'delete-payment-transfer') -} - -function canSubmitTransfer(transfer) { - return !isTransferDirty(transfer) && - canUseProtocolTransferAction(transfer, 'submit') && - canSubmitFinance(transfer, props.protocolId) -} - -function canWithdrawTransfer(transfer) { - return canUseProtocolTransferAction(transfer, 'withdraw') && - canWithdrawFinance(transfer, props.protocolId) -} - -function hasTransferAction(schedule, transfer) { - return canEditTransfer(schedule, transfer) || - canDeleteTransfer(schedule, transfer) || - canSubmitTransfer(transfer) || - canWithdrawTransfer(transfer) -} - -function openTransferTypeSheet(pIdx, sIdx, tIdx) { - transferTypeTarget.value = { pIdx, sIdx, tIdx } - transferTypeSheetActions.value = (props.transferTypes || []).map((opt) => ({ - name: opt.label || opt.value, - value: opt.value - })) - if (!transferTypeSheetActions.value.length) { - showToast('暂无转账方式选项') - return +function addTransfer(pIdx, sIdx) { + const schedule = props.modelValue?.[pIdx]?.schedules?.[sIdx] + if (!canAddTransfer(schedule)) return + const transfers = [...(schedule.transfers || []), markTransferDirty(emptyTransferRow())] + replaceSchedule(pIdx, sIdx, { ...schedule, transfers }) + const key = scheduleKey(pIdx, sIdx) + if (!expandedSchedules.value.has(key)) { + const next = new Set(expandedSchedules.value) + next.add(key) + expandedSchedules.value = next } - transferTypeSheetVisible.value = true -} - -function onTransferTypeSelect(action) { - const target = transferTypeTarget.value - if (!target) return - patchTransfer(target.pIdx, target.sIdx, target.tIdx, { transferType: action.value }) -} - -async function saveTransfer(pIdx, sIdx, tIdx) { - const sch = props.modelValue?.[pIdx]?.schedules?.[sIdx] - const tr = sch?.transfers?.[tIdx] - if (!tr || !canEditTransfer(sch, tr)) return - try { - assertCanSaveTransfer({ protocolId: props.protocolId, scheduleId: sch?.id }) - } catch (error) { - showToast(error.message) - return - } - const invalid = validateTransferRow(tr) - if (invalid) { - showToast(invalid) - return - } - const key = transferBusyKey(pIdx, sIdx, tIdx, tr) - busyKey.value = key - try { - const payload = buildTransferSavePayload(tr) - let remote - if (tr.id) { - remote = await performMarketAction(paymentContext(), 'edit-transfer', { - id: tr.id, - data: payload - }) - } else { - remote = await performMarketAction(paymentContext(), 'register-transfer', { - data: { scheduleId: String(sch.id), ...payload } - }) - } - const data = remote?.data || remote || {} - const transfers = getTransferList(pIdx, sIdx) - transfers[tIdx] = clearTransferDirty(applySavedTransfer(transfers[tIdx], data)) - setTransferList(pIdx, sIdx, transfers) - showSuccessToast(tr.id ? '转账记录已保存' : '转账记录已创建') - } catch (error) { - showToast(marketErrorMessage(error, '保存转账失败')) - } finally { - busyKey.value = '' - } -} - -async function submitFinance(tr, pIdx, sIdx, tIdx) { - if (!canSubmitTransfer(tr)) { - showToast('请先保存该转账记录后再提交财务确认') - return - } - try { - await showConfirmDialog({ - title: '提交财务确认', - message: '确认提交该笔转账给财务确认吗?' - }) - } catch { - return - } - busyKey.value = `submit:${tr.id}` - try { - const remote = await performMarketAction(paymentContext(), 'submit', { id: tr.id }) - const data = remote?.data || remote || {} - const transfers = getTransferList(pIdx, sIdx) - transfers[tIdx] = applySavedTransfer(transfers[tIdx], { ...data, payStatus: data.payStatus || 'jfqrz' }) - setTransferList(pIdx, sIdx, transfers) - showSuccessToast('已提交财务确认') - } catch (error) { - showToast(marketErrorMessage(error, '提交失败')) - } finally { - busyKey.value = '' - } -} - -async function withdrawFinance(tr, pIdx, sIdx, tIdx) { - if (!canWithdrawTransfer(tr)) return - try { - await showConfirmDialog({ - title: '撤回', - message: '确认撤回该笔转账的财务确认申请?撤回后可修改并重新提交。' - }) - } catch { - return - } - busyKey.value = `withdraw:${tr.id}` - try { - const remote = await performMarketAction(paymentContext(), 'withdraw', { id: tr.id }) - const data = remote?.data || remote || {} - const transfers = getTransferList(pIdx, sIdx) - transfers[tIdx] = applySavedTransfer(transfers[tIdx], { ...data, payStatus: data.payStatus || 'dfj' }) - setTransferList(pIdx, sIdx, transfers) - showSuccessToast('已撤回') - } catch (error) { - showToast(marketErrorMessage(error, '撤回失败')) - } finally { - busyKey.value = '' - } -} - -async function uploadTransferFile(pIdx, sIdx, tIdx, file) { - if (!file) return - const key = `${pIdx}-${sIdx}-${tIdx}` - const pending = createPendingMarketAttachment(file) - const transfersBegin = getTransferList(pIdx, sIdx) - const begin = { ...transfersBegin[tIdx] } - ensureTransferAttachments(begin) - begin.attachments = withPendingMarketAttachment(begin.attachments, pending) - transfersBegin[tIdx] = markTransferDirty(begin) - setTransferList(pIdx, sIdx, transfersBegin) - uploadKeys.value = new Set([...uploadKeys.value, key]) - try { - const uploaded = await uploadMarketAttachment( - { module: 'payment', marketRole: props.marketRole || 'specialist' }, - file - ) - const transfers = getTransferList(pIdx, sIdx) - const current = { ...transfers[tIdx] } - ensureTransferAttachments(current) - current.attachments = resolvePendingMarketAttachment( - current.attachments, - pending._pendingId, - uploaded - ) - transfers[tIdx] = markTransferDirty(current) - setTransferList(pIdx, sIdx, transfers) - showSuccessToast('附件上传成功') - } catch (error) { - const transfers = getTransferList(pIdx, sIdx) - const current = { ...transfers[tIdx] } - ensureTransferAttachments(current) - current.attachments = dropPendingMarketAttachment(current.attachments, pending._pendingId) - transfers[tIdx] = markTransferDirty(current) - setTransferList(pIdx, sIdx, transfers) - showToast(marketErrorMessage(error, '附件上传失败')) - } finally { - const next = new Set(uploadKeys.value) - next.delete(key) - uploadKeys.value = next - } -} - -function removeTransferFile(pIdx, sIdx, tIdx, index) { - const transfers = getTransferList(pIdx, sIdx) - const current = { ...transfers[tIdx] } - ensureTransferAttachments(current) - current.attachments = removeMarketAttachmentAt(current.attachments, index) - transfers[tIdx] = markTransferDirty(current) - setTransferList(pIdx, sIdx, transfers) } function activeScheduleIndices(schedules) { @@ -1421,50 +999,4 @@ function onDateConfirm({ selectedValues }) { .protocol-sample-pay__stat.is-arrears b { color: #b45309; } -.protocol-sample-pay__transfer { - margin: 8px 8px 10px; - padding: 10px; - border-radius: 12px; - border: 1px solid #e8f0ec; - background: linear-gradient(180deg, #f8fffc, #fff); -} -.protocol-sample-pay__transfer-title { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 6px; -} -.protocol-sample-pay__attach, -.protocol-sample-pay__datetime { - padding: 8px 12px; -} -.protocol-sample-pay__attach label, -.protocol-sample-pay__datetime label { - display: block; - margin-bottom: 6px; - color: #3d524a; - font-size: 12px; - font-weight: 650; -} -.protocol-sample-pay__transfer-actions { - display: flex; - flex-wrap: wrap; - gap: 6px; - justify-content: flex-end; - padding: 8px 4px 0; -} -.protocol-sample-pay__transfer-actions :deep(.van-button), -.protocol-sample-pay__add-transfer { - min-width: auto; - min-height: 28px; - height: 28px; - padding: 0 10px; - border-radius: 8px !important; - font-weight: 600; -} -.protocol-sample-pay__add-transfer { - margin: 0 8px; - width: calc(100% - 16px); -} diff --git a/mobile-oa-h5/src/market/components/ScheduleTransferEditor.vue b/mobile-oa-h5/src/market/components/ScheduleTransferEditor.vue new file mode 100644 index 00000000..b58d51d9 --- /dev/null +++ b/mobile-oa-h5/src/market/components/ScheduleTransferEditor.vue @@ -0,0 +1,527 @@ + + + + + diff --git a/mobile-oa-h5/src/market/mobile-parity-ui.test.js b/mobile-oa-h5/src/market/mobile-parity-ui.test.js index 996f25ff..2d4f6efb 100644 --- a/mobile-oa-h5/src/market/mobile-parity-ui.test.js +++ b/mobile-oa-h5/src/market/mobile-parity-ui.test.js @@ -82,6 +82,7 @@ test('market list visual contract keeps search white, tags filled and actions ri './components/PresignOrderCard.vue', './components/PresignTransferCard.vue', './components/ProtocolSamplePaymentPanel.vue', + './components/ScheduleTransferEditor.vue', './components/ProtocolSampleTransportPanel.vue', './components/ProtocolV1PaymentPanel.vue', '../views/market/MarketModuleDetail.vue' @@ -449,13 +450,15 @@ test('market forms share attachment cards and preserve their existing value shap assert.match(genericForm, /fieldsLocked/) const protocolPay = source('./components/ProtocolSamplePaymentPanel.vue') + const transferEditor = source('./components/ScheduleTransferEditor.vue') const protocolV1Pay = source('./components/ProtocolV1PaymentPanel.vue') const presignForm = source('../views/market/presign/PresignOrderForm.vue') - for (const content of [protocolPay, protocolV1Pay, presignForm, genericForm]) { + assert.match(protocolPay, / section.title), ['缴费概览', '应缴批次']) assert.deepEqual( sections[0].fields.map((field) => field.label), - ['协议号', '客户', '联系电话', '样本编码', '缴费方式', '合同额', '已缴 / 待缴', '整体进度', '协议状态', '缴费进度'] + ['客户', '联系电话', '样本编码', '缴费方式', '合同额', '已缴 / 待缴', '整体进度', '协议状态', '缴费进度'] ) assert.equal(sections[1].cards[0].variant, 'schedule') assert.deepEqual( @@ -611,8 +614,6 @@ test('protocol, freight and presign details define explicit Chinese nested cards customer: { motherName: '张三' }, samples: [{ id: 's1', sampleCode: 'S-1', sampleTypeName: '脐带血' }], attachments: { protocolAttachments: 'a.pdf' }, - storageCert: { makeStatus: '0', certImageUrls: 'cert.jpg' }, - fullmoonTodo: { handled: false, insuranceAttachments: 'ins.pdf', handleTime: '2026-10-01' }, audits: [] }) const freight = getMarketModule('freight').detailSections({ @@ -634,13 +635,8 @@ test('protocol, freight and presign details define explicit Chinese nested cards } } assert.ok(protocol.find((section) => section.key === 'samples').cards.length) - const storageCert = protocol.find((section) => section.key === 'storageCert') - const insurance = protocol.find((section) => section.key === 'insurance') - assert.ok(storageCert) - assert.ok(insurance) - assert.ok(storageCert.fields.some((field) => field.type === 'attachments' && field.label === '储存证图片')) - assert.ok(insurance.fields.some((field) => field.type === 'attachments' && field.label === '保险附件')) - assert.ok(insurance.fields.some((field) => field.label === '办理时间')) + assert.equal(protocol.find((section) => section.key === 'storageCert'), undefined) + assert.equal(protocol.find((section) => section.key === 'insurance'), undefined) assert.ok(protocol.find((section) => section.key === 'audits')) assert.ok(protocol.find((section) => section.key === 'statusChange')) assert.ok(freight.find((section) => section.key === 'items').cards.length) @@ -730,21 +726,22 @@ test('protocol create and edit forms expose every writable web business field', assert.match(samplePanel, /searchable/) assert.match(samplePanel, /market-form-field/) const payPanel = source('../market/components/ProtocolSamplePaymentPanel.vue') + const transferEditor = source('../market/components/ScheduleTransferEditor.vue') assert.match(payPanel, /applyPreferentialPriceToPayment/) assert.match(payPanel, /lastPreferentialByCode/) + assert.match(payPanel, / { assert.match(detail, /isPaymentEntryView|filterProtocolActionsForPaymentEntry/) }) + +test('ScheduleTransferEditor exposes save submit withdraw and MarketAttachmentCards', () => { + const src = source('./components/ScheduleTransferEditor.vue') + assert.match(src, /MarketAttachmentCards|PresignAttachmentUploader/) + assert.match(src, /提交财务/) + assert.match(src, /撤回/) + assert.match(src, /添加转账/) +}) diff --git a/mobile-oa-h5/src/market/protocol-mobile-parity.test.js b/mobile-oa-h5/src/market/protocol-mobile-parity.test.js index beb4cc87..f21a65fb 100644 --- a/mobile-oa-h5/src/market/protocol-mobile-parity.test.js +++ b/mobile-oa-h5/src/market/protocol-mobile-parity.test.js @@ -20,7 +20,7 @@ function cardLabels(card) { ] } -test('protocol detail exposes reception qc production storage and release information', () => { +test('protocol detail exposes reception and storage release without qc production or cert sections', () => { const sections = getMarketModule('protocol').detailSections({ source: 'cell_storage', basic: { protocolNo: 'P-1', protocolStatus: 'sxz' }, @@ -64,40 +64,66 @@ test('protocol detail exposes reception qc production storage and release inform const labels = [...sample.fields.map((field) => field.label), ...sample.cards.flatMap(cardLabels)] for (const expected of [ '质检接收', '接收时间', '接收附件', '生产接收', '处理时间', - '门控质检', '工单号', '过程质检 1', '生产信息', '生产任务 1', '入库放行', '生产结论', '质检结论', '入库状态', '质检附件', '生产附件' ]) assert.ok(labels.includes(expected), `missing ${expected}`) + for (const hidden of ['门控质检', '过程质检 1', '生产信息', '生产任务 1']) { + assert.ok(!labels.includes(hidden), `must not show ${hidden}`) + } + assert.equal(sections.find((section) => section.key === 'storageCert'), undefined) + assert.equal(sections.find((section) => section.key === 'insurance'), undefined) }) -test('protocol lifecycle keeps qc targets results batches and production inputs outputs', () => { +test('protocol detail shows refund registration attachments', () => { + const files = [{ fileUrl: '/minio/refund.pdf', fileName: '退费凭证.pdf' }] const sections = getMarketModule('protocol').detailSections({ - source: 'research_donation', - basic: { donationNo: 'D-1', donationStatus: 'sxz' }, - samples: [{ - id: 'sample-rich', sampleCode: 'D-S-1', - qcInfo: { - workOrderNo: 'QC-1', cellProductCode: 'CP-1', cellProductName: '细胞产品', - productionWorkOrderNo: 'PR-1', triggerSourceLabel: '样本接收', - inspectionBatches: [{ id: 'b1', batchNo: 'B-1', batchName: '第一批', assigneeUserNames: '质检员', taskCount: 1, completedTaskCount: 1 }], - tasks: [{ taskNo: 'QT-1', results: [{ paramName: '活率', paramUnit: '%', paramValue: '98' }] }] - }, - productionInfo: { - workOrderNo: 'PR-1', - tasks: [{ - taskNo: 'PT-1', - inputs: [{ inputType: '原料', inputCode: 'IN-1', inputName: '脐带血' }], - outputs: [{ outputType: '产物', outputCode: 'OUT-1', outputName: '细胞制剂' }] - }] - } + source: 'cell_storage', + basic: { protocolNo: 'P-1', protocolStatus: 'sxz' }, + refundRecords: [{ + id: 'rr-1', + refundAmount: 1200, + refundTime: '2026-10-16 09:00:00', + refundRemark: '客户申请', + refundAttachments: '/minio/refund.pdf', + refundAttachmentFiles: files }] }) - const sample = sections.find((section) => section.key === 'samples').cards[0] - const labels = [...sample.cards.flatMap(cardLabels)] - for (const expected of [ - '质检对象编码', '质检对象名称', '生产工单号', '触发来源', - '检测批次 1', '批次编号', '负责人', '检测结果 1', '参数值', - '投入物 1', '投入物编码', '产出物 1', '产出物编码' - ]) assert.ok(labels.includes(expected), `missing ${expected}`) + const refund = sections.find((section) => section.key === 'refundRecords') + assert.equal(refund.title, '退费登记') + assert.equal(refund.cards.length, 1) + assert.equal(refund.cards[0].fields.find((field) => field.label === '退费金额').value, '1200') + assert.deepEqual( + refund.cards[0].fields.find((field) => field.type === 'attachments').value, + files + ) +}) + +test('donation detail follows the web view and omits price and payment', () => { + const sections = getMarketModule('protocol').detailSections({ + source: 'research_donation', + basic: { donationNo: 'D-1', donationStatus: 'sxz', donationDate: '20261001', remark: '捐赠备注' }, + customer: { motherName: '李四' }, + samples: [{ + id: 's1', sampleCode: 'D-S-1', sampleTypeName: '脐带血', + price: 100, originalPrice: 200, storageYears: 20 + }], + payment: { payMode: 'dj', contractAmount: 100 }, + samplePayments: [{ sampleCode: 'D-S-1', payMode: 'dj', contractAmount: 100 }], + attachments: { protocolAttachments: 'a.pdf', healthSurveyFormUrls: 'h.pdf', consentFormUrls: 'c.pdf' } + }) + assert.deepEqual(sections.map((section) => section.key), [ + 'protocol', 'collection', 'samples', 'customer', 'audits', 'attachments', 'statusChange' + ]) + assert.equal(sections[0].title, '基本信息') + assert.deepEqual(sections[0].fields.map((field) => field.label), [ + '状态', '母亲姓名', '捐赠日期', '创建时间', '备注' + ]) + const sampleLabels = sections.find((section) => section.key === 'samples').cards[0].fields.map((field) => field.label) + for (const hidden of ['优惠价', '原价', '存储年限']) { + assert.ok(!sampleLabels.includes(hidden), `must not show ${hidden}`) + } + assert.equal(sections.find((section) => section.key === 'payment'), undefined) + const attachmentLabels = sections.find((section) => section.key === 'attachments').fields.map((field) => field.label) + assert.deepEqual(attachmentLabels, ['知情同意书', '传染病检测报告', '其他附件']) }) test('donation detail shows audit history and convert-to-storage status history', () => { @@ -112,7 +138,28 @@ test('donation detail shows audit history and convert-to-storage status history' const audit = sections.find((section) => section.key === 'audits') assert.equal(audit.cards.length, 1) const status = sections.find((section) => section.key === 'statusChange') - assert.deepEqual(status.fields.map((field) => field.label), ['转细胞存储时间', '转细胞存储描述']) + assert.equal(status.cards.length, 1) + assert.equal(status.cards[0].statusText, '转细胞存储') + assert.equal( + status.cards[0].fields.find((field) => field.label === '变更说明').value, + '已转细胞存储' + ) +}) + +test('status change history renders one card per record including same type', () => { + const sections = getMarketModule('protocol').detailSections({ + source: 'cell_storage', + basic: { protocolNo: 'P-1', protocolStatus: 'ygq' }, + statusChanges: [ + { id: 's1', changeType: 'expire', fromStatus: 'sxz', toStatus: 'ygq', changeTime: '2026-06-01 12:00:00', changeDesc: '第一次过期', operatorUserName: '李四' }, + { id: 's2', changeType: 'expire', fromStatus: 'ygq', toStatus: 'ygq', changeTime: '2026-09-15 18:00:00', changeDesc: '第二次过期', operatorUserName: '王五' } + ] + }) + const status = sections.find((section) => section.key === 'statusChange') + assert.equal(status.cards.length, 2) + assert.equal(status.cards[0].statusText, '过期') + assert.equal(status.cards[1].fields.find((field) => field.label === '变更说明').value, '第二次过期') + assert.equal(status.cards[0].fields.find((field) => field.label === '状态流转').value, '生效中 → 已过期') }) test('v1 protocol detail renders readonly payment details with backend transfer labels', () => { @@ -263,11 +310,14 @@ test('protocol transfer controls require backend nested capability and frontend test('protocol transfer UI calls DELETE for persisted rows without reloading form drafts', () => { const panel = source('./components/ProtocolSamplePaymentPanel.vue') + const editor = source('./components/ScheduleTransferEditor.vue') const form = source('../views/market/MarketModuleForm.vue') assert.doesNotMatch(form, /:can-manage-transfers="true"/) - assert.match(panel, /performMarketAction\(paymentContext\(\), 'delete-payment-transfer'/) - assert.match(panel, /title:\s*'提交财务确认'/) + assert.match(panel, / { test('protocol list and nested payment actions use OA-aligned soft semantic buttons', () => { const styles = source('../styles/market.css') const panel = source('./components/ProtocolSamplePaymentPanel.vue') + const editor = source('./components/ScheduleTransferEditor.vue') assert.match(styles, /\.market-list-card__actions \.van-button\s*\{[^}]*min-height:\s*32px;/) assert.match(styles, /\.market-list-card__actions \.van-button\s*\{[\s\S]*?padding:\s*0 12px;/) assert.match(styles, /\.market-page \.van-button--mini\s*\{[\s\S]*?min-height:\s*28px;/) assert.match(styles, /\.market-page \.van-button--mini\s*\{[\s\S]*?border-radius:\s*8px;/) assert.match(styles, /\.market-page \.van-button--primary\s*\{[\s\S]*?background:\s*var\(--market-primary\);/) assert.doesNotMatch(styles, /linear-gradient\(135deg,\s*#12b981/) - assert.match(panel, /\.protocol-sample-pay__transfer-actions[\s\S]*?\.van-button[\s\S]*?min-height:\s*28px;/) + assert.match(editor, /\.protocol-sample-pay__transfer-actions[\s\S]*?\.van-button[\s\S]*?min-height:\s*28px;/) assert.match(panel, /protocol-sample-pay__timeline/) assert.match(panel, /protocol-sample-pay__sum/) })