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.
mobile-oa-h5/src/market/attachment-presenter.js: normalize supported attachment shapes, derive safe display names, image classification, and workflow file kinds.mobile-oa-h5/src/market/attachment-presenter.test.js: unit coverage for strings, arrays, object metadata, image detection, and file kinds.mobile-oa-h5/src/market/components/MarketAttachmentCards.vue: workflow-standard 84×84 rendering, upload trigger, image preview, file download, and editable remove event.mobile-oa-h5/src/market/components/MarketFormField.vue: replace raw URL anchors with the shared component while preserving comma-separated form values.mobile-oa-h5/src/market/components/PresignAttachmentUploader.vue: reuse the shared renderer and preserve existing upload/remove events.mobile-oa-h5/src/market/components/MarketDetailSection.vue: support type: 'attachments' in top-level and nested detail fields.mobile-oa-h5/src/market/modules/protocol.js: expose protocol attachment values as typed attachments instead of joined path text.mobile-oa-h5/src/market/modules/payment.js: expose transfer vouchers as typed attachments.mobile-oa-h5/src/market/modules/sample.js: expose handling attachments as typed attachments.mobile-oa-h5/src/market/modules/presign.js: expose presign transfer attachments as typed attachments.mobile-oa-h5/src/views/market/MarketModuleDetail.vue: disable the detail-page back control.mobile-oa-h5/src/market/mobile-parity-ui.test.js: lock the visual and detail attachment contracts.mobile-oa-h5/src/market/module-contracts.test.js: lock typed attachment fields across adapters.Files:
mobile-oa-h5/src/market/attachment-presenter.jsCreate: 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
Run:
cd mobile-oa-h5
node --test src/market/attachment-presenter.test.js
Expected: FAIL because attachment-presenter.js does not exist.
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
Run the command from Step 2. Expected: all tests pass.
Files:
mobile-oa-h5/src/market/components/MarketAttachmentCards.vueModify: 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.
cd mobile-oa-h5
node --test src/market/mobile-parity-ui.test.js
Expected: FAIL because the component does not exist.
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.
Run the command from Step 2. Expected: all assertions pass.
Files:
mobile-oa-h5/src/market/components/MarketFormField.vuemobile-oa-h5/src/market/components/PresignAttachmentUploader.vueModify: 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.
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.
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.
Run the command from Step 2. Expected: all assertions pass.
Files:
mobile-oa-h5/src/market/components/MarketDetailSection.vuemobile-oa-h5/src/market/modules/protocol.jsmobile-oa-h5/src/market/modules/payment.jsmobile-oa-h5/src/market/modules/sample.jsmobile-oa-h5/src/market/modules/presign.jsmobile-oa-h5/src/views/market/MarketModuleDetail.vuemobile-oa-h5/src/market/module-contracts.test.jsModify: 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.
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.
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.
Run the command from Step 2. Expected: all assertions pass.
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.
cd mobile-oa-h5
pnpm build
Expected: Vite build exits with code 0.
git diff --check
Expected: no output and exit code 0.
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.
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.