2026-10-11-market-attachment-parity-implementation.md 10 KB

Market Attachment Parity 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: Remove the floating round back button from every market detail page and render market attachments with the workflow-standard thumbnail, file-icon, download, and editable-delete experience.

Architecture: Add a pure attachment normalizer and one shared market attachment-card component. Form fields and presign uploads retain their current upload APIs but delegate display, preview, download, and remove interactions to the shared component; detail adapters expose typed attachment fields consumed by MarketDetailSection.vue.

Tech Stack: Vue 3 Composition API, Vant 4, native Node test runner, Vite 6.


File map

  • Create mobile-oa-h5/src/market/attachment-presenter.js: normalize supported attachment shapes, derive safe display names, image classification, and workflow file kinds.
  • Create mobile-oa-h5/src/market/attachment-presenter.test.js: unit coverage for strings, arrays, object metadata, image detection, and file kinds.
  • Create mobile-oa-h5/src/market/components/MarketAttachmentCards.vue: workflow-standard 84×84 rendering, upload trigger, image preview, file download, and editable remove event.
  • Modify mobile-oa-h5/src/market/components/MarketFormField.vue: replace raw URL anchors with the shared component while preserving comma-separated form values.
  • Modify mobile-oa-h5/src/market/components/PresignAttachmentUploader.vue: reuse the shared renderer and preserve existing upload/remove events.
  • Modify mobile-oa-h5/src/market/components/MarketDetailSection.vue: support type: 'attachments' in top-level and nested detail fields.
  • Modify mobile-oa-h5/src/market/modules/protocol.js: expose protocol attachment values as typed attachments instead of joined path text.
  • Modify mobile-oa-h5/src/market/modules/payment.js: expose transfer vouchers as typed attachments.
  • Modify mobile-oa-h5/src/market/modules/sample.js: expose handling attachments as typed attachments.
  • Modify mobile-oa-h5/src/market/modules/presign.js: expose presign transfer attachments as typed attachments.
  • Modify mobile-oa-h5/src/views/market/MarketModuleDetail.vue: disable the detail-page back control.
  • Modify mobile-oa-h5/src/market/mobile-parity-ui.test.js: lock the visual and detail attachment contracts.
  • Modify mobile-oa-h5/src/market/module-contracts.test.js: lock typed attachment fields across adapters.

Task 1: Pure attachment normalization

Files:

  • Create: mobile-oa-h5/src/market/attachment-presenter.js
  • Create: mobile-oa-h5/src/market/attachment-presenter.test.js

  • [ ] Step 1: Write failing unit tests

Cover these public functions:

normalizeMarketAttachments('https://cdn/a%20b.pdf,https://cdn/photo.png')
// => [{ url: '...', name: 'a b.pdf', isImage: false, kind: 'pdf' }, ...]

normalizeMarketAttachments([
  { fileUrl: '/minio/x/report.xlsx', fileName: '费用明细.xlsx' }
])
// preserves fileName and raw URL

marketAttachmentKind('archive.zip') // => 'zip'
isMarketAttachmentImage({ name: 'photo.webp' }) // => true
  • Step 2: Run the focused test and verify RED

Run:

cd mobile-oa-h5
node --test src/market/attachment-presenter.test.js

Expected: FAIL because attachment-presenter.js does not exist.

  • Step 3: Implement the minimal pure utility

Implement safe URL decoding, comma splitting, array/object normalization, these image extensions:

const IMAGE_RE = /\.(?:jpe?g|png|gif|bmp|webp|svg|jfif|avif|heic|heif)(?:\?|$)/i

and these file kinds:

pdf: pdf
xls: xls, xlsx, csv
doc: doc, docx, ppt, pptx
zip: zip, rar, 7z
other: every remaining file
  • Step 4: Run the focused test and verify GREEN

Run the command from Step 2. Expected: all tests pass.

Task 2: Shared workflow-standard attachment cards

Files:

  • Create: mobile-oa-h5/src/market/components/MarketAttachmentCards.vue
  • Modify: mobile-oa-h5/src/market/mobile-parity-ui.test.js

  • [ ] Step 1: Write failing component-contract assertions

Assert the component contains:

assert.match(source, /normalizeMarketAttachments/)
assert.match(source, /downloadAttachmentFile/)
assert.match(source, /showImagePreview/)
assert.match(source, /market-attachment-card__delete/)
assert.match(source, /@error=/)
assert.match(source, /84px/)

Also assert file-icon kinds ft-pdf, ft-xls, ft-doc, ft-zip, and ft-other exist.

  • Step 2: Run the contract test and verify RED
cd mobile-oa-h5
node --test src/market/mobile-parity-ui.test.js

Expected: FAIL because the component does not exist.

  • Step 3: Implement the shared component

Required props and events:

defineProps({
  modelValue: { type: [Array, String, Object], default: '' },
  editable: { type: Boolean, default: false },
  uploading: { type: Boolean, default: false },
  maxSize: { type: Number, default: MARKET_ATTACHMENT_MAX_BYTES },
  accept: { type: String, default: MARKET_ATTACHMENT_ACCEPT }
})
defineEmits(['upload', 'remove'])

Use van-uploader for the first add card. Match backend validation: 10 MiB by default, 20 MiB for presign, and only JPG/JPEG, PNG, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX. Use image preview through showImagePreview, and non-image download through downloadAttachmentFile. Image error changes that card to the other file icon. Copy the approved 84×84 workflow styling and keep a visually small delete button with an unclipped 44×44px interactive hit area. When a name was inferred from a URL, omit fileName from the download request so the backend restores the stored original name.

  • Step 4: Run the contract test and verify GREEN

Run the command from Step 2. Expected: all assertions pass.

Task 3: Form integration and editable deletion

Files:

  • Modify: mobile-oa-h5/src/market/components/MarketFormField.vue
  • Modify: mobile-oa-h5/src/market/components/PresignAttachmentUploader.vue
  • Modify: mobile-oa-h5/src/market/mobile-parity-ui.test.js

  • [ ] Step 1: Add failing integration contracts

Assert both form components import and render MarketAttachmentCards. Assert MarketFormField.vue updates a comma-separated URL list on remove, and PresignAttachmentUploader.vue forwards upload and remove events.

  • Step 2: Run the focused test and verify RED
cd mobile-oa-h5
node --test src/market/mobile-parity-ui.test.js

Expected: FAIL because both components still use their separate raw-card implementations.

  • Step 3: Integrate the shared component

For generic fields:

<MarketAttachmentCards
  :model-value="modelValue"
  :editable="!disabled && !field.readOnly"
  :uploading="uploading"
  @upload="$emit('upload', $event)"
  @remove="removeAttachment"
/>

removeAttachment(index) must normalize the current value, remove the selected item, then emit a comma-separated URL string. For presign attachments, preserve the current array value and existing parent-owned remove(index) behavior.

  • Step 4: Run the focused test and verify GREEN

Run the command from Step 2. Expected: all assertions pass.

Task 4: Read-only detail attachments and back-button removal

Files:

  • Modify: mobile-oa-h5/src/market/components/MarketDetailSection.vue
  • Modify: mobile-oa-h5/src/market/modules/protocol.js
  • Modify: mobile-oa-h5/src/market/modules/payment.js
  • Modify: mobile-oa-h5/src/market/modules/sample.js
  • Modify: mobile-oa-h5/src/market/modules/presign.js
  • Modify: mobile-oa-h5/src/views/market/MarketModuleDetail.vue
  • Modify: mobile-oa-h5/src/market/module-contracts.test.js
  • Modify: mobile-oa-h5/src/market/mobile-parity-ui.test.js

  • [ ] Step 1: Write failing detail contracts

Assert:

assert.match(detailView, /:backable="false"/)
assert.doesNotMatch(detailView, /@back="backToList"/)

For each adapter, build representative details and verify every attachment-bearing field uses:

{ label: '...', type: 'attachments', value: originalAttachmentValue }

Also assert MarketDetailSection.vue renders MarketAttachmentCards with :editable="false" for top-level, nested, and subcard fields.

  • Step 2: Run both focused tests and verify RED
cd mobile-oa-h5
node --test src/market/mobile-parity-ui.test.js src/market/module-contracts.test.js

Expected: FAIL because detail fields still display attachment paths and the detail view still enables the floating back button.

  • Step 3: Implement typed attachment detail fields

Preserve the original attachment value in every adapter and add type: 'attachments'. Extend normalizeCard() and top-level field normalization in MarketDetailSection.vue to retain the type. Render the shared component when the type matches, otherwise keep the existing tag/text behavior.

Change the detail shell to:

<MarketPageShell
  :title="moduleTitle"
  :eyebrow="`${roleTitle} · 详情`"
  :show-header="false"
  :backable="false"
  :has-bottom-bar="allowedActions.length > 0"
>

Remove the now-unused backToList handler only if no other code path calls it; otherwise retain the handler but remove the template listener.

  • Step 4: Run focused tests and verify GREEN

Run the command from Step 2. Expected: all assertions pass.

Task 5: Full verification and graph impact check

Files:

  • Verify all modified files above.

  • [ ] Step 1: Run all mobile Node tests

cd mobile-oa-h5
node --test $(rg --files src | rg '\.test\.js$')

Expected: zero failures.

  • Step 2: Run the production build
cd mobile-oa-h5
pnpm build

Expected: Vite build exits with code 0.

  • Step 3: Check formatting and unintended whitespace
git diff --check

Expected: no output and exit code 0.

  • Step 4: Inspect the final diff against the design

Verify all acceptance criteria from docs/superpowers/specs/2026-10-11-market-attachment-parity-design.md, especially that no raw attachment path is rendered in the four attachment-bearing detail adapters.

  • Step 5: Skip knowledge-graph regeneration

This change modifies Vue templates, local CSS, and pure presentation helpers without changing function signatures or import relationships outside mobile-oa-h5; the repository knowledge-graph rule only has generated graphs for backend and web-admin, so no graph update command applies.