feat: design跳转视频生成弹窗

This commit is contained in:
zhangyh
2025-11-18 11:31:03 +08:00
parent 026a39f1a9
commit e1e884c913
3 changed files with 499 additions and 15 deletions

View File

@@ -0,0 +1 @@
<svg t="1763432312095" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4678" width="200" height="200"><path d="M509.92 176C325.504 176 176 325.504 176 509.92c0 184.416 149.504 333.92 333.92 333.92 184.416 0 333.92-149.504 333.92-333.92C843.84 325.504 694.32 176 509.92 176z m166.64 214.848a16 16 0 0 1 22.624 0l11.328 11.312a16 16 0 0 1 0 22.624l-254.08 254.08a16 16 0 0 1-22.624 0l-159.616-159.632a16 16 0 0 1 0-22.624l11.312-11.312a16 16 0 0 1 22.624 0l136.992 136.992z" fill="currentColor" p-id="4679"></path></svg>

After

Width:  |  Height:  |  Size: 562 B

View File

@@ -0,0 +1,248 @@
<template>
<a-modal
class="product-modal generalModel"
v-model:visible="showModal"
:footer="null"
width="78%"
:maskClosable="false"
:centered="true"
:closable="false"
wrapClassName="#app"
:keyboard="false"
>
<div class="generalModel_btn">
<div class="generalModel_closeIcon" @click.stop="handleClose()">
<svg
width="100%"
height="100%"
viewBox="0 0 46 46"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3" />
<rect
x="32.5063"
y="12"
width="3"
height="29"
rx="1.5"
transform="rotate(45 32.5063 12)"
fill="white"
/>
<rect
x="34.6274"
y="32.5059"
width="3"
height="29"
rx="1.5"
transform="rotate(135 34.6274 32.5059)"
fill="white"
/>
</svg>
</div>
</div>
<div class="content-wrapper">
<div class="title-container">
<div class="title">{{ type === 'first' ? 'First Frame' : 'Last Frame' }}</div>
</div>
<div class="content-container">
<div class="video-frames-wrapper">
<div class="video-frames-scroll">
<div
v-for="(frame, index) in frameList"
:key="index"
class="frame-item"
@click="selectFrame(index)"
>
<SvgIcon
v-if="selectedFrameIndex === index"
class="check-icon"
name="CCheck"
color="#000000"
size="24"
/>
<img v-if="frame.url" :src="frame.url" alt="" />
<div v-else class="frame-placeholder"></div>
</div>
</div>
</div>
</div>
<div class="action-container">
<button class="confirm-btn" @click="handleConfirm">Confirm</button>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
const props = defineProps<{
frameList: Array<{ url?: string }>
type: String // first last
}>()
const showModal = defineModel<boolean>('showModal', { required: true })
const selectedFrameIndex = ref<number | null>(null)
const emits = defineEmits(['confirm'])
// 当 type 改变时,重置选中状态
watch(() => props.type, () => {
selectedFrameIndex.value = null
})
// 当弹窗关闭时,重置选中状态
watch(() => showModal.value, (newVal) => {
if (!newVal) {
selectedFrameIndex.value = null
}
})
const handleClose = () => {
showModal.value = false
}
const selectFrame = (index: number) => {
selectedFrameIndex.value = index
}
const handleConfirm = () => {
if (selectedFrameIndex.value !== null) {
// 触发确认事件或回调
const selected = props.frameList[selectedFrameIndex.value]
emits('confirm', { data: selected })
}
handleClose()
}
</script>
<style lang="less" scoped>
.product-modal {
.content-wrapper {
// padding: 0;
position: relative;
// margin-top: 1.36rem;
height: 100%;
display: flex;
flex-direction: column;
.title-container {
.title {
font-size: 2.4rem;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
}
.content-container {
margin-top: 5.8rem;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
.video-frames-wrapper {
background: #ffffff;
border-radius: 2rem;
padding: 1.2rem 2.88rem;
flex: 1;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
.video-frames-scroll {
display: flex;
flex-wrap: wrap;
gap: 3.1rem;
align-content: flex-start;
overflow-y: auto;
overflow-x: auto;
flex: 1;
min-height: 0;
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
&::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 0.2rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
.frame-item {
flex-shrink: 0;
width: 16.47rem;
height: 24.6rem;
border-radius: 0;
border: 0.1rem solid #999999;
overflow: hidden;
cursor: pointer;
position: relative;
overflow: visible;
.check-icon {
position: absolute;
bottom: 0;
right: 0;
transform: translate(50%, 50%);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.frame-placeholder {
width: 100%;
height: 100%;
background: #f5f5f5;
}
}
}
}
}
}
.action-container {
display: flex;
justify-content: flex-end;
.confirm-btn {
width: 8.6rem;
height: 4rem;
background: #000000;
color: #ffffff;
border: none;
border-radius: 2rem;
font-size: 1.2rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
}
}
:deep(.generalModel .ant-modal-body) {
padding: 5rem 4.4rem 8rem 4.6rem;
}
.c-svg {
width: initial;
height: initial;
}
</style>

View File

@@ -31,7 +31,7 @@
</div>
<div class="imgBox" v-mousewheel>
<div
class="item"
class="item first-frame"
:class="{ active: item.id == selectImg.id }"
v-for="(item, index) in showFirstFrameList"
@click="selectImgItem(item)"
@@ -52,23 +52,23 @@
v-show="item.designOutfitUrl || item.imgUrl || item.url"
class="btnBox"
>
<div :class="{ active: item.isChecked }">
<div :class="{ active: item.isChecked }" v-if="!(isDesignPage && videoType === 3)">
<i class="fi fi-br-check"></i>
</div>
<div
@click.stop="setUploadDelete(item, index)"
v-if="source != 'design'"
v-if="source != 'design' || (isDesignPage && videoType === 3)"
>
<i class="fi fi-rr-trash icon_delete"></i>
</div>
</div>
</div>
<div
class="upload_item item"
v-show="
!isDesignPage && !(videoType === 3 && showFirstFrameList.length > 0)
"
>
<div class="upload_item item first_frames" v-show="showFirstFrameUpload">
<div
class="design-mask"
v-if="isDesignPage && videoType === 3"
@click="handleClickDesignMask('first')"
/>
<div class="upload_file_item">
<a-upload
key="common"
@@ -93,9 +93,14 @@
</div>
<template v-if="videoType === 3">
<div
v-show="lastFrameList?.length < 1"
class="upload_item item last_frames"
v-show="!isDesignPage && lastFrameList?.length < 1"
>
<div
class="design-mask"
v-if="isDesignPage && videoType === 3"
@click="handleClickDesignMask('last')"
/>
<div class="upload_file_item">
<a-upload
key="lastframes"
@@ -142,12 +147,12 @@
v-show="item.designOutfitUrl || item.imgUrl || item.url"
class="btnBox"
>
<div :class="{ active: item.isChecked }">
<div :class="{ active: item.isChecked }" v-if="!(isDesignPage && videoType === 3)">
<i class="fi fi-br-check"></i>
</div>
<div
@click.stop="setUploadDelete(item, index, true)"
v-if="source != 'design'"
v-if="source != 'design' || (isDesignPage && videoType === 3)"
>
<i class="fi fi-rr-trash icon_delete"></i>
</div>
@@ -277,6 +282,13 @@
</div>
</div>
<Tips v-model:showModal="showTips" />
<Product
v-model:showModal="showProductList"
:frameList="productFrameList"
:type="productType"
:key="productType"
@confirm="handleConfirmProduct"
/>
</div>
</template>
<script lang="ts">
@@ -304,13 +316,15 @@ import showViewVideo from '@/tool/mount'
import router from '@/router'
import promptInput from './promptInput.vue'
import Tips from './Tips.vue'
import Product from './Product.vue'
import { getFirstFrame, getFirstAndLastFrame } from './prompt'
export default defineComponent({
components: {
generalDrag,
promptInput,
Tips
Tips,
Product
},
props: {
isDesignPage: {
@@ -437,6 +451,7 @@ export default defineComponent({
if (props.isDesignPage) {
//标识design页面打开的tools
data.fileList = designList
console.log('11111111111111', data.fileList)
} else {
data.currentList = store.state.UploadFilesModule.modularData.toProduct
data.currentList = data.currentList ? data.currentList : []
@@ -881,6 +896,57 @@ export default defineComponent({
document.removeEventListener('click', openSpeed)
}
const setUploadDelete = (item: any, index: any, isLastFrame: boolean = false) => {
// 在 isDesignPage 模式且 videoType === 3 时,直接从列表中删除,并将图片重新添加到产品列表中以便重新选择
if (props.isDesignPage && videoType.value === 3) {
if (isLastFrame) {
// 从 lastFrameList 中删除
const deleteIndex = lastFrameList.value.findIndex((listItem: any) => listItem.id === item.id)
if (deleteIndex >= 0) {
lastFrameList.value.splice(deleteIndex, 1)
}
// 将删除的项重新添加到 lastFrameProductList 中,以便可以重新选择
const productItem = { ...item }
// 移除 frameType 和 isChecked 属性
delete productItem.frameType
delete productItem.isChecked
// 检查是否已存在,避免重复添加
const existingIndex = lastFrameProductList.value.findIndex(
(listItem: any) => listItem.id === productItem.id
)
if (existingIndex < 0) {
lastFrameProductList.value.unshift(productItem)
}
// 清空选中状态
if (data.lastSelectImg?.id === item.id) {
data.lastSelectImg = {}
}
} else {
// 从 firstFrameList 中删除
const deleteIndex = firstFrameList.value.findIndex((listItem: any) => listItem.id === item.id)
if (deleteIndex >= 0) {
firstFrameList.value.splice(deleteIndex, 1)
}
// 将删除的项重新添加到 firstFrameProductList 中,以便可以重新选择
const productItem = { ...item }
// 移除 frameType 和 isChecked 属性
delete productItem.frameType
delete productItem.isChecked
// 检查是否已存在,避免重复添加
const existingIndex = firstFrameProductList.value.findIndex(
(listItem: any) => listItem.id === productItem.id
)
if (existingIndex < 0) {
firstFrameProductList.value.unshift(productItem)
}
// 清空选中状态
if (data.selectImg?.id === item.id) {
data.selectImg = {}
}
}
return
}
// 非 design 页面的原有逻辑
let value = {
id: item.id
}
@@ -920,9 +986,148 @@ export default defineComponent({
const firstFrameList = ref([])
const lastFrameList = ref([])
const showFirstFrameList = computed(() => {
if(props.isDesignPage) return data.fileList
if (props.isDesignPage) {
// 在 design 页面,如果 videoType === 3显示 firstFrameList否则显示 data.fileList
return videoType.value === 3 ? firstFrameList.value : data.fileList
}
return videoType.value === 3 ? firstFrameList.value : data.fileList
})
const showProductList = ref(false)
const productType = ref('first')
// 首帧和尾帧的独立数据源
const firstFrameProductList = ref([])
const lastFrameProductList = ref([])
const showFirstFrameUpload = computed(() => {
// 只读依赖
const isDesign = props.isDesignPage
const isFirstAndLast = videoType.value === 3
const hasFirstFrames = Array.isArray(showFirstFrameList.value)
? showFirstFrameList.value.length > 0
: false
if (isDesign) {
// design 页面下,仅 videoType 为 3 时可展示上传,且没有首帧时显示上传按钮
return isFirstAndLast && !hasFirstFrames
}
// 非 design 页面下也是类似逻辑
return !(isFirstAndLast && hasFirstFrames)
})
const showLastFrameUpload = computed(() => {
if (videoType.value !== 3) return false
if (props.isDesignPage && lastFrameList.length > 0) return false
return true
})
// 根据 productType 返回对应的数据源
const productFrameList = computed(() => {
if (productType.value === 'first') {
return firstFrameProductList.value.length > 0
? firstFrameProductList.value
: data.fileList
} else {
return lastFrameProductList.value.length > 0
? lastFrameProductList.value
: data.fileList
}
})
const handleClickDesignMask = (type: 'first' | 'last') => {
productType.value = type
// 初始化独立的数据源,如果为空则从 fileList 复制
if (type === 'first') {
if (firstFrameProductList.value.length === 0) {
firstFrameProductList.value = JSON.parse(JSON.stringify(data.fileList || []))
}
} else {
if (lastFrameProductList.value.length === 0) {
lastFrameProductList.value = JSON.parse(JSON.stringify(data.fileList || []))
}
}
showProductList.value = true
}
const handleConfirmProduct = ({ data: productData }) => {
if (productType.value === 'first') {
// 准备图片数据
const imageItem: any = {
...productData,
isChecked: true,
type: 'ProductElement'
}
// 设置 minioUrl
imageItem.minioUrl =
productData.miniourl || getMinioUrl(imageItem.url || imageItem.imgUrl)
// 更新首帧产品列表,移除已选择的项
const productIndex = firstFrameProductList.value.findIndex(
(item: any) => item.id === productData.id
)
if (productIndex >= 0) {
firstFrameProductList.value.splice(productIndex, 1)
}
// 首尾帧模式,添加到 firstFrameList
// 先取消其他项的选中状态
firstFrameList.value.forEach((listItem: any) => {
listItem.isChecked = false
})
// 检查是否已存在,如果存在则更新,否则添加
const existingIndex = firstFrameList.value.findIndex(
(item: any) => item.id === productData.id
)
if (existingIndex >= 0) {
Object.assign(firstFrameList.value[existingIndex], imageItem)
} else {
imageItem.frameType = 'first'
firstFrameList.value.push(imageItem)
}
// 赋值给首帧选中对象
data.selectImg = { ...imageItem }
} else if (productType.value === 'last') {
// 准备图片数据
const imageItem: any = {
...productData,
isChecked: true,
type: 'ProductElement',
frameType: 'last'
}
// 设置 minioUrl
imageItem.minioUrl =
productData.miniourl || getMinioUrl(imageItem.url || imageItem.imgUrl)
// 更新尾帧产品列表,移除已选择的项
const productIndex = lastFrameProductList.value.findIndex(
(item: any) => item.id === productData.id
)
if (productIndex >= 0) {
lastFrameProductList.value.splice(productIndex, 1)
}
// 先取消其他项的选中状态
lastFrameList.value.forEach((listItem: any) => {
listItem.isChecked = false
})
// 检查是否已存在,如果存在则更新,否则添加
const existingIndex = lastFrameList.value.findIndex(
(item: any) => item.id === productData.id
)
if (existingIndex >= 0) {
Object.assign(lastFrameList.value[existingIndex], imageItem)
} else {
lastFrameList.value.push(imageItem)
}
// 赋值给尾帧选中对象
data.lastSelectImg = { ...imageItem }
}
showProductList.value = false
}
watch(
() => store.state.HomeStoreModule.uploadElement.length,
(newVal, oldVal) => {
@@ -952,6 +1157,20 @@ export default defineComponent({
}
)
// 监听 fileList 的变化,如果独立列表为空则重新初始化
watch(
() => data.fileList,
newFileList => {
if (firstFrameProductList.value.length === 0 && newFileList?.length > 0) {
firstFrameProductList.value = JSON.parse(JSON.stringify(newFileList))
}
if (lastFrameProductList.value.length === 0 && newFileList?.length > 0) {
lastFrameProductList.value = JSON.parse(JSON.stringify(newFileList))
}
},
{ deep: true }
)
watch(
() => store.state.HomeStoreModule.lastFrameList,
val => {
@@ -1039,7 +1258,14 @@ export default defineComponent({
setVideoRef,
handlePlayMotion,
isVideoPlaying,
showTips
showTips,
showProductList,
handleClickDesignMask,
productType,
handleConfirmProduct,
productFrameList,
showFirstFrameUpload,
showLastFrameUpload
}
},
directives: {
@@ -1239,6 +1465,15 @@ export default defineComponent({
}
> .upload_item {
border: none;
position: relative;
.design-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
}
.control-container {
width: 100%;