diff --git a/mobile-oa-h5/src/market/components/MarketDetailNestedCard.vue b/mobile-oa-h5/src/market/components/MarketDetailNestedCard.vue index 1d780add..3b5b3ca4 100644 --- a/mobile-oa-h5/src/market/components/MarketDetailNestedCard.vue +++ b/mobile-oa-h5/src/market/components/MarketDetailNestedCard.vue @@ -1,54 +1,173 @@ diff --git a/mobile-oa-h5/src/market/components/MarketDetailSection.vue b/mobile-oa-h5/src/market/components/MarketDetailSection.vue index 8d0bb950..deb78dc9 100644 --- a/mobile-oa-h5/src/market/components/MarketDetailSection.vue +++ b/mobile-oa-h5/src/market/components/MarketDetailSection.vue @@ -58,7 +58,9 @@ function normalizeDetailField(field, index) { value: type ? field?.value : String(field?.value ?? '-'), type, tag: Boolean(field?.tag), - tagType: field?.tagType + tagType: field?.tagType, + money: Boolean(field?.money), + wide: Boolean(field?.wide) } } @@ -66,6 +68,11 @@ function normalizeCard(card, index, prefix = 'card') { return { key: String(card?.key || `${prefix}-${index}`), title: String(card?.title || `明细 ${index + 1}`), + variant: String(card?.variant || ''), + transferIndex: Number(card?.transferIndex) || index + 1, + amount: card?.amount == null || card?.amount === '' ? '' : String(card.amount), + meta: card?.meta == null || card?.meta === '' ? '' : String(card.meta), + cardsLabel: String(card?.cardsLabel || ''), statusText: String(card?.statusText || card?.statusLabel || ''), statusTone: resolveStatusTone( card?.statusText || card?.statusLabel, 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 6c5a1568..0e89c4e6 100644 --- a/mobile-oa-h5/src/market/mobile-parity-ui.test.js +++ b/mobile-oa-h5/src/market/mobile-parity-ui.test.js @@ -427,13 +427,18 @@ test('payment detail presents every web field and nested transfer field with Chi sections[0].fields.map((field) => field.label), ['协议号', '客户', '联系电话', '样本编码', '缴费方式', '合同额', '已缴 / 待缴', '整体进度', '协议状态', '缴费进度'] ) + assert.equal(sections[1].cards[0].variant, 'schedule') assert.deepEqual( sections[1].cards[0].fields.map((field) => field.label), ['应缴金额', '应缴日', '已缴金额', '欠缴金额'] ) + const transfer = sections[1].cards[0].cards[0] + assert.equal(transfer.variant, 'transfer') + assert.equal(transfer.amount, '¥3,000.00') + assert.match(transfer.meta, /微信转账/) assert.deepEqual( - sections[1].cards[0].cards[0].fields.map((field) => field.label), - ['转账方式', '转账金额', '转账时间', '转账凭证', '备注', '财务备注'] + transfer.fields.map((field) => field.label), + ['备注', '财务备注', '转账凭证'] ) }) diff --git a/mobile-oa-h5/src/market/modules/payment.js b/mobile-oa-h5/src/market/modules/payment.js index 3ee5879d..d819a391 100644 --- a/mobile-oa-h5/src/market/modules/payment.js +++ b/mobile-oa-h5/src/market/modules/payment.js @@ -41,36 +41,42 @@ function displayDate(value) { function transferCard(transfer, index) { const statusText = label(PAY_STATUS, transfer?.payStatus) + const transferType = label(TRANSFER_TYPE, transfer?.transferType) + const transferTime = transfer?.transferTime || '—' return { key: String(transfer?.id || `transfer-${index}`), title: `转账笔 ${index + 1}`, + variant: 'transfer', + transferIndex: index + 1, + amount: money(transfer?.transferAmount), + meta: `${transferType} · ${transferTime}`, statusText, statusTone: statusText.includes('确认中') ? 'warning' : statusText === '付款完成' ? 'success' : 'info', fields: [ - { label: '转账方式', value: label(TRANSFER_TYPE, transfer?.transferType) }, - { label: '转账金额', value: money(transfer?.transferAmount) }, - { label: '转账时间', value: transfer?.transferTime || '—' }, - { label: '转账凭证', type: 'attachments', value: transfer?.transferAttachments || '' }, { label: '备注', value: transfer?.remark || '—' }, - { label: '财务备注', value: transfer?.financeRemark || '—' } + { label: '财务备注', value: transfer?.financeRemark || '—' }, + { label: '转账凭证', type: 'attachments', value: transfer?.transferAttachments || '' } ] } } function scheduleCard(schedule, index) { const statusText = label(SCHEDULE_STATUS, schedule?.scheduleStatus) + const transfers = schedule?.transfers || [] return { key: String(schedule?.id || `schedule-${index}`), title: `第 ${schedule?.scheduleNo || index + 1} 期`, + variant: 'schedule', + cardsLabel: '转账记录', statusText, statusTone: statusText === '已缴清' ? 'success' : statusText === '部分到账' ? 'primary' : 'warning', fields: [ - { label: '应缴金额', value: money(schedule?.dueAmount) }, + { label: '应缴金额', value: money(schedule?.dueAmount), money: true }, { label: '应缴日', value: displayDate(schedule?.dueDate) }, - { label: '已缴金额', value: money(schedule?.paidAmount) }, - { label: '欠缴金额', value: money(schedule?.arrearsAmount) } + { label: '已缴金额', value: money(schedule?.paidAmount), money: true }, + { label: '欠缴金额', value: money(schedule?.arrearsAmount), money: true } ], - cards: (schedule?.transfers || []).map(transferCard) + cards: transfers.map(transferCard) } } @@ -83,9 +89,13 @@ export default { }, listParams: ({ status } = {}) => ({ payMode: status || '' }), statusOptions: [{ text: '全部缴费模式', value: '' }], - filterFields: [ + // 缴费列表用搜索下方 van-dropdown-menu(对齐预签约/运费),不走筛选弹层 + filterFields: [], + inlineDropdownFilters: [ { - key: 'payMode', label: '缴费方式', type: 'select', options: [ + key: 'payMode', + allText: '全部缴费方式', + options: [ { text: '趸交', value: 'dj' }, { text: '分期', value: 'fq' }, { text: '分改趸', value: 'fgd' }, @@ -93,13 +103,14 @@ export default { ] }, { - key: 'progressStatus', label: '缴费进度', type: 'select', options: [ + key: 'progressStatus', + allText: '全部缴费进度', + options: [ { text: '待缴清', value: 'dfj' }, { text: '部分已缴', value: 'bfyj' }, { text: '已缴清', value: 'yjq' } ] - }, - { key: 'dueDateRange', label: '应缴费时间', type: 'date-range' } + } ], detailSections(detail) { const progress = label(PAYMENT_PROGRESS, detail?.progressStatus || detail?.paymentProgress) diff --git a/mobile-oa-h5/src/market/modules/protocol.js b/mobile-oa-h5/src/market/modules/protocol.js index 99ed67e0..0d92225b 100644 --- a/mobile-oa-h5/src/market/modules/protocol.js +++ b/mobile-oa-h5/src/market/modules/protocol.js @@ -25,6 +25,12 @@ const PAYMENT_STATUS_LABELS = Object.freeze({ dfj: '已付款', jfqrz: '付款确认中', yjf: '付款完成', tfqrz: '退费确认中', ytf: '已退费' }) +const TRANSFER_TYPE_LABELS = Object.freeze({ + wxzz: '微信转账', wx: '微信转账', wechat: '微信转账', + zzbzz: '支付宝转账', zfb: '支付宝转账', alipay: '支付宝转账', + yhkzz: '银行卡转账', bank: '银行卡转账', + xj: '现金', cash: '现金', lszz: '乐刷' +}) const SCHEDULE_STATUS_LABELS = Object.freeze({ open: '待缴清', cleared: '已缴清', void: '已作废' }) function value(source, key) { @@ -79,7 +85,11 @@ function transferAttachmentValue(transfer) { } function transferTypeText(transfer) { - return String(transfer?.transferTypeLabel || transfer?.transferType || '').trim() || '—' + 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) { @@ -293,33 +303,40 @@ function sampleLifecycleCards(item, source) { return cards } +function paymentTransferCard(transfer, transferIndex) { + const statusText = label(PAYMENT_STATUS_LABELS, transfer?.payStatus) + const transferType = transferTypeText(transfer) + const transferTime = value(transfer, 'transferTime') + return { + key: String(transfer?.id || `transfer-${transferIndex}`), + title: `转账 ${transferIndex + 1}`, + variant: 'transfer', + transferIndex: transferIndex + 1, + amount: value(transfer, 'transferAmount'), + meta: `${transferType} · ${transferTime}`, + statusText, + fields: [ + { label: '备注', value: value(transfer, 'remark') }, + { label: '财务备注', value: value(transfer, 'financeRemark') }, + { label: '转账凭证', value: transferAttachmentValue(transfer), type: 'attachments' } + ] + } +} + function paymentScheduleCards(pay) { return (pay?.schedules || []).map((schedule, scheduleIndex) => ({ key: String(schedule?.id || `schedule-${scheduleIndex}`), title: `第 ${schedule?.scheduleNo || scheduleIndex + 1} 期`, + variant: 'schedule', + cardsLabel: '转账记录', statusText: label(SCHEDULE_STATUS_LABELS, schedule?.scheduleStatus), fields: [ - { label: '期次', value: value(schedule, 'scheduleNo') }, - { label: '应缴金额', value: value(schedule, 'dueAmount') }, - { label: '实缴金额', value: value(schedule, 'paidAmount') }, - { label: '欠缴金额', value: value(schedule, 'arrearsAmount') }, + { label: '应缴金额', value: value(schedule, 'dueAmount'), money: true }, { label: '应缴日期', value: value(schedule, 'dueDate') }, - { label: '状态', value: label(SCHEDULE_STATUS_LABELS, schedule?.scheduleStatus), tag: true } + { label: '实缴金额', value: value(schedule, 'paidAmount'), money: true }, + { label: '欠缴金额', value: value(schedule, 'arrearsAmount'), money: true } ], - cards: (schedule?.transfers || []).map((transfer, transferIndex) => ({ - key: String(transfer?.id || `transfer-${transferIndex}`), - title: `转账 ${transferIndex + 1}`, - statusText: label(PAYMENT_STATUS_LABELS, transfer?.payStatus), - fields: [ - { label: '转账方式', value: transferTypeText(transfer) }, - { label: '转账金额', value: value(transfer, 'transferAmount') }, - { label: '转账时间', value: value(transfer, 'transferTime') }, - { label: '转账凭证', value: transferAttachmentValue(transfer), type: 'attachments' }, - { label: '备注', value: value(transfer, 'remark') }, - { label: '财务备注', value: value(transfer, 'financeRemark') }, - { label: '状态', value: label(PAYMENT_STATUS_LABELS, transfer?.payStatus), tag: true } - ] - })) + cards: (schedule?.transfers || []).map(paymentTransferCard) })) } @@ -327,29 +344,21 @@ function legacyPaymentDetailCards(payment, details) { return groupLegacyPaymentPeriods({ payment, details }).map((period, index) => ({ key: `legacy-payment-${period.scheduleNo || index + 1}`, title: `第 ${period.scheduleNo || index + 1} 期`, + variant: 'schedule', + cardsLabel: '转账记录', statusText: label(SCHEDULE_STATUS_LABELS, period.scheduleStatus), fields: [ - { label: '期次', value: String(period.scheduleNo || index + 1) }, - { label: '应缴金额', value: period.dueAmount == null ? '—' : String(period.dueAmount) }, - { label: '确认实缴金额', value: String(period.paidAmount) }, - { label: '欠缴金额', value: period.arrearsAmount == null ? '—' : String(period.arrearsAmount) }, + { label: '应缴金额', value: period.dueAmount == null ? '—' : String(period.dueAmount), money: true }, { label: '应缴日期', value: period.dueDate || '—' }, - { label: '期状态', value: label(SCHEDULE_STATUS_LABELS, period.scheduleStatus), tag: true } + { label: '确认实缴金额', value: String(period.paidAmount), money: true }, + { label: '欠缴金额', value: period.arrearsAmount == null ? '—' : String(period.arrearsAmount), money: true } ], - cards: period.transfers.map((detail, transferIndex) => ({ - key: String(detail?.id || `legacy-transfer-${period.scheduleNo}-${transferIndex}`), - title: `转账 ${transferIndex + 1}`, - statusText: label(PAYMENT_STATUS_LABELS, detail?.payStatus), - fields: [ - { label: '转账方式', value: transferTypeText(detail) }, - { label: '转账金额', value: value(detail, 'transferAmount') }, - { label: '转账时间', value: value(detail, 'transferTime') }, - { label: '转账凭证', value: transferAttachmentValue(detail), type: 'attachments' }, - { label: '备注', value: value(detail, 'remark') }, - { label: '财务备注', value: value(detail, 'financeRemark') }, - { label: '状态', value: label(PAYMENT_STATUS_LABELS, detail?.payStatus), tag: true } - ] - })) + cards: period.transfers.map((detail, transferIndex) => + paymentTransferCard( + { ...detail, id: detail?.id || `legacy-transfer-${period.scheduleNo}-${transferIndex}` }, + transferIndex + ) + ) })) } 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 a24c146c..066b2f87 100644 --- a/mobile-oa-h5/src/market/protocol-mobile-parity.test.js +++ b/mobile-oa-h5/src/market/protocol-mobile-parity.test.js @@ -138,13 +138,15 @@ test('v1 protocol detail renders readonly payment details with backend transfer const payment = sections.find((section) => section.key === 'payment') assert.equal(payment.cards.length, 1) assert.equal(payment.cards[0].cards.length, 2) + assert.equal(payment.cards[0].variant, 'schedule') assert.equal(payment.cards[0].fields.find((field) => field.label === '确认实缴金额').value, '600.00') assert.equal(payment.cards[0].fields.find((field) => field.label === '欠缴金额').value, '400.00') - assert.equal(payment.cards[0].cards[0].fields.find((field) => field.label === '转账方式').value, '微信支付') - for (const expected of ['期次', '应缴金额', '确认实缴金额', '欠缴金额', '应缴日期', '期状态']) { + assert.match(payment.cards[0].cards[0].meta, /微信支付/) + assert.equal(payment.cards[0].cards[0].amount, '600') + for (const expected of ['应缴金额', '确认实缴金额', '欠缴金额', '应缴日期']) { assert.ok(payment.cards[0].fields.some((field) => field.label === expected), `missing ${expected}`) } - for (const expected of ['转账方式', '转账金额', '转账时间', '转账凭证', '备注', '财务备注', '状态']) { + for (const expected of ['转账凭证', '备注', '财务备注']) { assert.ok(payment.cards[0].cards[0].fields.some((field) => field.label === expected), `missing transfer ${expected}`) } }) @@ -206,11 +208,15 @@ test('protocol detail exposes complete deposit and installment transfer fields', ]) const payment = sections.find((section) => section.key === 'payment').cards[0] const schedule = payment.cards[0] + assert.equal(schedule.variant, 'schedule') assert.deepEqual(schedule.fields.map((field) => field.label), [ - '期次', '应缴金额', '实缴金额', '欠缴金额', '应缴日期', '状态' + '应缴金额', '应缴日期', '实缴金额', '欠缴金额' ]) + assert.equal(schedule.cards[0].variant, 'transfer') + assert.equal(schedule.cards[0].amount, '3000') + assert.match(schedule.cards[0].meta, /微信/) assert.deepEqual(schedule.cards[0].fields.map((field) => field.label), [ - '转账方式', '转账金额', '转账时间', '转账凭证', '备注', '财务备注', '状态' + '备注', '财务备注', '转账凭证' ]) }) @@ -230,16 +236,21 @@ test('deposit and installment transfers prefer backend dictionary labels and fal const deposit = sections.find((section) => section.key === 'deposit').cards[0] assert.equal(deposit.fields.find((field) => field.label === '转账方式').value, '微信支付') const transfers = sections.find((section) => section.key === 'payment').cards[0].cards[0].cards - assert.equal(transfers[0].fields.find((field) => field.label === '转账方式').value, '支付宝') - assert.equal(transfers[1].fields.find((field) => field.label === '转账方式').value, 'unknown-code') + assert.match(transfers[0].meta, /^支付宝 ·/) + assert.match(transfers[1].meta, /^unknown-code ·/) }) test('detail renderer recursively renders transfer and task subcards', () => { const section = source('./components/MarketDetailSection.vue') const nested = source('./components/MarketDetailNestedCard.vue') + const styles = source('../styles/market.css') assert.match(section, / { @@ -275,7 +286,8 @@ test('protocol uploads use the Web 20MB rich attachment policy', () => { test('protocol list and nested payment actions keep 44px mobile touch targets', () => { const styles = source('../styles/market.css') const panel = source('./components/ProtocolSamplePaymentPanel.vue') - assert.match(styles, /\.market-list-card__actions \.van-button\s*\{[^}]*min-height:\s*44px;/) + 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 8px;/) assert.match(panel, /\.protocol-sample-pay__transfer-actions[\s\S]*?\.van-button\s*\{[^}]*min-height:\s*44px;/) }) diff --git a/mobile-oa-h5/src/styles/market.css b/mobile-oa-h5/src/styles/market.css index 7e58bf17..81c5191c 100644 --- a/mobile-oa-h5/src/styles/market.css +++ b/mobile-oa-h5/src/styles/market.css @@ -818,6 +818,143 @@ body.van-toast--unclickable .presign-order-page * { font-size: 13px; } +.market-detail-section__nested-card.is-schedule { + display: block; + padding: 0; + overflow: hidden; + border: 1px solid #eef4f1; + background: #fafcfb; +} + +.market-detail-section__nested-card.is-transfer { + display: block; + padding: 12px; + border: 1px solid var(--market-border); + border-radius: 10px; + background: #fff; + box-shadow: 0 1px 0 rgb(8 127 87 / 4%); +} + +.market-detail-section__schedule-head { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + background: var(--market-primary-soft); +} + +.market-detail-section__schedule-head strong { + flex: 1; + font-size: 14px; +} + +.market-detail-section__schedule-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 14px; + padding: 12px; + border-bottom: 1px dashed #eef4f1; +} + +.market-detail-section__transfer-list { + display: grid; + gap: 8px; + padding: 10px; +} + +.market-detail-section__transfer-list-label { + color: var(--market-muted); + font-size: 12px; + font-weight: 600; +} + +.market-detail-section__transfer-empty { + margin: 0; + padding: 4px 2px 2px; + color: var(--market-muted); + font-size: 12px; +} + +.market-detail-section__transfer-head { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #eef4f1; +} + +.market-detail-section__transfer-index { + display: grid; + width: 22px; + height: 22px; + flex: 0 0 22px; + place-items: center; + border-radius: 6px; + background: var(--market-primary-soft); + color: var(--market-primary-dark); + font-size: 11px; + font-weight: 700; +} + +.market-detail-section__transfer-main { + min-width: 0; + flex: 1; +} + +.market-detail-section__transfer-amount { + display: block; + color: #c98312; + font-size: 17px; + font-weight: 700; + line-height: 1.2; +} + +.market-detail-section__transfer-meta { + display: block; + margin-top: 4px; + color: var(--market-muted); + font-size: 12px; +} + +.market-detail-section__transfer-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 12px; +} + +.market-detail-section__transfer-metric { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; +} + +.market-detail-section__transfer-metric.is-wide { + grid-column: 1 / -1; +} + +.market-detail-section__transfer-metric > span { + color: var(--market-muted); + font-size: 11px; +} + +.market-detail-section__transfer-metric > b { + font-weight: 600; + overflow-wrap: anywhere; +} + +.market-detail-section__transfer-metric > b.is-money { + color: #c98312; + font-weight: 700; +} + +.market-detail-section__transfer-voucher { + margin-top: 8px; + padding-top: 8px; + border-top: 1px dashed #eef4f1; +} + .market-detail-section__nested-field { display: flex; grid-column: 1 / -1;