From 133433a260a26e6cbbdc9ef45f8dedbde904f0df Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Thu, 7 May 2026 13:15:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lang/cn.ts | 1 + src/lang/en.ts | 1 + .../BrandProfile/image-clip-dialog.vue | 15 +- .../MyListings/EditDetail/api.ts | 15 +- .../MyListings/EditDetail/index.vue | 279 ++++++++++++++---- .../MyListings/EditDetail/types.ts | 17 +- 6 files changed, 259 insertions(+), 69 deletions(-) diff --git a/src/lang/cn.ts b/src/lang/cn.ts index 9d9d82d7..1b09096d 100644 --- a/src/lang/cn.ts +++ b/src/lang/cn.ts @@ -1855,6 +1855,7 @@ export default { SelectCollection: '选择商品', SelectSketch: '选择线稿图', EditListingDetails: '编辑商品详情', + VideoWarning: '首次选中的图片素材会作为产品主图,视频不可作为产品主图' }, ApplySeller: { applySellerTitle: '申请成为卖家', diff --git a/src/lang/en.ts b/src/lang/en.ts index c97d6128..1c602e0b 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -1909,6 +1909,7 @@ export default { SelectCollection: 'Select Collection', SelectSketch: 'Select Sketch', EditListingDetails: 'Edit Listing Details', + VideoWarning:'The first selected item is the main product image. Videos cannot be used.' }, ApplySeller: { applySellerTitle: 'Apply to Become a Seller', diff --git a/src/views/SellerDashboard/BrandProfile/image-clip-dialog.vue b/src/views/SellerDashboard/BrandProfile/image-clip-dialog.vue index 1e6c2d89..c08b5ddd 100644 --- a/src/views/SellerDashboard/BrandProfile/image-clip-dialog.vue +++ b/src/views/SellerDashboard/BrandProfile/image-clip-dialog.vue @@ -124,8 +124,11 @@ const currentOrigin = ref("sketch") const coverOrigin = ref([]) const handleChangeOrigin = (type) => { + const targetOrigin = coverOrigin.value.find((el) => el.type === type) + if (!targetOrigin) return + currentOrigin.value = type - data.url = coverOrigin.value.filter((el) => el.type === type)[0].url + data.url = targetOrigin.url } const show = ref(false) @@ -136,7 +139,7 @@ coverOrigin.value = [] data.url = null - currentOrigin.value = 'sketch' + currentOrigin.value = "sketch" data.url = url data.callback = callback @@ -148,9 +151,11 @@ if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview data.isProduct = options.isProduct } - if (origin?.length && !data.url) { + if (origin?.length) { coverOrigin.value = origin - data.url = origin[0].url + const defaultOrigin = origin.find((el) => el.type === options?.coverFrom) || origin[0] + currentOrigin.value = defaultOrigin.type + data.url = defaultOrigin.url } show.value = true } @@ -160,7 +165,7 @@ const imageClipRef = ref(null) const onSubmit = () => { imageClipRef.value.getCropBlob().then((blob) => { - if (data.callback) data.callback(blobToFile(blob, "image.png")) + if (data.callback) data.callback(blobToFile(blob, "image.png"), currentOrigin.value) onCancel() }) } diff --git a/src/views/SellerDashboard/MyListings/EditDetail/api.ts b/src/views/SellerDashboard/MyListings/EditDetail/api.ts index f5e4057b..8870ba9a 100644 --- a/src/views/SellerDashboard/MyListings/EditDetail/api.ts +++ b/src/views/SellerDashboard/MyListings/EditDetail/api.ts @@ -1,5 +1,5 @@ import { Https } from "@/tool/https" -import type { SketchDetailResponse } from "./types" +import type { ListingImageCategory, SketchDetailResponse } from "./types" // 编辑时根据ID获取信息 export const fetchListingDetailById = (id) => { @@ -21,23 +21,26 @@ export const fetchSketchDetail = (data: SketchIDs): Promise { +export const fetchUpdateListing = (data: DetailData[]) => { return Https.axiosPost("/seller/listing/batch", data) } diff --git a/src/views/SellerDashboard/MyListings/EditDetail/index.vue b/src/views/SellerDashboard/MyListings/EditDetail/index.vue index c2fb007a..569001bf 100644 --- a/src/views/SellerDashboard/MyListings/EditDetail/index.vue +++ b/src/views/SellerDashboard/MyListings/EditDetail/index.vue @@ -88,8 +88,11 @@ fetchUpdateListing } from "./api" import type { + CoverSourceType, + CropType, ListingDetailImage, ListingDetailResponse, + ListingImageCategory, ListingItem, ProductMediaItem, RadioOption, @@ -124,6 +127,7 @@ desc: "", gender: "FEMALE", category: null, + coverFrom: "sketch", firstSelectedIndex: null, prodImageList: [], sketchList: [] @@ -170,14 +174,48 @@ return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0)) } - const getImageSelected = (value: ListingDetailImage["isSelected"]) => - value === true || value === 1 || value === "1" + const getImageSelected = (value: ListingDetailImage["isSelected"]) => { + if (value === true || value === 1 || value === "1") return true + if (typeof value === "string") return value.toLowerCase() === "true" + + return false + } + + const getDetailImageSelected = (image: ListingDetailImage) => + getImageSelected(image.isSelected) || + getImageSelected(image.isSeleted) || + getImageSelected(image.selected) + + const normalizeCoverSource = (value: unknown): CoverSourceType => + value === "mainProductImage" ? "mainProductImage" : "sketch" + + const isCoverSource = (value: unknown): value is CoverSourceType => + value === "sketch" || value === "mainProductImage" + + const resolveCoverSourceFromImageUrl = ( + imageUrl: string, + listing: ListingItem + ): CoverSourceType => { + if (imageUrl === listing.mainProductImage) return "mainProductImage" + if (imageUrl === listing.sketch) return "sketch" + + return normalizeCoverSource(imageUrl) + } + + const videoImageCategories = ["firstFrame", "gif", "video"] as const + type VideoImageCategory = (typeof videoImageCategories)[number] + + const isVideoImageCategory = (category: ListingDetailImage["category"]): category is VideoImageCategory => + videoImageCategories.includes(category as VideoImageCategory) const normalizeDetailGender = (value: ListingDetailResponse["designFor"]) => { const gender = String(value || "").toUpperCase() return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE" } + const getListingDesignFor = (gender: ListingItem["gender"]): "male" | "female" => + gender === "MALE" ? "male" : "female" + const normalizeDetailCategory = ( value: ListingDetailResponse["productCategory"] ): ListingItem["category"] => { @@ -191,6 +229,17 @@ const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => { const listing = createListingItem() + let coverFromImageUrl = "" + const videoGroupMap = new Map< + number, + { + sortOrder: number + firstFrameUrl?: string + gifUrl?: string + videoUrl?: string + selected: boolean + } + >() listing.productName = detail.title || "" listing.price = @@ -200,6 +249,11 @@ listing.category = normalizeDetailCategory(detail.productCategory) getSortedDetailImages(detail.images || []).forEach((image) => { + if (image.category === "cover_from") { + coverFromImageUrl = image.imageUrl || "" + return + } + const imageUrl = image.imageUrl || "" if (!imageUrl) return @@ -213,7 +267,7 @@ return } - if (image.category === "mainProductImage") { + if (image.category === "mainProductImage" || image.category === "main_product") { listing.mainProductImage = imageUrl return } @@ -221,22 +275,57 @@ if (image.category === "product") { listing.prodImageList.push({ url: imageUrl, - selected: getImageSelected(image.isSelected) + selected: getDetailImageSelected(image) }) return } + if (isVideoImageCategory(image.category)) { + if (image.sortOrder === null || typeof image.sortOrder === "undefined") return + + const sortOrder = Number(image.sortOrder) + if (!Number.isFinite(sortOrder)) return + + const group = videoGroupMap.get(sortOrder) || { + sortOrder, + selected: false + } + + if (image.category === "firstFrame") group.firstFrameUrl = imageUrl + if (image.category === "gif") group.gifUrl = imageUrl + if (image.category === "video") group.videoUrl = imageUrl + group.selected = group.selected || getDetailImageSelected(image) + videoGroupMap.set(sortOrder, group) + return + } + if (image.category === "apparel") { listing.sketchList.push({ url: imageUrl }) } }) + Array.from(videoGroupMap.values()) + .sort((prev, next) => prev.sortOrder - next.sortOrder) + .forEach((video) => { + const videoItem = createProductVideoItem(video, video.selected) + if (videoItem) listing.prodImageList.push(videoItem) + }) + if (!listing.mainProductImage) { listing.mainProductImage = - listing.prodImageList.find((item) => item.selected)?.url || "" + listing.prodImageList.find((item) => item.selected && !item.isVideo)?.url || "" + } + if (coverFromImageUrl) { + listing.coverFrom = resolveCoverSourceFromImageUrl(coverFromImageUrl, listing) } - const selectedIndex = listing.prodImageList.findIndex((item) => item.selected) + const mainProductIndex = listing.prodImageList.findIndex( + (item) => !item.isVideo && item.url === listing.mainProductImage + ) + const selectedIndex = + mainProductIndex === -1 + ? listing.prodImageList.findIndex((item) => item.selected && !item.isVideo) + : mainProductIndex listing.firstSelectedIndex = selectedIndex === -1 ? null : selectedIndex listing.productImage = listing.prodImageList.map((item) => item.url) @@ -247,7 +336,10 @@ return listing } - const createProductVideoItem = (video: SketchDetailVideo): ProductMediaItem | null => { + const createProductVideoItem = ( + video: SketchDetailVideo, + selected = false + ): ProductMediaItem | null => { const firstFrameUrl = video?.firstFrameUrl || "" const gifUrl = video?.gifUrl || "" const videoUrl = video?.videoUrl || "" @@ -260,7 +352,7 @@ gifUrl, videoUrl, isVideo: true, - selected: false + selected } } @@ -287,25 +379,29 @@ } } - const cropType = ref("") - const handleClickCrop = (data: any, type: string, paramThree: any = []) => { + const getCoverOriginList = (item: ListingItem) => { + const origin: Array<{ type: CoverSourceType; url: string }> = [] + if (item.sketch) { + origin.push({ type: "sketch", url: item.sketch }) + } + if (item.mainProductImage) { + origin.push({ type: "mainProductImage", url: item.mainProductImage }) + } + + return origin + } + + const cropType = ref("") + const handleClickCrop = (data: string | null, type: CropType, paramThree: number | unknown[] = []) => { // 处理来自TopImageSection的调用: (data, type, list) // 处理来自ApparelSketchList的调用: (data, type, index) const index = typeof paramThree === "number" ? paramThree : undefined - const list = Array.isArray(paramThree) ? paramThree : [] // console.log(data, type) // console.log(selectList.value[currentIndex.value]) - let origin = [] const currentItem = selectList.value[currentIndex.value] - if (currentItem.sketch) { - origin.push({ type: "sketch", url: currentItem.sketch }) - } - if (currentItem.mainProductImage) { - origin.push({ type: "mainProductImage", url: currentItem.mainProductImage }) - } - if (type !== "cover") origin = [] - const titleList = { + const origin = type === "cover" ? getCoverOriginList(currentItem) : [] + const titleList: Record = { sketch: "Crop Sketch", mainProductImage: "Crop Main Product Image", cover: "Crop Cover", @@ -315,17 +411,28 @@ cropType.value = type imageClipDialogRef.value.open( data, - (file) => { + (file: File, coverFrom?: CoverSourceType) => { // console.log(file) uploadFile(file).then((res) => { if (type === "apparel" && typeof index !== "undefined") { selectList.value[currentIndex.value].sketchList[index].url = res + } else if (type === "cover") { + selectList.value[currentIndex.value].cover = res + if (isCoverSource(coverFrom)) { + selectList.value[currentIndex.value].coverFrom = coverFrom + } } else { selectList.value[currentIndex.value][type] = res } }) }, - { ratio, isPreview: true, title: titleList[type], isProduct: true }, + { + ratio, + isPreview: true, + title: titleList[type], + isProduct: true, + coverFrom: currentItem.coverFrom + }, origin ) } @@ -377,49 +484,107 @@ return true } + type SaveListingImage = { + category: ListingImageCategory + imageUrl: string | null + isSelected: number + sortOrder: number + } + + const isMainProductMedia = (item: ListingItem, media: ProductMediaItem, index: number) => + !media.isVideo && + (item.firstSelectedIndex === index || + (Boolean(item.mainProductImage) && item.mainProductImage === media.url)) + + const getSortedProductMedia = (item: ListingItem) => { + return item.prodImageList + .map((media, index) => ({ media, index })) + .filter(({ media }) => !media.isVideo) + .sort((prev, next) => { + const prevRank = isMainProductMedia(item, prev.media, prev.index) + ? 0 + : prev.media.selected + ? 1 + : 2 + const nextRank = isMainProductMedia(item, next.media, next.index) + ? 0 + : next.media.selected + ? 1 + : 2 + + return prevRank - nextRank || prev.index - next.index + }) + } + + const buildListingImages = (item: ListingItem) => { + const images: SaveListingImage[] = [] + const sortOrderMap: Partial> = {} + const getNextSortOrder = (category: ListingImageCategory) => { + const sortOrder = (sortOrderMap[category] || 0) + 1 + sortOrderMap[category] = sortOrder + return sortOrder + } + const pushImage = ( + category: ListingImageCategory, + imageUrl: string | null, + isSelected = 1, + sortOrder = getNextSortOrder(category) + ) => { + images.push({ + category, + imageUrl, + isSelected, + sortOrder + }) + } + const getCoverFromImageUrl = () => { + if (item.coverFrom === "mainProductImage") return item.mainProductImage || null + return item.sketch + } + + pushImage("sketch", item.sketch) + pushImage("cover", item.cover) + pushImage("cover_from", getCoverFromImageUrl()) + + if (item.mainProductImage) { + pushImage("main_product", item.mainProductImage) + } + + getSortedProductMedia(item).forEach(({ media }) => { + pushImage("product", media.url, Number(!!media.selected)) + }) + + item.sketchList.forEach((sketch) => { + pushImage("apparel", sketch.url) + }) + + let videoSortOrder = 0 + item.prodImageList + .filter((media) => media.isVideo) + .forEach((media) => { + videoSortOrder += 1 + const isSelected = Number(!!media.selected) + + pushImage("firstFrame", media.firstFrameUrl || media.url, isSelected, videoSortOrder) + pushImage("gif", media.gifUrl || "", isSelected, videoSortOrder) + pushImage("video", media.videoUrl || "", isSelected, videoSortOrder) + }) + + return images + } + const handleSaveForm = async (type: StatusType) => { - const paramsList = [] - selectList.value.forEach((item: ListingItem) => { - const params = { + const paramsList = selectList.value.map((item: ListingItem) => { + return { id: itemId.value, title: item.productName, description: item.desc, price: item.price, status: type === "draft" ? 0 : 1, - images: [], - designFor: (item.gender || "FEMALE").toLowerCase(), + images: buildListingImages(item), + designFor: getListingDesignFor(item.gender), productCategory: item.category } - - ;["sketch", "cover"].forEach((el) => { - params.images.push({ - category: el, - imageUrl: item[el], - isSelected: 1 - }) - }) - if (item.mainProductImage) { - params.images.push({ - category: "main_product", - imageUrl: item.mainProductImage, - isSeleted: 1 - }) - } - item.prodImageList.forEach((item) => { - params.images.push({ - category: "product", - imageUrl: item.url, - isSelected: Number(!!item.selected) - }) - }) - item.sketchList.forEach((item) => { - params.images.push({ - category: "apparel", - imageUrl: item.url, - isSelected: 1 - }) - }) - paramsList.push(params) }) await fetchUpdateListing(paramsList) } diff --git a/src/views/SellerDashboard/MyListings/EditDetail/types.ts b/src/views/SellerDashboard/MyListings/EditDetail/types.ts index 0518f2db..013ccb8e 100644 --- a/src/views/SellerDashboard/MyListings/EditDetail/types.ts +++ b/src/views/SellerDashboard/MyListings/EditDetail/types.ts @@ -7,6 +7,18 @@ export type RadioOption = { export type TopImageType = "sketch" | "mainProductImage" | "cover" export type CropType = TopImageType | "apparel" +export type CoverSourceType = "sketch" | "mainProductImage" +export type ListingImageCategory = + | "cover" + | "cover_from" + | "main_product" + | "mainProductImage" + | "product" + | "sketch" + | "apparel" + | "firstFrame" + | "gif" + | "video" export type ProductMediaItem = { url: string @@ -29,15 +41,18 @@ export type ListingItem = { desc: string gender: string category: string[] | null + coverFrom: CoverSourceType firstSelectedIndex: number | null prodImageList: ProductMediaItem[] sketchList: Array<{ url: string | null }> } export type ListingDetailImage = { - category?: string | null + category?: ListingImageCategory | string | null imageUrl?: string | null isSelected?: boolean | number | string | null + isSeleted?: boolean | number | string | null + selected?: boolean | number | string | null sortOrder?: number | null }