feat: 视频显示到列表
This commit is contained in:
@@ -1744,7 +1744,7 @@ export default {
|
|||||||
cover: "封面",
|
cover: "封面",
|
||||||
productImageDesc: '从产品图中选取',
|
productImageDesc: '从产品图中选取',
|
||||||
cropDesc: '从主产品图或线稿图中裁剪',
|
cropDesc: '从主产品图或线稿图中裁剪',
|
||||||
productImageMainTitle: '产品图 ',
|
productImageMainTitle: '产品图/视频',
|
||||||
productImageSubTitle: ' (来自设计集)',
|
productImageSubTitle: ' (来自设计集)',
|
||||||
apparelSketchTitle: '服装线稿图 ',
|
apparelSketchTitle: '服装线稿图 ',
|
||||||
apparelSketchSubTitle: ' (来自设计集)',
|
apparelSketchSubTitle: ' (来自设计集)',
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { Https } from "@/tool/https"
|
import { Https } from "@/tool/https"
|
||||||
|
import type { 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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -91,7 +91,10 @@
|
|||||||
ListingDetailImage,
|
ListingDetailImage,
|
||||||
ListingDetailResponse,
|
ListingDetailResponse,
|
||||||
ListingItem,
|
ListingItem,
|
||||||
|
ProductMediaItem,
|
||||||
RadioOption,
|
RadioOption,
|
||||||
|
SketchDetailResponse,
|
||||||
|
SketchDetailVideo,
|
||||||
StatusType
|
StatusType
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
@@ -127,12 +130,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
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[]> = {
|
||||||
@@ -249,6 +247,23 @@
|
|||||||
return listing
|
return listing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createProductVideoItem = (video: SketchDetailVideo): 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: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 +272,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
|
||||||
@@ -438,13 +457,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]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ 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 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
|
||||||
sketch: string | null
|
sketch: string | null
|
||||||
@@ -21,10 +30,7 @@ export type ListingItem = {
|
|||||||
gender: string
|
gender: string
|
||||||
category: string[] | null
|
category: string[] | null
|
||||||
firstSelectedIndex: number | null
|
firstSelectedIndex: number | null
|
||||||
prodImageList: Array<{
|
prodImageList: ProductMediaItem[]
|
||||||
url: string
|
|
||||||
selected?: boolean
|
|
||||||
}>
|
|
||||||
sketchList: Array<{ url: string | null }>
|
sketchList: Array<{ url: string | null }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,4 +51,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