Compare commits
2 Commits
c8a65ee2cb
...
133433a260
| Author | SHA1 | Date | |
|---|---|---|---|
| 133433a260 | |||
| 90a59a3dc5 |
@@ -1744,7 +1744,7 @@ export default {
|
|||||||
cover: "封面",
|
cover: "封面",
|
||||||
productImageDesc: '从产品图中选取',
|
productImageDesc: '从产品图中选取',
|
||||||
cropDesc: '从主产品图或线稿图中裁剪',
|
cropDesc: '从主产品图或线稿图中裁剪',
|
||||||
productImageMainTitle: '产品图 ',
|
productImageMainTitle: '产品图/视频',
|
||||||
productImageSubTitle: ' (来自设计集)',
|
productImageSubTitle: ' (来自设计集)',
|
||||||
apparelSketchTitle: '服装线稿图 ',
|
apparelSketchTitle: '服装线稿图 ',
|
||||||
apparelSketchSubTitle: ' (来自设计集)',
|
apparelSketchSubTitle: ' (来自设计集)',
|
||||||
@@ -1855,6 +1855,7 @@ export default {
|
|||||||
SelectCollection: '选择商品',
|
SelectCollection: '选择商品',
|
||||||
SelectSketch: '选择线稿图',
|
SelectSketch: '选择线稿图',
|
||||||
EditListingDetails: '编辑商品详情',
|
EditListingDetails: '编辑商品详情',
|
||||||
|
VideoWarning: '首次选中的图片素材会作为产品主图,视频不可作为产品主图'
|
||||||
},
|
},
|
||||||
ApplySeller: {
|
ApplySeller: {
|
||||||
applySellerTitle: '申请成为卖家',
|
applySellerTitle: '申请成为卖家',
|
||||||
|
|||||||
@@ -1795,7 +1795,7 @@ export default {
|
|||||||
cover:'Cover',
|
cover:'Cover',
|
||||||
productImageDesc:'Choose from product image',
|
productImageDesc:'Choose from product image',
|
||||||
cropDesc:'Crop from main product image or sketch',
|
cropDesc:'Crop from main product image or sketch',
|
||||||
productImageMainTitle:'Product Image ',
|
productImageMainTitle:'Product Media ',
|
||||||
productImageSubTitle:'(from design collection)',
|
productImageSubTitle:'(from design collection)',
|
||||||
apparelSketchTitle:'Apparel Sketch ',
|
apparelSketchTitle:'Apparel Sketch ',
|
||||||
apparelSketchSubTitle:'(from design collection)',
|
apparelSketchSubTitle:'(from design collection)',
|
||||||
@@ -1909,6 +1909,7 @@ export default {
|
|||||||
SelectCollection: 'Select Collection',
|
SelectCollection: 'Select Collection',
|
||||||
SelectSketch: 'Select Sketch',
|
SelectSketch: 'Select Sketch',
|
||||||
EditListingDetails: 'Edit Listing Details',
|
EditListingDetails: 'Edit Listing Details',
|
||||||
|
VideoWarning:'The first selected item is the main product image. Videos cannot be used.'
|
||||||
},
|
},
|
||||||
ApplySeller: {
|
ApplySeller: {
|
||||||
applySellerTitle: 'Apply to Become a Seller',
|
applySellerTitle: 'Apply to Become a Seller',
|
||||||
|
|||||||
@@ -124,8 +124,11 @@
|
|||||||
const currentOrigin = ref("sketch")
|
const currentOrigin = ref("sketch")
|
||||||
const coverOrigin = ref([])
|
const coverOrigin = ref([])
|
||||||
const handleChangeOrigin = (type) => {
|
const handleChangeOrigin = (type) => {
|
||||||
|
const targetOrigin = coverOrigin.value.find((el) => el.type === type)
|
||||||
|
if (!targetOrigin) return
|
||||||
|
|
||||||
currentOrigin.value = type
|
currentOrigin.value = type
|
||||||
data.url = coverOrigin.value.filter((el) => el.type === type)[0].url
|
data.url = targetOrigin.url
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = ref(false)
|
const show = ref(false)
|
||||||
@@ -136,7 +139,7 @@
|
|||||||
|
|
||||||
coverOrigin.value = []
|
coverOrigin.value = []
|
||||||
data.url = null
|
data.url = null
|
||||||
currentOrigin.value = 'sketch'
|
currentOrigin.value = "sketch"
|
||||||
|
|
||||||
data.url = url
|
data.url = url
|
||||||
data.callback = callback
|
data.callback = callback
|
||||||
@@ -148,9 +151,11 @@
|
|||||||
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
|
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
|
||||||
data.isProduct = options.isProduct
|
data.isProduct = options.isProduct
|
||||||
}
|
}
|
||||||
if (origin?.length && !data.url) {
|
if (origin?.length) {
|
||||||
coverOrigin.value = origin
|
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
|
show.value = true
|
||||||
}
|
}
|
||||||
@@ -160,7 +165,7 @@
|
|||||||
const imageClipRef = ref(null)
|
const imageClipRef = ref(null)
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
imageClipRef.value.getCropBlob().then((blob) => {
|
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()
|
onCancel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { Https } from "@/tool/https"
|
import { Https } from "@/tool/https"
|
||||||
|
import type { ListingImageCategory, SketchDetailResponse } from "./types"
|
||||||
|
|
||||||
// 编辑时根据ID获取信息
|
// 编辑时根据ID获取信息
|
||||||
export const fetchListingDetailById = (id) => {
|
export const fetchListingDetailById = (id) => {
|
||||||
return Https.axiosGet("/seller/listing/detail", { params: { id } })
|
return Https.axiosGet("/seller/listing/detail", { params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SketchIDs {
|
type SketchIDs = Array<number | string>
|
||||||
designItemIds: Array
|
|
||||||
}
|
|
||||||
interface DetailReturns {
|
|
||||||
clothes: string[]
|
|
||||||
designItemId: number
|
|
||||||
toProductImageUrls: string[]
|
|
||||||
}
|
|
||||||
// 获取designItemId对应的产品图
|
// 获取designItemId对应的产品图
|
||||||
export const fetchSketchDetail = (data: SketchIDs): Array<DetailReturns> => {
|
export const fetchSketchDetail = (data: SketchIDs): Promise<SketchDetailResponse[]> => {
|
||||||
let params = "?"
|
let params = "?"
|
||||||
data.forEach((id, index) => {
|
data.forEach((id, index) => {
|
||||||
if (index === data.length - 1) {
|
if (index === data.length - 1) {
|
||||||
@@ -27,23 +21,26 @@ export const fetchSketchDetail = (data: SketchIDs): Array<DetailReturns> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ImageObj {
|
interface ImageObj {
|
||||||
id: number // 图片id,有值会更新,没有会自动新增
|
id?: number // 图片id,有值会更新,没有会自动新增
|
||||||
category: "cover" | "main_product" | "product" | "sketch" | "apparel" // 图片类型
|
category: ListingImageCategory // 图片类型
|
||||||
|
imageUrl?: string | null
|
||||||
|
isSelected?: number
|
||||||
|
sortOrder?: number
|
||||||
}
|
}
|
||||||
interface DetailData {
|
interface DetailData {
|
||||||
id: number | string // 商品Id
|
id: number | string // 商品Id
|
||||||
title: string // 商品名
|
title: string // 商品名
|
||||||
description: string // 商品描述
|
description: string // 商品描述
|
||||||
price: number // 价格
|
price: number | string // 价格
|
||||||
stock?: number // 库存
|
stock?: number // 库存
|
||||||
viewCount?: number // 浏览量
|
viewCount?: number // 浏览量
|
||||||
status: 0 | 1 | 2 // 0草稿 1发布 2删除
|
status: 0 | 1 | 2 // 0草稿 1发布 2删除
|
||||||
images: ImageObj[]
|
images: ImageObj[]
|
||||||
designFor: "male" | "female"
|
designFor: "male" | "female"
|
||||||
productCategory: "outwear" | "trousers" | "blouse" | "dress" | "skirt" | "accessories"
|
productCategory: string[] | null
|
||||||
}
|
}
|
||||||
// 保存/更新表单
|
// 保存/更新表单
|
||||||
export const fetchUpdateListing = (data: DetailData) => {
|
export const fetchUpdateListing = (data: DetailData[]) => {
|
||||||
return Https.axiosPost("/seller/listing/batch", data)
|
return Https.axiosPost("/seller/listing/batch", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
v-for="(item, index) in imageList"
|
v-for="(item, index) in imageList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="product-image-item flex flex-center"
|
class="product-image-item flex flex-center"
|
||||||
:class="{ selected: item.selected }"
|
:class="{ selected: item.selected, video: item.isVideo }"
|
||||||
@click="emit('select', index)"
|
@click="emit('select', index)"
|
||||||
|
@mouseenter="handleMouseEnter(index, item)"
|
||||||
|
@mouseleave="handleMouseLeave(index, item)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="item.selected"
|
v-if="item.selected"
|
||||||
@@ -21,8 +23,11 @@
|
|||||||
class="checked"
|
class="checked"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<img class="img-src" :src="item.url" alt="" />
|
<img class="img-src" :src="getDisplayUrl(item, index)" alt="" />
|
||||||
<div v-if="item.selected && index === firstSelectedIndex" class="main-pic">
|
<div v-if="item.isVideo && durationMap[index]" class="video-duration">
|
||||||
|
{{ durationMap[index] }}
|
||||||
|
</div>
|
||||||
|
<div v-if="item.selected && index === firstSelectedIndex && !item.isVideo" class="main-pic">
|
||||||
main
|
main
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,8 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import type { ListingItem } from "../types"
|
import type { ListingItem, ProductMediaItem } from "../types"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
imageList: ListingItem["prodImageList"]
|
imageList: ListingItem["prodImageList"]
|
||||||
@@ -43,6 +48,70 @@
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
|
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
|
||||||
|
const hoveredVideoIndex = ref<number | null>(null)
|
||||||
|
const durationMap = ref<Record<number, string>>({})
|
||||||
|
const videoSourceKey = computed(() =>
|
||||||
|
props.imageList
|
||||||
|
.map((item) => `${item.videoUrl || ""}::${item.url || ""}`)
|
||||||
|
.join("|")
|
||||||
|
)
|
||||||
|
|
||||||
|
const getDisplayUrl = (item: ProductMediaItem, index: number) => {
|
||||||
|
if (item.isVideo && hoveredVideoIndex.value === index && item.gifUrl) {
|
||||||
|
return item.gifUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = (index: number, item: ProductMediaItem) => {
|
||||||
|
if (!item.isVideo || !item.gifUrl) return
|
||||||
|
hoveredVideoIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = (index: number, item: ProductMediaItem) => {
|
||||||
|
if (!item.isVideo) return
|
||||||
|
if (hoveredVideoIndex.value === index) hoveredVideoIndex.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatVideoDuration = (duration: number) => {
|
||||||
|
if (!Number.isFinite(duration) || duration <= 0) return ""
|
||||||
|
|
||||||
|
const totalSeconds = Math.round(duration)
|
||||||
|
const minutes = Math.floor(totalSeconds / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
|
||||||
|
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVideoDurations = () => {
|
||||||
|
durationMap.value = {}
|
||||||
|
props.imageList.forEach((item, index) => {
|
||||||
|
if (!item.isVideo || !item.videoUrl) return
|
||||||
|
|
||||||
|
const video = document.createElement("video")
|
||||||
|
video.preload = "metadata"
|
||||||
|
video.src = item.videoUrl
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
durationMap.value = {
|
||||||
|
...durationMap.value,
|
||||||
|
[index]: formatVideoDuration(video.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
video.onerror = () => {
|
||||||
|
video.src = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
videoSourceKey,
|
||||||
|
() => {
|
||||||
|
hoveredVideoIndex.value = null
|
||||||
|
loadVideoDurations()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -135,6 +204,20 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-duration {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.8rem;
|
||||||
|
bottom: 0.8rem;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
border-radius: 1.2rem;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.main-pic {
|
.main-pic {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 2.4rem;
|
height: 2.4rem;
|
||||||
|
|||||||
@@ -88,10 +88,16 @@
|
|||||||
fetchUpdateListing
|
fetchUpdateListing
|
||||||
} from "./api"
|
} from "./api"
|
||||||
import type {
|
import type {
|
||||||
|
CoverSourceType,
|
||||||
|
CropType,
|
||||||
ListingDetailImage,
|
ListingDetailImage,
|
||||||
ListingDetailResponse,
|
ListingDetailResponse,
|
||||||
|
ListingImageCategory,
|
||||||
ListingItem,
|
ListingItem,
|
||||||
|
ProductMediaItem,
|
||||||
RadioOption,
|
RadioOption,
|
||||||
|
SketchDetailResponse,
|
||||||
|
SketchDetailVideo,
|
||||||
StatusType
|
StatusType
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
@@ -121,18 +127,14 @@
|
|||||||
desc: "",
|
desc: "",
|
||||||
gender: "FEMALE",
|
gender: "FEMALE",
|
||||||
category: null,
|
category: null,
|
||||||
|
coverFrom: "sketch",
|
||||||
firstSelectedIndex: null,
|
firstSelectedIndex: null,
|
||||||
prodImageList: [],
|
prodImageList: [],
|
||||||
sketchList: []
|
sketchList: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const genderOptions = computed(() => {
|
const genderOptions = computed(() => {
|
||||||
return (
|
return STORE.state.UserHabit?.sex.value
|
||||||
STORE.state.UserHabit?.sex.value.map((el) => ({
|
|
||||||
...el,
|
|
||||||
// name: el.key.toLowerCase()
|
|
||||||
})) || []
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const fallbackCategoryOptions: Record<string, RadioOption[]> = {
|
const fallbackCategoryOptions: Record<string, RadioOption[]> = {
|
||||||
@@ -172,14 +174,48 @@
|
|||||||
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
|
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImageSelected = (value: ListingDetailImage["isSelected"]) =>
|
const getImageSelected = (value: ListingDetailImage["isSelected"]) => {
|
||||||
value === true || value === 1 || value === "1"
|
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 normalizeDetailGender = (value: ListingDetailResponse["designFor"]) => {
|
||||||
const gender = String(value || "").toUpperCase()
|
const gender = String(value || "").toUpperCase()
|
||||||
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
|
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getListingDesignFor = (gender: ListingItem["gender"]): "male" | "female" =>
|
||||||
|
gender === "MALE" ? "male" : "female"
|
||||||
|
|
||||||
const normalizeDetailCategory = (
|
const normalizeDetailCategory = (
|
||||||
value: ListingDetailResponse["productCategory"]
|
value: ListingDetailResponse["productCategory"]
|
||||||
): ListingItem["category"] => {
|
): ListingItem["category"] => {
|
||||||
@@ -193,6 +229,17 @@
|
|||||||
|
|
||||||
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
|
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
|
||||||
const listing = createListingItem()
|
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.productName = detail.title || ""
|
||||||
listing.price =
|
listing.price =
|
||||||
@@ -202,6 +249,11 @@
|
|||||||
listing.category = normalizeDetailCategory(detail.productCategory)
|
listing.category = normalizeDetailCategory(detail.productCategory)
|
||||||
|
|
||||||
getSortedDetailImages(detail.images || []).forEach((image) => {
|
getSortedDetailImages(detail.images || []).forEach((image) => {
|
||||||
|
if (image.category === "cover_from") {
|
||||||
|
coverFromImageUrl = image.imageUrl || ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const imageUrl = image.imageUrl || ""
|
const imageUrl = image.imageUrl || ""
|
||||||
if (!imageUrl) return
|
if (!imageUrl) return
|
||||||
|
|
||||||
@@ -215,7 +267,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.category === "mainProductImage") {
|
if (image.category === "mainProductImage" || image.category === "main_product") {
|
||||||
listing.mainProductImage = imageUrl
|
listing.mainProductImage = imageUrl
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,22 +275,57 @@
|
|||||||
if (image.category === "product") {
|
if (image.category === "product") {
|
||||||
listing.prodImageList.push({
|
listing.prodImageList.push({
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
selected: getImageSelected(image.isSelected)
|
selected: getDetailImageSelected(image)
|
||||||
})
|
})
|
||||||
return
|
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") {
|
if (image.category === "apparel") {
|
||||||
listing.sketchList.push({ url: imageUrl })
|
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) {
|
if (!listing.mainProductImage) {
|
||||||
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.firstSelectedIndex = selectedIndex === -1 ? null : selectedIndex
|
||||||
|
|
||||||
listing.productImage = listing.prodImageList.map((item) => item.url)
|
listing.productImage = listing.prodImageList.map((item) => item.url)
|
||||||
@@ -249,6 +336,26 @@
|
|||||||
return listing
|
return listing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createProductVideoItem = (
|
||||||
|
video: SketchDetailVideo,
|
||||||
|
selected = false
|
||||||
|
): ProductMediaItem | null => {
|
||||||
|
const firstFrameUrl = video?.firstFrameUrl || ""
|
||||||
|
const gifUrl = video?.gifUrl || ""
|
||||||
|
const videoUrl = video?.videoUrl || ""
|
||||||
|
|
||||||
|
if (!firstFrameUrl || !videoUrl) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: firstFrameUrl,
|
||||||
|
firstFrameUrl,
|
||||||
|
gifUrl,
|
||||||
|
videoUrl,
|
||||||
|
isVideo: true,
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelectProdImg = (index: number) => {
|
const handleSelectProdImg = (index: number) => {
|
||||||
const listing = currentListing.value
|
const listing = currentListing.value
|
||||||
const target = prodImgList.value[index]
|
const target = prodImgList.value[index]
|
||||||
@@ -257,6 +364,10 @@
|
|||||||
target.selected = willSelect
|
target.selected = willSelect
|
||||||
|
|
||||||
if (willSelect && listing.firstSelectedIndex === null) {
|
if (willSelect && listing.firstSelectedIndex === null) {
|
||||||
|
if (target.isVideo) {
|
||||||
|
message.warning("The first selected item is the main product image. Videos cannot be used.")
|
||||||
|
return
|
||||||
|
}
|
||||||
listing.mainProductImage = target.url
|
listing.mainProductImage = target.url
|
||||||
listing.firstSelectedIndex = index
|
listing.firstSelectedIndex = index
|
||||||
return
|
return
|
||||||
@@ -268,25 +379,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cropType = ref("")
|
const getCoverOriginList = (item: ListingItem) => {
|
||||||
const handleClickCrop = (data: any, type: string, paramThree: any = []) => {
|
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<CropType | "">("")
|
||||||
|
const handleClickCrop = (data: string | null, type: CropType, paramThree: number | unknown[] = []) => {
|
||||||
// 处理来自TopImageSection的调用: (data, type, list)
|
// 处理来自TopImageSection的调用: (data, type, list)
|
||||||
// 处理来自ApparelSketchList的调用: (data, type, index)
|
// 处理来自ApparelSketchList的调用: (data, type, index)
|
||||||
const index = typeof paramThree === "number" ? paramThree : undefined
|
const index = typeof paramThree === "number" ? paramThree : undefined
|
||||||
const list = Array.isArray(paramThree) ? paramThree : []
|
|
||||||
|
|
||||||
// console.log(data, type)
|
// console.log(data, type)
|
||||||
// console.log(selectList.value[currentIndex.value])
|
// console.log(selectList.value[currentIndex.value])
|
||||||
let origin = []
|
|
||||||
const currentItem = selectList.value[currentIndex.value]
|
const currentItem = selectList.value[currentIndex.value]
|
||||||
if (currentItem.sketch) {
|
const origin = type === "cover" ? getCoverOriginList(currentItem) : []
|
||||||
origin.push({ type: "sketch", url: currentItem.sketch })
|
const titleList: Record<CropType, string> = {
|
||||||
}
|
|
||||||
if (currentItem.mainProductImage) {
|
|
||||||
origin.push({ type: "mainProductImage", url: currentItem.mainProductImage })
|
|
||||||
}
|
|
||||||
if (type !== "cover") origin = []
|
|
||||||
const titleList = {
|
|
||||||
sketch: "Crop Sketch",
|
sketch: "Crop Sketch",
|
||||||
mainProductImage: "Crop Main Product Image",
|
mainProductImage: "Crop Main Product Image",
|
||||||
cover: "Crop Cover",
|
cover: "Crop Cover",
|
||||||
@@ -296,17 +411,28 @@
|
|||||||
cropType.value = type
|
cropType.value = type
|
||||||
imageClipDialogRef.value.open(
|
imageClipDialogRef.value.open(
|
||||||
data,
|
data,
|
||||||
(file) => {
|
(file: File, coverFrom?: CoverSourceType) => {
|
||||||
// console.log(file)
|
// console.log(file)
|
||||||
uploadFile(file).then((res) => {
|
uploadFile(file).then((res) => {
|
||||||
if (type === "apparel" && typeof index !== "undefined") {
|
if (type === "apparel" && typeof index !== "undefined") {
|
||||||
selectList.value[currentIndex.value].sketchList[index].url = res
|
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 {
|
} else {
|
||||||
selectList.value[currentIndex.value][type] = res
|
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
|
origin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -358,49 +484,107 @@
|
|||||||
return true
|
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<Record<ListingImageCategory, number>> = {}
|
||||||
|
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 handleSaveForm = async (type: StatusType) => {
|
||||||
const paramsList = []
|
const paramsList = selectList.value.map((item: ListingItem) => {
|
||||||
selectList.value.forEach((item: ListingItem) => {
|
return {
|
||||||
const params = {
|
|
||||||
id: itemId.value,
|
id: itemId.value,
|
||||||
title: item.productName,
|
title: item.productName,
|
||||||
description: item.desc,
|
description: item.desc,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
status: type === "draft" ? 0 : 1,
|
status: type === "draft" ? 0 : 1,
|
||||||
images: [],
|
images: buildListingImages(item),
|
||||||
designFor: (item.gender || "FEMALE").toLowerCase(),
|
designFor: getListingDesignFor(item.gender),
|
||||||
productCategory: item.category
|
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)
|
await fetchUpdateListing(paramsList)
|
||||||
}
|
}
|
||||||
@@ -438,13 +622,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchItemDetial = (list) => {
|
const handleFetchItemDetial = (list) => {
|
||||||
fetchSketchDetail(list).then((res) => {
|
fetchSketchDetail(list).then((res: SketchDetailResponse[]) => {
|
||||||
res.forEach((item, index) => {
|
res.forEach((item, index) => {
|
||||||
if (!selectList.value[index]) return
|
if (!selectList.value[index]) return
|
||||||
selectList.value[index].sketchList = item.clothes.map((el) => ({ url: el }))
|
selectList.value[index].sketchList = (item.clothes || []).map((el) => ({ url: el }))
|
||||||
selectList.value[index].prodImageList = item.toProductImageUrls.map((el) => ({
|
const imageItems = (item.toProductImageUrls || []).map((el) => ({
|
||||||
url: el
|
url: el,
|
||||||
|
selected: false
|
||||||
}))
|
}))
|
||||||
|
const videoItems = (item.videos || [])
|
||||||
|
.map((video) => createProductVideoItem(video))
|
||||||
|
.filter((video): video is ProductMediaItem => Boolean(video))
|
||||||
|
selectList.value[index].prodImageList = [...imageItems, ...videoItems]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,27 @@ export type RadioOption = {
|
|||||||
|
|
||||||
export type TopImageType = "sketch" | "mainProductImage" | "cover"
|
export type TopImageType = "sketch" | "mainProductImage" | "cover"
|
||||||
export type CropType = TopImageType | "apparel"
|
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
|
||||||
|
selected?: boolean
|
||||||
|
isVideo?: boolean
|
||||||
|
videoUrl?: string
|
||||||
|
gifUrl?: string
|
||||||
|
firstFrameUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ListingItem = {
|
export type ListingItem = {
|
||||||
designItemId: number | string | null
|
designItemId: number | string | null
|
||||||
@@ -20,18 +41,18 @@ export type ListingItem = {
|
|||||||
desc: string
|
desc: string
|
||||||
gender: string
|
gender: string
|
||||||
category: string[] | null
|
category: string[] | null
|
||||||
|
coverFrom: CoverSourceType
|
||||||
firstSelectedIndex: number | null
|
firstSelectedIndex: number | null
|
||||||
prodImageList: Array<{
|
prodImageList: ProductMediaItem[]
|
||||||
url: string
|
|
||||||
selected?: boolean
|
|
||||||
}>
|
|
||||||
sketchList: Array<{ url: string | null }>
|
sketchList: Array<{ url: string | null }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListingDetailImage = {
|
export type ListingDetailImage = {
|
||||||
category?: string | null
|
category?: ListingImageCategory | string | null
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
isSelected?: boolean | number | string | null
|
isSelected?: boolean | number | string | null
|
||||||
|
isSeleted?: boolean | number | string | null
|
||||||
|
selected?: boolean | number | string | null
|
||||||
sortOrder?: number | null
|
sortOrder?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,4 +66,17 @@ export type ListingDetailResponse = {
|
|||||||
images?: ListingDetailImage[] | null
|
images?: ListingDetailImage[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SketchDetailVideo = {
|
||||||
|
firstFrameUrl?: string | null
|
||||||
|
gifUrl?: string | null
|
||||||
|
videoUrl?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SketchDetailResponse = {
|
||||||
|
clothes?: string[] | null
|
||||||
|
designItemId?: number | string | null
|
||||||
|
toProductImageUrls?: string[] | null
|
||||||
|
videos?: SketchDetailVideo[] | null
|
||||||
|
}
|
||||||
|
|
||||||
export type StatusType = "draft" | "publish"
|
export type StatusType = "draft" | "publish"
|
||||||
|
|||||||
Reference in New Issue
Block a user