Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite
This commit is contained in:
1
src/assets/icons/CCheck.svg
Normal file
1
src/assets/icons/CCheck.svg
Normal 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 |
File diff suppressed because it is too large
Load Diff
248
src/component/home/tools/poseTransfer/Product.vue
Normal file
248
src/component/home/tools/poseTransfer/Product.vue
Normal 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>
|
||||
@@ -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,26 @@
|
||||
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 +96,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 +150,15 @@
|
||||
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>
|
||||
@@ -180,19 +191,10 @@
|
||||
<div class="control-container">
|
||||
<div class="icon-list">
|
||||
<SvgIcon
|
||||
v-show="!isVideoPlaying(item.id)"
|
||||
class="play-icon"
|
||||
@click.stop="handlePlayMotion(item)"
|
||||
name="CPlay"
|
||||
size="10"
|
||||
color="#fff"
|
||||
/>
|
||||
<SvgIcon
|
||||
v-show="isVideoPlaying(item.id)"
|
||||
class="play-icon pause"
|
||||
@click.stop="handlePlayMotion(item)"
|
||||
name="CPause"
|
||||
size="10"
|
||||
:name="isVideoPlaying(item.id) ? 'CPause' : 'CPlay'"
|
||||
size="20"
|
||||
color="#fff"
|
||||
/>
|
||||
</div>
|
||||
@@ -277,6 +279,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 +313,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: {
|
||||
@@ -881,6 +892,61 @@ 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%;
|
||||
@@ -1252,26 +1487,27 @@ export default defineComponent({
|
||||
rgba(8, 9, 13, 0.27) 80.37%
|
||||
);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
.icon-list {
|
||||
height: 50%;
|
||||
width: calc(100% - 1.6rem);
|
||||
// height: 50%;
|
||||
// width: calc(100% - 1.6rem);
|
||||
// border-top: 1px solid #fff;
|
||||
padding-left: 1rem;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
.play-icon {
|
||||
width: initial;
|
||||
height: initial;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> .head {
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
@@ -1292,6 +1528,7 @@ export default defineComponent({
|
||||
font-size: 1.8rem;
|
||||
color: #000;
|
||||
margin-bottom: 1.4rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
> .poses {
|
||||
margin-top: 3rem;
|
||||
@@ -1393,6 +1630,7 @@ export default defineComponent({
|
||||
}
|
||||
.video-type-container {
|
||||
margin-bottom: 4rem;
|
||||
font-weight: 500;
|
||||
.title {
|
||||
font-size: 1.8rem;
|
||||
color: #000;
|
||||
|
||||
@@ -1,507 +1,518 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, toRefs, nextTick, useTemplateRef, watch } from "vue";
|
||||
import { ref, onMounted, reactive, toRefs, nextTick, useTemplateRef } from 'vue'
|
||||
|
||||
interface ContentItem {
|
||||
id: string;
|
||||
type: 'text' | 'input';
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
id: string
|
||||
type: 'text' | 'input'
|
||||
value: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
content: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const data = reactive({
|
||||
// content: [
|
||||
// { id: '1', type: 'text', value: '11111' },
|
||||
// { id: '2', type: 'input', value: '222', placeholder: '[请输入内容]' },
|
||||
// { id: '3', type: 'text', value: '333333' },
|
||||
// { id: '4', type: 'input', value: '', placeholder: '[请输入内容]' }
|
||||
// ] as ContentItem[]
|
||||
content: props.content
|
||||
// content: [
|
||||
// { id: '1', type: 'text', value: '11111' },
|
||||
// { id: '2', type: 'input', value: '222', placeholder: '[请输入内容]' },
|
||||
// { id: '3', type: 'text', value: '333333' },
|
||||
// { id: '4', type: 'input', value: '', placeholder: '[请输入内容]' }
|
||||
// ] as ContentItem[]
|
||||
content: props.content
|
||||
})
|
||||
|
||||
const editableArea = ref<HTMLElement>()
|
||||
const { content } = toRefs(data)
|
||||
|
||||
const cursorState = ref({
|
||||
isContainerClick: false
|
||||
isContainerClick: false
|
||||
})
|
||||
|
||||
// 检查并删除末尾的空文本框
|
||||
const removeLastEmptyTextIfNeeded = () => {
|
||||
const lastItem = content.value[content.value.length - 1]
|
||||
if (lastItem && lastItem.type === 'text' && lastItem.value === '') {
|
||||
content.value.pop()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
const lastItem = content.value[content.value.length - 1]
|
||||
if (lastItem && lastItem.type === 'text' && lastItem.value === '') {
|
||||
content.value.pop()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 确保末尾有空文本框
|
||||
const ensureEmptyTextAtEnd = () => {
|
||||
const lastItem = content.value[content.value.length - 1]
|
||||
if (!lastItem || lastItem.type !== 'text' || lastItem.value !== '') {
|
||||
const newItem: ContentItem = {
|
||||
id: Date.now().toString(),
|
||||
type: 'text',
|
||||
value: ''
|
||||
}
|
||||
content.value.push(newItem)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
const lastItem = content.value[content.value.length - 1]
|
||||
if (!lastItem || lastItem.type !== 'text' || lastItem.value !== '') {
|
||||
const newItem: ContentItem = {
|
||||
id: Date.now().toString(),
|
||||
type: 'text',
|
||||
value: ''
|
||||
}
|
||||
content.value.push(newItem)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 元素信息获取
|
||||
const getCurrentElementInfo = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return null
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return null
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
const node = range.startContainer
|
||||
let element: HTMLElement | null = null
|
||||
const range = selection.getRangeAt(0)
|
||||
const node = range.startContainer
|
||||
let element: HTMLElement | null = null
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
element = (node as Text).parentElement
|
||||
} else {
|
||||
element = node as HTMLElement
|
||||
}
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
element = (node as Text).parentElement
|
||||
} else {
|
||||
element = node as HTMLElement
|
||||
}
|
||||
|
||||
if (!element) return null
|
||||
if (!element) return null
|
||||
|
||||
let index = -1
|
||||
let type: 'text' | 'input' = 'text'
|
||||
let isAtStart = false
|
||||
let isAtEnd = false
|
||||
let index = -1
|
||||
let type: 'text' | 'input' = 'text'
|
||||
let isAtStart = false
|
||||
let isAtEnd = false
|
||||
|
||||
if (element.classList.contains('text-field')) {
|
||||
index = parseInt(element.getAttribute('data-index') || '-1')
|
||||
type = 'text'
|
||||
const textContent = element.textContent || ''
|
||||
isAtStart = range.startOffset === 0
|
||||
isAtEnd = range.startOffset === textContent.length
|
||||
} else if (element.classList.contains('input-content')) {
|
||||
const parent = element.parentElement
|
||||
index = parseInt(parent?.getAttribute('data-index') || '-1')
|
||||
type = 'input'
|
||||
if (element.classList.contains('text-field')) {
|
||||
index = parseInt(element.getAttribute('data-index') || '-1')
|
||||
type = 'text'
|
||||
const textContent = element.textContent || ''
|
||||
isAtStart = range.startOffset === 0
|
||||
isAtEnd = range.startOffset === textContent.length
|
||||
} else if (element.classList.contains('input-content')) {
|
||||
const parent = element.parentElement
|
||||
index = parseInt(parent?.getAttribute('data-index') || '-1')
|
||||
type = 'input'
|
||||
|
||||
const item = content.value[index]
|
||||
if (element.classList.contains('has-placeholder')) {
|
||||
// placeholder状态下,光标在任意位置都认为是"在元素内"
|
||||
isAtStart = range.startOffset === 0
|
||||
isAtEnd = true
|
||||
} else {
|
||||
// 正常内容状态
|
||||
const textContent = item.value
|
||||
isAtStart = range.startOffset === 0
|
||||
isAtEnd = range.startOffset === textContent.length
|
||||
}
|
||||
}
|
||||
const item = content.value[index]
|
||||
if (element.classList.contains('has-placeholder')) {
|
||||
// placeholder状态下,光标在任意位置都认为是"在元素内"
|
||||
isAtStart = range.startOffset === 0
|
||||
isAtEnd = true
|
||||
} else {
|
||||
// 正常内容状态
|
||||
const textContent = item.value
|
||||
isAtStart = range.startOffset === 0
|
||||
isAtEnd = range.startOffset === textContent.length
|
||||
}
|
||||
}
|
||||
|
||||
return { index, type, element, isAtStart, isAtEnd }
|
||||
return { index, type, element, isAtStart, isAtEnd }
|
||||
}
|
||||
|
||||
//光标设置
|
||||
const setCursorToElement = (element: HTMLElement, position: 'start' | 'end') => {
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
|
||||
if (element.classList.contains('input-content') && element.classList.contains('has-placeholder')) {
|
||||
// placeholder状态的特殊处理
|
||||
range.selectNodeContents(element)
|
||||
range.collapse(position === 'start')
|
||||
} else if (element.childNodes.length > 0) {
|
||||
const targetNode = position === 'start' ? element.firstChild! : element.lastChild!
|
||||
if (targetNode.nodeType === Node.TEXT_NODE) {
|
||||
const offset = position === 'start' ? 0 : (targetNode.textContent || '').length
|
||||
range.setStart(targetNode, offset)
|
||||
range.setEnd(targetNode, offset)
|
||||
} else {
|
||||
range.selectNodeContents(element)
|
||||
range.collapse(position === 'start')
|
||||
}
|
||||
} else {
|
||||
range.selectNodeContents(element)
|
||||
range.collapse(position === 'start')
|
||||
}
|
||||
if (
|
||||
element.classList.contains('input-content') &&
|
||||
element.classList.contains('has-placeholder')
|
||||
) {
|
||||
// placeholder状态的特殊处理
|
||||
range.selectNodeContents(element)
|
||||
range.collapse(position === 'start')
|
||||
} else if (element.childNodes.length > 0) {
|
||||
const targetNode = position === 'start' ? element.firstChild! : element.lastChild!
|
||||
if (targetNode.nodeType === Node.TEXT_NODE) {
|
||||
const offset = position === 'start' ? 0 : (targetNode.textContent || '').length
|
||||
range.setStart(targetNode, offset)
|
||||
range.setEnd(targetNode, offset)
|
||||
} else {
|
||||
range.selectNodeContents(element)
|
||||
range.collapse(position === 'start')
|
||||
}
|
||||
} else {
|
||||
range.selectNodeContents(element)
|
||||
range.collapse(position === 'start')
|
||||
}
|
||||
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const elementInfo = getCurrentElementInfo()
|
||||
if (!elementInfo) return
|
||||
const elementInfo = getCurrentElementInfo()
|
||||
if (!elementInfo) return
|
||||
|
||||
const { index, type, isAtStart, isAtEnd } = elementInfo
|
||||
const { index, type, isAtStart, isAtEnd } = elementInfo
|
||||
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
if (isAtStart && index > 0) {
|
||||
event.preventDefault()
|
||||
handleCrossElementDelete(index)
|
||||
} else if (type === 'input' && elementInfo.element?.classList.contains('has-placeholder')) {
|
||||
event.preventDefault()
|
||||
}
|
||||
// 其他情况让浏览器正常处理删除
|
||||
break
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
if (isAtStart && index > 0) {
|
||||
event.preventDefault()
|
||||
handleCrossElementDelete(index)
|
||||
} else if (
|
||||
type === 'input' &&
|
||||
elementInfo.element?.classList.contains('has-placeholder')
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
// 其他情况让浏览器正常处理删除
|
||||
break
|
||||
|
||||
case 'ArrowLeft':
|
||||
if (isAtStart && index > 0) {
|
||||
event.preventDefault()
|
||||
navigateToElement(index - 1, 'end')
|
||||
}
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (isAtStart && index > 0) {
|
||||
event.preventDefault()
|
||||
navigateToElement(index - 1, 'end')
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrowRight':
|
||||
if (isAtEnd && index < content.value.length - 1) {
|
||||
event.preventDefault()
|
||||
navigateToElement(index + 1, 'start')
|
||||
} else if (isAtEnd && index === content.value.length - 1) {
|
||||
// 在最后一个元素末尾按右箭头,确保有一个空文本框
|
||||
ensureEmptyTextAtEnd()
|
||||
nextTick(() => {
|
||||
navigateToElement(index + 1, 'start')
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowRight':
|
||||
if (isAtEnd && index < content.value.length - 1) {
|
||||
event.preventDefault()
|
||||
navigateToElement(index + 1, 'start')
|
||||
} else if (isAtEnd && index === content.value.length - 1) {
|
||||
// 在最后一个元素末尾按右箭头,确保有一个空文本框
|
||||
ensureEmptyTextAtEnd()
|
||||
nextTick(() => {
|
||||
navigateToElement(index + 1, 'start')
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 跨元素删除逻辑
|
||||
const handleCrossElementDelete = (currentIndex: number) => {
|
||||
const prevIndex = currentIndex - 1
|
||||
const prevItem = content.value[prevIndex]
|
||||
const prevIndex = currentIndex - 1
|
||||
const prevItem = content.value[prevIndex]
|
||||
|
||||
if (prevItem.type === 'input') {
|
||||
if (prevItem.value.trim() === '') {
|
||||
// 删除空输入框
|
||||
content.value.splice(prevIndex, 1)
|
||||
nextTick(() => {
|
||||
// 删除输入框后,先删除末尾的空文本框
|
||||
removeLastEmptyTextIfNeeded()
|
||||
// 然后聚焦到正确的位置
|
||||
if (prevIndex < content.value.length) {
|
||||
focusElement(prevIndex, 'end')
|
||||
} else if (content.value.length > 0) {
|
||||
focusElement(content.value.length - 1, 'end')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 删除输入框最后一个字符,但保留输入框
|
||||
const newValue = prevItem.value.slice(0, -1)
|
||||
content.value[prevIndex].value = newValue
|
||||
updateInputDisplay(prevIndex)
|
||||
nextTick(() => focusElement(prevIndex, 'end'))
|
||||
}
|
||||
} else {
|
||||
// 文本框:移动到前一个文本框末尾,让浏览器正常删除
|
||||
// 先删除末尾的空文本框
|
||||
removeLastEmptyTextIfNeeded()
|
||||
focusElement(prevIndex, 'end')
|
||||
}
|
||||
if (prevItem.type === 'input') {
|
||||
if (prevItem.value.trim() === '') {
|
||||
// 删除空输入框
|
||||
content.value.splice(prevIndex, 1)
|
||||
nextTick(() => {
|
||||
// 删除输入框后,先删除末尾的空文本框
|
||||
removeLastEmptyTextIfNeeded()
|
||||
// 然后聚焦到正确的位置
|
||||
if (prevIndex < content.value.length) {
|
||||
focusElement(prevIndex, 'end')
|
||||
} else if (content.value.length > 0) {
|
||||
focusElement(content.value.length - 1, 'end')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 删除输入框最后一个字符,但保留输入框
|
||||
const newValue = prevItem.value.slice(0, -1)
|
||||
content.value[prevIndex].value = newValue
|
||||
updateInputDisplay(prevIndex)
|
||||
nextTick(() => focusElement(prevIndex, 'end'))
|
||||
}
|
||||
} else {
|
||||
// 文本框:移动到前一个文本框末尾,让浏览器正常删除
|
||||
// 先删除末尾的空文本框
|
||||
removeLastEmptyTextIfNeeded()
|
||||
focusElement(prevIndex, 'end')
|
||||
}
|
||||
}
|
||||
|
||||
// 导航到元素
|
||||
const navigateToElement = (targetIndex: number, position: 'start' | 'end') => {
|
||||
const targetItem = content.value[targetIndex]
|
||||
const element = targetItem.type === 'text'
|
||||
? editableArea.value?.querySelector(`.text-field[data-index="${targetIndex}"]`) as HTMLElement
|
||||
: editableArea.value?.querySelector(`.input-field[data-index="${targetIndex}"] .input-content`) as HTMLElement
|
||||
const targetItem = content.value[targetIndex]
|
||||
const element =
|
||||
targetItem.type === 'text'
|
||||
? (editableArea.value?.querySelector(
|
||||
`.text-field[data-index="${targetIndex}"]`
|
||||
) as HTMLElement)
|
||||
: (editableArea.value?.querySelector(
|
||||
`.input-field[data-index="${targetIndex}"] .input-content`
|
||||
) as HTMLElement)
|
||||
|
||||
if (element) setCursorToElement(element, position)
|
||||
if (element) setCursorToElement(element, position)
|
||||
}
|
||||
|
||||
// 焦点设置
|
||||
const focusElement = (index: number, position: 'start' | 'end') => {
|
||||
const item = content.value[index]
|
||||
const element = item.type === 'text'
|
||||
? editableArea.value?.querySelector(`.text-field[data-index="${index}"]`) as HTMLElement
|
||||
: editableArea.value?.querySelector(`.input-field[data-index="${index}"] .input-content`) as HTMLElement
|
||||
const item = content.value[index]
|
||||
const element =
|
||||
item.type === 'text'
|
||||
? (editableArea.value?.querySelector(
|
||||
`.text-field[data-index="${index}"]`
|
||||
) as HTMLElement)
|
||||
: (editableArea.value?.querySelector(
|
||||
`.input-field[data-index="${index}"] .input-content`
|
||||
) as HTMLElement)
|
||||
|
||||
if (element) setCursorToElement(element, position)
|
||||
if (element) setCursorToElement(element, position)
|
||||
}
|
||||
|
||||
// 输入框显示管理
|
||||
const updateInputDisplay = (index: number) => {
|
||||
const item = content.value[index]
|
||||
if (item.type !== 'input') return
|
||||
const item = content.value[index]
|
||||
if (item.type !== 'input') return
|
||||
|
||||
const inputElement = editableArea.value?.querySelector(
|
||||
`.input-field[data-index="${index}"] .input-content`
|
||||
) as HTMLElement
|
||||
const inputElement = editableArea.value?.querySelector(
|
||||
`.input-field[data-index="${index}"] .input-content`
|
||||
) as HTMLElement
|
||||
|
||||
if (!inputElement) return
|
||||
if (!inputElement) return
|
||||
|
||||
const showPlaceholder = item.value.trim() === '' && item.placeholder
|
||||
const showPlaceholder = item.value.trim() === '' && item.placeholder
|
||||
|
||||
if (showPlaceholder) {
|
||||
inputElement.classList.add('has-placeholder')
|
||||
inputElement.textContent = item.placeholder
|
||||
} else {
|
||||
inputElement.classList.remove('has-placeholder')
|
||||
inputElement.textContent = item.value
|
||||
}
|
||||
if (showPlaceholder) {
|
||||
inputElement.classList.add('has-placeholder')
|
||||
inputElement.textContent = item.placeholder
|
||||
} else {
|
||||
inputElement.classList.remove('has-placeholder')
|
||||
inputElement.textContent = item.value
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框内容变化处理
|
||||
const handleInputChange = (index: number, event: Event) => {
|
||||
const target = event.target as HTMLSpanElement
|
||||
const item = content.value[index]
|
||||
const target = event.target as HTMLSpanElement
|
||||
const item = content.value[index]
|
||||
|
||||
// 如果当前显示placeholder,不更新实际值
|
||||
if (!target.classList.contains('has-placeholder')) {
|
||||
const newValue = target.textContent || ''
|
||||
content.value[index].value = newValue
|
||||
// 如果当前显示placeholder,不更新实际值
|
||||
if (!target.classList.contains('has-placeholder')) {
|
||||
const newValue = target.textContent || ''
|
||||
content.value[index].value = newValue
|
||||
|
||||
// 如果内容变空,显示placeholder
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
target.classList.add('has-placeholder')
|
||||
target.textContent = item.placeholder
|
||||
}
|
||||
}
|
||||
// 如果内容变空,显示placeholder
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
target.classList.add('has-placeholder')
|
||||
target.textContent = item.placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框键盘事件
|
||||
const handleInputKeydown = (event: KeyboardEvent, index: number) => {
|
||||
const target = event.target as HTMLSpanElement
|
||||
const target = event.target as HTMLSpanElement
|
||||
|
||||
if (event.key === 'Backspace') {
|
||||
// 如果显示placeholder,阻止删除
|
||||
if (target.classList.contains('has-placeholder')) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Backspace') {
|
||||
// 如果显示placeholder,阻止删除
|
||||
if (target.classList.contains('has-placeholder')) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0)
|
||||
const isAtStart = range.startOffset === 0
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0)
|
||||
const isAtStart = range.startOffset === 0
|
||||
|
||||
// 如果光标在输入框开头,阻止默认行为,让外部的handleBackspace处理
|
||||
if (isAtStart) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
||||
// 普通字符输入
|
||||
if (target.classList.contains('has-placeholder')) {
|
||||
event.preventDefault()
|
||||
target.textContent = event.key
|
||||
target.classList.remove('has-placeholder')
|
||||
content.value[index].value = event.key
|
||||
// 如果光标在输入框开头,阻止默认行为,让外部的handleBackspace处理
|
||||
if (isAtStart) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
||||
// 普通字符输入
|
||||
if (target.classList.contains('has-placeholder')) {
|
||||
event.preventDefault()
|
||||
target.textContent = event.key
|
||||
target.classList.remove('has-placeholder')
|
||||
content.value[index].value = event.key
|
||||
|
||||
// 移动光标到末尾
|
||||
nextTick(() => {
|
||||
setCursorToElement(target, 'end')
|
||||
})
|
||||
}
|
||||
}
|
||||
// 移动光标到末尾
|
||||
nextTick(() => {
|
||||
setCursorToElement(target, 'end')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputBlur = (index: number) => {
|
||||
updateInputDisplay(index)
|
||||
updateInputDisplay(index)
|
||||
}
|
||||
|
||||
// 容器点击处理
|
||||
const handleContainerClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
if (target === editableArea.value) {
|
||||
event.preventDefault()
|
||||
cursorState.value.isContainerClick = true
|
||||
if (target === editableArea.value) {
|
||||
event.preventDefault()
|
||||
cursorState.value.isContainerClick = true
|
||||
|
||||
// 确保末尾有空文本框并聚焦到它
|
||||
ensureEmptyTextAtEnd()
|
||||
nextTick(() => {
|
||||
focusElement(content.value.length - 1, 'start')
|
||||
})
|
||||
}
|
||||
// 确保末尾有空文本框并聚焦到它
|
||||
ensureEmptyTextAtEnd()
|
||||
nextTick(() => {
|
||||
focusElement(content.value.length - 1, 'start')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initPlaceholders = () => {
|
||||
nextTick(() => {
|
||||
content.value.forEach((_, index) => updateInputDisplay(index))
|
||||
// 确保初始状态下有一个空文本框
|
||||
ensureEmptyTextAtEnd()
|
||||
})
|
||||
nextTick(() => {
|
||||
content.value.forEach((_, index) => updateInputDisplay(index))
|
||||
// 确保初始状态下有一个空文本框
|
||||
ensureEmptyTextAtEnd()
|
||||
})
|
||||
}
|
||||
|
||||
const getFullText = () => {
|
||||
if(assistModel.value){
|
||||
return content.value.map(item => {
|
||||
if (item.type === 'text') {
|
||||
return item.value
|
||||
} else {
|
||||
// 如果 input 没有输入 value,则用 placeholder 填充,并去掉首尾的 []
|
||||
if (item.value) {
|
||||
return ` ${item.value} `
|
||||
} else if (item.placeholder) {
|
||||
let placeholderText = item.placeholder
|
||||
// 去掉首尾的 []
|
||||
if (placeholderText.startsWith('[') && placeholderText.endsWith(']')) {
|
||||
placeholderText = placeholderText.slice(1, -1)
|
||||
}
|
||||
return ` ${placeholderText} `
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}).join('')
|
||||
}
|
||||
return textareaValue.value
|
||||
if (assistModel.value) {
|
||||
return content.value
|
||||
.map(item => {
|
||||
if (item.type === 'text') {
|
||||
return item.value
|
||||
} else {
|
||||
// 如果 input 没有输入 value,则用 placeholder 填充,并去掉首尾的 []
|
||||
if (item.value) {
|
||||
return ` ${item.value} `
|
||||
} else if (item.placeholder) {
|
||||
let placeholderText = item.placeholder
|
||||
// 去掉首尾的 []
|
||||
if (placeholderText.startsWith('[') && placeholderText.endsWith(']')) {
|
||||
placeholderText = placeholderText.slice(1, -1)
|
||||
}
|
||||
return ` ${placeholderText} `
|
||||
}
|
||||
return ''
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
return textareaValue.value
|
||||
}
|
||||
|
||||
const textareaValue = ref('')
|
||||
const assistModel= ref(false)
|
||||
const assistModel = ref(false)
|
||||
const handleClickAssistBtn = () => {
|
||||
assistModel.value = !assistModel.value
|
||||
assistModel.value = !assistModel.value
|
||||
}
|
||||
|
||||
// 监听 assistModel 变化,切换到 textarea 模式时调整高度
|
||||
watch(assistModel, (newVal) => {
|
||||
if (!newVal) {
|
||||
// 切换到 textarea 模式
|
||||
nextTick(() => {
|
||||
handleInputResize()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef')
|
||||
const handleInputResize = () => {
|
||||
const textarea = textareaRef.value
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPlaceholders()
|
||||
// 如果初始状态是 textarea 模式,设置初始高度
|
||||
if (!assistModel.value) {
|
||||
nextTick(() => {
|
||||
handleInputResize()
|
||||
})
|
||||
}
|
||||
initPlaceholders()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
getFullText,
|
||||
content
|
||||
getFullText,
|
||||
content
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="!assistModel" class="textarea-container">
|
||||
<textarea
|
||||
class="area"
|
||||
v-model="textareaValue"
|
||||
ref="textareaRef"
|
||||
@input="handleInputResize"
|
||||
:placeholder="$t('poseTransfer.PormptPlaceholder')"
|
||||
/>
|
||||
<div class="asistant-btn" @click="handleClickAssistBtn">
|
||||
<i class="fi fi-bs-magic-wand asistant-icon"></i>
|
||||
<span>{{ $t('ProductImg.PromptAssit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="assistModel" ref="editableArea" class="promptInput" @keydown="handleKeydown" @click="handleContainerClick">
|
||||
<template v-for="(item, index) in content" :key="item.id">
|
||||
<span
|
||||
v-if="item.type === 'text'"
|
||||
class="text-field"
|
||||
:data-index="index"
|
||||
contenteditable="plaintext-only">
|
||||
{{item.value }}
|
||||
</span>
|
||||
<div v-show="!assistModel" class="textarea-container">
|
||||
<textarea
|
||||
class="area"
|
||||
v-model="textareaValue"
|
||||
ref="textareaRef"
|
||||
:placeholder="$t('poseTransfer.PormptPlaceholder')"
|
||||
/>
|
||||
<div class="asistant-btn" @click="handleClickAssistBtn">
|
||||
<i class="fi fi-bs-magic-wand asistant-icon"></i>
|
||||
<span>{{ $t('ProductImg.PromptAssit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="assistModel"
|
||||
ref="editableArea"
|
||||
class="promptInput"
|
||||
@keydown="handleKeydown"
|
||||
@click="handleContainerClick"
|
||||
>
|
||||
<div class="promptinput-wrapper">
|
||||
<template v-for="(item, index) in content" :key="item.id">
|
||||
<span
|
||||
v-if="item.type === 'text'"
|
||||
class="text-field"
|
||||
:data-index="index"
|
||||
contenteditable="plaintext-only"
|
||||
>
|
||||
{{ item.value }}
|
||||
</span>
|
||||
|
||||
<span v-else class="input-field" :data-index="index">
|
||||
<span
|
||||
class="input-content"
|
||||
contenteditable="plaintext-only"
|
||||
@input="(e) => handleInputChange(index, e)"
|
||||
@keydown="(e) => handleInputKeydown(e, index)"
|
||||
@blur="() => handleInputBlur(index)"></span>
|
||||
</span>
|
||||
</template>
|
||||
<div class="asistant-btn" @click="handleClickAssistBtn">
|
||||
<i class="fi fi-bs-magic-wand asistant-icon"></i>
|
||||
<span>{{ $t('ProductImg.PromptAssit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="input-field" :data-index="index">
|
||||
<span
|
||||
class="input-content"
|
||||
contenteditable="plaintext-only"
|
||||
@input="e => handleInputChange(index, e)"
|
||||
@keydown="e => handleInputKeydown(e, index)"
|
||||
@blur="() => handleInputBlur(index)"
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="asistant-btn" @click="handleClickAssistBtn">
|
||||
<i class="fi fi-bs-magic-wand asistant-icon"></i>
|
||||
<span>{{ $t('ProductImg.PromptAssit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.promptInput {
|
||||
--promptInputBorderRadius: 10px;
|
||||
--promptInputBorder: 2px solid #dcdfe6;
|
||||
--promptInputPadding: 1.5rem;
|
||||
--promptInputBorderRadius: 10px;
|
||||
--promptInputBorder: 2px solid #dcdfe6;
|
||||
--promptInputPadding: 1.5rem;
|
||||
|
||||
width: 100%;
|
||||
min-height: 12rem;
|
||||
border-radius: var(--promptInputBorderRadius);
|
||||
border: var(--promptInputBorder);
|
||||
padding: var(--promptInputPadding);
|
||||
background: white;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
border-radius: var(--promptInputBorderRadius);
|
||||
border: var(--promptInputBorder);
|
||||
padding: var(--promptInputPadding);
|
||||
background: white;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
position: relative;
|
||||
padding-bottom: 4rem;
|
||||
|
||||
box-sizing: content-box;
|
||||
|
||||
.text-field {
|
||||
display: inline;
|
||||
outline: none;
|
||||
padding: .2rem 0;
|
||||
font-size: 1.8rem;
|
||||
min-width: 2px;
|
||||
/* 确保空文本框也能点击 */
|
||||
.promptinput-wrapper {
|
||||
min-height: 12rem;
|
||||
max-height: 14rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
display: inline-block;
|
||||
// background: #e3f2fd;
|
||||
// border: 1px solid #bbdefb;
|
||||
margin: 0 .2rem;
|
||||
padding: .2rem 1rem;
|
||||
font-size: 1.8rem;
|
||||
border-radius: 4px;
|
||||
.text-field {
|
||||
display: inline;
|
||||
outline: none;
|
||||
padding: 0.2rem 0;
|
||||
font-size: 1.8rem;
|
||||
min-width: 2px;
|
||||
font-weight: 400;
|
||||
/* 确保空文本框也能点击 */
|
||||
}
|
||||
|
||||
.input-content {
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
min-width: 2rem;
|
||||
.input-field {
|
||||
display: inline-block;
|
||||
// background: #e3f2fd;
|
||||
// border: 1px solid #bbdefb;
|
||||
margin: 0 0.2rem;
|
||||
padding: 0.2rem 1rem;
|
||||
font-size: 1.8rem;
|
||||
border-radius: 4px;
|
||||
|
||||
&.has-placeholder {
|
||||
color: #b9b9b9;
|
||||
// font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
.input-content {
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
min-width: 2rem;
|
||||
|
||||
&.has-placeholder {
|
||||
color: #b9b9b9;
|
||||
// font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-container{
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #dcdfe6;
|
||||
padding: 1.5rem 1.5rem 3rem;
|
||||
height: auto;
|
||||
.area{
|
||||
.textarea-container {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #dcdfe6;
|
||||
padding: 1.5rem 1.5rem 3rem;
|
||||
height: auto;
|
||||
.area {
|
||||
width: 100%;
|
||||
min-height: 12rem;
|
||||
height: auto;
|
||||
max-height: 14rem;
|
||||
background: white;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
@@ -509,34 +520,34 @@ defineExpose({
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
position: relative;
|
||||
// padding-bottom: 4rem;
|
||||
resize: none;
|
||||
border: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.asistant-btn {
|
||||
height: 2.3rem;
|
||||
padding: 0 0.6rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #313131;
|
||||
position: absolute;
|
||||
bottom: 1.3rem;
|
||||
left: 1.3rem;
|
||||
display: flex;
|
||||
column-gap: 0.3rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
.asistant-icon {
|
||||
font-size: 1rem;
|
||||
margin-right: 0;
|
||||
width: initial;
|
||||
// margin-top: -0.2rem;
|
||||
}
|
||||
height: 2.3rem;
|
||||
padding: 0 0.6rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #313131;
|
||||
position: absolute;
|
||||
bottom: 1.3rem;
|
||||
left: 1.3rem;
|
||||
display: flex;
|
||||
column-gap: 0.3rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
.asistant-icon {
|
||||
font-size: 1rem;
|
||||
margin-right: 0;
|
||||
width: initial;
|
||||
// margin-top: -0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div ref="modalContainer"></div>
|
||||
<!-- <div ref="modalContainer"></div> -->
|
||||
<a-modal
|
||||
class="prompt-modal generalModel"
|
||||
v-model:visible="showModal"
|
||||
:footer="null"
|
||||
:get-container="() => $refs.modalContainer"
|
||||
width="78%"
|
||||
:maskClosable="false"
|
||||
:centered="true"
|
||||
@@ -198,6 +197,9 @@ const handleCopy = async (value: string) => {
|
||||
margin-top: 4.3rem;
|
||||
padding-left: 7.4rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
.example-wrapper {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
|
||||
@@ -623,11 +623,12 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
let isSelectObject = false
|
||||
watch(() => route.query,
|
||||
(query:any, oldQuery:any) => {
|
||||
isSelectObject = false
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => route.query,
|
||||
(query: any, oldQuery: any) => {
|
||||
isSelectObject = false
|
||||
}
|
||||
)
|
||||
let beforeUpload = async (file: any) => {
|
||||
const isJpgOrPng =
|
||||
file.type === 'image/jpeg' ||
|
||||
@@ -641,8 +642,8 @@ export default defineComponent({
|
||||
if (!isLt2M) {
|
||||
message.info(useI18n().t('MoodboardUpload.jsContent4'))
|
||||
}
|
||||
const objectId = route?.query?.id
|
||||
if (!objectId && !isSelectObject) {
|
||||
const objectId = route?.query?.id
|
||||
if (!objectId && !isSelectObject) {
|
||||
isSelectObject = true
|
||||
productImgData.selectObject.id = await createProbject()
|
||||
upload.value = {
|
||||
@@ -650,10 +651,7 @@ export default defineComponent({
|
||||
CollectionType: props.productimgMenu.value
|
||||
}
|
||||
}
|
||||
return !!(
|
||||
(isJpgOrPng && isLt2M && objectId) ||
|
||||
Upload.LIST_IGNORE
|
||||
)
|
||||
return !!((isJpgOrPng && isLt2M && objectId) || Upload.LIST_IGNORE)
|
||||
}
|
||||
let setGenerate = (item: any) => {
|
||||
item.isChecked = !item.isChecked
|
||||
@@ -1559,8 +1557,8 @@ export default defineComponent({
|
||||
:deep(.ant-upload-picture-card-wrapper) {
|
||||
.ant-upload-list-picture-card {
|
||||
.ant-upload-select-picture-card {
|
||||
width: 9.6rem;
|
||||
height: 13.4rem;
|
||||
width: 12.7rem;
|
||||
height: 17.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1579,9 +1577,14 @@ export default defineComponent({
|
||||
.input_box {
|
||||
.input_box_btnBox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.textarea {
|
||||
// flex: 1;
|
||||
min-height: 12.7rem;
|
||||
max-height: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1592,9 +1595,9 @@ export default defineComponent({
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #313131;
|
||||
position: absolute;
|
||||
bottom: 1.3rem;
|
||||
left: 1.3rem;
|
||||
// position: absolute;
|
||||
// bottom: 1.3rem;
|
||||
// left: 1.3rem;
|
||||
display: flex;
|
||||
column-gap: 0.3rem;
|
||||
justify-content: center;
|
||||
|
||||
@@ -295,8 +295,8 @@ export default {
|
||||
GeneratedVideo: '生成的视频',
|
||||
hint: '将这张图像转化为一张逼真、达到工作室水准的照片。在生活中真实存在的人',
|
||||
jsContent1: '生成视频预计需要三分钟,请问是否继续',
|
||||
NeedFirstFrame:'请选择首帧图片',
|
||||
NeedLastFrame:'请选择尾帧图片',
|
||||
NeedFirstFrame: '请选择首帧图片',
|
||||
NeedLastFrame: '请选择尾帧图片',
|
||||
SingleGarment:
|
||||
'专业产品摄影:服装展示于隐形模特上,模特不可见。完整保留设计细节——所有图案、颜色、质地及细节特征。', // 单品样衣
|
||||
SingleChildTryOn:
|
||||
@@ -308,18 +308,18 @@ export default {
|
||||
SeriesChildTryOn:
|
||||
'将此图像转化为逼真的真实儿童模特,保持站立姿势并直面镜头,保留服装的原有图案、风格和色彩,呈现工作室级别的照片质量。', // 系列儿童试穿
|
||||
UploadWithModel:
|
||||
'创建模特穿着该服装的真实感摄影棚照片。若原有模特存在则保留,否则生成合适模特。采用站立姿势面向镜头。精确保留服装细节——图案、颜色、质地及装饰元素。', // 上传线稿,带模特
|
||||
'生成真实模特穿着该服装的逼真摄影棚照片,采用站立姿势面向镜头。保留服装所有细节——包括图案、颜色、纹理、装饰元素,呈现摄影棚级别的8K分辨率照片,支持HDR效果、景深控制、柔光效果及高细节呈现。请勿返还原始图像。', // 上传线稿,带模特
|
||||
UploadWithoutModel:
|
||||
'专业产品摄影:服装以自然形态展示,无模特。采用影棚灯光。保留精确细节——图案、色彩、质感、装饰元素。', // 上传线稿,不带模特
|
||||
VideoType: '视频类型',
|
||||
FirstFrame: '首帧',
|
||||
LastFrame:'尾帧',
|
||||
LastFrame: '尾帧',
|
||||
FirstAndLastFrames: '首帧和尾帧',
|
||||
FirstFrameAndSkeleton: '首帧和骨架',
|
||||
Tips: '提示',
|
||||
TipsStart: '你可以使用"',
|
||||
TipsEnd: '编辑产品图"来生成首帧或尾帧图片',
|
||||
PormptPlaceholder:'输入你想描述的场景...',
|
||||
PormptPlaceholder: '输入你想描述的场景...',
|
||||
firstAndLastFrameText1: '随着视频的进行,使用',
|
||||
firstAndLastFramePlaceholder1: '[相机运动]',
|
||||
firstAndLastFrameText2: '来跟随动作,在',
|
||||
|
||||
@@ -318,7 +318,7 @@ export default {
|
||||
SeriesChildTryOn:
|
||||
'Transform this image into a realistic, the same real child model , Maintain a standing posture and face the camera directly, keep the same pattern, style and colour of these garment, studio-quality photograph.', // 系列儿童
|
||||
UploadWithModel:
|
||||
'Create realistic studio photo with model wearing this garment. Keep original model if present, or generate appropriate model. Standing pose, facing camera. Preserve exact garment details - patterns, colors, textures, embellishments.', // 上传线稿,带模特
|
||||
'Generate realistic studio photo of a real model wearing this garment, standing pose, facing camera. Preserve exact garment details - all patterns, colors, textures, embellishments, studio-quality photograph, 8K, HDR, DOF, soft lighting, high detail. Do not return the original image.', // 上传线稿,带模特
|
||||
UploadWithoutModel:
|
||||
'Professional product photo: garment displayed with natural shape, no model. Studio lighting. Preserve exact details - patterns, colors, textures, embellishments.', // 上传线稿,不带模特
|
||||
VideoType: 'Video Type',
|
||||
|
||||
Reference in New Issue
Block a user