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:
1 parent
bbc24b7c73
commit
4e586e905b
2 files changed
+123
-44
No files matched your search
@@ -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'])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user