bugfix: 多页编辑

This commit is contained in:
2026-04-29 14:16:23 +08:00
parent 7cf37e2da6
commit 56825fcbb1
3 changed files with 82 additions and 14 deletions

View File

@@ -0,0 +1,68 @@
# EditDetail Page Agent Notes
This directory owns the seller listing edit/create detail page.
## Files
- `index.vue`: route-level container. Keep orchestration here: history-state entry mode, API calls, page switching, validation, save/publish navigation, image crop dialog wiring.
- `api.ts`: API wrappers for listing detail, sketch detail, listing batch save, status update, and file upload.
- `types.ts`: shared page-local TypeScript types. Add new cross-component page types here instead of duplicating interfaces in child components.
- `components/TopImageSection.vue`: presentational block for `sketch`, `mainProductImage`, and `cover`.
- `components/ProductImageList.vue`: presentational product-image selector and selected/main badge display.
- `components/ApparelSketchList.vue`: presentational apparel sketch list and crop trigger.
- `components/ListingForm.vue`: presentational listing form. Emits field updates; does not call APIs.
- `components/Radio.vue`: local radio/multi-select button component.
- `Status.vue`: save/publish result status page.
## Component Boundaries
- Keep `index.vue` as the single source of truth for `selectList`, `currentPage`, `currentListing`, per-listing `firstSelectedIndex`, and crop/save behavior.
- Child components should receive props and emit events only. Do not import listing APIs or mutate parent state directly from children.
- If a new visual section is added to this page, prefer a new child component under `components/` plus shared types in `types.ts`.
## Image Category Mapping
Detail API images are mapped by `category`:
- `cover` -> `currentListing.cover`
- `sketch` -> `currentListing.sketch`
- `mainProductImage` -> `currentListing.mainProductImage`
- `main_product` or `product` -> `currentListing.prodImageList`
- `apparel` -> `currentListing.sketchList`
When saving, preserve the backend's expected image categories. Confirm backend naming before changing `main_product`, `product`, or `mainProductImage`.
## Product Image Rules
- The `main` badge represents the first selected product image, not the most recently selected one.
- `firstSelectedIndex` is stored on each `ListingItem` and passed to `ProductImageList.vue`.
- Selecting a product image should only set `mainProductImage` when no main image is currently tracked by that listing's `firstSelectedIndex`.
- Unselecting the current main product image clears `mainProductImage` and resets `firstSelectedIndex`.
## Crop Flow
- `TopImageSection.vue` and `ApparelSketchList.vue` emit `crop`.
- `index.vue` handles `handleClickCrop`, opens `ImageClipDialog`, uploads with `uploadFile`, then writes the returned URL into the correct field/list item.
- Keep cover crop ratio at `[4, 5]`; other crop types use `[9, 16]`.
## Form Flow
- `ListingForm.vue` accepts scalar form props and emits `update:*` events.
- `index.vue` writes those events back into `currentListing`.
- Category options are derived from current gender and Vuex `UserHabit` state.
## Validation And Navigation
- `validatePublishRequired()` validates each listing before publish.
- Draft currently requires `cover` before save.
- After save/publish, routing goes to `Status` with route param `status`.
## Verification
- Run `npm run build` after behavior or type changes.
- Build can fail before code bundling completes if the existing `feedbackSurvey.vue` Google Fonts import cannot be fetched. If the failure is only `fonts.googleapis.com` socket/DNS related, retry when network is available.
- Project ESLint currently fails before linting files because `.eslintrc.js` contains invalid env key `se6`; do not treat that as a page-specific regression.
## Known Caution
- This page has active local edits. Before broad refactors, inspect both staged and unstaged diffs.

View File

@@ -31,7 +31,7 @@
<TopImageSection :images="previewImageMap" @crop="handleClickCrop" />
<ProductImageList
:image-list="prodImgList"
:first-selected-index="firstSelectedIndex"
:first-selected-index="currentListing.firstSelectedIndex"
@select="handleSelectProdImg"
/>
<ApparelSketchList
@@ -130,6 +130,7 @@
desc: "",
gender: "FEMALE",
category: null,
firstSelectedIndex: null,
prodImageList: [],
sketchList: []
})
@@ -162,8 +163,6 @@
cover: currentListing.value.cover
}))
const firstSelectedIndex = ref<number | null>(null) //显示main标签的图片索引
const getSortedDetailImages = (images: ListingDetailImage[] = []) => {
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
}
@@ -234,6 +233,9 @@
listing.prodImageList.find((item) => item.selected)?.url || ""
}
const selectedIndex = listing.prodImageList.findIndex((item) => item.selected)
listing.firstSelectedIndex = selectedIndex === -1 ? null : selectedIndex
listing.productImage = listing.prodImageList.map((item) => item.url)
listing.apparelSketch = listing.sketchList
.map((item) => item.url)
@@ -243,20 +245,21 @@
}
const handleSelectProdImg = (index: number) => {
const listing = currentListing.value
const target = prodImgList.value[index]
const willSelect = !target.selected
target.selected = willSelect
if (willSelect && firstSelectedIndex.value === null) {
currentListing.value.mainProductImage = target.url
firstSelectedIndex.value = index
if (willSelect && listing.firstSelectedIndex === null) {
listing.mainProductImage = target.url
listing.firstSelectedIndex = index
return
}
if (!willSelect && currentListing.value.mainProductImage === target.url) {
firstSelectedIndex.value = null
currentListing.value.mainProductImage = ""
if (!willSelect && listing.mainProductImage === target.url) {
listing.firstSelectedIndex = null
listing.mainProductImage = ""
}
}
@@ -360,7 +363,7 @@
price: item.price,
status: type === "draft" ? 0 : 1,
images: [],
designFor: item.gender.toLowerCase,
designFor: (item.gender || "FEMALE").toLowerCase(),
productCategory: item.category
}
@@ -394,8 +397,6 @@
})
paramsList.push(params)
})
console.log(paramsList)
debugger
await fetchUpdateListing(paramsList)
}
const handleClickMenu = async (status: StatusType) => {
@@ -433,11 +434,9 @@
const handleGetDetailById = () => {
fetchListingDetailById(itemId.value).then((res: ListingDetailResponse) => {
const listing = createListingItemFromDetail(res)
const selectedIndex = listing.prodImageList.findIndex((item) => item.selected)
currentPage.value = 1
selectList.value = [listing]
firstSelectedIndex.value = selectedIndex === -1 ? null : selectedIndex
})
}

View File

@@ -20,6 +20,7 @@ export type ListingItem = {
desc: string
gender: string
category: string[] | null
firstSelectedIndex: number | null
prodImageList: Array<{
url: string
selected?: boolean