feat: 弹窗视频生成

This commit is contained in:
zhangyh
2025-11-19 10:31:26 +08:00
parent f425dcec39
commit 1edcef49f8
3 changed files with 455 additions and 55 deletions

View File

@@ -1,6 +1,9 @@
<template> <template>
<a-modal <a-modal
class="editDesignType_modal generalModel" class="editDesignType_modal generalModel"
:class="{
posetransfer: isPoseTransfer
}"
v-model:visible="scaleImage" v-model:visible="scaleImage"
:footer="null" :footer="null"
width="78%" width="78%"
@@ -48,7 +51,10 @@
<div class="scaleImage_content" v-if="scaleImage"> <div class="scaleImage_content" v-if="scaleImage">
<div class="productImg_modal"> <div class="productImg_modal">
<div class="productImg_left generalModel_state"> <div class="productImg_left generalModel_state">
<div class="productImg_content_item_title productImg_content_item_title_menu"> <div
v-if="!isPoseTransfer"
class="productImg_content_item_title productImg_content_item_title_menu"
>
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'"> <span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">
{{ $t('ProductImg.MagicTools') }} {{ $t('ProductImg.MagicTools') }}
</span> </span>
@@ -141,10 +147,7 @@
v-model="productimgBrightenValue" v-model="productimgBrightenValue"
/> />
</div> </div>
<div <div class="prompt-container" v-show="!isPoseTransfer">
class="prompt-container"
v-show="scaleImageList[scaleImageIndex]?.resultType != 'PoseTransfer'"
>
<div class="prompt-title">{{ $t('ProductImg.Prompt') }}</div> <div class="prompt-title">{{ $t('ProductImg.Prompt') }}</div>
<div class="input_border productImg_content_item_generate"> <div class="input_border productImg_content_item_generate">
<div class="input_box"> <div class="input_box">
@@ -165,40 +168,116 @@
</div> </div>
</div> </div>
</div> </div>
<div <template v-if="isPoseTransfer">
class="transferPose" <div class="video-type-container">
v-show="scaleImageList[scaleImageIndex]?.resultType == 'PoseTransfer'" <div class="title">{{ $t('poseTransfer.VideoType') }}</div>
> <a-select
<div class="head"> class="video-type-selection"
<div class="text">{{ $t('poseTransfer.Selectpose') }}</div> v-model:value="videoType"
</div> size="large"
<div class="imgBox" v-mousewheel> placeholder="Please select"
<div
class="item"
v-for="(item, index) in poseList"
:key="item.id"
@click="setSelectPose(item, index)"
> >
<video :ref="el => setVideoRef(item.id, el)" :src="item.video" /> <a-select-option
<div class="btnBox"> v-for="item in options"
<div :class="{ active: item.isChecked }"> :key="item.value"
<i class="fi fi-br-check"></i> :value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="upload-container" v-show="videoType === 3">
<div class="title">{{ $t('poseTransfer.SelectDesign') }}</div>
<div class="frames-list">
<div class="frame-item first" :class="{ showupload: !firstFrameImage }">
<img
v-if="firstFrameImage"
:src="
firstFrameImage.url ||
firstFrameImage.designOutfitUrl ||
firstFrameImage.productImage
"
alt=""
/>
<div
class="upload-placeholder"
v-show="!firstFrameImage"
@click="handleOpenProduct('first')"
>
<i class="fi fi-br-upload upload-icon"></i>
</div>
<div
v-if="firstFrameImage"
class="delete-btn"
@click.stop="handleDeleteFrame('first')"
>
<i class="fi fi-rr-trash icon_delete"></i>
</div> </div>
</div> </div>
<div class="control-container" @click.stop> <div class="frame-item last" :class="{ showupload: !lastFrameImage }">
<div class="icon-list"> <img
<SvgIcon v-if="lastFrameImage"
class="play-icon" :src="
@click.stop="handlePlayMotion(item)" lastFrameImage.url ||
:name="isVideoPlaying(item.id) ? 'CPause' : 'CPlay'" lastFrameImage.designOutfitUrl ||
size="20" lastFrameImage.productImage
color="#ffffff" "
/> alt=""
/>
<div
class="upload-placeholder"
v-show="!lastFrameImage"
@click="handleOpenProduct('last')"
>
<i class="fi fi-br-upload upload-icon"></i>
</div>
<div
v-if="lastFrameImage"
class="delete-btn"
@click.stop="handleDeleteFrame('last')"
>
<i class="fi fi-rr-trash icon_delete"></i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="prompt-input-container" v-show="!showMotion">
<div class="title">{{ $t('ProductImg.Prompt') }}</div>
<promptInput :content="prompt" ref="promptInputRef" />
</div>
<div class="transferPose" v-show="showMotion">
<div class="head">
<div class="text">{{ $t('poseTransfer.Selectpose') }}</div>
</div>
<div class="imgBox" v-mousewheel>
<div
class="item"
v-for="(item, index) in poseList"
:key="item.id"
@click="setSelectPose(item, index)"
>
<video :ref="el => setVideoRef(item.id, el)" :src="item.video" />
<div class="btnBox">
<div :class="{ active: item.isChecked }">
<i class="fi fi-br-check"></i>
</div>
</div>
<div class="control-container" @click.stop>
<div class="icon-list">
<SvgIcon
class="play-icon"
@click.stop="handlePlayMotion(item)"
:name="isVideoPlaying(item.id) ? 'CPause' : 'CPlay'"
size="20"
color="#ffffff"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<div class="generage_btn_box" style="margin-left: auto"> <div class="generage_btn_box" style="margin-left: auto">
<div class="generage_btn started_btn" v-show="!productimgIsProductimg"> <div class="generage_btn started_btn" v-show="!productimgIsProductimg">
<i <i
@@ -232,7 +311,7 @@
</div> </div>
<div class="scaleImage_content_imgBox" :class="{ active: isComparison }"> <div class="scaleImage_content_imgBox" :class="{ active: isComparison }">
<img <img
v-if="isComparison" v-if="showCompare"
:src="selectGenerate?.sourceUrl || selectGenerate?.productImage" :src="selectGenerate?.sourceUrl || selectGenerate?.productImage"
/> />
<div class="loadBox" v-show="selectGenerate"> <div class="loadBox" v-show="selectGenerate">
@@ -245,13 +324,31 @@
:src="generateCourse?.url || selectGenerate?.url" :src="generateCourse?.url || selectGenerate?.url"
alt="" alt=""
/> />
<img <div
v-show="selectGenerate?.resultType == 'PoseTransfer'" v-show="
:src="generateCourse?.firstFrameUrl || selectGenerate?.firstFrameUrl" selectGenerate?.resultType === 'PoseTransfer' &&
alt="" (generateCourse?.videoUrl || selectGenerate?.videoUrl)
@mouseenter.stop="gifPlay($event, generateCourse || selectGenerate)" "
@mouseleave.stop="gifPause($event, generateCourse || selectGenerate)" class="result-video-container"
/> >
<video
:ref="el => setGenerateResultVideoRef(el)"
:src="generateCourse?.videoUrl || selectGenerate?.videoUrl"
controlslist="nodownload nofullscreen noremoteplayback"
:controls="false"
/>
<div class="control-container" @click.stop>
<div class="icon-list">
<SvgIcon
class="play-icon"
@click.stop="handlePlayGenerateResult"
:name="isGenerateResultVideoPlaying ? 'CPause' : 'CPlay'"
size="20"
color="#ffffff"
/>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -260,6 +357,13 @@
<a-spin size="large" /> <a-spin size="large" />
</div> </div>
<Prompt v-model:showModal="showPromptAssist" :promptList="promptTextList" /> <Prompt v-model:showModal="showPromptAssist" :promptList="promptTextList" />
<Product
v-model:showModal="showProductList"
:frameList="fullProductImages"
:type="productType"
:key="productType"
@confirm="handleConfirmProduct"
/>
</a-modal> </a-modal>
</template> </template>
@@ -281,9 +385,15 @@ import { downloadIamge, getMinioUrl, downloadVideoWithFetch } from '@/tool/util'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import Prompt from '@/component/home/tools/toProduct/Prompt.vue' import Prompt from '@/component/home/tools/toProduct/Prompt.vue'
import promptInput from '@/component/home/tools/poseTransfer/promptInput.vue'
import Product from '@/component/home/tools/poseTransfer/Product.vue'
import {
getFirstFrame,
getFirstAndLastFrame
} from '@/component/home/tools/poseTransfer/prompt'
export default defineComponent({ export default defineComponent({
components: { Prompt }, components: { Prompt, promptInput, Product },
props: { props: {
productData: { productData: {
type: Object, type: Object,
@@ -314,6 +424,27 @@ export default defineComponent({
}) })
let { t } = useI18n() let { t } = useI18n()
const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef') const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef')
const videoType = ref(2)
const showMotion = computed(() => videoType.value === 1)
const options = ref([
{ value: 2, label: t('poseTransfer.FirstFrame') },
{ value: 3, label: t('poseTransfer.FirstAndLastFrames') },
{ value: 1, label: t('poseTransfer.FirstFrameAndSkeleton') }
])
const firstFrameImage = ref<any | null>(null)
const lastFrameImage = ref<any | null>(null)
const promptInputRef = useTemplateRef<HTMLTextAreaElement>('promptInputRef')
const prompt = computed(() => {
if (videoType.value === 2) {
return getFirstFrame(t)
}
if (videoType.value === 3) {
return getFirstAndLastFrame(t)
}
})
let productimg = reactive({ let productimg = reactive({
isGenerate: false, //判断是否进行generate isGenerate: false, //判断是否进行generate
textarea: null as any, textarea: null as any,
@@ -391,7 +522,7 @@ export default defineComponent({
} }
let scaleImage: any = ref(false) let scaleImage: any = ref(false)
let loadingShow = ref(false) let loadingShow = ref(false)
let isComparison = ref(false) let isComparison = ref(false) // home/design组件中修改的,当前组件没有修改
const visible = ref<boolean>(false) const visible = ref<boolean>(false)
const setVisible = (value: any): void => { const setVisible = (value: any): void => {
visible.value = value visible.value = value
@@ -477,14 +608,14 @@ export default defineComponent({
return obj return obj
} }
const getPoseTransformData = () => { const getPoseTransformData = () => {
return { const params: any = {
poseId: productimg.selectPose, poseId: productimg.selectPose,
projectId: productimg.selectObject.id, projectId: productimg.selectObject.id,
productImage: getMinioUrl( productImage: getMinioUrl(
productimg.scaleImageList[productimg.scaleImageIndex].sourceUrl productimg.scaleImageList[productimg.scaleImageIndex].sourceUrl
), ),
modelName: speed.speedData.value, modelName: speed.speedData.value,
mode: 1, mode: videoType.value,
parentId: parentId:
productimg.selectGenerate.parentId || productimg.selectGenerate.userLikeSortId, productimg.selectGenerate.parentId || productimg.selectGenerate.userLikeSortId,
userLikeSortId: userLikeSortId:
@@ -493,6 +624,38 @@ export default defineComponent({
: null, : null,
isDefaultLike: true //表示是否生成就like isDefaultLike: true //表示是否生成就like
} }
// 使用首帧/尾帧选择覆盖默认的源图
if (videoType.value === 2 || videoType.value === 3) {
if (firstFrameImage.value) {
const src =
firstFrameImage.value.miniourl ||
firstFrameImage.value.minioUrl ||
firstFrameImage.value.productImage ||
firstFrameImage.value.url ||
firstFrameImage.value.imgUrl
if (src) {
params.productImage = getMinioUrl(src)
}
}
}
if (videoType.value === 3 && lastFrameImage.value) {
const lastSrc =
lastFrameImage.value.miniourl ||
lastFrameImage.value.minioUrl ||
lastFrameImage.value.productImage ||
lastFrameImage.value.url ||
lastFrameImage.value.imgUrl
if (lastSrc) {
params.lastFrameProductImage = getMinioUrl(lastSrc)
}
}
if (videoType.value !== 1) {
const prompt = promptInputRef.value.getFullText()
params.prompt = prompt
}
return params
} }
let getPrductimg = async () => { let getPrductimg = async () => {
let obj = getData() let obj = getData()
@@ -901,6 +1064,44 @@ export default defineComponent({
} }
} }
} }
// 生成结果视频的引用和播放状态
const generateResultVideoRef = ref<HTMLVideoElement | null>(null)
const isGenerateResultVideoPlaying = ref(false)
// 设置生成结果视频 ref
const setGenerateResultVideoRef = (el: HTMLVideoElement | null) => {
if (el) {
generateResultVideoRef.value = el
// 初始化播放状态
isGenerateResultVideoPlaying.value = !el.paused
// 监听播放事件
el.addEventListener('play', () => {
isGenerateResultVideoPlaying.value = true
})
// 监听暂停事件
el.addEventListener('pause', () => {
isGenerateResultVideoPlaying.value = false
})
// 监听结束事件
el.addEventListener('ended', () => {
isGenerateResultVideoPlaying.value = false
})
}
}
// 播放/暂停生成结果视频
const handlePlayGenerateResult = () => {
const video = generateResultVideoRef.value
if (video) {
if (video.paused) {
video.play().catch(err => {
console.error('播放视频失败:', err)
})
} else {
video.pause()
}
}
}
const setSelectPose = (item: any, index: number) => { const setSelectPose = (item: any, index: number) => {
productimg.poseList.forEach((poseItem: any) => { productimg.poseList.forEach((poseItem: any) => {
poseItem.isChecked = false poseItem.isChecked = false
@@ -960,6 +1161,51 @@ export default defineComponent({
const newHeight = textarea.scrollHeight const newHeight = textarea.scrollHeight
textarea.style.height = newHeight + 'px' textarea.style.height = newHeight + 'px'
} }
const showProductList = ref(false)
const productType = ref('first')
const fullProductImages = computed(() => {
return productimg.likeDesignCollectionList.flatMap(item => item.childList || [])
})
const handleOpenProduct = (type: 'first' | 'last') => {
productType.value = type
showProductList.value = true
}
const handleConfirmProduct = ({ data: productData }: { data: any }) => {
if (!productData) return
if (productType.value === 'first') {
firstFrameImage.value = productData
} else if (productType.value === 'last') {
lastFrameImage.value = productData
}
showProductList.value = false
}
const handleDeleteFrame = (type: 'first' | 'last') => {
if (type === 'first') {
firstFrameImage.value = null
} else if (type === 'last') {
lastFrameImage.value = null
}
}
const isPoseTransfer = computed(() => {
return (
productimg.scaleImageList[productimg.scaleImageIndex]?.resultType ===
'PoseTransfer'
)
})
const showCompare = computed(() => {
// isComparison.value
if (!isPoseTransfer.value) {
return isComparison.value
} else {
return videoType.value === 3 ? false : true
}
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(prductimgTime) clearInterval(prductimgTime)
clearInterval(remPrductimgTime) clearInterval(remPrductimgTime)
@@ -992,13 +1238,31 @@ export default defineComponent({
setVideoRef, setVideoRef,
handlePlayMotion, handlePlayMotion,
isVideoPlaying, isVideoPlaying,
setGenerateResultVideoRef,
handlePlayGenerateResult,
isGenerateResultVideoPlaying,
generateProceedList, generateProceedList,
init, init,
cancelDsign, cancelDsign,
prductimgTime, prductimgTime,
remPrductimgTime, remPrductimgTime,
ifMaximumLength, ifMaximumLength,
textareaRef textareaRef,
videoType,
options,
showMotion,
prompt,
promptInputRef,
firstFrameImage,
lastFrameImage,
handleOpenProduct,
handleConfirmProduct,
handleDeleteFrame,
productType,
showProductList,
fullProductImages,
isPoseTransfer,
showCompare
} }
}, },
data() { data() {
@@ -1091,8 +1355,13 @@ export default defineComponent({
.editDesignType_modal { .editDesignType_modal {
overflow: visible !important; overflow: visible !important;
} }
.generalModel.posetransfer {
.ant-modal-body {
padding: 4.5rem 7rem;
}
}
</style> </style>
<style lang="less"> <style lang="less" scoped>
.editDesignType_modal { .editDesignType_modal {
.ant-modal-body { .ant-modal-body {
display: flex; display: flex;
@@ -1154,9 +1423,6 @@ export default defineComponent({
font-weight: 500; font-weight: 500;
} }
} }
&.productImg_content_item_title_similarity {
// margin-bottom: 8rem;
}
} }
.productImg_content_item_Direction { .productImg_content_item_Direction {
padding-bottom: 1rem; padding-bottom: 1rem;
@@ -1332,6 +1598,87 @@ export default defineComponent({
font-size: 1.8rem; font-size: 1.8rem;
} }
} }
.video-type-container {
margin-bottom: 3rem;
.title {
font-size: 1.7rem;
color: #000;
margin-bottom: 1.4rem;
font-weight: 500;
}
}
.upload-container {
margin-bottom: 2.8rem;
.title {
font-size: 1.7rem;
font-weight: 500;
color: #000;
margin-bottom: 1.4rem;
}
.frames-list {
display: flex;
height: 17.1rem;
column-gap: 2.3rem;
.frame-item {
position: relative;
width: 12.2rem;
height: 17.1rem;
border: 1px solid #d0d0d0;
border-radius: 11.8px;
&.showupload {
border-color: #000;
}
.upload-placeholder {
width: 100%;
height: 100%;
text-align: center;
line-height: 17.1rem;
cursor: pointer;
.upload-icon {
font-size: 3rem;
color: #000;
}
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.delete-btn {
position: absolute;
right: 1rem;
top: 1rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
border-radius: 0.5rem;
cursor: pointer;
z-index: 2;
transition: background 0.3s;
.icon_delete {
font-size: 1.2rem;
color: #fff;
display: flex;
}
}
}
}
}
.prompt-input-container {
margin-bottom: 4rem;
:deep(.promptInput) {
box-sizing: border-box;
}
.title {
font-weight: 500;
color: #000;
font-size: 1.7rem;
margin-bottom: 1.4rem;
}
}
.prompt-container { .prompt-container {
margin-top: 4rem; margin-top: 4rem;
margin-bottom: 3rem; margin-bottom: 3rem;
@@ -1340,18 +1687,20 @@ export default defineComponent({
padding-right: 0.8rem; padding-right: 0.8rem;
.prompt-title { .prompt-title {
margin-bottom: 1.4rem; margin-bottom: 1.4rem;
font-size: 1.6rem; font-size: 1.7rem;
color: #000;
font-weight: 500;
} }
.input_border { .input_border {
padding-bottom: 0; padding-bottom: 0;
.input_box { .input_box {
.input_box_btnBox { .input_box_btnBox {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
.textarea { .textarea {
width: 100%; width: 100%;
min-height: 12.7rem; min-height: 12.7rem;
max-height: 14rem; max-height: 14rem;
overflow-y: auto; overflow-y: auto;
@@ -1401,6 +1750,47 @@ export default defineComponent({
justify-content: center; justify-content: center;
> .imgBox { > .imgBox {
height: 100%; height: 100%;
position: relative;
.result-video-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
video {
width: auto;
height: 100%;
max-width: 50rem;
min-width: 40rem;
object-fit: contain;
}
.control-container {
width: 100%;
height: 3.3rem;
position: absolute;
bottom: 0;
left: 0;
background: linear-gradient(
180deg,
rgba(8, 9, 13, 0) 0%,
rgba(8, 9, 13, 0.27) 80.37%
);
display: flex;
z-index: 2;
.icon-list {
padding-left: 1rem;
display: flex;
box-sizing: border-box;
justify-content: flex-start;
align-items: center;
.play-icon {
width: 2rem;
height: 2rem;
}
}
}
}
} }
} }
img { img {
@@ -1523,4 +1913,13 @@ export default defineComponent({
} }
} }
} }
.video-type-container {
:deep(.video-type-selection) {
width: 100%;
.ant-select-selector {
border: 2px solid #d0d0d0;
border-radius: 1rem;
}
}
}
</style> </style>

View File

@@ -200,7 +200,7 @@ const handleConfirm = () => {
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: contain;
display: block; display: block;
} }

View File

@@ -509,6 +509,7 @@ defineExpose({
border: 2px solid #dcdfe6; border: 2px solid #dcdfe6;
padding: 1.5rem 1.5rem 3rem; padding: 1.5rem 1.5rem 3rem;
height: auto; height: auto;
font-size: 1.8rem;
.area { .area {
width: 100%; width: 100%;
min-height: 12rem; min-height: 12rem;