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>
<a-modal
class="editDesignType_modal generalModel"
:class="{
posetransfer: isPoseTransfer
}"
v-model:visible="scaleImage"
:footer="null"
width="78%"
@@ -48,7 +51,10 @@
<div class="scaleImage_content" v-if="scaleImage">
<div class="productImg_modal">
<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'">
{{ $t('ProductImg.MagicTools') }}
</span>
@@ -141,10 +147,7 @@
v-model="productimgBrightenValue"
/>
</div>
<div
class="prompt-container"
v-show="scaleImageList[scaleImageIndex]?.resultType != 'PoseTransfer'"
>
<div class="prompt-container" v-show="!isPoseTransfer">
<div class="prompt-title">{{ $t('ProductImg.Prompt') }}</div>
<div class="input_border productImg_content_item_generate">
<div class="input_box">
@@ -165,40 +168,116 @@
</div>
</div>
</div>
<div
class="transferPose"
v-show="scaleImageList[scaleImageIndex]?.resultType == 'PoseTransfer'"
>
<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)"
<template v-if="isPoseTransfer">
<div class="video-type-container">
<div class="title">{{ $t('poseTransfer.VideoType') }}</div>
<a-select
class="video-type-selection"
v-model:value="videoType"
size="large"
placeholder="Please select"
>
<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>
<a-select-option
v-for="item in options"
:key="item.value"
: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 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 class="frame-item last" :class="{ showupload: !lastFrameImage }">
<img
v-if="lastFrameImage"
:src="
lastFrameImage.url ||
lastFrameImage.designOutfitUrl ||
lastFrameImage.productImage
"
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 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 started_btn" v-show="!productimgIsProductimg">
<i
@@ -232,7 +311,7 @@
</div>
<div class="scaleImage_content_imgBox" :class="{ active: isComparison }">
<img
v-if="isComparison"
v-if="showCompare"
:src="selectGenerate?.sourceUrl || selectGenerate?.productImage"
/>
<div class="loadBox" v-show="selectGenerate">
@@ -245,13 +324,31 @@
:src="generateCourse?.url || selectGenerate?.url"
alt=""
/>
<img
v-show="selectGenerate?.resultType == 'PoseTransfer'"
:src="generateCourse?.firstFrameUrl || selectGenerate?.firstFrameUrl"
alt=""
@mouseenter.stop="gifPlay($event, generateCourse || selectGenerate)"
@mouseleave.stop="gifPause($event, generateCourse || selectGenerate)"
/>
<div
v-show="
selectGenerate?.resultType === 'PoseTransfer' &&
(generateCourse?.videoUrl || selectGenerate?.videoUrl)
"
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>
@@ -260,6 +357,13 @@
<a-spin size="large" />
</div>
<Prompt v-model:showModal="showPromptAssist" :promptList="promptTextList" />
<Product
v-model:showModal="showProductList"
:frameList="fullProductImages"
:type="productType"
:key="productType"
@confirm="handleConfirmProduct"
/>
</a-modal>
</template>
@@ -281,9 +385,15 @@ import { downloadIamge, getMinioUrl, downloadVideoWithFetch } from '@/tool/util'
import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
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({
components: { Prompt },
components: { Prompt, promptInput, Product },
props: {
productData: {
type: Object,
@@ -314,6 +424,27 @@ export default defineComponent({
})
let { t } = useI18n()
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({
isGenerate: false, //判断是否进行generate
textarea: null as any,
@@ -391,7 +522,7 @@ export default defineComponent({
}
let scaleImage: any = ref(false)
let loadingShow = ref(false)
let isComparison = ref(false)
let isComparison = ref(false) // home/design组件中修改的,当前组件没有修改
const visible = ref<boolean>(false)
const setVisible = (value: any): void => {
visible.value = value
@@ -477,14 +608,14 @@ export default defineComponent({
return obj
}
const getPoseTransformData = () => {
return {
const params: any = {
poseId: productimg.selectPose,
projectId: productimg.selectObject.id,
productImage: getMinioUrl(
productimg.scaleImageList[productimg.scaleImageIndex].sourceUrl
),
modelName: speed.speedData.value,
mode: 1,
mode: videoType.value,
parentId:
productimg.selectGenerate.parentId || productimg.selectGenerate.userLikeSortId,
userLikeSortId:
@@ -493,6 +624,38 @@ export default defineComponent({
: null,
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 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) => {
productimg.poseList.forEach((poseItem: any) => {
poseItem.isChecked = false
@@ -960,6 +1161,51 @@ export default defineComponent({
const newHeight = textarea.scrollHeight
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(() => {
clearInterval(prductimgTime)
clearInterval(remPrductimgTime)
@@ -992,13 +1238,31 @@ export default defineComponent({
setVideoRef,
handlePlayMotion,
isVideoPlaying,
setGenerateResultVideoRef,
handlePlayGenerateResult,
isGenerateResultVideoPlaying,
generateProceedList,
init,
cancelDsign,
prductimgTime,
remPrductimgTime,
ifMaximumLength,
textareaRef
textareaRef,
videoType,
options,
showMotion,
prompt,
promptInputRef,
firstFrameImage,
lastFrameImage,
handleOpenProduct,
handleConfirmProduct,
handleDeleteFrame,
productType,
showProductList,
fullProductImages,
isPoseTransfer,
showCompare
}
},
data() {
@@ -1091,8 +1355,13 @@ export default defineComponent({
.editDesignType_modal {
overflow: visible !important;
}
.generalModel.posetransfer {
.ant-modal-body {
padding: 4.5rem 7rem;
}
}
</style>
<style lang="less">
<style lang="less" scoped>
.editDesignType_modal {
.ant-modal-body {
display: flex;
@@ -1154,9 +1423,6 @@ export default defineComponent({
font-weight: 500;
}
}
&.productImg_content_item_title_similarity {
// margin-bottom: 8rem;
}
}
.productImg_content_item_Direction {
padding-bottom: 1rem;
@@ -1332,6 +1598,87 @@ export default defineComponent({
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 {
margin-top: 4rem;
margin-bottom: 3rem;
@@ -1340,18 +1687,20 @@ export default defineComponent({
padding-right: 0.8rem;
.prompt-title {
margin-bottom: 1.4rem;
font-size: 1.6rem;
font-size: 1.7rem;
color: #000;
font-weight: 500;
}
.input_border {
padding-bottom: 0;
.input_box {
.input_box_btnBox {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
display: flex;
flex-direction: column;
align-items: flex-start;
.textarea {
width: 100%;
width: 100%;
min-height: 12.7rem;
max-height: 14rem;
overflow-y: auto;
@@ -1401,6 +1750,47 @@ export default defineComponent({
justify-content: center;
> .imgBox {
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 {
@@ -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>

View File

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

View File

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