bugfix: 多页编辑
This commit is contained in:
68
src/views/SellerDashboard/MyListings/EditDetail/agents.md
Normal file
68
src/views/SellerDashboard/MyListings/EditDetail/agents.md
Normal 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.
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<TopImageSection :images="previewImageMap" @crop="handleClickCrop" />
|
<TopImageSection :images="previewImageMap" @crop="handleClickCrop" />
|
||||||
<ProductImageList
|
<ProductImageList
|
||||||
:image-list="prodImgList"
|
:image-list="prodImgList"
|
||||||
:first-selected-index="firstSelectedIndex"
|
:first-selected-index="currentListing.firstSelectedIndex"
|
||||||
@select="handleSelectProdImg"
|
@select="handleSelectProdImg"
|
||||||
/>
|
/>
|
||||||
<ApparelSketchList
|
<ApparelSketchList
|
||||||
@@ -130,6 +130,7 @@
|
|||||||
desc: "",
|
desc: "",
|
||||||
gender: "FEMALE",
|
gender: "FEMALE",
|
||||||
category: null,
|
category: null,
|
||||||
|
firstSelectedIndex: null,
|
||||||
prodImageList: [],
|
prodImageList: [],
|
||||||
sketchList: []
|
sketchList: []
|
||||||
})
|
})
|
||||||
@@ -162,8 +163,6 @@
|
|||||||
cover: currentListing.value.cover
|
cover: currentListing.value.cover
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const firstSelectedIndex = ref<number | null>(null) //显示main标签的图片索引
|
|
||||||
|
|
||||||
const getSortedDetailImages = (images: ListingDetailImage[] = []) => {
|
const getSortedDetailImages = (images: ListingDetailImage[] = []) => {
|
||||||
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
|
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
|
||||||
}
|
}
|
||||||
@@ -234,6 +233,9 @@
|
|||||||
listing.prodImageList.find((item) => item.selected)?.url || ""
|
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.productImage = listing.prodImageList.map((item) => item.url)
|
||||||
listing.apparelSketch = listing.sketchList
|
listing.apparelSketch = listing.sketchList
|
||||||
.map((item) => item.url)
|
.map((item) => item.url)
|
||||||
@@ -243,20 +245,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectProdImg = (index: number) => {
|
const handleSelectProdImg = (index: number) => {
|
||||||
|
const listing = currentListing.value
|
||||||
const target = prodImgList.value[index]
|
const target = prodImgList.value[index]
|
||||||
const willSelect = !target.selected
|
const willSelect = !target.selected
|
||||||
|
|
||||||
target.selected = willSelect
|
target.selected = willSelect
|
||||||
|
|
||||||
if (willSelect && firstSelectedIndex.value === null) {
|
if (willSelect && listing.firstSelectedIndex === null) {
|
||||||
currentListing.value.mainProductImage = target.url
|
listing.mainProductImage = target.url
|
||||||
firstSelectedIndex.value = index
|
listing.firstSelectedIndex = index
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!willSelect && currentListing.value.mainProductImage === target.url) {
|
if (!willSelect && listing.mainProductImage === target.url) {
|
||||||
firstSelectedIndex.value = null
|
listing.firstSelectedIndex = null
|
||||||
currentListing.value.mainProductImage = ""
|
listing.mainProductImage = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +363,7 @@
|
|||||||
price: item.price,
|
price: item.price,
|
||||||
status: type === "draft" ? 0 : 1,
|
status: type === "draft" ? 0 : 1,
|
||||||
images: [],
|
images: [],
|
||||||
designFor: item.gender.toLowerCase,
|
designFor: (item.gender || "FEMALE").toLowerCase(),
|
||||||
productCategory: item.category
|
productCategory: item.category
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,8 +397,6 @@
|
|||||||
})
|
})
|
||||||
paramsList.push(params)
|
paramsList.push(params)
|
||||||
})
|
})
|
||||||
console.log(paramsList)
|
|
||||||
debugger
|
|
||||||
await fetchUpdateListing(paramsList)
|
await fetchUpdateListing(paramsList)
|
||||||
}
|
}
|
||||||
const handleClickMenu = async (status: StatusType) => {
|
const handleClickMenu = async (status: StatusType) => {
|
||||||
@@ -433,11 +434,9 @@
|
|||||||
const handleGetDetailById = () => {
|
const handleGetDetailById = () => {
|
||||||
fetchListingDetailById(itemId.value).then((res: ListingDetailResponse) => {
|
fetchListingDetailById(itemId.value).then((res: ListingDetailResponse) => {
|
||||||
const listing = createListingItemFromDetail(res)
|
const listing = createListingItemFromDetail(res)
|
||||||
const selectedIndex = listing.prodImageList.findIndex((item) => item.selected)
|
|
||||||
|
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
selectList.value = [listing]
|
selectList.value = [listing]
|
||||||
firstSelectedIndex.value = selectedIndex === -1 ? null : selectedIndex
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export type ListingItem = {
|
|||||||
desc: string
|
desc: string
|
||||||
gender: string
|
gender: string
|
||||||
category: string[] | null
|
category: string[] | null
|
||||||
|
firstSelectedIndex: number | null
|
||||||
prodImageList: Array<{
|
prodImageList: Array<{
|
||||||
url: string
|
url: string
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user