|
|
@@ -0,0 +1,471 @@
|
|
|
+# Payment List Card Period Collapse Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 市场【缴费信息】列表卡片按期折叠展示,默认只显示「第几期 / 应缴 / 已缴」,点击可独立展开明细。
|
|
|
+
|
|
|
+**Architecture:** 对齐运费 `sampleBlocks`:`paymentCard` 产出 `periodBlocks` + `footerFields`(累计实缴等放在期次区之后);`MarketListCard` 渲染每期独立折叠行;样式对齐 Demo。不改后端与催缴模块。
|
|
|
+
|
|
|
+**Tech Stack:** Vue 3, Vant 4, CSS, Node.js `node:test`
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-10-14-payment-card-period-collapse-design.md`
|
|
|
+**Demo:** `docs/demos/2026-10-14-payment-card-period-collapse-demo.html`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| File | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| `mobile-oa-h5/src/market/list-presenter.js` | `isPaymentPeriodPresent` + `paymentPeriodBlock` + rewrite `paymentCard` |
|
|
|
+| `mobile-oa-h5/src/market/list-presenter.test.js` | presenter 行为断言 |
|
|
|
+| `mobile-oa-h5/src/market/components/MarketListCard.vue` | 期次折叠 UI + `footerFields` |
|
|
|
+| `mobile-oa-h5/src/styles/market.css` | `.market-list-card__periods*` 样式 |
|
|
|
+| `mobile-oa-h5/src/market/mobile-parity-ui.test.js` | 结构 / 默认折叠 / 44px 触控 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: Presenter — failing tests for `periodBlocks`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-oa-h5/src/market/list-presenter.test.js`
|
|
|
+- Test: `mobile-oa-h5/src/market/list-presenter.test.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: Update existing payment card test and add period-block cases**
|
|
|
+
|
|
|
+Replace the assertions in `test('payment card formats summary totals and progress', …)` that expect flat period fields, and add a focused test. Keep the existing fixture; change expectations as follows:
|
|
|
+
|
|
|
+```js
|
|
|
+test('payment card formats summary totals and progress', () => {
|
|
|
+ const card = buildMarketCard('payment', 'manager', {
|
|
|
+ protocolNo: 'P-1', motherName: '周雨桐', sampleCode: 'S-1', payMode: 'fq',
|
|
|
+ contractAmount: '10000.00', paidAmount: '3000.00', remainAmount: '7000.00',
|
|
|
+ signDate: '2026-10-01', storageYears: 20, sampleTypeName: '脐带血',
|
|
|
+ discountAmount: '1000.00', receivableAmount: '9000.00', remark: '首期已缴',
|
|
|
+ periods: [{ scheduleNo: 1, dueAmount: 3000, dueDate: '2026-10-10', displayPaidAmount: 3000, payDate: '2026-10-11', arrearsAmount: 0 }],
|
|
|
+ clearedScheduleCount: 1, activeScheduleCount: 4, paymentProgress: '部分已缴',
|
|
|
+ allowedActions: ['register-transfer']
|
|
|
+ })
|
|
|
+ assert.equal(card.status, '部分已缴')
|
|
|
+ assert.equal(card.subtitle, 'S-1')
|
|
|
+ assert.ok(card.fields.some((field) => field.label === '样本编码' && field.value === 'S-1'))
|
|
|
+ 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) => 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)))
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run tests and verify RED**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd mobile-oa-h5 && node --test --test-name-pattern="payment card" src/market/list-presenter.test.js
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL — `periodBlocks` / `footerFields` undefined, or flat `第1期·` fields still present.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Presenter — implement `periodBlocks` + `footerFields`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-oa-h5/src/market/list-presenter.js` (`paymentCard` and helpers near it)
|
|
|
+
|
|
|
+- [ ] **Step 1: Add helpers and rewrite `paymentCard`**
|
|
|
+
|
|
|
+Insert before `paymentCard` (after `chaseCard`):
|
|
|
+
|
|
|
+```js
|
|
|
+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.isArray(item.periods) ? item.periods : []).filter(isPaymentPeriodPresent)
|
|
|
+ return {
|
|
|
+ title: text(item.motherName),
|
|
|
+ subtitle: text(item.sampleCode),
|
|
|
+ status: progress,
|
|
|
+ fields: [
|
|
|
+ field('协议号', item.protocolNo, { wide: true }),
|
|
|
+ field('姓名', item.motherName), field('手机号', item.motherPhone),
|
|
|
+ field('样本编码', item.sampleCode, { wide: true }), field('签署日期', item.signDate),
|
|
|
+ field('存储年限', item.storageYears), field('存储项目', item.sampleTypeName, { wide: true }),
|
|
|
+ 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 })
|
|
|
+ ],
|
|
|
+ periodBlocks: periods.map(paymentPeriodBlock),
|
|
|
+ footerFields: [
|
|
|
+ field('累计实缴', money(item.paidAmount), { money: true }),
|
|
|
+ field('待缴金额', money(item.dueRemainAmount ?? item.remainAmount), { money: true }),
|
|
|
+ field('备注', item.remark, { wide: true })
|
|
|
+ ],
|
|
|
+ actions: ['view-protocol', 'view-payment-detail']
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Remove the old `Array.from({ length: 6 }, …)` + `periodFields` flatMap implementation entirely.
|
|
|
+
|
|
|
+- [ ] **Step 2: Run tests and verify GREEN**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd mobile-oa-h5 && node --test --test-name-pattern="payment card" src/market/list-presenter.test.js
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add mobile-oa-h5/src/market/list-presenter.js mobile-oa-h5/src/market/list-presenter.test.js
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(mobile-oa-h5): present payment periods as collapsible periodBlocks
|
|
|
+
|
|
|
+Stop flattening six installment field groups into list cards; emit periodBlocks and footerFields instead.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: UI parity tests for period collapse (RED)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-oa-h5/src/market/mobile-parity-ui.test.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add parity test**
|
|
|
+
|
|
|
+```js
|
|
|
+test('payment list cards collapse period blocks by default and allow independent expand', () => {
|
|
|
+ const card = source('./components/MarketListCard.vue')
|
|
|
+ const presenter = source('./list-presenter.js')
|
|
|
+ const styles = source('../styles/market.css')
|
|
|
+
|
|
|
+ assert.match(presenter, /periodBlocks:\s*periods\.map\(paymentPeriodBlock\)/)
|
|
|
+ assert.match(presenter, /footerFields:/)
|
|
|
+ assert.match(card, /card\.periodBlocks/)
|
|
|
+ assert.match(card, /card\.footerFields/)
|
|
|
+ assert.match(card, /periodOpenKeys/)
|
|
|
+ assert.match(card, /aria-expanded/)
|
|
|
+ assert.match(card, /应缴费金额/)
|
|
|
+ assert.match(card, /已缴费金额/)
|
|
|
+ assert.match(styles, /\.market-list-card__periods\s*\{/)
|
|
|
+ assert.match(styles, /\.market-list-card__period-toggle\s*\{[\s\S]*?min-height:\s*44px;/)
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run and verify RED**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd mobile-oa-h5 && node --test --test-name-pattern="payment list cards collapse period" src/market/mobile-parity-ui.test.js
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL — template/CSS not yet present.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: MarketListCard — render periods + footerFields
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-oa-h5/src/market/components/MarketListCard.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Insert period section and footer fields in template**
|
|
|
+
|
|
|
+After the existing `card.fields` block and **before** the `sampleBlocks` section, insert:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div v-if="card.periodBlocks?.length" class="market-list-card__periods">
|
|
|
+ <div
|
|
|
+ v-for="block in card.periodBlocks"
|
|
|
+ :key="block.key"
|
|
|
+ class="market-list-card__period"
|
|
|
+ :class="{ 'is-open': periodOpenKeys[block.key] }"
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="market-list-card__period-toggle"
|
|
|
+ :aria-expanded="periodOpenKeys[block.key] ? 'true' : 'false'"
|
|
|
+ :aria-label="periodOpenKeys[block.key] ? `收起第${block.scheduleNo}期` : `展开第${block.scheduleNo}期`"
|
|
|
+ @click="togglePeriod(block.key)"
|
|
|
+ >
|
|
|
+ <span class="market-list-card__period-no">第{{ block.scheduleNo }}期</span>
|
|
|
+ <span class="market-list-card__period-col">
|
|
|
+ <span>应缴费金额</span>
|
|
|
+ <b class="is-money">{{ block.dueAmount }}</b>
|
|
|
+ </span>
|
|
|
+ <span class="market-list-card__period-col">
|
|
|
+ <span>已缴费金额</span>
|
|
|
+ <b class="is-money">{{ block.paidAmount }}</b>
|
|
|
+ </span>
|
|
|
+ <van-icon class="market-list-card__period-chevron" name="arrow-down" />
|
|
|
+ </button>
|
|
|
+ <div v-show="periodOpenKeys[block.key]" class="market-list-card__period-body">
|
|
|
+ <div class="market-list-card__fields market-list-card__fields--compact">
|
|
|
+ <div
|
|
|
+ v-for="field in block.fields"
|
|
|
+ :key="`${block.key}:${field.label}`"
|
|
|
+ class="market-list-card__field"
|
|
|
+ :class="{ 'is-wide': field.wide }"
|
|
|
+ >
|
|
|
+ <span>{{ field.label }}</span>
|
|
|
+ <b :class="{ 'is-money': field.money }">{{ field.value }}</b>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="card.footerFields?.length" class="market-list-card__fields">
|
|
|
+ <div
|
|
|
+ v-for="field in card.footerFields"
|
|
|
+ :key="`footer:${field.label}:${field.value}`"
|
|
|
+ class="market-list-card__field"
|
|
|
+ :class="{ 'is-wide': field.wide }"
|
|
|
+ >
|
|
|
+ <span>{{ field.label }}</span>
|
|
|
+ <van-tag v-if="field.tag" class="market-tag" :type="resolveStatusTone(field.value)" size="medium" round :title="field.value">{{ field.value }}</van-tag>
|
|
|
+ <b v-else :class="{ 'is-money': field.money }">{{ field.value }}</b>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Add script state**
|
|
|
+
|
|
|
+Alongside `samplesOpen`, add:
|
|
|
+
|
|
|
+```js
|
|
|
+const periodOpenKeys = ref({})
|
|
|
+
|
|
|
+function togglePeriod(key) {
|
|
|
+ periodOpenKeys.value = {
|
|
|
+ ...periodOpenKeys.value,
|
|
|
+ [key]: !periodOpenKeys.value[key]
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.item?.id || props.index,
|
|
|
+ () => {
|
|
|
+ samplesOpen.value = false
|
|
|
+ periodOpenKeys.value = {}
|
|
|
+ }
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+Merge with the existing `samplesOpen` watch — keep a **single** watch that resets both `samplesOpen` and `periodOpenKeys`. Delete the old watch that only cleared `samplesOpen`.
|
|
|
+
|
|
|
+- [ ] **Step 3: Do not run full GREEN yet** — CSS still missing; proceed to Task 5.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: CSS for period rows
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-oa-h5/src/styles/market.css`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add styles after `.market-list-card__fields--compact` block**
|
|
|
+
|
|
|
+```css
|
|
|
+.market-list-card__periods {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #eef4f1;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #fafcfb;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period + .market-list-card__period {
|
|
|
+ border-top: 1px solid #eef4f1;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-toggle {
|
|
|
+ display: grid;
|
|
|
+ width: 100%;
|
|
|
+ min-height: 44px;
|
|
|
+ grid-template-columns: 64px 1fr 1fr 20px;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border: 0;
|
|
|
+ background: transparent;
|
|
|
+ color: var(--market-text);
|
|
|
+ font: inherit;
|
|
|
+ text-align: left;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-toggle:active {
|
|
|
+ background: #f0f7f3;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-no {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--market-primary-dark);
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-col {
|
|
|
+ display: flex;
|
|
|
+ min-width: 0;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-col > span {
|
|
|
+ color: var(--market-muted);
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-col > b {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #c98312;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-chevron {
|
|
|
+ color: var(--market-muted);
|
|
|
+ font-size: 14px;
|
|
|
+ transition: transform 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period.is-open .market-list-card__period-chevron {
|
|
|
+ transform: rotate(180deg);
|
|
|
+}
|
|
|
+
|
|
|
+.market-list-card__period-body {
|
|
|
+ padding: 0 12px 12px;
|
|
|
+ border-top: 1px dashed #eef4f1;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Align with Demo; use existing token vars where already defined (`--market-text`, `--market-muted`, `--market-primary-dark`). Money color `#c98312` matches `.market-list-card__field b.is-money`.
|
|
|
+
|
|
|
+- [ ] **Step 2: Run parity + presenter tests GREEN**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd mobile-oa-h5 && node --test --test-name-pattern="payment card|payment list cards collapse period" src/market/list-presenter.test.js src/market/mobile-parity-ui.test.js
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add mobile-oa-h5/src/market/components/MarketListCard.vue mobile-oa-h5/src/styles/market.css mobile-oa-h5/src/market/mobile-parity-ui.test.js
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(mobile-oa-h5): collapse payment installment rows on list cards
|
|
|
+
|
|
|
+Render periodBlocks as independent expandable summary rows with footer totals after the period section.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: Full market suite + build verification
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Verify only
|
|
|
+
|
|
|
+- [ ] **Step 1: Run all market tests**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd mobile-oa-h5 && node --test src/market/*.test.js
|
|
|
+```
|
|
|
+
|
|
|
+Expected: zero failures.
|
|
|
+
|
|
|
+- [ ] **Step 2: Production build**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd mobile-oa-h5 && pnpm build
|
|
|
+```
|
|
|
+
|
|
|
+Expected: Vite exit 0.
|
|
|
+
|
|
|
+- [ ] **Step 3: Manual smoke (optional but recommended)**
|
|
|
+
|
|
|
+Open市场专员 / 地区经理【缴费信息】列表:卡片默认矮;点第 N 期展开三明细;切换列表项后折叠状态重置;无数据期次不出现。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage checklist
|
|
|
+
|
|
|
+| Spec requirement | Task |
|
|
|
+|------------------|------|
|
|
|
+| 折叠行:期数 / 应缴 / 已缴 | Task 4 |
|
|
|
+| 展开:时间 + 欠缴 | Task 2 + 4 |
|
|
|
+| 默认折叠、独立多开 | Task 4 (`periodOpenKeys`) |
|
|
|
+| 不 pad 到 6、跳过空期 | Task 1–2 |
|
|
|
+| `footerFields` 在期次后 | Task 2 + 4 |
|
|
|
+| 仅 payment 列表 | Task 2(仅改 `paymentCard`) |
|
|
|
+| 44px 触控 | Task 5 |
|
|
|
+| 测试 | Task 1, 3, 6 |
|