Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite

This commit is contained in:
X1627315083
2025-11-19 09:36:01 +08:00
9 changed files with 2459 additions and 1616 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

File diff suppressed because it is too large Load Diff

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>
<div class="imgBox" v-mousewheel> <div class="imgBox" v-mousewheel>
<div <div
class="item" class="item first-frame"
:class="{ active: item.id == selectImg.id }" :class="{ active: item.id == selectImg.id }"
v-for="(item, index) in showFirstFrameList" v-for="(item, index) in showFirstFrameList"
@click="selectImgItem(item)" @click="selectImgItem(item)"
@@ -52,23 +52,26 @@
v-show="item.designOutfitUrl || item.imgUrl || item.url" v-show="item.designOutfitUrl || item.imgUrl || item.url"
class="btnBox" class="btnBox"
> >
<div :class="{ active: item.isChecked }"> <div
:class="{ active: item.isChecked }"
v-if="!(isDesignPage && videoType === 3)"
>
<i class="fi fi-br-check"></i> <i class="fi fi-br-check"></i>
</div> </div>
<div <div
@click.stop="setUploadDelete(item, index)" @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> <i class="fi fi-rr-trash icon_delete"></i>
</div> </div>
</div> </div>
</div> </div>
<div class="upload_item item first_frames" v-show="showFirstFrameUpload">
<div <div
class="upload_item item" class="design-mask"
v-show=" v-if="isDesignPage && videoType === 3"
!isDesignPage && !(videoType === 3 && showFirstFrameList.length > 0) @click="handleClickDesignMask('first')"
" />
>
<div class="upload_file_item"> <div class="upload_file_item">
<a-upload <a-upload
key="common" key="common"
@@ -93,9 +96,14 @@
</div> </div>
<template v-if="videoType === 3"> <template v-if="videoType === 3">
<div <div
v-show="lastFrameList?.length < 1"
class="upload_item item last_frames" 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"> <div class="upload_file_item">
<a-upload <a-upload
key="lastframes" key="lastframes"
@@ -142,12 +150,15 @@
v-show="item.designOutfitUrl || item.imgUrl || item.url" v-show="item.designOutfitUrl || item.imgUrl || item.url"
class="btnBox" class="btnBox"
> >
<div :class="{ active: item.isChecked }"> <div
:class="{ active: item.isChecked }"
v-if="!(isDesignPage && videoType === 3)"
>
<i class="fi fi-br-check"></i> <i class="fi fi-br-check"></i>
</div> </div>
<div <div
@click.stop="setUploadDelete(item, index, true)" @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> <i class="fi fi-rr-trash icon_delete"></i>
</div> </div>
@@ -180,19 +191,10 @@
<div class="control-container"> <div class="control-container">
<div class="icon-list"> <div class="icon-list">
<SvgIcon <SvgIcon
v-show="!isVideoPlaying(item.id)"
class="play-icon" class="play-icon"
@click.stop="handlePlayMotion(item)" @click.stop="handlePlayMotion(item)"
name="CPlay" :name="isVideoPlaying(item.id) ? 'CPause' : 'CPlay'"
size="10" size="20"
color="#fff"
/>
<SvgIcon
v-show="isVideoPlaying(item.id)"
class="play-icon pause"
@click.stop="handlePlayMotion(item)"
name="CPause"
size="10"
color="#fff" color="#fff"
/> />
</div> </div>
@@ -277,6 +279,13 @@
</div> </div>
</div> </div>
<Tips v-model:showModal="showTips" /> <Tips v-model:showModal="showTips" />
<Product
v-model:showModal="showProductList"
:frameList="productFrameList"
:type="productType"
:key="productType"
@confirm="handleConfirmProduct"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -304,13 +313,15 @@ import showViewVideo from '@/tool/mount'
import router from '@/router' import router from '@/router'
import promptInput from './promptInput.vue' import promptInput from './promptInput.vue'
import Tips from './Tips.vue' import Tips from './Tips.vue'
import Product from './Product.vue'
import { getFirstFrame, getFirstAndLastFrame } from './prompt' import { getFirstFrame, getFirstAndLastFrame } from './prompt'
export default defineComponent({ export default defineComponent({
components: { components: {
generalDrag, generalDrag,
promptInput, promptInput,
Tips Tips,
Product
}, },
props: { props: {
isDesignPage: { isDesignPage: {
@@ -881,6 +892,61 @@ export default defineComponent({
document.removeEventListener('click', openSpeed) document.removeEventListener('click', openSpeed)
} }
const setUploadDelete = (item: any, index: any, isLastFrame: boolean = false) => { 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 = { let value = {
id: item.id id: item.id
} }
@@ -920,9 +986,148 @@ export default defineComponent({
const firstFrameList = ref([]) const firstFrameList = ref([])
const lastFrameList = ref([]) const lastFrameList = ref([])
const showFirstFrameList = computed(() => { 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 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( watch(
() => store.state.HomeStoreModule.uploadElement.length, () => store.state.HomeStoreModule.uploadElement.length,
(newVal, oldVal) => { (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( watch(
() => store.state.HomeStoreModule.lastFrameList, () => store.state.HomeStoreModule.lastFrameList,
val => { val => {
@@ -1039,7 +1258,14 @@ export default defineComponent({
setVideoRef, setVideoRef,
handlePlayMotion, handlePlayMotion,
isVideoPlaying, isVideoPlaying,
showTips showTips,
showProductList,
handleClickDesignMask,
productType,
handleConfirmProduct,
productFrameList,
showFirstFrameUpload,
showLastFrameUpload
} }
}, },
directives: { directives: {
@@ -1239,6 +1465,15 @@ export default defineComponent({
} }
> .upload_item { > .upload_item {
border: none; border: none;
position: relative;
.design-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
} }
.control-container { .control-container {
width: 100%; width: 100%;
@@ -1252,26 +1487,27 @@ export default defineComponent({
rgba(8, 9, 13, 0.27) 80.37% rgba(8, 9, 13, 0.27) 80.37%
); );
display: flex; display: flex;
align-items: flex-end; align-items: center;
justify-content: center; justify-content: flex-start;
.icon-list { .icon-list {
height: 50%; // height: 50%;
width: calc(100% - 1.6rem); // width: calc(100% - 1.6rem);
// border-top: 1px solid #fff; // border-top: 1px solid #fff;
padding-left: 1rem;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
.play-icon { .play-icon {
width: initial; width: 2rem;
height: initial; height: 2rem;
} }
} }
} }
} }
> .head { > .head {
color: #000; color: #000;
font-weight: 600; font-weight: 500;
display: flex; display: flex;
align-items: center; align-items: center;
column-gap: 1rem; column-gap: 1rem;
@@ -1292,6 +1528,7 @@ export default defineComponent({
font-size: 1.8rem; font-size: 1.8rem;
color: #000; color: #000;
margin-bottom: 1.4rem; margin-bottom: 1.4rem;
font-weight: 500;
} }
> .poses { > .poses {
margin-top: 3rem; margin-top: 3rem;
@@ -1393,6 +1630,7 @@ export default defineComponent({
} }
.video-type-container { .video-type-container {
margin-bottom: 4rem; margin-bottom: 4rem;
font-weight: 500;
.title { .title {
font-size: 1.8rem; font-size: 1.8rem;
color: #000; color: #000;

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <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 { interface ContentItem {
id: string; id: string
type: 'text' | 'input'; type: 'text' | 'input'
value: string; value: string
placeholder?: string; placeholder?: string
} }
const props = defineProps({ const props = defineProps({
@@ -111,7 +111,10 @@ const setCursorToElement = (element: HTMLElement, position: 'start' | 'end') =>
const selection = window.getSelection() const selection = window.getSelection()
const range = document.createRange() const range = document.createRange()
if (element.classList.contains('input-content') && element.classList.contains('has-placeholder')) { if (
element.classList.contains('input-content') &&
element.classList.contains('has-placeholder')
) {
// placeholder状态的特殊处理 // placeholder状态的特殊处理
range.selectNodeContents(element) range.selectNodeContents(element)
range.collapse(position === 'start') range.collapse(position === 'start')
@@ -146,7 +149,10 @@ const handleKeydown = (event: KeyboardEvent) => {
if (isAtStart && index > 0) { if (isAtStart && index > 0) {
event.preventDefault() event.preventDefault()
handleCrossElementDelete(index) handleCrossElementDelete(index)
} else if (type === 'input' && elementInfo.element?.classList.contains('has-placeholder')) { } else if (
type === 'input' &&
elementInfo.element?.classList.contains('has-placeholder')
) {
event.preventDefault() event.preventDefault()
} }
// 其他情况让浏览器正常处理删除 // 其他情况让浏览器正常处理删除
@@ -211,9 +217,14 @@ const handleCrossElementDelete = (currentIndex: number) => {
// 导航到元素 // 导航到元素
const navigateToElement = (targetIndex: number, position: 'start' | 'end') => { const navigateToElement = (targetIndex: number, position: 'start' | 'end') => {
const targetItem = content.value[targetIndex] const targetItem = content.value[targetIndex]
const element = targetItem.type === 'text' const element =
? editableArea.value?.querySelector(`.text-field[data-index="${targetIndex}"]`) as HTMLElement targetItem.type === 'text'
: editableArea.value?.querySelector(`.input-field[data-index="${targetIndex}"] .input-content`) as HTMLElement ? (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)
} }
@@ -221,9 +232,14 @@ const navigateToElement = (targetIndex: number, position: 'start' | 'end') => {
// 焦点设置 // 焦点设置
const focusElement = (index: number, position: 'start' | 'end') => { const focusElement = (index: number, position: 'start' | 'end') => {
const item = content.value[index] const item = content.value[index]
const element = item.type === 'text' const element =
? editableArea.value?.querySelector(`.text-field[data-index="${index}"]`) as HTMLElement item.type === 'text'
: editableArea.value?.querySelector(`.input-field[data-index="${index}"] .input-content`) as HTMLElement ? (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)
} }
@@ -337,7 +353,8 @@ const initPlaceholders = () => {
const getFullText = () => { const getFullText = () => {
if (assistModel.value) { if (assistModel.value) {
return content.value.map(item => { return content.value
.map(item => {
if (item.type === 'text') { if (item.type === 'text') {
return item.value return item.value
} else { } else {
@@ -354,7 +371,8 @@ const getFullText = () => {
} }
return '' return ''
} }
}).join('') })
.join('')
} }
return textareaValue.value return textareaValue.value
} }
@@ -365,33 +383,10 @@ 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 textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef')
const handleInputResize = () => {
const textarea = textareaRef.value
if (textarea) {
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
}
}
onMounted(() => { onMounted(() => {
initPlaceholders() initPlaceholders()
// 如果初始状态是 textarea 模式,设置初始高度
if (!assistModel.value) {
nextTick(() => {
handleInputResize()
})
}
}) })
defineExpose({ defineExpose({
@@ -406,7 +401,6 @@ defineExpose({
class="area" class="area"
v-model="textareaValue" v-model="textareaValue"
ref="textareaRef" ref="textareaRef"
@input="handleInputResize"
:placeholder="$t('poseTransfer.PormptPlaceholder')" :placeholder="$t('poseTransfer.PormptPlaceholder')"
/> />
<div class="asistant-btn" @click="handleClickAssistBtn"> <div class="asistant-btn" @click="handleClickAssistBtn">
@@ -414,13 +408,21 @@ defineExpose({
<span>{{ $t('ProductImg.PromptAssit') }}</span> <span>{{ $t('ProductImg.PromptAssit') }}</span>
</div> </div>
</div> </div>
<div v-show="assistModel" ref="editableArea" class="promptInput" @keydown="handleKeydown" @click="handleContainerClick"> <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"> <template v-for="(item, index) in content" :key="item.id">
<span <span
v-if="item.type === 'text'" v-if="item.type === 'text'"
class="text-field" class="text-field"
:data-index="index" :data-index="index"
contenteditable="plaintext-only"> contenteditable="plaintext-only"
>
{{ item.value }} {{ item.value }}
</span> </span>
@@ -428,11 +430,13 @@ defineExpose({
<span <span
class="input-content" class="input-content"
contenteditable="plaintext-only" contenteditable="plaintext-only"
@input="(e) => handleInputChange(index, e)" @input="e => handleInputChange(index, e)"
@keydown="(e) => handleInputKeydown(e, index)" @keydown="e => handleInputKeydown(e, index)"
@blur="() => handleInputBlur(index)"></span> @blur="() => handleInputBlur(index)"
></span>
</span> </span>
</template> </template>
</div>
<div class="asistant-btn" @click="handleClickAssistBtn"> <div class="asistant-btn" @click="handleClickAssistBtn">
<i class="fi fi-bs-magic-wand asistant-icon"></i> <i class="fi fi-bs-magic-wand asistant-icon"></i>
<span>{{ $t('ProductImg.PromptAssit') }}</span> <span>{{ $t('ProductImg.PromptAssit') }}</span>
@@ -447,7 +451,7 @@ defineExpose({
--promptInputPadding: 1.5rem; --promptInputPadding: 1.5rem;
width: 100%; width: 100%;
min-height: 12rem; font-weight: 500;
border-radius: var(--promptInputBorderRadius); border-radius: var(--promptInputBorderRadius);
border: var(--promptInputBorder); border: var(--promptInputBorder);
padding: var(--promptInputPadding); padding: var(--promptInputPadding);
@@ -459,14 +463,21 @@ defineExpose({
cursor: text; cursor: text;
position: relative; position: relative;
padding-bottom: 4rem; padding-bottom: 4rem;
box-sizing: content-box;
.promptinput-wrapper {
min-height: 12rem;
max-height: 14rem;
overflow-y: auto;
}
.text-field { .text-field {
display: inline; display: inline;
outline: none; outline: none;
padding: .2rem 0; padding: 0.2rem 0;
font-size: 1.8rem; font-size: 1.8rem;
min-width: 2px; min-width: 2px;
font-weight: 400;
/* 确保空文本框也能点击 */ /* 确保空文本框也能点击 */
} }
@@ -474,8 +485,8 @@ defineExpose({
display: inline-block; display: inline-block;
// background: #e3f2fd; // background: #e3f2fd;
// border: 1px solid #bbdefb; // border: 1px solid #bbdefb;
margin: 0 .2rem; margin: 0 0.2rem;
padding: .2rem 1rem; padding: 0.2rem 1rem;
font-size: 1.8rem; font-size: 1.8rem;
border-radius: 4px; border-radius: 4px;
@@ -501,7 +512,7 @@ defineExpose({
.area { .area {
width: 100%; width: 100%;
min-height: 12rem; min-height: 12rem;
height: auto; max-height: 14rem;
background: white; background: white;
line-height: 1.6; line-height: 1.6;
outline: none; outline: none;
@@ -509,9 +520,9 @@ defineExpose({
user-select: text; user-select: text;
cursor: text; cursor: text;
position: relative; position: relative;
// padding-bottom: 4rem;
resize: none; resize: none;
border: none; border: none;
overflow-y: auto;
} }
} }

View File

@@ -1,10 +1,9 @@
<template> <template>
<div ref="modalContainer"></div> <!-- <div ref="modalContainer"></div> -->
<a-modal <a-modal
class="prompt-modal generalModel" class="prompt-modal generalModel"
v-model:visible="showModal" v-model:visible="showModal"
:footer="null" :footer="null"
:get-container="() => $refs.modalContainer"
width="78%" width="78%"
:maskClosable="false" :maskClosable="false"
:centered="true" :centered="true"
@@ -198,6 +197,9 @@ const handleCopy = async (value: string) => {
margin-top: 4.3rem; margin-top: 4.3rem;
padding-left: 7.4rem; padding-left: 7.4rem;
display: flex; display: flex;
justify-content: space-between;
overflow-x: auto;
overflow-y: hidden;
.example-wrapper { .example-wrapper {
display: flex; display: flex;
gap: 0; gap: 0;

View File

@@ -623,11 +623,12 @@ export default defineComponent({
} }
} }
let isSelectObject = false let isSelectObject = false
watch(() => route.query, watch(
() => route.query,
(query: any, oldQuery: any) => { (query: any, oldQuery: any) => {
isSelectObject = false isSelectObject = false
}, }
); )
let beforeUpload = async (file: any) => { let beforeUpload = async (file: any) => {
const isJpgOrPng = const isJpgOrPng =
file.type === 'image/jpeg' || file.type === 'image/jpeg' ||
@@ -650,10 +651,7 @@ export default defineComponent({
CollectionType: props.productimgMenu.value CollectionType: props.productimgMenu.value
} }
} }
return !!( return !!((isJpgOrPng && isLt2M && objectId) || Upload.LIST_IGNORE)
(isJpgOrPng && isLt2M && objectId) ||
Upload.LIST_IGNORE
)
} }
let setGenerate = (item: any) => { let setGenerate = (item: any) => {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
@@ -1559,8 +1557,8 @@ export default defineComponent({
:deep(.ant-upload-picture-card-wrapper) { :deep(.ant-upload-picture-card-wrapper) {
.ant-upload-list-picture-card { .ant-upload-list-picture-card {
.ant-upload-select-picture-card { .ant-upload-select-picture-card {
width: 9.6rem; width: 12.7rem;
height: 13.4rem; height: 17.8rem;
} }
} }
} }
@@ -1579,9 +1577,14 @@ export default defineComponent({
.input_box { .input_box {
.input_box_btnBox { .input_box_btnBox {
position: relative; position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
.textarea { .textarea {
// flex: 1;
min-height: 12.7rem; min-height: 12.7rem;
max-height: none; max-height: none;
width: 100%;
} }
} }
} }
@@ -1592,9 +1595,9 @@ export default defineComponent({
font-size: 1rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
color: #313131; color: #313131;
position: absolute; // position: absolute;
bottom: 1.3rem; // bottom: 1.3rem;
left: 1.3rem; // left: 1.3rem;
display: flex; display: flex;
column-gap: 0.3rem; column-gap: 0.3rem;
justify-content: center; justify-content: center;

View File

@@ -308,7 +308,7 @@ export default {
SeriesChildTryOn: SeriesChildTryOn:
'将此图像转化为逼真的真实儿童模特,保持站立姿势并直面镜头,保留服装的原有图案、风格和色彩,呈现工作室级别的照片质量。', // 系列儿童试穿 '将此图像转化为逼真的真实儿童模特,保持站立姿势并直面镜头,保留服装的原有图案、风格和色彩,呈现工作室级别的照片质量。', // 系列儿童试穿
UploadWithModel: UploadWithModel:
'创建模特穿着该服装的真实感摄影棚照片。若原有模特存在则保留,否则生成合适模特。采用站立姿势面向镜头。精确保留服装细节——图案、颜色、质地及装饰元素。', // 上传线稿,带模特 '生成真实模特穿着该服装的真摄影棚照片采用站立姿势面向镜头。保留服装所有细节——包括图案、颜色、纹理、装饰元素呈现摄影棚级别的8K分辨率照片支持HDR效果、景深控制、柔光效果及高细节呈现。请勿返还原始图像。', // 上传线稿,带模特
UploadWithoutModel: UploadWithoutModel:
'专业产品摄影:服装以自然形态展示,无模特。采用影棚灯光。保留精确细节——图案、色彩、质感、装饰元素。', // 上传线稿,不带模特 '专业产品摄影:服装以自然形态展示,无模特。采用影棚灯光。保留精确细节——图案、色彩、质感、装饰元素。', // 上传线稿,不带模特
VideoType: '视频类型', VideoType: '视频类型',

View File

@@ -318,7 +318,7 @@ export default {
SeriesChildTryOn: 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.', // 系列儿童 '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: 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: UploadWithoutModel:
'Professional product photo: garment displayed with natural shape, no model. Studio lighting. Preserve exact details - patterns, colors, textures, embellishments.', // 上传线稿,不带模特 'Professional product photo: garment displayed with natural shape, no model. Studio lighting. Preserve exact details - patterns, colors, textures, embellishments.', // 上传线稿,不带模特
VideoType: 'Video Type', VideoType: 'Video Type',