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 <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-14 14:40:00 +08:00
1 parent bbc24b7c73
commit 4e586e905b
2 files changed
+123 -44

No files matched your search

+70 -31
View File
@@ -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'])
}
}
+53 -13
View File
@@ -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'