From 4e586e905b0c72223ff8b5e89a026704fa98e1b7 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 14 Oct 2026 14:40:00 +0800 Subject: [PATCH] feat(mobile-oa-h5): present payment periods as collapsible periodBlocks Stop flattening six installment field groups into list cards; emit periodBlocks and footerFields instead. Co-authored-by: Cursor --- mobile-oa-h5/src/market/list-presenter.js | 101 ++++++++++++------ .../src/market/list-presenter.test.js | 66 +++++++++--- 2 files changed, 123 insertions(+), 44 deletions(-) diff --git a/mobile-oa-h5/src/market/list-presenter.js b/mobile-oa-h5/src/market/list-presenter.js index 7d8c7743..b051ee2e 100644 --- a/mobile-oa-h5/src/market/list-presenter.js +++ b/mobile-oa-h5/src/market/list-presenter.js @@ -24,7 +24,7 @@ const ACTION_LABELS = Object.freeze({ submit: '提交财务确认', withdraw: '撤回转账', link: '关联样本', - unlink: '取消运费' + unlink: '取消样本关联' }) const PAY_MODE_LABELS = Object.freeze({ dj: '趸交', fq: '分期', fgd: '分改趸', djfq: '趸交+分期' }) @@ -180,21 +180,48 @@ function chaseCard(item) { } } +function hasPresentValue(value) { + if (value == null) return false + if (typeof value === 'number') return Number.isFinite(value) + return String(value).trim() !== '' +} + +function isPaymentPeriodPresent(period) { + if (!period || typeof period !== 'object') return false + return ( + hasPresentValue(period.scheduleNo) || + hasPresentValue(period.dueAmount) || + hasPresentValue(period.displayPaidAmount) || + hasPresentValue(period.paidAmount) || + hasPresentValue(period.arrearsAmount) || + hasPresentValue(period.dueDate) || + hasPresentValue(period.payDate) + ) +} + +function paymentPeriodBlock(period, index) { + const scheduleNo = Number(period.scheduleNo) || index + 1 + const paidRaw = period.displayPaidAmount ?? period.paidAmount + return { + key: String(period.id || `period-${scheduleNo}`), + scheduleNo, + dueAmount: money(period.dueAmount), + paidAmount: money(paidRaw), + fields: [ + field('应缴费时间', period.dueDate), + field('已缴费时间', period.payDate), + field('欠缴费金额', money(period.arrearsAmount), { money: true }) + ] + } +} + function paymentCard(item) { const progress = label({}, item.paymentProgress || item.progress || (Number(item.paidAmount || 0) > 0 ? '部分已缴' : '待缴清')) - const periods = Array.from({ length: 6 }, (_, index) => item.periods?.[index] || {}) - const periodFields = periods.flatMap((period, index) => { - const prefix = `第${index + 1}期·` - return [ - field(`${prefix}应缴费金额(元)`, money(period.dueAmount), { money: true }), - field(`${prefix}应缴费时间`, period.dueDate), - field(`${prefix}已缴费金额`, money(period.displayPaidAmount ?? period.paidAmount), { money: true }), - field(`${prefix}已缴费时间`, period.payDate), - field(`${prefix}欠缴费金额`, money(period.arrearsAmount), { money: true }) - ] - }) + const periods = (Array.isArray(item.periods) ? item.periods : []).filter(isPaymentPeriodPresent) return { - title: text(item.motherName), subtitle: text(item.sampleCode), status: progress, + title: text(item.motherName), + subtitle: text(item.sampleCode), + status: progress, fields: [ field('协议号', item.protocolNo, { wide: true }), field('姓名', item.motherName), field('手机号', item.motherPhone), @@ -203,8 +230,10 @@ function paymentCard(item) { field('合同总额', money(item.contractAmount), { money: true }), field('缴费方式', label(PAY_MODE_LABELS, item.payMode), { tag: true }), field('优惠金额', money(item.discountAmount), { money: true }), - field('应收总额', money(item.receivableAmount), { money: true }), - ...periodFields, + field('应收总额', money(item.receivableAmount), { money: true }) + ], + periodBlocks: periods.map(paymentPeriodBlock), + footerFields: [ field('累计实缴', money(item.paidAmount), { money: true }), field('待缴金额', money(item.dueRemainAmount ?? item.remainAmount), { money: true }), field('备注', item.remark, { wide: true }) @@ -213,38 +242,48 @@ function paymentCard(item) { } } +function freightSampleBlock(row, index) { + const sampleCode = text(row?.sampleCode, '') + const title = sampleCode + ? `样本 ${index + 1} · ${sampleCode}` + : `样本 ${index + 1}` + return { + key: String(row?.id || row?.sampleId || `${sampleCode || 'sample'}-${index}`), + title, + status: label(SOURCE_LABELS, row?.sampleSource), + fields: [ + field('协议号', row?.protocolNo, { wide: true }), + field('母亲姓名', row?.motherName), + field('子女姓名', row?.newbornName), + field('样本编码', row?.sampleCode, { wide: true }), + field('样本类型', row?.sampleTypeName, { wide: true }) + ] + } +} + function freightCard(role, item) { const rows = Array.isArray(item.items) ? item.items : [] const first = rows[0] || {} const transportMode = label({}, item.transportModeLabel || item.transportMode) const source = label(SOURCE_LABELS, first.sampleSource || item.sourceLabel) - const itemFields = rows.flatMap((row, index) => { - const prefix = index === 0 ? '' : `样本${index + 1}·` - return [ - field(`${prefix}来源`, label(SOURCE_LABELS, row.sampleSource), { tag: true }), - field(`${prefix}协议号`, row.protocolNo, { wide: true }), - field(`${prefix}母亲姓名`, row.motherName), - field(`${prefix}子女姓名`, row.newbornName), - field(`${prefix}样本编码`, row.sampleCode, { wide: true }), - field(`${prefix}样本类型`, row.sampleTypeName, { wide: true }) - ] - }) + const sampleCount = Number(item.visibleItemCount ?? rows.length) || 0 return { title: text(item.freightNo), subtitle: `${transportMode} · ${money(item.freightAmount)}`, status: source, fields: [ - field('批次号', item.freightNo, { wide: true }), field('运输方式', transportMode, { tag: true }), + field('运输方式', transportMode, { tag: true }), field('运费金额', money(item.freightAmount), { money: true }), - field('关联数', String(item.visibleItemCount ?? item.items?.length ?? 0)), - ...itemFields, + field('关联数', String(sampleCount)), field('创建人', personName(item.creatorName || item.createByName, item.createBy)), field('创建时间', item.createTime), field('更新时间', item.updateTime), field('备注', item.remark, { wide: true }) ], - privacyNote: role === 'specialist' ? '仅展示当前权限范围内的关联样本' : '', - actions: allowed(item, ['edit', 'link', 'unlink']) + sampleBlocks: rows.map(freightSampleBlock), + sampleCount, + privacyNote: '', + actions: allowed(item, ['edit', 'delete', 'link', 'unlink']) } } diff --git a/mobile-oa-h5/src/market/list-presenter.test.js b/mobile-oa-h5/src/market/list-presenter.test.js index 8ac3d279..ab4a2539 100644 --- a/mobile-oa-h5/src/market/list-presenter.test.js +++ b/mobile-oa-h5/src/market/list-presenter.test.js @@ -137,11 +137,39 @@ test('payment card formats summary totals and progress', () => { assert.ok(card.fields.some((field) => field.label === '协议号' && field.value === 'P-1')) assert.ok(card.fields.some((field) => field.label === '签署日期' && field.value === '2026-10-01')) assert.ok(card.fields.some((field) => field.label === '存储项目' && field.value === '脐带血')) - assert.ok(card.fields.some((field) => field.label === '第1期·应缴费金额(元)' && field.value === '¥3,000.00')) - assert.ok(card.fields.some((field) => field.label === '待缴金额' && field.value === '¥7,000.00')) + assert.ok(!card.fields.some((field) => String(field.label).includes('第1期'))) + assert.equal(card.periodBlocks.length, 1) + assert.equal(card.periodBlocks[0].scheduleNo, 1) + assert.equal(card.periodBlocks[0].dueAmount, '¥3,000.00') + assert.equal(card.periodBlocks[0].paidAmount, '¥3,000.00') + assert.ok(card.periodBlocks[0].fields.some((f) => f.label === '应缴费时间' && f.value === '2026-10-10')) + assert.ok(card.periodBlocks[0].fields.some((f) => f.label === '已缴费时间' && f.value === '2026-10-11')) + assert.ok(card.periodBlocks[0].fields.some((f) => f.label === '欠缴费金额' && f.value === '¥0.00')) + assert.ok(card.footerFields.some((field) => field.label === '待缴金额' && field.value === '¥7,000.00')) + assert.ok(card.footerFields.some((field) => field.label === '累计实缴' && field.value === '¥3,000.00')) + assert.ok(card.footerFields.some((field) => field.label === '备注' && field.value === '首期已缴')) assert.deepEqual(card.actions, ['view-protocol', 'view-payment-detail']) }) +test('payment card periodBlocks skip empty periods and do not pad to six', () => { + const card = buildMarketCard('payment', 'specialist', { + motherName: '李婷', sampleCode: 'S-2', payMode: 'fq', + periods: [ + { scheduleNo: 1, dueAmount: 1000, dueDate: '2026-01-01', paidAmount: 1000, payDate: '2026-01-02', arrearsAmount: 0 }, + {}, + { scheduleNo: 3, dueAmount: 2000, dueDate: '2026-07-01', paidAmount: 0, arrearsAmount: 2000 }, + null, + undefined + ] + }) + assert.equal(card.periodBlocks.length, 2) + assert.equal(card.periodBlocks[0].key, 'period-1') + assert.equal(card.periodBlocks[1].scheduleNo, 3) + assert.equal(card.periodBlocks[1].dueAmount, '¥2,000.00') + assert.equal(card.periodBlocks[1].paidAmount, '¥0.00') + assert.ok(!card.fields.some((field) => /第\d+期/.test(field.label))) +}) + test('payment chase keeps the Web action column while paid totals remain card data', () => { const card = buildMarketCard('payment-chase', 'specialist', { protocolNo: 'P-1', protocolId: 'protocol-1', motherName: '周雨桐', @@ -153,7 +181,7 @@ test('payment chase keeps the Web action column while paid totals remain card da assert.ok(card.fields.some((field) => field.label === '催缴状态')) }) -test('freight card shows the scope-safe visible count and protocol number for both roles', () => { +test('freight card shows the scope-safe visible count and collapsible sample blocks for both roles', () => { const item = { freightNo: 'YF-1', transportModeLabel: '冷链配送', freightAmount: '120.00', visibleItemCount: 2, @@ -166,24 +194,32 @@ test('freight card shows the scope-safe visible count and protocol number for bo } const specialist = buildMarketCard('freight', 'specialist', item) const manager = buildMarketCard('freight', 'manager', item) - assert.equal(specialist.privacyNote, '仅展示当前权限范围内的关联样本') + assert.equal(specialist.privacyNote, '') assert.ok(specialist.fields.some((field) => field.label === '关联数' && field.value === '2')) - assert.ok(specialist.fields.some((field) => field.label === '协议号' && field.value === 'P-1')) + assert.equal(specialist.fields.some((field) => field.label === '批次号'), false) + assert.equal(specialist.sampleCount, 2) + assert.equal(specialist.sampleBlocks.length, 2) + assert.equal(specialist.sampleBlocks[0].fields.find((field) => field.label === '协议号').value, 'P-1') + assert.doesNotMatch( + specialist.fields.map((field) => field.label).join(','), + /协议号|样本编码|母亲姓名|批次号/ + ) assert.equal(manager.privacyNote, '') assert.ok(manager.fields.some((field) => field.label === '关联数' && field.value === '2')) - assert.ok(manager.fields.some((field) => field.label === '样本2·来源' && field.value === '科研捐赠')) - assert.ok(manager.fields.some((field) => field.label === '样本2·协议号' && field.value === 'P-2')) - assert.ok(manager.fields.some((field) => field.label === '样本2·母亲姓名' && field.value === '周雨桐')) - assert.ok(manager.fields.some((field) => field.label === '样本2·子女姓名' && field.value === '宝宝乙')) - assert.ok(manager.fields.some((field) => field.label === '样本2·样本编码' && field.value === 'S-2')) - assert.ok(manager.fields.some((field) => field.label === '样本2·样本类型' && field.value === '脐带')) + assert.equal(manager.sampleBlocks[1].title, '样本 2 · S-2') + assert.equal(manager.sampleBlocks[1].status, '科研捐赠') + assert.equal(manager.sampleBlocks[1].fields.find((field) => field.label === '协议号').value, 'P-2') + assert.equal(manager.sampleBlocks[1].fields.find((field) => field.label === '母亲姓名').value, '周雨桐') + assert.equal(manager.sampleBlocks[1].fields.find((field) => field.label === '子女姓名').value, '宝宝乙') + assert.equal(manager.sampleBlocks[1].fields.find((field) => field.label === '样本编码').value, 'S-2') + assert.equal(manager.sampleBlocks[1].fields.find((field) => field.label === '样本类型').value, '脐带') assert.ok(manager.fields.some((field) => field.label === '更新时间' && field.value === '2026-10-13 10:00:00')) assert.ok(manager.fields.some((field) => field.label === '备注' && field.value === '同批送达')) }) test('action labels cover module-specific operations', () => { assert.equal(actionLabel('link'), '关联样本') - assert.equal(actionLabel('unlink'), '取消运费') + assert.equal(actionLabel('unlink'), '取消样本关联') assert.equal(actionLabel('internal-action-code'), '操作') }) @@ -208,7 +244,11 @@ test('list cards hide unresolved reference and dictionary codes while retaining assert.equal(freight.title, 'F-KEEP-1') assert.ok(freight.subtitle.startsWith('未知 ·')) assert.equal(freight.fields.find((field) => field.label === '创建人').value, '未知人员') - assert.equal(freight.fields.find((field) => field.label === '样本编码').value, 'S-KEEP-2') + assert.equal( + freight.sampleBlocks[0].fields.find((field) => field.label === '样本编码').value, + 'S-KEEP-2' + ) + assert.equal(freight.sampleBlocks[0].status, '未知') const payment = buildMarketCard('payment', 'manager', { protocolNo: 'P-KEEP-2', sampleCode: 'S-KEEP-2', payMode: 'raw-pay-mode'